diff --git a/.core_files.yaml b/.core_files.yaml index ebc3ff376f8..654730e5613 100644 --- a/.core_files.yaml +++ b/.core_files.yaml @@ -3,102 +3,104 @@ core: &core - homeassistant/*.py - homeassistant/auth/** - - homeassistant/helpers/* + - homeassistant/helpers/** - homeassistant/package_constraints.txt - - homeassistant/util/* + - homeassistant/util/** - pyproject.toml - requirements.txt - setup.cfg # Our base platforms, that are used by other integrations base_platforms: &base_platforms - - homeassistant/components/air_quality/* - - homeassistant/components/alarm_control_panel/* - - homeassistant/components/binary_sensor/* - - homeassistant/components/button/* - - homeassistant/components/calendar/* - - homeassistant/components/camera/* - - homeassistant/components/climate/* - - homeassistant/components/cover/* - - homeassistant/components/device_tracker/* - - homeassistant/components/diagnostics/* - - homeassistant/components/fan/* - - homeassistant/components/geo_location/* - - homeassistant/components/humidifier/* - - homeassistant/components/image_processing/* - - homeassistant/components/light/* - - homeassistant/components/lock/* - - homeassistant/components/media_player/* - - homeassistant/components/notify/* - - homeassistant/components/number/* - - homeassistant/components/remote/* - - homeassistant/components/scene/* - - homeassistant/components/select/* - - homeassistant/components/sensor/* - - homeassistant/components/siren/* - - homeassistant/components/stt/* - - homeassistant/components/switch/* - - homeassistant/components/tts/* - - homeassistant/components/vacuum/* - - homeassistant/components/water_heater/* - - homeassistant/components/weather/* + - homeassistant/components/air_quality/** + - homeassistant/components/alarm_control_panel/** + - homeassistant/components/binary_sensor/** + - homeassistant/components/button/** + - homeassistant/components/calendar/** + - homeassistant/components/camera/** + - homeassistant/components/climate/** + - homeassistant/components/cover/** + - homeassistant/components/device_tracker/** + - homeassistant/components/diagnostics/** + - homeassistant/components/fan/** + - homeassistant/components/geo_location/** + - homeassistant/components/humidifier/** + - homeassistant/components/image_processing/** + - homeassistant/components/light/** + - homeassistant/components/lock/** + - homeassistant/components/media_player/** + - homeassistant/components/notify/** + - homeassistant/components/number/** + - homeassistant/components/remote/** + - homeassistant/components/scene/** + - homeassistant/components/select/** + - homeassistant/components/sensor/** + - homeassistant/components/siren/** + - homeassistant/components/stt/** + - homeassistant/components/switch/** + - homeassistant/components/tts/** + - homeassistant/components/update/** + - homeassistant/components/vacuum/** + - homeassistant/components/water_heater/** + - homeassistant/components/weather/** # Extra components that trigger the full suite components: &components - - homeassistant/components/alert/* - - homeassistant/components/alexa/* - - homeassistant/components/auth/* - - homeassistant/components/automation/* - - homeassistant/components/cloud/* - - homeassistant/components/config/* - - homeassistant/components/configurator/* - - homeassistant/components/conversation/* - - homeassistant/components/demo/* - - homeassistant/components/device_automation/* - - homeassistant/components/dhcp/* - - homeassistant/components/discovery/* - - homeassistant/components/energy/* - - homeassistant/components/ffmpeg/* - - homeassistant/components/frontend/* - - homeassistant/components/google_assistant/* - - homeassistant/components/group/* - - homeassistant/components/hassio/* + - homeassistant/components/alert/** + - homeassistant/components/alexa/** + - homeassistant/components/auth/** + - homeassistant/components/automation/** + - homeassistant/components/backup/** + - homeassistant/components/cloud/** + - homeassistant/components/config/** + - homeassistant/components/configurator/** + - homeassistant/components/conversation/** + - homeassistant/components/demo/** + - homeassistant/components/device_automation/** + - homeassistant/components/dhcp/** + - homeassistant/components/discovery/** + - homeassistant/components/energy/** + - homeassistant/components/ffmpeg/** + - homeassistant/components/frontend/** + - homeassistant/components/google_assistant/** + - homeassistant/components/group/** + - homeassistant/components/hassio/** - homeassistant/components/homeassistant/** - homeassistant/components/http/** - - homeassistant/components/image/* - - homeassistant/components/input_boolean/* - - homeassistant/components/input_button/* - - homeassistant/components/input_datetime/* - - homeassistant/components/input_number/* - - homeassistant/components/input_select/* - - homeassistant/components/input_text/* - - homeassistant/components/logbook/* - - homeassistant/components/logger/* - - homeassistant/components/lovelace/* - - homeassistant/components/media_source/* - - homeassistant/components/mjpeg/* - - homeassistant/components/mqtt/* - - homeassistant/components/network/* - - homeassistant/components/onboarding/* - - homeassistant/components/otp/* - - homeassistant/components/persistent_notification/* - - homeassistant/components/person/* - - homeassistant/components/recorder/* - - homeassistant/components/safe_mode/* - - homeassistant/components/script/* - - homeassistant/components/shopping_list/* - - homeassistant/components/ssdp/* - - homeassistant/components/stream/* - - homeassistant/components/sun/* - - homeassistant/components/system_health/* - - homeassistant/components/tag/* - - homeassistant/components/template/* - - homeassistant/components/timer/* - - homeassistant/components/usb/* - - homeassistant/components/webhook/* - - homeassistant/components/websocket_api/* - - homeassistant/components/zeroconf/* - - homeassistant/components/zone/* + - homeassistant/components/image/** + - homeassistant/components/input_boolean/** + - homeassistant/components/input_button/** + - homeassistant/components/input_datetime/** + - homeassistant/components/input_number/** + - homeassistant/components/input_select/** + - homeassistant/components/input_text/** + - homeassistant/components/logbook/** + - homeassistant/components/logger/** + - homeassistant/components/lovelace/** + - homeassistant/components/media_source/** + - homeassistant/components/mjpeg/** + - homeassistant/components/mqtt/** + - homeassistant/components/network/** + - homeassistant/components/onboarding/** + - homeassistant/components/otp/** + - homeassistant/components/persistent_notification/** + - homeassistant/components/person/** + - homeassistant/components/recorder/** + - homeassistant/components/safe_mode/** + - homeassistant/components/script/** + - homeassistant/components/shopping_list/** + - homeassistant/components/ssdp/** + - homeassistant/components/stream/** + - homeassistant/components/sun/** + - homeassistant/components/system_health/** + - homeassistant/components/tag/** + - homeassistant/components/template/** + - homeassistant/components/timer/** + - homeassistant/components/usb/** + - homeassistant/components/webhook/** + - homeassistant/components/websocket_api/** + - homeassistant/components/zeroconf/** + - homeassistant/components/zone/** # Testing related files that affect the whole test/linting suite tests: &tests @@ -107,26 +109,27 @@ tests: &tests - requirements_test_pre_commit.txt - requirements_test.txt - tests/auth/** - - tests/backports/* + - tests/backports/** - tests/common.py - tests/conftest.py - - tests/hassfest/* - - tests/helpers/* + - tests/hassfest/** + - tests/helpers/** - tests/ignore_uncaught_exceptions.py - - tests/mock/* - - tests/pylint/* - - tests/scripts/* - - tests/test_util/* + - tests/mock/** + - tests/pylint/** + - tests/scripts/** + - tests/test_util/** - tests/testing_config/** - tests/util/** other: &other - - .github/workflows/* + - .github/workflows/** - homeassistant/scripts/** requirements: &requirements - - .github/workflows/* + - .github/workflows/** - homeassistant/package_constraints.txt + - script/pip_check - requirements*.txt - setup.cfg diff --git a/.coveragerc b/.coveragerc index 360bd5f6911..73814938619 100644 --- a/.coveragerc +++ b/.coveragerc @@ -59,6 +59,7 @@ omit = homeassistant/components/ampio/* homeassistant/components/android_ip_webcam/* homeassistant/components/androidtv/__init__.py + homeassistant/components/androidtv/diagnostics.py homeassistant/components/anel_pwrctrl/switch.py homeassistant/components/anthemav/media_player.py homeassistant/components/apcupsd/* @@ -101,10 +102,8 @@ omit = homeassistant/components/baidu/tts.py homeassistant/components/balboa/__init__.py homeassistant/components/beewi_smartclim/sensor.py - homeassistant/components/bbb_gpio/* homeassistant/components/bbox/device_tracker.py homeassistant/components/bbox/sensor.py - homeassistant/components/bh1750/sensor.py homeassistant/components/bitcoin/sensor.py homeassistant/components/bizkaibus/sensor.py homeassistant/components/blink/__init__.py @@ -114,16 +113,10 @@ omit = homeassistant/components/blink/const.py homeassistant/components/blink/sensor.py homeassistant/components/blinksticklight/light.py - homeassistant/components/blinkt/light.py homeassistant/components/blockchain/sensor.py homeassistant/components/bloomsky/* homeassistant/components/bluesound/* homeassistant/components/bluetooth_tracker/* - homeassistant/components/bme280/__init__.py - homeassistant/components/bme280/const.py - homeassistant/components/bme280/sensor.py - homeassistant/components/bme680/sensor.py - homeassistant/components/bmp280/sensor.py homeassistant/components/bmw_connected_drive/__init__.py homeassistant/components/bmw_connected_drive/binary_sensor.py homeassistant/components/bmw_connected_drive/button.py @@ -137,6 +130,7 @@ omit = homeassistant/components/bosch_shc/cover.py homeassistant/components/bosch_shc/entity.py homeassistant/components/bosch_shc/sensor.py + homeassistant/components/bosch_shc/switch.py homeassistant/components/braviatv/__init__.py homeassistant/components/braviatv/const.py homeassistant/components/braviatv/media_player.py @@ -204,6 +198,8 @@ omit = homeassistant/components/decora/light.py homeassistant/components/decora_wifi/light.py homeassistant/components/delijn/* + homeassistant/components/deluge/__init__.py + homeassistant/components/deluge/coordinator.py homeassistant/components/deluge/sensor.py homeassistant/components/deluge/switch.py homeassistant/components/denon/media_player.py @@ -218,10 +214,10 @@ omit = homeassistant/components/devolo_home_control/sensor.py homeassistant/components/devolo_home_control/subscriber.py homeassistant/components/devolo_home_control/switch.py - homeassistant/components/dht/sensor.py homeassistant/components/digital_ocean/* homeassistant/components/digitalloggers/switch.py homeassistant/components/discogs/sensor.py + homeassistant/components/discord/__init__.py homeassistant/components/discord/notify.py homeassistant/components/dlib_face_detect/image_processing.py homeassistant/components/dlib_face_identify/image_processing.py @@ -303,7 +299,6 @@ omit = homeassistant/components/environment_canada/camera.py homeassistant/components/environment_canada/sensor.py homeassistant/components/environment_canada/weather.py - homeassistant/components/envirophat/sensor.py homeassistant/components/envisalink/* homeassistant/components/ephember/climate.py homeassistant/components/epson/__init__.py @@ -342,7 +337,15 @@ omit = homeassistant/components/faa_delays/binary_sensor.py homeassistant/components/fastdotcom/* homeassistant/components/ffmpeg/camera.py - homeassistant/components/fibaro/* + homeassistant/components/fibaro/__init__.py + homeassistant/components/fibaro/binary_sensor.py + homeassistant/components/fibaro/climate.py + homeassistant/components/fibaro/cover.py + homeassistant/components/fibaro/light.py + homeassistant/components/fibaro/lock.py + homeassistant/components/fibaro/scene.py + homeassistant/components/fibaro/sensor.py + homeassistant/components/fibaro/switch.py homeassistant/components/filesize/sensor.py homeassistant/components/fints/sensor.py homeassistant/components/fireservicerota/__init__.py @@ -398,7 +401,6 @@ omit = homeassistant/components/fritz/common.py homeassistant/components/fritz/const.py homeassistant/components/fritz/device_tracker.py - homeassistant/components/fritz/sensor.py homeassistant/components/fritz/services.py homeassistant/components/fritz/switch.py homeassistant/components/fritzbox_callmonitor/__init__.py @@ -490,7 +492,6 @@ omit = homeassistant/components/honeywell/climate.py homeassistant/components/horizon/media_player.py homeassistant/components/hp_ilo/sensor.py - homeassistant/components/htu21d/sensor.py homeassistant/components/huawei_lte/__init__.py homeassistant/components/huawei_lte/binary_sensor.py homeassistant/components/huawei_lte/device_tracker.py @@ -622,7 +623,6 @@ omit = homeassistant/components/launch_library/sensor.py homeassistant/components/lcn/binary_sensor.py homeassistant/components/lcn/climate.py - homeassistant/components/lcn/cover.py homeassistant/components/lcn/helpers.py homeassistant/components/lcn/scene.py homeassistant/components/lcn/sensor.py @@ -678,7 +678,6 @@ omit = homeassistant/components/map/* homeassistant/components/mastodon/notify.py homeassistant/components/matrix/* - homeassistant/components/mcp23017/* homeassistant/components/media_extractor/* homeassistant/components/mediaroom/media_player.py homeassistant/components/melcloud/__init__.py @@ -718,8 +717,6 @@ omit = homeassistant/components/mjpeg/camera.py homeassistant/components/mjpeg/util.py homeassistant/components/mochad/* - homeassistant/components/modbus/climate.py - homeassistant/components/modbus/binary_sensor.py homeassistant/components/modem_callerid/button.py homeassistant/components/modem_callerid/sensor.py homeassistant/components/moehlenhoff_alpha2/__init__.py @@ -729,7 +726,6 @@ omit = homeassistant/components/motion_blinds/const.py homeassistant/components/motion_blinds/cover.py homeassistant/components/motion_blinds/sensor.py - homeassistant/components/mpchc/media_player.py homeassistant/components/mpd/media_player.py homeassistant/components/mqtt_room/sensor.py homeassistant/components/msteams/notify.py @@ -751,7 +747,6 @@ omit = homeassistant/components/mysensors/handler.py homeassistant/components/mysensors/helpers.py homeassistant/components/mysensors/light.py - homeassistant/components/mysensors/notify.py homeassistant/components/mysensors/switch.py homeassistant/components/mystrom/binary_sensor.py homeassistant/components/mystrom/light.py @@ -862,12 +857,12 @@ omit = homeassistant/components/openweathermap/weather_update_coordinator.py homeassistant/components/opnsense/* homeassistant/components/opple/light.py - homeassistant/components/orangepi_gpio/* homeassistant/components/oru/* homeassistant/components/orvibo/switch.py homeassistant/components/osramlightify/light.py homeassistant/components/otp/sensor.py homeassistant/components/overkiz/__init__.py + homeassistant/components/overkiz/alarm_control_panel.py homeassistant/components/overkiz/binary_sensor.py homeassistant/components/overkiz/button.py homeassistant/components/overkiz/climate.py @@ -889,13 +884,9 @@ omit = homeassistant/components/ovo_energy/__init__.py homeassistant/components/ovo_energy/const.py homeassistant/components/ovo_energy/sensor.py - homeassistant/components/ozw/__init__.py - homeassistant/components/ozw/entity.py - homeassistant/components/ozw/services.py homeassistant/components/panasonic_bluray/media_player.py homeassistant/components/panasonic_viera/media_player.py homeassistant/components/pandora/media_player.py - homeassistant/components/pcal9535a/* homeassistant/components/pencom/switch.py homeassistant/components/philips_js/__init__.py homeassistant/components/philips_js/diagnostics.py @@ -904,10 +895,7 @@ omit = homeassistant/components/philips_js/remote.py homeassistant/components/philips_js/switch.py homeassistant/components/pi_hole/sensor.py - homeassistant/components/pi4ioe5v9xxxx/binary_sensor.py - homeassistant/components/pi4ioe5v9xxxx/switch.py homeassistant/components/picotts/tts.py - homeassistant/components/piglow/light.py homeassistant/components/pilight/* homeassistant/components/ping/__init__.py homeassistant/components/ping/const.py @@ -920,8 +908,10 @@ omit = homeassistant/components/plaato/const.py homeassistant/components/plaato/entity.py homeassistant/components/plaato/sensor.py + homeassistant/components/plex/cast.py homeassistant/components/plex/media_player.py homeassistant/components/plex/view.py + homeassistant/components/plugwise/select.py homeassistant/components/plum_lightpad/light.py homeassistant/components/pocketcasts/sensor.py homeassistant/components/point/__init__.py @@ -967,7 +957,6 @@ omit = homeassistant/components/rainmachine/model.py homeassistant/components/rainmachine/sensor.py homeassistant/components/rainmachine/switch.py - homeassistant/components/raspihats/* homeassistant/components/raspyrfm/* homeassistant/components/recollect_waste/__init__.py homeassistant/components/recollect_waste/sensor.py @@ -981,6 +970,7 @@ omit = homeassistant/components/remote_rpi_gpio/* homeassistant/components/rest/notify.py homeassistant/components/rest/switch.py + homeassistant/components/rfxtrx/diagnostics.py homeassistant/components/ridwell/__init__.py homeassistant/components/ridwell/sensor.py homeassistant/components/ridwell/switch.py @@ -1003,9 +993,6 @@ omit = homeassistant/components/rova/sensor.py homeassistant/components/rpi_camera/* homeassistant/components/rpi_gpio/* - homeassistant/components/rpi_gpio_pwm/light.py - homeassistant/components/rpi_pfio/* - homeassistant/components/rpi_rf/switch.py homeassistant/components/rtorrent/sensor.py homeassistant/components/russound_rio/media_player.py homeassistant/components/russound_rnet/media_player.py @@ -1016,6 +1003,7 @@ omit = homeassistant/components/screenlogic/__init__.py homeassistant/components/screenlogic/binary_sensor.py homeassistant/components/screenlogic/climate.py + homeassistant/components/screenlogic/diagnostics.py homeassistant/components/screenlogic/light.py homeassistant/components/screenlogic/number.py homeassistant/components/screenlogic/sensor.py @@ -1026,8 +1014,6 @@ omit = homeassistant/components/sense/__init__.py homeassistant/components/sense/binary_sensor.py homeassistant/components/sense/sensor.py - homeassistant/components/sensehat/light.py - homeassistant/components/sensehat/sensor.py homeassistant/components/senseme/__init__.py homeassistant/components/senseme/binary_sensor.py homeassistant/components/senseme/discovery.py @@ -1036,10 +1022,14 @@ omit = homeassistant/components/senseme/light.py homeassistant/components/senseme/switch.py homeassistant/components/sensibo/__init__.py + homeassistant/components/sensibo/binary_sensor.py homeassistant/components/sensibo/climate.py homeassistant/components/sensibo/coordinator.py homeassistant/components/sensibo/diagnostics.py + homeassistant/components/sensibo/entity.py homeassistant/components/sensibo/number.py + homeassistant/components/sensibo/select.py + homeassistant/components/sensibo/sensor.py homeassistant/components/serial/sensor.py homeassistant/components/serial_pm/sensor.py homeassistant/components/sesame/lock.py @@ -1055,7 +1045,6 @@ omit = homeassistant/components/shelly/number.py homeassistant/components/shelly/sensor.py homeassistant/components/shelly/utils.py - homeassistant/components/sht31/sensor.py homeassistant/components/sigfox/sensor.py homeassistant/components/simplepush/notify.py homeassistant/components/simplisafe/__init__.py @@ -1086,9 +1075,6 @@ omit = homeassistant/components/smappee/sensor.py homeassistant/components/smappee/switch.py homeassistant/components/smarty/* - homeassistant/components/smarthab/__init__.py - homeassistant/components/smarthab/cover.py - homeassistant/components/smarthab/light.py homeassistant/components/sms/__init__.py homeassistant/components/sms/const.py homeassistant/components/sms/gateway.py @@ -1194,6 +1180,7 @@ omit = homeassistant/components/synology_dsm/sensor.py homeassistant/components/synology_dsm/service.py homeassistant/components/synology_dsm/switch.py + homeassistant/components/synology_dsm/update.py homeassistant/components/synology_srm/device_tracker.py homeassistant/components/syslog/notify.py homeassistant/components/system_bridge/__init__.py @@ -1209,7 +1196,10 @@ omit = homeassistant/components/tado/sensor.py homeassistant/components/tado/water_heater.py homeassistant/components/tank_utility/sensor.py - homeassistant/components/tankerkoenig/* + homeassistant/components/tankerkoenig/__init__.py + homeassistant/components/tankerkoenig/binary_sensor.py + homeassistant/components/tankerkoenig/const.py + homeassistant/components/tankerkoenig/sensor.py homeassistant/components/tapsaff/binary_sensor.py homeassistant/components/tautulli/const.py homeassistant/components/tautulli/coordinator.py @@ -1244,7 +1234,6 @@ omit = homeassistant/components/tmb/sensor.py homeassistant/components/todoist/calendar.py homeassistant/components/todoist/const.py - homeassistant/components/tof/sensor.py homeassistant/components/tolo/__init__.py homeassistant/components/tolo/binary_sensor.py homeassistant/components/tolo/button.py @@ -1287,8 +1276,10 @@ omit = homeassistant/components/tradfri/light.py homeassistant/components/tradfri/sensor.py homeassistant/components/tradfri/switch.py + homeassistant/components/trafikverket_train/__init__.py homeassistant/components/trafikverket_train/sensor.py homeassistant/components/trafikverket_weatherstation/__init__.py + homeassistant/components/trafikverket_weatherstation/coordinator.py homeassistant/components/trafikverket_weatherstation/sensor.py homeassistant/components/transmission/sensor.py homeassistant/components/transmission/switch.py @@ -1335,10 +1326,8 @@ omit = homeassistant/components/upc_connect/* homeassistant/components/uscis/sensor.py homeassistant/components/vallox/__init__.py - homeassistant/components/vallox/const.py homeassistant/components/vallox/fan.py homeassistant/components/vallox/sensor.py - homeassistant/components/vallox/binary_sensor.py homeassistant/components/vasttrafik/sensor.py homeassistant/components/velbus/__init__.py homeassistant/components/velbus/binary_sensor.py @@ -1391,6 +1380,9 @@ omit = homeassistant/components/volumio/browse_media.py homeassistant/components/volumio/media_player.py homeassistant/components/volvooncall/* + homeassistant/components/vulcan/__init__.py + homeassistant/components/vulcan/calendar.py + homeassistant/components/vulcan/fetch_data.py homeassistant/components/w800rf32/* homeassistant/components/waqi/sensor.py homeassistant/components/waterfurnace/* @@ -1433,6 +1425,7 @@ omit = homeassistant/components/xiaomi_miio/air_quality.py homeassistant/components/xiaomi_miio/alarm_control_panel.py homeassistant/components/xiaomi_miio/binary_sensor.py + homeassistant/components/xiaomi_miio/button.py homeassistant/components/xiaomi_miio/device.py homeassistant/components/xiaomi_miio/device_tracker.py homeassistant/components/xiaomi_miio/fan.py @@ -1489,18 +1482,19 @@ omit = homeassistant/components/ziggo_mediabox_xl/media_player.py homeassistant/components/zoneminder/* homeassistant/components/supla/* - homeassistant/components/zwave/util.py homeassistant/components/zwave_js/discovery.py homeassistant/components/zwave_js/sensor.py homeassistant/components/zwave_me/__init__.py homeassistant/components/zwave_me/binary_sensor.py homeassistant/components/zwave_me/button.py + homeassistant/components/zwave_me/cover.py homeassistant/components/zwave_me/climate.py homeassistant/components/zwave_me/helpers.py homeassistant/components/zwave_me/light.py homeassistant/components/zwave_me/lock.py homeassistant/components/zwave_me/number.py homeassistant/components/zwave_me/sensor.py + homeassistant/components/zwave_me/siren.py homeassistant/components/zwave_me/switch.py [report] diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index 2ca9b754a3a..f365987b0d6 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -24,12 +24,12 @@ jobs: publish: ${{ steps.version.outputs.publish }} steps: - name: Checkout the repository - uses: actions/checkout@v2.4.0 + uses: actions/checkout@v3.0.0 with: fetch-depth: 0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.3.2 + uses: actions/setup-python@v3.0.0 with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -67,10 +67,10 @@ jobs: if: github.repository_owner == 'home-assistant' && needs.init.outputs.publish == 'true' steps: - name: Checkout the repository - uses: actions/checkout@v2.4.0 + uses: actions/checkout@v3.0.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.3.2 + uses: actions/setup-python@v3.0.0 with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -100,11 +100,11 @@ jobs: arch: ${{ fromJson(needs.init.outputs.architectures) }} steps: - name: Checkout the repository - uses: actions/checkout@v2.4.0 + uses: actions/checkout@v3.0.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} if: needs.init.outputs.channel == 'dev' - uses: actions/setup-python@v2.3.2 + uses: actions/setup-python@v3.0.0 with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -135,7 +135,7 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Build base image - uses: home-assistant/builder@2022.01.0 + uses: home-assistant/builder@2022.03.1 with: args: | $BUILD_ARGS \ @@ -173,7 +173,7 @@ jobs: - tinker steps: - name: Checkout the repository - uses: actions/checkout@v2.4.0 + uses: actions/checkout@v3.0.0 - name: Set build additional args run: | @@ -200,7 +200,7 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Build base image - uses: home-assistant/builder@2022.01.0 + uses: home-assistant/builder@2022.03.1 with: args: | $BUILD_ARGS \ @@ -216,7 +216,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout the repository - uses: actions/checkout@v2.4.0 + uses: actions/checkout@v3.0.0 - name: Initialize git uses: home-assistant/actions/helpers/git-init@master @@ -255,7 +255,7 @@ jobs: - "homeassistant" steps: - name: Checkout the repository - uses: actions/checkout@v2.4.0 + uses: actions/checkout@v3.0.0 - name: Login to DockerHub if: matrix.registry == 'homeassistant' @@ -339,7 +339,7 @@ jobs: # Create general tags if [[ "${{ needs.init.outputs.version }}" =~ d ]]; then - create_manifest"dev" "${{ needs.init.outputs.version }}" + create_manifest "dev" "${{ needs.init.outputs.version }}" elif [[ "${{ needs.init.outputs.version }}" =~ b ]]; then create_manifest "beta" "${{ needs.init.outputs.version }}" create_manifest "rc" "${{ needs.init.outputs.version }}" diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index b1ff5dca8d1..f447a83243e 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -11,18 +11,18 @@ on: workflow_dispatch: inputs: full: - description: 'Full run (regardless of changes)' + description: "Full run (regardless of changes)" default: false type: boolean lint-only: - description: 'Skip pytest' + description: "Skip pytest" default: false type: boolean env: CACHE_VERSION: 9 PIP_CACHE_VERSION: 3 - HA_SHORT_VERSION: 2022.3 + HA_SHORT_VERSION: 2022.4 DEFAULT_PYTHON: 3.9 PRE_COMMIT_CACHE: ~/.cache/pre-commit PIP_CACHE: /tmp/pip-cache @@ -51,7 +51,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out code from GitHub - uses: actions/checkout@v2.4.0 + uses: actions/checkout@v3.0.0 - name: Filter for core changes uses: dorny/paths-filter@v2.10.2 id: core @@ -62,7 +62,7 @@ jobs: integrations=$(ls -Ad ./homeassistant/components/[!_]* | xargs -n 1 basename) touch .integration_paths.yaml for integration in $integrations; do - echo "${integration}: [homeassistant/components/${integration}/*, tests/components/${integration}/*]" \ + echo "${integration}: [homeassistant/components/${integration}/**, tests/components/${integration}/**]" \ >> .integration_paths.yaml; done echo "Result:" @@ -119,7 +119,8 @@ jobs: || [[ "${{ github.ref }}" == "refs/heads/master" ]] \ || [[ "${{ github.ref }}" == "refs/heads/rc" ]] \ || [[ "${{ steps.core.outputs.any }}" == "true" ]] \ - || [[ "${{ github.event.inputs.full }}" == "true" ]]; + || [[ "${{ github.event.inputs.full }}" == "true" ]] \ + || [[ "${{ contains(github.event.pull_request.labels.*.name, 'ci-full-run') }}" == "true" ]]; then test_groups="[1, 2, 3, 4, 5, 6]" test_group_count=6 @@ -151,10 +152,10 @@ jobs: pre-commit-key: ${{ steps.generate-pre-commit-key.outputs.key }} steps: - name: Check out code from GitHub - uses: actions/checkout@v2.4.0 + uses: actions/checkout@v3.0.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python - uses: actions/setup-python@v2.3.2 + uses: actions/setup-python@v3.0.0 with: python-version: ${{ env.DEFAULT_PYTHON }} - name: Generate partial Python venv restore key @@ -171,7 +172,7 @@ jobs: env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v2.1.7 + uses: actions/cache@v3.0.0 with: path: venv key: >- @@ -188,7 +189,7 @@ jobs: # ${{ runner.os }}-${{ steps.python.outputs.python-version }}-base-venv-${{ env.CACHE_VERSION }}- - name: Restore pip wheel cache if: steps.cache-venv.outputs.cache-hit != 'true' - uses: actions/cache@v2.1.7 + uses: actions/cache@v3.0.0 with: path: ${{ env.PIP_CACHE }} key: >- @@ -211,7 +212,7 @@ jobs: hashFiles('.pre-commit-config.yaml') }}" - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v2.1.7 + uses: actions/cache@v3.0.0 with: path: ${{ env.PRE_COMMIT_CACHE }} key: >- @@ -232,15 +233,15 @@ jobs: - prepare-base steps: - name: Check out code from GitHub - uses: actions/checkout@v2.4.0 + uses: actions/checkout@v3.0.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.3.2 + uses: actions/setup-python@v3.0.0 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v2.1.7 + uses: actions/cache@v3.0.0 with: path: venv key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ @@ -252,7 +253,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v2.1.7 + uses: actions/cache@v3.0.0 with: path: ${{ env.PRE_COMMIT_CACHE }} key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }} @@ -282,15 +283,15 @@ jobs: - prepare-base steps: - name: Check out code from GitHub - uses: actions/checkout@v2.4.0 + uses: actions/checkout@v3.0.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.3.2 + uses: actions/setup-python@v3.0.0 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v2.1.7 + uses: actions/cache@v3.0.0 with: path: venv key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ @@ -302,7 +303,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v2.1.7 + uses: actions/cache@v3.0.0 with: path: ${{ env.PRE_COMMIT_CACHE }} key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }} @@ -333,15 +334,15 @@ jobs: needs: prepare-base steps: - name: Check out code from GitHub - uses: actions/checkout@v2.4.0 + uses: actions/checkout@v3.0.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.3.2 + uses: actions/setup-python@v3.0.0 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v2.1.7 + uses: actions/cache@v3.0.0 with: path: venv key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ @@ -353,7 +354,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v2.1.7 + uses: actions/cache@v3.0.0 with: path: ${{ env.PRE_COMMIT_CACHE }} key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }} @@ -375,15 +376,15 @@ jobs: - prepare-base steps: - name: Check out code from GitHub - uses: actions/checkout@v2.4.0 + uses: actions/checkout@v3.0.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.3.2 + uses: actions/setup-python@v3.0.0 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v2.1.7 + uses: actions/cache@v3.0.0 with: path: venv key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ @@ -395,7 +396,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v2.1.7 + uses: actions/cache@v3.0.0 with: path: ${{ env.PRE_COMMIT_CACHE }} key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }} @@ -434,6 +435,11 @@ jobs: . venv/bin/activate pre-commit run --hook-stage manual check-json --all-files + - name: Run prettier + run: | + . venv/bin/activate + pre-commit run --hook-stage manual prettier --all-files + - name: Register check executables problem matcher run: | echo "::add-matcher::.github/workflows/matchers/check-executables-have-shebangs.json" @@ -485,10 +491,10 @@ jobs: container: homeassistant/ci-azure:${{ matrix.python-version }} steps: - name: Check out code from GitHub - uses: actions/checkout@v2.4.0 + uses: actions/checkout@v3.0.0 - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache@v2.1.7 + uses: actions/cache@v3.0.0 with: path: venv key: ${{ runner.os }}-${{ matrix.python-version }}-${{ @@ -509,15 +515,15 @@ jobs: needs: prepare-base steps: - name: Check out code from GitHub - uses: actions/checkout@v2.4.0 + uses: actions/checkout@v3.0.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.3.2 + uses: actions/setup-python@v3.0.0 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v2.1.7 + uses: actions/cache@v3.0.0 with: path: venv key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ @@ -544,7 +550,7 @@ jobs: container: homeassistant/ci-azure:${{ matrix.python-version }} steps: - name: Check out code from GitHub - uses: actions/checkout@v2.4.0 + uses: actions/checkout@v3.0.0 - name: Generate partial Python venv restore key id: generate-python-key run: >- @@ -559,7 +565,7 @@ jobs: env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache@v2.1.7 + uses: actions/cache@v3.0.0 with: path: venv key: >- @@ -576,7 +582,7 @@ jobs: # ${{ runner.os }}-${{ matrix.python-version }}-venv-${{ env.CACHE_VERSION }}- - name: Restore pip wheel cache if: steps.cache-venv.outputs.cache-hit != 'true' - uses: actions/cache@v2.1.7 + uses: actions/cache@v3.0.0 with: path: ${{ env.PIP_CACHE }} key: >- @@ -612,10 +618,10 @@ jobs: container: homeassistant/ci-azure:${{ matrix.python-version }} steps: - name: Check out code from GitHub - uses: actions/checkout@v2.4.0 + uses: actions/checkout@v3.0.0 - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache@v2.1.7 + uses: actions/cache@v3.0.0 with: path: venv key: ${{ runner.os }}-${{ matrix.python-version }}-${{ @@ -654,10 +660,10 @@ jobs: container: homeassistant/ci-azure:${{ matrix.python-version }} steps: - name: Check out code from GitHub - uses: actions/checkout@v2.4.0 + uses: actions/checkout@v3.0.0 - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache@v2.1.7 + uses: actions/cache@v3.0.0 with: path: venv key: ${{ runner.os }}-${{ matrix.python-version }}-${{ @@ -686,7 +692,7 @@ jobs: pip-check: runs-on: ubuntu-latest - if: needs.changes.outputs.requirements == 'true' + if: needs.changes.outputs.requirements == 'true' || github.event.inputs.full == 'true' needs: - changes - prepare-tests @@ -698,10 +704,10 @@ jobs: container: homeassistant/ci-azure:${{ matrix.python-version }} steps: - name: Check out code from GitHub - uses: actions/checkout@v2.4.0 + uses: actions/checkout@v3.0.0 - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache@v2.1.7 + uses: actions/cache@v3.0.0 with: path: venv key: ${{ runner.os }}-${{ matrix.python-version }}-${{ @@ -741,10 +747,10 @@ jobs: container: homeassistant/ci-azure:${{ matrix.python-version }} steps: - name: Check out code from GitHub - uses: actions/checkout@v2.4.0 + uses: actions/checkout@v3.0.0 - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache@v2.1.7 + uses: actions/cache@v3.0.0 with: path: venv key: ${{ runner.os }}-${{ matrix.python-version }}-${{ @@ -812,24 +818,8 @@ jobs: --durations-min=1 \ -p no:sugar \ tests/components/${{ matrix.group }} - - name: Run pytest (partially); no coverage - if: needs.changes.outputs.test_full_suite == 'false' - timeout-minutes: 10 - run: | - . venv/bin/activate - python --version - python3 -X dev -m pytest \ - -qq \ - --timeout=9 \ - --durations=10 \ - -n auto \ - -o console_output_style=count \ - --durations=0 \ - --durations-min=1 \ - -p no:sugar \ - tests/components/${{ matrix.group }} - name: Upload coverage artifact - uses: actions/upload-artifact@v2.3.1 + uses: actions/upload-artifact@v3.0.0 with: name: coverage-${{ matrix.python-version }}-${{ matrix.group }} path: coverage.xml @@ -845,7 +835,7 @@ jobs: - pytest steps: - name: Check out code from GitHub - uses: actions/checkout@v2.4.0 + uses: actions/checkout@v3.0.0 - name: Download all coverage artifacts uses: actions/download-artifact@v2 - name: Upload coverage to Codecov (full coverage) diff --git a/.github/workflows/translations.yaml b/.github/workflows/translations.yaml index 1e637b02b1a..55273226b89 100644 --- a/.github/workflows/translations.yaml +++ b/.github/workflows/translations.yaml @@ -21,10 +21,10 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout the repository - uses: actions/checkout@v2.4.0 + uses: actions/checkout@v3.0.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.3.2 + uses: actions/setup-python@v3.0.0 with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -40,10 +40,10 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout the repository - uses: actions/checkout@v2.4.0 + uses: actions/checkout@v3.0.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.3.2 + uses: actions/setup-python@v3.0.0 with: python-version: ${{ env.DEFAULT_PYTHON }} diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index a0d6396ec30..6c31b9003c1 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -22,7 +22,7 @@ jobs: architectures: ${{ steps.info.outputs.architectures }} steps: - name: Checkout the repository - uses: actions/checkout@v2.4.0 + uses: actions/checkout@v3.0.0 - name: Get information id: info @@ -50,13 +50,13 @@ jobs: ) > .env_file - name: Upload env_file - uses: actions/upload-artifact@v2.3.1 + uses: actions/upload-artifact@v3.0.0 with: name: env_file path: ./.env_file - name: Upload requirements_diff - uses: actions/upload-artifact@v2.3.1 + uses: actions/upload-artifact@v3.0.0 with: name: requirements_diff path: ./requirements_diff.txt @@ -74,7 +74,7 @@ jobs: - "3.9-alpine3.14" steps: - name: Checkout the repository - uses: actions/checkout@v2.4.0 + uses: actions/checkout@v3.0.0 - name: Download env_file uses: actions/download-artifact@v2 @@ -99,7 +99,7 @@ jobs: pip: "Cython;numpy" skip-binary: aiohttp constraints: "homeassistant/package_constraints.txt" - requirements-diff: 'requirements_diff.txt' + requirements-diff: "requirements_diff.txt" requirements: "requirements.txt" integrations: @@ -115,7 +115,7 @@ jobs: - "3.9-alpine3.14" steps: - name: Checkout the repository - uses: actions/checkout@v2.4.0 + uses: actions/checkout@v3.0.0 - name: Download env_file uses: actions/download-artifact@v2 @@ -135,14 +135,9 @@ jobs: sed -i "s|# bluepy|bluepy|g" ${requirement_file} sed -i "s|# beacontools|beacontools|g" ${requirement_file} sed -i "s|# RPi.GPIO|RPi.GPIO|g" ${requirement_file} - sed -i "s|# raspihats|raspihats|g" ${requirement_file} - sed -i "s|# rpi-rf|rpi-rf|g" ${requirement_file} - sed -i "s|# blinkt|blinkt|g" ${requirement_file} sed -i "s|# fritzconnection|fritzconnection|g" ${requirement_file} sed -i "s|# pyuserinput|pyuserinput|g" ${requirement_file} sed -i "s|# evdev|evdev|g" ${requirement_file} - sed -i "s|# smbus-cffi|smbus-cffi|g" ${requirement_file} - sed -i "s|# i2csense|i2csense|g" ${requirement_file} sed -i "s|# python-eq3bt|python-eq3bt|g" ${requirement_file} sed -i "s|# pycups|pycups|g" ${requirement_file} sed -i "s|# homekit|homekit|g" ${requirement_file} @@ -152,9 +147,7 @@ jobs: sed -i "s|# PySwitchbot|PySwitchbot|g" ${requirement_file} sed -i "s|# pySwitchmate|pySwitchmate|g" ${requirement_file} sed -i "s|# face_recognition|face_recognition|g" ${requirement_file} - sed -i "s|# bme680|bme680|g" ${requirement_file} sed -i "s|# python-gammu|python-gammu|g" ${requirement_file} - sed -i "s|# homeassistant-pyozw|homeassistant-pyozw|g" ${requirement_file} done - name: Build wheels @@ -170,5 +163,5 @@ jobs: pip: "Cython;numpy;scikit-build" skip-binary: aiohttp,grpcio constraints: "homeassistant/package_constraints.txt" - requirements-diff: 'requirements_diff.txt' + requirements-diff: "requirements_diff.txt" requirements: "requirements_all.txt" diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7cdd28cd6e9..a32299a2224 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,11 +1,11 @@ repos: - repo: https://github.com/asottile/pyupgrade - rev: v2.31.0 + rev: v2.31.1 hooks: - id: pyupgrade args: [--py39-plus] - repo: https://github.com/psf/black - rev: 22.1.0 + rev: 22.3.0 hooks: - id: black args: @@ -31,12 +31,12 @@ repos: - pyflakes==2.4.0 - flake8-docstrings==1.6.0 - pydocstyle==6.1.1 - - flake8-comprehensions==3.7.0 + - flake8-comprehensions==3.8.0 - flake8-noqa==1.2.1 - mccabe==0.6.1 files: ^(homeassistant|script|tests)/.+\.py$ - repo: https://github.com/PyCQA/bandit - rev: 1.7.0 + rev: 1.7.4 hooks: - id: bandit args: @@ -45,7 +45,7 @@ repos: - --configfile=tests/bandit.yaml files: ^(homeassistant|script|tests)/.+\.py$ - repo: https://github.com/PyCQA/isort - rev: 5.10.0 + rev: 5.10.1 hooks: - id: isort - repo: https://github.com/pre-commit/pre-commit-hooks @@ -65,10 +65,9 @@ repos: hooks: - id: yamllint - repo: https://github.com/pre-commit/mirrors-prettier - rev: v2.2.1 + rev: v2.6.1 hooks: - id: prettier - stages: [manual] - repo: https://github.com/cdce8p/python-typing-update rev: v0.3.5 hooks: diff --git a/.prettierignore b/.prettierignore index 1102d3a4e26..99dcbe1a117 100644 --- a/.prettierignore +++ b/.prettierignore @@ -2,4 +2,3 @@ azure-*.yml docs/source/_templates/* homeassistant/components/*/translations/*.json -tests/fixtures/* diff --git a/.strict-typing b/.strict-typing index 621c6c315fc..e808b54c85e 100644 --- a/.strict-typing +++ b/.strict-typing @@ -28,10 +28,12 @@ homeassistant.util.unit_system # --- Add components below this line --- homeassistant.components +homeassistant.components.alert.* homeassistant.components.abode.* homeassistant.components.acer_projector.* homeassistant.components.accuweather.* homeassistant.components.actiontec.* +homeassistant.components.adguard.* homeassistant.components.aftership.* homeassistant.components.air_quality.* homeassistant.components.airly.* @@ -45,6 +47,7 @@ homeassistant.components.amcrest.* homeassistant.components.ampio.* homeassistant.components.aseko_pool_live.* homeassistant.components.automation.* +homeassistant.components.backup.* homeassistant.components.binary_sensor.* homeassistant.components.bluetooth_tracker.* homeassistant.components.bmw_connected_drive.* @@ -68,6 +71,7 @@ homeassistant.components.device_automation.* homeassistant.components.device_tracker.* homeassistant.components.devolo_home_control.* homeassistant.components.devolo_home_network.* +homeassistant.components.dhcp.* homeassistant.components.dlna_dmr.* homeassistant.components.dnsip.* homeassistant.components.dsmr.* @@ -78,6 +82,7 @@ homeassistant.components.esphome.* homeassistant.components.energy.* homeassistant.components.evil_genius_labs.* homeassistant.components.fastdotcom.* +homeassistant.components.filesize.* homeassistant.components.fitbit.* homeassistant.components.flunearyou.* homeassistant.components.flux_led.* @@ -94,6 +99,14 @@ homeassistant.components.group.* homeassistant.components.guardian.* homeassistant.components.history.* homeassistant.components.homeassistant.triggers.event +homeassistant.components.homekit +homeassistant.components.homekit.accessories +homeassistant.components.homekit.aidmanager +homeassistant.components.homekit.config_flow +homeassistant.components.homekit.diagnostics +homeassistant.components.homekit.logbook +homeassistant.components.homekit.type_triggers +homeassistant.components.homekit.util homeassistant.components.homekit_controller homeassistant.components.homekit_controller.alarm_control_panel homeassistant.components.homekit_controller.button @@ -114,6 +127,7 @@ homeassistant.components.isy994.* homeassistant.components.iqvia.* homeassistant.components.jellyfin.* homeassistant.components.jewish_calendar.* +homeassistant.components.kaleidescape.* homeassistant.components.knx.* homeassistant.components.kraken.* homeassistant.components.lametric.* @@ -125,10 +139,11 @@ homeassistant.components.lookin.* homeassistant.components.luftdaten.* homeassistant.components.mailbox.* homeassistant.components.media_player.* +homeassistant.components.media_source.* homeassistant.components.mjpeg.* homeassistant.components.modbus.* homeassistant.components.modem_callerid.* -homeassistant.components.media_source.* +homeassistant.components.moon.* homeassistant.components.mysensors.* homeassistant.components.nam.* homeassistant.components.nanoleaf.* @@ -146,6 +161,7 @@ homeassistant.components.oncue.* homeassistant.components.onewire.* homeassistant.components.open_meteo.* homeassistant.components.openuv.* +homeassistant.components.peco.* homeassistant.components.overkiz.* homeassistant.components.persistent_notification.* homeassistant.components.pi_hole.* @@ -156,9 +172,18 @@ homeassistant.components.pure_energie.* homeassistant.components.rainmachine.* homeassistant.components.rdw.* homeassistant.components.recollect_waste.* +homeassistant.components.recorder +homeassistant.components.recorder.const +homeassistant.components.recorder.backup +homeassistant.components.recorder.executor +homeassistant.components.recorder.history +homeassistant.components.recorder.models +homeassistant.components.recorder.pool homeassistant.components.recorder.purge homeassistant.components.recorder.repack homeassistant.components.recorder.statistics +homeassistant.components.recorder.util +homeassistant.components.recorder.websocket_api homeassistant.components.remote.* homeassistant.components.renault.* homeassistant.components.ridwell.* @@ -202,8 +227,10 @@ homeassistant.components.tts.* homeassistant.components.twentemilieu.* homeassistant.components.unifiprotect.* homeassistant.components.upcloud.* +homeassistant.components.update.* homeassistant.components.uptime.* homeassistant.components.uptimerobot.* +homeassistant.components.usb.* homeassistant.components.vacuum.* homeassistant.components.vallox.* homeassistant.components.velbus.* @@ -217,6 +244,8 @@ homeassistant.components.websocket_api.* homeassistant.components.wemo.* homeassistant.components.whois.* homeassistant.components.wiz.* +homeassistant.components.worldclock.* +homeassistant.components.yale_smart_alarm.* homeassistant.components.zodiac.* homeassistant.components.zeroconf.* homeassistant.components.zone.* diff --git a/CODEOWNERS b/CODEOWNERS index 283bd6442b1..6da9c48adc9 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -2,1133 +2,1210 @@ # People marked here will be automatically requested for a review # when the code that they own is touched. # https://github.com/blog/2392-introducing-code-owners +# https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners # Home Assistant Core setup.cfg @home-assistant/core -homeassistant/*.py @home-assistant/core -homeassistant/helpers/* @home-assistant/core -homeassistant/util/* @home-assistant/core +/homeassistant/*.py @home-assistant/core +/homeassistant/helpers/ @home-assistant/core +/homeassistant/util/ @home-assistant/core # Home Assistant Supervisor build.json @home-assistant/supervisor -machine/* @home-assistant/supervisor -rootfs/* @home-assistant/supervisor -Dockerfile @home-assistant/supervisor +/machine/ @home-assistant/supervisor +/rootfs/ @home-assistant/supervisor +/Dockerfile @home-assistant/supervisor # Other code -homeassistant/scripts/check_config.py @kellerza +/homeassistant/scripts/check_config.py @kellerza # Integrations -homeassistant/components/abode/* @shred86 -tests/components/abode/* @shred86 -homeassistant/components/accuweather/* @bieniu -tests/components/accuweather/* @bieniu -homeassistant/components/acmeda/* @atmurray -tests/components/acmeda/* @atmurray -homeassistant/components/adax/* @danielhiversen -tests/components/adax/* @danielhiversen -homeassistant/components/adguard/* @frenck -tests/components/adguard/* @frenck -homeassistant/components/advantage_air/* @Bre77 -tests/components/advantage_air/* @Bre77 -homeassistant/components/agent_dvr/* @ispysoftware -tests/components/agent_dvr/* @ispysoftware -homeassistant/components/airly/* @bieniu -tests/components/airly/* @bieniu -homeassistant/components/airnow/* @asymworks -tests/components/airnow/* @asymworks -homeassistant/components/airthings/* @danielhiversen -tests/components/airthings/* @danielhiversen -homeassistant/components/airtouch4/* @LonePurpleWolf -tests/components/airtouch4/* @LonePurpleWolf -homeassistant/components/airvisual/* @bachya -tests/components/airvisual/* @bachya -homeassistant/components/alexa/* @home-assistant/cloud @ochlocracy -tests/components/alexa/* @home-assistant/cloud @ochlocracy -homeassistant/components/almond/* @gcampax @balloob -tests/components/almond/* @gcampax @balloob -homeassistant/components/alpha_vantage/* @fabaff -homeassistant/components/ambee/* @frenck -tests/components/ambee/* @frenck -homeassistant/components/amberelectric/* @madpilot -tests/components/amberelectric/* @madpilot -homeassistant/components/ambiclimate/* @danielhiversen -tests/components/ambiclimate/* @danielhiversen -homeassistant/components/ambient_station/* @bachya -tests/components/ambient_station/* @bachya -homeassistant/components/amcrest/* @flacjacket -homeassistant/components/analytics/* @home-assistant/core @ludeeus -tests/components/analytics/* @home-assistant/core @ludeeus -homeassistant/components/androidtv/* @JeffLIrion @ollo69 -tests/components/androidtv/* @JeffLIrion @ollo69 -homeassistant/components/apache_kafka/* @bachya -tests/components/apache_kafka/* @bachya -homeassistant/components/api/* @home-assistant/core -tests/components/api/* @home-assistant/core -homeassistant/components/apple_tv/* @postlund -tests/components/apple_tv/* @postlund -homeassistant/components/apprise/* @caronc -tests/components/apprise/* @caronc -homeassistant/components/aprs/* @PhilRW -tests/components/aprs/* @PhilRW -homeassistant/components/arcam_fmj/* @elupus -tests/components/arcam_fmj/* @elupus -homeassistant/components/arest/* @fabaff -homeassistant/components/arris_tg2492lg/* @vanbalken -homeassistant/components/aseko_pool_live/* @milanmeu -tests/components/aseko_pool_live/* @milanmeu -homeassistant/components/asuswrt/* @kennedyshead @ollo69 -tests/components/asuswrt/* @kennedyshead @ollo69 -homeassistant/components/atag/* @MatsNL -tests/components/atag/* @MatsNL -homeassistant/components/aten_pe/* @mtdcr -homeassistant/components/atome/* @baqs -homeassistant/components/august/* @bdraco -tests/components/august/* @bdraco -homeassistant/components/aurora/* @djtimca -tests/components/aurora/* @djtimca -homeassistant/components/aurora_abb_powerone/* @davet2001 -tests/components/aurora_abb_powerone/* @davet2001 -homeassistant/components/aussie_broadband/* @nickw444 @Bre77 -tests/components/aussie_broadband/* @nickw444 @Bre77 -homeassistant/components/auth/* @home-assistant/core -tests/components/auth/* @home-assistant/core -homeassistant/components/automation/* @home-assistant/core -tests/components/automation/* @home-assistant/core -homeassistant/components/avea/* @pattyland -homeassistant/components/awair/* @ahayworth @danielsjf -tests/components/awair/* @ahayworth @danielsjf -homeassistant/components/axis/* @Kane610 -tests/components/axis/* @Kane610 -homeassistant/components/azure_devops/* @timmo001 -tests/components/azure_devops/* @timmo001 -homeassistant/components/azure_event_hub/* @eavanvalkenburg -tests/components/azure_event_hub/* @eavanvalkenburg -homeassistant/components/azure_service_bus/* @hfurubotten -homeassistant/components/balboa/* @garbled1 -tests/components/balboa/* @garbled1 -homeassistant/components/beewi_smartclim/* @alemuro -homeassistant/components/bitcoin/* @fabaff -homeassistant/components/bizkaibus/* @UgaitzEtxebarria -homeassistant/components/blebox/* @bbx-a @bbx-jp -tests/components/blebox/* @bbx-a @bbx-jp -homeassistant/components/blink/* @fronzbot -tests/components/blink/* @fronzbot -homeassistant/components/blueprint/* @home-assistant/core -tests/components/blueprint/* @home-assistant/core -homeassistant/components/bluesound/* @thrawnarn -homeassistant/components/bmp280/* @belidzs -homeassistant/components/bmw_connected_drive/* @gerard33 @rikroe -tests/components/bmw_connected_drive/* @gerard33 @rikroe -homeassistant/components/bond/* @bdraco @prystupa @joshs85 -tests/components/bond/* @bdraco @prystupa @joshs85 -homeassistant/components/bosch_shc/* @tschamm -tests/components/bosch_shc/* @tschamm -homeassistant/components/braviatv/* @bieniu @Drafteed -tests/components/braviatv/* @bieniu @Drafteed -homeassistant/components/broadlink/* @danielhiversen @felipediel @L-I-Am -tests/components/broadlink/* @danielhiversen @felipediel @L-I-Am -homeassistant/components/brother/* @bieniu -tests/components/brother/* @bieniu -homeassistant/components/brunt/* @eavanvalkenburg -tests/components/brunt/* @eavanvalkenburg -homeassistant/components/bsblan/* @liudger -tests/components/bsblan/* @liudger -homeassistant/components/bt_smarthub/* @jxwolstenholme -homeassistant/components/buienradar/* @mjj4791 @ties @Robbie1221 -tests/components/buienradar/* @mjj4791 @ties @Robbie1221 -homeassistant/components/button/* @home-assistant/core -tests/components/button/* @home-assistant/core -homeassistant/components/cast/* @emontnemery -tests/components/cast/* @emontnemery -homeassistant/components/cert_expiry/* @Cereal2nd @jjlawren -tests/components/cert_expiry/* @Cereal2nd @jjlawren -homeassistant/components/circuit/* @braam -homeassistant/components/cisco_ios/* @fbradyirl -homeassistant/components/cisco_mobility_express/* @fbradyirl -homeassistant/components/cisco_webex_teams/* @fbradyirl -homeassistant/components/climacell/* @raman325 -tests/components/climacell/* @raman325 -homeassistant/components/cloud/* @home-assistant/cloud -tests/components/cloud/* @home-assistant/cloud -homeassistant/components/cloudflare/* @ludeeus @ctalkington -tests/components/cloudflare/* @ludeeus @ctalkington -homeassistant/components/coinbase/* @tombrien -tests/components/coinbase/* @tombrien -homeassistant/components/color_extractor/* @GenericStudent -tests/components/color_extractor/* @GenericStudent -homeassistant/components/comfoconnect/* @michaelarnauts -tests/components/comfoconnect/* @michaelarnauts -homeassistant/components/compensation/* @Petro31 -tests/components/compensation/* @Petro31 -homeassistant/components/config/* @home-assistant/core -tests/components/config/* @home-assistant/core -homeassistant/components/configurator/* @home-assistant/core -tests/components/configurator/* @home-assistant/core -homeassistant/components/control4/* @lawtancool -tests/components/control4/* @lawtancool -homeassistant/components/conversation/* @home-assistant/core -tests/components/conversation/* @home-assistant/core -homeassistant/components/coolmaster/* @OnFreund -tests/components/coolmaster/* @OnFreund -homeassistant/components/coronavirus/* @home-assistant/core -tests/components/coronavirus/* @home-assistant/core -homeassistant/components/counter/* @fabaff -tests/components/counter/* @fabaff -homeassistant/components/cover/* @home-assistant/core -tests/components/cover/* @home-assistant/core -homeassistant/components/cpuspeed/* @fabaff @frenck -tests/components/cpuspeed/* @fabaff @frenck -homeassistant/components/crownstone/* @Crownstone @RicArch97 -tests/components/crownstone/* @Crownstone @RicArch97 -homeassistant/components/cups/* @fabaff -homeassistant/components/daikin/* @fredrike -tests/components/daikin/* @fredrike -homeassistant/components/darksky/* @fabaff -tests/components/darksky/* @fabaff -homeassistant/components/debugpy/* @frenck -tests/components/debugpy/* @frenck -homeassistant/components/deconz/* @Kane610 -tests/components/deconz/* @Kane610 -homeassistant/components/delijn/* @bollewolle @Emilv2 -homeassistant/components/demo/* @home-assistant/core -tests/components/demo/* @home-assistant/core -homeassistant/components/denonavr/* @ol-iver @starkillerOG -tests/components/denonavr/* @ol-iver @starkillerOG -homeassistant/components/derivative/* @afaucogney -tests/components/derivative/* @afaucogney -homeassistant/components/device_automation/* @home-assistant/core -tests/components/device_automation/* @home-assistant/core -homeassistant/components/devolo_home_control/* @2Fake @Shutgun -tests/components/devolo_home_control/* @2Fake @Shutgun -homeassistant/components/devolo_home_network/* @2Fake @Shutgun -tests/components/devolo_home_network/* @2Fake @Shutgun -homeassistant/components/dexcom/* @gagebenne -tests/components/dexcom/* @gagebenne -homeassistant/components/dhcp/* @bdraco -tests/components/dhcp/* @bdraco -homeassistant/components/dht/* @thegardenmonkey -homeassistant/components/diagnostics/* @home-assistant/core -tests/components/diagnostics/* @home-assistant/core -homeassistant/components/digital_ocean/* @fabaff -homeassistant/components/discogs/* @thibmaek -homeassistant/components/dlna_dmr/* @StevenLooman @chishm -tests/components/dlna_dmr/* @StevenLooman @chishm -homeassistant/components/dlna_dms/* @chishm -tests/components/dlna_dms/* @chishm -homeassistant/components/dnsip/* @gjohansson-ST -tests/components/dnsip/* @gjohansson-ST -homeassistant/components/doorbird/* @oblogic7 @bdraco @flacjacket -tests/components/doorbird/* @oblogic7 @bdraco @flacjacket -homeassistant/components/dsmr/* @Robbie1221 @frenck -tests/components/dsmr/* @Robbie1221 @frenck -homeassistant/components/dsmr_reader/* @depl0y -homeassistant/components/dunehd/* @bieniu -tests/components/dunehd/* @bieniu -homeassistant/components/dwd_weather_warnings/* @runningman84 @stephan192 @Hummel95 -homeassistant/components/dweet/* @fabaff -homeassistant/components/dynalite/* @ziv1234 -tests/components/dynalite/* @ziv1234 -homeassistant/components/eafm/* @Jc2k -tests/components/eafm/* @Jc2k -homeassistant/components/ecobee/* @marthoc -tests/components/ecobee/* @marthoc -homeassistant/components/econet/* @vangorra @w1ll1am23 -tests/components/econet/* @vangorra @w1ll1am23 -homeassistant/components/ecovacs/* @OverloadUT -homeassistant/components/edl21/* @mtdcr -homeassistant/components/efergy/* @tkdrob -tests/components/efergy/* @tkdrob -homeassistant/components/egardia/* @jeroenterheerdt -homeassistant/components/eight_sleep/* @mezz64 @raman325 -homeassistant/components/elgato/* @frenck -tests/components/elgato/* @frenck -homeassistant/components/elkm1/* @gwww @bdraco -tests/components/elkm1/* @gwww @bdraco -homeassistant/components/elmax/* @albertogeniola -tests/components/elmax/* @albertogeniola -homeassistant/components/elv/* @majuss -homeassistant/components/emby/* @mezz64 -homeassistant/components/emoncms/* @borpin -homeassistant/components/emonitor/* @bdraco -tests/components/emonitor/* @bdraco -homeassistant/components/emulated_kasa/* @kbickar -tests/components/emulated_kasa/* @kbickar -homeassistant/components/energy/* @home-assistant/core -tests/components/energy/* @home-assistant/core -homeassistant/components/enigma2/* @fbradyirl -homeassistant/components/enocean/* @bdurrer -tests/components/enocean/* @bdurrer -homeassistant/components/enphase_envoy/* @gtdiehl -tests/components/enphase_envoy/* @gtdiehl -homeassistant/components/entur_public_transport/* @hfurubotten -homeassistant/components/environment_canada/* @gwww @michaeldavie -tests/components/environment_canada/* @gwww @michaeldavie -homeassistant/components/envisalink/* @ufodone -homeassistant/components/ephember/* @ttroy50 -homeassistant/components/epson/* @pszafer -tests/components/epson/* @pszafer -homeassistant/components/epsonworkforce/* @ThaStealth -homeassistant/components/eq3btsmart/* @rytilahti -homeassistant/components/esphome/* @OttoWinter @jesserockz -tests/components/esphome/* @OttoWinter @jesserockz -homeassistant/components/evil_genius_labs/* @balloob -tests/components/evil_genius_labs/* @balloob -homeassistant/components/evohome/* @zxdavb -homeassistant/components/ezviz/* @RenierM26 @baqs -tests/components/ezviz/* @RenierM26 @baqs -homeassistant/components/faa_delays/* @ntilley905 -tests/components/faa_delays/* @ntilley905 -homeassistant/components/fastdotcom/* @rohankapoorcom -homeassistant/components/file/* @fabaff -tests/components/file/* @fabaff -homeassistant/components/filter/* @dgomes -tests/components/filter/* @dgomes -homeassistant/components/fireservicerota/* @cyberjunky -tests/components/fireservicerota/* @cyberjunky -homeassistant/components/firmata/* @DaAwesomeP -tests/components/firmata/* @DaAwesomeP -homeassistant/components/fivem/* @Sander0542 -tests/components/fivem/* @Sander0542 -homeassistant/components/fixer/* @fabaff -homeassistant/components/fjaraskupan/* @elupus -tests/components/fjaraskupan/* @elupus -homeassistant/components/flick_electric/* @ZephireNZ -tests/components/flick_electric/* @ZephireNZ -homeassistant/components/flipr/* @cnico -tests/components/flipr/* @cnico -homeassistant/components/flo/* @dmulcahey -tests/components/flo/* @dmulcahey -homeassistant/components/flock/* @fabaff -homeassistant/components/flume/* @ChrisMandich @bdraco -tests/components/flume/* @ChrisMandich @bdraco -homeassistant/components/flunearyou/* @bachya -tests/components/flunearyou/* @bachya -homeassistant/components/flux_led/* @icemanch @bdraco -tests/components/flux_led/* @icemanch @bdraco -homeassistant/components/forecast_solar/* @klaasnicolaas @frenck -tests/components/forecast_solar/* @klaasnicolaas @frenck -homeassistant/components/forked_daapd/* @uvjustin -tests/components/forked_daapd/* @uvjustin -homeassistant/components/fortios/* @kimfrellsen -homeassistant/components/foscam/* @skgsergio -tests/components/foscam/* @skgsergio -homeassistant/components/freebox/* @hacf-fr @Quentame -tests/components/freebox/* @hacf-fr @Quentame -homeassistant/components/freedompro/* @stefano055415 -tests/components/freedompro/* @stefano055415 -homeassistant/components/fritz/* @mammuth @AaronDavidSchneider @chemelli74 @mib1185 -tests/components/fritz/* @mammuth @AaronDavidSchneider @chemelli74 @mib1185 -homeassistant/components/fritzbox/* @mib1185 @flabbamann -tests/components/fritzbox/* @mib1185 @flabbamann -homeassistant/components/fronius/* @nielstron @farmio -tests/components/fronius/* @nielstron @farmio -homeassistant/components/frontend/* @home-assistant/frontend -tests/components/frontend/* @home-assistant/frontend -homeassistant/components/garages_amsterdam/* @klaasnicolaas -tests/components/garages_amsterdam/* @klaasnicolaas -homeassistant/components/gdacs/* @exxamalte -tests/components/gdacs/* @exxamalte -homeassistant/components/generic_hygrostat/* @Shulyaka -tests/components/generic_hygrostat/* @Shulyaka -homeassistant/components/geniushub/* @zxdavb -homeassistant/components/geo_json_events/* @exxamalte -tests/components/geo_json_events/* @exxamalte -homeassistant/components/geo_rss_events/* @exxamalte -tests/components/geo_rss_events/* @exxamalte -homeassistant/components/geonetnz_quakes/* @exxamalte -tests/components/geonetnz_quakes/* @exxamalte -homeassistant/components/geonetnz_volcano/* @exxamalte -tests/components/geonetnz_volcano/* @exxamalte -homeassistant/components/gios/* @bieniu -tests/components/gios/* @bieniu -homeassistant/components/github/* @timmo001 @ludeeus -tests/components/github/* @timmo001 @ludeeus -homeassistant/components/gitter/* @fabaff -homeassistant/components/glances/* @fabaff @engrbm87 -tests/components/glances/* @fabaff @engrbm87 -homeassistant/components/goalzero/* @tkdrob -tests/components/goalzero/* @tkdrob -homeassistant/components/gogogate2/* @vangorra @bdraco -tests/components/gogogate2/* @vangorra @bdraco -homeassistant/components/goodwe/* @mletenay @starkillerOG -tests/components/goodwe/* @mletenay @starkillerOG -homeassistant/components/google_assistant/* @home-assistant/cloud -tests/components/google_assistant/* @home-assistant/cloud -homeassistant/components/google_cloud/* @lufton -homeassistant/components/google_travel_time/* @eifinger -tests/components/google_travel_time/* @eifinger -homeassistant/components/gpsd/* @fabaff -homeassistant/components/gree/* @cmroche -tests/components/gree/* @cmroche -homeassistant/components/greeneye_monitor/* @jkeljo -tests/components/greeneye_monitor/* @jkeljo -homeassistant/components/group/* @home-assistant/core -tests/components/group/* @home-assistant/core -homeassistant/components/growatt_server/* @indykoning @muppet3000 @JasperPlant -tests/components/growatt_server/* @indykoning @muppet3000 @JasperPlant -homeassistant/components/guardian/* @bachya -tests/components/guardian/* @bachya -homeassistant/components/habitica/* @ASMfreaK @leikoilja -tests/components/habitica/* @ASMfreaK @leikoilja -homeassistant/components/harmony/* @ehendrix23 @bramkragten @bdraco @mkeesey @Aohzan -tests/components/harmony/* @ehendrix23 @bramkragten @bdraco @mkeesey @Aohzan -homeassistant/components/hassio/* @home-assistant/supervisor -tests/components/hassio/* @home-assistant/supervisor -homeassistant/components/heatmiser/* @andylockran -homeassistant/components/heos/* @andrewsayre -tests/components/heos/* @andrewsayre -homeassistant/components/here_travel_time/* @eifinger -tests/components/here_travel_time/* @eifinger -homeassistant/components/hikvision/* @mezz64 -homeassistant/components/hikvisioncam/* @fbradyirl -homeassistant/components/hisense_aehw4a1/* @bannhead -tests/components/hisense_aehw4a1/* @bannhead -homeassistant/components/history/* @home-assistant/core -tests/components/history/* @home-assistant/core -homeassistant/components/hive/* @Rendili @KJonline -tests/components/hive/* @Rendili @KJonline -homeassistant/components/hlk_sw16/* @jameshilliard -tests/components/hlk_sw16/* @jameshilliard -homeassistant/components/home_connect/* @DavidMStraub -tests/components/home_connect/* @DavidMStraub -homeassistant/components/home_plus_control/* @chemaaa -tests/components/home_plus_control/* @chemaaa -homeassistant/components/homeassistant/* @home-assistant/core -tests/components/homeassistant/* @home-assistant/core -homeassistant/components/homekit/* @bdraco -tests/components/homekit/* @bdraco -homeassistant/components/homekit_controller/* @Jc2k @bdraco -tests/components/homekit_controller/* @Jc2k @bdraco -homeassistant/components/homematic/* @pvizeli @danielperna84 -tests/components/homematic/* @pvizeli @danielperna84 -homeassistant/components/homewizard/* @DCSBL -tests/components/homewizard/* @DCSBL -homeassistant/components/honeywell/* @rdfurman -tests/components/honeywell/* @rdfurman -homeassistant/components/http/* @home-assistant/core -tests/components/http/* @home-assistant/core -homeassistant/components/huawei_lte/* @scop @fphammerle -tests/components/huawei_lte/* @scop @fphammerle -homeassistant/components/hue/* @balloob @marcelveldt -tests/components/hue/* @balloob @marcelveldt -homeassistant/components/huisbaasje/* @dennisschroer -tests/components/huisbaasje/* @dennisschroer -homeassistant/components/humidifier/* @home-assistant/core @Shulyaka -tests/components/humidifier/* @home-assistant/core @Shulyaka -homeassistant/components/hunterdouglas_powerview/* @bdraco -tests/components/hunterdouglas_powerview/* @bdraco -homeassistant/components/hvv_departures/* @vigonotion -tests/components/hvv_departures/* @vigonotion -homeassistant/components/hydrawise/* @ptcryan -homeassistant/components/hyperion/* @dermotduffy -tests/components/hyperion/* @dermotduffy -homeassistant/components/ialarm/* @RyuzakiKK -tests/components/ialarm/* @RyuzakiKK -homeassistant/components/iammeter/* @lewei50 -homeassistant/components/iaqualink/* @flz -tests/components/iaqualink/* @flz -homeassistant/components/icloud/* @Quentame @nzapponi -tests/components/icloud/* @Quentame @nzapponi -homeassistant/components/ign_sismologia/* @exxamalte -tests/components/ign_sismologia/* @exxamalte -homeassistant/components/image/* @home-assistant/core -tests/components/image/* @home-assistant/core -homeassistant/components/incomfort/* @zxdavb -homeassistant/components/influxdb/* @fabaff @mdegat01 -tests/components/influxdb/* @fabaff @mdegat01 -homeassistant/components/input_boolean/* @home-assistant/core -tests/components/input_boolean/* @home-assistant/core -homeassistant/components/input_button/* @home-assistant/core -tests/components/input_button/* @home-assistant/core -homeassistant/components/input_datetime/* @home-assistant/core -tests/components/input_datetime/* @home-assistant/core -homeassistant/components/input_number/* @home-assistant/core -tests/components/input_number/* @home-assistant/core -homeassistant/components/input_select/* @home-assistant/core -tests/components/input_select/* @home-assistant/core -homeassistant/components/input_text/* @home-assistant/core -tests/components/input_text/* @home-assistant/core -homeassistant/components/insteon/* @teharris1 -tests/components/insteon/* @teharris1 -homeassistant/components/integration/* @dgomes -tests/components/integration/* @dgomes -homeassistant/components/intellifire/* @jeeftor -tests/components/intellifire/* @jeeftor -homeassistant/components/intent/* @home-assistant/core -tests/components/intent/* @home-assistant/core -homeassistant/components/intesishome/* @jnimmo -homeassistant/components/ios/* @robbiet480 -tests/components/ios/* @robbiet480 -homeassistant/components/iotawatt/* @gtdiehl @jyavenard -tests/components/iotawatt/* @gtdiehl @jyavenard -homeassistant/components/iperf3/* @rohankapoorcom -homeassistant/components/ipma/* @dgomes @abmantis -tests/components/ipma/* @dgomes @abmantis -homeassistant/components/ipp/* @ctalkington -tests/components/ipp/* @ctalkington -homeassistant/components/iqvia/* @bachya -tests/components/iqvia/* @bachya -homeassistant/components/irish_rail_transport/* @ttroy50 -homeassistant/components/islamic_prayer_times/* @engrbm87 -tests/components/islamic_prayer_times/* @engrbm87 -homeassistant/components/iss/* @DurgNomis-drol -tests/components/iss/* @DurgNomis-drol -homeassistant/components/isy994/* @bdraco @shbatm -tests/components/isy994/* @bdraco @shbatm -homeassistant/components/izone/* @Swamp-Ig -tests/components/izone/* @Swamp-Ig -homeassistant/components/jellyfin/* @j-stienstra -tests/components/jellyfin/* @j-stienstra -homeassistant/components/jewish_calendar/* @tsvi -tests/components/jewish_calendar/* @tsvi -homeassistant/components/juicenet/* @jesserockz -tests/components/juicenet/* @jesserockz -homeassistant/components/kaiterra/* @Michsior14 -homeassistant/components/keba/* @dannerph -homeassistant/components/keenetic_ndms2/* @foxel -tests/components/keenetic_ndms2/* @foxel -homeassistant/components/kef/* @basnijholt -homeassistant/components/keyboard_remote/* @bendavid @lanrat -homeassistant/components/kmtronic/* @dgomes -tests/components/kmtronic/* @dgomes -homeassistant/components/knx/* @Julius2342 @farmio @marvin-w -tests/components/knx/* @Julius2342 @farmio @marvin-w -homeassistant/components/kodi/* @OnFreund @cgtobi -tests/components/kodi/* @OnFreund @cgtobi -homeassistant/components/konnected/* @heythisisnate -tests/components/konnected/* @heythisisnate -homeassistant/components/kostal_plenticore/* @stegm -tests/components/kostal_plenticore/* @stegm -homeassistant/components/kraken/* @eifinger -tests/components/kraken/* @eifinger -homeassistant/components/kulersky/* @emlove -tests/components/kulersky/* @emlove -homeassistant/components/lametric/* @robbiet480 @frenck -homeassistant/components/launch_library/* @ludeeus @DurgNomis-drol -tests/components/launch_library/* @ludeeus @DurgNomis-drol -homeassistant/components/lcn/* @alengwenus -tests/components/lcn/* @alengwenus -homeassistant/components/lg_netcast/* @Drafteed -homeassistant/components/life360/* @pnbruckner -homeassistant/components/linux_battery/* @fabaff -homeassistant/components/litejet/* @joncar -tests/components/litejet/* @joncar -homeassistant/components/litterrobot/* @natekspencer -tests/components/litterrobot/* @natekspencer -homeassistant/components/local_ip/* @issacg -tests/components/local_ip/* @issacg -homeassistant/components/logger/* @home-assistant/core -tests/components/logger/* @home-assistant/core -homeassistant/components/logi_circle/* @evanjd -tests/components/logi_circle/* @evanjd -homeassistant/components/lookin/* @ANMalko @bdraco -tests/components/lookin/* @ANMalko @bdraco -homeassistant/components/lovelace/* @home-assistant/frontend -tests/components/lovelace/* @home-assistant/frontend -homeassistant/components/luci/* @mzdrale -homeassistant/components/luftdaten/* @fabaff @frenck -tests/components/luftdaten/* @fabaff @frenck -homeassistant/components/lupusec/* @majuss -homeassistant/components/lutron/* @JonGilmore -homeassistant/components/lutron_caseta/* @swails @bdraco -tests/components/lutron_caseta/* @swails @bdraco -homeassistant/components/lyric/* @timmo001 -tests/components/lyric/* @timmo001 -homeassistant/components/mastodon/* @fabaff -homeassistant/components/matrix/* @tinloaf -homeassistant/components/mazda/* @bdr99 -tests/components/mazda/* @bdr99 -homeassistant/components/mcp23017/* @jardiamj -homeassistant/components/media_source/* @hunterjm -tests/components/media_source/* @hunterjm -homeassistant/components/mediaroom/* @dgomes -homeassistant/components/melcloud/* @vilppuvuorinen -tests/components/melcloud/* @vilppuvuorinen -homeassistant/components/melissa/* @kennedyshead -tests/components/melissa/* @kennedyshead -homeassistant/components/met/* @danielhiversen @thimic -tests/components/met/* @danielhiversen @thimic -homeassistant/components/met_eireann/* @DylanGore -tests/components/met_eireann/* @DylanGore -homeassistant/components/meteo_france/* @hacf-fr @oncleben31 @Quentame -tests/components/meteo_france/* @hacf-fr @oncleben31 @Quentame -homeassistant/components/meteoalarm/* @rolfberkenbosch -homeassistant/components/meteoclimatic/* @adrianmo -tests/components/meteoclimatic/* @adrianmo -homeassistant/components/metoffice/* @MrHarcombe -tests/components/metoffice/* @MrHarcombe -homeassistant/components/miflora/* @danielhiversen @basnijholt -homeassistant/components/mikrotik/* @engrbm87 -tests/components/mikrotik/* @engrbm87 -homeassistant/components/mill/* @danielhiversen -tests/components/mill/* @danielhiversen -homeassistant/components/min_max/* @fabaff -tests/components/min_max/* @fabaff -homeassistant/components/minecraft_server/* @elmurato -tests/components/minecraft_server/* @elmurato -homeassistant/components/minio/* @tkislan -tests/components/minio/* @tkislan -homeassistant/components/mobile_app/* @home-assistant/core -tests/components/mobile_app/* @home-assistant/core -homeassistant/components/modbus/* @adamchengtkc @janiversen @vzahradnik -tests/components/modbus/* @adamchengtkc @janiversen @vzahradnik -homeassistant/components/modem_callerid/* @tkdrob -tests/components/modem_callerid/* @tkdrob -homeassistant/components/modern_forms/* @wonderslug -tests/components/modern_forms/* @wonderslug -homeassistant/components/moehlenhoff_alpha2/* @j-a-n -tests/components/moehlenhoff_alpha2/* @j-a-n -homeassistant/components/monoprice/* @etsinko @OnFreund -tests/components/monoprice/* @etsinko @OnFreund -homeassistant/components/moon/* @fabaff -tests/components/moon/* @fabaff -homeassistant/components/motion_blinds/* @starkillerOG -tests/components/motion_blinds/* @starkillerOG -homeassistant/components/motioneye/* @dermotduffy -tests/components/motioneye/* @dermotduffy -homeassistant/components/mpd/* @fabaff -homeassistant/components/mqtt/* @emontnemery -tests/components/mqtt/* @emontnemery -homeassistant/components/msteams/* @peroyvind -homeassistant/components/mullvad/* @meichthys -tests/components/mullvad/* @meichthys -homeassistant/components/mutesync/* @currentoor -tests/components/mutesync/* @currentoor -homeassistant/components/my/* @home-assistant/core -tests/components/my/* @home-assistant/core -homeassistant/components/myq/* @bdraco @ehendrix23 -tests/components/myq/* @bdraco @ehendrix23 -homeassistant/components/mysensors/* @MartinHjelmare @functionpointer -tests/components/mysensors/* @MartinHjelmare @functionpointer -homeassistant/components/mystrom/* @fabaff -homeassistant/components/nam/* @bieniu -tests/components/nam/* @bieniu -homeassistant/components/nanoleaf/* @milanmeu -tests/components/nanoleaf/* @milanmeu -homeassistant/components/neato/* @dshokouhi @Santobert -tests/components/neato/* @dshokouhi @Santobert -homeassistant/components/nederlandse_spoorwegen/* @YarmoM -homeassistant/components/ness_alarm/* @nickw444 -tests/components/ness_alarm/* @nickw444 -homeassistant/components/nest/* @allenporter -tests/components/nest/* @allenporter -homeassistant/components/netatmo/* @cgtobi -tests/components/netatmo/* @cgtobi -homeassistant/components/netdata/* @fabaff -homeassistant/components/netgear/* @hacf-fr @Quentame @starkillerOG -tests/components/netgear/* @hacf-fr @Quentame @starkillerOG -homeassistant/components/nexia/* @bdraco -tests/components/nexia/* @bdraco -homeassistant/components/nextbus/* @vividboarder -tests/components/nextbus/* @vividboarder -homeassistant/components/nextcloud/* @meichthys -homeassistant/components/nfandroidtv/* @tkdrob -tests/components/nfandroidtv/* @tkdrob -homeassistant/components/nightscout/* @marciogranzotto -tests/components/nightscout/* @marciogranzotto -homeassistant/components/nilu/* @hfurubotten -homeassistant/components/nina/* @DeerMaximum -tests/components/nina/* @DeerMaximum -homeassistant/components/nissan_leaf/* @filcole -homeassistant/components/nmbs/* @thibmaek -homeassistant/components/no_ip/* @fabaff -tests/components/no_ip/* @fabaff -homeassistant/components/noaa_tides/* @jdelaney72 -homeassistant/components/notify/* @home-assistant/core -tests/components/notify/* @home-assistant/core -homeassistant/components/notify_events/* @matrozov @papajojo -tests/components/notify_events/* @matrozov @papajojo -homeassistant/components/notion/* @bachya -tests/components/notion/* @bachya -homeassistant/components/nsw_fuel_station/* @nickw444 -tests/components/nsw_fuel_station/* @nickw444 -homeassistant/components/nsw_rural_fire_service_feed/* @exxamalte -tests/components/nsw_rural_fire_service_feed/* @exxamalte -homeassistant/components/nuki/* @pschmitt @pvizeli @pree -tests/components/nuki/* @pschmitt @pvizeli @pree -homeassistant/components/numato/* @clssn -tests/components/numato/* @clssn -homeassistant/components/number/* @home-assistant/core @Shulyaka -tests/components/number/* @home-assistant/core @Shulyaka -homeassistant/components/nut/* @bdraco @ollo69 -tests/components/nut/* @bdraco @ollo69 -homeassistant/components/nws/* @MatthewFlamm -tests/components/nws/* @MatthewFlamm -homeassistant/components/nzbget/* @chriscla -tests/components/nzbget/* @chriscla -homeassistant/components/obihai/* @dshokouhi -homeassistant/components/octoprint/* @rfleming71 -tests/components/octoprint/* @rfleming71 -homeassistant/components/ohmconnect/* @robbiet480 -homeassistant/components/ombi/* @larssont -homeassistant/components/omnilogic/* @oliver84 @djtimca @gentoosu -tests/components/omnilogic/* @oliver84 @djtimca @gentoosu -homeassistant/components/onboarding/* @home-assistant/core -tests/components/onboarding/* @home-assistant/core -homeassistant/components/oncue/* @bdraco -tests/components/oncue/* @bdraco -homeassistant/components/ondilo_ico/* @JeromeHXP -tests/components/ondilo_ico/* @JeromeHXP -homeassistant/components/onewire/* @garbled1 @epenet -tests/components/onewire/* @garbled1 @epenet -homeassistant/components/onvif/* @hunterjm -tests/components/onvif/* @hunterjm -homeassistant/components/open_meteo/* @frenck -tests/components/open_meteo/* @frenck -homeassistant/components/openerz/* @misialq -tests/components/openerz/* @misialq -homeassistant/components/opengarage/* @danielhiversen -tests/components/opengarage/* @danielhiversen -homeassistant/components/openhome/* @bazwilliams -homeassistant/components/opentherm_gw/* @mvn23 -tests/components/opentherm_gw/* @mvn23 -homeassistant/components/openuv/* @bachya -tests/components/openuv/* @bachya -homeassistant/components/openweathermap/* @fabaff @freekode @nzapponi -tests/components/openweathermap/* @fabaff @freekode @nzapponi -homeassistant/components/opnsense/* @mtreinish -tests/components/opnsense/* @mtreinish -homeassistant/components/orangepi_gpio/* @pascallj -homeassistant/components/oru/* @bvlaicu -homeassistant/components/overkiz/* @imicknl @vlebourl @tetienne -tests/components/overkiz/* @imicknl @vlebourl @tetienne -homeassistant/components/ovo_energy/* @timmo001 -tests/components/ovo_energy/* @timmo001 -homeassistant/components/ozw/* @cgarwood @marcelveldt @MartinHjelmare -tests/components/ozw/* @cgarwood @marcelveldt @MartinHjelmare -homeassistant/components/p1_monitor/* @klaasnicolaas -tests/components/p1_monitor/* @klaasnicolaas -homeassistant/components/panel_custom/* @home-assistant/frontend -tests/components/panel_custom/* @home-assistant/frontend -homeassistant/components/panel_iframe/* @home-assistant/frontend -tests/components/panel_iframe/* @home-assistant/frontend -homeassistant/components/pcal9535a/* @Shulyaka -homeassistant/components/persistent_notification/* @home-assistant/core -tests/components/persistent_notification/* @home-assistant/core -homeassistant/components/philips_js/* @elupus -tests/components/philips_js/* @elupus -homeassistant/components/pi4ioe5v9xxxx/* @antonverburg -homeassistant/components/pi_hole/* @fabaff @johnluetke @shenxn -tests/components/pi_hole/* @fabaff @johnluetke @shenxn -homeassistant/components/picnic/* @corneyl -tests/components/picnic/* @corneyl -homeassistant/components/pilight/* @trekky12 -tests/components/pilight/* @trekky12 -homeassistant/components/plaato/* @JohNan -tests/components/plaato/* @JohNan -homeassistant/components/plex/* @jjlawren -tests/components/plex/* @jjlawren -homeassistant/components/plugwise/* @CoMPaTech @bouwew @brefra @frenck -tests/components/plugwise/* @CoMPaTech @bouwew @brefra @frenck -homeassistant/components/plum_lightpad/* @ColinHarrington @prystupa -tests/components/plum_lightpad/* @ColinHarrington @prystupa -homeassistant/components/point/* @fredrike -tests/components/point/* @fredrike -homeassistant/components/poolsense/* @haemishkyd -tests/components/poolsense/* @haemishkyd -homeassistant/components/powerwall/* @bdraco @jrester -tests/components/powerwall/* @bdraco @jrester -homeassistant/components/profiler/* @bdraco -tests/components/profiler/* @bdraco -homeassistant/components/progettihwsw/* @ardaseremet -tests/components/progettihwsw/* @ardaseremet -homeassistant/components/prometheus/* @knyar -tests/components/prometheus/* @knyar -homeassistant/components/prosegur/* @dgomes -tests/components/prosegur/* @dgomes -homeassistant/components/proxmoxve/* @jhollowe @Corbeno -homeassistant/components/ps4/* @ktnrg45 -tests/components/ps4/* @ktnrg45 -homeassistant/components/pure_energie/* @klaasnicolaas -tests/components/pure_energie/* @klaasnicolaas -homeassistant/components/push/* @dgomes -tests/components/push/* @dgomes -homeassistant/components/pvoutput/* @fabaff @frenck -tests/components/pvoutput/* @fabaff @frenck -homeassistant/components/pvpc_hourly_pricing/* @azogue -tests/components/pvpc_hourly_pricing/* @azogue -homeassistant/components/qbittorrent/* @geoffreylagaisse -homeassistant/components/qld_bushfire/* @exxamalte -tests/components/qld_bushfire/* @exxamalte -homeassistant/components/quantum_gateway/* @cisasteelersfan -homeassistant/components/qvr_pro/* @oblogic7 -homeassistant/components/qwikswitch/* @kellerza -tests/components/qwikswitch/* @kellerza -homeassistant/components/rachio/* @bdraco -tests/components/rachio/* @bdraco -homeassistant/components/radio_browser/* @frenck -tests/components/radio_browser/* @frenck -homeassistant/components/radiotherm/* @vinnyfuria -homeassistant/components/rainbird/* @konikvranik -homeassistant/components/raincloud/* @vanstinator -homeassistant/components/rainforest_eagle/* @gtdiehl @jcalbert -tests/components/rainforest_eagle/* @gtdiehl @jcalbert -homeassistant/components/rainmachine/* @bachya -tests/components/rainmachine/* @bachya -homeassistant/components/random/* @fabaff -tests/components/random/* @fabaff -homeassistant/components/rdw/* @frenck -tests/components/rdw/* @frenck -homeassistant/components/recollect_waste/* @bachya -tests/components/recollect_waste/* @bachya -homeassistant/components/recorder/* @home-assistant/core -tests/components/recorder/* @home-assistant/core -homeassistant/components/rejseplanen/* @DarkFox -homeassistant/components/renault/* @epenet -tests/components/renault/* @epenet -homeassistant/components/repetier/* @MTrab @ShadowBr0ther -homeassistant/components/rflink/* @javicalle -tests/components/rflink/* @javicalle -homeassistant/components/rfxtrx/* @danielhiversen @elupus @RobBie1221 -tests/components/rfxtrx/* @danielhiversen @elupus @RobBie1221 -homeassistant/components/ridwell/* @bachya -tests/components/ridwell/* @bachya -homeassistant/components/ring/* @balloob -tests/components/ring/* @balloob -homeassistant/components/risco/* @OnFreund -tests/components/risco/* @OnFreund -homeassistant/components/rituals_perfume_genie/* @milanmeu -tests/components/rituals_perfume_genie/* @milanmeu -homeassistant/components/rmvtransport/* @cgtobi -tests/components/rmvtransport/* @cgtobi -homeassistant/components/roku/* @ctalkington -tests/components/roku/* @ctalkington -homeassistant/components/roomba/* @pschmitt @cyr-ius @shenxn -tests/components/roomba/* @pschmitt @cyr-ius @shenxn -homeassistant/components/roon/* @pavoni -tests/components/roon/* @pavoni -homeassistant/components/rpi_gpio_pwm/* @soldag -homeassistant/components/rpi_power/* @shenxn @swetoast -tests/components/rpi_power/* @shenxn @swetoast -homeassistant/components/rtsp_to_webrtc/* @allenporter -tests/components/rtsp_to_webrtc/* @allenporter -homeassistant/components/ruckus_unleashed/* @gabe565 -tests/components/ruckus_unleashed/* @gabe565 -homeassistant/components/safe_mode/* @home-assistant/core -tests/components/safe_mode/* @home-assistant/core -homeassistant/components/saj/* @fredericvl -homeassistant/components/samsungtv/* @escoand @chemelli74 @epenet -tests/components/samsungtv/* @escoand @chemelli74 @epenet -homeassistant/components/scene/* @home-assistant/core -tests/components/scene/* @home-assistant/core -homeassistant/components/schluter/* @prairieapps -homeassistant/components/scrape/* @fabaff -tests/components/scrape/* @fabaff -homeassistant/components/screenlogic/* @dieselrabbit @bdraco -tests/components/screenlogic/* @dieselrabbit @bdraco -homeassistant/components/script/* @home-assistant/core -tests/components/script/* @home-assistant/core -homeassistant/components/search/* @home-assistant/core -tests/components/search/* @home-assistant/core -homeassistant/components/select/* @home-assistant/core -tests/components/select/* @home-assistant/core -homeassistant/components/sense/* @kbickar -tests/components/sense/* @kbickar -homeassistant/components/senseme/* @mikelawrence @bdraco -tests/components/senseme/* @mikelawrence @bdraco -homeassistant/components/sensibo/* @andrey-git @gjohansson-ST -tests/components/sensibo/* @andrey-git @gjohansson-ST -homeassistant/components/sentry/* @dcramer @frenck -tests/components/sentry/* @dcramer @frenck -homeassistant/components/serial/* @fabaff -homeassistant/components/seven_segments/* @fabaff -homeassistant/components/sharkiq/* @ajmarks -tests/components/sharkiq/* @ajmarks -homeassistant/components/shell_command/* @home-assistant/core -tests/components/shell_command/* @home-assistant/core -homeassistant/components/shelly/* @balloob @bieniu @thecode @chemelli74 -tests/components/shelly/* @balloob @bieniu @thecode @chemelli74 -homeassistant/components/shiftr/* @fabaff -homeassistant/components/shodan/* @fabaff -homeassistant/components/sia/* @eavanvalkenburg -tests/components/sia/* @eavanvalkenburg -homeassistant/components/sighthound/* @robmarkcole -tests/components/sighthound/* @robmarkcole -homeassistant/components/signal_messenger/* @bbernhard -tests/components/signal_messenger/* @bbernhard -homeassistant/components/simplisafe/* @bachya -tests/components/simplisafe/* @bachya -homeassistant/components/sinch/* @bendikrb -homeassistant/components/siren/* @home-assistant/core @raman325 -tests/components/siren/* @home-assistant/core @raman325 -homeassistant/components/sisyphus/* @jkeljo -homeassistant/components/sky_hub/* @rogerselwyn -homeassistant/components/slack/* @bachya -tests/components/slack/* @bachya -homeassistant/components/sleepiq/* @mfugate1 @kbickar -tests/components/sleepiq/* @mfugate1 @kbickar -homeassistant/components/slide/* @ualex73 -homeassistant/components/sma/* @kellerza @rklomp -tests/components/sma/* @kellerza @rklomp -homeassistant/components/smappee/* @bsmappee -tests/components/smappee/* @bsmappee -homeassistant/components/smart_meter_texas/* @grahamwetzler -tests/components/smart_meter_texas/* @grahamwetzler -homeassistant/components/smarthab/* @outadoc -tests/components/smarthab/* @outadoc -homeassistant/components/smartthings/* @andrewsayre -tests/components/smartthings/* @andrewsayre -homeassistant/components/smarttub/* @mdz -tests/components/smarttub/* @mdz -homeassistant/components/smarty/* @z0mbieprocess -homeassistant/components/smhi/* @gjohansson-ST -tests/components/smhi/* @gjohansson-ST -homeassistant/components/sms/* @ocalvo -homeassistant/components/smtp/* @fabaff -tests/components/smtp/* @fabaff -homeassistant/components/solaredge/* @frenck -tests/components/solaredge/* @frenck -homeassistant/components/solaredge_local/* @drobtravels @scheric -homeassistant/components/solarlog/* @Ernst79 -tests/components/solarlog/* @Ernst79 -homeassistant/components/solax/* @squishykid -tests/components/solax/* @squishykid -homeassistant/components/soma/* @ratsept @sebfortier2288 -tests/components/soma/* @ratsept @sebfortier2288 -homeassistant/components/somfy/* @tetienne -tests/components/somfy/* @tetienne -homeassistant/components/sonarr/* @ctalkington -tests/components/sonarr/* @ctalkington -homeassistant/components/songpal/* @rytilahti @shenxn -tests/components/songpal/* @rytilahti @shenxn -homeassistant/components/sonos/* @cgtobi @jjlawren -tests/components/sonos/* @cgtobi @jjlawren -homeassistant/components/spaceapi/* @fabaff -tests/components/spaceapi/* @fabaff -homeassistant/components/speedtestdotnet/* @rohankapoorcom @engrbm87 -tests/components/speedtestdotnet/* @rohankapoorcom @engrbm87 -homeassistant/components/spider/* @peternijssen -tests/components/spider/* @peternijssen -homeassistant/components/splunk/* @Bre77 -homeassistant/components/spotify/* @frenck -tests/components/spotify/* @frenck -homeassistant/components/sql/* @dgomes -tests/components/sql/* @dgomes -homeassistant/components/squeezebox/* @rajlaud -tests/components/squeezebox/* @rajlaud -homeassistant/components/srp_energy/* @briglx -tests/components/srp_energy/* @briglx -homeassistant/components/starline/* @anonym-tsk -tests/components/starline/* @anonym-tsk -homeassistant/components/statistics/* @fabaff @ThomDietrich -tests/components/statistics/* @fabaff @ThomDietrich -homeassistant/components/steamist/* @bdraco -tests/components/steamist/* @bdraco -homeassistant/components/stiebel_eltron/* @fucm -homeassistant/components/stookalert/* @fwestenberg @frenck -tests/components/stookalert/* @fwestenberg @frenck -homeassistant/components/stream/* @hunterjm @uvjustin @allenporter -tests/components/stream/* @hunterjm @uvjustin @allenporter -homeassistant/components/stt/* @pvizeli -tests/components/stt/* @pvizeli -homeassistant/components/subaru/* @G-Two -tests/components/subaru/* @G-Two -homeassistant/components/suez_water/* @ooii -homeassistant/components/sun/* @Swamp-Ig -tests/components/sun/* @Swamp-Ig -homeassistant/components/supla/* @mwegrzynek -homeassistant/components/surepetcare/* @benleb @danielhiversen -tests/components/surepetcare/* @benleb @danielhiversen -homeassistant/components/swiss_hydrological_data/* @fabaff -homeassistant/components/swiss_public_transport/* @fabaff -homeassistant/components/switchbot/* @danielhiversen @RenierM26 -tests/components/switchbot/* @danielhiversen @RenierM26 -homeassistant/components/switcher_kis/* @tomerfi @thecode -tests/components/switcher_kis/* @tomerfi @thecode -homeassistant/components/switchmate/* @danielhiversen -homeassistant/components/syncthing/* @zhulik -tests/components/syncthing/* @zhulik -homeassistant/components/syncthru/* @nielstron -tests/components/syncthru/* @nielstron -homeassistant/components/synology_dsm/* @hacf-fr @Quentame @mib1185 -tests/components/synology_dsm/* @hacf-fr @Quentame @mib1185 -homeassistant/components/synology_srm/* @aerialls -homeassistant/components/syslog/* @fabaff -homeassistant/components/system_bridge/* @timmo001 -tests/components/system_bridge/* @timmo001 -homeassistant/components/tado/* @michaelarnauts -tests/components/tado/* @michaelarnauts -homeassistant/components/tag/* @balloob @dmulcahey -tests/components/tag/* @balloob @dmulcahey -homeassistant/components/tailscale/* @frenck -tests/components/tailscale/* @frenck -homeassistant/components/tankerkoenig/* @guillempages -homeassistant/components/tapsaff/* @bazwilliams -homeassistant/components/tasmota/* @emontnemery -tests/components/tasmota/* @emontnemery -homeassistant/components/tautulli/* @ludeeus -homeassistant/components/tellduslive/* @fredrike -tests/components/tellduslive/* @fredrike -homeassistant/components/template/* @PhracturedBlue @tetienne @home-assistant/core -tests/components/template/* @PhracturedBlue @tetienne @home-assistant/core -homeassistant/components/tesla_wall_connector/* @einarhauks -tests/components/tesla_wall_connector/* @einarhauks -homeassistant/components/tfiac/* @fredrike @mellado -homeassistant/components/thethingsnetwork/* @fabaff -homeassistant/components/threshold/* @fabaff -tests/components/threshold/* @fabaff -homeassistant/components/tibber/* @danielhiversen -tests/components/tibber/* @danielhiversen -homeassistant/components/tile/* @bachya -tests/components/tile/* @bachya -homeassistant/components/time_date/* @fabaff -tests/components/time_date/* @fabaff -homeassistant/components/tmb/* @alemuro -homeassistant/components/todoist/* @boralyl -homeassistant/components/tolo/* @MatthiasLohr -tests/components/tolo/* @MatthiasLohr -homeassistant/components/totalconnect/* @austinmroczek -tests/components/totalconnect/* @austinmroczek -homeassistant/components/tplink/* @rytilahti @thegardenmonkey -tests/components/tplink/* @rytilahti @thegardenmonkey -homeassistant/components/traccar/* @ludeeus -tests/components/traccar/* @ludeeus -homeassistant/components/trace/* @home-assistant/core -tests/components/trace/* @home-assistant/core -homeassistant/components/tractive/* @Danielhiversen @zhulik @bieniu -tests/components/tractive/* @Danielhiversen @zhulik @bieniu -homeassistant/components/trafikverket_train/* @endor-force @gjohansson-ST -homeassistant/components/trafikverket_weatherstation/* @endor-force @gjohansson-ST -tests/components/trafikverket_weatherstation/* @endor-force @gjohansson-ST -homeassistant/components/transmission/* @engrbm87 @JPHutchins -tests/components/transmission/* @engrbm87 @JPHutchins -homeassistant/components/tts/* @pvizeli -tests/components/tts/* @pvizeli -homeassistant/components/tuya/* @Tuya @zlinoliver @METISU @frenck -tests/components/tuya/* @Tuya @zlinoliver @METISU @frenck -homeassistant/components/twentemilieu/* @frenck -tests/components/twentemilieu/* @frenck -homeassistant/components/twinkly/* @dr1rrb @Robbie1221 -tests/components/twinkly/* @dr1rrb @Robbie1221 -homeassistant/components/unifi/* @Kane610 -tests/components/unifi/* @Kane610 -homeassistant/components/unifiled/* @florisvdk -homeassistant/components/unifiprotect/* @briis @AngellusMortis @bdraco -tests/components/unifiprotect/* @briis @AngellusMortis @bdraco -homeassistant/components/upb/* @gwww -tests/components/upb/* @gwww -homeassistant/components/upc_connect/* @pvizeli @fabaff -homeassistant/components/upcloud/* @scop -tests/components/upcloud/* @scop -homeassistant/components/updater/* @home-assistant/core -tests/components/updater/* @home-assistant/core -homeassistant/components/upnp/* @StevenLooman @ehendrix23 -tests/components/upnp/* @StevenLooman @ehendrix23 -homeassistant/components/uptimerobot/* @ludeeus @chemelli74 -tests/components/uptimerobot/* @ludeeus @chemelli74 -homeassistant/components/usb/* @bdraco -tests/components/usb/* @bdraco -homeassistant/components/usgs_earthquakes_feed/* @exxamalte -tests/components/usgs_earthquakes_feed/* @exxamalte -homeassistant/components/utility_meter/* @dgomes -tests/components/utility_meter/* @dgomes -homeassistant/components/vallox/* @andre-richter @slovdahl @viiru- -tests/components/vallox/* @andre-richter @slovdahl @viiru- -homeassistant/components/velbus/* @Cereal2nd @brefra -tests/components/velbus/* @Cereal2nd @brefra -homeassistant/components/velux/* @Julius2342 -homeassistant/components/venstar/* @garbled1 -tests/components/venstar/* @garbled1 -homeassistant/components/vera/* @pavoni -tests/components/vera/* @pavoni -homeassistant/components/verisure/* @frenck -tests/components/verisure/* @frenck -homeassistant/components/versasense/* @flamm3blemuff1n -homeassistant/components/version/* @fabaff @ludeeus -tests/components/version/* @fabaff @ludeeus -homeassistant/components/vesync/* @markperdue @webdjoe @thegardenmonkey -tests/components/vesync/* @markperdue @webdjoe @thegardenmonkey -homeassistant/components/vicare/* @oischinger -tests/components/vicare/* @oischinger -homeassistant/components/vilfo/* @ManneW -tests/components/vilfo/* @ManneW -homeassistant/components/vivotek/* @HarlemSquirrel -homeassistant/components/vizio/* @raman325 -tests/components/vizio/* @raman325 -homeassistant/components/vlc_telnet/* @rodripf @MartinHjelmare -tests/components/vlc_telnet/* @rodripf @MartinHjelmare -homeassistant/components/volkszaehler/* @fabaff -homeassistant/components/volumio/* @OnFreund -tests/components/volumio/* @OnFreund -homeassistant/components/volvooncall/* @molobrakos @decompil3d -homeassistant/components/wake_on_lan/* @ntilley905 -tests/components/wake_on_lan/* @ntilley905 -homeassistant/components/wallbox/* @hesselonline -tests/components/wallbox/* @hesselonline -homeassistant/components/waqi/* @andrey-git -homeassistant/components/watson_tts/* @rutkai -homeassistant/components/watttime/* @bachya -tests/components/watttime/* @bachya -homeassistant/components/waze_travel_time/* @eifinger -tests/components/waze_travel_time/* @eifinger -homeassistant/components/weather/* @fabaff -tests/components/weather/* @fabaff -homeassistant/components/webostv/* @bendavid @thecode -tests/components/webostv/* @bendavid @thecode -homeassistant/components/websocket_api/* @home-assistant/core -tests/components/websocket_api/* @home-assistant/core -homeassistant/components/wemo/* @esev -tests/components/wemo/* @esev -homeassistant/components/whirlpool/* @abmantis -tests/components/whirlpool/* @abmantis -homeassistant/components/whois/* @frenck -tests/components/whois/* @frenck -homeassistant/components/wiffi/* @mampfes -tests/components/wiffi/* @mampfes -homeassistant/components/wilight/* @leofig-rj -tests/components/wilight/* @leofig-rj -homeassistant/components/wirelesstag/* @sergeymaysak -homeassistant/components/withings/* @vangorra -tests/components/withings/* @vangorra -homeassistant/components/wiz/* @sbidy -tests/components/wiz/* @sbidy -homeassistant/components/wled/* @frenck -tests/components/wled/* @frenck -homeassistant/components/wolflink/* @adamkrol93 -tests/components/wolflink/* @adamkrol93 -homeassistant/components/workday/* @fabaff -tests/components/workday/* @fabaff -homeassistant/components/worldclock/* @fabaff -tests/components/worldclock/* @fabaff -homeassistant/components/xbox/* @hunterjm -tests/components/xbox/* @hunterjm -homeassistant/components/xbox_live/* @MartinHjelmare -homeassistant/components/xiaomi_aqara/* @danielhiversen @syssi -tests/components/xiaomi_aqara/* @danielhiversen @syssi -homeassistant/components/xiaomi_miio/* @rytilahti @syssi @starkillerOG @bieniu -tests/components/xiaomi_miio/* @rytilahti @syssi @starkillerOG @bieniu -homeassistant/components/xiaomi_tv/* @simse -homeassistant/components/xmpp/* @fabaff @flowolf -homeassistant/components/yale_smart_alarm/* @gjohansson-ST -tests/components/yale_smart_alarm/* @gjohansson-ST -homeassistant/components/yamaha_musiccast/* @vigonotion @micha91 -tests/components/yamaha_musiccast/* @vigonotion @micha91 -homeassistant/components/yandex_transport/* @rishatik92 @devbis -tests/components/yandex_transport/* @rishatik92 @devbis -homeassistant/components/yeelight/* @zewelor @shenxn @starkillerOG @alexyao2015 -tests/components/yeelight/* @zewelor @shenxn @starkillerOG @alexyao2015 -homeassistant/components/yeelightsunflower/* @lindsaymarkward -homeassistant/components/yi/* @bachya -homeassistant/components/youless/* @gjong -tests/components/youless/* @gjong -homeassistant/components/zeroconf/* @bdraco -tests/components/zeroconf/* @bdraco -homeassistant/components/zerproc/* @emlove -tests/components/zerproc/* @emlove -homeassistant/components/zha/* @dmulcahey @adminiuga -tests/components/zha/* @dmulcahey @adminiuga -homeassistant/components/zodiac/* @JulienTant -tests/components/zodiac/* @JulienTant -homeassistant/components/zone/* @home-assistant/core -tests/components/zone/* @home-assistant/core -homeassistant/components/zoneminder/* @rohankapoorcom -homeassistant/components/zwave/* @home-assistant/z-wave -tests/components/zwave/* @home-assistant/z-wave -homeassistant/components/zwave_js/* @home-assistant/z-wave -tests/components/zwave_js/* @home-assistant/z-wave -homeassistant/components/zwave_me/* @lawfulchaos @Z-Wave-Me -tests/components/zwave_me/* @lawfulchaos @Z-Wave-Me +/homeassistant/components/abode/ @shred86 +/tests/components/abode/ @shred86 +/homeassistant/components/accuweather/ @bieniu +/tests/components/accuweather/ @bieniu +/homeassistant/components/acmeda/ @atmurray +/tests/components/acmeda/ @atmurray +/homeassistant/components/adax/ @danielhiversen +/tests/components/adax/ @danielhiversen +/homeassistant/components/adguard/ @frenck +/tests/components/adguard/ @frenck +/homeassistant/components/advantage_air/ @Bre77 +/tests/components/advantage_air/ @Bre77 +/homeassistant/components/agent_dvr/ @ispysoftware +/tests/components/agent_dvr/ @ispysoftware +/homeassistant/components/air_quality/ @home-assistant/core +/tests/components/air_quality/ @home-assistant/core +/homeassistant/components/airly/ @bieniu +/tests/components/airly/ @bieniu +/homeassistant/components/airnow/ @asymworks +/tests/components/airnow/ @asymworks +/homeassistant/components/airthings/ @danielhiversen +/tests/components/airthings/ @danielhiversen +/homeassistant/components/airtouch4/ @LonePurpleWolf +/tests/components/airtouch4/ @LonePurpleWolf +/homeassistant/components/airvisual/ @bachya +/tests/components/airvisual/ @bachya +/homeassistant/components/airzone/ @Noltari +/tests/components/airzone/ @Noltari +/homeassistant/components/alarm_control_panel/ @home-assistant/core +/tests/components/alarm_control_panel/ @home-assistant/core +/homeassistant/components/alert/ @home-assistant/core +/tests/components/alert/ @home-assistant/core +/homeassistant/components/alexa/ @home-assistant/cloud @ochlocracy +/tests/components/alexa/ @home-assistant/cloud @ochlocracy +/homeassistant/components/almond/ @gcampax @balloob +/tests/components/almond/ @gcampax @balloob +/homeassistant/components/alpha_vantage/ @fabaff +/homeassistant/components/ambee/ @frenck +/tests/components/ambee/ @frenck +/homeassistant/components/amberelectric/ @madpilot +/tests/components/amberelectric/ @madpilot +/homeassistant/components/ambiclimate/ @danielhiversen +/tests/components/ambiclimate/ @danielhiversen +/homeassistant/components/ambient_station/ @bachya +/tests/components/ambient_station/ @bachya +/homeassistant/components/amcrest/ @flacjacket +/homeassistant/components/analytics/ @home-assistant/core @ludeeus +/tests/components/analytics/ @home-assistant/core @ludeeus +/homeassistant/components/androidtv/ @JeffLIrion @ollo69 +/tests/components/androidtv/ @JeffLIrion @ollo69 +/homeassistant/components/apache_kafka/ @bachya +/tests/components/apache_kafka/ @bachya +/homeassistant/components/api/ @home-assistant/core +/tests/components/api/ @home-assistant/core +/homeassistant/components/apple_tv/ @postlund +/tests/components/apple_tv/ @postlund +/homeassistant/components/apprise/ @caronc +/tests/components/apprise/ @caronc +/homeassistant/components/aprs/ @PhilRW +/tests/components/aprs/ @PhilRW +/homeassistant/components/arcam_fmj/ @elupus +/tests/components/arcam_fmj/ @elupus +/homeassistant/components/arest/ @fabaff +/homeassistant/components/arris_tg2492lg/ @vanbalken +/homeassistant/components/aseko_pool_live/ @milanmeu +/tests/components/aseko_pool_live/ @milanmeu +/homeassistant/components/asuswrt/ @kennedyshead @ollo69 +/tests/components/asuswrt/ @kennedyshead @ollo69 +/homeassistant/components/atag/ @MatsNL +/tests/components/atag/ @MatsNL +/homeassistant/components/aten_pe/ @mtdcr +/homeassistant/components/atome/ @baqs +/homeassistant/components/august/ @bdraco +/tests/components/august/ @bdraco +/homeassistant/components/aurora/ @djtimca +/tests/components/aurora/ @djtimca +/homeassistant/components/aurora_abb_powerone/ @davet2001 +/tests/components/aurora_abb_powerone/ @davet2001 +/homeassistant/components/aussie_broadband/ @nickw444 @Bre77 +/tests/components/aussie_broadband/ @nickw444 @Bre77 +/homeassistant/components/auth/ @home-assistant/core +/tests/components/auth/ @home-assistant/core +/homeassistant/components/automation/ @home-assistant/core +/tests/components/automation/ @home-assistant/core +/homeassistant/components/avea/ @pattyland +/homeassistant/components/awair/ @ahayworth @danielsjf +/tests/components/awair/ @ahayworth @danielsjf +/homeassistant/components/axis/ @Kane610 +/tests/components/axis/ @Kane610 +/homeassistant/components/azure_devops/ @timmo001 +/tests/components/azure_devops/ @timmo001 +/homeassistant/components/azure_event_hub/ @eavanvalkenburg +/tests/components/azure_event_hub/ @eavanvalkenburg +/homeassistant/components/azure_service_bus/ @hfurubotten +/homeassistant/components/backup/ @home-assistant/core +/tests/components/backup/ @home-assistant/core +/homeassistant/components/balboa/ @garbled1 +/tests/components/balboa/ @garbled1 +/homeassistant/components/beewi_smartclim/ @alemuro +/homeassistant/components/binary_sensor/ @home-assistant/core +/tests/components/binary_sensor/ @home-assistant/core +/homeassistant/components/bitcoin/ @fabaff +/homeassistant/components/bizkaibus/ @UgaitzEtxebarria +/homeassistant/components/blebox/ @bbx-a @bbx-jp +/tests/components/blebox/ @bbx-a @bbx-jp +/homeassistant/components/blink/ @fronzbot +/tests/components/blink/ @fronzbot +/homeassistant/components/blueprint/ @home-assistant/core +/tests/components/blueprint/ @home-assistant/core +/homeassistant/components/bluesound/ @thrawnarn +/homeassistant/components/bmw_connected_drive/ @gerard33 @rikroe +/tests/components/bmw_connected_drive/ @gerard33 @rikroe +/homeassistant/components/bond/ @bdraco @prystupa @joshs85 +/tests/components/bond/ @bdraco @prystupa @joshs85 +/homeassistant/components/bosch_shc/ @tschamm +/tests/components/bosch_shc/ @tschamm +/homeassistant/components/braviatv/ @bieniu @Drafteed +/tests/components/braviatv/ @bieniu @Drafteed +/homeassistant/components/broadlink/ @danielhiversen @felipediel @L-I-Am +/tests/components/broadlink/ @danielhiversen @felipediel @L-I-Am +/homeassistant/components/brother/ @bieniu +/tests/components/brother/ @bieniu +/homeassistant/components/brunt/ @eavanvalkenburg +/tests/components/brunt/ @eavanvalkenburg +/homeassistant/components/bsblan/ @liudger +/tests/components/bsblan/ @liudger +/homeassistant/components/bt_smarthub/ @jxwolstenholme +/homeassistant/components/buienradar/ @mjj4791 @ties @Robbie1221 +/tests/components/buienradar/ @mjj4791 @ties @Robbie1221 +/homeassistant/components/button/ @home-assistant/core +/tests/components/button/ @home-assistant/core +/homeassistant/components/calendar/ @home-assistant/core +/tests/components/calendar/ @home-assistant/core +/homeassistant/components/camera/ @home-assistant/core +/tests/components/camera/ @home-assistant/core +/homeassistant/components/cast/ @emontnemery +/tests/components/cast/ @emontnemery +/homeassistant/components/cert_expiry/ @Cereal2nd @jjlawren +/tests/components/cert_expiry/ @Cereal2nd @jjlawren +/homeassistant/components/circuit/ @braam +/homeassistant/components/cisco_ios/ @fbradyirl +/homeassistant/components/cisco_mobility_express/ @fbradyirl +/homeassistant/components/cisco_webex_teams/ @fbradyirl +/homeassistant/components/climacell/ @raman325 +/tests/components/climacell/ @raman325 +/homeassistant/components/climate/ @home-assistant/core +/tests/components/climate/ @home-assistant/core +/homeassistant/components/cloud/ @home-assistant/cloud +/tests/components/cloud/ @home-assistant/cloud +/homeassistant/components/cloudflare/ @ludeeus @ctalkington +/tests/components/cloudflare/ @ludeeus @ctalkington +/homeassistant/components/coinbase/ @tombrien +/tests/components/coinbase/ @tombrien +/homeassistant/components/color_extractor/ @GenericStudent +/tests/components/color_extractor/ @GenericStudent +/homeassistant/components/comfoconnect/ @michaelarnauts +/tests/components/comfoconnect/ @michaelarnauts +/homeassistant/components/compensation/ @Petro31 +/tests/components/compensation/ @Petro31 +/homeassistant/components/config/ @home-assistant/core +/tests/components/config/ @home-assistant/core +/homeassistant/components/configurator/ @home-assistant/core +/tests/components/configurator/ @home-assistant/core +/homeassistant/components/control4/ @lawtancool +/tests/components/control4/ @lawtancool +/homeassistant/components/conversation/ @home-assistant/core +/tests/components/conversation/ @home-assistant/core +/homeassistant/components/coolmaster/ @OnFreund +/tests/components/coolmaster/ @OnFreund +/homeassistant/components/coronavirus/ @home-assistant/core +/tests/components/coronavirus/ @home-assistant/core +/homeassistant/components/counter/ @fabaff +/tests/components/counter/ @fabaff +/homeassistant/components/cover/ @home-assistant/core +/tests/components/cover/ @home-assistant/core +/homeassistant/components/cpuspeed/ @fabaff @frenck +/tests/components/cpuspeed/ @fabaff @frenck +/homeassistant/components/crownstone/ @Crownstone @RicArch97 +/tests/components/crownstone/ @Crownstone @RicArch97 +/homeassistant/components/cups/ @fabaff +/homeassistant/components/daikin/ @fredrike +/tests/components/daikin/ @fredrike +/homeassistant/components/darksky/ @fabaff +/tests/components/darksky/ @fabaff +/homeassistant/components/debugpy/ @frenck +/tests/components/debugpy/ @frenck +/homeassistant/components/deconz/ @Kane610 +/tests/components/deconz/ @Kane610 +/homeassistant/components/default_config/ @home-assistant/core +/tests/components/default_config/ @home-assistant/core +/homeassistant/components/delijn/ @bollewolle @Emilv2 +/homeassistant/components/deluge/ @tkdrob +/tests/components/deluge/ @tkdrob +/homeassistant/components/demo/ @home-assistant/core +/tests/components/demo/ @home-assistant/core +/homeassistant/components/denonavr/ @ol-iver @starkillerOG +/tests/components/denonavr/ @ol-iver @starkillerOG +/homeassistant/components/derivative/ @afaucogney +/tests/components/derivative/ @afaucogney +/homeassistant/components/device_automation/ @home-assistant/core +/tests/components/device_automation/ @home-assistant/core +/homeassistant/components/device_tracker/ @home-assistant/core +/tests/components/device_tracker/ @home-assistant/core +/homeassistant/components/devolo_home_control/ @2Fake @Shutgun +/tests/components/devolo_home_control/ @2Fake @Shutgun +/homeassistant/components/devolo_home_network/ @2Fake @Shutgun +/tests/components/devolo_home_network/ @2Fake @Shutgun +/homeassistant/components/dexcom/ @gagebenne +/tests/components/dexcom/ @gagebenne +/homeassistant/components/dhcp/ @bdraco +/tests/components/dhcp/ @bdraco +/homeassistant/components/diagnostics/ @home-assistant/core +/tests/components/diagnostics/ @home-assistant/core +/homeassistant/components/digital_ocean/ @fabaff +/homeassistant/components/discogs/ @thibmaek +/homeassistant/components/discord/ @tkdrob +/tests/components/discord/ @tkdrob +/homeassistant/components/discovery/ @home-assistant/core +/tests/components/discovery/ @home-assistant/core +/homeassistant/components/dlna_dmr/ @StevenLooman @chishm +/tests/components/dlna_dmr/ @StevenLooman @chishm +/homeassistant/components/dlna_dms/ @chishm +/tests/components/dlna_dms/ @chishm +/homeassistant/components/dnsip/ @gjohansson-ST +/tests/components/dnsip/ @gjohansson-ST +/homeassistant/components/doorbird/ @oblogic7 @bdraco @flacjacket +/tests/components/doorbird/ @oblogic7 @bdraco @flacjacket +/homeassistant/components/dsmr/ @Robbie1221 @frenck +/tests/components/dsmr/ @Robbie1221 @frenck +/homeassistant/components/dsmr_reader/ @depl0y +/homeassistant/components/dunehd/ @bieniu +/tests/components/dunehd/ @bieniu +/homeassistant/components/dwd_weather_warnings/ @runningman84 @stephan192 @Hummel95 +/homeassistant/components/dweet/ @fabaff +/homeassistant/components/dynalite/ @ziv1234 +/tests/components/dynalite/ @ziv1234 +/homeassistant/components/eafm/ @Jc2k +/tests/components/eafm/ @Jc2k +/homeassistant/components/ecobee/ @marthoc +/tests/components/ecobee/ @marthoc +/homeassistant/components/econet/ @vangorra @w1ll1am23 +/tests/components/econet/ @vangorra @w1ll1am23 +/homeassistant/components/ecovacs/ @OverloadUT +/homeassistant/components/edl21/ @mtdcr +/homeassistant/components/efergy/ @tkdrob +/tests/components/efergy/ @tkdrob +/homeassistant/components/egardia/ @jeroenterheerdt +/homeassistant/components/eight_sleep/ @mezz64 @raman325 +/homeassistant/components/elgato/ @frenck +/tests/components/elgato/ @frenck +/homeassistant/components/elkm1/ @gwww @bdraco +/tests/components/elkm1/ @gwww @bdraco +/homeassistant/components/elmax/ @albertogeniola +/tests/components/elmax/ @albertogeniola +/homeassistant/components/elv/ @majuss +/homeassistant/components/emby/ @mezz64 +/homeassistant/components/emoncms/ @borpin +/homeassistant/components/emonitor/ @bdraco +/tests/components/emonitor/ @bdraco +/homeassistant/components/emulated_kasa/ @kbickar +/tests/components/emulated_kasa/ @kbickar +/homeassistant/components/energy/ @home-assistant/core +/tests/components/energy/ @home-assistant/core +/homeassistant/components/enigma2/ @fbradyirl +/homeassistant/components/enocean/ @bdurrer +/tests/components/enocean/ @bdurrer +/homeassistant/components/enphase_envoy/ @gtdiehl +/tests/components/enphase_envoy/ @gtdiehl +/homeassistant/components/entur_public_transport/ @hfurubotten +/homeassistant/components/environment_canada/ @gwww @michaeldavie +/tests/components/environment_canada/ @gwww @michaeldavie +/homeassistant/components/envisalink/ @ufodone +/homeassistant/components/ephember/ @ttroy50 +/homeassistant/components/epson/ @pszafer +/tests/components/epson/ @pszafer +/homeassistant/components/epsonworkforce/ @ThaStealth +/homeassistant/components/eq3btsmart/ @rytilahti +/homeassistant/components/esphome/ @OttoWinter @jesserockz +/tests/components/esphome/ @OttoWinter @jesserockz +/homeassistant/components/evil_genius_labs/ @balloob +/tests/components/evil_genius_labs/ @balloob +/homeassistant/components/evohome/ @zxdavb +/homeassistant/components/ezviz/ @RenierM26 @baqs +/tests/components/ezviz/ @RenierM26 @baqs +/homeassistant/components/faa_delays/ @ntilley905 +/tests/components/faa_delays/ @ntilley905 +/homeassistant/components/fan/ @home-assistant/core +/tests/components/fan/ @home-assistant/core +/homeassistant/components/fastdotcom/ @rohankapoorcom +/homeassistant/components/fibaro/ @rappenze +/tests/components/fibaro/ @rappenze +/homeassistant/components/file/ @fabaff +/tests/components/file/ @fabaff +/homeassistant/components/filesize/ @gjohansson-ST +/tests/components/filesize/ @gjohansson-ST +/homeassistant/components/filter/ @dgomes +/tests/components/filter/ @dgomes +/homeassistant/components/fireservicerota/ @cyberjunky +/tests/components/fireservicerota/ @cyberjunky +/homeassistant/components/firmata/ @DaAwesomeP +/tests/components/firmata/ @DaAwesomeP +/homeassistant/components/fivem/ @Sander0542 +/tests/components/fivem/ @Sander0542 +/homeassistant/components/fixer/ @fabaff +/homeassistant/components/fjaraskupan/ @elupus +/tests/components/fjaraskupan/ @elupus +/homeassistant/components/flick_electric/ @ZephireNZ +/tests/components/flick_electric/ @ZephireNZ +/homeassistant/components/flipr/ @cnico +/tests/components/flipr/ @cnico +/homeassistant/components/flo/ @dmulcahey +/tests/components/flo/ @dmulcahey +/homeassistant/components/flock/ @fabaff +/homeassistant/components/flume/ @ChrisMandich @bdraco +/tests/components/flume/ @ChrisMandich @bdraco +/homeassistant/components/flunearyou/ @bachya +/tests/components/flunearyou/ @bachya +/homeassistant/components/flux_led/ @icemanch @bdraco +/tests/components/flux_led/ @icemanch @bdraco +/homeassistant/components/forecast_solar/ @klaasnicolaas @frenck +/tests/components/forecast_solar/ @klaasnicolaas @frenck +/homeassistant/components/forked_daapd/ @uvjustin +/tests/components/forked_daapd/ @uvjustin +/homeassistant/components/fortios/ @kimfrellsen +/homeassistant/components/foscam/ @skgsergio +/tests/components/foscam/ @skgsergio +/homeassistant/components/freebox/ @hacf-fr @Quentame +/tests/components/freebox/ @hacf-fr @Quentame +/homeassistant/components/freedompro/ @stefano055415 +/tests/components/freedompro/ @stefano055415 +/homeassistant/components/fritz/ @mammuth @AaronDavidSchneider @chemelli74 @mib1185 +/tests/components/fritz/ @mammuth @AaronDavidSchneider @chemelli74 @mib1185 +/homeassistant/components/fritzbox/ @mib1185 @flabbamann +/tests/components/fritzbox/ @mib1185 @flabbamann +/homeassistant/components/fronius/ @nielstron @farmio +/tests/components/fronius/ @nielstron @farmio +/homeassistant/components/frontend/ @home-assistant/frontend +/tests/components/frontend/ @home-assistant/frontend +/homeassistant/components/garages_amsterdam/ @klaasnicolaas +/tests/components/garages_amsterdam/ @klaasnicolaas +/homeassistant/components/gdacs/ @exxamalte +/tests/components/gdacs/ @exxamalte +/homeassistant/components/generic/ @davet2001 +/tests/components/generic/ @davet2001 +/homeassistant/components/generic_hygrostat/ @Shulyaka +/tests/components/generic_hygrostat/ @Shulyaka +/homeassistant/components/geniushub/ @zxdavb +/homeassistant/components/geo_json_events/ @exxamalte +/tests/components/geo_json_events/ @exxamalte +/homeassistant/components/geo_location/ @home-assistant/core +/tests/components/geo_location/ @home-assistant/core +/homeassistant/components/geo_rss_events/ @exxamalte +/tests/components/geo_rss_events/ @exxamalte +/homeassistant/components/geonetnz_quakes/ @exxamalte +/tests/components/geonetnz_quakes/ @exxamalte +/homeassistant/components/geonetnz_volcano/ @exxamalte +/tests/components/geonetnz_volcano/ @exxamalte +/homeassistant/components/gios/ @bieniu +/tests/components/gios/ @bieniu +/homeassistant/components/github/ @timmo001 @ludeeus +/tests/components/github/ @timmo001 @ludeeus +/homeassistant/components/gitter/ @fabaff +/homeassistant/components/glances/ @fabaff @engrbm87 +/tests/components/glances/ @fabaff @engrbm87 +/homeassistant/components/goalzero/ @tkdrob +/tests/components/goalzero/ @tkdrob +/homeassistant/components/gogogate2/ @vangorra @bdraco +/tests/components/gogogate2/ @vangorra @bdraco +/homeassistant/components/goodwe/ @mletenay @starkillerOG +/tests/components/goodwe/ @mletenay @starkillerOG +/homeassistant/components/google/ @allenporter +/tests/components/google/ @allenporter +/homeassistant/components/google_assistant/ @home-assistant/cloud +/tests/components/google_assistant/ @home-assistant/cloud +/homeassistant/components/google_cloud/ @lufton +/homeassistant/components/google_travel_time/ @eifinger +/tests/components/google_travel_time/ @eifinger +/homeassistant/components/gpsd/ @fabaff +/homeassistant/components/gree/ @cmroche +/tests/components/gree/ @cmroche +/homeassistant/components/greeneye_monitor/ @jkeljo +/tests/components/greeneye_monitor/ @jkeljo +/homeassistant/components/group/ @home-assistant/core +/tests/components/group/ @home-assistant/core +/homeassistant/components/growatt_server/ @indykoning @muppet3000 @JasperPlant +/tests/components/growatt_server/ @indykoning @muppet3000 @JasperPlant +/homeassistant/components/guardian/ @bachya +/tests/components/guardian/ @bachya +/homeassistant/components/habitica/ @ASMfreaK @leikoilja +/tests/components/habitica/ @ASMfreaK @leikoilja +/homeassistant/components/harmony/ @ehendrix23 @bramkragten @bdraco @mkeesey @Aohzan +/tests/components/harmony/ @ehendrix23 @bramkragten @bdraco @mkeesey @Aohzan +/homeassistant/components/hassio/ @home-assistant/supervisor +/tests/components/hassio/ @home-assistant/supervisor +/homeassistant/components/heatmiser/ @andylockran +/homeassistant/components/heos/ @andrewsayre +/tests/components/heos/ @andrewsayre +/homeassistant/components/here_travel_time/ @eifinger +/tests/components/here_travel_time/ @eifinger +/homeassistant/components/hikvision/ @mezz64 +/homeassistant/components/hikvisioncam/ @fbradyirl +/homeassistant/components/hisense_aehw4a1/ @bannhead +/tests/components/hisense_aehw4a1/ @bannhead +/homeassistant/components/history/ @home-assistant/core +/tests/components/history/ @home-assistant/core +/homeassistant/components/hive/ @Rendili @KJonline +/tests/components/hive/ @Rendili @KJonline +/homeassistant/components/hlk_sw16/ @jameshilliard +/tests/components/hlk_sw16/ @jameshilliard +/homeassistant/components/home_connect/ @DavidMStraub +/tests/components/home_connect/ @DavidMStraub +/homeassistant/components/home_plus_control/ @chemaaa +/tests/components/home_plus_control/ @chemaaa +/homeassistant/components/homeassistant/ @home-assistant/core +/tests/components/homeassistant/ @home-assistant/core +/homeassistant/components/homekit/ @bdraco +/tests/components/homekit/ @bdraco +/homeassistant/components/homekit_controller/ @Jc2k @bdraco +/tests/components/homekit_controller/ @Jc2k @bdraco +/homeassistant/components/homematic/ @pvizeli @danielperna84 +/tests/components/homematic/ @pvizeli @danielperna84 +/homeassistant/components/homewizard/ @DCSBL +/tests/components/homewizard/ @DCSBL +/homeassistant/components/honeywell/ @rdfurman +/tests/components/honeywell/ @rdfurman +/homeassistant/components/http/ @home-assistant/core +/tests/components/http/ @home-assistant/core +/homeassistant/components/huawei_lte/ @scop @fphammerle +/tests/components/huawei_lte/ @scop @fphammerle +/homeassistant/components/hue/ @balloob @marcelveldt +/tests/components/hue/ @balloob @marcelveldt +/homeassistant/components/huisbaasje/ @dennisschroer +/tests/components/huisbaasje/ @dennisschroer +/homeassistant/components/humidifier/ @home-assistant/core @Shulyaka +/tests/components/humidifier/ @home-assistant/core @Shulyaka +/homeassistant/components/hunterdouglas_powerview/ @bdraco +/tests/components/hunterdouglas_powerview/ @bdraco +/homeassistant/components/hvv_departures/ @vigonotion +/tests/components/hvv_departures/ @vigonotion +/homeassistant/components/hydrawise/ @ptcryan +/homeassistant/components/hyperion/ @dermotduffy +/tests/components/hyperion/ @dermotduffy +/homeassistant/components/ialarm/ @RyuzakiKK +/tests/components/ialarm/ @RyuzakiKK +/homeassistant/components/iammeter/ @lewei50 +/homeassistant/components/iaqualink/ @flz +/tests/components/iaqualink/ @flz +/homeassistant/components/icloud/ @Quentame @nzapponi +/tests/components/icloud/ @Quentame @nzapponi +/homeassistant/components/ign_sismologia/ @exxamalte +/tests/components/ign_sismologia/ @exxamalte +/homeassistant/components/image/ @home-assistant/core +/tests/components/image/ @home-assistant/core +/homeassistant/components/image_processing/ @home-assistant/core +/tests/components/image_processing/ @home-assistant/core +/homeassistant/components/incomfort/ @zxdavb +/homeassistant/components/influxdb/ @fabaff @mdegat01 +/tests/components/influxdb/ @fabaff @mdegat01 +/homeassistant/components/input_boolean/ @home-assistant/core +/tests/components/input_boolean/ @home-assistant/core +/homeassistant/components/input_button/ @home-assistant/core +/tests/components/input_button/ @home-assistant/core +/homeassistant/components/input_datetime/ @home-assistant/core +/tests/components/input_datetime/ @home-assistant/core +/homeassistant/components/input_number/ @home-assistant/core +/tests/components/input_number/ @home-assistant/core +/homeassistant/components/input_select/ @home-assistant/core +/tests/components/input_select/ @home-assistant/core +/homeassistant/components/input_text/ @home-assistant/core +/tests/components/input_text/ @home-assistant/core +/homeassistant/components/insteon/ @teharris1 +/tests/components/insteon/ @teharris1 +/homeassistant/components/integration/ @dgomes +/tests/components/integration/ @dgomes +/homeassistant/components/intellifire/ @jeeftor +/tests/components/intellifire/ @jeeftor +/homeassistant/components/intent/ @home-assistant/core +/tests/components/intent/ @home-assistant/core +/homeassistant/components/intesishome/ @jnimmo +/homeassistant/components/ios/ @robbiet480 +/tests/components/ios/ @robbiet480 +/homeassistant/components/iotawatt/ @gtdiehl @jyavenard +/tests/components/iotawatt/ @gtdiehl @jyavenard +/homeassistant/components/iperf3/ @rohankapoorcom +/homeassistant/components/ipma/ @dgomes @abmantis +/tests/components/ipma/ @dgomes @abmantis +/homeassistant/components/ipp/ @ctalkington +/tests/components/ipp/ @ctalkington +/homeassistant/components/iqvia/ @bachya +/tests/components/iqvia/ @bachya +/homeassistant/components/irish_rail_transport/ @ttroy50 +/homeassistant/components/islamic_prayer_times/ @engrbm87 +/tests/components/islamic_prayer_times/ @engrbm87 +/homeassistant/components/iss/ @DurgNomis-drol +/tests/components/iss/ @DurgNomis-drol +/homeassistant/components/isy994/ @bdraco @shbatm +/tests/components/isy994/ @bdraco @shbatm +/homeassistant/components/izone/ @Swamp-Ig +/tests/components/izone/ @Swamp-Ig +/homeassistant/components/jellyfin/ @j-stienstra +/tests/components/jellyfin/ @j-stienstra +/homeassistant/components/jewish_calendar/ @tsvi +/tests/components/jewish_calendar/ @tsvi +/homeassistant/components/juicenet/ @jesserockz +/tests/components/juicenet/ @jesserockz +/homeassistant/components/kaiterra/ @Michsior14 +/homeassistant/components/kaleidescape/ @SteveEasley +/tests/components/kaleidescape/ @SteveEasley +/homeassistant/components/keba/ @dannerph +/homeassistant/components/keenetic_ndms2/ @foxel +/tests/components/keenetic_ndms2/ @foxel +/homeassistant/components/kef/ @basnijholt +/homeassistant/components/keyboard_remote/ @bendavid @lanrat +/homeassistant/components/kmtronic/ @dgomes +/tests/components/kmtronic/ @dgomes +/homeassistant/components/knx/ @Julius2342 @farmio @marvin-w +/tests/components/knx/ @Julius2342 @farmio @marvin-w +/homeassistant/components/kodi/ @OnFreund @cgtobi +/tests/components/kodi/ @OnFreund @cgtobi +/homeassistant/components/konnected/ @heythisisnate +/tests/components/konnected/ @heythisisnate +/homeassistant/components/kostal_plenticore/ @stegm +/tests/components/kostal_plenticore/ @stegm +/homeassistant/components/kraken/ @eifinger +/tests/components/kraken/ @eifinger +/homeassistant/components/kulersky/ @emlove +/tests/components/kulersky/ @emlove +/homeassistant/components/lametric/ @robbiet480 @frenck +/homeassistant/components/launch_library/ @ludeeus @DurgNomis-drol +/tests/components/launch_library/ @ludeeus @DurgNomis-drol +/homeassistant/components/lcn/ @alengwenus +/tests/components/lcn/ @alengwenus +/homeassistant/components/lg_netcast/ @Drafteed +/homeassistant/components/life360/ @pnbruckner +/homeassistant/components/light/ @home-assistant/core +/tests/components/light/ @home-assistant/core +/homeassistant/components/linux_battery/ @fabaff +/homeassistant/components/litejet/ @joncar +/tests/components/litejet/ @joncar +/homeassistant/components/litterrobot/ @natekspencer +/tests/components/litterrobot/ @natekspencer +/homeassistant/components/local_ip/ @issacg +/tests/components/local_ip/ @issacg +/homeassistant/components/lock/ @home-assistant/core +/tests/components/lock/ @home-assistant/core +/homeassistant/components/logbook/ @home-assistant/core +/tests/components/logbook/ @home-assistant/core +/homeassistant/components/logger/ @home-assistant/core +/tests/components/logger/ @home-assistant/core +/homeassistant/components/logi_circle/ @evanjd +/tests/components/logi_circle/ @evanjd +/homeassistant/components/lookin/ @ANMalko @bdraco +/tests/components/lookin/ @ANMalko @bdraco +/homeassistant/components/lovelace/ @home-assistant/frontend +/tests/components/lovelace/ @home-assistant/frontend +/homeassistant/components/luci/ @mzdrale +/homeassistant/components/luftdaten/ @fabaff @frenck +/tests/components/luftdaten/ @fabaff @frenck +/homeassistant/components/lupusec/ @majuss +/homeassistant/components/lutron/ @JonGilmore +/homeassistant/components/lutron_caseta/ @swails @bdraco +/tests/components/lutron_caseta/ @swails @bdraco +/homeassistant/components/lyric/ @timmo001 +/tests/components/lyric/ @timmo001 +/homeassistant/components/mastodon/ @fabaff +/homeassistant/components/matrix/ @tinloaf +/homeassistant/components/mazda/ @bdr99 +/tests/components/mazda/ @bdr99 +/homeassistant/components/media_player/ @home-assistant/core +/tests/components/media_player/ @home-assistant/core +/homeassistant/components/media_source/ @hunterjm +/tests/components/media_source/ @hunterjm +/homeassistant/components/mediaroom/ @dgomes +/homeassistant/components/melcloud/ @vilppuvuorinen +/tests/components/melcloud/ @vilppuvuorinen +/homeassistant/components/melissa/ @kennedyshead +/tests/components/melissa/ @kennedyshead +/homeassistant/components/met/ @danielhiversen @thimic +/tests/components/met/ @danielhiversen @thimic +/homeassistant/components/met_eireann/ @DylanGore +/tests/components/met_eireann/ @DylanGore +/homeassistant/components/meteo_france/ @hacf-fr @oncleben31 @Quentame +/tests/components/meteo_france/ @hacf-fr @oncleben31 @Quentame +/homeassistant/components/meteoalarm/ @rolfberkenbosch +/homeassistant/components/meteoclimatic/ @adrianmo +/tests/components/meteoclimatic/ @adrianmo +/homeassistant/components/metoffice/ @MrHarcombe +/tests/components/metoffice/ @MrHarcombe +/homeassistant/components/miflora/ @danielhiversen @basnijholt +/homeassistant/components/mikrotik/ @engrbm87 +/tests/components/mikrotik/ @engrbm87 +/homeassistant/components/mill/ @danielhiversen +/tests/components/mill/ @danielhiversen +/homeassistant/components/min_max/ @fabaff +/tests/components/min_max/ @fabaff +/homeassistant/components/minecraft_server/ @elmurato +/tests/components/minecraft_server/ @elmurato +/homeassistant/components/minio/ @tkislan +/tests/components/minio/ @tkislan +/homeassistant/components/mobile_app/ @home-assistant/core +/tests/components/mobile_app/ @home-assistant/core +/homeassistant/components/modbus/ @adamchengtkc @janiversen @vzahradnik +/tests/components/modbus/ @adamchengtkc @janiversen @vzahradnik +/homeassistant/components/modem_callerid/ @tkdrob +/tests/components/modem_callerid/ @tkdrob +/homeassistant/components/modern_forms/ @wonderslug +/tests/components/modern_forms/ @wonderslug +/homeassistant/components/moehlenhoff_alpha2/ @j-a-n +/tests/components/moehlenhoff_alpha2/ @j-a-n +/homeassistant/components/monoprice/ @etsinko @OnFreund +/tests/components/monoprice/ @etsinko @OnFreund +/homeassistant/components/moon/ @fabaff @frenck +/tests/components/moon/ @fabaff @frenck +/homeassistant/components/motion_blinds/ @starkillerOG +/tests/components/motion_blinds/ @starkillerOG +/homeassistant/components/motioneye/ @dermotduffy +/tests/components/motioneye/ @dermotduffy +/homeassistant/components/mpd/ @fabaff +/homeassistant/components/mqtt/ @emontnemery +/tests/components/mqtt/ @emontnemery +/homeassistant/components/msteams/ @peroyvind +/homeassistant/components/mullvad/ @meichthys +/tests/components/mullvad/ @meichthys +/homeassistant/components/mutesync/ @currentoor +/tests/components/mutesync/ @currentoor +/homeassistant/components/my/ @home-assistant/core +/tests/components/my/ @home-assistant/core +/homeassistant/components/myq/ @bdraco @ehendrix23 +/tests/components/myq/ @bdraco @ehendrix23 +/homeassistant/components/mysensors/ @MartinHjelmare @functionpointer +/tests/components/mysensors/ @MartinHjelmare @functionpointer +/homeassistant/components/mystrom/ @fabaff +/homeassistant/components/nam/ @bieniu +/tests/components/nam/ @bieniu +/homeassistant/components/nanoleaf/ @milanmeu +/tests/components/nanoleaf/ @milanmeu +/homeassistant/components/neato/ @dshokouhi @Santobert +/tests/components/neato/ @dshokouhi @Santobert +/homeassistant/components/nederlandse_spoorwegen/ @YarmoM +/homeassistant/components/ness_alarm/ @nickw444 +/tests/components/ness_alarm/ @nickw444 +/homeassistant/components/nest/ @allenporter +/tests/components/nest/ @allenporter +/homeassistant/components/netatmo/ @cgtobi +/tests/components/netatmo/ @cgtobi +/homeassistant/components/netdata/ @fabaff +/homeassistant/components/netgear/ @hacf-fr @Quentame @starkillerOG +/tests/components/netgear/ @hacf-fr @Quentame @starkillerOG +/homeassistant/components/network/ @home-assistant/core +/tests/components/network/ @home-assistant/core +/homeassistant/components/nexia/ @bdraco +/tests/components/nexia/ @bdraco +/homeassistant/components/nextbus/ @vividboarder +/tests/components/nextbus/ @vividboarder +/homeassistant/components/nextcloud/ @meichthys +/homeassistant/components/nfandroidtv/ @tkdrob +/tests/components/nfandroidtv/ @tkdrob +/homeassistant/components/nightscout/ @marciogranzotto +/tests/components/nightscout/ @marciogranzotto +/homeassistant/components/nilu/ @hfurubotten +/homeassistant/components/nina/ @DeerMaximum +/tests/components/nina/ @DeerMaximum +/homeassistant/components/nissan_leaf/ @filcole +/homeassistant/components/nmbs/ @thibmaek +/homeassistant/components/no_ip/ @fabaff +/tests/components/no_ip/ @fabaff +/homeassistant/components/noaa_tides/ @jdelaney72 +/homeassistant/components/notify/ @home-assistant/core +/tests/components/notify/ @home-assistant/core +/homeassistant/components/notify_events/ @matrozov @papajojo +/tests/components/notify_events/ @matrozov @papajojo +/homeassistant/components/notion/ @bachya +/tests/components/notion/ @bachya +/homeassistant/components/nsw_fuel_station/ @nickw444 +/tests/components/nsw_fuel_station/ @nickw444 +/homeassistant/components/nsw_rural_fire_service_feed/ @exxamalte +/tests/components/nsw_rural_fire_service_feed/ @exxamalte +/homeassistant/components/nuki/ @pschmitt @pvizeli @pree +/tests/components/nuki/ @pschmitt @pvizeli @pree +/homeassistant/components/numato/ @clssn +/tests/components/numato/ @clssn +/homeassistant/components/number/ @home-assistant/core @Shulyaka +/tests/components/number/ @home-assistant/core @Shulyaka +/homeassistant/components/nut/ @bdraco @ollo69 +/tests/components/nut/ @bdraco @ollo69 +/homeassistant/components/nws/ @MatthewFlamm +/tests/components/nws/ @MatthewFlamm +/homeassistant/components/nzbget/ @chriscla +/tests/components/nzbget/ @chriscla +/homeassistant/components/obihai/ @dshokouhi +/homeassistant/components/octoprint/ @rfleming71 +/tests/components/octoprint/ @rfleming71 +/homeassistant/components/ohmconnect/ @robbiet480 +/homeassistant/components/ombi/ @larssont +/homeassistant/components/omnilogic/ @oliver84 @djtimca @gentoosu +/tests/components/omnilogic/ @oliver84 @djtimca @gentoosu +/homeassistant/components/onboarding/ @home-assistant/core +/tests/components/onboarding/ @home-assistant/core +/homeassistant/components/oncue/ @bdraco +/tests/components/oncue/ @bdraco +/homeassistant/components/ondilo_ico/ @JeromeHXP +/tests/components/ondilo_ico/ @JeromeHXP +/homeassistant/components/onewire/ @garbled1 @epenet +/tests/components/onewire/ @garbled1 @epenet +/homeassistant/components/onvif/ @hunterjm +/tests/components/onvif/ @hunterjm +/homeassistant/components/open_meteo/ @frenck +/tests/components/open_meteo/ @frenck +/homeassistant/components/openerz/ @misialq +/tests/components/openerz/ @misialq +/homeassistant/components/opengarage/ @danielhiversen +/tests/components/opengarage/ @danielhiversen +/homeassistant/components/openhome/ @bazwilliams +/homeassistant/components/opentherm_gw/ @mvn23 +/tests/components/opentherm_gw/ @mvn23 +/homeassistant/components/openuv/ @bachya +/tests/components/openuv/ @bachya +/homeassistant/components/openweathermap/ @fabaff @freekode @nzapponi +/tests/components/openweathermap/ @fabaff @freekode @nzapponi +/homeassistant/components/opnsense/ @mtreinish +/tests/components/opnsense/ @mtreinish +/homeassistant/components/oru/ @bvlaicu +/homeassistant/components/overkiz/ @imicknl @vlebourl @tetienne +/tests/components/overkiz/ @imicknl @vlebourl @tetienne +/homeassistant/components/ovo_energy/ @timmo001 +/tests/components/ovo_energy/ @timmo001 +/homeassistant/components/p1_monitor/ @klaasnicolaas +/tests/components/p1_monitor/ @klaasnicolaas +/homeassistant/components/panel_custom/ @home-assistant/frontend +/tests/components/panel_custom/ @home-assistant/frontend +/homeassistant/components/panel_iframe/ @home-assistant/frontend +/tests/components/panel_iframe/ @home-assistant/frontend +/homeassistant/components/peco/ @IceBotYT +/tests/components/peco/ @IceBotYT +/homeassistant/components/persistent_notification/ @home-assistant/core +/tests/components/persistent_notification/ @home-assistant/core +/homeassistant/components/philips_js/ @elupus +/tests/components/philips_js/ @elupus +/homeassistant/components/pi_hole/ @fabaff @johnluetke @shenxn +/tests/components/pi_hole/ @fabaff @johnluetke @shenxn +/homeassistant/components/picnic/ @corneyl +/tests/components/picnic/ @corneyl +/homeassistant/components/pilight/ @trekky12 +/tests/components/pilight/ @trekky12 +/homeassistant/components/plaato/ @JohNan +/tests/components/plaato/ @JohNan +/homeassistant/components/plex/ @jjlawren +/tests/components/plex/ @jjlawren +/homeassistant/components/plugwise/ @CoMPaTech @bouwew @brefra @frenck +/tests/components/plugwise/ @CoMPaTech @bouwew @brefra @frenck +/homeassistant/components/plum_lightpad/ @ColinHarrington @prystupa +/tests/components/plum_lightpad/ @ColinHarrington @prystupa +/homeassistant/components/point/ @fredrike +/tests/components/point/ @fredrike +/homeassistant/components/poolsense/ @haemishkyd +/tests/components/poolsense/ @haemishkyd +/homeassistant/components/powerwall/ @bdraco @jrester +/tests/components/powerwall/ @bdraco @jrester +/homeassistant/components/profiler/ @bdraco +/tests/components/profiler/ @bdraco +/homeassistant/components/progettihwsw/ @ardaseremet +/tests/components/progettihwsw/ @ardaseremet +/homeassistant/components/prometheus/ @knyar +/tests/components/prometheus/ @knyar +/homeassistant/components/prosegur/ @dgomes +/tests/components/prosegur/ @dgomes +/homeassistant/components/proxmoxve/ @jhollowe @Corbeno +/homeassistant/components/ps4/ @ktnrg45 +/tests/components/ps4/ @ktnrg45 +/homeassistant/components/pure_energie/ @klaasnicolaas +/tests/components/pure_energie/ @klaasnicolaas +/homeassistant/components/push/ @dgomes +/tests/components/push/ @dgomes +/homeassistant/components/pvoutput/ @fabaff @frenck +/tests/components/pvoutput/ @fabaff @frenck +/homeassistant/components/pvpc_hourly_pricing/ @azogue +/tests/components/pvpc_hourly_pricing/ @azogue +/homeassistant/components/qbittorrent/ @geoffreylagaisse +/homeassistant/components/qld_bushfire/ @exxamalte +/tests/components/qld_bushfire/ @exxamalte +/homeassistant/components/quantum_gateway/ @cisasteelersfan +/homeassistant/components/qvr_pro/ @oblogic7 +/homeassistant/components/qwikswitch/ @kellerza +/tests/components/qwikswitch/ @kellerza +/homeassistant/components/rachio/ @bdraco +/tests/components/rachio/ @bdraco +/homeassistant/components/radio_browser/ @frenck +/tests/components/radio_browser/ @frenck +/homeassistant/components/radiotherm/ @vinnyfuria +/homeassistant/components/rainbird/ @konikvranik +/homeassistant/components/raincloud/ @vanstinator +/homeassistant/components/rainforest_eagle/ @gtdiehl @jcalbert +/tests/components/rainforest_eagle/ @gtdiehl @jcalbert +/homeassistant/components/rainmachine/ @bachya +/tests/components/rainmachine/ @bachya +/homeassistant/components/random/ @fabaff +/tests/components/random/ @fabaff +/homeassistant/components/rdw/ @frenck +/tests/components/rdw/ @frenck +/homeassistant/components/recollect_waste/ @bachya +/tests/components/recollect_waste/ @bachya +/homeassistant/components/recorder/ @home-assistant/core +/tests/components/recorder/ @home-assistant/core +/homeassistant/components/rejseplanen/ @DarkFox +/homeassistant/components/remote/ @home-assistant/core +/tests/components/remote/ @home-assistant/core +/homeassistant/components/renault/ @epenet +/tests/components/renault/ @epenet +/homeassistant/components/repetier/ @MTrab @ShadowBr0ther +/homeassistant/components/rflink/ @javicalle +/tests/components/rflink/ @javicalle +/homeassistant/components/rfxtrx/ @danielhiversen @elupus @RobBie1221 +/tests/components/rfxtrx/ @danielhiversen @elupus @RobBie1221 +/homeassistant/components/ridwell/ @bachya +/tests/components/ridwell/ @bachya +/homeassistant/components/ring/ @balloob +/tests/components/ring/ @balloob +/homeassistant/components/risco/ @OnFreund +/tests/components/risco/ @OnFreund +/homeassistant/components/rituals_perfume_genie/ @milanmeu +/tests/components/rituals_perfume_genie/ @milanmeu +/homeassistant/components/rmvtransport/ @cgtobi +/tests/components/rmvtransport/ @cgtobi +/homeassistant/components/roku/ @ctalkington +/tests/components/roku/ @ctalkington +/homeassistant/components/roomba/ @pschmitt @cyr-ius @shenxn +/tests/components/roomba/ @pschmitt @cyr-ius @shenxn +/homeassistant/components/roon/ @pavoni +/tests/components/roon/ @pavoni +/homeassistant/components/rpi_power/ @shenxn @swetoast +/tests/components/rpi_power/ @shenxn @swetoast +/homeassistant/components/rss_feed_template/ @home-assistant/core +/tests/components/rss_feed_template/ @home-assistant/core +/homeassistant/components/rtsp_to_webrtc/ @allenporter +/tests/components/rtsp_to_webrtc/ @allenporter +/homeassistant/components/ruckus_unleashed/ @gabe565 +/tests/components/ruckus_unleashed/ @gabe565 +/homeassistant/components/safe_mode/ @home-assistant/core +/tests/components/safe_mode/ @home-assistant/core +/homeassistant/components/saj/ @fredericvl +/homeassistant/components/samsungtv/ @chemelli74 @epenet +/tests/components/samsungtv/ @chemelli74 @epenet +/homeassistant/components/scene/ @home-assistant/core +/tests/components/scene/ @home-assistant/core +/homeassistant/components/schluter/ @prairieapps +/homeassistant/components/scrape/ @fabaff +/tests/components/scrape/ @fabaff +/homeassistant/components/screenlogic/ @dieselrabbit @bdraco +/tests/components/screenlogic/ @dieselrabbit @bdraco +/homeassistant/components/script/ @home-assistant/core +/tests/components/script/ @home-assistant/core +/homeassistant/components/search/ @home-assistant/core +/tests/components/search/ @home-assistant/core +/homeassistant/components/season/ @frenck +/tests/components/season/ @frenck +/homeassistant/components/select/ @home-assistant/core +/tests/components/select/ @home-assistant/core +/homeassistant/components/sense/ @kbickar +/tests/components/sense/ @kbickar +/homeassistant/components/senseme/ @mikelawrence @bdraco +/tests/components/senseme/ @mikelawrence @bdraco +/homeassistant/components/sensibo/ @andrey-git @gjohansson-ST +/tests/components/sensibo/ @andrey-git @gjohansson-ST +/homeassistant/components/sensor/ @home-assistant/core +/tests/components/sensor/ @home-assistant/core +/homeassistant/components/sentry/ @dcramer @frenck +/tests/components/sentry/ @dcramer @frenck +/homeassistant/components/serial/ @fabaff +/homeassistant/components/seven_segments/ @fabaff +/homeassistant/components/sharkiq/ @JeffResc @funkybunch @AritroSaha10 +/tests/components/sharkiq/ @JeffResc @funkybunch @AritroSaha10 +/homeassistant/components/shell_command/ @home-assistant/core +/tests/components/shell_command/ @home-assistant/core +/homeassistant/components/shelly/ @balloob @bieniu @thecode @chemelli74 +/tests/components/shelly/ @balloob @bieniu @thecode @chemelli74 +/homeassistant/components/shiftr/ @fabaff +/homeassistant/components/shodan/ @fabaff +/homeassistant/components/sia/ @eavanvalkenburg +/tests/components/sia/ @eavanvalkenburg +/homeassistant/components/sighthound/ @robmarkcole +/tests/components/sighthound/ @robmarkcole +/homeassistant/components/signal_messenger/ @bbernhard +/tests/components/signal_messenger/ @bbernhard +/homeassistant/components/simplisafe/ @bachya +/tests/components/simplisafe/ @bachya +/homeassistant/components/sinch/ @bendikrb +/homeassistant/components/siren/ @home-assistant/core @raman325 +/tests/components/siren/ @home-assistant/core @raman325 +/homeassistant/components/sisyphus/ @jkeljo +/homeassistant/components/sky_hub/ @rogerselwyn +/homeassistant/components/slack/ @bachya +/tests/components/slack/ @bachya +/homeassistant/components/sleepiq/ @mfugate1 @kbickar +/tests/components/sleepiq/ @mfugate1 @kbickar +/homeassistant/components/slide/ @ualex73 +/homeassistant/components/sma/ @kellerza @rklomp +/tests/components/sma/ @kellerza @rklomp +/homeassistant/components/smappee/ @bsmappee +/tests/components/smappee/ @bsmappee +/homeassistant/components/smart_meter_texas/ @grahamwetzler +/tests/components/smart_meter_texas/ @grahamwetzler +/homeassistant/components/smartthings/ @andrewsayre +/tests/components/smartthings/ @andrewsayre +/homeassistant/components/smarttub/ @mdz +/tests/components/smarttub/ @mdz +/homeassistant/components/smarty/ @z0mbieprocess +/homeassistant/components/smhi/ @gjohansson-ST +/tests/components/smhi/ @gjohansson-ST +/homeassistant/components/sms/ @ocalvo +/homeassistant/components/smtp/ @fabaff +/tests/components/smtp/ @fabaff +/homeassistant/components/solaredge/ @frenck +/tests/components/solaredge/ @frenck +/homeassistant/components/solaredge_local/ @drobtravels @scheric +/homeassistant/components/solarlog/ @Ernst79 +/tests/components/solarlog/ @Ernst79 +/homeassistant/components/solax/ @squishykid +/tests/components/solax/ @squishykid +/homeassistant/components/soma/ @ratsept @sebfortier2288 +/tests/components/soma/ @ratsept @sebfortier2288 +/homeassistant/components/somfy/ @tetienne +/tests/components/somfy/ @tetienne +/homeassistant/components/sonarr/ @ctalkington +/tests/components/sonarr/ @ctalkington +/homeassistant/components/songpal/ @rytilahti @shenxn +/tests/components/songpal/ @rytilahti @shenxn +/homeassistant/components/sonos/ @cgtobi @jjlawren +/tests/components/sonos/ @cgtobi @jjlawren +/homeassistant/components/spaceapi/ @fabaff +/tests/components/spaceapi/ @fabaff +/homeassistant/components/speedtestdotnet/ @rohankapoorcom @engrbm87 +/tests/components/speedtestdotnet/ @rohankapoorcom @engrbm87 +/homeassistant/components/spider/ @peternijssen +/tests/components/spider/ @peternijssen +/homeassistant/components/splunk/ @Bre77 +/homeassistant/components/spotify/ @frenck +/tests/components/spotify/ @frenck +/homeassistant/components/sql/ @dgomes +/tests/components/sql/ @dgomes +/homeassistant/components/squeezebox/ @rajlaud +/tests/components/squeezebox/ @rajlaud +/homeassistant/components/srp_energy/ @briglx +/tests/components/srp_energy/ @briglx +/homeassistant/components/starline/ @anonym-tsk +/tests/components/starline/ @anonym-tsk +/homeassistant/components/statistics/ @fabaff @ThomDietrich +/tests/components/statistics/ @fabaff @ThomDietrich +/homeassistant/components/steamist/ @bdraco +/tests/components/steamist/ @bdraco +/homeassistant/components/stiebel_eltron/ @fucm +/homeassistant/components/stookalert/ @fwestenberg @frenck +/tests/components/stookalert/ @fwestenberg @frenck +/homeassistant/components/stream/ @hunterjm @uvjustin @allenporter +/tests/components/stream/ @hunterjm @uvjustin @allenporter +/homeassistant/components/stt/ @pvizeli +/tests/components/stt/ @pvizeli +/homeassistant/components/subaru/ @G-Two +/tests/components/subaru/ @G-Two +/homeassistant/components/suez_water/ @ooii +/homeassistant/components/sun/ @Swamp-Ig +/tests/components/sun/ @Swamp-Ig +/homeassistant/components/supla/ @mwegrzynek +/homeassistant/components/surepetcare/ @benleb @danielhiversen +/tests/components/surepetcare/ @benleb @danielhiversen +/homeassistant/components/swiss_hydrological_data/ @fabaff +/homeassistant/components/swiss_public_transport/ @fabaff +/homeassistant/components/switch/ @home-assistant/core +/tests/components/switch/ @home-assistant/core +/homeassistant/components/switch_as_x/ @home-assistant/core +/tests/components/switch_as_x/ @home-assistant/core +/homeassistant/components/switchbot/ @danielhiversen @RenierM26 +/tests/components/switchbot/ @danielhiversen @RenierM26 +/homeassistant/components/switcher_kis/ @tomerfi @thecode +/tests/components/switcher_kis/ @tomerfi @thecode +/homeassistant/components/switchmate/ @danielhiversen +/homeassistant/components/syncthing/ @zhulik +/tests/components/syncthing/ @zhulik +/homeassistant/components/syncthru/ @nielstron +/tests/components/syncthru/ @nielstron +/homeassistant/components/synology_dsm/ @hacf-fr @Quentame @mib1185 +/tests/components/synology_dsm/ @hacf-fr @Quentame @mib1185 +/homeassistant/components/synology_srm/ @aerialls +/homeassistant/components/syslog/ @fabaff +/homeassistant/components/system_bridge/ @timmo001 +/tests/components/system_bridge/ @timmo001 +/homeassistant/components/tado/ @michaelarnauts @north3221 +/tests/components/tado/ @michaelarnauts @north3221 +/homeassistant/components/tag/ @balloob @dmulcahey +/tests/components/tag/ @balloob @dmulcahey +/homeassistant/components/tailscale/ @frenck +/tests/components/tailscale/ @frenck +/homeassistant/components/tankerkoenig/ @guillempages @mib1185 +/tests/components/tankerkoenig/ @guillempages @mib1185 +/homeassistant/components/tapsaff/ @bazwilliams +/homeassistant/components/tasmota/ @emontnemery +/tests/components/tasmota/ @emontnemery +/homeassistant/components/tautulli/ @ludeeus +/homeassistant/components/tellduslive/ @fredrike +/tests/components/tellduslive/ @fredrike +/homeassistant/components/template/ @PhracturedBlue @tetienne @home-assistant/core +/tests/components/template/ @PhracturedBlue @tetienne @home-assistant/core +/homeassistant/components/tesla_wall_connector/ @einarhauks +/tests/components/tesla_wall_connector/ @einarhauks +/homeassistant/components/tfiac/ @fredrike @mellado +/homeassistant/components/thethingsnetwork/ @fabaff +/homeassistant/components/threshold/ @fabaff +/tests/components/threshold/ @fabaff +/homeassistant/components/tibber/ @danielhiversen +/tests/components/tibber/ @danielhiversen +/homeassistant/components/tile/ @bachya +/tests/components/tile/ @bachya +/homeassistant/components/time_date/ @fabaff +/tests/components/time_date/ @fabaff +/homeassistant/components/tmb/ @alemuro +/homeassistant/components/todoist/ @boralyl +/tests/components/todoist/ @boralyl +/homeassistant/components/tolo/ @MatthiasLohr +/tests/components/tolo/ @MatthiasLohr +/homeassistant/components/tomorrowio/ @raman325 +/tests/components/tomorrowio/ @raman325 +/homeassistant/components/totalconnect/ @austinmroczek +/tests/components/totalconnect/ @austinmroczek +/homeassistant/components/tplink/ @rytilahti @thegardenmonkey +/tests/components/tplink/ @rytilahti @thegardenmonkey +/homeassistant/components/traccar/ @ludeeus +/tests/components/traccar/ @ludeeus +/homeassistant/components/trace/ @home-assistant/core +/tests/components/trace/ @home-assistant/core +/homeassistant/components/tractive/ @Danielhiversen @zhulik @bieniu +/tests/components/tractive/ @Danielhiversen @zhulik @bieniu +/homeassistant/components/trafikverket_train/ @endor-force @gjohansson-ST +/tests/components/trafikverket_train/ @endor-force @gjohansson-ST +/homeassistant/components/trafikverket_weatherstation/ @endor-force @gjohansson-ST +/tests/components/trafikverket_weatherstation/ @endor-force @gjohansson-ST +/homeassistant/components/transmission/ @engrbm87 @JPHutchins +/tests/components/transmission/ @engrbm87 @JPHutchins +/homeassistant/components/tts/ @pvizeli +/tests/components/tts/ @pvizeli +/homeassistant/components/tuya/ @Tuya @zlinoliver @METISU @frenck +/tests/components/tuya/ @Tuya @zlinoliver @METISU @frenck +/homeassistant/components/twentemilieu/ @frenck +/tests/components/twentemilieu/ @frenck +/homeassistant/components/twinkly/ @dr1rrb @Robbie1221 +/tests/components/twinkly/ @dr1rrb @Robbie1221 +/homeassistant/components/unifi/ @Kane610 +/tests/components/unifi/ @Kane610 +/homeassistant/components/unifiled/ @florisvdk +/homeassistant/components/unifiprotect/ @briis @AngellusMortis @bdraco +/tests/components/unifiprotect/ @briis @AngellusMortis @bdraco +/homeassistant/components/upb/ @gwww +/tests/components/upb/ @gwww +/homeassistant/components/upc_connect/ @pvizeli @fabaff +/homeassistant/components/upcloud/ @scop +/tests/components/upcloud/ @scop +/homeassistant/components/update/ @home-assistant/core +/tests/components/update/ @home-assistant/core +/homeassistant/components/updater/ @home-assistant/core +/tests/components/updater/ @home-assistant/core +/homeassistant/components/upnp/ @StevenLooman @ehendrix23 +/tests/components/upnp/ @StevenLooman @ehendrix23 +/homeassistant/components/uptime/ @frenck +/tests/components/uptime/ @frenck +/homeassistant/components/uptimerobot/ @ludeeus @chemelli74 +/tests/components/uptimerobot/ @ludeeus @chemelli74 +/homeassistant/components/usb/ @bdraco +/tests/components/usb/ @bdraco +/homeassistant/components/usgs_earthquakes_feed/ @exxamalte +/tests/components/usgs_earthquakes_feed/ @exxamalte +/homeassistant/components/utility_meter/ @dgomes +/tests/components/utility_meter/ @dgomes +/homeassistant/components/vacuum/ @home-assistant/core +/tests/components/vacuum/ @home-assistant/core +/homeassistant/components/vallox/ @andre-richter @slovdahl @viiru- +/tests/components/vallox/ @andre-richter @slovdahl @viiru- +/homeassistant/components/velbus/ @Cereal2nd @brefra +/tests/components/velbus/ @Cereal2nd @brefra +/homeassistant/components/velux/ @Julius2342 +/homeassistant/components/venstar/ @garbled1 +/tests/components/venstar/ @garbled1 +/homeassistant/components/vera/ @pavoni +/tests/components/vera/ @pavoni +/homeassistant/components/verisure/ @frenck +/tests/components/verisure/ @frenck +/homeassistant/components/versasense/ @flamm3blemuff1n +/homeassistant/components/version/ @fabaff @ludeeus +/tests/components/version/ @fabaff @ludeeus +/homeassistant/components/vesync/ @markperdue @webdjoe @thegardenmonkey +/tests/components/vesync/ @markperdue @webdjoe @thegardenmonkey +/homeassistant/components/vicare/ @oischinger +/tests/components/vicare/ @oischinger +/homeassistant/components/vilfo/ @ManneW +/tests/components/vilfo/ @ManneW +/homeassistant/components/vivotek/ @HarlemSquirrel +/homeassistant/components/vizio/ @raman325 +/tests/components/vizio/ @raman325 +/homeassistant/components/vlc_telnet/ @rodripf @MartinHjelmare +/tests/components/vlc_telnet/ @rodripf @MartinHjelmare +/homeassistant/components/volkszaehler/ @fabaff +/homeassistant/components/volumio/ @OnFreund +/tests/components/volumio/ @OnFreund +/homeassistant/components/volvooncall/ @molobrakos @decompil3d +/homeassistant/components/vulcan/ @Antoni-Czaplicki +/tests/components/vulcan/ @Antoni-Czaplicki +/homeassistant/components/wake_on_lan/ @ntilley905 +/tests/components/wake_on_lan/ @ntilley905 +/homeassistant/components/wallbox/ @hesselonline +/tests/components/wallbox/ @hesselonline +/homeassistant/components/waqi/ @andrey-git +/homeassistant/components/water_heater/ @home-assistant/core +/tests/components/water_heater/ @home-assistant/core +/homeassistant/components/watson_tts/ @rutkai +/homeassistant/components/watttime/ @bachya +/tests/components/watttime/ @bachya +/homeassistant/components/waze_travel_time/ @eifinger +/tests/components/waze_travel_time/ @eifinger +/homeassistant/components/weather/ @fabaff +/tests/components/weather/ @fabaff +/homeassistant/components/webhook/ @home-assistant/core +/tests/components/webhook/ @home-assistant/core +/homeassistant/components/webostv/ @bendavid @thecode +/tests/components/webostv/ @bendavid @thecode +/homeassistant/components/websocket_api/ @home-assistant/core +/tests/components/websocket_api/ @home-assistant/core +/homeassistant/components/wemo/ @esev +/tests/components/wemo/ @esev +/homeassistant/components/whirlpool/ @abmantis +/tests/components/whirlpool/ @abmantis +/homeassistant/components/whois/ @frenck +/tests/components/whois/ @frenck +/homeassistant/components/wiffi/ @mampfes +/tests/components/wiffi/ @mampfes +/homeassistant/components/wilight/ @leofig-rj +/tests/components/wilight/ @leofig-rj +/homeassistant/components/wirelesstag/ @sergeymaysak +/homeassistant/components/withings/ @vangorra +/tests/components/withings/ @vangorra +/homeassistant/components/wiz/ @sbidy +/tests/components/wiz/ @sbidy +/homeassistant/components/wled/ @frenck +/tests/components/wled/ @frenck +/homeassistant/components/wolflink/ @adamkrol93 +/tests/components/wolflink/ @adamkrol93 +/homeassistant/components/workday/ @fabaff +/tests/components/workday/ @fabaff +/homeassistant/components/worldclock/ @fabaff +/tests/components/worldclock/ @fabaff +/homeassistant/components/xbox/ @hunterjm +/tests/components/xbox/ @hunterjm +/homeassistant/components/xbox_live/ @MartinHjelmare +/homeassistant/components/xiaomi_aqara/ @danielhiversen @syssi +/tests/components/xiaomi_aqara/ @danielhiversen @syssi +/homeassistant/components/xiaomi_miio/ @rytilahti @syssi @starkillerOG @bieniu +/tests/components/xiaomi_miio/ @rytilahti @syssi @starkillerOG @bieniu +/homeassistant/components/xiaomi_tv/ @simse +/homeassistant/components/xmpp/ @fabaff @flowolf +/homeassistant/components/yale_smart_alarm/ @gjohansson-ST +/tests/components/yale_smart_alarm/ @gjohansson-ST +/homeassistant/components/yamaha_musiccast/ @vigonotion @micha91 +/tests/components/yamaha_musiccast/ @vigonotion @micha91 +/homeassistant/components/yandex_transport/ @rishatik92 @devbis +/tests/components/yandex_transport/ @rishatik92 @devbis +/homeassistant/components/yeelight/ @zewelor @shenxn @starkillerOG @alexyao2015 +/tests/components/yeelight/ @zewelor @shenxn @starkillerOG @alexyao2015 +/homeassistant/components/yeelightsunflower/ @lindsaymarkward +/homeassistant/components/yi/ @bachya +/homeassistant/components/youless/ @gjong +/tests/components/youless/ @gjong +/homeassistant/components/zengge/ @emontnemery +/homeassistant/components/zeroconf/ @bdraco +/tests/components/zeroconf/ @bdraco +/homeassistant/components/zerproc/ @emlove +/tests/components/zerproc/ @emlove +/homeassistant/components/zha/ @dmulcahey @adminiuga +/tests/components/zha/ @dmulcahey @adminiuga +/homeassistant/components/zodiac/ @JulienTant +/tests/components/zodiac/ @JulienTant +/homeassistant/components/zone/ @home-assistant/core +/tests/components/zone/ @home-assistant/core +/homeassistant/components/zoneminder/ @rohankapoorcom +/homeassistant/components/zwave_js/ @home-assistant/z-wave +/tests/components/zwave_js/ @home-assistant/z-wave +/homeassistant/components/zwave_me/ @lawfulchaos @Z-Wave-Me +/tests/components/zwave_me/ @lawfulchaos @Z-Wave-Me # Individual files -homeassistant/components/demo/weather @fabaff +/homeassistant/components/demo/weather.py @fabaff + +# Remove codeowners from files +/homeassistant/components/*/translations/ diff --git a/Dockerfile b/Dockerfile index 7193d706b89..1d6ce675e74 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,8 +15,7 @@ RUN \ -r homeassistant/requirements.txt --use-deprecated=legacy-resolver COPY requirements_all.txt homeassistant/ RUN \ - sed -i "s|# homeassistant-pyozw|homeassistant-pyozw|g" homeassistant/requirements_all.txt \ - && pip3 install --no-cache-dir --no-index --only-binary=:all: --find-links "${WHEELS_LINKS}" \ + pip3 install --no-cache-dir --no-index --only-binary=:all: --find-links "${WHEELS_LINKS}" \ -r homeassistant/requirements_all.txt --use-deprecated=legacy-resolver ## Setup Home Assistant Core diff --git a/homeassistant/backports/enum.py b/homeassistant/backports/enum.py index 21302fe9f7b..9a96704a836 100644 --- a/homeassistant/backports/enum.py +++ b/homeassistant/backports/enum.py @@ -4,13 +4,15 @@ from __future__ import annotations from enum import Enum from typing import Any, TypeVar -T = TypeVar("T", bound="StrEnum") +_StrEnumT = TypeVar("_StrEnumT", bound="StrEnum") class StrEnum(str, Enum): """Partial backport of Python 3.11's StrEnum for our basic use cases.""" - def __new__(cls: type[T], value: str, *args: Any, **kwargs: Any) -> T: + def __new__( + cls: type[_StrEnumT], value: str, *args: Any, **kwargs: Any + ) -> _StrEnumT: """Create a new StrEnum instance.""" if not isinstance(value, str): raise TypeError(f"{value!r} is not a string") @@ -21,7 +23,7 @@ class StrEnum(str, Enum): return str(self.value) @staticmethod - def _generate_next_value_( # pylint: disable=arguments-differ # https://github.com/PyCQA/pylint/issues/5371 + def _generate_next_value_( name: str, start: int, count: int, last_values: list[Any] ) -> Any: """ diff --git a/homeassistant/components/abode/strings.json b/homeassistant/components/abode/strings.json index 14a60f827c3..b974007707e 100644 --- a/homeassistant/components/abode/strings.json +++ b/homeassistant/components/abode/strings.json @@ -26,11 +26,10 @@ "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/components/abode/translations/fr.json b/homeassistant/components/abode/translations/fr.json index fb5a079d405..a56c9732c6d 100644 --- a/homeassistant/components/abode/translations/fr.json +++ b/homeassistant/components/abode/translations/fr.json @@ -6,7 +6,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "invalid_mfa_code": "Code MFA non valide" }, "step": { @@ -19,14 +19,14 @@ "reauth_confirm": { "data": { "password": "Mot de passe", - "username": "Email" + "username": "Courriel" }, "title": "Remplissez vos informations de connexion Abode" }, "user": { "data": { "password": "Mot de passe", - "username": "Email" + "username": "Courriel" }, "title": "Remplissez vos informations de connexion Abode" } diff --git a/homeassistant/components/accuweather/diagnostics.py b/homeassistant/components/accuweather/diagnostics.py new file mode 100644 index 00000000000..e4305eb747a --- /dev/null +++ b/homeassistant/components/accuweather/diagnostics.py @@ -0,0 +1,28 @@ +"""Diagnostics support for AccuWeather.""" +from __future__ import annotations + +from homeassistant.components.diagnostics import async_redact_data +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE +from homeassistant.core import HomeAssistant + +from . import AccuWeatherDataUpdateCoordinator +from .const import DOMAIN + +TO_REDACT = {CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE} + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, config_entry: ConfigEntry +) -> dict: + """Return diagnostics for a config entry.""" + coordinator: AccuWeatherDataUpdateCoordinator = hass.data[DOMAIN][ + config_entry.entry_id + ] + + diagnostics_data = { + "config_entry_data": async_redact_data(dict(config_entry.data), TO_REDACT), + "coordinator_data": coordinator.data, + } + + return diagnostics_data diff --git a/homeassistant/components/accuweather/sensor.py b/homeassistant/components/accuweather/sensor.py index 448c00eb53f..220575541ad 100644 --- a/homeassistant/components/accuweather/sensor.py +++ b/homeassistant/components/accuweather/sensor.py @@ -58,11 +58,12 @@ async def async_setup_entry( async_add_entities(sensors) -class AccuWeatherSensor(CoordinatorEntity, SensorEntity): +class AccuWeatherSensor( + CoordinatorEntity[AccuWeatherDataUpdateCoordinator], SensorEntity +): """Define an AccuWeather entity.""" _attr_attribution = ATTRIBUTION - coordinator: AccuWeatherDataUpdateCoordinator entity_description: AccuWeatherSensorDescription def __init__( diff --git a/homeassistant/components/accuweather/strings.json b/homeassistant/components/accuweather/strings.json index c4305a0a7a5..432cc095c7b 100644 --- a/homeassistant/components/accuweather/strings.json +++ b/homeassistant/components/accuweather/strings.json @@ -2,8 +2,6 @@ "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%]", @@ -12,6 +10,9 @@ } } }, + "create_entry": { + "default": "Some 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." + }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]", @@ -24,7 +25,6 @@ "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" diff --git a/homeassistant/components/accuweather/translations/es.json b/homeassistant/components/accuweather/translations/es.json index aa24b5ff975..9d0396fddb3 100644 --- a/homeassistant/components/accuweather/translations/es.json +++ b/homeassistant/components/accuweather/translations/es.json @@ -16,7 +16,7 @@ "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.", + "description": "Si necesitas ayuda con la configuraci\u00f3n, echa un vistazo aqu\u00ed: https://www.home-assistant.io/integrations/accuweather/ \n\nAlgunos sensores no est\u00e1n habilitados por defecto. Los puedes habilitar en el registro de entidades despu\u00e9s de configurar la integraci\u00f3n.\nEl pron\u00f3stico del tiempo no est\u00e1 habilitado por defecto. Puedes habilitarlo en las opciones de la integraci\u00f3n.", "title": "AccuWeather" } } diff --git a/homeassistant/components/accuweather/translations/fr.json b/homeassistant/components/accuweather/translations/fr.json index 7c04e51da23..1de45f8da22 100644 --- a/homeassistant/components/accuweather/translations/fr.json +++ b/homeassistant/components/accuweather/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_api_key": "Cl\u00e9 API invalide", + "invalid_api_key": "Cl\u00e9 d'API non valide", "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": { diff --git a/homeassistant/components/accuweather/weather.py b/homeassistant/components/accuweather/weather.py index 4ab9342de62..536f66a3cb9 100644 --- a/homeassistant/components/accuweather/weather.py +++ b/homeassistant/components/accuweather/weather.py @@ -56,11 +56,11 @@ async def async_setup_entry( async_add_entities([AccuWeatherEntity(name, coordinator)]) -class AccuWeatherEntity(CoordinatorEntity, WeatherEntity): +class AccuWeatherEntity( + CoordinatorEntity[AccuWeatherDataUpdateCoordinator], WeatherEntity +): """Define an AccuWeather entity.""" - coordinator: AccuWeatherDataUpdateCoordinator - def __init__( self, name: str, coordinator: AccuWeatherDataUpdateCoordinator ) -> None: diff --git a/homeassistant/components/adax/manifest.json b/homeassistant/components/adax/manifest.json index 73ab948ecb4..b5c19269f09 100644 --- a/homeassistant/components/adax/manifest.json +++ b/homeassistant/components/adax/manifest.json @@ -3,12 +3,8 @@ "name": "Adax", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/adax", - "requirements": [ - "adax==0.2.0", "Adax-local==0.1.3" - ], - "codeowners": [ - "@danielhiversen" - ], + "requirements": ["adax==0.2.0", "Adax-local==0.1.3"], + "codeowners": ["@danielhiversen"], "iot_class": "local_polling", "loggers": ["adax", "adax_local"] } diff --git a/homeassistant/components/adax/translations/fr.json b/homeassistant/components/adax/translations/fr.json index eefd0693e24..edcdcdd2738 100644 --- a/homeassistant/components/adax/translations/fr.json +++ b/homeassistant/components/adax/translations/fr.json @@ -4,23 +4,23 @@ "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", "heater_not_available": "Chauffage non disponible. Essayez de r\u00e9initialiser le chauffage en appuyant sur + et OK pendant quelques secondes.", "heater_not_found": "Chauffage introuvable. Essayez de rapprocher le radiateur de l'ordinateur Home Assistant.", - "invalid_auth": "Authentification invalide" + "invalid_auth": "Authentification non valide" }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide" + "invalid_auth": "Authentification non valide" }, "step": { "cloud": { "data": { "account_id": "Identifiant de compte", - "password": "Mot der passe" + "password": "Mot de passe" } }, "local": { "data": { - "wifi_pswd": "Mot de passe WiFi", - "wifi_ssid": "identifiant Wifi" + "wifi_pswd": "Mot de passe Wi-Fi", + "wifi_ssid": "R\u00e9seau Wi-Fi" }, "description": "R\u00e9initialisez le radiateur en appuyant sur + et OK jusqu'\u00e0 ce que l'\u00e9cran affiche \u00ab\u00a0Reset\u00a0\u00bb. Appuyez ensuite sur le bouton OK du radiateur et maintenez-le enfonc\u00e9 jusqu'\u00e0 ce que le voyant bleu commence \u00e0 clignoter avant d'appuyer sur Soumettre. La configuration du chauffage peut prendre quelques minutes." }, diff --git a/homeassistant/components/adax/translations/zh-Hant.json b/homeassistant/components/adax/translations/zh-Hant.json index 0ad4bcba854..92018be07da 100644 --- a/homeassistant/components/adax/translations/zh-Hant.json +++ b/homeassistant/components/adax/translations/zh-Hant.json @@ -27,11 +27,11 @@ "user": { "data": { "account_id": "\u5e33\u865f ID", - "connection_type": "\u9078\u64c7\u9023\u7dda\u985e\u578b", + "connection_type": "\u9078\u64c7\u9023\u7dda\u985e\u5225", "host": "\u4e3b\u6a5f\u7aef", "password": "\u5bc6\u78bc" }, - "description": "\u9078\u64c7\u9023\u7dda\u985e\u578b\u3002\u672c\u5730\u7aef\u5c07\u9700\u8981\u5177\u5099\u85cd\u82bd\u52a0\u71b1\u5668" + "description": "\u9078\u64c7\u9023\u7dda\u985e\u5225\u3002\u672c\u5730\u7aef\u5c07\u9700\u8981\u5177\u5099\u85cd\u82bd\u52a0\u71b1\u5668" } } } diff --git a/homeassistant/components/adguard/config_flow.py b/homeassistant/components/adguard/config_flow.py index 9afdc4e02b8..bc9b11c9a72 100644 --- a/homeassistant/components/adguard/config_flow.py +++ b/homeassistant/components/adguard/config_flow.py @@ -27,7 +27,7 @@ class AdGuardHomeFlowHandler(ConfigFlow, domain=DOMAIN): VERSION = 1 - _hassio_discovery = None + _hassio_discovery: dict[str, Any] | None = None async def _show_setup_form( self, errors: dict[str, str] | None = None @@ -56,7 +56,6 @@ class AdGuardHomeFlowHandler(ConfigFlow, domain=DOMAIN): return self.async_show_form( step_id="hassio_confirm", description_placeholders={"addon": self._hassio_discovery["addon"]}, - data_schema=vol.Schema({}), errors=errors or {}, ) diff --git a/homeassistant/components/adguard/switch.py b/homeassistant/components/adguard/switch.py index f6c41e7f2e8..0cb7a48fee6 100644 --- a/homeassistant/components/adguard/switch.py +++ b/homeassistant/components/adguard/switch.py @@ -3,6 +3,7 @@ from __future__ import annotations from datetime import timedelta import logging +from typing import Any from adguardhome import AdGuardHome, AdGuardHomeConnectionError, AdGuardHomeError @@ -76,7 +77,7 @@ class AdGuardHomeSwitch(AdGuardHomeDeviceEntity, SwitchEntity): """Return the state of the switch.""" return self._state - async def async_turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs: Any) -> None: """Turn off the switch.""" try: await self._adguard_turn_off() @@ -88,7 +89,7 @@ class AdGuardHomeSwitch(AdGuardHomeDeviceEntity, SwitchEntity): """Turn off the switch.""" raise NotImplementedError() - async def async_turn_on(self, **kwargs) -> None: + async def async_turn_on(self, **kwargs: Any) -> None: """Turn on the switch.""" try: await self._adguard_turn_on() diff --git a/homeassistant/components/ads/services.yaml b/homeassistant/components/ads/services.yaml index f6458029fb4..53c514bb587 100644 --- a/homeassistant/components/ads/services.yaml +++ b/homeassistant/components/ads/services.yaml @@ -18,12 +18,12 @@ write_data_by_name: selector: select: options: - - 'bool' - - 'byte' - - 'dint' - - 'int' - - 'udint' - - 'uint' + - "bool" + - "byte" + - "dint" + - "int" + - "udint" + - "uint" value: name: Value description: The value to write to the variable. diff --git a/homeassistant/components/advantage_air/manifest.json b/homeassistant/components/advantage_air/manifest.json index 6f1d811c291..f95a32e186b 100644 --- a/homeassistant/components/advantage_air/manifest.json +++ b/homeassistant/components/advantage_air/manifest.json @@ -3,15 +3,9 @@ "name": "Advantage Air", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/advantage_air", - "codeowners": [ - "@Bre77" - ], - "requirements": [ - "advantage_air==0.3.1" - ], + "codeowners": ["@Bre77"], + "requirements": ["advantage_air==0.3.1"], "quality_scale": "platinum", "iot_class": "local_polling", - "loggers": [ - "advantage_air" - ] -} \ No newline at end of file + "loggers": ["advantage_air"] +} diff --git a/homeassistant/components/aemet/sensor.py b/homeassistant/components/aemet/sensor.py index f22c6d321b1..f98e3fff49e 100644 --- a/homeassistant/components/aemet/sensor.py +++ b/homeassistant/components/aemet/sensor.py @@ -64,7 +64,7 @@ async def async_setup_entry( async_add_entities(entities) -class AbstractAemetSensor(CoordinatorEntity, SensorEntity): +class AbstractAemetSensor(CoordinatorEntity[WeatherUpdateCoordinator], SensorEntity): """Abstract class for an AEMET OpenData sensor.""" _attr_extra_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION} diff --git a/homeassistant/components/aemet/strings.json b/homeassistant/components/aemet/strings.json index 360f7c680ea..75c810978ad 100644 --- a/homeassistant/components/aemet/strings.json +++ b/homeassistant/components/aemet/strings.json @@ -1,31 +1,30 @@ { - "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" - } - } + "config": { + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_location%]" }, - "options": { - "step": { - "init": { - "data": { - "station_updates": "Gather data from AEMET weather stations" - } - } - } + "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": "To generate API key go to https://opendata.aemet.es/centrodedescargas/altaUsuario" + } } + }, + "options": { + "step": { + "init": { + "data": { + "station_updates": "Gather data from AEMET weather stations" + } + } + } + } } diff --git a/homeassistant/components/aemet/translations/fr.json b/homeassistant/components/aemet/translations/fr.json index 4ad76320f03..0d3d0e77a5e 100644 --- a/homeassistant/components/aemet/translations/fr.json +++ b/homeassistant/components/aemet/translations/fr.json @@ -4,7 +4,7 @@ "already_configured": "L'emplacement est d\u00e9j\u00e0 configur\u00e9" }, "error": { - "invalid_api_key": "Cl\u00e9 API invalide" + "invalid_api_key": "Cl\u00e9 d'API non valide" }, "step": { "user": { diff --git a/homeassistant/components/aemet/weather.py b/homeassistant/components/aemet/weather.py index cf23d6f3643..2bbb4d0da55 100644 --- a/homeassistant/components/aemet/weather.py +++ b/homeassistant/components/aemet/weather.py @@ -43,7 +43,7 @@ async def async_setup_entry( async_add_entities(entities, False) -class AemetWeather(CoordinatorEntity, WeatherEntity): +class AemetWeather(CoordinatorEntity[WeatherUpdateCoordinator], WeatherEntity): """Implementation of an AEMET OpenData sensor.""" _attr_attribution = ATTRIBUTION diff --git a/homeassistant/components/aftership/manifest.json b/homeassistant/components/aftership/manifest.json index 6b74c771b03..2a64abe5f2d 100644 --- a/homeassistant/components/aftership/manifest.json +++ b/homeassistant/components/aftership/manifest.json @@ -2,9 +2,7 @@ "domain": "aftership", "name": "AfterShip", "documentation": "https://www.home-assistant.io/integrations/aftership", - "requirements": [ - "pyaftership==21.11.0" - ], + "requirements": ["pyaftership==21.11.0"], "codeowners": [], "iot_class": "cloud_polling" } diff --git a/homeassistant/components/aftership/sensor.py b/homeassistant/components/aftership/sensor.py index 62d138d2123..d816afa3b17 100644 --- a/homeassistant/components/aftership/sensor.py +++ b/homeassistant/components/aftership/sensor.py @@ -15,7 +15,10 @@ from homeassistant.const import CONF_API_KEY, CONF_NAME from homeassistant.core import HomeAssistant, ServiceCall 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.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util import Throttle @@ -128,9 +131,7 @@ class AfterShipSensor(SensorEntity): 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_dispatcher_connect(self.hass, UPDATE_TOPIC, self._force_update) ) async def _force_update(self) -> None: diff --git a/homeassistant/components/air_quality/manifest.json b/homeassistant/components/air_quality/manifest.json index 55fbdbdafd1..978089d1816 100644 --- a/homeassistant/components/air_quality/manifest.json +++ b/homeassistant/components/air_quality/manifest.json @@ -2,6 +2,6 @@ "domain": "air_quality", "name": "Air Quality", "documentation": "https://www.home-assistant.io/integrations/air_quality", - "codeowners": [], + "codeowners": ["@home-assistant/core"], "quality_scale": "internal" } diff --git a/homeassistant/components/airly/sensor.py b/homeassistant/components/airly/sensor.py index 3dab90f5620..9b647b93afa 100644 --- a/homeassistant/components/airly/sensor.py +++ b/homeassistant/components/airly/sensor.py @@ -134,10 +134,9 @@ async def async_setup_entry( async_add_entities(sensors, False) -class AirlySensor(CoordinatorEntity, SensorEntity): +class AirlySensor(CoordinatorEntity[AirlyDataUpdateCoordinator], SensorEntity): """Define an Airly sensor.""" - coordinator: AirlyDataUpdateCoordinator entity_description: AirlySensorEntityDescription def __init__( diff --git a/homeassistant/components/airly/strings.json b/homeassistant/components/airly/strings.json index c6b6f1e6a41..77f965b64c2 100644 --- a/homeassistant/components/airly/strings.json +++ b/homeassistant/components/airly/strings.json @@ -2,8 +2,7 @@ "config": { "step": { "user": { - "title": "Airly", - "description": "Set up Airly air quality integration. To generate API key go to https://developer.airly.eu/register", + "description": "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%]", diff --git a/homeassistant/components/airly/translations/fr.json b/homeassistant/components/airly/translations/fr.json index 945de28e07a..85d1a3b478e 100644 --- a/homeassistant/components/airly/translations/fr.json +++ b/homeassistant/components/airly/translations/fr.json @@ -4,7 +4,7 @@ "already_configured": "L'emplacement est d\u00e9j\u00e0 configur\u00e9" }, "error": { - "invalid_api_key": "Cl\u00e9 API invalide", + "invalid_api_key": "Cl\u00e9 d'API non valide", "wrong_location": "Aucune station de mesure Airly dans cette zone." }, "step": { diff --git a/homeassistant/components/airnow/sensor.py b/homeassistant/components/airnow/sensor.py index fcd3cfd6f45..780e40ed2ba 100644 --- a/homeassistant/components/airnow/sensor.py +++ b/homeassistant/components/airnow/sensor.py @@ -70,11 +70,9 @@ async def async_setup_entry( async_add_entities(entities, False) -class AirNowSensor(CoordinatorEntity, SensorEntity): +class AirNowSensor(CoordinatorEntity[AirNowDataUpdateCoordinator], SensorEntity): """Define an AirNow sensor.""" - coordinator: AirNowDataUpdateCoordinator - def __init__( self, coordinator: AirNowDataUpdateCoordinator, diff --git a/homeassistant/components/airnow/strings.json b/homeassistant/components/airnow/strings.json index 9fc5bd3bccc..0e86c4531dc 100644 --- a/homeassistant/components/airnow/strings.json +++ b/homeassistant/components/airnow/strings.json @@ -2,8 +2,7 @@ "config": { "step": { "user": { - "title": "AirNow", - "description": "Set up AirNow air quality integration. To generate API key go to https://docs.airnowapi.org/account/request/", + "description": "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%]", diff --git a/homeassistant/components/airnow/translations/fr.json b/homeassistant/components/airnow/translations/fr.json index 686dedb9bb6..1cfe1652771 100644 --- a/homeassistant/components/airnow/translations/fr.json +++ b/homeassistant/components/airnow/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "invalid_location": "Aucun r\u00e9sultat trouv\u00e9 pour cet emplacement", "unknown": "Erreur inattendue" }, diff --git a/homeassistant/components/airthings/manifest.json b/homeassistant/components/airthings/manifest.json index f3aba33ce80..31f91ee4c72 100644 --- a/homeassistant/components/airthings/manifest.json +++ b/homeassistant/components/airthings/manifest.json @@ -4,9 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/airthings", "requirements": ["airthings_cloud==0.1.0"], - "codeowners": [ - "@danielhiversen" - ], + "codeowners": ["@danielhiversen"], "iot_class": "cloud_polling", "loggers": ["airthings"] -} \ No newline at end of file +} diff --git a/homeassistant/components/airthings/strings.json b/homeassistant/components/airthings/strings.json index 32f3fbc6954..af1200baa58 100644 --- a/homeassistant/components/airthings/strings.json +++ b/homeassistant/components/airthings/strings.json @@ -18,4 +18,4 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_account%]" } } -} \ No newline at end of file +} diff --git a/homeassistant/components/airthings/translations/fr.json b/homeassistant/components/airthings/translations/fr.json index 1ad84e8bd99..89f12cfce73 100644 --- a/homeassistant/components/airthings/translations/fr.json +++ b/homeassistant/components/airthings/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/airtouch4/manifest.json b/homeassistant/components/airtouch4/manifest.json index 3e15f62710d..47084f9ce0b 100644 --- a/homeassistant/components/airtouch4/manifest.json +++ b/homeassistant/components/airtouch4/manifest.json @@ -3,12 +3,8 @@ "name": "AirTouch 4", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/airtouch4", - "requirements": [ - "airtouch4pyapi==1.0.5" - ], - "codeowners": [ - "@LonePurpleWolf" - ], + "requirements": ["airtouch4pyapi==1.0.5"], + "codeowners": ["@LonePurpleWolf"], "iot_class": "local_polling", "loggers": ["airtouch4pyapi"] -} \ No newline at end of file +} diff --git a/homeassistant/components/airvisual/translations/fr.json b/homeassistant/components/airvisual/translations/fr.json index 4817a720225..642fe40cc12 100644 --- a/homeassistant/components/airvisual/translations/fr.json +++ b/homeassistant/components/airvisual/translations/fr.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "\u00c9chec de connexion", "general_error": "Erreur inattendue", - "invalid_api_key": "Cl\u00e9 API invalide", + "invalid_api_key": "Cl\u00e9 d'API non valide", "location_not_found": "Emplacement introuvable" }, "step": { @@ -25,7 +25,7 @@ "api_key": "Cl\u00e9 d'API", "city": "Ville", "country": "Pays", - "state": "Etat" + "state": "\u00c9tat" }, "description": "Utilisez l'API cloud AirVisual pour surveiller une ville / un \u00e9tat / un pays.", "title": "Configurer un lieu g\u00e9ographique" diff --git a/homeassistant/components/airvisual/translations/sensor.fr.json b/homeassistant/components/airvisual/translations/sensor.fr.json index 3050d6fb158..47feff6bd79 100644 --- a/homeassistant/components/airvisual/translations/sensor.fr.json +++ b/homeassistant/components/airvisual/translations/sensor.fr.json @@ -11,7 +11,7 @@ "airvisual__pollutant_level": { "good": "Bon", "hazardous": "Hasardeux", - "moderate": "Mod\u00e9rer", + "moderate": "Mod\u00e9r\u00e9", "unhealthy": "Malsain", "unhealthy_sensitive": "Malsain pour les groupes sensibles", "very_unhealthy": "Tr\u00e8s malsain" diff --git a/homeassistant/components/airvisual/translations/sensor.he.json b/homeassistant/components/airvisual/translations/sensor.he.json index 28ac8c5c3e4..7ed68fa47ca 100644 --- a/homeassistant/components/airvisual/translations/sensor.he.json +++ b/homeassistant/components/airvisual/translations/sensor.he.json @@ -2,7 +2,8 @@ "state": { "airvisual__pollutant_level": { "good": "\u05d8\u05d5\u05d1", - "unhealthy": "\u05dc\u05d0 \u05d1\u05e8\u05d9\u05d0" + "unhealthy": "\u05dc\u05d0 \u05d1\u05e8\u05d9\u05d0", + "unhealthy_sensitive": "\u05dc\u05d0 \u05d1\u05e8\u05d9\u05d0 \u05dc\u05e7\u05d1\u05d5\u05e6\u05d5\u05ea \u05e8\u05d2\u05d9\u05e9\u05d5\u05ea" } } } \ No newline at end of file diff --git a/homeassistant/components/airvisual/translations/zh-Hant.json b/homeassistant/components/airvisual/translations/zh-Hant.json index fed34b3346b..e8779af7f52 100644 --- a/homeassistant/components/airvisual/translations/zh-Hant.json +++ b/homeassistant/components/airvisual/translations/zh-Hant.json @@ -45,7 +45,7 @@ "title": "\u91cd\u65b0\u8a8d\u8b49 AirVisual" }, "user": { - "description": "\u9078\u64c7\u6240\u8981\u76e3\u63a7\u7684 AirVisual \u8cc7\u6599\u985e\u578b\u3002", + "description": "\u9078\u64c7\u6240\u8981\u76e3\u63a7\u7684 AirVisual \u8cc7\u6599\u985e\u5225\u3002", "title": "\u8a2d\u5b9a AirVisual" } } diff --git a/homeassistant/components/airzone/__init__.py b/homeassistant/components/airzone/__init__.py new file mode 100644 index 00000000000..6952ac2a42d --- /dev/null +++ b/homeassistant/components/airzone/__init__.py @@ -0,0 +1,89 @@ +"""The Airzone integration.""" +from __future__ import annotations + +from typing import Any + +from aioairzone.common import ConnectionOptions +from aioairzone.const import ( + AZD_ID, + AZD_NAME, + AZD_SYSTEM, + AZD_THERMOSTAT_FW, + AZD_THERMOSTAT_MODEL, + AZD_ZONES, +) +from aioairzone.localapi import AirzoneLocalApi + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST, CONF_PORT, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import aiohttp_client +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN, MANUFACTURER +from .coordinator import AirzoneUpdateCoordinator + +PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.CLIMATE, Platform.SENSOR] + + +class AirzoneEntity(CoordinatorEntity[AirzoneUpdateCoordinator]): + """Define an Airzone entity.""" + + def __init__( + self, + coordinator: AirzoneUpdateCoordinator, + entry: ConfigEntry, + system_zone_id: str, + zone_data: dict[str, Any], + ) -> None: + """Initialize.""" + super().__init__(coordinator) + + self.system_id = zone_data[AZD_SYSTEM] + self.system_zone_id = system_zone_id + self.zone_id = zone_data[AZD_ID] + + self._attr_device_info: DeviceInfo = { + "identifiers": {(DOMAIN, f"{entry.entry_id}_{system_zone_id}")}, + "manufacturer": MANUFACTURER, + "model": self.get_zone_value(AZD_THERMOSTAT_MODEL), + "name": f"Airzone [{system_zone_id}] {zone_data[AZD_NAME]}", + "sw_version": self.get_zone_value(AZD_THERMOSTAT_FW), + } + + def get_zone_value(self, key): + """Return zone value by key.""" + value = None + if self.system_zone_id in self.coordinator.data[AZD_ZONES]: + zone = self.coordinator.data[AZD_ZONES][self.system_zone_id] + if key in zone: + value = zone[key] + return value + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Airzone from a config entry.""" + options = ConnectionOptions( + entry.data[CONF_HOST], + entry.data[CONF_PORT], + ) + + airzone = AirzoneLocalApi(aiohttp_client.async_get_clientsession(hass), options) + + coordinator = AirzoneUpdateCoordinator(hass, airzone) + await coordinator.async_config_entry_first_refresh() + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator + + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/airzone/binary_sensor.py b/homeassistant/components/airzone/binary_sensor.py new file mode 100644 index 00000000000..1d0c76906e8 --- /dev/null +++ b/homeassistant/components/airzone/binary_sensor.py @@ -0,0 +1,114 @@ +"""Support for the Airzone sensors.""" +from __future__ import annotations + +from collections.abc import Mapping +from dataclasses import dataclass +from typing import Any, Final + +from aioairzone.const import ( + AZD_AIR_DEMAND, + AZD_ERRORS, + AZD_FLOOR_DEMAND, + AZD_NAME, + AZD_PROBLEMS, + AZD_ZONES, +) + +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_PROBLEM, + DEVICE_CLASS_RUNNING, + BinarySensorEntity, + BinarySensorEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import AirzoneEntity +from .const import DOMAIN +from .coordinator import AirzoneUpdateCoordinator + + +@dataclass +class AirzoneBinarySensorEntityDescription(BinarySensorEntityDescription): + """A class that describes airzone binary sensor entities.""" + + attributes: dict[str, str] | None = None + + +BINARY_SENSOR_TYPES: Final[tuple[AirzoneBinarySensorEntityDescription, ...]] = ( + AirzoneBinarySensorEntityDescription( + device_class=DEVICE_CLASS_RUNNING, + key=AZD_AIR_DEMAND, + name="Air Demand", + ), + AirzoneBinarySensorEntityDescription( + device_class=DEVICE_CLASS_RUNNING, + key=AZD_FLOOR_DEMAND, + name="Floor Demand", + ), + AirzoneBinarySensorEntityDescription( + attributes={ + "errors": AZD_ERRORS, + }, + device_class=DEVICE_CLASS_PROBLEM, + entity_category=EntityCategory.DIAGNOSTIC, + key=AZD_PROBLEMS, + name="Problem", + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Add Airzone binary sensors from a config_entry.""" + coordinator = hass.data[DOMAIN][entry.entry_id] + + binary_sensors = [] + for system_zone_id, zone_data in coordinator.data[AZD_ZONES].items(): + for description in BINARY_SENSOR_TYPES: + if description.key in zone_data: + binary_sensors.append( + AirzoneBinarySensor( + coordinator, + description, + entry, + system_zone_id, + zone_data, + ) + ) + + async_add_entities(binary_sensors) + + +class AirzoneBinarySensor(AirzoneEntity, BinarySensorEntity): + """Define an Airzone sensor.""" + + def __init__( + self, + coordinator: AirzoneUpdateCoordinator, + description: AirzoneBinarySensorEntityDescription, + entry: ConfigEntry, + system_zone_id: str, + zone_data: dict[str, Any], + ) -> None: + """Initialize.""" + super().__init__(coordinator, entry, system_zone_id, zone_data) + self._attr_name = f"{zone_data[AZD_NAME]} {description.name}" + self._attr_unique_id = f"{entry.entry_id}_{system_zone_id}_{description.key}" + self.attributes = description.attributes + self.entity_description = description + + @property + def extra_state_attributes(self) -> Mapping[str, Any] | None: + """Return state attributes.""" + if not self.attributes: + return None + return {key: self.get_zone_value(val) for key, val in self.attributes.items()} + + @property + def is_on(self) -> bool | None: + """Return true if the binary sensor is on.""" + return self.get_zone_value(self.entity_description.key) diff --git a/homeassistant/components/airzone/climate.py b/homeassistant/components/airzone/climate.py new file mode 100644 index 00000000000..f9a32e8e5ff --- /dev/null +++ b/homeassistant/components/airzone/climate.py @@ -0,0 +1,191 @@ +"""Support for the Airzone climate.""" +from __future__ import annotations + +import logging +from typing import Final + +from aioairzone.common import OperationMode +from aioairzone.const import ( + API_MODE, + API_ON, + API_SET_POINT, + API_SYSTEM_ID, + API_ZONE_ID, + AZD_DEMAND, + AZD_HUMIDITY, + AZD_MASTER, + AZD_MODE, + AZD_MODES, + AZD_NAME, + AZD_ON, + AZD_TEMP, + AZD_TEMP_MAX, + AZD_TEMP_MIN, + AZD_TEMP_SET, + AZD_TEMP_UNIT, + AZD_ZONES, +) +from aioairzone.exceptions import AirzoneError +from aiohttp.client_exceptions import ClientConnectorError + +from homeassistant.components.climate import ClimateEntity +from homeassistant.components.climate.const import ( + CURRENT_HVAC_COOL, + CURRENT_HVAC_DRY, + CURRENT_HVAC_FAN, + CURRENT_HVAC_HEAT, + CURRENT_HVAC_IDLE, + CURRENT_HVAC_OFF, + HVAC_MODE_COOL, + HVAC_MODE_DRY, + HVAC_MODE_FAN_ONLY, + HVAC_MODE_HEAT, + HVAC_MODE_HEAT_COOL, + HVAC_MODE_OFF, + SUPPORT_TARGET_TEMPERATURE, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_TEMPERATURE +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import AirzoneEntity +from .const import API_TEMPERATURE_STEP, DOMAIN, TEMP_UNIT_LIB_TO_HASS +from .coordinator import AirzoneUpdateCoordinator + +_LOGGER = logging.getLogger(__name__) + + +HVAC_ACTION_LIB_TO_HASS: Final[dict[OperationMode, str]] = { + OperationMode.STOP: CURRENT_HVAC_OFF, + OperationMode.COOLING: CURRENT_HVAC_COOL, + OperationMode.HEATING: CURRENT_HVAC_HEAT, + OperationMode.FAN: CURRENT_HVAC_FAN, + OperationMode.DRY: CURRENT_HVAC_DRY, +} +HVAC_MODE_LIB_TO_HASS: Final[dict[OperationMode, str]] = { + OperationMode.STOP: HVAC_MODE_OFF, + OperationMode.COOLING: HVAC_MODE_COOL, + OperationMode.HEATING: HVAC_MODE_HEAT, + OperationMode.FAN: HVAC_MODE_FAN_ONLY, + OperationMode.DRY: HVAC_MODE_DRY, + OperationMode.AUTO: HVAC_MODE_HEAT_COOL, +} +HVAC_MODE_HASS_TO_LIB: Final[dict[str, OperationMode]] = { + HVAC_MODE_OFF: OperationMode.STOP, + HVAC_MODE_COOL: OperationMode.COOLING, + HVAC_MODE_HEAT: OperationMode.HEATING, + HVAC_MODE_FAN_ONLY: OperationMode.FAN, + HVAC_MODE_DRY: OperationMode.DRY, + HVAC_MODE_HEAT_COOL: OperationMode.AUTO, +} + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Add Airzone sensors from a config_entry.""" + coordinator = hass.data[DOMAIN][entry.entry_id] + async_add_entities( + AirzoneClimate( + coordinator, + entry, + system_zone_id, + zone_data, + ) + for system_zone_id, zone_data in coordinator.data[AZD_ZONES].items() + ) + + +class AirzoneClimate(AirzoneEntity, ClimateEntity): + """Define an Airzone sensor.""" + + def __init__( + self, + coordinator: AirzoneUpdateCoordinator, + entry: ConfigEntry, + system_zone_id: str, + zone_data: dict, + ) -> None: + """Initialize Airzone climate entity.""" + super().__init__(coordinator, entry, system_zone_id, zone_data) + self._attr_name = f"{zone_data[AZD_NAME]}" + self._attr_unique_id = f"{entry.entry_id}_{system_zone_id}" + self._attr_supported_features = SUPPORT_TARGET_TEMPERATURE + self._attr_target_temperature_step = API_TEMPERATURE_STEP + self._attr_max_temp = self.get_zone_value(AZD_TEMP_MAX) + self._attr_min_temp = self.get_zone_value(AZD_TEMP_MIN) + self._attr_temperature_unit = TEMP_UNIT_LIB_TO_HASS[ + self.get_zone_value(AZD_TEMP_UNIT) + ] + self._attr_hvac_modes = [ + HVAC_MODE_LIB_TO_HASS[mode] for mode in self.get_zone_value(AZD_MODES) + ] + self._async_update_attrs() + + async def _async_update_hvac_params(self, params) -> None: + """Send HVAC parameters to API.""" + try: + await self.coordinator.airzone.put_hvac(params) + except (AirzoneError, ClientConnectorError) as error: + raise HomeAssistantError( + f"Failed to set zone {self.name}: {error}" + ) from error + else: + self.coordinator.async_set_updated_data(self.coordinator.airzone.data()) + + async def async_set_hvac_mode(self, hvac_mode: str) -> None: + """Set hvac mode.""" + params = { + API_SYSTEM_ID: self.system_id, + API_ZONE_ID: self.zone_id, + } + if hvac_mode == HVAC_MODE_OFF: + params[API_ON] = 0 + else: + mode = HVAC_MODE_HASS_TO_LIB[hvac_mode] + if mode != self.get_zone_value(AZD_MODE): + if self.get_zone_value(AZD_MASTER): + params[API_MODE] = mode + else: + raise HomeAssistantError( + f"Mode can't be changed on slave zone {self.name}" + ) + params[API_ON] = 1 + _LOGGER.debug("Set hvac_mode=%s params=%s", hvac_mode, params) + await self._async_update_hvac_params(params) + + async def async_set_temperature(self, **kwargs) -> None: + """Set new target temperature.""" + temp = kwargs.get(ATTR_TEMPERATURE) + params = { + API_SYSTEM_ID: self.system_id, + API_ZONE_ID: self.zone_id, + API_SET_POINT: temp, + } + _LOGGER.debug("Set temp=%s params=%s", temp, params) + await self._async_update_hvac_params(params) + + @callback + def _handle_coordinator_update(self) -> None: + """Update attributes when the coordinator updates.""" + self._async_update_attrs() + super()._handle_coordinator_update() + + @callback + def _async_update_attrs(self) -> None: + """Update climate attributes.""" + self._attr_current_temperature = self.get_zone_value(AZD_TEMP) + self._attr_current_humidity = self.get_zone_value(AZD_HUMIDITY) + if self.get_zone_value(AZD_ON): + mode = self.get_zone_value(AZD_MODE) + self._attr_hvac_mode = HVAC_MODE_LIB_TO_HASS[mode] + if self.get_zone_value(AZD_DEMAND): + self._attr_hvac_action = HVAC_ACTION_LIB_TO_HASS[mode] + else: + self._attr_hvac_action = CURRENT_HVAC_IDLE + else: + self._attr_hvac_action = CURRENT_HVAC_OFF + self._attr_hvac_mode = HVAC_MODE_OFF + self._attr_target_temperature = self.get_zone_value(AZD_TEMP_SET) diff --git a/homeassistant/components/airzone/config_flow.py b/homeassistant/components/airzone/config_flow.py new file mode 100644 index 00000000000..57dde088820 --- /dev/null +++ b/homeassistant/components/airzone/config_flow.py @@ -0,0 +1,62 @@ +"""Config flow for Airzone.""" +from __future__ import annotations + +from typing import Any + +from aioairzone.common import ConnectionOptions +from aioairzone.exceptions import InvalidHost +from aioairzone.localapi import AirzoneLocalApi +from aiohttp.client_exceptions import ClientConnectorError +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers import aiohttp_client + +from .const import DEFAULT_LOCAL_API_PORT, DOMAIN + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle config flow for an Airzone device.""" + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the initial step.""" + errors = {} + + if user_input is not None: + self._async_abort_entries_match( + { + CONF_HOST: user_input[CONF_HOST], + CONF_PORT: user_input[CONF_PORT], + } + ) + + airzone = AirzoneLocalApi( + aiohttp_client.async_get_clientsession(self.hass), + ConnectionOptions( + user_input[CONF_HOST], + user_input[CONF_PORT], + ), + ) + + try: + await airzone.validate_airzone() + except (ClientConnectorError, InvalidHost): + errors["base"] = "cannot_connect" + else: + title = f"Airzone {user_input[CONF_HOST]}:{user_input[CONF_PORT]}" + return self.async_create_entry(title=title, data=user_input) + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required(CONF_HOST): str, + vol.Required(CONF_PORT, default=DEFAULT_LOCAL_API_PORT): int, + } + ), + errors=errors, + ) diff --git a/homeassistant/components/airzone/const.py b/homeassistant/components/airzone/const.py new file mode 100644 index 00000000000..094e2319476 --- /dev/null +++ b/homeassistant/components/airzone/const.py @@ -0,0 +1,19 @@ +"""Constants for the Airzone integration.""" + +from typing import Final + +from aioairzone.common import TemperatureUnit + +from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT + +DOMAIN: Final = "airzone" +MANUFACTURER: Final = "Airzone" + +AIOAIRZONE_DEVICE_TIMEOUT_SEC: Final = 10 +API_TEMPERATURE_STEP: Final = 0.5 +DEFAULT_LOCAL_API_PORT: Final = 3000 + +TEMP_UNIT_LIB_TO_HASS: Final[dict[TemperatureUnit, str]] = { + TemperatureUnit.CELSIUS: TEMP_CELSIUS, + TemperatureUnit.FAHRENHEIT: TEMP_FAHRENHEIT, +} diff --git a/homeassistant/components/airzone/coordinator.py b/homeassistant/components/airzone/coordinator.py new file mode 100644 index 00000000000..f81b75b42bf --- /dev/null +++ b/homeassistant/components/airzone/coordinator.py @@ -0,0 +1,42 @@ +"""The Airzone integration.""" +from __future__ import annotations + +from datetime import timedelta +import logging + +from aioairzone.localapi import AirzoneLocalApi +from aiohttp.client_exceptions import ClientConnectorError +import async_timeout + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import AIOAIRZONE_DEVICE_TIMEOUT_SEC, DOMAIN + +SCAN_INTERVAL = timedelta(seconds=60) + +_LOGGER = logging.getLogger(__name__) + + +class AirzoneUpdateCoordinator(DataUpdateCoordinator): + """Class to manage fetching data from the Airzone device.""" + + def __init__(self, hass: HomeAssistant, airzone: AirzoneLocalApi) -> None: + """Initialize.""" + self.airzone = airzone + + super().__init__( + hass, + _LOGGER, + name=DOMAIN, + update_interval=SCAN_INTERVAL, + ) + + async def _async_update_data(self): + """Update data via library.""" + async with async_timeout.timeout(AIOAIRZONE_DEVICE_TIMEOUT_SEC): + try: + await self.airzone.update_airzone() + except ClientConnectorError as error: + raise UpdateFailed(error) from error + return self.airzone.data() diff --git a/homeassistant/components/airzone/manifest.json b/homeassistant/components/airzone/manifest.json new file mode 100644 index 00000000000..3f8fbc5647b --- /dev/null +++ b/homeassistant/components/airzone/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "airzone", + "name": "Airzone", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/airzone", + "requirements": ["aioairzone==0.2.3"], + "codeowners": ["@Noltari"], + "iot_class": "local_polling", + "loggers": ["aioairzone"] +} diff --git a/homeassistant/components/airzone/sensor.py b/homeassistant/components/airzone/sensor.py new file mode 100644 index 00000000000..931e74a495d --- /dev/null +++ b/homeassistant/components/airzone/sensor.py @@ -0,0 +1,93 @@ +"""Support for the Airzone sensors.""" +from __future__ import annotations + +from typing import Any, Final + +from aioairzone.const import AZD_HUMIDITY, AZD_NAME, AZD_TEMP, AZD_TEMP_UNIT, AZD_ZONES + +from homeassistant.components.sensor import ( + STATE_CLASS_MEASUREMENT, + SensorEntity, + SensorEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_TEMPERATURE, + PERCENTAGE, + TEMP_CELSIUS, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import AirzoneEntity +from .const import DOMAIN, TEMP_UNIT_LIB_TO_HASS +from .coordinator import AirzoneUpdateCoordinator + +SENSOR_TYPES: Final[tuple[SensorEntityDescription, ...]] = ( + SensorEntityDescription( + device_class=DEVICE_CLASS_TEMPERATURE, + key=AZD_TEMP, + name="Temperature", + native_unit_of_measurement=TEMP_CELSIUS, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + device_class=DEVICE_CLASS_HUMIDITY, + key=AZD_HUMIDITY, + name="Humidity", + native_unit_of_measurement=PERCENTAGE, + state_class=STATE_CLASS_MEASUREMENT, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Add Airzone sensors from a config_entry.""" + coordinator = hass.data[DOMAIN][entry.entry_id] + + sensors = [] + for system_zone_id, zone_data in coordinator.data[AZD_ZONES].items(): + for description in SENSOR_TYPES: + if description.key in zone_data: + sensors.append( + AirzoneSensor( + coordinator, + description, + entry, + system_zone_id, + zone_data, + ) + ) + + async_add_entities(sensors) + + +class AirzoneSensor(AirzoneEntity, SensorEntity): + """Define an Airzone sensor.""" + + def __init__( + self, + coordinator: AirzoneUpdateCoordinator, + description: SensorEntityDescription, + entry: ConfigEntry, + system_zone_id: str, + zone_data: dict[str, Any], + ) -> None: + """Initialize.""" + super().__init__(coordinator, entry, system_zone_id, zone_data) + self._attr_name = f"{zone_data[AZD_NAME]} {description.name}" + self._attr_unique_id = f"{entry.entry_id}_{system_zone_id}_{description.key}" + self.entity_description = description + + if description.key == AZD_TEMP: + self._attr_native_unit_of_measurement = TEMP_UNIT_LIB_TO_HASS.get( + self.get_zone_value(AZD_TEMP_UNIT) + ) + + @property + def native_value(self): + """Return the state.""" + return self.get_zone_value(self.entity_description.key) diff --git a/homeassistant/components/airzone/strings.json b/homeassistant/components/airzone/strings.json new file mode 100644 index 00000000000..7de25789922 --- /dev/null +++ b/homeassistant/components/airzone/strings.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + }, + "step": { + "user": { + "data": { + "host": "[%key:common::config_flow::data::host%]", + "port": "[%key:common::config_flow::data::port%]" + }, + "description": "Set up Airzone integration." + } + } + } +} diff --git a/homeassistant/components/airzone/translations/bg.json b/homeassistant/components/airzone/translations/bg.json new file mode 100644 index 00000000000..cc5f200ef95 --- /dev/null +++ b/homeassistant/components/airzone/translations/bg.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "port": "\u041f\u043e\u0440\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airzone/translations/ca.json b/homeassistant/components/airzone/translations/ca.json new file mode 100644 index 00000000000..bbff47a9904 --- /dev/null +++ b/homeassistant/components/airzone/translations/ca.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3" + }, + "step": { + "user": { + "data": { + "host": "Amfitri\u00f3", + "port": "Port" + }, + "description": "Configura la integraci\u00f3 Airzone." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airzone/translations/de.json b/homeassistant/components/airzone/translations/de.json new file mode 100644 index 00000000000..7b3f5030f06 --- /dev/null +++ b/homeassistant/components/airzone/translations/de.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen" + }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Port" + }, + "description": "Richte die Airzone-Integration ein." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airzone/translations/el.json b/homeassistant/components/airzone/translations/el.json new file mode 100644 index 00000000000..7b04fe27743 --- /dev/null +++ b/homeassistant/components/airzone/translations/el.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af" + }, + "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" + }, + "step": { + "user": { + "data": { + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2", + "port": "\u0398\u03cd\u03c1\u03b1" + }, + "description": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 Airzone." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airzone/translations/en.json b/homeassistant/components/airzone/translations/en.json new file mode 100644 index 00000000000..b24e62fa34e --- /dev/null +++ b/homeassistant/components/airzone/translations/en.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "cannot_connect": "Failed to connect" + }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Port" + }, + "description": "Set up Airzone integration." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airzone/translations/et.json b/homeassistant/components/airzone/translations/et.json new file mode 100644 index 00000000000..307aa0de0a5 --- /dev/null +++ b/homeassistant/components/airzone/translations/et.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus" + }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Port" + }, + "description": "Seadista Airzone'i sidumine" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airzone/translations/fr.json b/homeassistant/components/airzone/translations/fr.json new file mode 100644 index 00000000000..1fdf1e4397b --- /dev/null +++ b/homeassistant/components/airzone/translations/fr.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion" + }, + "step": { + "user": { + "data": { + "host": "H\u00f4te", + "port": "Port" + }, + "description": "Configurer l'int\u00e9gration Airzone." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airzone/translations/he.json b/homeassistant/components/airzone/translations/he.json new file mode 100644 index 00000000000..c3a67844fdd --- /dev/null +++ b/homeassistant/components/airzone/translations/he.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + }, + "step": { + "user": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7", + "port": "\u05e4\u05ea\u05d7\u05d4" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airzone/translations/hu.json b/homeassistant/components/airzone/translations/hu.json new file mode 100644 index 00000000000..88e7449a1c4 --- /dev/null +++ b/homeassistant/components/airzone/translations/hu.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s" + }, + "step": { + "user": { + "data": { + "host": "C\u00edm", + "port": "Port" + }, + "description": "Airzone integr\u00e1ci\u00f3 be\u00e1ll\u00edt\u00e1sa." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airzone/translations/id.json b/homeassistant/components/airzone/translations/id.json new file mode 100644 index 00000000000..c37058bac34 --- /dev/null +++ b/homeassistant/components/airzone/translations/id.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung" + }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Port" + }, + "description": "Siapkan integrasi Airzone" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airzone/translations/it.json b/homeassistant/components/airzone/translations/it.json new file mode 100644 index 00000000000..db377b36fcc --- /dev/null +++ b/homeassistant/components/airzone/translations/it.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi" + }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Porta" + }, + "description": "Imposta l'integrazione Airzone." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airzone/translations/ja.json b/homeassistant/components/airzone/translations/ja.json new file mode 100644 index 00000000000..71b8bf1c908 --- /dev/null +++ b/homeassistant/components/airzone/translations/ja.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" + }, + "step": { + "user": { + "data": { + "host": "\u30db\u30b9\u30c8", + "port": "\u30dd\u30fc\u30c8" + }, + "description": "Airzone\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airzone/translations/nl.json b/homeassistant/components/airzone/translations/nl.json new file mode 100644 index 00000000000..4611c23eb1e --- /dev/null +++ b/homeassistant/components/airzone/translations/nl.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd" + }, + "error": { + "cannot_connect": "Kon niet verbinden" + }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Poort" + }, + "description": "Airzone integratie instellen." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airzone/translations/no.json b/homeassistant/components/airzone/translations/no.json new file mode 100644 index 00000000000..f078016b761 --- /dev/null +++ b/homeassistant/components/airzone/translations/no.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes" + }, + "step": { + "user": { + "data": { + "host": "Vert", + "port": "Port" + }, + "description": "Sett opp Airzone-integrasjon." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airzone/translations/pl.json b/homeassistant/components/airzone/translations/pl.json new file mode 100644 index 00000000000..38dc359b248 --- /dev/null +++ b/homeassistant/components/airzone/translations/pl.json @@ -0,0 +1,19 @@ +{ + "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": { + "host": "Nazwa hosta lub adres IP", + "port": "Port" + }, + "description": "Skonfiguruj integracj\u0119 Airzone." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airzone/translations/pt-BR.json b/homeassistant/components/airzone/translations/pt-BR.json new file mode 100644 index 00000000000..1a8df1fef99 --- /dev/null +++ b/homeassistant/components/airzone/translations/pt-BR.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha ao conectar" + }, + "step": { + "user": { + "data": { + "host": "Nome do host", + "port": "Porta" + }, + "description": "Configure a integra\u00e7\u00e3o Airzone." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airzone/translations/ru.json b/homeassistant/components/airzone/translations/ru.json new file mode 100644 index 00000000000..6032b0bdf00 --- /dev/null +++ b/homeassistant/components/airzone/translations/ru.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "port": "\u041f\u043e\u0440\u0442" + }, + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 Airzone." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airzone/translations/zh-Hant.json b/homeassistant/components/airzone/translations/zh-Hant.json new file mode 100644 index 00000000000..01e2db9730b --- /dev/null +++ b/homeassistant/components/airzone/translations/zh-Hant.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557" + }, + "step": { + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef", + "port": "\u901a\u8a0a\u57e0" + }, + "description": "\u8a2d\u5b9a Airzone \u6574\u5408\u3002" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/alarm_control_panel/manifest.json b/homeassistant/components/alarm_control_panel/manifest.json index e4cd0e27a39..461094e8ce6 100644 --- a/homeassistant/components/alarm_control_panel/manifest.json +++ b/homeassistant/components/alarm_control_panel/manifest.json @@ -2,6 +2,6 @@ "domain": "alarm_control_panel", "name": "Alarm Control Panel", "documentation": "https://www.home-assistant.io/integrations/alarm_control_panel", - "codeowners": [], + "codeowners": ["@home-assistant/core"], "quality_scale": "internal" } diff --git a/homeassistant/components/alarm_control_panel/translations/el.json b/homeassistant/components/alarm_control_panel/translations/el.json index d01fe947a8e..099e9b405ac 100644 --- a/homeassistant/components/alarm_control_panel/translations/el.json +++ b/homeassistant/components/alarm_control_panel/translations/el.json @@ -30,15 +30,15 @@ "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", + "armed_home": "\u039f\u03c0\u03bb\u03b9\u03c3\u03bc\u03ad\u03bd\u03bf\u03c2 \u03c3\u03c0\u03af\u03c4\u03b9", + "armed_night": "\u039f\u03c0\u03bb\u03b9\u03c3\u03bc\u03ad\u03bd\u03bf\u03c2 \u03b2\u03c1\u03ac\u03b4\u03c5", "armed_vacation": "\u039f\u03c0\u03bb\u03b9\u03c3\u03bc\u03cc\u03c2 \u03b4\u03b9\u03b1\u03ba\u03bf\u03c0\u03ce\u03bd", "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" + "triggered": "\u03a0\u03c5\u03c1\u03bf\u03b4\u03bf\u03c4\u03ae\u03b8\u03b7\u03ba\u03b5" } }, - "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" + "title": "\u03a0\u03af\u03bd\u03b1\u03ba\u03b1\u03c2 \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 \u03c3\u03c5\u03bd\u03b1\u03b3\u03b5\u03c1\u03bc\u03bf\u03cd" } \ No newline at end of file diff --git a/homeassistant/components/alarmdecoder/alarm_control_panel.py b/homeassistant/components/alarmdecoder/alarm_control_panel.py index 4d7b55db991..b16490357d1 100644 --- a/homeassistant/components/alarmdecoder/alarm_control_panel.py +++ b/homeassistant/components/alarmdecoder/alarm_control_panel.py @@ -22,6 +22,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_platform import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( @@ -95,8 +96,8 @@ class AlarmDecoderAlarmPanel(AlarmControlPanelEntity): 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 + async_dispatcher_connect( + self.hass, SIGNAL_PANEL_MESSAGE, self._message_callback ) ) diff --git a/homeassistant/components/alarmdecoder/binary_sensor.py b/homeassistant/components/alarmdecoder/binary_sensor.py index 87b6c86fc33..24eeffde691 100644 --- a/homeassistant/components/alarmdecoder/binary_sensor.py +++ b/homeassistant/components/alarmdecoder/binary_sensor.py @@ -4,6 +4,7 @@ import logging from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( @@ -90,26 +91,24 @@ class AlarmDecoderBinarySensor(BinarySensorEntity): 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 + async_dispatcher_connect(self.hass, SIGNAL_ZONE_FAULT, self._fault_callback) + ) + + self.async_on_remove( + async_dispatcher_connect( + self.hass, SIGNAL_ZONE_RESTORE, self._restore_callback ) ) self.async_on_remove( - self.hass.helpers.dispatcher.async_dispatcher_connect( - SIGNAL_ZONE_RESTORE, self._restore_callback + async_dispatcher_connect( + self.hass, SIGNAL_RFX_MESSAGE, self._rfx_message_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 + async_dispatcher_connect( + self.hass, SIGNAL_REL_MESSAGE, self._rel_message_callback ) ) diff --git a/homeassistant/components/alarmdecoder/sensor.py b/homeassistant/components/alarmdecoder/sensor.py index 897348f1386..90d0606d681 100644 --- a/homeassistant/components/alarmdecoder/sensor.py +++ b/homeassistant/components/alarmdecoder/sensor.py @@ -2,6 +2,7 @@ from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import SIGNAL_PANEL_MESSAGE @@ -26,8 +27,8 @@ class AlarmDecoderSensor(SensorEntity): 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 + async_dispatcher_connect( + self.hass, SIGNAL_PANEL_MESSAGE, self._message_callback ) ) diff --git a/homeassistant/components/alarmdecoder/translations/zh-Hant.json b/homeassistant/components/alarmdecoder/translations/zh-Hant.json index d1a96eedd15..19886544dfc 100644 --- a/homeassistant/components/alarmdecoder/translations/zh-Hant.json +++ b/homeassistant/components/alarmdecoder/translations/zh-Hant.json @@ -57,7 +57,7 @@ "zone_relayaddr": "\u4e2d\u7e7c\u4f4d\u5740", "zone_relaychan": "\u4e2d\u7e7c\u983b\u9053", "zone_rfid": "RF \u5e8f\u5217", - "zone_type": "\u5340\u57df\u985e\u578b" + "zone_type": "\u5340\u57df\u985e\u5225" }, "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" diff --git a/homeassistant/components/alert/__init__.py b/homeassistant/components/alert/__init__.py index 252bba54b1c..20f3eaf30ca 100644 --- a/homeassistant/components/alert/__init__.py +++ b/homeassistant/components/alert/__init__.py @@ -1,6 +1,10 @@ """Support for repeating alerts when conditions are met.""" +from __future__ import annotations + +from collections.abc import Callable from datetime import timedelta import logging +from typing import Any, final import voluptuous as vol @@ -23,10 +27,15 @@ from homeassistant.const import ( STATE_OFF, STATE_ON, ) -from homeassistant.core import HomeAssistant, ServiceCall -from homeassistant.helpers import event, service +from homeassistant.core import Event, HomeAssistant, ServiceCall +from homeassistant.helpers import service import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import ToggleEntity +from homeassistant.helpers.event import ( + async_track_point_in_time, + async_track_state_change_event, +) +from homeassistant.helpers.template import Template from homeassistant.helpers.typing import ConfigType from homeassistant.util.dt import now @@ -62,7 +71,7 @@ ALERT_SCHEMA = vol.Schema( 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, + vol.Required(CONF_NOTIFIERS): vol.All(cv.ensure_list, [cv.string]), } ) @@ -73,14 +82,14 @@ CONFIG_SCHEMA = vol.Schema( ALERT_SERVICE_SCHEMA = vol.Schema({vol.Required(ATTR_ENTITY_ID): cv.entity_ids}) -def is_on(hass, entity_id): +def is_on(hass: HomeAssistant, entity_id: str) -> bool: """Return if the alert is firing and not acknowledged.""" return hass.states.is_state(entity_id, STATE_ON) async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Alert component.""" - entities = [] + entities: list[Alert] = [] for object_id, cfg in config[DOMAIN].items(): if not cfg: @@ -144,10 +153,16 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: schema=ALERT_SERVICE_SCHEMA, ) hass.services.async_register( - DOMAIN, SERVICE_TURN_ON, async_handle_alert_service, schema=ALERT_SERVICE_SCHEMA + 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 + DOMAIN, + SERVICE_TOGGLE, + async_handle_alert_service, + schema=ALERT_SERVICE_SCHEMA, ) for alert in entities: @@ -163,20 +178,20 @@ class Alert(ToggleEntity): 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, - ): + hass: HomeAssistant, + entity_id: str, + name: str, + watched_entity_id: str, + state: str, + repeat: list[float], + skip_first: bool, + message_template: Template | None, + done_message_template: Template | None, + notifiers: list[str], + can_ack: bool, + title_template: Template | None, + data: dict[Any, Any], + ) -> None: """Initialize the alert.""" self.hass = hass self._attr_name = name @@ -204,16 +219,18 @@ class Alert(ToggleEntity): self._firing = False self._ack = False - self._cancel = None + self._cancel: Callable[[], None] | None = None self._send_done_message = False self.entity_id = f"{DOMAIN}.{entity_id}" - event.async_track_state_change_event( + async_track_state_change_event( hass, [watched_entity_id], self.watched_entity_change ) + @final # type: ignore[misc] @property - def state(self): # pylint: disable=overridden-final-method + # pylint: disable=overridden-final-method + def state(self) -> str: # type: ignore[override] """Return the alert status.""" if self._firing: if self._ack: @@ -221,17 +238,17 @@ class Alert(ToggleEntity): return STATE_ON return STATE_IDLE - async def watched_entity_change(self, ev): + async def watched_entity_change(self, event: Event) -> None: """Determine if the alert should start or stop.""" - if (to_state := ev.data.get("new_state")) is None: + if (to_state := event.data.get("new_state")) is None: return - _LOGGER.debug("Watched entity (%s) has changed", ev.data.get("entity_id")) + _LOGGER.debug("Watched entity (%s) has changed", event.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): + async def begin_alerting(self) -> None: """Begin the alert procedures.""" _LOGGER.debug("Beginning Alert: %s", self._attr_name) self._ack = False @@ -245,26 +262,27 @@ class Alert(ToggleEntity): self.async_write_ha_state() - async def end_alerting(self): + async def end_alerting(self) -> None: """End the alert procedures.""" _LOGGER.debug("Ending Alert: %s", self._attr_name) - self._cancel() + if self._cancel is not None: + self._cancel() + self._cancel = None + 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): + async def _schedule_notify(self) -> None: """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._cancel = 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): + async def _notify(self, *args: Any) -> None: """Send the alert notification.""" if not self._firing: return @@ -281,7 +299,7 @@ class Alert(ToggleEntity): await self._send_notification_message(message) await self._schedule_notify() - async def _notify_done_message(self, *args): + async def _notify_done_message(self) -> None: """Send notification of complete alert.""" _LOGGER.info("Alerting: %s", self._done_message_template) self._send_done_message = False @@ -293,15 +311,15 @@ class Alert(ToggleEntity): await self._send_notification_message(message) - async def _send_notification_message(self, message): + async def _send_notification_message(self, message: Any) -> None: 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}) + msg_payload[ATTR_TITLE] = title if self._data: - msg_payload.update({ATTR_DATA: self._data}) + msg_payload[ATTR_DATA] = self._data _LOGGER.debug(msg_payload) @@ -310,19 +328,19 @@ class Alert(ToggleEntity): DOMAIN_NOTIFY, target, msg_payload, context=self._context ) - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Async Unacknowledge alert.""" _LOGGER.debug("Reset Alert: %s", self._attr_name) self._ack = False self.async_write_ha_state() - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Async Acknowledge alert.""" _LOGGER.debug("Acknowledged Alert: %s", self._attr_name) self._ack = True self.async_write_ha_state() - async def async_toggle(self, **kwargs): + async def async_toggle(self, **kwargs: Any) -> None: """Async toggle alert.""" if self._ack: return await self.async_turn_on() diff --git a/homeassistant/components/alert/manifest.json b/homeassistant/components/alert/manifest.json index f5d3e08f2fe..bf9724ec2b9 100644 --- a/homeassistant/components/alert/manifest.json +++ b/homeassistant/components/alert/manifest.json @@ -3,7 +3,7 @@ "name": "Alert", "documentation": "https://www.home-assistant.io/integrations/alert", "after_dependencies": ["notify"], - "codeowners": [], + "codeowners": ["@home-assistant/core"], "quality_scale": "internal", "iot_class": "local_push" } diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index 327d5973892..db122e23f13 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -75,6 +75,8 @@ class AlexaCapability: https://developer.amazon.com/docs/device-apis/message-guide.html """ + # pylint: disable=no-self-use + supported_locales = {"en-US"} def __init__(self, entity: State, instance: str | None = None) -> None: @@ -86,28 +88,23 @@ class AlexaCapability: """Return the Alexa API name of this interface.""" raise NotImplementedError - @staticmethod - def properties_supported() -> list[dict]: + def properties_supported(self) -> list[dict]: """Return what properties this entity supports.""" return [] - @staticmethod - def properties_proactively_reported() -> bool: + def properties_proactively_reported(self) -> bool: """Return True if properties asynchronously reported.""" return False - @staticmethod - def properties_retrievable() -> bool: + def properties_retrievable(self) -> bool: """Return True if properties can be retrieved.""" return False - @staticmethod - def properties_non_controllable() -> bool | None: + def properties_non_controllable(self) -> bool | None: """Return True if non controllable.""" return None - @staticmethod - def get_property(name): + def get_property(self, name): """Read and return a property. Return value should be a dict, or raise UnsupportedProperty. @@ -117,13 +114,11 @@ class AlexaCapability: """ raise UnsupportedProperty(name) - @staticmethod - def supports_deactivation(): + def supports_deactivation(self): """Applicable only to scenes.""" return None - @staticmethod - def capability_proactively_reported(): + def capability_proactively_reported(self): """Return True if the capability is proactively reported. Set properties_proactively_reported() for proactively reported properties. @@ -131,16 +126,14 @@ class AlexaCapability: """ return None - @staticmethod - def capability_resources(): + def capability_resources(self): """Return the capability object. Applicable to ToggleController, RangeController, and ModeController interfaces. """ return [] - @staticmethod - def configuration(): + def configuration(self): """Return the configuration object. Applicable to the ThermostatController, SecurityControlPanel, ModeController, RangeController, @@ -148,8 +141,7 @@ class AlexaCapability: """ return [] - @staticmethod - def configurations(): + def configurations(self): """Return the configurations object. The plural configurations object is different that the singular configuration object. @@ -157,31 +149,29 @@ class AlexaCapability: """ return [] - @staticmethod - def inputs(): + def inputs(self): """Applicable only to media players.""" return [] - @staticmethod - def semantics(): + def semantics(self): """Return the semantics object. Applicable to ToggleController, RangeController, and ModeController interfaces. """ return [] - @staticmethod - def supported_operations(): + def supported_operations(self): """Return the supportedOperations object.""" return [] - @staticmethod - def camera_stream_configurations(): + def camera_stream_configurations(self): """Applicable only to CameraStreamController.""" return None def serialize_discovery(self): """Serialize according to the Discovery API.""" + # pylint: disable=assignment-from-none + # Methods may be overridden and return a value. result = {"type": "AlexaInterface", "interface": self.name(), "version": "3"} if (instance := self.instance) is not None: diff --git a/homeassistant/components/alexa/resources.py b/homeassistant/components/alexa/resources.py index 5c02eca4fb2..aec43f41f9a 100644 --- a/homeassistant/components/alexa/resources.py +++ b/homeassistant/components/alexa/resources.py @@ -200,6 +200,8 @@ class AlexaCapabilityResource: https://developer.amazon.com/docs/device-apis/resources-and-assets.html#capability-resources """ + # pylint: disable=no-self-use + def __init__(self, labels): """Initialize an Alexa resource.""" self._resource_labels = [] @@ -210,13 +212,11 @@ class AlexaCapabilityResource: """Return capabilityResources object serialized for an API response.""" return self.serialize_labels(self._resource_labels) - @staticmethod - def serialize_configuration(): + def serialize_configuration(self): """Return ModeResources, PresetResources friendlyNames serialized for an API response.""" return [] - @staticmethod - def serialize_labels(resources): + def serialize_labels(self, resources): """Return resource label objects for friendlyNames serialized for an API response.""" labels = [] for label in resources: diff --git a/homeassistant/components/alexa/smart_home_http.py b/homeassistant/components/alexa/smart_home_http.py index 95e13adfbd9..6a953a9f9d4 100644 --- a/homeassistant/components/alexa/smart_home_http.py +++ b/homeassistant/components/alexa/smart_home_http.py @@ -68,7 +68,10 @@ class AlexaConfig(AbstractConfig): entity_registry = er.async_get(self.hass) if registry_entry := entity_registry.async_get(entity_id): - auxiliary_entity = registry_entry.entity_category is not None + auxiliary_entity = ( + registry_entry.entity_category is not None + or registry_entry.hidden_by is not None + ) else: auxiliary_entity = False return not auxiliary_entity diff --git a/homeassistant/components/almond/config_flow.py b/homeassistant/components/almond/config_flow.py index bc945025c24..dfbdae219ca 100644 --- a/homeassistant/components/almond/config_flow.py +++ b/homeassistant/components/almond/config_flow.py @@ -8,7 +8,6 @@ from typing import Any from aiohttp import ClientError import async_timeout from pyalmond import AlmondLocalAuth, WebAlmondAPI -import voluptuous as vol from yarl import URL from homeassistant import core, data_entry_flow @@ -122,5 +121,4 @@ class AlmondFlowHandler( return self.async_show_form( step_id="hassio_confirm", description_placeholders={"addon": data["addon"]}, - data_schema=vol.Schema({}), ) diff --git a/homeassistant/components/almond/translations/fr.json b/homeassistant/components/almond/translations/fr.json index a464e8b56e9..6c38df7dec1 100644 --- a/homeassistant/components/almond/translations/fr.json +++ b/homeassistant/components/almond/translations/fr.json @@ -3,7 +3,7 @@ "abort": { "cannot_connect": "\u00c9chec de connexion", "missing_configuration": "Le composant n'est pas configur\u00e9. Veuillez suivre la documentation.", - "no_url_available": "Aucune URL disponible. Pour plus d'informations sur cette erreur, [consultez la section d'aide] ( {docs_url} )", + "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": { diff --git a/homeassistant/components/ambee/translations/fr.json b/homeassistant/components/ambee/translations/fr.json index dc968329b49..cfa3e0b9946 100644 --- a/homeassistant/components/ambee/translations/fr.json +++ b/homeassistant/components/ambee/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_api_key": "Cl\u00e9 API invalide" + "invalid_api_key": "Cl\u00e9 d'API non valide" }, "step": { "reauth_confirm": { diff --git a/homeassistant/components/amberelectric/binary_sensor.py b/homeassistant/components/amberelectric/binary_sensor.py index 422ff66db59..1931bcbd32c 100644 --- a/homeassistant/components/amberelectric/binary_sensor.py +++ b/homeassistant/components/amberelectric/binary_sensor.py @@ -24,7 +24,9 @@ PRICE_SPIKE_ICONS = { } -class AmberPriceGridSensor(CoordinatorEntity, BinarySensorEntity): +class AmberPriceGridSensor( + CoordinatorEntity[AmberUpdateCoordinator], BinarySensorEntity +): """Sensor to show single grid binary values.""" _attr_attribution = ATTRIBUTION diff --git a/homeassistant/components/amberelectric/coordinator.py b/homeassistant/components/amberelectric/coordinator.py index 904da59f65c..75cf3fd4360 100644 --- a/homeassistant/components/amberelectric/coordinator.py +++ b/homeassistant/components/amberelectric/coordinator.py @@ -10,6 +10,7 @@ from amberelectric.model.actual_interval import ActualInterval from amberelectric.model.channel import ChannelType from amberelectric.model.current_interval import CurrentInterval from amberelectric.model.forecast_interval import ForecastInterval +from amberelectric.model.interval import Descriptor from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -44,6 +45,27 @@ def is_feed_in(interval: ActualInterval | CurrentInterval | ForecastInterval) -> return interval.channel_type == ChannelType.FEED_IN +def normalize_descriptor(descriptor: Descriptor) -> str | None: + """Return the snake case versions of descriptor names. Returns None if the name is not recognized.""" + if descriptor is None: + return None + if descriptor.value == "spike": + return "spike" + if descriptor.value == "high": + return "high" + if descriptor.value == "neutral": + return "neutral" + if descriptor.value == "low": + return "low" + if descriptor.value == "veryLow": + return "very_low" + if descriptor.value == "extremelyLow": + return "extremely_low" + if descriptor.value == "negative": + return "negative" + return None + + class AmberUpdateCoordinator(DataUpdateCoordinator): """AmberUpdateCoordinator - In charge of downloading the data for a site, which all the sensors read.""" @@ -65,6 +87,7 @@ class AmberUpdateCoordinator(DataUpdateCoordinator): result: dict[str, dict[str, Any]] = { "current": {}, + "descriptors": {}, "forecasts": {}, "grid": {}, } @@ -81,6 +104,7 @@ class AmberUpdateCoordinator(DataUpdateCoordinator): raise UpdateFailed("No general channel configured") result["current"]["general"] = general[0] + result["descriptors"]["general"] = normalize_descriptor(general[0].descriptor) result["forecasts"]["general"] = [ interval for interval in forecasts if is_general(interval) ] @@ -92,6 +116,9 @@ class AmberUpdateCoordinator(DataUpdateCoordinator): ] if controlled_load: result["current"]["controlled_load"] = controlled_load[0] + result["descriptors"]["controlled_load"] = normalize_descriptor( + controlled_load[0].descriptor + ) result["forecasts"]["controlled_load"] = [ interval for interval in forecasts if is_controlled_load(interval) ] @@ -99,6 +126,9 @@ class AmberUpdateCoordinator(DataUpdateCoordinator): feed_in = [interval for interval in current if is_feed_in(interval)] if feed_in: result["current"]["feed_in"] = feed_in[0] + result["descriptors"]["feed_in"] = normalize_descriptor( + feed_in[0].descriptor + ) result["forecasts"]["feed_in"] = [ interval for interval in forecasts if is_feed_in(interval) ] diff --git a/homeassistant/components/amberelectric/manifest.json b/homeassistant/components/amberelectric/manifest.json index a4fd72f5bdb..9d13e205bd7 100644 --- a/homeassistant/components/amberelectric/manifest.json +++ b/homeassistant/components/amberelectric/manifest.json @@ -1,14 +1,10 @@ { - "domain": "amberelectric", - "name": "Amber Electric", - "documentation": "https://www.home-assistant.io/integrations/amberelectric", - "config_flow": true, - "codeowners": [ - "@madpilot" - ], - "requirements": [ - "amberelectric==1.0.3" - ], - "iot_class": "cloud_polling", - "loggers": ["amberelectric"] -} \ No newline at end of file + "domain": "amberelectric", + "name": "Amber Electric", + "documentation": "https://www.home-assistant.io/integrations/amberelectric", + "config_flow": true, + "codeowners": ["@madpilot"], + "requirements": ["amberelectric==1.0.4"], + "iot_class": "cloud_polling", + "loggers": ["amberelectric"] +} diff --git a/homeassistant/components/amberelectric/sensor.py b/homeassistant/components/amberelectric/sensor.py index 64ff09470e5..522cde2a95f 100644 --- a/homeassistant/components/amberelectric/sensor.py +++ b/homeassistant/components/amberelectric/sensor.py @@ -26,7 +26,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ATTRIBUTION, DOMAIN -from .coordinator import AmberUpdateCoordinator +from .coordinator import AmberUpdateCoordinator, normalize_descriptor ICONS = { "general": "mdi:transmission-tower", @@ -51,7 +51,7 @@ def friendly_channel_type(channel_type: str) -> str: return "General" -class AmberSensor(CoordinatorEntity, SensorEntity): +class AmberSensor(CoordinatorEntity[AmberUpdateCoordinator], SensorEntity): """Amber Base Sensor.""" _attr_attribution = ATTRIBUTION @@ -160,6 +160,7 @@ class AmberForecastSensor(AmberSensor): datum["end_time"] = interval.end_time.isoformat() datum["renewables"] = round(interval.renewables) datum["spike_status"] = interval.spike_status.value + datum["descriptor"] = normalize_descriptor(interval.descriptor) if interval.range is not None: datum["range_min"] = format_cents_to_dollars(interval.range.min) @@ -170,7 +171,16 @@ class AmberForecastSensor(AmberSensor): return data -class AmberGridSensor(CoordinatorEntity, SensorEntity): +class AmberPriceDescriptorSensor(AmberSensor): + """Amber Price Descriptor Sensor.""" + + @property + def native_value(self) -> str | None: + """Return the current price descriptor.""" + return self.coordinator.data[self.entity_description.key][self.channel_type] + + +class AmberGridSensor(CoordinatorEntity[AmberUpdateCoordinator], SensorEntity): """Sensor to show single grid specific values.""" _attr_attribution = ATTRIBUTION @@ -214,6 +224,16 @@ async def async_setup_entry( ) entities.append(AmberPriceSensor(coordinator, description, channel_type)) + for channel_type in current: + description = SensorEntityDescription( + key="descriptors", + name=f"{entry.title} - {friendly_channel_type(channel_type)} Price Descriptor", + icon=ICONS[channel_type], + ) + entities.append( + AmberPriceDescriptorSensor(coordinator, description, channel_type) + ) + for channel_type in forecasts: description = SensorEntityDescription( key="forecasts", diff --git a/homeassistant/components/amberelectric/strings.json b/homeassistant/components/amberelectric/strings.json index cdbff2022b3..61d2c061955 100644 --- a/homeassistant/components/amberelectric/strings.json +++ b/homeassistant/components/amberelectric/strings.json @@ -6,7 +6,6 @@ "api_token": "API Token", "site_id": "Site ID" }, - "title": "Amber Electric", "description": "Go to {api_url} to generate an API key" }, "site": { @@ -14,9 +13,8 @@ "site_nmi": "Site NMI", "site_name": "Site Name" }, - "title": "Amber Electric", "description": "Select the NMI of the site you would like to add" } } } -} \ No newline at end of file +} diff --git a/homeassistant/components/ambient_station/binary_sensor.py b/homeassistant/components/ambient_station/binary_sensor.py index 153fbf066db..f9fcec6aa6a 100644 --- a/homeassistant/components/ambient_station/binary_sensor.py +++ b/homeassistant/components/ambient_station/binary_sensor.py @@ -28,10 +28,21 @@ TYPE_BATT6 = "batt6" TYPE_BATT7 = "batt7" TYPE_BATT8 = "batt8" TYPE_BATT9 = "batt9" -TYPE_BATT_CO2 = "batt_co2" TYPE_BATTOUT = "battout" -TYPE_PM25_BATT = "batt_25" +TYPE_BATT_CO2 = "batt_co2" +TYPE_BATT_LIGHTNING = "batt_lightning" +TYPE_BATT_SM1 = "battsm1" +TYPE_BATT_SM10 = "battsm10" +TYPE_BATT_SM2 = "battsm2" +TYPE_BATT_SM3 = "battsm3" +TYPE_BATT_SM4 = "battsm4" +TYPE_BATT_SM5 = "battsm5" +TYPE_BATT_SM6 = "battsm6" +TYPE_BATT_SM7 = "battsm7" +TYPE_BATT_SM8 = "battsm8" +TYPE_BATT_SM9 = "battsm9" TYPE_PM25IN_BATT = "batt_25in" +TYPE_PM25_BATT = "batt_25" TYPE_RELAY1 = "relay1" TYPE_RELAY10 = "relay10" TYPE_RELAY2 = "relay2" @@ -131,7 +142,77 @@ BINARY_SENSOR_DESCRIPTIONS = ( ), AmbientBinarySensorDescription( key=TYPE_BATT10, - name="Battery 10", + name="Soil Monitor Battery 10", + device_class=BinarySensorDeviceClass.BATTERY, + entity_category=EntityCategory.DIAGNOSTIC, + on_state=0, + ), + AmbientBinarySensorDescription( + key=TYPE_BATT_SM1, + name="Soil Monitor Battery 1", + device_class=BinarySensorDeviceClass.BATTERY, + entity_category=EntityCategory.DIAGNOSTIC, + on_state=0, + ), + AmbientBinarySensorDescription( + key=TYPE_BATT_SM2, + name="Soil Monitor Battery 2", + device_class=BinarySensorDeviceClass.BATTERY, + entity_category=EntityCategory.DIAGNOSTIC, + on_state=0, + ), + AmbientBinarySensorDescription( + key=TYPE_BATT_SM3, + name="Soil Monitor Battery 3", + device_class=BinarySensorDeviceClass.BATTERY, + entity_category=EntityCategory.DIAGNOSTIC, + on_state=0, + ), + AmbientBinarySensorDescription( + key=TYPE_BATT_SM4, + name="Soil Monitor Battery 4", + device_class=BinarySensorDeviceClass.BATTERY, + entity_category=EntityCategory.DIAGNOSTIC, + on_state=0, + ), + AmbientBinarySensorDescription( + key=TYPE_BATT_SM5, + name="Soil Monitor Battery 5", + device_class=BinarySensorDeviceClass.BATTERY, + entity_category=EntityCategory.DIAGNOSTIC, + on_state=0, + ), + AmbientBinarySensorDescription( + key=TYPE_BATT_SM6, + name="Soil Monitor Battery 6", + device_class=BinarySensorDeviceClass.BATTERY, + entity_category=EntityCategory.DIAGNOSTIC, + on_state=0, + ), + AmbientBinarySensorDescription( + key=TYPE_BATT_SM7, + name="Soil Monitor Battery 7", + device_class=BinarySensorDeviceClass.BATTERY, + entity_category=EntityCategory.DIAGNOSTIC, + on_state=0, + ), + AmbientBinarySensorDescription( + key=TYPE_BATT_SM8, + name="Soil Monitor Battery 8", + device_class=BinarySensorDeviceClass.BATTERY, + entity_category=EntityCategory.DIAGNOSTIC, + on_state=0, + ), + AmbientBinarySensorDescription( + key=TYPE_BATT_SM9, + name="Soil Monitor Battery 9", + device_class=BinarySensorDeviceClass.BATTERY, + entity_category=EntityCategory.DIAGNOSTIC, + on_state=0, + ), + AmbientBinarySensorDescription( + key=TYPE_BATT_SM10, + name="Soil Monitor Battery 10", device_class=BinarySensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, on_state=0, @@ -143,6 +224,13 @@ BINARY_SENSOR_DESCRIPTIONS = ( entity_category=EntityCategory.DIAGNOSTIC, on_state=0, ), + AmbientBinarySensorDescription( + key=TYPE_BATT_LIGHTNING, + name="Lightning Detector Battery", + device_class=BinarySensorDeviceClass.BATTERY, + entity_category=EntityCategory.DIAGNOSTIC, + on_state=0, + ), AmbientBinarySensorDescription( key=TYPE_PM25IN_BATT, name="PM25 Indoor Battery", diff --git a/homeassistant/components/ambient_station/diagnostics.py b/homeassistant/components/ambient_station/diagnostics.py index cb9c7adab18..6005b206954 100644 --- a/homeassistant/components/ambient_station/diagnostics.py +++ b/homeassistant/components/ambient_station/diagnostics.py @@ -5,7 +5,7 @@ from typing import Any from homeassistant.components.diagnostics import async_redact_data from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_API_KEY +from homeassistant.const import CONF_API_KEY, CONF_LOCATION from homeassistant.core import HomeAssistant from . import AmbientStation @@ -14,7 +14,6 @@ from .const import CONF_APP_KEY, DOMAIN CONF_API_KEY_CAMEL = "apiKey" CONF_APP_KEY_CAMEL = "appKey" CONF_DEVICE_ID_CAMEL = "deviceId" -CONF_LOCATION = "location" CONF_MAC_ADDRESS = "mac_address" CONF_MAC_ADDRESS_CAMEL = "macAddress" CONF_TZ = "tz" diff --git a/homeassistant/components/ambient_station/sensor.py b/homeassistant/components/ambient_station/sensor.py index 3a17ecfc1f1..c837ef6fdec 100644 --- a/homeassistant/components/ambient_station/sensor.py +++ b/homeassistant/components/ambient_station/sensor.py @@ -32,6 +32,10 @@ from . import AmbientStation, AmbientWeatherEntity from .const import ATTR_LAST_DATA, DOMAIN, TYPE_SOLARRADIATION, TYPE_SOLARRADIATION_LX TYPE_24HOURRAININ = "24hourrainin" +TYPE_AQI_PM25 = "aqi_pm25" +TYPE_AQI_PM25_24H = "aqi_pm25_24h" +TYPE_AQI_PM25_IN = "aqi_pm25_in" +TYPE_AQI_PM25_IN_24H = "aqi_pm25_in_24h" TYPE_BAROMABSIN = "baromabsin" TYPE_BAROMRELIN = "baromrelin" TYPE_CO2 = "co2" @@ -53,6 +57,8 @@ TYPE_HUMIDITY8 = "humidity8" TYPE_HUMIDITY9 = "humidity9" TYPE_HUMIDITYIN = "humidityin" TYPE_LASTRAIN = "lastRain" +TYPE_LIGHTNING_PER_DAY = "lightning_day" +TYPE_LIGHTNING_PER_HOUR = "lightning_hour" TYPE_MAXDAILYGUST = "maxdailygust" TYPE_MONTHLYRAININ = "monthlyrainin" TYPE_PM25 = "pm25" @@ -112,6 +118,30 @@ SENSOR_DESCRIPTIONS = ( native_unit_of_measurement=PRECIPITATION_INCHES, state_class=SensorStateClass.TOTAL_INCREASING, ), + SensorEntityDescription( + key=TYPE_AQI_PM25, + name="AQI PM2.5", + device_class=SensorDeviceClass.AQI, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key=TYPE_AQI_PM25_24H, + name="AQI PM2.5 24h Avg", + device_class=SensorDeviceClass.AQI, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + SensorEntityDescription( + key=TYPE_AQI_PM25_IN, + name="AQI PM2.5 Indoor", + device_class=SensorDeviceClass.AQI, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key=TYPE_AQI_PM25_IN_24H, + name="AQI PM2.5 Indoor 24h Avg", + device_class=SensorDeviceClass.AQI, + state_class=SensorStateClass.TOTAL_INCREASING, + ), SensorEntityDescription( key=TYPE_BAROMABSIN, name="Abs Pressure", @@ -246,6 +276,20 @@ SENSOR_DESCRIPTIONS = ( icon="mdi:water", device_class=SensorDeviceClass.TIMESTAMP, ), + SensorEntityDescription( + key=TYPE_LIGHTNING_PER_DAY, + name="Lightning Strikes Per Day", + icon="mdi:lightning-bolt", + native_unit_of_measurement="strikes", + state_class=SensorStateClass.TOTAL_INCREASING, + ), + SensorEntityDescription( + key=TYPE_LIGHTNING_PER_HOUR, + name="Lightning Strikes Per Hour", + icon="mdi:lightning-bolt", + native_unit_of_measurement="strikes", + state_class=SensorStateClass.TOTAL_INCREASING, + ), SensorEntityDescription( key=TYPE_MAXDAILYGUST, name="Max Gust", diff --git a/homeassistant/components/ambient_station/translations/fr.json b/homeassistant/components/ambient_station/translations/fr.json index 1877a0af4ff..710786140c4 100644 --- a/homeassistant/components/ambient_station/translations/fr.json +++ b/homeassistant/components/ambient_station/translations/fr.json @@ -4,7 +4,7 @@ "already_configured": "Le service est d\u00e9j\u00e0 configur\u00e9" }, "error": { - "invalid_key": "Cl\u00e9 API invalide", + "invalid_key": "Cl\u00e9 d'API non valide", "no_devices": "Aucun appareil trouv\u00e9 dans le compte" }, "step": { diff --git a/homeassistant/components/amcrest/services.yaml b/homeassistant/components/amcrest/services.yaml index 7baad96d6d5..b79a333101b 100644 --- a/homeassistant/components/amcrest/services.yaml +++ b/homeassistant/components/amcrest/services.yaml @@ -99,9 +99,9 @@ set_color_bw: selector: select: options: - - 'auto' - - 'bw' - - 'color' + - "auto" + - "bw" + - "color" start_tour: name: Start tour @@ -142,16 +142,16 @@ ptz_control: selector: select: options: - - 'down' - - 'left' - - 'left_down' - - 'left_up' - - 'right' - - 'right_down' - - 'right_up' - - 'up' - - 'zoom_in' - - 'zoom_out' + - "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." diff --git a/homeassistant/components/analytics/analytics.py b/homeassistant/components/analytics/analytics.py index d1b8879bf7c..39c813b2696 100644 --- a/homeassistant/components/analytics/analytics.py +++ b/homeassistant/components/analytics/analytics.py @@ -31,6 +31,7 @@ from .const import ( ATTR_AUTOMATION_COUNT, ATTR_BASE, ATTR_BOARD, + ATTR_CERTIFICATE, ATTR_CONFIGURED, ATTR_CUSTOM_INTEGRATIONS, ATTR_DIAGNOSTICS, @@ -228,6 +229,7 @@ class Analytics: ) if self.preferences.get(ATTR_USAGE, False): + payload[ATTR_CERTIFICATE] = self.hass.http.ssl_certificate is not None payload[ATTR_INTEGRATIONS] = integrations payload[ATTR_CUSTOM_INTEGRATIONS] = custom_integrations if supervisor_info is not None: diff --git a/homeassistant/components/analytics/const.py b/homeassistant/components/analytics/const.py index 8576e22073f..63fdf820923 100644 --- a/homeassistant/components/analytics/const.py +++ b/homeassistant/components/analytics/const.py @@ -21,6 +21,7 @@ ATTR_AUTO_UPDATE = "auto_update" ATTR_AUTOMATION_COUNT = "automation_count" ATTR_BASE = "base" ATTR_BOARD = "board" +ATTR_CERTIFICATE = "certificate" ATTR_CONFIGURED = "configured" ATTR_CUSTOM_INTEGRATIONS = "custom_integrations" ATTR_DIAGNOSTICS = "diagnostics" diff --git a/homeassistant/components/analytics/manifest.json b/homeassistant/components/analytics/manifest.json index 2dae8d4e629..b52d85f5496 100644 --- a/homeassistant/components/analytics/manifest.json +++ b/homeassistant/components/analytics/manifest.json @@ -2,17 +2,9 @@ "domain": "analytics", "name": "Analytics", "documentation": "https://www.home-assistant.io/integrations/analytics", - "codeowners": [ - "@home-assistant/core", - "@ludeeus" - ], - "dependencies": [ - "api", - "websocket_api" - ], - "after_dependencies": [ - "energy" - ], + "codeowners": ["@home-assistant/core", "@ludeeus"], + "dependencies": ["api", "websocket_api"], + "after_dependencies": ["energy"], "quality_scale": "internal", "iot_class": "cloud_push" -} \ No newline at end of file +} diff --git a/homeassistant/components/android_ip_webcam/__init__.py b/homeassistant/components/android_ip_webcam/__init__.py index 67bb00f441d..cc24e6f4182 100644 --- a/homeassistant/components/android_ip_webcam/__init__.py +++ b/homeassistant/components/android_ip_webcam/__init__.py @@ -1,4 +1,6 @@ """Support for Android IP Webcam.""" +from __future__ import annotations + import asyncio from datetime import timedelta @@ -10,7 +12,6 @@ from homeassistant.const import ( CONF_HOST, CONF_NAME, CONF_PASSWORD, - CONF_PLATFORM, CONF_PORT, CONF_SCAN_INTERVAL, CONF_SENSORS, @@ -194,9 +195,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: 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] + username: str | None = cam_config.get(CONF_USERNAME) + password: str | None = cam_config.get(CONF_PASSWORD) + name: str = cam_config[CONF_NAME] interval = cam_config[CONF_SCAN_INTERVAL] switches = cam_config.get(CONF_SWITCHES) sensors = cam_config.get(CONF_SENSORS) @@ -238,17 +239,32 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: 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}) + # Remove incorrect config entry setup via mjpeg platform discovery. + mjpeg_config_entry = next( + ( + config_entry + for config_entry in hass.config_entries.async_entries("mjpeg") + if all( + config_entry.options.get(key) == val + for key, val in mjpeg_camera.items() + ) + ), + None, + ) + if mjpeg_config_entry: + await hass.config_entries.async_remove(mjpeg_config_entry.entry_id) + + mjpeg_camera[CONF_NAME] = name + hass.async_create_task( discovery.async_load_platform( - hass, Platform.CAMERA, "mjpeg", mjpeg_camera, config + hass, Platform.CAMERA, DOMAIN, mjpeg_camera, config ) ) diff --git a/homeassistant/components/android_ip_webcam/camera.py b/homeassistant/components/android_ip_webcam/camera.py new file mode 100644 index 00000000000..de1223c7f5f --- /dev/null +++ b/homeassistant/components/android_ip_webcam/camera.py @@ -0,0 +1,44 @@ +"""Support for Android IP Webcam Cameras.""" +from __future__ import annotations + +from homeassistant.components.mjpeg import MjpegCamera, filter_urllib3_logging +from homeassistant.const import HTTP_BASIC_AUTHENTICATION +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType + + +async def async_setup_platform( + hass: HomeAssistant, + config: ConfigType, + async_add_entities: AddEntitiesCallback, + discovery_info: DiscoveryInfoType | None = None, +) -> None: + """Set up the IP Webcam camera.""" + if discovery_info is None: + return + + filter_urllib3_logging() + async_add_entities([IPWebcamCamera(**discovery_info)]) + + +class IPWebcamCamera(MjpegCamera): + """Representation of a IP Webcam camera.""" + + def __init__( + self, + name: str, + mjpeg_url: str, + still_image_url: str, + username: str | None = None, + password: str = "", + ) -> None: + """Initialize the camera.""" + super().__init__( + name=name, + mjpeg_url=mjpeg_url, + still_image_url=still_image_url, + authentication=HTTP_BASIC_AUTHENTICATION, + username=username, + password=password, + ) diff --git a/homeassistant/components/androidtv/__init__.py b/homeassistant/components/androidtv/__init__.py index 157b618a264..c1875a883e3 100644 --- a/homeassistant/components/androidtv/__init__.py +++ b/homeassistant/components/androidtv/__init__.py @@ -1,13 +1,11 @@ """Support for functionality to interact with Android TV/Fire TV devices.""" -import logging import os from adb_shell.auth.keygen import keygen from androidtv.adb_manager.adb_manager_sync import ADBPythonSync -from androidtv.setup_async import setup +from androidtv.setup_async import setup as async_androidtv_setup -from homeassistant.components.media_player import DOMAIN as MP_DOMAIN -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_DEVICE_CLASS, CONF_HOST, @@ -17,11 +15,9 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers import entity_registry as er from homeassistant.helpers.device_registry import format_mac from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.storage import STORAGE_DIR -from homeassistant.helpers.typing import ConfigType from .const import ( ANDROID_DEV, @@ -35,7 +31,6 @@ from .const import ( DEVICE_FIRETV, DOMAIN, PROP_ETHMAC, - PROP_SERIALNO, PROP_WIFIMAC, SIGNAL_CONFIG_ENTITY, ) @@ -45,8 +40,6 @@ RELOAD_OPTIONS = [CONF_STATE_DETECTION_RULES] _INVALID_MACS = {"ff:ff:ff:ff:ff:ff"} -_LOGGER = logging.getLogger(__name__) - def get_androidtv_mac(dev_props): """Return formatted mac from device properties.""" @@ -89,7 +82,7 @@ async def async_connect_androidtv( _setup_androidtv, hass, config ) - aftv = await setup( + aftv = await async_androidtv_setup( config[CONF_HOST], config[CONF_PORT], adbkey, @@ -116,35 +109,6 @@ async def async_connect_androidtv( return aftv, None -def _migrate_aftv_entity(hass, aftv, entry_unique_id): - """Migrate a entity to new unique id.""" - entity_reg = er.async_get(hass) - - entity_unique_id = entry_unique_id - if entity_reg.async_get_entity_id(MP_DOMAIN, DOMAIN, entity_unique_id): - # entity already exist, nothing to do - return - - if not (old_unique_id := aftv.device_properties.get(PROP_SERIALNO)): - # serial no not found, exit - return - - migr_entity = entity_reg.async_get_entity_id(MP_DOMAIN, DOMAIN, old_unique_id) - if not migr_entity: - # old entity not found, exit - return - - try: - entity_reg.async_update_entity(migr_entity, new_unique_id=entity_unique_id) - except ValueError as exp: - _LOGGER.warning("Migration of old entity failed: %s", exp) - - -async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up the Android TV integration.""" - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Android TV platform.""" @@ -155,10 +119,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if not aftv: raise ConfigEntryNotReady(error_message) - # migrate existing entity to new unique ID - if entry.source == SOURCE_IMPORT: - _migrate_aftv_entity(hass, aftv, entry.unique_id) - async def async_close_connection(event): """Close Android TV connection on HA Stop.""" await aftv.adb_close() diff --git a/homeassistant/components/androidtv/config_flow.py b/homeassistant/components/androidtv/config_flow.py index 8f0efc34799..9df87eaffe2 100644 --- a/homeassistant/components/androidtv/config_flow.py +++ b/homeassistant/components/androidtv/config_flow.py @@ -2,13 +2,12 @@ import json import logging import os -import socket from androidtv import state_detection_rules_validator import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import CONF_DEVICE_CLASS, CONF_HOST, CONF_NAME, CONF_PORT +from homeassistant.const import CONF_DEVICE_CLASS, CONF_HOST, CONF_PORT from homeassistant.core import callback from homeassistant.helpers import config_validation as cv @@ -20,7 +19,6 @@ from .const import ( CONF_APPS, CONF_EXCLUDE_UNNAMED_APPS, CONF_GET_SOURCES, - CONF_MIGRATION_OPTIONS, CONF_SCREENCAP, CONF_STATE_DETECTION_RULES, CONF_TURN_OFF_COMMAND, @@ -59,23 +57,11 @@ def _is_file(value): return os.path.isfile(file_in) and os.access(file_in, os.R_OK) -def _get_ip(host): - """Get the ip address from the host name.""" - try: - return socket.gethostbyname(host) - except socket.gaierror: - return None - - class AndroidTVFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow.""" VERSION = 1 - def __init__(self): - """Initialize AndroidTV config flow.""" - self._import_options = None - @callback def _show_setup_form(self, user_input=None, error=None): """Show the setup form to the user.""" @@ -142,24 +128,17 @@ class AndroidTVFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if user_input is not None: host = user_input[CONF_HOST] adb_key = user_input.get(CONF_ADBKEY) - adb_server = user_input.get(CONF_ADB_SERVER_IP) - - if adb_key and adb_server: - return self._show_setup_form(user_input, "key_and_server") + if CONF_ADB_SERVER_IP in user_input: + if adb_key: + return self._show_setup_form(user_input, "key_and_server") + else: + user_input.pop(CONF_ADB_SERVER_PORT, None) if adb_key: - isfile = await self.hass.async_add_executor_job(_is_file, adb_key) - if not isfile: + if not await self.hass.async_add_executor_job(_is_file, adb_key): return self._show_setup_form(user_input, "adbkey_not_file") - ip_address = await self.hass.async_add_executor_job(_get_ip, host) - if not ip_address: - return self._show_setup_form(user_input, "invalid_host") - self._async_abort_entries_match({CONF_HOST: host}) - if ip_address != host: - self._async_abort_entries_match({CONF_HOST: ip_address}) - error, unique_id = await self._async_check_connection(user_input) if error is None: if not unique_id: @@ -169,26 +148,13 @@ class AndroidTVFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self._abort_if_unique_id_configured() return self.async_create_entry( - title=user_input.get(CONF_NAME) or host, + title=host, data=user_input, - options=self._import_options, ) user_input = user_input or {} return self._show_setup_form(user_input, error) - async def async_step_import(self, import_config=None): - """Import a config entry.""" - for entry in self._async_current_entries(): - if entry.data[CONF_HOST] == import_config[CONF_HOST]: - _LOGGER.warning( - "Host [%s] already configured. This yaml configuration has already been imported. Please remove it", - import_config[CONF_HOST], - ) - return self.async_abort(reason="already_configured") - self._import_options = import_config.pop(CONF_MIGRATION_OPTIONS, None) - return await self.async_step_user(import_config) - @staticmethod @callback def async_get_options_flow(config_entry): diff --git a/homeassistant/components/androidtv/const.py b/homeassistant/components/androidtv/const.py index f6f0c07286f..7f1e1288519 100644 --- a/homeassistant/components/androidtv/const.py +++ b/homeassistant/components/androidtv/const.py @@ -10,7 +10,6 @@ CONF_ADBKEY = "adbkey" CONF_APPS = "apps" CONF_EXCLUDE_UNNAMED_APPS = "exclude_unnamed_apps" CONF_GET_SOURCES = "get_sources" -CONF_MIGRATION_OPTIONS = "migration_options" CONF_SCREENCAP = "screencap" CONF_STATE_DETECTION_RULES = "state_detection_rules" CONF_TURN_OFF_COMMAND = "turn_off_command" diff --git a/homeassistant/components/androidtv/diagnostics.py b/homeassistant/components/androidtv/diagnostics.py new file mode 100644 index 00000000000..0a4245c40dc --- /dev/null +++ b/homeassistant/components/androidtv/diagnostics.py @@ -0,0 +1,81 @@ +"""Diagnostics support for AndroidTV.""" +from __future__ import annotations + +from typing import Any + +import attr + +from homeassistant.components.diagnostics import REDACTED, async_redact_data +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_CONNECTIONS, ATTR_IDENTIFIERS, CONF_UNIQUE_ID +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr, entity_registry as er + +from .const import ANDROID_DEV, DOMAIN, PROP_ETHMAC, PROP_SERIALNO, PROP_WIFIMAC + +TO_REDACT = {CONF_UNIQUE_ID} # UniqueID contain MAC Address +TO_REDACT_DEV = {PROP_ETHMAC, PROP_SERIALNO, PROP_WIFIMAC} + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, entry: ConfigEntry +) -> dict[str, dict[str, Any]]: + """Return diagnostics for a config entry.""" + data = {"entry": async_redact_data(entry.as_dict(), TO_REDACT)} + hass_data = hass.data[DOMAIN][entry.entry_id] + + # Get information from AndroidTV library + aftv = hass_data[ANDROID_DEV] + data_dev = {"device_class": aftv.DEVICE_CLASS} + for prop, value in aftv.device_properties.items(): + if prop in TO_REDACT_DEV and value: + data_dev[prop] = REDACTED + else: + data_dev[prop] = value + data["device_properties"] = data_dev + + # Gather information how this AndroidTV device is represented in Home Assistant + device_registry = dr.async_get(hass) + entity_registry = er.async_get(hass) + hass_device = device_registry.async_get_device( + identifiers={(DOMAIN, str(entry.unique_id))} + ) + if not hass_device: + return data + + data["device"] = { + **attr.asdict(hass_device), + "entities": {}, + } + data["device"][ATTR_IDENTIFIERS] = REDACTED + if ATTR_CONNECTIONS in data["device"]: + data["device"][ATTR_CONNECTIONS] = REDACTED + + hass_entities = er.async_entries_for_device( + entity_registry, + device_id=hass_device.id, + include_disabled_entities=True, + ) + + for entity_entry in hass_entities: + state = hass.states.get(entity_entry.entity_id) + state_dict = None + if state: + state_dict = dict(state.as_dict()) + # The entity_id is already provided at root level. + state_dict.pop("entity_id", None) + # The context doesn't provide useful information in this case. + state_dict.pop("context", None) + + entity_dict = async_redact_data( + { + **attr.asdict( + entity_entry, filter=lambda attr, value: attr.name != "entity_id" + ), + "state": state_dict, + }, + TO_REDACT, + ) + data["device"]["entities"][entity_entry.entity_id] = entity_dict + + return data diff --git a/homeassistant/components/androidtv/manifest.json b/homeassistant/components/androidtv/manifest.json index cd8e86a42a2..729cac082c7 100644 --- a/homeassistant/components/androidtv/manifest.json +++ b/homeassistant/components/androidtv/manifest.json @@ -3,8 +3,8 @@ "name": "Android TV", "documentation": "https://www.home-assistant.io/integrations/androidtv", "requirements": [ - "adb-shell[async]==0.4.0", - "androidtv[async]==0.0.63", + "adb-shell[async]==0.4.2", + "androidtv[async]==0.0.66", "pure-python-adb[async]==0.3.0.dev0" ], "codeowners": ["@JeffLIrion", "@ollo69"], diff --git a/homeassistant/components/androidtv/media_player.py b/homeassistant/components/androidtv/media_player.py index 1ab592143c6..db09dc8ec66 100644 --- a/homeassistant/components/androidtv/media_player.py +++ b/homeassistant/components/androidtv/media_player.py @@ -12,13 +12,12 @@ from adb_shell.exceptions import ( InvalidResponseError, TcpTimeoutException, ) -from androidtv import ha_state_detection_rules_validator from androidtv.constants import APPS, KEYS from androidtv.exceptions import LockNotAcquiredException import voluptuous as vol from homeassistant.components import persistent_notification -from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity +from homeassistant.components.media_player import MediaPlayerEntity from homeassistant.components.media_player.const import ( SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, @@ -32,17 +31,15 @@ from homeassistant.components.media_player.const import ( SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP, ) -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_COMMAND, ATTR_CONNECTIONS, ATTR_MANUFACTURER, ATTR_MODEL, ATTR_SW_VERSION, - CONF_DEVICE_CLASS, CONF_HOST, CONF_NAME, - CONF_PORT, STATE_IDLE, STATE_OFF, STATE_PAUSED, @@ -55,31 +52,21 @@ from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import get_androidtv_mac from .const import ( ANDROID_DEV, ANDROID_DEV_OPT, - CONF_ADB_SERVER_IP, - CONF_ADB_SERVER_PORT, - CONF_ADBKEY, CONF_APPS, CONF_EXCLUDE_UNNAMED_APPS, CONF_GET_SOURCES, - CONF_MIGRATION_OPTIONS, CONF_SCREENCAP, - CONF_STATE_DETECTION_RULES, CONF_TURN_OFF_COMMAND, CONF_TURN_ON_COMMAND, - DEFAULT_ADB_SERVER_PORT, - DEFAULT_DEVICE_CLASS, DEFAULT_EXCLUDE_UNNAMED_APPS, DEFAULT_GET_SOURCES, - DEFAULT_PORT, DEFAULT_SCREENCAP, DEVICE_ANDROIDTV, - DEVICE_CLASSES, DOMAIN, SIGNAL_CONFIG_ENTITY, ) @@ -123,34 +110,6 @@ SERVICE_UPLOAD = "upload" DEFAULT_NAME = "Android TV" -# Deprecated in Home Assistant 2022.2 -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_DEVICE_CLASS, default=DEFAULT_DEVICE_CLASS): vol.In( - DEVICE_CLASSES - ), - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_ADBKEY): cv.isfile, - vol.Optional(CONF_ADB_SERVER_IP): cv.string, - vol.Optional(CONF_ADB_SERVER_PORT, default=DEFAULT_ADB_SERVER_PORT): cv.port, - vol.Optional(CONF_GET_SOURCES, default=DEFAULT_GET_SOURCES): cv.boolean, - vol.Optional(CONF_APPS, default={}): vol.Schema( - {cv.string: vol.Any(cv.string, None)} - ), - vol.Optional(CONF_TURN_ON_COMMAND): cv.string, - vol.Optional(CONF_TURN_OFF_COMMAND): cv.string, - vol.Optional(CONF_STATE_DETECTION_RULES, default={}): vol.Schema( - {cv.string: ha_state_detection_rules_validator(vol.Invalid)} - ), - vol.Optional( - CONF_EXCLUDE_UNNAMED_APPS, default=DEFAULT_EXCLUDE_UNNAMED_APPS - ): cv.boolean, - vol.Optional(CONF_SCREENCAP, default=DEFAULT_SCREENCAP): cv.boolean, - } -) - # Translate from `AndroidTV` / `FireTV` reported state to HA state. ANDROIDTV_STATES = { "off": STATE_OFF, @@ -161,53 +120,6 @@ ANDROIDTV_STATES = { } -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the Android TV / Fire TV platform.""" - - host = config[CONF_HOST] - - # get main data - config_data = { - CONF_HOST: host, - CONF_DEVICE_CLASS: config.get(CONF_DEVICE_CLASS, DEFAULT_DEVICE_CLASS), - CONF_PORT: config.get(CONF_PORT, DEFAULT_PORT), - } - for key in (CONF_ADBKEY, CONF_ADB_SERVER_IP, CONF_ADB_SERVER_PORT, CONF_NAME): - if key in config: - config_data[key] = config[key] - - # get options - config_options = { - key: config[key] - for key in ( - CONF_APPS, - CONF_EXCLUDE_UNNAMED_APPS, - CONF_GET_SOURCES, - CONF_SCREENCAP, - CONF_STATE_DETECTION_RULES, - CONF_TURN_OFF_COMMAND, - CONF_TURN_ON_COMMAND, - ) - if key in config - } - - # save option to use with entry - if config_options: - config_data[CONF_MIGRATION_OPTIONS] = config_options - - # Launch config entries setup - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=config_data - ) - ) - - async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, @@ -217,10 +129,8 @@ async def async_setup_entry( aftv = hass.data[DOMAIN][entry.entry_id][ANDROID_DEV] device_class = aftv.DEVICE_CLASS device_type = "Android TV" if device_class == DEVICE_ANDROIDTV else "Fire TV" - if CONF_NAME in entry.data: - device_name = entry.data[CONF_NAME] - else: - device_name = f"{device_type} {entry.data[CONF_HOST]}" + # CONF_NAME may be present in entry.data for configuration imported from YAML + device_name = entry.data.get(CONF_NAME) or f"{device_type} {entry.data[CONF_HOST]}" device_args = [ aftv, diff --git a/homeassistant/components/androidtv/strings.json b/homeassistant/components/androidtv/strings.json index 1fd0231379f..7a46228bd4e 100644 --- a/homeassistant/components/androidtv/strings.json +++ b/homeassistant/components/androidtv/strings.json @@ -2,8 +2,6 @@ "config": { "step": { "user": { - "title": "Android TV", - "description": "Set required parameters to connect to your Android TV device", "data": { "host": "[%key:common::config_flow::data::host%]", "adbkey": "Path to your ADB key file (leave empty to auto generate)", @@ -29,7 +27,6 @@ "options": { "step": { "init": { - "title": "Android TV Options", "data": { "apps": "Configure applications list", "get_sources": "Retrieve the running apps as the list of sources", diff --git a/homeassistant/components/androidtv/translations/he.json b/homeassistant/components/androidtv/translations/he.json index 298416bccc5..81870f85c75 100644 --- a/homeassistant/components/androidtv/translations/he.json +++ b/homeassistant/components/androidtv/translations/he.json @@ -43,12 +43,12 @@ "init": { "data": { "apps": "\u05e7\u05d1\u05d9\u05e2\u05ea \u05ea\u05e6\u05d5\u05e8\u05d4 \u05e9\u05dc \u05e8\u05e9\u05d9\u05de\u05ea \u05d9\u05d9\u05e9\u05d5\u05de\u05d9\u05dd", - "exclude_unnamed_apps": "\u05d0\u05dc \u05ea\u05db\u05dc\u05d5\u05dc \u05d9\u05d9\u05e9\u05d5\u05dd \u05e2\u05dd \u05e9\u05dd \u05dc\u05d0 \u05d9\u05d3\u05d5\u05e2", - "get_sources": "\u05d4\u05d0\u05dd \u05dc\u05d0\u05d7\u05d6\u05e8 \u05d0\u05ea \u05d4\u05d9\u05d9\u05e9\u05d5\u05de\u05d9\u05dd \u05d4\u05e4\u05d5\u05e2\u05dc\u05d9\u05dd \u05db\u05e8\u05e9\u05d9\u05de\u05ea \u05d4\u05de\u05e7\u05d5\u05e8\u05d5\u05ea", - "screencap": "\u05e7\u05d5\u05d1\u05e2 \u05d0\u05dd \u05d9\u05e9 \u05dc\u05de\u05e9\u05d5\u05da \u05d0\u05ea \u05ea\u05de\u05d5\u05e0\u05ea \u05d4\u05d0\u05dc\u05d1\u05d5\u05dd \u05de\u05de\u05d4 \u05e9\u05de\u05d5\u05e6\u05d2 \u05e2\u05dc \u05d4\u05de\u05e1\u05da", + "exclude_unnamed_apps": "\u05d0\u05dc \u05ea\u05db\u05dc\u05d5\u05dc \u05d9\u05d9\u05e9\u05d5\u05de\u05d9\u05dd \u05e2\u05dd \u05e9\u05dd \u05dc\u05d0 \u05d9\u05d3\u05d5\u05e2 \u05d1\u05e8\u05e9\u05d9\u05de\u05ea \u05d4\u05de\u05e7\u05d5\u05e8\u05d5\u05ea", + "get_sources": "\u05d0\u05d7\u05d6\u05d5\u05e8 \u05d4\u05d9\u05d9\u05e9\u05d5\u05de\u05d9\u05dd \u05d4\u05e4\u05d5\u05e2\u05dc\u05d9\u05dd \u05db\u05e8\u05e9\u05d9\u05de\u05ea \u05d4\u05de\u05e7\u05d5\u05e8\u05d5\u05ea", + "screencap": "\u05e9\u05d9\u05de\u05d5\u05e9 \u05d1\u05dc\u05db\u05d9\u05d3\u05ea \u05de\u05e1\u05da \u05e2\u05d1\u05d5\u05e8 \u05ea\u05de\u05d5\u05e0\u05ea \u05d0\u05dc\u05d1\u05d5\u05dd", "state_detection_rules": "\u05e7\u05d1\u05d9\u05e2\u05ea \u05ea\u05e6\u05d5\u05e8\u05d4 \u05e9\u05dc \u05db\u05dc\u05dc\u05d9 \u05d6\u05d9\u05d4\u05d5\u05d9 \u05de\u05e6\u05d1\u05d9\u05dd", - "turn_off_command": "\u05e4\u05e7\u05d5\u05d3\u05ea \u05de\u05e2\u05d8\u05e4\u05ea ADB \u05db\u05d3\u05d9 \u05dc\u05e2\u05e7\u05d5\u05e3 \u05d0\u05ea \u05e4\u05e7\u05d5\u05d3\u05ea \u05d1\u05e8\u05d9\u05e8\u05ea \u05d4\u05de\u05d7\u05d3\u05dc turn_off", - "turn_on_command": "\u05e4\u05e7\u05d5\u05d3\u05ea \u05de\u05e2\u05d8\u05e4\u05ea ADB \u05db\u05d3\u05d9 \u05dc\u05e2\u05e7\u05d5\u05e3 \u05d0\u05ea \u05e4\u05e7\u05d5\u05d3\u05ea \u05d1\u05e8\u05d9\u05e8\u05ea \u05d4\u05de\u05d7\u05d3\u05dc turn_on" + "turn_off_command": "\u05d4\u05e4\u05e7\u05d5\u05d3\u05d4 \u05db\u05d9\u05d1\u05d5\u05d9 \u05de\u05e2\u05d8\u05e4\u05ea ADB (\u05d4\u05e9\u05d0\u05e8 \u05e8\u05d9\u05e7\u05d4 \u05db\u05d1\u05e8\u05d9\u05e8\u05ea \u05de\u05d7\u05d3\u05dc)", + "turn_on_command": "\u05d4\u05e4\u05e7\u05d5\u05d3\u05d4 \u05d4\u05e4\u05e2\u05dc \u05de\u05e2\u05d8\u05e4\u05ea ADB (\u05d4\u05e9\u05d0\u05e8 \u05e8\u05d9\u05e7\u05d4 \u05db\u05d1\u05e8\u05d9\u05e8\u05ea \u05de\u05d7\u05d3\u05dc)" }, "title": "\u05d0\u05e4\u05e9\u05e8\u05d5\u05d9\u05d5\u05ea \u05d8\u05dc\u05d5\u05d5\u05d9\u05d6\u05d9\u05ea \u05d0\u05e0\u05d3\u05e8\u05d5\u05d0\u05d9\u05d3" }, diff --git a/homeassistant/components/androidtv/translations/zh-Hant.json b/homeassistant/components/androidtv/translations/zh-Hant.json index 6f6e6fd8180..9e9d78f0629 100644 --- a/homeassistant/components/androidtv/translations/zh-Hant.json +++ b/homeassistant/components/androidtv/translations/zh-Hant.json @@ -17,7 +17,7 @@ "adb_server_ip": "ADB \u4f3a\u670d\u5668 IP \u4f4d\u5740\uff08\u4fdd\u7559\u7a7a\u767d\u70ba\u4e0d\u4f7f\u7528\uff09", "adb_server_port": "ADB \u4f3a\u670d\u5668\u901a\u8a0a\u57e0", "adbkey": "ADB \u91d1\u9470\u6a94\u6848\u8def\u5f91\uff08\u4fdd\u7559\u7a7a\u767d\u5c07\u6703\u81ea\u52d5\u7522\u751f\uff09", - "device_class": "\u88dd\u7f6e\u985e\u578b", + "device_class": "\u88dd\u7f6e\u985e\u5225", "host": "\u4e3b\u6a5f\u7aef", "port": "\u901a\u8a0a\u57e0" }, diff --git a/homeassistant/components/apcupsd/__init__.py b/homeassistant/components/apcupsd/__init__.py index a032430e1bc..7cbf33f8b47 100644 --- a/homeassistant/components/apcupsd/__init__.py +++ b/homeassistant/components/apcupsd/__init__.py @@ -1,5 +1,4 @@ """Support for APCUPSd via its Network Information Server (NIS).""" -# pylint: disable=import-error from datetime import timedelta import logging diff --git a/homeassistant/components/apcupsd/sensor.py b/homeassistant/components/apcupsd/sensor.py index 2fae17ac922..b7e7366796b 100644 --- a/homeassistant/components/apcupsd/sensor.py +++ b/homeassistant/components/apcupsd/sensor.py @@ -1,5 +1,4 @@ """Support for APCUPSd sensors.""" -# pylint: disable=import-error from __future__ import annotations import logging diff --git a/homeassistant/components/apns/__init__.py b/homeassistant/components/apns/__init__.py deleted file mode 100644 index 9332b0d1ede..00000000000 --- a/homeassistant/components/apns/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The apns component.""" diff --git a/homeassistant/components/apns/const.py b/homeassistant/components/apns/const.py deleted file mode 100644 index a8dc1204aa1..00000000000 --- a/homeassistant/components/apns/const.py +++ /dev/null @@ -1,2 +0,0 @@ -"""Constants for the apns component.""" -DOMAIN = "apns" diff --git a/homeassistant/components/apns/manifest.json b/homeassistant/components/apns/manifest.json deleted file mode 100644 index bcefdcf0639..00000000000 --- a/homeassistant/components/apns/manifest.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "disabled": "Integration library not compatible with Python 3.10", - "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", - "loggers": ["apns2", "hyper"] -} diff --git a/homeassistant/components/apns/notify.py b/homeassistant/components/apns/notify.py deleted file mode 100644 index 8d0dcc334e9..00000000000 --- a/homeassistant/components/apns/notify.py +++ /dev/null @@ -1,268 +0,0 @@ -"""APNS Notification platform.""" -# pylint: disable=import-error -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.core import ServiceCall -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} -) - -_LOGGER = logging.getLogger(__name__) - - -def get_service(hass, config, discovery_info=None): - """Return push service.""" - _LOGGER.warning( - "The Apple Push Notification Service (APNS) integration is deprecated " - "and will be removed in Home Assistant Core 2022.4" - ) - - 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+", encoding="utf8") as out: - for device in self.devices.values(): - _write_device(out, device) - - def register(self, call: ServiceCall) -> None: - """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", encoding="utf8") as out: - _write_device(out, device) - return - - if device != current_device: - self.devices[push_id] = device - self.write_devices() - - 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) - if (message_data := kwargs.get(ATTR_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/components/apns/services.yaml b/homeassistant/components/apns/services.yaml deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/homeassistant/components/apple_tv/manifest.json b/homeassistant/components/apple_tv/manifest.json index 03d662b9721..26a8e2737c8 100644 --- a/homeassistant/components/apple_tv/manifest.json +++ b/homeassistant/components/apple_tv/manifest.json @@ -13,12 +13,15 @@ "_touch-able._tcp.local.", "_appletv-v2._tcp.local.", "_hscp._tcp.local.", - {"type":"_airplay._tcp.local.", "properties": {"model":"appletv*"}}, - {"type":"_airplay._tcp.local.", "properties": {"model":"audioaccessory*"}}, - {"type":"_airplay._tcp.local.", "properties": {"am":"airport*"}}, - {"type":"_raop._tcp.local.", "properties": {"am":"appletv*"}}, - {"type":"_raop._tcp.local.", "properties": {"am":"audioaccessory*"}}, - {"type":"_raop._tcp.local.", "properties": {"am":"airport*"}} + { "type": "_airplay._tcp.local.", "properties": { "model": "appletv*" } }, + { + "type": "_airplay._tcp.local.", + "properties": { "model": "audioaccessory*" } + }, + { "type": "_airplay._tcp.local.", "properties": { "am": "airport*" } }, + { "type": "_raop._tcp.local.", "properties": { "am": "appletv*" } }, + { "type": "_raop._tcp.local.", "properties": { "am": "audioaccessory*" } }, + { "type": "_raop._tcp.local.", "properties": { "am": "airport*" } } ], "codeowners": ["@postlund"], "iot_class": "local_push", diff --git a/homeassistant/components/apple_tv/translations/fr.json b/homeassistant/components/apple_tv/translations/fr.json index 23fed83cfc4..510d22c79a5 100644 --- a/homeassistant/components/apple_tv/translations/fr.json +++ b/homeassistant/components/apple_tv/translations/fr.json @@ -16,7 +16,7 @@ }, "error": { "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "no_devices_found": "Aucun appareil trouv\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 inattendue" diff --git a/homeassistant/components/apple_tv/translations/he.json b/homeassistant/components/apple_tv/translations/he.json index 209cd7069f0..03e7dc5d4fe 100644 --- a/homeassistant/components/apple_tv/translations/he.json +++ b/homeassistant/components/apple_tv/translations/he.json @@ -14,7 +14,7 @@ "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" }, - "flow_title": "{name}", + "flow_title": "{name} ({type})", "step": { "pair_with_pin": { "data": { diff --git a/homeassistant/components/aqualogic/sensor.py b/homeassistant/components/aqualogic/sensor.py index 3337e9c40a2..5e6e35cce76 100644 --- a/homeassistant/components/aqualogic/sensor.py +++ b/homeassistant/components/aqualogic/sensor.py @@ -20,6 +20,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -146,8 +147,8 @@ class AquaLogicSensor(SensorEntity): 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 + async_dispatcher_connect( + self.hass, UPDATE_TOPIC, self.async_update_callback ) ) diff --git a/homeassistant/components/aqualogic/switch.py b/homeassistant/components/aqualogic/switch.py index 54a37056187..953b0c9b527 100644 --- a/homeassistant/components/aqualogic/switch.py +++ b/homeassistant/components/aqualogic/switch.py @@ -8,6 +8,7 @@ from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity from homeassistant.const import CONF_MONITORED_CONDITIONS from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -96,7 +97,5 @@ class AquaLogicSwitch(SwitchEntity): 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 - ) + async_dispatcher_connect(self.hass, UPDATE_TOPIC, self.async_write_ha_state) ) diff --git a/homeassistant/components/arcam_fmj/media_player.py b/homeassistant/components/arcam_fmj/media_player.py index 85457ebfcef..583fa5f66c4 100644 --- a/homeassistant/components/arcam_fmj/media_player.py +++ b/homeassistant/components/arcam_fmj/media_player.py @@ -230,7 +230,7 @@ class ArcamFmj(MediaPlayerEntity): ] root = BrowseMedia( - title="Root", + title="Arcam FMJ Receiver", media_class=MEDIA_CLASS_DIRECTORY, media_content_id="root", media_content_type="library", diff --git a/homeassistant/components/aseko_pool_live/entity.py b/homeassistant/components/aseko_pool_live/entity.py index 963bb536671..58974bcc326 100644 --- a/homeassistant/components/aseko_pool_live/entity.py +++ b/homeassistant/components/aseko_pool_live/entity.py @@ -8,11 +8,9 @@ from . import AsekoDataUpdateCoordinator from .const import DOMAIN -class AsekoEntity(CoordinatorEntity): +class AsekoEntity(CoordinatorEntity[AsekoDataUpdateCoordinator]): """Representation of an aseko entity.""" - coordinator: AsekoDataUpdateCoordinator - def __init__(self, unit: Unit, coordinator: AsekoDataUpdateCoordinator) -> None: """Initialize the aseko entity.""" super().__init__(coordinator) diff --git a/homeassistant/components/aseko_pool_live/manifest.json b/homeassistant/components/aseko_pool_live/manifest.json index 3b5b994282d..f3996503d70 100644 --- a/homeassistant/components/aseko_pool_live/manifest.json +++ b/homeassistant/components/aseko_pool_live/manifest.json @@ -4,9 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/aseko_pool_live", "requirements": ["aioaseko==0.0.2"], - "codeowners": [ - "@milanmeu" - ], + "codeowners": ["@milanmeu"], "iot_class": "cloud_polling", "loggers": ["aioaseko"] -} \ No newline at end of file +} diff --git a/homeassistant/components/aseko_pool_live/strings.json b/homeassistant/components/aseko_pool_live/strings.json index 4c3813220b6..7a91b2c9f8b 100644 --- a/homeassistant/components/aseko_pool_live/strings.json +++ b/homeassistant/components/aseko_pool_live/strings.json @@ -17,4 +17,4 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_account%]" } } -} \ No newline at end of file +} diff --git a/homeassistant/components/aseko_pool_live/translations/fr.json b/homeassistant/components/aseko_pool_live/translations/fr.json index d28b22f8d98..ec1568e9330 100644 --- a/homeassistant/components/aseko_pool_live/translations/fr.json +++ b/homeassistant/components/aseko_pool_live/translations/fr.json @@ -5,13 +5,13 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "step": { "user": { "data": { - "email": "Email", + "email": "Courriel", "password": "Mot de passe" } } diff --git a/homeassistant/components/aten_pe/switch.py b/homeassistant/components/aten_pe/switch.py index 1383a77cda6..92a58f37c8c 100644 --- a/homeassistant/components/aten_pe/switch.py +++ b/homeassistant/components/aten_pe/switch.py @@ -116,7 +116,5 @@ class AtenSwitch(SwitchEntity): status = await self._device.displayOutletStatus(self._outlet) if status == "on": self._attr_is_on = True - self._attr_current_power_w = await self._device.outletPower(self._outlet) elif status == "off": self._attr_is_on = False - self._attr_current_power_w = 0.0 diff --git a/homeassistant/components/august/__init__.py b/homeassistant/components/august/__init__.py index 031f513843f..570ec5983fe 100644 --- a/homeassistant/components/august/__init__.py +++ b/homeassistant/components/august/__init__.py @@ -1,10 +1,15 @@ """Support for August devices.""" +from __future__ import annotations + import asyncio +from collections.abc import ValuesView from itertools import chain import logging from aiohttp import ClientError, ClientResponseError +from yalexs.doorbell import Doorbell, DoorbellDetail from yalexs.exceptions import AugustApiAIOHTTPError +from yalexs.lock import Lock, LockDetail from yalexs.pubnub_activity import activities_from_pubnub_message from yalexs.pubnub_async import AugustPubNub, async_create_pubnub @@ -18,19 +23,19 @@ from homeassistant.exceptions import ( ) from .activity import ActivityStream -from .const import DATA_AUGUST, DOMAIN, MIN_TIME_BETWEEN_DETAIL_UPDATES, PLATFORMS +from .const import 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 = ( +API_CACHED_ATTRS = { "door_state", "door_state_datetime", "lock_status", "lock_status_datetime", -) +} async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @@ -52,7 +57,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - hass.data[DOMAIN][entry.entry_id][DATA_AUGUST].async_stop() + data: AugustData = hass.data[DOMAIN][entry.entry_id] + data.async_stop() unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) @@ -78,10 +84,8 @@ async def async_setup_august( await august_gateway.async_refresh_access_token_if_needed() hass.data.setdefault(DOMAIN, {}) - data = hass.data[DOMAIN][config_entry.entry_id] = { - DATA_AUGUST: AugustData(hass, august_gateway) - } - await data[DATA_AUGUST].async_setup() + data = hass.data[DOMAIN][config_entry.entry_id] = AugustData(hass, august_gateway) + await data.async_setup() hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) @@ -189,16 +193,16 @@ class AugustData(AugustSubscriberMixin): self.activity_stream.async_stop() @property - def doorbells(self): + def doorbells(self) -> ValuesView[Doorbell]: """Return a list of py-august Doorbell objects.""" return self._doorbells_by_id.values() @property - def locks(self): + def locks(self) -> ValuesView[Lock]: """Return a list of py-august Lock objects.""" return self._locks_by_id.values() - def get_device_detail(self, device_id): + def get_device_detail(self, device_id: str) -> DoorbellDetail | LockDetail: """Return the py-august LockDetail or DoorbellDetail object for a device.""" return self._device_detail_by_id[device_id] diff --git a/homeassistant/components/august/binary_sensor.py b/homeassistant/components/august/binary_sensor.py index 703a1e58d87..a33d1cb96dc 100644 --- a/homeassistant/components/august/binary_sensor.py +++ b/homeassistant/components/august/binary_sensor.py @@ -29,7 +29,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_call_later from . import AugustData -from .const import ACTIVITY_UPDATE_INTERVAL, DATA_AUGUST, DOMAIN +from .const import ACTIVITY_UPDATE_INTERVAL, DOMAIN from .entity import AugustEntityMixin _LOGGER = logging.getLogger(__name__) @@ -160,7 +160,7 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the August binary sensors.""" - data: AugustData = hass.data[DOMAIN][config_entry.entry_id][DATA_AUGUST] + data: AugustData = hass.data[DOMAIN][config_entry.entry_id] entities: list[BinarySensorEntity] = [] for door in data.locks: diff --git a/homeassistant/components/august/button.py b/homeassistant/components/august/button.py index d7f2a5ba4ae..5f4032153a2 100644 --- a/homeassistant/components/august/button.py +++ b/homeassistant/components/august/button.py @@ -7,7 +7,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import AugustData -from .const import DATA_AUGUST, DOMAIN +from .const import DOMAIN from .entity import AugustEntityMixin @@ -17,8 +17,8 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up August lock wake buttons.""" - data: AugustData = hass.data[DOMAIN][config_entry.entry_id][DATA_AUGUST] - async_add_entities([AugustWakeLockButton(data, lock) for lock in data.locks]) + data: AugustData = hass.data[DOMAIN][config_entry.entry_id] + async_add_entities(AugustWakeLockButton(data, lock) for lock in data.locks) class AugustWakeLockButton(AugustEntityMixin, ButtonEntity): diff --git a/homeassistant/components/august/camera.py b/homeassistant/components/august/camera.py index 26889427555..c5ab5fc3cfa 100644 --- a/homeassistant/components/august/camera.py +++ b/homeassistant/components/august/camera.py @@ -11,7 +11,7 @@ from homeassistant.helpers import aiohttp_client from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import AugustData -from .const import DATA_AUGUST, DEFAULT_NAME, DEFAULT_TIMEOUT, DOMAIN +from .const import DEFAULT_NAME, DEFAULT_TIMEOUT, DOMAIN from .entity import AugustEntityMixin @@ -21,13 +21,11 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up August cameras.""" - data: AugustData = hass.data[DOMAIN][config_entry.entry_id][DATA_AUGUST] + data: AugustData = hass.data[DOMAIN][config_entry.entry_id] session = aiohttp_client.async_get_clientsession(hass) async_add_entities( - [ - AugustCamera(data, doorbell, session, DEFAULT_TIMEOUT) - for doorbell in data.doorbells - ] + AugustCamera(data, doorbell, session, DEFAULT_TIMEOUT) + for doorbell in data.doorbells ) diff --git a/homeassistant/components/august/const.py b/homeassistant/components/august/const.py index ac6f463467f..9a724d4a87b 100644 --- a/homeassistant/components/august/const.py +++ b/homeassistant/components/august/const.py @@ -19,8 +19,6 @@ MANUFACTURER = "August Home Inc." DEFAULT_AUGUST_CONFIG_FILE = ".august.conf" -DATA_AUGUST = "data_august" - DEFAULT_NAME = "August" DOMAIN = "august" diff --git a/homeassistant/components/august/diagnostics.py b/homeassistant/components/august/diagnostics.py new file mode 100644 index 00000000000..ffd62cd8fb7 --- /dev/null +++ b/homeassistant/components/august/diagnostics.py @@ -0,0 +1,47 @@ +"""Diagnostics support for august.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.components.diagnostics import async_redact_data +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from . import AugustData +from .const import DOMAIN + +TO_REDACT = { + "HouseID", + "OfflineKeys", + "installUserID", + "invitations", + "key", + "pins", + "pubsubChannel", + "recentImage", + "remoteOperateSecret", + "users", + "zWaveDSK", +} + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, entry: ConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + data: AugustData = hass.data[DOMAIN][entry.entry_id] + + return { + "locks": { + lock.device_id: async_redact_data( + data.get_device_detail(lock.device_id).raw, TO_REDACT + ) + for lock in data.locks + }, + "doorbells": { + doorbell.device_id: async_redact_data( + data.get_device_detail(doorbell.device_id).raw, TO_REDACT + ) + for doorbell in data.doorbells + }, + } diff --git a/homeassistant/components/august/lock.py b/homeassistant/components/august/lock.py index e30f8301a8f..c993cf03b89 100644 --- a/homeassistant/components/august/lock.py +++ b/homeassistant/components/august/lock.py @@ -15,7 +15,7 @@ from homeassistant.helpers.restore_state import RestoreEntity import homeassistant.util.dt as dt_util from . import AugustData -from .const import DATA_AUGUST, DOMAIN +from .const import DOMAIN from .entity import AugustEntityMixin _LOGGER = logging.getLogger(__name__) @@ -29,8 +29,8 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up August locks.""" - data: AugustData = hass.data[DOMAIN][config_entry.entry_id][DATA_AUGUST] - async_add_entities([AugustLock(data, lock) for lock in data.locks]) + data: AugustData = hass.data[DOMAIN][config_entry.entry_id] + async_add_entities(AugustLock(data, lock) for lock in data.locks) class AugustLock(AugustEntityMixin, RestoreEntity, LockEntity): diff --git a/homeassistant/components/august/manifest.json b/homeassistant/components/august/manifest.json index 1eb923b91a8..329522bef28 100644 --- a/homeassistant/components/august/manifest.json +++ b/homeassistant/components/august/manifest.json @@ -2,7 +2,7 @@ "domain": "august", "name": "August", "documentation": "https://www.home-assistant.io/integrations/august", - "requirements": ["yalexs==1.1.22"], + "requirements": ["yalexs==1.1.23"], "codeowners": ["@bdraco"], "dhcp": [ { diff --git a/homeassistant/components/august/sensor.py b/homeassistant/components/august/sensor.py index cd95e1c9926..29f17c69661 100644 --- a/homeassistant/components/august/sensor.py +++ b/homeassistant/components/august/sensor.py @@ -31,7 +31,6 @@ from .const import ( ATTR_OPERATION_KEYPAD, ATTR_OPERATION_METHOD, ATTR_OPERATION_REMOTE, - DATA_AUGUST, DOMAIN, OPERATION_METHOD_AUTORELOCK, OPERATION_METHOD_KEYPAD, @@ -53,19 +52,19 @@ def _retrieve_linked_keypad_battery_state(detail: KeypadDetail) -> int | None: return detail.battery_percentage -T = TypeVar("T", LockDetail, KeypadDetail) +_T = TypeVar("_T", LockDetail, KeypadDetail) @dataclass -class AugustRequiredKeysMixin(Generic[T]): +class AugustRequiredKeysMixin(Generic[_T]): """Mixin for required keys.""" - value_fn: Callable[[T], int | None] + value_fn: Callable[[_T], int | None] @dataclass class AugustSensorEntityDescription( - SensorEntityDescription, AugustRequiredKeysMixin[T] + SensorEntityDescription, AugustRequiredKeysMixin[_T] ): """Describes August sensor entity.""" @@ -93,7 +92,7 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the August sensors.""" - data: AugustData = hass.data[DOMAIN][config_entry.entry_id][DATA_AUGUST] + data: AugustData = hass.data[DOMAIN][config_entry.entry_id] entities: list[SensorEntity] = [] migrate_unique_id_devices = [] operation_sensors = [] @@ -256,10 +255,10 @@ class AugustOperatorSensor(AugustEntityMixin, RestoreEntity, SensorEntity): return f"{self._device_id}_lock_operator" -class AugustBatterySensor(AugustEntityMixin, SensorEntity, Generic[T]): +class AugustBatterySensor(AugustEntityMixin, SensorEntity, Generic[_T]): """Representation of an August sensor.""" - entity_description: AugustSensorEntityDescription[T] + entity_description: AugustSensorEntityDescription[_T] _attr_device_class = SensorDeviceClass.BATTERY _attr_native_unit_of_measurement = PERCENTAGE @@ -268,7 +267,7 @@ class AugustBatterySensor(AugustEntityMixin, SensorEntity, Generic[T]): data: AugustData, device, old_device, - description: AugustSensorEntityDescription[T], + description: AugustSensorEntityDescription[_T], ): """Initialize the sensor.""" super().__init__(data, device) diff --git a/homeassistant/components/august/translations/fr.json b/homeassistant/components/august/translations/fr.json index 8b61f7b3267..78631c8daa1 100644 --- a/homeassistant/components/august/translations/fr.json +++ b/homeassistant/components/august/translations/fr.json @@ -6,7 +6,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/aurora/__init__.py b/homeassistant/components/aurora/__init__.py index a029f2cf61b..01d6092a4f2 100644 --- a/homeassistant/components/aurora/__init__.py +++ b/homeassistant/components/aurora/__init__.py @@ -118,7 +118,7 @@ class AuroraDataUpdateCoordinator(DataUpdateCoordinator): raise UpdateFailed(f"Error updating from NOAA: {error}") from error -class AuroraEntity(CoordinatorEntity): +class AuroraEntity(CoordinatorEntity[AuroraDataUpdateCoordinator]): """Implementation of the base Aurora Entity.""" _attr_extra_state_attributes = {"attribution": ATTRIBUTION} diff --git a/homeassistant/components/aurora/strings.json b/homeassistant/components/aurora/strings.json index 31af19748d6..92b8422d524 100644 --- a/homeassistant/components/aurora/strings.json +++ b/homeassistant/components/aurora/strings.json @@ -1,26 +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%]" - } + "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 (%)" - } + "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/components/aurora_abb_powerone/manifest.json b/homeassistant/components/aurora_abb_powerone/manifest.json index d3ab0022a70..056f2dc98c2 100644 --- a/homeassistant/components/aurora_abb_powerone/manifest.json +++ b/homeassistant/components/aurora_abb_powerone/manifest.json @@ -4,9 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/aurora_abb_powerone", "requirements": ["aurorapy==0.2.6"], - "codeowners": [ - "@davet2001" - ], + "codeowners": ["@davet2001"], "iot_class": "local_polling", "loggers": ["aurorapy"] } diff --git a/homeassistant/components/aurora_abb_powerone/translations/fr.json b/homeassistant/components/aurora_abb_powerone/translations/fr.json index d87822fb7c3..206167a5669 100644 --- a/homeassistant/components/aurora_abb_powerone/translations/fr.json +++ b/homeassistant/components/aurora_abb_powerone/translations/fr.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", - "no_serial_ports": "Aucun port com trouv\u00e9. Besoin d'un p\u00e9riph\u00e9rique RS485 valide pour communiquer." + "no_serial_ports": "Aucun port com trouv\u00e9. Un p\u00e9riph\u00e9rique RS485 valide est requis pour communiquer." }, "error": { "cannot_connect": "Connexion impossible, veuillez v\u00e9rifier le port s\u00e9rie, l'adresse, la connexion \u00e9lectrique et que l'onduleur est allum\u00e9 (\u00e0 la lumi\u00e8re du jour)", diff --git a/homeassistant/components/aussie_broadband/__init__.py b/homeassistant/components/aussie_broadband/__init__.py index f3a07616d93..6136ff6f8f3 100644 --- a/homeassistant/components/aussie_broadband/__init__.py +++ b/homeassistant/components/aussie_broadband/__init__.py @@ -6,6 +6,7 @@ import logging from aiohttp import ClientError from aussiebb.asyncio import AussieBB +from aussiebb.const import FETCH_TYPES from aussiebb.exceptions import AuthenticationException, UnrecognisedServiceType from homeassistant.config_entries import ConfigEntry @@ -31,7 +32,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) try: await client.login() - services = await client.get_services() + services = await client.get_services(drop_types=FETCH_TYPES) except AuthenticationException as exc: raise ConfigEntryAuthFailed() from exc except ClientError as exc: diff --git a/homeassistant/components/aussie_broadband/config_flow.py b/homeassistant/components/aussie_broadband/config_flow.py index 5eaf39853b5..6e101250386 100644 --- a/homeassistant/components/aussie_broadband/config_flow.py +++ b/homeassistant/components/aussie_broadband/config_flow.py @@ -5,6 +5,7 @@ from typing import Any from aiohttp import ClientError from aussiebb.asyncio import AussieBB, AuthenticationException +from aussiebb.const import FETCH_TYPES import voluptuous as vol from homeassistant import config_entries @@ -54,7 +55,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._abort_if_unique_id_configured() self.data = user_input - self.services = await self.client.get_services() # type: ignore[union-attr] + self.services = await self.client.get_services(drop_types=FETCH_TYPES) # type: ignore[union-attr] if not self.services: return self.async_abort(reason="no_services_found") diff --git a/homeassistant/components/aussie_broadband/manifest.json b/homeassistant/components/aussie_broadband/manifest.json index 5476371f755..fdb37181f52 100644 --- a/homeassistant/components/aussie_broadband/manifest.json +++ b/homeassistant/components/aussie_broadband/manifest.json @@ -3,15 +3,8 @@ "name": "Aussie Broadband", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/aussie_broadband", - "requirements": [ - "pyaussiebb==0.0.11" - ], - "codeowners": [ - "@nickw444", - "@Bre77" - ], + "requirements": ["pyaussiebb==0.0.15"], + "codeowners": ["@nickw444", "@Bre77"], "iot_class": "cloud_polling", - "loggers": [ - "aussiebb" - ] -} \ No newline at end of file + "loggers": ["aussiebb"] +} diff --git a/homeassistant/components/aussie_broadband/strings.json b/homeassistant/components/aussie_broadband/strings.json index d5b7d2f1fa1..c2052defa81 100644 --- a/homeassistant/components/aussie_broadband/strings.json +++ b/homeassistant/components/aussie_broadband/strings.json @@ -47,4 +47,4 @@ "unknown": "[%key:common::config_flow::error::unknown%]" } } -} \ No newline at end of file +} diff --git a/homeassistant/components/aussie_broadband/translations/fr.json b/homeassistant/components/aussie_broadband/translations/fr.json index 518f05e8ac3..d540e5fd2f9 100644 --- a/homeassistant/components/aussie_broadband/translations/fr.json +++ b/homeassistant/components/aussie_broadband/translations/fr.json @@ -6,8 +6,8 @@ "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" }, "error": { - "cannot_connect": "Impossible de se connecter", - "invalid_auth": "Authentification invalide", + "cannot_connect": "\u00c9chec de connexion", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "step": { @@ -18,6 +18,13 @@ "description": "Mettre \u00e0 jour le mot de passe pour {username}", "title": "R\u00e9-authentifier l'int\u00e9gration" }, + "reauth_confirm": { + "data": { + "password": "Mot de passe" + }, + "description": "Mettre \u00e0 jour le mot de passe pour {username}", + "title": "R\u00e9-authentifier l'int\u00e9gration" + }, "service": { "data": { "services": "Services" @@ -34,8 +41,8 @@ }, "options": { "abort": { - "cannot_connect": "Impossible de se connecter", - "invalid_auth": "Authentification invalide", + "cannot_connect": "\u00c9chec de connexion", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index a8d009ac2bb..3c9cd07a146 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -54,6 +54,7 @@ from homeassistant.helpers.script import ( CONF_MAX, CONF_MAX_EXCEEDED, Script, + script_stack_cv, ) from homeassistant.helpers.script_variables import ScriptVariables from homeassistant.helpers.service import ( @@ -505,6 +506,10 @@ class AutomationEntity(ToggleEntity, RestoreEntity): EVENT_AUTOMATION_TRIGGERED, event_data, context=trigger_context ) + # Make a new empty script stack; automations are allowed + # to recursively trigger themselves + script_stack_cv.set([]) + try: with trace_path("action"): await self.action_script.async_run( diff --git a/homeassistant/components/automation/translations/fr.json b/homeassistant/components/automation/translations/fr.json index 30426582414..62731da356a 100644 --- a/homeassistant/components/automation/translations/fr.json +++ b/homeassistant/components/automation/translations/fr.json @@ -1,8 +1,8 @@ { "state": { "_": { - "off": "Inactif", - "on": "Actif" + "off": "D\u00e9sactiv\u00e9", + "on": "Activ\u00e9" } }, "title": "Automatisation" diff --git a/homeassistant/components/awair/config_flow.py b/homeassistant/components/awair/config_flow.py index 4c4ccad8f52..1eff98dd78d 100644 --- a/homeassistant/components/awair/config_flow.py +++ b/homeassistant/components/awair/config_flow.py @@ -17,23 +17,6 @@ class AwairFlowHandler(ConfigFlow, domain=DOMAIN): 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 = {} diff --git a/homeassistant/components/awair/manifest.json b/homeassistant/components/awair/manifest.json index 085f2573a21..57b3c242620 100644 --- a/homeassistant/components/awair/manifest.json +++ b/homeassistant/components/awair/manifest.json @@ -2,7 +2,7 @@ "domain": "awair", "name": "Awair", "documentation": "https://www.home-assistant.io/integrations/awair", - "requirements": ["python_awair==0.2.1"], + "requirements": ["python_awair==0.2.3"], "codeowners": ["@ahayworth", "@danielsjf"], "config_flow": true, "iot_class": "cloud_polling", diff --git a/homeassistant/components/awair/sensor.py b/homeassistant/components/awair/sensor.py index 89c505bd4b9..ddf76c0e93d 100644 --- a/homeassistant/components/awair/sensor.py +++ b/homeassistant/components/awair/sensor.py @@ -3,22 +3,14 @@ from __future__ import annotations from python_awair.air_data import AirData from python_awair.devices import AwairDevice -import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import ( - ATTR_ATTRIBUTION, - ATTR_CONNECTIONS, - ATTR_NAME, - CONF_ACCESS_TOKEN, -) +from homeassistant.components.sensor import SensorEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_ATTRIBUTION, ATTR_CONNECTIONS, ATTR_NAME from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import AwairDataUpdateCoordinator, AwairResult @@ -31,37 +23,12 @@ from .const import ( ATTRIBUTION, DOMAIN, DUST_ALIASES, - LOGGER, SENSOR_TYPE_SCORE, SENSOR_TYPES, SENSOR_TYPES_DUST, AwairSensorEntityDescription, ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - {vol.Required(CONF_ACCESS_TOKEN): cv.string}, - extra=vol.ALLOW_EXTRA, -) - - -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> 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, @@ -102,7 +69,7 @@ async def async_setup_entry( async_add_entities(entities) -class AwairSensor(CoordinatorEntity, SensorEntity): +class AwairSensor(CoordinatorEntity[AwairDataUpdateCoordinator], SensorEntity): """Defines an Awair sensor entity.""" entity_description: AwairSensorEntityDescription diff --git a/homeassistant/components/awair/translations/fr.json b/homeassistant/components/awair/translations/fr.json index 65d550b52a6..7182117fa53 100644 --- a/homeassistant/components/awair/translations/fr.json +++ b/homeassistant/components/awair/translations/fr.json @@ -13,14 +13,14 @@ "reauth": { "data": { "access_token": "Jeton d'acc\u00e8s", - "email": "Email" + "email": "Courriel" }, "description": "Veuillez ressaisir votre jeton d'acc\u00e8s d\u00e9veloppeur Awair." }, "user": { "data": { "access_token": "Jeton d'acc\u00e8s", - "email": "Email" + "email": "Courriel" }, "description": "Vous devez vous inscrire pour un jeton d'acc\u00e8s d\u00e9veloppeur Awair sur: https://developer.getawair.com/onboard/login" } diff --git a/homeassistant/components/axis/config_flow.py b/homeassistant/components/axis/config_flow.py index 5ec68073610..23b1ba4c753 100644 --- a/homeassistant/components/axis/config_flow.py +++ b/homeassistant/components/axis/config_flow.py @@ -11,6 +11,7 @@ from homeassistant.config_entries import SOURCE_IGNORE from homeassistant.const import ( CONF_HOST, CONF_MAC, + CONF_MODEL, CONF_NAME, CONF_PASSWORD, CONF_PORT, @@ -22,7 +23,6 @@ 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, diff --git a/homeassistant/components/axis/const.py b/homeassistant/components/axis/const.py index c9267568707..a13cae0dd0a 100644 --- a/homeassistant/components/axis/const.py +++ b/homeassistant/components/axis/const.py @@ -10,7 +10,6 @@ DOMAIN = "axis" ATTR_MANUFACTURER = "Axis Communications AB" CONF_EVENTS = "events" -CONF_MODEL = "model" CONF_STREAM_PROFILE = "stream_profile" CONF_VIDEO_SOURCE = "video_source" diff --git a/homeassistant/components/axis/device.py b/homeassistant/components/axis/device.py index 3a101753445..2338a90d562 100644 --- a/homeassistant/components/axis/device.py +++ b/homeassistant/components/axis/device.py @@ -16,6 +16,7 @@ from homeassistant.components.mqtt.models import ReceiveMessage from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_HOST, + CONF_MODEL, CONF_NAME, CONF_PASSWORD, CONF_PORT, @@ -33,7 +34,6 @@ from homeassistant.setup import async_when_setup from .const import ( ATTR_MANUFACTURER, CONF_EVENTS, - CONF_MODEL, CONF_STREAM_PROFILE, CONF_VIDEO_SOURCE, DEFAULT_EVENTS, diff --git a/homeassistant/components/axis/manifest.json b/homeassistant/components/axis/manifest.json index c07db62a04f..c69d7346b9e 100644 --- a/homeassistant/components/axis/manifest.json +++ b/homeassistant/components/axis/manifest.json @@ -5,7 +5,7 @@ "documentation": "https://www.home-assistant.io/integrations/axis", "requirements": ["axis==44"], "dhcp": [ - {"registered_devices": true}, + { "registered_devices": true }, { "hostname": "axis-00408c*", "macaddress": "00408C*" @@ -27,15 +27,15 @@ "zeroconf": [ { "type": "_axis-video._tcp.local.", - "properties": {"macaddress": "00408c*"} + "properties": { "macaddress": "00408c*" } }, { "type": "_axis-video._tcp.local.", - "properties": {"macaddress": "accc8e*"} + "properties": { "macaddress": "accc8e*" } }, { "type": "_axis-video._tcp.local.", - "properties": {"macaddress": "b8a44f*"} + "properties": { "macaddress": "b8a44f*" } } ], "after_dependencies": ["mqtt"], diff --git a/homeassistant/components/axis/translations/fr.json b/homeassistant/components/axis/translations/fr.json index ea3f93feb50..b22a14eeb69 100644 --- a/homeassistant/components/axis/translations/fr.json +++ b/homeassistant/components/axis/translations/fr.json @@ -9,7 +9,7 @@ "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours", "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide" + "invalid_auth": "Authentification non valide" }, "flow_title": "Appareil Axis: {name} ( {host} )", "step": { diff --git a/homeassistant/components/azure_devops/__init__.py b/homeassistant/components/azure_devops/__init__.py index 1b1c65ae6d1..c6de8515979 100644 --- a/homeassistant/components/azure_devops/__init__.py +++ b/homeassistant/components/azure_devops/__init__.py @@ -94,10 +94,9 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return unload_ok -class AzureDevOpsEntity(CoordinatorEntity): +class AzureDevOpsEntity(CoordinatorEntity[DataUpdateCoordinator[list[DevOpsBuild]]]): """Defines a base Azure DevOps entity.""" - coordinator: DataUpdateCoordinator[list[DevOpsBuild]] entity_description: AzureDevOpsEntityDescription def __init__( diff --git a/homeassistant/components/azure_devops/translations/fr.json b/homeassistant/components/azure_devops/translations/fr.json index 17bc6104112..311ed28d8e9 100644 --- a/homeassistant/components/azure_devops/translations/fr.json +++ b/homeassistant/components/azure_devops/translations/fr.json @@ -6,7 +6,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "project_error": "Impossible d'obtenir les informations sur le projet." }, "flow_title": "{project_url}", diff --git a/homeassistant/components/azure_event_hub/strings.json b/homeassistant/components/azure_event_hub/strings.json index 3bfc60b6305..716b691959e 100644 --- a/homeassistant/components/azure_event_hub/strings.json +++ b/homeassistant/components/azure_event_hub/strings.json @@ -1,49 +1,49 @@ { - "config": { - "step": { - "user": { - "title": "Setup your Azure Event Hub integration", - "data": { - "event_hub_instance_name": "Event Hub Instance Name", - "use_connection_string": "Use Connection String" - } - }, - "conn_string": { - "title": "Connection String method", - "description": "Please enter the connection string for: {event_hub_instance_name}", - "data": { - "event_hub_connection_string": "Event Hub Connection String" - } - }, - "sas": { - "title": "SAS Credentials method", - "description": "Please enter the SAS (shared access signature) credentials for: {event_hub_instance_name}", - "data": { - "event_hub_namespace": "Event Hub Namespace", - "event_hub_sas_policy": "Event Hub SAS Policy", - "event_hub_sas_key": "Event Hub SAS Key" - } + "config": { + "step": { + "user": { + "title": "Setup your Azure Event Hub integration", + "data": { + "event_hub_instance_name": "Event Hub Instance Name", + "use_connection_string": "Use Connection String" } }, - "error": { - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", - "unknown": "[%key:common::config_flow::error::unknown%]" + "conn_string": { + "title": "Connection String method", + "description": "Please enter the connection string for: {event_hub_instance_name}", + "data": { + "event_hub_connection_string": "Event Hub Connection String" + } }, - "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_service%]", - "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", - "cannot_connect": "Connecting with the credentials from the configuration.yaml failed, please remove from yaml and use the config flow.", - "unknown": "Connecting with the credentials from the configuration.yaml failed with an unknown error, please remove from yaml and use the config flow." + "sas": { + "title": "SAS Credentials method", + "description": "Please enter the SAS (shared access signature) credentials for: {event_hub_instance_name}", + "data": { + "event_hub_namespace": "Event Hub Namespace", + "event_hub_sas_policy": "Event Hub SAS Policy", + "event_hub_sas_key": "Event Hub SAS Key" + } } }, - "options": { - "step": { - "options": { - "title": "Options for the Azure Event Hub.", - "data": { - "send_interval": "Interval between sending batches to the hub." - } + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_service%]", + "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", + "cannot_connect": "Connecting with the credentials from the configuration.yaml failed, please remove from yaml and use the config flow.", + "unknown": "Connecting with the credentials from the configuration.yaml failed with an unknown error, please remove from yaml and use the config flow." + } + }, + "options": { + "step": { + "options": { + "title": "Options for the Azure Event Hub.", + "data": { + "send_interval": "Interval between sending batches to the hub." } } } - } \ No newline at end of file + } +} diff --git a/homeassistant/components/azure_event_hub/translations/fr.json b/homeassistant/components/azure_event_hub/translations/fr.json index 2cfeb2f4d32..9e238c3836d 100644 --- a/homeassistant/components/azure_event_hub/translations/fr.json +++ b/homeassistant/components/azure_event_hub/translations/fr.json @@ -7,7 +7,7 @@ "unknown": "La connexion avec les informations d'identification du fichier configuration.yaml a \u00e9chou\u00e9 avec une erreur inconnue, veuillez supprimer de yaml et utiliser le flux de configuration." }, "error": { - "cannot_connect": "Impossible de se connecter", + "cannot_connect": "\u00c9chec de connexion", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/azure_event_hub/translations/nl.json b/homeassistant/components/azure_event_hub/translations/nl.json index 92b3eee0a8d..646d820c2ad 100644 --- a/homeassistant/components/azure_event_hub/translations/nl.json +++ b/homeassistant/components/azure_event_hub/translations/nl.json @@ -2,9 +2,9 @@ "config": { "abort": { "already_configured": "Service is al geconfigureerd", - "cannot_connect": "Verbinding maken met de credentails uit de configuration.yaml is mislukt, verwijder deze uit de yaml en gebruik de config flow.", + "cannot_connect": "Verbinding maken met de credentials uit de configuration.yaml is mislukt, verwijder deze uit yaml en gebruik de config flow.", "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk.", - "unknown": "Verbinding maken met de credentails uit de configuration.yaml is mislukt met een onbekende fout, verwijder a.u.b. de yaml en gebruik de config flow." + "unknown": "Verbinding maken met de credentials uit de configuration.yaml is mislukt met een onbekende fout, verwijder deze uit yaml en gebruik de config flow." }, "error": { "cannot_connect": "Kan geen verbinding maken", diff --git a/homeassistant/components/backup/__init__.py b/homeassistant/components/backup/__init__.py new file mode 100644 index 00000000000..711051507d4 --- /dev/null +++ b/homeassistant/components/backup/__init__.py @@ -0,0 +1,26 @@ +"""The Backup integration.""" +from homeassistant.components.hassio import is_hassio +from homeassistant.core import HomeAssistant +from homeassistant.helpers.typing import ConfigType + +from .const import DOMAIN, LOGGER +from .http import async_register_http_views +from .manager import BackupManager +from .websocket import async_register_websocket_handlers + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up the Backup integration.""" + if is_hassio(hass): + LOGGER.error( + "The backup integration is not supported on this installation method, " + "please remove it from your configuration" + ) + return False + + hass.data[DOMAIN] = BackupManager(hass) + + async_register_websocket_handlers(hass) + async_register_http_views(hass) + + return True diff --git a/homeassistant/components/backup/const.py b/homeassistant/components/backup/const.py new file mode 100644 index 00000000000..a4a08fff75d --- /dev/null +++ b/homeassistant/components/backup/const.py @@ -0,0 +1,15 @@ +"""Constants for the Backup integration.""" +from logging import getLogger + +DOMAIN = "backup" +LOGGER = getLogger(__package__) + +EXCLUDE_FROM_BACKUP = [ + "__pycache__/*", + ".DS_Store", + "*.db-shm", + "*.log.*", + "*.log", + "backups/*.tar", + "OZW_Log.txt", +] diff --git a/homeassistant/components/backup/http.py b/homeassistant/components/backup/http.py new file mode 100644 index 00000000000..6f1f0d00167 --- /dev/null +++ b/homeassistant/components/backup/http.py @@ -0,0 +1,49 @@ +"""Http view for the Backup integration.""" +from __future__ import annotations + +from http import HTTPStatus + +from aiohttp.hdrs import CONTENT_DISPOSITION +from aiohttp.web import FileResponse, Request, Response + +from homeassistant.components.http.view import HomeAssistantView +from homeassistant.core import HomeAssistant, callback +from homeassistant.util import slugify + +from .const import DOMAIN +from .manager import BackupManager + + +@callback +def async_register_http_views(hass: HomeAssistant) -> None: + """Register the http views.""" + hass.http.register_view(DownloadBackupView) + + +class DownloadBackupView(HomeAssistantView): + """Generate backup view.""" + + url = "/api/backup/download/{slug}" + name = "api:backup:download" + + async def get( # pylint: disable=no-self-use + self, + request: Request, + slug: str, + ) -> FileResponse | Response: + """Download a backup file.""" + if not request["hass_user"].is_admin: + return Response(status=HTTPStatus.UNAUTHORIZED) + + manager: BackupManager = request.app["hass"].data[DOMAIN] + backup = await manager.get_backup(slug) + + if backup is None or not backup.path.exists(): + return Response(status=HTTPStatus.NOT_FOUND) + + return FileResponse( + path=backup.path.as_posix(), + headers={ + CONTENT_DISPOSITION: f"attachment; filename={slugify(backup.name)}.tar" + }, + ) diff --git a/homeassistant/components/backup/manager.py b/homeassistant/components/backup/manager.py new file mode 100644 index 00000000000..eee9919e711 --- /dev/null +++ b/homeassistant/components/backup/manager.py @@ -0,0 +1,248 @@ +"""Backup manager for the Backup integration.""" +from __future__ import annotations + +import asyncio +from dataclasses import asdict, dataclass +import hashlib +import json +from pathlib import Path +import tarfile +from tarfile import TarError +from tempfile import TemporaryDirectory +from typing import Any, Protocol + +from securetar import SecureTarFile, atomic_contents_add + +from homeassistant.const import __version__ as HAVERSION +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import integration_platform +from homeassistant.util import dt, json as json_util + +from .const import DOMAIN, EXCLUDE_FROM_BACKUP, LOGGER + + +@dataclass +class Backup: + """Backup class.""" + + slug: str + name: str + date: str + path: Path + size: float + + def as_dict(self) -> dict: + """Return a dict representation of this backup.""" + return {**asdict(self), "path": self.path.as_posix()} + + +class BackupPlatformProtocol(Protocol): + """Define the format that backup platforms can have.""" + + async def async_pre_backup(self, hass: HomeAssistant) -> None: + """Perform operations before a backup starts.""" + + async def async_post_backup(self, hass: HomeAssistant) -> None: + """Perform operations after a backup finishes.""" + + +class BackupManager: + """Backup manager for the Backup integration.""" + + def __init__(self, hass: HomeAssistant) -> None: + """Initialize the backup manager.""" + self.hass = hass + self.backup_dir = Path(hass.config.path("backups")) + self.backing_up = False + self.backups: dict[str, Backup] = {} + self.platforms: dict[str, BackupPlatformProtocol] = {} + self.loaded_backups = False + self.loaded_platforms = False + + async def _add_platform( + self, + hass: HomeAssistant, + integration_domain: str, + platform: BackupPlatformProtocol, + ) -> None: + """Add a platform to the backup manager.""" + if not hasattr(platform, "async_pre_backup") or not hasattr( + platform, "async_post_backup" + ): + LOGGER.warning( + "%s does not implement required functions for the backup platform", + integration_domain, + ) + return + self.platforms[integration_domain] = platform + + async def load_backups(self) -> None: + """Load data of stored backup files.""" + backups = await self.hass.async_add_executor_job(self._read_backups) + LOGGER.debug("Loaded %s backups", len(backups)) + self.backups = backups + self.loaded_backups = True + + async def load_platforms(self) -> None: + """Load backup platforms.""" + await integration_platform.async_process_integration_platforms( + self.hass, DOMAIN, self._add_platform + ) + LOGGER.debug("Loaded %s platforms", len(self.platforms)) + self.loaded_platforms = True + + def _read_backups(self) -> dict[str, Backup]: + """Read backups from disk.""" + backups: dict[str, Backup] = {} + for backup_path in self.backup_dir.glob("*.tar"): + try: + with tarfile.open(backup_path, "r:") as backup_file: + if data_file := backup_file.extractfile("./backup.json"): + data = json.loads(data_file.read()) + backup = Backup( + slug=data["slug"], + name=data["name"], + date=data["date"], + path=backup_path, + size=round(backup_path.stat().st_size / 1_048_576, 2), + ) + backups[backup.slug] = backup + except (OSError, TarError, json.JSONDecodeError) as err: + LOGGER.warning("Unable to read backup %s: %s", backup_path, err) + return backups + + async def get_backups(self) -> dict[str, Backup]: + """Return backups.""" + if not self.loaded_backups: + await self.load_backups() + + return self.backups + + async def get_backup(self, slug: str) -> Backup | None: + """Return a backup.""" + if not self.loaded_backups: + await self.load_backups() + + if not (backup := self.backups.get(slug)): + return None + + if not backup.path.exists(): + LOGGER.debug( + "Removing tracked backup (%s) that does not exists on the expected path %s", + backup.slug, + backup.path, + ) + self.backups.pop(slug) + return None + + return backup + + async def remove_backup(self, slug: str) -> None: + """Remove a backup.""" + if (backup := await self.get_backup(slug)) is None: + return + + await self.hass.async_add_executor_job(backup.path.unlink, True) + LOGGER.debug("Removed backup located at %s", backup.path) + self.backups.pop(slug) + + async def generate_backup(self) -> Backup: + """Generate a backup.""" + if self.backing_up: + raise HomeAssistantError("Backup already in progress") + + if not self.loaded_platforms: + await self.load_platforms() + + try: + self.backing_up = True + pre_backup_results = await asyncio.gather( + *( + platform.async_pre_backup(self.hass) + for platform in self.platforms.values() + ), + return_exceptions=True, + ) + for result in pre_backup_results: + if isinstance(result, Exception): + raise result + + backup_name = f"Core {HAVERSION}" + date_str = dt.now().isoformat() + slug = _generate_slug(date_str, backup_name) + + backup_data = { + "slug": slug, + "name": backup_name, + "date": date_str, + "type": "partial", + "folders": ["homeassistant"], + "homeassistant": {"version": HAVERSION}, + "compressed": True, + } + tar_file_path = Path(self.backup_dir, f"{backup_data['slug']}.tar") + + if not self.backup_dir.exists(): + LOGGER.debug("Creating backup directory") + self.hass.async_add_executor_job(self.backup_dir.mkdir) + + await self.hass.async_add_executor_job( + self._generate_backup_contents, + tar_file_path, + backup_data, + ) + backup = Backup( + slug=slug, + name=backup_name, + date=date_str, + path=tar_file_path, + size=round(tar_file_path.stat().st_size / 1_048_576, 2), + ) + if self.loaded_backups: + self.backups[slug] = backup + LOGGER.debug("Generated new backup with slug %s", slug) + return backup + finally: + self.backing_up = False + post_backup_results = await asyncio.gather( + *( + platform.async_post_backup(self.hass) + for platform in self.platforms.values() + ), + return_exceptions=True, + ) + for result in post_backup_results: + if isinstance(result, Exception): + raise result + + def _generate_backup_contents( + self, + tar_file_path: Path, + backup_data: dict[str, Any], + ) -> None: + """Generate backup contents.""" + with TemporaryDirectory() as tmp_dir, SecureTarFile( + tar_file_path, "w", gzip=False + ) as tar_file: + tmp_dir_path = Path(tmp_dir) + json_util.save_json( + tmp_dir_path.joinpath("./backup.json").as_posix(), + backup_data, + ) + with SecureTarFile( + tmp_dir_path.joinpath("./homeassistant.tar.gz").as_posix(), + "w", + ) as core_tar: + atomic_contents_add( + tar_file=core_tar, + origin_path=Path(self.hass.config.path()), + excludes=EXCLUDE_FROM_BACKUP, + arcname="data", + ) + tar_file.add(tmp_dir_path, arcname=".") + + +def _generate_slug(date: str, name: str) -> str: + """Generate a backup slug.""" + return hashlib.sha1(f"{date} - {name}".lower().encode()).hexdigest()[:8] diff --git a/homeassistant/components/backup/manifest.json b/homeassistant/components/backup/manifest.json new file mode 100644 index 00000000000..eaf6a9fd979 --- /dev/null +++ b/homeassistant/components/backup/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "backup", + "name": "Backup", + "documentation": "https://www.home-assistant.io/integrations/backup", + "dependencies": ["http", "websocket_api"], + "codeowners": ["@home-assistant/core"], + "requirements": ["securetar==2022.2.0"], + "iot_class": "calculated", + "quality_scale": "internal" +} diff --git a/homeassistant/components/backup/websocket.py b/homeassistant/components/backup/websocket.py new file mode 100644 index 00000000000..5c12a764941 --- /dev/null +++ b/homeassistant/components/backup/websocket.py @@ -0,0 +1,69 @@ +"""Websocket commands for the Backup integration.""" +import voluptuous as vol + +from homeassistant.components import websocket_api +from homeassistant.core import HomeAssistant, callback + +from .const import DOMAIN +from .manager import BackupManager + + +@callback +def async_register_websocket_handlers(hass: HomeAssistant) -> None: + """Register websocket commands.""" + websocket_api.async_register_command(hass, handle_info) + websocket_api.async_register_command(hass, handle_create) + websocket_api.async_register_command(hass, handle_remove) + + +@websocket_api.require_admin +@websocket_api.websocket_command({vol.Required("type"): "backup/info"}) +@websocket_api.async_response +async def handle_info( + hass: HomeAssistant, + connection: websocket_api.ActiveConnection, + msg: dict, +) -> None: + """List all stored backups.""" + manager: BackupManager = hass.data[DOMAIN] + backups = await manager.get_backups() + connection.send_result( + msg["id"], + { + "backups": list(backups.values()), + "backing_up": manager.backing_up, + }, + ) + + +@websocket_api.require_admin +@websocket_api.websocket_command( + { + vol.Required("type"): "backup/remove", + vol.Required("slug"): str, + } +) +@websocket_api.async_response +async def handle_remove( + hass: HomeAssistant, + connection: websocket_api.ActiveConnection, + msg: dict, +) -> None: + """Remove a backup.""" + manager: BackupManager = hass.data[DOMAIN] + await manager.remove_backup(msg["slug"]) + connection.send_result(msg["id"]) + + +@websocket_api.require_admin +@websocket_api.websocket_command({vol.Required("type"): "backup/generate"}) +@websocket_api.async_response +async def handle_create( + hass: HomeAssistant, + connection: websocket_api.ActiveConnection, + msg: dict, +) -> None: + """Generate a backup.""" + manager: BackupManager = hass.data[DOMAIN] + backup = await manager.generate_backup() + connection.send_result(msg["id"], backup) diff --git a/homeassistant/components/balboa/const.py b/homeassistant/components/balboa/const.py index f5b28804952..9fb0b34d003 100644 --- a/homeassistant/components/balboa/const.py +++ b/homeassistant/components/balboa/const.py @@ -10,7 +10,6 @@ from homeassistant.components.climate.const import ( HVAC_MODE_HEAT, HVAC_MODE_OFF, ) -from homeassistant.components.fan import SPEED_HIGH, SPEED_LOW, SPEED_OFF from homeassistant.const import Platform _LOGGER = logging.getLogger(__name__) @@ -21,7 +20,6 @@ CLIMATE_SUPPORTED_FANSTATES = [FAN_OFF, FAN_LOW, FAN_MEDIUM, FAN_HIGH] CLIMATE_SUPPORTED_MODES = [HVAC_MODE_AUTO, HVAC_MODE_HEAT, HVAC_MODE_OFF] CONF_SYNC_TIME = "sync_time" DEFAULT_SYNC_TIME = False -FAN_SUPPORTED_SPEEDS = [SPEED_OFF, SPEED_LOW, SPEED_HIGH] PLATFORMS = [Platform.BINARY_SENSOR, Platform.CLIMATE] AUX = "Aux" diff --git a/homeassistant/components/balboa/manifest.json b/homeassistant/components/balboa/manifest.json index d6ef4094b07..b5c4636e551 100644 --- a/homeassistant/components/balboa/manifest.json +++ b/homeassistant/components/balboa/manifest.json @@ -3,12 +3,8 @@ "name": "Balboa Spa Client", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/balboa", - "requirements": [ - "pybalboa==0.13" - ], - "codeowners": [ - "@garbled1" - ], + "requirements": ["pybalboa==0.13"], + "codeowners": ["@garbled1"], "iot_class": "local_push", "loggers": ["pybalboa"] } diff --git a/homeassistant/components/bbb_gpio/__init__.py b/homeassistant/components/bbb_gpio/__init__.py deleted file mode 100644 index 511292cfb6f..00000000000 --- a/homeassistant/components/bbb_gpio/__init__.py +++ /dev/null @@ -1,63 +0,0 @@ -"""Support for controlling GPIO pins of a Beaglebone Black.""" -import logging - -from Adafruit_BBIO import GPIO # pylint: disable=import-error - -from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP -from homeassistant.core import HomeAssistant -from homeassistant.helpers.typing import ConfigType - -DOMAIN = "bbb_gpio" - -_LOGGER = logging.getLogger(__name__) - - -def setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up the BeagleBone Black GPIO component.""" - _LOGGER.warning( - "The BeagleBone Black GPIO integration is deprecated and will be removed " - "in Home Assistant Core 2022.4; this integration is removed under " - "Architectural Decision Record 0019, more information can be found here: " - "https://github.com/home-assistant/architecture/blob/master/adr/0019-GPIO.md" - ) - - def cleanup_gpio(event): - """Stuff to do before stopping.""" - 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/components/bbb_gpio/binary_sensor.py b/homeassistant/components/bbb_gpio/binary_sensor.py deleted file mode 100644 index 360d9d84376..00000000000 --- a/homeassistant/components/bbb_gpio/binary_sensor.py +++ /dev/null @@ -1,79 +0,0 @@ -"""Support for binary sensor using Beaglebone Black GPIO.""" -from __future__ import annotations - -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 -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 - -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: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> 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.""" - - _attr_should_poll = False - - def __init__(self, pin, params): - """Initialize the Beaglebone Black binary sensor.""" - self._pin = pin - self._attr_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 is_on(self) -> bool: - """Return the state of the entity.""" - return self._state != self._invert_logic diff --git a/homeassistant/components/bbb_gpio/manifest.json b/homeassistant/components/bbb_gpio/manifest.json deleted file mode 100644 index c57530a9bf8..00000000000 --- a/homeassistant/components/bbb_gpio/manifest.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "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", - "loggers": ["Adafruit_BBIO"] -} diff --git a/homeassistant/components/bbb_gpio/switch.py b/homeassistant/components/bbb_gpio/switch.py deleted file mode 100644 index fc830d2a1a8..00000000000 --- a/homeassistant/components/bbb_gpio/switch.py +++ /dev/null @@ -1,80 +0,0 @@ -"""Allows to configure a switch using BeagleBone Black GPIO.""" -from __future__ import annotations - -import voluptuous as vol - -from homeassistant.components import bbb_gpio -from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity -from homeassistant.const import CONF_NAME, DEVICE_DEFAULT_NAME -from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType - -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: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> 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(SwitchEntity): - """Representation of a BeagleBone Black GPIO.""" - - _attr_should_poll = False - - def __init__(self, pin, params): - """Initialize the pin.""" - self._pin = pin - self._attr_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 is_on(self) -> bool: - """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/components/bh1750/__init__.py b/homeassistant/components/bh1750/__init__.py deleted file mode 100644 index ce7ecc65366..00000000000 --- a/homeassistant/components/bh1750/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The bh1750 component.""" diff --git a/homeassistant/components/bh1750/manifest.json b/homeassistant/components/bh1750/manifest.json deleted file mode 100644 index 807f7a9e05f..00000000000 --- a/homeassistant/components/bh1750/manifest.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "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", - "loggers": ["i2csense", "smbus"] -} diff --git a/homeassistant/components/bh1750/sensor.py b/homeassistant/components/bh1750/sensor.py deleted file mode 100644 index d6239f90d43..00000000000 --- a/homeassistant/components/bh1750/sensor.py +++ /dev/null @@ -1,138 +0,0 @@ -"""Support for BH1750 light sensor.""" -from __future__ import annotations - -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, - SensorDeviceClass, - SensorEntity, -) -from homeassistant.const import CONF_NAME, LIGHT_LUX -from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType - -_LOGGER = logging.getLogger(__name__) - -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: HomeAssistant, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the BH1750 sensor.""" - _LOGGER.warning( - "The BH1750 integration is deprecated and will be removed " - "in Home Assistant Core 2022.4; this integration is removed under " - "Architectural Decision Record 0019, more information can be found here: " - "https://github.com/home-assistant/architecture/blob/master/adr/0019-GPIO.md" - ) - - name = config[CONF_NAME] - bus_number = config[CONF_I2C_BUS] - 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 - - 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.""" - - _attr_device_class = SensorDeviceClass.ILLUMINANCE - - def __init__(self, bh1750_sensor, name, unit, multiplier=1.0): - """Initialize the sensor.""" - self._attr_name = name - self._attr_native_unit_of_measurement = unit - self._multiplier = multiplier - self.bh1750_sensor = bh1750_sensor - - 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._attr_native_value = 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/components/binary_sensor/manifest.json b/homeassistant/components/binary_sensor/manifest.json index be2feb9d207..d1c631ee94b 100644 --- a/homeassistant/components/binary_sensor/manifest.json +++ b/homeassistant/components/binary_sensor/manifest.json @@ -2,6 +2,6 @@ "domain": "binary_sensor", "name": "Binary Sensor", "documentation": "https://www.home-assistant.io/integrations/binary_sensor", - "codeowners": [], + "codeowners": ["@home-assistant/core"], "quality_scale": "internal" } diff --git a/homeassistant/components/binary_sensor/strings.json b/homeassistant/components/binary_sensor/strings.json index 62ff8187731..5d17fb92cb1 100644 --- a/homeassistant/components/binary_sensor/strings.json +++ b/homeassistant/components/binary_sensor/strings.json @@ -230,4 +230,4 @@ "sound": "sound", "vibration": "vibration" } -} \ No newline at end of file +} diff --git a/homeassistant/components/binary_sensor/translations/el.json b/homeassistant/components/binary_sensor/translations/el.json index 1cfe13d694b..ec78f1cd575 100644 --- a/homeassistant/components/binary_sensor/translations/el.json +++ b/homeassistant/components/binary_sensor/translations/el.json @@ -127,8 +127,8 @@ "on": "\u0395\u03bd\u03b5\u03c1\u03b3\u03cc" }, "battery": { - "off": "\u039a\u03b1\u03bd\u03bf\u03bd\u03b9\u03ba\u03cc\u03c2", - "on": "\u03a7\u03b1\u03bc\u03b7\u03bb\u03cc\u03c2" + "off": "\u039a\u03b1\u03bd\u03bf\u03bd\u03b9\u03ba\u03ae", + "on": "\u03a7\u03b1\u03bc\u03b7\u03bb\u03ae" }, "battery_charging": { "off": "\u0394\u03b5 \u03c6\u03bf\u03c1\u03c4\u03af\u03b6\u03b5\u03b9", @@ -147,7 +147,7 @@ "on": "\u039a\u03c1\u03cd\u03bf" }, "connectivity": { - "off": "\u0391\u03c0\u03bf\u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7", + "off": "\u0391\u03c0\u03bf\u03c3\u03c5\u03bd\u03b4\u03b5\u03b4\u03b5\u03bc\u03ad\u03bd\u03bf\u03c2", "on": "\u03a3\u03c5\u03bd\u03b4\u03b5\u03b4\u03b5\u03bc\u03ad\u03bd\u03bf\u03c2" }, "door": { @@ -156,7 +156,7 @@ }, "garage_door": { "off": "\u039a\u03bb\u03b5\u03b9\u03c3\u03c4\u03cc", - "on": "\u0386\u03bd\u03bf\u03b9\u03b3\u03bc\u03b1" + "on": "\u0391\u03bd\u03bf\u03b9\u03c7\u03c4\u03cc" }, "gas": { "off": "\u0394\u03b5\u03bd \u03b5\u03bd\u03c4\u03bf\u03c0\u03af\u03c3\u03c4\u03b7\u03ba\u03b5", diff --git a/homeassistant/components/binary_sensor/translations/es.json b/homeassistant/components/binary_sensor/translations/es.json index 3bf3c1a74a2..1e328150c39 100644 --- a/homeassistant/components/binary_sensor/translations/es.json +++ b/homeassistant/components/binary_sensor/translations/es.json @@ -134,6 +134,9 @@ "off": "No est\u00e1 cargando", "on": "Cargando" }, + "carbon_monoxide": { + "on": "Detectado" + }, "co": { "off": "No detectado", "on": "Detectado" diff --git a/homeassistant/components/binary_sensor/translations/fr.json b/homeassistant/components/binary_sensor/translations/fr.json index 1705fc7aed2..a9bd4ba30b1 100644 --- a/homeassistant/components/binary_sensor/translations/fr.json +++ b/homeassistant/components/binary_sensor/translations/fr.json @@ -1,7 +1,7 @@ { "device_automation": { "condition_type": { - "is_bat_low": "{entity_name} batterie faible", + "is_bat_low": "{entity_name} est faible en batterie", "is_co": "{entity_name} d\u00e9tecte du monoxyde de carbone", "is_cold": "{entity_name} est froid", "is_connected": "{entity_name} est connect\u00e9", @@ -12,7 +12,7 @@ "is_moist": "{entity_name} est humide", "is_motion": "{entity_name} d\u00e9tecte du mouvement", "is_moving": "{entity_name} se d\u00e9place", - "is_no_co": "{entity_name} ne d\u00e9tecte pas le monoxyde de carbone", + "is_no_co": "{entity_name} ne d\u00e9tecte pas de monoxyde de carbone", "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", @@ -21,7 +21,7 @@ "is_no_sound": "{entity_name} ne d\u00e9tecte pas de son", "is_no_update": "{entity_name} est \u00e0 jour", "is_no_vibration": "{entity_name} ne d\u00e9tecte pas de vibration", - "is_not_bat_low": "{entity_name} batterie normale", + "is_not_bat_low": "{entity_name} n'est pas faible en batterie", "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", @@ -34,7 +34,7 @@ "is_not_powered": "{entity_name} n'est pas aliment\u00e9", "is_not_present": "{entity_name} n'est pas pr\u00e9sent", "is_not_running": "{entity_name} n'est pas en cours d'ex\u00e9cution", - "is_not_tampered": "{entity_name} ne d\u00e9tecte pas la falsification", + "is_not_tampered": "{entity_name} ne d\u00e9tecte pas de manipulation", "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", @@ -47,48 +47,48 @@ "is_running": "{entity_name} est en cours d'ex\u00e9cution", "is_smoke": "{entity_name} d\u00e9tecte de la fum\u00e9e", "is_sound": "{entity_name} d\u00e9tecte du son", - "is_tampered": "{entity_name} d\u00e9tecte une falsification", + "is_tampered": "{entity_name} d\u00e9tecte une manipulation", "is_unsafe": "{entity_name} est dangereux", - "is_update": "{entity_name} a une mise \u00e0 jour disponible", - "is_vibration": "{entity_name} d\u00e9tecte des vibrations" + "is_update": "Une mise \u00e0 jour est disponible pour {entity_name}", + "is_vibration": "{entity_name} d\u00e9tecte une vibration" }, "trigger_type": { - "bat_low": "{entity_name} batterie faible", - "co": "{entity_name} commenc\u00e9 \u00e0 d\u00e9tecter du monoxyde de carbone", + "bat_low": "{entity_name} est devenu faible en batterie", + "co": "{entity_name} a commenc\u00e9 \u00e0 d\u00e9tecter du monoxyde de carbone", "cold": "{entity_name} est devenu froid", - "connected": "{entity_name} connect\u00e9", + "connected": "{entity_name} s'est connect\u00e9", "gas": "{entity_name} a commenc\u00e9 \u00e0 d\u00e9tecter du gaz", "hot": "{entity_name} est devenu chaud", - "is_not_tampered": "{entity_name} a cess\u00e9 de d\u00e9tecter la falsification", - "is_tampered": "{entity_name} commenc\u00e9 \u00e0 d\u00e9tecter une falsification", - "light": "{entity_name} a commenc\u00e9 \u00e0 d\u00e9tecter la lumi\u00e8re", - "locked": "{entity_name} verrouill\u00e9", + "is_not_tampered": "{entity_name} a cess\u00e9 de d\u00e9tecter une manipulation", + "is_tampered": "{entity_name} a commenc\u00e9 \u00e0 d\u00e9tecter une manipulation", + "light": "{entity_name} a commenc\u00e9 \u00e0 d\u00e9tecter de la lumi\u00e8re", + "locked": "{entity_name} s'est 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_co": "{entity_name} cess\u00e9 de d\u00e9tecter le monoxyde de carbone", - "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_co": "{entity_name} a cess\u00e9 de d\u00e9tecter du monoxyde de carbone", + "no_gas": "{entity_name} a arr\u00eat\u00e9 de d\u00e9tecter du gaz", + "no_light": "{entity_name} a arr\u00eat\u00e9 de d\u00e9tecter de la lumi\u00e8re", + "no_motion": "{entity_name} a arr\u00eat\u00e9 de d\u00e9tecter du 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_update": "{entity_name} a \u00e9t\u00e9 mis \u00e0 jour", - "no_vibration": "{entity_name} a cess\u00e9 de d\u00e9tecter des vibrations", - "not_bat_low": "{entity_name} batterie normale", + "no_vibration": "{entity_name} a cess\u00e9 de d\u00e9tecter une vibration", + "not_bat_low": "{entity_name} n'est plus faible en batterie", "not_cold": "{entity_name} n'est plus froid", - "not_connected": "{entity_name} d\u00e9connect\u00e9", + "not_connected": "{entity_name} s'est d\u00e9connect\u00e9", "not_hot": "{entity_name} n'est plus chaud", - "not_locked": "{entity_name} d\u00e9verrouill\u00e9", + "not_locked": "{entity_name} s'est 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_opened": "{entity_name} s'est 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_running": "{entity_name} n'est plus en cours d'ex\u00e9cution", - "not_tampered": "{entity_name} a cess\u00e9 de d\u00e9tecter la falsification", + "not_tampered": "{entity_name} a cess\u00e9 de d\u00e9tecter une manipulation", "not_unsafe": "{entity_name} est devenu s\u00fbr", "occupied": "{entity_name} est devenu occup\u00e9", "opened": "{entity_name} ouvert", @@ -99,11 +99,11 @@ "running": "{entity_name} commenc\u00e9 \u00e0 s'ex\u00e9cuter", "smoke": "{entity_name} commenc\u00e9 \u00e0 d\u00e9tecter la fum\u00e9e", "sound": "{entity_name} commenc\u00e9 \u00e0 d\u00e9tecter le son", - "tampered": "{entity_name} commenc\u00e9 \u00e0 d\u00e9tecter une falsification", + "tampered": "{entity_name} a commenc\u00e9 \u00e0 d\u00e9tecter une manipulation", "turned_off": "{entity_name} est d\u00e9sactiv\u00e9", "turned_on": "{entity_name} est activ\u00e9", "unsafe": "{entity_name} est devenu dangereux", - "update": "{entity_name} a une mise \u00e0 jour disponible", + "update": "Une mise \u00e0 jour est disponible pour {entity_name}", "vibration": "{entity_name} a commenc\u00e9 \u00e0 d\u00e9tecter les vibrations" } }, @@ -111,35 +111,39 @@ "co": "monoxyde de carbone", "cold": "froid", "gas": "gaz", - "heat": "Chauffer", + "heat": "chaleur", "moisture": "humidit\u00e9", "motion": "mouvement", "occupancy": "occupation", - "power": "Puissance", - "problem": "Probl\u00e8me", + "power": "puissance", + "problem": "probl\u00e8me", "smoke": "fum\u00e9e", "sound": "son", "vibration": "vibration" }, "state": { "_": { - "off": "Inactif", - "on": "Actif" + "off": "D\u00e9sactiv\u00e9", + "on": "Activ\u00e9" }, "battery": { - "off": "Normal", + "off": "Normale", "on": "Faible" }, "battery_charging": { "off": "Pas en charge", "on": "En charge" }, + "carbon_monoxide": { + "off": "Non d\u00e9tect\u00e9", + "on": "D\u00e9tect\u00e9" + }, "co": { - "off": "Clair", - "on": "D\u00e9tect\u00e9e" + "off": "Non d\u00e9tect\u00e9", + "on": "D\u00e9tect\u00e9" }, "cold": { - "off": "Normale", + "off": "Normal", "on": "Froid" }, "connectivity": { @@ -159,7 +163,7 @@ "on": "D\u00e9tect\u00e9" }, "heat": { - "off": "Normale", + "off": "Normal", "on": "Chaud" }, "light": { @@ -175,7 +179,7 @@ "on": "Humide" }, "motion": { - "off": "RAS", + "off": "Non d\u00e9tect\u00e9", "on": "D\u00e9tect\u00e9" }, "moving": { @@ -183,8 +187,8 @@ "on": "En mouvement" }, "occupancy": { - "off": "RAS", - "on": "D\u00e9tect\u00e9" + "off": "Non d\u00e9tect\u00e9e", + "on": "D\u00e9tect\u00e9e" }, "opening": { "off": "Ferm\u00e9", @@ -211,8 +215,8 @@ "on": "Dangereux" }, "smoke": { - "off": "Non d\u00e9tect\u00e9", - "on": "D\u00e9tect\u00e9" + "off": "Non d\u00e9tect\u00e9e", + "on": "D\u00e9tect\u00e9e" }, "sound": { "off": "Non d\u00e9tect\u00e9", @@ -223,7 +227,7 @@ "on": "Mise \u00e0 jour disponible" }, "vibration": { - "off": "RAS", + "off": "Non d\u00e9tect\u00e9e", "on": "D\u00e9tect\u00e9e" }, "window": { diff --git a/homeassistant/components/binary_sensor/translations/he.json b/homeassistant/components/binary_sensor/translations/he.json index 3866d6a93d6..293a4f3f264 100644 --- a/homeassistant/components/binary_sensor/translations/he.json +++ b/homeassistant/components/binary_sensor/translations/he.json @@ -134,6 +134,10 @@ "off": "\u05dc\u05d0 \u05e0\u05d8\u05e2\u05df", "on": "\u05e0\u05d8\u05e2\u05df" }, + "carbon_monoxide": { + "off": "\u05e0\u05e7\u05d9", + "on": "\u05d6\u05d5\u05d4\u05d4" + }, "co": { "off": "\u05e0\u05e7\u05d9", "on": "\u05d6\u05d5\u05d4\u05d4" diff --git a/homeassistant/components/binary_sensor/translations/id.json b/homeassistant/components/binary_sensor/translations/id.json index 57dc0ab6930..5215f57814a 100644 --- a/homeassistant/components/binary_sensor/translations/id.json +++ b/homeassistant/components/binary_sensor/translations/id.json @@ -134,6 +134,10 @@ "off": "Tidak mengisi daya", "on": "Mengisi daya" }, + "carbon_monoxide": { + "off": "Tidak ada", + "on": "Terdeteksi" + }, "co": { "off": "Tidak ada", "on": "Terdeteksi" diff --git a/homeassistant/components/binary_sensor/translations/it.json b/homeassistant/components/binary_sensor/translations/it.json index 5c81e8942e4..6054fd9d9fe 100644 --- a/homeassistant/components/binary_sensor/translations/it.json +++ b/homeassistant/components/binary_sensor/translations/it.json @@ -56,7 +56,7 @@ "bat_low": "{entity_name} batteria scarica", "co": "{entity_name} ha iniziato a rilevare il monossido di carbonio", "cold": "{entity_name} \u00e8 diventato freddo", - "connected": "{entity_name} connesso", + "connected": "{entity_name} \u00e8 connesso", "gas": "{entity_name} ha iniziato a rilevare il gas", "hot": "{entity_name} \u00e8 diventato caldo", "is_not_tampered": "{entity_name} ha smesso di rilevare manomissioni", @@ -139,7 +139,7 @@ "on": "Rilevato" }, "co": { - "off": "Non Rilevato", + "off": "Non rilevato", "on": "Rilevato" }, "cold": { @@ -228,7 +228,7 @@ }, "vibration": { "off": "Assente", - "on": "Rilevata" + "on": "Rilevato" }, "window": { "off": "Chiusa", diff --git a/homeassistant/components/binary_sensor/translations/pt-BR.json b/homeassistant/components/binary_sensor/translations/pt-BR.json index 779830e0961..d858dec6664 100644 --- a/homeassistant/components/binary_sensor/translations/pt-BR.json +++ b/homeassistant/components/binary_sensor/translations/pt-BR.json @@ -135,8 +135,8 @@ "on": "Carregando" }, "carbon_monoxide": { - "off": "Remover", - "on": "Detectou" + "off": "Normal", + "on": "Detectado" }, "co": { "off": "Limpo", diff --git a/homeassistant/components/binary_sensor/translations/sv.json b/homeassistant/components/binary_sensor/translations/sv.json index c651d895fda..c6685403e24 100644 --- a/homeassistant/components/binary_sensor/translations/sv.json +++ b/homeassistant/components/binary_sensor/translations/sv.json @@ -89,6 +89,9 @@ "vibration": "{entity_name} b\u00f6rjade detektera vibrationer" } }, + "device_class": { + "motion": "r\u00f6relse" + }, "state": { "_": { "off": "Av", diff --git a/homeassistant/components/binary_sensor/translations/tr.json b/homeassistant/components/binary_sensor/translations/tr.json index 40dc3bdcb7d..2b7eb33e6f0 100644 --- a/homeassistant/components/binary_sensor/translations/tr.json +++ b/homeassistant/components/binary_sensor/translations/tr.json @@ -134,6 +134,10 @@ "off": "\u015earj olmuyor", "on": "\u015earj Oluyor" }, + "carbon_monoxide": { + "off": "Temiz", + "on": "Alg\u0131land\u0131" + }, "co": { "off": "Temiz", "on": "Alg\u0131land\u0131" diff --git a/homeassistant/components/blink/manifest.json b/homeassistant/components/blink/manifest.json index 58e2d3a1b52..c302f2dab6c 100644 --- a/homeassistant/components/blink/manifest.json +++ b/homeassistant/components/blink/manifest.json @@ -2,7 +2,7 @@ "domain": "blink", "name": "Blink", "documentation": "https://www.home-assistant.io/integrations/blink", - "requirements": ["blinkpy==0.18.0"], + "requirements": ["blinkpy==0.19.0"], "codeowners": ["@fronzbot"], "dhcp": [ { @@ -16,7 +16,7 @@ { "hostname": "blink*", "macaddress": "20A171*" - } + } ], "config_flow": true, "iot_class": "cloud_polling", diff --git a/homeassistant/components/blink/strings.json b/homeassistant/components/blink/strings.json index 6e438b58590..c0428703762 100644 --- a/homeassistant/components/blink/strings.json +++ b/homeassistant/components/blink/strings.json @@ -4,8 +4,8 @@ "user": { "title": "Sign-in with Blink account", "data": { - "username": "[%key:common::config_flow::data::username%]", - "password": "[%key:common::config_flow::data::password%]" + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" } }, "2fa": { @@ -21,7 +21,7 @@ "unknown": "[%key:common::config_flow::error::unknown%]" }, "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } }, "options": { @@ -34,5 +34,5 @@ "description": "Configure Blink integration" } } - } + } } diff --git a/homeassistant/components/blink/translations/fr.json b/homeassistant/components/blink/translations/fr.json index bef14d641f7..bb1686e9700 100644 --- a/homeassistant/components/blink/translations/fr.json +++ b/homeassistant/components/blink/translations/fr.json @@ -6,7 +6,7 @@ "error": { "cannot_connect": "\u00c9chec de connexion", "invalid_access_token": "Jeton d'acc\u00e8s non valide", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/blinkt/__init__.py b/homeassistant/components/blinkt/__init__.py deleted file mode 100644 index 0f61a211559..00000000000 --- a/homeassistant/components/blinkt/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The blinkt component.""" diff --git a/homeassistant/components/blinkt/light.py b/homeassistant/components/blinkt/light.py deleted file mode 100644 index 5b720e13697..00000000000 --- a/homeassistant/components/blinkt/light.py +++ /dev/null @@ -1,103 +0,0 @@ -"""Support for Blinkt! lights on Raspberry Pi.""" -from __future__ import annotations - -import importlib -import logging - -import voluptuous as vol - -from homeassistant.components.light import ( - ATTR_BRIGHTNESS, - ATTR_HS_COLOR, - PLATFORM_SCHEMA, - SUPPORT_BRIGHTNESS, - SUPPORT_COLOR, - LightEntity, -) -from homeassistant.const import CONF_NAME -from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -import homeassistant.util.color as color_util - -SUPPORT_BLINKT = SUPPORT_BRIGHTNESS | SUPPORT_COLOR - -DEFAULT_NAME = "blinkt" - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - {vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string} -) - -_LOGGER = logging.getLogger(__name__) - - -def setup_platform( - hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the Blinkt Light platform.""" - _LOGGER.warning( - "The Blinkt! integration is deprecated and will be removed " - "in Home Assistant Core 2022.4; this integration is removed under " - "Architectural Decision Record 0019, more information can be found here: " - "https://github.com/home-assistant/architecture/blob/master/adr/0019-GPIO.md" - ) - - blinkt = importlib.import_module("blinkt") - - # ensure that the lights are off when exiting - blinkt.set_clear_on_exit() - - name = config[CONF_NAME] - - add_entities( - [BlinktLight(blinkt, name, index) for index in range(blinkt.NUM_PIXELS)] - ) - - -class BlinktLight(LightEntity): - """Representation of a Blinkt! Light.""" - - _attr_supported_features = SUPPORT_BLINKT - _attr_should_poll = False - _attr_assumed_state = True - - def __init__(self, blinkt, name, index): - """Initialize a Blinkt Light. - - Default brightness and white color. - """ - self._blinkt = blinkt - self._attr_name = f"{name}_{index}" - self._index = index - self._attr_is_on = False - self._attr_brightness = 255 - self._attr_hs_color = [0, 0] - - def turn_on(self, **kwargs): - """Instruct the light to turn on and set correct brightness & color.""" - if ATTR_HS_COLOR in kwargs: - self._attr_hs_color = kwargs[ATTR_HS_COLOR] - if ATTR_BRIGHTNESS in kwargs: - self._attr_brightness = kwargs[ATTR_BRIGHTNESS] - - percent_bright = self.brightness / 255 - rgb_color = color_util.color_hs_to_RGB(*self.hs_color) - self._blinkt.set_pixel( - self._index, rgb_color[0], rgb_color[1], rgb_color[2], percent_bright - ) - - self._blinkt.show() - - self._attr_is_on = True - self.schedule_update_ha_state() - - def turn_off(self, **kwargs): - """Instruct the light to turn off.""" - self._blinkt.set_pixel(self._index, 0, 0, 0, 0) - self._blinkt.show() - self._attr_is_on = False - self.schedule_update_ha_state() diff --git a/homeassistant/components/blinkt/manifest.json b/homeassistant/components/blinkt/manifest.json deleted file mode 100644 index ac659f78e11..00000000000 --- a/homeassistant/components/blinkt/manifest.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "domain": "blinkt", - "name": "Blinkt!", - "documentation": "https://www.home-assistant.io/integrations/blinkt", - "requirements": ["blinkt==0.1.0"], - "codeowners": [], - "iot_class": "local_push" -} diff --git a/homeassistant/components/bluesound/media_player.py b/homeassistant/components/bluesound/media_player.py index 1995d7810a4..443d8faa5de 100644 --- a/homeassistant/components/bluesound/media_player.py +++ b/homeassistant/components/bluesound/media_player.py @@ -15,10 +15,15 @@ import async_timeout import voluptuous as vol import xmltodict +from homeassistant.components import media_source from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity +from homeassistant.components.media_player.browse_media import ( + async_process_play_media_url, +) from homeassistant.components.media_player.const import ( ATTR_MEDIA_ENQUEUE, MEDIA_TYPE_MUSIC, + SUPPORT_BROWSE_MEDIA, SUPPORT_CLEAR_PLAYLIST, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, @@ -800,7 +805,7 @@ class BluesoundPlayer(MediaPlayerEntity): if self.is_grouped and not self.is_master: return SUPPORT_VOLUME_STEP | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE - supported = SUPPORT_CLEAR_PLAYLIST + supported = SUPPORT_CLEAR_PLAYLIST | SUPPORT_BROWSE_MEDIA if self._status.get("indexing", "0") == "0": supported = ( @@ -1029,6 +1034,12 @@ class BluesoundPlayer(MediaPlayerEntity): if self.is_grouped and not self.is_master: return + if media_source.is_media_source_id(media_id): + play_item = await media_source.async_resolve_media(self.hass, media_id) + media_id = play_item.url + + media_id = async_process_play_media_url(self.hass, media_id) + url = f"Play?url={media_id}" if kwargs.get(ATTR_MEDIA_ENQUEUE): @@ -1063,3 +1074,11 @@ class BluesoundPlayer(MediaPlayerEntity): if mute: return await self.send_bluesound_command("Volume?mute=1") return await self.send_bluesound_command("Volume?mute=0") + + async def async_browse_media(self, media_content_type=None, media_content_id=None): + """Implement the websocket media browsing helper.""" + return await media_source.async_browse_media( + self.hass, + media_content_id, + content_filter=lambda item: item.media_content_type.startswith("audio/"), + ) diff --git a/homeassistant/components/bme280/__init__.py b/homeassistant/components/bme280/__init__.py deleted file mode 100644 index 59ad42da944..00000000000 --- a/homeassistant/components/bme280/__init__.py +++ /dev/null @@ -1,112 +0,0 @@ -"""The bme280 component.""" -import logging - -import voluptuous as vol - -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN -from homeassistant.const import CONF_MONITORED_CONDITIONS, CONF_NAME, CONF_SCAN_INTERVAL -from homeassistant.core import HomeAssistant -from homeassistant.helpers import config_validation as cv, discovery -from homeassistant.helpers.typing import ConfigType - -from .const import ( - CONF_DELTA_TEMP, - CONF_FILTER_MODE, - CONF_I2C_ADDRESS, - CONF_I2C_BUS, - CONF_OPERATION_MODE, - CONF_OVERSAMPLING_HUM, - CONF_OVERSAMPLING_PRES, - CONF_OVERSAMPLING_TEMP, - CONF_SPI_BUS, - CONF_SPI_DEV, - CONF_T_STANDBY, - DEFAULT_DELTA_TEMP, - DEFAULT_FILTER_MODE, - DEFAULT_I2C_ADDRESS, - DEFAULT_I2C_BUS, - DEFAULT_MONITORED, - DEFAULT_NAME, - DEFAULT_OPERATION_MODE, - DEFAULT_OVERSAMPLING_HUM, - DEFAULT_OVERSAMPLING_PRES, - DEFAULT_OVERSAMPLING_TEMP, - DEFAULT_SCAN_INTERVAL, - DEFAULT_T_STANDBY, - DOMAIN, - SENSOR_KEYS, -) - -CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.All( - cv.ensure_list, - [ - vol.Schema( - { - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_SPI_BUS): vol.Coerce(int), - vol.Optional(CONF_SPI_DEV): vol.Coerce(int), - 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_DELTA_TEMP, default=DEFAULT_DELTA_TEMP - ): vol.Coerce(float), - vol.Optional( - CONF_MONITORED_CONDITIONS, default=DEFAULT_MONITORED - ): vol.All(cv.ensure_list, [vol.In(SENSOR_KEYS)]), - vol.Optional( - CONF_OVERSAMPLING_TEMP, default=DEFAULT_OVERSAMPLING_TEMP - ): vol.Coerce(int), - vol.Optional( - CONF_OVERSAMPLING_PRES, default=DEFAULT_OVERSAMPLING_PRES - ): vol.Coerce(int), - vol.Optional( - CONF_OVERSAMPLING_HUM, default=DEFAULT_OVERSAMPLING_HUM - ): vol.Coerce(int), - vol.Optional( - CONF_OPERATION_MODE, default=DEFAULT_OPERATION_MODE - ): vol.Coerce(int), - vol.Optional( - CONF_T_STANDBY, default=DEFAULT_T_STANDBY - ): vol.Coerce(int), - vol.Optional( - CONF_FILTER_MODE, default=DEFAULT_FILTER_MODE - ): vol.Coerce(int), - vol.Optional( - CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL - ): cv.time_period, - } - ) - ], - ) - }, - extra=vol.ALLOW_EXTRA, -) - -_LOGGER = logging.getLogger(__name__) - - -async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up BME280 component.""" - _LOGGER.warning( - "The Bosch BME280 Environmental Sensor integration is deprecated and " - "will be removed in Home Assistant Core 2022.4; " - "this integration is removed under Architectural Decision Record 0019, " - "more information can be found here: " - "https://github.com/home-assistant/architecture/blob/master/adr/0019-GPIO.md" - ) - - bme280_config = config[DOMAIN] - for bme280_conf in bme280_config: - discovery_info = {SENSOR_DOMAIN: bme280_conf} - hass.async_create_task( - discovery.async_load_platform( - hass, SENSOR_DOMAIN, DOMAIN, discovery_info, config - ) - ) - return True diff --git a/homeassistant/components/bme280/const.py b/homeassistant/components/bme280/const.py deleted file mode 100644 index 1bb0828dd1e..00000000000 --- a/homeassistant/components/bme280/const.py +++ /dev/null @@ -1,60 +0,0 @@ -"""Constants for the BME280 component.""" -from __future__ import annotations - -from datetime import timedelta - -from homeassistant.components.sensor import SensorDeviceClass, SensorEntityDescription -from homeassistant.const import PERCENTAGE, TEMP_CELSIUS - -# Common -DOMAIN = "bme280" -CONF_OVERSAMPLING_TEMP = "oversampling_temperature" -CONF_OVERSAMPLING_PRES = "oversampling_pressure" -CONF_OVERSAMPLING_HUM = "oversampling_humidity" -CONF_T_STANDBY = "time_standby" -CONF_FILTER_MODE = "filter_mode" -DEFAULT_NAME = "BME280 Sensor" -DEFAULT_OVERSAMPLING_TEMP = 1 -DEFAULT_OVERSAMPLING_PRES = 1 -DEFAULT_OVERSAMPLING_HUM = 1 -DEFAULT_T_STANDBY = 5 -DEFAULT_FILTER_MODE = 0 -DEFAULT_SCAN_INTERVAL = 300 -SENSOR_TEMP = "temperature" -SENSOR_HUMID = "humidity" -SENSOR_PRESS = "pressure" -SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( - SensorEntityDescription( - key=SENSOR_TEMP, - name="Temperature", - native_unit_of_measurement=TEMP_CELSIUS, - device_class=SensorDeviceClass.TEMPERATURE, - ), - SensorEntityDescription( - key=SENSOR_HUMID, - name="Humidity", - native_unit_of_measurement=PERCENTAGE, - device_class=SensorDeviceClass.HUMIDITY, - ), - SensorEntityDescription( - key=SENSOR_PRESS, - name="Pressure", - native_unit_of_measurement="mb", - device_class=SensorDeviceClass.PRESSURE, - ), -) -SENSOR_KEYS: list[str] = [desc.key for desc in SENSOR_TYPES] -DEFAULT_MONITORED = [SENSOR_TEMP, SENSOR_HUMID, SENSOR_PRESS] -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=3) -# SPI -CONF_SPI_DEV = "spi_dev" -CONF_SPI_BUS = "spi_bus" -# I2C -CONF_I2C_ADDRESS = "i2c_address" -CONF_I2C_BUS = "i2c_bus" -CONF_DELTA_TEMP = "delta_temperature" -CONF_OPERATION_MODE = "operation_mode" -DEFAULT_OPERATION_MODE = 3 # Normal mode (forced mode: 2) -DEFAULT_I2C_ADDRESS = "0x76" -DEFAULT_I2C_BUS = 1 -DEFAULT_DELTA_TEMP = 0.0 diff --git a/homeassistant/components/bme280/manifest.json b/homeassistant/components/bme280/manifest.json deleted file mode 100644 index 8a283b40f5f..00000000000 --- a/homeassistant/components/bme280/manifest.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "domain": "bme280", - "name": "Bosch BME280 Environmental Sensor", - "documentation": "https://www.home-assistant.io/integrations/bme280", - "requirements": [ - "i2csense==0.0.4", - "smbus-cffi==0.5.1", - "bme280spi==0.2.0" - ], - "codeowners": [], - "iot_class": "local_push", - "loggers": ["bme280spi", "i2csense", "smbus"] -} diff --git a/homeassistant/components/bme280/sensor.py b/homeassistant/components/bme280/sensor.py deleted file mode 100644 index 1ceae298e25..00000000000 --- a/homeassistant/components/bme280/sensor.py +++ /dev/null @@ -1,149 +0,0 @@ -"""Support for BME280 temperature, humidity and pressure sensor.""" -from __future__ import annotations - -from functools import partial -import logging - -from bme280spi import BME280 as BME280_spi # pylint: disable=import-error -from i2csense.bme280 import BME280 as BME280_i2c # pylint: disable=import-error -import smbus - -from homeassistant.components.sensor import ( - DOMAIN as SENSOR_DOMAIN, - SensorEntity, - SensorEntityDescription, -) -from homeassistant.const import CONF_MONITORED_CONDITIONS, CONF_NAME, CONF_SCAN_INTERVAL -from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from homeassistant.helpers.update_coordinator import ( - CoordinatorEntity, - DataUpdateCoordinator, - UpdateFailed, -) - -from .const import ( - CONF_DELTA_TEMP, - CONF_FILTER_MODE, - CONF_I2C_ADDRESS, - CONF_I2C_BUS, - CONF_OPERATION_MODE, - CONF_OVERSAMPLING_HUM, - CONF_OVERSAMPLING_PRES, - CONF_OVERSAMPLING_TEMP, - CONF_SPI_BUS, - CONF_SPI_DEV, - CONF_T_STANDBY, - DOMAIN, - MIN_TIME_BETWEEN_UPDATES, - SENSOR_HUMID, - SENSOR_PRESS, - SENSOR_TEMP, - SENSOR_TYPES, -) - -_LOGGER = logging.getLogger(__name__) - - -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the BME280 sensor.""" - if discovery_info is None: - return - sensor_conf = discovery_info[SENSOR_DOMAIN] - name = sensor_conf[CONF_NAME] - scan_interval = max(sensor_conf[CONF_SCAN_INTERVAL], MIN_TIME_BETWEEN_UPDATES) - if CONF_SPI_BUS in sensor_conf and CONF_SPI_DEV in sensor_conf: - spi_dev = sensor_conf[CONF_SPI_DEV] - spi_bus = sensor_conf[CONF_SPI_BUS] - _LOGGER.debug("BME280 sensor initialize at %s.%s", spi_bus, spi_dev) - sensor = await hass.async_add_executor_job( - partial( - BME280_spi, - t_mode=sensor_conf[CONF_OVERSAMPLING_TEMP], - p_mode=sensor_conf[CONF_OVERSAMPLING_PRES], - h_mode=sensor_conf[CONF_OVERSAMPLING_HUM], - standby=sensor_conf[CONF_T_STANDBY], - filter=sensor_conf[CONF_FILTER_MODE], - spi_bus=sensor_conf[CONF_SPI_BUS], - spi_dev=sensor_conf[CONF_SPI_DEV], - ) - ) - if not sensor.sample_ok: - _LOGGER.error("BME280 sensor not detected at %s.%s", spi_bus, spi_dev) - return - else: - i2c_address = sensor_conf[CONF_I2C_ADDRESS] - bus = smbus.SMBus(sensor_conf[CONF_I2C_BUS]) - sensor = await hass.async_add_executor_job( - partial( - BME280_i2c, - bus, - i2c_address, - osrs_t=sensor_conf[CONF_OVERSAMPLING_TEMP], - osrs_p=sensor_conf[CONF_OVERSAMPLING_PRES], - osrs_h=sensor_conf[CONF_OVERSAMPLING_HUM], - mode=sensor_conf[CONF_OPERATION_MODE], - t_sb=sensor_conf[CONF_T_STANDBY], - filter_mode=sensor_conf[CONF_FILTER_MODE], - delta_temp=sensor_conf[CONF_DELTA_TEMP], - ) - ) - if not sensor.sample_ok: - _LOGGER.error("BME280 sensor not detected at %s", i2c_address) - return - - async def async_update_data(): - await hass.async_add_executor_job(sensor.update) - if not sensor.sample_ok: - raise UpdateFailed(f"Bad update of sensor {name}") - return sensor - - coordinator = DataUpdateCoordinator( - hass, - _LOGGER, - name=DOMAIN, - update_method=async_update_data, - update_interval=scan_interval, - ) - await coordinator.async_refresh() - monitored_conditions = sensor_conf[CONF_MONITORED_CONDITIONS] - entities = [ - BME280Sensor(name, coordinator, description) - for description in SENSOR_TYPES - if description.key in monitored_conditions - ] - async_add_entities(entities, True) - - -class BME280Sensor(CoordinatorEntity, SensorEntity): - """Implementation of the BME280 sensor.""" - - def __init__(self, name, coordinator, description: SensorEntityDescription): - """Initialize the sensor.""" - super().__init__(coordinator) - self.entity_description = description - self._attr_name = f"{name} {description.name}" - - @property - def native_value(self): - """Return the state of the sensor.""" - sensor_type = self.entity_description.key - if sensor_type == SENSOR_TEMP: - temperature = round(self.coordinator.data.temperature, 1) - state = temperature - elif sensor_type == SENSOR_HUMID: - state = round(self.coordinator.data.humidity, 1) - elif sensor_type == SENSOR_PRESS: - state = round(self.coordinator.data.pressure, 1) - return state - - @property - def should_poll(self) -> bool: - """Return False if entity should not poll.""" - return False diff --git a/homeassistant/components/bme680/__init__.py b/homeassistant/components/bme680/__init__.py deleted file mode 100644 index dc88286a603..00000000000 --- a/homeassistant/components/bme680/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The bme680 component.""" diff --git a/homeassistant/components/bme680/manifest.json b/homeassistant/components/bme680/manifest.json deleted file mode 100644 index c4db1d640de..00000000000 --- a/homeassistant/components/bme680/manifest.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "domain": "bme680", - "name": "Bosch BME680 Environmental Sensor", - "documentation": "https://www.home-assistant.io/integrations/bme680", - "requirements": ["bme680==1.0.5", "smbus-cffi==0.5.1"], - "codeowners": [], - "iot_class": "local_push", - "loggers": ["bme680", "smbus"] -} diff --git a/homeassistant/components/bme680/sensor.py b/homeassistant/components/bme680/sensor.py deleted file mode 100644 index 8ea3bf32334..00000000000 --- a/homeassistant/components/bme680/sensor.py +++ /dev/null @@ -1,391 +0,0 @@ -"""Support for BME680 Sensor over SMBus.""" -from __future__ import annotations - -import logging -import threading -from time import monotonic, sleep - -import bme680 # pylint: disable=import-error -from smbus import SMBus -import voluptuous as vol - -from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, - SensorDeviceClass, - SensorEntity, - SensorEntityDescription, -) -from homeassistant.const import ( - CONF_MONITORED_CONDITIONS, - CONF_NAME, - PERCENTAGE, - TEMP_CELSIUS, -) -from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType - -_LOGGER = logging.getLogger(__name__) - -CONF_I2C_ADDRESS = "i2c_address" -CONF_I2C_BUS = "i2c_bus" -CONF_OVERSAMPLING_TEMP = "oversampling_temperature" -CONF_OVERSAMPLING_PRES = "oversampling_pressure" -CONF_OVERSAMPLING_HUM = "oversampling_humidity" -CONF_FILTER_SIZE = "filter_size" -CONF_GAS_HEATER_TEMP = "gas_heater_temperature" -CONF_GAS_HEATER_DURATION = "gas_heater_duration" -CONF_AQ_BURN_IN_TIME = "aq_burn_in_time" -CONF_AQ_HUM_BASELINE = "aq_humidity_baseline" -CONF_AQ_HUM_WEIGHTING = "aq_humidity_bias" -CONF_TEMP_OFFSET = "temp_offset" - - -DEFAULT_NAME = "BME680 Sensor" -DEFAULT_I2C_ADDRESS = 0x77 -DEFAULT_I2C_BUS = 1 -DEFAULT_OVERSAMPLING_TEMP = 8 # Temperature oversampling x 8 -DEFAULT_OVERSAMPLING_PRES = 4 # Pressure oversampling x 4 -DEFAULT_OVERSAMPLING_HUM = 2 # Humidity oversampling x 2 -DEFAULT_FILTER_SIZE = 3 # IIR Filter Size -DEFAULT_GAS_HEATER_TEMP = 320 # Temperature in celsius 200 - 400 -DEFAULT_GAS_HEATER_DURATION = 150 # Heater duration in ms 1 - 4032 -DEFAULT_AQ_BURN_IN_TIME = 300 # 300 second burn in time for AQ gas measurement -DEFAULT_AQ_HUM_BASELINE = 40 # 40%, an optimal indoor humidity. -DEFAULT_AQ_HUM_WEIGHTING = 25 # 25% Weighting of humidity to gas in AQ score -DEFAULT_TEMP_OFFSET = 0 # No calibration out of the box. - -SENSOR_TEMP = "temperature" -SENSOR_HUMID = "humidity" -SENSOR_PRESS = "pressure" -SENSOR_GAS = "gas" -SENSOR_AQ = "airquality" -SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( - SensorEntityDescription( - key=SENSOR_TEMP, - name="Temperature", - native_unit_of_measurement=TEMP_CELSIUS, - device_class=SensorDeviceClass.TEMPERATURE, - ), - SensorEntityDescription( - key=SENSOR_HUMID, - name="Humidity", - native_unit_of_measurement=PERCENTAGE, - device_class=SensorDeviceClass.HUMIDITY, - ), - SensorEntityDescription( - key=SENSOR_PRESS, - name="Pressure", - native_unit_of_measurement="mb", - device_class=SensorDeviceClass.PRESSURE, - ), - SensorEntityDescription( - key=SENSOR_GAS, - name="Gas Resistance", - native_unit_of_measurement="Ohms", - ), - SensorEntityDescription( - key=SENSOR_AQ, - name="Air Quality", - native_unit_of_measurement=PERCENTAGE, - ), -) -SENSOR_KEYS: list[str] = [desc.key for desc in SENSOR_TYPES] -DEFAULT_MONITORED = [SENSOR_TEMP, SENSOR_HUMID, SENSOR_PRESS, SENSOR_AQ] -OVERSAMPLING_VALUES = {0, 1, 2, 4, 8, 16} -FILTER_VALUES = {0, 1, 3, 7, 15, 31, 63, 127} - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_I2C_ADDRESS, default=DEFAULT_I2C_ADDRESS): cv.positive_int, - vol.Optional(CONF_MONITORED_CONDITIONS, default=DEFAULT_MONITORED): vol.All( - cv.ensure_list, [vol.In(SENSOR_KEYS)] - ), - vol.Optional(CONF_I2C_BUS, default=DEFAULT_I2C_BUS): cv.positive_int, - vol.Optional( - CONF_OVERSAMPLING_TEMP, default=DEFAULT_OVERSAMPLING_TEMP - ): vol.All(vol.Coerce(int), vol.In(OVERSAMPLING_VALUES)), - vol.Optional( - CONF_OVERSAMPLING_PRES, default=DEFAULT_OVERSAMPLING_PRES - ): vol.All(vol.Coerce(int), vol.In(OVERSAMPLING_VALUES)), - vol.Optional(CONF_OVERSAMPLING_HUM, default=DEFAULT_OVERSAMPLING_HUM): vol.All( - vol.Coerce(int), vol.In(OVERSAMPLING_VALUES) - ), - vol.Optional(CONF_FILTER_SIZE, default=DEFAULT_FILTER_SIZE): vol.All( - vol.Coerce(int), vol.In(FILTER_VALUES) - ), - vol.Optional(CONF_GAS_HEATER_TEMP, default=DEFAULT_GAS_HEATER_TEMP): vol.All( - vol.Coerce(int), vol.Range(200, 400) - ), - vol.Optional( - CONF_GAS_HEATER_DURATION, default=DEFAULT_GAS_HEATER_DURATION - ): vol.All(vol.Coerce(int), vol.Range(1, 4032)), - vol.Optional( - CONF_AQ_BURN_IN_TIME, default=DEFAULT_AQ_BURN_IN_TIME - ): cv.positive_int, - vol.Optional(CONF_AQ_HUM_BASELINE, default=DEFAULT_AQ_HUM_BASELINE): vol.All( - vol.Coerce(int), vol.Range(1, 100) - ), - vol.Optional(CONF_AQ_HUM_WEIGHTING, default=DEFAULT_AQ_HUM_WEIGHTING): vol.All( - vol.Coerce(int), vol.Range(1, 100) - ), - vol.Optional(CONF_TEMP_OFFSET, default=DEFAULT_TEMP_OFFSET): vol.All( - vol.Coerce(float), vol.Range(-100.0, 100.0) - ), - } -) - - -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the BME680 sensor.""" - _LOGGER.warning( - "The Bosch BME680 Environmental Sensor integration is deprecated and " - "will be removed in Home Assistant Core 2022.4; " - "this integration is removed under Architectural Decision Record 0019, " - "more information can be found here: " - "https://github.com/home-assistant/architecture/blob/master/adr/0019-GPIO.md" - ) - - name = config[CONF_NAME] - - sensor_handler = await hass.async_add_executor_job(_setup_bme680, config) - if sensor_handler is None: - return - - monitored_conditions = config[CONF_MONITORED_CONDITIONS] - entities = [ - BME680Sensor(sensor_handler, name, description) - for description in SENSOR_TYPES - if description.key in monitored_conditions - ] - - async_add_entities(entities) - - -def _setup_bme680(config): - """Set up and configure the BME680 sensor.""" - - sensor_handler = None - sensor = None - try: - i2c_address = config[CONF_I2C_ADDRESS] - bus = SMBus(config[CONF_I2C_BUS]) - sensor = bme680.BME680(i2c_address, bus) - - # Configure Oversampling - os_lookup = { - 0: bme680.OS_NONE, - 1: bme680.OS_1X, - 2: bme680.OS_2X, - 4: bme680.OS_4X, - 8: bme680.OS_8X, - 16: bme680.OS_16X, - } - sensor.set_temperature_oversample(os_lookup[config[CONF_OVERSAMPLING_TEMP]]) - sensor.set_temp_offset(config[CONF_TEMP_OFFSET]) - sensor.set_humidity_oversample(os_lookup[config[CONF_OVERSAMPLING_HUM]]) - sensor.set_pressure_oversample(os_lookup[config[CONF_OVERSAMPLING_PRES]]) - - # Configure IIR Filter - filter_lookup = { - 0: bme680.FILTER_SIZE_0, - 1: bme680.FILTER_SIZE_1, - 3: bme680.FILTER_SIZE_3, - 7: bme680.FILTER_SIZE_7, - 15: bme680.FILTER_SIZE_15, - 31: bme680.FILTER_SIZE_31, - 63: bme680.FILTER_SIZE_63, - 127: bme680.FILTER_SIZE_127, - } - sensor.set_filter(filter_lookup[config[CONF_FILTER_SIZE]]) - - # Configure the Gas Heater - if ( - SENSOR_GAS in config[CONF_MONITORED_CONDITIONS] - or SENSOR_AQ in config[CONF_MONITORED_CONDITIONS] - ): - sensor.set_gas_status(bme680.ENABLE_GAS_MEAS) - sensor.set_gas_heater_duration(config[CONF_GAS_HEATER_DURATION]) - sensor.set_gas_heater_temperature(config[CONF_GAS_HEATER_TEMP]) - sensor.select_gas_heater_profile(0) - else: - sensor.set_gas_status(bme680.DISABLE_GAS_MEAS) - except (RuntimeError, OSError): - _LOGGER.error("BME680 sensor not detected at 0x%02x", i2c_address) - return None - - sensor_handler = BME680Handler( - sensor, - ( - SENSOR_GAS in config[CONF_MONITORED_CONDITIONS] - or SENSOR_AQ in config[CONF_MONITORED_CONDITIONS] - ), - config[CONF_AQ_BURN_IN_TIME], - config[CONF_AQ_HUM_BASELINE], - config[CONF_AQ_HUM_WEIGHTING], - ) - sleep(0.5) # Wait for device to stabilize - if not sensor_handler.sensor_data.temperature: - _LOGGER.error("BME680 sensor failed to Initialize") - return None - - return sensor_handler - - -class BME680Handler: - """BME680 sensor working in i2C bus.""" - - class SensorData: - """Sensor data representation.""" - - def __init__(self): - """Initialize the sensor data object.""" - self.temperature = None - self.humidity = None - self.pressure = None - self.gas_resistance = None - self.air_quality = None - - def __init__( - self, - sensor, - gas_measurement=False, - burn_in_time=300, - hum_baseline=40, - hum_weighting=25, - ): - """Initialize the sensor handler.""" - self.sensor_data = BME680Handler.SensorData() - self._sensor = sensor - self._gas_sensor_running = False - self._hum_baseline = hum_baseline - self._hum_weighting = hum_weighting - self._gas_baseline = None - - if gas_measurement: - - threading.Thread( - target=self._run_gas_sensor, - kwargs={"burn_in_time": burn_in_time}, - name="BME680Handler_run_gas_sensor", - ).start() - self.update(first_read=True) - - def _run_gas_sensor(self, burn_in_time): - """Calibrate the Air Quality Gas Baseline.""" - if self._gas_sensor_running: - return - - self._gas_sensor_running = True - - # Pause to allow initial data read for device validation. - sleep(1) - - start_time = monotonic() - curr_time = monotonic() - burn_in_data = [] - - _LOGGER.info( - "Beginning %d second gas sensor burn in for Air Quality", burn_in_time - ) - while curr_time - start_time < burn_in_time: - curr_time = monotonic() - if self._sensor.get_sensor_data() and self._sensor.data.heat_stable: - gas_resistance = self._sensor.data.gas_resistance - burn_in_data.append(gas_resistance) - self.sensor_data.gas_resistance = gas_resistance - _LOGGER.debug( - "AQ Gas Resistance Baseline reading %2f Ohms", gas_resistance - ) - sleep(1) - - _LOGGER.debug( - "AQ Gas Resistance Burn In Data (Size: %d): \n\t%s", - len(burn_in_data), - burn_in_data, - ) - self._gas_baseline = sum(burn_in_data[-50:]) / 50.0 - _LOGGER.info("Completed gas sensor burn in for Air Quality") - _LOGGER.info("AQ Gas Resistance Baseline: %f", self._gas_baseline) - while True: - if self._sensor.get_sensor_data() and self._sensor.data.heat_stable: - self.sensor_data.gas_resistance = self._sensor.data.gas_resistance - self.sensor_data.air_quality = self._calculate_aq_score() - sleep(1) - - def update(self, first_read=False): - """Read sensor data.""" - if first_read: - # Attempt first read, it almost always fails first attempt - self._sensor.get_sensor_data() - if self._sensor.get_sensor_data(): - self.sensor_data.temperature = self._sensor.data.temperature - self.sensor_data.humidity = self._sensor.data.humidity - self.sensor_data.pressure = self._sensor.data.pressure - - def _calculate_aq_score(self): - """Calculate the Air Quality Score.""" - hum_baseline = self._hum_baseline - hum_weighting = self._hum_weighting - gas_baseline = self._gas_baseline - - gas_resistance = self.sensor_data.gas_resistance - gas_offset = gas_baseline - gas_resistance - - hum = self.sensor_data.humidity - hum_offset = hum - hum_baseline - - # Calculate hum_score as the distance from the hum_baseline. - if hum_offset > 0: - hum_score = ( - (100 - hum_baseline - hum_offset) / (100 - hum_baseline) * hum_weighting - ) - else: - hum_score = (hum_baseline + hum_offset) / hum_baseline * hum_weighting - - # Calculate gas_score as the distance from the gas_baseline. - if gas_offset > 0: - gas_score = (gas_resistance / gas_baseline) * (100 - hum_weighting) - else: - gas_score = 100 - hum_weighting - - # Calculate air quality score. - return hum_score + gas_score - - -class BME680Sensor(SensorEntity): - """Implementation of the BME680 sensor.""" - - def __init__(self, bme680_client, name, description: SensorEntityDescription): - """Initialize the sensor.""" - self.entity_description = description - self._attr_name = f"{name} {description.name}" - self.bme680_client = bme680_client - - async def async_update(self): - """Get the latest data from the BME680 and update the states.""" - await self.hass.async_add_executor_job(self.bme680_client.update) - sensor_type = self.entity_description.key - if sensor_type == SENSOR_TEMP: - self._attr_native_value = round( - self.bme680_client.sensor_data.temperature, 1 - ) - elif sensor_type == SENSOR_HUMID: - self._attr_native_value = round(self.bme680_client.sensor_data.humidity, 1) - elif sensor_type == SENSOR_PRESS: - self._attr_native_value = round(self.bme680_client.sensor_data.pressure, 1) - elif sensor_type == SENSOR_GAS: - self._attr_native_value = int( - round(self.bme680_client.sensor_data.gas_resistance, 0) - ) - elif sensor_type == SENSOR_AQ: - aq_score = self.bme680_client.sensor_data.air_quality - if aq_score is not None: - self._attr_native_value = round(aq_score, 1) diff --git a/homeassistant/components/bmp280/__init__.py b/homeassistant/components/bmp280/__init__.py deleted file mode 100644 index 0c884eafbf1..00000000000 --- a/homeassistant/components/bmp280/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The Bosch BMP280 sensor integration.""" diff --git a/homeassistant/components/bmp280/manifest.json b/homeassistant/components/bmp280/manifest.json deleted file mode 100644 index 5347c93f4fa..00000000000 --- a/homeassistant/components/bmp280/manifest.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "domain": "bmp280", - "name": "Bosch BMP280 Environmental Sensor", - "documentation": "https://www.home-assistant.io/integrations/bmp280", - "codeowners": ["@belidzs"], - "requirements": ["adafruit-circuitpython-bmp280==3.1.1", "RPi.GPIO==0.7.1a4"], - "quality_scale": "silver", - "iot_class": "local_polling" -} diff --git a/homeassistant/components/bmp280/sensor.py b/homeassistant/components/bmp280/sensor.py deleted file mode 100644 index 5138590c1dd..00000000000 --- a/homeassistant/components/bmp280/sensor.py +++ /dev/null @@ -1,148 +0,0 @@ -"""Platform for Bosch BMP280 Environmental Sensor integration.""" -from __future__ import annotations - -from datetime import timedelta -import logging - -from adafruit_bmp280 import Adafruit_BMP280_I2C -import board -from busio import I2C -import voluptuous as vol - -from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, - SensorDeviceClass, - SensorEntity, -) -from homeassistant.const import CONF_NAME, PRESSURE_HPA, TEMP_CELSIUS -from homeassistant.core import HomeAssistant -from homeassistant.exceptions import PlatformNotReady -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 - -_LOGGER = logging.getLogger(__name__) - -DEFAULT_NAME = "BMP280" -SCAN_INTERVAL = timedelta(seconds=15) - -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=3) - -MIN_I2C_ADDRESS = 0x76 -MAX_I2C_ADDRESS = 0x77 - -CONF_I2C_ADDRESS = "i2c_address" - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Required(CONF_I2C_ADDRESS): vol.All( - vol.Coerce(int), vol.Range(min=MIN_I2C_ADDRESS, max=MAX_I2C_ADDRESS) - ), - } -) - - -def setup_platform( - hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the sensor platform.""" - _LOGGER.warning( - "The Bosch BMP280 Environmental Sensor integration is deprecated and " - "will be removed in Home Assistant Core 2022.4; " - "this integration is removed under Architectural Decision Record 0019, " - "more information can be found here: " - "https://github.com/home-assistant/architecture/blob/master/adr/0019-GPIO.md" - ) - - try: - # initializing I2C bus using the auto-detected pins - i2c = I2C(board.SCL, board.SDA) - # initializing the sensor - bmp280 = Adafruit_BMP280_I2C(i2c, address=config[CONF_I2C_ADDRESS]) - except ValueError as error: - # this usually happens when the board is I2C capable, but the device can't be found at the configured address - if str(error.args[0]).startswith("No I2C device at address"): - _LOGGER.error( - "%s. Hint: Check wiring and make sure that the SDO pin is tied to either ground (0x76) or VCC (0x77)", - error.args[0], - ) - raise PlatformNotReady() from error - _LOGGER.error(error) - return - # use custom name if there's any - name = config[CONF_NAME] - # BMP280 has both temperature and pressure sensing capability - add_entities( - [Bmp280TemperatureSensor(bmp280, name), Bmp280PressureSensor(bmp280, name)] - ) - - -class Bmp280Sensor(SensorEntity): - """Base class for BMP280 entities.""" - - def __init__( - self, - bmp280: Adafruit_BMP280_I2C, - name: str, - unit_of_measurement: str, - device_class: str, - ) -> None: - """Initialize the sensor.""" - self._bmp280 = bmp280 - self._attr_name = name - self._attr_native_unit_of_measurement = unit_of_measurement - - -class Bmp280TemperatureSensor(Bmp280Sensor): - """Representation of a Bosch BMP280 Temperature Sensor.""" - - def __init__(self, bmp280: Adafruit_BMP280_I2C, name: str) -> None: - """Initialize the entity.""" - super().__init__( - bmp280, f"{name} Temperature", TEMP_CELSIUS, SensorDeviceClass.TEMPERATURE - ) - - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self): - """Fetch new state data for the sensor.""" - try: - self._attr_native_value = round(self._bmp280.temperature, 1) - if not self.available: - _LOGGER.warning("Communication restored with temperature sensor") - self._attr_available = True - except OSError: - # this is thrown when a working sensor is unplugged between two updates - _LOGGER.warning( - "Unable to read temperature data due to a communication problem" - ) - self._attr_available = False - - -class Bmp280PressureSensor(Bmp280Sensor): - """Representation of a Bosch BMP280 Barometric Pressure Sensor.""" - - def __init__(self, bmp280: Adafruit_BMP280_I2C, name: str) -> None: - """Initialize the entity.""" - super().__init__( - bmp280, f"{name} Pressure", PRESSURE_HPA, SensorDeviceClass.PRESSURE - ) - - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self): - """Fetch new state data for the sensor.""" - try: - self._attr_native_value = round(self._bmp280.pressure) - if not self.available: - _LOGGER.warning("Communication restored with pressure sensor") - self._attr_available = True - except OSError: - # this is thrown when a working sensor is unplugged between two updates - _LOGGER.warning( - "Unable to read pressure data due to a communication problem" - ) - self._attr_available = False diff --git a/homeassistant/components/bmw_connected_drive/__init__.py b/homeassistant/components/bmw_connected_drive/__init__.py index 400ab504c22..232788b21d7 100644 --- a/homeassistant/components/bmw_connected_drive/__init__.py +++ b/homeassistant/components/bmw_connected_drive/__init__.py @@ -11,7 +11,7 @@ from bimmer_connected.vehicle import ConnectedDriveVehicle import voluptuous as vol from homeassistant.components.notify import DOMAIN as NOTIFY_DOMAIN -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_DEVICE_ID, CONF_NAME, @@ -33,7 +33,6 @@ import homeassistant.util.dt as dt_util from .const import ( ATTRIBUTION, CONF_ACCOUNT, - CONF_ALLOWED_REGIONS, CONF_READ_ONLY, DATA_ENTRIES, DATA_HASS_CONFIG, @@ -44,16 +43,7 @@ _LOGGER = logging.getLogger(__name__) DOMAIN = "bmw_connected_drive" ATTR_VIN = "vin" -ACCOUNT_SCHEMA = vol.Schema( - { - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Required(CONF_REGION): vol.In(CONF_ALLOWED_REGIONS), - vol.Optional(CONF_READ_ONLY): cv.boolean, - } -) - -CONFIG_SCHEMA = vol.Schema({DOMAIN: {cv.string: ACCOUNT_SCHEMA}}, extra=vol.ALLOW_EXTRA) +CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) SERVICE_SCHEMA = vol.Schema( vol.Any( @@ -91,17 +81,10 @@ UNDO_UPDATE_LISTENER = "undo_update_listener" async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the BMW Connected Drive component from configuration.yaml.""" + # Store full yaml config in data for platform.NOTIFY hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][DATA_HASS_CONFIG] = config - if DOMAIN in config: - for entry_config in config[DOMAIN].values(): - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=entry_config - ) - ) - return True diff --git a/homeassistant/components/bmw_connected_drive/config_flow.py b/homeassistant/components/bmw_connected_drive/config_flow.py index 3b07830c077..fec25390ff4 100644 --- a/homeassistant/components/bmw_connected_drive/config_flow.py +++ b/homeassistant/components/bmw_connected_drive/config_flow.py @@ -74,10 +74,6 @@ class BMWConnectedDriveConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): step_id="user", data_schema=DATA_SCHEMA, errors=errors ) - async def async_step_import(self, user_input: dict[str, Any]) -> FlowResult: - """Handle import.""" - return await self.async_step_user(user_input) - @staticmethod @callback def async_get_options_flow( diff --git a/homeassistant/components/bmw_connected_drive/const.py b/homeassistant/components/bmw_connected_drive/const.py index 0f79a16b702..f7908910803 100644 --- a/homeassistant/components/bmw_connected_drive/const.py +++ b/homeassistant/components/bmw_connected_drive/const.py @@ -12,7 +12,6 @@ ATTR_DIRECTION = "direction" CONF_ALLOWED_REGIONS = ["china", "north_america", "rest_of_world"] CONF_READ_ONLY = "read_only" -CONF_USE_LOCATION = "use_location" CONF_ACCOUNT = "account" diff --git a/homeassistant/components/bmw_connected_drive/translations/fr.json b/homeassistant/components/bmw_connected_drive/translations/fr.json index aadce398cdc..5181620d465 100644 --- a/homeassistant/components/bmw_connected_drive/translations/fr.json +++ b/homeassistant/components/bmw_connected_drive/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide" + "invalid_auth": "Authentification non valide" }, "step": { "user": { diff --git a/homeassistant/components/bmw_connected_drive/translations/pl.json b/homeassistant/components/bmw_connected_drive/translations/pl.json index 70467c6f9b9..146916edd5b 100644 --- a/homeassistant/components/bmw_connected_drive/translations/pl.json +++ b/homeassistant/components/bmw_connected_drive/translations/pl.json @@ -21,7 +21,7 @@ "step": { "account_options": { "data": { - "read_only": "Tylko odczyt (tylko czujniki i powiadomienia, brak wykonywania us\u0142ug, brak blokady)", + "read_only": "Tylko odczyt (tylko sensory i powiadomienia, brak wykonywania us\u0142ug, brak blokady)", "use_location": "U\u017cyj lokalizacji Home Assistant do sondowania lokalizacji samochodu (wymagane w przypadku pojazd\u00f3w innych ni\u017c i3/i8 wyprodukowanych przed 7/2014)" } } diff --git a/homeassistant/components/bond/diagnostics.py b/homeassistant/components/bond/diagnostics.py new file mode 100644 index 00000000000..6af62c3fb24 --- /dev/null +++ b/homeassistant/components/bond/diagnostics.py @@ -0,0 +1,39 @@ +"""Diagnostics support for bond.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.components.diagnostics import async_redact_data +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from .const import DOMAIN, HUB +from .utils import BondHub + +TO_REDACT = {"access_token"} + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, entry: ConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + data = hass.data[DOMAIN][entry.entry_id] + hub: BondHub = data[HUB] + return { + "entry": { + "title": entry.title, + "data": async_redact_data(entry.data, TO_REDACT), + }, + "hub": { + "version": hub._version, # pylint: disable=protected-access + }, + "devices": [ + { + "device_id": device.device_id, + "props": device.props, + "attrs": device._attrs, # pylint: disable=protected-access + "supported_actions": device._supported_actions, # pylint: disable=protected-access + } + for device in hub.devices + ], + } diff --git a/homeassistant/components/bond/fan.py b/homeassistant/components/bond/fan.py index 32edfb206a6..a2e35456ca3 100644 --- a/homeassistant/components/bond/fan.py +++ b/homeassistant/components/bond/fan.py @@ -10,7 +10,6 @@ from bond_api import Action, BPUPSubscriptions, DeviceType, Direction import voluptuous as vol from homeassistant.components.fan import ( - ATTR_SPEED, DIRECTION_FORWARD, DIRECTION_REVERSE, SUPPORT_DIRECTION, @@ -57,7 +56,7 @@ async def async_setup_entry( platform.async_register_entity_service( SERVICE_SET_FAN_SPEED_TRACKED_STATE, - {vol.Required(ATTR_SPEED): vol.All(vol.Number(scale=0), vol.Range(0, 100))}, + {vol.Required("speed"): vol.All(vol.Number(scale=0), vol.Range(0, 100))}, "async_set_speed_belief", ) @@ -107,7 +106,9 @@ class BondFan(BondEntity, FanEntity): """Return the current speed percentage for the fan.""" if not self._speed or not self._power: return 0 - return ranged_value_to_percentage(self._speed_range, self._speed) + return min( + 100, max(0, ranged_value_to_percentage(self._speed_range, self._speed)) + ) @property def speed_count(self) -> int: @@ -183,7 +184,6 @@ class BondFan(BondEntity, FanEntity): async def async_turn_on( self, - speed: str | None = None, percentage: int | None = None, preset_mode: str | None = None, **kwargs: Any, diff --git a/homeassistant/components/bond/services.yaml b/homeassistant/components/bond/services.yaml index 821f7156af6..6be18eaa1ef 100644 --- a/homeassistant/components/bond/services.yaml +++ b/homeassistant/components/bond/services.yaml @@ -115,4 +115,3 @@ stop: entity: integration: bond domain: light - diff --git a/homeassistant/components/bond/translations/fr.json b/homeassistant/components/bond/translations/fr.json index f968622e214..e821e6b85fc 100644 --- a/homeassistant/components/bond/translations/fr.json +++ b/homeassistant/components/bond/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "old_firmware": "Ancien micrologiciel non pris en charge sur l'appareil Bond - veuillez mettre \u00e0 niveau avant de continuer", "unknown": "Erreur inattendue" }, diff --git a/homeassistant/components/bosch_shc/__init__.py b/homeassistant/components/bosch_shc/__init__.py index afcf2571c31..2b95702e44c 100644 --- a/homeassistant/components/bosch_shc/__init__.py +++ b/homeassistant/components/bosch_shc/__init__.py @@ -19,7 +19,7 @@ from .const import ( DOMAIN, ) -PLATFORMS = [Platform.BINARY_SENSOR, Platform.COVER, Platform.SENSOR] +PLATFORMS = [Platform.BINARY_SENSOR, Platform.COVER, Platform.SENSOR, Platform.SWITCH] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/bosch_shc/manifest.json b/homeassistant/components/bosch_shc/manifest.json index f4fd65748f1..a5927162e50 100644 --- a/homeassistant/components/bosch_shc/manifest.json +++ b/homeassistant/components/bosch_shc/manifest.json @@ -3,7 +3,7 @@ "name": "Bosch SHC", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/bosch_shc", - "requirements": ["boschshcpy==0.2.29"], + "requirements": ["boschshcpy==0.2.30"], "zeroconf": [{ "type": "_http._tcp.local.", "name": "bosch shc*" }], "iot_class": "local_push", "codeowners": ["@tschamm"], diff --git a/homeassistant/components/bosch_shc/sensor.py b/homeassistant/components/bosch_shc/sensor.py index dc1806f0ce5..331a5ebb5f3 100644 --- a/homeassistant/components/bosch_shc/sensor.py +++ b/homeassistant/components/bosch_shc/sensor.py @@ -145,6 +145,13 @@ async def async_setup_entry( entry_id=config_entry.entry_id, ) ) + entities.append( + CommunicationQualitySensor( + device=sensor, + parent_id=session.information.unique_id, + entry_id=config_entry.entry_id, + ) + ) if entities: async_add_entities(entities) @@ -241,6 +248,23 @@ class TemperatureRatingSensor(SHCEntity, SensorEntity): return self._device.temperature_rating.name +class CommunicationQualitySensor(SHCEntity, SensorEntity): + """Representation of an SHC communication quality reporting sensor.""" + + _attr_icon = "mdi:wifi" + + def __init__(self, device: SHCDevice, parent_id: str, entry_id: str) -> None: + """Initialize an SHC communication quality reporting sensor.""" + super().__init__(device, parent_id, entry_id) + self._attr_name = f"{device.name} Communication Quality" + self._attr_unique_id = f"{device.serial}_communication_quality" + + @property + def native_value(self): + """Return the state of the sensor.""" + return self._device.communicationquality.name + + class HumidityRatingSensor(SHCEntity, SensorEntity): """Representation of an SHC humidity rating sensor.""" diff --git a/homeassistant/components/bosch_shc/switch.py b/homeassistant/components/bosch_shc/switch.py new file mode 100644 index 00000000000..aa49d873f6f --- /dev/null +++ b/homeassistant/components/bosch_shc/switch.py @@ -0,0 +1,227 @@ +"""Platform for switch integration.""" +from __future__ import annotations + +from dataclasses import dataclass + +from boschshcpy import ( + SHCCamera360, + SHCCameraEyes, + SHCLightSwitch, + SHCSession, + SHCSmartPlug, + SHCSmartPlugCompact, +) +from boschshcpy.device import SHCDevice + +from homeassistant.components.switch import ( + SwitchDeviceClass, + SwitchEntity, + SwitchEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import StateType + +from .const import DATA_SESSION, DOMAIN +from .entity import SHCEntity + + +@dataclass +class SHCSwitchRequiredKeysMixin: + """Mixin for SHC switch required keys.""" + + on_key: str + on_value: StateType + should_poll: bool + + +@dataclass +class SHCSwitchEntityDescription( + SwitchEntityDescription, + SHCSwitchRequiredKeysMixin, +): + """Class describing SHC switch entities.""" + + +SWITCH_TYPES: dict[str, SHCSwitchEntityDescription] = { + "smartplug": SHCSwitchEntityDescription( + key="smartplug", + device_class=SwitchDeviceClass.OUTLET, + on_key="state", + on_value=SHCSmartPlug.PowerSwitchService.State.ON, + should_poll=False, + ), + "smartplugcompact": SHCSwitchEntityDescription( + key="smartplugcompact", + device_class=SwitchDeviceClass.OUTLET, + on_key="state", + on_value=SHCSmartPlugCompact.PowerSwitchService.State.ON, + should_poll=False, + ), + "lightswitch": SHCSwitchEntityDescription( + key="lightswitch", + device_class=SwitchDeviceClass.SWITCH, + on_key="state", + on_value=SHCLightSwitch.PowerSwitchService.State.ON, + should_poll=False, + ), + "cameraeyes": SHCSwitchEntityDescription( + key="cameraeyes", + device_class=SwitchDeviceClass.SWITCH, + on_key="cameralight", + on_value=SHCCameraEyes.CameraLightService.State.ON, + should_poll=True, + ), + "camera360": SHCSwitchEntityDescription( + key="camera360", + device_class=SwitchDeviceClass.SWITCH, + on_key="privacymode", + on_value=SHCCamera360.PrivacyModeService.State.DISABLED, + should_poll=True, + ), +} + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the SHC switch platform.""" + entities: list[SwitchEntity] = [] + session: SHCSession = hass.data[DOMAIN][config_entry.entry_id][DATA_SESSION] + + for switch in session.device_helper.smart_plugs: + + entities.append( + SHCSwitch( + device=switch, + parent_id=session.information.unique_id, + entry_id=config_entry.entry_id, + description=SWITCH_TYPES["smartplug"], + ) + ) + entities.append( + SHCRoutingSwitch( + device=switch, + parent_id=session.information.unique_id, + entry_id=config_entry.entry_id, + ) + ) + + for switch in session.device_helper.light_switches: + + entities.append( + SHCSwitch( + device=switch, + parent_id=session.information.unique_id, + entry_id=config_entry.entry_id, + description=SWITCH_TYPES["lightswitch"], + ) + ) + + for switch in session.device_helper.smart_plugs_compact: + + entities.append( + SHCSwitch( + device=switch, + parent_id=session.information.unique_id, + entry_id=config_entry.entry_id, + description=SWITCH_TYPES["smartplugcompact"], + ) + ) + + for switch in session.device_helper.camera_eyes: + + entities.append( + SHCSwitch( + device=switch, + parent_id=session.information.unique_id, + entry_id=config_entry.entry_id, + description=SWITCH_TYPES["cameraeyes"], + ) + ) + + for switch in session.device_helper.camera_360: + + entities.append( + SHCSwitch( + device=switch, + parent_id=session.information.unique_id, + entry_id=config_entry.entry_id, + description=SWITCH_TYPES["camera360"], + ) + ) + + if entities: + async_add_entities(entities) + + +class SHCSwitch(SHCEntity, SwitchEntity): + """Representation of a SHC switch.""" + + entity_description: SHCSwitchEntityDescription + + def __init__( + self, + device: SHCDevice, + parent_id: str, + entry_id: str, + description: SHCSwitchEntityDescription, + ) -> None: + """Initialize a SHC switch.""" + super().__init__(device, parent_id, entry_id) + self.entity_description = description + + @property + def is_on(self) -> bool: + """Return the state of the switch.""" + return ( + getattr(self._device, self.entity_description.on_key) + == self.entity_description.on_value + ) + + def turn_on(self, **kwargs) -> None: + """Turn the switch on.""" + setattr(self._device, self.entity_description.on_key, True) + + def turn_off(self, **kwargs) -> None: + """Turn the switch off.""" + setattr(self._device, self.entity_description.on_key, False) + + @property + def should_poll(self) -> bool: + """Switch needs polling.""" + return self.entity_description.should_poll + + def update(self) -> None: + """Trigger an update of the device.""" + self._device.update() + + +class SHCRoutingSwitch(SHCEntity, SwitchEntity): + """Representation of a SHC routing switch.""" + + _attr_icon = "mdi:wifi" + _attr_entity_category = EntityCategory.CONFIG + + def __init__(self, device: SHCDevice, parent_id: str, entry_id: str) -> None: + """Initialize an SHC communication quality reporting sensor.""" + super().__init__(device, parent_id, entry_id) + self._attr_name = f"{device.name} Routing" + self._attr_unique_id = f"{device.serial}_routing" + + @property + def is_on(self) -> bool: + """Return the state of the switch.""" + return self._device.routing.name == "ENABLED" + + def turn_on(self, **kwargs) -> None: + """Turn the switch on.""" + self._device.routing = True + + def turn_off(self, **kwargs) -> None: + """Turn the switch off.""" + self._device.routing = False diff --git a/homeassistant/components/bosch_shc/translations/fr.json b/homeassistant/components/bosch_shc/translations/fr.json index 43eeb04490d..c34f5549663 100644 --- a/homeassistant/components/bosch_shc/translations/fr.json +++ b/homeassistant/components/bosch_shc/translations/fr.json @@ -6,7 +6,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "pairing_failed": "L'appairage a \u00e9chou\u00e9\u00a0; veuillez v\u00e9rifier que le Bosch Smart Home Controller est en mode d'appairage (voyant clignotant) et que votre mot de passe est correct.", "session_error": "Erreur de session\u00a0: l'API renvoie un r\u00e9sultat non-OK.", "unknown": "Erreur inattendue" diff --git a/homeassistant/components/braviatv/media_player.py b/homeassistant/components/braviatv/media_player.py index 69df0b245b8..99a2e5a1cb1 100644 --- a/homeassistant/components/braviatv/media_player.py +++ b/homeassistant/components/braviatv/media_player.py @@ -67,10 +67,9 @@ async def async_setup_entry( ) -class BraviaTVMediaPlayer(CoordinatorEntity, MediaPlayerEntity): +class BraviaTVMediaPlayer(CoordinatorEntity[BraviaTVCoordinator], MediaPlayerEntity): """Representation of a Bravia TV Media Player.""" - coordinator: BraviaTVCoordinator _attr_device_class = MediaPlayerDeviceClass.TV _attr_supported_features = SUPPORT_BRAVIA diff --git a/homeassistant/components/braviatv/remote.py b/homeassistant/components/braviatv/remote.py index 7e01f26d0a5..016f8363b09 100644 --- a/homeassistant/components/braviatv/remote.py +++ b/homeassistant/components/braviatv/remote.py @@ -37,11 +37,9 @@ async def async_setup_entry( ) -class BraviaTVRemote(CoordinatorEntity, RemoteEntity): +class BraviaTVRemote(CoordinatorEntity[BraviaTVCoordinator], RemoteEntity): """Representation of a Bravia TV Remote.""" - coordinator: BraviaTVCoordinator - def __init__( self, coordinator: BraviaTVCoordinator, diff --git a/homeassistant/components/braviatv/strings.json b/homeassistant/components/braviatv/strings.json index fa0f91861e2..c00b143a442 100644 --- a/homeassistant/components/braviatv/strings.json +++ b/homeassistant/components/braviatv/strings.json @@ -2,8 +2,7 @@ "config": { "step": { "user": { - "title": "Sony Bravia TV", - "description": "Set up Sony Bravia TV integration. If you have problems with configuration go to: https://www.home-assistant.io/integrations/braviatv \n\nEnsure that your TV is turned on.", + "description": "Ensure that your TV is turned on before trying to set it up.", "data": { "host": "[%key:common::config_flow::data::host%]" } diff --git a/homeassistant/components/broadlink/config_flow.py b/homeassistant/components/broadlink/config_flow.py index 8a32ba02ee8..da8b489a98b 100644 --- a/homeassistant/components/broadlink/config_flow.py +++ b/homeassistant/components/broadlink/config_flow.py @@ -188,7 +188,9 @@ class BroadlinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): await self.async_set_unique_id(device.mac.hex()) _LOGGER.error( - "Failed to authenticate to the device at %s: %s", device.host[0], err_msg + "Failed to authenticate to the device at %s: %s", + device.host[0], + err_msg, # pylint: disable=used-before-assignment ) return self.async_show_form(step_id="auth", errors=errors) @@ -251,7 +253,9 @@ class BroadlinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_finish() _LOGGER.error( - "Failed to unlock the device at %s: %s", device.host[0], err_msg + "Failed to unlock the device at %s: %s", + device.host[0], + err_msg, # pylint: disable=used-before-assignment ) else: diff --git a/homeassistant/components/broadlink/manifest.json b/homeassistant/components/broadlink/manifest.json index db1601edd67..f291ba83afa 100644 --- a/homeassistant/components/broadlink/manifest.json +++ b/homeassistant/components/broadlink/manifest.json @@ -2,11 +2,11 @@ "domain": "broadlink", "name": "Broadlink", "documentation": "https://www.home-assistant.io/integrations/broadlink", - "requirements": ["broadlink==0.18.0"], + "requirements": ["broadlink==0.18.1"], "codeowners": ["@danielhiversen", "@felipediel", "@L-I-Am"], "config_flow": true, "dhcp": [ - {"registered_devices": true}, + { "registered_devices": true }, { "macaddress": "34EA34*" }, diff --git a/homeassistant/components/broadlink/translations/el.json b/homeassistant/components/broadlink/translations/el.json index 6db366dae32..09db61f3704 100644 --- a/homeassistant/components/broadlink/translations/el.json +++ b/homeassistant/components/broadlink/translations/el.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", - "already_in_progress": "\u03a5\u03c0\u03ac\u03c1\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03bc\u03b9\u03b1 \u03c1\u03bf\u03ae \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7\u03c2 \u03c0\u03b1\u03c1\u03b1\u03bc\u03ad\u03c4\u03c1\u03c9\u03bd \u03c3\u03b5 \u03b5\u03be\u03ad\u03bb\u03b9\u03be\u03b7 \u03b3\u03b9\u03b1 \u03b1\u03c5\u03c4\u03ae\u03bd \u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae", + "already_in_progress": "\u0397 \u03c1\u03bf\u03ae \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7\u03c2 \u03b2\u03c1\u03af\u03c3\u03ba\u03b5\u03c4\u03b1\u03b9 \u03ae\u03b4\u03b7 \u03c3\u03b5 \u03b5\u03be\u03ad\u03bb\u03b9\u03be\u03b7", "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", "invalid_host": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03cc\u03bd\u03bf\u03bc\u03b1 \u03ba\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03bf\u03cd \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae \u03ae \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP", "not_supported": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b4\u03b5\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9", @@ -25,14 +25,14 @@ "title": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03cc\u03bd\u03bf\u03bc\u03b1 \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae" }, "reset": { - "description": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03c3\u03b1\u03c2 \u03b5\u03af\u03bd\u03b1\u03b9 \u03ba\u03bb\u03b5\u03b9\u03b4\u03c9\u03bc\u03ad\u03bd\u03b7 \u03b3\u03b9\u03b1 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2. \u0391\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03bf\u03b4\u03b7\u03b3\u03af\u03b5\u03c2 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03c4\u03bf \u03be\u03b5\u03ba\u03bb\u03b5\u03b9\u03b4\u03ce\u03c3\u03b5\u03c4\u03b5:\n1. \u0395\u03c1\u03b3\u03bf\u03c3\u03c4\u03b1\u03c3\u03b9\u03b1\u03ba\u03ae \u03b5\u03c0\u03b1\u03bd\u03b1\u03c6\u03bf\u03c1\u03ac \u03c4\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2.\n2. \u03a7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b5\u03c0\u03af\u03c3\u03b7\u03bc\u03b7 \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03c0\u03c1\u03bf\u03c3\u03b8\u03ad\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03c3\u03c4\u03bf \u03c4\u03bf\u03c0\u03b9\u03ba\u03cc \u03c3\u03b1\u03c2 \u03b4\u03af\u03ba\u03c4\u03c5\u03bf.\n3. \u03a3\u03c4\u03b1\u03bc\u03b1\u03c4\u03ae\u03c3\u03c4\u03b5. \u039c\u03b7\u03bd \u03bf\u03bb\u03bf\u03ba\u03bb\u03b7\u03c1\u03ce\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7. \u039a\u03bb\u03b5\u03af\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae.\n4. \u039a\u03ac\u03bd\u03c4\u03b5 \u03ba\u03bb\u03b9\u03ba \u03c3\u03c4\u03b7\u03bd \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03a5\u03c0\u03bf\u03b2\u03bf\u03bb\u03ae.", + "description": "{name} ({model} \u03c3\u03c4\u03bf {host}) \u03b5\u03af\u03bd\u03b1\u03b9 \u03ba\u03bb\u03b5\u03b9\u03b4\u03c9\u03bc\u03ad\u03bd\u03bf. \u03a0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03be\u03b5\u03ba\u03bb\u03b5\u03b9\u03b4\u03ce\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03c0\u03c1\u03b1\u03b3\u03bc\u03b1\u03c4\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03c4\u03b5 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03ba\u03b1\u03b9 \u03bd\u03b1 \u03bf\u03bb\u03bf\u03ba\u03bb\u03b7\u03c1\u03ce\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7. \u039f\u03b4\u03b7\u03b3\u03af\u03b5\u03c2:\n1. \u0391\u03bd\u03bf\u03af\u03be\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae Broadlink.\n2. \u039a\u03ac\u03bd\u03c4\u03b5 \u03ba\u03bb\u03b9\u03ba \u03c3\u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae.\n3. \u039a\u03ac\u03bd\u03c4\u03b5 \u03ba\u03bb\u03b9\u03ba \u03c3\u03c4\u03bf `...` \u03c3\u03c4\u03bf \u03b5\u03c0\u03ac\u03bd\u03c9 \u03b4\u03b5\u03be\u03af \u03bc\u03ad\u03c1\u03bf\u03c2.\n4. \u039c\u03b5\u03c4\u03b1\u03ba\u03b9\u03bd\u03b7\u03b8\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03bf \u03ba\u03ac\u03c4\u03c9 \u03bc\u03ad\u03c1\u03bf\u03c2 \u03c4\u03b7\u03c2 \u03c3\u03b5\u03bb\u03af\u03b4\u03b1\u03c2.\n5. \u0391\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf \u03ba\u03bb\u03b5\u03af\u03b4\u03c9\u03bc\u03b1.", "title": "\u039e\u03b5\u03ba\u03bb\u03b5\u03af\u03b4\u03c9\u03bc\u03b1 \u03c4\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2" }, "unlock": { "data": { "unlock": "\u039d\u03b1\u03b9, \u03ba\u03ac\u03bd\u03c4\u03bf." }, - "description": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03c3\u03b1\u03c2 \u03b5\u03af\u03bd\u03b1\u03b9 \u03ba\u03bb\u03b5\u03b9\u03b4\u03c9\u03bc\u03ad\u03bd\u03b7. \u0391\u03c5\u03c4\u03cc \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03bf\u03b4\u03b7\u03b3\u03ae\u03c3\u03b5\u03b9 \u03c3\u03b5 \u03c0\u03c1\u03bf\u03b2\u03bb\u03ae\u03bc\u03b1\u03c4\u03b1 \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03c3\u03c4\u03bf Home Assistant. \u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c4\u03bf \u03be\u03b5\u03ba\u03bb\u03b5\u03b9\u03b4\u03ce\u03c3\u03b5\u03c4\u03b5;", + "description": "{name} ({model} \u03c3\u03c4\u03bf {host}) \u03b5\u03af\u03bd\u03b1\u03b9 \u03ba\u03bb\u03b5\u03b9\u03b4\u03c9\u03bc\u03ad\u03bd\u03bf. \u0391\u03c5\u03c4\u03cc \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03bf\u03b4\u03b7\u03b3\u03ae\u03c3\u03b5\u03b9 \u03c3\u03b5 \u03c0\u03c1\u03bf\u03b2\u03bb\u03ae\u03bc\u03b1\u03c4\u03b1 \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03c3\u03c4\u03bf Home Assistant. \u0398\u03b1 \u03b8\u03ad\u03bb\u03b1\u03c4\u03b5 \u03bd\u03b1 \u03c4\u03bf \u03be\u03b5\u03ba\u03bb\u03b5\u03b9\u03b4\u03ce\u03c3\u03b5\u03c4\u03b5;", "title": "\u039e\u03b5\u03ba\u03bb\u03b5\u03af\u03b4\u03c9\u03bc\u03b1 \u03c4\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 (\u03c0\u03c1\u03bf\u03b1\u03b9\u03c1\u03b5\u03c4\u03b9\u03ba\u03cc)" }, "user": { diff --git a/homeassistant/components/brother/strings.json b/homeassistant/components/brother/strings.json index 0b00a3b30cd..9d7d42abefa 100644 --- a/homeassistant/components/brother/strings.json +++ b/homeassistant/components/brother/strings.json @@ -3,14 +3,13 @@ "flow_title": "{model} {serial_number}", "step": { "user": { - "description": "Set up Brother printer integration. If you have problems with configuration go to: https://www.home-assistant.io/integrations/brother", "data": { "host": "[%key:common::config_flow::data::host%]", "type": "Type of the printer" } }, "zeroconf_confirm": { - "description": "Do you want to add the Brother Printer {model} with serial number `{serial_number}` to Home Assistant?", + "description": "Do you want to add the printer {model} with serial number `{serial_number}` to Home Assistant?", "title": "Discovered Brother Printer", "data": { "type": "Type of the printer" diff --git a/homeassistant/components/brother/translations/fr.json b/homeassistant/components/brother/translations/fr.json index ada9ca7385d..851d46f1772 100644 --- a/homeassistant/components/brother/translations/fr.json +++ b/homeassistant/components/brother/translations/fr.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "\u00c9chec de connexion", "snmp_error": "Serveur SNMP d\u00e9sactiv\u00e9 ou imprimante non prise en charge.", - "wrong_host": "Nom d'h\u00f4te ou adresse IP invalide." + "wrong_host": "Nom d'h\u00f4te ou adresse IP non valide." }, "flow_title": "{model} {serial_number}", "step": { diff --git a/homeassistant/components/brother/translations/zh-Hant.json b/homeassistant/components/brother/translations/zh-Hant.json index 88bd481749f..016d83309f2 100644 --- a/homeassistant/components/brother/translations/zh-Hant.json +++ b/homeassistant/components/brother/translations/zh-Hant.json @@ -14,13 +14,13 @@ "user": { "data": { "host": "\u4e3b\u6a5f\u7aef", - "type": "\u5370\u8868\u6a5f\u985e\u578b" + "type": "\u5370\u8868\u6a5f\u985e\u5225" }, "description": "\u8a2d\u5b9a Brother \u5370\u8868\u6a5f\u6574\u5408\u3002\u5047\u5982\u9700\u8981\u5354\u52a9\uff0c\u8acb\u53c3\u8003\uff1ahttps://www.home-assistant.io/integrations/brother" }, "zeroconf_confirm": { "data": { - "type": "\u5370\u8868\u6a5f\u985e\u578b" + "type": "\u5370\u8868\u6a5f\u985e\u5225" }, "description": "\u662f\u5426\u8981\u5c07\u5e8f\u865f\u70ba `{serial_number}` \u4e4b Brother \u5370\u8868\u6a5f {model} \u65b0\u589e\u81f3 Home Assistant\uff1f", "title": "\u81ea\u52d5\u63a2\u7d22\u5230 Brother \u5370\u8868\u6a5f" diff --git a/homeassistant/components/browser/services.yaml b/homeassistant/components/browser/services.yaml index 1014e50db21..dd3ddd095cc 100644 --- a/homeassistant/components/browser/services.yaml +++ b/homeassistant/components/browser/services.yaml @@ -1,7 +1,6 @@ browse_url: name: Browse - description: - Open a URL in the default browser on the host machine of Home Assistant. + description: Open a URL in the default browser on the host machine of Home Assistant. fields: url: name: URL diff --git a/homeassistant/components/brunt/config_flow.py b/homeassistant/components/brunt/config_flow.py index 636a9affddd..c81eb2de6ca 100644 --- a/homeassistant/components/brunt/config_flow.py +++ b/homeassistant/components/brunt/config_flow.py @@ -111,9 +111,3 @@ class BruntConfigFlow(ConfigFlow, domain=DOMAIN): self.hass.config_entries.async_update_entry(self._reauth_entry, data=user_input) await self.hass.config_entries.async_reload(self._reauth_entry.entry_id) return self.async_abort(reason="reauth_successful") - - async def async_step_import(self, import_config: dict[str, Any]) -> FlowResult: - """Import config from configuration.yaml.""" - await self.async_set_unique_id(import_config[CONF_USERNAME].lower()) - self._abort_if_unique_id_configured() - return await self.async_step_user(import_config) diff --git a/homeassistant/components/brunt/cover.py b/homeassistant/components/brunt/cover.py index d3efdce0a5b..8bbb11914f7 100644 --- a/homeassistant/components/brunt/cover.py +++ b/homeassistant/components/brunt/cover.py @@ -2,7 +2,6 @@ from __future__ import annotations from collections.abc import MutableMapping -import logging from typing import Any from aiohttp.client_exceptions import ClientResponseError @@ -16,12 +15,11 @@ from homeassistant.components.cover import ( CoverDeviceClass, CoverEntity, ) -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -39,28 +37,9 @@ from .const import ( REGULAR_INTERVAL, ) -_LOGGER = logging.getLogger(__name__) - COVER_FEATURES = SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_SET_POSITION -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Component setup, run import config flow for each entry in config.""" - _LOGGER.warning( - "Loading brunt via platform config is deprecated; The configuration has been migrated to a config entry and can be safely removed from configuration.yaml" - ) - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=config - ) - ) - - async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, diff --git a/homeassistant/components/brunt/strings.json b/homeassistant/components/brunt/strings.json index 37b2f95bc08..2dd4a441d60 100644 --- a/homeassistant/components/brunt/strings.json +++ b/homeassistant/components/brunt/strings.json @@ -1,29 +1,29 @@ { - "config": { - "step": { - "user": { - "title": "Setup your Brunt integration", - "data": { - "username": "[%key:common::config_flow::data::username%]", - "password": "[%key:common::config_flow::data::password%]" - } - }, - "reauth_confirm": { - "title": "[%key:common::config_flow::title::reauth%]", - "description": "Please reenter the password for: {username}", - "data": { - "password": "[%key:common::config_flow::data::password%]" - } + "config": { + "step": { + "user": { + "title": "Setup your Brunt integration", + "data": { + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" } }, - "error": { - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", - "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", - "unknown": "[%key:common::config_flow::error::unknown%]" - }, - "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", - "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" + "reauth_confirm": { + "title": "[%key:common::config_flow::title::reauth%]", + "description": "Please reenter the password for: {username}", + "data": { + "password": "[%key:common::config_flow::data::password%]" + } } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } - } \ No newline at end of file + } +} diff --git a/homeassistant/components/brunt/translations/fr.json b/homeassistant/components/brunt/translations/fr.json index 611a57ea313..5368237550e 100644 --- a/homeassistant/components/brunt/translations/fr.json +++ b/homeassistant/components/brunt/translations/fr.json @@ -6,7 +6,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/button/translations/fr.json b/homeassistant/components/button/translations/fr.json index 5e6adf70da1..701770befcb 100644 --- a/homeassistant/components/button/translations/fr.json +++ b/homeassistant/components/button/translations/fr.json @@ -4,7 +4,7 @@ "press": "Appuyez sur le bouton {entity_name}" }, "trigger_type": { - "pressed": "{entity_name} a \u00e9t\u00e9 press\u00e9" + "pressed": "Appui sur {entity_name}" } }, "title": "Bouton" diff --git a/homeassistant/components/caldav/calendar.py b/homeassistant/components/caldav/calendar.py index f44a59f18eb..1955751d4fd 100644 --- a/homeassistant/components/caldav/calendar.py +++ b/homeassistant/components/caldav/calendar.py @@ -13,7 +13,7 @@ from homeassistant.components.calendar import ( ENTITY_ID_FORMAT, PLATFORM_SCHEMA, CalendarEventDevice, - calculate_offset, + extract_offset, get_date, is_offset_reached, ) @@ -147,9 +147,12 @@ class WebDavCalendarEventDevice(CalendarEventDevice): if event is None: self._event = event return - event = calculate_offset(event, OFFSET) + (summary, offset) = extract_offset(event["summary"], OFFSET) + event["summary"] = summary self._event = event - self._attr_extra_state_attributes = {"offset_reached": is_offset_reached(event)} + self._attr_extra_state_attributes = { + "offset_reached": is_offset_reached(get_date(event["start"]), offset) + } class WebDavCalendarData: diff --git a/homeassistant/components/calendar/__init__.py b/homeassistant/components/calendar/__init__.py index 3cec3792612..4449084373b 100644 --- a/homeassistant/components/calendar/__init__.py +++ b/homeassistant/components/calendar/__init__.py @@ -1,11 +1,11 @@ """Support for Google Calendar event device sensors.""" from __future__ import annotations -from datetime import timedelta +import datetime from http import HTTPStatus import logging import re -from typing import cast, final +from typing import Any, cast, final from aiohttp import web @@ -30,7 +30,7 @@ _LOGGER = logging.getLogger(__name__) DOMAIN = "calendar" ENTITY_ID_FORMAT = DOMAIN + ".{}" -SCAN_INTERVAL = timedelta(seconds=60) +SCAN_INTERVAL = datetime.timedelta(seconds=60) async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: @@ -62,18 +62,22 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return await component.async_unload_entry(entry) -def get_date(date): +def get_date(date: dict[str, Any]) -> datetime.datetime: """Get the dateTime from date or dateTime as a local.""" if "date" in date: + parsed_date = dt.parse_date(date["date"]) + assert parsed_date return dt.start_of_local_day( - dt.dt.datetime.combine(dt.parse_date(date["date"]), dt.dt.time.min) + datetime.datetime.combine(parsed_date, datetime.time.min) ) - return dt.as_local(dt.parse_datetime(date["dateTime"])) + parsed_datetime = dt.parse_datetime(date["dateTime"]) + assert parsed_datetime + return dt.as_local(parsed_datetime) -def normalize_event(event): +def normalize_event(event: dict[str, Any]) -> dict[str, Any]: """Normalize a calendar event.""" - normalized_event = {} + normalized_event: dict[str, Any] = {} start = event.get("start") end = event.get("end") @@ -97,15 +101,14 @@ def normalize_event(event): return normalized_event -def calculate_offset(event, offset): - """Calculate event offset. +def extract_offset(summary: str, offset_prefix: str) -> tuple[str, datetime.timedelta]: + """Extract the offset from the event summary. - Return the updated event with the offset_time included. + Return a tuple with the updated event summary and offset time. """ - summary = event.get("summary", "") # check if we have an offset tag in the message # time is HH:MM or MM - reg = f"{offset}([+-]?[0-9]{{0,2}}(:[0-9]{{0,2}})?)" + reg = f"{offset_prefix}([+-]?[0-9]{{0,2}}(:[0-9]{{0,2}})?)" search = re.search(reg, summary) if search and search.group(1): time = search.group(1) @@ -117,34 +120,30 @@ def calculate_offset(event, offset): offset_time = time_period_str(time) summary = (summary[: search.start()] + summary[search.end() :]).strip() - event["summary"] = summary - else: - offset_time = dt.dt.timedelta() # default it - - event["offset_time"] = offset_time - return event + return (summary, offset_time) + return (summary, datetime.timedelta()) -def is_offset_reached(event): +def is_offset_reached( + start: datetime.datetime, offset_time: datetime.timedelta +) -> bool: """Have we reached the offset time specified in the event title.""" - start = get_date(event["start"]) - if start is None or event["offset_time"] == dt.dt.timedelta(): + if offset_time == datetime.timedelta(): return False - - return start + event["offset_time"] <= dt.now(start.tzinfo) + return start + offset_time <= dt.now(start.tzinfo) class CalendarEventDevice(Entity): """Base class for calendar event entities.""" @property - def event(self): + def event(self) -> dict[str, Any] | None: """Return the next upcoming event.""" raise NotImplementedError() @final @property - def state_attributes(self): + def state_attributes(self) -> dict[str, Any] | None: """Return the entity state attributes.""" if (event := self.event) is None: return None @@ -160,7 +159,7 @@ class CalendarEventDevice(Entity): } @property - def state(self): + def state(self) -> str | None: """Return the state of the calendar event.""" if (event := self.event) is None: return STATE_OFF @@ -179,7 +178,12 @@ class CalendarEventDevice(Entity): return STATE_OFF - async def async_get_events(self, hass, start_date, end_date): + async def async_get_events( + self, + hass: HomeAssistant, + start_date: datetime.datetime, + end_date: datetime.datetime, + ) -> list[dict[str, Any]]: """Return calendar events within a datetime range.""" raise NotImplementedError() @@ -194,18 +198,21 @@ class CalendarEventView(http.HomeAssistantView): """Initialize calendar view.""" self.component = component - async def get(self, request, entity_id): + async def get(self, request: web.Request, entity_id: str) -> web.Response: """Return calendar events.""" entity = self.component.get_entity(entity_id) start = request.query.get("start") end = request.query.get("end") - if None in (start, end, entity): + if start is None or end is None or entity is None: return web.Response(status=HTTPStatus.BAD_REQUEST) + assert isinstance(entity, CalendarEventDevice) try: start_date = dt.parse_datetime(start) end_date = dt.parse_datetime(end) except (ValueError, AttributeError): return web.Response(status=HTTPStatus.BAD_REQUEST) + if start_date is None or end_date is None: + return web.Response(status=HTTPStatus.BAD_REQUEST) event_list = await entity.async_get_events( request.app["hass"], start_date, end_date ) diff --git a/homeassistant/components/calendar/manifest.json b/homeassistant/components/calendar/manifest.json index 2455744ee4e..2fb4df84414 100644 --- a/homeassistant/components/calendar/manifest.json +++ b/homeassistant/components/calendar/manifest.json @@ -3,6 +3,6 @@ "name": "Calendar", "documentation": "https://www.home-assistant.io/integrations/calendar", "dependencies": ["http"], - "codeowners": [], + "codeowners": ["@home-assistant/core"], "quality_scale": "internal" } diff --git a/homeassistant/components/calendar/translations/fr.json b/homeassistant/components/calendar/translations/fr.json index 70aaa6f0292..18d131e6143 100644 --- a/homeassistant/components/calendar/translations/fr.json +++ b/homeassistant/components/calendar/translations/fr.json @@ -1,8 +1,8 @@ { "state": { "_": { - "off": "Inactif", - "on": "Actif" + "off": "D\u00e9sactiv\u00e9", + "on": "Activ\u00e9" } }, "title": "Calendrier" diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index b955f1a0249..f3258681b52 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -10,7 +10,6 @@ from dataclasses import dataclass from datetime import datetime, timedelta from functools import partial import hashlib -import inspect import logging import os from random import SystemRandom @@ -161,18 +160,9 @@ async def _async_get_image( """ with suppress(asyncio.CancelledError, asyncio.TimeoutError): async with async_timeout.timeout(timeout): - # Calling inspect will be removed in 2022.1 after all - # custom components have had a chance to change their signature - sig = inspect.signature(camera.async_camera_image) - if "height" in sig.parameters and "width" in sig.parameters: - image_bytes = await camera.async_camera_image( - width=width, height=height - ) - else: - camera.async_warn_old_async_camera_image_signature() - image_bytes = await camera.async_camera_image() - - if image_bytes: + if image_bytes := await camera.async_camera_image( + width=width, height=height + ): content_type = camera.content_type image = Image(content_type, image_bytes) if ( @@ -576,27 +566,9 @@ class Camera(Entity): self, width: int | None = None, height: int | None = None ) -> bytes | None: """Return bytes of camera image.""" - sig = inspect.signature(self.camera_image) - # Calling inspect will be removed in 2022.1 after all - # custom components have had a chance to change their signature - if "height" in sig.parameters and "width" in sig.parameters: - return await self.hass.async_add_executor_job( - partial(self.camera_image, width=width, height=height) - ) - self.async_warn_old_async_camera_image_signature() - return await self.hass.async_add_executor_job(self.camera_image) - - # Remove in 2022.1 after all custom components have had a chance to change their signature - @callback - def async_warn_old_async_camera_image_signature(self) -> None: - """Warn once when calling async_camera_image with the function old signature.""" - if self._warned_old_signature: - return - _LOGGER.warning( - "The camera entity %s does not support requesting width and height, please open an issue with the integration author", - self.entity_id, + return await self.hass.async_add_executor_job( + partial(self.camera_image, width=width, height=height) ) - self._warned_old_signature = True async def handle_async_still_stream( self, request: web.Request, interval: float diff --git a/homeassistant/components/camera/manifest.json b/homeassistant/components/camera/manifest.json index aaa60b61a47..aa4ca61e1d5 100644 --- a/homeassistant/components/camera/manifest.json +++ b/homeassistant/components/camera/manifest.json @@ -3,8 +3,8 @@ "name": "Camera", "documentation": "https://www.home-assistant.io/integrations/camera", "dependencies": ["http"], - "requirements": ["PyTurboJPEG==1.6.5"], + "requirements": ["PyTurboJPEG==1.6.6"], "after_dependencies": ["media_player"], - "codeowners": [], + "codeowners": ["@home-assistant/core"], "quality_scale": "internal" } diff --git a/homeassistant/components/camera/recorder.py b/homeassistant/components/camera/recorder.py new file mode 100644 index 00000000000..5c141220881 --- /dev/null +++ b/homeassistant/components/camera/recorder.py @@ -0,0 +1,10 @@ +"""Integration platform for recorder.""" +from __future__ import annotations + +from homeassistant.core import HomeAssistant, callback + + +@callback +def exclude_attributes(hass: HomeAssistant) -> set[str]: + """Exclude access_token and entity_picture from being recorded in the database.""" + return {"access_token", "entity_picture"} diff --git a/homeassistant/components/camera/translations/el.json b/homeassistant/components/camera/translations/el.json index 56a57402b4d..f2faa8acf8a 100644 --- a/homeassistant/components/camera/translations/el.json +++ b/homeassistant/components/camera/translations/el.json @@ -2,7 +2,7 @@ "state": { "_": { "idle": "\u0391\u03b4\u03c1\u03b1\u03bd\u03ad\u03c2", - "recording": "\u039a\u03b1\u03c4\u03b1\u03b3\u03c1\u03ac\u03c6\u03b5\u03b9", + "recording": "\u039a\u03b1\u03c4\u03b1\u03b3\u03c1\u03b1\u03c6\u03ae", "streaming": "\u039c\u03b5\u03c4\u03ac\u03b4\u03bf\u03c3\u03b7 \u03a1\u03bf\u03ae\u03c2" } }, diff --git a/homeassistant/components/camera/translations/fr.json b/homeassistant/components/camera/translations/fr.json index d4f5cd31afc..01482ff57cc 100644 --- a/homeassistant/components/camera/translations/fr.json +++ b/homeassistant/components/camera/translations/fr.json @@ -1,7 +1,7 @@ { "state": { "_": { - "idle": "En veille", + "idle": "Inactif", "recording": "Enregistrement", "streaming": "Diffusion en cours" } diff --git a/homeassistant/components/canary/alarm_control_panel.py b/homeassistant/components/canary/alarm_control_panel.py index 4d29d4893e7..165bae0b497 100644 --- a/homeassistant/components/canary/alarm_control_panel.py +++ b/homeassistant/components/canary/alarm_control_panel.py @@ -48,10 +48,11 @@ async def async_setup_entry( async_add_entities(alarms, True) -class CanaryAlarm(CoordinatorEntity, AlarmControlPanelEntity): +class CanaryAlarm( + CoordinatorEntity[CanaryDataUpdateCoordinator], AlarmControlPanelEntity +): """Representation of a Canary alarm control panel.""" - coordinator: CanaryDataUpdateCoordinator _attr_supported_features = ( SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY | SUPPORT_ALARM_ARM_NIGHT ) diff --git a/homeassistant/components/canary/camera.py b/homeassistant/components/canary/camera.py index a4c5a5ac837..46826d80291 100644 --- a/homeassistant/components/canary/camera.py +++ b/homeassistant/components/canary/camera.py @@ -78,11 +78,9 @@ async def async_setup_entry( async_add_entities(cameras, True) -class CanaryCamera(CoordinatorEntity, Camera): +class CanaryCamera(CoordinatorEntity[CanaryDataUpdateCoordinator], Camera): """An implementation of a Canary security camera.""" - coordinator: CanaryDataUpdateCoordinator - def __init__( self, hass: HomeAssistant, diff --git a/homeassistant/components/canary/sensor.py b/homeassistant/components/canary/sensor.py index cf2b0970311..3de088016a9 100644 --- a/homeassistant/components/canary/sensor.py +++ b/homeassistant/components/canary/sensor.py @@ -76,11 +76,9 @@ async def async_setup_entry( async_add_entities(sensors, True) -class CanarySensor(CoordinatorEntity, SensorEntity): +class CanarySensor(CoordinatorEntity[CanaryDataUpdateCoordinator], SensorEntity): """Representation of a Canary sensor.""" - coordinator: CanaryDataUpdateCoordinator - def __init__( self, coordinator: CanaryDataUpdateCoordinator, diff --git a/homeassistant/components/canary/translations/el.json b/homeassistant/components/canary/translations/el.json index 6b11d642829..62ac5b949fc 100644 --- a/homeassistant/components/canary/translations/el.json +++ b/homeassistant/components/canary/translations/el.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" }, - "flow_title": "Canary: {name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/cast/helpers.py b/homeassistant/components/cast/helpers.py index ad36fb4e339..3c6ba2af43f 100644 --- a/homeassistant/components/cast/helpers.py +++ b/homeassistant/components/cast/helpers.py @@ -39,9 +39,17 @@ class ChromecastInfo: Uses blocking HTTP / HTTPS. """ + cast_info = self.cast_info + if self.cast_info.cast_type is None or self.cast_info.manufacturer is None: + # Manufacturer and cast type is not available in mDNS data, get it over http + cast_info = dial.get_cast_type( + cast_info, + zconf=ChromeCastZeroconf.get_zeroconf(), + ) + if not self.is_audio_group or self.is_dynamic_group is not None: # We have all information, no need to check HTTP API. - return self + return ChromecastInfo(cast_info=cast_info) # Fill out missing group information via HTTP API. is_dynamic_group = False @@ -57,7 +65,7 @@ class ChromecastInfo: ) return ChromecastInfo( - cast_info=self.cast_info, + cast_info=cast_info, is_dynamic_group=is_dynamic_group, ) diff --git a/homeassistant/components/cast/manifest.json b/homeassistant/components/cast/manifest.json index 2316a884b73..1e933d5e10e 100644 --- a/homeassistant/components/cast/manifest.json +++ b/homeassistant/components/cast/manifest.json @@ -3,7 +3,7 @@ "name": "Google Cast", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/cast", - "requirements": ["pychromecast==10.2.3"], + "requirements": ["pychromecast==11.0.0"], "after_dependencies": [ "cloud", "http", diff --git a/homeassistant/components/cast/translations/es.json b/homeassistant/components/cast/translations/es.json index dad23682dac..d38067588a8 100644 --- a/homeassistant/components/cast/translations/es.json +++ b/homeassistant/components/cast/translations/es.json @@ -12,7 +12,7 @@ "known_hosts": "Lista opcional de hosts conocidos si el descubrimiento mDNS no funciona." }, "description": "Introduce la configuraci\u00f3n de Google Cast.", - "title": "Google Cast" + "title": "Configuraci\u00f3n de Google Cast" }, "confirm": { "description": "\u00bfQuieres iniciar la configuraci\u00f3n?" diff --git a/homeassistant/components/cast/translations/fr.json b/homeassistant/components/cast/translations/fr.json index c07122f820f..87e7b1609fa 100644 --- a/homeassistant/components/cast/translations/fr.json +++ b/homeassistant/components/cast/translations/fr.json @@ -15,7 +15,7 @@ "title": "Google Cast" }, "confirm": { - "description": "Voulez-vous commencer la configuration ?" + "description": "Voulez-vous commencer la configuration\u00a0?" } } }, @@ -36,7 +36,7 @@ "data": { "known_hosts": "H\u00f4tes connus" }, - "description": "H\u00f4tes connus - Une liste de noms d'h\u00f4te ou d'adresses IP s\u00e9par\u00e9s par des virgules des p\u00e9riph\u00e9riques de diffusion, \u00e0 utiliser si la d\u00e9couverte mDNS ne fonctionne pas.", + "description": "H\u00f4tes connus \u2013\u00a0Une liste de noms d'h\u00f4te ou d'adresses IP s\u00e9par\u00e9s par des virgules des appareils de diffusion, \u00e0 utiliser si la d\u00e9couverte mDNS ne fonctionne pas.", "title": "Configuration de Google Cast" } } diff --git a/homeassistant/components/cast/translations/zh-Hant.json b/homeassistant/components/cast/translations/zh-Hant.json index cc538845603..58972071cbf 100644 --- a/homeassistant/components/cast/translations/zh-Hant.json +++ b/homeassistant/components/cast/translations/zh-Hant.json @@ -11,7 +11,7 @@ "data": { "known_hosts": "\u5df2\u77e5\u4e3b\u6a5f" }, - "description": "\u5df2\u77e5\u4e3b\u6a5f - \u4ee5\u9017\u865f\u5206\u9694\u7684 Chromecast \u88dd\u7f6e\u4e3b\u6a5f\u540d\u7a31 hostnames \u6216 IP \u4f4d\u5740\u3001\u5047\u5982 mDNS \u63a2\u7d22\u5931\u6548\u7684\u72c0\u6cc1\u3002", + "description": "\u5df2\u77e5\u4e3b\u6a5f - \u4ee5\u9017\u865f\u5206\u9694\u7684 Chromecast \u88dd\u7f6e\u4e3b\u6a5f\u540d\u7a31 hostnames \u6216 IP \u4f4d\u5740\u3001\u5047\u5982 mDNS \u641c\u7d22\u5931\u6548\u7684\u72c0\u6cc1\u3002", "title": "Google Cast \u8a2d\u5b9a" }, "confirm": { @@ -36,7 +36,7 @@ "data": { "known_hosts": "\u5df2\u77e5\u4e3b\u6a5f" }, - "description": "\u5df2\u77e5\u4e3b\u6a5f - \u4ee5\u9017\u865f\u5206\u9694\u7684 Chromecast \u88dd\u7f6e\u4e3b\u6a5f\u540d\u7a31 hostnames \u6216 IP \u4f4d\u5740\u3001\u5047\u5982 mDNS \u63a2\u7d22\u5931\u6548\u7684\u72c0\u6cc1\u3002", + "description": "\u5df2\u77e5\u4e3b\u6a5f - \u4ee5\u9017\u865f\u5206\u9694\u7684 Chromecast \u88dd\u7f6e\u4e3b\u6a5f\u540d\u7a31 hostnames \u6216 IP \u4f4d\u5740\u3001\u5047\u5982 mDNS \u641c\u7d22\u5931\u6548\u7684\u72c0\u6cc1\u3002", "title": "Google Cast \u8a2d\u5b9a" } } diff --git a/homeassistant/components/climacell/__init__.py b/homeassistant/components/climacell/__init__.py index e3edc778955..cf80b83fc36 100644 --- a/homeassistant/components/climacell/__init__.py +++ b/homeassistant/components/climacell/__init__.py @@ -15,7 +15,8 @@ from pyclimacell.exceptions import ( UnknownException, ) -from homeassistant.config_entries import ConfigEntry +from homeassistant.components.tomorrowio import DOMAIN as TOMORROW_DOMAIN +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( CONF_API_KEY, CONF_API_VERSION, @@ -36,22 +37,6 @@ from homeassistant.helpers.update_coordinator import ( from .const import ( ATTRIBUTION, - CC_ATTR_CLOUD_COVER, - CC_ATTR_CONDITION, - CC_ATTR_HUMIDITY, - CC_ATTR_OZONE, - CC_ATTR_PRECIPITATION, - CC_ATTR_PRECIPITATION_PROBABILITY, - CC_ATTR_PRECIPITATION_TYPE, - CC_ATTR_PRESSURE, - CC_ATTR_TEMPERATURE, - CC_ATTR_TEMPERATURE_HIGH, - CC_ATTR_TEMPERATURE_LOW, - CC_ATTR_VISIBILITY, - CC_ATTR_WIND_DIRECTION, - CC_ATTR_WIND_GUST, - CC_ATTR_WIND_SPEED, - CC_SENSOR_TYPES, CC_V3_ATTR_CLOUD_COVER, CC_V3_ATTR_CONDITION, CC_V3_ATTR_HUMIDITY, @@ -142,8 +127,21 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if params: hass.config_entries.async_update_entry(entry, **params) - api_class = ClimaCellV3 if entry.data[CONF_API_VERSION] == 3 else ClimaCellV4 - api = api_class( + hass.async_create_task( + hass.config_entries.flow.async_init( + TOMORROW_DOMAIN, + context={"source": SOURCE_IMPORT, "old_config_entry_id": entry.entry_id}, + data=entry.data, + ) + ) + + # Eventually we will remove the code that sets up the platforms and force users to + # migrate. This will only impact users still on the V3 API because we can't + # automatically migrate them, but for V4 users, we can skip the platform setup. + if entry.data[CONF_API_VERSION] == 4: + return True + + api = ClimaCellV3( entry.data[CONF_API_KEY], entry.data.get(CONF_LATITUDE, hass.config.latitude), entry.data.get(CONF_LONGITUDE, hass.config.longitude), @@ -172,7 +170,7 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> config_entry, PLATFORMS ) - hass.data[DOMAIN].pop(config_entry.entry_id) + hass.data[DOMAIN].pop(config_entry.entry_id, None) if not hass.data[DOMAIN]: hass.data.pop(DOMAIN) @@ -208,89 +206,62 @@ class ClimaCellDataUpdateCoordinator(DataUpdateCoordinator): """Update data via library.""" data: dict[str, Any] = {FORECASTS: {}} try: - if self._api_version == 3: - data[CURRENT] = await self._api.realtime( - [ - CC_V3_ATTR_TEMPERATURE, - CC_V3_ATTR_HUMIDITY, - CC_V3_ATTR_PRESSURE, - CC_V3_ATTR_WIND_SPEED, - CC_V3_ATTR_WIND_DIRECTION, - CC_V3_ATTR_CONDITION, - CC_V3_ATTR_VISIBILITY, - CC_V3_ATTR_OZONE, - CC_V3_ATTR_WIND_GUST, - CC_V3_ATTR_CLOUD_COVER, - CC_V3_ATTR_PRECIPITATION_TYPE, - *(sensor_type.key for sensor_type in CC_V3_SENSOR_TYPES), - ] - ) - data[FORECASTS][HOURLY] = await self._api.forecast_hourly( - [ - CC_V3_ATTR_TEMPERATURE, - CC_V3_ATTR_WIND_SPEED, - CC_V3_ATTR_WIND_DIRECTION, - CC_V3_ATTR_CONDITION, - CC_V3_ATTR_PRECIPITATION, - CC_V3_ATTR_PRECIPITATION_PROBABILITY, - ], - None, - timedelta(hours=24), - ) + data[CURRENT] = await self._api.realtime( + [ + CC_V3_ATTR_TEMPERATURE, + CC_V3_ATTR_HUMIDITY, + CC_V3_ATTR_PRESSURE, + CC_V3_ATTR_WIND_SPEED, + CC_V3_ATTR_WIND_DIRECTION, + CC_V3_ATTR_CONDITION, + CC_V3_ATTR_VISIBILITY, + CC_V3_ATTR_OZONE, + CC_V3_ATTR_WIND_GUST, + CC_V3_ATTR_CLOUD_COVER, + CC_V3_ATTR_PRECIPITATION_TYPE, + *(sensor_type.key for sensor_type in CC_V3_SENSOR_TYPES), + ] + ) + data[FORECASTS][HOURLY] = await self._api.forecast_hourly( + [ + CC_V3_ATTR_TEMPERATURE, + CC_V3_ATTR_WIND_SPEED, + CC_V3_ATTR_WIND_DIRECTION, + CC_V3_ATTR_CONDITION, + CC_V3_ATTR_PRECIPITATION, + CC_V3_ATTR_PRECIPITATION_PROBABILITY, + ], + None, + timedelta(hours=24), + ) - data[FORECASTS][DAILY] = await self._api.forecast_daily( - [ - CC_V3_ATTR_TEMPERATURE, - CC_V3_ATTR_WIND_SPEED, - CC_V3_ATTR_WIND_DIRECTION, - CC_V3_ATTR_CONDITION, - CC_V3_ATTR_PRECIPITATION_DAILY, - CC_V3_ATTR_PRECIPITATION_PROBABILITY, - ], - None, - timedelta(days=14), - ) + data[FORECASTS][DAILY] = await self._api.forecast_daily( + [ + CC_V3_ATTR_TEMPERATURE, + CC_V3_ATTR_WIND_SPEED, + CC_V3_ATTR_WIND_DIRECTION, + CC_V3_ATTR_CONDITION, + CC_V3_ATTR_PRECIPITATION_DAILY, + CC_V3_ATTR_PRECIPITATION_PROBABILITY, + ], + None, + timedelta(days=14), + ) - data[FORECASTS][NOWCAST] = await self._api.forecast_nowcast( - [ - CC_V3_ATTR_TEMPERATURE, - CC_V3_ATTR_WIND_SPEED, - CC_V3_ATTR_WIND_DIRECTION, - CC_V3_ATTR_CONDITION, - CC_V3_ATTR_PRECIPITATION, - ], - None, - timedelta( - minutes=min(300, self._config_entry.options[CONF_TIMESTEP] * 30) - ), - self._config_entry.options[CONF_TIMESTEP], - ) - else: - return await self._api.realtime_and_all_forecasts( - [ - CC_ATTR_TEMPERATURE, - CC_ATTR_HUMIDITY, - CC_ATTR_PRESSURE, - CC_ATTR_WIND_SPEED, - CC_ATTR_WIND_DIRECTION, - CC_ATTR_CONDITION, - CC_ATTR_VISIBILITY, - CC_ATTR_OZONE, - CC_ATTR_WIND_GUST, - CC_ATTR_CLOUD_COVER, - CC_ATTR_PRECIPITATION_TYPE, - *(sensor_type.key for sensor_type in CC_SENSOR_TYPES), - ], - [ - CC_ATTR_TEMPERATURE_LOW, - CC_ATTR_TEMPERATURE_HIGH, - CC_ATTR_WIND_SPEED, - CC_ATTR_WIND_DIRECTION, - CC_ATTR_CONDITION, - CC_ATTR_PRECIPITATION, - CC_ATTR_PRECIPITATION_PROBABILITY, - ], - ) + data[FORECASTS][NOWCAST] = await self._api.forecast_nowcast( + [ + CC_V3_ATTR_TEMPERATURE, + CC_V3_ATTR_WIND_SPEED, + CC_V3_ATTR_WIND_DIRECTION, + CC_V3_ATTR_CONDITION, + CC_V3_ATTR_PRECIPITATION, + ], + None, + timedelta( + minutes=min(300, self._config_entry.options[CONF_TIMESTEP] * 30) + ), + self._config_entry.options[CONF_TIMESTEP], + ) except ( CantConnectException, InvalidAPIKeyException, @@ -302,7 +273,7 @@ class ClimaCellDataUpdateCoordinator(DataUpdateCoordinator): return data -class ClimaCellEntity(CoordinatorEntity): +class ClimaCellEntity(CoordinatorEntity[ClimaCellDataUpdateCoordinator]): """Base ClimaCell Entity.""" def __init__( @@ -341,14 +312,6 @@ class ClimaCellEntity(CoordinatorEntity): return items.get("value") - def _get_current_property(self, property_name: str) -> int | str | float | None: - """ - Get property from current conditions. - - Used for V4 API. - """ - return self.coordinator.data.get(CURRENT, {}).get(property_name) - @property def attribution(self): """Return the attribution.""" diff --git a/homeassistant/components/climacell/config_flow.py b/homeassistant/components/climacell/config_flow.py index 61cae798ff1..ffc76479a4d 100644 --- a/homeassistant/components/climacell/config_flow.py +++ b/homeassistant/components/climacell/config_flow.py @@ -1,84 +1,15 @@ """Config flow for ClimaCell integration.""" from __future__ import annotations -import logging from typing import Any -from pyclimacell import ClimaCellV3 -from pyclimacell.exceptions import ( - CantConnectException, - InvalidAPIKeyException, - RateLimitedException, -) -from pyclimacell.pyclimacell import ClimaCellV4 import voluptuous as vol -from homeassistant import config_entries, core -from homeassistant.const import ( - CONF_API_KEY, - CONF_API_VERSION, - CONF_LATITUDE, - CONF_LONGITUDE, - CONF_NAME, -) -from homeassistant.core import HomeAssistant, callback +from homeassistant import config_entries +from homeassistant.core import callback 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 ( - CC_ATTR_TEMPERATURE, - CC_V3_ATTR_TEMPERATURE, - CONF_TIMESTEP, - DEFAULT_NAME, - DEFAULT_TIMESTEP, - DOMAIN, -) - -_LOGGER = logging.getLogger(__name__) - - -def _get_config_schema( - hass: core.HomeAssistant, input_dict: dict[str, Any] = None -) -> vol.Schema: - """ - Return schema defaults for init step based on user input/config dict. - - Retain info already provided for future form views by setting them as - defaults in schema. - """ - if input_dict is None: - input_dict = {} - - return vol.Schema( - { - vol.Required( - CONF_NAME, default=input_dict.get(CONF_NAME, DEFAULT_NAME) - ): str, - vol.Required(CONF_API_KEY, default=input_dict.get(CONF_API_KEY)): str, - vol.Required(CONF_API_VERSION, default=4): vol.In([3, 4]), - vol.Inclusive( - CONF_LATITUDE, - "location", - default=input_dict.get(CONF_LATITUDE, hass.config.latitude), - ): cv.latitude, - vol.Inclusive( - CONF_LONGITUDE, - "location", - default=input_dict.get(CONF_LONGITUDE, hass.config.longitude), - ): cv.longitude, - }, - extra=vol.REMOVE_EXTRA, - ) - - -def _get_unique_id(hass: HomeAssistant, input_dict: dict[str, Any]): - """Return unique ID from config data.""" - return ( - f"{input_dict[CONF_API_KEY]}" - f"_{input_dict.get(CONF_LATITUDE, hass.config.latitude)}" - f"_{input_dict.get(CONF_LONGITUDE, hass.config.longitude)}" - ) +from .const import CONF_TIMESTEP, DEFAULT_TIMESTEP, DOMAIN class ClimaCellOptionsConfigFlow(config_entries.OptionsFlow): @@ -117,45 +48,3 @@ class ClimaCellConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) -> ClimaCellOptionsConfigFlow: """Get the options flow for this handler.""" return ClimaCellOptionsConfigFlow(config_entry) - - async def async_step_user(self, user_input: dict[str, Any] = None) -> FlowResult: - """Handle the initial step.""" - errors = {} - if user_input is not None: - await self.async_set_unique_id( - unique_id=_get_unique_id(self.hass, user_input) - ) - self._abort_if_unique_id_configured() - - try: - if user_input[CONF_API_VERSION] == 3: - api_class = ClimaCellV3 - field = CC_V3_ATTR_TEMPERATURE - else: - api_class = ClimaCellV4 - field = CC_ATTR_TEMPERATURE - await api_class( - user_input[CONF_API_KEY], - str(user_input.get(CONF_LATITUDE, self.hass.config.latitude)), - str(user_input.get(CONF_LONGITUDE, self.hass.config.longitude)), - session=async_get_clientsession(self.hass), - ).realtime([field]) - - return self.async_create_entry( - title=user_input[CONF_NAME], data=user_input - ) - except CantConnectException: - errors["base"] = "cannot_connect" - except InvalidAPIKeyException: - errors[CONF_API_KEY] = "invalid_api_key" - except RateLimitedException: - errors[CONF_API_KEY] = "rate_limited" - except Exception: # pylint: disable=broad-except - _LOGGER.exception("Unexpected exception") - errors["base"] = "unknown" - - return self.async_show_form( - step_id="user", - data_schema=_get_config_schema(self.hass, user_input), - errors=errors, - ) diff --git a/homeassistant/components/climacell/const.py b/homeassistant/components/climacell/const.py index 7ee804f42f1..f7ca21259e1 100644 --- a/homeassistant/components/climacell/const.py +++ b/homeassistant/components/climacell/const.py @@ -5,17 +5,7 @@ from collections.abc import Callable from dataclasses import dataclass from enum import IntEnum -from pyclimacell.const import ( - DAILY, - HOURLY, - NOWCAST, - HealthConcernType, - PollenIndex, - PrecipitationType, - PrimaryPollutantType, - V3PollenIndex, - WeatherCode, -) +from pyclimacell.const import DAILY, HOURLY, NOWCAST, V3PollenIndex from homeassistant.components.sensor import SensorDeviceClass, SensorEntityDescription from homeassistant.components.weather import ( @@ -37,22 +27,7 @@ from homeassistant.const import ( CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONCENTRATION_PARTS_PER_BILLION, CONCENTRATION_PARTS_PER_MILLION, - IRRADIATION_BTUS_PER_HOUR_SQUARE_FOOT, - IRRADIATION_WATTS_PER_SQUARE_METER, - LENGTH_KILOMETERS, - LENGTH_METERS, - LENGTH_MILES, - PERCENTAGE, - PRESSURE_HPA, - PRESSURE_INHG, - SPEED_METERS_PER_SECOND, - SPEED_MILES_PER_HOUR, - TEMP_CELSIUS, - TEMP_FAHRENHEIT, ) -from homeassistant.util.distance import convert as distance_convert -from homeassistant.util.pressure import convert as pressure_convert -from homeassistant.util.temperature import convert as temp_convert CONF_TIMESTEP = "timestep" FORECAST_TYPES = [DAILY, HOURLY, NOWCAST] @@ -78,75 +53,6 @@ ATTR_WIND_GUST = "wind_gust" ATTR_CLOUD_COVER = "cloud_cover" ATTR_PRECIPITATION_TYPE = "precipitation_type" -# V4 constants -CONDITIONS = { - WeatherCode.WIND: ATTR_CONDITION_WINDY, - WeatherCode.LIGHT_WIND: ATTR_CONDITION_WINDY, - WeatherCode.STRONG_WIND: ATTR_CONDITION_WINDY, - WeatherCode.FREEZING_RAIN: ATTR_CONDITION_SNOWY_RAINY, - WeatherCode.HEAVY_FREEZING_RAIN: ATTR_CONDITION_SNOWY_RAINY, - WeatherCode.LIGHT_FREEZING_RAIN: ATTR_CONDITION_SNOWY_RAINY, - WeatherCode.FREEZING_DRIZZLE: ATTR_CONDITION_SNOWY_RAINY, - WeatherCode.ICE_PELLETS: ATTR_CONDITION_HAIL, - WeatherCode.HEAVY_ICE_PELLETS: ATTR_CONDITION_HAIL, - WeatherCode.LIGHT_ICE_PELLETS: ATTR_CONDITION_HAIL, - WeatherCode.SNOW: ATTR_CONDITION_SNOWY, - WeatherCode.HEAVY_SNOW: ATTR_CONDITION_SNOWY, - WeatherCode.LIGHT_SNOW: ATTR_CONDITION_SNOWY, - WeatherCode.FLURRIES: ATTR_CONDITION_SNOWY, - WeatherCode.THUNDERSTORM: ATTR_CONDITION_LIGHTNING, - WeatherCode.RAIN: ATTR_CONDITION_POURING, - WeatherCode.HEAVY_RAIN: ATTR_CONDITION_RAINY, - WeatherCode.LIGHT_RAIN: ATTR_CONDITION_RAINY, - WeatherCode.DRIZZLE: ATTR_CONDITION_RAINY, - WeatherCode.FOG: ATTR_CONDITION_FOG, - WeatherCode.LIGHT_FOG: ATTR_CONDITION_FOG, - WeatherCode.CLOUDY: ATTR_CONDITION_CLOUDY, - WeatherCode.MOSTLY_CLOUDY: ATTR_CONDITION_CLOUDY, - WeatherCode.PARTLY_CLOUDY: ATTR_CONDITION_PARTLYCLOUDY, -} - -# Weather constants -CC_ATTR_TIMESTAMP = "startTime" -CC_ATTR_TEMPERATURE = "temperature" -CC_ATTR_TEMPERATURE_HIGH = "temperatureMax" -CC_ATTR_TEMPERATURE_LOW = "temperatureMin" -CC_ATTR_PRESSURE = "pressureSeaLevel" -CC_ATTR_HUMIDITY = "humidity" -CC_ATTR_WIND_SPEED = "windSpeed" -CC_ATTR_WIND_DIRECTION = "windDirection" -CC_ATTR_OZONE = "pollutantO3" -CC_ATTR_CONDITION = "weatherCode" -CC_ATTR_VISIBILITY = "visibility" -CC_ATTR_PRECIPITATION = "precipitationIntensityAvg" -CC_ATTR_PRECIPITATION_PROBABILITY = "precipitationProbability" -CC_ATTR_WIND_GUST = "windGust" -CC_ATTR_CLOUD_COVER = "cloudCover" -CC_ATTR_PRECIPITATION_TYPE = "precipitationType" - -# Sensor attributes -CC_ATTR_PARTICULATE_MATTER_25 = "particulateMatter25" -CC_ATTR_PARTICULATE_MATTER_10 = "particulateMatter10" -CC_ATTR_NITROGEN_DIOXIDE = "pollutantNO2" -CC_ATTR_CARBON_MONOXIDE = "pollutantCO" -CC_ATTR_SULFUR_DIOXIDE = "pollutantSO2" -CC_ATTR_EPA_AQI = "epaIndex" -CC_ATTR_EPA_PRIMARY_POLLUTANT = "epaPrimaryPollutant" -CC_ATTR_EPA_HEALTH_CONCERN = "epaHealthConcern" -CC_ATTR_CHINA_AQI = "mepIndex" -CC_ATTR_CHINA_PRIMARY_POLLUTANT = "mepPrimaryPollutant" -CC_ATTR_CHINA_HEALTH_CONCERN = "mepHealthConcern" -CC_ATTR_POLLEN_TREE = "treeIndex" -CC_ATTR_POLLEN_WEED = "weedIndex" -CC_ATTR_POLLEN_GRASS = "grassIndex" -CC_ATTR_FIRE_INDEX = "fireIndex" -CC_ATTR_FEELS_LIKE = "temperatureApparent" -CC_ATTR_DEW_POINT = "dewPoint" -CC_ATTR_PRESSURE_SURFACE_LEVEL = "pressureSurfaceLevel" -CC_ATTR_SOLAR_GHI = "solarGHI" -CC_ATTR_CLOUD_BASE = "cloudBase" -CC_ATTR_CLOUD_CEILING = "cloudCeiling" - @dataclass class ClimaCellSensorEntityDescription(SensorEntityDescription): @@ -169,187 +75,6 @@ class ClimaCellSensorEntityDescription(SensorEntityDescription): ) -CC_SENSOR_TYPES = ( - ClimaCellSensorEntityDescription( - key=CC_ATTR_FEELS_LIKE, - name="Feels Like", - unit_imperial=TEMP_FAHRENHEIT, - unit_metric=TEMP_CELSIUS, - metric_conversion=lambda val: temp_convert(val, TEMP_FAHRENHEIT, TEMP_CELSIUS), - is_metric_check=True, - device_class=SensorDeviceClass.TEMPERATURE, - ), - ClimaCellSensorEntityDescription( - key=CC_ATTR_DEW_POINT, - name="Dew Point", - unit_imperial=TEMP_FAHRENHEIT, - unit_metric=TEMP_CELSIUS, - metric_conversion=lambda val: temp_convert(val, TEMP_FAHRENHEIT, TEMP_CELSIUS), - is_metric_check=True, - device_class=SensorDeviceClass.TEMPERATURE, - ), - ClimaCellSensorEntityDescription( - key=CC_ATTR_PRESSURE_SURFACE_LEVEL, - name="Pressure (Surface Level)", - unit_imperial=PRESSURE_INHG, - unit_metric=PRESSURE_HPA, - metric_conversion=lambda val: pressure_convert( - val, PRESSURE_INHG, PRESSURE_HPA - ), - is_metric_check=True, - device_class=SensorDeviceClass.PRESSURE, - ), - ClimaCellSensorEntityDescription( - key=CC_ATTR_SOLAR_GHI, - name="Global Horizontal Irradiance", - unit_imperial=IRRADIATION_BTUS_PER_HOUR_SQUARE_FOOT, - unit_metric=IRRADIATION_WATTS_PER_SQUARE_METER, - metric_conversion=3.15459, - is_metric_check=True, - ), - ClimaCellSensorEntityDescription( - key=CC_ATTR_CLOUD_BASE, - name="Cloud Base", - unit_imperial=LENGTH_MILES, - unit_metric=LENGTH_KILOMETERS, - metric_conversion=lambda val: distance_convert( - val, LENGTH_MILES, LENGTH_KILOMETERS - ), - is_metric_check=True, - ), - ClimaCellSensorEntityDescription( - key=CC_ATTR_CLOUD_CEILING, - name="Cloud Ceiling", - unit_imperial=LENGTH_MILES, - unit_metric=LENGTH_KILOMETERS, - metric_conversion=lambda val: distance_convert( - val, LENGTH_MILES, LENGTH_KILOMETERS - ), - is_metric_check=True, - ), - ClimaCellSensorEntityDescription( - key=CC_ATTR_CLOUD_COVER, - name="Cloud Cover", - unit_imperial=PERCENTAGE, - unit_metric=PERCENTAGE, - ), - ClimaCellSensorEntityDescription( - key=CC_ATTR_WIND_GUST, - name="Wind Gust", - unit_imperial=SPEED_MILES_PER_HOUR, - unit_metric=SPEED_METERS_PER_SECOND, - metric_conversion=lambda val: distance_convert(val, LENGTH_MILES, LENGTH_METERS) - / 3600, - is_metric_check=True, - ), - ClimaCellSensorEntityDescription( - key=CC_ATTR_PRECIPITATION_TYPE, - name="Precipitation Type", - value_map=PrecipitationType, - device_class="climacell__precipitation_type", - icon="mdi:weather-snowy-rainy", - ), - ClimaCellSensorEntityDescription( - key=CC_ATTR_OZONE, - name="Ozone", - unit_imperial=CONCENTRATION_PARTS_PER_BILLION, - unit_metric=CONCENTRATION_PARTS_PER_BILLION, - ), - ClimaCellSensorEntityDescription( - key=CC_ATTR_PARTICULATE_MATTER_25, - name="Particulate Matter < 2.5 μm", - unit_imperial=CONCENTRATION_MICROGRAMS_PER_CUBIC_FOOT, - unit_metric=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - metric_conversion=3.2808399**3, - is_metric_check=True, - ), - ClimaCellSensorEntityDescription( - key=CC_ATTR_PARTICULATE_MATTER_10, - name="Particulate Matter < 10 μm", - unit_imperial=CONCENTRATION_MICROGRAMS_PER_CUBIC_FOOT, - unit_metric=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - metric_conversion=3.2808399**3, - is_metric_check=True, - ), - ClimaCellSensorEntityDescription( - key=CC_ATTR_NITROGEN_DIOXIDE, - name="Nitrogen Dioxide", - unit_imperial=CONCENTRATION_PARTS_PER_BILLION, - unit_metric=CONCENTRATION_PARTS_PER_BILLION, - ), - ClimaCellSensorEntityDescription( - key=CC_ATTR_CARBON_MONOXIDE, - name="Carbon Monoxide", - unit_imperial=CONCENTRATION_PARTS_PER_MILLION, - unit_metric=CONCENTRATION_PARTS_PER_MILLION, - device_class=SensorDeviceClass.CO, - ), - ClimaCellSensorEntityDescription( - key=CC_ATTR_SULFUR_DIOXIDE, - name="Sulfur Dioxide", - unit_imperial=CONCENTRATION_PARTS_PER_BILLION, - unit_metric=CONCENTRATION_PARTS_PER_BILLION, - ), - ClimaCellSensorEntityDescription( - key=CC_ATTR_EPA_AQI, - name="US EPA Air Quality Index", - ), - ClimaCellSensorEntityDescription( - key=CC_ATTR_EPA_PRIMARY_POLLUTANT, - name="US EPA Primary Pollutant", - value_map=PrimaryPollutantType, - ), - ClimaCellSensorEntityDescription( - key=CC_ATTR_EPA_HEALTH_CONCERN, - name="US EPA Health Concern", - value_map=HealthConcernType, - device_class="climacell__health_concern", - icon="mdi:hospital", - ), - ClimaCellSensorEntityDescription( - key=CC_ATTR_CHINA_AQI, - name="China MEP Air Quality Index", - ), - ClimaCellSensorEntityDescription( - key=CC_ATTR_CHINA_PRIMARY_POLLUTANT, - name="China MEP Primary Pollutant", - value_map=PrimaryPollutantType, - ), - ClimaCellSensorEntityDescription( - key=CC_ATTR_CHINA_HEALTH_CONCERN, - name="China MEP Health Concern", - value_map=HealthConcernType, - device_class="climacell__health_concern", - icon="mdi:hospital", - ), - ClimaCellSensorEntityDescription( - key=CC_ATTR_POLLEN_TREE, - name="Tree Pollen Index", - value_map=PollenIndex, - device_class="climacell__pollen_index", - icon="mdi:flower-pollen", - ), - ClimaCellSensorEntityDescription( - key=CC_ATTR_POLLEN_WEED, - name="Weed Pollen Index", - value_map=PollenIndex, - device_class="climacell__pollen_index", - icon="mdi:flower-pollen", - ), - ClimaCellSensorEntityDescription( - key=CC_ATTR_POLLEN_GRASS, - name="Grass Pollen Index", - value_map=PollenIndex, - device_class="climacell__pollen_index", - icon="mdi:flower-pollen", - ), - ClimaCellSensorEntityDescription( - CC_ATTR_FIRE_INDEX, - name="Fire Index", - icon="mdi:fire", - ), -) - # V3 constants CONDITIONS_V3 = { "breezy": ATTR_CONDITION_WINDY, diff --git a/homeassistant/components/climacell/manifest.json b/homeassistant/components/climacell/manifest.json index 4928d92447e..f0eee5ef0da 100644 --- a/homeassistant/components/climacell/manifest.json +++ b/homeassistant/components/climacell/manifest.json @@ -1,9 +1,10 @@ { "domain": "climacell", "name": "ClimaCell", - "config_flow": true, + "config_flow": false, "documentation": "https://www.home-assistant.io/integrations/climacell", "requirements": ["pyclimacell==0.18.2"], + "after_dependencies": ["tomorrowio"], "codeowners": ["@raman325"], "iot_class": "cloud_polling", "loggers": ["pyclimacell"] diff --git a/homeassistant/components/climacell/sensor.py b/homeassistant/components/climacell/sensor.py index 597e1095f89..4eb9dddb9c3 100644 --- a/homeassistant/components/climacell/sensor.py +++ b/homeassistant/components/climacell/sensor.py @@ -1,8 +1,6 @@ """Sensor component that handles additional ClimaCell data for your location.""" from __future__ import annotations -from abc import abstractmethod - from pyclimacell.const import CURRENT from homeassistant.components.sensor import SensorEntity @@ -13,12 +11,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import slugify from . import ClimaCellDataUpdateCoordinator, ClimaCellEntity -from .const import ( - CC_SENSOR_TYPES, - CC_V3_SENSOR_TYPES, - DOMAIN, - ClimaCellSensorEntityDescription, -) +from .const import CC_V3_SENSOR_TYPES, DOMAIN, ClimaCellSensorEntityDescription async def async_setup_entry( @@ -28,24 +21,18 @@ async def async_setup_entry( ) -> None: """Set up a config entry.""" coordinator = hass.data[DOMAIN][config_entry.entry_id] - api_class: type[BaseClimaCellSensorEntity] - sensor_types: tuple[ClimaCellSensorEntityDescription, ...] - - if (api_version := config_entry.data[CONF_API_VERSION]) == 3: - api_class = ClimaCellV3SensorEntity - sensor_types = CC_V3_SENSOR_TYPES - else: - api_class = ClimaCellSensorEntity - sensor_types = CC_SENSOR_TYPES + api_version = config_entry.data[CONF_API_VERSION] entities = [ - api_class(hass, config_entry, coordinator, api_version, description) - for description in sensor_types + ClimaCellV3SensorEntity( + hass, config_entry, coordinator, api_version, description + ) + for description in CC_V3_SENSOR_TYPES ] async_add_entities(entities) -class BaseClimaCellSensorEntity(ClimaCellEntity, SensorEntity): - """Base ClimaCell sensor entity.""" +class ClimaCellV3SensorEntity(ClimaCellEntity, SensorEntity): + """Sensor entity that talks to ClimaCell v3 API to retrieve non-weather data.""" entity_description: ClimaCellSensorEntityDescription @@ -72,15 +59,12 @@ class BaseClimaCellSensorEntity(ClimaCellEntity, SensorEntity): else description.unit_imperial ) - @property - @abstractmethod - def _state(self) -> str | int | float | None: - """Return the raw state.""" - @property def native_value(self) -> str | int | float | None: """Return the state.""" - state = self._state + state = self._get_cc_value( + self.coordinator.data[CURRENT], self.entity_description.key + ) if ( state is not None and not isinstance(state, str) @@ -102,23 +86,3 @@ class BaseClimaCellSensorEntity(ClimaCellEntity, SensorEntity): return self.entity_description.value_map(state).name.lower() # type: ignore[misc] return state - - -class ClimaCellSensorEntity(BaseClimaCellSensorEntity): - """Sensor entity that talks to ClimaCell v4 API to retrieve non-weather data.""" - - @property - def _state(self) -> str | int | float | None: - """Return the raw state.""" - return self._get_current_property(self.entity_description.key) - - -class ClimaCellV3SensorEntity(BaseClimaCellSensorEntity): - """Sensor entity that talks to ClimaCell v3 API to retrieve non-weather data.""" - - @property - def _state(self) -> str | int | float | None: - """Return the raw state.""" - return self._get_cc_value( - self.coordinator.data[CURRENT], self.entity_description.key - ) diff --git a/homeassistant/components/climacell/strings.json b/homeassistant/components/climacell/strings.json index 7b6e01b8dd4..25ddee09dd0 100644 --- a/homeassistant/components/climacell/strings.json +++ b/homeassistant/components/climacell/strings.json @@ -1,24 +1,4 @@ { - "config": { - "step": { - "user": { - "description": "If [%key:common::config_flow::data::latitude%] and [%key:common::config_flow::data::longitude%] are not provided, the default values in the Home Assistant configuration will be used. An entity will be created for each forecast type but only the ones you select will be enabled by default.", - "data": { - "name": "[%key:common::config_flow::data::name%]", - "api_key": "[%key:common::config_flow::data::api_key%]", - "api_version": "API Version", - "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%]", - "unknown": "[%key:common::config_flow::error::unknown%]", - "rate_limited": "Currently rate limited, please try again later." - } - }, "options": { "step": { "init": { diff --git a/homeassistant/components/climacell/translations/el.json b/homeassistant/components/climacell/translations/el.json index 1f9956a75fa..26c6d5e8d40 100644 --- a/homeassistant/components/climacell/translations/el.json +++ b/homeassistant/components/climacell/translations/el.json @@ -26,7 +26,7 @@ "timestep": "\u039b\u03b5\u03c0\u03c4\u03ac \u03bc\u03b5\u03c4\u03b1\u03be\u03cd \u03c4\u03c9\u03bd \u03b4\u03b5\u03bb\u03c4\u03af\u03c9\u03bd NowCast" }, "description": "\u0395\u03ac\u03bd \u03b5\u03c0\u03b9\u03bb\u03ad\u03be\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7\u03bd \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1 \u03b4\u03b5\u03bb\u03c4\u03af\u03c9\u03bd 'nowcast', \u03bc\u03c0\u03bf\u03c1\u03b5\u03af\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03bd \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc \u03c4\u03c9\u03bd \u03bb\u03b5\u03c0\u03c4\u03ce\u03bd \u03bc\u03b5\u03c4\u03b1\u03be\u03cd \u03ba\u03ac\u03b8\u03b5 \u03b4\u03b5\u03bb\u03c4\u03af\u03bf\u03c5. \u039f \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc\u03c2 \u03c4\u03c9\u03bd \u03b4\u03b5\u03bb\u03c4\u03af\u03c9\u03bd \u03c0\u03bf\u03c5 \u03c0\u03b1\u03c1\u03ad\u03c7\u03bf\u03bd\u03c4\u03b1\u03b9 \u03b5\u03be\u03b1\u03c1\u03c4\u03ac\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03c4\u03bf\u03bd \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc \u03c4\u03c9\u03bd \u03bb\u03b5\u03c0\u03c4\u03ce\u03bd \u03c0\u03bf\u03c5 \u03b5\u03c0\u03b9\u03bb\u03ad\u03b3\u03bf\u03bd\u03c4\u03b1\u03b9 \u03bc\u03b5\u03c4\u03b1\u03be\u03cd \u03c4\u03c9\u03bd \u03b4\u03b5\u03bb\u03c4\u03af\u03c9\u03bd.", - "title": "\u0395\u03bd\u03b7\u03bc\u03ad\u03c1\u03c9\u03c3\u03b5 \u03c4\u03b9\u03c2 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 ClimaCell" + "title": "\u0395\u03bd\u03b7\u03bc\u03ad\u03c1\u03c9\u03c3\u03b7 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ce\u03bd ClimaCell" } } }, diff --git a/homeassistant/components/climacell/translations/en.json b/homeassistant/components/climacell/translations/en.json index 3e5cd436ba8..a35be85d5b2 100644 --- a/homeassistant/components/climacell/translations/en.json +++ b/homeassistant/components/climacell/translations/en.json @@ -1,24 +1,4 @@ { - "config": { - "error": { - "cannot_connect": "Failed to connect", - "invalid_api_key": "Invalid API key", - "rate_limited": "Currently rate limited, please try again later.", - "unknown": "Unexpected error" - }, - "step": { - "user": { - "data": { - "api_key": "API Key", - "api_version": "API Version", - "latitude": "Latitude", - "longitude": "Longitude", - "name": "Name" - }, - "description": "If Latitude and Longitude are not provided, the default values in the Home Assistant configuration will be used. An entity will be created for each forecast type but only the ones you select will be enabled by default." - } - } - }, "options": { "step": { "init": { @@ -29,6 +9,5 @@ "title": "Update ClimaCell Options" } } - }, - "title": "ClimaCell" + } } \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/fr.json b/homeassistant/components/climacell/translations/fr.json index 38182d67c4a..19b986a3db7 100644 --- a/homeassistant/components/climacell/translations/fr.json +++ b/homeassistant/components/climacell/translations/fr.json @@ -2,7 +2,7 @@ "config": { "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_api_key": "Cl\u00e9 API invalide", + "invalid_api_key": "Cl\u00e9 d'API non valide", "rate_limited": "Nombre maximal de tentatives de connexion d\u00e9pass\u00e9, veuillez r\u00e9essayer ult\u00e9rieurement", "unknown": "Erreur inattendue" }, diff --git a/homeassistant/components/climacell/translations/sensor.he.json b/homeassistant/components/climacell/translations/sensor.he.json new file mode 100644 index 00000000000..2a509464928 --- /dev/null +++ b/homeassistant/components/climacell/translations/sensor.he.json @@ -0,0 +1,7 @@ +{ + "state": { + "climacell__health_concern": { + "unhealthy_for_sensitive_groups": "\u05dc\u05d0 \u05d1\u05e8\u05d9\u05d0 \u05dc\u05e7\u05d1\u05d5\u05e6\u05d5\u05ea \u05e8\u05d2\u05d9\u05e9\u05d5\u05ea" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/zh-Hant.json b/homeassistant/components/climacell/translations/zh-Hant.json index 5ef7396b0e5..68e06219ae7 100644 --- a/homeassistant/components/climacell/translations/zh-Hant.json +++ b/homeassistant/components/climacell/translations/zh-Hant.json @@ -15,7 +15,7 @@ "longitude": "\u7d93\u5ea6", "name": "\u540d\u7a31" }, - "description": "\u5047\u5982\u672a\u63d0\u4f9b\u7def\u5ea6\u8207\u7d93\u5ea6\uff0c\u5c07\u6703\u4f7f\u7528 Home Assistant \u8a2d\u5b9a\u4f5c\u70ba\u9810\u8a2d\u503c\u3002\u6bcf\u4e00\u500b\u9810\u5831\u985e\u578b\u90fd\u6703\u7522\u751f\u4e00\u7d44\u5be6\u9ad4\uff0c\u6216\u8005\u9810\u8a2d\u70ba\u6240\u9078\u64c7\u555f\u7528\u7684\u9810\u5831\u3002" + "description": "\u5047\u5982\u672a\u63d0\u4f9b\u7def\u5ea6\u8207\u7d93\u5ea6\uff0c\u5c07\u6703\u4f7f\u7528 Home Assistant \u8a2d\u5b9a\u4f5c\u70ba\u9810\u8a2d\u503c\u3002\u6bcf\u4e00\u500b\u9810\u5831\u985e\u5225\u90fd\u6703\u7522\u751f\u4e00\u7d44\u5be6\u9ad4\uff0c\u6216\u8005\u9810\u8a2d\u70ba\u6240\u9078\u64c7\u555f\u7528\u7684\u9810\u5831\u3002" } } }, diff --git a/homeassistant/components/climacell/weather.py b/homeassistant/components/climacell/weather.py index e62ed4bab7c..0167cb72513 100644 --- a/homeassistant/components/climacell/weather.py +++ b/homeassistant/components/climacell/weather.py @@ -6,15 +6,7 @@ from collections.abc import Mapping from datetime import datetime from typing import Any, cast -from pyclimacell.const import ( - CURRENT, - DAILY, - FORECASTS, - HOURLY, - NOWCAST, - PrecipitationType, - WeatherCode, -) +from pyclimacell.const import CURRENT, DAILY, FORECASTS, HOURLY, NOWCAST from homeassistant.components.weather import ( ATTR_FORECAST_CONDITION, @@ -54,22 +46,6 @@ from .const import ( ATTR_CLOUD_COVER, ATTR_PRECIPITATION_TYPE, ATTR_WIND_GUST, - CC_ATTR_CLOUD_COVER, - CC_ATTR_CONDITION, - CC_ATTR_HUMIDITY, - CC_ATTR_OZONE, - CC_ATTR_PRECIPITATION, - CC_ATTR_PRECIPITATION_PROBABILITY, - CC_ATTR_PRECIPITATION_TYPE, - CC_ATTR_PRESSURE, - CC_ATTR_TEMPERATURE, - CC_ATTR_TEMPERATURE_HIGH, - CC_ATTR_TEMPERATURE_LOW, - CC_ATTR_TIMESTAMP, - CC_ATTR_VISIBILITY, - CC_ATTR_WIND_DIRECTION, - CC_ATTR_WIND_GUST, - CC_ATTR_WIND_SPEED, CC_V3_ATTR_CLOUD_COVER, CC_V3_ATTR_CONDITION, CC_V3_ATTR_HUMIDITY, @@ -88,12 +64,10 @@ from .const import ( CC_V3_ATTR_WIND_GUST, CC_V3_ATTR_WIND_SPEED, CLEAR_CONDITIONS, - CONDITIONS, CONDITIONS_V3, CONF_TIMESTEP, DEFAULT_FORECAST_TYPE, DOMAIN, - MAX_FORECASTS, ) @@ -105,10 +79,8 @@ async def async_setup_entry( """Set up a config entry.""" coordinator = hass.data[DOMAIN][config_entry.entry_id] api_version = config_entry.data[CONF_API_VERSION] - - api_class = ClimaCellV3WeatherEntity if api_version == 3 else ClimaCellWeatherEntity entities = [ - api_class(config_entry, coordinator, api_version, forecast_type) + ClimaCellV3WeatherEntity(config_entry, coordinator, api_version, forecast_type) for forecast_type in (DAILY, HOURLY, NOWCAST) ] async_add_entities(entities) @@ -267,154 +239,6 @@ class BaseClimaCellWeatherEntity(ClimaCellEntity, WeatherEntity): return self._visibility -class ClimaCellWeatherEntity(BaseClimaCellWeatherEntity): - """Entity that talks to ClimaCell v4 API to retrieve weather data.""" - - _attr_temperature_unit = TEMP_FAHRENHEIT - - @staticmethod - def _translate_condition( - condition: int | str | None, sun_is_up: bool = True - ) -> str | None: - """Translate ClimaCell condition into an HA condition.""" - if condition is None: - return None - # We won't guard here, instead we will fail hard - condition = WeatherCode(condition) - if condition in (WeatherCode.CLEAR, WeatherCode.MOSTLY_CLEAR): - if sun_is_up: - return CLEAR_CONDITIONS["day"] - return CLEAR_CONDITIONS["night"] - return CONDITIONS[condition] - - @property - def temperature(self): - """Return the platform temperature.""" - return self._get_current_property(CC_ATTR_TEMPERATURE) - - @property - def _pressure(self): - """Return the raw pressure.""" - return self._get_current_property(CC_ATTR_PRESSURE) - - @property - def humidity(self): - """Return the humidity.""" - return self._get_current_property(CC_ATTR_HUMIDITY) - - @property - def wind_gust(self): - """Return the wind gust speed.""" - return self._get_current_property(CC_ATTR_WIND_GUST) - - @property - def cloud_cover(self): - """Return the cloud cover.""" - return self._get_current_property(CC_ATTR_CLOUD_COVER) - - @property - def precipitation_type(self): - """Return precipitation type.""" - precipitation_type = self._get_current_property(CC_ATTR_PRECIPITATION_TYPE) - if precipitation_type is None: - return None - return PrecipitationType(precipitation_type).name.lower() - - @property - def _wind_speed(self): - """Return the raw wind speed.""" - return self._get_current_property(CC_ATTR_WIND_SPEED) - - @property - def wind_bearing(self): - """Return the wind bearing.""" - return self._get_current_property(CC_ATTR_WIND_DIRECTION) - - @property - def ozone(self): - """Return the O3 (ozone) level.""" - return self._get_current_property(CC_ATTR_OZONE) - - @property - def condition(self): - """Return the condition.""" - return self._translate_condition( - self._get_current_property(CC_ATTR_CONDITION), - is_up(self.hass), - ) - - @property - def _visibility(self): - """Return the raw visibility.""" - return self._get_current_property(CC_ATTR_VISIBILITY) - - @property - def forecast(self): - """Return the forecast.""" - # Check if forecasts are available - raw_forecasts = self.coordinator.data.get(FORECASTS, {}).get(self.forecast_type) - if not raw_forecasts: - return None - - forecasts = [] - max_forecasts = MAX_FORECASTS[self.forecast_type] - forecast_count = 0 - - # Set default values (in cases where keys don't exist), None will be - # returned. Override properties per forecast type as needed - for forecast in raw_forecasts: - forecast_dt = dt_util.parse_datetime(forecast[CC_ATTR_TIMESTAMP]) - - # Throw out past data - if forecast_dt.date() < dt_util.utcnow().date(): - continue - - values = forecast["values"] - use_datetime = True - - condition = values.get(CC_ATTR_CONDITION) - precipitation = values.get(CC_ATTR_PRECIPITATION) - precipitation_probability = values.get(CC_ATTR_PRECIPITATION_PROBABILITY) - - temp = values.get(CC_ATTR_TEMPERATURE_HIGH) - temp_low = None - wind_direction = values.get(CC_ATTR_WIND_DIRECTION) - wind_speed = values.get(CC_ATTR_WIND_SPEED) - - if self.forecast_type == DAILY: - use_datetime = False - temp_low = values.get(CC_ATTR_TEMPERATURE_LOW) - if precipitation: - precipitation = precipitation * 24 - elif self.forecast_type == NOWCAST: - # Precipitation is forecasted in CONF_TIMESTEP increments but in a - # per hour rate, so value needs to be converted to an amount. - if precipitation: - precipitation = ( - precipitation / 60 * self._config_entry.options[CONF_TIMESTEP] - ) - - forecasts.append( - self._forecast_dict( - forecast_dt, - use_datetime, - condition, - precipitation, - precipitation_probability, - temp, - temp_low, - wind_direction, - wind_speed, - ) - ) - - forecast_count += 1 - if forecast_count == max_forecasts: - break - - return forecasts - - class ClimaCellV3WeatherEntity(BaseClimaCellWeatherEntity): """Entity that talks to ClimaCell v3 API to retrieve weather data.""" diff --git a/homeassistant/components/climate/manifest.json b/homeassistant/components/climate/manifest.json index 5d950ccbe2d..8b54d3a91ad 100644 --- a/homeassistant/components/climate/manifest.json +++ b/homeassistant/components/climate/manifest.json @@ -2,6 +2,6 @@ "domain": "climate", "name": "Climate", "documentation": "https://www.home-assistant.io/integrations/climate", - "codeowners": [], + "codeowners": ["@home-assistant/core"], "quality_scale": "internal" } diff --git a/homeassistant/components/climate/recorder.py b/homeassistant/components/climate/recorder.py new file mode 100644 index 00000000000..879e6bfbbac --- /dev/null +++ b/homeassistant/components/climate/recorder.py @@ -0,0 +1,32 @@ +"""Integration platform for recorder.""" +from __future__ import annotations + +from homeassistant.core import HomeAssistant, callback + +from .const import ( + ATTR_FAN_MODES, + ATTR_HVAC_MODES, + ATTR_MAX_HUMIDITY, + ATTR_MAX_TEMP, + ATTR_MIN_HUMIDITY, + ATTR_MIN_TEMP, + ATTR_PRESET_MODES, + ATTR_SWING_MODES, + ATTR_TARGET_TEMP_STEP, +) + + +@callback +def exclude_attributes(hass: HomeAssistant) -> set[str]: + """Exclude static attributes from being recorded in the database.""" + return { + ATTR_HVAC_MODES, + ATTR_FAN_MODES, + ATTR_SWING_MODES, + ATTR_MIN_TEMP, + ATTR_MAX_TEMP, + ATTR_MIN_HUMIDITY, + ATTR_MAX_HUMIDITY, + ATTR_TARGET_TEMP_STEP, + ATTR_PRESET_MODES, + } diff --git a/homeassistant/components/climate/services.yaml b/homeassistant/components/climate/services.yaml index 7b9d7fe4a72..40d518456b4 100644 --- a/homeassistant/components/climate/services.yaml +++ b/homeassistant/components/climate/services.yaml @@ -71,13 +71,20 @@ set_temperature: selector: select: options: - - "off" - - "auto" - - "cool" - - "dry" - - "fan_only" - - "heat_cool" - - "heat" + - label: "Off" + value: "off" + - label: "Auto" + value: "auto" + - label: "Cool" + value: "cool" + - label: "Dry" + value: "dry" + - label: "Fan Only" + value: "fan_only" + - label: "Heat/Cool" + value: "heat_cool" + - label: "Heat" + value: "heat" set_humidity: name: Set target humidity @@ -124,13 +131,20 @@ set_hvac_mode: selector: select: options: - - "off" - - "auto" - - "cool" - - "dry" - - "fan_only" - - "heat_cool" - - "heat" + - label: "Off" + value: "off" + - label: "Auto" + value: "auto" + - label: "Cool" + value: "cool" + - label: "Dry" + value: "dry" + - label: "Fan Only" + value: "fan_only" + - label: "Heat/Cool" + value: "heat_cool" + - label: "Heat" + value: "heat" set_swing_mode: name: Set swing mode diff --git a/homeassistant/components/climate/translations/bg.json b/homeassistant/components/climate/translations/bg.json index 7c7389545eb..6c3eb3b612a 100644 --- a/homeassistant/components/climate/translations/bg.json +++ b/homeassistant/components/climate/translations/bg.json @@ -18,7 +18,7 @@ "_": { "auto": "\u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u043d", "cool": "\u041e\u0445\u043b\u0430\u0436\u0434\u0430\u043d\u0435", - "dry": "\u0421\u0443\u0445", + "dry": "\u0418\u0437\u0441\u0443\u0448\u0430\u0432\u0430\u043d\u0435", "fan_only": "\u0421\u0430\u043c\u043e \u0432\u0435\u043d\u0442\u0438\u043b\u0430\u0442\u043e\u0440", "heat": "\u041e\u0442\u043e\u043f\u043b\u0435\u043d\u0438\u0435", "heat_cool": "\u041e\u0442\u043e\u043f\u043b\u0435\u043d\u0438\u0435/\u041e\u0445\u043b\u0430\u0436\u0434\u0430\u043d\u0435", diff --git a/homeassistant/components/climate/translations/el.json b/homeassistant/components/climate/translations/el.json index a7aa5386417..b195ee8124c 100644 --- a/homeassistant/components/climate/translations/el.json +++ b/homeassistant/components/climate/translations/el.json @@ -17,13 +17,13 @@ "state": { "_": { "auto": "\u0391\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03bf", - "cool": "\u0394\u03c1\u03bf\u03c3\u03b5\u03c1\u03cc", + "cool": "\u03a8\u03cd\u03be\u03b7", "dry": "\u039e\u03b7\u03c1\u03cc", "fan_only": "\u0391\u03bd\u03b5\u03bc\u03b9\u03c3\u03c4\u03ae\u03c1\u03b1\u03c2 \u03bc\u03cc\u03bd\u03bf", - "heat": "\u0398\u03b5\u03c1\u03bc\u03cc", - "heat_cool": "\u0398\u03ad\u03c1\u03bc\u03b1\u03bd\u03c3\u03b7 / \u03a8\u03cd\u03be\u03b7", + "heat": "\u0398\u03ad\u03c1\u03bc\u03b1\u03bd\u03c3\u03b7", + "heat_cool": "\u0398\u03ad\u03c1\u03bc\u03b1\u03bd\u03c3\u03b7/\u03a8\u03cd\u03be\u03b7", "off": "\u0391\u03bd\u03b5\u03bd\u03b5\u03c1\u03b3\u03cc" } }, - "title": "\u03a4\u03bf \u03ba\u03bb\u03af\u03bc\u03b1" + "title": "\u039a\u03bb\u03af\u03bc\u03b1" } \ No newline at end of file diff --git a/homeassistant/components/climate/translations/fr.json b/homeassistant/components/climate/translations/fr.json index 913c2579478..58de5fc4e76 100644 --- a/homeassistant/components/climate/translations/fr.json +++ b/homeassistant/components/climate/translations/fr.json @@ -22,7 +22,7 @@ "fan_only": "Ventilateur seul", "heat": "Chauffe", "heat_cool": "Chaud/Froid", - "off": "Inactif" + "off": "D\u00e9sactiv\u00e9" } }, "title": "Thermostat" diff --git a/homeassistant/components/cloud/alexa_config.py b/homeassistant/components/cloud/alexa_config.py index 11b122e2b5a..cf52b458a28 100644 --- a/homeassistant/components/cloud/alexa_config.py +++ b/homeassistant/components/cloud/alexa_config.py @@ -133,7 +133,10 @@ class CloudAlexaConfig(alexa_config.AbstractConfig): entity_registry = er.async_get(self.hass) if registry_entry := entity_registry.async_get(entity_id): - auxiliary_entity = registry_entry.entity_category is not None + auxiliary_entity = ( + registry_entry.entity_category is not None + or registry_entry.hidden_by is not None + ) else: auxiliary_entity = False diff --git a/homeassistant/components/cloud/google_config.py b/homeassistant/components/cloud/google_config.py index 7988a648901..e2b21ffc56d 100644 --- a/homeassistant/components/cloud/google_config.py +++ b/homeassistant/components/cloud/google_config.py @@ -124,7 +124,10 @@ class CloudGoogleConfig(AbstractConfig): entity_registry = er.async_get(self.hass) if registry_entry := entity_registry.async_get(entity_id): - auxiliary_entity = registry_entry.entity_category is not None + auxiliary_entity = ( + registry_entry.entity_category is not None + or registry_entry.hidden_by is not None + ) else: auxiliary_entity = False diff --git a/homeassistant/components/cloud/http_api.py b/homeassistant/components/cloud/http_api.py index 0ea1fc1d3f3..6086cef703a 100644 --- a/homeassistant/components/cloud/http_api.py +++ b/homeassistant/components/cloud/http_api.py @@ -420,7 +420,11 @@ async def _account_data(hass: HomeAssistant, cloud: Cloud): """Generate the auth data JSON response.""" if not cloud.is_logged_in: - return {"logged_in": False, "cloud": STATE_DISCONNECTED} + return { + "logged_in": False, + "cloud": STATE_DISCONNECTED, + "http_use_ssl": hass.config.api.use_ssl, + } claims = cloud.claims client = cloud.client @@ -457,6 +461,7 @@ async def _account_data(hass: HomeAssistant, cloud: Cloud): "remote_connected": remote.is_connected, "remote_domain": remote.instance_domain, "http_use_ssl": hass.config.api.use_ssl, + "active_subscription": not cloud.subscription_expired, } diff --git a/homeassistant/components/cloud/strings.json b/homeassistant/components/cloud/strings.json index d38a0c272a7..a799a8cee59 100644 --- a/homeassistant/components/cloud/strings.json +++ b/homeassistant/components/cloud/strings.json @@ -14,4 +14,4 @@ "subscription_expiration": "Subscription Expiration" } } -} \ No newline at end of file +} diff --git a/homeassistant/components/cloudflare/strings.json b/homeassistant/components/cloudflare/strings.json index 31df9a62341..89bc67feeed 100644 --- a/homeassistant/components/cloudflare/strings.json +++ b/homeassistant/components/cloudflare/strings.json @@ -18,7 +18,7 @@ "records": { "title": "Choose the Records to Update", "data": { - "records": "Records" + "records": "Records" } }, "reauth_confirm": { diff --git a/homeassistant/components/cloudflare/translations/fr.json b/homeassistant/components/cloudflare/translations/fr.json index 73c2d76b4fc..6b38336df24 100644 --- a/homeassistant/components/cloudflare/translations/fr.json +++ b/homeassistant/components/cloudflare/translations/fr.json @@ -7,8 +7,8 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", - "invalid_zone": "Zone invalide" + "invalid_auth": "Authentification non valide", + "invalid_zone": "Zone non valide" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/co2signal/manifest.json b/homeassistant/components/co2signal/manifest.json index 2af5c8bcb2f..687d02b634a 100644 --- a/homeassistant/components/co2signal/manifest.json +++ b/homeassistant/components/co2signal/manifest.json @@ -2,11 +2,9 @@ "domain": "co2signal", "name": "CO2 Signal", "documentation": "https://www.home-assistant.io/integrations/co2signal", - "requirements": [ - "co2signal==0.4.2" - ], + "requirements": ["co2signal==0.4.2"], "codeowners": [], "iot_class": "cloud_polling", "config_flow": true, "loggers": ["CO2Signal"] -} \ No newline at end of file +} diff --git a/homeassistant/components/co2signal/sensor.py b/homeassistant/components/co2signal/sensor.py index b10cd054ff9..23303474e3b 100644 --- a/homeassistant/components/co2signal/sensor.py +++ b/homeassistant/components/co2signal/sensor.py @@ -9,13 +9,13 @@ from homeassistant.components.sensor import SensorEntity, SensorStateClass from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_ATTRIBUTION, PERCENTAGE from homeassistant.core import HomeAssistant -from homeassistant.helpers import update_coordinator from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType +from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import CO2SignalCoordinator, CO2SignalResponse +from . import CO2SignalCoordinator from .const import ATTRIBUTION, DOMAIN SCAN_INTERVAL = timedelta(minutes=3) @@ -55,7 +55,7 @@ async def async_setup_entry( async_add_entities(CO2Sensor(coordinator, description) for description in SENSORS) -class CO2Sensor(update_coordinator.CoordinatorEntity[CO2SignalResponse], SensorEntity): +class CO2Sensor(CoordinatorEntity[CO2SignalCoordinator], SensorEntity): """Implementation of the CO2Signal sensor.""" _attr_state_class = SensorStateClass.MEASUREMENT @@ -98,7 +98,7 @@ class CO2Sensor(update_coordinator.CoordinatorEntity[CO2SignalResponse], SensorE @property def native_value(self) -> StateType: """Return sensor state.""" - if (value := self.coordinator.data["data"][self._description.key]) is None: # type: ignore[misc] + if (value := self.coordinator.data["data"][self._description.key]) is None: # type: ignore[literal-required] return None return round(value, 2) diff --git a/homeassistant/components/co2signal/translations/fr.json b/homeassistant/components/co2signal/translations/fr.json index 1ed60fd3227..6467c1bf43a 100644 --- a/homeassistant/components/co2signal/translations/fr.json +++ b/homeassistant/components/co2signal/translations/fr.json @@ -7,7 +7,7 @@ }, "error": { "api_ratelimit": "Limite de d\u00e9bit API d\u00e9pass\u00e9e", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/coinbase/config_flow.py b/homeassistant/components/coinbase/config_flow.py index ea54c955236..0687bd3f305 100644 --- a/homeassistant/components/coinbase/config_flow.py +++ b/homeassistant/components/coinbase/config_flow.py @@ -18,6 +18,8 @@ from .const import ( API_TYPE_VAULT, CONF_CURRENCIES, CONF_EXCHANGE_BASE, + CONF_EXCHANGE_PRECISION, + CONF_EXCHANGE_PRECISION_DEFAULT, CONF_EXCHANGE_RATES, CONF_OPTIONS, CONF_YAML_API_TOKEN, @@ -177,6 +179,9 @@ class OptionsFlowHandler(config_entries.OptionsFlow): default_currencies = self.config_entry.options.get(CONF_CURRENCIES, []) default_exchange_rates = self.config_entry.options.get(CONF_EXCHANGE_RATES, []) default_exchange_base = self.config_entry.options.get(CONF_EXCHANGE_BASE, "USD") + default_exchange_precision = self.config_entry.options.get( + CONF_EXCHANGE_PRECISION, CONF_EXCHANGE_PRECISION_DEFAULT + ) if user_input is not None: # Pass back user selected options, even if bad @@ -189,6 +194,9 @@ class OptionsFlowHandler(config_entries.OptionsFlow): if CONF_EXCHANGE_RATES in user_input: default_exchange_base = user_input[CONF_EXCHANGE_BASE] + if CONF_EXCHANGE_PRECISION in user_input: + default_exchange_precision = user_input[CONF_EXCHANGE_PRECISION] + try: await validate_options(self.hass, self.config_entry, user_input) except CurrencyUnavailable: @@ -217,6 +225,9 @@ class OptionsFlowHandler(config_entries.OptionsFlow): CONF_EXCHANGE_BASE, default=default_exchange_base, ): vol.In(WALLETS), + vol.Optional( + CONF_EXCHANGE_PRECISION, default=default_exchange_precision + ): int, } ), errors=errors, diff --git a/homeassistant/components/coinbase/const.py b/homeassistant/components/coinbase/const.py index e5573f1890f..85535613851 100644 --- a/homeassistant/components/coinbase/const.py +++ b/homeassistant/components/coinbase/const.py @@ -3,6 +3,8 @@ CONF_CURRENCIES = "account_balance_currencies" CONF_EXCHANGE_BASE = "exchange_base" CONF_EXCHANGE_RATES = "exchange_rate_currencies" +CONF_EXCHANGE_PRECISION = "exchange_rate_precision" +CONF_EXCHANGE_PRECISION_DEFAULT = 2 CONF_OPTIONS = "options" CONF_TITLE = "title" DOMAIN = "coinbase" diff --git a/homeassistant/components/coinbase/manifest.json b/homeassistant/components/coinbase/manifest.json index add24a8fd41..752c881d25b 100644 --- a/homeassistant/components/coinbase/manifest.json +++ b/homeassistant/components/coinbase/manifest.json @@ -2,13 +2,9 @@ "domain": "coinbase", "name": "Coinbase", "documentation": "https://www.home-assistant.io/integrations/coinbase", - "requirements": [ - "coinbase==2.1.0" - ], - "codeowners": [ - "@tombrien" - ], + "requirements": ["coinbase==2.1.0"], + "codeowners": ["@tombrien"], "config_flow": true, "iot_class": "cloud_polling", "loggers": ["coinbase"] -} \ No newline at end of file +} diff --git a/homeassistant/components/coinbase/sensor.py b/homeassistant/components/coinbase/sensor.py index 8b573cffb83..fb9ce0fe434 100644 --- a/homeassistant/components/coinbase/sensor.py +++ b/homeassistant/components/coinbase/sensor.py @@ -22,6 +22,8 @@ from .const import ( API_RESOURCE_TYPE, API_TYPE_VAULT, CONF_CURRENCIES, + CONF_EXCHANGE_PRECISION, + CONF_EXCHANGE_PRECISION_DEFAULT, CONF_EXCHANGE_RATES, DOMAIN, ) @@ -66,6 +68,10 @@ async def async_setup_entry( exchange_base_currency = instance.exchange_rates[API_ACCOUNT_CURRENCY] + exchange_precision = config_entry.options.get( + CONF_EXCHANGE_PRECISION, CONF_EXCHANGE_PRECISION_DEFAULT + ) + for currency in desired_currencies: if currency not in provided_currencies: _LOGGER.warning( @@ -80,9 +86,7 @@ async def async_setup_entry( for rate in config_entry.options[CONF_EXCHANGE_RATES]: entities.append( ExchangeRateSensor( - instance, - rate, - exchange_base_currency, + instance, rate, exchange_base_currency, exchange_precision ) ) @@ -178,14 +182,16 @@ class AccountSensor(SensorEntity): class ExchangeRateSensor(SensorEntity): """Representation of a Coinbase.com sensor.""" - def __init__(self, coinbase_data, exchange_currency, exchange_base): + def __init__(self, coinbase_data, exchange_currency, exchange_base, precision): """Initialize the sensor.""" self._coinbase_data = coinbase_data self.currency = exchange_currency self._name = f"{exchange_currency} Exchange Rate" self._id = f"coinbase-{coinbase_data.user_id}-xe-{exchange_currency}" + self._precision = precision self._state = round( - 1 / float(self._coinbase_data.exchange_rates[API_RATES][self.currency]), 2 + 1 / float(self._coinbase_data.exchange_rates[API_RATES][self.currency]), + self._precision, ) self._unit_of_measurement = exchange_base self._attr_state_class = SensorStateClass.MEASUREMENT @@ -231,5 +237,6 @@ class ExchangeRateSensor(SensorEntity): """Get the latest state of the sensor.""" self._coinbase_data.update() self._state = round( - 1 / float(self._coinbase_data.exchange_rates.rates[self.currency]), 2 + 1 / float(self._coinbase_data.exchange_rates.rates[self.currency]), + self._precision, ) diff --git a/homeassistant/components/coinbase/strings.json b/homeassistant/components/coinbase/strings.json index 23602e79f1e..96bf021e394 100644 --- a/homeassistant/components/coinbase/strings.json +++ b/homeassistant/components/coinbase/strings.json @@ -28,7 +28,8 @@ "data": { "account_balance_currencies": "Wallet balances to report.", "exchange_rate_currencies": "Exchange rates to report.", - "exchange_base": "Base currency for exchange rate sensors." + "exchange_base": "Base currency for exchange rate sensors.", + "exchnage_rate_precision": "Number of decimal places for exchange rates." } } }, @@ -38,4 +39,4 @@ "exchange_rate_unavailable": "One or more of the requested exchange rates is not provided by Coinbase." } } -} \ No newline at end of file +} diff --git a/homeassistant/components/coinbase/translations/en.json b/homeassistant/components/coinbase/translations/en.json index 74205395410..019159c8057 100644 --- a/homeassistant/components/coinbase/translations/en.json +++ b/homeassistant/components/coinbase/translations/en.json @@ -14,9 +14,7 @@ "user": { "data": { "api_key": "API Key", - "api_token": "API Secret", - "currencies": "Account Balance Currencies", - "exchange_rates": "Exchange Rates" + "api_token": "API Secret" }, "description": "Please enter the details of your API key as provided by Coinbase.", "title": "Coinbase API Key Details" @@ -26,9 +24,7 @@ "options": { "error": { "currency_unavailable": "One or more of the requested currency balances is not provided by your Coinbase API.", - "currency_unavaliable": "One or more of the requested currency balances is not provided by your Coinbase API.", "exchange_rate_unavailable": "One or more of the requested exchange rates is not provided by Coinbase.", - "exchange_rate_unavaliable": "One or more of the requested exchange rates is not provided by Coinbase.", "unknown": "Unexpected error" }, "step": { @@ -36,7 +32,8 @@ "data": { "account_balance_currencies": "Wallet balances to report.", "exchange_base": "Base currency for exchange rate sensors.", - "exchange_rate_currencies": "Exchange rates to report." + "exchange_rate_currencies": "Exchange rates to report.", + "exchnage_rate_precision": "Number of decimal places for exchange rates." }, "description": "Adjust Coinbase Options" } diff --git a/homeassistant/components/coinbase/translations/fr.json b/homeassistant/components/coinbase/translations/fr.json index 74ee4c61f97..01cd7ec6dbf 100644 --- a/homeassistant/components/coinbase/translations/fr.json +++ b/homeassistant/components/coinbase/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "invalid_auth_key": "Identifiants API rejet\u00e9s par Coinbase en raison d'une cl\u00e9 API non valide.", "invalid_auth_secret": "Identifiants API rejet\u00e9s par Coinbase en raison d'un secret API non valide.", "unknown": "Erreur inattendue" diff --git a/homeassistant/components/coinbase/translations/pl.json b/homeassistant/components/coinbase/translations/pl.json index e93e71d9e26..14432be5928 100644 --- a/homeassistant/components/coinbase/translations/pl.json +++ b/homeassistant/components/coinbase/translations/pl.json @@ -35,7 +35,7 @@ "init": { "data": { "account_balance_currencies": "Salda portfela do zg\u0142oszenia.", - "exchange_base": "Waluta bazowa dla czujnik\u00f3w kurs\u00f3w walut.", + "exchange_base": "Waluta bazowa dla sensor\u00f3w kurs\u00f3w walut.", "exchange_rate_currencies": "Kursy walut do zg\u0142oszenia." }, "description": "Dostosuj opcje Coinbase" diff --git a/homeassistant/components/color_extractor/__init__.py b/homeassistant/components/color_extractor/__init__.py index cbc5b0c8821..b0884643be9 100644 --- a/homeassistant/components/color_extractor/__init__.py +++ b/homeassistant/components/color_extractor/__init__.py @@ -80,7 +80,7 @@ async def async_setup(hass: HomeAssistant, hass_config: ConfigType) -> bool: except UnidentifiedImageError as ex: _LOGGER.error( "Bad image from %s '%s' provided, are you sure it's an image? %s", - image_type, + image_type, # pylint: disable=used-before-assignment image_reference, ex, ) diff --git a/homeassistant/components/color_extractor/manifest.json b/homeassistant/components/color_extractor/manifest.json index 7ffdad3660b..4ffea0f9bb0 100644 --- a/homeassistant/components/color_extractor/manifest.json +++ b/homeassistant/components/color_extractor/manifest.json @@ -1,9 +1,8 @@ { - "domain": "color_extractor", - "name": "ColorExtractor", - "config_flow": false, - "documentation": "https://www.home-assistant.io/integrations/color_extractor", - "requirements": ["colorthief==0.2.1"], - "codeowners": ["@GenericStudent"] - } - \ No newline at end of file + "domain": "color_extractor", + "name": "ColorExtractor", + "config_flow": false, + "documentation": "https://www.home-assistant.io/integrations/color_extractor", + "requirements": ["colorthief==0.2.1"], + "codeowners": ["@GenericStudent"] +} diff --git a/homeassistant/components/comfoconnect/fan.py b/homeassistant/components/comfoconnect/fan.py index 278de0557b9..b2845068dbe 100644 --- a/homeassistant/components/comfoconnect/fan.py +++ b/homeassistant/components/comfoconnect/fan.py @@ -120,7 +120,6 @@ class ComfoConnectFan(FanEntity): def turn_on( self, - speed: str | None = None, percentage: int | None = None, preset_mode: str | None = None, **kwargs, diff --git a/homeassistant/components/config/__init__.py b/homeassistant/components/config/__init__.py index 772f7376acc..329134e5486 100644 --- a/homeassistant/components/config/__init__.py +++ b/homeassistant/components/config/__init__.py @@ -9,7 +9,7 @@ import voluptuous as vol from homeassistant.components import frontend from homeassistant.components.http import HomeAssistantView from homeassistant.const import CONF_ID, EVENT_COMPONENT_LOADED -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.typing import ConfigType from homeassistant.setup import ATTR_COMPONENT @@ -29,7 +29,6 @@ SECTIONS = ( "script", "scene", ) -ON_DEMAND = ("zwave",) ACTION_CREATE_UPDATE = "create_update" ACTION_DELETE = "delete" @@ -53,21 +52,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: key = f"{DOMAIN}.{panel_name}" hass.bus.async_fire(EVENT_COMPONENT_LOADED, {ATTR_COMPONENT: key}) - @callback - def component_loaded(event): - """Respond to components being loaded.""" - panel_name = event.data.get(ATTR_COMPONENT) - if panel_name in ON_DEMAND: - hass.async_create_task(setup_panel(panel_name)) - - hass.bus.async_listen(EVENT_COMPONENT_LOADED, component_loaded) - tasks = [asyncio.create_task(setup_panel(panel_name)) for panel_name in SECTIONS] - for panel_name in ON_DEMAND: - if panel_name in hass.config.components: - tasks.append(asyncio.create_task(setup_panel(panel_name))) - if tasks: await asyncio.wait(tasks) diff --git a/homeassistant/components/config/config_entries.py b/homeassistant/components/config/config_entries.py index 07bdc794128..64151c7d90d 100644 --- a/homeassistant/components/config/config_entries.py +++ b/homeassistant/components/config/config_entries.py @@ -1,13 +1,14 @@ """Http views to control the config manager.""" from __future__ import annotations +import asyncio from http import HTTPStatus from aiohttp import web import aiohttp.web_exceptions import voluptuous as vol -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries, data_entry_flow, loader from homeassistant.auth.permissions.const import CAT_CONFIG_ENTRIES, POLICY_EDIT from homeassistant.components import websocket_api from homeassistant.components.http import HomeAssistantView @@ -17,7 +18,7 @@ from homeassistant.helpers.data_entry_flow import ( FlowManagerIndexView, FlowManagerResourceView, ) -from homeassistant.loader import async_get_config_flows +from homeassistant.loader import Integration, async_get_config_flows async def async_setup(hass): @@ -48,11 +49,50 @@ class ConfigManagerEntryIndexView(HomeAssistantView): async def get(self, request): """List available config entries.""" - hass = request.app["hass"] + hass: HomeAssistant = request.app["hass"] - return self.json( - [entry_json(entry) for entry in hass.config_entries.async_entries()] - ) + kwargs = {} + if "domain" in request.query: + kwargs["domain"] = request.query["domain"] + + entries = hass.config_entries.async_entries(**kwargs) + + if "type" not in request.query: + return self.json([entry_json(entry) for entry in entries]) + + integrations = {} + type_filter = request.query["type"] + + async def load_integration( + hass: HomeAssistant, domain: str + ) -> Integration | None: + """Load integration.""" + try: + return await loader.async_get_integration(hass, domain) + except loader.IntegrationNotFound: + return None + + # Fetch all the integrations so we can check their type + for integration in await asyncio.gather( + *( + load_integration(hass, domain) + for domain in {entry.domain for entry in entries} + ) + ): + if integration: + integrations[integration.domain] = integration + + entries = [ + entry + for entry in entries + if (type_filter != "helper" and entry.domain not in integrations) + or ( + entry.domain in integrations + and integrations[entry.domain].integration_type == type_filter + ) + ] + + return self.json([entry_json(entry) for entry in entries]) class ConfigManagerEntryResourceView(HomeAssistantView): @@ -179,7 +219,10 @@ class ConfigManagerAvailableFlowView(HomeAssistantView): async def get(self, request): """List available flow handlers.""" hass = request.app["hass"] - return self.json(await async_get_config_flows(hass)) + kwargs = {} + if "type" in request.query: + kwargs["type_filter"] = request.query["type"] + return self.json(await async_get_config_flows(hass, **kwargs)) class OptionManagerFlowIndexView(FlowManagerIndexView): diff --git a/homeassistant/components/config/entity_registry.py b/homeassistant/components/config/entity_registry.py index f5ffc574b86..2bb585e12c6 100644 --- a/homeassistant/components/config/entity_registry.py +++ b/homeassistant/components/config/entity_registry.py @@ -78,6 +78,16 @@ def websocket_get_entity(hass, connection, msg): er.RegistryEntryDisabler.USER.value, ), ), + # We only allow setting hidden_by user via API. + vol.Optional("hidden_by"): vol.Any( + None, + vol.All( + vol.Coerce(er.RegistryEntryHider), + er.RegistryEntryHider.USER.value, + ), + ), + vol.Inclusive("options_domain", "entity_option"): str, + vol.Inclusive("options", "entity_option"): vol.Any(None, dict), } ) @callback @@ -88,7 +98,8 @@ def websocket_update_entity(hass, connection, msg): """ registry = er.async_get(hass) - if msg["entity_id"] not in registry.entities: + entity_id = msg["entity_id"] + if not (entity_entry := registry.async_get(entity_id)): connection.send_message( websocket_api.error_message(msg["id"], ERR_NOT_FOUND, "Entity not found") ) @@ -96,11 +107,11 @@ def websocket_update_entity(hass, connection, msg): changes = {} - for key in ("area_id", "device_class", "disabled_by", "icon", "name"): + for key in ("area_id", "device_class", "disabled_by", "hidden_by", "icon", "name"): if key in msg: changes[key] = msg[key] - if "new_entity_id" in msg and msg["new_entity_id"] != msg["entity_id"]: + if "new_entity_id" in msg and msg["new_entity_id"] != entity_id: changes["new_entity_id"] = msg["new_entity_id"] if hass.states.get(msg["new_entity_id"]) is not None: connection.send_message( @@ -113,10 +124,10 @@ def websocket_update_entity(hass, connection, msg): return if "disabled_by" in msg and msg["disabled_by"] is None: - entity = registry.entities[msg["entity_id"]] - if entity.device_id: + # Don't allow enabling an entity of a disabled device + if entity_entry.device_id: device_registry = dr.async_get(hass) - device = device_registry.async_get(entity.device_id) + device = device_registry.async_get(entity_entry.device_id) if device.disabled: connection.send_message( websocket_api.error_message( @@ -127,15 +138,31 @@ def websocket_update_entity(hass, connection, msg): try: if changes: - entry = registry.async_update_entity(msg["entity_id"], **changes) + entity_entry = registry.async_update_entity(entity_id, **changes) except ValueError as err: connection.send_message( websocket_api.error_message(msg["id"], "invalid_info", str(err)) ) return - result = {"entity_entry": _entry_ext_dict(entry)} + + if "new_entity_id" in msg: + entity_id = msg["new_entity_id"] + + try: + if "options_domain" in msg: + entity_entry = registry.async_update_entity_options( + entity_id, msg["options_domain"], msg["options"] + ) + except ValueError as err: + connection.send_message( + websocket_api.error_message(msg["id"], "invalid_info", str(err)) + ) + return + + result = {"entity_entry": _entry_ext_dict(entity_entry)} if "disabled_by" in changes and changes["disabled_by"] is None: - config_entry = hass.config_entries.async_get_entry(entry.config_entry_id) + # Enabling an entity requires a config entry reload, or HA restart + config_entry = hass.config_entries.async_get_entry(entity_entry.config_entry_id) if config_entry and not config_entry.supports_unload: result["require_restart"] = True else: @@ -178,6 +205,7 @@ def _entry_dict(entry): "disabled_by": entry.disabled_by, "entity_category": entry.entity_category, "entity_id": entry.entity_id, + "hidden_by": entry.hidden_by, "icon": entry.icon, "name": entry.name, "platform": entry.platform, @@ -190,6 +218,7 @@ def _entry_ext_dict(entry): data = _entry_dict(entry) data["capabilities"] = entry.capabilities data["device_class"] = entry.device_class + data["options"] = entry.options data["original_device_class"] = entry.original_device_class data["original_icon"] = entry.original_icon data["original_name"] = entry.original_name diff --git a/homeassistant/components/config/zwave.py b/homeassistant/components/config/zwave.py deleted file mode 100644 index 63b7bdf9868..00000000000 --- a/homeassistant/components/config/zwave.py +++ /dev/null @@ -1,259 +0,0 @@ -"""Provide configuration end points for Z-Wave.""" -from collections import deque -from http import HTTPStatus -import logging - -from aiohttp.web import Response - -from homeassistant.components.http import HomeAssistantView -from homeassistant.components.zwave import DEVICE_CONFIG_SCHEMA_ENTRY, const -import homeassistant.core as ha -import homeassistant.helpers.config_validation as cv - -from . import EditKeyBasedConfigView - -_LOGGER = logging.getLogger(__name__) -CONFIG_PATH = "zwave_device_config.yaml" -OZW_LOG_FILENAME = "OZW_Log.txt" - - -async def async_setup(hass): - """Set up the Z-Wave config API.""" - hass.http.register_view( - EditKeyBasedConfigView( - "zwave", - "device_config", - CONFIG_PATH, - cv.entity_id, - DEVICE_CONFIG_SCHEMA_ENTRY, - ) - ) - hass.http.register_view(ZWaveNodeValueView) - hass.http.register_view(ZWaveNodeGroupView) - hass.http.register_view(ZWaveNodeConfigView) - hass.http.register_view(ZWaveUserCodeView) - hass.http.register_view(ZWaveLogView) - hass.http.register_view(ZWaveConfigWriteView) - hass.http.register_view(ZWaveProtectionView) - - return True - - -class ZWaveLogView(HomeAssistantView): - """View to read the ZWave log file.""" - - url = "/api/zwave/ozwlog" - name = "api:zwave:ozwlog" - - # pylint: disable=no-self-use - async def get(self, request): - """Retrieve the lines from ZWave log.""" - try: - lines = int(request.query.get("lines", 0)) - except ValueError: - return Response(text="Invalid datetime", status=HTTPStatus.BAD_REQUEST) - - hass = request.app["hass"] - response = await hass.async_add_executor_job(self._get_log, hass, lines) - - return Response(text="\n".join(response)) - - def _get_log(self, hass, lines): - """Retrieve the logfile content.""" - logfilepath = hass.config.path(OZW_LOG_FILENAME) - with open(logfilepath, encoding="utf8") as logfile: - data = (line.rstrip() for line in logfile) - if lines == 0: - loglines = list(data) - else: - loglines = deque(data, lines) - return loglines - - -class ZWaveConfigWriteView(HomeAssistantView): - """View to save the ZWave configuration to zwcfg_xxxxx.xml.""" - - url = "/api/zwave/saveconfig" - name = "api:zwave:saveconfig" - - @ha.callback - def post(self, request): - """Save cache configuration to zwcfg_xxxxx.xml.""" - hass = request.app["hass"] - if (network := hass.data.get(const.DATA_NETWORK)) is None: - return self.json_message( - "No Z-Wave network data found", HTTPStatus.NOT_FOUND - ) - _LOGGER.info("Z-Wave configuration written to file") - network.write_config() - return self.json_message("Z-Wave configuration saved to file") - - -class ZWaveNodeValueView(HomeAssistantView): - """View to return the node values.""" - - url = r"/api/zwave/values/{node_id:\d+}" - name = "api:zwave:values" - - @ha.callback - def get(self, request, node_id): - """Retrieve groups of node.""" - nodeid = int(node_id) - hass = request.app["hass"] - values_list = hass.data[const.DATA_ENTITY_VALUES] - - values_data = {} - # Return a list of values for this node that are used as a - # primary value for an entity - for entity_values in values_list: - if entity_values.primary.node.node_id != nodeid: - continue - - values_data[entity_values.primary.value_id] = { - "label": entity_values.primary.label, - "index": entity_values.primary.index, - "instance": entity_values.primary.instance, - "poll_intensity": entity_values.primary.poll_intensity, - } - return self.json(values_data) - - -class ZWaveNodeGroupView(HomeAssistantView): - """View to return the nodes group configuration.""" - - url = r"/api/zwave/groups/{node_id:\d+}" - name = "api:zwave:groups" - - @ha.callback - def get(self, request, node_id): - """Retrieve groups of node.""" - nodeid = int(node_id) - hass = request.app["hass"] - network = hass.data.get(const.DATA_NETWORK) - if (node := network.nodes.get(nodeid)) is None: - return self.json_message("Node not found", HTTPStatus.NOT_FOUND) - groupdata = node.groups - groups = {} - for key, value in groupdata.items(): - groups[key] = { - "associations": value.associations, - "association_instances": value.associations_instances, - "label": value.label, - "max_associations": value.max_associations, - } - return self.json(groups) - - -class ZWaveNodeConfigView(HomeAssistantView): - """View to return the nodes configuration options.""" - - url = r"/api/zwave/config/{node_id:\d+}" - name = "api:zwave:config" - - @ha.callback - def get(self, request, node_id): - """Retrieve configurations of node.""" - nodeid = int(node_id) - hass = request.app["hass"] - network = hass.data.get(const.DATA_NETWORK) - if (node := network.nodes.get(nodeid)) is None: - return self.json_message("Node not found", HTTPStatus.NOT_FOUND) - config = {} - for value in node.get_values( - class_id=const.COMMAND_CLASS_CONFIGURATION - ).values(): - config[value.index] = { - "label": value.label, - "type": value.type, - "help": value.help, - "data_items": value.data_items, - "data": value.data, - "max": value.max, - "min": value.min, - } - return self.json(config) - - -class ZWaveUserCodeView(HomeAssistantView): - """View to return the nodes usercode configuration.""" - - url = r"/api/zwave/usercodes/{node_id:\d+}" - name = "api:zwave:usercodes" - - @ha.callback - def get(self, request, node_id): - """Retrieve usercodes of node.""" - nodeid = int(node_id) - hass = request.app["hass"] - network = hass.data.get(const.DATA_NETWORK) - if (node := network.nodes.get(nodeid)) is None: - return self.json_message("Node not found", HTTPStatus.NOT_FOUND) - usercodes = {} - if not node.has_command_class(const.COMMAND_CLASS_USER_CODE): - return self.json(usercodes) - for value in node.get_values(class_id=const.COMMAND_CLASS_USER_CODE).values(): - if value.genre != const.GENRE_USER: - continue - usercodes[value.index] = { - "code": value.data, - "label": value.label, - "length": len(value.data), - } - return self.json(usercodes) - - -class ZWaveProtectionView(HomeAssistantView): - """View for the protection commandclass of a node.""" - - url = r"/api/zwave/protection/{node_id:\d+}" - name = "api:zwave:protection" - - async def get(self, request, node_id): - """Retrieve the protection commandclass options of node.""" - nodeid = int(node_id) - hass = request.app["hass"] - network = hass.data.get(const.DATA_NETWORK) - - def _fetch_protection(): - """Get protection data.""" - if (node := network.nodes.get(nodeid)) is None: - return self.json_message("Node not found", HTTPStatus.NOT_FOUND) - protection_options = {} - if not node.has_command_class(const.COMMAND_CLASS_PROTECTION): - return self.json(protection_options) - protections = node.get_protections() - protection_options = { - "value_id": f"{list(protections)[0]:d}", - "selected": node.get_protection_item(list(protections)[0]), - "options": node.get_protection_items(list(protections)[0]), - } - return self.json(protection_options) - - return await hass.async_add_executor_job(_fetch_protection) - - async def post(self, request, node_id): - """Change the selected option in protection commandclass.""" - nodeid = int(node_id) - hass = request.app["hass"] - network = hass.data.get(const.DATA_NETWORK) - protection_data = await request.json() - - def _set_protection(): - """Set protection data.""" - node = network.nodes.get(nodeid) - selection = protection_data["selection"] - value_id = int(protection_data[const.ATTR_VALUE_ID]) - if node is None: - return self.json_message("Node not found", HTTPStatus.NOT_FOUND) - if not node.has_command_class(const.COMMAND_CLASS_PROTECTION): - return self.json_message( - "No protection commandclass on this node", HTTPStatus.NOT_FOUND - ) - state = node.set_protection(value_id, selection) - if not state: - return self.json_message( - "Protection setting did not complete", HTTPStatus.ACCEPTED - ) - return self.json_message("Protection setting successfully set") - - return await hass.async_add_executor_job(_set_protection) diff --git a/homeassistant/components/configurator/translations/el.json b/homeassistant/components/configurator/translations/el.json index a8242694284..4a9830847cf 100644 --- a/homeassistant/components/configurator/translations/el.json +++ b/homeassistant/components/configurator/translations/el.json @@ -1,7 +1,7 @@ { "state": { "_": { - "configure": "\u0394\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03ce\u03c3\u03c4\u03b5", + "configure": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7", "configured": "\u0394\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03ce\u03b8\u03b7\u03ba\u03b5" } }, diff --git a/homeassistant/components/control4/translations/fr.json b/homeassistant/components/control4/translations/fr.json index 7d9bd88a810..8fd08ac8a8a 100644 --- a/homeassistant/components/control4/translations/fr.json +++ b/homeassistant/components/control4/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/cover/translations/el.json b/homeassistant/components/cover/translations/el.json index e02c8e3a97b..d026618c731 100644 --- a/homeassistant/components/cover/translations/el.json +++ b/homeassistant/components/cover/translations/el.json @@ -29,11 +29,11 @@ "state": { "_": { "closed": "\u039a\u03bb\u03b5\u03b9\u03c3\u03c4\u03cc", - "closing": "\u039a\u03bb\u03b5\u03af\u03c3\u03b9\u03bc\u03bf", + "closing": "\u039a\u03bb\u03b5\u03af\u03bd\u03b5\u03b9", "open": "\u0391\u03bd\u03bf\u03b9\u03c7\u03c4\u03cc", - "opening": "\u0386\u03bd\u03bf\u03b9\u03b3\u03bc\u03b1", + "opening": "\u0391\u03bd\u03bf\u03af\u03b3\u03b5\u03b9", "stopped": "\u03a3\u03c4\u03b1\u03bc\u03ac\u03c4\u03b7\u03c3\u03b5" } }, - "title": "\u039a\u03ac\u03bb\u03c5\u03c8\u03b7" + "title": "\u039a\u03ac\u03bb\u03c5\u03bc\u03bc\u03b1" } \ No newline at end of file diff --git a/homeassistant/components/cpuspeed/config_flow.py b/homeassistant/components/cpuspeed/config_flow.py index 4b3c39f148a..3c7d529364c 100644 --- a/homeassistant/components/cpuspeed/config_flow.py +++ b/homeassistant/components/cpuspeed/config_flow.py @@ -6,7 +6,6 @@ from typing import Any from cpuinfo import cpuinfo from homeassistant.config_entries import ConfigFlow -from homeassistant.const import CONF_NAME from homeassistant.data_entry_flow import FlowResult from .const import DOMAIN @@ -36,8 +35,3 @@ class CPUSpeedFlowHandler(ConfigFlow, domain=DOMAIN): title=self._imported_name or "CPU Speed", data={}, ) - - async def async_step_import(self, config: dict[str, Any]) -> FlowResult: - """Handle a flow initialized by importing a config.""" - self._imported_name = config.get(CONF_NAME) - return await self.async_step_user(user_input={}) diff --git a/homeassistant/components/cpuspeed/sensor.py b/homeassistant/components/cpuspeed/sensor.py index 8d21b365ad6..2c9db7e7300 100644 --- a/homeassistant/components/cpuspeed/sensor.py +++ b/homeassistant/components/cpuspeed/sensor.py @@ -2,17 +2,12 @@ from __future__ import annotations from cpuinfo import cpuinfo -import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import CONF_NAME, FREQUENCY_GIGAHERTZ +from homeassistant.components.sensor import SensorEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import FREQUENCY_GIGAHERTZ from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType - -from .const import DOMAIN, LOGGER ATTR_BRAND = "brand" ATTR_HZ = "ghz_advertised" @@ -21,34 +16,6 @@ ATTR_ARCH = "arch" HZ_ACTUAL = "hz_actual" HZ_ADVERTISED = "hz_advertised" -DEFAULT_NAME = "CPU speed" - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - {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 CPU speed sensor.""" - LOGGER.warning( - "Configuration of the CPU Speed platform in YAML is deprecated and will be " - "removed in Home Assistant 2022.4; Your existing configuration " - "has been imported into the UI automatically and can be safely removed " - "from your configuration.yaml file" - ) - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={CONF_NAME: config[CONF_NAME]}, - ) - ) - async def async_setup_entry( hass: HomeAssistant, diff --git a/homeassistant/components/cpuspeed/translations/fr.json b/homeassistant/components/cpuspeed/translations/fr.json index 20b9ebbb35c..5de49927c49 100644 --- a/homeassistant/components/cpuspeed/translations/fr.json +++ b/homeassistant/components/cpuspeed/translations/fr.json @@ -7,7 +7,7 @@ }, "step": { "user": { - "description": "Voulez-vous commencer la configuration ?", + "description": "Voulez-vous commencer la configuration\u00a0?", "title": "Vitesse CPU" } } diff --git a/homeassistant/components/crownstone/manifest.json b/homeassistant/components/crownstone/manifest.json index 786f54ad636..cdc79e7f0b5 100644 --- a/homeassistant/components/crownstone/manifest.json +++ b/homeassistant/components/crownstone/manifest.json @@ -12,5 +12,10 @@ "codeowners": ["@Crownstone", "@RicArch97"], "after_dependencies": ["usb"], "iot_class": "cloud_push", - "loggers": ["crownstone_cloud", "crownstone_core", "crownstone_sse", "crownstone_uart"] + "loggers": [ + "crownstone_cloud", + "crownstone_core", + "crownstone_sse", + "crownstone_uart" + ] } diff --git a/homeassistant/components/crownstone/strings.json b/homeassistant/components/crownstone/strings.json index 25c9fd10293..f2e885d73db 100644 --- a/homeassistant/components/crownstone/strings.json +++ b/homeassistant/components/crownstone/strings.json @@ -72,4 +72,4 @@ } } } -} \ No newline at end of file +} diff --git a/homeassistant/components/crownstone/translations/fr.json b/homeassistant/components/crownstone/translations/fr.json index 783cd25bd49..61a2f0da436 100644 --- a/homeassistant/components/crownstone/translations/fr.json +++ b/homeassistant/components/crownstone/translations/fr.json @@ -7,7 +7,7 @@ }, "error": { "account_not_verified": "Compte non v\u00e9rifi\u00e9. Veuillez activer votre compte via l'e-mail d'activation de Crownstone.", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "step": { @@ -34,7 +34,7 @@ }, "user": { "data": { - "email": "Email", + "email": "Courriel", "password": "Mot de passe" }, "title": "Compte Crownstone" diff --git a/homeassistant/components/daikin/climate.py b/homeassistant/components/daikin/climate.py index 0ac6b0cf353..4b97c8dc21a 100644 --- a/homeassistant/components/daikin/climate.py +++ b/homeassistant/components/daikin/climate.py @@ -163,7 +163,9 @@ class DaikinClimate(ClimateEntity): # temperature elif attr == ATTR_TEMPERATURE: try: - values[HA_ATTR_TO_DAIKIN[ATTR_TARGET_TEMPERATURE]] = str(int(value)) + values[HA_ATTR_TO_DAIKIN[ATTR_TARGET_TEMPERATURE]] = str( + round(float(value), 1) + ) except ValueError: _LOGGER.error("Invalid temperature %s", value) diff --git a/homeassistant/components/daikin/translations/fr.json b/homeassistant/components/daikin/translations/fr.json index 3b2dcd8ce27..d021f758834 100644 --- a/homeassistant/components/daikin/translations/fr.json +++ b/homeassistant/components/daikin/translations/fr.json @@ -5,9 +5,9 @@ "cannot_connect": "\u00c9chec de connexion" }, "error": { - "api_password": "Authentification invalide, utilisez la cl\u00e9 API ou le mot de passe.", + "api_password": "Authentification non valide, utilisez soit la cl\u00e9 d'API soit le mot de passe.", "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "step": { @@ -17,7 +17,7 @@ "host": "H\u00f4te", "password": "Mot de passe" }, - "description": "Saisissez l'adresse IP de votre Daikin AC. \n\n Notez que Cl\u00e9 d'API et Mot de passe sont utilis\u00e9s respectivement par les p\u00e9riph\u00e9riques BRP072Cxx et SKYFi.", + "description": "Saisissez l'Adresse IP de votre Daikin AC. \n\nNotez que Cl\u00e9 d'API et Mot de passe sont utilis\u00e9s respectivement par les appareils BRP072Cxx et SKYFi.", "title": "Configurer Daikin AC" } } diff --git a/homeassistant/components/deconz/binary_sensor.py b/homeassistant/components/deconz/binary_sensor.py index fd674dc1cba..1a37236fae4 100644 --- a/homeassistant/components/deconz/binary_sensor.py +++ b/homeassistant/components/deconz/binary_sensor.py @@ -70,7 +70,7 @@ ENTITY_DESCRIPTIONS = { Alarm: [ DeconzBinarySensorDescription( key="alarm", - value_fn=lambda device: device.alarm, + value_fn=lambda device: device.alarm, # type: ignore[no-any-return] suffix="", update_key="alarm", device_class=BinarySensorDeviceClass.SAFETY, @@ -79,7 +79,7 @@ ENTITY_DESCRIPTIONS = { CarbonMonoxide: [ DeconzBinarySensorDescription( key="carbon_monoxide", - value_fn=lambda device: device.carbon_monoxide, + value_fn=lambda device: device.carbon_monoxide, # type: ignore[no-any-return] suffix="", update_key="carbonmonoxide", device_class=BinarySensorDeviceClass.CO, @@ -88,14 +88,14 @@ ENTITY_DESCRIPTIONS = { Fire: [ DeconzBinarySensorDescription( key="fire", - value_fn=lambda device: device.fire, + value_fn=lambda device: device.fire, # type: ignore[no-any-return] suffix="", update_key="fire", device_class=BinarySensorDeviceClass.SMOKE, ), DeconzBinarySensorDescription( key="in_test_mode", - value_fn=lambda device: device.in_test_mode, + value_fn=lambda device: device.in_test_mode, # type: ignore[no-any-return] suffix="Test Mode", update_key="test", device_class=BinarySensorDeviceClass.SMOKE, @@ -105,7 +105,7 @@ ENTITY_DESCRIPTIONS = { GenericFlag: [ DeconzBinarySensorDescription( key="flag", - value_fn=lambda device: device.flag, + value_fn=lambda device: device.flag, # type: ignore[no-any-return] suffix="", update_key="flag", ) @@ -113,7 +113,7 @@ ENTITY_DESCRIPTIONS = { OpenClose: [ DeconzBinarySensorDescription( key="open", - value_fn=lambda device: device.open, + value_fn=lambda device: device.open, # type: ignore[no-any-return] suffix="", update_key="open", device_class=BinarySensorDeviceClass.OPENING, @@ -122,7 +122,7 @@ ENTITY_DESCRIPTIONS = { Presence: [ DeconzBinarySensorDescription( key="presence", - value_fn=lambda device: device.presence, + value_fn=lambda device: device.presence, # type: ignore[no-any-return] suffix="", update_key="presence", device_class=BinarySensorDeviceClass.MOTION, @@ -131,7 +131,7 @@ ENTITY_DESCRIPTIONS = { Vibration: [ DeconzBinarySensorDescription( key="vibration", - value_fn=lambda device: device.vibration, + value_fn=lambda device: device.vibration, # type: ignore[no-any-return] suffix="", update_key="vibration", device_class=BinarySensorDeviceClass.VIBRATION, @@ -140,7 +140,7 @@ ENTITY_DESCRIPTIONS = { Water: [ DeconzBinarySensorDescription( key="water", - value_fn=lambda device: device.water, + value_fn=lambda device: device.water, # type: ignore[no-any-return] suffix="", update_key="water", device_class=BinarySensorDeviceClass.MOISTURE, @@ -151,7 +151,7 @@ ENTITY_DESCRIPTIONS = { BINARY_SENSOR_DESCRIPTIONS = [ DeconzBinarySensorDescription( key="tampered", - value_fn=lambda device: device.tampered, + value_fn=lambda device: device.tampered, # type: ignore[no-any-return] suffix="Tampered", update_key="tampered", device_class=BinarySensorDeviceClass.TAMPER, @@ -159,7 +159,7 @@ BINARY_SENSOR_DESCRIPTIONS = [ ), DeconzBinarySensorDescription( key="low_battery", - value_fn=lambda device: device.low_battery, + value_fn=lambda device: device.low_battery, # type: ignore[no-any-return] suffix="Low Battery", update_key="lowbattery", device_class=BinarySensorDeviceClass.BATTERY, @@ -266,11 +266,11 @@ class DeconzBinarySensor(DeconzDevice, BinarySensorEntity): @property def extra_state_attributes(self) -> dict[str, bool | float | int | list | None]: """Return the state attributes of the sensor.""" - if self.entity_description.key not in PROVIDES_EXTRA_ATTRIBUTES: - return - attr: dict[str, bool | float | int | list | None] = {} + if self.entity_description.key not in PROVIDES_EXTRA_ATTRIBUTES: + return attr + if self._device.on is not None: attr[ATTR_ON] = self._device.on diff --git a/homeassistant/components/deconz/deconz_device.py b/homeassistant/components/deconz/deconz_device.py index 45f57729a6f..f429faf54ad 100644 --- a/homeassistant/components/deconz/deconz_device.py +++ b/homeassistant/components/deconz/deconz_device.py @@ -1,7 +1,10 @@ """Base class for deCONZ devices.""" + from __future__ import annotations -from pydeconz.group import Scene as PydeconzScene +from pydeconz.group import Group as DeconzGroup, Scene as PydeconzScene +from pydeconz.light import DeconzLight +from pydeconz.sensor import DeconzSensor from homeassistant.core import callback from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE @@ -9,25 +12,30 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import DeviceInfo, Entity from .const import DOMAIN as DECONZ_DOMAIN +from .gateway import DeconzGateway class DeconzBase: """Common base for deconz entities and events.""" - def __init__(self, device, gateway): + def __init__( + self, + device: DeconzGroup | DeconzLight | DeconzSensor, + gateway: DeconzGateway, + ) -> None: """Set up device and add update callback to get data from websocket.""" self._device = device self.gateway = gateway @property - def unique_id(self): + def unique_id(self) -> str: """Return a unique identifier for this device.""" return self._device.unique_id @property - def serial(self): + def serial(self) -> str | None: """Return a serial number for this device.""" - if self._device.unique_id is None or self._device.unique_id.count(":") != 7: + if not self._device.unique_id or self._device.unique_id.count(":") != 7: return None return self._device.unique_id.split("-", 1)[0] @@ -56,14 +64,18 @@ class DeconzDevice(DeconzBase, Entity): TYPE = "" - def __init__(self, device, gateway): + def __init__( + self, + device: DeconzGroup | DeconzLight | DeconzSensor, + gateway: DeconzGateway, + ) -> None: """Set up device and add update callback to get data from websocket.""" super().__init__(device, gateway) self.gateway.entities[self.TYPE].add(self.unique_id) self._attr_name = self._device.name - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Subscribe to device events.""" self._device.register_callback(self.async_update_callback) self.gateway.deconz_ids[self.entity_id] = self._device.deconz_id @@ -82,12 +94,12 @@ class DeconzDevice(DeconzBase, Entity): self.gateway.entities[self.TYPE].remove(self.unique_id) @callback - def async_update_connection_state(self): + def async_update_connection_state(self) -> None: """Update the device's available state.""" self.async_write_ha_state() @callback - def async_update_callback(self): + def async_update_callback(self) -> None: """Update the device's state.""" if self.gateway.ignore_state_updates: return @@ -95,7 +107,7 @@ class DeconzDevice(DeconzBase, Entity): self.async_write_ha_state() @property - def available(self): + def available(self) -> bool: """Return True if device is available.""" return self.gateway.available and self._device.reachable @@ -105,7 +117,7 @@ class DeconzSceneMixin(DeconzDevice): _device: PydeconzScene - def __init__(self, device, gateway) -> None: + def __init__(self, device: PydeconzScene, gateway: DeconzGateway) -> None: """Set up a scene.""" super().__init__(device, gateway) diff --git a/homeassistant/components/deconz/deconz_event.py b/homeassistant/components/deconz/deconz_event.py index 300aef3f82a..c0b4f763c37 100644 --- a/homeassistant/components/deconz/deconz_event.py +++ b/homeassistant/components/deconz/deconz_event.py @@ -1,5 +1,7 @@ """Representation of a deCONZ remote or keypad.""" +from __future__ import annotations + from pydeconz.sensor import ( ANCILLARY_CONTROL_EMERGENCY, ANCILLARY_CONTROL_FIRE, @@ -23,6 +25,7 @@ from homeassistant.util import slugify from .const import CONF_ANGLE, CONF_GESTURE, LOGGER from .deconz_device import DeconzBase +from .gateway import DeconzGateway CONF_DECONZ_EVENT = "deconz_event" CONF_DECONZ_ALARM_EVENT = "deconz_alarm_event" @@ -35,11 +38,13 @@ SUPPORTED_DECONZ_ALARM_EVENTS = { } -async def async_setup_events(gateway) -> None: +async def async_setup_events(gateway: DeconzGateway) -> None: """Set up the deCONZ events.""" @callback - def async_add_sensor(sensors=gateway.api.sensors.values()): + def async_add_sensor( + sensors: AncillaryControl | Switch = gateway.api.sensors.values(), + ) -> None: """Create DeconzEvent.""" new_events = [] known_events = {event.unique_id for event in gateway.events} @@ -74,7 +79,7 @@ async def async_setup_events(gateway) -> None: @callback -def async_unload_events(gateway) -> None: +def async_unload_events(gateway: DeconzGateway) -> None: """Unload all deCONZ events.""" for event in gateway.events: event.async_will_remove_from_hass() @@ -89,18 +94,22 @@ class DeconzEvent(DeconzBase): instead of a sensor entity in hass. """ - def __init__(self, device, gateway): + def __init__( + self, + device: AncillaryControl | Switch, + gateway: DeconzGateway, + ) -> None: """Register callback that will be used for signals.""" super().__init__(device, gateway) self._device.register_callback(self.async_update_callback) - self.device_id = None + self.device_id: str | None = None self.event_id = slugify(self._device.name) LOGGER.debug("deCONZ event created: %s", self.event_id) @property - def device(self): + def device(self) -> AncillaryControl | Switch: """Return Event device.""" return self._device @@ -110,7 +119,7 @@ class DeconzEvent(DeconzBase): self._device.remove_callback(self.async_update_callback) @callback - def async_update_callback(self): + def async_update_callback(self) -> None: """Fire the event if reason is that state is updated.""" if ( self.gateway.ignore_state_updates @@ -154,8 +163,10 @@ class DeconzEvent(DeconzBase): class DeconzAlarmEvent(DeconzEvent): """Alarm control panel companion event when user interacts with a keypad.""" + _device: AncillaryControl + @callback - def async_update_callback(self): + def async_update_callback(self) -> None: """Fire the event if reason is new action is updated.""" if ( self.gateway.ignore_state_updates diff --git a/homeassistant/components/deconz/device_trigger.py b/homeassistant/components/deconz/device_trigger.py index ae539ee5d48..c76aaf481bf 100644 --- a/homeassistant/components/deconz/device_trigger.py +++ b/homeassistant/components/deconz/device_trigger.py @@ -2,8 +2,14 @@ from __future__ import annotations +from typing import Any + import voluptuous as vol +from homeassistant.components.automation import ( + AutomationActionType, + AutomationTriggerInfo, +) from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.components.device_automation.exceptions import ( InvalidDeviceAutomationConfig, @@ -17,11 +23,13 @@ from homeassistant.const import ( CONF_TYPE, CONF_UNIQUE_ID, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import CALLBACK_TYPE, HomeAssistant from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.typing import ConfigType from . import DOMAIN from .deconz_event import CONF_DECONZ_EVENT, CONF_GESTURE, DeconzAlarmEvent, DeconzEvent +from .gateway import DeconzGateway CONF_SUBTYPE = "subtype" @@ -622,7 +630,8 @@ def _get_deconz_event_from_device( device: dr.DeviceEntry, ) -> DeconzAlarmEvent | DeconzEvent: """Resolve deconz event from device.""" - for gateway in hass.data.get(DOMAIN, {}).values(): + gateways: dict[str, DeconzGateway] = hass.data.get(DOMAIN, {}) + for gateway in gateways.values(): for deconz_event in gateway.events: if device.id == deconz_event.device_id: return deconz_event @@ -632,7 +641,10 @@ def _get_deconz_event_from_device( ) -async def async_validate_trigger_config(hass, config): +async def async_validate_trigger_config( + hass: HomeAssistant, + config: dict[str, Any], +) -> vol.Schema: """Validate config.""" config = TRIGGER_SCHEMA(config) @@ -656,32 +668,42 @@ async def async_validate_trigger_config(hass, config): return config -async def async_attach_trigger(hass, config, action, automation_info): +async def async_attach_trigger( + hass: HomeAssistant, + config: ConfigType, + action: AutomationActionType, + automation_info: AutomationTriggerInfo, +) -> CALLBACK_TYPE: """Listen for state changes based on configuration.""" + event_data: dict[str, int | str] = {} + device_registry = dr.async_get(hass) - device = device_registry.async_get(config[CONF_DEVICE_ID]) - - trigger = (config[CONF_TYPE], config[CONF_SUBTYPE]) - - trigger = REMOTES[device.model][trigger] + device = device_registry.devices[config[CONF_DEVICE_ID]] deconz_event = _get_deconz_event_from_device(hass, device) + if event_id := deconz_event.serial: + event_data[CONF_UNIQUE_ID] = event_id - event_id = deconz_event.serial + if device_model := device.model: + config_trigger = (config[CONF_TYPE], config[CONF_SUBTYPE]) + event_data |= REMOTES[device_model][config_trigger] - event_config = { + raw_event_config = { event_trigger.CONF_PLATFORM: "event", event_trigger.CONF_EVENT_TYPE: CONF_DECONZ_EVENT, - event_trigger.CONF_EVENT_DATA: {CONF_UNIQUE_ID: event_id, **trigger}, + event_trigger.CONF_EVENT_DATA: event_data, } - event_config = event_trigger.TRIGGER_SCHEMA(event_config) + event_config = event_trigger.TRIGGER_SCHEMA(raw_event_config) return await event_trigger.async_attach_trigger( hass, event_config, action, automation_info, platform_type="device" ) -async def async_get_triggers(hass, device_id): +async def async_get_triggers( + hass: HomeAssistant, + device_id: str, +) -> list | None: """List device triggers. Make sure device is a supported remote model. @@ -689,10 +711,10 @@ async def async_get_triggers(hass, device_id): Generate device trigger list. """ device_registry = dr.async_get(hass) - device = device_registry.async_get(device_id) + device = device_registry.devices[device_id] if device.model not in REMOTES: - return + return None triggers = [] for trigger, subtype in REMOTES[device.model].keys(): diff --git a/homeassistant/components/deconz/fan.py b/homeassistant/components/deconz/fan.py index d1ff85f9d65..222bf51470f 100644 --- a/homeassistant/components/deconz/fan.py +++ b/homeassistant/components/deconz/fan.py @@ -13,15 +13,7 @@ from pydeconz.light import ( Fan, ) -from homeassistant.components.fan import ( - DOMAIN, - SPEED_HIGH, - SPEED_LOW, - SPEED_MEDIUM, - SPEED_OFF, - SUPPORT_SET_SPEED, - FanEntity, -) +from homeassistant.components.fan import DOMAIN, SUPPORT_SET_SPEED, FanEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -41,19 +33,6 @@ ORDERED_NAMED_FAN_SPEEDS = [ FAN_SPEED_100_PERCENT, ] -LEGACY_SPEED_TO_DECONZ = { - SPEED_OFF: FAN_SPEED_OFF, - SPEED_LOW: FAN_SPEED_25_PERCENT, - SPEED_MEDIUM: FAN_SPEED_50_PERCENT, - SPEED_HIGH: FAN_SPEED_100_PERCENT, -} -LEGACY_DECONZ_TO_SPEED = { - FAN_SPEED_OFF: SPEED_OFF, - FAN_SPEED_25_PERCENT: SPEED_LOW, - FAN_SPEED_50_PERCENT: SPEED_MEDIUM, - FAN_SPEED_100_PERCENT: SPEED_HIGH, -} - async def async_setup_entry( hass: HomeAssistant, @@ -130,41 +109,6 @@ class DeconzFan(DeconzDevice, FanEntity): """Return the number of speeds the fan supports.""" return len(ORDERED_NAMED_FAN_SPEEDS) - @property - def speed_list(self) -> list: - """Get the list of available speeds. - - Legacy fan support. - """ - return list(LEGACY_SPEED_TO_DECONZ) - - def speed_to_percentage(self, speed: str) -> int: - """Convert speed to percentage. - - Legacy fan support. - """ - if speed == SPEED_OFF: - return 0 - - if speed not in LEGACY_SPEED_TO_DECONZ: - speed = SPEED_MEDIUM - - return ordered_list_item_to_percentage( - ORDERED_NAMED_FAN_SPEEDS, LEGACY_SPEED_TO_DECONZ[speed] - ) - - def percentage_to_speed(self, percentage: int) -> str: - """Convert percentage to speed. - - Legacy fan support. - """ - if percentage == 0: - return SPEED_OFF - return LEGACY_DECONZ_TO_SPEED.get( - percentage_to_ordered_list_item(ORDERED_NAMED_FAN_SPEEDS, percentage), - SPEED_MEDIUM, - ) - @callback def async_update_callback(self) -> None: """Store latest configured speed from the device.""" @@ -174,36 +118,23 @@ class DeconzFan(DeconzDevice, FanEntity): async def async_set_percentage(self, percentage: int) -> None: """Set the speed percentage of the fan.""" + if percentage == 0: + return await self.async_turn_off() await self._device.set_speed( percentage_to_ordered_list_item(ORDERED_NAMED_FAN_SPEEDS, percentage) ) - async def async_set_speed(self, speed: str) -> None: - """Set the speed of the fan. - - Legacy fan support. - """ - if speed not in LEGACY_SPEED_TO_DECONZ: - raise ValueError(f"Unsupported speed {speed}") - - await self._device.set_speed(LEGACY_SPEED_TO_DECONZ[speed]) - async def async_turn_on( self, - speed: str | None = None, percentage: int | None = None, preset_mode: str | None = None, **kwargs: Any, ) -> None: """Turn on fan.""" - new_speed = self._default_on_speed - if percentage is not None: - new_speed = percentage_to_ordered_list_item( - ORDERED_NAMED_FAN_SPEEDS, percentage - ) - - await self._device.set_speed(new_speed) + await self.async_set_percentage(percentage) + return + await self._device.set_speed(self._default_on_speed) async def async_turn_off(self, **kwargs: Any) -> None: """Turn off fan.""" diff --git a/homeassistant/components/deconz/gateway.py b/homeassistant/components/deconz/gateway.py index 3bd0278a27a..821f3f477ab 100644 --- a/homeassistant/components/deconz/gateway.py +++ b/homeassistant/components/deconz/gateway.py @@ -4,7 +4,7 @@ from __future__ import annotations import asyncio from types import MappingProxyType -from typing import Any, cast +from typing import TYPE_CHECKING, Any, cast import async_timeout from pydeconz import DeconzSession, errors, group, light, sensor @@ -37,9 +37,11 @@ from .const import ( LOGGER, PLATFORMS, ) -from .deconz_event import DeconzAlarmEvent, DeconzEvent from .errors import AuthenticationRequired, CannotConnect +if TYPE_CHECKING: + from .deconz_event import DeconzAlarmEvent, DeconzEvent + class DeconzGateway: """Manages a single deCONZ gateway.""" diff --git a/homeassistant/components/deconz/light.py b/homeassistant/components/deconz/light.py index e3cf6442079..d9aeec37fb2 100644 --- a/homeassistant/components/deconz/light.py +++ b/homeassistant/components/deconz/light.py @@ -220,11 +220,11 @@ class DeconzBaseLight(DeconzDevice, LightEntity): elif "IKEA" in self._device.manufacturer: data["transition_time"] = 0 - if (alert := FLASH_TO_DECONZ.get(kwargs.get(ATTR_FLASH))) is not None: + if (alert := FLASH_TO_DECONZ.get(kwargs.get(ATTR_FLASH, ""))) is not None: data["alert"] = alert del data["on"] - if (effect := EFFECT_TO_DECONZ.get(kwargs.get(ATTR_EFFECT))) is not None: + if (effect := EFFECT_TO_DECONZ.get(kwargs.get(ATTR_EFFECT, ""))) is not None: data["effect"] = effect await self._device.set_state(**data) @@ -240,7 +240,7 @@ class DeconzBaseLight(DeconzDevice, LightEntity): data["brightness"] = 0 data["transition_time"] = int(attr_transition * 10) - if (alert := FLASH_TO_DECONZ.get(kwargs.get(ATTR_FLASH))) is not None: + if (alert := FLASH_TO_DECONZ.get(kwargs.get(ATTR_FLASH, ""))) is not None: data["alert"] = alert del data["on"] diff --git a/homeassistant/components/deconz/logbook.py b/homeassistant/components/deconz/logbook.py index 1c41feda7da..3dedeb4bfac 100644 --- a/homeassistant/components/deconz/logbook.py +++ b/homeassistant/components/deconz/logbook.py @@ -4,9 +4,8 @@ from __future__ import annotations from collections.abc import Callable from homeassistant.const import ATTR_DEVICE_ID, CONF_EVENT -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import Event, HomeAssistant, callback import homeassistant.helpers.device_registry as dr -from homeassistant.helpers.event import Event from .const import CONF_GESTURE, DOMAIN as DECONZ_DOMAIN from .deconz_event import CONF_DECONZ_ALARM_EVENT, CONF_DECONZ_EVENT diff --git a/homeassistant/components/deconz/manifest.json b/homeassistant/components/deconz/manifest.json index bbbafffed7a..8ca9b93dbe8 100644 --- a/homeassistant/components/deconz/manifest.json +++ b/homeassistant/components/deconz/manifest.json @@ -3,20 +3,14 @@ "name": "deCONZ", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/deconz", - "requirements": [ - "pydeconz==87" - ], + "requirements": ["pydeconz==87"], "ssdp": [ { "manufacturer": "Royal Philips Electronics" } ], - "codeowners": [ - "@Kane610" - ], + "codeowners": ["@Kane610"], "quality_scale": "platinum", "iot_class": "local_push", - "loggers": [ - "pydeconz" - ] -} \ No newline at end of file + "loggers": ["pydeconz"] +} diff --git a/homeassistant/components/deconz/number.py b/homeassistant/components/deconz/number.py index bf138aaef63..532cb92ebdf 100644 --- a/homeassistant/components/deconz/number.py +++ b/homeassistant/components/deconz/number.py @@ -28,7 +28,7 @@ class DeconzNumberDescriptionMixin: suffix: str update_key: str - value_fn: Callable[[PydeconzSensor], bool | None] + value_fn: Callable[[PydeconzSensor], float | None] @dataclass @@ -40,7 +40,7 @@ ENTITY_DESCRIPTIONS = { Presence: [ DeconzNumberDescription( key="delay", - value_fn=lambda device: device.delay, + value_fn=lambda device: device.delay, # type: ignore[no-any-return] suffix="Delay", update_key=PRESENCE_DELAY, max_value=65535, diff --git a/homeassistant/components/deconz/sensor.py b/homeassistant/components/deconz/sensor.py index b0df644f1bd..95db9625139 100644 --- a/homeassistant/components/deconz/sensor.py +++ b/homeassistant/components/deconz/sensor.py @@ -75,7 +75,7 @@ class DeconzSensorDescriptionMixin: """Required values when describing secondary sensor attributes.""" update_key: str - value_fn: Callable[[PydeconzSensor], float | int | None] + value_fn: Callable[[PydeconzSensor], float | int | str | None] @dataclass @@ -334,14 +334,14 @@ class DeconzSensor(DeconzDevice, SensorEntity): """Return the state of the sensor.""" if self.entity_description.device_class is SensorDeviceClass.TIMESTAMP: return dt_util.parse_datetime( - self.entity_description.value_fn(self._device) + self.entity_description.value_fn(self._device) # type: ignore[arg-type] ) return self.entity_description.value_fn(self._device) @property - def extra_state_attributes(self) -> dict[str, bool | float | int | None]: + def extra_state_attributes(self) -> dict[str, bool | float | int | str | None]: """Return the state attributes of the sensor.""" - attr: dict[str, bool | float | int | None] = {} + attr: dict[str, bool | float | int | str | None] = {} if self.entity_description.key not in PROVIDES_EXTRA_ATTRIBUTES: return attr diff --git a/homeassistant/components/deconz/translations/fr.json b/homeassistant/components/deconz/translations/fr.json index 464cc2e139e..39a32fc2434 100644 --- a/homeassistant/components/deconz/translations/fr.json +++ b/homeassistant/components/deconz/translations/fr.json @@ -42,10 +42,10 @@ "button_2": "Deuxi\u00e8me bouton", "button_3": "Troisi\u00e8me bouton", "button_4": "Quatri\u00e8me bouton", - "button_5": "5\u00e8me bouton", - "button_6": "6\u00e8me bouton", - "button_7": "7\u00e8me bouton", - "button_8": "8\u00e8me bouton", + "button_5": "Cinqui\u00e8me bouton", + "button_6": "Sixi\u00e8me bouton", + "button_7": "Septi\u00e8me bouton", + "button_8": "Huiti\u00e8me bouton", "close": "Ferm\u00e9", "dim_down": "Assombrir", "dim_up": "\u00c9claircir", @@ -70,8 +70,8 @@ "remote_button_quadruple_press": "Quadruple clic sur le bouton \" {subtype} \"", "remote_button_quintuple_press": "Quintuple clic sur le bouton \" {subtype} \"", "remote_button_rotated": "Bouton \"{subtype}\" tourn\u00e9", - "remote_button_rotated_fast": "Bouton pivot\u00e9 rapidement \" {subtype} \"", - "remote_button_rotation_stopped": "La rotation du bouton \" {subtype} \" s'est arr\u00eat\u00e9e", + "remote_button_rotated_fast": "Bouton \u00ab\u00a0{subtype}\u00a0\u00bb tourn\u00e9 rapidement", + "remote_button_rotation_stopped": "La rotation du bouton \u00ab\u00a0{subtype}\u00a0\u00bb a cess\u00e9", "remote_button_short_press": "Bouton \"{subtype}\" appuy\u00e9", "remote_button_short_release": "Bouton \"{subtype}\" rel\u00e2ch\u00e9", "remote_button_triple_press": "Triple clic sur le bouton \" {subtype} \"", diff --git a/homeassistant/components/deconz/translations/pl.json b/homeassistant/components/deconz/translations/pl.json index 72d62aff959..945830a5734 100644 --- a/homeassistant/components/deconz/translations/pl.json +++ b/homeassistant/components/deconz/translations/pl.json @@ -97,7 +97,7 @@ "step": { "deconz_devices": { "data": { - "allow_clip_sensor": "Zezwalaj na czujniki deCONZ CLIP", + "allow_clip_sensor": "Zezwalaj na sensory deCONZ CLIP", "allow_deconz_groups": "Zezwalaj na grupy \u015bwiate\u0142 deCONZ", "allow_new_devices": "Zezwalaj na automatyczne dodawanie nowych urz\u0105dze\u0144" }, diff --git a/homeassistant/components/deconz/translations/zh-Hant.json b/homeassistant/components/deconz/translations/zh-Hant.json index b93cb320993..d6d4dfeba45 100644 --- a/homeassistant/components/deconz/translations/zh-Hant.json +++ b/homeassistant/components/deconz/translations/zh-Hant.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "Bridge \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", - "no_bridges": "\u672a\u641c\u5c0b\u5230 deCONZ Bridfe", + "no_bridges": "\u672a\u767c\u73fe\u5230 deCONZ Bridfe", "no_hardware_available": "deCONZ \u6c92\u6709\u4efb\u4f55\u7121\u7dda\u96fb\u88dd\u7f6e\u9023\u7dda", "not_deconz_bridge": "\u975e deCONZ Bridge \u88dd\u7f6e", "updated_instance": "\u4f7f\u7528\u65b0\u4e3b\u6a5f\u7aef\u4f4d\u5740\u66f4\u65b0 deCONZ \u88dd\u7f6e" @@ -101,7 +101,7 @@ "allow_deconz_groups": "\u5141\u8a31 deCONZ \u71c8\u5149\u7fa4\u7d44", "allow_new_devices": "\u5141\u8a31\u81ea\u52d5\u5316\u65b0\u589e\u88dd\u7f6e" }, - "description": "\u8a2d\u5b9a deCONZ \u53ef\u8996\u88dd\u7f6e\u985e\u578b", + "description": "\u8a2d\u5b9a deCONZ \u53ef\u8996\u88dd\u7f6e\u985e\u5225", "title": "deCONZ \u9078\u9805" } } diff --git a/homeassistant/components/decora_wifi/light.py b/homeassistant/components/decora_wifi/light.py index 84caf0ad29a..03cbe31b336 100644 --- a/homeassistant/components/decora_wifi/light.py +++ b/homeassistant/components/decora_wifi/light.py @@ -110,6 +110,11 @@ class DecoraWifiLight(LightEntity): """Return the display name of this switch.""" return self._switch.name + @property + def unique_id(self): + """Return the ID of this light.""" + return self._switch.serial + @property def brightness(self): """Return the brightness of the dimmer switch.""" diff --git a/homeassistant/components/default_config/__init__.py b/homeassistant/components/default_config/__init__.py index 9e6ab268172..574d97c6d29 100644 --- a/homeassistant/components/default_config/__init__.py +++ b/homeassistant/components/default_config/__init__.py @@ -5,6 +5,7 @@ try: except ImportError: av = None +from homeassistant.components.hassio import is_hassio from homeassistant.core import HomeAssistant from homeassistant.helpers.typing import ConfigType from homeassistant.setup import async_setup_component @@ -14,6 +15,9 @@ DOMAIN = "default_config" async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Initialize default configuration.""" + if not is_hassio(hass): + await async_setup_component(hass, "backup", config) + if av is None: return True diff --git a/homeassistant/components/default_config/manifest.json b/homeassistant/components/default_config/manifest.json index 9a65af96852..1ab827529c6 100644 --- a/homeassistant/components/default_config/manifest.json +++ b/homeassistant/components/default_config/manifest.json @@ -35,6 +35,6 @@ "zeroconf", "zone" ], - "codeowners": [], + "codeowners": ["@home-assistant/core"], "quality_scale": "internal" -} \ No newline at end of file +} diff --git a/homeassistant/components/deluge/__init__.py b/homeassistant/components/deluge/__init__.py index ad40b688fcf..2253eee43d5 100644 --- a/homeassistant/components/deluge/__init__.py +++ b/homeassistant/components/deluge/__init__.py @@ -1 +1,87 @@ -"""The deluge component.""" +"""The Deluge integration.""" +from __future__ import annotations + +import logging +import socket +from ssl import SSLError + +from deluge_client.client import DelugeRPCClient + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + CONF_HOST, + CONF_PASSWORD, + CONF_PORT, + CONF_USERNAME, + Platform, +) +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady +from homeassistant.helpers.device_registry import DeviceEntryType +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import CONF_WEB_PORT, DEFAULT_NAME, DOMAIN +from .coordinator import DelugeDataUpdateCoordinator + +PLATFORMS = [Platform.SENSOR, Platform.SWITCH] + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Deluge from a config entry.""" + host = entry.data[CONF_HOST] + port = entry.data[CONF_PORT] + username = entry.data[CONF_USERNAME] + password = entry.data[CONF_PASSWORD] + api = await hass.async_add_executor_job( + DelugeRPCClient, host, port, username, password + ) + api.web_port = entry.data[CONF_WEB_PORT] + try: + await hass.async_add_executor_job(api.connect) + except ( + ConnectionRefusedError, + socket.timeout, + SSLError, + ) as ex: + raise ConfigEntryNotReady("Connection to Deluge Daemon failed") from ex + except Exception as ex: # pylint:disable=broad-except + if type(ex).__name__ == "BadLoginError": + raise ConfigEntryAuthFailed( + "Credentials for Deluge client are not valid" + ) from ex + _LOGGER.error("Unknown error connecting to Deluge: %s", ex) + + coordinator = DelugeDataUpdateCoordinator(hass, api, entry) + await coordinator.async_config_entry_first_refresh() + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok + + +class DelugeEntity(CoordinatorEntity[DelugeDataUpdateCoordinator]): + """Representation of a Deluge entity.""" + + def __init__(self, coordinator: DelugeDataUpdateCoordinator) -> None: + """Initialize a Deluge entity.""" + super().__init__(coordinator) + self._server_unique_id = coordinator.config_entry.entry_id + self._attr_device_info = DeviceInfo( + configuration_url=f"http://{coordinator.api.host}:{coordinator.api.web_port}", + entry_type=DeviceEntryType.SERVICE, + identifiers={(DOMAIN, coordinator.config_entry.entry_id)}, + manufacturer=DEFAULT_NAME, + name=DEFAULT_NAME, + sw_version=coordinator.api.deluge_version, + ) diff --git a/homeassistant/components/deluge/config_flow.py b/homeassistant/components/deluge/config_flow.py new file mode 100644 index 00000000000..fc0dd5ff300 --- /dev/null +++ b/homeassistant/components/deluge/config_flow.py @@ -0,0 +1,125 @@ +"""Config flow for the Deluge integration.""" +from __future__ import annotations + +import logging +import socket +from ssl import SSLError +from typing import Any + +from deluge_client.client import DelugeRPCClient +import voluptuous as vol + +from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlow +from homeassistant.const import ( + CONF_HOST, + CONF_MONITORED_VARIABLES, + CONF_NAME, + CONF_PASSWORD, + CONF_PORT, + CONF_SOURCE, + CONF_USERNAME, +) +from homeassistant.data_entry_flow import FlowResult +import homeassistant.helpers.config_validation as cv + +from .const import ( + CONF_WEB_PORT, + DEFAULT_NAME, + DEFAULT_RPC_PORT, + DEFAULT_WEB_PORT, + DOMAIN, +) + +_LOGGER = logging.getLogger(__name__) + + +class DelugeFlowHandler(ConfigFlow, domain=DOMAIN): + """Handle a config flow for Deluge.""" + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle a flow initiated by the user.""" + errors = {} + title = None + + if user_input is not None: + if CONF_NAME in user_input: + title = user_input.pop(CONF_NAME) + if (error := await self.validate_input(user_input)) is None: + for entry in self._async_current_entries(): + if ( + user_input[CONF_HOST] == entry.data[CONF_HOST] + and user_input[CONF_PORT] == entry.data[CONF_PORT] + ): + if self.context.get(CONF_SOURCE) == SOURCE_REAUTH: + self.hass.config_entries.async_update_entry( + entry, data=user_input + ) + await self.hass.config_entries.async_reload(entry.entry_id) + return self.async_abort(reason="reauth_successful") + return self.async_abort(reason="already_configured") + return self.async_create_entry( + title=title or DEFAULT_NAME, + data=user_input, + ) + errors["base"] = error + user_input = user_input or {} + schema = vol.Schema( + { + vol.Required(CONF_HOST, default=user_input.get(CONF_HOST)): cv.string, + vol.Required( + CONF_USERNAME, default=user_input.get(CONF_USERNAME) + ): cv.string, + vol.Required(CONF_PASSWORD, default=""): cv.string, + vol.Optional( + CONF_PORT, default=user_input.get(CONF_PORT, DEFAULT_RPC_PORT) + ): int, + vol.Optional( + CONF_WEB_PORT, + default=user_input.get(CONF_WEB_PORT, DEFAULT_WEB_PORT), + ): int, + } + ) + return self.async_show_form(step_id="user", data_schema=schema, errors=errors) + + async def async_step_reauth(self, config: dict[str, Any]) -> FlowResult: + """Handle a reauthorization flow request.""" + return await self.async_step_user() + + async def async_step_import(self, config: dict[str, Any]) -> FlowResult: + """Import a config entry from configuration.yaml.""" + if CONF_MONITORED_VARIABLES in config: + config.pop(CONF_MONITORED_VARIABLES) + config[CONF_WEB_PORT] = DEFAULT_WEB_PORT + + for entry in self._async_current_entries(): + if entry.data[CONF_HOST] == config[CONF_HOST]: + _LOGGER.warning( + "Deluge yaml config has been imported. Please remove it" + ) + return self.async_abort(reason="already_configured") + return await self.async_step_user(config) + + async def validate_input(self, user_input: dict[str, Any]) -> str | None: + """Handle common flow input validation.""" + host = user_input[CONF_HOST] + port = user_input[CONF_PORT] + username = user_input[CONF_USERNAME] + password = user_input[CONF_PASSWORD] + api = DelugeRPCClient( + host=host, port=port, username=username, password=password + ) + try: + await self.hass.async_add_executor_job(api.connect) + except ( + ConnectionRefusedError, + socket.timeout, + SSLError, + ): + return "cannot_connect" + except Exception as ex: # pylint:disable=broad-except + if type(ex).__name__ == "BadLoginError": + return "invalid_auth" # pragma: no cover + return "unknown" + return None diff --git a/homeassistant/components/deluge/const.py b/homeassistant/components/deluge/const.py new file mode 100644 index 00000000000..505c20e860f --- /dev/null +++ b/homeassistant/components/deluge/const.py @@ -0,0 +1,12 @@ +"""Constants for the Deluge integration.""" +import logging + +CONF_WEB_PORT = "web_port" +DEFAULT_NAME = "Deluge" +DEFAULT_RPC_PORT = 58846 +DEFAULT_WEB_PORT = 8112 +DHT_UPLOAD = 1000 +DHT_DOWNLOAD = 1000 +DOMAIN = "deluge" + +LOGGER = logging.getLogger(__package__) diff --git a/homeassistant/components/deluge/coordinator.py b/homeassistant/components/deluge/coordinator.py new file mode 100644 index 00000000000..0ac97e77674 --- /dev/null +++ b/homeassistant/components/deluge/coordinator.py @@ -0,0 +1,68 @@ +"""Data update coordinator for the Deluge integration.""" +from __future__ import annotations + +from datetime import timedelta +import socket +from ssl import SSLError + +from deluge_client.client import DelugeRPCClient, FailedToReconnectException + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import LOGGER + + +class DelugeDataUpdateCoordinator(DataUpdateCoordinator): + """Data update coordinator for the Deluge integration.""" + + config_entry: ConfigEntry + + def __init__( + self, hass: HomeAssistant, api: DelugeRPCClient, entry: ConfigEntry + ) -> None: + """Initialize the coordinator.""" + super().__init__( + hass=hass, + logger=LOGGER, + name=entry.title, + update_interval=timedelta(seconds=30), + ) + self.api = api + self.config_entry = entry + + async def _async_update_data(self) -> dict[Platform, dict[str, int | str]]: + """Get the latest data from Deluge and updates the state.""" + data = {} + try: + data[Platform.SENSOR] = await self.hass.async_add_executor_job( + self.api.call, + "core.get_session_status", + [ + "upload_rate", + "download_rate", + "dht_upload_rate", + "dht_download_rate", + ], + ) + data[Platform.SWITCH] = await self.hass.async_add_executor_job( + self.api.call, "core.get_torrents_status", {}, ["paused"] + ) + except ( + ConnectionRefusedError, + socket.timeout, + SSLError, + FailedToReconnectException, + ) as ex: + raise UpdateFailed(f"Connection to Deluge Daemon Lost: {ex}") from ex + except Exception as ex: + if type(ex).__name__ == "BadLoginError": + raise ConfigEntryAuthFailed( + "Credentials for Deluge client are not valid" + ) from ex + LOGGER.error("Unknown error connecting to Deluge: %s", ex) + raise ex + return data diff --git a/homeassistant/components/deluge/manifest.json b/homeassistant/components/deluge/manifest.json index 5bf4651096c..920e560b70f 100644 --- a/homeassistant/components/deluge/manifest.json +++ b/homeassistant/components/deluge/manifest.json @@ -3,7 +3,8 @@ "name": "Deluge", "documentation": "https://www.home-assistant.io/integrations/deluge", "requirements": ["deluge-client==1.7.1"], - "codeowners": [], + "codeowners": ["@tkdrob"], + "config_flow": true, "iot_class": "local_polling", "loggers": ["deluge_client"] } diff --git a/homeassistant/components/deluge/sensor.py b/homeassistant/components/deluge/sensor.py index 01241b0d120..99e63e6ef17 100644 --- a/homeassistant/components/deluge/sensor.py +++ b/homeassistant/components/deluge/sensor.py @@ -1,16 +1,15 @@ """Support for monitoring the Deluge BitTorrent client API.""" from __future__ import annotations -import logging - -from deluge_client import DelugeRPCClient, FailedToReconnectException import voluptuous as vol from homeassistant.components.sensor import ( PLATFORM_SCHEMA, SensorEntity, SensorEntityDescription, + SensorStateClass, ) +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( CONF_HOST, CONF_MONITORED_VARIABLES, @@ -20,20 +19,17 @@ from homeassistant.const import ( CONF_USERNAME, DATA_RATE_KILOBYTES_PER_SECOND, STATE_IDLE, + Platform, ) from homeassistant.core import HomeAssistant -from homeassistant.exceptions import PlatformNotReady +from homeassistant.helpers import entity_platform import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType -_LOGGER = logging.getLogger(__name__) -_THROTTLED_REFRESH = None +from . import DelugeEntity +from .const import DEFAULT_NAME, DEFAULT_RPC_PORT, DOMAIN +from .coordinator import DelugeDataUpdateCoordinator -DEFAULT_NAME = "Deluge" -DEFAULT_PORT = 58846 -DHT_UPLOAD = 1000 -DHT_DOWNLOAD = 1000 SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="current_status", @@ -43,21 +39,24 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key="download_speed", name="Down Speed", native_unit_of_measurement=DATA_RATE_KILOBYTES_PER_SECOND, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="upload_speed", name="Up Speed", native_unit_of_measurement=DATA_RATE_KILOBYTES_PER_SECOND, + state_class=SensorStateClass.MEASUREMENT, ), ) SENSOR_KEYS: list[str] = [desc.key for desc in SENSOR_TYPES] +# Deprecated in Home Assistant 2022.3 PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_PORT, default=DEFAULT_RPC_PORT): cv.port, vol.Required(CONF_USERNAME): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_MONITORED_VARIABLES, default=[]): vol.All( @@ -67,92 +66,70 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -def setup_platform( +async def async_setup_platform( hass: HomeAssistant, config: ConfigType, - add_entities: AddEntitiesCallback, + async_add_entities: entity_platform.AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: - """Set up the Deluge sensors.""" + """Set up the Deluge sensor component.""" + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=config + ) + ) - name = config[CONF_NAME] - host = config[CONF_HOST] - username = config[CONF_USERNAME] - password = config[CONF_PASSWORD] - port = config[CONF_PORT] - deluge_api = DelugeRPCClient(host, port, username, password) - try: - deluge_api.connect() - except ConnectionRefusedError as err: - _LOGGER.error("Connection to Deluge Daemon failed") - raise PlatformNotReady from err - monitored_variables = config[CONF_MONITORED_VARIABLES] - entities = [ - DelugeSensor(deluge_api, name, description) +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: entity_platform.AddEntitiesCallback, +) -> None: + """Set up the Deluge sensor.""" + async_add_entities( + DelugeSensor(hass.data[DOMAIN][entry.entry_id], description) for description in SENSOR_TYPES - if description.key in monitored_variables - ] - - add_entities(entities) + ) -class DelugeSensor(SensorEntity): +class DelugeSensor(DelugeEntity, SensorEntity): """Representation of a Deluge sensor.""" def __init__( - self, deluge_client, client_name, description: SensorEntityDescription - ): + self, + coordinator: DelugeDataUpdateCoordinator, + description: SensorEntityDescription, + ) -> None: """Initialize the sensor.""" + super().__init__(coordinator) self.entity_description = description - self.client = deluge_client - self.data = None + self._attr_name = f"{coordinator.config_entry.title} {description.name}" + self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{description.key}" - self._attr_available = False - self._attr_name = f"{client_name} {description.name}" + @property + def native_value(self) -> StateType: + """Return the state of the sensor.""" + if self.coordinator.data: + data = self.coordinator.data[Platform.SENSOR] + upload = data[b"upload_rate"] - data[b"dht_upload_rate"] + download = data[b"download_rate"] - data[b"dht_download_rate"] + if self.entity_description.key == "current_status": + if data: + if upload > 0 and download > 0: + return "Up/Down" + if upload > 0 and download == 0: + return "Seeding" + if upload == 0 and download > 0: + return "Downloading" + return STATE_IDLE - def update(self): - """Get the latest data from Deluge and updates the state.""" - - try: - self.data = self.client.call( - "core.get_session_status", - [ - "upload_rate", - "download_rate", - "dht_upload_rate", - "dht_download_rate", - ], - ) - self._attr_available = True - except FailedToReconnectException: - _LOGGER.error("Connection to Deluge Daemon Lost") - self._attr_available = False - return - - upload = self.data[b"upload_rate"] - self.data[b"dht_upload_rate"] - download = self.data[b"download_rate"] - self.data[b"dht_download_rate"] - - sensor_type = self.entity_description.key - if sensor_type == "current_status": - if self.data: - if upload > 0 and download > 0: - self._attr_native_value = "Up/Down" - elif upload > 0 and download == 0: - self._attr_native_value = "Seeding" - elif upload == 0 and download > 0: - self._attr_native_value = "Downloading" - else: - self._attr_native_value = STATE_IDLE - else: - self._attr_native_value = None - - if self.data: - if sensor_type == "download_speed": - kb_spd = float(download) - kb_spd = kb_spd / 1024 - self._attr_native_value = round(kb_spd, 2 if kb_spd < 0.1 else 1) - elif sensor_type == "upload_speed": - kb_spd = float(upload) - kb_spd = kb_spd / 1024 - self._attr_native_value = round(kb_spd, 2 if kb_spd < 0.1 else 1) + if data: + if self.entity_description.key == "download_speed": + kb_spd = float(download) + kb_spd = kb_spd / 1024 + return round(kb_spd, 2 if kb_spd < 0.1 else 1) + if self.entity_description.key == "upload_speed": + kb_spd = float(upload) + kb_spd = kb_spd / 1024 + return round(kb_spd, 2 if kb_spd < 0.1 else 1) + return None diff --git a/homeassistant/components/deluge/strings.json b/homeassistant/components/deluge/strings.json new file mode 100644 index 00000000000..f11e1a2bd3e --- /dev/null +++ b/homeassistant/components/deluge/strings.json @@ -0,0 +1,23 @@ +{ + "config": { + "step": { + "user": { + "description": "To be able to use this integration, you have to enable the following option in deluge settings: Daemon > Allow remote controls", + "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%]", + "web_port": "Web port (for visiting service)" + } + } + }, + "error": { + "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_service%]" + } + } +} diff --git a/homeassistant/components/deluge/switch.py b/homeassistant/components/deluge/switch.py index 16a5f023052..d438d236e5c 100644 --- a/homeassistant/components/deluge/switch.py +++ b/homeassistant/components/deluge/switch.py @@ -1,118 +1,90 @@ """Support for setting the Deluge BitTorrent client in Pause.""" from __future__ import annotations -import logging +from typing import Any -from deluge_client import DelugeRPCClient, FailedToReconnectException import voluptuous as vol from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_PORT, CONF_USERNAME, - STATE_OFF, - STATE_ON, + Platform, ) from homeassistant.core import HomeAssistant -from homeassistant.exceptions import PlatformNotReady +from homeassistant.helpers import entity_platform import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -_LOGGER = logging.getLogger(__name__) - -DEFAULT_NAME = "Deluge Switch" -DEFAULT_PORT = 58846 +from . import DelugeEntity +from .const import DEFAULT_RPC_PORT, DOMAIN +from .coordinator import DelugeDataUpdateCoordinator +# Deprecated in Home Assistant 2022.3 PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_PORT, default=DEFAULT_RPC_PORT): cv.port, vol.Required(CONF_USERNAME): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_NAME, default="Deluge Switch"): cv.string, } ) -def setup_platform( +async def async_setup_platform( hass: HomeAssistant, config: ConfigType, - add_entities: AddEntitiesCallback, + async_add_entities: entity_platform.AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, +) -> None: + """Set up the Deluge sensor component.""" + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=config + ) + ) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: entity_platform.AddEntitiesCallback, ) -> None: """Set up the Deluge switch.""" - - name = config[CONF_NAME] - host = config[CONF_HOST] - username = config[CONF_USERNAME] - password = config[CONF_PASSWORD] - port = config[CONF_PORT] - - deluge_api = DelugeRPCClient(host, port, username, password) - try: - deluge_api.connect() - except ConnectionRefusedError as err: - _LOGGER.error("Connection to Deluge Daemon failed") - raise PlatformNotReady from err - - add_entities([DelugeSwitch(deluge_api, name)]) + async_add_entities([DelugeSwitch(hass.data[DOMAIN][entry.entry_id])]) -class DelugeSwitch(SwitchEntity): +class DelugeSwitch(DelugeEntity, SwitchEntity): """Representation of a Deluge switch.""" - def __init__(self, deluge_client, name): + def __init__(self, coordinator: DelugeDataUpdateCoordinator) -> None: """Initialize the Deluge switch.""" - self._name = name - self.deluge_client = deluge_client - self._state = STATE_OFF - self._available = False + super().__init__(coordinator) + self._attr_name = coordinator.config_entry.title + self._attr_unique_id = f"{coordinator.config_entry.entry_id}_enabled" - @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 == STATE_ON - - @property - def available(self): - """Return true if device is available.""" - return self._available - - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" - torrent_ids = self.deluge_client.call("core.get_session_state") - self.deluge_client.call("core.resume_torrent", torrent_ids) + torrent_ids = self.coordinator.api.call("core.get_session_state") + self.coordinator.api.call("core.resume_torrent", torrent_ids) - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" - torrent_ids = self.deluge_client.call("core.get_session_state") - self.deluge_client.call("core.pause_torrent", torrent_ids) + torrent_ids = self.coordinator.api.call("core.get_session_state") + self.coordinator.api.call("core.pause_torrent", torrent_ids) - def update(self): - """Get the latest data from deluge and updates the state.""" - - try: - torrent_list = self.deluge_client.call( - "core.get_torrents_status", {}, ["paused"] - ) - self._available = True - except FailedToReconnectException: - _LOGGER.error("Connection to Deluge Daemon Lost") - self._available = False - return - for torrent in torrent_list.values(): - item = torrent.popitem() - if not item[1]: - self._state = STATE_ON - return - - self._state = STATE_OFF + @property + def is_on(self) -> bool: + """Return state of the switch.""" + if self.coordinator.data: + data: dict = self.coordinator.data[Platform.SWITCH] + for torrent in data.values(): + item = torrent.popitem() + if not item[1]: + return True + return False diff --git a/homeassistant/components/deluge/translations/en.json b/homeassistant/components/deluge/translations/en.json new file mode 100644 index 00000000000..a3a2f539126 --- /dev/null +++ b/homeassistant/components/deluge/translations/en.json @@ -0,0 +1,23 @@ +{ + "config": { + "step": { + "user": { + "description": "To be able to use this integration, you have to enable the following option in deluge settings: Daemon > Allow remote controls", + "data": { + "host": "Host", + "username": "Username", + "password": "Password", + "port": "Port", + "web_port": "Web port (for visiting service)" + } + } + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication" + }, + "abort": { + "already_configured": "Service is already configured" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/demo/__init__.py b/homeassistant/components/demo/__init__.py index abee8310e17..9c399c67f35 100644 --- a/homeassistant/components/demo/__init__.py +++ b/homeassistant/components/demo/__init__.py @@ -5,6 +5,7 @@ from random import random from homeassistant import config_entries, setup from homeassistant.components import persistent_notification +from homeassistant.components.recorder import get_instance from homeassistant.components.recorder.statistics import ( async_add_external_statistics, get_last_statistics, @@ -40,6 +41,7 @@ COMPONENTS_WITH_CONFIG_ENTRY_DEMO_PLATFORM = [ "sensor", "siren", "switch", + "update", "vacuum", "water_heater", ] @@ -244,7 +246,7 @@ async def _insert_statistics(hass): } statistic_id = f"{DOMAIN}:energy_consumption" sum_ = 0 - last_stats = await hass.async_add_executor_job( + last_stats = await get_instance(hass).async_add_executor_job( get_last_statistics, hass, 1, statistic_id, True ) if "domain:energy_consumption" in last_stats: diff --git a/homeassistant/components/demo/fan.py b/homeassistant/components/demo/fan.py index e70f5efc626..8fcc6a810ed 100644 --- a/homeassistant/components/demo/fan.py +++ b/homeassistant/components/demo/fan.py @@ -196,7 +196,6 @@ class DemoPercentageFan(BaseDemoFan, FanEntity): def turn_on( self, - speed: str = None, percentage: int = None, preset_mode: str = None, **kwargs, @@ -267,7 +266,6 @@ class AsyncDemoPercentageFan(BaseDemoFan, FanEntity): async def async_turn_on( self, - speed: str = None, percentage: int = None, preset_mode: str = None, **kwargs, diff --git a/homeassistant/components/demo/update.py b/homeassistant/components/demo/update.py new file mode 100644 index 00000000000..648ad59bb55 --- /dev/null +++ b/homeassistant/components/demo/update.py @@ -0,0 +1,162 @@ +"""Demo platform that offers fake update entities.""" +from __future__ import annotations + +import asyncio +from typing import Any + +from homeassistant.components.update import UpdateDeviceClass, UpdateEntity +from homeassistant.components.update.const import UpdateEntityFeature +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import DEVICE_DEFAULT_NAME +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType + +from . import DOMAIN + +FAKE_INSTALL_SLEEP_TIME = 0.5 + + +async def async_setup_platform( + hass: HomeAssistant, + config: ConfigType, + async_add_entities: AddEntitiesCallback, + discovery_info: DiscoveryInfoType | None = None, +) -> None: + """Set up demo update entities.""" + async_add_entities( + [ + DemoUpdate( + unique_id="update_no_install", + name="Demo Update No Install", + title="Awesomesoft Inc.", + installed_version="1.0.0", + latest_version="1.0.1", + release_summary="Awesome update, fixing everything!", + release_url="https://www.example.com/release/1.0.1", + support_install=False, + ), + DemoUpdate( + unique_id="update_2_date", + name="Demo No Update", + title="AdGuard Home", + installed_version="1.0.0", + latest_version="1.0.0", + ), + DemoUpdate( + unique_id="update_addon", + name="Demo add-on", + title="AdGuard Home", + installed_version="1.0.0", + latest_version="1.0.1", + release_summary="Awesome update, fixing everything!", + release_url="https://www.example.com/release/1.0.1", + ), + DemoUpdate( + unique_id="update_light_bulb", + name="Demo Living Room Bulb Update", + title="Philips Lamps Firmware", + installed_version="1.93.3", + latest_version="1.94.2", + release_summary="Added support for effects", + release_url="https://www.example.com/release/1.93.3", + device_class=UpdateDeviceClass.FIRMWARE, + ), + DemoUpdate( + unique_id="update_support_progress", + name="Demo Update with Progress", + title="Philips Lamps Firmware", + installed_version="1.93.3", + latest_version="1.94.2", + support_progress=True, + release_summary="Added support for effects", + support_release_notes=True, + release_url="https://www.example.com/release/1.93.3", + device_class=UpdateDeviceClass.FIRMWARE, + ), + ] + ) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Demo config entry.""" + await async_setup_platform(hass, {}, async_add_entities) + + +async def _fake_install() -> None: + """Fake install an update.""" + await asyncio.sleep(FAKE_INSTALL_SLEEP_TIME) + + +class DemoUpdate(UpdateEntity): + """Representation of a demo update entity.""" + + _attr_should_poll = False + + def __init__( + self, + *, + unique_id: str, + name: str, + title: str | None, + installed_version: str | None, + latest_version: str | None, + release_summary: str | None = None, + release_url: str | None = None, + support_progress: bool = False, + support_install: bool = True, + support_release_notes: bool = False, + device_class: UpdateDeviceClass | None = None, + ) -> None: + """Initialize the Demo select entity.""" + self._attr_installed_version = installed_version + self._attr_device_class = device_class + self._attr_latest_version = latest_version + self._attr_name = name or DEVICE_DEFAULT_NAME + self._attr_release_summary = release_summary + self._attr_release_url = release_url + self._attr_title = title + self._attr_unique_id = unique_id + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, unique_id)}, + name=name, + ) + if support_install: + self._attr_supported_features |= ( + UpdateEntityFeature.INSTALL + | UpdateEntityFeature.BACKUP + | UpdateEntityFeature.SPECIFIC_VERSION + ) + if support_progress: + self._attr_supported_features |= UpdateEntityFeature.PROGRESS + + if support_release_notes: + self._attr_supported_features |= UpdateEntityFeature.RELEASE_NOTES + + async def async_install( + self, version: str | None, backup: bool, **kwargs: Any + ) -> None: + """Install an update.""" + if self.supported_features & UpdateEntityFeature.PROGRESS: + for progress in range(0, 100, 10): + self._attr_in_progress = progress + self.async_write_ha_state() + await _fake_install() + + self._attr_in_progress = False + self._attr_installed_version = ( + version if version is not None else self.latest_version + ) + self.async_write_ha_state() + + def release_notes(self) -> str | None: + """Return the release notes.""" + return ( + "Long release notes.\n\n**With** " + f"markdown support!\n\n***\n\n{self.release_summary}" + ) diff --git a/homeassistant/components/denonavr/config_flow.py b/homeassistant/components/denonavr/config_flow.py index 2d5cef14f5b..238c87bbf5e 100644 --- a/homeassistant/components/denonavr/config_flow.py +++ b/homeassistant/components/denonavr/config_flow.py @@ -11,7 +11,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.components import ssdp -from homeassistant.const import CONF_HOST, CONF_TYPE +from homeassistant.const import CONF_HOST, CONF_MODEL, CONF_TYPE from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.httpx_client import get_async_client @@ -28,7 +28,6 @@ IGNORED_MODELS = ["HEOS 1", "HEOS 3", "HEOS 5", "HEOS 7"] CONF_SHOW_ALL_SOURCES = "show_all_sources" CONF_ZONE2 = "zone2" CONF_ZONE3 = "zone3" -CONF_MODEL = "model" CONF_MANUFACTURER = "manufacturer" CONF_SERIAL_NUMBER = "serial_number" CONF_UPDATE_AUDYSSEY = "update_audyssey" diff --git a/homeassistant/components/denonavr/media_player.py b/homeassistant/components/denonavr/media_player.py index fe9b81c65d3..fb8c6fa3015 100644 --- a/homeassistant/components/denonavr/media_player.py +++ b/homeassistant/components/denonavr/media_player.py @@ -35,7 +35,13 @@ from homeassistant.components.media_player.const import ( SUPPORT_VOLUME_STEP, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_COMMAND, CONF_HOST, STATE_PAUSED, STATE_PLAYING +from homeassistant.const import ( + ATTR_COMMAND, + CONF_HOST, + CONF_MODEL, + STATE_PAUSED, + STATE_PLAYING, +) from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.entity import DeviceInfo @@ -44,7 +50,6 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import CONF_RECEIVER from .config_flow import ( CONF_MANUFACTURER, - CONF_MODEL, CONF_SERIAL_NUMBER, CONF_TYPE, CONF_UPDATE_AUDYSSEY, diff --git a/homeassistant/components/denonavr/strings.json b/homeassistant/components/denonavr/strings.json index eaa06c5ff88..e2a90a5c01b 100644 --- a/homeassistant/components/denonavr/strings.json +++ b/homeassistant/components/denonavr/strings.json @@ -3,14 +3,14 @@ "flow_title": "{name}", "step": { "user": { - "title": "Denon AVR Network Receivers", - "description": "Connect to your receiver, if the IP address is not set, auto-discovery is used", "data": { "host": "[%key:common::config_flow::data::ip%]" + }, + "data_description": { + "host": "Leave blank to use auto-discovery" } }, "confirm": { - "title": "Denon AVR Network Receivers", "description": "Please confirm adding the receiver" }, "select": { @@ -35,7 +35,6 @@ "options": { "step": { "init": { - "title": "Denon AVR Network Receivers", "description": "Specify optional settings", "data": { "show_all_sources": "Show all sources", diff --git a/homeassistant/components/denonavr/translations/zh-Hant.json b/homeassistant/components/denonavr/translations/zh-Hant.json index a8ee7f87fd8..073de46866d 100644 --- a/homeassistant/components/denonavr/translations/zh-Hant.json +++ b/homeassistant/components/denonavr/translations/zh-Hant.json @@ -4,7 +4,7 @@ "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\uff0c\u8acb\u518d\u8a66\u4e00\u6b21\u3002\u95dc\u9589\u4e3b\u96fb\u6e90\u3001\u5c07\u4e59\u592a\u7db2\u8def\u65b7\u7dda\u5f8c\u91cd\u65b0\u9023\u7dda\uff0c\u53ef\u80fd\u6703\u6709\u6240\u5e6b\u52a9", - "not_denonavr_manufacturer": "\u4e26\u975e Denon AVR \u7db2\u8def\u63a5\u6536\u5668\uff0c\u6240\u63a2\u7d22\u4e4b\u88fd\u9020\u5ee0\u5546\u4e0d\u7b26\u5408", + "not_denonavr_manufacturer": "\u4e26\u975e Denon AVR \u7db2\u8def\u63a5\u6536\u5668\uff0c\u6240\u767c\u73fe\u4e4b\u88fd\u9020\u5ee0\u5546\u4e0d\u7b26\u5408", "not_denonavr_missing": "\u4e26\u975e Denon AVR \u7db2\u8def\u63a5\u6536\u5668\uff0c\u63a2\u7d22\u8cc7\u8a0a\u4e0d\u5b8c\u6574" }, "error": { diff --git a/homeassistant/components/derivative/__init__.py b/homeassistant/components/derivative/__init__.py index afee8d5d175..e3fe9d85f41 100644 --- a/homeassistant/components/derivative/__init__.py +++ b/homeassistant/components/derivative/__init__.py @@ -1 +1,23 @@ -"""The derivative component.""" +"""The Derivative integration.""" +from __future__ import annotations + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Derivative from a config entry.""" + hass.config_entries.async_setup_platforms(entry, (Platform.SENSOR,)) + entry.async_on_unload(entry.add_update_listener(config_entry_update_listener)) + return True + + +async def config_entry_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Update listener, called when the config entry options are changed.""" + await hass.config_entries.async_reload(entry.entry_id) + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + return await hass.config_entries.async_unload_platforms(entry, (Platform.SENSOR,)) diff --git a/homeassistant/components/derivative/config_flow.py b/homeassistant/components/derivative/config_flow.py new file mode 100644 index 00000000000..6249e9fd1cc --- /dev/null +++ b/homeassistant/components/derivative/config_flow.py @@ -0,0 +1,99 @@ +"""Config flow for Derivative integration.""" +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, cast + +import voluptuous as vol + +from homeassistant.const import ( + CONF_NAME, + CONF_SOURCE, + CONF_UNIT_OF_MEASUREMENT, + TIME_DAYS, + TIME_HOURS, + TIME_MINUTES, + TIME_SECONDS, +) +from homeassistant.helpers import selector +from homeassistant.helpers.schema_config_entry_flow import ( + SchemaConfigFlowHandler, + SchemaFlowFormStep, + SchemaFlowMenuStep, +) + +from .const import ( + CONF_ROUND_DIGITS, + CONF_TIME_WINDOW, + CONF_UNIT_PREFIX, + CONF_UNIT_TIME, + DOMAIN, +) + +UNIT_PREFIXES = [ + {"value": "none", "label": "none"}, + {"value": "n", "label": "n (nano)"}, + {"value": "µ", "label": "µ (micro)"}, + {"value": "m", "label": "m (milli)"}, + {"value": "k", "label": "k (kilo)"}, + {"value": "M", "label": "M (mega)"}, + {"value": "G", "label": "G (giga)"}, + {"value": "T", "label": "T (tera)"}, + {"value": "P", "label": "P (peta)"}, +] +TIME_UNITS = [ + {"value": TIME_SECONDS, "label": "Seconds"}, + {"value": TIME_MINUTES, "label": "Minutes"}, + {"value": TIME_HOURS, "label": "Hours"}, + {"value": TIME_DAYS, "label": "Days"}, +] + +OPTIONS_SCHEMA = vol.Schema( + { + vol.Required(CONF_ROUND_DIGITS, default=2): selector.selector( + { + "number": { + "min": 0, + "max": 6, + "mode": "box", + CONF_UNIT_OF_MEASUREMENT: "decimals", + } + } + ), + vol.Required(CONF_TIME_WINDOW): selector.selector({"duration": {}}), + vol.Required(CONF_UNIT_PREFIX, default="none"): selector.selector( + {"select": {"options": UNIT_PREFIXES}} + ), + vol.Required(CONF_UNIT_TIME, default=TIME_HOURS): selector.selector( + {"select": {"options": TIME_UNITS}} + ), + } +) + +CONFIG_SCHEMA = vol.Schema( + { + vol.Required(CONF_NAME): selector.selector({"text": {}}), + vol.Required(CONF_SOURCE): selector.selector( + {"entity": {"domain": "sensor"}}, + ), + } +).extend(OPTIONS_SCHEMA.schema) + +CONFIG_FLOW: dict[str, SchemaFlowFormStep | SchemaFlowMenuStep] = { + "user": SchemaFlowFormStep(CONFIG_SCHEMA) +} + +OPTIONS_FLOW: dict[str, SchemaFlowFormStep | SchemaFlowMenuStep] = { + "init": SchemaFlowFormStep(OPTIONS_SCHEMA) +} + + +class ConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN): + """Handle a config or options flow for Derivative.""" + + config_flow = CONFIG_FLOW + options_flow = OPTIONS_FLOW + + def async_config_entry_title(self, options: Mapping[str, Any]) -> str: + """Return config entry title.""" + return cast(str, options[CONF_NAME]) diff --git a/homeassistant/components/derivative/const.py b/homeassistant/components/derivative/const.py new file mode 100644 index 00000000000..32f2777dc80 --- /dev/null +++ b/homeassistant/components/derivative/const.py @@ -0,0 +1,9 @@ +"""Constants for the Derivative integration.""" + +DOMAIN = "derivative" + +CONF_ROUND_DIGITS = "round" +CONF_TIME_WINDOW = "time_window" +CONF_UNIT = "unit" +CONF_UNIT_PREFIX = "unit_prefix" +CONF_UNIT_TIME = "unit_time" diff --git a/homeassistant/components/derivative/manifest.json b/homeassistant/components/derivative/manifest.json index 2b86c07cfe4..338e3d48533 100644 --- a/homeassistant/components/derivative/manifest.json +++ b/homeassistant/components/derivative/manifest.json @@ -1,7 +1,9 @@ { "domain": "derivative", + "integration_type": "helper", "name": "Derivative", "documentation": "https://www.home-assistant.io/integrations/derivative", "codeowners": ["@afaucogney"], - "iot_class": "calculated" + "iot_class": "calculated", + "config_flow": true } diff --git a/homeassistant/components/derivative/sensor.py b/homeassistant/components/derivative/sensor.py index 9be01f27e4d..fb5bf7e518d 100644 --- a/homeassistant/components/derivative/sensor.py +++ b/homeassistant/components/derivative/sensor.py @@ -8,6 +8,7 @@ import logging import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, CONF_NAME, @@ -20,24 +21,26 @@ from homeassistant.const import ( TIME_SECONDS, ) from homeassistant.core import HomeAssistant, callback -import homeassistant.helpers.config_validation as cv +from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from .const import ( + CONF_ROUND_DIGITS, + CONF_TIME_WINDOW, + CONF_UNIT, + CONF_UNIT_PREFIX, + CONF_UNIT_TIME, +) + # mypy: allow-untyped-defs, no-check-untyped-defs _LOGGER = logging.getLogger(__name__) ATTR_SOURCE_ID = "source" -CONF_ROUND_DIGITS = "round" -CONF_UNIT_PREFIX = "unit_prefix" -CONF_UNIT_TIME = "unit_time" -CONF_UNIT = "unit" -CONF_TIME_WINDOW = "time_window" - # SI Metric prefixes UNIT_PREFIXES = { None: 1, @@ -76,6 +79,36 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Initialize Derivative config entry.""" + registry = er.async_get(hass) + # Validate + resolve entity registry id to entity_id + source_entity_id = er.async_validate_entity_id( + registry, config_entry.options[CONF_SOURCE] + ) + + unit_prefix = config_entry.options[CONF_UNIT_PREFIX] + if unit_prefix == "none": + unit_prefix = None + + derivative_sensor = DerivativeSensor( + name=config_entry.title, + round_digits=int(config_entry.options[CONF_ROUND_DIGITS]), + source_entity=source_entity_id, + time_window=cv.time_period_dict(config_entry.options[CONF_TIME_WINDOW]), + unique_id=config_entry.entry_id, + unit_of_measurement=None, + unit_prefix=unit_prefix, + unit_time=config_entry.options[CONF_UNIT_TIME], + ) + + async_add_entities([derivative_sensor]) + + async def async_setup_platform( hass: HomeAssistant, config: ConfigType, @@ -84,13 +117,14 @@ async def async_setup_platform( ) -> None: """Set up the derivative sensor.""" derivative = DerivativeSensor( - source_entity=config[CONF_SOURCE], name=config.get(CONF_NAME), round_digits=config[CONF_ROUND_DIGITS], + source_entity=config[CONF_SOURCE], + time_window=config[CONF_TIME_WINDOW], + unit_of_measurement=config.get(CONF_UNIT), unit_prefix=config[CONF_UNIT_PREFIX], unit_time=config[CONF_UNIT_TIME], - unit_of_measurement=config.get(CONF_UNIT), - time_window=config[CONF_TIME_WINDOW], + unique_id=None, ) async_add_entities([derivative]) @@ -101,15 +135,18 @@ class DerivativeSensor(RestoreEntity, SensorEntity): def __init__( self, - source_entity, + *, name, round_digits, + source_entity, + time_window, + unit_of_measurement, unit_prefix, unit_time, - unit_of_measurement, - time_window, + unique_id, ): """Initialize the derivative sensor.""" + self._attr_unique_id = unique_id self._sensor_source_id = source_entity self._round_digits = round_digits self._state = 0 @@ -213,8 +250,10 @@ class DerivativeSensor(RestoreEntity, SensorEntity): self._state = derivative self.async_write_ha_state() - async_track_state_change_event( - self.hass, [self._sensor_source_id], calc_derivative + self.async_on_remove( + async_track_state_change_event( + self.hass, self._sensor_source_id, calc_derivative + ) ) @property diff --git a/homeassistant/components/derivative/strings.json b/homeassistant/components/derivative/strings.json new file mode 100644 index 00000000000..0a58b28a1c6 --- /dev/null +++ b/homeassistant/components/derivative/strings.json @@ -0,0 +1,43 @@ +{ + "title": "Derivative sensor", + "config": { + "step": { + "user": { + "title": "Add Derivative sensor", + "description": "Create a sensor that estimates the derivative of a sensor.", + "data": { + "name": "Name", + "round": "Precision", + "source": "Input sensor", + "time_window": "Time window", + "unit_prefix": "Metric prefix", + "unit_time": "Time unit" + }, + "data_description": { + "round": "Controls the number of decimal digits in the output.", + "time_window": "If set, the sensor's value is a time weighted moving average of derivatives within this window.", + "unit_prefix": "The output will be scaled according to the selected metric prefix and time unit of the derivative." + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "name": "[%key:component::derivative::config::step::user::data::name%]", + "round": "[%key:component::derivative::config::step::user::data::round%]", + "source": "[%key:component::derivative::config::step::user::data::source%]", + "time_window": "[%key:component::derivative::config::step::user::data::time_window%]", + "unit_prefix": "[%key:component::derivative::config::step::user::data::unit_prefix%]", + "unit_time": "[%key:component::derivative::config::step::user::data::unit_time%]" + }, + "data_description": { + "round": "[%key:component::derivative::config::step::user::data_description::round%]", + "time_window": "[%key:component::derivative::config::step::user::data_description::time_window%]", + "unit_prefix": "[%key:component::derivative::config::step::user::data_description::unit_prefix%]." + } + } + } + } +} diff --git a/homeassistant/components/derivative/translations/en.json b/homeassistant/components/derivative/translations/en.json new file mode 100644 index 00000000000..b91318b5237 --- /dev/null +++ b/homeassistant/components/derivative/translations/en.json @@ -0,0 +1,43 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "Name", + "round": "Precision", + "source": "Input sensor", + "time_window": "Time window", + "unit_prefix": "Metric prefix", + "unit_time": "Time unit" + }, + "data_description": { + "round": "Controls the number of decimal digits in the output.", + "time_window": "If set, the sensor's value is a time weighted moving average of derivatives within this window.", + "unit_prefix": "The output will be scaled according to the selected metric prefix and time unit of the derivative." + }, + "description": "Create a sensor that estimates the derivative of a sensor.", + "title": "Add Derivative sensor" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "name": "Name", + "round": "Precision", + "source": "Input sensor", + "time_window": "Time window", + "unit_prefix": "Metric prefix", + "unit_time": "Time unit" + }, + "data_description": { + "round": "Controls the number of decimal digits in the output.", + "time_window": "If set, the sensor's value is a time weighted moving average of derivatives within this window.", + "unit_prefix": "The output will be scaled according to the selected metric prefix and time unit of the derivative.." + } + } + } + }, + "title": "Derivative sensor" +} \ No newline at end of file diff --git a/homeassistant/components/device_automation/__init__.py b/homeassistant/components/device_automation/__init__.py index 75613b3d118..46a3bf6815d 100644 --- a/homeassistant/components/device_automation/__init__.py +++ b/homeassistant/components/device_automation/__init__.py @@ -20,7 +20,6 @@ from homeassistant.helpers import ( device_registry as dr, entity_registry as er, ) -from homeassistant.helpers.frame import report from homeassistant.helpers.typing import ConfigType from homeassistant.loader import IntegrationNotFound, bind_hass from homeassistant.requirements import async_get_integration_with_requirements @@ -88,24 +87,6 @@ TYPES = { } -@bind_hass -async def async_get_device_automations( - hass: HomeAssistant, - automation_type: DeviceAutomationType | str, - device_ids: Iterable[str] | None = None, -) -> Mapping[str, Any]: - """Return all the device automations for a type optionally limited to specific device ids.""" - if isinstance(automation_type, str): - report( - "uses str for async_get_device_automations automation_type. This is " - "deprecated and will stop working in Home Assistant 2022.4, it should be " - "updated to use DeviceAutomationType instead", - error_if_core=False, - ) - automation_type = DeviceAutomationType[automation_type.upper()] - return await _async_get_device_automations(hass, automation_type, device_ids) - - async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up device automation.""" websocket_api.async_register_command(hass, websocket_device_automation_list_actions) @@ -156,26 +137,18 @@ async def async_get_device_automation_platform( # noqa: D103 @overload async def async_get_device_automation_platform( # noqa: D103 - hass: HomeAssistant, domain: str, automation_type: DeviceAutomationType | str + hass: HomeAssistant, domain: str, automation_type: DeviceAutomationType ) -> "DeviceAutomationPlatformType": ... async def async_get_device_automation_platform( - hass: HomeAssistant, domain: str, automation_type: DeviceAutomationType | str + hass: HomeAssistant, domain: str, automation_type: DeviceAutomationType ) -> "DeviceAutomationPlatformType": """Load device automation platform for integration. Throws InvalidDeviceAutomationConfig if the integration is not found or does not support device automation. """ - if isinstance(automation_type, str): - report( - "uses str for async_get_device_automation_platform automation_type. This " - "is deprecated and will stop working in Home Assistant 2022.4, it should " - "be updated to use DeviceAutomationType instead", - error_if_core=False, - ) - automation_type = DeviceAutomationType[automation_type.upper()] platform_name = automation_type.value.section try: integration = await async_get_integration_with_requirements(hass, domain) @@ -215,10 +188,11 @@ async def _async_get_device_automations_from_domain( ) -async def _async_get_device_automations( +@bind_hass +async def async_get_device_automations( hass: HomeAssistant, automation_type: DeviceAutomationType, - device_ids: Iterable[str] | None, + device_ids: Iterable[str] | None = None, ) -> Mapping[str, list[dict[str, Any]]]: """List device automations.""" device_registry = dr.async_get(hass) @@ -336,7 +310,7 @@ async def websocket_device_automation_list_actions(hass, connection, msg): """Handle request for device actions.""" device_id = msg["device_id"] actions = ( - await _async_get_device_automations( + await async_get_device_automations( hass, DeviceAutomationType.ACTION, [device_id] ) ).get(device_id) @@ -355,7 +329,7 @@ async def websocket_device_automation_list_conditions(hass, connection, msg): """Handle request for device conditions.""" device_id = msg["device_id"] conditions = ( - await _async_get_device_automations( + await async_get_device_automations( hass, DeviceAutomationType.CONDITION, [device_id] ) ).get(device_id) @@ -374,7 +348,7 @@ async def websocket_device_automation_list_triggers(hass, connection, msg): """Handle request for device triggers.""" device_id = msg["device_id"] triggers = ( - await _async_get_device_automations( + await async_get_device_automations( hass, DeviceAutomationType.TRIGGER, [device_id] ) ).get(device_id) diff --git a/homeassistant/components/device_tracker/manifest.json b/homeassistant/components/device_tracker/manifest.json index 6e29d977f66..7abd68b03e2 100644 --- a/homeassistant/components/device_tracker/manifest.json +++ b/homeassistant/components/device_tracker/manifest.json @@ -4,6 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/device_tracker", "dependencies": ["zone"], "after_dependencies": [], - "codeowners": [], + "codeowners": ["@home-assistant/core"], "quality_scale": "internal" } diff --git a/homeassistant/components/device_tracker/strings.json b/homeassistant/components/device_tracker/strings.json index 5d492ea8e79..48cb667e730 100644 --- a/homeassistant/components/device_tracker/strings.json +++ b/homeassistant/components/device_tracker/strings.json @@ -16,4 +16,4 @@ "not_home": "[%key:common::state::not_home%]" } } -} \ No newline at end of file +} diff --git a/homeassistant/components/device_tracker/translations/el.json b/homeassistant/components/device_tracker/translations/el.json index 7d42d825345..a577138f055 100644 --- a/homeassistant/components/device_tracker/translations/el.json +++ b/homeassistant/components/device_tracker/translations/el.json @@ -12,8 +12,8 @@ "state": { "_": { "home": "\u03a3\u03c0\u03af\u03c4\u03b9", - "not_home": "\u0395\u03ba\u03c4\u03cc\u03c2 \u03a3\u03c0\u03b9\u03c4\u03b9\u03bf\u03cd" + "not_home": "\u0395\u03ba\u03c4\u03cc\u03c2 \u03c3\u03c0\u03b9\u03c4\u03b9\u03bf\u03cd" } }, - "title": "\u03a3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b1\u03bd\u03b9\u03c7\u03bd\u03b5\u03c5\u03c4\u03ae" + "title": "\u0395\u03bd\u03c4\u03bf\u03c0\u03b9\u03c3\u03c4\u03ae\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2" } \ No newline at end of file diff --git a/homeassistant/components/devolo_home_control/strings.json b/homeassistant/components/devolo_home_control/strings.json index ba1bc20bfd2..293763c890e 100644 --- a/homeassistant/components/devolo_home_control/strings.json +++ b/homeassistant/components/devolo_home_control/strings.json @@ -26,4 +26,3 @@ } } } - diff --git a/homeassistant/components/devolo_home_control/translations/fr.json b/homeassistant/components/devolo_home_control/translations/fr.json index 020b469092d..14a4bdb9c08 100644 --- a/homeassistant/components/devolo_home_control/translations/fr.json +++ b/homeassistant/components/devolo_home_control/translations/fr.json @@ -5,7 +5,7 @@ "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" }, "error": { - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "reauth_failed": "Veuillez utiliser le m\u00eame utilisateur mydevolo que pr\u00e9c\u00e9demment." }, "step": { @@ -13,14 +13,14 @@ "data": { "mydevolo_url": "URL mydevolo", "password": "Mot de passe", - "username": "Email / devolo ID" + "username": "Courriel / ID devolo" } }, "zeroconf_confirm": { "data": { - "mydevolo_url": "mydevolo URL", + "mydevolo_url": "URL mydevolo", "password": "Mot de passe", - "username": "Email / devolo ID" + "username": "Courriel / ID devolo" } } } diff --git a/homeassistant/components/devolo_home_network/strings.json b/homeassistant/components/devolo_home_network/strings.json index 685e139d2b8..63be57d9485 100644 --- a/homeassistant/components/devolo_home_network/strings.json +++ b/homeassistant/components/devolo_home_network/strings.json @@ -22,4 +22,4 @@ "home_control": "The devolo Home Control Central Unit does not work with this integration." } } -} \ No newline at end of file +} diff --git a/homeassistant/components/devolo_home_network/translations/fr.json b/homeassistant/components/devolo_home_network/translations/fr.json index 49dc24e0db1..50e601bb14a 100644 --- a/homeassistant/components/devolo_home_network/translations/fr.json +++ b/homeassistant/components/devolo_home_network/translations/fr.json @@ -14,10 +14,10 @@ "data": { "ip_address": "Adresse IP" }, - "description": "Voulez-vous commencer la configuration ?" + "description": "Voulez-vous commencer la configuration\u00a0?" }, "zeroconf_confirm": { - "description": "Voulez-vous ajouter le p\u00e9riph\u00e9rique r\u00e9seau domestique devolo avec le nom d'h\u00f4te ` {host_name} ` \u00e0 Home Assistant\u00a0?", + "description": "Voulez-vous ajouter l'appareil de r\u00e9seau domestique devolo portant le nom d'h\u00f4te `{host_name}` \u00e0 Home Assistant\u00a0?", "title": "Appareil r\u00e9seau domestique devolo d\u00e9couvert" } } diff --git a/homeassistant/components/devolo_home_network/translations/zh-Hant.json b/homeassistant/components/devolo_home_network/translations/zh-Hant.json index 17eb11eb070..bccc6aa24e3 100644 --- a/homeassistant/components/devolo_home_network/translations/zh-Hant.json +++ b/homeassistant/components/devolo_home_network/translations/zh-Hant.json @@ -18,7 +18,7 @@ }, "zeroconf_confirm": { "description": "\u662f\u5426\u8981\u5c07\u4e3b\u6a5f\u540d\u7a31\u70ba `{host_name}` \u7684 Devolo \u667a\u80fd\u5bb6\u5ead\u7db2\u8def\u88dd\u7f6e\u65b0\u589e\u81f3 Home Assistant\uff1f", - "title": "\u81ea\u52d5\u63a2\u7d22\u5230 Devolo \u667a\u80fd\u5bb6\u5ead\u7db2\u8def\u88dd\u7f6e" + "title": "\u81ea\u52d5\u641c\u7d22\u5230 Devolo \u667a\u80fd\u5bb6\u5ead\u7db2\u8def\u88dd\u7f6e" } } } diff --git a/homeassistant/components/dexcom/strings.json b/homeassistant/components/dexcom/strings.json index 5cc2b363665..588556eda49 100644 --- a/homeassistant/components/dexcom/strings.json +++ b/homeassistant/components/dexcom/strings.json @@ -29,4 +29,4 @@ } } } -} \ No newline at end of file +} diff --git a/homeassistant/components/dexcom/translations/fr.json b/homeassistant/components/dexcom/translations/fr.json index 095c769a1be..316e39e2e5e 100644 --- a/homeassistant/components/dexcom/translations/fr.json +++ b/homeassistant/components/dexcom/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/dhcp/__init__.py b/homeassistant/components/dhcp/__init__.py index 0b5f8a49a34..1756f620f46 100644 --- a/homeassistant/components/dhcp/__init__.py +++ b/homeassistant/components/dhcp/__init__.py @@ -2,6 +2,9 @@ from __future__ import annotations from abc import abstractmethod +import asyncio +from collections.abc import Callable, Iterable +import contextlib from dataclasses import dataclass from datetime import timedelta import fnmatch @@ -9,7 +12,7 @@ from ipaddress import ip_address as make_ip_address import logging import os import threading -from typing import Any, Final +from typing import TYPE_CHECKING, Any, Final, cast from aiodiscover import DiscoverHosts from aiodiscover.discovery import ( @@ -51,12 +54,16 @@ from homeassistant.helpers.event import ( ) from homeassistant.helpers.frame import report from homeassistant.helpers.typing import ConfigType -from homeassistant.loader import async_get_dhcp +from homeassistant.loader import DHCPMatcher, async_get_dhcp from homeassistant.util.async_ import run_callback_threadsafe from homeassistant.util.network import is_invalid, is_link_local, is_loopback from .const import DOMAIN +if TYPE_CHECKING: + from scapy.packet import Packet + from scapy.sendrecv import AsyncSniffer + FILTER = "udp and (port 67 or 68)" REQUESTED_ADDR = "requested_addr" MESSAGE_TYPE = "message-type" @@ -115,7 +122,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: watchers: list[WatcherBase] = [] address_data: dict[str, dict[str, str]] = {} integration_matchers = await async_get_dhcp(hass) - # For the passive classes we need to start listening # for state changes and connect the dispatchers before # everything else starts up or we will miss events @@ -124,13 +130,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: await passive_watcher.async_start() watchers.append(passive_watcher) - async def _initialize(_): + async def _initialize(event: Event) -> None: for active_cls in (DHCPWatcher, NetworkWatcher): active_watcher = active_cls(hass, address_data, integration_matchers) await active_watcher.async_start() watchers.append(active_watcher) - async def _async_stop(*_): + async def _async_stop(event: Event) -> None: for watcher in watchers: await watcher.async_stop() @@ -143,7 +149,12 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: class WatcherBase: """Base class for dhcp and device tracker watching.""" - def __init__(self, hass, address_data, integration_matchers): + def __init__( + self, + hass: HomeAssistant, + address_data: dict[str, dict[str, str]], + integration_matchers: list[DHCPMatcher], + ) -> None: """Initialize class.""" super().__init__() @@ -152,11 +163,11 @@ class WatcherBase: self._address_data = address_data @abstractmethod - async def async_stop(self): + async def async_stop(self) -> None: """Stop the watcher.""" @abstractmethod - async def async_start(self): + async def async_start(self) -> None: """Start the watcher.""" def process_client(self, ip_address: str, hostname: str, mac_address: str) -> None: @@ -197,8 +208,8 @@ class WatcherBase: data = {MAC_ADDRESS: mac_address, HOSTNAME: hostname} self._address_data[ip_address] = data - lowercase_hostname = data[HOSTNAME].lower() - uppercase_mac = data[MAC_ADDRESS].upper() + lowercase_hostname = hostname.lower() + uppercase_mac = mac_address.upper() _LOGGER.debug( "Processing updated address data for %s: mac=%s hostname=%s", @@ -218,22 +229,24 @@ class WatcherBase: if entry := self.hass.config_entries.async_get_entry(entry_id): device_domains.add(entry.domain) - for entry in self._integration_matchers: - if entry.get(REGISTERED_DEVICES) and not entry["domain"] in device_domains: + for matcher in self._integration_matchers: + domain = matcher["domain"] + + if matcher.get(REGISTERED_DEVICES) and domain not in device_domains: continue - if MAC_ADDRESS in entry and not fnmatch.fnmatch( - uppercase_mac, entry[MAC_ADDRESS] - ): + if ( + matcher_mac := matcher.get(MAC_ADDRESS) + ) is not None and not fnmatch.fnmatch(uppercase_mac, matcher_mac): continue - if HOSTNAME in entry and not fnmatch.fnmatch( - lowercase_hostname, entry[HOSTNAME] - ): + if ( + matcher_hostname := matcher.get(HOSTNAME) + ) is not None and not fnmatch.fnmatch(lowercase_hostname, matcher_hostname): continue - _LOGGER.debug("Matched %s against %s", data, entry) - matched_domains.add(entry["domain"]) + _LOGGER.debug("Matched %s against %s", data, matcher) + matched_domains.add(domain) for domain in matched_domains: discovery_flow.async_create_flow( @@ -243,7 +256,7 @@ class WatcherBase: DhcpServiceInfo( ip=ip_address, hostname=lowercase_hostname, - macaddress=data[MAC_ADDRESS], + macaddress=mac_address, ), ) @@ -251,14 +264,19 @@ class WatcherBase: class NetworkWatcher(WatcherBase): """Class to query ptr records routers.""" - def __init__(self, hass, address_data, integration_matchers): + def __init__( + self, + hass: HomeAssistant, + address_data: dict[str, dict[str, str]], + integration_matchers: list[DHCPMatcher], + ) -> None: """Initialize class.""" super().__init__(hass, address_data, integration_matchers) - self._unsub = None - self._discover_hosts = None - self._discover_task = None + self._unsub: Callable[[], None] | None = None + self._discover_hosts: DiscoverHosts | None = None + self._discover_task: asyncio.Task | None = None - async def async_stop(self): + async def async_stop(self) -> None: """Stop scanning for new devices on the network.""" if self._unsub: self._unsub() @@ -267,7 +285,7 @@ class NetworkWatcher(WatcherBase): self._discover_task.cancel() self._discover_task = None - async def async_start(self): + async def async_start(self) -> None: """Start scanning for new devices on the network.""" self._discover_hosts = DiscoverHosts() self._unsub = async_track_time_interval( @@ -276,14 +294,15 @@ class NetworkWatcher(WatcherBase): self.async_start_discover() @callback - def async_start_discover(self, *_): + def async_start_discover(self, *_: Any) -> None: """Start a new discovery task if one is not running.""" if self._discover_task and not self._discover_task.done(): return self._discover_task = self.hass.async_create_task(self.async_discover()) - async def async_discover(self): + async def async_discover(self) -> None: """Process discovery.""" + assert self._discover_hosts is not None for host in await self._discover_hosts.async_discover(): self.async_process_client( host[DISCOVERY_IP_ADDRESS], @@ -295,18 +314,23 @@ class NetworkWatcher(WatcherBase): class DeviceTrackerWatcher(WatcherBase): """Class to watch dhcp data from routers.""" - def __init__(self, hass, address_data, integration_matchers): + def __init__( + self, + hass: HomeAssistant, + address_data: dict[str, dict[str, str]], + integration_matchers: list[DHCPMatcher], + ) -> None: """Initialize class.""" super().__init__(hass, address_data, integration_matchers) - self._unsub = None + self._unsub: Callable[[], None] | None = None - async def async_stop(self): + async def async_stop(self) -> None: """Stop watching for new device trackers.""" if self._unsub: self._unsub() self._unsub = None - async def async_start(self): + async def async_start(self) -> None: """Stop watching for new device trackers.""" self._unsub = async_track_state_added_domain( self.hass, [DEVICE_TRACKER_DOMAIN], self._async_process_device_event @@ -315,12 +339,12 @@ class DeviceTrackerWatcher(WatcherBase): self._async_process_device_state(state) @callback - def _async_process_device_event(self, event: Event): + def _async_process_device_event(self, event: Event) -> None: """Process a device tracker state change event.""" self._async_process_device_state(event.data["new_state"]) @callback - def _async_process_device_state(self, state: State): + def _async_process_device_state(self, state: State) -> None: """Process a device tracker state.""" if state.state != STATE_HOME: return @@ -343,18 +367,23 @@ class DeviceTrackerWatcher(WatcherBase): class DeviceTrackerRegisteredWatcher(WatcherBase): """Class to watch data from device tracker registrations.""" - def __init__(self, hass, address_data, integration_matchers): + def __init__( + self, + hass: HomeAssistant, + address_data: dict[str, dict[str, str]], + integration_matchers: list[DHCPMatcher], + ) -> None: """Initialize class.""" super().__init__(hass, address_data, integration_matchers) - self._unsub = None + self._unsub: Callable[[], None] | None = None - async def async_stop(self): + async def async_stop(self) -> None: """Stop watching for device tracker registrations.""" if self._unsub: self._unsub() self._unsub = None - async def async_start(self): + async def async_start(self) -> None: """Stop watching for device tracker registrations.""" self._unsub = async_dispatcher_connect( self.hass, CONNECTED_DEVICE_REGISTERED, self._async_process_device_data @@ -376,26 +405,32 @@ class DeviceTrackerRegisteredWatcher(WatcherBase): class DHCPWatcher(WatcherBase): """Class to watch dhcp requests.""" - def __init__(self, hass, address_data, integration_matchers): + def __init__( + self, + hass: HomeAssistant, + address_data: dict[str, dict[str, str]], + integration_matchers: list[DHCPMatcher], + ) -> None: """Initialize class.""" super().__init__(hass, address_data, integration_matchers) - self._sniffer = None + self._sniffer: AsyncSniffer | None = None self._started = threading.Event() - async def async_stop(self): + async def async_stop(self) -> None: """Stop watching for new device trackers.""" await self.hass.async_add_executor_job(self._stop) - def _stop(self): + def _stop(self) -> None: """Stop the thread.""" if self._started.is_set(): + assert self._sniffer is not None self._sniffer.stop() - async def async_start(self): + async def async_start(self) -> None: """Start watching for dhcp packets.""" await self.hass.async_add_executor_job(self._start) - def _start(self): + def _start(self) -> None: """Start watching for dhcp packets.""" # Local import because importing from scapy has side effects such as opening # sockets @@ -417,20 +452,25 @@ class DHCPWatcher(WatcherBase): AsyncSniffer, ) - def _handle_dhcp_packet(packet): + def _handle_dhcp_packet(packet: Packet) -> None: """Process a dhcp packet.""" if DHCP not in packet: return - options = packet[DHCP].options - request_type = _decode_dhcp_option(options, MESSAGE_TYPE) - if request_type != DHCP_REQUEST: + options_dict = _dhcp_options_as_dict(packet[DHCP].options) + if options_dict.get(MESSAGE_TYPE) != DHCP_REQUEST: # Not a DHCP request return - ip_address = _decode_dhcp_option(options, REQUESTED_ADDR) or packet[IP].src - hostname = _decode_dhcp_option(options, HOSTNAME) or "" - mac_address = _format_mac(packet[Ether].src) + ip_address = options_dict.get(REQUESTED_ADDR) or cast(str, packet[IP].src) + assert isinstance(ip_address, str) + hostname = "" + if (hostname_bytes := options_dict.get(HOSTNAME)) and isinstance( + hostname_bytes, bytes + ): + with contextlib.suppress(AttributeError, UnicodeDecodeError): + hostname = hostname_bytes.decode() + mac_address = _format_mac(cast(str, packet[Ether].src)) if ip_address is not None and mac_address is not None: self.process_client(ip_address, hostname, mac_address) @@ -470,29 +510,19 @@ class DHCPWatcher(WatcherBase): self._sniffer.thread.name = self.__class__.__name__ -def _decode_dhcp_option(dhcp_options, key): - """Extract and decode data from a packet option.""" - for option in dhcp_options: - if len(option) < 2 or option[0] != key: - continue - - value = option[1] - if value is None or key != HOSTNAME: - return value - - # hostname is unicode - try: - return value.decode() - except (AttributeError, UnicodeDecodeError): - return None +def _dhcp_options_as_dict( + dhcp_options: Iterable[tuple[str, int | bytes | None]] +) -> dict[str, str | int | bytes | None]: + """Extract data from packet options as a dict.""" + return {option[0]: option[1] for option in dhcp_options if len(option) >= 2} -def _format_mac(mac_address): +def _format_mac(mac_address: str) -> str: """Format a mac address for matching.""" return format_mac(mac_address).replace(":", "") -def _verify_l2socket_setup(cap_filter): +def _verify_l2socket_setup(cap_filter: str) -> None: """Create a socket using the scapy configured l2socket. Try to create the socket @@ -504,7 +534,7 @@ def _verify_l2socket_setup(cap_filter): conf.L2socket(filter=cap_filter) -def _verify_working_pcap(cap_filter): +def _verify_working_pcap(cap_filter: str) -> None: """Verify we can create a packet filter. If we cannot create a filter we will be listening for diff --git a/homeassistant/components/dht/__init__.py b/homeassistant/components/dht/__init__.py deleted file mode 100644 index 23aa2b9d9df..00000000000 --- a/homeassistant/components/dht/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The dht component.""" diff --git a/homeassistant/components/dht/manifest.json b/homeassistant/components/dht/manifest.json deleted file mode 100644 index 3eb3cfd202c..00000000000 --- a/homeassistant/components/dht/manifest.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "domain": "dht", - "name": "DHT Sensor", - "documentation": "https://www.home-assistant.io/integrations/dht", - "requirements": [ - "adafruit-circuitpython-dht==3.7.0", - "RPi.GPIO==0.7.1a4" - ], - "codeowners": [ - "@thegardenmonkey" - ], - "iot_class": "local_polling" -} \ No newline at end of file diff --git a/homeassistant/components/dht/sensor.py b/homeassistant/components/dht/sensor.py deleted file mode 100644 index ef5cc0f97f3..00000000000 --- a/homeassistant/components/dht/sensor.py +++ /dev/null @@ -1,202 +0,0 @@ -"""Support for Adafruit DHT temperature and humidity sensor.""" -from __future__ import annotations - -from datetime import timedelta -import logging - -import adafruit_dht -import board -import voluptuous as vol - -from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, - SensorDeviceClass, - SensorEntity, - SensorEntityDescription, - SensorStateClass, -) -from homeassistant.const import ( - CONF_MONITORED_CONDITIONS, - CONF_NAME, - CONF_PIN, - PERCENTAGE, - TEMP_CELSIUS, -) -from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from homeassistant.util import Throttle - -_LOGGER = logging.getLogger(__name__) - -CONF_SENSOR = "sensor" -CONF_HUMIDITY_OFFSET = "humidity_offset" -CONF_TEMPERATURE_OFFSET = "temperature_offset" - -DEFAULT_NAME = "DHT Sensor" - -# DHT11 is able to deliver data once per second, DHT22 once every two -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30) - -SENSOR_TEMPERATURE = "temperature" -SENSOR_HUMIDITY = "humidity" -SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( - SensorEntityDescription( - key=SENSOR_TEMPERATURE, - name="Temperature", - native_unit_of_measurement=TEMP_CELSIUS, - device_class=SensorDeviceClass.TEMPERATURE, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key=SENSOR_HUMIDITY, - name="Humidity", - native_unit_of_measurement=PERCENTAGE, - device_class=SensorDeviceClass.HUMIDITY, - state_class=SensorStateClass.MEASUREMENT, - ), -) - -SENSOR_KEYS: list[str] = [desc.key for desc in SENSOR_TYPES] - - -def validate_pin_input(value): - """Validate that the GPIO PIN is prefixed with a D.""" - try: - int(value) - return f"D{value}" - except ValueError: - return value.upper() - - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_SENSOR): cv.string, - vol.Required(CONF_PIN): vol.All(cv.string, validate_pin_input), - vol.Optional(CONF_MONITORED_CONDITIONS, default=[]): vol.All( - cv.ensure_list, [vol.In(SENSOR_KEYS)] - ), - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_TEMPERATURE_OFFSET, default=0): vol.All( - vol.Coerce(float), vol.Range(min=-100, max=100) - ), - vol.Optional(CONF_HUMIDITY_OFFSET, default=0): vol.All( - vol.Coerce(float), vol.Range(min=-100, max=100) - ), - } -) - - -def setup_platform( - hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the DHT sensor.""" - _LOGGER.warning( - "The DHT Sensor integration is deprecated and will be removed " - "in Home Assistant Core 2022.4; this integration is removed under " - "Architectural Decision Record 0019, more information can be found here: " - "https://github.com/home-assistant/architecture/blob/master/adr/0019-GPIO.md" - ) - - available_sensors = { - "AM2302": adafruit_dht.DHT22, - "DHT11": adafruit_dht.DHT11, - "DHT22": adafruit_dht.DHT22, - } - sensor = available_sensors.get(config[CONF_SENSOR]) - pin = config[CONF_PIN] - temperature_offset = config[CONF_TEMPERATURE_OFFSET] - humidity_offset = config[CONF_HUMIDITY_OFFSET] - name = config[CONF_NAME] - - if not sensor: - _LOGGER.error("DHT sensor type is not supported") - return - - data = DHTClient(sensor, pin, name) - - monitored_conditions = config[CONF_MONITORED_CONDITIONS] - entities = [ - DHTSensor(data, name, temperature_offset, humidity_offset, description) - for description in SENSOR_TYPES - if description.key in monitored_conditions - ] - - add_entities(entities, True) - - -class DHTSensor(SensorEntity): - """Implementation of the DHT sensor.""" - - def __init__( - self, - dht_client, - name, - temperature_offset, - humidity_offset, - description: SensorEntityDescription, - ): - """Initialize the sensor.""" - self.entity_description = description - self.dht_client = dht_client - self.temperature_offset = temperature_offset - self.humidity_offset = humidity_offset - - self._attr_name = f"{name} {description.name}" - - def update(self): - """Get the latest data from the DHT and updates the states.""" - self.dht_client.update() - temperature_offset = self.temperature_offset - humidity_offset = self.humidity_offset - data = self.dht_client.data - - sensor_type = self.entity_description.key - if sensor_type == SENSOR_TEMPERATURE and sensor_type in data: - temperature = data[SENSOR_TEMPERATURE] - _LOGGER.debug( - "Temperature %.1f \u00b0C + offset %.1f", - temperature, - temperature_offset, - ) - if -20 <= temperature < 80: - self._attr_native_value = round(temperature + temperature_offset, 1) - elif sensor_type == SENSOR_HUMIDITY and sensor_type in data: - humidity = data[SENSOR_HUMIDITY] - _LOGGER.debug("Humidity %.1f%% + offset %.1f", humidity, humidity_offset) - if 0 <= humidity <= 100: - self._attr_native_value = round(humidity + humidity_offset, 1) - - -class DHTClient: - """Get the latest data from the DHT sensor.""" - - def __init__(self, sensor, pin, name): - """Initialize the sensor.""" - self.sensor = sensor - self.pin = getattr(board, pin) - self.data = {} - self.name = name - - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self): - """Get the latest data the DHT sensor.""" - dht = self.sensor(self.pin) - try: - temperature = dht.temperature - humidity = dht.humidity - except RuntimeError: - _LOGGER.debug("Unexpected value from DHT sensor: %s", self.name) - except Exception: # pylint: disable=broad-except - _LOGGER.exception("Error updating DHT sensor: %s", self.name) - else: - if temperature: - self.data[SENSOR_TEMPERATURE] = temperature - if humidity: - self.data[SENSOR_HUMIDITY] = humidity - finally: - dht.exit() diff --git a/homeassistant/components/diagnostics/translations/fr.json b/homeassistant/components/diagnostics/translations/fr.json index f5936aced5b..cfa7ba1e755 100644 --- a/homeassistant/components/diagnostics/translations/fr.json +++ b/homeassistant/components/diagnostics/translations/fr.json @@ -1,3 +1,3 @@ { - "title": "Diagnostics" + "title": "Diagnostiques" } \ No newline at end of file diff --git a/homeassistant/components/diagnostics/util.py b/homeassistant/components/diagnostics/util.py index ba4f3d20f9a..8db2399fd53 100644 --- a/homeassistant/components/diagnostics/util.py +++ b/homeassistant/components/diagnostics/util.py @@ -8,7 +8,7 @@ from homeassistant.core import callback from .const import REDACTED -T = TypeVar("T") +_T = TypeVar("_T") @overload @@ -17,18 +17,18 @@ def async_redact_data(data: Mapping, to_redact: Iterable[Any]) -> dict: # type: @overload -def async_redact_data(data: T, to_redact: Iterable[Any]) -> T: +def async_redact_data(data: _T, to_redact: Iterable[Any]) -> _T: ... @callback -def async_redact_data(data: T, to_redact: Iterable[Any]) -> T: +def async_redact_data(data: _T, to_redact: Iterable[Any]) -> _T: """Redact sensitive data in a dict.""" if not isinstance(data, (Mapping, list)): return data if isinstance(data, list): - return cast(T, [async_redact_data(val, to_redact) for val in data]) + return cast(_T, [async_redact_data(val, to_redact) for val in data]) redacted = {**data} @@ -40,4 +40,4 @@ def async_redact_data(data: T, to_redact: Iterable[Any]) -> T: elif isinstance(value, list): redacted[key] = [async_redact_data(item, to_redact) for item in value] - return cast(T, redacted) + return cast(_T, redacted) diff --git a/homeassistant/components/dialogflow/translations/fr.json b/homeassistant/components/dialogflow/translations/fr.json index 302c0df7f05..661d86566b0 100644 --- a/homeassistant/components/dialogflow/translations/fr.json +++ b/homeassistant/components/dialogflow/translations/fr.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "cloud_not_connected": "Pas connect\u00e9 \u00e0 Home Assistant Cloud", + "cloud_not_connected": "Non connect\u00e9 \u00e0 Home Assistant Cloud.", "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible.", "webhook_not_internet_accessible": "Votre installation de Home Assistant doit \u00eatre accessible depuis internet pour recevoir des messages webhook." }, diff --git a/homeassistant/components/directv/strings.json b/homeassistant/components/directv/strings.json index 4384867dfa4..8ed52cd3632 100644 --- a/homeassistant/components/directv/strings.json +++ b/homeassistant/components/directv/strings.json @@ -12,7 +12,7 @@ } }, "error": { - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", diff --git a/homeassistant/components/discord/__init__.py b/homeassistant/components/discord/__init__.py index 67b9f1b39ba..ec41ac9d073 100644 --- a/homeassistant/components/discord/__init__.py +++ b/homeassistant/components/discord/__init__.py @@ -1 +1,57 @@ """The discord integration.""" +from aiohttp.client_exceptions import ClientConnectorError +import nextcord + +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.const import CONF_API_TOKEN, CONF_PLATFORM, Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady +from homeassistant.helpers import discovery +from homeassistant.helpers.typing import ConfigType + +from .const import DOMAIN + +PLATFORMS = [Platform.NOTIFY] + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up the Discord component.""" + # Iterate all entries for notify to only get Discord + if Platform.NOTIFY in config: + for entry in config[Platform.NOTIFY]: + if entry[CONF_PLATFORM] == DOMAIN: + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=entry + ) + ) + + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Discord from a config entry.""" + nextcord.VoiceClient.warn_nacl = False + discord_bot = nextcord.Client() + try: + await discord_bot.login(entry.data[CONF_API_TOKEN]) + except nextcord.LoginFailure as ex: + raise ConfigEntryAuthFailed("Invalid token given") from ex + except (ClientConnectorError, nextcord.HTTPException, nextcord.NotFound) as ex: + raise ConfigEntryNotReady("Failed to connect") from ex + finally: + await discord_bot.close() + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = entry.data + + hass.async_create_task( + discovery.async_load_platform( + hass, + Platform.NOTIFY, + DOMAIN, + hass.data[DOMAIN][entry.entry_id], + hass.data[DOMAIN], + ) + ) + + return True diff --git a/homeassistant/components/discord/config_flow.py b/homeassistant/components/discord/config_flow.py new file mode 100644 index 00000000000..8abd2a6be37 --- /dev/null +++ b/homeassistant/components/discord/config_flow.py @@ -0,0 +1,112 @@ +"""Config flow for Discord integration.""" +from __future__ import annotations + +import logging + +from aiohttp.client_exceptions import ClientConnectorError +import nextcord +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_API_TOKEN, CONF_NAME, CONF_TOKEN +from homeassistant.data_entry_flow import FlowResult + +from .const import DOMAIN, URL_PLACEHOLDER + +_LOGGER = logging.getLogger(__name__) + +CONFIG_SCHEMA = vol.Schema({vol.Required(CONF_API_TOKEN): str}) + + +class DiscordFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Discord.""" + + async def async_step_reauth(self, user_input: dict | None = None) -> FlowResult: + """Handle a reauthorization flow request.""" + if user_input is not None: + return await self.async_step_reauth_confirm() + + self._set_confirm_only() + return self.async_show_form(step_id="reauth") + + async def async_step_reauth_confirm( + self, user_input: dict[str, str] | None = None + ) -> FlowResult: + """Confirm reauth dialog.""" + errors = {} + + if user_input: + error, info = await _async_try_connect(user_input[CONF_API_TOKEN]) + if info and (entry := await self.async_set_unique_id(str(info.id))): + self.hass.config_entries.async_update_entry( + entry, data=entry.data | user_input + ) + await self.hass.config_entries.async_reload(entry.entry_id) + return self.async_abort(reason="reauth_successful") + if error: + errors["base"] = error + + user_input = user_input or {} + return self.async_show_form( + step_id="reauth_confirm", + data_schema=CONFIG_SCHEMA, + description_placeholders=URL_PLACEHOLDER, + errors=errors, + ) + + async def async_step_user( + self, user_input: dict[str, str] | None = None + ) -> FlowResult: + """Handle a flow initiated by the user.""" + errors = {} + + if user_input is not None: + error, info = await _async_try_connect(user_input[CONF_API_TOKEN]) + if error is not None: + errors["base"] = error + elif info is not None: + await self.async_set_unique_id(str(info.id)) + self._abort_if_unique_id_configured() + return self.async_create_entry( + title=info.name, + data=user_input | {CONF_NAME: user_input.get(CONF_NAME, info.name)}, + ) + + user_input = user_input or {} + return self.async_show_form( + step_id="user", + data_schema=CONFIG_SCHEMA, + description_placeholders=URL_PLACEHOLDER, + errors=errors, + ) + + async def async_step_import(self, import_config: dict[str, str]) -> FlowResult: + """Import a config entry from configuration.yaml.""" + _LOGGER.warning( + "Configuration of the Discord integration in YAML is deprecated and " + "will be removed in Home Assistant 2022.6; Your existing configuration " + "has been imported into the UI automatically and can be safely removed " + "from your configuration.yaml file" + ) + for entry in self._async_current_entries(): + if entry.data[CONF_API_TOKEN] == import_config[CONF_TOKEN]: + return self.async_abort(reason="already_configured") + import_config[CONF_API_TOKEN] = import_config.pop(CONF_TOKEN) + return await self.async_step_user(import_config) + + +async def _async_try_connect(token: str) -> tuple[str | None, nextcord.AppInfo | None]: + """Try connecting to Discord.""" + discord_bot = nextcord.Client() + try: + await discord_bot.login(token) + info = await discord_bot.application_info() + except nextcord.LoginFailure: + return "invalid_auth", None + except (ClientConnectorError, nextcord.HTTPException, nextcord.NotFound): + return "cannot_connect", None + except Exception as ex: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception: %s", ex) + return "unknown", None + await discord_bot.close() + return None, info diff --git a/homeassistant/components/discord/const.py b/homeassistant/components/discord/const.py new file mode 100644 index 00000000000..9f11c3e2d7a --- /dev/null +++ b/homeassistant/components/discord/const.py @@ -0,0 +1,10 @@ +"""Constants for the Discord integration.""" + +from typing import Final + +from homeassistant.const import CONF_URL + +DEFAULT_NAME = "Discord" +DOMAIN: Final = "discord" + +URL_PLACEHOLDER = {CONF_URL: "https://www.home-assistant.io/integrations/discord"} diff --git a/homeassistant/components/discord/manifest.json b/homeassistant/components/discord/manifest.json index 02b31a3aa99..b631c5fa7e7 100644 --- a/homeassistant/components/discord/manifest.json +++ b/homeassistant/components/discord/manifest.json @@ -1,9 +1,10 @@ { "domain": "discord", "name": "Discord", + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/discord", "requirements": ["nextcord==2.0.0a8"], - "codeowners": [], + "codeowners": ["@tkdrob"], "iot_class": "cloud_push", "loggers": ["discord"] } diff --git a/homeassistant/components/discord/notify.py b/homeassistant/components/discord/notify.py index 2e13b68225c..098857876a1 100644 --- a/homeassistant/components/discord/notify.py +++ b/homeassistant/components/discord/notify.py @@ -3,8 +3,10 @@ from __future__ import annotations import logging import os.path +from typing import Any, cast import nextcord +from nextcord.abc import Messageable import voluptuous as vol from homeassistant.components.notify import ( @@ -13,8 +15,10 @@ from homeassistant.components.notify import ( PLATFORM_SCHEMA, BaseNotificationService, ) -from homeassistant.const import CONF_TOKEN +from homeassistant.const import CONF_API_TOKEN, CONF_TOKEN +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType _LOGGER = logging.getLogger(__name__) @@ -29,30 +33,40 @@ ATTR_EMBED_THUMBNAIL = "thumbnail" ATTR_EMBED_URL = "url" ATTR_IMAGES = "images" +# Deprecated in Home Assistant 2022.4 PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Required(CONF_TOKEN): cv.string}) -def get_service(hass, config, discovery_info=None): +async def async_get_service( + hass: HomeAssistant, + config: ConfigType, + discovery_info: DiscoveryInfoType | None = None, +) -> DiscordNotificationService | None: """Get the Discord notification service.""" - token = config[CONF_TOKEN] - return DiscordNotificationService(hass, token) + if discovery_info is None: + return None + return DiscordNotificationService(hass, discovery_info[CONF_API_TOKEN]) class DiscordNotificationService(BaseNotificationService): """Implement the notification service for Discord.""" - def __init__(self, hass, token): + def __init__(self, hass: HomeAssistant, token: str) -> None: """Initialize the service.""" self.token = token self.hass = hass - def file_exists(self, filename): + def file_exists(self, filename: str) -> bool: """Check if a file exists on disk and is in authorized path.""" if not self.hass.config.is_allowed_path(filename): + _LOGGER.warning("Path not allowed: %s", filename) return False - return os.path.isfile(filename) + if not os.path.isfile(filename): + _LOGGER.warning("Not a file: %s", filename) + return False + return True - async def async_send_message(self, message, **kwargs): + async def async_send_message(self, message: str, **kwargs: Any) -> None: """Login to Discord, send message to channel(s) and log out.""" nextcord.VoiceClient.warn_nacl = False discord_bot = nextcord.Client() @@ -98,24 +112,24 @@ class DiscordNotificationService(BaseNotificationService): if image_exists: images.append(image) - else: - _LOGGER.warning("Image not found: %s", image) await discord_bot.login(self.token) try: for channelid in kwargs[ATTR_TARGET]: channelid = int(channelid) + # Must create new instances of File for each channel. + files = [nextcord.File(image) for image in images] if images else [] try: - channel = await discord_bot.fetch_channel(channelid) + channel = cast( + Messageable, await discord_bot.fetch_channel(channelid) + ) except nextcord.NotFound: try: channel = await discord_bot.fetch_user(channelid) except nextcord.NotFound: _LOGGER.warning("Channel not found for ID: %s", channelid) continue - # Must create new instances of File for each channel. - files = [nextcord.File(image) for image in images] if images else [] await channel.send(message, files=files, embeds=embeds) except (nextcord.HTTPException, nextcord.NotFound) as error: _LOGGER.warning("Communication error: %s", error) diff --git a/homeassistant/components/discord/strings.json b/homeassistant/components/discord/strings.json new file mode 100644 index 00000000000..07c8fa8bdb5 --- /dev/null +++ b/homeassistant/components/discord/strings.json @@ -0,0 +1,27 @@ +{ + "config": { + "step": { + "user": { + "description": "Refer to the documentation on getting your Discord bot key.\n\n{url}", + "data": { + "api_token": "[%key:common::config_flow::data::api_token%]" + } + }, + "reauth_confirm": { + "description": "Refer to the documentation on getting your Discord bot key.\n\n{url}", + "data": { + "api_token": "[%key:common::config_flow::data::api_token%]" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_service%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" + } + } +} diff --git a/homeassistant/components/discord/translations/en.json b/homeassistant/components/discord/translations/en.json new file mode 100644 index 00000000000..77e16e9312d --- /dev/null +++ b/homeassistant/components/discord/translations/en.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Service is already configured", + "reauth_successful": "Re-authentication was successful" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "api_token": "API Token" + }, + "description": "Refer to the documentation on getting your Discord bot key.\n\n{url}" + }, + "reauth_confirm": { + "data": { + "api_token": "API Token" + }, + "description": "Refer to the documentation on getting your Discord bot key.\n\n{url}" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/discovery/manifest.json b/homeassistant/components/discovery/manifest.json index 3e7d31fcb1c..6f97993c788 100644 --- a/homeassistant/components/discovery/manifest.json +++ b/homeassistant/components/discovery/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/discovery", "requirements": ["netdisco==3.0.0"], "after_dependencies": ["zeroconf"], - "codeowners": [], + "codeowners": ["@home-assistant/core"], "quality_scale": "internal", "loggers": ["netdisco"] } diff --git a/homeassistant/components/dlink/switch.py b/homeassistant/components/dlink/switch.py index 788127805c0..4d5d7b5c639 100644 --- a/homeassistant/components/dlink/switch.py +++ b/homeassistant/components/dlink/switch.py @@ -101,14 +101,6 @@ class SmartPlugSwitch(SwitchEntity): return attrs - @property - def current_power_w(self): - """Return the current power usage in Watt.""" - try: - return float(self.data.current_consumption) - except (ValueError, TypeError): - return None - @property def is_on(self): """Return true if switch is on.""" diff --git a/homeassistant/components/dlna_dmr/data.py b/homeassistant/components/dlna_dmr/data.py index 07046ba4acc..1a1a28d758c 100644 --- a/homeassistant/components/dlna_dmr/data.py +++ b/homeassistant/components/dlna_dmr/data.py @@ -5,8 +5,10 @@ import asyncio from collections import defaultdict from typing import NamedTuple, cast -from async_upnp_client import UpnpEventHandler, UpnpFactory, UpnpRequester from async_upnp_client.aiohttp import AiohttpNotifyServer, AiohttpSessionRequester +from async_upnp_client.client import UpnpRequester +from async_upnp_client.client_factory import UpnpFactory +from async_upnp_client.event_handler import UpnpEventHandler from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant @@ -46,7 +48,9 @@ class DlnaDmrData: """Clean up resources when Home Assistant is stopped.""" LOGGER.debug("Cleaning resources in DlnaDmrData") async with self.lock: - tasks = (server.stop_server() for server in self.event_notifiers.values()) + tasks = ( + server.async_stop_server() for server in self.event_notifiers.values() + ) asyncio.gather(*tasks) self.event_notifiers = {} self.event_notifier_refs = defaultdict(int) @@ -76,14 +80,14 @@ class DlnaDmrData: return self.event_notifiers[listen_addr].event_handler # Start event handler + source = (listen_addr.host or "0.0.0.0", listen_addr.port) server = AiohttpNotifyServer( requester=self.requester, - listen_port=listen_addr.port, - listen_host=listen_addr.host, + source=source, callback_url=listen_addr.callback_url, loop=hass.loop, ) - await server.start_server() + await server.async_start_server() LOGGER.debug("Started event handler at %s", server.callback_url) self.event_notifiers[listen_addr] = server @@ -103,7 +107,7 @@ class DlnaDmrData: # Shutdown the server when it has no more users if self.event_notifier_refs[listen_addr] == 0: server = self.event_notifiers.pop(listen_addr) - await server.stop_server() + await server.async_stop_server() # Remove the cleanup listener when there's nothing left to cleanup if not self.event_notifiers: diff --git a/homeassistant/components/dlna_dmr/manifest.json b/homeassistant/components/dlna_dmr/manifest.json index 4001fc9dddc..ccd0ca6e922 100644 --- a/homeassistant/components/dlna_dmr/manifest.json +++ b/homeassistant/components/dlna_dmr/manifest.json @@ -3,7 +3,7 @@ "name": "DLNA Digital Media Renderer", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/dlna_dmr", - "requirements": ["async-upnp-client==0.23.5"], + "requirements": ["async-upnp-client==0.27.0"], "dependencies": ["ssdp"], "after_dependencies": ["media_source"], "ssdp": [ diff --git a/homeassistant/components/dlna_dmr/media_player.py b/homeassistant/components/dlna_dmr/media_player.py index 265c6e9dde6..d4d994a779b 100644 --- a/homeassistant/components/dlna_dmr/media_player.py +++ b/homeassistant/components/dlna_dmr/media_player.py @@ -8,7 +8,7 @@ from datetime import datetime, timedelta import functools from typing import Any, TypeVar -from async_upnp_client import UpnpService, UpnpStateVariable +from async_upnp_client.client import UpnpService, UpnpStateVariable from async_upnp_client.const import NotificationSubType from async_upnp_client.exceptions import UpnpError, UpnpResponseError from async_upnp_client.profiles.dlna import DmrDevice, PlayMode, TransportState @@ -77,8 +77,6 @@ _T = TypeVar("_T", bound="DlnaDmrEntity") _R = TypeVar("_R") _P = ParamSpec("_P") -Func = TypeVar("Func", bound=Callable[..., Any]) - def catch_request_errors( func: Callable[Concatenate[_T, _P], Awaitable[_R]] # type: ignore[misc] diff --git a/homeassistant/components/dlna_dmr/translations/fr.json b/homeassistant/components/dlna_dmr/translations/fr.json index f7a1b9cd71c..6554a3994d6 100644 --- a/homeassistant/components/dlna_dmr/translations/fr.json +++ b/homeassistant/components/dlna_dmr/translations/fr.json @@ -4,21 +4,21 @@ "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", "alternative_integration": "L'appareil est mieux pris en charge par une autre int\u00e9gration", "cannot_connect": "\u00c9chec de connexion", - "could_not_connect": "\u00c9chec de la connexion au p\u00e9riph\u00e9rique DLNA", - "discovery_error": "\u00c9chec de la d\u00e9couverte d'un p\u00e9riph\u00e9rique DLNA correspondant", + "could_not_connect": "\u00c9chec de la connexion \u00e0 l'appareil DLNA", + "discovery_error": "\u00c9chec de la d\u00e9couverte d'un appareil DLNA correspondant", "incomplete_config": "Il manque une variable requise dans la configuration", "non_unique_id": "Plusieurs appareils trouv\u00e9s avec le m\u00eame identifiant unique", "not_dmr": "L'appareil n'est pas un moteur de rendu multim\u00e9dia num\u00e9rique pris en charge" }, "error": { "cannot_connect": "\u00c9chec de connexion", - "could_not_connect": "\u00c9chec de la connexion au p\u00e9riph\u00e9rique DLNA", + "could_not_connect": "\u00c9chec de la connexion \u00e0 l'appareil DLNA", "not_dmr": "L'appareil n'est pas un moteur de rendu multim\u00e9dia num\u00e9rique pris en charge" }, "flow_title": "{name}", "step": { "confirm": { - "description": "Voulez-vous commencer la configuration ?" + "description": "Voulez-vous commencer la configuration\u00a0?" }, "import_turn_on": { "description": "Veuillez allumer l'appareil et cliquer sur soumettre pour continuer la migration" @@ -36,13 +36,13 @@ "url": "URL" }, "description": "Choisissez un appareil \u00e0 configurer ou laissez vide pour saisir une URL", - "title": "P\u00e9riph\u00e9riques DLNA DMR d\u00e9couverts" + "title": "Appareils DLNA DMR d\u00e9couverts" } } }, "options": { "error": { - "invalid_url": "URL invalide" + "invalid_url": "URL non valide" }, "step": { "init": { diff --git a/homeassistant/components/dlna_dmr/translations/zh-Hant.json b/homeassistant/components/dlna_dmr/translations/zh-Hant.json index 406b23b573f..f085767565d 100644 --- a/homeassistant/components/dlna_dmr/translations/zh-Hant.json +++ b/homeassistant/components/dlna_dmr/translations/zh-Hant.json @@ -5,7 +5,7 @@ "alternative_integration": "\u4f7f\u7528\u5176\u4ed6\u6574\u5408\u4ee5\u53d6\u5f97\u66f4\u4f73\u7684\u88dd\u7f6e\u652f\u63f4", "cannot_connect": "\u9023\u7dda\u5931\u6557", "could_not_connect": "DLNA \u88dd\u7f6e\u9023\u7dda\u5931\u6557\u3002", - "discovery_error": "DLNA \u88dd\u7f6e\u63a2\u7d22\u5931\u6557", + "discovery_error": "DLNA \u88dd\u7f6e\u641c\u7d22\u5931\u6557", "incomplete_config": "\u6240\u7f3a\u5c11\u7684\u8a2d\u5b9a\u70ba\u5fc5\u9808\u8b8a\u6578", "non_unique_id": "\u627e\u5230\u591a\u7d44\u88dd\u7f6e\u4f7f\u7528\u4e86\u76f8\u540c\u552f\u4e00 ID", "not_dmr": "\u88dd\u7f6e\u70ba\u975e\u652f\u63f4 Digital Media Renderer" @@ -36,7 +36,7 @@ "url": "\u7db2\u5740" }, "description": "\u9078\u64c7\u88dd\u7f6e\u9032\u884c\u8a2d\u5b9a\u6216\u4fdd\u7559\u7a7a\u767d\u4ee5\u8f38\u5165 URL", - "title": "\u5df2\u63a2\u7d22\u5230\u7684 DLNA DMR \u88dd\u7f6e" + "title": "\u5df2\u767c\u73fe\u7684 DLNA DMR \u88dd\u7f6e" } } }, diff --git a/homeassistant/components/dlna_dms/__init__.py b/homeassistant/components/dlna_dms/__init__.py index b09547e07c8..5cd6321a5df 100644 --- a/homeassistant/components/dlna_dms/__init__.py +++ b/homeassistant/components/dlna_dms/__init__.py @@ -8,14 +8,22 @@ from __future__ import annotations from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from .const import LOGGER +from .const import CONF_SOURCE_ID, LOGGER from .dms import get_domain_data +from .util import generate_source_id async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up DLNA DMS device from a config entry.""" LOGGER.debug("Setting up config entry: %s", entry.unique_id) + # Soft-migrate entry if it's missing data keys + if CONF_SOURCE_ID not in entry.data: + LOGGER.debug("Adding CONF_SOURCE_ID to entry %s", entry.data) + data = dict(entry.data) + data[CONF_SOURCE_ID] = generate_source_id(hass, entry.title) + hass.config_entries.async_update_entry(entry, data=data) + # Forward setup to this domain's data manager return await get_domain_data(hass).async_setup_entry(entry) diff --git a/homeassistant/components/dlna_dms/config_flow.py b/homeassistant/components/dlna_dms/config_flow.py index 7ae3a104fc1..bf310f7b234 100644 --- a/homeassistant/components/dlna_dms/config_flow.py +++ b/homeassistant/components/dlna_dms/config_flow.py @@ -13,17 +13,13 @@ from homeassistant import config_entries from homeassistant.components import ssdp from homeassistant.const import CONF_DEVICE_ID, CONF_HOST, CONF_URL from homeassistant.data_entry_flow import AbortFlow, FlowResult -from homeassistant.exceptions import IntegrationError -from .const import DEFAULT_NAME, DOMAIN +from .const import CONF_SOURCE_ID, CONFIG_VERSION, DEFAULT_NAME, DOMAIN +from .util import generate_source_id LOGGER = logging.getLogger(__name__) -class ConnectError(IntegrationError): - """Error occurred when trying to connect to a device.""" - - class DlnaDmsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a DLNA DMS config flow. @@ -32,7 +28,7 @@ class DlnaDmsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): the DMS is an embedded device. """ - VERSION = 1 + VERSION = CONFIG_VERSION def __init__(self) -> None: """Initialize flow.""" @@ -50,7 +46,7 @@ class DlnaDmsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if user_input is not None and (host := user_input.get(CONF_HOST)): # User has chosen a device discovery = self._discoveries[host] - await self._async_parse_discovery(discovery) + await self._async_parse_discovery(discovery, raise_on_progress=False) return self._create_entry() if not (discoveries := await self._async_get_discoveries()): @@ -100,8 +96,6 @@ class DlnaDmsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Allow the user to confirm adding the device.""" - LOGGER.debug("async_step_confirm: %s", user_input) - if user_input is not None: return self._create_entry() @@ -111,17 +105,24 @@ class DlnaDmsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): def _create_entry(self) -> FlowResult: """Create a config entry, assuming all required information is now known.""" LOGGER.debug( - "_async_create_entry: location: %s, USN: %s", self._location, self._usn + "_create_entry: name: %s, location: %s, USN: %s", + self._name, + self._location, + self._usn, ) assert self._name assert self._location assert self._usn - data = {CONF_URL: self._location, CONF_DEVICE_ID: self._usn} + data = { + CONF_URL: self._location, + CONF_DEVICE_ID: self._usn, + CONF_SOURCE_ID: generate_source_id(self.hass, self._name), + } return self.async_create_entry(title=self._name, data=data) async def _async_parse_discovery( - self, discovery_info: ssdp.SsdpServiceInfo + self, discovery_info: ssdp.SsdpServiceInfo, raise_on_progress: bool = True ) -> None: """Get required details from an SSDP discovery. @@ -140,7 +141,7 @@ class DlnaDmsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self._location = discovery_info.ssdp_location self._usn = discovery_info.ssdp_usn - await self.async_set_unique_id(self._usn) + await self.async_set_unique_id(self._usn, raise_on_progress=raise_on_progress) # Abort if already configured, but update the last-known location self._abort_if_unique_id_configured( @@ -155,8 +156,6 @@ class DlnaDmsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def _async_get_discoveries(self) -> list[ssdp.SsdpServiceInfo]: """Get list of unconfigured DLNA devices discovered by SSDP.""" - LOGGER.debug("_get_discoveries") - # Get all compatible devices from ssdp's cache discoveries: list[ssdp.SsdpServiceInfo] = [] for udn_st in DmsDevice.DEVICE_TYPES: diff --git a/homeassistant/components/dlna_dms/const.py b/homeassistant/components/dlna_dms/const.py index 8c260272d5f..5d1c887fd49 100644 --- a/homeassistant/components/dlna_dms/const.py +++ b/homeassistant/components/dlna_dms/const.py @@ -12,6 +12,9 @@ LOGGER = logging.getLogger(__package__) DOMAIN: Final = "dlna_dms" DEFAULT_NAME: Final = "DLNA Media Server" +CONF_SOURCE_ID: Final = "source_id" +CONFIG_VERSION: Final = 1 + SOURCE_SEP: Final = "/" ROOT_OBJECT_ID: Final = "0" PATH_SEP: Final = "/" diff --git a/homeassistant/components/dlna_dms/dms.py b/homeassistant/components/dlna_dms/dms.py index d7ee08f85f8..d47f480132b 100644 --- a/homeassistant/components/dlna_dms/dms.py +++ b/homeassistant/components/dlna_dms/dms.py @@ -7,8 +7,9 @@ from dataclasses import dataclass import functools from typing import Any, TypeVar, cast -from async_upnp_client import UpnpEventHandler, UpnpFactory, UpnpRequester from async_upnp_client.aiohttp import AiohttpSessionRequester +from async_upnp_client.client import UpnpRequester +from async_upnp_client.client_factory import UpnpFactory from async_upnp_client.const import NotificationSubType from async_upnp_client.exceptions import UpnpActionError, UpnpConnectionError, UpnpError from async_upnp_client.profiles.dlna import ContentDirectoryErrorCode, DmsDevice @@ -24,9 +25,9 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_DEVICE_ID, CONF_URL from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import aiohttp_client -from homeassistant.util import slugify from .const import ( + CONF_SOURCE_ID, DLNA_BROWSE_FILTER, DLNA_PATH_FILTER, DLNA_RESOLVE_FILTER, @@ -42,17 +43,15 @@ from .const import ( ) _DlnaDmsDeviceMethod = TypeVar("_DlnaDmsDeviceMethod", bound="DmsDeviceSource") -_RetType = TypeVar("_RetType") +_R = TypeVar("_R") class DlnaDmsData: """Storage class for domain global data.""" hass: HomeAssistant - lock: asyncio.Lock requester: UpnpRequester upnp_factory: UpnpFactory - event_handler: UpnpEventHandler devices: dict[str, DmsDeviceSource] # Indexed by config_entry.unique_id sources: dict[str, DmsDeviceSource] # Indexed by source_id @@ -62,69 +61,32 @@ class DlnaDmsData: ) -> None: """Initialize global data.""" self.hass = hass - self.lock = asyncio.Lock() session = aiohttp_client.async_get_clientsession(hass, verify_ssl=False) self.requester = AiohttpSessionRequester(session, with_sleep=True) self.upnp_factory = UpnpFactory(self.requester, non_strict=True) - # NOTE: event_handler is not actually used, and is only created to - # satisfy the DmsDevice.__init__ signature - self.event_handler = UpnpEventHandler("", self.requester) self.devices = {} self.sources = {} async def async_setup_entry(self, config_entry: ConfigEntry) -> bool: """Create a DMS device connection from a config entry.""" assert config_entry.unique_id - async with self.lock: - source_id = self._generate_source_id(config_entry.title) - device = DmsDeviceSource(self.hass, config_entry, source_id) - self.devices[config_entry.unique_id] = device - self.sources[device.source_id] = device - - # Update the device when the associated config entry is modified - config_entry.async_on_unload( - config_entry.add_update_listener(self.async_update_entry) - ) - + device = DmsDeviceSource(self.hass, config_entry) + self.devices[config_entry.unique_id] = device + # source_id must be unique, which generate_source_id should guarantee. + # Ensure this is the case, for debugging purposes. + assert device.source_id not in self.sources + self.sources[device.source_id] = device await device.async_added_to_hass() return True async def async_unload_entry(self, config_entry: ConfigEntry) -> bool: """Unload a config entry and disconnect the corresponding DMS device.""" assert config_entry.unique_id - async with self.lock: - device = self.devices.pop(config_entry.unique_id) - del self.sources[device.source_id] + device = self.devices.pop(config_entry.unique_id) + del self.sources[device.source_id] await device.async_will_remove_from_hass() return True - async def async_update_entry( - self, hass: HomeAssistant, config_entry: ConfigEntry - ) -> None: - """Update a DMS device when the config entry changes.""" - assert config_entry.unique_id - async with self.lock: - device = self.devices[config_entry.unique_id] - # Update the source_id to match the new name - del self.sources[device.source_id] - device.source_id = self._generate_source_id(config_entry.title) - self.sources[device.source_id] = device - - def _generate_source_id(self, name: str) -> str: - """Generate a unique source ID. - - Caller should hold self.lock when calling this method. - """ - source_id_base = slugify(name) - if source_id_base not in self.sources: - return source_id_base - - tries = 1 - while (suggested_source_id := f"{source_id_base}_{tries}") in self.sources: - tries += 1 - - return suggested_source_id - @callback def get_domain_data(hass: HomeAssistant) -> DlnaDmsData: @@ -162,12 +124,12 @@ class ActionError(DlnaDmsDeviceError): def catch_request_errors( - func: Callable[[_DlnaDmsDeviceMethod, str], Coroutine[Any, Any, _RetType]] -) -> Callable[[_DlnaDmsDeviceMethod, str], Coroutine[Any, Any, _RetType]]: + func: Callable[[_DlnaDmsDeviceMethod, str], Coroutine[Any, Any, _R]] +) -> Callable[[_DlnaDmsDeviceMethod, str], Coroutine[Any, Any, _R]]: """Catch UpnpError errors.""" @functools.wraps(func) - async def wrapper(self: _DlnaDmsDeviceMethod, req_param: str) -> _RetType: + async def wrapper(self: _DlnaDmsDeviceMethod, req_param: str) -> _R: """Catch UpnpError errors and check availability before and after request.""" if not self.available: LOGGER.warning("Device disappeared when trying to call %s", func.__name__) @@ -200,12 +162,6 @@ def catch_request_errors( class DmsDeviceSource: """DMS Device wrapper, providing media files as a media_source.""" - hass: HomeAssistant - config_entry: ConfigEntry - - # Unique slug used for media-source URIs - source_id: str - # Last known URL for the device, used when adding this wrapper to hass to # try to connect before SSDP has rediscovered it, or when SSDP discovery # fails. @@ -220,13 +176,10 @@ class DmsDeviceSource: # Track BOOTID in SSDP advertisements for device changes _bootid: int | None = None - def __init__( - self, hass: HomeAssistant, config_entry: ConfigEntry, source_id: str - ) -> None: + def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None: """Initialize a DMS Source.""" self.hass = hass self.config_entry = config_entry - self.source_id = source_id self.location = self.config_entry.data[CONF_URL] self._device_lock = asyncio.Lock() @@ -334,16 +287,13 @@ class DmsDeviceSource: async def device_connect(self) -> None: """Connect to the device now that it's available.""" LOGGER.debug("Connecting to device at %s", self.location) + assert self.location async with self._device_lock: if self._device: LOGGER.debug("Trying to connect when device already connected") return - if not self.location: - LOGGER.debug("Not connecting because location is not known") - return - domain_data = get_domain_data(self.hass) # Connect to the base UPNP device @@ -352,7 +302,7 @@ class DmsDeviceSource: ) # Create profile wrapper - self._device = DmsDevice(upnp_device, domain_data.event_handler) + self._device = DmsDevice(upnp_device, event_handler=None) # Update state variables. We don't care if they change, so this is # only done once, here. @@ -394,13 +344,15 @@ class DmsDeviceSource: """Return a name for the media server.""" return self.config_entry.title + @property + def source_id(self) -> str: + """Return a unique ID (slug) for this source for people to use in URLs.""" + return self.config_entry.data[CONF_SOURCE_ID] + @property def icon(self) -> str | None: """Return an URL to an icon for the media server.""" - if not self._device: - return None - - return self._device.icon + return self._device.icon if self._device else None # MediaSource methods @@ -409,6 +361,8 @@ class DmsDeviceSource: LOGGER.debug("async_resolve_media(%s)", identifier) action, parameters = _parse_identifier(identifier) + assert action is not None, f"Invalid identifier: {identifier}" + if action is Action.OBJECT: return await self.async_resolve_object(parameters) @@ -416,11 +370,8 @@ class DmsDeviceSource: object_id = await self.async_resolve_path(parameters) return await self.async_resolve_object(object_id) - if action is Action.SEARCH: - return await self.async_resolve_search(parameters) - - LOGGER.debug("Invalid identifier %s", identifier) - raise Unresolvable(f"Invalid identifier {identifier}") + assert action is Action.SEARCH + return await self.async_resolve_search(parameters) async def async_browse_media(self, identifier: str | None) -> BrowseMediaSource: """Browse media.""" @@ -575,9 +526,6 @@ class DmsDeviceSource: children=children, ) - if media_source.children: - media_source.calculate_children_class() - return media_source def _didl_to_play_media(self, item: didl_lite.DidlObject) -> DidlPlayMedia: @@ -646,9 +594,6 @@ class DmsDeviceSource: thumbnail=self._didl_thumbnail_url(item), ) - if media_source.children: - media_source.calculate_children_class() - return media_source def _didl_thumbnail_url(self, item: didl_lite.DidlObject) -> str | None: @@ -675,8 +620,7 @@ class DmsDeviceSource: """Make an identifier for BrowseMediaSource.""" return f"{self.source_id}/{action}{object_id}" - @property # type: ignore - @functools.cache + @functools.cached_property def _sort_criteria(self) -> list[str]: """Return criteria to be used for sorting results. diff --git a/homeassistant/components/dlna_dms/manifest.json b/homeassistant/components/dlna_dms/manifest.json index 84cfc2e69fd..dffd0b9b654 100644 --- a/homeassistant/components/dlna_dms/manifest.json +++ b/homeassistant/components/dlna_dms/manifest.json @@ -3,7 +3,7 @@ "name": "DLNA Digital Media Server", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/dlna_dms", - "requirements": ["async-upnp-client==0.23.5"], + "requirements": ["async-upnp-client==0.27.0"], "dependencies": ["ssdp"], "after_dependencies": ["media_source"], "ssdp": [ diff --git a/homeassistant/components/dlna_dms/translations/bg.json b/homeassistant/components/dlna_dms/translations/bg.json new file mode 100644 index 00000000000..4356e0973c1 --- /dev/null +++ b/homeassistant/components/dlna_dms/translations/bg.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u0437\u0430\u043f\u043e\u0447\u043d\u0435\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u0432\u0430\u043d\u0435\u0442\u043e?" + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlna_dms/translations/es.json b/homeassistant/components/dlna_dms/translations/es.json new file mode 100644 index 00000000000..b4bc90a666a --- /dev/null +++ b/homeassistant/components/dlna_dms/translations/es.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "no_devices_found": "No se han encontrado dispositivos en la red" + }, + "step": { + "user": { + "description": "Escoge un dispositivo a configurar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlna_dms/translations/fr.json b/homeassistant/components/dlna_dms/translations/fr.json new file mode 100644 index 00000000000..3c4fe096ea9 --- /dev/null +++ b/homeassistant/components/dlna_dms/translations/fr.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours", + "bad_ssdp": "Il manque une valeur requise dans les donn\u00e9es SSDP", + "no_devices_found": "Aucun appareil trouv\u00e9 sur le r\u00e9seau", + "not_dms": "L'appareil n'est pas un serveur multim\u00e9dia pris en charge" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "description": "Voulez-vous commencer la configuration\u00a0?" + }, + "user": { + "data": { + "host": "H\u00f4te" + }, + "description": "S\u00e9lectionnez l'appareil \u00e0 configurer", + "title": "Appareils DNLA DMA d\u00e9couverts" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlna_dms/translations/he.json b/homeassistant/components/dlna_dms/translations/he.json new file mode 100644 index 00000000000..cfe995b8921 --- /dev/null +++ b/homeassistant/components/dlna_dms/translations/he.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05ea\u05d7\u05d9\u05dc \u05d1\u05d4\u05d2\u05d3\u05e8\u05d4?" + }, + "user": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlna_dms/translations/id.json b/homeassistant/components/dlna_dms/translations/id.json new file mode 100644 index 00000000000..020045c0bcd --- /dev/null +++ b/homeassistant/components/dlna_dms/translations/id.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "already_in_progress": "Alur konfigurasi sedang berlangsung", + "bad_ssdp": "Data SSDP tidak memiliki nilai yang diperlukan", + "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan", + "not_dms": "Perangkat bukan Server Media yang didukung" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "description": "Ingin memulai penyiapan?" + }, + "user": { + "data": { + "host": "Host" + }, + "description": "Pilih perangkat untuk dikonfigurasi", + "title": "Perangkat DLNA DMA yang ditemukan" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlna_dms/translations/it.json b/homeassistant/components/dlna_dms/translations/it.json new file mode 100644 index 00000000000..a8c0d50c0cf --- /dev/null +++ b/homeassistant/components/dlna_dms/translations/it.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso", + "bad_ssdp": "Ai dati SSDP manca un valore richiesto", + "no_devices_found": "Nessun dispositivo trovato sulla rete", + "not_dms": "Il dispositivo non \u00e8 un server multimediale supportato" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "description": "Vuoi iniziare la configurazione?" + }, + "user": { + "data": { + "host": "Host" + }, + "description": "Seleziona un dispositivo da configurare", + "title": "Dispositivi DLNA DMA rilevati" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlna_dms/translations/nl.json b/homeassistant/components/dlna_dms/translations/nl.json new file mode 100644 index 00000000000..d480118b76a --- /dev/null +++ b/homeassistant/components/dlna_dms/translations/nl.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd", + "already_in_progress": "De configuratiestroom is al aan de gang", + "bad_ssdp": "SSDP-gegevens missen een vereiste waarde", + "no_devices_found": "Geen apparaten gevonden op het netwerk", + "not_dms": "Apparaat is geen ondersteunde mediaserver" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "description": "Wilt u beginnen met instellen?" + }, + "user": { + "data": { + "host": "Host" + }, + "description": "Kies een apparaat om te configureren", + "title": "Ontdekte DLNA DMA-apparaten" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlna_dms/translations/pt-BR.json b/homeassistant/components/dlna_dms/translations/pt-BR.json index 125a31fe9b5..5672318a2c8 100644 --- a/homeassistant/components/dlna_dms/translations/pt-BR.json +++ b/homeassistant/components/dlna_dms/translations/pt-BR.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", - "already_in_progress": "A configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "O fluxo de configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", "bad_ssdp": "Falta um valor obrigat\u00f3rio nos dados SSDP", "no_devices_found": "Nenhum dispositivo encontrado na rede", "not_dms": "O dispositivo n\u00e3o \u00e9 um servidor de m\u00eddia compat\u00edvel" @@ -14,7 +14,7 @@ }, "user": { "data": { - "host": "Host" + "host": "Nome do host" }, "description": "Escolha um dispositivo para configurar", "title": "Dispositivos DLNA DMA descobertos" diff --git a/homeassistant/components/dlna_dms/translations/tr.json b/homeassistant/components/dlna_dms/translations/tr.json new file mode 100644 index 00000000000..dc8a5bf02e8 --- /dev/null +++ b/homeassistant/components/dlna_dms/translations/tr.json @@ -0,0 +1,24 @@ +{ + "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", + "bad_ssdp": "SSDP verilerinde gerekli bir de\u011fer eksik", + "no_devices_found": "A\u011fda cihaz bulunamad\u0131", + "not_dms": "Cihaz desteklenen bir Medya Sunucusu de\u011fil" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "description": "Kuruluma ba\u015flamak ister misiniz?" + }, + "user": { + "data": { + "host": "Sunucu" + }, + "description": "Yap\u0131land\u0131rmak i\u00e7in bir cihaz se\u00e7in", + "title": "Ke\u015ffedilen DLNA DMA cihazlar\u0131" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlna_dms/translations/zh-Hant.json b/homeassistant/components/dlna_dms/translations/zh-Hant.json index 2f06619c006..404b9b29b9a 100644 --- a/homeassistant/components/dlna_dms/translations/zh-Hant.json +++ b/homeassistant/components/dlna_dms/translations/zh-Hant.json @@ -16,8 +16,8 @@ "data": { "host": "\u4e3b\u6a5f\u7aef" }, - "description": "\u9078\u64c7\u88dd\u7f6e\u4ee5\u8a2d\u5b9a", - "title": "\u5df2\u63a2\u7d22\u5230\u7684 DLNA DMA \u88dd\u7f6e" + "description": "\u9078\u64c7\u6240\u8981\u8a2d\u5b9a\u7684\u88dd\u7f6e", + "title": "\u5df2\u767c\u73fe\u7684 DLNA DMA \u88dd\u7f6e" } } } diff --git a/homeassistant/components/dlna_dms/util.py b/homeassistant/components/dlna_dms/util.py new file mode 100644 index 00000000000..74c5bd2e01b --- /dev/null +++ b/homeassistant/components/dlna_dms/util.py @@ -0,0 +1,27 @@ +"""Small utility functions for the dlna_dms integration.""" +from __future__ import annotations + +from homeassistant.core import HomeAssistant +from homeassistant.util import slugify + +from .const import CONF_SOURCE_ID, DOMAIN + + +def generate_source_id(hass: HomeAssistant, name: str) -> str: + """Generate a unique source ID.""" + other_entries = hass.config_entries.async_entries(DOMAIN) + other_source_ids: set[str] = { + other_source_id + for entry in other_entries + if (other_source_id := entry.data.get(CONF_SOURCE_ID)) + } + + source_id_base = slugify(name) + if source_id_base not in other_source_ids: + return source_id_base + + tries = 1 + while (suggested_source_id := f"{source_id_base}_{tries}") in other_source_ids: + tries += 1 + + return suggested_source_id diff --git a/homeassistant/components/dnsip/__init__.py b/homeassistant/components/dnsip/__init__.py index 2f20f18580e..f679fb4ad30 100644 --- a/homeassistant/components/dnsip/__init__.py +++ b/homeassistant/components/dnsip/__init__.py @@ -1,15 +1,11 @@ """The dnsip component.""" from __future__ import annotations -import logging - from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from .const import PLATFORMS -_LOGGER = logging.getLogger(__name__) - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up DNS IP from a config entry.""" diff --git a/homeassistant/components/dnsip/config_flow.py b/homeassistant/components/dnsip/config_flow.py index 2db0034b697..a5b51f06a45 100644 --- a/homeassistant/components/dnsip/config_flow.py +++ b/homeassistant/components/dnsip/config_flow.py @@ -82,14 +82,6 @@ class DnsIPConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Return Option handler.""" return DnsIPOptionsFlowHandler(config_entry) - async def async_step_import(self, config: dict[str, Any]) -> FlowResult: - """Import a configuration from config.yaml.""" - - hostname = config.get(CONF_HOSTNAME, DEFAULT_HOSTNAME) - self._async_abort_entries_match({CONF_HOSTNAME: hostname}) - config[CONF_HOSTNAME] = hostname - return await self.async_step_user(user_input=config) - async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: diff --git a/homeassistant/components/dnsip/sensor.py b/homeassistant/components/dnsip/sensor.py index 7dfc3aaa544..a770afe388d 100644 --- a/homeassistant/components/dnsip/sensor.py +++ b/homeassistant/components/dnsip/sensor.py @@ -6,20 +6,14 @@ import logging import aiodns from aiodns.error import DNSError -import voluptuous as vol -from homeassistant.components.sensor import ( - PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, - SensorEntity, -) -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.components.sensor import SensorEntity +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .const import ( CONF_HOSTNAME, @@ -27,10 +21,6 @@ from .const import ( CONF_IPV6, CONF_RESOLVER, CONF_RESOLVER_IPV6, - DEFAULT_HOSTNAME, - DEFAULT_IPV6, - DEFAULT_RESOLVER, - DEFAULT_RESOLVER_IPV6, DOMAIN, ) @@ -38,38 +28,6 @@ _LOGGER = logging.getLogger(__name__) SCAN_INTERVAL = timedelta(seconds=120) -PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_HOSTNAME, default=DEFAULT_HOSTNAME): cv.string, - vol.Optional(CONF_RESOLVER, default=DEFAULT_RESOLVER): cv.string, - vol.Optional(CONF_RESOLVER_IPV6, default=DEFAULT_RESOLVER_IPV6): cv.string, - vol.Optional(CONF_IPV6, default=DEFAULT_IPV6): cv.boolean, - } -) - - -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_devices: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the DNS IP sensor.""" - _LOGGER.warning( - "Configuration of the DNS IP platform in YAML is deprecated and will be " - "removed in Home Assistant 2022.4; Your existing configuration " - "has been imported into the UI automatically and can be safely removed " - "from your configuration.yaml file" - ) - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data=config, - ) - ) - async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback diff --git a/homeassistant/components/dnsip/strings.json b/homeassistant/components/dnsip/strings.json index cd95c9db27f..41ce6c5aeb7 100644 --- a/homeassistant/components/dnsip/strings.json +++ b/homeassistant/components/dnsip/strings.json @@ -1,29 +1,29 @@ { - "config": { - "step": { - "user": { - "data": { - "hostname": "The hostname for which to perform the DNS query", - "resolver": "Resolver for IPV4 lookup", - "resolver_ipv6": "Resolver for IPV6 lookup" - } - } - }, - "error": { - "invalid_hostname": "Invalid hostname" + "config": { + "step": { + "user": { + "data": { + "hostname": "The hostname for which to perform the DNS query", + "resolver": "Resolver for IPV4 lookup", + "resolver_ipv6": "Resolver for IPV6 lookup" } + } }, - "options": { - "step": { - "init": { - "data": { - "resolver": "Resolver for IPV4 lookup", - "resolver_ipv6": "Resolver for IPV6 lookup" - } - } - }, - "error": { - "invalid_resolver": "Invalid IP address for resolver" - } + "error": { + "invalid_hostname": "Invalid hostname" } -} \ No newline at end of file + }, + "options": { + "step": { + "init": { + "data": { + "resolver": "Resolver for IPV4 lookup", + "resolver_ipv6": "Resolver for IPV6 lookup" + } + } + }, + "error": { + "invalid_resolver": "Invalid IP address for resolver" + } + } +} diff --git a/homeassistant/components/dnsip/translations/fr.json b/homeassistant/components/dnsip/translations/fr.json index ae6da0296c2..23c0d057ce4 100644 --- a/homeassistant/components/dnsip/translations/fr.json +++ b/homeassistant/components/dnsip/translations/fr.json @@ -1,27 +1,27 @@ { "config": { "error": { - "invalid_hostname": "Nom d'h\u00f4te invalide" + "invalid_hostname": "Nom d'h\u00f4te non valide" }, "step": { "user": { "data": { "hostname": "Le nom d'h\u00f4te pour lequel la requ\u00eate DNS doit \u00eatre effectu\u00e9e.", - "resolver": "R\u00e9solveur pour la recherche IPV4", - "resolver_ipv6": "R\u00e9solveur pour la recherche IPV6" + "resolver": "R\u00e9solveur pour la recherche IPv4", + "resolver_ipv6": "R\u00e9solveur pour la recherche IPv6" } } } }, "options": { "error": { - "invalid_resolver": "Adresse IP invalide pour le r\u00e9solveur" + "invalid_resolver": "Adresse IP non valide pour le r\u00e9solveur" }, "step": { "init": { "data": { - "resolver": "R\u00e9solveur pour la recherche IPV4", - "resolver_ipv6": "R\u00e9solveur pour la recherche IPV6" + "resolver": "R\u00e9solveur pour la recherche IPv4", + "resolver_ipv6": "R\u00e9solveur pour la recherche IPv6" } } } diff --git a/homeassistant/components/dnsip/translations/it.json b/homeassistant/components/dnsip/translations/it.json index 2ed18baa178..e0ddeb92a8b 100644 --- a/homeassistant/components/dnsip/translations/it.json +++ b/homeassistant/components/dnsip/translations/it.json @@ -7,8 +7,8 @@ "user": { "data": { "hostname": "Il nome host per il quale eseguire la query DNS", - "resolver": "Risolutore per la ricerca IPV4", - "resolver_ipv6": "Risolutore per la ricerca IPV6" + "resolver": "Risolutore per la ricerca IPv4", + "resolver_ipv6": "Risolutore per la ricerca IPv6" } } } @@ -20,8 +20,8 @@ "step": { "init": { "data": { - "resolver": "Risolutore per ricerca IPV4", - "resolver_ipv6": "Risolutore per ricerca IPV6" + "resolver": "Risolutore per ricerca IPv4", + "resolver_ipv6": "Risolutore per ricerca IPv6" } } } diff --git a/homeassistant/components/doorbird/manifest.json b/homeassistant/components/doorbird/manifest.json index 6fc29343d04..03d226a75ed 100644 --- a/homeassistant/components/doorbird/manifest.json +++ b/homeassistant/components/doorbird/manifest.json @@ -7,7 +7,7 @@ "zeroconf": [ { "type": "_axis-video._tcp.local.", - "properties": {"macaddress": "1ccae3*"} + "properties": { "macaddress": "1ccae3*" } } ], "codeowners": ["@oblogic7", "@bdraco", "@flacjacket"], diff --git a/homeassistant/components/doorbird/strings.json b/homeassistant/components/doorbird/strings.json index e710c587d5d..44fd07c405e 100644 --- a/homeassistant/components/doorbird/strings.json +++ b/homeassistant/components/doorbird/strings.json @@ -5,14 +5,15 @@ "data": { "events": "Comma separated list of events." }, - "description": "Add an comma separated event name for each event you wish to track. After entering them here, use the DoorBird app to assign them to a specific event. See the documentation at https://www.home-assistant.io/integrations/doorbird/#events. Example: somebody_pressed_the_button, motion" + "data_description": { + "events": "Add an comma separated event name for each event you wish to track. After entering them here, use the DoorBird app to assign them to a specific event.\n\nExample: somebody_pressed_the_button, motion" + } } } }, "config": { "step": { "user": { - "title": "Connect to the DoorBird", "data": { "password": "[%key:common::config_flow::data::password%]", "host": "[%key:common::config_flow::data::host%]", diff --git a/homeassistant/components/doorbird/translations/en.json b/homeassistant/components/doorbird/translations/en.json index c67658196c4..db1cea2d73f 100644 --- a/homeassistant/components/doorbird/translations/en.json +++ b/homeassistant/components/doorbird/translations/en.json @@ -3,8 +3,7 @@ "abort": { "already_configured": "Device is already configured", "link_local_address": "Link local addresses are not supported", - "not_doorbird_device": "This device is not a DoorBird", - "not_ipv4_address": "Only IPv4 addresess are supported" + "not_doorbird_device": "This device is not a DoorBird" }, "error": { "cannot_connect": "Failed to connect", diff --git a/homeassistant/components/doorbird/translations/fr.json b/homeassistant/components/doorbird/translations/fr.json index 68165d762f9..40569250e5f 100644 --- a/homeassistant/components/doorbird/translations/fr.json +++ b/homeassistant/components/doorbird/translations/fr.json @@ -7,7 +7,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "flow_title": "{name} ({host})", diff --git a/homeassistant/components/dsmr/config_flow.py b/homeassistant/components/dsmr/config_flow.py index 8bbf6197a10..6152a3756e3 100644 --- a/homeassistant/components/dsmr/config_flow.py +++ b/homeassistant/components/dsmr/config_flow.py @@ -9,6 +9,10 @@ from typing import Any from async_timeout import timeout from dsmr_parser import obis_references as obis_ref from dsmr_parser.clients.protocol import create_dsmr_reader, create_tcp_dsmr_reader +from dsmr_parser.clients.rfxtrx_protocol import ( + create_rfxtrx_dsmr_reader, + create_rfxtrx_tcp_dsmr_reader, +) from dsmr_parser.objects import DSMRObject import serial import serial.tools.list_ports @@ -22,13 +26,16 @@ from homeassistant.data_entry_flow import FlowResult from .const import ( CONF_DSMR_VERSION, + CONF_PROTOCOL, CONF_SERIAL_ID, CONF_SERIAL_ID_GAS, CONF_TIME_BETWEEN_UPDATE, DEFAULT_TIME_BETWEEN_UPDATE, DOMAIN, + DSMR_PROTOCOL, DSMR_VERSIONS, LOGGER, + RFXTRX_DSMR_PROTOCOL, ) CONF_MANUAL_PATH = "Enter Manually" @@ -37,11 +44,14 @@ CONF_MANUAL_PATH = "Enter Manually" class DSMRConnection: """Test the connection to DSMR and receive telegram to read serial ids.""" - def __init__(self, host: str | None, port: int, dsmr_version: str) -> None: + def __init__( + self, host: str | None, port: int, dsmr_version: str, protocol: str + ) -> None: """Initialize.""" self._host = host self._port = port self._dsmr_version = dsmr_version + self._protocol = protocol self._telegram: dict[str, DSMRObject] = {} self._equipment_identifier = obis_ref.EQUIPMENT_IDENTIFIER if dsmr_version == "5L": @@ -78,16 +88,24 @@ class DSMRConnection: transport.close() if self._host is None: + if self._protocol == DSMR_PROTOCOL: + create_reader = create_dsmr_reader + else: + create_reader = create_rfxtrx_dsmr_reader reader_factory = partial( - create_dsmr_reader, + create_reader, self._port, self._dsmr_version, update_telegram, loop=hass.loop, ) else: + if self._protocol == DSMR_PROTOCOL: + create_reader = create_tcp_dsmr_reader + else: + create_reader = create_rfxtrx_tcp_dsmr_reader reader_factory = partial( - create_tcp_dsmr_reader, + create_reader, self._host, self._port, self._dsmr_version, @@ -113,10 +131,15 @@ class DSMRConnection: async def _validate_dsmr_connection( - hass: core.HomeAssistant, data: dict[str, Any] + hass: core.HomeAssistant, data: dict[str, Any], protocol: str ) -> dict[str, str | None]: """Validate the user input allows us to connect.""" - conn = DSMRConnection(data.get(CONF_HOST), data[CONF_PORT], data[CONF_DSMR_VERSION]) + conn = DSMRConnection( + data.get(CONF_HOST), + data[CONF_PORT], + data[CONF_DSMR_VERSION], + protocol, + ) if not await conn.validate_connect(hass): raise CannotConnect @@ -260,9 +283,14 @@ class DSMRFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): data = input_data try: - info = await _validate_dsmr_connection(self.hass, data) + try: + protocol = DSMR_PROTOCOL + info = await _validate_dsmr_connection(self.hass, data, protocol) + except CannotCommunicate: + protocol = RFXTRX_DSMR_PROTOCOL + info = await _validate_dsmr_connection(self.hass, data, protocol) - data = {**data, **info} + data = {**data, **info, CONF_PROTOCOL: protocol} if info[CONF_SERIAL_ID]: await self.async_set_unique_id(info[CONF_SERIAL_ID]) diff --git a/homeassistant/components/dsmr/const.py b/homeassistant/components/dsmr/const.py index f68931e6fa5..2533aa8d025 100644 --- a/homeassistant/components/dsmr/const.py +++ b/homeassistant/components/dsmr/const.py @@ -17,6 +17,7 @@ LOGGER = logging.getLogger(__package__) PLATFORMS = [Platform.SENSOR] CONF_DSMR_VERSION = "dsmr_version" +CONF_PROTOCOL = "protocol" CONF_RECONNECT_INTERVAL = "reconnect_interval" CONF_PRECISION = "precision" CONF_TIME_BETWEEN_UPDATE = "time_between_update" @@ -32,11 +33,14 @@ DEFAULT_TIME_BETWEEN_UPDATE = 30 DATA_TASK = "task" -DEVICE_NAME_ENERGY = "Energy Meter" +DEVICE_NAME_ELECTRICITY = "Electricity Meter" DEVICE_NAME_GAS = "Gas Meter" DSMR_VERSIONS = {"2.2", "4", "5", "5B", "5L", "5S", "Q3D"} +DSMR_PROTOCOL = "dsmr_protocol" +RFXTRX_DSMR_PROTOCOL = "rfxtrx_dsmr_protocol" + SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( DSMRSensorEntityDescription( key=obis_references.CURRENT_ELECTRICITY_USAGE, diff --git a/homeassistant/components/dsmr/sensor.py b/homeassistant/components/dsmr/sensor.py index 94ae2864905..9c684493a3f 100644 --- a/homeassistant/components/dsmr/sensor.py +++ b/homeassistant/components/dsmr/sensor.py @@ -9,6 +9,10 @@ from functools import partial from dsmr_parser import obis_references as obis_ref from dsmr_parser.clients.protocol import create_dsmr_reader, create_tcp_dsmr_reader +from dsmr_parser.clients.rfxtrx_protocol import ( + create_rfxtrx_dsmr_reader, + create_rfxtrx_tcp_dsmr_reader, +) from dsmr_parser.objects import DSMRObject import serial @@ -29,6 +33,7 @@ from homeassistant.util import Throttle from .const import ( CONF_DSMR_VERSION, CONF_PRECISION, + CONF_PROTOCOL, CONF_RECONNECT_INTERVAL, CONF_SERIAL_ID, CONF_SERIAL_ID_GAS, @@ -37,9 +42,10 @@ from .const import ( DEFAULT_PRECISION, DEFAULT_RECONNECT_INTERVAL, DEFAULT_TIME_BETWEEN_UPDATE, - DEVICE_NAME_ENERGY, + DEVICE_NAME_ELECTRICITY, DEVICE_NAME_GAS, DOMAIN, + DSMR_PROTOCOL, LOGGER, SENSORS, ) @@ -77,9 +83,14 @@ async def async_setup_entry( # Creates an asyncio.Protocol factory for reading DSMR telegrams from # serial and calls update_entities_telegram to update entities on arrival + protocol = entry.data.get(CONF_PROTOCOL, DSMR_PROTOCOL) if CONF_HOST in entry.data: + if protocol == DSMR_PROTOCOL: + create_reader = create_tcp_dsmr_reader + else: + create_reader = create_rfxtrx_tcp_dsmr_reader reader_factory = partial( - create_tcp_dsmr_reader, + create_reader, entry.data[CONF_HOST], entry.data[CONF_PORT], dsmr_version, @@ -88,8 +99,12 @@ async def async_setup_entry( keep_alive_interval=60, ) else: + if protocol == DSMR_PROTOCOL: + create_reader = create_dsmr_reader + else: + create_reader = create_rfxtrx_dsmr_reader reader_factory = partial( - create_dsmr_reader, + create_reader, entry.data[CONF_PORT], dsmr_version, update_entities_telegram, @@ -186,7 +201,7 @@ class DSMREntity(SensorEntity): self.telegram: dict[str, DSMRObject] = {} device_serial = entry.data[CONF_SERIAL_ID] - device_name = DEVICE_NAME_ENERGY + device_name = DEVICE_NAME_ELECTRICITY if entity_description.is_gas: device_serial = entry.data[CONF_SERIAL_ID_GAS] device_name = DEVICE_NAME_GAS diff --git a/homeassistant/components/dsmr/translations/zh-Hant.json b/homeassistant/components/dsmr/translations/zh-Hant.json index 9d95685a87f..427011d5585 100644 --- a/homeassistant/components/dsmr/translations/zh-Hant.json +++ b/homeassistant/components/dsmr/translations/zh-Hant.json @@ -34,9 +34,9 @@ }, "user": { "data": { - "type": "\u9023\u7dda\u985e\u578b" + "type": "\u9023\u7dda\u985e\u5225" }, - "title": "\u9078\u64c7\u9023\u7dda\u985e\u578b" + "title": "\u9078\u64c7\u9023\u7dda\u985e\u5225" } } }, diff --git a/homeassistant/components/dunehd/config_flow.py b/homeassistant/components/dunehd/config_flow.py index 6c6f12280f5..434bbc7bd84 100644 --- a/homeassistant/components/dunehd/config_flow.py +++ b/homeassistant/components/dunehd/config_flow.py @@ -76,6 +76,12 @@ class DuneHDConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self, user_input: dict[str, str] | None = None ) -> FlowResult: """Handle configuration by yaml file.""" + _LOGGER.warning( + "Configuration of the Dune HD integration in YAML is deprecated and will be " + "removed in Home Assistant 2022.6; Your existing configuration " + "has been imported into the UI automatically and can be safely removed " + "from your configuration.yaml file" + ) assert user_input is not None host: str = user_input[CONF_HOST] diff --git a/homeassistant/components/dunehd/media_player.py b/homeassistant/components/dunehd/media_player.py index 437b5b66a99..252cf82df42 100644 --- a/homeassistant/components/dunehd/media_player.py +++ b/homeassistant/components/dunehd/media_player.py @@ -93,11 +93,10 @@ class DuneHDPlayerEntity(MediaPlayerEntity): self._state: dict[str, Any] = {} self._unique_id = unique_id - def update(self) -> bool: + def update(self) -> None: """Update internal status of the entity.""" self._state = self._player.update_state() self.__update_title() - return True @property def state(self) -> str | None: diff --git a/homeassistant/components/dunehd/strings.json b/homeassistant/components/dunehd/strings.json index 4c0d8879858..f7e12b39f16 100644 --- a/homeassistant/components/dunehd/strings.json +++ b/homeassistant/components/dunehd/strings.json @@ -2,8 +2,7 @@ "config": { "step": { "user": { - "title": "Dune HD", - "description": "Set up Dune HD integration. If you have problems with configuration go to: https://www.home-assistant.io/integrations/dunehd \n\nEnsure that your player is turned on.", + "description": "Ensure that your player is turned on.", "data": { "host": "[%key:common::config_flow::data::host%]" } diff --git a/homeassistant/components/eafm/strings.json b/homeassistant/components/eafm/strings.json index 9c829abcd1a..c704d361253 100644 --- a/homeassistant/components/eafm/strings.json +++ b/homeassistant/components/eafm/strings.json @@ -1,17 +1,17 @@ { - "config": { - "step": { - "user": { - "title": "Track a flood monitoring station", - "description": "Select the station you want to monitor", - "data": { - "station": "Station" - } - } - }, - "abort": { - "no_stations": "No flood monitoring stations found.", - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + "config": { + "step": { + "user": { + "title": "Track a flood monitoring station", + "description": "Select the station you want to monitor", + "data": { + "station": "Station" } + } + }, + "abort": { + "no_stations": "No flood monitoring stations found.", + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } + } } diff --git a/homeassistant/components/ecobee/manifest.json b/homeassistant/components/ecobee/manifest.json index c9fe52b7e13..47a6e607e3b 100644 --- a/homeassistant/components/ecobee/manifest.json +++ b/homeassistant/components/ecobee/manifest.json @@ -3,19 +3,15 @@ "name": "ecobee", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ecobee", - "requirements": [ - "python-ecobee-api==0.2.14" - ], - "codeowners": [ - "@marthoc" - ], + "requirements": ["python-ecobee-api==0.2.14"], + "codeowners": ["@marthoc"], "homekit": { "models": ["EB-*", "ecobee*"] }, "zeroconf": [ - {"type":"_sideplay._tcp.local.", "properties": {"mdl":"eb-*"}}, - {"type":"_sideplay._tcp.local.", "properties": {"mdl":"ecobee*"}} + { "type": "_sideplay._tcp.local.", "properties": { "mdl": "eb-*" } }, + { "type": "_sideplay._tcp.local.", "properties": { "mdl": "ecobee*" } } ], "iot_class": "cloud_polling", "loggers": ["pyecobee"] -} \ No newline at end of file +} diff --git a/homeassistant/components/econet/__init__.py b/homeassistant/components/econet/__init__.py index 100b58b107d..a706ceb8e7e 100644 --- a/homeassistant/components/econet/__init__.py +++ b/homeassistant/components/econet/__init__.py @@ -16,7 +16,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, TEMP_FAHRENHEIT, Platform from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers.dispatcher import dispatcher_send +from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send from homeassistant.helpers.entity import DeviceInfo, Entity from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import ConfigType @@ -119,9 +119,7 @@ class EcoNetEntity(Entity): """Subscribe to device events.""" await super().async_added_to_hass() self.async_on_remove( - self.hass.helpers.dispatcher.async_dispatcher_connect( - PUSH_UPDATE, self.on_update_received - ) + async_dispatcher_connect(self.hass, PUSH_UPDATE, self.on_update_received) ) @callback diff --git a/homeassistant/components/econet/strings.json b/homeassistant/components/econet/strings.json index 9d043e47ebc..358f159cd7e 100644 --- a/homeassistant/components/econet/strings.json +++ b/homeassistant/components/econet/strings.json @@ -19,4 +19,4 @@ } } } -} \ No newline at end of file +} diff --git a/homeassistant/components/econet/translations/fr.json b/homeassistant/components/econet/translations/fr.json index e6081bef90a..b03145b3c73 100644 --- a/homeassistant/components/econet/translations/fr.json +++ b/homeassistant/components/econet/translations/fr.json @@ -3,16 +3,16 @@ "abort": { "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide" + "invalid_auth": "Authentification non valide" }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide" + "invalid_auth": "Authentification non valide" }, "step": { "user": { "data": { - "email": "Email", + "email": "Courriel", "password": "Mot de passe" }, "title": "Configurer le compte Rheem EcoNet" diff --git a/homeassistant/components/edimax/switch.py b/homeassistant/components/edimax/switch.py index 3574ee88172..6f780f8da61 100644 --- a/homeassistant/components/edimax/switch.py +++ b/homeassistant/components/edimax/switch.py @@ -48,10 +48,7 @@ class SmartPlugSwitch(SwitchEntity): """Initialize the switch.""" self.smartplug = smartplug self._name = name - self._now_power = None - self._now_energy_day = None self._state = False - self._supports_power_monitoring = False self._info = None self._mac = None @@ -65,16 +62,6 @@ class SmartPlugSwitch(SwitchEntity): """Return the name of the Smart Plug, if any.""" return self._name - @property - def current_power_w(self): - """Return the current power usage in W.""" - return self._now_power - - @property - def today_energy_kwh(self): - """Return the today total energy usage in kWh.""" - return self._now_energy_day - @property def is_on(self): """Return true if switch is on.""" @@ -93,17 +80,5 @@ class SmartPlugSwitch(SwitchEntity): if not self._info: self._info = self.smartplug.info self._mac = self._info["mac"] - self._supports_power_monitoring = self._info["model"] != "SP1101W" - - if self._supports_power_monitoring: - try: - self._now_power = float(self.smartplug.now_power) - except (TypeError, ValueError): - self._now_power = None - - try: - self._now_energy_day = float(self.smartplug.now_energy_day) - except (TypeError, ValueError): - self._now_energy_day = None self._state = self.smartplug.state == "ON" diff --git a/homeassistant/components/efergy/__init__.py b/homeassistant/components/efergy/__init__.py index 372dbe77e75..915eb0daf46 100644 --- a/homeassistant/components/efergy/__init__.py +++ b/homeassistant/components/efergy/__init__.py @@ -4,14 +4,14 @@ from __future__ import annotations from pyefergy import Efergy, exceptions from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_ATTRIBUTION, CONF_API_KEY, Platform +from homeassistant.const import CONF_API_KEY, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import device_registry as dr from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.entity import DeviceInfo, Entity -from .const import ATTRIBUTION, DATA_KEY_API, DEFAULT_NAME, DOMAIN +from .const import DEFAULT_NAME, DOMAIN PLATFORMS = [Platform.SENSOR] @@ -34,7 +34,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: "API Key is no longer valid. Please reauthenticate" ) from ex - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {DATA_KEY_API: api} + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = api hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True @@ -42,8 +42,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: 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: + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): hass.data[DOMAIN].pop(entry.entry_id) return unload_ok @@ -51,21 +50,17 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: class EfergyEntity(Entity): """Representation of a Efergy entity.""" - def __init__( - self, - api: Efergy, - server_unique_id: str, - ) -> None: + _attr_attribution = "Data provided by Efergy" + + def __init__(self, api: Efergy, server_unique_id: str) -> None: """Initialize an Efergy entity.""" self.api = api - self._server_unique_id = server_unique_id - self._attr_extra_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION} self._attr_device_info = DeviceInfo( configuration_url="https://engage.efergy.com/user/login", - connections={(dr.CONNECTION_NETWORK_MAC, self.api.info["mac"])}, - identifiers={(DOMAIN, self._server_unique_id)}, + connections={(dr.CONNECTION_NETWORK_MAC, api.info["mac"])}, + identifiers={(DOMAIN, server_unique_id)}, manufacturer=DEFAULT_NAME, name=DEFAULT_NAME, - model=self.api.info["type"], - sw_version=self.api.info["version"], + model=api.info["type"], + sw_version=api.info["version"], ) diff --git a/homeassistant/components/efergy/config_flow.py b/homeassistant/components/efergy/config_flow.py index 8283bfce62d..5ff6e9ba9f2 100644 --- a/homeassistant/components/efergy/config_flow.py +++ b/homeassistant/components/efergy/config_flow.py @@ -1,7 +1,6 @@ """Config flow for Efergy integration.""" from __future__ import annotations -import logging from typing import Any from pyefergy import Efergy, exceptions @@ -12,9 +11,7 @@ from homeassistant.const import CONF_API_KEY from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .const import DEFAULT_NAME, DOMAIN - -_LOGGER = logging.getLogger(__name__) +from .const import DEFAULT_NAME, DOMAIN, LOGGER class EfergyFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @@ -69,6 +66,6 @@ class EfergyFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): except exceptions.InvalidAuth: return None, "invalid_auth" except Exception: # pylint: disable=broad-except - _LOGGER.exception("Unexpected exception") + LOGGER.exception("Unexpected exception") return None, "unknown" return api.info["hid"], None diff --git a/homeassistant/components/efergy/const.py b/homeassistant/components/efergy/const.py index f5e9af6a4c8..5a0ca11693b 100644 --- a/homeassistant/components/efergy/const.py +++ b/homeassistant/components/efergy/const.py @@ -1,12 +1,13 @@ """Constants for the Efergy integration.""" from datetime import timedelta - -ATTRIBUTION = "Data provided by Efergy" +import logging +from typing import Final CONF_CURRENT_VALUES = "current_values" -DATA_KEY_API = "api" DEFAULT_NAME = "Efergy" -DOMAIN = "efergy" +DOMAIN: Final = "efergy" + +LOGGER = logging.getLogger(__package__) MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30) diff --git a/homeassistant/components/efergy/sensor.py b/homeassistant/components/efergy/sensor.py index 00a10b713d2..abe34a21bcc 100644 --- a/homeassistant/components/efergy/sensor.py +++ b/homeassistant/components/efergy/sensor.py @@ -1,7 +1,6 @@ """Support for Efergy sensors.""" from __future__ import annotations -import logging from re import sub from typing import cast @@ -21,9 +20,7 @@ from homeassistant.helpers import entity_platform from homeassistant.helpers.typing import StateType from . import EfergyEntity -from .const import CONF_CURRENT_VALUES, DATA_KEY_API, DOMAIN - -_LOGGER = logging.getLogger(__name__) +from .const import CONF_CURRENT_VALUES, DOMAIN, LOGGER SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( @@ -112,7 +109,7 @@ async def async_setup_entry( async_add_entities: entity_platform.AddEntitiesCallback, ) -> None: """Set up Efergy sensors.""" - api: Efergy = hass.data[DOMAIN][entry.entry_id][DATA_KEY_API] + api: Efergy = hass.data[DOMAIN][entry.entry_id] sensors = [] for description in SENSOR_TYPES: if description.key != CONF_CURRENT_VALUES: @@ -174,8 +171,8 @@ class EfergySensor(EfergyEntity, SensorEntity): except (ConnectError, DataError, ServiceError) as ex: if self._attr_available: self._attr_available = False - _LOGGER.error("Error getting data: %s", ex) + LOGGER.error("Error getting data: %s", ex) return if not self._attr_available: self._attr_available = True - _LOGGER.info("Connection has resumed") + LOGGER.info("Connection has resumed") diff --git a/homeassistant/components/efergy/strings.json b/homeassistant/components/efergy/strings.json index dc625c92840..924d5a56bcf 100644 --- a/homeassistant/components/efergy/strings.json +++ b/homeassistant/components/efergy/strings.json @@ -2,7 +2,6 @@ "config": { "step": { "user": { - "title": "Efergy", "data": { "api_key": "[%key:common::config_flow::data::api_key%]" } diff --git a/homeassistant/components/efergy/translations/fr.json b/homeassistant/components/efergy/translations/fr.json index 2e98eca19e7..a506454bc14 100644 --- a/homeassistant/components/efergy/translations/fr.json +++ b/homeassistant/components/efergy/translations/fr.json @@ -6,7 +6,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/eight_sleep/__init__.py b/homeassistant/components/eight_sleep/__init__.py index 3c08a75448f..3495d1da28b 100644 --- a/homeassistant/components/eight_sleep/__init__.py +++ b/homeassistant/components/eight_sleep/__init__.py @@ -3,6 +3,7 @@ from __future__ import annotations from datetime import timedelta import logging +from typing import Union from pyeight.eight import EightSleep from pyeight.user import EightUser @@ -28,8 +29,6 @@ from homeassistant.helpers.update_coordinator import ( _LOGGER = logging.getLogger(__name__) -CONF_PARTNER = "partner" - DATA_EIGHT = "eight_sleep" DATA_HEAT = "heat" DATA_USER = "user" @@ -43,9 +42,6 @@ USER_ENTITY = "user" HEAT_SCAN_INTERVAL = timedelta(seconds=60) USER_SCAN_INTERVAL = timedelta(seconds=300) -SIGNAL_UPDATE_HEAT = "eight_heat_update" -SIGNAL_UPDATE_USER = "eight_user_update" - NAME_MAP = { "left_current_sleep": "Left Sleep Session", "left_current_sleep_fitness": "Left Sleep Fitness", @@ -82,16 +78,12 @@ SERVICE_EIGHT_SCHEMA = vol.Schema( CONFIG_SCHEMA = vol.Schema( { - DOMAIN: vol.All( - cv.deprecated(CONF_PARTNER), - vol.Schema( - { - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_PARTNER): cv.boolean, - } - ), - ) + DOMAIN: vol.Schema( + { + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + } + ), }, extra=vol.ALLOW_EXTRA, ) @@ -227,7 +219,11 @@ class EightSleepUserDataCoordinator(DataUpdateCoordinator): await self.api.update_user_data() -class EightSleepBaseEntity(CoordinatorEntity): +class EightSleepBaseEntity( + CoordinatorEntity[ + Union[EightSleepUserDataCoordinator, EightSleepHeatDataCoordinator] + ] +): """The base Eight Sleep entity class.""" def __init__( diff --git a/homeassistant/components/eight_sleep/services.yaml b/homeassistant/components/eight_sleep/services.yaml index 537f04bd306..de864afc160 100644 --- a/homeassistant/components/eight_sleep/services.yaml +++ b/homeassistant/components/eight_sleep/services.yaml @@ -27,4 +27,4 @@ heat_set: number: min: -100 max: 100 - unit_of_measurement: '°' + unit_of_measurement: "°" diff --git a/homeassistant/components/elgato/light.py b/homeassistant/components/elgato/light.py index cc3dd93d9c4..d9f73963870 100644 --- a/homeassistant/components/elgato/light.py +++ b/homeassistant/components/elgato/light.py @@ -61,14 +61,14 @@ async def async_setup_entry( ) -class ElgatoLight(ElgatoEntity, CoordinatorEntity, LightEntity): +class ElgatoLight( + ElgatoEntity, CoordinatorEntity[DataUpdateCoordinator[State]], LightEntity +): """Defines an Elgato Light.""" - coordinator: DataUpdateCoordinator[State] - def __init__( self, - coordinator: DataUpdateCoordinator, + coordinator: DataUpdateCoordinator[State], client: Elgato, info: Info, mac: str | None, diff --git a/homeassistant/components/elgato/translations/fr.json b/homeassistant/components/elgato/translations/fr.json index 5f3e99b0425..50588b11cbd 100644 --- a/homeassistant/components/elgato/translations/fr.json +++ b/homeassistant/components/elgato/translations/fr.json @@ -14,11 +14,11 @@ "host": "H\u00f4te", "port": "Port" }, - "description": "Configurez votre Elgato Key Light pour l'int\u00e9grer \u00e0 Home Assistant." + "description": "Configurez votre Elgato Light pour l'int\u00e9grer \u00e0 Home Assistant." }, "zeroconf_confirm": { - "description": "Voulez-vous ajouter l'Elgato Key Light avec le num\u00e9ro de s\u00e9rie `{serial_number}` \u00e0 Home Assistant?", - "title": "Appareil Elgato Key Light d\u00e9couvert" + "description": "Voulez-vous ajouter l'Elgato Light portant le num\u00e9ro de s\u00e9rie `{serial_number}` \u00e0 Home Assistant\u00a0?", + "title": "Appareil Elgato Light d\u00e9couvert" } } } diff --git a/homeassistant/components/elgato/translations/zh-Hant.json b/homeassistant/components/elgato/translations/zh-Hant.json index 9d6bd9ab761..7e51ce29807 100644 --- a/homeassistant/components/elgato/translations/zh-Hant.json +++ b/homeassistant/components/elgato/translations/zh-Hant.json @@ -18,7 +18,7 @@ }, "zeroconf_confirm": { "description": "\u662f\u5426\u8981\u5c07\u5e8f\u865f\u70ba `{serial_number}` \u4e4b Elgato \u7167\u660e\u88dd\u7f6e\u65b0\u589e\u81f3 Home Assistant\uff1f", - "title": "\u81ea\u52d5\u63a2\u7d22\u5230 Elgato \u7167\u660e\u88dd\u7f6e" + "title": "\u6240\u767c\u73fe\u7684 Elgato \u7167\u660e\u88dd\u7f6e" } } } diff --git a/homeassistant/components/elkm1/manifest.json b/homeassistant/components/elkm1/manifest.json index 909bfa3bd02..695b6bcd999 100644 --- a/homeassistant/components/elkm1/manifest.json +++ b/homeassistant/components/elkm1/manifest.json @@ -3,10 +3,7 @@ "name": "Elk-M1 Control", "documentation": "https://www.home-assistant.io/integrations/elkm1", "requirements": ["elkm1-lib==1.2.0"], - "dhcp": [ - {"registered_devices": true}, - {"macaddress":"00409D*"} - ], + "dhcp": [{ "registered_devices": true }, { "macaddress": "00409D*" }], "codeowners": ["@gwww", "@bdraco"], "dependencies": ["network"], "config_flow": true, diff --git a/homeassistant/components/elkm1/services.yaml b/homeassistant/components/elkm1/services.yaml index 18608f3a476..1f130416363 100644 --- a/homeassistant/components/elkm1/services.yaml +++ b/homeassistant/components/elkm1/services.yaml @@ -112,14 +112,14 @@ alarm_display_message: name: Line 1 description: Up to 16 characters of text (truncated if too long). example: The answer to life. - default: '' + default: "" selector: text: line2: name: Line 2 description: Up to 16 characters of text (truncated if too long). example: the universe, and everything. - default: '' + default: "" selector: text: diff --git a/homeassistant/components/elkm1/translations/el.json b/homeassistant/components/elkm1/translations/el.json index 9e7fe27ce72..9f600806d83 100644 --- a/homeassistant/components/elkm1/translations/el.json +++ b/homeassistant/components/elkm1/translations/el.json @@ -45,7 +45,7 @@ "temperature_unit": "\u0397 \u03bc\u03bf\u03bd\u03ac\u03b4\u03b1 \u03b8\u03b5\u03c1\u03bc\u03bf\u03ba\u03c1\u03b1\u03c3\u03af\u03b1\u03c2 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af \u03c4\u03bf ElkM1.", "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" }, - "description": "\u0397 \u03c3\u03c5\u03bc\u03b2\u03bf\u03bb\u03bf\u03c3\u03b5\u03b9\u03c1\u03ac \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7\u03c2 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03ad\u03c7\u03b5\u03b9 \u03c4\u03b7 \u03bc\u03bf\u03c1\u03c6\u03ae 'address[:port]' \u03b3\u03b9\u03b1 'secure' \u03ba\u03b1\u03b9 'non-secure'. \u03a0\u03b1\u03c1\u03ac\u03b4\u03b5\u03b9\u03b3\u03bc\u03b1: '192.168.1.1'. \u0397 \u03b8\u03cd\u03c1\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03c0\u03c1\u03bf\u03b1\u03b9\u03c1\u03b5\u03c4\u03b9\u03ba\u03ae \u03ba\u03b1\u03b9 \u03b7 \u03c0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03b7 \u03c4\u03b9\u03bc\u03ae \u03b5\u03af\u03bd\u03b1\u03b9 2101 \u03b3\u03b9\u03b1 '\u03bc\u03b7 \u03b1\u03c3\u03c6\u03b1\u03bb\u03ae' \u03ba\u03b1\u03b9 2601 \u03b3\u03b9\u03b1 '\u03b1\u03c3\u03c6\u03b1\u03bb\u03ae'. \u0393\u03b9\u03b1 \u03c4\u03bf \u03c3\u03b5\u03b9\u03c1\u03b9\u03b1\u03ba\u03cc \u03c0\u03c1\u03c9\u03c4\u03cc\u03ba\u03bf\u03bb\u03bb\u03bf, \u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03ad\u03c7\u03b5\u03b9 \u03c4\u03b7 \u03bc\u03bf\u03c1\u03c6\u03ae 'tty[:baud]'. \u03a0\u03b1\u03c1\u03ac\u03b4\u03b5\u03b9\u03b3\u03bc\u03b1: '/dev/ttyS1'. \u03a4\u03bf baud \u03b5\u03af\u03bd\u03b1\u03b9 \u03c0\u03c1\u03bf\u03b1\u03b9\u03c1\u03b5\u03c4\u03b9\u03ba\u03cc \u03ba\u03b1\u03b9 \u03b7 \u03c0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03b5\u03af\u03bd\u03b1\u03b9 115200.", + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03c3\u03cd\u03c3\u03c4\u03b7\u03bc\u03b1 \u03c0\u03bf\u03c5 \u03b1\u03bd\u03b1\u03ba\u03b1\u03bb\u03cd\u03c6\u03b8\u03b7\u03ba\u03b5 \u03ae \"\u039c\u03b7 \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b7 \u03ba\u03b1\u03c4\u03b1\u03c7\u03ce\u03c1\u03b9\u03c3\u03b7\" \u03b5\u03ac\u03bd \u03b4\u03b5\u03bd \u03ad\u03c7\u03bf\u03c5\u03bd \u03b5\u03bd\u03c4\u03bf\u03c0\u03b9\u03c3\u03c4\u03b5\u03af \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2.", "title": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03c3\u03c4\u03bf Elk-M1 Control" } } diff --git a/homeassistant/components/elkm1/translations/fr.json b/homeassistant/components/elkm1/translations/fr.json index 87193a9adf7..07b5d132968 100644 --- a/homeassistant/components/elkm1/translations/fr.json +++ b/homeassistant/components/elkm1/translations/fr.json @@ -8,7 +8,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "flow_title": "{mac_address} ({host})", diff --git a/homeassistant/components/elkm1/translations/he.json b/homeassistant/components/elkm1/translations/he.json index eb49e33c019..8d7b5c9b945 100644 --- a/homeassistant/components/elkm1/translations/he.json +++ b/homeassistant/components/elkm1/translations/he.json @@ -15,6 +15,7 @@ "data": { "password": "\u05e1\u05d9\u05e1\u05de\u05d4", "protocol": "\u05e4\u05e8\u05d5\u05d8\u05d5\u05e7\u05d5\u05dc", + "temperature_unit": "\u05d9\u05d7\u05d9\u05d3\u05ea \u05d4\u05d8\u05de\u05e4\u05e8\u05d8\u05d5\u05e8\u05d4 \u05e9\u05d1\u05d4 \u05de\u05e9\u05ea\u05de\u05e9 ElkM1.", "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" }, "title": "\u05d4\u05ea\u05d7\u05d1\u05e8 \u05d0\u05dc \u05d1\u05e7\u05e8\u05ea Elk-M1" @@ -23,6 +24,7 @@ "data": { "password": "\u05e1\u05d9\u05e1\u05de\u05d4", "protocol": "\u05e4\u05e8\u05d5\u05d8\u05d5\u05e7\u05d5\u05dc", + "temperature_unit": "\u05d9\u05d7\u05d9\u05d3\u05ea \u05d4\u05d8\u05de\u05e4\u05e8\u05d8\u05d5\u05e8\u05d4 \u05e9\u05d1\u05d4 \u05de\u05e9\u05ea\u05de\u05e9 ElkM1.", "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" }, "title": "\u05d4\u05ea\u05d7\u05d1\u05e8 \u05d0\u05dc \u05d1\u05e7\u05e8\u05ea Elk-M1" diff --git a/homeassistant/components/elkm1/translations/zh-Hant.json b/homeassistant/components/elkm1/translations/zh-Hant.json index 7b25413c6fc..543f93626ce 100644 --- a/homeassistant/components/elkm1/translations/zh-Hant.json +++ b/homeassistant/components/elkm1/translations/zh-Hant.json @@ -20,7 +20,7 @@ "temperature_unit": "ElkM1 \u6240\u4f7f\u7528\u6eab\u5ea6\u55ae\u4f4d\u3002", "username": "\u4f7f\u7528\u8005\u540d\u7a31" }, - "description": "\u9023\u7dda\u81f3\u6240\u63a2\u7d22\u7684\u7cfb\u7d71\uff1a{mac_address} ({host})", + "description": "\u9023\u7dda\u81f3\u6240\u767c\u73fe\u7684\u7cfb\u7d71\uff1a{mac_address} ({host})", "title": "\u9023\u7dda\u81f3 Elk-M1 Control" }, "manual_connection": { @@ -45,7 +45,7 @@ "temperature_unit": "ElkM1 \u4f7f\u7528\u6eab\u5ea6\u55ae\u4f4d\u3002", "username": "\u4f7f\u7528\u8005\u540d\u7a31" }, - "description": "\u9078\u64c7\u6240\u63a2\u7d22\u5230\u7684\u7cfb\u7d71\uff0c\u6216\u5047\u5982\u6c92\u627e\u5230\u7684\u8a71\u9032\u884c\u624b\u52d5\u8f38\u5165\u3002", + "description": "\u9078\u64c7\u6240\u767c\u73fe\u5230\u7684\u7cfb\u7d71\uff0c\u6216\u5047\u5982\u6c92\u627e\u5230\u7684\u8a71\u9032\u884c\u624b\u52d5\u8f38\u5165\u3002", "title": "\u9023\u7dda\u81f3 Elk-M1 Control" } } diff --git a/homeassistant/components/elmax/common.py b/homeassistant/components/elmax/common.py index e9854cc5d7a..2d66ca9f72e 100644 --- a/homeassistant/components/elmax/common.py +++ b/homeassistant/components/elmax/common.py @@ -116,11 +116,9 @@ class ElmaxCoordinator(DataUpdateCoordinator[PanelStatus]): ) from err -class ElmaxEntity(CoordinatorEntity): +class ElmaxEntity(CoordinatorEntity[ElmaxCoordinator]): """Wrapper for Elmax entities.""" - coordinator: ElmaxCoordinator - def __init__( self, panel: PanelEntry, diff --git a/homeassistant/components/elmax/manifest.json b/homeassistant/components/elmax/manifest.json index 8e230dcab38..be6cf6c74d7 100644 --- a/homeassistant/components/elmax/manifest.json +++ b/homeassistant/components/elmax/manifest.json @@ -4,9 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/elmax", "requirements": ["elmax_api==0.0.2"], - "codeowners": [ - "@albertogeniola" - ], + "codeowners": ["@albertogeniola"], "iot_class": "cloud_polling", "loggers": ["elmax_api"] -} \ No newline at end of file +} diff --git a/homeassistant/components/elmax/strings.json b/homeassistant/components/elmax/strings.json index 3bfce6bb0b0..a9c823f3a1a 100644 --- a/homeassistant/components/elmax/strings.json +++ b/homeassistant/components/elmax/strings.json @@ -28,4 +28,4 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } } -} \ No newline at end of file +} diff --git a/homeassistant/components/elmax/translations/fr.json b/homeassistant/components/elmax/translations/fr.json index c6c68b9df3a..7fe57a8290f 100644 --- a/homeassistant/components/elmax/translations/fr.json +++ b/homeassistant/components/elmax/translations/fr.json @@ -4,12 +4,12 @@ "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" }, "error": { - "bad_auth": "Authentification invalide", - "invalid_auth": "Authentification invalide", + "bad_auth": "Authentification non valide", + "invalid_auth": "Authentification non valide", "invalid_pin": "Le code PIN fourni n\u2019est pas valide", "network_error": "Une erreur r\u00e9seau s'est produite", "no_panel_online": "Aucun panneau de contr\u00f4le Elmax en ligne n'a \u00e9t\u00e9 trouv\u00e9.", - "unknown": "Erreur inconnue", + "unknown": "Erreur inattendue", "unknown_error": "une erreur inattendue est apparue" }, "step": { diff --git a/homeassistant/components/elv/switch.py b/homeassistant/components/elv/switch.py index 103915eecb4..8a7da161da0 100644 --- a/homeassistant/components/elv/switch.py +++ b/homeassistant/components/elv/switch.py @@ -6,7 +6,7 @@ import logging import pypca from serial import SerialException -from homeassistant.components.switch import ATTR_CURRENT_POWER_W, SwitchEntity +from homeassistant.components.switch import SwitchEntity from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -14,8 +14,6 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType _LOGGER = logging.getLogger(__name__) -ATTR_TOTAL_ENERGY_KWH = "total_energy_kwh" - DEFAULT_NAME = "PCA 301" @@ -57,7 +55,6 @@ class SmartPlugSwitch(SwitchEntity): self._name = "PCA 301" self._state = None self._available = True - self._emeter_params = {} self._pca = pca @property @@ -83,23 +80,11 @@ class SmartPlugSwitch(SwitchEntity): """Turn the switch off.""" self._pca.turn_off(self._device_id) - @property - def extra_state_attributes(self): - """Return the state attributes of the device.""" - return self._emeter_params - def update(self): """Update the PCA switch's state.""" try: - self._emeter_params[ - ATTR_CURRENT_POWER_W - ] = f"{self._pca.get_current_power(self._device_id):.1f}" - self._emeter_params[ - ATTR_TOTAL_ENERGY_KWH - ] = f"{self._pca.get_total_consumption(self._device_id):.2f}" - - self._available = True self._state = self._pca.get_state(self._device_id) + self._available = True except (OSError) as ex: if self._available: diff --git a/homeassistant/components/emonitor/manifest.json b/homeassistant/components/emonitor/manifest.json index 6548c71171c..6b409c795bb 100644 --- a/homeassistant/components/emonitor/manifest.json +++ b/homeassistant/components/emonitor/manifest.json @@ -5,8 +5,8 @@ "documentation": "https://www.home-assistant.io/integrations/emonitor", "requirements": ["aioemonitor==1.0.5"], "dhcp": [ - {"hostname": "emonitor*", "macaddress": "0090C2*"}, - {"registered_devices": true} + { "hostname": "emonitor*", "macaddress": "0090C2*" }, + { "registered_devices": true } ], "codeowners": ["@bdraco"], "iot_class": "local_polling", diff --git a/homeassistant/components/emonitor/sensor.py b/homeassistant/components/emonitor/sensor.py index b99dd35093e..27117d88fe4 100644 --- a/homeassistant/components/emonitor/sensor.py +++ b/homeassistant/components/emonitor/sensor.py @@ -1,9 +1,12 @@ """Support for a Emonitor channel sensor.""" -from aioemonitor.monitor import EmonitorChannel +from __future__ import annotations + +from aioemonitor.monitor import EmonitorChannel, EmonitorStatus from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, + SensorEntityDescription, SensorStateClass, ) from homeassistant.config_entries import ConfigEntry @@ -21,6 +24,16 @@ from homeassistant.helpers.update_coordinator import ( from . import name_short_mac from .const import DOMAIN +SENSORS = ( + SensorEntityDescription(key="inst_power"), + SensorEntityDescription( + key="avg_power", name="Average", entity_registry_enabled_default=False + ), + SensorEntityDescription( + key="max_power", name="Max", entity_registry_enabled_default=False + ), +) + async def async_setup_entry( hass: HomeAssistant, @@ -30,7 +43,7 @@ async def async_setup_entry( """Set up entry.""" coordinator = hass.data[DOMAIN][config_entry.entry_id] channels = coordinator.data.channels - entities = [] + entities: list[EmonitorPowerSensor] = [] seen_channels = set() for channel_number, channel in channels.items(): seen_channels.add(channel_number) @@ -39,7 +52,10 @@ async def async_setup_entry( if channel.paired_with_channel in seen_channels: continue - entities.append(EmonitorPowerSensor(coordinator, channel_number)) + entities.extend( + EmonitorPowerSensor(coordinator, description, channel_number) + for description in SENSORS + ) async_add_entities(entities) @@ -51,59 +67,62 @@ class EmonitorPowerSensor(CoordinatorEntity, SensorEntity): _attr_native_unit_of_measurement = POWER_WATT _attr_state_class = SensorStateClass.MEASUREMENT - def __init__(self, coordinator: DataUpdateCoordinator, channel_number: int) -> None: + def __init__( + self, + coordinator: DataUpdateCoordinator, + description: SensorEntityDescription, + channel_number: int, + ) -> None: """Initialize the channel sensor.""" + self.entity_description = description self.channel_number = channel_number super().__init__(coordinator) - self._attr_unique_id = f"{self.mac_address}_{channel_number}" + mac_address = self.emonitor_status.network.mac_address + device_name = name_short_mac(mac_address[-6:]) + label = self.channel_data.label or f"{device_name} {channel_number}" + if description.name: + self._attr_name = f"{label} {description.name}" + self._attr_unique_id = f"{mac_address}_{channel_number}_{description.key}" + else: + self._attr_name = label + self._attr_unique_id = f"{mac_address}_{channel_number}" + self._attr_device_info = DeviceInfo( + connections={(dr.CONNECTION_NETWORK_MAC, mac_address)}, + manufacturer="Powerhouse Dynamics, Inc.", + name=device_name, + sw_version=self.emonitor_status.hardware.firmware_version, + ) + + @property + def channels(self) -> dict[int, EmonitorChannel]: + """Return the channels dict.""" + channels: dict[int, EmonitorChannel] = self.emonitor_status.channels + return channels @property def channel_data(self) -> EmonitorChannel: - """Channel data.""" - return self.coordinator.data.channels[self.channel_number] + """Return the channel data.""" + return self.channels[self.channel_number] @property - def paired_channel_data(self) -> EmonitorChannel: - """Channel data.""" - return self.coordinator.data.channels[self.channel_data.paired_with_channel] - - @property - def name(self) -> str: - """Name of the sensor.""" - return self.channel_data.label + def emonitor_status(self) -> EmonitorStatus: + """Return the EmonitorStatus.""" + return self.coordinator.data def _paired_attr(self, attr_name: str) -> float: """Cumulative attributes for channel and paired channel.""" - attr_val = getattr(self.channel_data, attr_name) - if self.channel_data.paired_with_channel: - attr_val += getattr(self.paired_channel_data, attr_name) + channel_data = self.channels[self.channel_number] + attr_val = getattr(channel_data, attr_name) + if paired_channel := channel_data.paired_with_channel: + attr_val += getattr(self.channels[paired_channel], attr_name) return attr_val @property def native_value(self) -> StateType: """State of the sensor.""" - return self._paired_attr("inst_power") + return self._paired_attr(self.entity_description.key) @property def extra_state_attributes(self) -> dict: """Return the device specific state attributes.""" - return { - "channel": self.channel_number, - "avg_power": self._paired_attr("avg_power"), - "max_power": self._paired_attr("max_power"), - } - - @property - def mac_address(self) -> str: - """Return the mac address of the device.""" - return self.coordinator.data.network.mac_address - - @property - def device_info(self) -> DeviceInfo: - """Return info about the emonitor device.""" - return DeviceInfo( - connections={(dr.CONNECTION_NETWORK_MAC, self.mac_address)}, - manufacturer="Powerhouse Dynamics, Inc.", - name=name_short_mac(self.mac_address[-6:]), - sw_version=self.coordinator.data.hardware.firmware_version, - ) + return {"channel": self.channel_number} diff --git a/homeassistant/components/emulated_hue/hue_api.py b/homeassistant/components/emulated_hue/hue_api.py index 210e95f13b7..6f8cf77b13d 100644 --- a/homeassistant/components/emulated_hue/hue_api.py +++ b/homeassistant/components/emulated_hue/hue_api.py @@ -26,14 +26,7 @@ from homeassistant.components.cover import ( ATTR_POSITION, SUPPORT_SET_POSITION, ) -from homeassistant.components.fan import ( - ATTR_SPEED, - SPEED_HIGH, - SPEED_LOW, - SPEED_MEDIUM, - SPEED_OFF, - SUPPORT_SET_SPEED, -) +from homeassistant.components.fan import ATTR_PERCENTAGE, SUPPORT_SET_SPEED from homeassistant.components.http import HomeAssistantView from homeassistant.components.humidifier.const import ( ATTR_HUMIDITY, @@ -540,14 +533,7 @@ class HueOneLightChangeView(HomeAssistantView): ): domain = entity.domain # Convert 0-100 to a fan speed - if (brightness := parsed[STATE_BRIGHTNESS]) == 0: - data[ATTR_SPEED] = SPEED_OFF - elif 0 < brightness <= 33.3: - data[ATTR_SPEED] = SPEED_LOW - elif 33.3 < brightness <= 66.6: - data[ATTR_SPEED] = SPEED_MEDIUM - elif 66.6 < brightness <= 100: - data[ATTR_SPEED] = SPEED_HIGH + data[ATTR_PERCENTAGE] = parsed[STATE_BRIGHTNESS] # Map the off command to on if entity.domain in config.off_maps_to_on_domains: @@ -679,15 +665,9 @@ def get_entity_state(config, entity): # Convert 0.0-1.0 to 0-254 data[STATE_BRIGHTNESS] = round(min(1.0, level) * HUE_API_STATE_BRI_MAX) elif entity.domain == fan.DOMAIN: - speed = entity.attributes.get(ATTR_SPEED, 0) - # Convert 0.0-1.0 to 0-254 - data[STATE_BRIGHTNESS] = 0 - if speed == SPEED_LOW: - data[STATE_BRIGHTNESS] = 85 - elif speed == SPEED_MEDIUM: - data[STATE_BRIGHTNESS] = 170 - elif speed == SPEED_HIGH: - data[STATE_BRIGHTNESS] = HUE_API_STATE_BRI_MAX + percentage = entity.attributes.get(ATTR_PERCENTAGE) or 0 + # Convert 0-100 to 0-254 + data[STATE_BRIGHTNESS] = round(percentage * HUE_API_STATE_BRI_MAX / 100) elif entity.domain == cover.DOMAIN: level = entity.attributes.get(ATTR_CURRENT_POSITION, 0) data[STATE_BRIGHTNESS] = round(level / 100 * HUE_API_STATE_BRI_MAX) diff --git a/homeassistant/components/emulated_kasa/__init__.py b/homeassistant/components/emulated_kasa/__init__.py index 967edc8d157..9643962ecb4 100644 --- a/homeassistant/components/emulated_kasa/__init__.py +++ b/homeassistant/components/emulated_kasa/__init__.py @@ -5,7 +5,6 @@ from sense_energy import PlugInstance, SenseLink import voluptuous as vol from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN -from homeassistant.components.switch import ATTR_CURRENT_POWER_W from homeassistant.const import ( CONF_ENTITIES, CONF_NAME, @@ -105,8 +104,6 @@ async def validate_configs(hass, entity_configs): entity_config[CONF_POWER] = power_val elif state.domain == SENSOR_DOMAIN: pass - elif ATTR_CURRENT_POWER_W in state.attributes: - pass else: _LOGGER.debug("No power value defined for: %s", entity_id) @@ -132,8 +129,6 @@ def get_plug_devices(hass, entity_configs): power = float(hass.states.get(power_val).state) elif isinstance(power_val, Template): power = float(power_val.async_render()) - elif ATTR_CURRENT_POWER_W in state.attributes: - power = float(state.attributes[ATTR_CURRENT_POWER_W]) elif state.domain == SENSOR_DOMAIN: power = float(state.state) else: diff --git a/homeassistant/components/emulated_roku/strings.json b/homeassistant/components/emulated_roku/strings.json index ef05c1265cc..fe5f603b04a 100644 --- a/homeassistant/components/emulated_roku/strings.json +++ b/homeassistant/components/emulated_roku/strings.json @@ -1,7 +1,9 @@ { "title": "Emulated Roku", "config": { - "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/energy/data.py b/homeassistant/components/energy/data.py index d07d3406073..d33f915628d 100644 --- a/homeassistant/components/energy/data.py +++ b/homeassistant/components/energy/data.py @@ -291,7 +291,7 @@ class EnergyManager: "device_consumption", ): if key in update: - data[key] = update[key] # type: ignore[misc] + data[key] = update[key] # type: ignore[literal-required] self.data = data self._store.async_delay_save(lambda: cast(dict, self.data), 60) diff --git a/homeassistant/components/enocean/device.py b/homeassistant/components/enocean/device.py index 36477d21cff..b57b053f4a7 100644 --- a/homeassistant/components/enocean/device.py +++ b/homeassistant/components/enocean/device.py @@ -2,6 +2,7 @@ from enocean.protocol.packet import Packet from enocean.utils import combine_hex +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity from .const import SIGNAL_RECEIVE_MESSAGE, SIGNAL_SEND_MESSAGE @@ -18,8 +19,8 @@ class EnOceanEntity(Entity): async def async_added_to_hass(self): """Register callbacks.""" self.async_on_remove( - self.hass.helpers.dispatcher.async_dispatcher_connect( - SIGNAL_RECEIVE_MESSAGE, self._message_received_callback + async_dispatcher_connect( + self.hass, SIGNAL_RECEIVE_MESSAGE, self._message_received_callback ) ) diff --git a/homeassistant/components/enocean/translations/fr.json b/homeassistant/components/enocean/translations/fr.json index d2eda66257e..a5aa07870af 100644 --- a/homeassistant/components/enocean/translations/fr.json +++ b/homeassistant/components/enocean/translations/fr.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "invalid_dongle_path": "Lien vers la cl\u00e9 USB invalide", + "invalid_dongle_path": "Chemin d'acc\u00e8s \u00e0 la cl\u00e9 non valide", "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." }, "error": { diff --git a/homeassistant/components/enphase_envoy/__init__.py b/homeassistant/components/enphase_envoy/__init__.py index 7b3765bd25c..696baa31775 100644 --- a/homeassistant/components/enphase_envoy/__init__.py +++ b/homeassistant/components/enphase_envoy/__init__.py @@ -11,7 +11,7 @@ import httpx from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryAuthFailed +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers.httpx_client import get_async_client from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -38,7 +38,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_update_data(): """Fetch data from API endpoint.""" - data = {} async with async_timeout.timeout(30): try: await envoy_reader.getData() @@ -47,15 +46,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except httpx.HTTPError as err: raise UpdateFailed(f"Error communicating with API: {err}") from err - for description in SENSORS: - if description.key != "inverters": - data[description.key] = await getattr( - envoy_reader, description.key - )() - else: - data[ - "inverters_production" - ] = await envoy_reader.inverters_production() + data = { + description.key: await getattr(envoy_reader, description.key)() + for description in SENSORS + } + data["inverters_production"] = await envoy_reader.inverters_production() _LOGGER.debug("Retrieved data from API: %s", data) @@ -78,8 +73,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if not entry.unique_id: try: serial = await envoy_reader.get_full_serial_number() - except httpx.HTTPError: - pass + except httpx.HTTPError as ex: + raise ConfigEntryNotReady( + f"Could not obtain serial number from envoy: {ex}" + ) from ex else: hass.config_entries.async_update_entry(entry, unique_id=serial) diff --git a/homeassistant/components/enphase_envoy/const.py b/homeassistant/components/enphase_envoy/const.py index 747a4886f15..c79c3af604b 100644 --- a/homeassistant/components/enphase_envoy/const.py +++ b/homeassistant/components/enphase_envoy/const.py @@ -71,10 +71,4 @@ SENSORS = ( state_class=SensorStateClass.TOTAL_INCREASING, device_class=SensorDeviceClass.ENERGY, ), - SensorEntityDescription( - key="inverters", - name="Inverter", - native_unit_of_measurement=POWER_WATT, - state_class=SensorStateClass.MEASUREMENT, - ), ) diff --git a/homeassistant/components/enphase_envoy/manifest.json b/homeassistant/components/enphase_envoy/manifest.json index a27b5a6bc79..4ecd90867b0 100644 --- a/homeassistant/components/enphase_envoy/manifest.json +++ b/homeassistant/components/enphase_envoy/manifest.json @@ -2,12 +2,8 @@ "domain": "enphase_envoy", "name": "Enphase Envoy", "documentation": "https://www.home-assistant.io/integrations/enphase_envoy", - "requirements": [ - "envoy_reader==0.20.1" - ], - "codeowners": [ - "@gtdiehl" - ], + "requirements": ["envoy_reader==0.20.1"], + "codeowners": ["@gtdiehl"], "config_flow": true, "zeroconf": [ { diff --git a/homeassistant/components/enphase_envoy/sensor.py b/homeassistant/components/enphase_envoy/sensor.py index 03431ddc3ea..c8d791907a6 100644 --- a/homeassistant/components/enphase_envoy/sensor.py +++ b/homeassistant/components/enphase_envoy/sensor.py @@ -1,16 +1,77 @@ """Support for Enphase Envoy solar energy monitor.""" from __future__ import annotations -from homeassistant.components.sensor import SensorEntity +from collections.abc import Callable +from dataclasses import dataclass +import datetime +import logging +from typing import cast + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) from homeassistant.config_entries import ConfigEntry +from homeassistant.const import POWER_WATT 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 homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) +from homeassistant.util import dt as dt_util from .const import COORDINATOR, DOMAIN, NAME, SENSORS ICON = "mdi:flash" +_LOGGER = logging.getLogger(__name__) + +INVERTERS_KEY = "inverters" +LAST_REPORTED_KEY = "last_reported" + + +@dataclass +class EnvoyRequiredKeysMixin: + """Mixin for required keys.""" + + value_fn: Callable[[tuple[float, str]], datetime.datetime | float | None] + + +@dataclass +class EnvoySensorEntityDescription(SensorEntityDescription, EnvoyRequiredKeysMixin): + """Describes an Envoy inverter sensor entity.""" + + +def _inverter_last_report_time( + watt_report_time: tuple[float, str] +) -> datetime.datetime | None: + if (report_time := watt_report_time[1]) is None: + return None + if (last_reported_dt := dt_util.parse_datetime(report_time)) is None: + return None + if last_reported_dt.tzinfo is None: + return last_reported_dt.replace(tzinfo=dt_util.UTC) + return last_reported_dt + + +INVERTER_SENSORS = ( + EnvoySensorEntityDescription( + key=INVERTERS_KEY, + native_unit_of_measurement=POWER_WATT, + state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda watt_report_time: watt_report_time[0], + ), + EnvoySensorEntityDescription( + key=LAST_REPORTED_KEY, + name="Last Reported", + device_class=SensorDeviceClass.TIMESTAMP, + entity_registry_enabled_default=False, + value_fn=_inverter_last_report_time, + ), +) async def async_setup_entry( @@ -19,129 +80,116 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up envoy sensor platform.""" - data = hass.data[DOMAIN][config_entry.entry_id] - coordinator = data[COORDINATOR] - name = data[NAME] + data: dict = hass.data[DOMAIN][config_entry.entry_id] + coordinator: DataUpdateCoordinator = data[COORDINATOR] + envoy_data: dict = coordinator.data + envoy_name: str = data[NAME] + envoy_serial_num = config_entry.unique_id + assert envoy_serial_num is not None + _LOGGER.debug("Envoy data: %s", envoy_data) - entities = [] - for sensor_description in SENSORS: - if ( - sensor_description.key == "inverters" - and coordinator.data.get("inverters_production") is not None - ): - for inverter in coordinator.data["inverters_production"]: - entity_name = f"{name} {sensor_description.name} {inverter}" - split_name = entity_name.split(" ") - serial_number = split_name[-1] - entities.append( - Envoy( - sensor_description, - entity_name, - name, - config_entry.unique_id, - serial_number, - coordinator, - ) - ) - elif sensor_description.key != "inverters": - data = coordinator.data.get(sensor_description.key) - if isinstance(data, str) and "not available" in data: - continue - - entity_name = f"{name} {sensor_description.name}" - entities.append( - Envoy( - sensor_description, - entity_name, - name, - config_entry.unique_id, - None, - coordinator, - ) + entities: list[Envoy | EnvoyInverter] = [] + for description in SENSORS: + sensor_data = envoy_data.get(description.key) + if isinstance(sensor_data, str) and "not available" in sensor_data: + continue + entities.append( + Envoy( + coordinator, + description, + envoy_name, + envoy_serial_num, ) + ) + + if production := envoy_data.get("inverters_production"): + entities.extend( + EnvoyInverter( + coordinator, + description, + envoy_name, + envoy_serial_num, + str(inverter), + ) + for description in INVERTER_SENSORS + for inverter in production + ) async_add_entities(entities) class Envoy(CoordinatorEntity, SensorEntity): - """Envoy entity.""" + """Envoy inverter entity.""" + + _attr_icon = ICON def __init__( self, - description, - name, - device_name, - device_serial_number, - serial_number, - coordinator, - ): + coordinator: DataUpdateCoordinator, + description: SensorEntityDescription, + envoy_name: str, + envoy_serial_num: str, + ) -> None: """Initialize Envoy entity.""" self.entity_description = description - self._name = name - self._serial_number = serial_number - self._device_name = device_name - self._device_serial_number = device_serial_number - + self._attr_name = f"{envoy_name} {description.name}" + self._attr_unique_id = f"{envoy_serial_num}_{description.key}" + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, envoy_serial_num)}, + manufacturer="Enphase", + model="Envoy", + name=envoy_name, + ) super().__init__(coordinator) @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def unique_id(self): - """Return the unique id of the sensor.""" - if self._serial_number: - return self._serial_number - if self._device_serial_number: - return f"{self._device_serial_number}_{self.entity_description.key}" - - @property - def native_value(self): + def native_value(self) -> float | None: """Return the state of the sensor.""" - if self.entity_description.key != "inverters": - value = self.coordinator.data.get(self.entity_description.key) + if (value := self.coordinator.data.get(self.entity_description.key)) is None: + return None + return cast(float, value) - elif ( - self.entity_description.key == "inverters" - and self.coordinator.data.get("inverters_production") is not None - ): - value = self.coordinator.data.get("inverters_production").get( - self._serial_number - )[0] + +class EnvoyInverter(CoordinatorEntity, SensorEntity): + """Envoy inverter entity.""" + + _attr_icon = ICON + entity_description: EnvoySensorEntityDescription + + def __init__( + self, + coordinator: DataUpdateCoordinator, + description: EnvoySensorEntityDescription, + envoy_name: str, + envoy_serial_num: str, + serial_number: str, + ) -> None: + """Initialize Envoy inverter entity.""" + self.entity_description = description + self._serial_number = serial_number + if description.name: + self._attr_name = ( + f"{envoy_name} Inverter {serial_number} {description.name}" + ) else: - return None - - return value - - @property - def icon(self): - """Icon to use in the frontend, if any.""" - return ICON - - @property - def extra_state_attributes(self): - """Return the state attributes.""" - if ( - self.entity_description.key == "inverters" - and self.coordinator.data.get("inverters_production") is not None - ): - value = self.coordinator.data.get("inverters_production").get( - self._serial_number - )[1] - return {"last_reported": value} - - return None - - @property - def device_info(self) -> DeviceInfo | None: - """Return the device_info of the device.""" - if not self._device_serial_number: - return None - return DeviceInfo( - identifiers={(DOMAIN, str(self._device_serial_number))}, + self._attr_name = f"{envoy_name} Inverter {serial_number}" + if description.key == INVERTERS_KEY: + self._attr_unique_id = serial_number + else: + self._attr_unique_id = f"{serial_number}_{description.key}" + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, serial_number)}, + name=f"Inverter {serial_number}", manufacturer="Enphase", - model="Envoy", - name=self._device_name, + model="Inverter", + via_device=(DOMAIN, envoy_serial_num), ) + super().__init__(coordinator) + + @property + def native_value(self) -> datetime.datetime | float | None: + """Return the state of the sensor.""" + watt_report_time: tuple[float, str] = self.coordinator.data[ + "inverters_production" + ][self._serial_number] + return self.entity_description.value_fn(watt_report_time) diff --git a/homeassistant/components/enphase_envoy/translations/fr.json b/homeassistant/components/enphase_envoy/translations/fr.json index 165c54c67d1..b11196812b2 100644 --- a/homeassistant/components/enphase_envoy/translations/fr.json +++ b/homeassistant/components/enphase_envoy/translations/fr.json @@ -6,7 +6,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "flow_title": "{serial} ({host})", diff --git a/homeassistant/components/environment_canada/manifest.json b/homeassistant/components/environment_canada/manifest.json index 4d1f1ecdff0..a0399629030 100644 --- a/homeassistant/components/environment_canada/manifest.json +++ b/homeassistant/components/environment_canada/manifest.json @@ -2,7 +2,7 @@ "domain": "environment_canada", "name": "Environment Canada", "documentation": "https://www.home-assistant.io/integrations/environment_canada", - "requirements": ["env_canada==0.5.20"], + "requirements": ["env_canada==0.5.21"], "codeowners": ["@gwww", "@michaeldavie"], "config_flow": true, "iot_class": "cloud_polling", diff --git a/homeassistant/components/environment_canada/sensor.py b/homeassistant/components/environment_canada/sensor.py index 4cfe6bbbfb2..2e124d1ec7c 100644 --- a/homeassistant/components/environment_canada/sensor.py +++ b/homeassistant/components/environment_canada/sensor.py @@ -209,13 +209,23 @@ SENSOR_TYPES: tuple[ECSensorEntityDescription, ...] = ( ), ) + +def _get_aqhi_value(data): + if (aqhi := data.current) is not None: + return aqhi + if data.forecasts and (hourly := data.forecasts.get("hourly")) is not None: + if values := list(hourly.values()): + return values[0] + return None + + AQHI_SENSOR = ECSensorEntityDescription( key="aqhi", name="AQHI", device_class=SensorDeviceClass.AQI, native_unit_of_measurement="AQI", state_class=SensorStateClass.MEASUREMENT, - value_fn=lambda data: data.current, + value_fn=_get_aqhi_value, ) ALERT_TYPES: tuple[ECSensorEntityDescription, ...] = ( diff --git a/homeassistant/components/environment_canada/translations/fr.json b/homeassistant/components/environment_canada/translations/fr.json index d09b1eec095..20e95a1e646 100644 --- a/homeassistant/components/environment_canada/translations/fr.json +++ b/homeassistant/components/environment_canada/translations/fr.json @@ -1,7 +1,7 @@ { "config": { "error": { - "bad_station_id": "L'ID de station est invalide, manquant ou introuvable dans la base de donn\u00e9es d'ID de station", + "bad_station_id": "L'ID de station est non valide, manquant ou introuvable dans la base de donn\u00e9es des ID de stations", "cannot_connect": "\u00c9chec de connexion", "error_response": "R\u00e9ponse d'Environnement Canada par erreur", "too_many_attempts": "Les connexions \u00e0 Environnement Canada sont limit\u00e9es en termes de taux; R\u00e9essayez dans 60 secondes", diff --git a/homeassistant/components/environment_canada/weather.py b/homeassistant/components/environment_canada/weather.py index 4d226261b94..1b81750fb47 100644 --- a/homeassistant/components/environment_canada/weather.py +++ b/homeassistant/components/environment_canada/weather.py @@ -80,12 +80,16 @@ class ECWeather(CoordinatorEntity, WeatherEntity): @property def temperature(self): """Return the temperature.""" - if self.ec_data.conditions.get("temperature", {}).get("value"): - return float(self.ec_data.conditions["temperature"]["value"]) - if self.ec_data.hourly_forecasts and self.ec_data.hourly_forecasts[0].get( - "temperature" + if ( + temperature := self.ec_data.conditions.get("temperature", {}).get("value") + ) is not None: + return float(temperature) + if ( + self.ec_data.hourly_forecasts + and (temperature := self.ec_data.hourly_forecasts[0].get("temperature")) + is not None ): - return float(self.ec_data.hourly_forecasts[0]["temperature"]) + return float(temperature) return None @property diff --git a/homeassistant/components/envirophat/__init__.py b/homeassistant/components/envirophat/__init__.py deleted file mode 100644 index 68d3a99441c..00000000000 --- a/homeassistant/components/envirophat/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The envirophat component.""" diff --git a/homeassistant/components/envirophat/manifest.json b/homeassistant/components/envirophat/manifest.json deleted file mode 100644 index 9bb90facbf3..00000000000 --- a/homeassistant/components/envirophat/manifest.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "domain": "envirophat", - "name": "Enviro pHAT", - "documentation": "https://www.home-assistant.io/integrations/envirophat", - "requirements": ["envirophat==0.0.6", "smbus-cffi==0.5.1"], - "codeowners": [], - "iot_class": "local_polling" -} diff --git a/homeassistant/components/envirophat/sensor.py b/homeassistant/components/envirophat/sensor.py deleted file mode 100644 index 0b8634d019d..00000000000 --- a/homeassistant/components/envirophat/sensor.py +++ /dev/null @@ -1,281 +0,0 @@ -"""Support for Enviro pHAT sensors.""" -from __future__ import annotations - -from datetime import timedelta -import importlib -import logging - -import voluptuous as vol - -from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, - SensorDeviceClass, - SensorEntity, - SensorEntityDescription, -) -from homeassistant.const import ( - CONF_DISPLAY_OPTIONS, - CONF_NAME, - ELECTRIC_POTENTIAL_VOLT, - PRESSURE_HPA, - TEMP_CELSIUS, -) -from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from homeassistant.util import Throttle - -_LOGGER = logging.getLogger(__name__) - -DEFAULT_NAME = "envirophat" -CONF_USE_LEDS = "use_leds" - -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) - -SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( - SensorEntityDescription( - key="light", - name="light", - icon="mdi:weather-sunny", - ), - SensorEntityDescription( - key="light_red", - name="light_red", - icon="mdi:invert-colors", - ), - SensorEntityDescription( - key="light_green", - name="light_green", - icon="mdi:invert-colors", - ), - SensorEntityDescription( - key="light_blue", - name="light_blue", - icon="mdi:invert-colors", - ), - SensorEntityDescription( - key="accelerometer_x", - name="accelerometer_x", - native_unit_of_measurement="G", - icon="mdi:earth", - ), - SensorEntityDescription( - key="accelerometer_y", - name="accelerometer_y", - native_unit_of_measurement="G", - icon="mdi:earth", - ), - SensorEntityDescription( - key="accelerometer_z", - name="accelerometer_z", - native_unit_of_measurement="G", - icon="mdi:earth", - ), - SensorEntityDescription( - key="magnetometer_x", - name="magnetometer_x", - icon="mdi:magnet", - ), - SensorEntityDescription( - key="magnetometer_y", - name="magnetometer_y", - icon="mdi:magnet", - ), - SensorEntityDescription( - key="magnetometer_z", - name="magnetometer_z", - icon="mdi:magnet", - ), - SensorEntityDescription( - key="temperature", - name="temperature", - native_unit_of_measurement=TEMP_CELSIUS, - device_class=SensorDeviceClass.TEMPERATURE, - ), - SensorEntityDescription( - key="pressure", - name="pressure", - native_unit_of_measurement=PRESSURE_HPA, - icon="mdi:gauge", - ), - SensorEntityDescription( - key="voltage_0", - name="voltage_0", - native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, - icon="mdi:flash", - ), - SensorEntityDescription( - key="voltage_1", - name="voltage_1", - native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, - icon="mdi:flash", - ), - SensorEntityDescription( - key="voltage_2", - name="voltage_2", - native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, - icon="mdi:flash", - ), - SensorEntityDescription( - key="voltage_3", - name="voltage_3", - native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, - icon="mdi:flash", - ), -) - -SENSOR_KEYS: list[str] = [desc.key for desc in SENSOR_TYPES] - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_DISPLAY_OPTIONS, default=SENSOR_KEYS): [vol.In(SENSOR_KEYS)], - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_USE_LEDS, default=False): cv.boolean, - } -) - - -def setup_platform( - hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the Sense HAT sensor platform.""" - _LOGGER.warning( - "The Enviro pHAT integration is deprecated and will be removed " - "in Home Assistant Core 2022.4; this integration is removed under " - "Architectural Decision Record 0019, more information can be found here: " - "https://github.com/home-assistant/architecture/blob/master/adr/0019-GPIO.md" - ) - - try: - envirophat = importlib.import_module("envirophat") - except OSError: - _LOGGER.error("No Enviro pHAT was found") - return - - data = EnvirophatData(envirophat, config.get(CONF_USE_LEDS)) - - display_options = config[CONF_DISPLAY_OPTIONS] - entities = [ - EnvirophatSensor(data, description) - for description in SENSOR_TYPES - if description.key in display_options - ] - add_entities(entities, True) - - -class EnvirophatSensor(SensorEntity): - """Representation of an Enviro pHAT sensor.""" - - def __init__(self, data, description: SensorEntityDescription): - """Initialize the sensor.""" - self.entity_description = description - self.data = data - - def update(self): - """Get the latest data and updates the states.""" - self.data.update() - - sensor_type = self.entity_description.key - if sensor_type == "light": - self._attr_native_value = self.data.light - elif sensor_type == "light_red": - self._attr_native_value = self.data.light_red - elif sensor_type == "light_green": - self._attr_native_value = self.data.light_green - elif sensor_type == "light_blue": - self._attr_native_value = self.data.light_blue - elif sensor_type == "accelerometer_x": - self._attr_native_value = self.data.accelerometer_x - elif sensor_type == "accelerometer_y": - self._attr_native_value = self.data.accelerometer_y - elif sensor_type == "accelerometer_z": - self._attr_native_value = self.data.accelerometer_z - elif sensor_type == "magnetometer_x": - self._attr_native_value = self.data.magnetometer_x - elif sensor_type == "magnetometer_y": - self._attr_native_value = self.data.magnetometer_y - elif sensor_type == "magnetometer_z": - self._attr_native_value = self.data.magnetometer_z - elif sensor_type == "temperature": - self._attr_native_value = self.data.temperature - elif sensor_type == "pressure": - self._attr_native_value = self.data.pressure - elif sensor_type == "voltage_0": - self._attr_native_value = self.data.voltage_0 - elif sensor_type == "voltage_1": - self._attr_native_value = self.data.voltage_1 - elif sensor_type == "voltage_2": - self._attr_native_value = self.data.voltage_2 - elif sensor_type == "voltage_3": - self._attr_native_value = self.data.voltage_3 - - -class EnvirophatData: - """Get the latest data and update.""" - - def __init__(self, envirophat, use_leds): - """Initialize the data object.""" - self.envirophat = envirophat - self.use_leds = use_leds - # sensors readings - self.light = None - self.light_red = None - self.light_green = None - self.light_blue = None - self.accelerometer_x = None - self.accelerometer_y = None - self.accelerometer_z = None - self.magnetometer_x = None - self.magnetometer_y = None - self.magnetometer_z = None - self.temperature = None - self.pressure = None - self.voltage_0 = None - self.voltage_1 = None - self.voltage_2 = None - self.voltage_3 = None - - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self): - """Get the latest data from Enviro pHAT.""" - # Light sensor reading: 16-bit integer - self.light = self.envirophat.light.light() - if self.use_leds: - self.envirophat.leds.on() - # the three color values scaled against the overall light, 0-255 - self.light_red, self.light_green, self.light_blue = self.envirophat.light.rgb() - if self.use_leds: - self.envirophat.leds.off() - - # accelerometer readings in G - ( - self.accelerometer_x, - self.accelerometer_y, - self.accelerometer_z, - ) = self.envirophat.motion.accelerometer() - - # raw magnetometer reading - ( - self.magnetometer_x, - self.magnetometer_y, - self.magnetometer_z, - ) = self.envirophat.motion.magnetometer() - - # temperature resolution of BMP280 sensor: 0.01°C - self.temperature = round(self.envirophat.weather.temperature(), 2) - - # pressure resolution of BMP280 sensor: 0.16 Pa, rounding to 0.1 Pa - # with conversion to 100 Pa = 1 hPa - self.pressure = round(self.envirophat.weather.pressure() / 100.0, 3) - - # Voltage sensor, reading between 0-3.3V - ( - self.voltage_0, - self.voltage_1, - self.voltage_2, - self.voltage_3, - ) = self.envirophat.analog.read_all() diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index 0154e2eba28..8aa2dba4d07 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -31,6 +31,7 @@ from homeassistant import const from homeassistant.components import zeroconf from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( + ATTR_DEVICE_ID, CONF_HOST, CONF_MODE, CONF_PASSWORD, @@ -192,7 +193,13 @@ async def async_setup_entry( # noqa: C901 hass.async_create_task(tag.async_scan_tag(tag_id, device_id)) return - hass.bus.async_fire(service.service, service_data) + hass.bus.async_fire( + service.service, + { + ATTR_DEVICE_ID: device_id, + **service_data, + }, + ) else: hass.async_create_task( hass.services.async_call( @@ -513,6 +520,7 @@ async def _cleanup_instance( data = domain_data.pop_entry_data(entry) for disconnect_cb in data.disconnect_callbacks: disconnect_cb() + data.disconnect_callbacks = [] for cleanup_callback in data.cleanup_callbacks: cleanup_callback() await data.client.disconnect() diff --git a/homeassistant/components/esphome/fan.py b/homeassistant/components/esphome/fan.py index 5ae4a47c52e..91cb8bb6fdb 100644 --- a/homeassistant/components/esphome/fan.py +++ b/homeassistant/components/esphome/fan.py @@ -92,7 +92,6 @@ class EsphomeFan(EsphomeEntity[FanInfo, FanState], FanEntity): async def async_turn_on( self, - speed: str | None = None, percentage: int | None = None, preset_mode: str | None = None, **kwargs: Any, diff --git a/homeassistant/components/esphome/translations/fr.json b/homeassistant/components/esphome/translations/fr.json index 7125ef9c395..330c2823409 100644 --- a/homeassistant/components/esphome/translations/fr.json +++ b/homeassistant/components/esphome/translations/fr.json @@ -7,7 +7,7 @@ }, "error": { "connection_error": "Impossible de se connecter \u00e0 ESP. Assurez-vous que votre fichier YAML contient une ligne 'api:'.", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "invalid_psk": "La cl\u00e9 de chiffrement de transport n\u2019est pas valide. Assurez-vous qu\u2019elle correspond \u00e0 ce que vous avez dans votre configuration", "resolve_error": "Impossible de r\u00e9soudre l'adresse de l'ESP. Si cette erreur persiste, veuillez d\u00e9finir une adresse IP statique: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" }, diff --git a/homeassistant/components/evil_genius_labs/__init__.py b/homeassistant/components/evil_genius_labs/__init__.py index 2fbd85938f0..ff858db566a 100644 --- a/homeassistant/components/evil_genius_labs/__init__.py +++ b/homeassistant/components/evil_genius_labs/__init__.py @@ -11,12 +11,12 @@ import pyevilgenius from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant -from homeassistant.helpers import ( - aiohttp_client, - device_registry as dr, - update_coordinator, -) +from homeassistant.helpers import aiohttp_client, device_registry as dr from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) from .const import DOMAIN @@ -49,7 +49,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return unload_ok -class EvilGeniusUpdateCoordinator(update_coordinator.DataUpdateCoordinator[dict]): +class EvilGeniusUpdateCoordinator(DataUpdateCoordinator[dict]): """Update coordinator for Evil Genius data.""" info: dict @@ -81,11 +81,9 @@ class EvilGeniusUpdateCoordinator(update_coordinator.DataUpdateCoordinator[dict] return cast(dict, await self.client.get_data()) -class EvilGeniusEntity(update_coordinator.CoordinatorEntity): +class EvilGeniusEntity(CoordinatorEntity[EvilGeniusUpdateCoordinator]): """Base entity for Evil Genius.""" - coordinator: EvilGeniusUpdateCoordinator - @property def device_info(self) -> DeviceInfo: """Return device info.""" diff --git a/homeassistant/components/evil_genius_labs/translations/fr.json b/homeassistant/components/evil_genius_labs/translations/fr.json index 80b3d803851..e7f5b200894 100644 --- a/homeassistant/components/evil_genius_labs/translations/fr.json +++ b/homeassistant/components/evil_genius_labs/translations/fr.json @@ -2,7 +2,7 @@ "config": { "error": { "cannot_connect": "\u00c9chec de connexion", - "timeout": "D\u00e9lai d'attente pour \u00e9tablir la connexion", + "timeout": "D\u00e9lai d'attente pour \u00e9tablir la connexion expir\u00e9", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/evil_genius_labs/translations/he.json b/homeassistant/components/evil_genius_labs/translations/he.json index 00011f86933..ab2db7eb7d0 100644 --- a/homeassistant/components/evil_genius_labs/translations/he.json +++ b/homeassistant/components/evil_genius_labs/translations/he.json @@ -2,6 +2,7 @@ "config": { "error": { "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "timeout": "\u05e4\u05e1\u05e7 \u05d6\u05de\u05df \u05dc\u05d9\u05e6\u05d9\u05e8\u05ea \u05d7\u05d9\u05d1\u05d5\u05e8", "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" }, "step": { diff --git a/homeassistant/components/evohome/services.yaml b/homeassistant/components/evohome/services.yaml index bdcb116e4e3..52428dd5e1e 100644 --- a/homeassistant/components/evohome/services.yaml +++ b/homeassistant/components/evohome/services.yaml @@ -14,12 +14,12 @@ set_system_mode: selector: select: options: - - 'Auto' - - 'AutoWithEco' - - 'Away' - - 'Custom' - - 'DayOff' - - 'HeatingOff' + - "Auto" + - "AutoWithEco" + - "Away" + - "Custom" + - "DayOff" + - "HeatingOff" period: name: Period description: >- diff --git a/homeassistant/components/ezviz/binary_sensor.py b/homeassistant/components/ezviz/binary_sensor.py index 942ceeecdb2..43ac914a50c 100644 --- a/homeassistant/components/ezviz/binary_sensor.py +++ b/homeassistant/components/ezviz/binary_sensor.py @@ -54,8 +54,6 @@ async def async_setup_entry( class EzvizBinarySensor(EzvizEntity, BinarySensorEntity): """Representation of a Ezviz sensor.""" - coordinator: EzvizDataUpdateCoordinator - def __init__( self, coordinator: EzvizDataUpdateCoordinator, diff --git a/homeassistant/components/ezviz/camera.py b/homeassistant/components/ezviz/camera.py index 6680466ecf0..ed48ed4ee03 100644 --- a/homeassistant/components/ezviz/camera.py +++ b/homeassistant/components/ezviz/camera.py @@ -7,18 +7,16 @@ from pyezviz.exceptions import HTTPError, InvalidHost, PyEzvizError import voluptuous as vol from homeassistant.components import ffmpeg -from homeassistant.components.camera import PLATFORM_SCHEMA, SUPPORT_STREAM, Camera +from homeassistant.components.camera import SUPPORT_STREAM, Camera from homeassistant.components.ffmpeg import get_ffmpeg_manager from homeassistant.config_entries import ( SOURCE_IGNORE, - SOURCE_IMPORT, SOURCE_INTEGRATION_DISCOVERY, ConfigEntry, ) from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv, entity_platform -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .const import ( ATTR_DIRECTION, @@ -27,7 +25,6 @@ from .const import ( ATTR_SERIAL, ATTR_SPEED, ATTR_TYPE, - CONF_CAMERAS, CONF_FFMPEG_ARGUMENTS, DATA_COORDINATOR, DEFAULT_CAMERA_USERNAME, @@ -47,62 +44,9 @@ from .const import ( from .coordinator import EzvizDataUpdateCoordinator from .entity import EzvizEntity -CAMERA_SCHEMA = vol.Schema( - {vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string} -) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_CAMERAS, default={}): {cv.string: CAMERA_SCHEMA}, - } -) - _LOGGER = logging.getLogger(__name__) -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: entity_platform.AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up a Ezviz IP Camera from platform config.""" - _LOGGER.warning( - "Loading ezviz via platform config is deprecated, it will be automatically imported. Please remove it afterwards" - ) - - # Check if entry config exists and skips import if it does. - if hass.config_entries.async_entries(DOMAIN): - return - - # Check if importing camera account. - if CONF_CAMERAS in config: - cameras_conf = config[CONF_CAMERAS] - for serial, camera in cameras_conf.items(): - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={ - ATTR_SERIAL: serial, - CONF_USERNAME: camera[CONF_USERNAME], - CONF_PASSWORD: camera[CONF_PASSWORD], - }, - ) - ) - - # Check if importing main ezviz cloud account. - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data=config, - ) - ) - - async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, @@ -228,8 +172,6 @@ async def async_setup_entry( class EzvizCamera(EzvizEntity, Camera): """An implementation of a Ezviz security camera.""" - coordinator: EzvizDataUpdateCoordinator - def __init__( self, hass: HomeAssistant, diff --git a/homeassistant/components/ezviz/config_flow.py b/homeassistant/components/ezviz/config_flow.py index 780d06383f9..36cf2ac456e 100644 --- a/homeassistant/components/ezviz/config_flow.py +++ b/homeassistant/components/ezviz/config_flow.py @@ -307,50 +307,6 @@ class EzvizConfigFlow(ConfigFlow, domain=DOMAIN): }, ) - async def async_step_import(self, import_config): - """Handle config import from yaml.""" - _LOGGER.debug("import config: %s", import_config) - - # Check importing camera. - if ATTR_SERIAL in import_config: - return await self.async_step_import_camera(import_config) - - # Validate and setup of main ezviz cloud account. - try: - return await self._validate_and_create_auth(import_config) - - except InvalidURL: - _LOGGER.error("Error importing Ezviz platform config: invalid host") - return self.async_abort(reason="invalid_host") - - except InvalidHost: - _LOGGER.error("Error importing Ezviz platform config: cannot connect") - return self.async_abort(reason="cannot_connect") - - except (AuthTestResultFailed, PyEzvizError): - _LOGGER.error("Error importing Ezviz platform config: invalid auth") - return self.async_abort(reason="invalid_auth") - - except Exception: # pylint: disable=broad-except - _LOGGER.exception( - "Error importing ezviz platform config: unexpected exception" - ) - - return self.async_abort(reason="unknown") - - async def async_step_import_camera(self, data): - """Create RTSP auth entry per camera in config.""" - - await self.async_set_unique_id(data[ATTR_SERIAL]) - self._abort_if_unique_id_configured() - - _LOGGER.debug("Create camera with: %s", data) - - cam_serial = data.pop(ATTR_SERIAL) - data[CONF_TYPE] = ATTR_TYPE_CAMERA - - return self.async_create_entry(title=cam_serial, data=data) - class EzvizOptionsFlowHandler(OptionsFlow): """Handle Ezviz client options.""" diff --git a/homeassistant/components/ezviz/const.py b/homeassistant/components/ezviz/const.py index 6131904be99..5340f48d0f6 100644 --- a/homeassistant/components/ezviz/const.py +++ b/homeassistant/components/ezviz/const.py @@ -5,7 +5,6 @@ MANUFACTURER = "Ezviz" # Configuration ATTR_SERIAL = "serial" -CONF_CAMERAS = "cameras" CONF_FFMPEG_ARGUMENTS = "ffmpeg_arguments" ATTR_HOME = "HOME_MODE" ATTR_AWAY = "AWAY_MODE" diff --git a/homeassistant/components/ezviz/entity.py b/homeassistant/components/ezviz/entity.py index 288c4a5d9eb..2ab42a93286 100644 --- a/homeassistant/components/ezviz/entity.py +++ b/homeassistant/components/ezviz/entity.py @@ -10,7 +10,7 @@ from .const import DOMAIN, MANUFACTURER from .coordinator import EzvizDataUpdateCoordinator -class EzvizEntity(CoordinatorEntity, Entity): +class EzvizEntity(CoordinatorEntity[EzvizDataUpdateCoordinator], Entity): """Generic entity encapsulating common features of Ezviz device.""" def __init__( diff --git a/homeassistant/components/ezviz/services.yaml b/homeassistant/components/ezviz/services.yaml index 2635662e636..9733d7418a3 100644 --- a/homeassistant/components/ezviz/services.yaml +++ b/homeassistant/components/ezviz/services.yaml @@ -61,8 +61,8 @@ set_alarm_detection_sensibility: fields: level: name: Sensitivity Level - description: 'Sensibility level (1-6) for type 0 (Normal camera) - or (1-100) for type 3 (PIR sensor camera).' + description: "Sensibility level (1-6) for type 0 (Normal camera) + or (1-100) for type 3 (PIR sensor camera)." required: true example: 3 default: 3 @@ -74,15 +74,15 @@ set_alarm_detection_sensibility: mode: box type_value: name: Detection type - description: 'Type of detection. Options : 0 - Camera or 3 - PIR Sensor Camera' + description: "Type of detection. Options : 0 - Camera or 3 - PIR Sensor Camera" required: true - example: '0' - default: '0' + example: "0" + default: "0" selector: select: options: - - '0' - - '3' + - "0" + - "3" sound_alarm: name: Sound Alarm description: Sounds the alarm on your camera. diff --git a/homeassistant/components/ezviz/switch.py b/homeassistant/components/ezviz/switch.py index ea8f1e83f70..c2e562f62da 100644 --- a/homeassistant/components/ezviz/switch.py +++ b/homeassistant/components/ezviz/switch.py @@ -39,7 +39,6 @@ async def async_setup_entry( class EzvizSwitch(EzvizEntity, SwitchEntity): """Representation of a Ezviz sensor.""" - coordinator: EzvizDataUpdateCoordinator _attr_device_class = SwitchDeviceClass.SWITCH def __init__( diff --git a/homeassistant/components/ezviz/translations/fr.json b/homeassistant/components/ezviz/translations/fr.json index ddce689a2ba..475eb0bcfc7 100644 --- a/homeassistant/components/ezviz/translations/fr.json +++ b/homeassistant/components/ezviz/translations/fr.json @@ -7,7 +7,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "invalid_host": "Nom d'h\u00f4te ou adresse IP non valide" }, "flow_title": "{serial}", diff --git a/homeassistant/components/fan/__init__.py b/homeassistant/components/fan/__init__.py index 87f50c7d938..89de0cabb10 100644 --- a/homeassistant/components/fan/__init__.py +++ b/homeassistant/components/fan/__init__.py @@ -28,8 +28,6 @@ from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass from homeassistant.util.percentage import ( - ordered_list_item_to_percentage, - percentage_to_ordered_list_item, percentage_to_ranged_value, ranged_value_to_percentage, ) @@ -47,7 +45,6 @@ SUPPORT_OSCILLATE = 2 SUPPORT_DIRECTION = 4 SUPPORT_PRESET_MODE = 8 -SERVICE_SET_SPEED = "set_speed" SERVICE_INCREASE_SPEED = "increase_speed" SERVICE_DECREASE_SPEED = "decrease_speed" SERVICE_OSCILLATE = "oscillate" @@ -55,37 +52,16 @@ SERVICE_SET_DIRECTION = "set_direction" SERVICE_SET_PERCENTAGE = "set_percentage" SERVICE_SET_PRESET_MODE = "set_preset_mode" -SPEED_OFF = "off" -SPEED_LOW = "low" -SPEED_MEDIUM = "medium" -SPEED_HIGH = "high" - DIRECTION_FORWARD = "forward" DIRECTION_REVERSE = "reverse" -ATTR_SPEED = "speed" ATTR_PERCENTAGE = "percentage" ATTR_PERCENTAGE_STEP = "percentage_step" -ATTR_SPEED_LIST = "speed_list" ATTR_OSCILLATING = "oscillating" ATTR_DIRECTION = "direction" ATTR_PRESET_MODE = "preset_mode" ATTR_PRESET_MODES = "preset_modes" -_NOT_SPEED_OFF = "off" - -OFF_SPEED_VALUES = [SPEED_OFF, None] - -LEGACY_SPEED_LIST = [SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH] - - -class NoValidSpeedsError(ValueError): - """Exception class when there are no valid speeds.""" - - -class NotValidSpeedError(ValueError): - """Exception class when the speed in not in the speed list.""" - class NotValidPresetModeError(ValueError): """Exception class when the preset_mode in not in the preset_modes list.""" @@ -94,10 +70,7 @@ class NotValidPresetModeError(ValueError): @bind_hass def is_on(hass, entity_id: str) -> bool: """Return if the fans are on based on the statemachine.""" - state = hass.states.get(entity_id) - if ATTR_SPEED in state.attributes: - return state.attributes[ATTR_SPEED] not in OFF_SPEED_VALUES - return state.state == STATE_ON + return hass.states.get(entity_id).state == STATE_ON async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: @@ -113,24 +86,15 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: component.async_register_entity_service( SERVICE_TURN_ON, { - vol.Optional(ATTR_SPEED): cv.string, vol.Optional(ATTR_PERCENTAGE): vol.All( vol.Coerce(int), vol.Range(min=0, max=100) ), vol.Optional(ATTR_PRESET_MODE): cv.string, }, - "async_turn_on_compat", + "async_turn_on", ) component.async_register_entity_service(SERVICE_TURN_OFF, {}, "async_turn_off") component.async_register_entity_service(SERVICE_TOGGLE, {}, "async_toggle") - # After the transition to percentage and preset_modes concludes, - # remove this service - component.async_register_entity_service( - SERVICE_SET_SPEED, - {vol.Required(ATTR_SPEED): cv.string}, - "async_set_speed_deprecated", - [SUPPORT_SET_SPEED], - ) component.async_register_entity_service( SERVICE_INCREASE_SPEED, { @@ -212,29 +176,6 @@ class FanEntity(ToggleEntity): _attr_speed_count: int _attr_supported_features: int = 0 - def set_speed(self, speed: str) -> None: - """Set the speed of the fan.""" - raise NotImplementedError() - - async def async_set_speed_deprecated(self, speed: str): - """Set the speed of the fan.""" - _LOGGER.error( - "The fan.set_speed service is deprecated and will fail in 2022.3 and later, use fan.set_percentage or fan.set_preset_mode instead" - ) - await self.async_set_speed(speed) - - async def async_set_speed(self, speed: str): - """Set the speed of the fan.""" - if speed == SPEED_OFF: - await self.async_turn_off() - return - - if self.preset_modes and speed in self.preset_modes: - await self.async_set_preset_mode(speed) - return - - await self.async_set_percentage(self.speed_to_percentage(speed)) - def set_percentage(self, percentage: int) -> None: """Set the speed of the fan, as a percentage.""" raise NotImplementedError() @@ -298,10 +239,8 @@ class FanEntity(ToggleEntity): """Set the direction of the fan.""" await self.hass.async_add_executor_job(self.set_direction, direction) - # pylint: disable=arguments-differ def turn_on( self, - speed: str | None = None, percentage: int | None = None, preset_mode: str | None = None, **kwargs, @@ -309,64 +248,21 @@ class FanEntity(ToggleEntity): """Turn on the fan.""" raise NotImplementedError() - async def async_turn_on_compat( - self, - speed: str | None = None, - percentage: int | None = None, - preset_mode: str | None = None, - **kwargs, - ) -> None: - """Turn on the fan. - - This _compat version wraps async_turn_on with - backwards and forward compatibility. - - This compatibility shim will be removed in 2022.3 - """ - if preset_mode is not None: - self._valid_preset_mode_or_raise(preset_mode) - speed = preset_mode - percentage = None - elif speed is not None: - _LOGGER.error( - "Calling fan.turn_on with the speed argument is deprecated and will fail in 2022.3 and later, use percentage or preset_mode instead" - ) - if self.preset_modes and speed in self.preset_modes: - preset_mode = speed - percentage = None - else: - percentage = self.speed_to_percentage(speed) - elif percentage is not None: - speed = self.percentage_to_speed(percentage) - - await self.async_turn_on( - speed=speed, - percentage=percentage, - preset_mode=preset_mode, - **kwargs, - ) - - # pylint: disable=arguments-differ async def async_turn_on( self, - speed: str | None = None, percentage: int | None = None, preset_mode: str | None = None, **kwargs, ) -> None: """Turn on the fan.""" - if speed == SPEED_OFF: - await self.async_turn_off() - else: - await self.hass.async_add_executor_job( - ft.partial( - self.turn_on, - speed=speed, - percentage=percentage, - preset_mode=preset_mode, - **kwargs, - ) + await self.hass.async_add_executor_job( + ft.partial( + self.turn_on, + percentage=percentage, + preset_mode=preset_mode, + **kwargs, ) + ) def oscillate(self, oscillating: bool) -> None: """Oscillate the fan.""" @@ -379,16 +275,9 @@ class FanEntity(ToggleEntity): @property def is_on(self): """Return true if the entity is on.""" - return self.speed not in [SPEED_OFF, None] - - @property - def speed(self) -> str | None: - """Return the current speed.""" - if preset_mode := self.preset_mode: - return preset_mode - if (percentage := self.percentage) is None: - return None - return self.percentage_to_speed(percentage) + return ( + self.percentage is not None and self.percentage > 0 + ) or self.preset_mode is not None @property def percentage(self) -> int | None: @@ -409,14 +298,6 @@ class FanEntity(ToggleEntity): """Return the step size for percentage.""" return 100 / self.speed_count - @property - def speed_list(self) -> list: - """Get the list of available speeds.""" - speeds = [SPEED_OFF, *LEGACY_SPEED_LIST] - if preset_modes := self.preset_modes: - speeds.extend(preset_modes) - return speeds - @property def current_direction(self) -> str | None: """Return the current direction of the fan.""" @@ -431,8 +312,6 @@ class FanEntity(ToggleEntity): def capability_attributes(self): """Return capability attributes.""" attrs = {} - if self.supported_features & SUPPORT_SET_SPEED: - attrs[ATTR_SPEED_LIST] = self.speed_list if ( self.supported_features & SUPPORT_SET_SPEED @@ -442,22 +321,6 @@ class FanEntity(ToggleEntity): return attrs - def speed_to_percentage(self, speed: str) -> int: # pylint: disable=no-self-use - """Map a legacy speed to a percentage.""" - if speed in OFF_SPEED_VALUES: - return 0 - if speed not in LEGACY_SPEED_LIST: - raise NotValidSpeedError(f"The speed {speed} is not a valid speed.") - return ordered_list_item_to_percentage(LEGACY_SPEED_LIST, speed) - - def percentage_to_speed( # pylint: disable=no-self-use - self, percentage: int - ) -> str: - """Map a percentage to a legacy speed.""" - if percentage == 0: - return SPEED_OFF - return percentage_to_ordered_list_item(LEGACY_SPEED_LIST, percentage) - @final @property def state_attributes(self) -> dict: @@ -472,7 +335,6 @@ class FanEntity(ToggleEntity): data[ATTR_OSCILLATING] = self.oscillating if supported_features & SUPPORT_SET_SPEED: - data[ATTR_SPEED] = self.speed data[ATTR_PERCENTAGE] = self.percentage data[ATTR_PERCENTAGE_STEP] = self.percentage_step diff --git a/homeassistant/components/fan/manifest.json b/homeassistant/components/fan/manifest.json index 76573e08cbb..bb968240f0b 100644 --- a/homeassistant/components/fan/manifest.json +++ b/homeassistant/components/fan/manifest.json @@ -2,6 +2,6 @@ "domain": "fan", "name": "Fan", "documentation": "https://www.home-assistant.io/integrations/fan", - "codeowners": [], + "codeowners": ["@home-assistant/core"], "quality_scale": "internal" } diff --git a/homeassistant/components/fan/recorder.py b/homeassistant/components/fan/recorder.py new file mode 100644 index 00000000000..e7305b64f16 --- /dev/null +++ b/homeassistant/components/fan/recorder.py @@ -0,0 +1,12 @@ +"""Integration platform for recorder.""" +from __future__ import annotations + +from homeassistant.core import HomeAssistant, callback + +from . import ATTR_PRESET_MODES + + +@callback +def exclude_attributes(hass: HomeAssistant) -> set[str]: + """Exclude static attributes from being recorded in the database.""" + return {ATTR_PRESET_MODES} diff --git a/homeassistant/components/fan/reproduce_state.py b/homeassistant/components/fan/reproduce_state.py index 140fdfe9178..be018aa4b54 100644 --- a/homeassistant/components/fan/reproduce_state.py +++ b/homeassistant/components/fan/reproduce_state.py @@ -20,13 +20,11 @@ from . import ( ATTR_OSCILLATING, ATTR_PERCENTAGE, ATTR_PRESET_MODE, - ATTR_SPEED, DOMAIN, SERVICE_OSCILLATE, SERVICE_SET_DIRECTION, SERVICE_SET_PERCENTAGE, SERVICE_SET_PRESET_MODE, - SERVICE_SET_SPEED, ) _LOGGER = logging.getLogger(__name__) @@ -35,7 +33,6 @@ VALID_STATES = {STATE_ON, STATE_OFF} ATTRIBUTES = { # attribute: service ATTR_DIRECTION: SERVICE_SET_DIRECTION, ATTR_OSCILLATING: SERVICE_OSCILLATE, - ATTR_SPEED: SERVICE_SET_SPEED, ATTR_PERCENTAGE: SERVICE_SET_PERCENTAGE, ATTR_PRESET_MODE: SERVICE_SET_PRESET_MODE, } diff --git a/homeassistant/components/fan/services.yaml b/homeassistant/components/fan/services.yaml index ee39229699d..cfc44029e23 100644 --- a/homeassistant/components/fan/services.yaml +++ b/homeassistant/components/fan/services.yaml @@ -116,8 +116,10 @@ set_direction: selector: select: options: - - "forward" - - "reverse" + - label: "Forward" + value: "forward" + - label: "Reverse" + value: "reverse" increase_speed: name: Increase speed diff --git a/homeassistant/components/fan/translations/el.json b/homeassistant/components/fan/translations/el.json index 10f04bacd1e..b9c4962a7a6 100644 --- a/homeassistant/components/fan/translations/el.json +++ b/homeassistant/components/fan/translations/el.json @@ -17,8 +17,8 @@ }, "state": { "_": { - "off": "\u039a\u03bb\u03b5\u03b9\u03c3\u03c4\u03cc", - "on": "\u0391\u03bd\u03bf\u03b9\u03c7\u03c4\u03cc" + "off": "\u0391\u03bd\u03b5\u03bd\u03b5\u03c1\u03b3\u03cc", + "on": "\u0395\u03bd\u03b5\u03c1\u03b3\u03cc" } }, "title": "\u0391\u03bd\u03b5\u03bc\u03b9\u03c3\u03c4\u03ae\u03c1\u03b1\u03c2" diff --git a/homeassistant/components/fan/translations/fr.json b/homeassistant/components/fan/translations/fr.json index e1c9567dc0f..e9cf666b725 100644 --- a/homeassistant/components/fan/translations/fr.json +++ b/homeassistant/components/fan/translations/fr.json @@ -17,8 +17,8 @@ }, "state": { "_": { - "off": "Inactif", - "on": "Actif" + "off": "D\u00e9sactiv\u00e9", + "on": "Activ\u00e9" } }, "title": "Ventilateur" diff --git a/homeassistant/components/fibaro/__init__.py b/homeassistant/components/fibaro/__init__.py index cfe39201913..1db73538fa0 100644 --- a/homeassistant/components/fibaro/__init__.py +++ b/homeassistant/components/fibaro/__init__.py @@ -3,10 +3,13 @@ from __future__ import annotations from collections import defaultdict import logging +from typing import Any from fiblary3.client.v4.client import Client as FibaroClient, StateHandler +from fiblary3.common.exceptions import HTTPException import voluptuous as vol +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( ATTR_ARMED, ATTR_BATTERY_LEVEL, @@ -17,28 +20,26 @@ from homeassistant.const import ( CONF_URL, CONF_USERNAME, CONF_WHITE_VALUE, - EVENT_HOMEASSISTANT_STOP, Platform, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers import discovery +from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import ConfigType -from homeassistant.util import convert, slugify +from homeassistant.util import slugify + +from .const import CONF_IMPORT_PLUGINS, DOMAIN _LOGGER = logging.getLogger(__name__) -ATTR_CURRENT_POWER_W = "current_power_w" - CONF_COLOR = "color" CONF_DEVICE_CONFIG = "device_config" CONF_DIMMING = "dimming" CONF_GATEWAYS = "gateways" CONF_PLUGINS = "plugins" CONF_RESET_COLOR = "reset_color" -DOMAIN = "fibaro" -FIBARO_CONTROLLERS = "fibaro_controllers" +FIBARO_CONTROLLER = "fibaro_controller" FIBARO_DEVICES = "fibaro_devices" PLATFORMS = [ Platform.BINARY_SENSOR, @@ -102,11 +103,14 @@ GATEWAY_CONFIG = vol.Schema( ) CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - {vol.Required(CONF_GATEWAYS): vol.All(cv.ensure_list, [GATEWAY_CONFIG])} - ) - }, + vol.All( + cv.deprecated(DOMAIN), + { + DOMAIN: vol.Schema( + {vol.Required(CONF_GATEWAYS): vol.All(cv.ensure_list, [GATEWAY_CONFIG])} + ) + }, + ), extra=vol.ALLOW_EXTRA, ) @@ -116,21 +120,19 @@ class FibaroController: def __init__(self, config): """Initialize the Fibaro controller.""" - self._client = FibaroClient( config[CONF_URL], config[CONF_USERNAME], config[CONF_PASSWORD] ) self._scene_map = None # Whether to import devices from plugins - self._import_plugins = config[CONF_PLUGINS] - self._device_config = config[CONF_DEVICE_CONFIG] + self._import_plugins = config[CONF_IMPORT_PLUGINS] self._room_map = None # Mapping roomId to room object self._device_map = None # Mapping deviceId to device object self.fibaro_devices = None # List of devices by type self._callbacks = {} # Update value callbacks by deviceId self._state_handler = None # Fiblary's StateHandler object - self._excluded_devices = config[CONF_EXCLUDE] self.hub_serial = None # Unique serial number of the hub + self.name = None # The friendly name of the hub def connect(self): """Start the communication with the Fibaro controller.""" @@ -138,6 +140,7 @@ class FibaroController: login = self._client.login.get() info = self._client.info.get() self.hub_serial = slugify(info.serialNumber) + self.name = slugify(info.hcName) except AssertionError: _LOGGER.error("Can't connect to Fibaro HC. Please check URL") return False @@ -152,6 +155,23 @@ class FibaroController: self._read_scenes() return True + def connect_with_error_handling(self) -> None: + """Translate connect errors to easily differentiate auth and connect failures. + + When there is a better error handling in the used library this can be improved. + """ + try: + connected = self.connect() + if not connected: + raise FibaroConnectFailed("Connect status is false") + except HTTPException as http_ex: + if http_ex.details == "Forbidden": + raise FibaroAuthFailed from http_ex + + raise FibaroConnectFailed from http_ex + except Exception as ex: + raise FibaroConnectFailed from ex + def enable_state_handler(self): """Start StateHandler thread for monitoring updates.""" self._state_handler = StateHandler(self._client, self._on_state_change) @@ -299,16 +319,11 @@ class FibaroController: device.ha_id = ( f"{slugify(room_name)}_{slugify(device.name)}_{device.id}" ) - if ( - device.enabled - and ( - "isPlugin" not in device - or (not device.isPlugin or self._import_plugins) - ) - and device.ha_id not in self._excluded_devices + if device.enabled and ( + "isPlugin" not in device + or (not device.isPlugin or self._import_plugins) ): device.mapped_type = self._map_device_to_type(device) - device.device_config = self._device_config.get(device.ha_id, {}) else: device.mapped_type = None if (dtype := device.mapped_type) is None: @@ -357,39 +372,78 @@ class FibaroController: pass -def setup(hass: HomeAssistant, base_config: ConfigType) -> bool: - """Set up the Fibaro Component.""" +async def async_setup(hass: HomeAssistant, base_config: ConfigType) -> bool: + """Migrate configuration from configuration.yaml.""" + if DOMAIN not in base_config: + return True gateways = base_config[DOMAIN][CONF_GATEWAYS] - hass.data[FIBARO_CONTROLLERS] = {} - - def stop_fibaro(event): - """Stop Fibaro Thread.""" - _LOGGER.info("Shutting down Fibaro connection") - for controller in hass.data[FIBARO_CONTROLLERS].values(): - controller.disable_state_handler() - - hass.data[FIBARO_DEVICES] = {} - for platform in PLATFORMS: - hass.data[FIBARO_DEVICES][platform] = [] - - for gateway in gateways: - controller = FibaroController(gateway) - if controller.connect(): - hass.data[FIBARO_CONTROLLERS][controller.hub_serial] = controller - for platform in PLATFORMS: - hass.data[FIBARO_DEVICES][platform].extend( - controller.fibaro_devices[platform] - ) - - if hass.data[FIBARO_CONTROLLERS]: - for platform in PLATFORMS: - discovery.load_platform(hass, platform, DOMAIN, {}, base_config) - for controller in hass.data[FIBARO_CONTROLLERS].values(): - controller.enable_state_handler() - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_fibaro) + if gateways is None: return True - return False + # check if already configured + if hass.config_entries.async_entries(DOMAIN): + return True + + for gateway in gateways: + # prepare new config based on configuration.yaml + conf = { + CONF_URL: gateway[CONF_URL], + CONF_USERNAME: gateway[CONF_USERNAME], + CONF_PASSWORD: gateway[CONF_PASSWORD], + CONF_IMPORT_PLUGINS: gateway[CONF_PLUGINS], + } + + # import into config flow based configuration + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=conf + ) + ) + + return True + + +def _init_controller(data: dict[str, Any]) -> FibaroController: + """Validate the user input allows us to connect to fibaro.""" + controller = FibaroController(data) + controller.connect_with_error_handling() + return controller + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up the Fibaro Component.""" + try: + controller = await hass.async_add_executor_job(_init_controller, entry.data) + except FibaroConnectFailed as connect_ex: + raise ConfigEntryNotReady( + f"Could not connect to controller at {entry.data[CONF_URL]}" + ) from connect_ex + except FibaroAuthFailed: + return False + + data: dict[str, Any] = {} + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = data + data[FIBARO_CONTROLLER] = controller + devices = data[FIBARO_DEVICES] = {} + for platform in PLATFORMS: + devices[platform] = [*controller.fibaro_devices[platform]] + + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + + controller.enable_state_handler() + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + _LOGGER.debug("Shutting down Fibaro connection") + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + + hass.data[DOMAIN][entry.entry_id][FIBARO_CONTROLLER].disable_state_handler() + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok class FibaroDevice(Entity): @@ -473,15 +527,6 @@ class FibaroDevice(Entity): else: self.dont_know_message(cmd) - @property - def current_power_w(self): - """Return the current power usage in W.""" - if "power" in self.fibaro_device.properties and ( - power := self.fibaro_device.properties.power - ): - return convert(power, float, 0.0) - return None - @property def current_binary_state(self): """Return the current binary state.""" @@ -511,11 +556,15 @@ class FibaroDevice(Entity): ) if "fibaroAlarmArm" in self.fibaro_device.interfaces: attr[ATTR_ARMED] = bool(self.fibaro_device.properties.armed) - if "power" in self.fibaro_device.interfaces: - attr[ATTR_CURRENT_POWER_W] = convert( - self.fibaro_device.properties.power, float, 0.0 - ) except (ValueError, KeyError): pass return attr + + +class FibaroConnectFailed(HomeAssistantError): + """Error to indicate we cannot connect to fibaro home center.""" + + +class FibaroAuthFailed(HomeAssistantError): + """Error to indicate that authentication failed on fibaro home center.""" diff --git a/homeassistant/components/fibaro/binary_sensor.py b/homeassistant/components/fibaro/binary_sensor.py index f7b20ec9983..1b07d3671ae 100644 --- a/homeassistant/components/fibaro/binary_sensor.py +++ b/homeassistant/components/fibaro/binary_sensor.py @@ -2,16 +2,16 @@ from __future__ import annotations from homeassistant.components.binary_sensor import ( - DOMAIN, + ENTITY_ID_FORMAT, BinarySensorDeviceClass, BinarySensorEntity, ) -from homeassistant.const import CONF_DEVICE_CLASS, CONF_ICON +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import FIBARO_DEVICES, FibaroDevice +from .const import DOMAIN SENSOR_TYPES = { "com.fibaro.floodSensor": ["Flood", "mdi:water", "flood"], @@ -28,20 +28,18 @@ SENSOR_TYPES = { } -def setup_platform( +async def async_setup_entry( hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Perform the setup for Fibaro controller devices.""" - if discovery_info is None: - return - - add_entities( + async_add_entities( [ FibaroBinarySensor(device) - for device in hass.data[FIBARO_DEVICES]["binary_sensor"] + for device in hass.data[DOMAIN][entry.entry_id][FIBARO_DEVICES][ + "binary_sensor" + ] ], True, ) @@ -54,9 +52,8 @@ class FibaroBinarySensor(FibaroDevice, BinarySensorEntity): """Initialize the binary_sensor.""" self._state = None super().__init__(fibaro_device) - self.entity_id = f"{DOMAIN}.{self.ha_id}" + self.entity_id = ENTITY_ID_FORMAT.format(self.ha_id) stype = None - devconf = fibaro_device.device_config if fibaro_device.type in SENSOR_TYPES: stype = fibaro_device.type elif fibaro_device.baseType in SENSOR_TYPES: @@ -67,9 +64,6 @@ class FibaroBinarySensor(FibaroDevice, BinarySensorEntity): else: self._device_class = None self._icon = None - # device_config overrides: - self._device_class = devconf.get(CONF_DEVICE_CLASS, self._device_class) - self._icon = devconf.get(CONF_ICON, self._icon) @property def icon(self): diff --git a/homeassistant/components/fibaro/climate.py b/homeassistant/components/fibaro/climate.py index 1c065ca3fb4..b639324acd0 100644 --- a/homeassistant/components/fibaro/climate.py +++ b/homeassistant/components/fibaro/climate.py @@ -3,7 +3,7 @@ from __future__ import annotations import logging -from homeassistant.components.climate import ClimateEntity +from homeassistant.components.climate import ENTITY_ID_FORMAT, ClimateEntity from homeassistant.components.climate.const import ( HVAC_MODE_AUTO, HVAC_MODE_COOL, @@ -17,12 +17,13 @@ from homeassistant.components.climate.const import ( SUPPORT_PRESET_MODE, SUPPORT_TARGET_TEMPERATURE, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import FIBARO_DEVICES, FibaroDevice +from .const import DOMAIN PRESET_RESUME = "resume" PRESET_MOIST = "moist" @@ -98,18 +99,17 @@ HA_OPMODES_HVAC = { } -def setup_platform( +async def async_setup_entry( hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Perform the setup for Fibaro controller devices.""" - if discovery_info is None: - return - - add_entities( - [FibaroThermostat(device) for device in hass.data[FIBARO_DEVICES]["climate"]], + async_add_entities( + [ + FibaroThermostat(device) + for device in hass.data[DOMAIN][entry.entry_id][FIBARO_DEVICES]["climate"] + ], True, ) @@ -125,7 +125,7 @@ class FibaroThermostat(FibaroDevice, ClimateEntity): self._op_mode_device = None self._fan_mode_device = None self._support_flags = 0 - self.entity_id = f"climate.{self.ha_id}" + self.entity_id = ENTITY_ID_FORMAT.format(self.ha_id) self._hvac_support = [] self._preset_support = [] self._fan_support = [] diff --git a/homeassistant/components/fibaro/config_flow.py b/homeassistant/components/fibaro/config_flow.py new file mode 100644 index 00000000000..f528fd8a184 --- /dev/null +++ b/homeassistant/components/fibaro/config_flow.py @@ -0,0 +1,81 @@ +"""Config flow for Fibaro integration.""" +from __future__ import annotations + +import logging +from typing import Any + +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.typing import ConfigType + +from . import FibaroAuthFailed, FibaroConnectFailed, FibaroController +from .const import CONF_IMPORT_PLUGINS, DOMAIN + +_LOGGER = logging.getLogger(__name__) + +STEP_USER_DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_URL): str, + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + vol.Optional(CONF_IMPORT_PLUGINS, default=False): bool, + } +) + + +def _connect_to_fibaro(data: dict[str, Any]) -> FibaroController: + """Validate the user input allows us to connect to fibaro.""" + controller = FibaroController(data) + controller.connect_with_error_handling() + return controller + + +async def _validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, Any]: + """Validate the user input allows us to connect. + + Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user. + """ + controller = await hass.async_add_executor_job(_connect_to_fibaro, data) + + _LOGGER.debug( + "Successfully connected to fibaro home center %s with name %s", + controller.hub_serial, + controller.name, + ) + return {"serial_number": controller.hub_serial, "name": controller.name} + + +class FibaroConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Fibaro.""" + + VERSION = 1 + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the initial step.""" + errors = {} + + if user_input is not None: + try: + info = await _validate_input(self.hass, user_input) + except FibaroConnectFailed: + errors["base"] = "cannot_connect" + except FibaroAuthFailed: + errors["base"] = "invalid_auth" + else: + await self.async_set_unique_id(info["serial_number"]) + self._abort_if_unique_id_configured() + return self.async_create_entry(title=info["name"], data=user_input) + + return self.async_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors + ) + + async def async_step_import(self, import_config: ConfigType | None) -> FlowResult: + """Import a config entry.""" + return await self.async_step_user(import_config) diff --git a/homeassistant/components/fibaro/const.py b/homeassistant/components/fibaro/const.py new file mode 100644 index 00000000000..cf564ab0bfc --- /dev/null +++ b/homeassistant/components/fibaro/const.py @@ -0,0 +1,4 @@ +"""Constants for the Fibaro integration.""" + +DOMAIN = "fibaro" +CONF_IMPORT_PLUGINS = "import_plugins" diff --git a/homeassistant/components/fibaro/cover.py b/homeassistant/components/fibaro/cover.py index f98d907945b..b87ae1cbf47 100644 --- a/homeassistant/components/fibaro/cover.py +++ b/homeassistant/components/fibaro/cover.py @@ -4,28 +4,29 @@ from __future__ import annotations from homeassistant.components.cover import ( ATTR_POSITION, ATTR_TILT_POSITION, - DOMAIN, + ENTITY_ID_FORMAT, CoverEntity, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import FIBARO_DEVICES, FibaroDevice +from .const import DOMAIN -def setup_platform( +async def async_setup_entry( hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Fibaro covers.""" - if discovery_info is None: - return - - add_entities( - [FibaroCover(device) for device in hass.data[FIBARO_DEVICES]["cover"]], True + async_add_entities( + [ + FibaroCover(device) + for device in hass.data[DOMAIN][entry.entry_id][FIBARO_DEVICES]["cover"] + ], + True, ) @@ -35,7 +36,7 @@ class FibaroCover(FibaroDevice, CoverEntity): def __init__(self, fibaro_device): """Initialize the Vera device.""" super().__init__(fibaro_device) - self.entity_id = f"{DOMAIN}.{self.ha_id}" + self.entity_id = ENTITY_ID_FORMAT.format(self.ha_id) @staticmethod def bound(position): diff --git a/homeassistant/components/fibaro/light.py b/homeassistant/components/fibaro/light.py index f3f0ee23181..4d1c039f137 100644 --- a/homeassistant/components/fibaro/light.py +++ b/homeassistant/components/fibaro/light.py @@ -8,19 +8,19 @@ from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_HS_COLOR, ATTR_WHITE_VALUE, - DOMAIN, + ENTITY_ID_FORMAT, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_WHITE_VALUE, LightEntity, ) -from homeassistant.const import CONF_WHITE_VALUE +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType import homeassistant.util.color as color_util -from . import CONF_COLOR, CONF_DIMMING, CONF_RESET_COLOR, FIBARO_DEVICES, FibaroDevice +from . import FIBARO_DEVICES, FibaroDevice +from .const import DOMAIN def scaleto255(value): @@ -40,18 +40,18 @@ def scaleto100(value): return max(0, min(100, ((value * 100.0) / 255.0))) -async def async_setup_platform( +async def async_setup_entry( hass: HomeAssistant, - config: ConfigType, + entry: ConfigEntry, async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, ) -> None: """Perform the setup for Fibaro controller devices.""" - if discovery_info is None: - return - async_add_entities( - [FibaroLight(device) for device in hass.data[FIBARO_DEVICES]["light"]], True + [ + FibaroLight(device) + for device in hass.data[DOMAIN][entry.entry_id][FIBARO_DEVICES]["light"] + ], + True, ) @@ -67,8 +67,7 @@ class FibaroLight(FibaroDevice, LightEntity): self._update_lock = asyncio.Lock() self._white = 0 - devconf = fibaro_device.device_config - self._reset_color = devconf.get(CONF_RESET_COLOR, False) + self._reset_color = False supports_color = ( "color" in fibaro_device.properties or "colorComponents" in fibaro_device.properties @@ -91,15 +90,15 @@ class FibaroLight(FibaroDevice, LightEntity): ) # Configuration can override default capability detection - if devconf.get(CONF_DIMMING, supports_dimming): + if supports_dimming: self._supported_flags |= SUPPORT_BRIGHTNESS - if devconf.get(CONF_COLOR, supports_color): + if supports_color: self._supported_flags |= SUPPORT_COLOR - if devconf.get(CONF_WHITE_VALUE, supports_white_v): + if supports_white_v: self._supported_flags |= SUPPORT_WHITE_VALUE super().__init__(fibaro_device) - self.entity_id = f"{DOMAIN}.{self.ha_id}" + self.entity_id = ENTITY_ID_FORMAT.format(self.ha_id) @property def brightness(self): @@ -194,9 +193,23 @@ class FibaroLight(FibaroDevice, LightEntity): self.call_turn_off() @property - def is_on(self): - """Return true if device is on.""" - return self.current_binary_state + def is_on(self) -> bool | None: + """Return true if device is on. + + Dimmable and RGB lights can be on based on different + properties, so we need to check here several values. + """ + props = self.fibaro_device.properties + if self.current_binary_state: + return True + if "brightness" in props and props.brightness != "0": + return True + if "currentProgram" in props and props.currentProgram != "0": + return True + if "currentProgramID" in props and props.currentProgramID != "0": + return True + + return False async def async_update(self): """Update the state.""" diff --git a/homeassistant/components/fibaro/lock.py b/homeassistant/components/fibaro/lock.py index bcfd10767e2..5b86a99cbc9 100644 --- a/homeassistant/components/fibaro/lock.py +++ b/homeassistant/components/fibaro/lock.py @@ -1,26 +1,27 @@ """Support for Fibaro locks.""" from __future__ import annotations -from homeassistant.components.lock import DOMAIN, LockEntity +from homeassistant.components.lock import ENTITY_ID_FORMAT, LockEntity +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import FIBARO_DEVICES, FibaroDevice +from .const import DOMAIN -def setup_platform( +async def async_setup_entry( hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Fibaro locks.""" - if discovery_info is None: - return - - add_entities( - [FibaroLock(device) for device in hass.data[FIBARO_DEVICES]["lock"]], True + async_add_entities( + [ + FibaroLock(device) + for device in hass.data[DOMAIN][entry.entry_id][FIBARO_DEVICES]["lock"] + ], + True, ) @@ -31,7 +32,7 @@ class FibaroLock(FibaroDevice, LockEntity): """Initialize the Fibaro device.""" self._state = False super().__init__(fibaro_device) - self.entity_id = f"{DOMAIN}.{self.ha_id}" + self.entity_id = ENTITY_ID_FORMAT.format(self.ha_id) def lock(self, **kwargs): """Lock the device.""" diff --git a/homeassistant/components/fibaro/manifest.json b/homeassistant/components/fibaro/manifest.json index 7bc7d5a0e49..218a6aad857 100644 --- a/homeassistant/components/fibaro/manifest.json +++ b/homeassistant/components/fibaro/manifest.json @@ -3,7 +3,8 @@ "name": "Fibaro", "documentation": "https://www.home-assistant.io/integrations/fibaro", "requirements": ["fiblary3==0.1.8"], - "codeowners": [], + "codeowners": ["@rappenze"], "iot_class": "local_push", + "config_flow": true, "loggers": ["fiblary3"] } diff --git a/homeassistant/components/fibaro/scene.py b/homeassistant/components/fibaro/scene.py index 02ebd5cd99e..cc476a29cbb 100644 --- a/homeassistant/components/fibaro/scene.py +++ b/homeassistant/components/fibaro/scene.py @@ -4,25 +4,26 @@ from __future__ import annotations from typing import Any from homeassistant.components.scene import Scene +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import FIBARO_DEVICES, FibaroDevice +from .const import DOMAIN -async def async_setup_platform( +async def async_setup_entry( hass: HomeAssistant, - config: ConfigType, + entry: ConfigEntry, async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, ) -> None: """Perform the setup for Fibaro scenes.""" - if discovery_info is None: - return - async_add_entities( - [FibaroScene(scene) for scene in hass.data[FIBARO_DEVICES]["scene"]], True + [ + FibaroScene(scene) + for scene in hass.data[DOMAIN][entry.entry_id][FIBARO_DEVICES]["scene"] + ], + True, ) diff --git a/homeassistant/components/fibaro/sensor.py b/homeassistant/components/fibaro/sensor.py index 901552d0363..45d33728ab9 100644 --- a/homeassistant/components/fibaro/sensor.py +++ b/homeassistant/components/fibaro/sensor.py @@ -4,25 +4,27 @@ from __future__ import annotations from contextlib import suppress from homeassistant.components.sensor import ( - DOMAIN, + ENTITY_ID_FORMAT, SensorDeviceClass, SensorEntity, SensorStateClass, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONCENTRATION_PARTS_PER_MILLION, ENERGY_KILO_WATT_HOUR, LIGHT_LUX, PERCENTAGE, + POWER_WATT, TEMP_CELSIUS, TEMP_FAHRENHEIT, ) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util import convert from . import FIBARO_DEVICES, FibaroDevice +from .const import DOMAIN SENSOR_TYPES = { "com.fibaro.temperatureSensor": [ @@ -54,25 +56,23 @@ SENSOR_TYPES = { } -def setup_platform( +async def async_setup_entry( hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Fibaro controller devices.""" - if discovery_info is None: - return - entities: list[SensorEntity] = [] - for device in hass.data[FIBARO_DEVICES]["sensor"]: + for device in hass.data[DOMAIN][entry.entry_id][FIBARO_DEVICES]["sensor"]: entities.append(FibaroSensor(device)) for device_type in ("cover", "light", "switch"): - for device in hass.data[FIBARO_DEVICES][device_type]: + for device in hass.data[DOMAIN][entry.entry_id][FIBARO_DEVICES][device_type]: if "energy" in device.interfaces: entities.append(FibaroEnergySensor(device)) + if "power" in device.interfaces: + entities.append(FibaroPowerSensor(device)) - add_entities(entities, True) + async_add_entities(entities, True) class FibaroSensor(FibaroDevice, SensorEntity): @@ -83,7 +83,7 @@ class FibaroSensor(FibaroDevice, SensorEntity): self.current_value = None self.last_changed_time = None super().__init__(fibaro_device) - self.entity_id = f"{DOMAIN}.{self.ha_id}" + self.entity_id = ENTITY_ID_FORMAT.format(self.ha_id) if fibaro_device.type in SENSOR_TYPES: self._unit = SENSOR_TYPES[fibaro_device.type][1] self._icon = SENSOR_TYPES[fibaro_device.type][2] @@ -139,7 +139,7 @@ class FibaroEnergySensor(FibaroDevice, SensorEntity): def __init__(self, fibaro_device): """Initialize the sensor.""" super().__init__(fibaro_device) - self.entity_id = f"{DOMAIN}.{self.ha_id}_energy" + self.entity_id = ENTITY_ID_FORMAT.format(f"{self.ha_id}_energy") self._attr_name = f"{fibaro_device.friendly_name} Energy" self._attr_unique_id = f"{fibaro_device.unique_id_str}_energy" @@ -149,3 +149,25 @@ class FibaroEnergySensor(FibaroDevice, SensorEntity): self._attr_native_value = convert( self.fibaro_device.properties.energy, float ) + + +class FibaroPowerSensor(FibaroDevice, SensorEntity): + """Representation of a Fibaro Power Sensor.""" + + _attr_device_class = SensorDeviceClass.POWER + _attr_state_class = SensorStateClass.MEASUREMENT + _attr_native_unit_of_measurement = POWER_WATT + + def __init__(self, fibaro_device): + """Initialize the sensor.""" + super().__init__(fibaro_device) + self.entity_id = ENTITY_ID_FORMAT.format(f"{self.ha_id}_power") + self._attr_name = f"{fibaro_device.friendly_name} Power" + self._attr_unique_id = f"{fibaro_device.unique_id_str}_power" + + def update(self): + """Update the state.""" + with suppress(KeyError, ValueError): + self._attr_native_value = convert( + self.fibaro_device.properties.power, float + ) diff --git a/homeassistant/components/fibaro/strings.json b/homeassistant/components/fibaro/strings.json new file mode 100644 index 00000000000..99c25c9f6e0 --- /dev/null +++ b/homeassistant/components/fibaro/strings.json @@ -0,0 +1,22 @@ +{ + "config": { + "step": { + "user": { + "data": { + "url": "URL in the format http://HOST/api/", + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]", + "import_plugins": "Import entities from fibaro plugins?" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + } +} diff --git a/homeassistant/components/fibaro/switch.py b/homeassistant/components/fibaro/switch.py index 3de9235cb0e..73056c095c6 100644 --- a/homeassistant/components/fibaro/switch.py +++ b/homeassistant/components/fibaro/switch.py @@ -1,27 +1,27 @@ """Support for Fibaro switches.""" from __future__ import annotations -from homeassistant.components.switch import DOMAIN, SwitchEntity +from homeassistant.components.switch import ENTITY_ID_FORMAT, SwitchEntity +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from homeassistant.util import convert from . import FIBARO_DEVICES, FibaroDevice +from .const import DOMAIN -def setup_platform( +async def async_setup_entry( hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Fibaro switches.""" - if discovery_info is None: - return - - add_entities( - [FibaroSwitch(device) for device in hass.data[FIBARO_DEVICES]["switch"]], True + async_add_entities( + [ + FibaroSwitch(device) + for device in hass.data[DOMAIN][entry.entry_id][FIBARO_DEVICES]["switch"] + ], + True, ) @@ -32,7 +32,7 @@ class FibaroSwitch(FibaroDevice, SwitchEntity): """Initialize the Fibaro device.""" self._state = False super().__init__(fibaro_device) - self.entity_id = f"{DOMAIN}.{self.ha_id}" + self.entity_id = ENTITY_ID_FORMAT.format(self.ha_id) def turn_on(self, **kwargs): """Turn device on.""" @@ -44,20 +44,6 @@ class FibaroSwitch(FibaroDevice, SwitchEntity): self.call_turn_off() self._state = False - @property - def current_power_w(self): - """Return the current power usage in W.""" - if "power" in self.fibaro_device.interfaces: - return convert(self.fibaro_device.properties.power, float, 0.0) - return None - - @property - def today_energy_kwh(self): - """Return the today total energy usage in kWh.""" - if "energy" in self.fibaro_device.interfaces: - return convert(self.fibaro_device.properties.energy, float, 0.0) - return None - @property def is_on(self): """Return true if device is on.""" diff --git a/homeassistant/components/fibaro/translations/en.json b/homeassistant/components/fibaro/translations/en.json new file mode 100644 index 00000000000..2baeb3a7213 --- /dev/null +++ b/homeassistant/components/fibaro/translations/en.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "url": "URL in the format http://HOST/api/", + "import_plugins": "Import entities from fibaro plugins?", + "password": "Password", + "username": "Username" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/filesize/__init__.py b/homeassistant/components/filesize/__init__.py index 9e2ab2477a9..8f5b098d221 100644 --- a/homeassistant/components/filesize/__init__.py +++ b/homeassistant/components/filesize/__init__.py @@ -1,6 +1,35 @@ """The filesize component.""" +from __future__ import annotations -from homeassistant.const import Platform +import logging +import pathlib -DOMAIN = "filesize" -PLATFORMS = [Platform.SENSOR] +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_FILE_PATH +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady + +from .const import PLATFORMS + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up from a config entry.""" + + path = entry.data[CONF_FILE_PATH] + get_path = await hass.async_add_executor_job(pathlib.Path, path) + + if not get_path.exists() and not get_path.is_file(): + raise ConfigEntryNotReady(f"Can not access file {path}") + + if not hass.config.is_allowed_path(path): + raise ConfigEntryNotReady(f"Filepath {path} is not valid or allowed") + + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/filesize/config_flow.py b/homeassistant/components/filesize/config_flow.py new file mode 100644 index 00000000000..7838353fa12 --- /dev/null +++ b/homeassistant/components/filesize/config_flow.py @@ -0,0 +1,80 @@ +"""The filesize config flow.""" +from __future__ import annotations + +import logging +import pathlib +from typing import Any + +import voluptuous as vol + +from homeassistant.config_entries import ConfigFlow +from homeassistant.const import CONF_FILE_PATH +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResult + +from .const import DOMAIN + +DATA_SCHEMA = vol.Schema({vol.Required(CONF_FILE_PATH): str}) + +_LOGGER = logging.getLogger(__name__) + + +def validate_path(hass: HomeAssistant, path: str) -> pathlib.Path: + """Validate path.""" + try: + get_path = pathlib.Path(path) + except OSError as error: + _LOGGER.error("Can not access file %s, error %s", path, error) + raise NotValidError from error + + if not hass.config.is_allowed_path(path): + _LOGGER.error("Filepath %s is not valid or allowed", path) + raise NotAllowedError + + return get_path + + +class FilesizeConfigFlow(ConfigFlow, domain=DOMAIN): + """Config flow for Filesize.""" + + VERSION = 1 + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle a flow initialized by the user.""" + errors: dict[str, Any] = {} + + if user_input is not None: + try: + get_path = validate_path(self.hass, user_input[CONF_FILE_PATH]) + except NotValidError: + errors["base"] = "not_valid" + except NotAllowedError: + errors["base"] = "not_allowed" + else: + fullpath = str(get_path.absolute()) + await self.async_set_unique_id(fullpath) + self._abort_if_unique_id_configured() + + name = str(user_input[CONF_FILE_PATH]).rsplit("/", maxsplit=1)[-1] + return self.async_create_entry( + title=name, + data={CONF_FILE_PATH: user_input[CONF_FILE_PATH]}, + ) + + return self.async_show_form( + step_id="user", data_schema=DATA_SCHEMA, errors=errors + ) + + async def async_step_import(self, user_input: dict[str, Any]) -> FlowResult: + """Handle import from configuration.yaml.""" + return await self.async_step_user(user_input) + + +class NotValidError(Exception): + """Path is not valid error.""" + + +class NotAllowedError(Exception): + """Path is not allowed error.""" diff --git a/homeassistant/components/filesize/const.py b/homeassistant/components/filesize/const.py new file mode 100644 index 00000000000..a47f1f99d38 --- /dev/null +++ b/homeassistant/components/filesize/const.py @@ -0,0 +1,8 @@ +"""The filesize constants.""" + +from homeassistant.const import Platform + +DOMAIN = "filesize" +PLATFORMS = [Platform.SENSOR] + +CONF_FILE_PATHS = "file_paths" diff --git a/homeassistant/components/filesize/manifest.json b/homeassistant/components/filesize/manifest.json index 1db5009b7e4..c84fd2f0cc1 100644 --- a/homeassistant/components/filesize/manifest.json +++ b/homeassistant/components/filesize/manifest.json @@ -2,6 +2,7 @@ "domain": "filesize", "name": "File Size", "documentation": "https://www.home-assistant.io/integrations/filesize", - "codeowners": [], - "iot_class": "local_polling" + "codeowners": ["@gjohansson-ST"], + "iot_class": "local_polling", + "config_flow": true } diff --git a/homeassistant/components/filesize/sensor.py b/homeassistant/components/filesize/sensor.py index 56542a0aadd..97fe5f5511d 100644 --- a/homeassistant/components/filesize/sensor.py +++ b/homeassistant/components/filesize/sensor.py @@ -1,7 +1,7 @@ """Sensor for monitoring the size of a file.""" from __future__ import annotations -import datetime +from datetime import datetime, timedelta import logging import os import pathlib @@ -10,86 +10,176 @@ import voluptuous as vol from homeassistant.components.sensor import ( PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + SensorDeviceClass, SensorEntity, + SensorEntityDescription, + SensorStateClass, ) -from homeassistant.const import DATA_MEGABYTES +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.const import CONF_FILE_PATH, DATA_BYTES, DATA_MEGABYTES from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.device_registry import DeviceEntryType +from homeassistant.helpers.entity import DeviceInfo, EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.reload import setup_reload_service from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, + UpdateFailed, +) +import homeassistant.util.dt as dt_util -from . import DOMAIN, PLATFORMS +from .const import CONF_FILE_PATHS, DOMAIN _LOGGER = logging.getLogger(__name__) - -CONF_FILE_PATHS = "file_paths" ICON = "mdi:file" +SENSOR_TYPES = ( + SensorEntityDescription( + key="file", + icon=ICON, + name="Size", + native_unit_of_measurement=DATA_MEGABYTES, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="bytes", + entity_registry_enabled_default=False, + icon=ICON, + name="Size bytes", + native_unit_of_measurement=DATA_BYTES, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + ), + SensorEntityDescription( + key="last_updated", + entity_registry_enabled_default=False, + icon=ICON, + name="Last Updated", + device_class=SensorDeviceClass.TIMESTAMP, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + ), +) + PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( {vol.Required(CONF_FILE_PATHS): vol.All(cv.ensure_list, [cv.isfile])} ) -def setup_platform( +async def async_setup_platform( hass: HomeAssistant, config: ConfigType, - add_entities: AddEntitiesCallback, + async_add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the file size sensor.""" - - setup_reload_service(hass, DOMAIN, PLATFORMS) - - sensors = [] - paths = set() + _LOGGER.warning( + # Filesize config flow added in 2022.4 and should be removed in 2022.6 + "Configuration of the Filesize sensor platform in YAML is deprecated and " + "will be removed in Home Assistant 2022.6; Your existing configuration " + "has been imported into the UI automatically and can be safely removed " + "from your configuration.yaml file" + ) for path in config[CONF_FILE_PATHS]: - try: - fullpath = str(pathlib.Path(path).absolute()) - except OSError as error: - _LOGGER.error("Can not access file %s, error %s", path, error) - continue - - if fullpath in paths: - continue - paths.add(fullpath) - - if not hass.config.is_allowed_path(path): - _LOGGER.error("Filepath %s is not valid or allowed", path) - continue - - sensors.append(Filesize(fullpath)) - - if sensors: - add_entities(sensors, True) + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={CONF_FILE_PATH: path}, + ) + ) -class Filesize(SensorEntity): - """Encapsulates file size information.""" +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the platform from config entry.""" - _attr_native_unit_of_measurement = DATA_MEGABYTES - _attr_icon = ICON + path = entry.data[CONF_FILE_PATH] + get_path = await hass.async_add_executor_job(pathlib.Path, path) + fullpath = str(get_path.absolute()) - def __init__(self, path: str) -> None: - """Initialize the data object.""" - self._path = path # Need to check its a valid path - self._attr_name = path.split("/")[-1] + coordinator = FileSizeCoordinator(hass, fullpath) + await coordinator.async_config_entry_first_refresh() - def update(self) -> None: - """Update the sensor.""" + if get_path.exists() and get_path.is_file(): + async_add_entities( + [ + FilesizeEntity(description, fullpath, entry.entry_id, coordinator) + for description in SENSOR_TYPES + ] + ) + + +class FileSizeCoordinator(DataUpdateCoordinator): + """Filesize coordinator.""" + + def __init__(self, hass: HomeAssistant, path: str) -> None: + """Initialize filesize coordinator.""" + super().__init__( + hass, + _LOGGER, + name=DOMAIN, + update_interval=timedelta(seconds=60), + ) + self._path = path + + async def _async_update_data(self) -> dict[str, float | int | datetime]: + """Fetch file information.""" try: statinfo = os.stat(self._path) except OSError as error: - _LOGGER.error("Can not retrieve file statistics %s", error) - self._attr_native_value = None - return + raise UpdateFailed(f"Can not retrieve file statistics {error}") from error size = statinfo.st_size - last_updated = datetime.datetime.fromtimestamp(statinfo.st_mtime).isoformat() - self._attr_native_value = round(size / 1e6, 2) if size else None - self._attr_extra_state_attributes = { - "path": self._path, - "last_updated": last_updated, + last_updated = datetime.fromtimestamp(statinfo.st_mtime).replace( + tzinfo=dt_util.UTC + ) + _LOGGER.debug("size %s, last updated %s", size, last_updated) + data: dict[str, int | float | datetime] = { + "file": round(size / 1e6, 2), "bytes": size, + "last_updated": last_updated, } + + return data + + +class FilesizeEntity(CoordinatorEntity[FileSizeCoordinator], SensorEntity): + """Encapsulates file size information.""" + + entity_description: SensorEntityDescription + + def __init__( + self, + description: SensorEntityDescription, + path: str, + entry_id: str, + coordinator: FileSizeCoordinator, + ) -> None: + """Initialize the data object.""" + super().__init__(coordinator) + base_name = path.split("/")[-1] + self._attr_name = f"{base_name} {description.name}" + self._attr_unique_id = ( + entry_id if description.key == "file" else f"{entry_id}-{description.key}" + ) + self.entity_description = description + self._attr_device_info = DeviceInfo( + entry_type=DeviceEntryType.SERVICE, + identifiers={(DOMAIN, entry_id)}, + name=base_name, + ) + + @property + def native_value(self) -> float | int | datetime: + """Return the value of the sensor.""" + value: float | int | datetime = self.coordinator.data[ + self.entity_description.key + ] + return value diff --git a/homeassistant/components/filesize/services.yaml b/homeassistant/components/filesize/services.yaml deleted file mode 100644 index a794303f8f1..00000000000 --- a/homeassistant/components/filesize/services.yaml +++ /dev/null @@ -1,3 +0,0 @@ -reload: - name: Reload - description: Reload all filesize entities. diff --git a/homeassistant/components/filesize/strings.json b/homeassistant/components/filesize/strings.json new file mode 100644 index 00000000000..90c286e7088 --- /dev/null +++ b/homeassistant/components/filesize/strings.json @@ -0,0 +1,19 @@ +{ + "config": { + "step": { + "user": { + "data": { + "file_path": "Path to file" + } + } + }, + "error": { + "not_valid": "Path is not valid", + "not_allowed": "Path is not allowed" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" + } + }, + "title": "Filesize" +} diff --git a/homeassistant/components/filesize/translations/en.json b/homeassistant/components/filesize/translations/en.json new file mode 100644 index 00000000000..cd8954e5a71 --- /dev/null +++ b/homeassistant/components/filesize/translations/en.json @@ -0,0 +1,19 @@ +{ + "config": { + "step": { + "user": { + "data": { + "file_path": "Path to file" + } + } + }, + "error": { + "not_valid": "Path is not valid", + "not_allowed": "Path is not allowed" + }, + "abort": { + "already_configured": "Filepath is already configured" + } + }, + "title": "Filesize" + } \ No newline at end of file diff --git a/homeassistant/components/filter/sensor.py b/homeassistant/components/filter/sensor.py index a5b54a621a7..59ba706577c 100644 --- a/homeassistant/components/filter/sensor.py +++ b/homeassistant/components/filter/sensor.py @@ -13,7 +13,7 @@ import voluptuous as vol from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.components.input_number import DOMAIN as INPUT_NUMBER_DOMAIN -from homeassistant.components.recorder import history +from homeassistant.components.recorder import get_instance, history from homeassistant.components.sensor import ( ATTR_STATE_CLASS, DEVICE_CLASSES as SENSOR_DEVICE_CLASSES, @@ -296,7 +296,7 @@ class SensorFilter(SensorEntity): # Retrieve the largest window_size of each type if largest_window_items > 0: - filter_history = await self.hass.async_add_executor_job( + filter_history = await get_instance(self.hass).async_add_executor_job( partial( history.get_last_state_changes, self.hass, @@ -308,7 +308,7 @@ class SensorFilter(SensorEntity): history_list.extend(filter_history[self._entity]) if largest_window_time > timedelta(seconds=0): start = dt_util.utcnow() - largest_window_time - filter_history = await self.hass.async_add_executor_job( + filter_history = await get_instance(self.hass).async_add_executor_job( partial( history.state_changes_during_period, self.hass, diff --git a/homeassistant/components/fireservicerota/switch.py b/homeassistant/components/fireservicerota/switch.py index e625ac5deb5..48ec1a77c54 100644 --- a/homeassistant/components/fireservicerota/switch.py +++ b/homeassistant/components/fireservicerota/switch.py @@ -136,16 +136,15 @@ class ResponseSwitch(SwitchEntity): """Handle updated incident data from the client.""" self.async_schedule_update_ha_state(True) - async def async_update(self) -> bool: + async def async_update(self) -> None: """Update FireServiceRota response data.""" data = await self._client.async_response_update() if not data or "status" not in data: - return False + return self._state = data["status"] == "acknowledged" self._state_attributes = data self._state_icon = data["status"] _LOGGER.debug("Set state of entity 'Response Switch' to '%s'", self._state) - return True diff --git a/homeassistant/components/fireservicerota/translations/fr.json b/homeassistant/components/fireservicerota/translations/fr.json index 477e8df621c..bf663e8ad90 100644 --- a/homeassistant/components/fireservicerota/translations/fr.json +++ b/homeassistant/components/fireservicerota/translations/fr.json @@ -8,14 +8,14 @@ "default": "Authentification r\u00e9ussie" }, "error": { - "invalid_auth": "Authentification invalide" + "invalid_auth": "Authentification non valide" }, "step": { "reauth": { "data": { "password": "Mot de passe" }, - "description": "Les jetons d'authentification sont invalides, connectez-vous pour les recr\u00e9er." + "description": "Les jetons d'authentification ne sont plus valides, connectez-vous pour les recr\u00e9er." }, "user": { "data": { diff --git a/homeassistant/components/firmata/strings.json b/homeassistant/components/firmata/strings.json index 90e62325c2a..dd4e308a485 100644 --- a/homeassistant/components/firmata/strings.json +++ b/homeassistant/components/firmata/strings.json @@ -1,8 +1,8 @@ { - "config": { - "abort": { - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" - }, - "step": {} - } + "config": { + "abort": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + }, + "step": {} + } } diff --git a/homeassistant/components/fivem/__init__.py b/homeassistant/components/fivem/__init__.py index 2004aacd165..1fe5ccf0b8f 100644 --- a/homeassistant/components/fivem/__init__.py +++ b/homeassistant/components/fivem/__init__.py @@ -128,10 +128,9 @@ class FiveMEntityDescription(EntityDescription): extra_attrs: list[str] | None = None -class FiveMEntity(CoordinatorEntity): +class FiveMEntity(CoordinatorEntity[FiveMDataUpdateCoordinator]): """Representation of a FiveM base entity.""" - coordinator: FiveMDataUpdateCoordinator entity_description: FiveMEntityDescription def __init__( diff --git a/homeassistant/components/fivem/manifest.json b/homeassistant/components/fivem/manifest.json index 4a18df0fc95..8ad886f917a 100644 --- a/homeassistant/components/fivem/manifest.json +++ b/homeassistant/components/fivem/manifest.json @@ -3,11 +3,7 @@ "name": "FiveM", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/fivem", - "requirements": [ - "fivem-api==0.1.2" - ], - "codeowners": [ - "@Sander0542" - ], + "requirements": ["fivem-api==0.1.2"], + "codeowners": ["@Sander0542"], "iot_class": "local_polling" -} \ No newline at end of file +} diff --git a/homeassistant/components/fivem/strings.json b/homeassistant/components/fivem/strings.json index 03afcc11f27..4378ef535bd 100644 --- a/homeassistant/components/fivem/strings.json +++ b/homeassistant/components/fivem/strings.json @@ -18,4 +18,4 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" } } -} \ No newline at end of file +} diff --git a/homeassistant/components/fjaraskupan/binary_sensor.py b/homeassistant/components/fjaraskupan/binary_sensor.py index 9f24a3d39d2..f1672530c45 100644 --- a/homeassistant/components/fjaraskupan/binary_sensor.py +++ b/homeassistant/components/fjaraskupan/binary_sensor.py @@ -67,7 +67,7 @@ async def async_setup_entry( async_setup_entry_platform(hass, config_entry, async_add_entities, _constructor) -class BinarySensor(CoordinatorEntity[State], BinarySensorEntity): +class BinarySensor(CoordinatorEntity[DataUpdateCoordinator[State]], BinarySensorEntity): """Grease filter sensor.""" entity_description: EntityDescription diff --git a/homeassistant/components/fjaraskupan/fan.py b/homeassistant/components/fjaraskupan/fan.py index bbc04a9607c..4b04910a167 100644 --- a/homeassistant/components/fjaraskupan/fan.py +++ b/homeassistant/components/fjaraskupan/fan.py @@ -70,7 +70,7 @@ async def async_setup_entry( async_setup_entry_platform(hass, config_entry, async_add_entities, _constructor) -class Fan(CoordinatorEntity[State], FanEntity): +class Fan(CoordinatorEntity[DataUpdateCoordinator[State]], FanEntity): """Fan entity.""" def __init__( @@ -100,7 +100,6 @@ class Fan(CoordinatorEntity[State], FanEntity): async def async_turn_on( self, - speed: str = None, percentage: int = None, preset_mode: str = None, **kwargs, diff --git a/homeassistant/components/fjaraskupan/light.py b/homeassistant/components/fjaraskupan/light.py index 8c44460a099..7c1e5d34138 100644 --- a/homeassistant/components/fjaraskupan/light.py +++ b/homeassistant/components/fjaraskupan/light.py @@ -37,7 +37,7 @@ async def async_setup_entry( async_setup_entry_platform(hass, config_entry, async_add_entities, _constructor) -class Light(CoordinatorEntity[State], LightEntity): +class Light(CoordinatorEntity[DataUpdateCoordinator[State]], LightEntity): """Light device.""" def __init__( diff --git a/homeassistant/components/fjaraskupan/manifest.json b/homeassistant/components/fjaraskupan/manifest.json index d01995bd28b..3ff6e599a6b 100644 --- a/homeassistant/components/fjaraskupan/manifest.json +++ b/homeassistant/components/fjaraskupan/manifest.json @@ -3,12 +3,8 @@ "name": "Fj\u00e4r\u00e5skupan", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/fjaraskupan", - "requirements": [ - "fjaraskupan==1.0.2" - ], - "codeowners": [ - "@elupus" - ], + "requirements": ["fjaraskupan==1.0.2"], + "codeowners": ["@elupus"], "iot_class": "local_polling", "loggers": ["bleak", "fjaraskupan"] -} \ No newline at end of file +} diff --git a/homeassistant/components/fjaraskupan/number.py b/homeassistant/components/fjaraskupan/number.py index eecb0b3b8e1..bbde9bd8898 100644 --- a/homeassistant/components/fjaraskupan/number.py +++ b/homeassistant/components/fjaraskupan/number.py @@ -34,7 +34,9 @@ async def async_setup_entry( async_setup_entry_platform(hass, config_entry, async_add_entities, _constructor) -class PeriodicVentingTime(CoordinatorEntity[State], NumberEntity): +class PeriodicVentingTime( + CoordinatorEntity[DataUpdateCoordinator[State]], NumberEntity +): """Periodic Venting.""" _attr_max_value: float = 59 diff --git a/homeassistant/components/fjaraskupan/sensor.py b/homeassistant/components/fjaraskupan/sensor.py index 8c19b3e3cec..fbd9d5f6d08 100644 --- a/homeassistant/components/fjaraskupan/sensor.py +++ b/homeassistant/components/fjaraskupan/sensor.py @@ -39,7 +39,7 @@ async def async_setup_entry( async_setup_entry_platform(hass, config_entry, async_add_entities, _constructor) -class RssiSensor(CoordinatorEntity[State], SensorEntity): +class RssiSensor(CoordinatorEntity[DataUpdateCoordinator[State]], SensorEntity): """Sensor device.""" def __init__( diff --git a/homeassistant/components/fjaraskupan/strings.json b/homeassistant/components/fjaraskupan/strings.json index c72fc777772..c6d5edd02d4 100644 --- a/homeassistant/components/fjaraskupan/strings.json +++ b/homeassistant/components/fjaraskupan/strings.json @@ -10,4 +10,4 @@ "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]" } } -} \ No newline at end of file +} diff --git a/homeassistant/components/flick_electric/translations/fr.json b/homeassistant/components/flick_electric/translations/fr.json index fc7605c0975..9757b7cc39b 100644 --- a/homeassistant/components/flick_electric/translations/fr.json +++ b/homeassistant/components/flick_electric/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/flipr/manifest.json b/homeassistant/components/flipr/manifest.json index 77388393d3f..35fbe8259a2 100644 --- a/homeassistant/components/flipr/manifest.json +++ b/homeassistant/components/flipr/manifest.json @@ -3,11 +3,8 @@ "name": "Flipr", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/flipr", - "requirements": [ - "flipr-api==1.4.2"], - "codeowners": [ - "@cnico" - ], + "requirements": ["flipr-api==1.4.2"], + "codeowners": ["@cnico"], "iot_class": "cloud_polling", "loggers": ["flipr_api"] } diff --git a/homeassistant/components/flipr/translations/fr.json b/homeassistant/components/flipr/translations/fr.json index ec9260aa8a7..7b0bad8b9e3 100644 --- a/homeassistant/components/flipr/translations/fr.json +++ b/homeassistant/components/flipr/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "no_flipr_id_found": "Aucun identifiant Flipr n'est associ\u00e9 \u00e0 votre compte pour le moment. Vous devez d'abord v\u00e9rifier qu'il fonctionne avec l'application mobile de Flipr.", "unknown": "Erreur inattendue" }, @@ -19,7 +19,7 @@ }, "user": { "data": { - "email": "Email", + "email": "Courriel", "password": "Mot de passe" }, "description": "Connectez-vous \u00e0 votre compte Flipr.", diff --git a/homeassistant/components/flo/services.yaml b/homeassistant/components/flo/services.yaml index fb3dbb3ee0a..a074ebafe99 100644 --- a/homeassistant/components/flo/services.yaml +++ b/homeassistant/components/flo/services.yaml @@ -15,9 +15,9 @@ set_sleep_mode: selector: select: options: - - '120' - - '1440' - - '4320' + - "120" + - "1440" + - "4320" revert_to_mode: name: Revert to mode description: The mode to revert to after sleep_minutes has elapsed. @@ -25,8 +25,8 @@ set_sleep_mode: selector: select: options: - - 'away' - - 'home' + - "away" + - "home" set_away_mode: name: Set away mode description: Set the location into away mode. diff --git a/homeassistant/components/flo/translations/fr.json b/homeassistant/components/flo/translations/fr.json index 45620fe7795..bb317c2149f 100644 --- a/homeassistant/components/flo/translations/fr.json +++ b/homeassistant/components/flo/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/flume/strings.json b/homeassistant/components/flume/strings.json index 5c95cfca22e..080f10deee1 100644 --- a/homeassistant/components/flume/strings.json +++ b/homeassistant/components/flume/strings.json @@ -22,7 +22,7 @@ "data": { "password": "[%key:common::config_flow::data::password%]" } - } + } }, "abort": { "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", diff --git a/homeassistant/components/flume/translations/fr.json b/homeassistant/components/flume/translations/fr.json index 43157234eff..588bacc687e 100644 --- a/homeassistant/components/flume/translations/fr.json +++ b/homeassistant/components/flume/translations/fr.json @@ -6,7 +6,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/flux_led/__init__.py b/homeassistant/components/flux_led/__init__.py index 997f053aa3c..17dc28a5edf 100644 --- a/homeassistant/components/flux_led/__init__.py +++ b/homeassistant/components/flux_led/__init__.py @@ -41,6 +41,7 @@ from .discovery import ( async_trigger_discovery, async_update_entry_from_discovery, ) +from .util import mac_matches_by_one _LOGGER = logging.getLogger(__name__) @@ -90,18 +91,27 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def _async_migrate_unique_ids(hass: HomeAssistant, entry: ConfigEntry) -> None: """Migrate entities when the mac address gets discovered.""" - if not (unique_id := entry.unique_id): - return - entry_id = entry.entry_id @callback def _async_migrator(entity_entry: er.RegistryEntry) -> dict[str, Any] | None: - # Old format {entry_id}..... - # New format {unique_id}.... - entity_unique_id = entity_entry.unique_id - if not entity_unique_id.startswith(entry_id): + if not (unique_id := entry.unique_id): + return None + entry_id = entry.entry_id + entity_unique_id = entity_entry.unique_id + entity_mac = entity_unique_id[: len(unique_id)] + new_unique_id = None + if entity_unique_id.startswith(entry_id): + # Old format {entry_id}....., New format {unique_id}.... + new_unique_id = f"{unique_id}{entity_unique_id[len(entry_id):]}" + elif ( + ":" in entity_mac + and entity_mac != unique_id + and mac_matches_by_one(entity_mac, unique_id) + ): + # Old format {dhcp_mac}....., New format {discovery_mac}.... + new_unique_id = f"{unique_id}{entity_unique_id[len(unique_id):]}" + else: return None - new_unique_id = f"{unique_id}{entity_unique_id[len(entry_id):]}" _LOGGER.info( "Migrating unique_id from [%s] to [%s]", entity_unique_id, @@ -112,6 +122,13 @@ async def _async_migrate_unique_ids(hass: HomeAssistant, entry: ConfigEntry) -> await er.async_migrate_entries(hass, entry.entry_id, _async_migrator) +async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Handle options update.""" + coordinator: FluxLedUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + if entry.title != coordinator.title: + await hass.config_entries.async_reload(entry.entry_id) + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Flux LED/MagicLight from a config entry.""" host = entry.data[CONF_HOST] @@ -148,7 +165,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if entry.unique_id and discovery.get(ATTR_ID): mac = dr.format_mac(cast(str, discovery[ATTR_ID])) - if mac != entry.unique_id: + if not mac_matches_by_one(mac, entry.unique_id): # The device is offline and another flux_led device is now using the ip address raise ConfigEntryNotReady( f"Unexpected device found at {host}; Expected {entry.unique_id}, found {mac}" @@ -157,7 +174,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if not discovery_cached: # Only update the entry once we have verified the unique id # is either missing or we have verified it matches - async_update_entry_from_discovery(hass, entry, discovery, device.model_num) + async_update_entry_from_discovery( + hass, entry, discovery, device.model_num, True + ) await _async_migrate_unique_ids(hass, entry) @@ -173,6 +192,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await _async_sync_time() # set at startup entry.async_on_unload(async_track_time_change(hass, _async_sync_time, 2, 40, 30)) + # There must not be any awaits between here and the return + # to avoid a race condition where the add_update_listener is not + # in place in time for the check in async_update_entry_from_discovery + entry.async_on_unload(entry.add_update_listener(_async_update_listener)) return True diff --git a/homeassistant/components/flux_led/config_flow.py b/homeassistant/components/flux_led/config_flow.py index 5bdd18d1dbd..dfb6ff4a174 100644 --- a/homeassistant/components/flux_led/config_flow.py +++ b/homeassistant/components/flux_led/config_flow.py @@ -19,7 +19,7 @@ from homeassistant import config_entries from homeassistant.components import dhcp from homeassistant.const import CONF_HOST from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult +from homeassistant.data_entry_flow import AbortFlow, FlowResult from homeassistant.helpers import device_registry as dr from homeassistant.helpers.typing import DiscoveryInfoType @@ -43,7 +43,7 @@ from .discovery import ( async_populate_data_from_discovery, async_update_entry_from_discovery, ) -from .util import format_as_flux_mac +from .util import format_as_flux_mac, mac_matches_by_one CONF_DEVICE: Final = "device" @@ -57,6 +57,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Initialize the config flow.""" self._discovered_devices: dict[str, FluxLEDDiscovery] = {} self._discovered_device: FluxLEDDiscovery | None = None + self._allow_update_mac = False @staticmethod @callback @@ -85,37 +86,65 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self, discovery_info: DiscoveryInfoType ) -> FlowResult: """Handle integration discovery.""" + self._allow_update_mac = True self._discovered_device = cast(FluxLEDDiscovery, discovery_info) return await self._async_handle_discovery() + async def _async_set_discovered_mac( + self, device: FluxLEDDiscovery, allow_update_mac: bool + ) -> None: + """Set the discovered mac. + + We only allow it to be updated if it comes from udp + discovery since the dhcp mac can be one digit off from + the udp discovery mac for devices with multiple network interfaces + """ + mac_address = device[ATTR_ID] + assert mac_address is not None + mac = dr.format_mac(mac_address) + await self.async_set_unique_id(mac) + for entry in self._async_current_entries(include_ignore=False): + if entry.data[CONF_HOST] == device[ATTR_IPADDR] or ( + entry.unique_id + and ":" in entry.unique_id + and mac_matches_by_one(entry.unique_id, mac) + ): + if async_update_entry_from_discovery( + self.hass, entry, device, None, allow_update_mac + ): + self.hass.async_create_task( + self.hass.config_entries.async_reload(entry.entry_id) + ) + raise AbortFlow("already_configured") + async def _async_handle_discovery(self) -> FlowResult: """Handle any discovery.""" device = self._discovered_device assert device is not None - mac_address = device[ATTR_ID] - assert mac_address is not None - mac = dr.format_mac(mac_address) + await self._async_set_discovered_mac(device, self._allow_update_mac) host = device[ATTR_IPADDR] - await self.async_set_unique_id(mac) - for entry in self._async_current_entries(include_ignore=False): - if entry.unique_id == mac or entry.data[CONF_HOST] == host: - if async_update_entry_from_discovery(self.hass, entry, device, None): - self.hass.async_create_task( - self.hass.config_entries.async_reload(entry.entry_id) - ) - return self.async_abort(reason="already_configured") self.context[CONF_HOST] = host for progress in self._async_in_progress(): if progress.get("context", {}).get(CONF_HOST) == host: return self.async_abort(reason="already_in_progress") if not device[ATTR_MODEL_DESCRIPTION]: + mac_address = device[ATTR_ID] + assert mac_address is not None + mac = dr.format_mac(mac_address) try: device = await self._async_try_connect(host, device) except FLUX_LED_EXCEPTIONS: return self.async_abort(reason="cannot_connect") else: - if device[ATTR_MODEL_DESCRIPTION]: + discovered_mac = device[ATTR_ID] + if device[ATTR_MODEL_DESCRIPTION] or ( + discovered_mac is not None + and (formatted_discovered_mac := dr.format_mac(discovered_mac)) + and formatted_discovered_mac != mac + and mac_matches_by_one(discovered_mac, mac) + ): self._discovered_device = device + await self._async_set_discovered_mac(device, True) return await self.async_step_discovery_confirm() async def async_step_discovery_confirm( diff --git a/homeassistant/components/flux_led/const.py b/homeassistant/components/flux_led/const.py index 9113d52f5c8..9124be5bb9e 100644 --- a/homeassistant/components/flux_led/const.py +++ b/homeassistant/components/flux_led/const.py @@ -53,7 +53,6 @@ STARTUP_SCAN_TIMEOUT: Final = 5 DISCOVER_SCAN_TIMEOUT: Final = 10 DIRECTED_DISCOVERY_TIMEOUT: Final = 15 -CONF_MODEL: Final = "model" CONF_MODEL_NUM: Final = "model_num" CONF_MODEL_INFO: Final = "model_info" CONF_MODEL_DESCRIPTION: Final = "model_description" diff --git a/homeassistant/components/flux_led/coordinator.py b/homeassistant/components/flux_led/coordinator.py index 0a285518c66..5f2c3c097c0 100644 --- a/homeassistant/components/flux_led/coordinator.py +++ b/homeassistant/components/flux_led/coordinator.py @@ -28,6 +28,7 @@ class FluxLedUpdateCoordinator(DataUpdateCoordinator): ) -> None: """Initialize DataUpdateCoordinator to gather data for specific device.""" self.device = device + self.title = entry.title self.entry = entry super().__init__( hass, diff --git a/homeassistant/components/flux_led/discovery.py b/homeassistant/components/flux_led/discovery.py index 62b80243c8b..67dbbc74e2e 100644 --- a/homeassistant/components/flux_led/discovery.py +++ b/homeassistant/components/flux_led/discovery.py @@ -19,19 +19,19 @@ from flux_led.const import ( ATTR_REMOTE_ACCESS_PORT, ATTR_VERSION_NUM, ) +from flux_led.models_db import get_model_description from flux_led.scanner import FluxLEDDiscovery from homeassistant import config_entries from homeassistant.components import network -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_HOST, CONF_NAME +from homeassistant.config_entries import ConfigEntry, ConfigEntryState +from homeassistant.const import CONF_HOST, CONF_MODEL, CONF_NAME from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import device_registry as dr from homeassistant.util.network import is_ip_address from .const import ( CONF_MINOR_VERSION, - CONF_MODEL, CONF_MODEL_DESCRIPTION, CONF_MODEL_INFO, CONF_MODEL_NUM, @@ -42,7 +42,7 @@ from .const import ( DOMAIN, FLUX_LED_DISCOVERY, ) -from .util import format_as_flux_mac +from .util import format_as_flux_mac, mac_matches_by_one _LOGGER = logging.getLogger(__name__) @@ -80,13 +80,17 @@ def async_build_cached_discovery(entry: ConfigEntry) -> FluxLEDDiscovery: @callback -def async_name_from_discovery(device: FluxLEDDiscovery) -> str: +def async_name_from_discovery( + device: FluxLEDDiscovery, model_num: int | None = None +) -> str: """Convert a flux_led discovery to a human readable name.""" if (mac_address := device[ATTR_ID]) is None: return device[ATTR_IPADDR] short_mac = mac_address[-6:] if device[ATTR_MODEL_DESCRIPTION]: return f"{device[ATTR_MODEL_DESCRIPTION]} {short_mac}" + if model_num is not None: + return f"{get_model_description(model_num, None)} {short_mac}" return f"{device[ATTR_MODEL]} {short_mac}" @@ -102,9 +106,9 @@ def async_populate_data_from_discovery( device.get(discovery_key) is not None and conf_key not in data_updates # Prefer the model num from TCP instead of UDP - and current_data.get(conf_key) != device[discovery_key] # type: ignore[misc] + and current_data.get(conf_key) != device[discovery_key] # type: ignore[literal-required] ): - data_updates[conf_key] = device[discovery_key] # type: ignore[misc] + data_updates[conf_key] = device[discovery_key] # type: ignore[literal-required] @callback @@ -113,25 +117,33 @@ def async_update_entry_from_discovery( entry: config_entries.ConfigEntry, device: FluxLEDDiscovery, model_num: int | None, + allow_update_mac: bool, ) -> bool: """Update a config entry from a flux_led discovery.""" data_updates: dict[str, Any] = {} mac_address = device[ATTR_ID] assert mac_address is not None updates: dict[str, Any] = {} - if not entry.unique_id: - updates["unique_id"] = dr.format_mac(mac_address) + formatted_mac = dr.format_mac(mac_address) + if not entry.unique_id or ( + allow_update_mac + and entry.unique_id != formatted_mac + and mac_matches_by_one(formatted_mac, entry.unique_id) + ): + updates["unique_id"] = formatted_mac if model_num and entry.data.get(CONF_MODEL_NUM) != model_num: data_updates[CONF_MODEL_NUM] = model_num async_populate_data_from_discovery(entry.data, data_updates, device) if is_ip_address(entry.title): - updates["title"] = async_name_from_discovery(device) + updates["title"] = async_name_from_discovery(device, model_num) title_matches_name = entry.title == entry.data.get(CONF_NAME) if data_updates or title_matches_name: updates["data"] = {**entry.data, **data_updates} if title_matches_name: del updates["data"][CONF_NAME] - if updates: + # If the title has changed and the config entry is loaded, a listener is + # in place, and we should not reload + if updates and not ("title" in updates and entry.state is ConfigEntryState.LOADED): return hass.config_entries.async_update_entry(entry, **updates) return False diff --git a/homeassistant/components/flux_led/entity.py b/homeassistant/components/flux_led/entity.py index da92931d1e6..ef9038b1435 100644 --- a/homeassistant/components/flux_led/entity.py +++ b/homeassistant/components/flux_led/entity.py @@ -15,6 +15,7 @@ from homeassistant.const import ( ATTR_MODEL, ATTR_NAME, ATTR_SW_VERSION, + CONF_MODEL, CONF_NAME, ) from homeassistant.core import callback @@ -23,7 +24,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import DeviceInfo, Entity from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import CONF_MINOR_VERSION, CONF_MODEL, DOMAIN, SIGNAL_STATE_UPDATED +from .const import CONF_MINOR_VERSION, DOMAIN, SIGNAL_STATE_UPDATED from .coordinator import FluxLedUpdateCoordinator @@ -66,11 +67,9 @@ class FluxBaseEntity(Entity): self._attr_device_info = _async_device_info(self._device, entry) -class FluxEntity(CoordinatorEntity): +class FluxEntity(CoordinatorEntity[FluxLedUpdateCoordinator]): """Representation of a Flux entity with a coordinator.""" - coordinator: FluxLedUpdateCoordinator - def __init__( self, coordinator: FluxLedUpdateCoordinator, diff --git a/homeassistant/components/flux_led/light.py b/homeassistant/components/flux_led/light.py index 0d179cd2b77..2942a13c734 100644 --- a/homeassistant/components/flux_led/light.py +++ b/homeassistant/components/flux_led/light.py @@ -187,7 +187,9 @@ async def async_setup_entry( ) -class FluxLight(FluxOnOffEntity, CoordinatorEntity, LightEntity): +class FluxLight( + FluxOnOffEntity, CoordinatorEntity[FluxLedUpdateCoordinator], LightEntity +): """Representation of a Flux light.""" _attr_supported_features = SUPPORT_TRANSITION | SUPPORT_EFFECT diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index a1b541c177d..cbd5e51b163 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -9,43 +9,42 @@ "codeowners": ["@icemanch", "@bdraco"], "iot_class": "local_push", "dhcp": [ - {"registered_devices": true}, - { - "macaddress": "18B905*", - "hostname": "[ba][lk]*" - }, - { - "macaddress": "249494*", - "hostname": "[ba][lk]*" - }, - { - "macaddress": "7CB94C*", - "hostname": "[ba][lk]*" - }, - { - "macaddress": "ACCF23*", - "hostname": "[hba][flk]*" - }, - { - "macaddress": "B4E842*", - "hostname": "[ba][lk]*" - }, - { - "macaddress": "F0FE6B*", - "hostname": "[hba][flk]*" - }, - { - "macaddress": "8CCE4E*", - "hostname": "lwip*" - }, - { - "hostname": "zengge_[0-9a-f][0-9a-f]_*" - }, - { - "macaddress": "C82E47*", - "hostname": "sta*" - } + { "registered_devices": true }, + { + "macaddress": "18B905*", + "hostname": "[ba][lk]*" + }, + { + "macaddress": "249494*", + "hostname": "[ba][lk]*" + }, + { + "macaddress": "7CB94C*", + "hostname": "[ba][lk]*" + }, + { + "macaddress": "ACCF23*", + "hostname": "[hba][flk]*" + }, + { + "macaddress": "B4E842*", + "hostname": "[ba][lk]*" + }, + { + "macaddress": "F0FE6B*", + "hostname": "[hba][flk]*" + }, + { + "macaddress": "8CCE4E*", + "hostname": "lwip*" + }, + { + "hostname": "zengge_[0-9a-f][0-9a-f]_*" + }, + { + "macaddress": "C82E47*", + "hostname": "sta*" + } ], "loggers": ["flux_led"] } - diff --git a/homeassistant/components/flux_led/number.py b/homeassistant/components/flux_led/number.py index b4e6e87a829..06f706aee21 100644 --- a/homeassistant/components/flux_led/number.py +++ b/homeassistant/components/flux_led/number.py @@ -92,7 +92,9 @@ async def async_setup_entry( async_add_entities(entities) -class FluxSpeedNumber(FluxEntity, CoordinatorEntity, NumberEntity): +class FluxSpeedNumber( + FluxEntity, CoordinatorEntity[FluxLedUpdateCoordinator], NumberEntity +): """Defines a flux_led speed number.""" _attr_min_value = 1 @@ -122,7 +124,9 @@ class FluxSpeedNumber(FluxEntity, CoordinatorEntity, NumberEntity): await self.coordinator.async_request_refresh() -class FluxConfigNumber(FluxEntity, CoordinatorEntity, NumberEntity): +class FluxConfigNumber( + FluxEntity, CoordinatorEntity[FluxLedUpdateCoordinator], NumberEntity +): """Base class for flux config numbers.""" _attr_entity_category = EntityCategory.CONFIG diff --git a/homeassistant/components/flux_led/services.yaml b/homeassistant/components/flux_led/services.yaml index 8a16f456311..b17d81f9174 100644 --- a/homeassistant/components/flux_led/services.yaml +++ b/homeassistant/components/flux_led/services.yaml @@ -27,8 +27,8 @@ set_custom_effect: unit_of_measurement: "%" transition: description: Effect transition. - example: 'jump' - default: 'gradual' + example: "jump" + default: "gradual" required: false selector: select: @@ -66,8 +66,8 @@ set_zones: unit_of_measurement: "%" effect: description: Effect - example: 'running_water' - default: 'static' + example: "running_water" + default: "static" required: false selector: select: diff --git a/homeassistant/components/flux_led/strings.json b/homeassistant/components/flux_led/strings.json index f311f559589..98fd86d3fa2 100644 --- a/homeassistant/components/flux_led/strings.json +++ b/homeassistant/components/flux_led/strings.json @@ -10,11 +10,11 @@ }, "discovery_confirm": { "description": "Do you want to setup {model} {id} ({ipaddr})?" - } + } }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" - }, + }, "abort": { "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", diff --git a/homeassistant/components/flux_led/switch.py b/homeassistant/components/flux_led/switch.py index e8c34f12b11..18b079beff9 100644 --- a/homeassistant/components/flux_led/switch.py +++ b/homeassistant/components/flux_led/switch.py @@ -52,7 +52,9 @@ async def async_setup_entry( async_add_entities(entities) -class FluxSwitch(FluxOnOffEntity, CoordinatorEntity, SwitchEntity): +class FluxSwitch( + FluxOnOffEntity, CoordinatorEntity[FluxLedUpdateCoordinator], SwitchEntity +): """Representation of a Flux switch.""" async def _async_turn_on(self, **kwargs: Any) -> None: diff --git a/homeassistant/components/flux_led/translations/fr.json b/homeassistant/components/flux_led/translations/fr.json index c2177a0cb1f..baea6899999 100644 --- a/homeassistant/components/flux_led/translations/fr.json +++ b/homeassistant/components/flux_led/translations/fr.json @@ -17,7 +17,7 @@ "data": { "host": "H\u00f4te" }, - "description": "Si vous laissez l'h\u00f4te vide, la d\u00e9couverte sera utilis\u00e9e pour trouver des p\u00e9riph\u00e9riques." + "description": "Si vous laissez l'h\u00f4te vide, la d\u00e9couverte sera utilis\u00e9e pour trouver des appareils." } } }, diff --git a/homeassistant/components/flux_led/translations/zh-Hant.json b/homeassistant/components/flux_led/translations/zh-Hant.json index 4e14b58ff18..e1c5ded8c75 100644 --- a/homeassistant/components/flux_led/translations/zh-Hant.json +++ b/homeassistant/components/flux_led/translations/zh-Hant.json @@ -17,7 +17,7 @@ "data": { "host": "\u4e3b\u6a5f\u7aef" }, - "description": "\u5047\u5982\u4e3b\u6a5f\u7aef\u4f4d\u5740\u6b04\u4f4d\u70ba\u7a7a\u767d\uff0c\u5c07\u6703\u63a2\u7d22\u6240\u6709\u53ef\u7528\u88dd\u7f6e\u3002" + "description": "\u5047\u5982\u4e3b\u6a5f\u7aef\u4f4d\u5740\u6b04\u4f4d\u70ba\u7a7a\u767d\uff0c\u5c07\u6703\u641c\u7d22\u6240\u6709\u53ef\u7528\u88dd\u7f6e\u3002" } } }, @@ -27,7 +27,7 @@ "data": { "custom_effect_colors": "\u81ea\u8a02\u7279\u6548\uff1a1 \u5230 16 \u7a2e [R,G,B] \u984f\u8272\u3002\u4f8b\u5982\uff1a[255,0,255]\u3001[60,128,0]", "custom_effect_speed_pct": "\u81ea\u8a02\u7279\u6548\uff1a\u984f\u8272\u5207\u63db\u7684\u901f\u5ea6\u767e\u5206\u6bd4\u3002", - "custom_effect_transition": "\u81ea\u8a02\u7279\u6548\uff1a\u984f\u8272\u9593\u7684\u8f49\u63db\u985e\u578b\u3002", + "custom_effect_transition": "\u81ea\u8a02\u7279\u6548\uff1a\u984f\u8272\u9593\u7684\u8f49\u63db\u985e\u5225\u3002", "mode": "\u9078\u64c7\u4eae\u5ea6\u6a21\u5f0f\u3002" } } diff --git a/homeassistant/components/flux_led/util.py b/homeassistant/components/flux_led/util.py index 70983433e18..9adbacb4273 100644 --- a/homeassistant/components/flux_led/util.py +++ b/homeassistant/components/flux_led/util.py @@ -28,6 +28,18 @@ def _human_readable_option(const_option: str) -> str: return const_option.replace("_", " ").title() +def mac_matches_by_one(formatted_mac_1: str, formatted_mac_2: str) -> bool: + """Check if a mac address is only one digit off. + + Some of the devices have two mac addresses which are + one off from each other. We need to treat them as the same + since its the same device. + """ + mac_int_1 = int(formatted_mac_1.replace(":", ""), 16) + mac_int_2 = int(formatted_mac_2.replace(":", ""), 16) + return abs(mac_int_1 - mac_int_2) < 2 + + def _flux_color_mode_to_hass( flux_color_mode: str | None, flux_color_modes: set[str] ) -> str: diff --git a/homeassistant/components/forecast_solar/__init__.py b/homeassistant/components/forecast_solar/__init__.py index 760ad04af98..18d542c1d3b 100644 --- a/homeassistant/components/forecast_solar/__init__.py +++ b/homeassistant/components/forecast_solar/__init__.py @@ -4,7 +4,7 @@ from __future__ import annotations from datetime import timedelta import logging -from forecast_solar import ForecastSolar +from forecast_solar import Estimate, ForecastSolar from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, Platform @@ -16,6 +16,7 @@ from .const import ( CONF_AZIMUTH, CONF_DAMPING, CONF_DECLINATION, + CONF_INVERTER_SIZE, CONF_MODULES_POWER, DOMAIN, ) @@ -29,6 +30,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # this if statement is here to catch that. api_key = entry.options.get(CONF_API_KEY) or None + if ( + inverter_size := entry.options.get(CONF_INVERTER_SIZE) + ) is not None and inverter_size > 0: + inverter_size = inverter_size / 1000 + session = async_get_clientsession(hass) forecast = ForecastSolar( api_key=api_key, @@ -39,6 +45,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: azimuth=(entry.options[CONF_AZIMUTH] - 180), kwp=(entry.options[CONF_MODULES_POWER] / 1000), damping=entry.options.get(CONF_DAMPING, 0), + inverter=inverter_size, ) # Free account have a resolution of 1 hour, using that as the default @@ -47,7 +54,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if api_key is not None: update_interval = timedelta(minutes=30) - coordinator: DataUpdateCoordinator = DataUpdateCoordinator( + coordinator: DataUpdateCoordinator[Estimate] = DataUpdateCoordinator( hass, logging.getLogger(__name__), name=DOMAIN, diff --git a/homeassistant/components/forecast_solar/config_flow.py b/homeassistant/components/forecast_solar/config_flow.py index e7f41777062..86de17ef285 100644 --- a/homeassistant/components/forecast_solar/config_flow.py +++ b/homeassistant/components/forecast_solar/config_flow.py @@ -15,6 +15,7 @@ from .const import ( CONF_AZIMUTH, CONF_DAMPING, CONF_DECLINATION, + CONF_INVERTER_SIZE, CONF_MODULES_POWER, DOMAIN, ) @@ -118,6 +119,14 @@ class ForecastSolarOptionFlowHandler(OptionsFlow): CONF_DAMPING, default=self.config_entry.options.get(CONF_DAMPING, 0.0), ): vol.Coerce(float), + vol.Optional( + CONF_INVERTER_SIZE, + description={ + "suggested_value": self.config_entry.options.get( + CONF_INVERTER_SIZE + ) + }, + ): vol.Coerce(int), } ), ) diff --git a/homeassistant/components/forecast_solar/const.py b/homeassistant/components/forecast_solar/const.py index 63d5bd10084..d9742cf5dfc 100644 --- a/homeassistant/components/forecast_solar/const.py +++ b/homeassistant/components/forecast_solar/const.py @@ -14,6 +14,7 @@ CONF_DECLINATION = "declination" CONF_AZIMUTH = "azimuth" CONF_MODULES_POWER = "modules power" CONF_DAMPING = "damping" +CONF_INVERTER_SIZE = "inverter_size" SENSORS: tuple[ForecastSolarSensorEntityDescription, ...] = ( ForecastSolarSensorEntityDescription( diff --git a/homeassistant/components/forecast_solar/diagnostics.py b/homeassistant/components/forecast_solar/diagnostics.py new file mode 100644 index 00000000000..7fdcd22d0fc --- /dev/null +++ b/homeassistant/components/forecast_solar/diagnostics.py @@ -0,0 +1,58 @@ +"""Diagnostics support for Forecast.Solar integration.""" +from __future__ import annotations + +from typing import Any + +from forecast_solar import Estimate + +from homeassistant.components.diagnostics import async_redact_data +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.update_coordinator import DataUpdateCoordinator + +from .const import DOMAIN + +TO_REDACT = { + CONF_API_KEY, + CONF_LATITUDE, + CONF_LONGITUDE, +} + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, entry: ConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + coordinator: DataUpdateCoordinator[Estimate] = hass.data[DOMAIN][entry.entry_id] + + return { + "entry": { + "title": entry.title, + "data": async_redact_data(entry.data, TO_REDACT), + "options": async_redact_data(entry.options, TO_REDACT), + }, + "data": { + "energy_production_today": coordinator.data.energy_production_today, + "energy_production_tomorrow": coordinator.data.energy_production_tomorrow, + "energy_current_hour": coordinator.data.energy_current_hour, + "power_production_now": coordinator.data.power_production_now, + "watts": { + watt_datetime.isoformat(): watt_value + for watt_datetime, watt_value in coordinator.data.watts.items() + }, + "wh_days": { + wh_datetime.isoformat(): wh_value + for wh_datetime, wh_value in coordinator.data.wh_days.items() + }, + "wh_hours": { + wh_datetime.isoformat(): wh_value + for wh_datetime, wh_value in coordinator.data.wh_hours.items() + }, + }, + "account": { + "type": coordinator.data.account_type.value, + "rate_limit": coordinator.data.api_rate_limit, + "timezone": coordinator.data.timezone, + }, + } diff --git a/homeassistant/components/forecast_solar/manifest.json b/homeassistant/components/forecast_solar/manifest.json index dc4b88d160c..472f5cac213 100644 --- a/homeassistant/components/forecast_solar/manifest.json +++ b/homeassistant/components/forecast_solar/manifest.json @@ -3,7 +3,7 @@ "name": "Forecast.Solar", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/forecast_solar", - "requirements": ["forecast_solar==2.1.0"], + "requirements": ["forecast_solar==2.2.0"], "codeowners": ["@klaasnicolaas", "@frenck"], "quality_scale": "platinum", "iot_class": "cloud_polling" diff --git a/homeassistant/components/forecast_solar/strings.json b/homeassistant/components/forecast_solar/strings.json index e1ae451a04f..b10e927eb8b 100644 --- a/homeassistant/components/forecast_solar/strings.json +++ b/homeassistant/components/forecast_solar/strings.json @@ -22,6 +22,7 @@ "api_key": "Forecast.Solar API Key (optional)", "azimuth": "Azimuth (360 degrees, 0 = North, 90 = East, 180 = South, 270 = West)", "damping": "Damping factor: adjusts the results in the morning and evening", + "inverter_size": "Inverter size (Watt)", "declination": "Declination (0 = Horizontal, 90 = Vertical)", "modules power": "Total Watt peak power of your solar modules" } diff --git a/homeassistant/components/forecast_solar/translations/en.json b/homeassistant/components/forecast_solar/translations/en.json index f9eef2b5c0a..db9bead2e8c 100644 --- a/homeassistant/components/forecast_solar/translations/en.json +++ b/homeassistant/components/forecast_solar/translations/en.json @@ -21,6 +21,7 @@ "api_key": "Forecast.Solar API Key (optional)", "azimuth": "Azimuth (360 degrees, 0 = North, 90 = East, 180 = South, 270 = West)", "damping": "Damping factor: adjusts the results in the morning and evening", + "inverter_size": "Inverter size (Watt)", "declination": "Declination (0 = Horizontal, 90 = Vertical)", "modules power": "Total Watt peak power of your solar modules" }, diff --git a/homeassistant/components/forked_daapd/const.py b/homeassistant/components/forked_daapd/const.py index 29da9f7244e..482218630a7 100644 --- a/homeassistant/components/forked_daapd/const.py +++ b/homeassistant/components/forked_daapd/const.py @@ -1,5 +1,6 @@ """Const for forked-daapd.""" from homeassistant.components.media_player.const import ( + SUPPORT_BROWSE_MEDIA, SUPPORT_CLEAR_PLAYLIST, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, @@ -78,6 +79,7 @@ SUPPORTED_FEATURES = ( | SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_PLAY_MEDIA + | SUPPORT_BROWSE_MEDIA ) SUPPORTED_FEATURES_ZONE = ( SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | SUPPORT_TURN_ON | SUPPORT_TURN_OFF diff --git a/homeassistant/components/forked_daapd/media_player.py b/homeassistant/components/forked_daapd/media_player.py index a68f56e2965..f2c64fa81da 100644 --- a/homeassistant/components/forked_daapd/media_player.py +++ b/homeassistant/components/forked_daapd/media_player.py @@ -6,7 +6,11 @@ import logging from pyforked_daapd import ForkedDaapdAPI from pylibrespot_java import LibrespotJavaAPI +from homeassistant.components import media_source from homeassistant.components.media_player import MediaPlayerEntity +from homeassistant.components.media_player.browse_media import ( + async_process_play_media_url, +) from homeassistant.components.media_player.const import MEDIA_TYPE_MUSIC from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -660,7 +664,14 @@ class ForkedDaapdMaster(MediaPlayerEntity): async def async_play_media(self, media_type, media_id, **kwargs): """Play a URI.""" + if media_source.is_media_source_id(media_id): + media_type = MEDIA_TYPE_MUSIC + play_item = await media_source.async_resolve_media(self.hass, media_id) + media_id = play_item.url + if media_type == MEDIA_TYPE_MUSIC: + media_id = async_process_play_media_url(self.hass, media_id) + saved_state = self.state # save play state saved_mute = self.is_volume_muted sleep_future = asyncio.create_task( @@ -875,3 +886,11 @@ class ForkedDaapdUpdater: self._api, outputs_to_add, ) + + async def async_browse_media(self, media_content_type=None, media_content_id=None): + """Implement the websocket media browsing helper.""" + return await media_source.async_browse_media( + self.hass, + media_content_id, + content_filter=lambda item: item.media_content_type.startswith("audio/"), + ) diff --git a/homeassistant/components/foscam/services.yaml b/homeassistant/components/foscam/services.yaml index 04dba8ccbaa..a7e5394802b 100644 --- a/homeassistant/components/foscam/services.yaml +++ b/homeassistant/components/foscam/services.yaml @@ -12,14 +12,14 @@ ptz: selector: select: options: - - 'bottom_left' - - 'bottom_right' - - 'down' - - 'left' - - 'right' - - 'top_left' - - 'top_right' - - 'up' + - "bottom_left" + - "bottom_right" + - "down" + - "left" + - "right" + - "top_left" + - "top_right" + - "up" travel_time: description: "Travel time in seconds." default: 0.125 diff --git a/homeassistant/components/foscam/translations/fr.json b/homeassistant/components/foscam/translations/fr.json index 7c0bb8398da..f728c6d7414 100644 --- a/homeassistant/components/foscam/translations/fr.json +++ b/homeassistant/components/foscam/translations/fr.json @@ -5,8 +5,8 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", - "invalid_response": "R\u00e9ponse invalide de l\u2019appareil", + "invalid_auth": "Authentification non valide", + "invalid_response": "R\u00e9ponse de l'appareil non valide", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/freebox/strings.json b/homeassistant/components/freebox/strings.json index cb48e5322de..53a5fd59de3 100644 --- a/homeassistant/components/freebox/strings.json +++ b/homeassistant/components/freebox/strings.json @@ -2,7 +2,6 @@ "config": { "step": { "user": { - "title": "Freebox", "data": { "host": "[%key:common::config_flow::data::host%]", "port": "[%key:common::config_flow::data::port%]" diff --git a/homeassistant/components/freedompro/fan.py b/homeassistant/components/freedompro/fan.py index fe8ccc6d28d..dc7e3bb3d9d 100644 --- a/homeassistant/components/freedompro/fan.py +++ b/homeassistant/components/freedompro/fan.py @@ -92,9 +92,7 @@ class FreedomproFan(CoordinatorEntity, FanEntity): await super().async_added_to_hass() self._handle_coordinator_update() - async def async_turn_on( - self, speed=None, percentage=None, preset_mode=None, **kwargs - ): + async def async_turn_on(self, percentage=None, preset_mode=None, **kwargs): """Async function to turn on the fan.""" payload = {"on": True} payload = json.dumps(payload) diff --git a/homeassistant/components/freedompro/manifest.json b/homeassistant/components/freedompro/manifest.json index 17486271268..6fe9e5b31d6 100644 --- a/homeassistant/components/freedompro/manifest.json +++ b/homeassistant/components/freedompro/manifest.json @@ -3,9 +3,7 @@ "name": "Freedompro", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/freedompro", - "codeowners": [ - "@stefano055415" - ], + "codeowners": ["@stefano055415"], "requirements": ["pyfreedompro==1.1.0"], "iot_class": "cloud_polling", "loggers": ["pyfreedompro"] diff --git a/homeassistant/components/freedompro/translations/fr.json b/homeassistant/components/freedompro/translations/fr.json index 090c95fa3c2..cbd28831a2a 100644 --- a/homeassistant/components/freedompro/translations/fr.json +++ b/homeassistant/components/freedompro/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide" + "invalid_auth": "Authentification non valide" }, "step": { "user": { diff --git a/homeassistant/components/fritz/__init__.py b/homeassistant/components/fritz/__init__.py index 0b334ff616a..55253fe3d1e 100644 --- a/homeassistant/components/fritz/__init__.py +++ b/homeassistant/components/fritz/__init__.py @@ -1,7 +1,7 @@ """Support for AVM Fritz!Box functions.""" import logging -from fritzconnection.core.exceptions import FritzSecurityError +from fritzconnection.core.exceptions import FritzConnectionException from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME @@ -28,10 +28,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: await avm_wrapper.async_setup(entry.options) - except FritzSecurityError as ex: - raise ConfigEntryAuthFailed from ex except FRITZ_EXCEPTIONS as ex: raise ConfigEntryNotReady from ex + except FritzConnectionException as ex: + raise ConfigEntryAuthFailed from ex if ( "X_AVM-DE_UPnP1" in avm_wrapper.connection.services diff --git a/homeassistant/components/fritz/common.py b/homeassistant/components/fritz/common.py index 21039d45afa..d7b8d916803 100644 --- a/homeassistant/components/fritz/common.py +++ b/homeassistant/components/fritz/common.py @@ -91,6 +91,11 @@ def _cleanup_entity_filter(device: er.RegistryEntry) -> bool: ) +def _ha_is_stopping(activity: str) -> None: + """Inform that HA is stopping.""" + _LOGGER.info("Cannot execute %s: HomeAssistant is shutting down", activity) + + class ClassSetupMissing(Exception): """Raised when a Class func is called before setup.""" @@ -351,6 +356,10 @@ class FritzBoxTools(update_coordinator.DataUpdateCoordinator): def scan_devices(self, now: datetime | None = None) -> None: """Scan for new devices and return a list of found device ids.""" + if self.hass.is_stopping: + _ha_is_stopping("scan devices") + return + _LOGGER.debug("Checking host info for FRITZ!Box device %s", self.host) self._update_available, self._latest_firmware = self._update_device_info() @@ -603,6 +612,10 @@ class AvmWrapper(FritzBoxTools): ) -> dict: """Return service details.""" + if self.hass.is_stopping: + _ha_is_stopping(f"{service_name}/{action_name}") + return {} + if f"{service_name}{service_suffix}" not in self.connection.services: return {} @@ -817,7 +830,7 @@ class FritzData: profile_switches: dict = field(default_factory=dict) -class FritzDeviceBase(update_coordinator.CoordinatorEntity): +class FritzDeviceBase(update_coordinator.CoordinatorEntity[AvmWrapper]): """Entity base class for a device connected to a FRITZ!Box device.""" def __init__(self, avm_wrapper: AvmWrapper, device: FritzDevice) -> None: diff --git a/homeassistant/components/fritz/config_flow.py b/homeassistant/components/fritz/config_flow.py index 046f00ba3a9..ebc5512f920 100644 --- a/homeassistant/components/fritz/config_flow.py +++ b/homeassistant/components/fritz/config_flow.py @@ -1,11 +1,13 @@ """Config flow to configure the FRITZ!Box Tools integration.""" from __future__ import annotations +import ipaddress import logging import socket from typing import Any from urllib.parse import ParseResult, urlparse +from fritzconnection import FritzConnection from fritzconnection.core.exceptions import FritzConnectionException, FritzSecurityError import voluptuous as vol @@ -19,7 +21,6 @@ from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNA from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult -from .common import AvmWrapper from .const import ( CONF_OLD_DISCOVERY, DEFAULT_CONF_OLD_DISCOVERY, @@ -49,29 +50,25 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialize FRITZ!Box Tools flow.""" self._host: str | None = None - self._entry: ConfigEntry - self._name: str - self._password: str + self._entry: ConfigEntry | None = None + self._name: str = "" + self._password: str = "" self._port: int | None = None - self._username: str - self.avm_wrapper: AvmWrapper + self._username: str = "" + self._model: str = "" - async def fritz_tools_init(self) -> str | None: + def fritz_tools_init(self) -> str | None: """Initialize FRITZ!Box Tools class.""" - if not self._host or not self._port: - return None - - self.avm_wrapper = AvmWrapper( - hass=self.hass, - host=self._host, - port=self._port, - username=self._username, - password=self._password, - ) - try: - await self.avm_wrapper.async_setup() + connection = FritzConnection( + address=self._host, + port=self._port, + user=self._username, + password=self._password, + timeout=60.0, + pool_maxsize=30, + ) except FritzSecurityError: return ERROR_AUTH_INVALID except FritzConnectionException: @@ -80,9 +77,11 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN): _LOGGER.exception("Unexpected exception") return ERROR_UNKNOWN + self._model = connection.call_action("DeviceInfo:1", "GetInfo")["NewModelName"] + if ( - "X_AVM-DE_UPnP1" in self.avm_wrapper.connection.services - and not (await self.avm_wrapper.async_get_upnp_configuration())["NewEnable"] + "X_AVM-DE_UPnP1" in connection.services + and not connection.call_action("X_AVM-DE_UPnP1", "GetInfo")["NewEnable"] ): return ERROR_UPNP_NOT_CONFIGURED @@ -109,10 +108,10 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN): return self.async_create_entry( title=self._name, data={ - CONF_HOST: self.avm_wrapper.host, - CONF_PASSWORD: self.avm_wrapper.password, - CONF_PORT: self.avm_wrapper.port, - CONF_USERNAME: self.avm_wrapper.username, + CONF_HOST: self._host, + CONF_PASSWORD: self._password, + CONF_PORT: self._port, + CONF_USERNAME: self._username, }, options={ CONF_CONSIDER_HOME: DEFAULT_CONSIDER_HOME.total_seconds(), @@ -131,6 +130,9 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN): ) self.context[CONF_HOST] = self._host + if ipaddress.ip_address(self._host).is_link_local: + return self.async_abort(reason="ignore_ip6_link_local") + if uuid := discovery_info.upnp.get(ssdp.ATTR_UPNP_UDN): if uuid.startswith("uuid:"): uuid = uuid[5:] @@ -163,7 +165,7 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN): self._username = user_input[CONF_USERNAME] self._password = user_input[CONF_PASSWORD] - error = await self.fritz_tools_init() + error = await self.hass.async_add_executor_job(self.fritz_tools_init) if error: errors["base"] = error @@ -213,8 +215,8 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN): self._username = user_input[CONF_USERNAME] self._password = user_input[CONF_PASSWORD] - if not (error := await self.fritz_tools_init()): - self._name = self.avm_wrapper.model + if not (error := await self.hass.async_add_executor_job(self.fritz_tools_init)): + self._name = self._model if await self.async_check_configured_entry(): error = "already_configured" @@ -226,10 +228,7 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_reauth(self, data: dict[str, Any]) -> FlowResult: """Handle flow upon an API authentication error.""" - if cfg_entry := self.hass.config_entries.async_get_entry( - self.context["entry_id"] - ): - self._entry = cfg_entry + self._entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) self._host = data[CONF_HOST] self._port = data[CONF_PORT] self._username = data[CONF_USERNAME] @@ -265,11 +264,12 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN): self._username = user_input[CONF_USERNAME] self._password = user_input[CONF_PASSWORD] - if error := await self.fritz_tools_init(): + if error := await self.hass.async_add_executor_job(self.fritz_tools_init): return self._show_setup_form_reauth_confirm( user_input=user_input, errors={"base": error} ) + assert isinstance(self._entry, ConfigEntry) self.hass.config_entries.async_update_entry( self._entry, data={ diff --git a/homeassistant/components/fritz/manifest.json b/homeassistant/components/fritz/manifest.json index b278bd8d196..5eb210d2091 100644 --- a/homeassistant/components/fritz/manifest.json +++ b/homeassistant/components/fritz/manifest.json @@ -2,17 +2,9 @@ "domain": "fritz", "name": "AVM FRITZ!Box Tools", "documentation": "https://www.home-assistant.io/integrations/fritz", - "requirements": [ - "fritzconnection==1.8.0", - "xmltodict==0.12.0" - ], + "requirements": ["fritzconnection==1.8.0", "xmltodict==0.12.0"], "dependencies": ["network"], - "codeowners": [ - "@mammuth", - "@AaronDavidSchneider", - "@chemelli74", - "@mib1185" - ], + "codeowners": ["@mammuth", "@AaronDavidSchneider", "@chemelli74", "@mib1185"], "config_flow": true, "ssdp": [ { diff --git a/homeassistant/components/fritz/sensor.py b/homeassistant/components/fritz/sensor.py index 9811adf6829..0f3d8cb1ae0 100644 --- a/homeassistant/components/fritz/sensor.py +++ b/homeassistant/components/fritz/sensor.py @@ -308,14 +308,13 @@ class FritzBoxSensor(FritzBoxBaseEntity, SensorEntity): """Update data.""" _LOGGER.debug("Updating FRITZ!Box sensors") + status: FritzStatus = self._avm_wrapper.fritz_status try: - status: FritzStatus = self._avm_wrapper.fritz_status - self._attr_available = True + self._attr_native_value = ( + self._last_device_value + ) = self.entity_description.value_fn(status, self._last_device_value) except FritzConnectionException: _LOGGER.error("Error getting the state from the FRITZ!Box", exc_info=True) self._attr_available = False return - - self._attr_native_value = ( - self._last_device_value - ) = self.entity_description.value_fn(status, self._last_device_value) + self._attr_available = True diff --git a/homeassistant/components/fritz/strings.json b/homeassistant/components/fritz/strings.json index a65b2900f66..9d6628aca0e 100644 --- a/homeassistant/components/fritz/strings.json +++ b/homeassistant/components/fritz/strings.json @@ -32,24 +32,25 @@ "abort": { "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "ignore_ip6_link_local": "IPv6 link local address is not supported.", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", - "upnp_not_configured": "Missing UPnP settings on device.", + "upnp_not_configured": "Missing UPnP settings on device.", "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" } }, "options": { - "step": { - "init": { - "data": { - "consider_home": "Seconds to consider a device at 'home'", - "old_discovery": "Enable old discovery method" - } + "step": { + "init": { + "data": { + "consider_home": "Seconds to consider a device at 'home'", + "old_discovery": "Enable old discovery method" } } + } } } diff --git a/homeassistant/components/fritz/translations/ca.json b/homeassistant/components/fritz/translations/ca.json index 2e240bb9833..d39805a6302 100644 --- a/homeassistant/components/fritz/translations/ca.json +++ b/homeassistant/components/fritz/translations/ca.json @@ -10,7 +10,8 @@ "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs", "cannot_connect": "Ha fallat la connexi\u00f3", "connection_error": "Ha fallat la connexi\u00f3", - "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida" + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "upnp_not_configured": "Falta la configuraci\u00f3 UPnP al dispositiu." }, "flow_title": "{name}", "step": { @@ -56,7 +57,8 @@ "step": { "init": { "data": { - "consider_home": "Segons d'espera abans de considerar un dispositiu a 'casa'" + "consider_home": "Segons d'espera abans de considerar un dispositiu a 'casa'", + "old_discovery": "Activa el m\u00e8tode de descobriment antic" } } } diff --git a/homeassistant/components/fritz/translations/de.json b/homeassistant/components/fritz/translations/de.json index 47938084f5b..ae36b3450ca 100644 --- a/homeassistant/components/fritz/translations/de.json +++ b/homeassistant/components/fritz/translations/de.json @@ -10,7 +10,8 @@ "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", "cannot_connect": "Verbindung fehlgeschlagen", "connection_error": "Verbindung fehlgeschlagen", - "invalid_auth": "Ung\u00fcltige Authentifizierung" + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "upnp_not_configured": "Fehlende UPnP-Einstellungen auf dem Ger\u00e4t." }, "flow_title": "FRITZ!Box Tools: {name}", "step": { @@ -56,7 +57,8 @@ "step": { "init": { "data": { - "consider_home": "Sekunden, um ein Ger\u00e4t als 'zu Hause' zu betrachten" + "consider_home": "Sekunden, um ein Ger\u00e4t als 'zu Hause' zu betrachten", + "old_discovery": "Alte Erkennungsmethode aktivieren" } } } diff --git a/homeassistant/components/fritz/translations/el.json b/homeassistant/components/fritz/translations/el.json index 443f8b95c93..d514848040f 100644 --- a/homeassistant/components/fritz/translations/el.json +++ b/homeassistant/components/fritz/translations/el.json @@ -10,7 +10,8 @@ "already_in_progress": "\u0397 \u03c1\u03bf\u03ae \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7\u03c2 \u03b2\u03c1\u03af\u03c3\u03ba\u03b5\u03c4\u03b1\u03b9 \u03ae\u03b4\u03b7 \u03c3\u03b5 \u03b5\u03be\u03ad\u03bb\u03b9\u03be\u03b7", "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", "connection_error": "\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\u03bf\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2" + "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2", + "upnp_not_configured": "\u039b\u03b5\u03af\u03c0\u03bf\u03c5\u03bd \u03bf\u03b9 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 UPnP \u03c3\u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae." }, "flow_title": "{name}", "step": { @@ -56,7 +57,8 @@ "step": { "init": { "data": { - "consider_home": "\u0394\u03b5\u03c5\u03c4\u03b5\u03c1\u03cc\u03bb\u03b5\u03c0\u03c4\u03b1 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b8\u03b5\u03c9\u03c1\u03b7\u03b8\u03b5\u03af \u03bc\u03b9\u03b1 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03c3\u03c4\u03bf \"\u03c3\u03c0\u03af\u03c4\u03b9\"" + "consider_home": "\u0394\u03b5\u03c5\u03c4\u03b5\u03c1\u03cc\u03bb\u03b5\u03c0\u03c4\u03b1 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b8\u03b5\u03c9\u03c1\u03b7\u03b8\u03b5\u03af \u03bc\u03b9\u03b1 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03c3\u03c4\u03bf \"\u03c3\u03c0\u03af\u03c4\u03b9\"", + "old_discovery": "\u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03c0\u03b1\u03bb\u03b9\u03ac\u03c2 \u03bc\u03b5\u03b8\u03cc\u03b4\u03bf\u03c5 \u03b1\u03bd\u03b1\u03ba\u03ac\u03bb\u03c5\u03c8\u03b7\u03c2" } } } diff --git a/homeassistant/components/fritz/translations/en.json b/homeassistant/components/fritz/translations/en.json index c6fa4a16036..e7ee1568684 100644 --- a/homeassistant/components/fritz/translations/en.json +++ b/homeassistant/components/fritz/translations/en.json @@ -3,6 +3,7 @@ "abort": { "already_configured": "Device is already configured", "already_in_progress": "Configuration flow is already in progress", + "ignore_ip6_link_local": "IPv6 link local address is not supported.", "reauth_successful": "Re-authentication was successful" }, "error": { @@ -52,4 +53,4 @@ } } } -} +} \ No newline at end of file diff --git a/homeassistant/components/fritz/translations/et.json b/homeassistant/components/fritz/translations/et.json index 2866ae13336..2051e9f4e63 100644 --- a/homeassistant/components/fritz/translations/et.json +++ b/homeassistant/components/fritz/translations/et.json @@ -10,7 +10,8 @@ "already_in_progress": "Seadistamine on juba k\u00e4ivitatud", "cannot_connect": "\u00dchendamine nurjus", "connection_error": "\u00dchendamine nurjus", - "invalid_auth": "Tuvastamine nurjus" + "invalid_auth": "Tuvastamine nurjus", + "upnp_not_configured": "Puuduvad seadme UPnP-seaded." }, "flow_title": "{name}", "step": { @@ -56,7 +57,8 @@ "step": { "init": { "data": { - "consider_home": "Millal m\u00e4\u00e4rata seade olema kodus (sekundites)" + "consider_home": "Millal m\u00e4\u00e4rata seade olema kodus (sekundites)", + "old_discovery": "Luba vana avastamismeetod" } } } diff --git a/homeassistant/components/fritz/translations/fr.json b/homeassistant/components/fritz/translations/fr.json index 38c8a9e802d..3219164d98b 100644 --- a/homeassistant/components/fritz/translations/fr.json +++ b/homeassistant/components/fritz/translations/fr.json @@ -10,7 +10,8 @@ "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours", "cannot_connect": "\u00c9chec de connexion", "connection_error": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide" + "invalid_auth": "Authentification non valide", + "upnp_not_configured": "Param\u00e8tres UPnP manquants sur l'appareil." }, "flow_title": "{name}", "step": { @@ -56,7 +57,8 @@ "step": { "init": { "data": { - "consider_home": "Secondes pour consid\u00e9rer un appareil \u00e0 la 'maison'" + "consider_home": "Secondes pour consid\u00e9rer un appareil \u00e0 la 'maison'", + "old_discovery": "Activer l'ancienne m\u00e9thode de d\u00e9couverte" } } } diff --git a/homeassistant/components/fritz/translations/hu.json b/homeassistant/components/fritz/translations/hu.json index 733a4fb1a8e..81d3bb19e27 100644 --- a/homeassistant/components/fritz/translations/hu.json +++ b/homeassistant/components/fritz/translations/hu.json @@ -10,7 +10,8 @@ "already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van", "cannot_connect": "Sikertelen csatlakoz\u00e1s", "connection_error": "Nem siker\u00fclt csatlakozni", - "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s" + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "upnp_not_configured": "Hi\u00e1nyz\u00f3 UPnP-be\u00e1ll\u00edt\u00e1sok az eszk\u00f6z\u00f6n." }, "flow_title": "{name}", "step": { @@ -56,7 +57,8 @@ "step": { "init": { "data": { - "consider_home": "M\u00e1sodpercek egy eszk\u00f6z \"otthon\" tart\u00e1s\u00e1ra" + "consider_home": "M\u00e1sodpercek egy eszk\u00f6z \"otthon\" tart\u00e1s\u00e1ra", + "old_discovery": "R\u00e9gi felder\u00edt\u00e9si m\u00f3dszer enged\u00e9lyez\u00e9se" } } } diff --git a/homeassistant/components/fritz/translations/id.json b/homeassistant/components/fritz/translations/id.json index 5aae1443d02..816885b6391 100644 --- a/homeassistant/components/fritz/translations/id.json +++ b/homeassistant/components/fritz/translations/id.json @@ -10,7 +10,8 @@ "already_in_progress": "Alur konfigurasi sedang berlangsung", "cannot_connect": "Gagal terhubung", "connection_error": "Gagal terhubung", - "invalid_auth": "Autentikasi tidak valid" + "invalid_auth": "Autentikasi tidak valid", + "upnp_not_configured": "Pengaturan UPnP pada perangkat tidak ada." }, "flow_title": "{name}", "step": { @@ -56,7 +57,8 @@ "step": { "init": { "data": { - "consider_home": "Wakti dalam detik untuk mempertimbangkan perangkat sebagai 'di rumah'" + "consider_home": "Wakti dalam detik untuk mempertimbangkan perangkat sebagai 'di rumah'", + "old_discovery": "Aktifkan metode penemuan lawas" } } } diff --git a/homeassistant/components/fritz/translations/it.json b/homeassistant/components/fritz/translations/it.json index 0169d275205..bf577223b6e 100644 --- a/homeassistant/components/fritz/translations/it.json +++ b/homeassistant/components/fritz/translations/it.json @@ -10,7 +10,8 @@ "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso", "cannot_connect": "Impossibile connettersi", "connection_error": "Impossibile connettersi", - "invalid_auth": "Autenticazione non valida" + "invalid_auth": "Autenticazione non valida", + "upnp_not_configured": "Impostazioni UPnP mancanti sul dispositivo." }, "flow_title": "{name}", "step": { @@ -56,7 +57,8 @@ "step": { "init": { "data": { - "consider_home": "Secondi per considerare un dispositivo \"a casa\"" + "consider_home": "Secondi per considerare un dispositivo \"a casa\"", + "old_discovery": "Abilita il vecchio metodo di rilevamento" } } } diff --git a/homeassistant/components/fritz/translations/ja.json b/homeassistant/components/fritz/translations/ja.json index 156caabbfa3..afbda92d3fe 100644 --- a/homeassistant/components/fritz/translations/ja.json +++ b/homeassistant/components/fritz/translations/ja.json @@ -10,7 +10,8 @@ "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059", "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "connection_error": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", - "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c" + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "upnp_not_configured": "\u30c7\u30d0\u30a4\u30b9\u306bUPnP\u306e\u8a2d\u5b9a\u304c\u3042\u308a\u307e\u305b\u3093\u3002" }, "flow_title": "{name}", "step": { @@ -56,7 +57,8 @@ "step": { "init": { "data": { - "consider_home": "'\u30db\u30fc\u30e0' \u3067\u30c7\u30d0\u30a4\u30b9\u3092\u691c\u8a0e\u3059\u308b\u79d2\u6570" + "consider_home": "'\u30db\u30fc\u30e0' \u3067\u30c7\u30d0\u30a4\u30b9\u3092\u691c\u8a0e\u3059\u308b\u79d2\u6570", + "old_discovery": "\u53e4\u3044\u691c\u51fa\u65b9\u6cd5\u3092\u6709\u52b9\u306b\u3059\u308b" } } } diff --git a/homeassistant/components/fritz/translations/nl.json b/homeassistant/components/fritz/translations/nl.json index 06c29cd9d38..b5577f11a0d 100644 --- a/homeassistant/components/fritz/translations/nl.json +++ b/homeassistant/components/fritz/translations/nl.json @@ -10,7 +10,8 @@ "already_in_progress": "De configuratiestroom is al aan de gang", "cannot_connect": "Kan geen verbinding maken", "connection_error": "Kan geen verbinding maken", - "invalid_auth": "Ongeldige authenticatie" + "invalid_auth": "Ongeldige authenticatie", + "upnp_not_configured": "Ontbrekende UPnP instellingen op apparaat." }, "flow_title": "{name}", "step": { @@ -56,7 +57,8 @@ "step": { "init": { "data": { - "consider_home": "Seconden om een apparaat als \"thuis\" te beschouwen" + "consider_home": "Seconden om een apparaat als \"thuis\" te beschouwen", + "old_discovery": "Oude detectiemethode inschakelen" } } } diff --git a/homeassistant/components/fritz/translations/no.json b/homeassistant/components/fritz/translations/no.json index 0838efbf649..6d6a805bab8 100644 --- a/homeassistant/components/fritz/translations/no.json +++ b/homeassistant/components/fritz/translations/no.json @@ -10,7 +10,8 @@ "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", "cannot_connect": "Tilkobling mislyktes", "connection_error": "Tilkobling mislyktes", - "invalid_auth": "Ugyldig godkjenning" + "invalid_auth": "Ugyldig godkjenning", + "upnp_not_configured": "Mangler UPnP-innstillinger p\u00e5 enheten." }, "flow_title": "{name}", "step": { @@ -56,7 +57,8 @@ "step": { "init": { "data": { - "consider_home": "Sekunder \u00e5 vurdere en enhet hjemme" + "consider_home": "Sekunder \u00e5 vurdere en enhet hjemme", + "old_discovery": "Aktiver gammel s\u00f8kemetode" } } } diff --git a/homeassistant/components/fritz/translations/pl.json b/homeassistant/components/fritz/translations/pl.json index 5632ae67694..7ec1f539aab 100644 --- a/homeassistant/components/fritz/translations/pl.json +++ b/homeassistant/components/fritz/translations/pl.json @@ -10,7 +10,8 @@ "already_in_progress": "Konfiguracja jest ju\u017c w toku", "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", - "invalid_auth": "Niepoprawne uwierzytelnienie" + "invalid_auth": "Niepoprawne uwierzytelnienie", + "upnp_not_configured": "Brak ustawie\u0144 UPnP w urz\u0105dzeniu." }, "flow_title": "{name}", "step": { @@ -56,7 +57,8 @@ "step": { "init": { "data": { - "consider_home": "Czas w sekundach, zanim urz\u0105dzenie otrzyma stan \"w domu\"" + "consider_home": "Czas w sekundach, zanim urz\u0105dzenie otrzyma stan \"w domu\"", + "old_discovery": "W\u0142\u0105cz star\u0105 metod\u0119 wykrywania" } } } diff --git a/homeassistant/components/fritz/translations/pt-BR.json b/homeassistant/components/fritz/translations/pt-BR.json index a28f063ab6d..2170b34b1ee 100644 --- a/homeassistant/components/fritz/translations/pt-BR.json +++ b/homeassistant/components/fritz/translations/pt-BR.json @@ -10,7 +10,8 @@ "already_in_progress": "O fluxo de configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", "cannot_connect": "Falha ao conectar", "connection_error": "Falha ao conectar", - "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "upnp_not_configured": "Faltam configura\u00e7\u00f5es de UPnP no dispositivo." }, "flow_title": "{name}", "step": { @@ -56,7 +57,8 @@ "step": { "init": { "data": { - "consider_home": "Segundos para considerar um dispositivo em 'casa'" + "consider_home": "Segundos para considerar um dispositivo em 'casa'", + "old_discovery": "Ativar m\u00e9todo de descoberta antigo" } } } diff --git a/homeassistant/components/fritz/translations/ru.json b/homeassistant/components/fritz/translations/ru.json index 54619e22a36..82530cca298 100644 --- a/homeassistant/components/fritz/translations/ru.json +++ b/homeassistant/components/fritz/translations/ru.json @@ -10,7 +10,8 @@ "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.", "connection_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", - "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438." + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", + "upnp_not_configured": "\u041e\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u044e\u0442 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 UPnP \u043d\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0435." }, "flow_title": "{name}", "step": { @@ -56,7 +57,8 @@ "step": { "init": { "data": { - "consider_home": "\u0412\u0440\u0435\u043c\u044f, \u0432 \u0442\u0435\u0447\u0435\u043d\u0438\u0438 \u043a\u043e\u0442\u043e\u0440\u043e\u0433\u043e \u0441\u0447\u0438\u0442\u0430\u0442\u044c \u0447\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0434\u043e\u043c\u0430 (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)" + "consider_home": "\u0412\u0440\u0435\u043c\u044f, \u0432 \u0442\u0435\u0447\u0435\u043d\u0438\u0438 \u043a\u043e\u0442\u043e\u0440\u043e\u0433\u043e \u0441\u0447\u0438\u0442\u0430\u0442\u044c \u0447\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0434\u043e\u043c\u0430 (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)", + "old_discovery": "\u0410\u043a\u0442\u0438\u0432\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0441\u0442\u0430\u0440\u044b\u0439 \u043c\u0435\u0442\u043e\u0434 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u044f" } } } diff --git a/homeassistant/components/fritz/translations/tr.json b/homeassistant/components/fritz/translations/tr.json index 686c248cd39..9a9cd3da6bf 100644 --- a/homeassistant/components/fritz/translations/tr.json +++ b/homeassistant/components/fritz/translations/tr.json @@ -10,7 +10,8 @@ "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", "cannot_connect": "Ba\u011flanma hatas\u0131", "connection_error": "Ba\u011flanma hatas\u0131", - "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "upnp_not_configured": "Cihazda UPnP ayarlar\u0131 eksik." }, "flow_title": "{name}", "step": { @@ -56,7 +57,8 @@ "step": { "init": { "data": { - "consider_home": "Bir cihaz\u0131 'evde' varsaymak i\u00e7in saniye" + "consider_home": "Bir cihaz\u0131 'evde' varsaymak i\u00e7in saniye", + "old_discovery": "Eski ke\u015fif y\u00f6ntemini etkinle\u015ftir" } } } diff --git a/homeassistant/components/fritz/translations/zh-Hant.json b/homeassistant/components/fritz/translations/zh-Hant.json index 861ec6d62ce..7eeb3ae5e4b 100644 --- a/homeassistant/components/fritz/translations/zh-Hant.json +++ b/homeassistant/components/fritz/translations/zh-Hant.json @@ -10,7 +10,8 @@ "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", "cannot_connect": "\u9023\u7dda\u5931\u6557", "connection_error": "\u9023\u7dda\u5931\u6557", - "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548" + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "upnp_not_configured": "\u672a\u8a2d\u5b9a\u88dd\u7f6e UPnP \u8a2d\u5b9a\u3002" }, "flow_title": "{name}", "step": { @@ -56,7 +57,8 @@ "step": { "init": { "data": { - "consider_home": "\u8996\u70ba\u5728\u5bb6\u7684\u7b49\u5019\u79d2\u6578" + "consider_home": "\u8996\u70ba\u5728\u5bb6\u7684\u7b49\u5019\u79d2\u6578", + "old_discovery": "\u958b\u555f\u820a\u641c\u7d22\u6a21\u5f0f" } } } diff --git a/homeassistant/components/fritzbox/__init__.py b/homeassistant/components/fritzbox/__init__.py index e5ef4536bc4..7bb71e52560 100644 --- a/homeassistant/components/fritzbox/__init__.py +++ b/homeassistant/components/fritzbox/__init__.py @@ -93,11 +93,9 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return unload_ok -class FritzBoxEntity(CoordinatorEntity): +class FritzBoxEntity(CoordinatorEntity[FritzboxDataUpdateCoordinator]): """Basis FritzBox entity.""" - coordinator: FritzboxDataUpdateCoordinator - def __init__( self, coordinator: FritzboxDataUpdateCoordinator, diff --git a/homeassistant/components/fritzbox/translations/fr.json b/homeassistant/components/fritzbox/translations/fr.json index 8a75221ea88..69f024e26d7 100644 --- a/homeassistant/components/fritzbox/translations/fr.json +++ b/homeassistant/components/fritzbox/translations/fr.json @@ -8,7 +8,7 @@ "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" }, "error": { - "invalid_auth": "Authentification invalide" + "invalid_auth": "Authentification non valide" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/fritzbox_callmonitor/translations/fr.json b/homeassistant/components/fritzbox_callmonitor/translations/fr.json index 2d1cadb8a48..134633e2802 100644 --- a/homeassistant/components/fritzbox_callmonitor/translations/fr.json +++ b/homeassistant/components/fritzbox_callmonitor/translations/fr.json @@ -6,7 +6,7 @@ "no_devices_found": "Aucun appareil trouv\u00e9 sur le r\u00e9seau" }, "error": { - "invalid_auth": "Authentification invalide" + "invalid_auth": "Authentification non valide" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/fronius/__init__.py b/homeassistant/components/fronius/__init__.py index 12811e84079..03340f19081 100644 --- a/homeassistant/components/fronius/__init__.py +++ b/homeassistant/components/fronius/__init__.py @@ -30,7 +30,7 @@ from .coordinator import ( _LOGGER: Final = logging.getLogger(__name__) PLATFORMS: Final = [Platform.SENSOR] -FroniusCoordinatorType = TypeVar("FroniusCoordinatorType", bound=FroniusCoordinatorBase) +_FroniusCoordinatorT = TypeVar("_FroniusCoordinatorT", bound=FroniusCoordinatorBase) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @@ -199,8 +199,8 @@ class FroniusSolarNet: @staticmethod async def _init_optional_coordinator( - coordinator: FroniusCoordinatorType, - ) -> FroniusCoordinatorType | None: + coordinator: _FroniusCoordinatorT, + ) -> _FroniusCoordinatorT | None: """Initialize an update coordinator and return it if devices are found.""" try: await coordinator.async_config_entry_first_refresh() diff --git a/homeassistant/components/fronius/config_flow.py b/homeassistant/components/fronius/config_flow.py index 00ddd9335a3..15b8cd7a3b8 100644 --- a/homeassistant/components/fronius/config_flow.py +++ b/homeassistant/components/fronius/config_flow.py @@ -10,7 +10,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.components.dhcp import DhcpServiceInfo -from homeassistant.const import CONF_HOST, CONF_RESOURCE +from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResult from homeassistant.exceptions import HomeAssistantError @@ -110,10 +110,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors ) - async def async_step_import(self, conf: dict) -> FlowResult: - """Import a configuration from config.yaml.""" - return await self.async_step_user(user_input={CONF_HOST: conf[CONF_RESOURCE]}) - async def async_step_dhcp(self, discovery_info: DhcpServiceInfo) -> FlowResult: """Handle a flow initiated by the DHCP client.""" for entry in self._async_current_entries(include_ignore=False): diff --git a/homeassistant/components/fronius/coordinator.py b/homeassistant/components/fronius/coordinator.py index e9d47296864..c1090dab1b1 100644 --- a/homeassistant/components/fronius/coordinator.py +++ b/homeassistant/components/fronius/coordinator.py @@ -31,7 +31,7 @@ if TYPE_CHECKING: from . import FroniusSolarNet from .sensor import _FroniusSensorEntity - FroniusEntityType = TypeVar("FroniusEntityType", bound=_FroniusSensorEntity) + _FroniusEntityT = TypeVar("_FroniusEntityT", bound=_FroniusSensorEntity) class FroniusCoordinatorBase( @@ -84,7 +84,7 @@ class FroniusCoordinatorBase( def add_entities_for_seen_keys( self, async_add_entities: AddEntitiesCallback, - entity_constructor: type[FroniusEntityType], + entity_constructor: type[_FroniusEntityT], ) -> None: """ Add entities for received keys and registers listener for future seen keys. diff --git a/homeassistant/components/fronius/sensor.py b/homeassistant/components/fronius/sensor.py index a266998a9b5..c3b219c4b22 100644 --- a/homeassistant/components/fronius/sensor.py +++ b/homeassistant/components/fronius/sensor.py @@ -1,23 +1,17 @@ """Support for Fronius devices.""" from __future__ import annotations -import logging from typing import TYPE_CHECKING, Any, Final -import voluptuous as vol - from homeassistant.components.sensor import ( DOMAIN as SENSOR_DOMAIN, - PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, SensorEntityDescription, SensorStateClass, ) -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - CONF_MONITORED_CONDITIONS, - CONF_RESOURCE, ELECTRIC_CURRENT_AMPERE, ELECTRIC_POTENTIAL_VOLT, ENERGY_WATT_HOUR, @@ -29,10 +23,8 @@ from homeassistant.const import ( TEMP_CELSIUS, ) from homeassistant.core import HomeAssistant, callback -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import DeviceInfo, EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN @@ -49,39 +41,8 @@ if TYPE_CHECKING: FroniusStorageUpdateCoordinator, ) -_LOGGER: Final = logging.getLogger(__name__) - -ELECTRIC_CHARGE_AMPERE_HOURS: Final = "Ah" ENERGY_VOLT_AMPERE_REACTIVE_HOUR: Final = "varh" -PLATFORM_SCHEMA = vol.All( - PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_RESOURCE): cv.url, - vol.Optional(CONF_MONITORED_CONDITIONS): object, - } - ), -) - - -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Import Fronius configuration from yaml.""" - _LOGGER.warning( - "Loading Fronius via platform setup is deprecated. Please remove it from your yaml 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, @@ -631,13 +592,13 @@ STORAGE_ENTITY_DESCRIPTIONS: list[SensorEntityDescription] = [ SensorEntityDescription( key="capacity_maximum", name="Capacity maximum", - native_unit_of_measurement=ELECTRIC_CHARGE_AMPERE_HOURS, + native_unit_of_measurement=ENERGY_WATT_HOUR, entity_category=EntityCategory.DIAGNOSTIC, ), SensorEntityDescription( key="capacity_designed", name="Capacity designed", - native_unit_of_measurement=ELECTRIC_CHARGE_AMPERE_HOURS, + native_unit_of_measurement=ENERGY_WATT_HOUR, entity_category=EntityCategory.DIAGNOSTIC, ), SensorEntityDescription( @@ -691,10 +652,9 @@ STORAGE_ENTITY_DESCRIPTIONS: list[SensorEntityDescription] = [ ] -class _FroniusSensorEntity(CoordinatorEntity, SensorEntity): +class _FroniusSensorEntity(CoordinatorEntity["FroniusCoordinatorBase"], SensorEntity): """Defines a Fronius coordinator entity.""" - coordinator: FroniusCoordinatorBase entity_descriptions: list[SensorEntityDescription] _entity_id_prefix: str diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index bcbf5b81ff8..a26018e05ce 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,9 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": [ - "home-assistant-frontend==20220301.2" - ], + "requirements": ["home-assistant-frontend==20220405.0"], "dependencies": [ "api", "auth", @@ -18,8 +16,6 @@ "system_log", "websocket_api" ], - "codeowners": [ - "@home-assistant/frontend" - ], + "codeowners": ["@home-assistant/frontend"], "quality_scale": "internal" -} \ No newline at end of file +} diff --git a/homeassistant/components/frontend/services.yaml b/homeassistant/components/frontend/services.yaml index 478202a4a0a..2a562ab348a 100644 --- a/homeassistant/components/frontend/services.yaml +++ b/homeassistant/components/frontend/services.yaml @@ -5,12 +5,12 @@ set_theme: description: Set a theme unless the client selected per-device theme. fields: name: - name: Name - description: Name of a predefined theme, 'default' or 'none'. + name: Theme + description: Name of a predefined theme required: true example: "default" selector: - text: + theme: mode: name: Mode description: The mode the theme is for. @@ -18,8 +18,10 @@ set_theme: selector: select: options: - - "dark" - - "light" + - label: "Dark" + value: "dark" + - label: "Light" + value: "light" reload_themes: name: Reload themes diff --git a/homeassistant/components/gdacs/strings.json b/homeassistant/components/gdacs/strings.json index 955936cf986..b728865b347 100644 --- a/homeassistant/components/gdacs/strings.json +++ b/homeassistant/components/gdacs/strings.json @@ -12,4 +12,4 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" } } -} \ No newline at end of file +} diff --git a/homeassistant/components/generic/__init__.py b/homeassistant/components/generic/__init__.py index 3ca526f2029..f243f1639b3 100644 --- a/homeassistant/components/generic/__init__.py +++ b/homeassistant/components/generic/__init__.py @@ -1,6 +1,28 @@ """The generic component.""" +from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform +from homeassistant.core import HomeAssistant DOMAIN = "generic" PLATFORMS = [Platform.CAMERA] + + +async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Handle options update.""" + await hass.config_entries.async_reload(entry.entry_id) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up generic IP camera from a config entry.""" + + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + + entry.async_on_unload(entry.add_update_listener(_async_update_listener)) + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/generic/camera.py b/homeassistant/components/generic/camera.py index b4aaad38618..72fec27b733 100644 --- a/homeassistant/components/generic/camera.py +++ b/homeassistant/components/generic/camera.py @@ -12,6 +12,7 @@ from homeassistant.components.camera import ( SUPPORT_STREAM, Camera, ) +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( CONF_AUTHENTICATION, CONF_NAME, @@ -23,15 +24,13 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import TemplateError -from homeassistant.helpers import config_validation as cv +from homeassistant.helpers import config_validation as cv, template as template_helper from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.httpx_client import get_async_client -from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import DOMAIN, PLATFORMS +from . import DOMAIN from .const import ( - ALLOWED_RTSP_TRANSPORT_PROTOCOLS, CONF_CONTENT_TYPE, CONF_FRAMERATE, CONF_LIMIT_REFETCH_TO_URL_CHANGE, @@ -41,6 +40,7 @@ from .const import ( DEFAULT_NAME, FFMPEG_OPTION_MAP, GET_IMAGE_TIMEOUT, + RTSP_TRANSPORTS, ) _LOGGER = logging.getLogger(__name__) @@ -62,7 +62,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( cv.small_float, cv.positive_int ), vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean, - vol.Optional(CONF_RTSP_TRANSPORT): vol.In(ALLOWED_RTSP_TRANSPORT_PROTOCOLS), + vol.Optional(CONF_RTSP_TRANSPORT): vol.In(RTSP_TRANSPORTS.keys()), } ) @@ -75,25 +75,78 @@ async def async_setup_platform( ) -> None: """Set up a generic IP Camera.""" - await async_setup_reload_service(hass, DOMAIN, PLATFORMS) + _LOGGER.warning( + "Loading generic IP camera via configuration.yaml is deprecated, " + "it will be automatically imported. Once you have confirmed correct " + "operation, please remove 'generic' (IP camera) section(s) from " + "configuration.yaml" + ) + image = config.get(CONF_STILL_IMAGE_URL) + stream = config.get(CONF_STREAM_SOURCE) + config_new = { + CONF_NAME: config[CONF_NAME], + CONF_STILL_IMAGE_URL: image.template if image is not None else None, + CONF_STREAM_SOURCE: stream.template if stream is not None else None, + CONF_AUTHENTICATION: config.get(CONF_AUTHENTICATION), + CONF_USERNAME: config.get(CONF_USERNAME), + CONF_PASSWORD: config.get(CONF_PASSWORD), + CONF_LIMIT_REFETCH_TO_URL_CHANGE: config.get(CONF_LIMIT_REFETCH_TO_URL_CHANGE), + CONF_CONTENT_TYPE: config.get(CONF_CONTENT_TYPE), + CONF_FRAMERATE: config.get(CONF_FRAMERATE), + CONF_VERIFY_SSL: config.get(CONF_VERIFY_SSL), + } - async_add_entities([GenericCamera(hass, config)]) + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=config_new + ) + ) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up a generic IP Camera.""" + + async_add_entities( + [GenericCamera(hass, entry.options, entry.unique_id, entry.title)] + ) + + +def generate_auth(device_info) -> httpx.Auth | None: + """Generate httpx.Auth object from credentials.""" + username = device_info.get(CONF_USERNAME) + password = device_info.get(CONF_PASSWORD) + authentication = device_info.get(CONF_AUTHENTICATION) + if username: + if authentication == HTTP_DIGEST_AUTHENTICATION: + return httpx.DigestAuth(username=username, password=password) + return httpx.BasicAuth(username=username, password=password) + return None class GenericCamera(Camera): """A generic implementation of an IP camera.""" - def __init__(self, hass, device_info): + def __init__(self, hass, device_info, identifier, title): """Initialize a generic camera.""" super().__init__() self.hass = hass + self._attr_unique_id = identifier self._authentication = device_info.get(CONF_AUTHENTICATION) - self._name = device_info.get(CONF_NAME) + self._name = device_info.get(CONF_NAME, title) self._still_image_url = device_info.get(CONF_STILL_IMAGE_URL) - if self._still_image_url: + if ( + not isinstance(self._still_image_url, template_helper.Template) + and self._still_image_url + ): + self._still_image_url = cv.template(self._still_image_url) + if self._still_image_url not in [None, ""]: self._still_image_url.hass = hass self._stream_source = device_info.get(CONF_STREAM_SOURCE) - if self._stream_source is not None: + if self._stream_source not in (None, ""): + if not isinstance(self._stream_source, template_helper.Template): + self._stream_source = cv.template(self._stream_source) self._stream_source.hass = hass self._limit_refetch = device_info[CONF_LIMIT_REFETCH_TO_URL_CHANGE] self._attr_frame_interval = 1 / device_info[CONF_FRAMERATE] @@ -104,17 +157,7 @@ class GenericCamera(Camera): self.stream_options[FFMPEG_OPTION_MAP[CONF_RTSP_TRANSPORT]] = device_info[ CONF_RTSP_TRANSPORT ] - - username = device_info.get(CONF_USERNAME) - password = device_info.get(CONF_PASSWORD) - - if username and password: - if self._authentication == HTTP_DIGEST_AUTHENTICATION: - self._auth = httpx.DigestAuth(username=username, password=password) - else: - self._auth = httpx.BasicAuth(username=username, password=password) - else: - self._auth = None + self._auth = generate_auth(device_info) self._last_url = None self._last_image = None diff --git a/homeassistant/components/generic/config_flow.py b/homeassistant/components/generic/config_flow.py new file mode 100644 index 00000000000..d3b2a260477 --- /dev/null +++ b/homeassistant/components/generic/config_flow.py @@ -0,0 +1,344 @@ +"""Config flow for generic (IP Camera).""" +from __future__ import annotations + +import contextlib +from errno import EHOSTUNREACH, EIO +from functools import partial +import io +import logging +from types import MappingProxyType +from typing import Any +from urllib.parse import urlparse, urlunparse + +import PIL +from async_timeout import timeout +import av +from httpx import HTTPStatusError, RequestError, TimeoutException +import voluptuous as vol + +from homeassistant.components.stream.const import SOURCE_TIMEOUT +from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow +from homeassistant.const import ( + CONF_AUTHENTICATION, + CONF_NAME, + CONF_PASSWORD, + CONF_USERNAME, + CONF_VERIFY_SSL, + HTTP_BASIC_AUTHENTICATION, + HTTP_DIGEST_AUTHENTICATION, +) +from homeassistant.data_entry_flow import FlowResult +from homeassistant.exceptions import TemplateError +from homeassistant.helpers import config_validation as cv, template as template_helper +from homeassistant.helpers.httpx_client import get_async_client +from homeassistant.util import slugify + +from .camera import generate_auth +from .const import ( + CONF_CONTENT_TYPE, + CONF_FRAMERATE, + CONF_LIMIT_REFETCH_TO_URL_CHANGE, + CONF_RTSP_TRANSPORT, + CONF_STILL_IMAGE_URL, + CONF_STREAM_SOURCE, + DEFAULT_NAME, + DOMAIN, + FFMPEG_OPTION_MAP, + GET_IMAGE_TIMEOUT, + RTSP_TRANSPORTS, +) + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_DATA = { + CONF_NAME: DEFAULT_NAME, + CONF_AUTHENTICATION: HTTP_BASIC_AUTHENTICATION, + CONF_LIMIT_REFETCH_TO_URL_CHANGE: False, + CONF_FRAMERATE: 2, + CONF_VERIFY_SSL: True, +} + +SUPPORTED_IMAGE_TYPES = {"png", "jpeg", "gif", "svg+xml"} + + +def build_schema( + user_input: dict[str, Any] | MappingProxyType[str, Any], + is_options_flow: bool = False, +): + """Create schema for camera config setup.""" + spec = { + vol.Optional( + CONF_STILL_IMAGE_URL, + description={"suggested_value": user_input.get(CONF_STILL_IMAGE_URL, "")}, + ): str, + vol.Optional( + CONF_STREAM_SOURCE, + description={"suggested_value": user_input.get(CONF_STREAM_SOURCE, "")}, + ): str, + vol.Optional( + CONF_RTSP_TRANSPORT, + description={"suggested_value": user_input.get(CONF_RTSP_TRANSPORT)}, + ): vol.In(RTSP_TRANSPORTS), + vol.Optional( + CONF_AUTHENTICATION, + description={"suggested_value": user_input.get(CONF_AUTHENTICATION)}, + ): vol.In([HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION]), + vol.Optional( + CONF_USERNAME, + description={"suggested_value": user_input.get(CONF_USERNAME, "")}, + ): str, + vol.Optional( + CONF_PASSWORD, + description={"suggested_value": user_input.get(CONF_PASSWORD, "")}, + ): str, + vol.Required( + CONF_FRAMERATE, + description={"suggested_value": user_input.get(CONF_FRAMERATE, 2)}, + ): int, + vol.Required( + CONF_VERIFY_SSL, default=user_input.get(CONF_VERIFY_SSL, True) + ): bool, + } + if is_options_flow: + spec[ + vol.Required( + CONF_LIMIT_REFETCH_TO_URL_CHANGE, + default=user_input.get(CONF_LIMIT_REFETCH_TO_URL_CHANGE, False), + ) + ] = bool + return vol.Schema(spec) + + +def get_image_type(image): + """Get the format of downloaded bytes that could be an image.""" + fmt = None + imagefile = io.BytesIO(image) + with contextlib.suppress(PIL.UnidentifiedImageError): + img = PIL.Image.open(imagefile) + fmt = img.format.lower() + + if fmt is None: + # if PIL can't figure it out, could be svg. + with contextlib.suppress(UnicodeDecodeError): + if image.decode("utf-8").lstrip().startswith(" tuple[dict[str, str], str | None]: + """Verify that the still image is valid before we create an entity.""" + fmt = None + if not (url := info.get(CONF_STILL_IMAGE_URL)): + return {}, None + if not isinstance(url, template_helper.Template) and url: + url = cv.template(url) + url.hass = hass + try: + url = url.async_render(parse_result=False) + except TemplateError as err: + _LOGGER.error("Error parsing template %s: %s", url, err) + return {CONF_STILL_IMAGE_URL: "template_error"}, None + verify_ssl = info.get(CONF_VERIFY_SSL) + auth = generate_auth(info) + try: + async_client = get_async_client(hass, verify_ssl=verify_ssl) + async with timeout(GET_IMAGE_TIMEOUT): + response = await async_client.get(url, auth=auth, timeout=GET_IMAGE_TIMEOUT) + response.raise_for_status() + image = response.content + except ( + TimeoutError, + RequestError, + HTTPStatusError, + TimeoutException, + ) as err: + _LOGGER.error("Error getting camera image from %s: %s", url, type(err).__name__) + return {CONF_STILL_IMAGE_URL: "unable_still_load"}, None + + if not image: + return {CONF_STILL_IMAGE_URL: "unable_still_load"}, None + fmt = get_image_type(image) + _LOGGER.debug( + "Still image at '%s' detected format: %s", + info[CONF_STILL_IMAGE_URL], + fmt, + ) + if fmt not in SUPPORTED_IMAGE_TYPES: + return {CONF_STILL_IMAGE_URL: "invalid_still_image"}, None + return {}, f"image/{fmt}" + + +def slug_url(url) -> str | None: + """Convert a camera url into a string suitable for a camera name.""" + if not url: + return None + url_no_scheme = urlparse(url)._replace(scheme="") + return slugify(urlunparse(url_no_scheme).strip("/")) + + +async def async_test_stream(hass, info) -> dict[str, str]: + """Verify that the stream is valid before we create an entity.""" + if not (stream_source := info.get(CONF_STREAM_SOURCE)): + return {} + try: + # For RTSP streams, prefer TCP. This code is duplicated from + # homeassistant.components.stream.__init__.py:create_stream() + # It may be possible & better to call create_stream() directly. + stream_options: dict[str, str] = {} + if isinstance(stream_source, str) and stream_source[:7] == "rtsp://": + stream_options = { + "rtsp_flags": "prefer_tcp", + "stimeout": "5000000", + } + if rtsp_transport := info.get(CONF_RTSP_TRANSPORT): + stream_options[FFMPEG_OPTION_MAP[CONF_RTSP_TRANSPORT]] = rtsp_transport + _LOGGER.debug("Attempting to open stream %s", stream_source) + container = await hass.async_add_executor_job( + partial( + av.open, + stream_source, + options=stream_options, + timeout=SOURCE_TIMEOUT, + ) + ) + _ = container.streams.video[0] + except (av.error.FileNotFoundError): # pylint: disable=c-extension-no-member + return {CONF_STREAM_SOURCE: "stream_file_not_found"} + except (av.error.HTTPNotFoundError): # pylint: disable=c-extension-no-member + return {CONF_STREAM_SOURCE: "stream_http_not_found"} + except (av.error.TimeoutError): # pylint: disable=c-extension-no-member + return {CONF_STREAM_SOURCE: "timeout"} + except av.error.HTTPUnauthorizedError: # pylint: disable=c-extension-no-member + return {CONF_STREAM_SOURCE: "stream_unauthorised"} + except (KeyError, IndexError): + return {CONF_STREAM_SOURCE: "stream_no_video"} + except PermissionError: + return {CONF_STREAM_SOURCE: "stream_not_permitted"} + except OSError as err: + if err.errno == EHOSTUNREACH: + return {CONF_STREAM_SOURCE: "stream_no_route_to_host"} + if err.errno == EIO: # input/output error + return {CONF_STREAM_SOURCE: "stream_io_error"} + raise err + return {} + + +class GenericIPCamConfigFlow(ConfigFlow, domain=DOMAIN): + """Config flow for generic IP camera.""" + + VERSION = 1 + + @staticmethod + def async_get_options_flow( + config_entry: ConfigEntry, + ) -> GenericOptionsFlowHandler: + """Get the options flow for this handler.""" + return GenericOptionsFlowHandler(config_entry) + + def check_for_existing(self, options): + """Check whether an existing entry is using the same URLs.""" + return any( + entry.options[CONF_STILL_IMAGE_URL] == options[CONF_STILL_IMAGE_URL] + and entry.options[CONF_STREAM_SOURCE] == options[CONF_STREAM_SOURCE] + for entry in self._async_current_entries() + ) + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the start of the config flow.""" + errors = {} + if user_input: + # Secondary validation because serialised vol can't seem to handle this complexity: + if not user_input.get(CONF_STILL_IMAGE_URL) and not user_input.get( + CONF_STREAM_SOURCE + ): + errors["base"] = "no_still_image_or_stream_url" + else: + errors, still_format = await async_test_still(self.hass, user_input) + errors = errors | await async_test_stream(self.hass, user_input) + still_url = user_input.get(CONF_STILL_IMAGE_URL) + stream_url = user_input.get(CONF_STREAM_SOURCE) + name = slug_url(still_url) or slug_url(stream_url) or DEFAULT_NAME + + if not errors: + user_input[CONF_CONTENT_TYPE] = still_format + user_input[CONF_LIMIT_REFETCH_TO_URL_CHANGE] = False + await self.async_set_unique_id(self.flow_id) + return self.async_create_entry( + title=name, data={}, options=user_input + ) + else: + user_input = DEFAULT_DATA.copy() + + return self.async_show_form( + step_id="user", + data_schema=build_schema(user_input), + errors=errors, + ) + + async def async_step_import(self, import_config) -> FlowResult: + """Handle config import from yaml.""" + # abort if we've already got this one. + if self.check_for_existing(import_config): + return self.async_abort(reason="already_exists") + errors, still_format = await async_test_still(self.hass, import_config) + errors = errors | await async_test_stream(self.hass, import_config) + still_url = import_config.get(CONF_STILL_IMAGE_URL) + stream_url = import_config.get(CONF_STREAM_SOURCE) + name = import_config.get( + CONF_NAME, slug_url(still_url) or slug_url(stream_url) or DEFAULT_NAME + ) + if CONF_LIMIT_REFETCH_TO_URL_CHANGE not in import_config: + import_config[CONF_LIMIT_REFETCH_TO_URL_CHANGE] = False + if not errors: + import_config[CONF_CONTENT_TYPE] = still_format + await self.async_set_unique_id(self.flow_id) + return self.async_create_entry(title=name, data={}, options=import_config) + _LOGGER.error( + "Error importing generic IP camera platform config: unexpected error '%s'", + list(errors.values()), + ) + return self.async_abort(reason="unknown") + + +class GenericOptionsFlowHandler(OptionsFlow): + """Handle Generic IP Camera options.""" + + def __init__(self, config_entry: ConfigEntry) -> None: + """Initialize Generic IP Camera options flow.""" + self.config_entry = config_entry + + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Manage Generic IP Camera options.""" + errors: dict[str, str] = {} + + if user_input is not None: + errors, still_format = await async_test_still(self.hass, user_input) + errors = errors | await async_test_stream(self.hass, user_input) + still_url = user_input.get(CONF_STILL_IMAGE_URL) + stream_url = user_input.get(CONF_STREAM_SOURCE) + if not errors: + return self.async_create_entry( + title=slug_url(still_url) or slug_url(stream_url) or DEFAULT_NAME, + data={ + CONF_AUTHENTICATION: user_input.get(CONF_AUTHENTICATION), + CONF_STREAM_SOURCE: user_input.get(CONF_STREAM_SOURCE), + CONF_PASSWORD: user_input.get(CONF_PASSWORD), + CONF_STILL_IMAGE_URL: user_input.get(CONF_STILL_IMAGE_URL), + CONF_CONTENT_TYPE: still_format, + CONF_USERNAME: user_input.get(CONF_USERNAME), + CONF_LIMIT_REFETCH_TO_URL_CHANGE: user_input[ + CONF_LIMIT_REFETCH_TO_URL_CHANGE + ], + CONF_FRAMERATE: user_input[CONF_FRAMERATE], + CONF_VERIFY_SSL: user_input[CONF_VERIFY_SSL], + }, + ) + return self.async_show_form( + step_id="init", + data_schema=build_schema(user_input or self.config_entry.options, True), + errors=errors, + ) diff --git a/homeassistant/components/generic/const.py b/homeassistant/components/generic/const.py index 1b3ba657ecc..60b4cec61a6 100644 --- a/homeassistant/components/generic/const.py +++ b/homeassistant/components/generic/const.py @@ -1,5 +1,6 @@ """Constants for the generic (IP Camera) integration.""" +DOMAIN = "generic" DEFAULT_NAME = "Generic Camera" CONF_CONTENT_TYPE = "content_type" CONF_LIMIT_REFETCH_TO_URL_CHANGE = "limit_refetch_to_url_change" @@ -8,6 +9,15 @@ CONF_STREAM_SOURCE = "stream_source" CONF_FRAMERATE = "framerate" CONF_RTSP_TRANSPORT = "rtsp_transport" FFMPEG_OPTION_MAP = {CONF_RTSP_TRANSPORT: "rtsp_transport"} -ALLOWED_RTSP_TRANSPORT_PROTOCOLS = {"tcp", "udp", "udp_multicast", "http"} - +RTSP_TRANSPORTS = { + "tcp": "TCP", + "udp": "UDP", + "udp_multicast": "UDP Multicast", + "http": "HTTP", +} GET_IMAGE_TIMEOUT = 10 + +DEFAULT_USERNAME = None +DEFAULT_PASSWORD = None +DEFAULT_IMAGE_URL = None +DEFAULT_STREAM_SOURCE = None diff --git a/homeassistant/components/generic/manifest.json b/homeassistant/components/generic/manifest.json index ab6aa18c4d2..66efa0925c5 100644 --- a/homeassistant/components/generic/manifest.json +++ b/homeassistant/components/generic/manifest.json @@ -1,7 +1,9 @@ { "domain": "generic", "name": "Generic Camera", + "config_flow": true, + "requirements": ["av==9.0.0", "pillow==9.0.1"], "documentation": "https://www.home-assistant.io/integrations/generic", - "codeowners": [], + "codeowners": ["@davet2001"], "iot_class": "local_push" } diff --git a/homeassistant/components/generic/strings.json b/homeassistant/components/generic/strings.json new file mode 100644 index 00000000000..eb1bfcc3c55 --- /dev/null +++ b/homeassistant/components/generic/strings.json @@ -0,0 +1,76 @@ +{ + "config": { + "error": { + "unknown": "[%key:common::config_flow::error::unknown%]", + "already_exists": "A camera with these URL settings already exists.", + "unable_still_load": "Unable to load valid image from still image URL (e.g. invalid host, URL or authentication failure). Review log for more info.", + "no_still_image_or_stream_url": "You must specify at least a still image or stream URL", + "invalid_still_image": "URL did not return a valid still image", + "stream_file_not_found": "File not found while trying to connect to stream (is ffmpeg installed?)", + "stream_http_not_found": "HTTP 404 Not found while trying to connect to stream", + "timeout": "Timeout while loading URL", + "stream_no_route_to_host": "Could not find host while trying to connect to stream", + "stream_io_error": "Input/Output error while trying to connect to stream. Wrong RTSP transport protocol?", + "stream_unauthorised": "Authorisation failed while trying to connect to stream", + "stream_not_permitted": "Operation not permitted while trying to connect to stream. Wrong RTSP transport protocol?", + "stream_no_video": "Stream has no video" + }, + "abort": { + "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", + "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]" + }, + "step": { + "user": { + "description": "Enter the settings to connect to the camera.", + "data": { + "still_image_url": "Still Image URL (e.g. http://...)", + "stream_source": "Stream Source URL (e.g. rtsp://...)", + "rtsp_transport": "RTSP transport protocol", + "authentication": "Authentication", + "limit_refetch_to_url_change": "Limit refetch to url change", + "password": "[%key:common::config_flow::data::password%]", + "username": "[%key:common::config_flow::data::username%]", + "content_type": "Content Type", + "framerate": "Frame Rate (Hz)", + "verify_ssl": "[%key:common::config_flow::data::verify_ssl%]" + } + }, + "confirm": { + "description": "[%key:common::config_flow::description::confirm_setup%]" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "still_image_url": "[%key:component::generic::config::step::user::data::still_image_url%]", + "stream_source": "[%key:component::generic::config::step::user::data::stream_source%]", + "rtsp_transport": "[%key:component::generic::config::step::user::data::rtsp_transport%]", + "authentication": "[%key:component::generic::config::step::user::data::authentication%]", + "limit_refetch_to_url_change": "[%key:component::generic::config::step::user::data::limit_refetch_to_url_change%]", + "password": "[%key:common::config_flow::data::password%]", + "username": "[%key:common::config_flow::data::username%]", + "content_type": "[%key:component::generic::config::step::user::data::content_type%]", + "framerate": "[%key:component::generic::config::step::user::data::framerate%]", + "verify_ssl": "[%key:common::config_flow::data::verify_ssl%]" + } + } + }, + "error": { + "unknown": "[%key:common::config_flow::error::unknown%]", + "already_exists": "[%key:component::generic::config::error::already_exists%]", + "unable_still_load": "[%key:component::generic::config::error::unable_still_load%]", + "no_still_image_or_stream_url": "[%key:component::generic::config::error::no_still_image_or_stream_url%]", + "invalid_still_image": "[%key:component::generic::config::error::invalid_still_image%]", + "stream_file_not_found": "[%key:component::generic::config::error::stream_file_not_found%]", + "stream_http_not_found": "[%key:component::generic::config::error::stream_http_not_found%]", + "timeout": "[%key:component::generic::config::error::timeout%]", + "stream_no_route_to_host": "[%key:component::generic::config::error::stream_no_route_to_host%]", + "stream_io_error": "[%key:component::generic::config::error::stream_io_error%]", + "stream_unauthorised": "[%key:component::generic::config::error::stream_unauthorised%]", + "stream_not_permitted": "[%key:component::generic::config::error::stream_not_permitted%]", + "stream_no_video": "[%key:component::generic::config::error::stream_no_video%]" + } + } +} diff --git a/homeassistant/components/generic/translations/en.json b/homeassistant/components/generic/translations/en.json new file mode 100644 index 00000000000..5346c2b5106 --- /dev/null +++ b/homeassistant/components/generic/translations/en.json @@ -0,0 +1,76 @@ +{ + "config": { + "abort": { + "no_devices_found": "No devices found on the network", + "single_instance_allowed": "Already configured. Only a single configuration possible." + }, + "error": { + "already_exists": "A camera with these URL settings already exists.", + "invalid_still_image": "URL did not return a valid still image", + "no_still_image_or_stream_url": "You must specify at least a still image or stream URL", + "stream_file_not_found": "File not found while trying to connect to stream (is ffmpeg installed?)", + "stream_http_not_found": "HTTP 404 Not found while trying to connect to stream", + "stream_io_error": "Input/Output error while trying to connect to stream. Wrong RTSP transport protocol?", + "stream_no_route_to_host": "Could not find host while trying to connect to stream", + "stream_no_video": "Stream has no video", + "stream_not_permitted": "Operation not permitted while trying to connect to stream. Wrong RTSP transport protocol?", + "stream_unauthorised": "Authorisation failed while trying to connect to stream", + "timeout": "Timeout while loading URL", + "unable_still_load": "Unable to load valid image from still image URL (e.g. invalid host, URL or authentication failure). Review log for more info.", + "unknown": "Unexpected error" + }, + "step": { + "confirm": { + "description": "Do you want to start set up?" + }, + "user": { + "data": { + "authentication": "Authentication", + "content_type": "Content Type", + "framerate": "Frame Rate (Hz)", + "limit_refetch_to_url_change": "Limit refetch to url change", + "password": "Password", + "rtsp_transport": "RTSP transport protocol", + "still_image_url": "Still Image URL (e.g. http://...)", + "stream_source": "Stream Source URL (e.g. rtsp://...)", + "username": "Username", + "verify_ssl": "Verify SSL certificate" + }, + "description": "Enter the settings to connect to the camera." + } + } + }, + "options": { + "error": { + "already_exists": "A camera with these URL settings already exists.", + "invalid_still_image": "URL did not return a valid still image", + "no_still_image_or_stream_url": "You must specify at least a still image or stream URL", + "stream_file_not_found": "File not found while trying to connect to stream (is ffmpeg installed?)", + "stream_http_not_found": "HTTP 404 Not found while trying to connect to stream", + "stream_io_error": "Input/Output error while trying to connect to stream. Wrong RTSP transport protocol?", + "stream_no_route_to_host": "Could not find host while trying to connect to stream", + "stream_no_video": "Stream has no video", + "stream_not_permitted": "Operation not permitted while trying to connect to stream. Wrong RTSP transport protocol?", + "stream_unauthorised": "Authorisation failed while trying to connect to stream", + "timeout": "Timeout while loading URL", + "unable_still_load": "Unable to load valid image from still image URL (e.g. invalid host, URL or authentication failure). Review log for more info.", + "unknown": "Unexpected error" + }, + "step": { + "init": { + "data": { + "authentication": "Authentication", + "content_type": "Content Type", + "framerate": "Frame Rate (Hz)", + "limit_refetch_to_url_change": "Limit refetch to url change", + "password": "Password", + "rtsp_transport": "RTSP transport protocol", + "still_image_url": "Still Image URL (e.g. http://...)", + "stream_source": "Stream Source URL (e.g. rtsp://...)", + "username": "Username", + "verify_ssl": "Verify SSL certificate" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/geniushub/services.yaml b/homeassistant/components/geniushub/services.yaml index 1edcf243cc0..7d4cd14b19e 100644 --- a/homeassistant/components/geniushub/services.yaml +++ b/homeassistant/components/geniushub/services.yaml @@ -21,9 +21,9 @@ set_zone_mode: selector: select: options: - - 'off' - - 'timer' - - 'footprint' + - "off" + - "timer" + - "footprint" set_zone_override: name: Set zone override @@ -47,7 +47,7 @@ set_zone_override: min: 4 max: 28 step: 0.1 - unit_of_measurement: '°' + unit_of_measurement: "°" duration: name: Duration description: >- diff --git a/homeassistant/components/geo_json_events/geo_location.py b/homeassistant/components/geo_json_events/geo_location.py index f8ed84e532b..896fb7d36da 100644 --- a/homeassistant/components/geo_json_events/geo_location.py +++ b/homeassistant/components/geo_json_events/geo_location.py @@ -4,7 +4,7 @@ from __future__ import annotations from datetime import timedelta import logging -from geojson_client.generic_feed import GenericFeedManager +from aio_geojson_generic_client import GenericFeedManager import voluptuous as vol from homeassistant.components.geo_location import PLATFORM_SCHEMA, GeolocationEvent @@ -18,10 +18,14 @@ from homeassistant.const import ( LENGTH_KILOMETERS, ) from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import aiohttp_client import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.event import track_time_interval +from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType _LOGGER = logging.getLogger(__name__) @@ -44,10 +48,10 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -def setup_platform( +async def async_setup_platform( hass: HomeAssistant, config: ConfigType, - add_entities: AddEntitiesCallback, + async_add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the GeoJSON Events platform.""" @@ -59,27 +63,30 @@ def setup_platform( ) radius_in_km = config[CONF_RADIUS] # Initialize the entity manager. - feed = GeoJsonFeedEntityManager( - hass, add_entities, scan_interval, coordinates, url, radius_in_km + manager = GeoJsonFeedEntityManager( + hass, async_add_entities, scan_interval, coordinates, url, radius_in_km ) + await manager.async_init() - def start_feed_manager(event): + async def start_feed_manager(event=None): """Start feed manager.""" - feed.startup() + await manager.async_update() - hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_feed_manager) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, start_feed_manager) class GeoJsonFeedEntityManager: """Feed Entity Manager for GeoJSON feeds.""" def __init__( - self, hass, add_entities, scan_interval, coordinates, url, radius_in_km + self, hass, async_add_entities, scan_interval, coordinates, url, radius_in_km ): """Initialize the GeoJSON Feed Manager.""" self._hass = hass + websession = aiohttp_client.async_get_clientsession(hass) self._feed_manager = GenericFeedManager( + websession, self._generate_entity, self._update_entity, self._remove_entity, @@ -87,37 +94,42 @@ class GeoJsonFeedEntityManager: url, filter_radius=radius_in_km, ) - self._add_entities = add_entities + self._async_add_entities = async_add_entities self._scan_interval = scan_interval - def startup(self): - """Start up this manager.""" - self._feed_manager.update() - self._init_regular_updates() + async def async_init(self): + """Schedule initial and regular updates based on configured time interval.""" - def _init_regular_updates(self): - """Schedule regular updates at the specified interval.""" - track_time_interval( - self._hass, lambda now: self._feed_manager.update(), self._scan_interval - ) + async def update(event_time): + """Update.""" + await self.async_update() + + # Trigger updates at regular intervals. + async_track_time_interval(self._hass, update, self._scan_interval) + _LOGGER.debug("Feed entity manager initialized") + + async def async_update(self): + """Refresh data.""" + await self._feed_manager.update() + _LOGGER.debug("Feed entity manager updated") def get_entry(self, external_id): """Get feed entry by external id.""" return self._feed_manager.feed_entries.get(external_id) - def _generate_entity(self, external_id): + async def _generate_entity(self, external_id): """Generate new entity.""" new_entity = GeoJsonLocationEvent(self, external_id) # Add new entities to HA. - self._add_entities([new_entity], True) + self._async_add_entities([new_entity], True) - def _update_entity(self, external_id): + async def _update_entity(self, external_id): """Update entity.""" - dispatcher_send(self._hass, f"geo_json_events_update_{external_id}") + async_dispatcher_send(self._hass, f"geo_json_events_update_{external_id}") - def _remove_entity(self, external_id): + async def _remove_entity(self, external_id): """Remove entity.""" - dispatcher_send(self._hass, f"geo_json_events_delete_{external_id}") + async_dispatcher_send(self._hass, f"geo_json_events_delete_{external_id}") class GeoJsonLocationEvent(GeolocationEvent): diff --git a/homeassistant/components/geo_json_events/manifest.json b/homeassistant/components/geo_json_events/manifest.json index 8f54c816649..ea5b56bee1e 100644 --- a/homeassistant/components/geo_json_events/manifest.json +++ b/homeassistant/components/geo_json_events/manifest.json @@ -2,8 +2,8 @@ "domain": "geo_json_events", "name": "GeoJSON", "documentation": "https://www.home-assistant.io/integrations/geo_json_events", - "requirements": ["geojson_client==0.6"], + "requirements": ["aio_geojson_generic_client==0.1"], "codeowners": ["@exxamalte"], "iot_class": "cloud_polling", - "loggers": ["geojson_client"] + "loggers": ["aio_geojson_generic_client"] } diff --git a/homeassistant/components/geo_location/manifest.json b/homeassistant/components/geo_location/manifest.json index c222df8b2aa..2e0d7061099 100644 --- a/homeassistant/components/geo_location/manifest.json +++ b/homeassistant/components/geo_location/manifest.json @@ -2,6 +2,6 @@ "domain": "geo_location", "name": "Geolocation", "documentation": "https://www.home-assistant.io/integrations/geo_location", - "codeowners": [], + "codeowners": ["@home-assistant/core"], "quality_scale": "internal" } diff --git a/homeassistant/components/geofency/translations/fr.json b/homeassistant/components/geofency/translations/fr.json index db163efeac7..84da031d50d 100644 --- a/homeassistant/components/geofency/translations/fr.json +++ b/homeassistant/components/geofency/translations/fr.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "cloud_not_connected": "Pas connect\u00e9 \u00e0 Home Assistant Cloud", + "cloud_not_connected": "Non connect\u00e9 \u00e0 Home Assistant Cloud.", "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible.", "webhook_not_internet_accessible": "Votre installation de Home Assistant doit \u00eatre accessible depuis internet pour recevoir des messages webhook." }, diff --git a/homeassistant/components/geonetnz_quakes/strings.json b/homeassistant/components/geonetnz_quakes/strings.json index 3f2d702492f..3a84bdae701 100644 --- a/homeassistant/components/geonetnz_quakes/strings.json +++ b/homeassistant/components/geonetnz_quakes/strings.json @@ -6,6 +6,8 @@ "data": { "radius": "Radius", "mmi": "MMI" } } }, - "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" } + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" + } } } diff --git a/homeassistant/components/geonetnz_volcano/strings.json b/homeassistant/components/geonetnz_volcano/strings.json index 09f523c5266..867d2840fb7 100644 --- a/homeassistant/components/geonetnz_volcano/strings.json +++ b/homeassistant/components/geonetnz_volcano/strings.json @@ -6,6 +6,8 @@ "data": { "radius": "Radius" } } }, - "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_location%]" } + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_location%]" + } } } diff --git a/homeassistant/components/gios/sensor.py b/homeassistant/components/gios/sensor.py index ff589d34791..c14a99051e4 100644 --- a/homeassistant/components/gios/sensor.py +++ b/homeassistant/components/gios/sensor.py @@ -69,10 +69,9 @@ async def async_setup_entry( async_add_entities(sensors) -class GiosSensor(CoordinatorEntity, SensorEntity): +class GiosSensor(CoordinatorEntity[GiosDataUpdateCoordinator], SensorEntity): """Define an GIOS sensor.""" - coordinator: GiosDataUpdateCoordinator entity_description: GiosSensorEntityDescription def __init__( diff --git a/homeassistant/components/gios/strings.json b/homeassistant/components/gios/strings.json index ef2fec9f84f..18db42b69c0 100644 --- a/homeassistant/components/gios/strings.json +++ b/homeassistant/components/gios/strings.json @@ -3,7 +3,6 @@ "step": { "user": { "title": "GIO\u015a (Polish Chief Inspectorate Of Environmental Protection)", - "description": "Set up GIO\u015a (Polish Chief Inspectorate Of Environmental Protection) air quality integration. If you need help with the configuration have a look here: https://www.home-assistant.io/integrations/gios", "data": { "name": "[%key:common::config_flow::data::name%]", "station_id": "ID of the measuring station" diff --git a/homeassistant/components/github/__init__.py b/homeassistant/components/github/__init__.py index 4ecff6e9648..404aeae11b5 100644 --- a/homeassistant/components/github/__init__.py +++ b/homeassistant/components/github/__init__.py @@ -38,6 +38,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) await coordinator.async_config_entry_first_refresh() + await coordinator.subscribe() hass.data[DOMAIN][repository] = coordinator @@ -45,7 +46,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.config_entries.async_setup_platforms(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(async_reload_entry)) - return True @@ -77,6 +77,10 @@ def async_cleanup_device_registry( async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" + repositories: dict[str, GitHubDataUpdateCoordinator] = hass.data[DOMAIN] + for coordinator in repositories.values(): + coordinator.unsubscribe() + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): hass.data.pop(DOMAIN) return unload_ok diff --git a/homeassistant/components/github/const.py b/homeassistant/components/github/const.py index efe9d7baa5e..a186f4684b3 100644 --- a/homeassistant/components/github/const.py +++ b/homeassistant/components/github/const.py @@ -11,7 +11,18 @@ DOMAIN = "github" CLIENT_ID = "1440cafcc86e3ea5d6a2" DEFAULT_REPOSITORIES = ["home-assistant/core", "esphome/esphome"] -DEFAULT_UPDATE_INTERVAL = timedelta(seconds=300) +FALLBACK_UPDATE_INTERVAL = timedelta(hours=1, minutes=30) CONF_ACCESS_TOKEN = "access_token" CONF_REPOSITORIES = "repositories" + + +REFRESH_EVENT_TYPES = ( + "CreateEvent", + "ForkEvent", + "IssuesEvent", + "PullRequestEvent", + "PushEvent", + "ReleaseEvent", + "WatchEvent", +) diff --git a/homeassistant/components/github/coordinator.py b/homeassistant/components/github/coordinator.py index 10f30bb1006..679c3d89aeb 100644 --- a/homeassistant/components/github/coordinator.py +++ b/homeassistant/components/github/coordinator.py @@ -6,15 +6,17 @@ from typing import Any from aiogithubapi import ( GitHubAPI, GitHubConnectionException, + GitHubEventModel, GitHubException, GitHubRatelimitException, GitHubResponseModel, ) +from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import DEFAULT_UPDATE_INTERVAL, DOMAIN, LOGGER +from .const import FALLBACK_UPDATE_INTERVAL, LOGGER, REFRESH_EVENT_TYPES GRAPHQL_REPOSITORY_QUERY = """ query ($owner: String!, $repository: String!) { @@ -109,13 +111,14 @@ class GitHubDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): self.repository = repository self._client = client self._last_response: GitHubResponseModel[dict[str, Any]] | None = None + self._subscription_id: str | None = None self.data = {} super().__init__( hass, LOGGER, - name=DOMAIN, - update_interval=DEFAULT_UPDATE_INTERVAL, + name=repository, + update_interval=FALLBACK_UPDATE_INTERVAL, ) async def _async_update_data(self) -> GitHubResponseModel[dict[str, Any]]: @@ -136,3 +139,26 @@ class GitHubDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): else: self._last_response = response return response.data["data"]["repository"] + + async def _handle_event(self, event: GitHubEventModel) -> None: + """Handle an event.""" + if event.type in REFRESH_EVENT_TYPES: + await self.async_request_refresh() + + @staticmethod + async def _handle_error(error: GitHubException) -> None: + """Handle an error.""" + LOGGER.error("An error occurred while processing new events - %s", error) + + async def subscribe(self) -> None: + """Subscribe to repository events.""" + self._subscription_id = await self._client.repos.events.subscribe( + self.repository, + event_callback=self._handle_event, + error_callback=self._handle_error, + ) + self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.unsubscribe) + + def unsubscribe(self, *args) -> None: + """Unsubscribe to repository events.""" + self._client.repos.events.unsubscribe(subscription_id=self._subscription_id) diff --git a/homeassistant/components/github/manifest.json b/homeassistant/components/github/manifest.json index 196095a5b6e..25c7cbdd512 100644 --- a/homeassistant/components/github/manifest.json +++ b/homeassistant/components/github/manifest.json @@ -2,16 +2,9 @@ "domain": "github", "name": "GitHub", "documentation": "https://www.home-assistant.io/integrations/github", - "requirements": [ - "aiogithubapi==22.2.3" - ], - "codeowners": [ - "@timmo001", - "@ludeeus" - ], + "requirements": ["aiogithubapi==22.2.4"], + "codeowners": ["@timmo001", "@ludeeus"], "iot_class": "cloud_polling", "config_flow": true, - "loggers": [ - "aiogithubapi" - ] -} \ No newline at end of file + "loggers": ["aiogithubapi"] +} diff --git a/homeassistant/components/github/sensor.py b/homeassistant/components/github/sensor.py index a09e440e2ce..8dff4b04b01 100644 --- a/homeassistant/components/github/sensor.py +++ b/homeassistant/components/github/sensor.py @@ -172,12 +172,11 @@ async def async_setup_entry( ) -class GitHubSensorEntity(CoordinatorEntity[dict[str, Any]], SensorEntity): +class GitHubSensorEntity(CoordinatorEntity[GitHubDataUpdateCoordinator], SensorEntity): """Defines a GitHub sensor entity.""" _attr_attribution = "Data provided by the GitHub API" - coordinator: GitHubDataUpdateCoordinator entity_description: GitHubSensorEntityDescription def __init__( diff --git a/homeassistant/components/github/strings.json b/homeassistant/components/github/strings.json index ac80086ed3a..fa981d3dcb5 100644 --- a/homeassistant/components/github/strings.json +++ b/homeassistant/components/github/strings.json @@ -16,4 +16,4 @@ "could_not_register": "Could not register integration with GitHub" } } -} \ No newline at end of file +} diff --git a/homeassistant/components/goalzero/manifest.json b/homeassistant/components/goalzero/manifest.json index 5cb00c11191..bb26567b8cc 100644 --- a/homeassistant/components/goalzero/manifest.json +++ b/homeassistant/components/goalzero/manifest.json @@ -4,10 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/goalzero", "requirements": ["goalzero==0.2.1"], - "dhcp": [ - {"registered_devices": true}, - {"hostname": "yeti*"} - ], + "dhcp": [{ "registered_devices": true }, { "hostname": "yeti*" }], "codeowners": ["@tkdrob"], "quality_scale": "silver", "iot_class": "local_polling", diff --git a/homeassistant/components/goalzero/strings.json b/homeassistant/components/goalzero/strings.json index 5147299b564..619b379c7a3 100644 --- a/homeassistant/components/goalzero/strings.json +++ b/homeassistant/components/goalzero/strings.json @@ -2,15 +2,13 @@ "config": { "step": { "user": { - "title": "Goal Zero Yeti", - "description": "First, you need to download the Goal Zero app: https://www.goalzero.com/product-features/yeti-app/\n\nFollow the instructions to connect your Yeti to your Wi-fi network. DHCP reservation on your router is recommended. If not set up, the device may become unavailable until Home Assistant detects the new ip address. Refer to your router's user manual.", + "description": "Please refer to the documentation to make sure all requirements are met.", "data": { "host": "[%key:common::config_flow::data::host%]", "name": "[%key:common::config_flow::data::name%]" } }, "confirm_discovery": { - "title": "Goal Zero Yeti", "description": "DHCP reservation on your router is recommended. If not set up, the device may become unavailable until Home Assistant detects the new ip address. Refer to your router's user manual." } }, diff --git a/homeassistant/components/goalzero/translations/el.json b/homeassistant/components/goalzero/translations/el.json index 31d089f53ec..865106aa33c 100644 --- a/homeassistant/components/goalzero/translations/el.json +++ b/homeassistant/components/goalzero/translations/el.json @@ -1,14 +1,14 @@ { "config": { "abort": { - "already_configured": "\u039f \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc\u03c2 \u03b5\u03af\u03bd\u03b1\u03b9 \u03ae\u03b4\u03b7 \u03c0\u03b1\u03c1\u03b1\u03bc\u03b5\u03c4\u03c1\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03bf\u03c2", + "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", "invalid_host": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03cc\u03bd\u03bf\u03bc\u03b1 \u03ba\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03bf\u03cd \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae \u03ae \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP", "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" }, "error": { "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", - "invalid_host": "\u0391\u03c5\u03c4\u03cc \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03c4\u03bf Yeti \u03c0\u03bf\u03c5 \u03c8\u03ac\u03c7\u03bd\u03b5\u03c4\u03b5", - "unknown": "\u0386\u03b3\u03bd\u03c9\u03c3\u03c4\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + "invalid_host": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03cc\u03bd\u03bf\u03bc\u03b1 \u03ba\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03bf\u03cd \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae \u03ae \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" }, "step": { "confirm_discovery": { @@ -20,7 +20,7 @@ "host": "\u0394\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae\u03c2", "name": "\u038c\u03bd\u03bf\u03bc\u03b1" }, - "description": "\u0391\u03c1\u03c7\u03b9\u03ba\u03ac, \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03ba\u03b1\u03c4\u03b5\u03b2\u03ac\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae Goal Zero: https://www.goalzero.com/product-features/yeti-app/ \n\n \u0391\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03bf\u03b4\u03b7\u03b3\u03af\u03b5\u03c2 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03c3\u03c5\u03bd\u03b4\u03ad\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf Yeti \u03c3\u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03bf Wi-Fi. \u03a3\u03c4\u03b7 \u03c3\u03c5\u03bd\u03ad\u03c7\u03b5\u03b9\u03b1, \u03bb\u03ac\u03b2\u03b5\u03c4\u03b5 \u03c4\u03b7\u03bd ip \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae \u03b1\u03c0\u03cc \u03c4\u03bf \u03b4\u03c1\u03bf\u03bc\u03bf\u03bb\u03bf\u03b3\u03b7\u03c4\u03ae \u03c3\u03b1\u03c2. \u03a4\u03bf DHCP \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af \u03c3\u03c4\u03b9\u03c2 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 \u03c4\u03bf\u03c5 \u03b4\u03c1\u03bf\u03bc\u03bf\u03bb\u03bf\u03b3\u03b7\u03c4\u03ae \u03c3\u03b1\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03b1\u03c3\u03c6\u03b1\u03bb\u03b9\u03c3\u03c4\u03b5\u03af \u03cc\u03c4\u03b9 \u03b7 ip \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae \u03b4\u03b5\u03bd \u03b8\u03b1 \u03b1\u03bb\u03bb\u03ac\u03be\u03b5\u03b9. \u0391\u03bd\u03b1\u03c4\u03c1\u03ad\u03be\u03c4\u03b5 \u03c3\u03c4\u03bf \u03b5\u03b3\u03c7\u03b5\u03b9\u03c1\u03af\u03b4\u03b9\u03bf \u03c7\u03c1\u03ae\u03c3\u03b7\u03c2 \u03c4\u03bf\u03c5 \u03b4\u03c1\u03bf\u03bc\u03bf\u03bb\u03bf\u03b3\u03b7\u03c4\u03ae \u03c3\u03b1\u03c2.", + "description": "\u03a0\u03c1\u03ce\u03c4\u03b1, \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03ba\u03b1\u03c4\u03b5\u03b2\u03ac\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae Goal Zero: https://www.goalzero.com/product-features/yeti-app/ \n\n \u0391\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03bf\u03b4\u03b7\u03b3\u03af\u03b5\u03c2 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03c3\u03c5\u03bd\u03b4\u03ad\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf Yeti \u03c3\u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03bf Wi-Fi. \u03a3\u03c5\u03bd\u03b9\u03c3\u03c4\u03ac\u03c4\u03b1\u03b9 \u03b7 \u03ba\u03c1\u03ac\u03c4\u03b7\u03c3\u03b7 DHCP \u03c3\u03c4\u03bf \u03b4\u03c1\u03bf\u03bc\u03bf\u03bb\u03bf\u03b3\u03b7\u03c4\u03ae \u03c3\u03b1\u03c2. \u0395\u03ac\u03bd \u03b4\u03b5\u03bd \u03ad\u03c7\u03b5\u03b9 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af, \u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b5\u03bd\u03b4\u03ad\u03c7\u03b5\u03c4\u03b1\u03b9 \u03bd\u03b1 \u03bc\u03b7\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03b9\u03b1\u03b8\u03ad\u03c3\u03b9\u03bc\u03b7 \u03ad\u03c9\u03c2 \u03cc\u03c4\u03bf\u03c5 \u03bf \u0392\u03bf\u03b7\u03b8\u03cc\u03c2 \u039f\u03b9\u03ba\u03af\u03b1\u03c2 \u03b5\u03bd\u03c4\u03bf\u03c0\u03af\u03c3\u03b5\u03b9 \u03c4\u03b7 \u03bd\u03ad\u03b1 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP. \u0391\u03bd\u03b1\u03c4\u03c1\u03ad\u03be\u03c4\u03b5 \u03c3\u03c4\u03bf \u03b5\u03b3\u03c7\u03b5\u03b9\u03c1\u03af\u03b4\u03b9\u03bf \u03c7\u03c1\u03ae\u03c3\u03b7\u03c2 \u03c4\u03bf\u03c5 \u03b4\u03c1\u03bf\u03bc\u03bf\u03bb\u03bf\u03b3\u03b7\u03c4\u03ae \u03c3\u03b1\u03c2.", "title": "Goal Zero Yeti" } } diff --git a/homeassistant/components/goalzero/translations/fr.json b/homeassistant/components/goalzero/translations/fr.json index 7def0d06402..b554ec1ea1d 100644 --- a/homeassistant/components/goalzero/translations/fr.json +++ b/homeassistant/components/goalzero/translations/fr.json @@ -20,7 +20,7 @@ "host": "H\u00f4te", "name": "Nom" }, - "description": "Tout d'abord, vous devez t\u00e9l\u00e9charger l'application Goal Zero: https://www.goalzero.com/product-features/yeti-app/\n\n Suivez les instructions pour connecter votre Yeti \u00e0 votre r\u00e9seau Wifi. Ensuite, r\u00e9cup\u00e9rez l'adresse IP de votre routeur. DHCP doit \u00eatre configur\u00e9 dans les param\u00e8tres de votre routeur pour le p\u00e9riph\u00e9rique afin de garantir que l'adresse IP de l'h\u00f4te ne change pas. Reportez-vous au manuel d'utilisation de votre routeur.", + "description": "Vous devez tout d'abord t\u00e9l\u00e9charger l'application Goal Zero\u00a0: https://www.goalzero.com/product-features/yeti-app/\n\nSuivez les instructions pour connecter votre Yeti \u00e0 votre r\u00e9seau Wi-Fi. Il est recommand\u00e9 d'utiliser la r\u00e9servation DHCP sur votre routeur, sans quoi l'appareil risque de devenir indisponible le temps que Home Assistant d\u00e9tecte la nouvelle adresse IP. Reportez-vous au manuel d'utilisation de votre routeur.", "title": "Goal Zero Yeti" } } diff --git a/homeassistant/components/gogogate2/common.py b/homeassistant/components/gogogate2/common.py index 5d0392e6db2..bfbadf86ee2 100644 --- a/homeassistant/components/gogogate2/common.py +++ b/homeassistant/components/gogogate2/common.py @@ -66,7 +66,7 @@ class DeviceDataUpdateCoordinator(DataUpdateCoordinator): self.api = api -class GoGoGate2Entity(CoordinatorEntity): +class GoGoGate2Entity(CoordinatorEntity[DeviceDataUpdateCoordinator]): """Base class for gogogate2 entities.""" def __init__( diff --git a/homeassistant/components/gogogate2/translations/fr.json b/homeassistant/components/gogogate2/translations/fr.json index 94cee628a79..d51dd6f2fde 100644 --- a/homeassistant/components/gogogate2/translations/fr.json +++ b/homeassistant/components/gogogate2/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide" + "invalid_auth": "Authentification non valide" }, "flow_title": "{device} ({ip_address})", "step": { diff --git a/homeassistant/components/goodwe/manifest.json b/homeassistant/components/goodwe/manifest.json index 102cd0ac2eb..45895d0d4b0 100644 --- a/homeassistant/components/goodwe/manifest.json +++ b/homeassistant/components/goodwe/manifest.json @@ -2,12 +2,9 @@ "domain": "goodwe", "name": "GoodWe Inverter", "documentation": "https://www.home-assistant.io/integrations/goodwe", - "codeowners": [ - "@mletenay", - "@starkillerOG" - ], + "codeowners": ["@mletenay", "@starkillerOG"], "requirements": ["goodwe==0.2.15"], "config_flow": true, "iot_class": "local_polling", "loggers": ["goodwe"] -} \ No newline at end of file +} diff --git a/homeassistant/components/goodwe/translations/fr.json b/homeassistant/components/goodwe/translations/fr.json index 544d6cfda68..7f7238bd960 100644 --- a/homeassistant/components/goodwe/translations/fr.json +++ b/homeassistant/components/goodwe/translations/fr.json @@ -5,7 +5,7 @@ "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours" }, "error": { - "connection_error": "Impossible de se connecter" + "connection_error": "\u00c9chec de connexion" }, "step": { "user": { diff --git a/homeassistant/components/google/__init__.py b/homeassistant/components/google/__init__.py index 9f85d41774b..f158db884dc 100644 --- a/homeassistant/components/google/__init__.py +++ b/homeassistant/components/google/__init__.py @@ -1,24 +1,20 @@ """Support for Google - Calendar Event Devices.""" +from __future__ import annotations + +import asyncio from collections.abc import Mapping -from datetime import datetime, timedelta, timezone -from enum import Enum +from datetime import datetime, timedelta import logging -import os from typing import Any -from googleapiclient import discovery as google_discovery -import httplib2 -from oauth2client.client import ( - FlowExchangeError, - OAuth2DeviceCodeError, - OAuth2WebServerFlow, -) +from httplib2.error import ServerNotFoundError from oauth2client.file import Storage import voluptuous as vol from voluptuous.error import Error as VoluptuousError import yaml -from homeassistant.components import persistent_notification +from homeassistant import config_entries +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_CLIENT_ID, CONF_CLIENT_SECRET, @@ -26,19 +22,28 @@ from homeassistant.const import ( CONF_ENTITIES, CONF_NAME, CONF_OFFSET, - Platform, ) from homeassistant.core import HomeAssistant, ServiceCall -from homeassistant.helpers import discovery +from homeassistant.exceptions import ConfigEntryAuthFailed, HomeAssistantError +from homeassistant.helpers import config_entry_oauth2_flow import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.entity import generate_entity_id -from homeassistant.helpers.event import track_utc_time_change from homeassistant.helpers.typing import ConfigType -from homeassistant.util import convert + +from . import config_flow +from .api import DeviceAuth, GoogleCalendarService +from .const import ( + CONF_CALENDAR_ACCESS, + DATA_CONFIG, + DATA_SERVICE, + DISCOVER_CALENDAR, + DOMAIN, + FeatureAccess, +) _LOGGER = logging.getLogger(__name__) -DOMAIN = "google" ENTITY_ID_FORMAT = DOMAIN + ".{}" CONF_TRACK_NEW = "track_new_calendar" @@ -48,9 +53,7 @@ CONF_TRACK = "track" CONF_SEARCH = "search" CONF_IGNORE_AVAILABILITY = "ignore_availability" CONF_MAX_RESULTS = "max_results" -CONF_CALENDAR_ACCESS = "calendar_access" -DEFAULT_CONF_TRACK_NEW = True DEFAULT_CONF_OFFSET = "!!" EVENT_CALENDAR_ID = "calendar_id" @@ -75,27 +78,11 @@ SERVICE_SCAN_CALENDARS = "scan_for_calendars" SERVICE_FOUND_CALENDARS = "found_calendar" SERVICE_ADD_EVENT = "add_event" -DATA_INDEX = "google_calendars" - YAML_DEVICES = f"{DOMAIN}_calendars.yaml" TOKEN_FILE = f".{DOMAIN}.token" - -class FeatureAccess(Enum): - """Class to represent different access scopes.""" - - read_only = "https://www.googleapis.com/auth/calendar.readonly" - read_write = "https://www.googleapis.com/auth/calendar" - - def __init__(self, scope: str) -> None: - """Init instance.""" - self._scope = scope - - @property - def scope(self) -> str: - """Google calendar scope for the feature.""" - return self._scope +PLATFORMS = ["calendar"] CONFIG_SCHEMA = vol.Schema( @@ -104,7 +91,7 @@ CONFIG_SCHEMA = vol.Schema( { vol.Required(CONF_CLIENT_ID): cv.string, vol.Required(CONF_CLIENT_SECRET): cv.string, - vol.Optional(CONF_TRACK_NEW): cv.boolean, + vol.Optional(CONF_TRACK_NEW, default=True): cv.boolean, vol.Optional(CONF_CALENDAR_ACCESS, default="read_write"): cv.enum( FeatureAccess ), @@ -160,184 +147,117 @@ ADD_EVENT_SERVICE_SCHEMA = vol.Schema( ) -def do_authentication( - hass: HomeAssistant, hass_config: ConfigType, config: ConfigType -) -> bool: - """Notify user of actions and authenticate. - - Notify user of user_code and verification_url then poll - until we have an access token. - """ - oauth = OAuth2WebServerFlow( - client_id=config[CONF_CLIENT_ID], - client_secret=config[CONF_CLIENT_SECRET], - scope=config[CONF_CALENDAR_ACCESS].scope, - redirect_uri="Home-Assistant.io", - ) - try: - dev_flow = oauth.step1_get_device_and_user_codes() - except OAuth2DeviceCodeError as err: - persistent_notification.create( - hass, - f"Error: {err}
You will need to restart hass after fixing." "", - title=NOTIFICATION_TITLE, - notification_id=NOTIFICATION_ID, - ) - return False - - persistent_notification.create( +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up the Google component.""" + conf = config.get(DOMAIN, {}) + hass.data[DOMAIN] = {DATA_CONFIG: conf} + config_flow.OAuth2FlowHandler.async_register_implementation( hass, - ( - f"In order to authorize Home-Assistant to view your calendars " - f'you must visit: {dev_flow.verification_url} and enter ' - f"code: {dev_flow.user_code}" - ), - title=NOTIFICATION_TITLE, - notification_id=NOTIFICATION_ID, - ) - - def step2_exchange(now: datetime) -> None: - """Keep trying to validate the user_code until it expires.""" - _LOGGER.debug("Attempting to validate user code") - - # For some reason, oauth.step1_get_device_and_user_codes() returns a datetime - # object without tzinfo. For the comparison below to work, it needs one. - user_code_expiry = dev_flow.user_code_expiry.replace(tzinfo=timezone.utc) - - if now >= user_code_expiry: - persistent_notification.create( - hass, - "Authentication code expired, please restart " - "Home-Assistant and try again", - title=NOTIFICATION_TITLE, - notification_id=NOTIFICATION_ID, - ) - listener() - return - - try: - credentials = oauth.step2_exchange(device_flow_info=dev_flow) - except FlowExchangeError: - # not ready yet, call again - return - - storage = Storage(hass.config.path(TOKEN_FILE)) - storage.put(credentials) - do_setup(hass, hass_config, config) - listener() - persistent_notification.create( + DeviceAuth( hass, - ( - f"We are all setup now. Check {YAML_DEVICES} for calendars that have " - f"been found" - ), - title=NOTIFICATION_TITLE, - notification_id=NOTIFICATION_ID, - ) - - listener = track_utc_time_change( - hass, step2_exchange, second=range(0, 60, dev_flow.interval) + conf[CONF_CLIENT_ID], + conf[CONF_CLIENT_SECRET], + ), ) - return True - - -def setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up the Google platform.""" - if DATA_INDEX not in hass.data: - hass.data[DATA_INDEX] = {} - - if not (conf := config.get(DOMAIN, {})): - # component is set up by tts platform - return True - - token_file = hass.config.path(TOKEN_FILE) - if not os.path.isfile(token_file): - _LOGGER.debug("Token file does not exist, authenticating for first time") - do_authentication(hass, config, conf) - else: - if not check_correct_scopes(hass, token_file, conf): - _LOGGER.debug("Existing scopes are not sufficient, re-authenticating") - do_authentication(hass, config, conf) - else: - do_setup(hass, config, conf) - - return True - - -def check_correct_scopes( - hass: HomeAssistant, token_file: str, config: ConfigType -) -> bool: - """Check for the correct scopes in file.""" - creds = Storage(token_file).get() - if not creds or not creds.scopes: - return False - target_scope = config[CONF_CALENDAR_ACCESS].scope - return target_scope in creds.scopes - - -class GoogleCalendarService: - """Calendar service interface to Google.""" - - def __init__(self, token_file: str) -> None: - """Init the Google Calendar service.""" - self.token_file = token_file - - def get(self) -> google_discovery.Resource: - """Get the calendar service from the storage file token.""" - credentials = Storage(self.token_file).get() - http = credentials.authorize(httplib2.Http()) - service = google_discovery.build( - "calendar", "v3", http=http, cache_discovery=False + # Import credentials from the old token file into the new way as + # a ConfigEntry managed by home assistant. + storage = Storage(hass.config.path(TOKEN_FILE)) + creds = await hass.async_add_executor_job(storage.get) + if creds and conf[CONF_CALENDAR_ACCESS].scope in creds.scopes: + _LOGGER.debug("Importing configuration entry with credentials") + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + "creds": creds, + }, + ) ) - return service + return True -def setup_services( +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Google from a config entry.""" + implementation = ( + await config_entry_oauth2_flow.async_get_config_entry_implementation( + hass, entry + ) + ) + assert isinstance(implementation, DeviceAuth) + session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation) + required_scope = hass.data[DOMAIN][DATA_CONFIG][CONF_CALENDAR_ACCESS].scope + if required_scope not in session.token.get("scope", []): + raise ConfigEntryAuthFailed( + "Required scopes are not available, reauth required" + ) + calendar_service = GoogleCalendarService(hass, session) + hass.data[DOMAIN][DATA_SERVICE] = calendar_service + + await async_setup_services(hass, hass.data[DOMAIN][DATA_CONFIG], calendar_service) + + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + + +async def async_setup_services( hass: HomeAssistant, - hass_config: ConfigType, config: ConfigType, - track_new_found_calendars: bool, calendar_service: GoogleCalendarService, ) -> None: """Set up the service listeners.""" - def _found_calendar(call: ServiceCall) -> None: - """Check if we know about a calendar and generate PLATFORM_DISCOVER.""" + created_calendars = set() + calendars = await hass.async_add_executor_job( + load_config, hass.config.path(YAML_DEVICES) + ) + + async def _found_calendar(call: ServiceCall) -> None: calendar = get_calendar_info(hass, call.data) - if hass.data[DATA_INDEX].get(calendar[CONF_CAL_ID]) is not None: + calendar_id = calendar[CONF_CAL_ID] + + if calendar_id in created_calendars: return + created_calendars.add(calendar_id) - hass.data[DATA_INDEX].update({calendar[CONF_CAL_ID]: calendar}) + # Populate the yaml file with all discovered calendars + if calendar_id not in calendars: + calendars[calendar_id] = calendar + await hass.async_add_executor_job( + update_config, hass.config.path(YAML_DEVICES), calendar + ) + else: + # Prefer entity/name information from yaml, overriding api + calendar = calendars[calendar_id] + async_dispatcher_send(hass, DISCOVER_CALENDAR, calendar) - update_config( - hass.config.path(YAML_DEVICES), hass.data[DATA_INDEX][calendar[CONF_CAL_ID]] - ) + hass.services.async_register(DOMAIN, SERVICE_FOUND_CALENDARS, _found_calendar) - discovery.load_platform( - hass, - Platform.CALENDAR, - DOMAIN, - hass.data[DATA_INDEX][calendar[CONF_CAL_ID]], - hass_config, - ) - - hass.services.register(DOMAIN, SERVICE_FOUND_CALENDARS, _found_calendar) - - def _scan_for_calendars(call: ServiceCall) -> None: + async def _scan_for_calendars(call: ServiceCall) -> None: """Scan for new calendars.""" - service = calendar_service.get() - cal_list = service.calendarList() - calendars = cal_list.list().execute()["items"] + try: + calendars = await calendar_service.async_list_calendars() + except ServerNotFoundError as err: + raise HomeAssistantError(str(err)) from err + tasks = [] for calendar in calendars: - calendar["track"] = track_new_found_calendars - hass.services.call(DOMAIN, SERVICE_FOUND_CALENDARS, calendar) + calendar[CONF_TRACK] = config[CONF_TRACK_NEW] + tasks.append( + hass.services.async_call(DOMAIN, SERVICE_FOUND_CALENDARS, calendar) + ) + await asyncio.gather(*tasks) - hass.services.register(DOMAIN, SERVICE_SCAN_CALENDARS, _scan_for_calendars) + hass.services.async_register(DOMAIN, SERVICE_SCAN_CALENDARS, _scan_for_calendars) - def _add_event(call: ServiceCall) -> None: + async def _add_event(call: ServiceCall) -> None: """Add a new event to calendar.""" - service = calendar_service.get() start = {} end = {} @@ -372,44 +292,23 @@ def setup_services( start = {"dateTime": start_dt, "timeZone": str(hass.config.time_zone)} end = {"dateTime": end_dt, "timeZone": str(hass.config.time_zone)} - event = { - "summary": call.data[EVENT_SUMMARY], - "description": call.data[EVENT_DESCRIPTION], - "start": start, - "end": end, - } - service_data = {"calendarId": call.data[EVENT_CALENDAR_ID], "body": event} - event = service.events().insert(**service_data).execute() + await calendar_service.async_create_event( + call.data[EVENT_CALENDAR_ID], + { + "summary": call.data[EVENT_SUMMARY], + "description": call.data[EVENT_DESCRIPTION], + "start": start, + "end": end, + }, + ) # Only expose the add event service if we have the correct permissions if config.get(CONF_CALENDAR_ACCESS) is FeatureAccess.read_write: - hass.services.register( + hass.services.async_register( DOMAIN, SERVICE_ADD_EVENT, _add_event, schema=ADD_EVENT_SERVICE_SCHEMA ) -def do_setup(hass: HomeAssistant, hass_config: ConfigType, config: ConfigType) -> None: - """Run the setup after we have everything configured.""" - _LOGGER.debug("Setting up integration") - # Load calendars the user has configured - hass.data[DATA_INDEX] = load_config(hass.config.path(YAML_DEVICES)) - - calendar_service = GoogleCalendarService(hass.config.path(TOKEN_FILE)) - track_new_found_calendars = convert( - config.get(CONF_TRACK_NEW), bool, DEFAULT_CONF_TRACK_NEW - ) - assert track_new_found_calendars is not None - setup_services( - hass, hass_config, config, track_new_found_calendars, calendar_service - ) - - for calendar in hass.data[DATA_INDEX].values(): - discovery.load_platform(hass, Platform.CALENDAR, DOMAIN, calendar, hass_config) - - # Look for any new calendars - hass.services.call(DOMAIN, SERVICE_SCAN_CALENDARS, None) - - def get_calendar_info( hass: HomeAssistant, calendar: Mapping[str, Any] ) -> dict[str, Any]: @@ -443,7 +342,8 @@ def load_config(path: str) -> dict[str, Any]: except VoluptuousError as exception: # keep going _LOGGER.warning("Calendar Invalid Data: %s", exception) - except FileNotFoundError: + except FileNotFoundError as err: + _LOGGER.debug("Error reading calendar configuration: %s", err) # When YAML file could not be loaded/did not contain a dict return {} @@ -452,6 +352,9 @@ def load_config(path: str) -> dict[str, Any]: def update_config(path: str, calendar: dict[str, Any]) -> None: """Write the google_calendar_devices.yaml.""" - with open(path, "a", encoding="utf8") as out: - out.write("\n") - yaml.dump([calendar], out, default_flow_style=False) + try: + with open(path, "a", encoding="utf8") as out: + out.write("\n") + yaml.dump([calendar], out, default_flow_style=False) + except FileNotFoundError as err: + _LOGGER.debug("Error persisting calendar configuration: %s", err) diff --git a/homeassistant/components/google/api.py b/homeassistant/components/google/api.py new file mode 100644 index 00000000000..ea3d23dcb01 --- /dev/null +++ b/homeassistant/components/google/api.py @@ -0,0 +1,243 @@ +"""Client library for talking to Google APIs.""" + +from __future__ import annotations + +from collections.abc import Awaitable, Callable +import datetime +import logging +from typing import Any + +from googleapiclient import discovery as google_discovery +import oauth2client +from oauth2client.client import ( + Credentials, + DeviceFlowInfo, + FlowExchangeError, + OAuth2Credentials, + OAuth2DeviceCodeError, + OAuth2WebServerFlow, +) + +from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET +from homeassistant.core import CALLBACK_TYPE, HomeAssistant +from homeassistant.helpers import config_entry_oauth2_flow +from homeassistant.helpers.event import async_track_time_interval +from homeassistant.util import dt + +from .const import CONF_CALENDAR_ACCESS, DATA_CONFIG, DEVICE_AUTH_IMPL, DOMAIN + +_LOGGER = logging.getLogger(__name__) + +EVENT_PAGE_SIZE = 100 +EXCHANGE_TIMEOUT_SECONDS = 60 + + +class OAuthError(Exception): + """OAuth related error.""" + + +class DeviceAuth(config_entry_oauth2_flow.LocalOAuth2Implementation): + """OAuth implementation for Device Auth.""" + + def __init__(self, hass: HomeAssistant, client_id: str, client_secret: str) -> None: + """Initialize InstalledAppAuth.""" + super().__init__( + hass, + DEVICE_AUTH_IMPL, + client_id, + client_secret, + oauth2client.GOOGLE_AUTH_URI, + oauth2client.GOOGLE_TOKEN_URI, + ) + + async def async_resolve_external_data(self, external_data: Any) -> dict: + """Resolve a Google API Credentials object to Home Assistant token.""" + creds: Credentials = external_data["creds"] + return { + "access_token": creds.access_token, + "refresh_token": creds.refresh_token, + "scope": " ".join(creds.scopes), + "token_type": "Bearer", + "expires_in": creds.token_expiry.timestamp(), + } + + +class DeviceFlow: + """OAuth2 device flow for exchanging a code for an access token.""" + + def __init__( + self, + hass: HomeAssistant, + oauth_flow: OAuth2WebServerFlow, + device_flow_info: DeviceFlowInfo, + ) -> None: + """Initialize DeviceFlow.""" + self._hass = hass + self._oauth_flow = oauth_flow + self._device_flow_info: DeviceFlowInfo = device_flow_info + self._exchange_task_unsub: CALLBACK_TYPE | None = None + + @property + def verification_url(self) -> str: + """Return the verification url that the user should visit to enter the code.""" + return self._device_flow_info.verification_url + + @property + def user_code(self) -> str: + """Return the code that the user should enter at the verification url.""" + return self._device_flow_info.user_code + + async def start_exchange_task( + self, finished_cb: Callable[[Credentials | None], Awaitable[None]] + ) -> None: + """Start the device auth exchange flow polling. + + The callback is invoked with the valid credentials or with None on timeout. + """ + _LOGGER.debug("Starting exchange flow") + assert not self._exchange_task_unsub + max_timeout = dt.utcnow() + datetime.timedelta(seconds=EXCHANGE_TIMEOUT_SECONDS) + # For some reason, oauth.step1_get_device_and_user_codes() returns a datetime + # object without tzinfo. For the comparison below to work, it needs one. + user_code_expiry = self._device_flow_info.user_code_expiry.replace( + tzinfo=datetime.timezone.utc + ) + expiration_time = min(user_code_expiry, max_timeout) + + def _exchange() -> Credentials: + return self._oauth_flow.step2_exchange( + device_flow_info=self._device_flow_info + ) + + async def _poll_attempt(now: datetime.datetime) -> None: + assert self._exchange_task_unsub + _LOGGER.debug("Attempting OAuth code exchange") + # Note: The callback is invoked with None when the device code has expired + creds: Credentials | None = None + if now < expiration_time: + try: + creds = await self._hass.async_add_executor_job(_exchange) + except FlowExchangeError: + _LOGGER.debug("Token not yet ready; trying again later") + return + self._exchange_task_unsub() + self._exchange_task_unsub = None + await finished_cb(creds) + + self._exchange_task_unsub = async_track_time_interval( + self._hass, + _poll_attempt, + datetime.timedelta(seconds=self._device_flow_info.interval), + ) + + +async def async_create_device_flow(hass: HomeAssistant) -> DeviceFlow: + """Create a new Device flow.""" + conf = hass.data[DOMAIN][DATA_CONFIG] + oauth_flow = OAuth2WebServerFlow( + client_id=conf[CONF_CLIENT_ID], + client_secret=conf[CONF_CLIENT_SECRET], + scope=conf[CONF_CALENDAR_ACCESS].scope, + redirect_uri="", + ) + try: + device_flow_info = await hass.async_add_executor_job( + oauth_flow.step1_get_device_and_user_codes + ) + except OAuth2DeviceCodeError as err: + raise OAuthError(str(err)) from err + return DeviceFlow(hass, oauth_flow, device_flow_info) + + +def _async_google_creds(hass: HomeAssistant, token: dict[str, Any]) -> Credentials: + """Convert a Home Assistant token to a Google API Credentials object.""" + conf = hass.data[DOMAIN][DATA_CONFIG] + return OAuth2Credentials( + access_token=token["access_token"], + client_id=conf[CONF_CLIENT_ID], + client_secret=conf[CONF_CLIENT_SECRET], + refresh_token=token["refresh_token"], + token_expiry=token["expires_at"], + token_uri=oauth2client.GOOGLE_TOKEN_URI, + scopes=[conf[CONF_CALENDAR_ACCESS].scope], + user_agent=None, + ) + + +def _api_time_format(time: datetime.datetime | None) -> str | None: + """Convert a datetime to the api string format.""" + return time.isoformat("T") if time else None + + +class GoogleCalendarService: + """Calendar service interface to Google.""" + + def __init__( + self, hass: HomeAssistant, session: config_entry_oauth2_flow.OAuth2Session + ) -> None: + """Init the Google Calendar service.""" + self._hass = hass + self._session = session + + async def _async_get_service(self) -> google_discovery.Resource: + """Get the calendar service with valid credetnails.""" + await self._session.async_ensure_token_valid() + creds = _async_google_creds(self._hass, self._session.token) + + def _build() -> google_discovery.Resource: + return google_discovery.build( + "calendar", "v3", credentials=creds, cache_discovery=False + ) + + return await self._hass.async_add_executor_job(_build) + + async def async_list_calendars( + self, + ) -> list[dict[str, Any]]: + """Return the list of calendars the user has added to their list.""" + service = await self._async_get_service() + + def _list_calendars() -> list[dict[str, Any]]: + cal_list = service.calendarList() + return cal_list.list().execute()["items"] + + return await self._hass.async_add_executor_job(_list_calendars) + + async def async_create_event( + self, calendar_id: str, event: dict[str, Any] + ) -> dict[str, Any]: + """Return the list of calendars the user has added to their list.""" + service = await self._async_get_service() + + def _create_event() -> dict[str, Any]: + events = service.events() + return events.insert(calendarId=calendar_id, body=event).execute() + + return await self._hass.async_add_executor_job(_create_event) + + async def async_list_events( + self, + calendar_id: str, + start_time: datetime.datetime | None = None, + end_time: datetime.datetime | None = None, + search: str | None = None, + page_token: str | None = None, + ) -> tuple[list[dict[str, Any]], str | None]: + """Return the list of events.""" + service = await self._async_get_service() + + def _list_events() -> tuple[list[dict[str, Any]], str | None]: + events = service.events() + result = events.list( + calendarId=calendar_id, + timeMin=_api_time_format(start_time if start_time else dt.now()), + timeMax=_api_time_format(end_time), + q=search, + maxResults=EVENT_PAGE_SIZE, + pageToken=page_token, + singleEvents=True, # Flattens recurring events + orderBy="startTime", + ).execute() + return (result["items"], result.get("nextPageToken")) + + return await self._hass.async_add_executor_job(_list_events) diff --git a/homeassistant/components/google/calendar.py b/homeassistant/components/google/calendar.py index 90b5bbb3d89..d04d913ae67 100644 --- a/homeassistant/components/google/calendar.py +++ b/homeassistant/components/google/calendar.py @@ -6,31 +6,36 @@ from datetime import datetime, timedelta import logging from typing import Any -from googleapiclient import discovery as google_discovery from httplib2 import ServerNotFoundError from homeassistant.components.calendar import ( ENTITY_ID_FORMAT, CalendarEventDevice, - calculate_offset, + extract_offset, + get_date, is_offset_reached, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_DEVICE_ID, CONF_ENTITIES, CONF_NAME, CONF_OFFSET -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError, PlatformNotReady +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import generate_entity_id from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from homeassistant.util import Throttle, dt +from homeassistant.util import Throttle from . import ( CONF_CAL_ID, CONF_IGNORE_AVAILABILITY, CONF_SEARCH, CONF_TRACK, + DATA_SERVICE, DEFAULT_CONF_OFFSET, - TOKEN_FILE, - GoogleCalendarService, + DOMAIN, + SERVICE_SCAN_CALENDARS, ) +from .api import GoogleCalendarService +from .const import DISCOVER_CALENDAR _LOGGER = logging.getLogger(__name__) @@ -41,21 +46,47 @@ DEFAULT_GOOGLE_SEARCH_PARAMS = { MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15) +# Events have a transparency that determine whether or not they block time on calendar. +# When an event is opaque, it means "Show me as busy" which is the default. Events that +# are not opaque are ignored by default. +TRANSPARENCY = "transparency" +OPAQUE = "opaque" -def setup_platform( - hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - disc_info: DiscoveryInfoType | None = None, + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: - """Set up the calendar platform for event devices.""" - if disc_info is None: - return + """Set up the google calendar platform.""" - if not any(data[CONF_TRACK] for data in disc_info[CONF_ENTITIES]): - return + @callback + def async_discover(discovery_info: dict[str, Any]) -> None: + _async_setup_entities( + hass, + entry, + async_add_entities, + discovery_info, + ) - calendar_service = GoogleCalendarService(hass.config.path(TOKEN_FILE)) + entry.async_on_unload( + async_dispatcher_connect(hass, DISCOVER_CALENDAR, async_discover) + ) + + # Look for any new calendars + try: + await hass.services.async_call(DOMAIN, SERVICE_SCAN_CALENDARS, blocking=True) + except HomeAssistantError as err: + # This can happen if there's a connection error during setup. + raise PlatformNotReady(str(err)) from err + + +@callback +def _async_setup_entities( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, + disc_info: dict[str, Any], +) -> None: + calendar_service = hass.data[DOMAIN][DATA_SERVICE] entities = [] for data in disc_info[CONF_ENTITIES]: if not data[CONF_TRACK]: @@ -68,7 +99,7 @@ def setup_platform( ) entities.append(entity) - add_entities(entities, True) + async_add_entities(entities, True) class GoogleCalendarEventDevice(CalendarEventDevice): @@ -82,12 +113,10 @@ class GoogleCalendarEventDevice(CalendarEventDevice): entity_id: str, ) -> None: """Create the Calendar event device.""" - self.data = GoogleCalendarData( - calendar_service, - calendar_id, - data.get(CONF_SEARCH), - data.get(CONF_IGNORE_AVAILABILITY, False), - ) + self._calendar_service = calendar_service + self._calendar_id = calendar_id + self._search: str | None = data.get(CONF_SEARCH) + self._ignore_availability: bool = data.get(CONF_IGNORE_AVAILABILITY, False) self._event: dict[str, Any] | None = None self._name: str = data[CONF_NAME] self._offset = data.get(CONF_OFFSET, DEFAULT_CONF_OFFSET) @@ -109,121 +138,53 @@ class GoogleCalendarEventDevice(CalendarEventDevice): """Return the name of the entity.""" return self._name - async def async_get_events( - self, hass: HomeAssistant, start_date: datetime, end_date: datetime - ) -> list[dict[str, Any]]: - """Get all events in a specific time frame.""" - return await self.data.async_get_events(hass, start_date, end_date) - - def update(self) -> None: - """Update event data.""" - self.data.update() - event = copy.deepcopy(self.data.event) - if event is None: - self._event = event - return - event = calculate_offset(event, self._offset) - self._offset_reached = is_offset_reached(event) - self._event = event - - -class GoogleCalendarData: - """Class to utilize calendar service object to get next event.""" - - def __init__( - self, - calendar_service: GoogleCalendarService, - calendar_id: str, - search: str | None, - ignore_availability: bool, - ) -> None: - """Set up how we are going to search the google calendar.""" - self.calendar_service = calendar_service - self.calendar_id = calendar_id - self.search = search - self.ignore_availability = ignore_availability - self.event: dict[str, Any] | None = None - - def _prepare_query( - self, - ) -> tuple[google_discovery.Resource | None, dict[str, Any] | None]: - try: - service = self.calendar_service.get() - except ServerNotFoundError as err: - _LOGGER.error("Unable to connect to Google: %s", err) - return None, None - params = dict(DEFAULT_GOOGLE_SEARCH_PARAMS) - params["calendarId"] = self.calendar_id - params["maxResults"] = 100 # Page size - - if self.search: - params["q"] = self.search - - return service, params + def _event_filter(self, event: dict[str, Any]) -> bool: + """Return True if the event is visible.""" + if self._ignore_availability: + return True + return event.get(TRANSPARENCY, OPAQUE) == OPAQUE async def async_get_events( self, hass: HomeAssistant, start_date: datetime, end_date: datetime ) -> list[dict[str, Any]]: """Get all events in a specific time frame.""" - service, params = await hass.async_add_executor_job(self._prepare_query) - if service is None or params is None: - return [] - params["timeMin"] = start_date.isoformat("T") - params["timeMax"] = end_date.isoformat("T") - event_list: list[dict[str, Any]] = [] - events = await hass.async_add_executor_job(service.events) page_token: str | None = None while True: - page_token = await self.async_get_events_page( - hass, events, params, page_token, event_list - ) + try: + items, page_token = await self._calendar_service.async_list_events( + self._calendar_id, + start_time=start_date, + end_time=end_date, + search=self._search, + page_token=page_token, + ) + except ServerNotFoundError as err: + _LOGGER.error("Unable to connect to Google: %s", err) + return [] + + event_list.extend(filter(self._event_filter, items)) if not page_token: break return event_list - async def async_get_events_page( - self, - hass: HomeAssistant, - events: google_discovery.Resource, - params: dict[str, Any], - page_token: str | None, - event_list: list[dict[str, Any]], - ) -> str | None: - """Get a page of events in a specific time frame.""" - params["pageToken"] = page_token - result = await hass.async_add_executor_job(events.list(**params).execute) - - items = result.get("items", []) - for item in items: - if not self.ignore_availability and "transparency" in item: - if item["transparency"] == "opaque": - event_list.append(item) - else: - event_list.append(item) - return result.get("nextPageToken") - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self) -> None: + async def async_update(self) -> None: """Get the latest data.""" - service, params = self._prepare_query() - if service is None or params is None: + try: + items, _ = await self._calendar_service.async_list_events( + self._calendar_id, search=self._search + ) + except ServerNotFoundError as err: + _LOGGER.error("Unable to connect to Google: %s", err) return - params["timeMin"] = dt.now().isoformat("T") - events = service.events() - result = events.list(**params).execute() - - items = result.get("items", []) - - new_event = None - for item in items: - if not self.ignore_availability and "transparency" in item: - if item["transparency"] == "opaque": - new_event = item - break - else: - new_event = item - break - - self.event = new_event + # Pick the first visible event and apply offset calculations. + valid_items = filter(self._event_filter, items) + self._event = copy.deepcopy(next(valid_items, None)) + if self._event: + (summary, offset) = extract_offset(self._event["summary"], self._offset) + self._event["summary"] = summary + self._offset_reached = is_offset_reached( + get_date(self._event["start"]), offset + ) diff --git a/homeassistant/components/google/config_flow.py b/homeassistant/components/google/config_flow.py new file mode 100644 index 00000000000..c70dd83fcae --- /dev/null +++ b/homeassistant/components/google/config_flow.py @@ -0,0 +1,128 @@ +"""Config flow for Google integration.""" +from __future__ import annotations + +import logging +from typing import Any + +from oauth2client.client import Credentials + +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers import config_entry_oauth2_flow + +from .api import DeviceFlow, OAuthError, async_create_device_flow +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +class OAuth2FlowHandler( + config_entry_oauth2_flow.AbstractOAuth2FlowHandler, domain=DOMAIN +): + """Config flow to handle Google Calendars OAuth2 authentication.""" + + DOMAIN = DOMAIN + + def __init__(self) -> None: + """Set up instance.""" + super().__init__() + self._reauth = False + self._device_flow: DeviceFlow | None = None + + @property + def logger(self) -> logging.Logger: + """Return logger.""" + return logging.getLogger(__name__) + + async def async_step_import(self, info: dict[str, Any]) -> FlowResult: + """Import existing auth from Nest.""" + if self._async_current_entries(): + return self.async_abort(reason="single_instance_allowed") + implementations = await config_entry_oauth2_flow.async_get_implementations( + self.hass, self.DOMAIN + ) + assert len(implementations) == 1 + self.flow_impl = list(implementations.values())[0] + self.external_data = info + return await super().async_step_creation(info) + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle external yaml configuration.""" + if not self._reauth and self._async_current_entries(): + return self.async_abort(reason="already_configured") + return await super().async_step_user(user_input) + + async def async_step_auth( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Create an entry for auth.""" + # The default behavior from the parent class is to redirect the + # user with an external step. When using the device flow, we instead + # prompt the user to visit a URL and enter a code. The device flow + # background task will poll the exchange endpoint to get valid + # creds or until a timeout is complete. + if user_input is not None: + return self.async_show_progress_done(next_step_id="creation") + + if not self._device_flow: + _LOGGER.debug("Creating DeviceAuth flow") + try: + device_flow = await async_create_device_flow(self.hass) + except OAuthError as err: + _LOGGER.error("Error initializing device flow: %s", str(err)) + return self.async_abort(reason="oauth_error") + self._device_flow = device_flow + + async def _exchange_finished(creds: Credentials | None) -> None: + self.external_data = {"creds": creds} # is None on timeout/expiration + self.hass.async_create_task( + self.hass.config_entries.flow.async_configure( + flow_id=self.flow_id, user_input={} + ) + ) + + await device_flow.start_exchange_task(_exchange_finished) + + return self.async_show_progress( + step_id="auth", + description_placeholders={ + "url": self._device_flow.verification_url, + "user_code": self._device_flow.user_code, + }, + progress_action="exchange", + ) + + async def async_step_creation( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle external yaml configuration.""" + if self.external_data.get("creds") is None: + return self.async_abort(reason="code_expired") + return await super().async_step_creation(user_input) + + async def async_oauth_create_entry(self, data: dict) -> FlowResult: + """Create an entry for the flow, or update existing entry.""" + existing_entries = self._async_current_entries() + if existing_entries: + assert len(existing_entries) == 1 + entry = existing_entries[0] + self.hass.config_entries.async_update_entry(entry, data=data) + await self.hass.config_entries.async_reload(entry.entry_id) + return self.async_abort(reason="reauth_successful") + return self.async_create_entry(title=self.flow_impl.name, data=data) + + async def async_step_reauth( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Perform reauth upon an API authentication error.""" + self._reauth = True + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Confirm reauth dialog.""" + if user_input is None: + return self.async_show_form(step_id="reauth_confirm") + return await self.async_step_user() diff --git a/homeassistant/components/google/const.py b/homeassistant/components/google/const.py new file mode 100644 index 00000000000..d5cdabb0638 --- /dev/null +++ b/homeassistant/components/google/const.py @@ -0,0 +1,30 @@ +"""Constants for google integration.""" +from __future__ import annotations + +from enum import Enum + +DOMAIN = "google" +DEVICE_AUTH_IMPL = "device_auth" + +CONF_CALENDAR_ACCESS = "calendar_access" +DATA_CALENDARS = "calendars" +DATA_SERVICE = "service" +DATA_CONFIG = "config" + +DISCOVER_CALENDAR = "google_discover_calendar" + + +class FeatureAccess(Enum): + """Class to represent different access scopes.""" + + read_only = "https://www.googleapis.com/auth/calendar.readonly" + read_write = "https://www.googleapis.com/auth/calendar" + + def __init__(self, scope: str) -> None: + """Init instance.""" + self._scope = scope + + @property + def scope(self) -> str: + """Google calendar scope for the feature.""" + return self._scope diff --git a/homeassistant/components/google/manifest.json b/homeassistant/components/google/manifest.json index 1695c7d0d84..589ecb25b21 100644 --- a/homeassistant/components/google/manifest.json +++ b/homeassistant/components/google/manifest.json @@ -1,13 +1,15 @@ { "domain": "google", "name": "Google Calendars", + "config_flow": true, + "dependencies": ["http"], "documentation": "https://www.home-assistant.io/integrations/calendar.google/", "requirements": [ - "google-api-python-client==1.6.4", - "httplib2==0.19.0", + "google-api-python-client==2.38.0", + "httplib2==0.20.4", "oauth2client==4.1.3" ], - "codeowners": [], + "codeowners": ["@allenporter"], "iot_class": "cloud_polling", "loggers": ["googleapiclient"] } diff --git a/homeassistant/components/google/strings.json b/homeassistant/components/google/strings.json new file mode 100644 index 00000000000..14f020f08fd --- /dev/null +++ b/homeassistant/components/google/strings.json @@ -0,0 +1,31 @@ +{ + "config": { + "step": { + "pick_implementation": { + "title": "[%key:common::config_flow::title::oauth2_pick_implementation%]" + }, + "reauth_confirm": { + "title": "[%key:common::config_flow::title::reauth%]", + "description": "The Nest integration needs to re-authenticate your account" + }, + "auth": { + "title": "Link Google Account" + } + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", + "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", + "oauth_error": "[%key:common::config_flow::abort::oauth2_error%]", + "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", + "code_expired": "Authentication code expired or credential setup is invalid, please try again.", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", + "invalid_access_token": "[%key:common::config_flow::error::invalid_access_token%]" + }, + "create_entry": { + "default": "[%key:common::config_flow::create_entry::authenticated%]" + }, + "progress": { + "exchange": "To link your Google account, visit the [{url}]({url}) and enter code:\n\n{user_code}" + } + } +} diff --git a/homeassistant/components/google/translations/en.json b/homeassistant/components/google/translations/en.json new file mode 100644 index 00000000000..51d1ad9aab8 --- /dev/null +++ b/homeassistant/components/google/translations/en.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "Account is already configured", + "already_in_progress": "Configuration flow is already in progress", + "code_expired": "Authentication code expired, please try again.", + "invalid_access_token": "Invalid access token", + "missing_configuration": "The component is not configured. Please follow the documentation.", + "oauth_error": "Received invalid token data.", + "reauth_successful": "Re-authentication was successful" + }, + "create_entry": { + "default": "Successfully authenticated" + }, + "progress": { + "exchange": "To link your Google account, visit the [{url}]({url}) and enter code:\n\n{user_code}" + }, + "step": { + "auth": { + "title": "Link Google Account" + }, + "pick_implementation": { + "title": "Pick Authentication Method" + }, + "reauth_confirm": { + "description": "The Nest integration needs to re-authenticate your account", + "title": "Reauthenticate Integration" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google_assistant/helpers.py b/homeassistant/components/google_assistant/helpers.py index a348299e282..fd6aecd5d42 100644 --- a/homeassistant/components/google_assistant/helpers.py +++ b/homeassistant/components/google_assistant/helpers.py @@ -20,10 +20,7 @@ from homeassistant.const import ( STATE_UNAVAILABLE, ) from homeassistant.core import Context, HomeAssistant, State, callback -from homeassistant.helpers import start -from homeassistant.helpers.area_registry import AreaEntry -from homeassistant.helpers.device_registry import DeviceEntry -from homeassistant.helpers.entity_registry import RegistryEntry +from homeassistant.helpers import area_registry, device_registry, entity_registry, start from homeassistant.helpers.event import async_call_later from homeassistant.helpers.network import get_url from homeassistant.helpers.storage import Store @@ -48,51 +45,33 @@ SYNC_DELAY = 15 _LOGGER = logging.getLogger(__name__) -async def _get_entity_and_device( +@callback +def _get_registry_entries( hass: HomeAssistant, entity_id: str -) -> tuple[RegistryEntry, DeviceEntry] | None: - """Fetch the entity and device entries for a entity_id.""" - dev_reg, ent_reg = await gather( - hass.helpers.device_registry.async_get_registry(), - hass.helpers.entity_registry.async_get_registry(), - ) +) -> tuple[device_registry.DeviceEntry, area_registry.AreaEntry]: + """Get registry entries.""" + ent_reg = entity_registry.async_get(hass) + dev_reg = device_registry.async_get(hass) + area_reg = area_registry.async_get(hass) - if not (entity_entry := ent_reg.async_get(entity_id)): - return None, None - device_entry = dev_reg.devices.get(entity_entry.device_id) - return entity_entry, device_entry + if (entity_entry := ent_reg.async_get(entity_id)) and entity_entry.device_id: + device_entry = dev_reg.devices.get(entity_entry.device_id) + else: + device_entry = None - -async def _get_area( - hass: HomeAssistant, - entity_entry: RegistryEntry | None, - device_entry: DeviceEntry | None, -) -> AreaEntry | None: - """Calculate the area for an entity.""" if entity_entry and entity_entry.area_id: area_id = entity_entry.area_id elif device_entry and device_entry.area_id: area_id = device_entry.area_id else: - return None + area_id = None - area_reg = await hass.helpers.area_registry.async_get_registry() - return area_reg.areas.get(area_id) + if area_id is not None: + area_entry = area_reg.async_get_area(area_id) + else: + area_entry = None - -async def _get_device_info(device_entry: DeviceEntry | None) -> dict[str, str] | None: - """Retrieve the device info for a device.""" - if not device_entry: - return None - - device_info = {} - if device_entry.manufacturer: - device_info["manufacturer"] = device_entry.manufacturer - if device_entry.model: - device_info["model"] = device_entry.model - if device_entry.sw_version: - device_info["swVersion"] = device_entry.sw_version - return device_info + return device_entry, area_entry class AbstractConfig(ABC): @@ -352,7 +331,12 @@ class AbstractConfig(ABC): payload = await request.json() if _LOGGER.isEnabledFor(logging.DEBUG): - _LOGGER.debug("Received local message:\n%s\n", pprint.pformat(payload)) + _LOGGER.debug( + "Received local message from %s (JS %s):\n%s\n", + request.remote, + request.headers.get("HA-Cloud-Version", "unknown"), + pprint.pformat(payload), + ) if not self.enabled: return json_response(smart_home.turned_off_response(payload)) @@ -559,60 +543,71 @@ class GoogleEntity: trait.might_2fa(domain, features, device_class) for trait in self.traits() ) - async def sync_serialize(self, agent_user_id): + def sync_serialize(self, agent_user_id, instance_uuid): """Serialize entity for a SYNC response. https://developers.google.com/actions/smarthome/create-app#actiondevicessync """ state = self.state - + traits = self.traits() entity_config = self.config.entity_config.get(state.entity_id, {}) name = (entity_config.get(CONF_NAME) or state.name).strip() - domain = state.domain - device_class = state.attributes.get(ATTR_DEVICE_CLASS) - entity_entry, device_entry = await _get_entity_and_device( - self.hass, state.entity_id - ) - traits = self.traits() - - device_type = get_google_type(domain, device_class) + # Find entity/device/area registry entries + device_entry, area_entry = _get_registry_entries(self.hass, self.entity_id) + # Build the device info device = { "id": state.entity_id, "name": {"name": name}, "attributes": {}, "traits": [trait.name for trait in traits], "willReportState": self.config.should_report_state, - "type": device_type, + "type": get_google_type( + state.domain, state.attributes.get(ATTR_DEVICE_CLASS) + ), } - # use aliases + # Add aliases if aliases := entity_config.get(CONF_ALIASES): device["name"]["nicknames"] = [name] + aliases + # Add local SDK info if enabled if self.config.is_local_sdk_active and self.should_expose_local(): device["otherDeviceIds"] = [{"deviceId": self.entity_id}] device["customData"] = { "webhookId": self.config.get_local_webhook_id(agent_user_id), "httpPort": self.hass.http.server_port, "httpSSL": self.hass.config.api.use_ssl, - "uuid": await self.hass.helpers.instance_id.async_get(), + "uuid": instance_uuid, "baseUrl": get_url(self.hass, prefer_external=True), "proxyDeviceId": agent_user_id, } + # Add trait sync attributes for trt in traits: device["attributes"].update(trt.sync_attributes()) + # Add roomhint if room := entity_config.get(CONF_ROOM_HINT): device["roomHint"] = room - else: - area = await _get_area(self.hass, entity_entry, device_entry) - if area and area.name: - device["roomHint"] = area.name + elif area_entry and area_entry.name: + device["roomHint"] = area_entry.name - if device_info := await _get_device_info(device_entry): + # Add deviceInfo + if not device_entry: + return device + + device_info = {} + + if device_entry.manufacturer: + device_info["manufacturer"] = device_entry.manufacturer + if device_entry.model: + device_info["model"] = device_entry.model + if device_entry.sw_version: + device_info["swVersion"] = device_entry.sw_version + + if device_info: device["deviceInfo"] = device_info return device diff --git a/homeassistant/components/google_assistant/http.py b/homeassistant/components/google_assistant/http.py index f068dabe47a..3f3db1f2b5b 100644 --- a/homeassistant/components/google_assistant/http.py +++ b/homeassistant/components/google_assistant/http.py @@ -126,7 +126,10 @@ class GoogleConfig(AbstractConfig): entity_registry = er.async_get(self.hass) registry_entry = entity_registry.async_get(state.entity_id) if registry_entry: - auxiliary_entity = registry_entry.entity_category is not None + auxiliary_entity = ( + registry_entry.entity_category is not None + or registry_entry.hidden_by is not None + ) else: auxiliary_entity = False diff --git a/homeassistant/components/google_assistant/smart_home.py b/homeassistant/components/google_assistant/smart_home.py index 5f38194e3e3..805c9100d9f 100644 --- a/homeassistant/components/google_assistant/smart_home.py +++ b/homeassistant/components/google_assistant/smart_home.py @@ -4,6 +4,7 @@ from itertools import product import logging from homeassistant.const import ATTR_ENTITY_ID, __version__ +from homeassistant.helpers import instance_id from homeassistant.util.decorator import Registry from .const import ( @@ -86,22 +87,17 @@ async def async_devices_sync(hass, data, payload): await data.config.async_connect_agent_user(agent_user_id) entities = async_get_entities(hass, data.config) - results = await asyncio.gather( - *( - entity.sync_serialize(agent_user_id) - for entity in entities - if entity.should_expose() - ), - return_exceptions=True, - ) - + instance_uuid = await instance_id.async_get(hass) devices = [] - for entity, result in zip(entities, results): - if isinstance(result, Exception): - _LOGGER.error("Error serializing %s", entity.entity_id, exc_info=result) - else: - devices.append(result) + for entity in entities: + if not entity.should_expose(): + continue + + try: + devices.append(entity.sync_serialize(agent_user_id, instance_uuid)) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Error serializing %s", entity.entity_id) response = {"agentUserId": agent_user_id, "devices": devices} @@ -252,6 +248,7 @@ async def async_devices_disconnect(hass, data: RequestData, payload): https://developers.google.com/assistant/smarthome/develop/process-intents#DISCONNECT """ + assert data.context.user_id is not None await data.config.async_disconnect_agent_user(data.context.user_id) return None diff --git a/homeassistant/components/google_cloud/manifest.json b/homeassistant/components/google_cloud/manifest.json index 90c5eebaeb2..87da1f55fca 100644 --- a/homeassistant/components/google_cloud/manifest.json +++ b/homeassistant/components/google_cloud/manifest.json @@ -2,7 +2,7 @@ "domain": "google_cloud", "name": "Google Cloud Platform", "documentation": "https://www.home-assistant.io/integrations/google_cloud", - "requirements": ["google-cloud-texttospeech==0.4.0"], + "requirements": ["google-cloud-texttospeech==2.11.0"], "codeowners": ["@lufton"], "iot_class": "cloud_push" } diff --git a/homeassistant/components/google_cloud/tts.py b/homeassistant/components/google_cloud/tts.py index 3d65f4eb297..0de580ef7b7 100644 --- a/homeassistant/components/google_cloud/tts.py +++ b/homeassistant/components/google_cloud/tts.py @@ -122,13 +122,9 @@ SUPPORTED_OPTIONS = [ CONF_TEXT_TYPE, ] -GENDER_SCHEMA = vol.All( - vol.Upper, vol.In(texttospeech.enums.SsmlVoiceGender.__members__) -) +GENDER_SCHEMA = vol.All(vol.Upper, vol.In(texttospeech.SsmlVoiceGender.__members__)) VOICE_SCHEMA = cv.matches_regex(VOICE_REGEX) -SCHEMA_ENCODING = vol.All( - vol.Upper, vol.In(texttospeech.enums.AudioEncoding.__members__) -) +SCHEMA_ENCODING = vol.All(vol.Upper, vol.In(texttospeech.AudioEncoding.__members__)) SPEED_SCHEMA = vol.All(vol.Coerce(float), vol.Clamp(min=MIN_SPEED, max=MAX_SPEED)) PITCH_SCHEMA = vol.All(vol.Coerce(float), vol.Clamp(min=MIN_PITCH, max=MAX_PITCH)) GAIN_SCHEMA = vol.All(vol.Coerce(float), vol.Clamp(min=MIN_GAIN, max=MAX_GAIN)) @@ -263,27 +259,32 @@ class GoogleCloudTTSProvider(Provider): try: params = {options[CONF_TEXT_TYPE]: message} - # pylint: disable=no-member - synthesis_input = texttospeech.types.SynthesisInput(**params) + synthesis_input = texttospeech.SynthesisInput(**params) - voice = texttospeech.types.VoiceSelectionParams( + voice = texttospeech.VoiceSelectionParams( language_code=language, - ssml_gender=texttospeech.enums.SsmlVoiceGender[options[CONF_GENDER]], + ssml_gender=texttospeech.SsmlVoiceGender[options[CONF_GENDER]], name=_voice, ) - audio_config = texttospeech.types.AudioConfig( - audio_encoding=texttospeech.enums.AudioEncoding[_encoding], + audio_config = texttospeech.AudioConfig( + audio_encoding=texttospeech.AudioEncoding[_encoding], speaking_rate=options[CONF_SPEED], pitch=options[CONF_PITCH], volume_gain_db=options[CONF_GAIN], effects_profile_id=options[CONF_PROFILES], ) - # pylint: enable=no-member + + request = { + "voice": voice, + "audio_config": audio_config, + "input": synthesis_input, + } async with async_timeout.timeout(10): + assert self.hass response = await self.hass.async_add_executor_job( - self._client.synthesize_speech, synthesis_input, voice, audio_config + self._client.synthesize_speech, request ) return _encoding, response.audio_content diff --git a/homeassistant/components/google_pubsub/__init__.py b/homeassistant/components/google_pubsub/__init__.py index 1de7e98d776..22b7b358dbc 100644 --- a/homeassistant/components/google_pubsub/__init__.py +++ b/homeassistant/components/google_pubsub/__init__.py @@ -6,7 +6,7 @@ import json import logging import os -from google.cloud import pubsub_v1 +from google.cloud.pubsub_v1 import PublisherClient import voluptuous as vol from homeassistant.const import EVENT_STATE_CHANGED, STATE_UNAVAILABLE, STATE_UNKNOWN @@ -52,13 +52,9 @@ def setup(hass: HomeAssistant, yaml_config: ConfigType) -> bool: entities_filter = config[CONF_FILTER] - publisher = pubsub_v1.PublisherClient.from_service_account_json( - service_principal_path - ) + publisher = PublisherClient.from_service_account_json(service_principal_path) - topic_path = publisher.topic_path( # pylint: disable=no-member - project_id, topic_name - ) + topic_path = publisher.topic_path(project_id, topic_name) encoder = DateTimeJSONEncoder() diff --git a/homeassistant/components/google_pubsub/manifest.json b/homeassistant/components/google_pubsub/manifest.json index cd68e17b59d..c6690edb52d 100644 --- a/homeassistant/components/google_pubsub/manifest.json +++ b/homeassistant/components/google_pubsub/manifest.json @@ -2,7 +2,7 @@ "domain": "google_pubsub", "name": "Google Pub/Sub", "documentation": "https://www.home-assistant.io/integrations/google_pubsub", - "requirements": ["google-cloud-pubsub==2.9.0"], + "requirements": ["google-cloud-pubsub==2.11.0"], "codeowners": [], "iot_class": "cloud_push" } diff --git a/homeassistant/components/google_translate/manifest.json b/homeassistant/components/google_translate/manifest.json index 70f5e129950..30c959c4a01 100644 --- a/homeassistant/components/google_translate/manifest.json +++ b/homeassistant/components/google_translate/manifest.json @@ -2,7 +2,7 @@ "domain": "google_translate", "name": "Google Translate Text-to-Speech", "documentation": "https://www.home-assistant.io/integrations/google_translate", - "requirements": ["gTTS==2.2.3"], + "requirements": ["gTTS==2.2.4"], "codeowners": [], "iot_class": "cloud_push", "loggers": ["gtts"] diff --git a/homeassistant/components/google_translate/tts.py b/homeassistant/components/google_translate/tts.py index 9f6ca3a88b8..eb6faf22bbc 100644 --- a/homeassistant/components/google_translate/tts.py +++ b/homeassistant/components/google_translate/tts.py @@ -35,6 +35,7 @@ SUPPORT_LANGUAGES = [ "id", "is", "it", + "iw", "ja", "jw", "km", diff --git a/homeassistant/components/google_travel_time/strings.json b/homeassistant/components/google_travel_time/strings.json index 769c9a4dac7..e0043a4b342 100644 --- a/homeassistant/components/google_travel_time/strings.json +++ b/homeassistant/components/google_travel_time/strings.json @@ -27,7 +27,7 @@ "mode": "Travel Mode", "language": "Language", "time_type": "Time Type", - "time": "Time", + "time": "Time", "avoid": "Avoid", "transit_mode": "Transit Mode", "transit_routing_preference": "Transit Routing Preference", @@ -36,4 +36,4 @@ } } } -} \ No newline at end of file +} diff --git a/homeassistant/components/google_travel_time/translations/zh-Hant.json b/homeassistant/components/google_travel_time/translations/zh-Hant.json index 929810a8564..f8ac86306a2 100644 --- a/homeassistant/components/google_travel_time/translations/zh-Hant.json +++ b/homeassistant/components/google_travel_time/translations/zh-Hant.json @@ -26,7 +26,7 @@ "language": "\u8a9e\u8a00", "mode": "\u65c5\u884c\u6a21\u5f0f", "time": "\u6642\u9593", - "time_type": "\u6642\u9593\u985e\u578b", + "time_type": "\u6642\u9593\u985e\u5225", "transit_mode": "\u79fb\u52d5\u6a21\u5f0f", "transit_routing_preference": "\u504f\u597d\u79fb\u52d5\u8def\u7dda", "units": "\u55ae\u4f4d" diff --git a/homeassistant/components/gpslogger/translations/fr.json b/homeassistant/components/gpslogger/translations/fr.json index a69191d05c6..4e207dddfcd 100644 --- a/homeassistant/components/gpslogger/translations/fr.json +++ b/homeassistant/components/gpslogger/translations/fr.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "cloud_not_connected": "Pas connect\u00e9 \u00e0 Home Assistant Cloud", + "cloud_not_connected": "Non connect\u00e9 \u00e0 Home Assistant Cloud.", "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible.", "webhook_not_internet_accessible": "Votre installation de Home Assistant doit \u00eatre accessible depuis internet pour recevoir des messages webhook." }, diff --git a/homeassistant/components/gree/entity.py b/homeassistant/components/gree/entity.py index 7407a90b4d0..66be66f9dc9 100644 --- a/homeassistant/components/gree/entity.py +++ b/homeassistant/components/gree/entity.py @@ -7,7 +7,7 @@ from .bridge import DeviceDataUpdateCoordinator from .const import DOMAIN -class GreeEntity(CoordinatorEntity): +class GreeEntity(CoordinatorEntity[DeviceDataUpdateCoordinator]): """Generic Gree entity (base class).""" def __init__(self, coordinator: DeviceDataUpdateCoordinator, desc: str) -> None: diff --git a/homeassistant/components/gree/manifest.json b/homeassistant/components/gree/manifest.json index c3b4f1f028a..cd826f3291d 100644 --- a/homeassistant/components/gree/manifest.json +++ b/homeassistant/components/gree/manifest.json @@ -3,7 +3,7 @@ "name": "Gree Climate", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/gree", - "requirements": ["greeclimate==1.0.2"], + "requirements": ["greeclimate==1.1.0"], "codeowners": ["@cmroche"], "iot_class": "local_polling", "loggers": ["greeclimate"] diff --git a/homeassistant/components/gree/strings.json b/homeassistant/components/gree/strings.json index 9f3518bcf8d..ad8f0f41ae7 100644 --- a/homeassistant/components/gree/strings.json +++ b/homeassistant/components/gree/strings.json @@ -10,4 +10,4 @@ "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]" } } -} \ No newline at end of file +} diff --git a/homeassistant/components/gree/translations/fr.json b/homeassistant/components/gree/translations/fr.json index e9ae4e0b644..3d447c0cbb0 100644 --- a/homeassistant/components/gree/translations/fr.json +++ b/homeassistant/components/gree/translations/fr.json @@ -6,7 +6,7 @@ }, "step": { "confirm": { - "description": "Voulez-vous commencer la configuration ?" + "description": "Voulez-vous commencer la configuration\u00a0?" } } } diff --git a/homeassistant/components/greeneye_monitor/manifest.json b/homeassistant/components/greeneye_monitor/manifest.json index 4640d062ac7..4618683f47b 100644 --- a/homeassistant/components/greeneye_monitor/manifest.json +++ b/homeassistant/components/greeneye_monitor/manifest.json @@ -2,14 +2,8 @@ "domain": "greeneye_monitor", "name": "GreenEye Monitor (GEM)", "documentation": "https://www.home-assistant.io/integrations/greeneye_monitor", - "requirements": [ - "greeneye_monitor==3.0.3" - ], - "codeowners": [ - "@jkeljo" - ], + "requirements": ["greeneye_monitor==3.0.3"], + "codeowners": ["@jkeljo"], "iot_class": "local_push", - "loggers": [ - "greeneye" - ] -} \ No newline at end of file + "loggers": ["greeneye"] +} diff --git a/homeassistant/components/group/__init__.py b/homeassistant/components/group/__init__.py index 8e595d75db6..9627ad86734 100644 --- a/homeassistant/components/group/__init__.py +++ b/homeassistant/components/group/__init__.py @@ -11,6 +11,7 @@ from typing import Any, Union, cast import voluptuous as vol from homeassistant import core as ha +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ASSUMED_STATE, ATTR_ENTITY_ID, @@ -27,8 +28,7 @@ from homeassistant.const import ( Platform, ) from homeassistant.core import HomeAssistant, ServiceCall, callback, split_entity_id -from homeassistant.helpers import start -import homeassistant.helpers.config_validation as cv +from homeassistant.helpers import config_validation as cv, entity_registry as er, start from homeassistant.helpers.entity import Entity, async_generate_entity_id from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.event import async_track_state_change_event @@ -39,6 +39,8 @@ from homeassistant.helpers.reload import async_reload_integration_platforms from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass +from .const import CONF_HIDE_MEMBERS + # mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs DOMAIN = "group" @@ -63,8 +65,10 @@ PLATFORMS = [ Platform.COVER, Platform.FAN, Platform.LIGHT, + Platform.LOCK, Platform.MEDIA_PLAYER, Platform.NOTIFY, + Platform.SWITCH, ] REG_KEY = f"{DOMAIN}_registry" @@ -218,6 +222,44 @@ def groups_with_entity(hass: HomeAssistant, entity_id: str) -> list[str]: return groups +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up a config entry.""" + hass.config_entries.async_setup_platforms(entry, (entry.options["group_type"],)) + entry.async_on_unload(entry.add_update_listener(config_entry_update_listener)) + return True + + +async def config_entry_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Update listener, called when the config entry options are changed.""" + await hass.config_entries.async_reload(entry.entry_id) + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + return await hass.config_entries.async_unload_platforms( + entry, (entry.options["group_type"],) + ) + + +async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Remove a config entry.""" + # Unhide the group members + registry = er.async_get(hass) + + if not entry.options[CONF_HIDE_MEMBERS]: + return + + for member in entry.options[CONF_ENTITIES]: + if not (entity_id := er.async_resolve_entity_id(registry, member)): + continue + if (entity_entry := registry.async_get(entity_id)) is None: + continue + if entity_entry.hidden_by != er.RegistryEntryHider.INTEGRATION: + continue + + registry.async_update_entity(entity_id, hidden_by=None) + + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up all groups found defined in the configuration.""" if DOMAIN not in hass.data: @@ -412,7 +454,7 @@ class GroupEntity(Entity): self.async_update_group_state() self.async_write_ha_state() - start.async_at_start(self.hass, _update_at_start) + self.async_on_remove(start.async_at_start(self.hass, _update_at_start)) @callback def async_defer_or_update_ha_state(self) -> None: @@ -647,7 +689,7 @@ class Group(Entity): async def async_added_to_hass(self): """Handle addition to Home Assistant.""" - start.async_at_start(self.hass, self._async_start) + self.async_on_remove(start.async_at_start(self.hass, self._async_start)) async def async_will_remove_from_hass(self): """Handle removal from Home Assistant.""" diff --git a/homeassistant/components/group/binary_sensor.py b/homeassistant/components/group/binary_sensor.py index de0c3d393ca..54a98a68e43 100644 --- a/homeassistant/components/group/binary_sensor.py +++ b/homeassistant/components/group/binary_sensor.py @@ -9,6 +9,7 @@ from homeassistant.components.binary_sensor import ( PLATFORM_SCHEMA, BinarySensorEntity, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ENTITY_ID, CONF_DEVICE_CLASS, @@ -20,7 +21,7 @@ from homeassistant.const import ( STATE_UNKNOWN, ) from homeassistant.core import Event, HomeAssistant, callback -import homeassistant.helpers.config_validation as cv +from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -49,7 +50,7 @@ async def async_setup_platform( async_add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: - """Set up the Group Binary Sensor platform.""" + """Set up the Binary Sensor Group platform.""" async_add_entities( [ BinarySensorGroup( @@ -63,6 +64,27 @@ async def async_setup_platform( ) +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Initialize Binary Sensor Group config entry.""" + registry = er.async_get(hass) + entities = er.async_validate_entity_ids( + registry, config_entry.options[CONF_ENTITIES] + ) + mode = config_entry.options[CONF_ALL] + + async_add_entities( + [ + BinarySensorGroup( + config_entry.entry_id, config_entry.title, None, entities, mode + ) + ] + ) + + class BinarySensorGroup(GroupEntity, BinarySensorEntity): """Representation of a BinarySensorGroup.""" diff --git a/homeassistant/components/group/config_flow.py b/homeassistant/components/group/config_flow.py new file mode 100644 index 00000000000..8ddee492834 --- /dev/null +++ b/homeassistant/components/group/config_flow.py @@ -0,0 +1,205 @@ +"""Config flow for Group integration.""" +from __future__ import annotations + +from collections.abc import Callable, Mapping +from functools import partial +from typing import Any, cast + +import voluptuous as vol + +from homeassistant.const import CONF_ENTITIES +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import entity_registry as er, selector +from homeassistant.helpers.schema_config_entry_flow import ( + SchemaConfigFlowHandler, + SchemaFlowFormStep, + SchemaFlowMenuStep, + SchemaOptionsFlowHandler, + entity_selector_without_own_entities, +) + +from . import DOMAIN +from .binary_sensor import CONF_ALL +from .const import CONF_HIDE_MEMBERS + + +def basic_group_options_schema( + domain: str, + handler: SchemaConfigFlowHandler | SchemaOptionsFlowHandler, + options: dict[str, Any], +) -> vol.Schema: + """Generate options schema.""" + handler = cast(SchemaOptionsFlowHandler, handler) + return vol.Schema( + { + vol.Required(CONF_ENTITIES): entity_selector_without_own_entities( + handler, {"domain": domain, "multiple": True} + ), + vol.Required(CONF_HIDE_MEMBERS, default=False): selector.selector( + {"boolean": {}} + ), + } + ) + + +def basic_group_config_schema(domain: str) -> vol.Schema: + """Generate config schema.""" + return vol.Schema( + { + vol.Required("name"): selector.selector({"text": {}}), + vol.Required(CONF_ENTITIES): selector.selector( + {"entity": {"domain": domain, "multiple": True}} + ), + vol.Required(CONF_HIDE_MEMBERS, default=False): selector.selector( + {"boolean": {}} + ), + } + ) + + +def binary_sensor_options_schema( + handler: SchemaConfigFlowHandler | SchemaOptionsFlowHandler, + options: dict[str, Any], +) -> vol.Schema: + """Generate options schema.""" + return basic_group_options_schema("binary_sensor", handler, options).extend( + { + vol.Required(CONF_ALL, default=False): selector.selector({"boolean": {}}), + } + ) + + +BINARY_SENSOR_CONFIG_SCHEMA = basic_group_config_schema("binary_sensor").extend( + { + vol.Required(CONF_ALL, default=False): selector.selector({"boolean": {}}), + } +) + + +def light_switch_options_schema( + domain: str, + handler: SchemaConfigFlowHandler | SchemaOptionsFlowHandler, + options: dict[str, Any], +) -> vol.Schema: + """Generate options schema.""" + return basic_group_options_schema(domain, handler, options).extend( + { + vol.Required( + CONF_ALL, default=False, description={"advanced": True} + ): selector.selector({"boolean": {}}), + } + ) + + +GROUP_TYPES = [ + "binary_sensor", + "cover", + "fan", + "light", + "lock", + "media_player", + "switch", +] + + +@callback +def choose_options_step(options: dict[str, Any]) -> str: + """Return next step_id for options flow according to group_type.""" + return cast(str, options["group_type"]) + + +def set_group_type(group_type: str) -> Callable[[dict[str, Any]], dict[str, Any]]: + """Set group type.""" + + @callback + def _set_group_type(user_input: dict[str, Any]) -> dict[str, Any]: + """Add group type to user input.""" + return {"group_type": group_type, **user_input} + + return _set_group_type + + +CONFIG_FLOW: dict[str, SchemaFlowFormStep | SchemaFlowMenuStep] = { + "user": SchemaFlowMenuStep(GROUP_TYPES), + "binary_sensor": SchemaFlowFormStep( + BINARY_SENSOR_CONFIG_SCHEMA, set_group_type("binary_sensor") + ), + "cover": SchemaFlowFormStep( + basic_group_config_schema("cover"), set_group_type("cover") + ), + "fan": SchemaFlowFormStep(basic_group_config_schema("fan"), set_group_type("fan")), + "light": SchemaFlowFormStep( + basic_group_config_schema("light"), set_group_type("light") + ), + "lock": SchemaFlowFormStep( + basic_group_config_schema("lock"), set_group_type("lock") + ), + "media_player": SchemaFlowFormStep( + basic_group_config_schema("media_player"), set_group_type("media_player") + ), + "switch": SchemaFlowFormStep( + basic_group_config_schema("switch"), set_group_type("switch") + ), +} + + +OPTIONS_FLOW: dict[str, SchemaFlowFormStep | SchemaFlowMenuStep] = { + "init": SchemaFlowFormStep(None, next_step=choose_options_step), + "binary_sensor": SchemaFlowFormStep(binary_sensor_options_schema), + "cover": SchemaFlowFormStep(partial(basic_group_options_schema, "cover")), + "fan": SchemaFlowFormStep(partial(basic_group_options_schema, "fan")), + "light": SchemaFlowFormStep(partial(light_switch_options_schema, "light")), + "lock": SchemaFlowFormStep(partial(basic_group_options_schema, "lock")), + "media_player": SchemaFlowFormStep( + partial(basic_group_options_schema, "media_player") + ), + "switch": SchemaFlowFormStep(partial(light_switch_options_schema, "switch")), +} + + +class GroupConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN): + """Handle a config or options flow for groups.""" + + config_flow = CONFIG_FLOW + options_flow = OPTIONS_FLOW + + @callback + def async_config_entry_title(self, options: Mapping[str, Any]) -> str: + """Return config entry title. + + The options parameter contains config entry options, which is the union of user + input from the config flow steps. + """ + return cast(str, options["name"]) if "name" in options else "" + + @callback + def async_config_flow_finished(self, options: Mapping[str, Any]) -> None: + """Hide the group members if requested.""" + if options[CONF_HIDE_MEMBERS]: + _async_hide_members( + self.hass, options[CONF_ENTITIES], er.RegistryEntryHider.INTEGRATION + ) + + @callback + @staticmethod + def async_options_flow_finished( + hass: HomeAssistant, options: Mapping[str, Any] + ) -> None: + """Hide or unhide the group members as requested.""" + hidden_by = ( + er.RegistryEntryHider.INTEGRATION if options[CONF_HIDE_MEMBERS] else None + ) + _async_hide_members(hass, options[CONF_ENTITIES], hidden_by) + + +def _async_hide_members( + hass: HomeAssistant, members: list[str], hidden_by: er.RegistryEntryHider | None +) -> None: + """Hide or unhide group members.""" + registry = er.async_get(hass) + for member in members: + if not (entity_id := er.async_resolve_entity_id(registry, member)): + continue + if entity_id not in registry.entities: + continue + registry.async_update_entity(entity_id, hidden_by=hidden_by) diff --git a/homeassistant/components/group/const.py b/homeassistant/components/group/const.py new file mode 100644 index 00000000000..82817e71add --- /dev/null +++ b/homeassistant/components/group/const.py @@ -0,0 +1,3 @@ +"""Constants for the Group integration.""" + +CONF_HIDE_MEMBERS = "hide_members" diff --git a/homeassistant/components/group/cover.py b/homeassistant/components/group/cover.py index a4c550b8119..c2d263ab8ad 100644 --- a/homeassistant/components/group/cover.py +++ b/homeassistant/components/group/cover.py @@ -22,6 +22,7 @@ from homeassistant.components.cover import ( SUPPORT_STOP_TILT, CoverEntity, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ASSUMED_STATE, ATTR_ENTITY_ID, @@ -43,7 +44,7 @@ from homeassistant.const import ( STATE_OPENING, ) from homeassistant.core import Event, HomeAssistant, State, callback -import homeassistant.helpers.config_validation as cv +from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -75,7 +76,7 @@ async def async_setup_platform( async_add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: - """Set up the Group Cover platform.""" + """Set up the Cover Group platform.""" async_add_entities( [ CoverGroup( @@ -85,6 +86,22 @@ async def async_setup_platform( ) +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Initialize Cover Group config entry.""" + registry = er.async_get(hass) + entities = er.async_validate_entity_ids( + registry, config_entry.options[CONF_ENTITIES] + ) + + async_add_entities( + [CoverGroup(config_entry.entry_id, config_entry.title, entities)] + ) + + class CoverGroup(GroupEntity, CoverEntity): """Representation of a CoverGroup.""" diff --git a/homeassistant/components/group/fan.py b/homeassistant/components/group/fan.py index 7920e0f5d20..0f39e9de974 100644 --- a/homeassistant/components/group/fan.py +++ b/homeassistant/components/group/fan.py @@ -25,6 +25,7 @@ from homeassistant.components.fan import ( SUPPORT_SET_SPEED, FanEntity, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ASSUMED_STATE, ATTR_ENTITY_ID, @@ -35,7 +36,7 @@ from homeassistant.const import ( STATE_ON, ) from homeassistant.core import Event, HomeAssistant, State, callback -import homeassistant.helpers.config_validation as cv +from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -72,12 +73,26 @@ async def async_setup_platform( async_add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: - """Set up the Group Cover platform.""" + """Set up the Fan Group platform.""" async_add_entities( [FanGroup(config.get(CONF_UNIQUE_ID), config[CONF_NAME], config[CONF_ENTITIES])] ) +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Initialize Fan Group config entry.""" + registry = er.async_get(hass) + entities = er.async_validate_entity_ids( + registry, config_entry.options[CONF_ENTITIES] + ) + + async_add_entities([FanGroup(config_entry.entry_id, config_entry.title, entities)]) + + class FanGroup(GroupEntity, FanEntity): """Representation of a FanGroup.""" @@ -193,7 +208,6 @@ class FanGroup(GroupEntity, FanEntity): async def async_turn_on( self, - speed: str | None = None, percentage: int | None = None, preset_mode: str | None = None, **kwargs: Any, diff --git a/homeassistant/components/group/light.py b/homeassistant/components/group/light.py index ea74136b204..aa9e6ad0b53 100644 --- a/homeassistant/components/group/light.py +++ b/homeassistant/components/group/light.py @@ -36,6 +36,7 @@ from homeassistant.components.light import ( SUPPORT_WHITE_VALUE, LightEntity, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, @@ -46,9 +47,10 @@ from homeassistant.const import ( SERVICE_TURN_ON, STATE_ON, STATE_UNAVAILABLE, + STATE_UNKNOWN, ) from homeassistant.core import Event, HomeAssistant, State, callback -import homeassistant.helpers.config_validation as cv +from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -57,6 +59,7 @@ from . import GroupEntity from .util import find_state_attributes, mean_tuple, reduce_attribute DEFAULT_NAME = "Light Group" +CONF_ALL = "all" # No limit on parallel updates to enable a group calling another group PARALLEL_UPDATES = 0 @@ -66,6 +69,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_UNIQUE_ID): cv.string, vol.Required(CONF_ENTITIES): cv.entities_domain(light.DOMAIN), + vol.Optional(CONF_ALL): cv.boolean, } ) @@ -86,12 +90,32 @@ async def async_setup_platform( async_add_entities( [ LightGroup( - config.get(CONF_UNIQUE_ID), config[CONF_NAME], config[CONF_ENTITIES] + config.get(CONF_UNIQUE_ID), + config[CONF_NAME], + config[CONF_ENTITIES], + config.get(CONF_ALL), ) ] ) +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Initialize Light Group config entry.""" + registry = er.async_get(hass) + entities = er.async_validate_entity_ids( + registry, config_entry.options[CONF_ENTITIES] + ) + mode = config_entry.options.get(CONF_ALL, False) + + async_add_entities( + [LightGroup(config_entry.entry_id, config_entry.title, entities, mode)] + ) + + FORWARDED_ATTRIBUTES = frozenset( { ATTR_BRIGHTNESS, @@ -115,12 +139,13 @@ class LightGroup(GroupEntity, LightEntity): _attr_available = False _attr_icon = "mdi:lightbulb-group" - _attr_is_on = False _attr_max_mireds = 500 _attr_min_mireds = 154 _attr_should_poll = False - def __init__(self, unique_id: str | None, name: str, entity_ids: list[str]) -> None: + def __init__( + self, unique_id: str | None, name: str, entity_ids: list[str], mode: str | None + ) -> None: """Initialize a light group.""" self._entity_ids = entity_ids self._white_value: int | None = None @@ -128,6 +153,9 @@ class LightGroup(GroupEntity, LightEntity): self._attr_name = name self._attr_extra_state_attributes = {ATTR_ENTITY_ID: entity_ids} self._attr_unique_id = unique_id + self.mode = any + if mode: + self.mode = all async def async_added_to_hass(self) -> None: """Register callbacks.""" @@ -190,7 +218,22 @@ class LightGroup(GroupEntity, LightEntity): states: list[State] = list(filter(None, all_states)) on_states = [state for state in states if state.state == STATE_ON] - self._attr_is_on = len(on_states) > 0 + # filtered_states are members currently in the state machine + filtered_states: list[str] = [x.state for x in all_states if x is not None] + + valid_state = self.mode( + state not in (STATE_UNKNOWN, STATE_UNAVAILABLE) for state in filtered_states + ) + + if not valid_state: + # Set as unknown if any / all member is unknown or unavailable + self._attr_is_on = None + else: + # Set as ON if any / all member is ON + self._attr_is_on = self.mode( + list(map(lambda x: x == STATE_ON, filtered_states)) + ) + self._attr_available = any(state.state != STATE_UNAVAILABLE for state in states) self._attr_brightness = reduce_attribute(on_states, ATTR_BRIGHTNESS) diff --git a/homeassistant/components/group/lock.py b/homeassistant/components/group/lock.py new file mode 100644 index 00000000000..fe9503137c6 --- /dev/null +++ b/homeassistant/components/group/lock.py @@ -0,0 +1,186 @@ +"""This platform allows several locks to be grouped into one lock.""" +from __future__ import annotations + +import logging +from typing import Any + +import voluptuous as vol + +from homeassistant.components.lock import DOMAIN, PLATFORM_SCHEMA, LockEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + ATTR_ENTITY_ID, + CONF_ENTITIES, + CONF_NAME, + CONF_UNIQUE_ID, + SERVICE_LOCK, + SERVICE_OPEN, + SERVICE_UNLOCK, + STATE_JAMMED, + STATE_LOCKED, + STATE_LOCKING, + STATE_UNAVAILABLE, + STATE_UNKNOWN, + STATE_UNLOCKING, +) +from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.helpers import config_validation as cv, entity_registry as er +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.event import async_track_state_change_event +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType + +from . import GroupEntity + +DEFAULT_NAME = "Lock Group" + +# No limit on parallel updates to enable a group calling another group +PARALLEL_UPDATES = 0 + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_ENTITIES): cv.entities_domain(DOMAIN), + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_UNIQUE_ID): cv.string, + } +) + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_platform( + hass: HomeAssistant, + config: ConfigType, + async_add_entities: AddEntitiesCallback, + discovery_info: DiscoveryInfoType | None = None, +) -> None: + """Set up the Lock Group platform.""" + async_add_entities( + [ + LockGroup( + config.get(CONF_UNIQUE_ID), + config[CONF_NAME], + config[CONF_ENTITIES], + ) + ] + ) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Initialize Lock Group config entry.""" + registry = er.async_get(hass) + entities = er.async_validate_entity_ids( + registry, config_entry.options[CONF_ENTITIES] + ) + async_add_entities( + [ + LockGroup( + config_entry.entry_id, + config_entry.title, + entities, + ) + ] + ) + + +class LockGroup(GroupEntity, LockEntity): + """Representation of a lock group.""" + + _attr_available = False + _attr_should_poll = False + + def __init__( + self, + unique_id: str | None, + name: str, + entity_ids: list[str], + ) -> None: + """Initialize a lock group.""" + self._entity_ids = entity_ids + + self._attr_name = name + self._attr_extra_state_attributes = {ATTR_ENTITY_ID: entity_ids} + self._attr_unique_id = unique_id + + async def async_added_to_hass(self) -> None: + """Register callbacks.""" + + @callback + def async_state_changed_listener(event: Event) -> None: + """Handle child updates.""" + self.async_set_context(event.context) + self.async_defer_or_update_ha_state() + + self.async_on_remove( + async_track_state_change_event( + self.hass, self._entity_ids, async_state_changed_listener + ) + ) + + await super().async_added_to_hass() + + async def async_lock(self, **kwargs: Any) -> None: + """Forward the lock command to all locks in the group.""" + data = {ATTR_ENTITY_ID: self._entity_ids} + _LOGGER.debug("Forwarded lock command: %s", data) + + await self.hass.services.async_call( + DOMAIN, + SERVICE_LOCK, + data, + blocking=True, + context=self._context, + ) + + async def async_unlock(self, **kwargs: Any) -> None: + """Forward the unlock command to all locks in the group.""" + data = {ATTR_ENTITY_ID: self._entity_ids} + await self.hass.services.async_call( + DOMAIN, + SERVICE_UNLOCK, + data, + blocking=True, + context=self._context, + ) + + async def async_open(self, **kwargs: Any) -> None: + """Forward the open command to all locks in the group.""" + data = {ATTR_ENTITY_ID: self._entity_ids} + await self.hass.services.async_call( + DOMAIN, + SERVICE_OPEN, + data, + blocking=True, + context=self._context, + ) + + @callback + def async_update_group_state(self) -> None: + """Query all members and determine the lock group state.""" + states = [ + state.state + for entity_id in self._entity_ids + if (state := self.hass.states.get(entity_id)) is not None + ] + + valid_state = all( + state not in (STATE_UNKNOWN, STATE_UNAVAILABLE) for state in states + ) + + if not valid_state: + # Set as unknown if any member is unknown or unavailable + self._attr_is_jammed = None + self._attr_is_locking = None + self._attr_is_unlocking = None + self._attr_is_locked = None + else: + # Set attributes based on member states and let the lock entity sort out the correct state + self._attr_is_jammed = STATE_JAMMED in states + self._attr_is_locking = STATE_LOCKING in states + self._attr_is_unlocking = STATE_UNLOCKING in states + self._attr_is_locked = all(state == STATE_LOCKED for state in states) + + self._attr_available = any(state != STATE_UNAVAILABLE for state in states) diff --git a/homeassistant/components/group/manifest.json b/homeassistant/components/group/manifest.json index 6d8fd446c27..1894434ece2 100644 --- a/homeassistant/components/group/manifest.json +++ b/homeassistant/components/group/manifest.json @@ -1,8 +1,10 @@ { "domain": "group", + "integration_type": "helper", "name": "Group", "documentation": "https://www.home-assistant.io/integrations/group", "codeowners": ["@home-assistant/core"], "quality_scale": "internal", - "iot_class": "calculated" + "iot_class": "calculated", + "config_flow": true } diff --git a/homeassistant/components/group/media_player.py b/homeassistant/components/group/media_player.py index 509c0cb4083..97d8f51536c 100644 --- a/homeassistant/components/group/media_player.py +++ b/homeassistant/components/group/media_player.py @@ -32,6 +32,7 @@ from homeassistant.components.media_player import ( SUPPORT_VOLUME_STEP, MediaPlayerEntity, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, @@ -55,7 +56,7 @@ from homeassistant.const import ( STATE_UNKNOWN, ) from homeassistant.core import HomeAssistant, State, callback -import homeassistant.helpers.config_validation as cv +from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, EventType @@ -86,17 +87,33 @@ async def async_setup_platform( async_add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: - """Set up the Media Group platform.""" + """Set up the MediaPlayer Group platform.""" async_add_entities( [ - MediaGroup( + MediaPlayerGroup( config.get(CONF_UNIQUE_ID), config[CONF_NAME], config[CONF_ENTITIES] ) ] ) -class MediaGroup(MediaPlayerEntity): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Initialize MediaPlayer Group config entry.""" + registry = er.async_get(hass) + entities = er.async_validate_entity_ids( + registry, config_entry.options[CONF_ENTITIES] + ) + + async_add_entities( + [MediaPlayerGroup(config_entry.entry_id, config_entry.title, entities)] + ) + + +class MediaPlayerGroup(MediaPlayerEntity): """Representation of a Media Group.""" def __init__(self, unique_id: str | None, name: str, entities: list[str]) -> None: diff --git a/homeassistant/components/group/services.yaml b/homeassistant/components/group/services.yaml index 3e7f1eb203d..cba11b1723d 100644 --- a/homeassistant/components/group/services.yaml +++ b/homeassistant/components/group/services.yaml @@ -25,7 +25,7 @@ set: description: Name of icon for the group. example: "mdi:camera" selector: - text: + icon: entities: name: Entities description: List of all members in the group. Not compatible with 'delta'. diff --git a/homeassistant/components/group/strings.json b/homeassistant/components/group/strings.json index e29407bf932..26494255996 100644 --- a/homeassistant/components/group/strings.json +++ b/homeassistant/components/group/strings.json @@ -1,5 +1,132 @@ { "title": "Group", + "config": { + "step": { + "user": { + "title": "Add Group", + "description": "Groups allow you to create a new entity that represents multiple entities of the same type.", + "menu_options": { + "binary_sensor": "Binary sensor group", + "cover": "Cover group", + "fan": "Fan group", + "light": "Light group", + "lock": "Lock group", + "media_player": "Media player group", + "switch": "Switch group" + } + }, + "binary_sensor": { + "title": "[%key:component::group::config::step::user::title%]", + "description": "If \"all entities\" is enabled, the group's state is on only if all members are on. If \"all entities\" is disabled, the group's state is on if any member is on.", + "data": { + "all": "All entities", + "entities": "Members", + "hide_members": "Hide members", + "name": "Name" + } + }, + "cover": { + "title": "[%key:component::group::config::step::user::title%]", + "data": { + "entities": "[%key:component::group::config::step::binary_sensor::data::entities%]", + "hide_members": "[%key:component::group::config::step::binary_sensor::data::hide_members%]", + "name": "[%key:component::group::config::step::binary_sensor::data::name%]" + } + }, + "fan": { + "title": "[%key:component::group::config::step::user::title%]", + "data": { + "entities": "[%key:component::group::config::step::binary_sensor::data::entities%]", + "hide_members": "[%key:component::group::config::step::binary_sensor::data::hide_members%]", + "name": "[%key:component::group::config::step::binary_sensor::data::name%]" + } + }, + "light": { + "title": "[%key:component::group::config::step::user::title%]", + "data": { + "entities": "[%key:component::group::config::step::binary_sensor::data::entities%]", + "hide_members": "[%key:component::group::config::step::binary_sensor::data::hide_members%]", + "name": "[%key:component::group::config::step::binary_sensor::data::name%]" + } + }, + "lock": { + "title": "[%key:component::group::config::step::user::title%]", + "data": { + "entities": "[%key:component::group::config::step::binary_sensor::data::entities%]", + "hide_members": "[%key:component::group::config::step::binary_sensor::data::hide_members%]", + "name": "[%key:component::group::config::step::binary_sensor::data::name%]" + } + }, + "media_player": { + "title": "[%key:component::group::config::step::user::title%]", + "data": { + "entities": "[%key:component::group::config::step::binary_sensor::data::entities%]", + "hide_members": "[%key:component::group::config::step::binary_sensor::data::hide_members%]", + "name": "[%key:component::group::config::step::binary_sensor::data::name%]" + } + }, + "switch": { + "title": "[%key:component::group::config::step::user::title%]", + "data": { + "entities": "[%key:component::group::config::step::binary_sensor::data::entities%]", + "hide_members": "[%key:component::group::config::step::binary_sensor::data::hide_members%]", + "name": "[%key:component::group::config::step::binary_sensor::data::name%]" + } + } + } + }, + "options": { + "step": { + "binary_sensor": { + "description": "[%key:component::group::config::step::binary_sensor::description%]", + "data": { + "all": "[%key:component::group::config::step::binary_sensor::data::all%]", + "entities": "[%key:component::group::config::step::binary_sensor::data::entities%]", + "hide_members": "[%key:component::group::config::step::binary_sensor::data::hide_members%]" + } + }, + "cover": { + "data": { + "entities": "[%key:component::group::config::step::binary_sensor::data::entities%]", + "hide_members": "[%key:component::group::config::step::binary_sensor::data::hide_members%]" + } + }, + "fan": { + "data": { + "entities": "[%key:component::group::config::step::binary_sensor::data::entities%]", + "hide_members": "[%key:component::group::config::step::binary_sensor::data::hide_members%]" + } + }, + "light": { + "description": "[%key:component::group::config::step::binary_sensor::description%]", + "data": { + "all": "[%key:component::group::config::step::binary_sensor::data::all%]", + "entities": "[%key:component::group::config::step::binary_sensor::data::entities%]", + "hide_members": "[%key:component::group::config::step::binary_sensor::data::hide_members%]" + } + }, + "lock": { + "data": { + "entities": "[%key:component::group::config::step::binary_sensor::data::entities%]", + "hide_members": "[%key:component::group::config::step::binary_sensor::data::hide_members%]" + } + }, + "media_player": { + "data": { + "entities": "[%key:component::group::config::step::binary_sensor::data::entities%]", + "hide_members": "[%key:component::group::config::step::binary_sensor::data::hide_members%]" + } + }, + "switch": { + "description": "[%key:component::group::config::step::binary_sensor::description%]", + "data": { + "all": "[%key:component::group::config::step::binary_sensor::data::all%]", + "entities": "[%key:component::group::config::step::binary_sensor::data::entities%]", + "hide_members": "[%key:component::group::config::step::binary_sensor::data::hide_members%]" + } + } + } + }, "state": { "_": { "off": "[%key:common::state::off%]", diff --git a/homeassistant/components/group/switch.py b/homeassistant/components/group/switch.py new file mode 100644 index 00000000000..6b879e55cea --- /dev/null +++ b/homeassistant/components/group/switch.py @@ -0,0 +1,173 @@ +"""This platform allows several switches to be grouped into one switch.""" +from __future__ import annotations + +import logging +from typing import Any + +import voluptuous as vol + +from homeassistant.components.switch import DOMAIN, PLATFORM_SCHEMA, SwitchEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + ATTR_ENTITY_ID, + CONF_ENTITIES, + CONF_NAME, + CONF_UNIQUE_ID, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_ON, + STATE_UNAVAILABLE, + STATE_UNKNOWN, +) +from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.helpers import config_validation as cv, entity_registry as er +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.event import async_track_state_change_event +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType + +from . import GroupEntity + +DEFAULT_NAME = "Switch Group" +CONF_ALL = "all" + +# No limit on parallel updates to enable a group calling another group +PARALLEL_UPDATES = 0 + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_ENTITIES): cv.entities_domain(DOMAIN), + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_UNIQUE_ID): cv.string, + vol.Optional(CONF_ALL, default=False): cv.boolean, + } +) + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_platform( + hass: HomeAssistant, + config: ConfigType, + async_add_entities: AddEntitiesCallback, + discovery_info: DiscoveryInfoType | None = None, +) -> None: + """Set up the Switch Group platform.""" + async_add_entities( + [ + SwitchGroup( + config.get(CONF_UNIQUE_ID), + config[CONF_NAME], + config[CONF_ENTITIES], + config.get(CONF_ALL, False), + ) + ] + ) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Initialize Switch Group config entry.""" + registry = er.async_get(hass) + entities = er.async_validate_entity_ids( + registry, config_entry.options[CONF_ENTITIES] + ) + async_add_entities( + [ + SwitchGroup( + config_entry.entry_id, + config_entry.title, + entities, + config_entry.options.get(CONF_ALL), + ) + ] + ) + + +class SwitchGroup(GroupEntity, SwitchEntity): + """Representation of a switch group.""" + + _attr_available = False + _attr_should_poll = False + + def __init__( + self, + unique_id: str | None, + name: str, + entity_ids: list[str], + mode: bool | None, + ) -> None: + """Initialize a switch group.""" + self._entity_ids = entity_ids + + self._attr_name = name + self._attr_extra_state_attributes = {ATTR_ENTITY_ID: entity_ids} + self._attr_unique_id = unique_id + self.mode = any + if mode: + self.mode = all + + async def async_added_to_hass(self) -> None: + """Register callbacks.""" + + @callback + def async_state_changed_listener(event: Event) -> None: + """Handle child updates.""" + self.async_set_context(event.context) + self.async_defer_or_update_ha_state() + + self.async_on_remove( + async_track_state_change_event( + self.hass, self._entity_ids, async_state_changed_listener + ) + ) + + await super().async_added_to_hass() + + async def async_turn_on(self, **kwargs: Any) -> None: + """Forward the turn_on command to all switches in the group.""" + data = {ATTR_ENTITY_ID: self._entity_ids} + _LOGGER.debug("Forwarded turn_on command: %s", data) + + await self.hass.services.async_call( + DOMAIN, + SERVICE_TURN_ON, + data, + blocking=True, + context=self._context, + ) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Forward the turn_off command to all switches in the group.""" + data = {ATTR_ENTITY_ID: self._entity_ids} + await self.hass.services.async_call( + DOMAIN, + SERVICE_TURN_OFF, + data, + blocking=True, + context=self._context, + ) + + @callback + def async_update_group_state(self) -> None: + """Query all members and determine the switch group state.""" + states = [ + state.state + for entity_id in self._entity_ids + if (state := self.hass.states.get(entity_id)) is not None + ] + + valid_state = self.mode( + state not in (STATE_UNKNOWN, STATE_UNAVAILABLE) for state in states + ) + + if not valid_state: + # Set as unknown if any / all member is unknown or unavailable + self._attr_is_on = None + else: + # Set as ON if any / all member is ON + self._attr_is_on = self.mode(state == STATE_ON for state in states) + + self._attr_available = any(state != STATE_UNAVAILABLE for state in states) diff --git a/homeassistant/components/group/translations/bg.json b/homeassistant/components/group/translations/bg.json index c737a09216e..d28a170bcd0 100644 --- a/homeassistant/components/group/translations/bg.json +++ b/homeassistant/components/group/translations/bg.json @@ -1,4 +1,13 @@ { + "config": { + "step": { + "media_player": { + "data": { + "name": "\u0418\u043c\u0435 \u043d\u0430 \u0433\u0440\u0443\u043f\u0430" + } + } + } + }, "state": { "_": { "closed": "\u0417\u0430\u0442\u0432\u043e\u0440\u0435\u043d\u0430", diff --git a/homeassistant/components/group/translations/ca.json b/homeassistant/components/group/translations/ca.json index 552a2c9677e..b17d98b5084 100644 --- a/homeassistant/components/group/translations/ca.json +++ b/homeassistant/components/group/translations/ca.json @@ -1,4 +1,115 @@ { + "config": { + "step": { + "binary_sensor": { + "data": { + "all": "Totes les entitats", + "entities": "Membres", + "name": "Nom" + }, + "description": "Si \"totes les entitats\" est\u00e0 activat, l'estat del grup estar\u00e0 activat (ON) si tots els membres estan activats. Si \"totes les entitats\" est\u00e0 desactivat, l'estat del grup s'activar\u00e0 si hi ha activat qualsevol membre.", + "title": "Nou grup" + }, + "cover": { + "data": { + "entities": "Membres", + "name": "Nom", + "title": "Nou grup" + }, + "description": "Selecciona les opcions del grup" + }, + "cover_options": { + "data": { + "entities": "Membres del grup" + }, + "description": "Selecciona les opcions del grup" + }, + "fan": { + "data": { + "entities": "Membres", + "name": "Nom", + "title": "Nou grup" + }, + "description": "Selecciona les opcions del grup" + }, + "fan_options": { + "data": { + "entities": "Membres del grup" + }, + "description": "Selecciona les opcions del grup" + }, + "init": { + "data": { + "group_type": "Tipus de grup" + }, + "description": "Selecciona el tipus de grup" + }, + "light": { + "data": { + "entities": "Membres", + "name": "Nom", + "title": "Nou grup" + }, + "description": "Selecciona les opcions del grup" + }, + "light_options": { + "data": { + "entities": "Membres del grup" + }, + "description": "Selecciona les opcions del grup" + }, + "media_player": { + "data": { + "entities": "Membres", + "name": "Nom", + "title": "Nou grup" + }, + "description": "Selecciona les opcions del grup" + }, + "media_player_options": { + "data": { + "entities": "Membres del grup" + }, + "description": "Selecciona les opcions del grup" + }, + "user": { + "data": { + "group_type": "Tipus de grup" + }, + "title": "Nou grup" + } + } + }, + "options": { + "step": { + "binary_sensor_options": { + "data": { + "all": "Totes les entitats", + "entities": "Membres" + } + }, + "cover_options": { + "data": { + "entities": "Membres" + } + }, + "fan_options": { + "data": { + "entities": "Membres" + } + }, + "light_options": { + "data": { + "entities": "Membres" + } + }, + "media_player_options": { + "data": { + "entities": "Membres" + } + } + } + }, "state": { "_": { "closed": "Tancat/ada", diff --git a/homeassistant/components/group/translations/de.json b/homeassistant/components/group/translations/de.json index 80da069e72a..c06dd2a4a8f 100644 --- a/homeassistant/components/group/translations/de.json +++ b/homeassistant/components/group/translations/de.json @@ -1,4 +1,115 @@ { + "config": { + "step": { + "binary_sensor": { + "data": { + "all": "Alle Entit\u00e4ten", + "entities": "Mitglieder", + "name": "Name" + }, + "description": "Wenn \"alle Entit\u00e4ten\" aktiviert ist, ist der Status der Gruppe nur dann eingeschaltet, wenn alle Mitglieder eingeschaltet sind. Wenn \"alle Entit\u00e4ten\" deaktiviert ist, ist der Status der Gruppe eingeschaltet, wenn irgendein Mitglied eingeschaltet ist.", + "title": "Neue Gruppe" + }, + "cover": { + "data": { + "entities": "Mitglieder", + "name": "Name", + "title": "Neue Gruppe" + }, + "description": "Gruppenoptionen ausw\u00e4hlen" + }, + "cover_options": { + "data": { + "entities": "Gruppenmitglieder" + }, + "description": "Gruppenoptionen ausw\u00e4hlen" + }, + "fan": { + "data": { + "entities": "Mitglieder", + "name": "Name", + "title": "Neue Gruppe" + }, + "description": "Gruppenoptionen ausw\u00e4hlen" + }, + "fan_options": { + "data": { + "entities": "Gruppenmitglieder" + }, + "description": "Gruppenoptionen ausw\u00e4hlen" + }, + "init": { + "data": { + "group_type": "Gruppentyp" + }, + "description": "Gruppentyp ausw\u00e4hlen" + }, + "light": { + "data": { + "entities": "Mitglieder", + "name": "Name", + "title": "Neue Gruppe" + }, + "description": "Gruppenoptionen ausw\u00e4hlen" + }, + "light_options": { + "data": { + "entities": "Gruppenmitglieder" + }, + "description": "Gruppenoptionen ausw\u00e4hlen" + }, + "media_player": { + "data": { + "entities": "Mitglieder", + "name": "Name", + "title": "Neue Gruppe" + }, + "description": "Gruppenoptionen ausw\u00e4hlen" + }, + "media_player_options": { + "data": { + "entities": "Gruppenmitglieder" + }, + "description": "Gruppenoptionen ausw\u00e4hlen" + }, + "user": { + "data": { + "group_type": "Gruppentyp" + }, + "title": "Neue Gruppe" + } + } + }, + "options": { + "step": { + "binary_sensor_options": { + "data": { + "all": "Alle Entit\u00e4ten", + "entities": "Mitglieder" + } + }, + "cover_options": { + "data": { + "entities": "Mitglieder" + } + }, + "fan_options": { + "data": { + "entities": "Mitglieder" + } + }, + "light_options": { + "data": { + "entities": "Mitglieder" + } + }, + "media_player_options": { + "data": { + "entities": "Mitglieder" + } + } + } + }, "state": { "_": { "closed": "Geschlossen", diff --git a/homeassistant/components/group/translations/el.json b/homeassistant/components/group/translations/el.json index e22d7a788af..bebfb8f1cb8 100644 --- a/homeassistant/components/group/translations/el.json +++ b/homeassistant/components/group/translations/el.json @@ -1,16 +1,127 @@ { + "config": { + "step": { + "binary_sensor": { + "data": { + "all": "\u038c\u03bb\u03b5\u03c2 \u03bf\u03b9 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2", + "entities": "\u039c\u03ad\u03bb\u03b7", + "name": "\u038c\u03bd\u03bf\u03bc\u03b1" + }, + "description": "\u0395\u03ac\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03b7 \u03b7 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \"\u03cc\u03bb\u03b5\u03c2 \u03bf\u03b9 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2\", \u03b7 \u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03b7 \u03bc\u03cc\u03bd\u03bf \u03b5\u03ac\u03bd \u03cc\u03bb\u03b1 \u03c4\u03b1 \u03bc\u03ad\u03bb\u03b7 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03b1. \u0395\u03ac\u03bd \u03b7 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \"\u03cc\u03bb\u03b5\u03c2 \u03bf\u03b9 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2\" \u03b5\u03af\u03bd\u03b1\u03b9 \u03b1\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03b7, \u03b7 \u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03b7 \u03b5\u03ac\u03bd \u03bf\u03c0\u03bf\u03b9\u03bf\u03b4\u03ae\u03c0\u03bf\u03c4\u03b5 \u03bc\u03ad\u03bb\u03bf\u03c2 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03bf.", + "title": "\u039d\u03ad\u03b1 \u03bf\u03bc\u03ac\u03b4\u03b1" + }, + "cover": { + "data": { + "entities": "\u039c\u03ad\u03bb\u03b7 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2", + "name": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2", + "title": "\u039d\u03ad\u03b1 \u03bf\u03bc\u03ac\u03b4\u03b1" + }, + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2" + }, + "cover_options": { + "data": { + "entities": "\u039c\u03ad\u03bb\u03b7 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2" + }, + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2" + }, + "fan": { + "data": { + "entities": "\u039c\u03ad\u03bb\u03b7 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2", + "name": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2", + "title": "\u039d\u03ad\u03b1 \u03bf\u03bc\u03ac\u03b4\u03b1" + }, + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2" + }, + "fan_options": { + "data": { + "entities": "\u039c\u03ad\u03bb\u03b7 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2" + }, + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2" + }, + "init": { + "data": { + "group_type": "\u03a4\u03cd\u03c0\u03bf\u03c2 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2" + }, + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03cd\u03c0\u03bf \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2" + }, + "light": { + "data": { + "entities": "\u039c\u03ad\u03bb\u03b7 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2", + "name": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2", + "title": "\u039d\u03ad\u03b1 \u03bf\u03bc\u03ac\u03b4\u03b1" + }, + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2" + }, + "light_options": { + "data": { + "entities": "\u039c\u03ad\u03bb\u03b7 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2" + }, + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2" + }, + "media_player": { + "data": { + "entities": "\u039c\u03ad\u03bb\u03b7 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2", + "name": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2", + "title": "\u039d\u03ad\u03b1 \u03bf\u03bc\u03ac\u03b4\u03b1" + }, + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2" + }, + "media_player_options": { + "data": { + "entities": "\u039c\u03ad\u03bb\u03b7 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2" + }, + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2" + }, + "user": { + "data": { + "group_type": "\u03a4\u03cd\u03c0\u03bf\u03c2 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2" + }, + "title": "\u039d\u03ad\u03b1 \u03bf\u03bc\u03ac\u03b4\u03b1" + } + } + }, + "options": { + "step": { + "binary_sensor_options": { + "data": { + "all": "\u038c\u03bb\u03b5\u03c2 \u03bf\u03b9 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2", + "entities": "\u039c\u03ad\u03bb\u03b7" + } + }, + "cover_options": { + "data": { + "entities": "\u039c\u03ad\u03bb\u03b7" + } + }, + "fan_options": { + "data": { + "entities": "\u039c\u03ad\u03bb\u03b7" + } + }, + "light_options": { + "data": { + "entities": "\u039c\u03ad\u03bb\u03b7" + } + }, + "media_player_options": { + "data": { + "entities": "\u039c\u03ad\u03bb\u03b7" + } + } + } + }, "state": { "_": { - "closed": "\u039a\u03bb\u03b5\u03b9\u03c3\u03c4\u03cc", + "closed": "\u039a\u03bb\u03b5\u03b9\u03c3\u03c4\u03ae", "home": "\u03a3\u03c0\u03af\u03c4\u03b9", - "locked": "\u039a\u03bb\u03b5\u03b9\u03b4\u03c9\u03bc\u03ad\u03bd\u03bf", - "not_home": "\u0395\u03ba\u03c4\u03cc\u03c2 \u03a3\u03c0\u03b9\u03c4\u03b9\u03bf\u03cd", + "locked": "\u039a\u03bb\u03b5\u03b9\u03b4\u03c9\u03bc\u03ad\u03bd\u03b7", + "not_home": "\u0395\u03ba\u03c4\u03cc\u03c2 \u03c3\u03c0\u03b9\u03c4\u03b9\u03bf\u03cd", "off": "\u0391\u03bd\u03b5\u03bd\u03b5\u03c1\u03b3\u03cc", "ok": "\u0395\u03bd\u03c4\u03ac\u03be\u03b5\u03b9", "on": "\u0395\u03bd\u03b5\u03c1\u03b3\u03cc", - "open": "\u0391\u03bd\u03bf\u03b9\u03c7\u03c4\u03cc", + "open": "\u0391\u03bd\u03bf\u03b9\u03c7\u03c4\u03ae", "problem": "\u03a0\u03c1\u03cc\u03b2\u03bb\u03b7\u03bc\u03b1", - "unlocked": "\u039e\u03b5\u03ba\u03bb\u03b5\u03af\u03b4\u03c9\u03c4\u03bf" + "unlocked": "\u039e\u03b5\u03ba\u03bb\u03b5\u03af\u03b4\u03c9\u03c4\u03b7" } }, "title": "\u039f\u03bc\u03ac\u03b4\u03b1" diff --git a/homeassistant/components/group/translations/en.json b/homeassistant/components/group/translations/en.json index 271ada378ca..a67d23b812d 100644 --- a/homeassistant/components/group/translations/en.json +++ b/homeassistant/components/group/translations/en.json @@ -1,4 +1,131 @@ { + "config": { + "step": { + "binary_sensor": { + "data": { + "all": "All entities", + "entities": "Members", + "hide_members": "Hide members", + "name": "Name" + }, + "description": "If \"all entities\" is enabled, the group's state is on only if all members are on. If \"all entities\" is disabled, the group's state is on if any member is on.", + "title": "Add Group" + }, + "cover": { + "data": { + "entities": "Members", + "hide_members": "Hide members", + "name": "Name" + }, + "title": "Add Group" + }, + "fan": { + "data": { + "entities": "Members", + "hide_members": "Hide members", + "name": "Name" + }, + "title": "Add Group" + }, + "light": { + "data": { + "entities": "Members", + "hide_members": "Hide members", + "name": "Name" + }, + "title": "Add Group" + }, + "lock": { + "data": { + "entities": "Members", + "hide_members": "Hide members", + "name": "Name" + }, + "title": "Add Group" + }, + "media_player": { + "data": { + "entities": "Members", + "hide_members": "Hide members", + "name": "Name" + }, + "title": "Add Group" + }, + "switch": { + "data": { + "entities": "Members", + "hide_members": "Hide members", + "name": "Name" + }, + "title": "Add Group" + }, + "user": { + "description": "Groups allow you to create a new entity that represents multiple entities of the same type.", + "menu_options": { + "binary_sensor": "Binary sensor group", + "cover": "Cover group", + "fan": "Fan group", + "light": "Light group", + "lock": "Lock group", + "media_player": "Media player group", + "switch": "Switch group" + }, + "title": "Add Group" + } + } + }, + "options": { + "step": { + "binary_sensor": { + "data": { + "all": "All entities", + "entities": "Members", + "hide_members": "Hide members" + }, + "description": "If \"all entities\" is enabled, the group's state is on only if all members are on. If \"all entities\" is disabled, the group's state is on if any member is on." + }, + "cover": { + "data": { + "entities": "Members", + "hide_members": "Hide members" + } + }, + "fan": { + "data": { + "entities": "Members", + "hide_members": "Hide members" + } + }, + "light": { + "data": { + "all": "All entities", + "entities": "Members", + "hide_members": "Hide members" + }, + "description": "If \"all entities\" is enabled, the group's state is on only if all members are on. If \"all entities\" is disabled, the group's state is on if any member is on." + }, + "lock": { + "data": { + "entities": "Members", + "hide_members": "Hide members" + } + }, + "media_player": { + "data": { + "entities": "Members", + "hide_members": "Hide members" + } + }, + "switch": { + "data": { + "all": "All entities", + "entities": "Members", + "hide_members": "Hide members" + }, + "description": "If \"all entities\" is enabled, the group's state is on only if all members are on. If \"all entities\" is disabled, the group's state is on if any member is on." + } + } + }, "state": { "_": { "closed": "Closed", diff --git a/homeassistant/components/group/translations/es.json b/homeassistant/components/group/translations/es.json index 9aac8e09780..e74adda486d 100644 --- a/homeassistant/components/group/translations/es.json +++ b/homeassistant/components/group/translations/es.json @@ -1,4 +1,19 @@ { + "config": { + "step": { + "cover": { + "data": { + "name": "Nombre del Grupo" + }, + "description": "Seleccionar opciones de grupo" + }, + "cover_options": { + "data": { + "entities": "Miembros del grupo" + } + } + } + }, "state": { "_": { "closed": "Cerrado", diff --git a/homeassistant/components/group/translations/et.json b/homeassistant/components/group/translations/et.json index dacd0973f1d..c45094aeedd 100644 --- a/homeassistant/components/group/translations/et.json +++ b/homeassistant/components/group/translations/et.json @@ -1,4 +1,115 @@ { + "config": { + "step": { + "binary_sensor": { + "data": { + "all": "K\u00f5ik olemid", + "entities": "Liikmed", + "name": "Nimi" + }, + "description": "Kui \"k\u00f5ik olemid\" on lubatud,on r\u00fchma olek sees ainult siis kui k\u00f5ik liikmed on sisse l\u00fclitatud. Kui \"k\u00f5ik olemid\" on keelatud, on r\u00fchma olek sees kui m\u00f5ni liige on sisse l\u00fclitatud.", + "title": "Uus r\u00fchm" + }, + "cover": { + "data": { + "entities": "Liikmed", + "name": "Nimi", + "title": "Uus r\u00fchm" + }, + "description": "R\u00fchmasuvandite valimine" + }, + "cover_options": { + "data": { + "entities": "R\u00fchma liikmed" + }, + "description": "R\u00fchmasuvandite valimine" + }, + "fan": { + "data": { + "entities": "Liikmed", + "name": "Nimi", + "title": "Uus r\u00fchm" + }, + "description": "R\u00fchmasuvandite valimine" + }, + "fan_options": { + "data": { + "entities": "R\u00fchma liikmed" + }, + "description": "R\u00fchmasuvandite valimine" + }, + "init": { + "data": { + "group_type": "R\u00fchma t\u00fc\u00fcp" + }, + "description": "Vali r\u00fchma t\u00fc\u00fcp" + }, + "light": { + "data": { + "entities": "Liikmed", + "name": "Nimi", + "title": "Uus r\u00fchm" + }, + "description": "R\u00fchmasuvandite valimine" + }, + "light_options": { + "data": { + "entities": "R\u00fchma liikmed" + }, + "description": "R\u00fchmasuvandite valimine" + }, + "media_player": { + "data": { + "entities": "Liikmed", + "name": "Nimi", + "title": "Uus r\u00fchm" + }, + "description": "R\u00fchmasuvandite valimine" + }, + "media_player_options": { + "data": { + "entities": "R\u00fchma liikmed" + }, + "description": "R\u00fchmasuvandite valimine" + }, + "user": { + "data": { + "group_type": "R\u00fchma t\u00fc\u00fcp" + }, + "title": "Uus r\u00fchm" + } + } + }, + "options": { + "step": { + "binary_sensor_options": { + "data": { + "all": "K\u00f5ik olemid", + "entities": "Liikmed" + } + }, + "cover_options": { + "data": { + "entities": "Liikmed" + } + }, + "fan_options": { + "data": { + "entities": "Liikmed" + } + }, + "light_options": { + "data": { + "entities": "Liikmed" + } + }, + "media_player_options": { + "data": { + "entities": "Liikmed" + } + } + } + }, "state": { "_": { "closed": "Suletud", diff --git a/homeassistant/components/group/translations/fr.json b/homeassistant/components/group/translations/fr.json index f1ade09f650..8e8b5ee4233 100644 --- a/homeassistant/components/group/translations/fr.json +++ b/homeassistant/components/group/translations/fr.json @@ -1,13 +1,124 @@ { + "config": { + "step": { + "binary_sensor": { + "data": { + "all": "Toutes les entit\u00e9s", + "entities": "Membres", + "name": "Nom" + }, + "description": "Si \u00ab\u00a0toutes les entit\u00e9s\u00a0\u00bb est activ\u00e9, l'\u00e9tat du groupe n'est activ\u00e9 que si tous les membres sont activ\u00e9s. Si \u00ab\u00a0toutes les entit\u00e9s\u00a0\u00bb est d\u00e9sactiv\u00e9, l'\u00e9tat du groupe est activ\u00e9 si au moins un membre est activ\u00e9.", + "title": "Nouveau groupe" + }, + "cover": { + "data": { + "entities": "Membres", + "name": "Nom", + "title": "Nouveau groupe" + }, + "description": "S\u00e9lectionnez les options du groupe" + }, + "cover_options": { + "data": { + "entities": "Membres du groupe" + }, + "description": "S\u00e9lectionnez les options du groupe" + }, + "fan": { + "data": { + "entities": "Membres", + "name": "Nom", + "title": "Nouveau groupe" + }, + "description": "S\u00e9lectionnez les options du groupe" + }, + "fan_options": { + "data": { + "entities": "Membres du groupe" + }, + "description": "S\u00e9lectionnez les options du groupe" + }, + "init": { + "data": { + "group_type": "Type de groupe" + }, + "description": "S\u00e9lectionnez le type de groupe" + }, + "light": { + "data": { + "entities": "Membres", + "name": "Nom", + "title": "Nouveau groupe" + }, + "description": "S\u00e9lectionnez les options du groupe" + }, + "light_options": { + "data": { + "entities": "Membres du groupe" + }, + "description": "S\u00e9lectionnez les options du groupe" + }, + "media_player": { + "data": { + "entities": "Membres", + "name": "Nom", + "title": "Nouveau groupe" + }, + "description": "S\u00e9lectionnez les options du groupe" + }, + "media_player_options": { + "data": { + "entities": "Membres du groupe" + }, + "description": "S\u00e9lectionnez les options du groupe" + }, + "user": { + "data": { + "group_type": "Type de groupe" + }, + "title": "Nouveau groupe" + } + } + }, + "options": { + "step": { + "binary_sensor_options": { + "data": { + "all": "Toutes les entit\u00e9s", + "entities": "Membres" + } + }, + "cover_options": { + "data": { + "entities": "Membres" + } + }, + "fan_options": { + "data": { + "entities": "Membres" + } + }, + "light_options": { + "data": { + "entities": "Membres" + } + }, + "media_player_options": { + "data": { + "entities": "Membres" + } + } + } + }, "state": { "_": { "closed": "Ferm\u00e9", "home": "Pr\u00e9sent", "locked": "Verrouill\u00e9", "not_home": "Absent", - "off": "Inactif", + "off": "D\u00e9sactiv\u00e9", "ok": "OK", - "on": "Actif", + "on": "Activ\u00e9", "open": "Ouvert", "problem": "Probl\u00e8me", "unlocked": "D\u00e9verrouill\u00e9" diff --git a/homeassistant/components/group/translations/he.json b/homeassistant/components/group/translations/he.json index 798a8e1e7c6..06c1a8e0fa8 100644 --- a/homeassistant/components/group/translations/he.json +++ b/homeassistant/components/group/translations/he.json @@ -1,4 +1,115 @@ { + "config": { + "step": { + "binary_sensor": { + "data": { + "all": "\u05db\u05dc \u05d4\u05d9\u05e9\u05d5\u05d9\u05d5\u05ea", + "entities": "\u05d7\u05d1\u05e8\u05d9\u05dd", + "name": "\u05e9\u05dd" + }, + "description": "\u05d0\u05dd \u05d4\u05d0\u05e4\u05e9\u05e8\u05d5\u05ea \"\u05db\u05dc \u05d4\u05d9\u05e9\u05d5\u05d9\u05d5\u05ea\" \u05d6\u05de\u05d9\u05e0\u05d4, \u05de\u05e6\u05d1 \u05d4\u05e7\u05d1\u05d5\u05e6\u05d4 \u05de\u05d5\u05e4\u05e2\u05dc \u05e8\u05e7 \u05d0\u05dd \u05db\u05dc \u05d4\u05d7\u05d1\u05e8\u05d9\u05dd \u05e4\u05d5\u05e2\u05dc\u05d9\u05dd. \u05d0\u05dd \"\u05db\u05dc \u05d4\u05d9\u05e9\u05d5\u05d9\u05d5\u05ea\" \u05d0\u05d9\u05e0\u05d5 \u05d6\u05de\u05d9\u05df, \u05de\u05e6\u05d1 \u05d4\u05e7\u05d1\u05d5\u05e6\u05d4 \u05de\u05d5\u05e4\u05e2\u05dc \u05d0\u05dd \u05d7\u05d1\u05e8 \u05db\u05dc\u05e9\u05d4\u05d5 \u05e4\u05d5\u05e2\u05dc.", + "title": "\u05e7\u05d1\u05d5\u05e6\u05d4 \u05d7\u05d3\u05e9\u05d4" + }, + "cover": { + "data": { + "entities": "\u05d7\u05d1\u05e8\u05d9\u05dd", + "name": "\u05e9\u05dd", + "title": "\u05e7\u05d1\u05d5\u05e6\u05d4 \u05d7\u05d3\u05e9\u05d4" + }, + "description": "\u05d1\u05d7\u05d9\u05e8\u05ea \u05d0\u05e4\u05e9\u05e8\u05d5\u05d9\u05d5\u05ea \u05e7\u05d1\u05d5\u05e6\u05d4" + }, + "cover_options": { + "data": { + "entities": "\u05d7\u05d1\u05e8\u05d9 \u05d4\u05e7\u05d1\u05d5\u05e6\u05d4" + }, + "description": "\u05d1\u05d7\u05d9\u05e8\u05ea \u05d0\u05e4\u05e9\u05e8\u05d5\u05d9\u05d5\u05ea \u05e7\u05d1\u05d5\u05e6\u05d4" + }, + "fan": { + "data": { + "entities": "\u05d7\u05d1\u05e8\u05d9\u05dd", + "name": "\u05e9\u05dd", + "title": "\u05e7\u05d1\u05d5\u05e6\u05d4 \u05d7\u05d3\u05e9\u05d4" + }, + "description": "\u05d1\u05d7\u05d9\u05e8\u05ea \u05d0\u05e4\u05e9\u05e8\u05d5\u05d9\u05d5\u05ea \u05e7\u05d1\u05d5\u05e6\u05d4" + }, + "fan_options": { + "data": { + "entities": "\u05d7\u05d1\u05e8\u05d9 \u05d4\u05e7\u05d1\u05d5\u05e6\u05d4" + }, + "description": "\u05d1\u05d7\u05d9\u05e8\u05ea \u05d0\u05e4\u05e9\u05e8\u05d5\u05d9\u05d5\u05ea \u05e7\u05d1\u05d5\u05e6\u05d4" + }, + "init": { + "data": { + "group_type": "\u05e1\u05d5\u05d2 \u05e7\u05d1\u05d5\u05e6\u05d4" + }, + "description": "\u05d1\u05d7\u05d9\u05e8\u05ea \u05e1\u05d5\u05d2 \u05e7\u05d1\u05d5\u05e6\u05d4" + }, + "light": { + "data": { + "entities": "\u05d7\u05d1\u05e8\u05d9\u05dd", + "name": "\u05e9\u05dd", + "title": "\u05e7\u05d1\u05d5\u05e6\u05d4 \u05d7\u05d3\u05e9\u05d4" + }, + "description": "\u05d1\u05d7\u05d9\u05e8\u05ea \u05d0\u05e4\u05e9\u05e8\u05d5\u05d9\u05d5\u05ea \u05e7\u05d1\u05d5\u05e6\u05d4" + }, + "light_options": { + "data": { + "entities": "\u05d7\u05d1\u05e8\u05d9 \u05d4\u05e7\u05d1\u05d5\u05e6\u05d4" + }, + "description": "\u05d1\u05d7\u05d9\u05e8\u05ea \u05d0\u05e4\u05e9\u05e8\u05d5\u05d9\u05d5\u05ea \u05e7\u05d1\u05d5\u05e6\u05d4" + }, + "media_player": { + "data": { + "entities": "\u05d7\u05d1\u05e8\u05d9\u05dd", + "name": "\u05e9\u05dd", + "title": "\u05e7\u05d1\u05d5\u05e6\u05d4 \u05d7\u05d3\u05e9\u05d4" + }, + "description": "\u05d1\u05d7\u05d9\u05e8\u05ea \u05d0\u05e4\u05e9\u05e8\u05d5\u05d9\u05d5\u05ea \u05e7\u05d1\u05d5\u05e6\u05d4" + }, + "media_player_options": { + "data": { + "entities": "\u05d7\u05d1\u05e8\u05d9 \u05d4\u05e7\u05d1\u05d5\u05e6\u05d4" + }, + "description": "\u05d1\u05d7\u05d9\u05e8\u05ea \u05d0\u05e4\u05e9\u05e8\u05d5\u05d9\u05d5\u05ea \u05e7\u05d1\u05d5\u05e6\u05d4" + }, + "user": { + "data": { + "group_type": "\u05e1\u05d5\u05d2 \u05e7\u05d1\u05d5\u05e6\u05d4" + }, + "title": "\u05e7\u05d1\u05d5\u05e6\u05d4 \u05d7\u05d3\u05e9\u05d4" + } + } + }, + "options": { + "step": { + "binary_sensor_options": { + "data": { + "all": "\u05db\u05dc \u05d4\u05d9\u05e9\u05d5\u05d9\u05d5\u05ea", + "entities": "\u05d7\u05d1\u05e8\u05d9\u05dd" + } + }, + "cover_options": { + "data": { + "entities": "\u05d7\u05d1\u05e8\u05d9\u05dd" + } + }, + "fan_options": { + "data": { + "entities": "\u05d7\u05d1\u05e8\u05d9\u05dd" + } + }, + "light_options": { + "data": { + "entities": "\u05d7\u05d1\u05e8\u05d9\u05dd" + } + }, + "media_player_options": { + "data": { + "entities": "\u05d7\u05d1\u05e8\u05d9\u05dd" + } + } + } + }, "state": { "_": { "closed": "\u05e1\u05d2\u05d5\u05e8", diff --git a/homeassistant/components/group/translations/hu.json b/homeassistant/components/group/translations/hu.json index 7438229f852..08c7d5e5afb 100644 --- a/homeassistant/components/group/translations/hu.json +++ b/homeassistant/components/group/translations/hu.json @@ -1,4 +1,115 @@ { + "config": { + "step": { + "binary_sensor": { + "data": { + "all": "Minden entit\u00e1s", + "entities": "A csoport tagjai", + "name": "N\u00e9v" + }, + "description": "Ha \u201eMinden entit\u00e1s\u201d enged\u00e9lyezve van, a csoport \u00e1llapota csak akkor van bekapcsolva, ha minden tag \u00e1llapota bekapcsolt. Ha \u201eMinden entit\u00e1s\u201d le van tiltva, a csoport \u00e1llapota akkor van bekapcsolva, ha b\u00e1rmelyik tag bekapcsolt \u00e1llapotban van.", + "title": "\u00daj csoport" + }, + "cover": { + "data": { + "entities": "A csoport tagjai", + "name": "Csoport neve", + "title": "\u00daj csoport" + }, + "description": "Csoport be\u00e1ll\u00edt\u00e1sai" + }, + "cover_options": { + "data": { + "entities": "A csoport tagjai" + }, + "description": "Csoport be\u00e1ll\u00edt\u00e1sai" + }, + "fan": { + "data": { + "entities": "A csoport tagjai", + "name": "A csoport elnevez\u00e9se", + "title": "\u00daj csoport" + }, + "description": "Csoport be\u00e1ll\u00edt\u00e1sai" + }, + "fan_options": { + "data": { + "entities": "A csoport tagjai" + }, + "description": "Csoport be\u00e1ll\u00edt\u00e1sai" + }, + "init": { + "data": { + "group_type": "A csoport t\u00edpusa" + }, + "description": "A csoport t\u00edpusa" + }, + "light": { + "data": { + "entities": "A csoport tagjai", + "name": "A csoport elnevez\u00e9se", + "title": "\u00daj csoport" + }, + "description": "Csoport be\u00e1ll\u00edt\u00e1sai" + }, + "light_options": { + "data": { + "entities": "A csoport tagjai" + }, + "description": "Csoport be\u00e1ll\u00edt\u00e1sai" + }, + "media_player": { + "data": { + "entities": "A csoport tagjai", + "name": "A csoport elnevez\u00e9se", + "title": "\u00daj csoport" + }, + "description": "Csoport be\u00e1ll\u00edt\u00e1sai" + }, + "media_player_options": { + "data": { + "entities": "A csoport tagjai" + }, + "description": "Csoport be\u00e1ll\u00edt\u00e1sai" + }, + "user": { + "data": { + "group_type": "Csoport t\u00edpusa" + }, + "title": "\u00daj csoport" + } + } + }, + "options": { + "step": { + "binary_sensor_options": { + "data": { + "all": "Minden entit\u00e1s", + "entities": "A csoport tagjai" + } + }, + "cover_options": { + "data": { + "entities": "A csoport tagjai" + } + }, + "fan_options": { + "data": { + "entities": "A csoport tagjai" + } + }, + "light_options": { + "data": { + "entities": "A csoport tagjai" + } + }, + "media_player_options": { + "data": { + "entities": "A csoport tagjai" + } + } + } + }, "state": { "_": { "closed": "Z\u00e1rva", diff --git a/homeassistant/components/group/translations/id.json b/homeassistant/components/group/translations/id.json index 553cbce0550..2a92526d644 100644 --- a/homeassistant/components/group/translations/id.json +++ b/homeassistant/components/group/translations/id.json @@ -1,4 +1,115 @@ { + "config": { + "step": { + "binary_sensor": { + "data": { + "all": "Semua entitas", + "entities": "Anggota", + "name": "Nama" + }, + "description": "Jika \"semua entitas\" diaktifkan, status grup akan menyala jika semua anggota nyala. Jika \"semua entitas\" dinonaktifkan, status grup akan menyala jika ada salah satu atau lebih anggota yang menyala.", + "title": "Grup Baru" + }, + "cover": { + "data": { + "entities": "Anggota", + "name": "Nama", + "title": "Grup Baru" + }, + "description": "Pilih opsi grup" + }, + "cover_options": { + "data": { + "entities": "Anggota grup" + }, + "description": "Pilih opsi grup" + }, + "fan": { + "data": { + "entities": "Anggota", + "name": "Nama", + "title": "Grup Baru" + }, + "description": "Pilih opsi grup" + }, + "fan_options": { + "data": { + "entities": "Anggota grup" + }, + "description": "Pilih opsi grup" + }, + "init": { + "data": { + "group_type": "Jenis grup" + }, + "description": "Pilih jenis grup" + }, + "light": { + "data": { + "entities": "Anggota", + "name": "Nama", + "title": "Grup Baru" + }, + "description": "Pilih opsi grup" + }, + "light_options": { + "data": { + "entities": "Anggota grup" + }, + "description": "Pilih opsi grup" + }, + "media_player": { + "data": { + "entities": "Anggota", + "name": "Nama", + "title": "Grup Baru" + }, + "description": "Pilih opsi grup" + }, + "media_player_options": { + "data": { + "entities": "Anggota grup" + }, + "description": "Pilih opsi grup" + }, + "user": { + "data": { + "group_type": "Jenis grup" + }, + "title": "Grup Baru" + } + } + }, + "options": { + "step": { + "binary_sensor_options": { + "data": { + "all": "Semua entitas", + "entities": "Anggota" + } + }, + "cover_options": { + "data": { + "entities": "Anggota" + } + }, + "fan_options": { + "data": { + "entities": "Anggota" + } + }, + "light_options": { + "data": { + "entities": "Anggota" + } + }, + "media_player_options": { + "data": { + "entities": "Anggota" + } + } + } + }, "state": { "_": { "closed": "Tertutup", diff --git a/homeassistant/components/group/translations/it.json b/homeassistant/components/group/translations/it.json index 67e13aa7cfa..6dc9ff2cce8 100644 --- a/homeassistant/components/group/translations/it.json +++ b/homeassistant/components/group/translations/it.json @@ -1,4 +1,115 @@ { + "config": { + "step": { + "binary_sensor": { + "data": { + "all": "Tutte le entit\u00e0", + "entities": "Membri", + "name": "Nome" + }, + "description": "Se \"tutte le entit\u00e0\" \u00e8 abilitata, lo stato del gruppo \u00e8 attivo solo se tutti i membri sono attivi. Se \"tutte le entit\u00e0\" \u00e8 disabilitata, lo stato del gruppo \u00e8 attivo se un membro \u00e8 attivo.", + "title": "Nuovo gruppo" + }, + "cover": { + "data": { + "entities": "Membri", + "name": "Nome", + "title": "Nuovo gruppo" + }, + "description": "Seleziona le opzioni di gruppo" + }, + "cover_options": { + "data": { + "entities": "Membri del gruppo" + }, + "description": "Seleziona le opzioni di gruppo" + }, + "fan": { + "data": { + "entities": "Membri", + "name": "Nome", + "title": "Nuovo gruppo" + }, + "description": "Seleziona le opzioni di gruppo" + }, + "fan_options": { + "data": { + "entities": "Membri del gruppo" + }, + "description": "Seleziona le opzioni di gruppo" + }, + "init": { + "data": { + "group_type": "Tipo di gruppo" + }, + "description": "Seleziona il tipo di gruppo" + }, + "light": { + "data": { + "entities": "Membri", + "name": "Nome", + "title": "Nuovo gruppo" + }, + "description": "Seleziona le opzioni di gruppo" + }, + "light_options": { + "data": { + "entities": "Membri del gruppo" + }, + "description": "Seleziona le opzioni di gruppo" + }, + "media_player": { + "data": { + "entities": "Membri", + "name": "Nome", + "title": "Nuovo gruppo" + }, + "description": "Seleziona le opzioni di gruppo" + }, + "media_player_options": { + "data": { + "entities": "Membri del gruppo" + }, + "description": "Seleziona le opzioni di gruppo" + }, + "user": { + "data": { + "group_type": "Tipo di gruppo" + }, + "title": "Nuovo gruppo" + } + } + }, + "options": { + "step": { + "binary_sensor_options": { + "data": { + "all": "Tutte le entit\u00e0", + "entities": "Membri" + } + }, + "cover_options": { + "data": { + "entities": "Membri" + } + }, + "fan_options": { + "data": { + "entities": "Membri" + } + }, + "light_options": { + "data": { + "entities": "Membri" + } + }, + "media_player_options": { + "data": { + "entities": "Membri" + } + } + } + }, "state": { "_": { "closed": "Chiuso", diff --git a/homeassistant/components/group/translations/ja.json b/homeassistant/components/group/translations/ja.json index 02aff2e2b84..dac7583169d 100644 --- a/homeassistant/components/group/translations/ja.json +++ b/homeassistant/components/group/translations/ja.json @@ -1,4 +1,115 @@ { + "config": { + "step": { + "binary_sensor": { + "data": { + "all": "\u3059\u3079\u3066\u306e\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3", + "entities": "\u30e1\u30f3\u30d0\u30fc", + "name": "\u540d\u524d" + }, + "description": "\"\u3059\u3079\u3066\u306e\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\" \u304c\u6709\u52b9\u306b\u306a\u3063\u3066\u3044\u308b\u5834\u5408\u3001\u30b0\u30eb\u30fc\u30d7\u306e\u72b6\u614b\u306f\u3001\u3059\u3079\u3066\u306e\u30e1\u30f3\u30d0\u30fc\u304c\u30aa\u30f3\u306e\u5834\u5408\u306b\u306e\u307f\u30aa\u30f3\u306b\u306a\u308a\u307e\u3059\u3002 \"\u3059\u3079\u3066\u306e\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\" \u304c\u7121\u52b9\u306b\u306a\u3063\u3066\u3044\u308b\u5834\u5408\u3001\u3044\u305a\u308c\u304b\u306e\u30e1\u30f3\u30d0\u30fc\u304c\u30aa\u30f3\u3067\u3042\u308c\u3070\u3001\u30b0\u30eb\u30fc\u30d7\u306e\u72b6\u614b\u306f\u30aa\u30f3\u306b\u306a\u308a\u307e\u3059\u3002", + "title": "\u65b0\u3057\u3044\u30b0\u30eb\u30fc\u30d7" + }, + "cover": { + "data": { + "entities": "\u30b0\u30eb\u30fc\u30d7\u306e\u30e1\u30f3\u30d0\u30fc", + "name": "\u30b0\u30eb\u30fc\u30d7\u540d", + "title": "\u65b0\u3057\u3044\u30b0\u30eb\u30fc\u30d7" + }, + "description": "\u30b0\u30eb\u30fc\u30d7\u306e\u30aa\u30d7\u30b7\u30e7\u30f3\u3092\u9078\u629e" + }, + "cover_options": { + "data": { + "entities": "\u30b0\u30eb\u30fc\u30d7\u306e\u30e1\u30f3\u30d0\u30fc" + }, + "description": "\u30b0\u30eb\u30fc\u30d7\u306e\u30aa\u30d7\u30b7\u30e7\u30f3\u3092\u9078\u629e" + }, + "fan": { + "data": { + "entities": "\u30b0\u30eb\u30fc\u30d7\u306e\u30e1\u30f3\u30d0\u30fc", + "name": "\u30b0\u30eb\u30fc\u30d7\u540d", + "title": "\u65b0\u3057\u3044\u30b0\u30eb\u30fc\u30d7" + }, + "description": "\u30b0\u30eb\u30fc\u30d7\u306e\u30aa\u30d7\u30b7\u30e7\u30f3\u3092\u9078\u629e" + }, + "fan_options": { + "data": { + "entities": "\u30b0\u30eb\u30fc\u30d7\u306e\u30e1\u30f3\u30d0\u30fc" + }, + "description": "\u30b0\u30eb\u30fc\u30d7\u306e\u30aa\u30d7\u30b7\u30e7\u30f3\u3092\u9078\u629e" + }, + "init": { + "data": { + "group_type": "\u30b0\u30eb\u30fc\u30d7\u30bf\u30a4\u30d7" + }, + "description": "\u30b0\u30eb\u30fc\u30d7\u306e\u7a2e\u985e\u3092\u9078\u629e" + }, + "light": { + "data": { + "entities": "\u30b0\u30eb\u30fc\u30d7\u306e\u30e1\u30f3\u30d0\u30fc", + "name": "\u30b0\u30eb\u30fc\u30d7\u540d", + "title": "\u65b0\u3057\u3044\u30b0\u30eb\u30fc\u30d7" + }, + "description": "\u30b0\u30eb\u30fc\u30d7\u306e\u30aa\u30d7\u30b7\u30e7\u30f3\u3092\u9078\u629e" + }, + "light_options": { + "data": { + "entities": "\u30b0\u30eb\u30fc\u30d7\u306e\u30e1\u30f3\u30d0\u30fc" + }, + "description": "\u30b0\u30eb\u30fc\u30d7\u306e\u30aa\u30d7\u30b7\u30e7\u30f3\u3092\u9078\u629e" + }, + "media_player": { + "data": { + "entities": "\u30b0\u30eb\u30fc\u30d7\u306e\u30e1\u30f3\u30d0\u30fc", + "name": "\u30b0\u30eb\u30fc\u30d7\u540d", + "title": "\u65b0\u3057\u3044\u30b0\u30eb\u30fc\u30d7" + }, + "description": "\u30b0\u30eb\u30fc\u30d7\u306e\u30aa\u30d7\u30b7\u30e7\u30f3\u3092\u9078\u629e" + }, + "media_player_options": { + "data": { + "entities": "\u30b0\u30eb\u30fc\u30d7\u306e\u30e1\u30f3\u30d0\u30fc" + }, + "description": "\u30b0\u30eb\u30fc\u30d7\u306e\u30aa\u30d7\u30b7\u30e7\u30f3\u3092\u9078\u629e" + }, + "user": { + "data": { + "group_type": "\u30b0\u30eb\u30fc\u30d7\u30bf\u30a4\u30d7" + }, + "title": "\u65b0\u3057\u3044\u30b0\u30eb\u30fc\u30d7" + } + } + }, + "options": { + "step": { + "binary_sensor_options": { + "data": { + "all": "\u3059\u3079\u3066\u306e\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3", + "entities": "\u30e1\u30f3\u30d0\u30fc" + } + }, + "cover_options": { + "data": { + "entities": "\u30e1\u30f3\u30d0\u30fc" + } + }, + "fan_options": { + "data": { + "entities": "\u30e1\u30f3\u30d0\u30fc" + } + }, + "light_options": { + "data": { + "entities": "\u30e1\u30f3\u30d0\u30fc" + } + }, + "media_player_options": { + "data": { + "entities": "\u30e1\u30f3\u30d0\u30fc" + } + } + } + }, "state": { "_": { "closed": "\u9589\u9396", diff --git a/homeassistant/components/group/translations/nl.json b/homeassistant/components/group/translations/nl.json index be9b55699b0..c636c56f744 100644 --- a/homeassistant/components/group/translations/nl.json +++ b/homeassistant/components/group/translations/nl.json @@ -1,4 +1,115 @@ { + "config": { + "step": { + "binary_sensor": { + "data": { + "all": "Alle entiteiten", + "entities": "Leden", + "name": "Naam" + }, + "description": "Als \"alle entiteiten\" is ingeschakeld, is de groep alleen ingeschakeld als alle leden zijn ingeschakeld. Als \"all entities\" is uitgeschakeld, is de groep ingeschakeld als een lid is ingeschakeld.", + "title": "Nieuwe groep" + }, + "cover": { + "data": { + "entities": "Leden", + "name": "Naam", + "title": "Nieuwe groep" + }, + "description": "Selecteer groepsopties" + }, + "cover_options": { + "data": { + "entities": "Groepsleden" + }, + "description": "Selecteer groepsopties" + }, + "fan": { + "data": { + "entities": "Leden", + "name": "Naam", + "title": "Nieuwe groep" + }, + "description": "Selecteer groepsopties" + }, + "fan_options": { + "data": { + "entities": "Groepsleden" + }, + "description": "Selecteer groepsopties" + }, + "init": { + "data": { + "group_type": "Groepstype" + }, + "description": "Selecteer groepstype" + }, + "light": { + "data": { + "entities": "Leden", + "name": "Naam", + "title": "Nieuwe groep" + }, + "description": "Selecteer groepsopties" + }, + "light_options": { + "data": { + "entities": "Groepsleden" + }, + "description": "Selecteer groepsopties" + }, + "media_player": { + "data": { + "entities": "Leden", + "name": "Naam", + "title": "Nieuwe groep" + }, + "description": "Selecteer groepsopties" + }, + "media_player_options": { + "data": { + "entities": "Groepsleden" + }, + "description": "Selecteer groepsopties" + }, + "user": { + "data": { + "group_type": "Groepstype" + }, + "title": "Nieuwe groep" + } + } + }, + "options": { + "step": { + "binary_sensor_options": { + "data": { + "all": "Alle entiteiten", + "entities": "Leden" + } + }, + "cover_options": { + "data": { + "entities": "Leden" + } + }, + "fan_options": { + "data": { + "entities": "Leden" + } + }, + "light_options": { + "data": { + "entities": "Leden" + } + }, + "media_player_options": { + "data": { + "entities": "Leden" + } + } + } + }, "state": { "_": { "closed": "Gesloten", diff --git a/homeassistant/components/group/translations/no.json b/homeassistant/components/group/translations/no.json index 763021190c1..0046479d686 100644 --- a/homeassistant/components/group/translations/no.json +++ b/homeassistant/components/group/translations/no.json @@ -1,4 +1,115 @@ { + "config": { + "step": { + "binary_sensor": { + "data": { + "all": "Alle enheter", + "entities": "Medlemmer", + "name": "Navn" + }, + "description": "Hvis \"alle enheter\" er aktivert, er gruppens tilstand p\u00e5 bare hvis alle medlemmene er p\u00e5. Hvis \"alle enheter\" er deaktivert, er gruppens tilstand p\u00e5 hvis et medlem er p\u00e5.", + "title": "Ny gruppe" + }, + "cover": { + "data": { + "entities": "Medlemmer", + "name": "Navn", + "title": "Ny gruppe" + }, + "description": "Velg gruppealternativer" + }, + "cover_options": { + "data": { + "entities": "Gruppemedlemmer" + }, + "description": "Velg gruppealternativer" + }, + "fan": { + "data": { + "entities": "Medlemmer", + "name": "Navn", + "title": "Ny gruppe" + }, + "description": "Velg gruppealternativer" + }, + "fan_options": { + "data": { + "entities": "Gruppemedlemmer" + }, + "description": "Velg gruppealternativer" + }, + "init": { + "data": { + "group_type": "Gruppetype" + }, + "description": "Velg gruppetype" + }, + "light": { + "data": { + "entities": "Medlemmer", + "name": "Navn", + "title": "Ny gruppe" + }, + "description": "Velg gruppealternativer" + }, + "light_options": { + "data": { + "entities": "Gruppemedlemmer" + }, + "description": "Velg gruppealternativer" + }, + "media_player": { + "data": { + "entities": "Medlemmer", + "name": "Navn", + "title": "Ny gruppe" + }, + "description": "Velg gruppealternativer" + }, + "media_player_options": { + "data": { + "entities": "Gruppemedlemmer" + }, + "description": "Velg gruppealternativer" + }, + "user": { + "data": { + "group_type": "Gruppetype" + }, + "title": "Ny gruppe" + } + } + }, + "options": { + "step": { + "binary_sensor_options": { + "data": { + "all": "Alle enheter", + "entities": "Medlemmer" + } + }, + "cover_options": { + "data": { + "entities": "Medlemmer" + } + }, + "fan_options": { + "data": { + "entities": "Medlemmer" + } + }, + "light_options": { + "data": { + "entities": "Medlemmer" + } + }, + "media_player_options": { + "data": { + "entities": "Medlemmer" + } + } + } + }, "state": { "_": { "closed": "Lukket", diff --git a/homeassistant/components/group/translations/pl.json b/homeassistant/components/group/translations/pl.json index b45f3f85cfb..08595dd8a59 100644 --- a/homeassistant/components/group/translations/pl.json +++ b/homeassistant/components/group/translations/pl.json @@ -1,4 +1,115 @@ { + "config": { + "step": { + "binary_sensor": { + "data": { + "all": "Wszystkie encje", + "entities": "Encje", + "name": "Nazwa" + }, + "description": "Je\u015bli \u201ewszystkie encje\u201d jest w\u0142\u0105czone, stan grupy jest w\u0142\u0105czony tylko wtedy, gdy wszystkie encje s\u0105 w\u0142\u0105czone. Je\u015bli \u201ewszystkie encje\u201d s\u0105 wy\u0142\u0105czone, stan grupy jest w\u0142\u0105czony, je\u015bli kt\u00f3rakolwiek encja jest w\u0142\u0105czona.", + "title": "Nowa grupa" + }, + "cover": { + "data": { + "entities": "Encje", + "name": "Nazwa", + "title": "Nowa grupa" + }, + "description": "Wybierz opcje grupy" + }, + "cover_options": { + "data": { + "entities": "Encje w grupie" + }, + "description": "Wybierz opcje grupy" + }, + "fan": { + "data": { + "entities": "Encje", + "name": "Nazwa", + "title": "Nowa grupa" + }, + "description": "Wybierz opcje grupy" + }, + "fan_options": { + "data": { + "entities": "Encje w grupie" + }, + "description": "Wybierz opcje grupy" + }, + "init": { + "data": { + "group_type": "Rodzaj grupy" + }, + "description": "Wybierz rodzaj grupy" + }, + "light": { + "data": { + "entities": "Encje", + "name": "Nazwa", + "title": "Nowa grupa" + }, + "description": "Wybierz opcje grupy" + }, + "light_options": { + "data": { + "entities": "Encje w grupie" + }, + "description": "Wybierz opcje grupy" + }, + "media_player": { + "data": { + "entities": "Encje", + "name": "Nazwa", + "title": "Nowa grupa" + }, + "description": "Wybierz opcje grupy" + }, + "media_player_options": { + "data": { + "entities": "Encje w grupie" + }, + "description": "Wybierz opcje grupy" + }, + "user": { + "data": { + "group_type": "Rodzaj grupy" + }, + "title": "Nowa grupa" + } + } + }, + "options": { + "step": { + "binary_sensor_options": { + "data": { + "all": "Wszystkie encje", + "entities": "Encje" + } + }, + "cover_options": { + "data": { + "entities": "Encje" + } + }, + "fan_options": { + "data": { + "entities": "Encje" + } + }, + "light_options": { + "data": { + "entities": "Encje" + } + }, + "media_player_options": { + "data": { + "entities": "Encje" + } + } + } + }, "state": { "_": { "closed": "zamkni\u0119te", diff --git a/homeassistant/components/group/translations/pt-BR.json b/homeassistant/components/group/translations/pt-BR.json index e0cbc7c02fd..5959ba66da7 100644 --- a/homeassistant/components/group/translations/pt-BR.json +++ b/homeassistant/components/group/translations/pt-BR.json @@ -1,4 +1,115 @@ { + "config": { + "step": { + "binary_sensor": { + "data": { + "all": "Todas as entidades", + "entities": "Membros", + "name": "Nome" + }, + "description": "Se \"todas as entidades\" estiver habilitada, o estado do grupo estar\u00e1 ativado somente se todos os membros estiverem ativados. Se \"todas as entidades\" estiver desabilitada, o estado do grupo estar\u00e1 ativado se algum membro estiver ativado.", + "title": "Novo grupo" + }, + "cover": { + "data": { + "entities": "Membros", + "name": "Nome", + "title": "Novo grupo" + }, + "description": "Selecione as op\u00e7\u00f5es do grupo" + }, + "cover_options": { + "data": { + "entities": "Membros do grupo" + }, + "description": "Selecione as op\u00e7\u00f5es do grupo" + }, + "fan": { + "data": { + "entities": "Membros", + "name": "Nome", + "title": "Novo grupo" + }, + "description": "Selecione as op\u00e7\u00f5es do grupo" + }, + "fan_options": { + "data": { + "entities": "Membros do grupo" + }, + "description": "Selecione as op\u00e7\u00f5es do grupo" + }, + "init": { + "data": { + "group_type": "Tipo de grupo" + }, + "description": "Selecione o tipo de grupo" + }, + "light": { + "data": { + "entities": "Membros", + "name": "Nome", + "title": "Novo grupo" + }, + "description": "Selecione as op\u00e7\u00f5es do grupo" + }, + "light_options": { + "data": { + "entities": "Membros do grupo" + }, + "description": "Selecione as op\u00e7\u00f5es do grupo" + }, + "media_player": { + "data": { + "entities": "Membros", + "name": "Nome", + "title": "Novo grupo" + }, + "description": "Selecione as op\u00e7\u00f5es do grupo" + }, + "media_player_options": { + "data": { + "entities": "Membros do grupo" + }, + "description": "Selecione as op\u00e7\u00f5es do grupo" + }, + "user": { + "data": { + "group_type": "Tipo de grupo" + }, + "title": "Novo grupo" + } + } + }, + "options": { + "step": { + "binary_sensor_options": { + "data": { + "all": "Todas as entidades", + "entities": "Membros" + } + }, + "cover_options": { + "data": { + "entities": "Membros" + } + }, + "fan_options": { + "data": { + "entities": "Membros" + } + }, + "light_options": { + "data": { + "entities": "Membros" + } + }, + "media_player_options": { + "data": { + "entities": "Membros" + } + } + } + }, "state": { "_": { "closed": "Fechado", diff --git a/homeassistant/components/group/translations/pt.json b/homeassistant/components/group/translations/pt.json index 1dfe38b7b71..9333d19b997 100644 --- a/homeassistant/components/group/translations/pt.json +++ b/homeassistant/components/group/translations/pt.json @@ -1,4 +1,33 @@ { + "config": { + "step": { + "media_player": { + "data": { + "entities": "Membros", + "name": "Nome" + } + } + } + }, + "options": { + "step": { + "fan_options": { + "data": { + "entities": "Membros" + } + }, + "light_options": { + "data": { + "entities": "Membros" + } + }, + "media_player_options": { + "data": { + "entities": "Membros" + } + } + } + }, "state": { "_": { "closed": "Fechada", diff --git a/homeassistant/components/group/translations/ru.json b/homeassistant/components/group/translations/ru.json index 7e8ab4d8be1..baba258b654 100644 --- a/homeassistant/components/group/translations/ru.json +++ b/homeassistant/components/group/translations/ru.json @@ -1,4 +1,115 @@ { + "config": { + "step": { + "binary_sensor": { + "data": { + "all": "\u0412\u0441\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b", + "entities": "\u0423\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u0438", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435" + }, + "description": "\u0415\u0441\u043b\u0438 \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440 \"\u0412\u0441\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b\", \u0433\u0440\u0443\u043f\u043f\u0430 \u043f\u043e\u043b\u0443\u0447\u0438\u0442 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435 \"\u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e\" \u0442\u043e\u043b\u044c\u043a\u043e \u043a\u043e\u0433\u0434\u0430 \u0431\u0443\u0434\u0443\u0442 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u044b \u0432\u0441\u0435 \u0435\u0451 \u0443\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u0438. \u0412 \u043f\u0440\u043e\u0442\u0438\u0432\u043d\u043e\u043c \u0441\u043b\u0443\u0447\u0430\u0435 \u0433\u0440\u0443\u043f\u043f\u0430 \u043f\u043e\u043b\u0443\u0447\u0438\u0442 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435 \"\u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e\" \u043a\u043e\u0433\u0434\u0430 \u0431\u0443\u0434\u0435\u0442 \u0432\u043a\u043b\u044e\u0447\u0435\u043d \u043b\u044e\u0431\u043e\u0439 \u0438\u0437 \u0435\u0451 \u0443\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u043e\u0432.", + "title": "\u041d\u043e\u0432\u0430\u044f \u0433\u0440\u0443\u043f\u043f\u0430" + }, + "cover": { + "data": { + "entities": "\u0423\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u0438", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", + "title": "\u041d\u043e\u0432\u0430\u044f \u0433\u0440\u0443\u043f\u043f\u0430" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0433\u0440\u0443\u043f\u043f\u044b." + }, + "cover_options": { + "data": { + "entities": "\u0423\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u0438 \u0433\u0440\u0443\u043f\u043f\u044b" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0433\u0440\u0443\u043f\u043f\u044b." + }, + "fan": { + "data": { + "entities": "\u0423\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u0438", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", + "title": "\u041d\u043e\u0432\u0430\u044f \u0433\u0440\u0443\u043f\u043f\u0430" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0433\u0440\u0443\u043f\u043f\u044b." + }, + "fan_options": { + "data": { + "entities": "\u0423\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u0438 \u0433\u0440\u0443\u043f\u043f\u044b" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0433\u0440\u0443\u043f\u043f\u044b." + }, + "init": { + "data": { + "group_type": "\u0422\u0438\u043f \u0433\u0440\u0443\u043f\u043f\u044b" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0442\u0438\u043f \u0433\u0440\u0443\u043f\u043f\u044b." + }, + "light": { + "data": { + "entities": "\u0423\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u0438", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", + "title": "\u041d\u043e\u0432\u0430\u044f \u0433\u0440\u0443\u043f\u043f\u0430" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0433\u0440\u0443\u043f\u043f\u044b." + }, + "light_options": { + "data": { + "entities": "\u0423\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u0438 \u0433\u0440\u0443\u043f\u043f\u044b" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0433\u0440\u0443\u043f\u043f\u044b." + }, + "media_player": { + "data": { + "entities": "\u0423\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u0438", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", + "title": "\u041d\u043e\u0432\u0430\u044f \u0433\u0440\u0443\u043f\u043f\u0430" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0433\u0440\u0443\u043f\u043f\u044b." + }, + "media_player_options": { + "data": { + "entities": "\u0423\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u0438 \u0433\u0440\u0443\u043f\u043f\u044b" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0433\u0440\u0443\u043f\u043f\u044b." + }, + "user": { + "data": { + "group_type": "\u0422\u0438\u043f \u0433\u0440\u0443\u043f\u043f\u044b" + }, + "title": "\u041d\u043e\u0432\u0430\u044f \u0433\u0440\u0443\u043f\u043f\u0430" + } + } + }, + "options": { + "step": { + "binary_sensor_options": { + "data": { + "all": "\u0412\u0441\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b", + "entities": "\u0423\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u0438" + } + }, + "cover_options": { + "data": { + "entities": "\u0423\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u0438" + } + }, + "fan_options": { + "data": { + "entities": "\u0423\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u0438" + } + }, + "light_options": { + "data": { + "entities": "\u0423\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u0438" + } + }, + "media_player_options": { + "data": { + "entities": "\u0423\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u0438" + } + } + } + }, "state": { "_": { "closed": "\u0417\u0430\u043a\u0440\u044b\u0442\u043e", diff --git a/homeassistant/components/group/translations/sv.json b/homeassistant/components/group/translations/sv.json index 50b3f605682..9ee5390db8a 100644 --- a/homeassistant/components/group/translations/sv.json +++ b/homeassistant/components/group/translations/sv.json @@ -1,4 +1,72 @@ { + "config": { + "step": { + "binary_sensor": { + "data": { + "all": "Alla entiteter", + "entities": "Medlemmar", + "name": "Namn" + }, + "title": "Ny grupp" + }, + "cover": { + "data": { + "title": "Ny grupp" + } + }, + "fan": { + "data": { + "title": "Ny grupp" + } + }, + "light": { + "data": { + "title": "Ny grupp" + } + }, + "media_player": { + "data": { + "title": "Ny grupp" + } + }, + "user": { + "data": { + "group_type": "Grupptyp" + }, + "title": "Ny grupp" + } + } + }, + "options": { + "step": { + "binary_sensor_options": { + "data": { + "all": "Alla entiteter", + "entities": "Medlemmar" + } + }, + "cover_options": { + "data": { + "entities": "Medlemmar" + } + }, + "fan_options": { + "data": { + "entities": "Medlemmar" + } + }, + "light_options": { + "data": { + "entities": "Medlemmar" + } + }, + "media_player_options": { + "data": { + "entities": "Medlemmar" + } + } + } + }, "state": { "_": { "closed": "St\u00e4ngd", diff --git a/homeassistant/components/group/translations/tr.json b/homeassistant/components/group/translations/tr.json index b0a2fb76c9c..bbb4600a4ed 100644 --- a/homeassistant/components/group/translations/tr.json +++ b/homeassistant/components/group/translations/tr.json @@ -1,4 +1,66 @@ { + "config": { + "step": { + "cover": { + "data": { + "entities": "Grup \u00fcyeleri", + "name": "Grup ismi" + }, + "description": "Grup se\u00e7eneklerini se\u00e7in" + }, + "cover_options": { + "data": { + "entities": "Grup \u00fcyeleri" + }, + "description": "Grup se\u00e7eneklerini se\u00e7in" + }, + "fan": { + "data": { + "entities": "Grup \u00fcyeleri", + "name": "Grup ismi" + }, + "description": "Grup se\u00e7eneklerini se\u00e7in" + }, + "fan_options": { + "data": { + "entities": "Grup \u00fcyeleri" + }, + "description": "Grup se\u00e7eneklerini se\u00e7in" + }, + "init": { + "data": { + "group_type": "Grup t\u00fcr\u00fc" + }, + "description": "Grup t\u00fcr\u00fcn\u00fc se\u00e7in" + }, + "light": { + "data": { + "entities": "Grup \u00fcyeleri", + "name": "Grup ismi" + }, + "description": "Grup se\u00e7eneklerini se\u00e7in" + }, + "light_options": { + "data": { + "entities": "Grup \u00fcyeleri" + }, + "description": "Grup se\u00e7eneklerini se\u00e7in" + }, + "media_player": { + "data": { + "entities": "Grup \u00fcyeleri", + "name": "Grup ismi" + }, + "description": "Grup se\u00e7eneklerini se\u00e7in" + }, + "media_player_options": { + "data": { + "entities": "Grup \u00fcyeleri" + }, + "description": "Grup se\u00e7eneklerini se\u00e7in" + } + } + }, "state": { "_": { "closed": "Kapal\u0131", diff --git a/homeassistant/components/group/translations/zh-Hant.json b/homeassistant/components/group/translations/zh-Hant.json index 790feb0c5ff..9187455ad17 100644 --- a/homeassistant/components/group/translations/zh-Hant.json +++ b/homeassistant/components/group/translations/zh-Hant.json @@ -1,4 +1,115 @@ { + "config": { + "step": { + "binary_sensor": { + "data": { + "all": "\u6240\u6709\u5be6\u9ad4", + "entities": "\u6210\u54e1", + "name": "\u540d\u7a31" + }, + "description": "\u5047\u5982\u958b\u555f \"\u6240\u6709\u5be6\u9ad4\"\uff0c\u50c5\u65bc\u7576\u6240\u6709\u6210\u54e1\u90fd\u70ba\u958b\u555f\u6642\u3001\u88dd\u614b\u624d\u6703\u986f\u793a\u70ba\u958b\u555f\u3002\u5047\u5982 \"\u6240\u6709\u5be6\u9ad4\" \u70ba\u95dc\u9589\u3001\u5247\u4efb\u4f55\u6210\u54e1\u958b\u59cb\u6642\uff0c\u7686\u6703\u986f\u793a\u70ba\u958b\u555f\u3002", + "title": "\u65b0\u589e\u7fa4\u7d44" + }, + "cover": { + "data": { + "entities": "\u6210\u54e1", + "name": "\u540d\u7a31", + "title": "\u65b0\u589e\u7fa4\u7d44" + }, + "description": "\u9078\u64c7\u7fa4\u7d44\u9078\u9805" + }, + "cover_options": { + "data": { + "entities": "\u7fa4\u7d44\u6210\u54e1" + }, + "description": "\u9078\u64c7\u7fa4\u7d44\u9078\u9805" + }, + "fan": { + "data": { + "entities": "\u6210\u54e1", + "name": "\u540d\u7a31", + "title": "\u65b0\u589e\u7fa4\u7d44" + }, + "description": "\u9078\u64c7\u7fa4\u7d44\u9078\u9805" + }, + "fan_options": { + "data": { + "entities": "\u7fa4\u7d44\u6210\u54e1" + }, + "description": "\u9078\u64c7\u7fa4\u7d44\u9078\u9805" + }, + "init": { + "data": { + "group_type": "\u7fa4\u7d44\u985e\u5225" + }, + "description": "\u9078\u64c7\u7fa4\u7d44\u985e\u5225" + }, + "light": { + "data": { + "entities": "\u6210\u54e1", + "name": "\u540d\u7a31", + "title": "\u65b0\u589e\u7fa4\u7d44" + }, + "description": "\u9078\u64c7\u7fa4\u7d44\u9078\u9805" + }, + "light_options": { + "data": { + "entities": "\u7fa4\u7d44\u6210\u54e1" + }, + "description": "\u9078\u64c7\u7fa4\u7d44\u9078\u9805" + }, + "media_player": { + "data": { + "entities": "\u6210\u54e1", + "name": "\u540d\u7a31", + "title": "\u65b0\u589e\u7fa4\u7d44" + }, + "description": "\u9078\u64c7\u7fa4\u7d44\u9078\u9805" + }, + "media_player_options": { + "data": { + "entities": "\u7fa4\u7d44\u6210\u54e1" + }, + "description": "\u9078\u64c7\u7fa4\u7d44\u9078\u9805" + }, + "user": { + "data": { + "group_type": "\u7fa4\u7d44\u985e\u5225" + }, + "title": "\u65b0\u589e\u7fa4\u7d44" + } + } + }, + "options": { + "step": { + "binary_sensor_options": { + "data": { + "all": "\u6240\u6709\u5be6\u9ad4", + "entities": "\u6210\u54e1" + } + }, + "cover_options": { + "data": { + "entities": "\u6210\u54e1" + } + }, + "fan_options": { + "data": { + "entities": "\u6210\u54e1" + } + }, + "light_options": { + "data": { + "entities": "\u6210\u54e1" + } + }, + "media_player_options": { + "data": { + "entities": "\u6210\u54e1" + } + } + } + }, "state": { "_": { "closed": "\u95dc\u9589", diff --git a/homeassistant/components/growatt_server/strings.json b/homeassistant/components/growatt_server/strings.json index 45e25c0ba33..9717aa217f3 100644 --- a/homeassistant/components/growatt_server/strings.json +++ b/homeassistant/components/growatt_server/strings.json @@ -1,28 +1,28 @@ { - "config": { - "abort": { - "no_plants": "No plants have been found on this account" - }, - "error": { - "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" - }, - "step": { - "plant": { - "data": { - "plant_id": "Plant" - }, - "title": "Select your plant" - }, - "user": { - "data": { - "name": "[%key:common::config_flow::data::name%]", - "password": "[%key:common::config_flow::data::password%]", - "username": "[%key:common::config_flow::data::username%]", - "url": "[%key:common::config_flow::data::url%]" - }, - "title": "Enter your Growatt information" - } - } + "config": { + "abort": { + "no_plants": "No plants have been found on this account" }, - "title": "Growatt Server" -} \ No newline at end of file + "error": { + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" + }, + "step": { + "plant": { + "data": { + "plant_id": "Plant" + }, + "title": "Select your plant" + }, + "user": { + "data": { + "name": "[%key:common::config_flow::data::name%]", + "password": "[%key:common::config_flow::data::password%]", + "username": "[%key:common::config_flow::data::username%]", + "url": "[%key:common::config_flow::data::url%]" + }, + "title": "Enter your Growatt information" + } + } + }, + "title": "Growatt Server" +} diff --git a/homeassistant/components/growatt_server/translations/fr.json b/homeassistant/components/growatt_server/translations/fr.json index 939111c4151..f86c8041502 100644 --- a/homeassistant/components/growatt_server/translations/fr.json +++ b/homeassistant/components/growatt_server/translations/fr.json @@ -4,7 +4,7 @@ "no_plants": "Aucune plante n'a \u00e9t\u00e9 trouv\u00e9e sur ce compte" }, "error": { - "invalid_auth": "Authentification invalide" + "invalid_auth": "Authentification non valide" }, "step": { "plant": { diff --git a/homeassistant/components/guardian/manifest.json b/homeassistant/components/guardian/manifest.json index 7ba3e1971f4..9a663dc104e 100644 --- a/homeassistant/components/guardian/manifest.json +++ b/homeassistant/components/guardian/manifest.json @@ -8,18 +8,18 @@ "codeowners": ["@bachya"], "iot_class": "local_polling", "dhcp": [ - { - "hostname": "gvc*", - "macaddress": "30AEA4*" - }, - { - "hostname": "gvc*", - "macaddress": "B4E62D*" - }, - { - "hostname": "guardian*", - "macaddress": "30AEA4*" - } + { + "hostname": "gvc*", + "macaddress": "30AEA4*" + }, + { + "hostname": "gvc*", + "macaddress": "B4E62D*" + }, + { + "hostname": "guardian*", + "macaddress": "30AEA4*" + } ], "loggers": ["aioguardian"] } diff --git a/homeassistant/components/habitica/translations/fr.json b/homeassistant/components/habitica/translations/fr.json index 00fcd36a508..9f04d9ed588 100644 --- a/homeassistant/components/habitica/translations/fr.json +++ b/homeassistant/components/habitica/translations/fr.json @@ -1,13 +1,13 @@ { "config": { "error": { - "invalid_credentials": "Authentification invalide", + "invalid_credentials": "Authentification non valide", "unknown": "Erreur inattendue" }, "step": { "user": { "data": { - "api_key": "Cl\u00e9 API", + "api_key": "Cl\u00e9 d'API", "api_user": "ID utilisateur de l'API d'Habitica", "name": "Remplacez le nom d\u2019utilisateur d\u2019Habitica. Sera utilis\u00e9 pour les appels de service", "url": "URL" diff --git a/homeassistant/components/hangouts/hangouts_bot.py b/homeassistant/components/hangouts/hangouts_bot.py index c29fd9b2cdf..11181c5b0ff 100644 --- a/homeassistant/components/hangouts/hangouts_bot.py +++ b/homeassistant/components/hangouts/hangouts_bot.py @@ -194,7 +194,11 @@ class HangoutsBot: return await self.hass.helpers.intent.async_handle( DOMAIN, intent_type, - {key: {"value": value} for key, value in match.groupdict().items()}, + {"conv_id": {"value": conv_id}} + | { + key: {"value": value} + for key, value in match.groupdict().items() + }, text, ) diff --git a/homeassistant/components/hangouts/manifest.json b/homeassistant/components/hangouts/manifest.json index 983dc60414a..b8b1004bb78 100644 --- a/homeassistant/components/hangouts/manifest.json +++ b/homeassistant/components/hangouts/manifest.json @@ -1,9 +1,9 @@ { "domain": "hangouts", - "name": "Google Hangouts", + "name": "Google Chat", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/hangouts", - "requirements": ["hangups==0.4.17"], + "requirements": ["hangups==0.4.18"], "codeowners": [], "iot_class": "cloud_push", "loggers": ["hangups", "urwid"] diff --git a/homeassistant/components/hangouts/strings.json b/homeassistant/components/hangouts/strings.json index 0128363a1ab..fcc2da456bb 100644 --- a/homeassistant/components/hangouts/strings.json +++ b/homeassistant/components/hangouts/strings.json @@ -16,7 +16,7 @@ "password": "[%key:common::config_flow::data::password%]", "authorization_code": "Authorization Code (required for manual authentication)" }, - "title": "Google Hangouts Login" + "title": "Google Chat Login" }, "2fa": { "data": { diff --git a/homeassistant/components/hangouts/translations/de.json b/homeassistant/components/hangouts/translations/de.json index 42770308346..02481670a09 100644 --- a/homeassistant/components/hangouts/translations/de.json +++ b/homeassistant/components/hangouts/translations/de.json @@ -14,7 +14,6 @@ "data": { "2fa": "2FA PIN" }, - "description": "Leer", "title": "2-Faktor-Authentifizierung" }, "user": { diff --git a/homeassistant/components/hangouts/translations/el.json b/homeassistant/components/hangouts/translations/el.json index c4e46912a51..4b453c5bbaa 100644 --- a/homeassistant/components/hangouts/translations/el.json +++ b/homeassistant/components/hangouts/translations/el.json @@ -14,6 +14,7 @@ "data": { "2fa": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 PIN \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7\u03c2 2 \u03c0\u03b1\u03c1\u03b1\u03b3\u03cc\u03bd\u03c4\u03c9\u03bd" }, + "description": "\u039a\u03b5\u03bd\u03cc", "title": "\u03a0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 2 \u03c0\u03b1\u03c1\u03b1\u03b3\u03cc\u03bd\u03c4\u03c9\u03bd" }, "user": { diff --git a/homeassistant/components/hangouts/translations/en.json b/homeassistant/components/hangouts/translations/en.json index b2d7076bd75..4829e843c6c 100644 --- a/homeassistant/components/hangouts/translations/en.json +++ b/homeassistant/components/hangouts/translations/en.json @@ -22,7 +22,7 @@ "email": "Email", "password": "Password" }, - "title": "Google Hangouts Login" + "title": "Google Chat Login" } } } diff --git a/homeassistant/components/hangouts/translations/fr.json b/homeassistant/components/hangouts/translations/fr.json index ab2d2fc5168..da674b1c775 100644 --- a/homeassistant/components/hangouts/translations/fr.json +++ b/homeassistant/components/hangouts/translations/fr.json @@ -20,7 +20,7 @@ "user": { "data": { "authorization_code": "Code d'autorisation (requis pour l'authentification manuelle)", - "email": "Email", + "email": "Courriel", "password": "Mot de passe" }, "description": "Vide", diff --git a/homeassistant/components/harmony/subscriber.py b/homeassistant/components/harmony/subscriber.py index f71b486fd16..2731d8555f0 100644 --- a/homeassistant/components/harmony/subscriber.py +++ b/homeassistant/components/harmony/subscriber.py @@ -3,7 +3,6 @@ import asyncio import logging -# pylint: disable-next=deprecated-typing-alias # Issue with Python 3.9.0 and 3.9.1 with collections.abc.Callable # https://bugs.python.org/issue42965 from typing import Any, Callable, NamedTuple, Optional diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index b5549c1b5e4..7b8608a7fad 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -42,7 +42,7 @@ from homeassistant.helpers.device_registry import ( ) from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.typing import ConfigType -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.loader import bind_hass from homeassistant.util.dt import utcnow @@ -51,6 +51,8 @@ from .auth import async_setup_auth_view from .const import ( ATTR_ADDON, ATTR_ADDONS, + ATTR_AUTO_UPDATE, + ATTR_CHANGELOG, ATTR_DISCOVERY, ATTR_FOLDERS, ATTR_HOMEASSISTANT, @@ -63,6 +65,9 @@ from .const import ( ATTR_URL, ATTR_VERSION, DATA_KEY_ADDONS, + DATA_KEY_CORE, + DATA_KEY_OS, + DATA_KEY_SUPERVISOR, DOMAIN, SupervisorEntityModel, ) @@ -77,7 +82,7 @@ _LOGGER = logging.getLogger(__name__) STORAGE_KEY = DOMAIN STORAGE_VERSION = 1 -PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR] +PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR, Platform.UPDATE] CONF_FRONTEND_REPO = "development_repo" @@ -93,6 +98,8 @@ DATA_STORE = "hassio_store" DATA_INFO = "hassio_info" DATA_OS_INFO = "hassio_os_info" DATA_SUPERVISOR_INFO = "hassio_supervisor_info" +DATA_ADDONS_CHANGELOGS = "hassio_addons_changelogs" +DATA_ADDONS_INFO = "hassio_addons_info" DATA_ADDONS_STATS = "hassio_addons_stats" HASSIO_UPDATE_INTERVAL = timedelta(minutes=5) @@ -239,14 +246,22 @@ async def async_uninstall_addon(hass: HomeAssistant, slug: str) -> dict: @bind_hass @api_data -async def async_update_addon(hass: HomeAssistant, slug: str) -> dict: +async def async_update_addon( + hass: HomeAssistant, + slug: str, + backup: bool = False, +) -> dict: """Update add-on. The caller of the function should handle HassioAPIError. """ hassio = hass.data[DOMAIN] command = f"/addons/{slug}/update" - return await hassio.send_command(command, timeout=None) + return await hassio.send_command( + command, + payload={"backup": backup}, + timeout=None, + ) @bind_hass @@ -323,6 +338,52 @@ async def async_create_backup( return await hassio.send_command(command, payload=payload, timeout=None) +@bind_hass +@api_data +async def async_update_os(hass: HomeAssistant, version: str | None = None) -> dict: + """Update Home Assistant Operating System. + + The caller of the function should handle HassioAPIError. + """ + hassio = hass.data[DOMAIN] + command = "/os/update" + return await hassio.send_command( + command, + payload={"version": version}, + timeout=None, + ) + + +@bind_hass +@api_data +async def async_update_supervisor(hass: HomeAssistant) -> dict: + """Update Home Assistant Supervisor. + + The caller of the function should handle HassioAPIError. + """ + hassio = hass.data[DOMAIN] + command = "/supervisor/update" + return await hassio.send_command(command, timeout=None) + + +@bind_hass +@api_data +async def async_update_core( + hass: HomeAssistant, version: str | None = None, backup: bool = False +) -> dict: + """Update Home Assistant Core. + + The caller of the function should handle HassioAPIError. + """ + hassio = hass.data[DOMAIN] + command = "/core/update" + return await hassio.send_command( + command, + payload={"version": version, "backup": backup}, + timeout=None, + ) + + @callback @bind_hass def get_info(hass): @@ -363,6 +424,16 @@ def get_supervisor_info(hass): return hass.data.get(DATA_SUPERVISOR_INFO) +@callback +@bind_hass +def get_addons_info(hass): + """Return Addons info. + + Async friendly. + """ + return hass.data.get(DATA_ADDONS_INFO) + + @callback @bind_hass def get_addons_stats(hass): @@ -373,6 +444,16 @@ def get_addons_stats(hass): return hass.data.get(DATA_ADDONS_STATS) +@callback +@bind_hass +def get_addons_changelogs(hass): + """Return Addons changelogs. + + Async friendly. + """ + return hass.data.get(DATA_ADDONS_CHANGELOGS) + + @callback @bind_hass def get_os_info(hass): @@ -528,11 +609,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: DOMAIN, service, async_service_handler, schema=settings.schema ) - async def update_addon_stats(slug): - """Update single addon stats.""" - stats = await hassio.get_addon_stats(slug) - return (slug, stats) - async def update_info_data(now): """Update last available supervisor information.""" @@ -553,18 +629,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: hassio.get_os_info(), ) - addons = [ - addon - for addon in hass.data[DATA_SUPERVISOR_INFO].get("addons", []) - if addon[ATTR_STATE] == ATTR_STARTED - ] - stats_data = await asyncio.gather( - *[update_addon_stats(addon[ATTR_SLUG]) for addon in addons] - ) - hass.data[DATA_ADDONS_STATS] = dict(stats_data) - - if ADDONS_COORDINATOR in hass.data: - await hass.data[ADDONS_COORDINATOR].async_refresh() except HassioAPIError as err: _LOGGER.warning("Can't read Supervisor data: %s", err) @@ -699,6 +763,42 @@ def async_register_os_in_dev_reg( dev_reg.async_get_or_create(config_entry_id=entry_id, **params) +@callback +def async_register_core_in_dev_reg( + entry_id: str, + dev_reg: DeviceRegistry, + core_dict: dict[str, Any], +) -> None: + """Register OS in the device registry.""" + params = DeviceInfo( + identifiers={(DOMAIN, "core")}, + manufacturer="Home Assistant", + model=SupervisorEntityModel.CORE, + sw_version=core_dict[ATTR_VERSION], + name="Home Assistant Core", + entry_type=DeviceEntryType.SERVICE, + ) + dev_reg.async_get_or_create(config_entry_id=entry_id, **params) + + +@callback +def async_register_supervisor_in_dev_reg( + entry_id: str, + dev_reg: DeviceRegistry, + supervisor_dict: dict[str, Any], +) -> None: + """Register OS in the device registry.""" + params = DeviceInfo( + identifiers={(DOMAIN, "supervisor")}, + manufacturer="Home Assistant", + model=SupervisorEntityModel.SUPERVIOSR, + sw_version=supervisor_dict[ATTR_VERSION], + name="Home Assistant Supervisor", + entry_type=DeviceEntryType.SERVICE, + ) + dev_reg.async_get_or_create(config_entry_id=entry_id, **params) + + @callback def async_remove_addons_from_dev_reg(dev_reg: DeviceRegistry, addons: set[str]) -> None: """Remove addons from the device registry.""" @@ -718,8 +818,9 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator): hass, _LOGGER, name=DOMAIN, - update_method=self._async_update_data, + update_interval=HASSIO_UPDATE_INTERVAL, ) + self.hassio: HassIO = hass.data[DOMAIN] self.data = {} self.entry_id = config_entry.entry_id self.dev_reg = dev_reg @@ -727,9 +828,16 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator): async def _async_update_data(self) -> dict[str, Any]: """Update data via library.""" + try: + await self.force_data_refresh() + except HassioAPIError as err: + raise UpdateFailed(f"Error on Supervisor API: {err}") from err + new_data = {} supervisor_info = get_supervisor_info(self.hass) + addons_info = get_addons_info(self.hass) addons_stats = get_addons_stats(self.hass) + addons_changelogs = get_addons_changelogs(self.hass) store_data = get_store(self.hass) repositories = { @@ -741,6 +849,10 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator): addon[ATTR_SLUG]: { **addon, **((addons_stats or {}).get(addon[ATTR_SLUG], {})), + ATTR_AUTO_UPDATE: addons_info.get(addon[ATTR_SLUG], {}).get( + ATTR_AUTO_UPDATE, False + ), + ATTR_CHANGELOG: (addons_changelogs or {}).get(addon[ATTR_SLUG]), ATTR_REPOSITORY: repositories.get( addon.get(ATTR_REPOSITORY), addon.get(ATTR_REPOSITORY, "") ), @@ -748,16 +860,25 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator): for addon in supervisor_info.get("addons", []) } if self.is_hass_os: - new_data["os"] = get_os_info(self.hass) + new_data[DATA_KEY_OS] = get_os_info(self.hass) + + new_data[DATA_KEY_CORE] = get_core_info(self.hass) + new_data[DATA_KEY_SUPERVISOR] = supervisor_info # If this is the initial refresh, register all addons and return the dict if not self.data: async_register_addons_in_dev_reg( self.entry_id, self.dev_reg, new_data[DATA_KEY_ADDONS].values() ) + async_register_core_in_dev_reg( + self.entry_id, self.dev_reg, new_data[DATA_KEY_CORE] + ) + async_register_supervisor_in_dev_reg( + self.entry_id, self.dev_reg, new_data[DATA_KEY_SUPERVISOR] + ) if self.is_hass_os: async_register_os_in_dev_reg( - self.entry_id, self.dev_reg, new_data["os"] + self.entry_id, self.dev_reg, new_data[DATA_KEY_OS] ) # Remove add-ons that are no longer installed from device registry @@ -782,3 +903,58 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator): return {} return new_data + + async def force_info_update_supervisor(self) -> None: + """Force update of the supervisor info.""" + self.hass.data[DATA_SUPERVISOR_INFO] = await self.hassio.get_supervisor_info() + await self.async_refresh() + + async def force_data_refresh(self) -> None: + """Force update of the addon info.""" + await self.hassio.refresh_updates() + ( + self.hass.data[DATA_INFO], + self.hass.data[DATA_CORE_INFO], + self.hass.data[DATA_SUPERVISOR_INFO], + self.hass.data[DATA_OS_INFO], + ) = await asyncio.gather( + self.hassio.get_info(), + self.hassio.get_core_info(), + self.hassio.get_supervisor_info(), + self.hassio.get_os_info(), + ) + + addons = [ + addon + for addon in self.hass.data[DATA_SUPERVISOR_INFO].get("addons", []) + if addon[ATTR_STATE] == ATTR_STARTED + ] + stats_data = await asyncio.gather( + *[self._update_addon_stats(addon[ATTR_SLUG]) for addon in addons] + ) + self.hass.data[DATA_ADDONS_STATS] = dict(stats_data) + self.hass.data[DATA_ADDONS_CHANGELOGS] = dict( + await asyncio.gather( + *[self._update_addon_changelog(addon[ATTR_SLUG]) for addon in addons] + ) + ) + self.hass.data[DATA_ADDONS_INFO] = dict( + await asyncio.gather( + *[self._update_addon_info(addon[ATTR_SLUG]) for addon in addons] + ) + ) + + async def _update_addon_stats(self, slug): + """Update single addon stats.""" + stats = await self.hassio.get_addon_stats(slug) + return (slug, stats) + + async def _update_addon_changelog(self, slug): + """Return the changelog for an add-on.""" + changelog = await self.hassio.get_addon_changelog(slug) + return (slug, changelog) + + async def _update_addon_info(self, slug): + """Return the info for an add-on.""" + info = await self.hassio.get_addon_info(slug) + return (slug, info) diff --git a/homeassistant/components/hassio/binary_sensor.py b/homeassistant/components/hassio/binary_sensor.py index b5525fe9ce4..c2bcd5eaf68 100644 --- a/homeassistant/components/hassio/binary_sensor.py +++ b/homeassistant/components/hassio/binary_sensor.py @@ -32,6 +32,7 @@ class HassioBinarySensorEntityDescription(BinarySensorEntityDescription): COMMON_ENTITY_DESCRIPTIONS = ( HassioBinarySensorEntityDescription( + # Deprecated, scheduled to be removed in 2022.6 device_class=BinarySensorDeviceClass.UPDATE, entity_registry_enabled_default=False, key=ATTR_UPDATE_AVAILABLE, diff --git a/homeassistant/components/hassio/const.py b/homeassistant/components/hassio/const.py index 7cdc87708ae..8c27fdebb17 100644 --- a/homeassistant/components/hassio/const.py +++ b/homeassistant/components/hassio/const.py @@ -39,10 +39,12 @@ WS_TYPE_SUBSCRIBE = "supervisor/subscribe" EVENT_SUPERVISOR_EVENT = "supervisor_event" +ATTR_AUTO_UPDATE = "auto_update" ATTR_VERSION = "version" ATTR_VERSION_LATEST = "version_latest" ATTR_UPDATE_AVAILABLE = "update_available" ATTR_CPU_PERCENT = "cpu_percent" +ATTR_CHANGELOG = "changelog" ATTR_MEMORY_PERCENT = "memory_percent" ATTR_SLUG = "slug" ATTR_STATE = "state" @@ -53,6 +55,8 @@ ATTR_REPOSITORY = "repository" DATA_KEY_ADDONS = "addons" DATA_KEY_OS = "os" +DATA_KEY_SUPERVISOR = "supervisor" +DATA_KEY_CORE = "core" class SupervisorEntityModel(str, Enum): @@ -60,3 +64,5 @@ class SupervisorEntityModel(str, Enum): ADDON = "Home Assistant Add-on" OS = "Home Assistant Operating System" + CORE = "Home Assistant Core" + SUPERVIOSR = "Home Assistant Supervisor" diff --git a/homeassistant/components/hassio/entity.py b/homeassistant/components/hassio/entity.py index 5dd41166c32..fb9f70f1417 100644 --- a/homeassistant/components/hassio/entity.py +++ b/homeassistant/components/hassio/entity.py @@ -8,10 +8,16 @@ from homeassistant.helpers.entity import DeviceInfo, EntityDescription from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import DOMAIN, HassioDataUpdateCoordinator -from .const import ATTR_SLUG, DATA_KEY_ADDONS, DATA_KEY_OS +from .const import ( + ATTR_SLUG, + DATA_KEY_ADDONS, + DATA_KEY_CORE, + DATA_KEY_OS, + DATA_KEY_SUPERVISOR, +) -class HassioAddonEntity(CoordinatorEntity): +class HassioAddonEntity(CoordinatorEntity[HassioDataUpdateCoordinator]): """Base entity for a Hass.io add-on.""" def __init__( @@ -33,12 +39,13 @@ class HassioAddonEntity(CoordinatorEntity): """Return True if entity is available.""" return ( super().available + and DATA_KEY_ADDONS in self.coordinator.data and self.entity_description.key - in self.coordinator.data[DATA_KEY_ADDONS][self._addon_slug] + in self.coordinator.data[DATA_KEY_ADDONS].get(self._addon_slug, {}) ) -class HassioOSEntity(CoordinatorEntity): +class HassioOSEntity(CoordinatorEntity[HassioDataUpdateCoordinator]): """Base Entity for Hass.io OS.""" def __init__( @@ -58,5 +65,57 @@ class HassioOSEntity(CoordinatorEntity): """Return True if entity is available.""" return ( super().available + and DATA_KEY_OS in self.coordinator.data and self.entity_description.key in self.coordinator.data[DATA_KEY_OS] ) + + +class HassioSupervisorEntity(CoordinatorEntity[HassioDataUpdateCoordinator]): + """Base Entity for Supervisor.""" + + def __init__( + self, + coordinator: HassioDataUpdateCoordinator, + entity_description: EntityDescription, + ) -> None: + """Initialize base entity.""" + super().__init__(coordinator) + self.entity_description = entity_description + self._attr_name = f"Home Assistant Supervisor: {entity_description.name}" + self._attr_unique_id = f"home_assistant_supervisor_{entity_description.key}" + self._attr_device_info = DeviceInfo(identifiers={(DOMAIN, "supervisor")}) + + @property + def available(self) -> bool: + """Return True if entity is available.""" + return ( + super().available + and DATA_KEY_OS in self.coordinator.data + and self.entity_description.key + in self.coordinator.data[DATA_KEY_SUPERVISOR] + ) + + +class HassioCoreEntity(CoordinatorEntity[HassioDataUpdateCoordinator]): + """Base Entity for Core.""" + + def __init__( + self, + coordinator: HassioDataUpdateCoordinator, + entity_description: EntityDescription, + ) -> None: + """Initialize base entity.""" + super().__init__(coordinator) + self.entity_description = entity_description + self._attr_name = f"Home Assistant Core: {entity_description.name}" + self._attr_unique_id = f"home_assistant_core_{entity_description.key}" + self._attr_device_info = DeviceInfo(identifiers={(DOMAIN, "core")}) + + @property + def available(self) -> bool: + """Return True if entity is available.""" + return ( + super().available + and DATA_KEY_CORE in self.coordinator.data + and self.entity_description.key in self.coordinator.data[DATA_KEY_CORE] + ) diff --git a/homeassistant/components/hassio/handler.py b/homeassistant/components/hassio/handler.py index 4a0312bcecb..4146753b753 100644 --- a/homeassistant/components/hassio/handler.py +++ b/homeassistant/components/hassio/handler.py @@ -127,6 +127,15 @@ class HassIO: """ return self.send_command(f"/addons/{addon}/stats", method="get") + def get_addon_changelog(self, addon): + """Return changelog for an Add-on. + + This method returns a coroutine. + """ + return self.send_command( + f"/addons/{addon}/changelog", method="get", return_text=True + ) + @api_data def get_store(self): """Return data from the store. @@ -159,6 +168,14 @@ class HassIO: """ return self.send_command("/homeassistant/stop") + @_api_bool + def refresh_updates(self): + """Refresh available updates. + + This method return a coroutine. + """ + return self.send_command("/refresh_updates", timeout=None) + @api_data def retrieve_discovery_messages(self): """Return all discovery data from Hass.io API. @@ -212,7 +229,14 @@ class HassIO: "/supervisor/options", payload={"diagnostics": diagnostics} ) - async def send_command(self, command, method="post", payload=None, timeout=10): + async def send_command( + self, + command, + method="post", + payload=None, + timeout=10, + return_text=False, + ): """Send API command to Hass.io. This method is a coroutine. @@ -230,8 +254,10 @@ class HassIO: _LOGGER.error("%s return code %d", command, request.status) raise HassioAPIError() - answer = await request.json() - return answer + if return_text: + return await request.text(encoding="utf-8") + + return await request.json() except asyncio.TimeoutError: _LOGGER.error("Timeout on %s request", command) diff --git a/homeassistant/components/hassio/manifest.json b/homeassistant/components/hassio/manifest.json index cc0bcc77265..5de80fdbd19 100644 --- a/homeassistant/components/hassio/manifest.json +++ b/homeassistant/components/hassio/manifest.json @@ -2,15 +2,9 @@ "domain": "hassio", "name": "Home Assistant Supervisor", "documentation": "https://www.home-assistant.io/integrations/hassio", - "dependencies": [ - "http" - ], - "after_dependencies": [ - "panel_custom" - ], - "codeowners": [ - "@home-assistant/supervisor" - ], + "dependencies": ["http"], + "after_dependencies": ["panel_custom"], + "codeowners": ["@home-assistant/supervisor"], "iot_class": "local_polling", "quality_scale": "internal" -} \ No newline at end of file +} diff --git a/homeassistant/components/hassio/translations/ja.json b/homeassistant/components/hassio/translations/ja.json index dbf79471303..da3409d91cd 100644 --- a/homeassistant/components/hassio/translations/ja.json +++ b/homeassistant/components/hassio/translations/ja.json @@ -10,7 +10,7 @@ "installed_addons": "\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u6e08\u307f\u306e\u30a2\u30c9\u30aa\u30f3", "supervisor_api": "Supervisor API", "supervisor_version": "Supervisor\u306e\u30d0\u30fc\u30b8\u30e7\u30f3", - "supported": "\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u307e\u3059", + "supported": "\u30b5\u30dd\u30fc\u30c8", "update_channel": "\u30a2\u30c3\u30d7\u30c7\u30fc\u30c8\u30c1\u30e3\u30f3\u30cd\u30eb", "version_api": "\u30d0\u30fc\u30b8\u30e7\u30f3API" } diff --git a/homeassistant/components/hassio/update.py b/homeassistant/components/hassio/update.py new file mode 100644 index 00000000000..8af3d88088a --- /dev/null +++ b/homeassistant/components/hassio/update.py @@ -0,0 +1,303 @@ +"""Update platform for Supervisor.""" +from __future__ import annotations + +from typing import Any + +from awesomeversion import AwesomeVersion, AwesomeVersionStrategy + +from homeassistant.components.update import ( + UpdateEntity, + UpdateEntityDescription, + UpdateEntityFeature, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_ICON, ATTR_NAME +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import ( + ADDONS_COORDINATOR, + async_update_addon, + async_update_core, + async_update_os, + async_update_supervisor, +) +from .const import ( + ATTR_AUTO_UPDATE, + ATTR_CHANGELOG, + ATTR_VERSION, + ATTR_VERSION_LATEST, + DATA_KEY_ADDONS, + DATA_KEY_CORE, + DATA_KEY_OS, + DATA_KEY_SUPERVISOR, +) +from .entity import ( + HassioAddonEntity, + HassioCoreEntity, + HassioOSEntity, + HassioSupervisorEntity, +) +from .handler import HassioAPIError + +ENTITY_DESCRIPTION = UpdateEntityDescription( + name="Update", + key=ATTR_VERSION_LATEST, +) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Supervisor update based on a config entry.""" + coordinator = hass.data[ADDONS_COORDINATOR] + + entities = [ + SupervisorSupervisorUpdateEntity( + coordinator=coordinator, + entity_description=ENTITY_DESCRIPTION, + ), + SupervisorCoreUpdateEntity( + coordinator=coordinator, + entity_description=ENTITY_DESCRIPTION, + ), + ] + + for addon in coordinator.data[DATA_KEY_ADDONS].values(): + entities.append( + SupervisorAddonUpdateEntity( + addon=addon, + coordinator=coordinator, + entity_description=ENTITY_DESCRIPTION, + ) + ) + + if coordinator.is_hass_os: + entities.append( + SupervisorOSUpdateEntity( + coordinator=coordinator, + entity_description=ENTITY_DESCRIPTION, + ) + ) + + async_add_entities(entities) + + +class SupervisorAddonUpdateEntity(HassioAddonEntity, UpdateEntity): + """Update entity to handle updates for the Supervisor add-ons.""" + + _attr_supported_features = ( + UpdateEntityFeature.INSTALL + | UpdateEntityFeature.BACKUP + | UpdateEntityFeature.RELEASE_NOTES + ) + + @property + def _addon_data(self) -> dict: + """Return the add-on data.""" + return self.coordinator.data[DATA_KEY_ADDONS][self._addon_slug] + + @property + def auto_update(self): + """Return true if auto-update is enabled for the add-on.""" + return self._addon_data[ATTR_AUTO_UPDATE] + + @property + def title(self) -> str | None: + """Return the title of the update.""" + return self._addon_data[ATTR_NAME] + + @property + def latest_version(self) -> str | None: + """Latest version available for install.""" + return self._addon_data[ATTR_VERSION_LATEST] + + @property + def installed_version(self) -> str | None: + """Version installed and in use.""" + return self._addon_data[ATTR_VERSION] + + @property + def release_summary(self) -> str | None: + """Release summary for the add-on.""" + return self._strip_release_notes() + + @property + def entity_picture(self) -> str | None: + """Return the icon of the add-on if any.""" + if not self.available: + return None + if self._addon_data[ATTR_ICON]: + return f"/api/hassio/addons/{self._addon_slug}/icon" + return None + + def _strip_release_notes(self) -> str | None: + """Strip the release notes to contain the needed sections.""" + if (notes := self._addon_data[ATTR_CHANGELOG]) is None: + return None + + if ( + f"# {self.latest_version}" in notes + and f"# {self.installed_version}" in notes + ): + # Split the release notes to only what is between the versions if we can + new_notes = notes.split(f"# {self.installed_version}")[0] + if f"# {self.latest_version}" in new_notes: + # Make sure the latest version is still there. + # This can be False if the order of the release notes are not correct + # In that case we just return the whole release notes + return new_notes + return notes + + async def async_release_notes(self) -> str | None: + """Return the release notes for the update.""" + return self._strip_release_notes() + + async def async_install( + self, + version: str | None = None, + backup: bool | None = False, + **kwargs: Any, + ) -> None: + """Install an update.""" + try: + await async_update_addon(self.hass, slug=self._addon_slug, backup=backup) + except HassioAPIError as err: + raise HomeAssistantError(f"Error updating {self.title}: {err}") from err + else: + await self.coordinator.force_info_update_supervisor() + + +class SupervisorOSUpdateEntity(HassioOSEntity, UpdateEntity): + """Update entity to handle updates for the Home Assistant Operating System.""" + + _attr_supported_features = ( + UpdateEntityFeature.INSTALL | UpdateEntityFeature.SPECIFIC_VERSION + ) + _attr_title = "Home Assistant Operating System" + + @property + def latest_version(self) -> str: + """Return native value of entity.""" + return self.coordinator.data[DATA_KEY_OS][ATTR_VERSION_LATEST] + + @property + def installed_version(self) -> str: + """Return native value of entity.""" + return self.coordinator.data[DATA_KEY_OS][ATTR_VERSION] + + @property + def entity_picture(self) -> str | None: + """Return the iconof the entity.""" + return "https://brands.home-assistant.io/homeassistant/icon.png" + + @property + def release_url(self) -> str | None: + """URL to the full release notes of the latest version available.""" + version = AwesomeVersion(self.latest_version) + if version.dev or version.strategy == AwesomeVersionStrategy.UNKNOWN: + return "https://github.com/home-assistant/operating-system/commits/dev" + return ( + f"https://github.com/home-assistant/operating-system/releases/tag/{version}" + ) + + async def async_install( + self, version: str | None, backup: bool, **kwargs: Any + ) -> None: + """Install an update.""" + try: + await async_update_os(self.hass, version) + except HassioAPIError as err: + raise HomeAssistantError( + f"Error updating Home Assistant Operating System: {err}" + ) from err + + +class SupervisorSupervisorUpdateEntity(HassioSupervisorEntity, UpdateEntity): + """Update entity to handle updates for the Home Assistant Supervisor.""" + + _attr_auto_update = True + _attr_supported_features = UpdateEntityFeature.INSTALL + _attr_title = "Home Assistant Supervisor" + + @property + def latest_version(self) -> str: + """Return native value of entity.""" + return self.coordinator.data[DATA_KEY_SUPERVISOR][ATTR_VERSION_LATEST] + + @property + def installed_version(self) -> str: + """Return native value of entity.""" + return self.coordinator.data[DATA_KEY_SUPERVISOR][ATTR_VERSION] + + @property + def release_url(self) -> str | None: + """URL to the full release notes of the latest version available.""" + version = AwesomeVersion(self.latest_version) + if version.dev or version.strategy == AwesomeVersionStrategy.UNKNOWN: + return "https://github.com/home-assistant/supervisor/commits/main" + return f"https://github.com/home-assistant/supervisor/releases/tag/{version}" + + @property + def entity_picture(self) -> str | None: + """Return the iconof the entity.""" + return "https://brands.home-assistant.io/hassio/icon.png" + + async def async_install( + self, version: str | None, backup: bool, **kwargs: Any + ) -> None: + """Install an update.""" + try: + await async_update_supervisor(self.hass) + except HassioAPIError as err: + raise HomeAssistantError( + f"Error updating Home Assistant Supervisor: {err}" + ) from err + + +class SupervisorCoreUpdateEntity(HassioCoreEntity, UpdateEntity): + """Update entity to handle updates for Home Assistant Core.""" + + _attr_supported_features = ( + UpdateEntityFeature.INSTALL + | UpdateEntityFeature.SPECIFIC_VERSION + | UpdateEntityFeature.BACKUP + ) + _attr_title = "Home Assistant Core" + + @property + def latest_version(self) -> str: + """Return native value of entity.""" + return self.coordinator.data[DATA_KEY_CORE][ATTR_VERSION_LATEST] + + @property + def installed_version(self) -> str: + """Return native value of entity.""" + return self.coordinator.data[DATA_KEY_CORE][ATTR_VERSION] + + @property + def entity_picture(self) -> str | None: + """Return the iconof the entity.""" + return "https://brands.home-assistant.io/homeassistant/icon.png" + + @property + def release_url(self) -> str | None: + """URL to the full release notes of the latest version available.""" + version = AwesomeVersion(self.latest_version) + if version.dev: + return "https://github.com/home-assistant/core/commits/dev" + return f"https://{'rc' if version.beta else 'www'}.home-assistant.io/latest-release-notes/" + + async def async_install( + self, version: str | None, backup: bool, **kwargs: Any + ) -> None: + """Install an update.""" + try: + await async_update_core(self.hass, version=version, backup=backup) + except HassioAPIError as err: + raise HomeAssistantError( + f"Error updating Home Assistant Core {err}" + ) from err diff --git a/homeassistant/components/hdmi_cec/services.yaml b/homeassistant/components/hdmi_cec/services.yaml index 943450a6796..7ad8b36473f 100644 --- a/homeassistant/components/hdmi_cec/services.yaml +++ b/homeassistant/components/hdmi_cec/services.yaml @@ -72,9 +72,9 @@ volume: selector: select: options: - - 'off' - - 'on' - - 'toggle' + - "off" + - "on" + - "toggle" up: name: Up description: Increases volume x levels. diff --git a/homeassistant/components/heos/media_player.py b/homeassistant/components/heos/media_player.py index dad97e25653..227f7737ef4 100644 --- a/homeassistant/components/heos/media_player.py +++ b/homeassistant/components/heos/media_player.py @@ -7,13 +7,18 @@ from operator import ior from pyheos import HeosError, const as heos_const +from homeassistant.components import media_source from homeassistant.components.media_player import MediaPlayerEntity +from homeassistant.components.media_player.browse_media import ( + async_process_play_media_url, +) from homeassistant.components.media_player.const import ( ATTR_MEDIA_ENQUEUE, DOMAIN, MEDIA_TYPE_MUSIC, MEDIA_TYPE_PLAYLIST, MEDIA_TYPE_URL, + SUPPORT_BROWSE_MEDIA, SUPPORT_CLEAR_PLAYLIST, SUPPORT_GROUPING, SUPPORT_NEXT_TRACK, @@ -57,6 +62,7 @@ BASE_SUPPORTED_FEATURES = ( | SUPPORT_SELECT_SOURCE | SUPPORT_PLAY_MEDIA | SUPPORT_GROUPING + | SUPPORT_BROWSE_MEDIA ) PLAY_STATE_TO_STATE = { @@ -186,7 +192,14 @@ class HeosMediaPlayer(MediaPlayerEntity): @log_command_error("play media") async def async_play_media(self, media_type, media_id, **kwargs): """Play a piece of media.""" + if media_source.is_media_source_id(media_id): + media_type = MEDIA_TYPE_URL + play_item = await media_source.async_resolve_media(self.hass, media_id) + media_id = play_item.url + if media_type in (MEDIA_TYPE_URL, MEDIA_TYPE_MUSIC): + media_id = async_process_play_media_url(self.hass, media_id) + await self._player.play_url(media_id) return @@ -420,3 +433,11 @@ class HeosMediaPlayer(MediaPlayerEntity): def volume_level(self) -> float: """Volume level of the media player (0..1).""" return self._player.volume / 100 + + async def async_browse_media(self, media_content_type=None, media_content_id=None): + """Implement the websocket media browsing helper.""" + return await media_source.async_browse_media( + self.hass, + media_content_id, + content_filter=lambda item: item.media_content_type.startswith("audio/"), + ) diff --git a/homeassistant/components/here_travel_time/__init__.py b/homeassistant/components/here_travel_time/__init__.py index 9a5c8ec32ac..dde0b28632e 100644 --- a/homeassistant/components/here_travel_time/__init__.py +++ b/homeassistant/components/here_travel_time/__init__.py @@ -1 +1,166 @@ -"""The here_travel_time component.""" +"""The HERE Travel Time integration.""" +from __future__ import annotations + +from datetime import datetime, time, timedelta +import logging + +import async_timeout +from herepy import NoRouteFoundError, RouteMode, RoutingApi, RoutingResponse + +from homeassistant.const import ATTR_ATTRIBUTION, CONF_UNIT_SYSTEM_IMPERIAL, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers.location import find_coordinates +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +from homeassistant.util import dt + +from .const import ( + ATTR_DESTINATION, + ATTR_DESTINATION_NAME, + ATTR_DISTANCE, + ATTR_DURATION, + ATTR_DURATION_IN_TRAFFIC, + ATTR_ORIGIN, + ATTR_ORIGIN_NAME, + ATTR_ROUTE, + DEFAULT_SCAN_INTERVAL, + DOMAIN, + NO_ROUTE_ERROR_MESSAGE, + TRAFFIC_MODE_ENABLED, + TRAVEL_MODES_VEHICLE, +) +from .model import HERERoutingData, HERETravelTimeConfig + +PLATFORMS = [Platform.SENSOR] + +_LOGGER = logging.getLogger(__name__) + + +class HereTravelTimeDataUpdateCoordinator(DataUpdateCoordinator): + """HERETravelTime DataUpdateCoordinator.""" + + def __init__( + self, + hass: HomeAssistant, + api: RoutingApi, + config: HERETravelTimeConfig, + ) -> None: + """Initialize.""" + super().__init__( + hass, + _LOGGER, + name=DOMAIN, + update_interval=timedelta(seconds=DEFAULT_SCAN_INTERVAL), + ) + self._api = api + self.config = config + + async def _async_update_data(self) -> HERERoutingData | None: + """Get the latest data from the HERE Routing API.""" + try: + async with async_timeout.timeout(10): + return await self.hass.async_add_executor_job(self._update) + except NoRouteFoundError as error: + raise UpdateFailed(NO_ROUTE_ERROR_MESSAGE) from error + + def _update(self) -> HERERoutingData | None: + """Get the latest data from the HERE Routing API.""" + if self.config.origin_entity_id is not None: + origin = find_coordinates(self.hass, self.config.origin_entity_id) + else: + origin = self.config.origin + + if self.config.destination_entity_id is not None: + destination = find_coordinates(self.hass, self.config.destination_entity_id) + else: + destination = self.config.destination + if destination is not None and origin is not None: + here_formatted_destination = destination.split(",") + here_formatted_origin = origin.split(",") + arrival: str | None = None + departure: str | None = None + if self.config.arrival is not None: + arrival = convert_time_to_isodate(self.config.arrival) + if self.config.departure is not None: + departure = convert_time_to_isodate(self.config.departure) + + if arrival is None and departure is None: + departure = "now" + + _LOGGER.debug( + "Requesting route for origin: %s, destination: %s, route_mode: %s, mode: %s, traffic_mode: %s, arrival: %s, departure: %s", + here_formatted_origin, + here_formatted_destination, + RouteMode[self.config.route_mode], + RouteMode[self.config.travel_mode], + RouteMode[TRAFFIC_MODE_ENABLED], + arrival, + departure, + ) + + response: RoutingResponse = self._api.public_transport_timetable( + here_formatted_origin, + here_formatted_destination, + True, + [ + RouteMode[self.config.route_mode], + RouteMode[self.config.travel_mode], + RouteMode[TRAFFIC_MODE_ENABLED], + ], + arrival=arrival, + departure=departure, + ) + + _LOGGER.debug("Raw response is: %s", response.response) + + attribution: str | None = None + if "sourceAttribution" in response.response: + attribution = build_hass_attribution( + response.response.get("sourceAttribution") + ) + route: list = response.response["route"] + summary: dict = route[0]["summary"] + waypoint: list = route[0]["waypoint"] + distance: float = summary["distance"] + traffic_time: float = summary["baseTime"] + if self.config.travel_mode in TRAVEL_MODES_VEHICLE: + traffic_time = summary["trafficTime"] + if self.config.units == CONF_UNIT_SYSTEM_IMPERIAL: + # Convert to miles. + distance = distance / 1609.344 + else: + # Convert to kilometers + distance = distance / 1000 + return HERERoutingData( + { + ATTR_ATTRIBUTION: attribution, + ATTR_DURATION: summary["baseTime"] / 60, # type: ignore[misc] + ATTR_DURATION_IN_TRAFFIC: traffic_time / 60, + ATTR_DISTANCE: distance, + ATTR_ROUTE: response.route_short, + ATTR_ORIGIN: ",".join(here_formatted_origin), + ATTR_DESTINATION: ",".join(here_formatted_destination), + ATTR_ORIGIN_NAME: waypoint[0]["mappedRoadName"], + ATTR_DESTINATION_NAME: waypoint[1]["mappedRoadName"], + } + ) + return None + + +def build_hass_attribution(source_attribution: dict) -> str | None: + """Build a hass frontend ready string out of the sourceAttribution.""" + if (suppliers := source_attribution.get("supplier")) is not None: + supplier_titles = [] + for supplier in suppliers: + if (title := supplier.get("title")) is not None: + supplier_titles.append(title) + joined_supplier_titles = ",".join(supplier_titles) + return f"With the support of {joined_supplier_titles}. All information is provided without warranty of any kind." + return None + + +def convert_time_to_isodate(simple_time: time) -> str: + """Take a time like 08:00:00 and combine it with the current date.""" + combined = datetime.combine(dt.start_of_local_day(), simple_time) + if combined < datetime.now(): + combined = combined + timedelta(days=1) + return combined.isoformat() diff --git a/homeassistant/components/here_travel_time/const.py b/homeassistant/components/here_travel_time/const.py new file mode 100644 index 00000000000..a6b958ebf5e --- /dev/null +++ b/homeassistant/components/here_travel_time/const.py @@ -0,0 +1,77 @@ +"""Constants for the HERE Travel Time integration.""" +from homeassistant.const import ( + CONF_UNIT_SYSTEM, + CONF_UNIT_SYSTEM_IMPERIAL, + CONF_UNIT_SYSTEM_METRIC, +) + +DOMAIN = "here_travel_time" +DEFAULT_SCAN_INTERVAL = 300 + +CONF_DESTINATION = "destination" +CONF_ORIGIN = "origin" +CONF_TRAFFIC_MODE = "traffic_mode" +CONF_ROUTE_MODE = "route_mode" +CONF_ARRIVAL = "arrival" +CONF_DEPARTURE = "departure" +CONF_ARRIVAL_TIME = "arrival_time" +CONF_DEPARTURE_TIME = "departure_time" +CONF_TIME_TYPE = "time_type" +CONF_TIME = "time" + +ARRIVAL_TIME = "Arrival Time" +DEPARTURE_TIME = "Departure Time" +TIME_TYPES = [ARRIVAL_TIME, DEPARTURE_TIME] + +DEFAULT_NAME = "HERE Travel Time" + +TRACKABLE_DOMAINS = ["device_tracker", "sensor", "zone", "person"] + +TRAVEL_MODE_BICYCLE = "bicycle" +TRAVEL_MODE_CAR = "car" +TRAVEL_MODE_PEDESTRIAN = "pedestrian" +TRAVEL_MODE_PUBLIC = "publicTransport" +TRAVEL_MODE_PUBLIC_TIME_TABLE = "publicTransportTimeTable" +TRAVEL_MODE_TRUCK = "truck" +TRAVEL_MODES = [ + TRAVEL_MODE_BICYCLE, + TRAVEL_MODE_CAR, + TRAVEL_MODE_PEDESTRIAN, + TRAVEL_MODE_PUBLIC, + TRAVEL_MODE_PUBLIC_TIME_TABLE, + TRAVEL_MODE_TRUCK, +] + +TRAVEL_MODES_PUBLIC = [TRAVEL_MODE_PUBLIC, TRAVEL_MODE_PUBLIC_TIME_TABLE] +TRAVEL_MODES_VEHICLE = [TRAVEL_MODE_CAR, TRAVEL_MODE_TRUCK] + +TRAFFIC_MODE_ENABLED = "traffic_enabled" +TRAFFIC_MODE_DISABLED = "traffic_disabled" +TRAFFIC_MODES = [TRAFFIC_MODE_ENABLED, TRAFFIC_MODE_DISABLED] + +ROUTE_MODE_FASTEST = "fastest" +ROUTE_MODE_SHORTEST = "shortest" +ROUTE_MODES = [ROUTE_MODE_FASTEST, ROUTE_MODE_SHORTEST] + +ICON_BICYCLE = "mdi:bike" +ICON_CAR = "mdi:car" +ICON_PEDESTRIAN = "mdi:walk" +ICON_PUBLIC = "mdi:bus" +ICON_TRUCK = "mdi:truck" + +UNITS = [CONF_UNIT_SYSTEM_METRIC, CONF_UNIT_SYSTEM_IMPERIAL] + +ATTR_DURATION = "duration" +ATTR_DISTANCE = "distance" +ATTR_ROUTE = "route" +ATTR_ORIGIN = "origin" +ATTR_DESTINATION = "destination" + +ATTR_UNIT_SYSTEM = CONF_UNIT_SYSTEM +ATTR_TRAFFIC_MODE = CONF_TRAFFIC_MODE + +ATTR_DURATION_IN_TRAFFIC = "duration_in_traffic" +ATTR_ORIGIN_NAME = "origin_name" +ATTR_DESTINATION_NAME = "destination_name" + +NO_ROUTE_ERROR_MESSAGE = "HERE could not find a route based on the input" diff --git a/homeassistant/components/here_travel_time/model.py b/homeassistant/components/here_travel_time/model.py new file mode 100644 index 00000000000..5cea81d1ece --- /dev/null +++ b/homeassistant/components/here_travel_time/model.py @@ -0,0 +1,35 @@ +"""Model Classes for here_travel_time.""" +from __future__ import annotations + +from dataclasses import dataclass +from datetime import time +from typing import TypedDict + + +class HERERoutingData(TypedDict): + """Routing information calculated from a herepy.RoutingResponse.""" + + ATTR_ATTRIBUTION: str | None + ATTR_DURATION: float + ATTR_DURATION_IN_TRAFFIC: float + ATTR_DISTANCE: float + ATTR_ROUTE: str + ATTR_ORIGIN: str + ATTR_DESTINATION: str + ATTR_ORIGIN_NAME: str + ATTR_DESTINATION_NAME: str + + +@dataclass +class HERETravelTimeConfig: + """Configuration for HereTravelTimeDataUpdateCoordinator.""" + + origin: str | None + destination: str | None + origin_entity_id: str | None + destination_entity_id: str | None + travel_mode: str + route_mode: str + units: str + arrival: time + departure: time diff --git a/homeassistant/components/here_travel_time/sensor.py b/homeassistant/components/here_travel_time/sensor.py index 37509a0c03d..a0449f7b5c0 100644 --- a/homeassistant/components/here_travel_time/sensor.py +++ b/homeassistant/components/here_travel_time/sensor.py @@ -1,7 +1,7 @@ """Support for HERE travel time sensors.""" from __future__ import annotations -from datetime import datetime, timedelta +from datetime import timedelta import logging import herepy @@ -18,15 +18,17 @@ from homeassistant.const import ( CONF_UNIT_SYSTEM, CONF_UNIT_SYSTEM_IMPERIAL, CONF_UNIT_SYSTEM_METRIC, - EVENT_HOMEASSISTANT_START, TIME_MINUTES, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.location import find_coordinates +from homeassistant.helpers.start import async_at_start from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from homeassistant.util import dt +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from . import HereTravelTimeDataUpdateCoordinator +from .model import HERETravelTimeConfig _LOGGER = logging.getLogger(__name__) @@ -175,22 +177,29 @@ async def async_setup_platform( destination = None destination_entity_id = config[CONF_DESTINATION_ENTITY_ID] - travel_mode = config[CONF_MODE] traffic_mode = config[CONF_TRAFFIC_MODE] - route_mode = config[CONF_ROUTE_MODE] name = config[CONF_NAME] - units = config.get(CONF_UNIT_SYSTEM, hass.config.units.name) - arrival = config.get(CONF_ARRIVAL) - departure = config.get(CONF_DEPARTURE) - here_data = HERETravelTimeData( - here_client, travel_mode, traffic_mode, route_mode, units, arrival, departure + here_travel_time_config = HERETravelTimeConfig( + origin=origin, + destination=destination, + origin_entity_id=origin_entity_id, + destination_entity_id=destination_entity_id, + travel_mode=config[CONF_MODE], + route_mode=config[CONF_ROUTE_MODE], + units=config.get(CONF_UNIT_SYSTEM, hass.config.units.name), + arrival=config.get(CONF_ARRIVAL), + departure=config.get(CONF_DEPARTURE), ) - sensor = HERETravelTimeSensor( - name, origin, destination, origin_entity_id, destination_entity_id, here_data + coordinator = HereTravelTimeDataUpdateCoordinator( + hass, + here_client, + here_travel_time_config, ) + sensor = HERETravelTimeSensor(name, traffic_mode, coordinator) + async_add_entities([sensor]) @@ -216,236 +225,76 @@ def _are_valid_client_credentials(here_client: herepy.RoutingApi) -> bool: return True -class HERETravelTimeSensor(SensorEntity): +class HERETravelTimeSensor(SensorEntity, CoordinatorEntity): """Representation of a HERE travel time sensor.""" def __init__( self, name: str, - origin: str, - destination: str, - origin_entity_id: str, - destination_entity_id: str, - here_data: HERETravelTimeData, + traffic_mode: bool, + coordinator: HereTravelTimeDataUpdateCoordinator, ) -> None: """Initialize the sensor.""" - self._name = name - self._origin_entity_id = origin_entity_id - self._destination_entity_id = destination_entity_id - self._here_data = here_data - self._unit_of_measurement = TIME_MINUTES - self._attrs = { - ATTR_UNIT_SYSTEM: self._here_data.units, - ATTR_MODE: self._here_data.travel_mode, - ATTR_TRAFFIC_MODE: self._here_data.traffic_mode, - } - if self._origin_entity_id is None: - self._here_data.origin = origin - - if self._destination_entity_id is None: - self._here_data.destination = destination + super().__init__(coordinator) + self._traffic_mode = traffic_mode + self._attr_native_unit_of_measurement = TIME_MINUTES + self._attr_name = name async def async_added_to_hass(self) -> None: - """Delay the sensor update to avoid entity not found warnings.""" + """Wait for start so origin and destination entities can be resolved.""" + await super().async_added_to_hass() - @callback - def delayed_sensor_update(event): - """Update sensor after Home Assistant started.""" - self.async_schedule_update_ha_state(True) + async def _update_at_start(_): + await self.async_update() - self.hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_START, delayed_sensor_update - ) + self.async_on_remove(async_at_start(self.hass, _update_at_start)) @property def native_value(self) -> str | None: """Return the state of the sensor.""" - if self._here_data.traffic_mode and self._here_data.traffic_time is not None: - return str(round(self._here_data.traffic_time / 60)) - if self._here_data.base_time is not None: - return str(round(self._here_data.base_time / 60)) - + if self.coordinator.data is not None: + return str( + round( + self.coordinator.data.get( + ATTR_DURATION_IN_TRAFFIC + if self._traffic_mode + else ATTR_DURATION + ) + ) + ) return None - @property - def name(self) -> str: - """Get the name of the sensor.""" - return self._name - @property def extra_state_attributes( self, ) -> dict[str, None | float | str | bool] | None: """Return the state attributes.""" - if self._here_data.base_time is None: - return None - - res = self._attrs - if self._here_data.attribution is not None: - res[ATTR_ATTRIBUTION] = self._here_data.attribution - res[ATTR_DURATION] = self._here_data.base_time / 60 - res[ATTR_DISTANCE] = self._here_data.distance - res[ATTR_ROUTE] = self._here_data.route - res[ATTR_DURATION_IN_TRAFFIC] = self._here_data.traffic_time / 60 - res[ATTR_ORIGIN] = self._here_data.origin - res[ATTR_DESTINATION] = self._here_data.destination - res[ATTR_ORIGIN_NAME] = self._here_data.origin_name - res[ATTR_DESTINATION_NAME] = self._here_data.destination_name - return res + if self.coordinator.data is not None: + res = { + ATTR_UNIT_SYSTEM: self.coordinator.config.units, + ATTR_MODE: self.coordinator.config.travel_mode, + ATTR_TRAFFIC_MODE: self._traffic_mode, + **self.coordinator.data, + } + res.pop(ATTR_ATTRIBUTION) + return res + return None @property - def native_unit_of_measurement(self) -> str: - """Return the unit this state is expressed in.""" - return self._unit_of_measurement + def attribution(self) -> str | None: + """Return the attribution.""" + if self.coordinator.data is not None: + return self.coordinator.data.get(ATTR_ATTRIBUTION) @property def icon(self) -> str: """Icon to use in the frontend depending on travel_mode.""" - if self._here_data.travel_mode == TRAVEL_MODE_BICYCLE: + if self.coordinator.config.travel_mode == TRAVEL_MODE_BICYCLE: return ICON_BICYCLE - if self._here_data.travel_mode == TRAVEL_MODE_PEDESTRIAN: + if self.coordinator.config.travel_mode == TRAVEL_MODE_PEDESTRIAN: return ICON_PEDESTRIAN - if self._here_data.travel_mode in TRAVEL_MODES_PUBLIC: + if self.coordinator.config.travel_mode in TRAVEL_MODES_PUBLIC: return ICON_PUBLIC - if self._here_data.travel_mode == TRAVEL_MODE_TRUCK: + if self.coordinator.config.travel_mode == TRAVEL_MODE_TRUCK: return ICON_TRUCK return ICON_CAR - - async def async_update(self) -> None: - """Update Sensor Information.""" - # Convert device_trackers to HERE friendly location - if self._origin_entity_id is not None: - self._here_data.origin = find_coordinates(self.hass, self._origin_entity_id) - - if self._destination_entity_id is not None: - self._here_data.destination = find_coordinates( - self.hass, self._destination_entity_id - ) - - await self.hass.async_add_executor_job(self._here_data.update) - - -class HERETravelTimeData: - """HERETravelTime data object.""" - - def __init__( - self, - here_client: herepy.RoutingApi, - travel_mode: str, - traffic_mode: bool, - route_mode: str, - units: str, - arrival: datetime, - departure: datetime, - ) -> None: - """Initialize herepy.""" - self.origin = None - self.destination = None - self.travel_mode = travel_mode - self.traffic_mode = traffic_mode - self.route_mode = route_mode - self.arrival = arrival - self.departure = departure - self.attribution = None - self.traffic_time = None - self.distance = None - self.route = None - self.base_time = None - self.origin_name = None - self.destination_name = None - self.units = units - self._client = here_client - self.combine_change = True - - def update(self) -> None: - """Get the latest data from HERE.""" - if self.traffic_mode: - traffic_mode = TRAFFIC_MODE_ENABLED - else: - traffic_mode = TRAFFIC_MODE_DISABLED - - if self.destination is not None and self.origin is not None: - # Convert location to HERE friendly location - destination = self.destination.split(",") - origin = self.origin.split(",") - if (arrival := self.arrival) is not None: - arrival = convert_time_to_isodate(arrival) - if (departure := self.departure) is not None: - departure = convert_time_to_isodate(departure) - - if departure is None and arrival is None: - departure = "now" - - _LOGGER.debug( - "Requesting route for origin: %s, destination: %s, route_mode: %s, mode: %s, traffic_mode: %s, arrival: %s, departure: %s", - origin, - destination, - herepy.RouteMode[self.route_mode], - herepy.RouteMode[self.travel_mode], - herepy.RouteMode[traffic_mode], - arrival, - departure, - ) - - try: - response = self._client.public_transport_timetable( - origin, - destination, - self.combine_change, - [ - herepy.RouteMode[self.route_mode], - herepy.RouteMode[self.travel_mode], - herepy.RouteMode[traffic_mode], - ], - arrival=arrival, - departure=departure, - ) - except herepy.NoRouteFoundError: - # Better error message for cryptic no route error codes - _LOGGER.error(NO_ROUTE_ERROR_MESSAGE) - return - - _LOGGER.debug("Raw response is: %s", response.response) - - source_attribution = response.response.get("sourceAttribution") - if source_attribution is not None: - self.attribution = self._build_hass_attribution(source_attribution) - route = response.response["route"] - summary = route[0]["summary"] - waypoint = route[0]["waypoint"] - self.base_time = summary["baseTime"] - if self.travel_mode in TRAVEL_MODES_VEHICLE: - self.traffic_time = summary["trafficTime"] - else: - self.traffic_time = self.base_time - distance = summary["distance"] - if self.units == CONF_UNIT_SYSTEM_IMPERIAL: - # Convert to miles. - self.distance = distance / 1609.344 - else: - # Convert to kilometers - self.distance = distance / 1000 - self.route = response.route_short - self.origin_name = waypoint[0]["mappedRoadName"] - self.destination_name = waypoint[1]["mappedRoadName"] - - @staticmethod - def _build_hass_attribution(source_attribution: dict) -> str | None: - """Build a hass frontend ready string out of the sourceAttribution.""" - suppliers = source_attribution.get("supplier") - if suppliers is not None: - supplier_titles = [] - for supplier in suppliers: - if (title := supplier.get("title")) is not None: - supplier_titles.append(title) - joined_supplier_titles = ",".join(supplier_titles) - attribution = f"With the support of {joined_supplier_titles}. All information is provided without warranty of any kind." - return attribution - - -def convert_time_to_isodate(timestr: str) -> str: - """Take a string like 08:00:00 and combine it with the current date.""" - combined = datetime.combine(dt.start_of_local_day(), dt.parse_time(timestr)) - if combined < datetime.now(): - combined = combined + timedelta(days=1) - return combined.isoformat() diff --git a/homeassistant/components/history/__init__.py b/homeassistant/components/history/__init__.py index 1cbd18f44a7..2bf285d25e6 100644 --- a/homeassistant/components/history/__init__.py +++ b/homeassistant/components/history/__init__.py @@ -14,7 +14,11 @@ import voluptuous as vol from homeassistant.components import frontend, websocket_api from homeassistant.components.http import HomeAssistantView -from homeassistant.components.recorder import history, models as history_models +from homeassistant.components.recorder import ( + get_instance, + history, + models as history_models, +) from homeassistant.components.recorder.statistics import ( list_statistic_ids, statistics_during_period, @@ -142,7 +146,7 @@ async def ws_get_statistics_during_period( else: end_time = None - statistics = await hass.async_add_executor_job( + statistics = await get_instance(hass).async_add_executor_job( statistics_during_period, hass, start_time, @@ -164,9 +168,10 @@ async def ws_get_list_statistic_ids( hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict ) -> None: """Fetch a list of available statistic_id.""" - statistic_ids = await hass.async_add_executor_job( + statistic_ids = await get_instance(hass).async_add_executor_job( list_statistic_ids, hass, + None, msg.get("statistic_type"), ) connection.send_result(msg["id"], statistic_ids) @@ -220,6 +225,7 @@ class HistoryPeriodView(HomeAssistantView): ) minimal_response = "minimal_response" in request.query + no_attributes = "no_attributes" in request.query hass = request.app["hass"] @@ -232,7 +238,7 @@ class HistoryPeriodView(HomeAssistantView): return cast( web.Response, - await hass.async_add_executor_job( + await get_instance(hass).async_add_executor_job( self._sorted_significant_states_json, hass, start_time, @@ -241,6 +247,7 @@ class HistoryPeriodView(HomeAssistantView): include_start_time_state, significant_changes_only, minimal_response, + no_attributes, ), ) @@ -253,6 +260,7 @@ class HistoryPeriodView(HomeAssistantView): include_start_time_state, significant_changes_only, minimal_response, + no_attributes, ): """Fetch significant stats from the database as json.""" timer_start = time.perf_counter() @@ -268,6 +276,7 @@ class HistoryPeriodView(HomeAssistantView): include_start_time_state, significant_changes_only, minimal_response, + no_attributes, ) result = list(result.values()) @@ -355,7 +364,14 @@ class Filters: """Generate the entity filter query.""" includes = [] if self.included_domains: - includes.append(history_models.States.domain.in_(self.included_domains)) + includes.append( + or_( + *[ + history_models.States.entity_id.like(f"{domain}.%") + for domain in self.included_domains + ] + ).self_group() + ) if self.included_entities: includes.append(history_models.States.entity_id.in_(self.included_entities)) for glob in self.included_entity_globs: @@ -363,7 +379,14 @@ class Filters: excludes = [] if self.excluded_domains: - excludes.append(history_models.States.domain.in_(self.excluded_domains)) + excludes.append( + or_( + *[ + history_models.States.entity_id.like(f"{domain}.%") + for domain in self.excluded_domains + ] + ).self_group() + ) if self.excluded_entities: excludes.append(history_models.States.entity_id.in_(self.excluded_entities)) for glob in self.excluded_entity_globs: diff --git a/homeassistant/components/history_stats/sensor.py b/homeassistant/components/history_stats/sensor.py index 2af3706e4e8..5177d5f5239 100644 --- a/homeassistant/components/history_stats/sensor.py +++ b/homeassistant/components/history_stats/sensor.py @@ -7,7 +7,7 @@ import math import voluptuous as vol -from homeassistant.components.recorder import history +from homeassistant.components.recorder import get_instance, history from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( CONF_ENTITY_ID, @@ -225,21 +225,23 @@ class HistoryStatsSensor(SensorEntity): # Don't compute anything as the value cannot have changed return - await self.hass.async_add_executor_job( + await get_instance(self.hass).async_add_executor_job( self._update, start, end, now_timestamp, start_timestamp, end_timestamp ) def _update(self, start, end, now_timestamp, start_timestamp, end_timestamp): # Get history between start and end history_list = history.state_changes_during_period( - self.hass, start, end, str(self._entity_id) + self.hass, start, end, str(self._entity_id), no_attributes=True ) if self._entity_id not in history_list: return # Get the first state - last_state = history.get_state(self.hass, start, self._entity_id) + last_state = history.get_state( + self.hass, start, self._entity_id, no_attributes=True + ) last_state = last_state is not None and last_state in self._entity_states last_time = start_timestamp elapsed = 0 diff --git a/homeassistant/components/hive/manifest.json b/homeassistant/components/hive/manifest.json index 2ce097fb8cc..19958b51bd7 100644 --- a/homeassistant/components/hive/manifest.json +++ b/homeassistant/components/hive/manifest.json @@ -3,13 +3,8 @@ "name": "Hive", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/hive", - "requirements": [ - "pyhiveapi==0.4.2" - ], - "codeowners": [ - "@Rendili", - "@KJonline" - ], + "requirements": ["pyhiveapi==0.4.2"], + "codeowners": ["@Rendili", "@KJonline"], "iot_class": "cloud_polling", "loggers": ["apyhiveapi"] -} \ No newline at end of file +} diff --git a/homeassistant/components/hive/strings.json b/homeassistant/components/hive/strings.json index 0a7a587b2db..7628abc5b06 100644 --- a/homeassistant/components/hive/strings.json +++ b/homeassistant/components/hive/strings.json @@ -50,4 +50,4 @@ } } } -} \ No newline at end of file +} diff --git a/homeassistant/components/hive/switch.py b/homeassistant/components/hive/switch.py index 4eb286462f4..cb9ac79d51e 100644 --- a/homeassistant/components/hive/switch.py +++ b/homeassistant/components/hive/switch.py @@ -69,11 +69,6 @@ class HiveDevicePlug(HiveEntity, SwitchEntity): ATTR_MODE: self.attributes.get(ATTR_MODE), } - @property - def current_power_w(self): - """Return the current power usage in W.""" - return self.device["status"].get("power_usage") - @property def is_on(self): """Return true if switch is on.""" diff --git a/homeassistant/components/hlk_sw16/strings.json b/homeassistant/components/hlk_sw16/strings.json index 2480ac60918..d6e3212b4ea 100644 --- a/homeassistant/components/hlk_sw16/strings.json +++ b/homeassistant/components/hlk_sw16/strings.json @@ -18,4 +18,4 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } } -} \ No newline at end of file +} diff --git a/homeassistant/components/hlk_sw16/translations/fr.json b/homeassistant/components/hlk_sw16/translations/fr.json index 45620fe7795..bb317c2149f 100644 --- a/homeassistant/components/hlk_sw16/translations/fr.json +++ b/homeassistant/components/hlk_sw16/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/home_connect/api.py b/homeassistant/components/home_connect/api.py index f3c98e618b8..bce3f5ece61 100644 --- a/homeassistant/components/home_connect/api.py +++ b/homeassistant/components/home_connect/api.py @@ -312,6 +312,7 @@ class Dishwasher( """Dishwasher class.""" PROGRAMS = [ + {"name": "Dishcare.Dishwasher.Program.PreRinse"}, {"name": "Dishcare.Dishwasher.Program.Auto1"}, {"name": "Dishcare.Dishwasher.Program.Auto2"}, {"name": "Dishcare.Dishwasher.Program.Auto3"}, diff --git a/homeassistant/components/home_connect/translations/fr.json b/homeassistant/components/home_connect/translations/fr.json index 5eba6fa03c1..87a2af8c10a 100644 --- a/homeassistant/components/home_connect/translations/fr.json +++ b/homeassistant/components/home_connect/translations/fr.json @@ -2,7 +2,7 @@ "config": { "abort": { "missing_configuration": "Le composant n'est pas configur\u00e9. Veuillez suivre la documentation.", - "no_url_available": "Aucune URL disponible. Pour plus d'informations sur cette erreur, [consultez la section d'aide] ( {docs_url} )" + "no_url_available": "Aucune URL disponible. Pour plus d'informations sur cette erreur, [consultez la section d'aide]({docs_url})" }, "create_entry": { "default": "Authentification r\u00e9ussie" diff --git a/homeassistant/components/home_plus_control/translations/fr.json b/homeassistant/components/home_plus_control/translations/fr.json index 489e0499324..4e250b743b7 100644 --- a/homeassistant/components/home_plus_control/translations/fr.json +++ b/homeassistant/components/home_plus_control/translations/fr.json @@ -3,9 +3,9 @@ "abort": { "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9", "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours", - "authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration de l'URL d'authentification d\u00e9pass\u00e9.", + "authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration de l'URL d'authentification expir\u00e9.", "missing_configuration": "Le composant n'est pas configur\u00e9. Veuillez suivre la documentation.", - "no_url_available": "Aucune URL disponible. Pour plus d'informations sur cette erreur, [consultez la section d'aide] ( {docs_url} )", + "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." }, "create_entry": { diff --git a/homeassistant/components/homeassistant/__init__.py b/homeassistant/components/homeassistant/__init__.py index c2d855d3135..f79213e2484 100644 --- a/homeassistant/components/homeassistant/__init__.py +++ b/homeassistant/components/homeassistant/__init__.py @@ -23,6 +23,7 @@ from homeassistant.const import ( import homeassistant.core as ha from homeassistant.exceptions import HomeAssistantError, Unauthorized, UnknownUser from homeassistant.helpers import config_validation as cv, recorder, restore_state +from homeassistant.helpers.entity_component import async_update_entity from homeassistant.helpers.service import ( async_extract_config_entry_ids, async_extract_referenced_entity_ids, @@ -199,8 +200,7 @@ async def async_setup(hass: ha.HomeAssistant, config: ConfigType) -> bool: # no ) tasks = [ - hass.helpers.entity_component.async_update_entity(entity) - for entity in call.data[ATTR_ENTITY_ID] + async_update_entity(hass, entity) for entity in call.data[ATTR_ENTITY_ID] ] if tasks: diff --git a/homeassistant/components/homeassistant/strings.json b/homeassistant/components/homeassistant/strings.json index 09be9283b5c..43180b237b9 100644 --- a/homeassistant/components/homeassistant/strings.json +++ b/homeassistant/components/homeassistant/strings.json @@ -15,4 +15,4 @@ "virtualenv": "Virtual Environment" } } -} \ No newline at end of file +} diff --git a/homeassistant/components/homeassistant/translations/zh-Hant.json b/homeassistant/components/homeassistant/translations/zh-Hant.json index 21897b04560..03812830b26 100644 --- a/homeassistant/components/homeassistant/translations/zh-Hant.json +++ b/homeassistant/components/homeassistant/translations/zh-Hant.json @@ -5,7 +5,7 @@ "dev": "\u958b\u767c\u7248", "docker": "Docker", "hassio": "Supervisor", - "installation_type": "\u5b89\u88dd\u985e\u578b", + "installation_type": "\u5b89\u88dd\u985e\u5225", "os_name": "\u4f5c\u696d\u7cfb\u7d71\u5bb6\u65cf", "os_version": "\u4f5c\u696d\u7cfb\u7d71\u7248\u672c", "python_version": "Python \u7248\u672c", diff --git a/homeassistant/components/homeassistant/triggers/numeric_state.py b/homeassistant/components/homeassistant/triggers/numeric_state.py index 823bb608b4d..2d73f38d110 100644 --- a/homeassistant/components/homeassistant/triggers/numeric_state.py +++ b/homeassistant/components/homeassistant/triggers/numeric_state.py @@ -74,7 +74,7 @@ async def async_validate_trigger_config( """Validate trigger config.""" config = _TRIGGER_SCHEMA(config) registry = er.async_get(hass) - config[CONF_ENTITY_ID] = er.async_resolve_entity_ids( + config[CONF_ENTITY_ID] = er.async_validate_entity_ids( registry, cv.entity_ids_or_uuids(config[CONF_ENTITY_ID]) ) return config diff --git a/homeassistant/components/homeassistant/triggers/state.py b/homeassistant/components/homeassistant/triggers/state.py index cafaea40c31..91629cc9933 100644 --- a/homeassistant/components/homeassistant/triggers/state.py +++ b/homeassistant/components/homeassistant/triggers/state.py @@ -77,7 +77,7 @@ async def async_validate_trigger_config( config = TRIGGER_STATE_SCHEMA(config) registry = er.async_get(hass) - config[CONF_ENTITY_ID] = er.async_resolve_entity_ids( + config[CONF_ENTITY_ID] = er.async_validate_entity_ids( registry, cv.entity_ids_or_uuids(config[CONF_ENTITY_ID]) ) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 4cd0940adb7..af21bbdaf84 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -2,14 +2,18 @@ from __future__ import annotations import asyncio +from collections.abc import Iterable from copy import deepcopy import ipaddress import logging import os +from typing import Any, cast +from uuid import UUID from aiohttp import web from pyhap.const import STANDALONE_AID import voluptuous as vol +from zeroconf.asyncio import AsyncZeroconf from homeassistant.components import device_automation, network, zeroconf from homeassistant.components.binary_sensor import ( @@ -39,7 +43,7 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP, SERVICE_RELOAD, ) -from homeassistant.core import CoreState, HomeAssistant, ServiceCall, callback +from homeassistant.core import CoreState, HomeAssistant, ServiceCall, State, callback from homeassistant.exceptions import HomeAssistantError, Unauthorized from homeassistant.helpers import device_registry, entity_registry import homeassistant.helpers.config_validation as cv @@ -67,7 +71,7 @@ from . import ( # noqa: F401 type_switches, type_thermostats, ) -from .accessories import HomeBridge, HomeDriver, get_accessory +from .accessories import HomeAccessory, HomeBridge, HomeDriver, get_accessory from .aidmanager import AccessoryAidStorage from .const import ( ATTR_INTEGRATION, @@ -114,7 +118,7 @@ from .util import ( _LOGGER = logging.getLogger(__name__) -MAX_DEVICES = 150 +MAX_DEVICES = 150 # includes the bridge # #### Driver Status #### STATUS_READY = 0 @@ -129,7 +133,9 @@ _HOMEKIT_CONFIG_UPDATE_TIME = ( ) -def _has_all_unique_names_and_ports(bridges): +def _has_all_unique_names_and_ports( + bridges: list[dict[str, Any]] +) -> list[dict[str, Any]]: """Validate that each homekit bridge configured has a unique name.""" names = [bridge[CONF_NAME] for bridge in bridges] ports = [bridge[CONF_PORT] for bridge in bridges] @@ -184,7 +190,9 @@ def _async_all_homekit_instances(hass: HomeAssistant) -> list[HomeKit]: ] -def _async_get_entries_by_name(current_entries): +def _async_get_entries_by_name( + current_entries: list[ConfigEntry], +) -> dict[str, ConfigEntry]: """Return a dict of the entries by name.""" # For backwards compat, its possible the first bridge is using the default @@ -221,7 +229,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: @callback -def _async_update_config_entry_if_from_yaml(hass, entries_by_name, conf): +def _async_update_config_entry_if_from_yaml( + hass: HomeAssistant, entries_by_name: dict[str, ConfigEntry], conf: ConfigType +) -> bool: """Update a config entry with the latest yaml. Returns True if a matching config entry was found @@ -346,13 +356,15 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: """Remove a config entry.""" - return await hass.async_add_executor_job( + await hass.async_add_executor_job( remove_state_files_for_entry_id, hass, entry.entry_id ) @callback -def _async_import_options_from_data_if_missing(hass: HomeAssistant, entry: ConfigEntry): +def _async_import_options_from_data_if_missing( + hass: HomeAssistant, entry: ConfigEntry +) -> None: options = deepcopy(dict(entry.options)) data = deepcopy(dict(entry.data)) modified = False @@ -367,7 +379,7 @@ def _async_import_options_from_data_if_missing(hass: HomeAssistant, entry: Confi @callback -def _async_register_events_and_services(hass: HomeAssistant): +def _async_register_events_and_services(hass: HomeAssistant) -> None: """Register events and services for HomeKit.""" hass.http.register_view(HomeKitPairingQRView) @@ -381,7 +393,7 @@ def _async_register_events_and_services(hass: HomeAssistant): ) continue - entity_ids = service.data.get("entity_id") + entity_ids = cast(list[str], service.data.get("entity_id")) await homekit.async_reset_accessories(entity_ids) hass.services.async_register( @@ -453,27 +465,29 @@ def _async_register_events_and_services(hass: HomeAssistant): class HomeKit: """Class to handle all actions between HomeKit and Home Assistant.""" + driver: HomeDriver + def __init__( self, - hass, - name, - port, - ip_address, - entity_filter, - exclude_accessory_mode, - entity_config, - homekit_mode, - advertise_ip=None, - entry_id=None, - entry_title=None, - devices=None, - ): + hass: HomeAssistant, + name: str, + port: int, + ip_address: str | None, + entity_filter: EntityFilter, + exclude_accessory_mode: bool, + entity_config: dict, + homekit_mode: str, + advertise_ip: str | None, + entry_id: str, + entry_title: str, + devices: Iterable[str] | None = None, + ) -> None: """Initialize a HomeKit object.""" self.hass = hass self._name = name self._port = port self._ip_address = ip_address - self._filter: EntityFilter = entity_filter + self._filter = entity_filter self._config = entity_config self._exclude_accessory_mode = exclude_accessory_mode self._advertise_ip = advertise_ip @@ -481,13 +495,12 @@ class HomeKit: self._entry_title = entry_title self._homekit_mode = homekit_mode self._devices = devices or [] - self.aid_storage = None + self.aid_storage: AccessoryAidStorage | None = None self.status = STATUS_READY - self.bridge = None - self.driver = None + self.bridge: HomeBridge | None = None - def setup(self, async_zeroconf_instance, uuid): + def setup(self, async_zeroconf_instance: AsyncZeroconf, uuid: UUID) -> None: """Set up bridge and accessory driver.""" persist_file = get_persist_fullpath_for_entry_id(self.hass, self._entry_id) @@ -510,22 +523,24 @@ class HomeKit: if os.path.exists(persist_file): self.driver.load() - async def async_reset_accessories(self, entity_ids): + async def async_reset_accessories(self, entity_ids: Iterable[str]) -> None: """Reset the accessory to load the latest configuration.""" if not self.bridge: await self.async_reset_accessories_in_accessory_mode(entity_ids) return await self.async_reset_accessories_in_bridge_mode(entity_ids) - async def async_reset_accessories_in_accessory_mode(self, entity_ids): + async def async_reset_accessories_in_accessory_mode( + self, entity_ids: Iterable[str] + ) -> None: """Reset accessories in accessory mode.""" - acc = self.driver.accessory + acc = cast(HomeAccessory, self.driver.accessory) if acc.entity_id not in entity_ids: return await acc.stop() if not (state := self.hass.states.get(acc.entity_id)): _LOGGER.warning( - "The underlying entity %s disappeared during reset", acc.entity + "The underlying entity %s disappeared during reset", acc.entity_id ) return if new_acc := self._async_create_single_accessory([state]): @@ -533,9 +548,14 @@ class HomeKit: self.hass.async_add_job(new_acc.run) await self.async_config_changed() - async def async_reset_accessories_in_bridge_mode(self, entity_ids): + async def async_reset_accessories_in_bridge_mode( + self, entity_ids: Iterable[str] + ) -> None: """Reset accessories in bridge mode.""" + assert self.aid_storage is not None + assert self.bridge is not None new = [] + acc: HomeAccessory | None for entity_id in entity_ids: aid = self.aid_storage.get_or_allocate_aid_for_entity_id(entity_id) if aid not in self.bridge.accessories: @@ -545,12 +565,13 @@ class HomeKit: self._name, entity_id, ) - acc = await self.async_remove_bridge_accessory(aid) - if state := self.hass.states.get(acc.entity_id): + if (acc := await self.async_remove_bridge_accessory(aid)) and ( + state := self.hass.states.get(acc.entity_id) + ): new.append(state) else: _LOGGER.warning( - "The underlying entity %s disappeared during reset", acc.entity + "The underlying entity %s disappeared during reset", entity_id ) if not new: @@ -560,23 +581,22 @@ class HomeKit: await self.async_config_changed() await asyncio.sleep(_HOMEKIT_CONFIG_UPDATE_TIME) for state in new: - acc = self.add_bridge_accessory(state) - if acc: + if acc := self.add_bridge_accessory(state): self.hass.async_add_job(acc.run) await self.async_config_changed() - async def async_config_changed(self): + async def async_config_changed(self) -> None: """Call config changed which writes out the new config to disk.""" await self.hass.async_add_executor_job(self.driver.config_changed) - def add_bridge_accessory(self, state): + def add_bridge_accessory(self, state: State) -> HomeAccessory | None: """Try adding accessory to bridge if configured beforehand.""" if self._would_exceed_max_devices(state.entity_id): - return + return None if state_needs_accessory_mode(state): if self._exclude_accessory_mode: - return + return None _LOGGER.warning( "The bridge %s has entity %s. For best performance, " "and to prevent unexpected unavailability, create and " @@ -586,6 +606,8 @@ class HomeKit: state.entity_id, ) + assert self.aid_storage is not None + assert self.bridge is not None aid = self.aid_storage.get_or_allocate_aid_for_entity_id(state.entity_id) conf = self._config.get(state.entity_id, {}).copy() # If an accessory cannot be created or added due to an exception @@ -602,9 +624,10 @@ class HomeKit: ) return None - def _would_exceed_max_devices(self, name): + def _would_exceed_max_devices(self, name: str | None) -> bool: """Check if adding another devices would reach the limit and log.""" # The bridge itself counts as an accessory + assert self.bridge is not None if len(self.bridge.accessories) + 1 >= MAX_DEVICES: _LOGGER.warning( "Cannot add %s as this would exceed the %d device limit. Consider using the filter option", @@ -614,16 +637,20 @@ class HomeKit: return True return False - def add_bridge_triggers_accessory(self, device, device_triggers): + def add_bridge_triggers_accessory( + self, device: device_registry.DeviceEntry, device_triggers: list[dict[str, Any]] + ) -> None: """Add device automation triggers to the bridge.""" if self._would_exceed_max_devices(device.name): return + assert self.aid_storage is not None + assert self.bridge is not None aid = self.aid_storage.get_or_allocate_aid(device.id, device.id) # If an accessory cannot be created or added due to an exception # of any kind (usually in pyhap) it should not prevent # the rest of the accessories from being created - config = {} + config: dict[str, Any] = {} self._fill_config_from_device_registry_entry(device, config) self.bridge.add_accessory( DeviceTriggerAccessory( @@ -638,13 +665,15 @@ class HomeKit: ) ) - async def async_remove_bridge_accessory(self, aid): + async def async_remove_bridge_accessory(self, aid: int) -> HomeAccessory | None: """Try adding accessory to bridge if configured beforehand.""" + assert self.bridge is not None if acc := self.bridge.accessories.pop(aid, None): await acc.stop() - return acc + return cast(HomeAccessory, acc) + return None - async def async_configure_accessories(self): + async def async_configure_accessories(self) -> list[State]: """Configure accessories for the included states.""" dev_reg = device_registry.async_get(self.hass) ent_reg = entity_registry.async_get(self.hass) @@ -667,8 +696,8 @@ class HomeKit: if ent_reg_ent := ent_reg.async_get(entity_id): if ( ent_reg_ent.entity_category is not None - and not self._filter.explicitly_included(entity_id) - ): + or ent_reg_ent.hidden_by is not None + ) and not self._filter.explicitly_included(entity_id): continue await self._async_set_device_info_attributes( @@ -680,7 +709,7 @@ class HomeKit: return entity_states - async def async_start(self, *args): + async def async_start(self, *args: Any) -> None: """Load storage and start.""" if self.status != STATUS_READY: return @@ -704,7 +733,7 @@ class HomeKit: self._async_show_setup_message() @callback - def _async_show_setup_message(self): + def _async_show_setup_message(self) -> None: """Show the pairing setup message.""" async_show_setup_message( self.hass, @@ -715,7 +744,7 @@ class HomeKit: ) @callback - def async_unpair(self): + def async_unpair(self) -> None: """Remove all pairings for an accessory so it can be repaired.""" state = self.driver.state for client_uuid in list(state.paired_clients): @@ -730,8 +759,9 @@ class HomeKit: self._async_show_setup_message() @callback - def _async_register_bridge(self): + def _async_register_bridge(self) -> None: """Register the bridge as a device so homekit_controller and exclude it from discovery.""" + assert self.driver is not None dev_reg = device_registry.async_get(self.hass) formatted_mac = device_registry.format_mac(self.driver.state.mac) # Connections and identifiers are both used here. @@ -753,7 +783,9 @@ class HomeKit: hk_mode_name = "Accessory" if is_accessory_mode else "Bridge" dev_reg.async_get_or_create( config_entry_id=self._entry_id, - identifiers={identifier}, + identifiers={ + identifier # type: ignore[arg-type] + }, # this needs to be migrated as a 2 item tuple at some point connections={connection}, manufacturer=MANUFACTURER, name=accessory_friendly_name(self._entry_title, self.driver.accessory), @@ -762,12 +794,17 @@ class HomeKit: ) @callback - def _async_purge_old_bridges(self, dev_reg, identifier, connection): + def _async_purge_old_bridges( + self, + dev_reg: device_registry.DeviceRegistry, + identifier: tuple[str, str, str], + connection: tuple[str, str], + ) -> None: """Purge bridges that exist from failed pairing or manual resets.""" devices_to_purge = [] for entry in dev_reg.devices.values(): if self._entry_id in entry.config_entries and ( - identifier not in entry.identifiers + identifier not in entry.identifiers # type: ignore[comparison-overlap] or connection not in entry.connections ): devices_to_purge.append(entry.id) @@ -776,7 +813,9 @@ class HomeKit: dev_reg.async_remove_device(device_id) @callback - def _async_create_single_accessory(self, entity_states): + def _async_create_single_accessory( + self, entity_states: list[State] + ) -> HomeAccessory | None: """Create a single HomeKit accessory (accessory mode).""" if not entity_states: _LOGGER.error( @@ -796,7 +835,9 @@ class HomeKit: ) return acc - async def _async_create_bridge_accessory(self, entity_states): + async def _async_create_bridge_accessory( + self, entity_states: Iterable[State] + ) -> HomeAccessory: """Create a HomeKit bridge with accessories. (bridge mode).""" self.bridge = HomeBridge(self.hass, self.driver, self._name) for state in entity_states: @@ -820,12 +861,11 @@ class HomeKit: valid_device_ids, ) ).items(): - self.add_bridge_triggers_accessory( - dev_reg.async_get(device_id), device_triggers - ) + if device := dev_reg.async_get(device_id): + self.add_bridge_triggers_accessory(device, device_triggers) return self.bridge - async def _async_create_accessories(self): + async def _async_create_accessories(self) -> bool: """Create the accessories.""" entity_states = await self.async_configure_accessories() if self._homekit_mode == HOMEKIT_MODE_ACCESSORY: @@ -839,7 +879,7 @@ class HomeKit: self.driver.accessory = acc return True - async def async_stop(self, *args): + async def async_stop(self, *args: Any) -> None: """Stop the accessory driver.""" if self.status != STATUS_RUNNING: return @@ -848,7 +888,12 @@ class HomeKit: await self.driver.async_stop() @callback - def _async_configure_linked_sensors(self, ent_reg_ent, device_lookup, state): + def _async_configure_linked_sensors( + self, + ent_reg_ent: entity_registry.RegistryEntry, + device_lookup: dict[str, dict[tuple[str, str | None], str]], + state: State, + ) -> None: if ( ent_reg_ent is None or ent_reg_ent.device_id is None @@ -905,7 +950,12 @@ class HomeKit: current_humidity_sensor_entity_id, ) - async def _async_set_device_info_attributes(self, ent_reg_ent, dev_reg, entity_id): + async def _async_set_device_info_attributes( + self, + ent_reg_ent: entity_registry.RegistryEntry, + dev_reg: device_registry.DeviceRegistry, + entity_id: str, + ) -> None: """Set attributes that will be used for homekit device info.""" ent_cfg = self._config.setdefault(entity_id, {}) if ent_reg_ent.device_id: @@ -920,7 +970,9 @@ class HomeKit: except IntegrationNotFound: ent_cfg[ATTR_INTEGRATION] = ent_reg_ent.platform - def _fill_config_from_device_registry_entry(self, device_entry, config): + def _fill_config_from_device_registry_entry( + self, device_entry: device_registry.DeviceEntry, config: dict[str, Any] + ) -> None: """Populate a config dict from the registry.""" if device_entry.manufacturer: config[ATTR_MANUFACTURER] = device_entry.manufacturer @@ -943,7 +995,7 @@ class HomeKitPairingQRView(HomeAssistantView): name = "api:homekit:pairingqr" requires_auth = False - async def get(self, request): + async def get(self, request: web.Request) -> web.Response: """Retrieve the pairing QRCode image.""" # pylint: disable=no-self-use if not request.query_string: diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index 4129c3225b7..1d06fa04a57 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -2,6 +2,8 @@ from __future__ import annotations import logging +from typing import Any, cast +from uuid import UUID from pyhap.accessory import Accessory, Bridge from pyhap.accessory_driver import AccessoryDriver @@ -34,7 +36,15 @@ from homeassistant.const import ( TEMP_FAHRENHEIT, __version__, ) -from homeassistant.core import Context, callback as ha_callback, split_entity_id +from homeassistant.core import ( + CALLBACK_TYPE, + Context, + Event, + HomeAssistant, + State, + callback as ha_callback, + split_entity_id, +) from homeassistant.helpers.event import async_track_state_change_event from homeassistant.util.decorator import Registry @@ -95,7 +105,9 @@ SWITCH_TYPES = { TYPES: Registry[str, type[HomeAccessory]] = Registry() -def get_accessory(hass, driver, state, aid, config): # noqa: C901 +def get_accessory( # noqa: C901 + hass: HomeAssistant, driver: HomeDriver, state: State, aid: int | None, config: dict +) -> HomeAccessory | None: """Take state and return an accessory object if supported.""" if not aid: _LOGGER.warning( @@ -173,9 +185,19 @@ def get_accessory(hass, driver, state, aid, config): # noqa: C901 a_type = "TemperatureSensor" elif device_class == SensorDeviceClass.HUMIDITY and unit == PERCENTAGE: a_type = "HumiditySensor" + elif ( + device_class == SensorDeviceClass.PM10 + or SensorDeviceClass.PM10 in state.entity_id + ): + a_type = "PM10Sensor" elif ( device_class == SensorDeviceClass.PM25 or SensorDeviceClass.PM25 in state.entity_id + ): + a_type = "PM25Sensor" + elif ( + device_class == SensorDeviceClass.GAS + or SensorDeviceClass.GAS in state.entity_id ): a_type = "AirQualitySensor" elif device_class == SensorDeviceClass.CO: @@ -222,22 +244,22 @@ def get_accessory(hass, driver, state, aid, config): # noqa: C901 return TYPES[a_type](hass, driver, name, state.entity_id, aid, config) -class HomeAccessory(Accessory): +class HomeAccessory(Accessory): # type: ignore[misc] """Adapter class for Accessory.""" def __init__( self, - hass, - driver, - name, - entity_id, - aid, - config, - *args, - category=CATEGORY_OTHER, - device_id=None, - **kwargs, - ): + hass: HomeAssistant, + driver: HomeDriver, + name: str, + entity_id: str, + aid: int, + config: dict, + *args: Any, + category: str = CATEGORY_OTHER, + device_id: str | None = None, + **kwargs: Any, + ) -> None: """Initialize a Accessory object.""" super().__init__( driver=driver, @@ -248,7 +270,7 @@ class HomeAccessory(Accessory): ) self.config = config or {} if device_id: - self.device_id = device_id + self.device_id: str | None = device_id serial_number = device_id domain = None else: @@ -275,6 +297,7 @@ class HomeAccessory(Accessory): sw_version = format_version(self.config[ATTR_SW_VERSION]) if sw_version is None: sw_version = format_version(__version__) + assert sw_version is not None hw_version = None if self.config.get(ATTR_HW_VERSION) is not None: hw_version = format_version(self.config[ATTR_HW_VERSION]) @@ -298,7 +321,7 @@ class HomeAccessory(Accessory): self.category = category self.entity_id = entity_id self.hass = hass - self._subscriptions = [] + self._subscriptions: list[CALLBACK_TYPE] = [] if device_id: return @@ -315,7 +338,9 @@ class HomeAccessory(Accessory): ) """Add battery service if available""" - entity_attributes = self.hass.states.get(self.entity_id).attributes + state = self.hass.states.get(self.entity_id) + assert state is not None + entity_attributes = state.attributes battery_found = entity_attributes.get(ATTR_BATTERY_LEVEL) if self.linked_battery_sensor: @@ -357,15 +382,15 @@ class HomeAccessory(Accessory): ) @property - def available(self): + def available(self) -> bool: """Return if accessory is available.""" state = self.hass.states.get(self.entity_id) return state is not None and state.state != STATE_UNAVAILABLE - async def run(self): + async def run(self) -> None: """Handle accessory driver started event.""" - state = self.hass.states.get(self.entity_id) - self.async_update_state_callback(state) + if state := self.hass.states.get(self.entity_id): + self.async_update_state_callback(state) self._subscriptions.append( async_track_state_change_event( self.hass, [self.entity_id], self.async_update_event_state_callback @@ -374,10 +399,11 @@ class HomeAccessory(Accessory): battery_charging_state = None battery_state = None - if self.linked_battery_sensor: - linked_battery_sensor_state = self.hass.states.get( + if self.linked_battery_sensor and ( + linked_battery_sensor_state := self.hass.states.get( self.linked_battery_sensor ) + ): battery_state = linked_battery_sensor_state.state battery_charging_state = linked_battery_sensor_state.attributes.get( ATTR_BATTERY_CHARGING @@ -408,12 +434,12 @@ class HomeAccessory(Accessory): self.async_update_battery(battery_state, battery_charging_state) @ha_callback - def async_update_event_state_callback(self, event): + def async_update_event_state_callback(self, event: Event) -> None: """Handle state change event listener callback.""" self.async_update_state_callback(event.data.get("new_state")) @ha_callback - def async_update_state_callback(self, new_state): + def async_update_state_callback(self, new_state: State | None) -> None: """Handle state change listener callback.""" _LOGGER.debug("New_state: %s", new_state) if new_state is None: @@ -435,7 +461,7 @@ class HomeAccessory(Accessory): self.async_update_state(new_state) @ha_callback - def async_update_linked_battery_callback(self, event): + def async_update_linked_battery_callback(self, event: Event) -> None: """Handle linked battery sensor state change listener callback.""" if (new_state := event.data.get("new_state")) is None: return @@ -446,19 +472,19 @@ class HomeAccessory(Accessory): self.async_update_battery(new_state.state, battery_charging_state) @ha_callback - def async_update_linked_battery_charging_callback(self, event): + def async_update_linked_battery_charging_callback(self, event: Event) -> None: """Handle linked battery charging sensor state change listener callback.""" if (new_state := event.data.get("new_state")) is None: return self.async_update_battery(None, new_state.state == STATE_ON) @ha_callback - def async_update_battery(self, battery_level, battery_charging): + def async_update_battery(self, battery_level: Any, battery_charging: Any) -> None: """Update battery service if available. Only call this function if self._support_battery_level is True. """ - if not self._char_battery: + if not self._char_battery or not self._char_low_battery: # Battery appeared after homekit was started return @@ -485,7 +511,7 @@ class HomeAccessory(Accessory): ) @ha_callback - def async_update_state(self, new_state): + def async_update_state(self, new_state: State) -> None: """Handle state change to update HomeKit value. Overridden by accessory types. @@ -493,7 +519,13 @@ class HomeAccessory(Accessory): raise NotImplementedError() @ha_callback - def async_call_service(self, domain, service, service_data, value=None): + def async_call_service( + self, + domain: str, + service: str, + service_data: dict[str, Any] | None, + value: Any | None = None, + ) -> None: """Fire event and call service for changes from HomeKit.""" event_data = { ATTR_ENTITY_ID: self.entity_id, @@ -511,7 +543,7 @@ class HomeAccessory(Accessory): ) @ha_callback - def async_reset(self): + def async_reset(self) -> None: """Reset and recreate an accessory.""" self.hass.async_create_task( self.hass.services.async_call( @@ -521,16 +553,16 @@ class HomeAccessory(Accessory): ) ) - async def stop(self): + async def stop(self) -> None: """Cancel any subscriptions when the bridge is stopped.""" while self._subscriptions: self._subscriptions.pop(0)() -class HomeBridge(Bridge): +class HomeBridge(Bridge): # type: ignore[misc] """Adapter class for Bridge.""" - def __init__(self, hass, driver, name): + def __init__(self, hass: HomeAssistant, driver: HomeDriver, name: str) -> None: """Initialize a Bridge object.""" super().__init__(driver, name) self.set_info_service( @@ -541,10 +573,10 @@ class HomeBridge(Bridge): ) self.hass = hass - def setup_message(self): + def setup_message(self) -> None: """Prevent print of pyhap setup message to terminal.""" - async def async_get_snapshot(self, info): + async def async_get_snapshot(self, info: dict) -> bytes: """Get snapshot from accessory if supported.""" if (acc := self.accessories.get(info["aid"])) is None: raise ValueError("Requested snapshot for missing accessory") @@ -553,13 +585,20 @@ class HomeBridge(Bridge): "Got a request for snapshot, but the Accessory " 'does not define a "async_get_snapshot" method' ) - return await acc.async_get_snapshot(info) + return cast(bytes, await acc.async_get_snapshot(info)) -class HomeDriver(AccessoryDriver): +class HomeDriver(AccessoryDriver): # type: ignore[misc] """Adapter class for AccessoryDriver.""" - def __init__(self, hass, entry_id, bridge_name, entry_title, **kwargs): + def __init__( + self, + hass: HomeAssistant, + entry_id: str, + bridge_name: str, + entry_title: str, + **kwargs: Any, + ) -> None: """Initialize a AccessoryDriver object.""" super().__init__(**kwargs) self.hass = hass @@ -567,16 +606,18 @@ class HomeDriver(AccessoryDriver): self._bridge_name = bridge_name self._entry_title = entry_title - @pyhap_callback - def pair(self, client_uuid, client_public, client_permissions): + @pyhap_callback # type: ignore[misc] + def pair( + self, client_uuid: UUID, client_public: str, client_permissions: int + ) -> bool: """Override super function to dismiss setup message if paired.""" success = super().pair(client_uuid, client_public, client_permissions) if success: async_dismiss_setup_message(self.hass, self._entry_id) - return success + return cast(bool, success) - @pyhap_callback - def unpair(self, client_uuid): + @pyhap_callback # type: ignore[misc] + def unpair(self, client_uuid: UUID) -> None: """Override super function to show setup message if unpaired.""" super().unpair(client_uuid) diff --git a/homeassistant/components/homekit/aidmanager.py b/homeassistant/components/homekit/aidmanager.py index c7ddc29a788..ddba9d02bcd 100644 --- a/homeassistant/components/homekit/aidmanager.py +++ b/homeassistant/components/homekit/aidmanager.py @@ -9,13 +9,15 @@ can't change the hash without causing breakages for HA users. This module generates and stores them in a HA storage. """ +from __future__ import annotations + +from collections.abc import Generator import random from fnvhash import fnv1a_32 -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.entity_registry import RegistryEntry +from homeassistant.helpers.entity_registry import EntityRegistry, RegistryEntry from homeassistant.helpers.storage import Store from .util import get_aid_storage_filename_for_entry_id @@ -32,12 +34,12 @@ AID_MIN = 2 AID_MAX = 18446744073709551615 -def get_system_unique_id(entity: RegistryEntry): +def get_system_unique_id(entity: RegistryEntry) -> str: """Determine the system wide unique_id for an entity.""" return f"{entity.platform}.{entity.domain}.{entity.unique_id}" -def _generate_aids(unique_id: str, entity_id: str) -> int: +def _generate_aids(unique_id: str | None, entity_id: str) -> Generator[int, None, None]: """Generate accessory aid.""" if unique_id: @@ -65,39 +67,41 @@ class AccessoryAidStorage: persist over reboots. """ - def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: + def __init__(self, hass: HomeAssistant, entry_id: str) -> None: """Create a new entity map store.""" self.hass = hass - self.allocations = {} - self.allocated_aids = set() - self._entry = entry - self.store = None - self._entity_registry = None + self.allocations: dict[str, int] = {} + self.allocated_aids: set[int] = set() + self._entry_id = entry_id + self.store: Store | None = None + self._entity_registry: EntityRegistry | None = None - async def async_initialize(self): + async def async_initialize(self) -> None: """Load the latest AID data.""" self._entity_registry = ( await self.hass.helpers.entity_registry.async_get_registry() ) - aidstore = get_aid_storage_filename_for_entry_id(self._entry) + aidstore = get_aid_storage_filename_for_entry_id(self._entry_id) self.store = Store(self.hass, AID_MANAGER_STORAGE_VERSION, aidstore) if not (raw_storage := await self.store.async_load()): # There is no data about aid allocations yet return + assert isinstance(raw_storage, dict) self.allocations = raw_storage.get(ALLOCATIONS_KEY, {}) self.allocated_aids = set(self.allocations.values()) - def get_or_allocate_aid_for_entity_id(self, entity_id: str): + def get_or_allocate_aid_for_entity_id(self, entity_id: str) -> int: """Generate a stable aid for an entity id.""" + assert self._entity_registry is not None if not (entity := self._entity_registry.async_get(entity_id)): return self.get_or_allocate_aid(None, entity_id) sys_unique_id = get_system_unique_id(entity) return self.get_or_allocate_aid(sys_unique_id, entity_id) - def get_or_allocate_aid(self, unique_id: str, entity_id: str): + def get_or_allocate_aid(self, unique_id: str | None, entity_id: str) -> int: """Allocate (and return) a new aid for an accessory.""" if unique_id and unique_id in self.allocations: return self.allocations[unique_id] @@ -119,7 +123,7 @@ class AccessoryAidStorage: f"Unable to generate unique aid allocation for {entity_id} [{unique_id}]" ) - def delete_aid(self, storage_key: str): + def delete_aid(self, storage_key: str) -> None: """Delete an aid allocation.""" if storage_key not in self.allocations: return @@ -129,15 +133,17 @@ class AccessoryAidStorage: self.async_schedule_save() @callback - def async_schedule_save(self): + def async_schedule_save(self) -> None: """Schedule saving the entity map cache.""" + assert self.store is not None self.store.async_delay_save(self._data_to_save, AID_MANAGER_SAVE_DELAY) - async def async_save(self): + async def async_save(self) -> None: """Save the entity map cache.""" + assert self.store is not None return await self.store.async_save(self._data_to_save()) @callback - def _data_to_save(self): + def _data_to_save(self) -> dict: """Return data of entity map to store in a file.""" return {ALLOCATIONS_KEY: self.allocations} diff --git a/homeassistant/components/homekit/config_flow.py b/homeassistant/components/homekit/config_flow.py index 0863b8f583a..79193cd3dac 100644 --- a/homeassistant/components/homekit/config_flow.py +++ b/homeassistant/components/homekit/config_flow.py @@ -2,11 +2,12 @@ from __future__ import annotations import asyncio +from collections.abc import Iterable from copy import deepcopy import random import re import string -from typing import Any, Final +from typing import Any import voluptuous as vol @@ -27,6 +28,7 @@ from homeassistant.const import ( CONF_PORT, ) from homeassistant.core import HomeAssistant, callback, split_entity_id +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import device_registry, entity_registry import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entityfilter import ( @@ -119,7 +121,7 @@ DEFAULT_DOMAINS = [ "water_heater", ] -_EMPTY_ENTITY_FILTER: Final = { +_EMPTY_ENTITY_FILTER: dict[str, list[str]] = { CONF_INCLUDE_DOMAINS: [], CONF_EXCLUDE_DOMAINS: [], CONF_INCLUDE_ENTITIES: [], @@ -151,9 +153,9 @@ def _async_build_entites_filter( return entity_filter -def _async_cameras_from_entities(entities: list[str]) -> set[str]: +def _async_cameras_from_entities(entities: list[str]) -> dict[str, str]: return { - entity_id + entity_id: entity_id for entity_id in entities if entity_id.startswith(CAMERA_ENTITY_PREFIX) } @@ -181,9 +183,11 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialize config flow.""" - self.hk_data = {} + self.hk_data: dict[str, Any] = {} - async def async_step_user(self, user_input=None): + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Choose specific domains in bridge mode.""" if user_input is not None: entity_filter = deepcopy(_EMPTY_ENTITY_FILTER) @@ -205,7 +209,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ), ) - async def async_step_pairing(self, user_input=None): + async def async_step_pairing( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Pairing instructions.""" if user_input is not None: port = async_find_next_available_port(self.hass, DEFAULT_CONFIG_FLOW_PORT) @@ -227,7 +233,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): description_placeholders={CONF_NAME: self.hk_data[CONF_NAME]}, ) - async def _async_add_entries_for_accessory_mode_entities(self, last_assigned_port): + async def _async_add_entries_for_accessory_mode_entities( + self, last_assigned_port: int + ) -> None: """Generate new flows for entities that need their own instances.""" accessory_mode_entity_ids = _async_get_entity_ids_for_accessory_mode( self.hass, self.hk_data[CONF_FILTER][CONF_INCLUDE_DOMAINS] @@ -249,12 +257,13 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) ) - async def async_step_accessory(self, accessory_input): + async def async_step_accessory(self, accessory_input: dict) -> FlowResult: """Handle creation a single accessory in accessory mode.""" entity_id = accessory_input[CONF_ENTITY_ID] port = accessory_input[CONF_PORT] state = self.hass.states.get(entity_id) + assert state is not None name = state.attributes.get(ATTR_FRIENDLY_NAME) or state.entity_id entity_filter = _EMPTY_ENTITY_FILTER.copy() entity_filter[CONF_INCLUDE_ENTITIES] = [entity_id] @@ -274,7 +283,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): title=f"{name}:{entry_data[CONF_PORT]}", data=entry_data ) - async def async_step_import(self, user_input=None): + async def async_step_import(self, user_input: dict) -> FlowResult: """Handle import from yaml.""" if not self._async_is_unique_name_port(user_input): return self.async_abort(reason="port_name_in_use") @@ -283,7 +292,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) @callback - def _async_current_names(self): + def _async_current_names(self) -> set[str]: """Return a set of bridge names.""" return { entry.data[CONF_NAME] @@ -292,7 +301,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): } @callback - def _async_available_name(self, requested_name): + def _async_available_name(self, requested_name: str) -> str: """Return an available for the bridge.""" current_names = self._async_current_names() valid_mdns_name = re.sub("[^A-Za-z0-9 ]+", " ", requested_name) @@ -301,7 +310,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return valid_mdns_name acceptable_mdns_chars = string.ascii_uppercase + string.digits - suggested_name = None + suggested_name: str | None = None while not suggested_name or suggested_name in current_names: trailer = "".join(random.choices(acceptable_mdns_chars, k=2)) suggested_name = f"{valid_mdns_name} {trailer}" @@ -309,7 +318,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return suggested_name @callback - def _async_is_unique_name_port(self, user_input): + def _async_is_unique_name_port(self, user_input: dict[str, str]) -> bool: """Determine is a name or port is already used.""" name = user_input[CONF_NAME] port = user_input[CONF_PORT] @@ -320,7 +329,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback - def async_get_options_flow(config_entry): + def async_get_options_flow( + config_entry: config_entries.ConfigEntry, + ) -> OptionsFlowHandler: """Get the options flow for this handler.""" return OptionsFlowHandler(config_entry) @@ -331,10 +342,12 @@ class OptionsFlowHandler(config_entries.OptionsFlow): def __init__(self, config_entry: config_entries.ConfigEntry) -> None: """Initialize options flow.""" self.config_entry = config_entry - self.hk_options = {} - self.included_cameras = set() + self.hk_options: dict[str, Any] = {} + self.included_cameras: dict[str, str] = {} - async def async_step_yaml(self, user_input=None): + async def async_step_yaml( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """No options for yaml managed entries.""" if user_input is not None: # Apparently not possible to abort an options flow @@ -343,7 +356,9 @@ class OptionsFlowHandler(config_entries.OptionsFlow): return self.async_show_form(step_id="yaml") - async def async_step_advanced(self, user_input=None): + async def async_step_advanced( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Choose advanced options.""" if ( not self.show_advanced_options @@ -352,17 +367,16 @@ class OptionsFlowHandler(config_entries.OptionsFlow): ): if user_input: self.hk_options.update(user_input) + if ( + self.show_advanced_options + and self.hk_options[CONF_HOMEKIT_MODE] == HOMEKIT_MODE_BRIDGE + ): + self.hk_options[CONF_DEVICES] = user_input[CONF_DEVICES] for key in (CONF_DOMAINS, CONF_ENTITIES): if key in self.hk_options: del self.hk_options[key] - if ( - self.show_advanced_options - and self.hk_options[CONF_HOMEKIT_MODE] == HOMEKIT_MODE_BRIDGE - ): - self.hk_options[CONF_DEVICES] = user_input[CONF_DEVICES] - if CONF_INCLUDE_EXCLUDE_MODE in self.hk_options: del self.hk_options[CONF_INCLUDE_EXCLUDE_MODE] @@ -386,7 +400,9 @@ class OptionsFlowHandler(config_entries.OptionsFlow): ), ) - async def async_step_cameras(self, user_input=None): + async def async_step_cameras( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Choose camera config.""" if user_input is not None: entity_config = self.hk_options[CONF_ENTITY_CONFIG] @@ -433,7 +449,9 @@ class OptionsFlowHandler(config_entries.OptionsFlow): ) return self.async_show_form(step_id="cameras", data_schema=data_schema) - async def async_step_accessory(self, user_input=None): + async def async_step_accessory( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Choose entity for the accessory.""" domains = self.hk_options[CONF_DOMAINS] @@ -448,7 +466,9 @@ class OptionsFlowHandler(config_entries.OptionsFlow): entity_filter = self.hk_options.get(CONF_FILTER, {}) entities = entity_filter.get(CONF_INCLUDE_ENTITIES, []) - all_supported_entities = _async_get_matching_entities(self.hass, domains) + all_supported_entities = _async_get_matching_entities( + self.hass, domains, include_entity_category=True + ) # In accessory mode we can only have one default_value = next( iter( @@ -470,7 +490,9 @@ class OptionsFlowHandler(config_entries.OptionsFlow): ), ) - async def async_step_include(self, user_input=None): + async def async_step_include( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Choose entities to include from the domain on the bridge.""" domains = self.hk_options[CONF_DOMAINS] if user_input is not None: @@ -485,7 +507,9 @@ class OptionsFlowHandler(config_entries.OptionsFlow): entity_filter = self.hk_options.get(CONF_FILTER, {}) entities = entity_filter.get(CONF_INCLUDE_ENTITIES, []) - all_supported_entities = _async_get_matching_entities(self.hass, domains) + all_supported_entities = _async_get_matching_entities( + self.hass, domains, include_entity_category=True + ) if not entities: entities = entity_filter.get(CONF_EXCLUDE_ENTITIES, []) # Strip out entities that no longer exist to prevent error in the UI @@ -507,7 +531,9 @@ class OptionsFlowHandler(config_entries.OptionsFlow): ), ) - async def async_step_exclude(self, user_input=None): + async def async_step_exclude( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Choose entities to exclude from the domain on the bridge.""" domains = self.hk_options[CONF_DOMAINS] @@ -516,13 +542,13 @@ class OptionsFlowHandler(config_entries.OptionsFlow): entities = cv.ensure_list(user_input[CONF_ENTITIES]) entity_filter[CONF_INCLUDE_DOMAINS] = domains entity_filter[CONF_EXCLUDE_ENTITIES] = entities - self.included_cameras = set() + self.included_cameras = {} if CAMERA_DOMAIN in entity_filter[CONF_INCLUDE_DOMAINS]: camera_entities = _async_get_matching_entities( self.hass, [CAMERA_DOMAIN] ) self.included_cameras = { - entity_id + entity_id: entity_id for entity_id in camera_entities if entity_id not in entities } @@ -537,18 +563,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow): all_supported_entities = _async_get_matching_entities(self.hass, domains) if not entities: entities = entity_filter.get(CONF_EXCLUDE_ENTITIES, []) - ent_reg = entity_registry.async_get(self.hass) - entity_cat_entities = set() - for entity_id in all_supported_entities: - if ent_reg_ent := ent_reg.async_get(entity_id): - if ent_reg_ent.entity_category is not None: - entity_cat_entities.add(entity_id) - # Remove entity category entities since we will exclude them anyways - all_supported_entities = { - k: v - for k, v in all_supported_entities.items() - if k not in entity_cat_entities - } + # Strip out entities that no longer exist to prevent error in the UI default_value = [ entity_id for entity_id in entities if entity_id in all_supported_entities @@ -568,7 +583,9 @@ class OptionsFlowHandler(config_entries.OptionsFlow): ), ) - async def async_step_init(self, user_input=None): + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle options flow.""" if self.config_entry.source == SOURCE_IMPORT: return await self.async_step_yaml(user_input) @@ -612,39 +629,62 @@ class OptionsFlowHandler(config_entries.OptionsFlow): ) -async def _async_get_supported_devices(hass): +async def _async_get_supported_devices(hass: HomeAssistant) -> dict[str, str]: """Return all supported devices.""" results = await device_automation.async_get_device_automations( hass, device_automation.DeviceAutomationType.TRIGGER ) dev_reg = device_registry.async_get(hass) - unsorted = { - device_id: dev_reg.async_get(device_id).name or device_id - for device_id in results - } + unsorted: dict[str, str] = {} + for device_id in results: + entry = dev_reg.async_get(device_id) + unsorted[device_id] = entry.name or device_id if entry else device_id return dict(sorted(unsorted.items(), key=lambda item: item[1])) +def _exclude_by_entity_registry( + ent_reg: entity_registry.EntityRegistry, + entity_id: str, + include_entity_category: bool, +) -> bool: + """Filter out hidden entities and ones with entity category (unless specified).""" + return bool( + (entry := ent_reg.async_get(entity_id)) + and ( + entry.hidden_by is not None + or (not include_entity_category or entry.entity_category is not None) + ) + ) + + def _async_get_matching_entities( - hass: HomeAssistant, domains: list[str] | None = None + hass: HomeAssistant, + domains: list[str] | None = None, + include_entity_category: bool = False, ) -> dict[str, str]: """Fetch all entities or entities in the given domains.""" + ent_reg = entity_registry.async_get(hass) return { state.entity_id: f"{state.attributes.get(ATTR_FRIENDLY_NAME, state.entity_id)} ({state.entity_id})" for state in sorted( hass.states.async_all(domains and set(domains)), key=lambda item: item.entity_id, ) + if not _exclude_by_entity_registry( + ent_reg, state.entity_id, include_entity_category + ) } -def _domains_set_from_entities(entity_ids): +def _domains_set_from_entities(entity_ids: Iterable[str]) -> set[str]: """Build a set of domains for the given entity ids.""" return {split_entity_id(entity_id)[0] for entity_id in entity_ids} @callback -def _async_get_entity_ids_for_accessory_mode(hass, include_domains): +def _async_get_entity_ids_for_accessory_mode( + hass: HomeAssistant, include_domains: Iterable[str] +) -> list[str]: """Build a list of entities that should be paired in accessory mode.""" accessory_mode_domains = { domain for domain in include_domains if domain in DOMAINS_NEED_ACCESSORY_MODE @@ -661,7 +701,7 @@ def _async_get_entity_ids_for_accessory_mode(hass, include_domains): @callback -def _async_entity_ids_with_accessory_mode(hass): +def _async_entity_ids_with_accessory_mode(hass: HomeAssistant) -> set[str]: """Return a set of entity ids that have config entries in accessory mode.""" entity_ids = set() diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py index ed7b3d6b293..264801c521f 100644 --- a/homeassistant/components/homekit/const.py +++ b/homeassistant/components/homekit/const.py @@ -150,6 +150,8 @@ SERV_WINDOW_COVERING = "WindowCovering" CHAR_ACTIVE = "Active" CHAR_ACTIVE_IDENTIFIER = "ActiveIdentifier" CHAR_AIR_PARTICULATE_DENSITY = "AirParticulateDensity" +CHAR_PM25_DENSITY = "PM2.5Density" +CHAR_PM10_DENSITY = "PM10Density" CHAR_AIR_QUALITY = "AirQuality" CHAR_BATTERY_LEVEL = "BatteryLevel" CHAR_BRIGHTNESS = "Brightness" @@ -235,7 +237,6 @@ PROP_MIN_VALUE = "minValue" PROP_MIN_STEP = "minStep" PROP_CELSIUS = {"minValue": -273, "maxValue": 999} PROP_VALID_VALUES = "ValidValues" - # #### Thresholds #### THRESHOLD_CO = 25 THRESHOLD_CO2 = 1000 diff --git a/homeassistant/components/homekit/diagnostics.py b/homeassistant/components/homekit/diagnostics.py index 2a54c1ef543..f717f02ea02 100644 --- a/homeassistant/components/homekit/diagnostics.py +++ b/homeassistant/components/homekit/diagnostics.py @@ -18,7 +18,6 @@ async def async_get_config_entry_diagnostics( ) -> dict[str, Any]: """Return diagnostics for a config entry.""" homekit: HomeKit = hass.data[DOMAIN][entry.entry_id][HOMEKIT] - driver: AccessoryDriver = homekit.driver data: dict[str, Any] = { "status": homekit.status, "config-entry": { @@ -28,8 +27,9 @@ async def async_get_config_entry_diagnostics( "options": dict(entry.options), }, } - if not driver: + if not hasattr(homekit, "driver"): return data + driver: AccessoryDriver = homekit.driver data.update(driver.get_accessories()) state: State = driver.state data.update( diff --git a/homeassistant/components/homekit/logbook.py b/homeassistant/components/homekit/logbook.py index 0ea5a5d542a..b6805f8cf6c 100644 --- a/homeassistant/components/homekit/logbook.py +++ b/homeassistant/components/homekit/logbook.py @@ -1,16 +1,22 @@ """Describe logbook events.""" +from collections.abc import Callable +from typing import Any + from homeassistant.const import ATTR_ENTITY_ID, ATTR_SERVICE -from homeassistant.core import callback +from homeassistant.core import Event, HomeAssistant, callback from .const import ATTR_DISPLAY_NAME, ATTR_VALUE, DOMAIN, EVENT_HOMEKIT_CHANGED @callback -def async_describe_events(hass, async_describe_event): +def async_describe_events( + hass: HomeAssistant, + async_describe_event: Callable[[str, str, Callable[[Event], dict[str, Any]]], None], +) -> None: """Describe logbook events.""" @callback - def async_describe_logbook_event(event): + def async_describe_logbook_event(event: Event) -> dict[str, Any]: """Describe a logbook event.""" data = event.data entity_id = data.get(ATTR_ENTITY_ID) diff --git a/homeassistant/components/homekit/strings.json b/homeassistant/components/homekit/strings.json index ead140bdaf1..d2b6951a698 100644 --- a/homeassistant/components/homekit/strings.json +++ b/homeassistant/components/homekit/strings.json @@ -1,73 +1,73 @@ { - "options": { - "step": { - "yaml": { - "title": "Adjust HomeKit Options", - "description": "This entry is controlled via YAML" - }, - "init": { - "data": { - "mode": "HomeKit Mode", - "include_exclude_mode": "Inclusion Mode", - "domains": "[%key:component::homekit::config::step::user::data::include_domains%]" - }, - "description": "HomeKit can be configured expose a bridge or a single accessory. In accessory mode, only a single entity can be used. Accessory mode is required for media players with the TV device class to function properly. Entities in the \u201cDomains to include\u201d will be included to HomeKit. You will be able to select which entities to include or exclude from this list on the next screen.", - "title": "Select mode and domains." - }, - "accessory": { - "data": { - "entities": "Entity" - }, - "title": "Select the entity for the accessory" - }, - "include": { - "data": { - "entities": "Entities" - }, - "description": "All “{domains}” entities will be included unless specific entities are selected.", - "title": "Select the entities to be included" - }, - "exclude": { - "data": { - "entities": "[%key:component::homekit::options::step::include::data::entities%]" - }, - "description": "All “{domains}” entities will be included except for the excluded entities and categorized entities.", - "title": "Select the entities to be excluded" - }, - "cameras": { - "data": { - "camera_copy": "Cameras that support native H.264 streams", - "camera_audio": "Cameras that support audio" - }, - "description": "Check all cameras that support native H.264 streams. If the camera does not output a H.264 stream, the system will transcode the video to H.264 for HomeKit. Transcoding requires a performant CPU and is unlikely to work on single board computers.", - "title": "Camera Configuration" - }, - "advanced": { - "data": { - "devices": "Devices (Triggers)", - "auto_start": "Autostart (disable if you are calling the homekit.start service manually)" - }, - "description": "Programmable switches are created for each selected device. When a device trigger fires, HomeKit can be configured to run an automation or scene.", - "title": "Advanced Configuration" - } - } - }, - "config": { - "step": { - "user": { - "data": { - "include_domains": "Domains to include" - }, - "description": "Choose the domains to be included. All supported entities in the domain will be included except for categorized entities. A separate HomeKit instance in accessory mode will be created for each tv media player, activity based remote, lock, and camera.", - "title": "Select domains to be included" - }, - "pairing": { - "title": "Pair HomeKit", - "description": "To complete pairing following the instructions in \u201cNotifications\u201d under \u201cHomeKit Pairing\u201d." - } + "options": { + "step": { + "yaml": { + "title": "Adjust HomeKit Options", + "description": "This entry is controlled via YAML" + }, + "init": { + "data": { + "mode": "HomeKit Mode", + "include_exclude_mode": "Inclusion Mode", + "domains": "[%key:component::homekit::config::step::user::data::include_domains%]" }, - "abort": { - "port_name_in_use": "An accessory or bridge with the same name or port is already configured." - } + "description": "HomeKit can be configured expose a bridge or a single accessory. In accessory mode, only a single entity can be used. Accessory mode is required for media players with the TV device class to function properly. Entities in the \u201cDomains to include\u201d will be included to HomeKit. You will be able to select which entities to include or exclude from this list on the next screen.", + "title": "Select mode and domains." + }, + "accessory": { + "data": { + "entities": "Entity" + }, + "title": "Select the entity for the accessory" + }, + "include": { + "data": { + "entities": "Entities" + }, + "description": "All “{domains}” entities will be included unless specific entities are selected.", + "title": "Select the entities to be included" + }, + "exclude": { + "data": { + "entities": "[%key:component::homekit::options::step::include::data::entities%]" + }, + "description": "All “{domains}” entities will be included except for the excluded entities and categorized entities.", + "title": "Select the entities to be excluded" + }, + "cameras": { + "data": { + "camera_copy": "Cameras that support native H.264 streams", + "camera_audio": "Cameras that support audio" + }, + "description": "Check all cameras that support native H.264 streams. If the camera does not output a H.264 stream, the system will transcode the video to H.264 for HomeKit. Transcoding requires a performant CPU and is unlikely to work on single board computers.", + "title": "Camera Configuration" + }, + "advanced": { + "data": { + "devices": "Devices (Triggers)", + "auto_start": "Autostart (disable if you are calling the homekit.start service manually)" + }, + "description": "Programmable switches are created for each selected device. When a device trigger fires, HomeKit can be configured to run an automation or scene.", + "title": "Advanced Configuration" + } } + }, + "config": { + "step": { + "user": { + "data": { + "include_domains": "Domains to include" + }, + "description": "Choose the domains to be included. All supported entities in the domain will be included except for categorized entities. A separate HomeKit instance in accessory mode will be created for each tv media player, activity based remote, lock, and camera.", + "title": "Select domains to be included" + }, + "pairing": { + "title": "Pair HomeKit", + "description": "To complete pairing following the instructions in \u201cNotifications\u201d under \u201cHomeKit Pairing\u201d." + } + }, + "abort": { + "port_name_in_use": "An accessory or bridge with the same name or port is already configured." + } + } } diff --git a/homeassistant/components/homekit/translations/es.json b/homeassistant/components/homekit/translations/es.json index 6008d399d64..cc1925b8095 100644 --- a/homeassistant/components/homekit/translations/es.json +++ b/homeassistant/components/homekit/translations/es.json @@ -35,6 +35,11 @@ "description": "Verifique todas las c\u00e1maras que admiten transmisiones H.264 nativas. Si la c\u00e1mara no emite una transmisi\u00f3n H.264, el sistema transcodificar\u00e1 el video a H.264 para HomeKit. La transcodificaci\u00f3n requiere una CPU de alto rendimiento y es poco probable que funcione en ordenadores de placa \u00fanica.", "title": "Seleccione el c\u00f3dec de video de la c\u00e1mara." }, + "exclude": { + "data": { + "entities": "Entidades" + } + }, "include_exclude": { "data": { "entities": "Entidades", diff --git a/homeassistant/components/homekit/translations/fr.json b/homeassistant/components/homekit/translations/fr.json index 7de4c531d6a..1977f7e6c18 100644 --- a/homeassistant/components/homekit/translations/fr.json +++ b/homeassistant/components/homekit/translations/fr.json @@ -28,7 +28,7 @@ "advanced": { "data": { "auto_start": "D\u00e9marrage automatique (d\u00e9sactiver si vous utilisez Z-Wave ou un autre syst\u00e8me de d\u00e9marrage diff\u00e9r\u00e9)", - "devices": "P\u00e9riph\u00e9riques (d\u00e9clencheurs)" + "devices": "Appareils (d\u00e9clencheurs)" }, "description": "Ces param\u00e8tres ne doivent \u00eatre ajust\u00e9s que si le pont HomeKit n'est pas fonctionnel.", "title": "Configuration avanc\u00e9e" diff --git a/homeassistant/components/homekit/translations/nl.json b/homeassistant/components/homekit/translations/nl.json index 9788f4904b6..2e16ffde8ef 100644 --- a/homeassistant/components/homekit/translations/nl.json +++ b/homeassistant/components/homekit/translations/nl.json @@ -12,7 +12,7 @@ "data": { "include_domains": "Domeinen om op te nemen" }, - "description": "Kies de domeinen die moeten worden opgenomen. Alle ondersteunde entiteiten in het domein zullen worden opgenomen. Voor elke tv-mediaspeler, camera, activiteiten gebaseerde afstandsbediening en slot wordt een afzonderlijke HomeKit-instantie in accessoiremodus aangemaakt.", + "description": "Kies de domeinen die moeten worden opgenomen. Alle ondersteunde entiteiten in het domein zullen worden opgenomen, behalve de gecategoriseerde entiteiten. Voor elke tv-mediaspeler, camera, activiteiten gebaseerde afstandsbediening en slot wordt een afzonderlijke HomeKit-instantie in accessoiremodus aangemaakt.", "title": "Selecteer domeinen die u wilt opnemen" } } diff --git a/homeassistant/components/homekit/type_sensors.py b/homeassistant/components/homekit/type_sensors.py index 46e241efab0..594d95494f1 100644 --- a/homeassistant/components/homekit/type_sensors.py +++ b/homeassistant/components/homekit/type_sensors.py @@ -35,6 +35,8 @@ from .const import ( CHAR_LEAK_DETECTED, CHAR_MOTION_DETECTED, CHAR_OCCUPANCY_DETECTED, + CHAR_PM10_DENSITY, + CHAR_PM25_DENSITY, CHAR_SMOKE_DETECTED, PROP_CELSIUS, SERV_AIR_QUALITY_SENSOR, @@ -51,7 +53,13 @@ from .const import ( THRESHOLD_CO, THRESHOLD_CO2, ) -from .util import convert_to_float, density_to_air_quality, temperature_to_homekit +from .util import ( + convert_to_float, + density_to_air_quality, + density_to_air_quality_pm10, + density_to_air_quality_pm25, + temperature_to_homekit, +) _LOGGER = logging.getLogger(__name__) @@ -156,6 +164,15 @@ class AirQualitySensor(HomeAccessory): """Initialize a AirQualitySensor accessory object.""" super().__init__(*args, category=CATEGORY_SENSOR) state = self.hass.states.get(self.entity_id) + + self.create_services() + + # Set the state so it is in sync on initial + # GET to avoid an event storm after homekit startup + self.async_update_state(state) + + def create_services(self): + """Initialize a AirQualitySensor accessory object.""" serv_air_quality = self.add_preload_service( SERV_AIR_QUALITY_SENSOR, [CHAR_AIR_PARTICULATE_DENSITY] ) @@ -163,9 +180,6 @@ class AirQualitySensor(HomeAccessory): self.char_density = serv_air_quality.configure_char( CHAR_AIR_PARTICULATE_DENSITY, value=0 ) - # Set the state so it is in sync on initial - # GET to avoid an event storm after homekit startup - self.async_update_state(state) @callback def async_update_state(self, new_state): @@ -179,6 +193,60 @@ class AirQualitySensor(HomeAccessory): _LOGGER.debug("%s: Set air_quality to %d", self.entity_id, air_quality) +@TYPES.register("PM10Sensor") +class PM10Sensor(AirQualitySensor): + """Generate a PM10Sensor accessory as PM 10 sensor.""" + + def create_services(self): + """Override the init function for PM 10 Sensor.""" + serv_air_quality = self.add_preload_service( + SERV_AIR_QUALITY_SENSOR, [CHAR_PM10_DENSITY] + ) + self.char_quality = serv_air_quality.configure_char(CHAR_AIR_QUALITY, value=0) + self.char_density = serv_air_quality.configure_char(CHAR_PM10_DENSITY, value=0) + + @callback + def async_update_state(self, new_state): + """Update accessory after state change.""" + density = convert_to_float(new_state.state) + if not density: + return + if self.char_density.value != density: + self.char_density.set_value(density) + _LOGGER.debug("%s: Set density to %d", self.entity_id, density) + air_quality = density_to_air_quality_pm10(density) + if self.char_quality.value != air_quality: + self.char_quality.set_value(air_quality) + _LOGGER.debug("%s: Set air_quality to %d", self.entity_id, air_quality) + + +@TYPES.register("PM25Sensor") +class PM25Sensor(AirQualitySensor): + """Generate a PM25Sensor accessory as PM 2.5 sensor.""" + + def create_services(self): + """Override the init function for PM 2.5 Sensor.""" + serv_air_quality = self.add_preload_service( + SERV_AIR_QUALITY_SENSOR, [CHAR_PM25_DENSITY] + ) + self.char_quality = serv_air_quality.configure_char(CHAR_AIR_QUALITY, value=0) + self.char_density = serv_air_quality.configure_char(CHAR_PM25_DENSITY, value=0) + + @callback + def async_update_state(self, new_state): + """Update accessory after state change.""" + density = convert_to_float(new_state.state) + if not density: + return + if self.char_density.value != density: + self.char_density.set_value(density) + _LOGGER.debug("%s: Set density to %d", self.entity_id, density) + air_quality = density_to_air_quality_pm25(density) + if self.char_quality.value != air_quality: + self.char_quality.set_value(air_quality) + _LOGGER.debug("%s: Set air_quality to %d", self.entity_id, air_quality) + + @TYPES.register("CarbonMonoxideSensor") class CarbonMonoxideSensor(HomeAccessory): """Generate a CarbonMonoxidSensor accessory as CO sensor.""" diff --git a/homeassistant/components/homekit/type_triggers.py b/homeassistant/components/homekit/type_triggers.py index 6d5f67f9915..4b3a7e73cac 100644 --- a/homeassistant/components/homekit/type_triggers.py +++ b/homeassistant/components/homekit/type_triggers.py @@ -1,8 +1,12 @@ """Class to hold all sensor accessories.""" +from __future__ import annotations + import logging +from typing import Any from pyhap.const import CATEGORY_SENSOR +from homeassistant.core import CALLBACK_TYPE, Context from homeassistant.helpers.trigger import async_initialize_triggers from .accessories import TYPES, HomeAccessory @@ -22,14 +26,21 @@ _LOGGER = logging.getLogger(__name__) class DeviceTriggerAccessory(HomeAccessory): """Generate a Programmable switch.""" - def __init__(self, *args, device_triggers=None, device_id=None): + def __init__( + self, + *args: Any, + device_triggers: list[dict[str, Any]] | None = None, + device_id: str | None = None, + ) -> None: """Initialize a Programmable switch accessory object.""" super().__init__(*args, category=CATEGORY_SENSOR, device_id=device_id) + assert device_triggers is not None self._device_triggers = device_triggers - self._remove_triggers = None + self._remove_triggers: CALLBACK_TYPE | None = None self.triggers = [] + assert device_triggers is not None for idx, trigger in enumerate(device_triggers): - type_ = trigger.get("type") + type_ = trigger["type"] subtype = trigger.get("subtype") trigger_name = ( f"{type_.title()} {subtype.title()}" if subtype else type_.title() @@ -53,7 +64,12 @@ class DeviceTriggerAccessory(HomeAccessory): serv_service_label.configure_char(CHAR_SERVICE_LABEL_NAMESPACE, value=1) serv_stateless_switch.add_linked_service(serv_service_label) - async def async_trigger(self, run_variables, context=None, skip_condition=False): + async def async_trigger( + self, + run_variables: dict, + context: Context | None = None, + skip_condition: bool = False, + ) -> None: """Trigger button press. This method is a coroutine. @@ -67,7 +83,7 @@ class DeviceTriggerAccessory(HomeAccessory): # Attach the trigger using the helper in async run # and detach it in async stop - async def run(self): + async def run(self) -> None: """Handle accessory driver started event.""" self._remove_triggers = await async_initialize_triggers( self.hass, @@ -78,12 +94,12 @@ class DeviceTriggerAccessory(HomeAccessory): _LOGGER.log, ) - async def stop(self): + async def stop(self) -> None: """Handle accessory driver stop event.""" if self._remove_triggers: self._remove_triggers() @property - def available(self): + def available(self) -> bool: """Return available.""" return True diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py index 7fa4ffa8bf6..be5c166b71d 100644 --- a/homeassistant/components/homekit/util.py +++ b/homeassistant/components/homekit/util.py @@ -8,7 +8,9 @@ import os import re import secrets import socket +from typing import Any, cast +from pyhap.accessory import Accessory import pyqrcode import voluptuous as vol @@ -34,7 +36,7 @@ from homeassistant.const import ( CONF_TYPE, TEMP_CELSIUS, ) -from homeassistant.core import HomeAssistant, callback, split_entity_id +from homeassistant.core import HomeAssistant, State, callback, split_entity_id import homeassistant.helpers.config_validation as cv from homeassistant.helpers.storage import STORAGE_DIR import homeassistant.util.temperature as temp_util @@ -242,7 +244,7 @@ HOMEKIT_CHAR_TRANSLATIONS = { } -def validate_entity_config(values): +def validate_entity_config(values: dict) -> dict[str, dict]: """Validate config entry for CONF_ENTITY.""" if not isinstance(values, dict): raise vol.Invalid("expected a dictionary") @@ -288,7 +290,7 @@ def validate_entity_config(values): return entities -def get_media_player_features(state): +def get_media_player_features(state: State) -> list[str]: """Determine features for media players.""" features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) @@ -306,7 +308,7 @@ def get_media_player_features(state): return supported_modes -def validate_media_player_features(state, feature_list): +def validate_media_player_features(state: State, feature_list: str) -> bool: """Validate features for media players.""" if not (supported_modes := get_media_player_features(state)): _LOGGER.error("%s does not support any media_player features", state.entity_id) @@ -329,7 +331,9 @@ def validate_media_player_features(state, feature_list): return True -def async_show_setup_message(hass, entry_id, bridge_name, pincode, uri): +def async_show_setup_message( + hass: HomeAssistant, entry_id: str, bridge_name: str, pincode: bytes, uri: str +) -> None: """Display persistent notification with setup information.""" pin = pincode.decode() _LOGGER.info("Pincode: %s", pin) @@ -351,12 +355,12 @@ def async_show_setup_message(hass, entry_id, bridge_name, pincode, uri): persistent_notification.async_create(hass, message, "HomeKit Pairing", entry_id) -def async_dismiss_setup_message(hass, entry_id): +def async_dismiss_setup_message(hass: HomeAssistant, entry_id: str) -> None: """Dismiss persistent notification and remove QR code.""" persistent_notification.async_dismiss(hass, entry_id) -def convert_to_float(state): +def convert_to_float(state: Any) -> float | None: """Return float of state, catch errors.""" try: return float(state) @@ -384,17 +388,17 @@ def cleanup_name_for_homekit(name: str | None) -> str: return name.translate(HOMEKIT_CHAR_TRANSLATIONS)[:MAX_NAME_LENGTH] -def temperature_to_homekit(temperature, unit): +def temperature_to_homekit(temperature: float | int, unit: str) -> float: """Convert temperature to Celsius for HomeKit.""" return round(temp_util.convert(temperature, unit, TEMP_CELSIUS), 1) -def temperature_to_states(temperature, unit): +def temperature_to_states(temperature: float | int, unit: str) -> float: """Convert temperature back from Celsius to Home Assistant unit.""" return round(temp_util.convert(temperature, TEMP_CELSIUS, unit) * 2) / 2 -def density_to_air_quality(density): +def density_to_air_quality(density: float) -> int: """Map PM2.5 density to HomeKit AirQuality level.""" if density <= 35: return 1 @@ -407,22 +411,48 @@ def density_to_air_quality(density): return 5 -def get_persist_filename_for_entry_id(entry_id: str): +def density_to_air_quality_pm10(density: float) -> int: + """Map PM10 density to HomeKit AirQuality level.""" + if density <= 40: + return 1 + if density <= 80: + return 2 + if density <= 120: + return 3 + if density <= 300: + return 4 + return 5 + + +def density_to_air_quality_pm25(density: float) -> int: + """Map PM2.5 density to HomeKit AirQuality level.""" + if density <= 25: + return 1 + if density <= 50: + return 2 + if density <= 100: + return 3 + if density <= 300: + return 4 + return 5 + + +def get_persist_filename_for_entry_id(entry_id: str) -> str: """Determine the filename of the homekit state file.""" return f"{DOMAIN}.{entry_id}.state" -def get_aid_storage_filename_for_entry_id(entry_id: str): +def get_aid_storage_filename_for_entry_id(entry_id: str) -> str: """Determine the ilename of homekit aid storage file.""" return f"{DOMAIN}.{entry_id}.aids" -def get_persist_fullpath_for_entry_id(hass: HomeAssistant, entry_id: str): +def get_persist_fullpath_for_entry_id(hass: HomeAssistant, entry_id: str) -> str: """Determine the path to the homekit state file.""" return hass.config.path(STORAGE_DIR, get_persist_filename_for_entry_id(entry_id)) -def get_aid_storage_fullpath_for_entry_id(hass: HomeAssistant, entry_id: str): +def get_aid_storage_fullpath_for_entry_id(hass: HomeAssistant, entry_id: str) -> str: """Determine the path to the homekit aid storage file.""" return hass.config.path( STORAGE_DIR, get_aid_storage_filename_for_entry_id(entry_id) @@ -433,7 +463,7 @@ def _format_version_part(version_part: str) -> str: return str(max(0, min(MAX_VERSION_PART, coerce_int(version_part)))) -def format_version(version): +def format_version(version: str) -> str | None: """Extract the version string in a format homekit can consume.""" split_ver = str(version).replace("-", ".").replace(" ", ".") num_only = NUMBERS_ONLY_RE.sub("", split_ver) @@ -443,12 +473,12 @@ def format_version(version): return None if _is_zero_but_true(value) else value -def _is_zero_but_true(value): +def _is_zero_but_true(value: Any) -> bool: """Zero but true values can crash apple watches.""" return convert_to_float(value) == 0 -def remove_state_files_for_entry_id(hass: HomeAssistant, entry_id: str): +def remove_state_files_for_entry_id(hass: HomeAssistant, entry_id: str) -> bool: """Remove the state files from disk.""" persist_file_path = get_persist_fullpath_for_entry_id(hass, entry_id) aid_storage_path = get_aid_storage_fullpath_for_entry_id(hass, entry_id) @@ -458,7 +488,7 @@ def remove_state_files_for_entry_id(hass: HomeAssistant, entry_id: str): return True -def _get_test_socket(): +def _get_test_socket() -> socket.socket: """Create a socket to test binding ports.""" test_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) test_socket.setblocking(False) @@ -501,9 +531,10 @@ def _async_find_next_available_port(start_port: int, exclude_ports: set) -> int: if port == MAX_PORT: raise continue + raise RuntimeError("unreachable") -def pid_is_alive(pid) -> bool: +def pid_is_alive(pid: int) -> bool: """Check to see if a process is alive.""" try: os.kill(pid, 0) @@ -513,14 +544,14 @@ def pid_is_alive(pid) -> bool: return False -def accessory_friendly_name(hass_name, accessory): +def accessory_friendly_name(hass_name: str, accessory: Accessory) -> str: """Return the combined name for the accessory. The mDNS name and the Home Assistant config entry name are usually different which means they need to see both to identify the accessory. """ - accessory_mdns_name = accessory.display_name + accessory_mdns_name = cast(str, accessory.display_name) if hass_name.casefold().startswith(accessory_mdns_name.casefold()): return hass_name if accessory_mdns_name.casefold().startswith(hass_name.casefold()): @@ -528,7 +559,7 @@ def accessory_friendly_name(hass_name, accessory): return f"{hass_name} ({accessory_mdns_name})" -def state_needs_accessory_mode(state): +def state_needs_accessory_mode(state: State) -> bool: """Return if the entity represented by the state must be paired in accessory mode.""" if state.domain in (CAMERA_DOMAIN, LOCK_DOMAIN): return True diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py index e4f8f715bc8..6b538658b23 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -18,6 +18,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import Event, HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import DeviceInfo, Entity from homeassistant.helpers.typing import ConfigType @@ -65,8 +66,10 @@ class HomeKitEntity(Entity): async def async_added_to_hass(self) -> None: """Entity added to hass.""" self.async_on_remove( - self.hass.helpers.dispatcher.async_dispatcher_connect( - self._accessory.signal_state_updated, self.async_write_ha_state + async_dispatcher_connect( + self.hass, + self._accessory.signal_state_updated, + self.async_write_ha_state, ) ) diff --git a/homeassistant/components/homekit_controller/fan.py b/homeassistant/components/homekit_controller/fan.py index 71d71ce469f..3f48d34559b 100644 --- a/homeassistant/components/homekit_controller/fan.py +++ b/homeassistant/components/homekit_controller/fan.py @@ -117,7 +117,6 @@ class BaseHomeKitFan(HomeKitEntity, FanEntity): async def async_turn_on( self, - speed: str | None = None, percentage: int | None = None, preset_mode: str | None = None, **kwargs: Any, diff --git a/homeassistant/components/homekit_controller/strings.select.json b/homeassistant/components/homekit_controller/strings.select.json index 83f83e56ec2..af925b6681e 100644 --- a/homeassistant/components/homekit_controller/strings.select.json +++ b/homeassistant/components/homekit_controller/strings.select.json @@ -1,9 +1,9 @@ { - "state": { - "homekit_controller__ecobee_mode": { - "home": "Home", - "sleep": "Sleep", - "away": "Away" - } + "state": { + "homekit_controller__ecobee_mode": { + "home": "Home", + "sleep": "Sleep", + "away": "Away" } -} \ No newline at end of file + } +} diff --git a/homeassistant/components/homekit_controller/translations/fr.json b/homeassistant/components/homekit_controller/translations/fr.json index 18f3e82aa76..01286ceb991 100644 --- a/homeassistant/components/homekit_controller/translations/fr.json +++ b/homeassistant/components/homekit_controller/translations/fr.json @@ -7,7 +7,7 @@ "already_paired": "Cet accessoire est d\u00e9j\u00e0 associ\u00e9 \u00e0 un autre appareil. R\u00e9initialisez l\u2019accessoire et r\u00e9essayez.", "ignored_model": "La prise en charge de HomeKit pour ce mod\u00e8le est bloqu\u00e9e car une int\u00e9gration native plus compl\u00e8te est disponible.", "invalid_config_entry": "Cet appareil est pr\u00eat \u00e0 \u00eatre coupl\u00e9, mais il existe d\u00e9j\u00e0 une entr\u00e9e de configuration en conflit dans Home Assistant \u00e0 supprimer.", - "invalid_properties": "Propri\u00e9t\u00e9s invalides annonc\u00e9es par l'appareil.", + "invalid_properties": "Propri\u00e9t\u00e9s annonc\u00e9es par l'appareil non valides.", "no_devices": "Aucun appareil non appair\u00e9 n'a pu \u00eatre trouv\u00e9" }, "error": { @@ -65,8 +65,8 @@ }, "trigger_type": { "double_press": "\" {subtype} \" appuy\u00e9 deux fois", - "long_press": "\" {subtype} \" enfonc\u00e9 et maintenu", - "single_press": "\" {subtype} \" press\u00e9" + "long_press": "\u00ab\u00a0{subtype}\u00a0\u00bb enfonc\u00e9 et maintenu", + "single_press": "\u00ab\u00a0{subtype}\u00a0\u00bb enfonc\u00e9" } }, "title": "Accessoire HomeKit" diff --git a/homeassistant/components/homekit_controller/translations/select.fr.json b/homeassistant/components/homekit_controller/translations/select.fr.json index 9da1f007992..91f99ef94ef 100644 --- a/homeassistant/components/homekit_controller/translations/select.fr.json +++ b/homeassistant/components/homekit_controller/translations/select.fr.json @@ -1,7 +1,7 @@ { "state": { "homekit_controller__ecobee_mode": { - "away": "Loin", + "away": "Absent", "home": "Domicile", "sleep": "Sommeil" } diff --git a/homeassistant/components/homematic/entity.py b/homeassistant/components/homematic/entity.py index 8e83484505b..1fe3799bbd9 100644 --- a/homeassistant/components/homematic/entity.py +++ b/homeassistant/components/homematic/entity.py @@ -50,7 +50,7 @@ class HMDevice(Entity): self._data: dict[str, str] = {} self._connected = False self._available = False - self._channel_map: set[str] = set() + self._channel_map: dict[str, str] = {} if entity_description is not None: self.entity_description = entity_description @@ -127,7 +127,7 @@ class HMDevice(Entity): has_changed = False # Is data needed for this instance? - if f"{attribute}:{device.partition(':')[2]}" in self._channel_map: + if device.partition(":")[2] == self._channel_map.get(attribute): self._data[attribute] = value has_changed = True @@ -143,12 +143,12 @@ class HMDevice(Entity): def _subscribe_homematic_events(self): """Subscribe all required events to handle job.""" for metadata in ( - self._hmdevice.SENSORNODE, - self._hmdevice.BINARYNODE, - self._hmdevice.ATTRIBUTENODE, - self._hmdevice.WRITENODE, - self._hmdevice.EVENTNODE, self._hmdevice.ACTIONNODE, + self._hmdevice.EVENTNODE, + self._hmdevice.WRITENODE, + self._hmdevice.ATTRIBUTENODE, + self._hmdevice.BINARYNODE, + self._hmdevice.SENSORNODE, ): for node, channels in metadata.items(): # Data is needed for this instance @@ -159,7 +159,9 @@ class HMDevice(Entity): else: channel = self._channel # Remember the channel for this attribute to ignore invalid events later - self._channel_map.add(f"{node}:{channel!s}") + self._channel_map[node] = str(channel) + + _LOGGER.debug("Channel map for %s: %s", self._address, str(self._channel_map)) # Set callbacks self._hmdevice.setEventCallback(callback=self._hm_event_callback, bequeath=True) diff --git a/homeassistant/components/homematic/services.yaml b/homeassistant/components/homematic/services.yaml index 15099c790fb..28b6577cdf9 100644 --- a/homeassistant/components/homematic/services.yaml +++ b/homeassistant/components/homematic/services.yaml @@ -103,11 +103,11 @@ set_device_value: selector: select: options: - - 'boolean' - - 'dateTime.iso8601' - - 'double' - - 'int' - - 'string' + - "boolean" + - "dateTime.iso8601" + - "double" + - "int" + - "string" reconnect: name: Reconnect diff --git a/homeassistant/components/homematicip_cloud/light.py b/homeassistant/components/homematicip_cloud/light.py index a45759f9633..457f2fdcd25 100644 --- a/homeassistant/components/homematicip_cloud/light.py +++ b/homeassistant/components/homematicip_cloud/light.py @@ -32,9 +32,6 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import DOMAIN as HMIPC_DOMAIN, HomematicipGenericEntity from .hap import HomematicipHAP -ATTR_TODAY_ENERGY_KWH = "today_energy_kwh" -ATTR_CURRENT_POWER_W = "current_power_w" - async def async_setup_entry( hass: HomeAssistant, @@ -94,19 +91,6 @@ class HomematicipLight(HomematicipGenericEntity, LightEntity): class HomematicipLightMeasuring(HomematicipLight): """Representation of the HomematicIP measuring light.""" - @property - def extra_state_attributes(self) -> dict[str, Any]: - """Return the state attributes of the light.""" - state_attr = super().extra_state_attributes - - current_power_w = self._device.currentPowerConsumption - if current_power_w > 0.05: - state_attr[ATTR_CURRENT_POWER_W] = round(current_power_w, 2) - - state_attr[ATTR_TODAY_ENERGY_KWH] = round(self._device.energyCounter, 2) - - return state_attr - class HomematicipMultiDimmer(HomematicipGenericEntity, LightEntity): """Representation of HomematicIP Cloud dimmer.""" diff --git a/homeassistant/components/homematicip_cloud/services.yaml b/homeassistant/components/homematicip_cloud/services.yaml index ae8a6f34049..ebb83a0845f 100644 --- a/homeassistant/components/homematicip_cloud/services.yaml +++ b/homeassistant/components/homematicip_cloud/services.yaml @@ -59,7 +59,7 @@ activate_vacation: min: 0 max: 55 step: 0.5 - unit_of_measurement: '°' + unit_of_measurement: "°" accesspoint_id: name: Accesspoint ID description: The ID of the Homematic IP Access Point diff --git a/homeassistant/components/homematicip_cloud/switch.py b/homeassistant/components/homematicip_cloud/switch.py index 3237fdf1f00..82eafe1f212 100644 --- a/homeassistant/components/homematicip_cloud/switch.py +++ b/homeassistant/components/homematicip_cloud/switch.py @@ -166,15 +166,3 @@ class HomematicipGroupSwitch(HomematicipGenericEntity, SwitchEntity): class HomematicipSwitchMeasuring(HomematicipSwitch): """Representation of the HomematicIP measuring switch.""" - - @property - def current_power_w(self) -> float: - """Return the current power usage in W.""" - return self._device.currentPowerConsumption - - @property - def today_energy_kwh(self) -> int: - """Return the today total energy usage in kWh.""" - if self._device.energyCounter is None: - return 0 - return round(self._device.energyCounter) diff --git a/homeassistant/components/homematicip_cloud/translations/fr.json b/homeassistant/components/homematicip_cloud/translations/fr.json index 106ff6225d5..01e1d1ed8c2 100644 --- a/homeassistant/components/homematicip_cloud/translations/fr.json +++ b/homeassistant/components/homematicip_cloud/translations/fr.json @@ -15,7 +15,7 @@ "init": { "data": { "hapid": "ID du point d'acc\u00e8s (SGTIN)", - "name": "Nom (facultatif, utilis\u00e9 comme pr\u00e9fixe de nom pour tous les p\u00e9riph\u00e9riques)", + "name": "Nom (facultatif, utilis\u00e9 comme pr\u00e9fixe de nom pour tous les appareils)", "pin": "Code PIN" }, "title": "Choisissez le point d'acc\u00e8s HomematicIP" diff --git a/homeassistant/components/homewizard/manifest.json b/homeassistant/components/homewizard/manifest.json index 0705da4938b..1bd856334b7 100644 --- a/homeassistant/components/homewizard/manifest.json +++ b/homeassistant/components/homewizard/manifest.json @@ -4,9 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/homewizard", "codeowners": ["@DCSBL"], "dependencies": [], - "requirements": [ - "aiohwenergy==0.8.0" - ], + "requirements": ["aiohwenergy==0.8.0"], "zeroconf": ["_hwenergy._tcp.local."], "config_flow": true, "iot_class": "local_polling", diff --git a/homeassistant/components/homewizard/sensor.py b/homeassistant/components/homewizard/sensor.py index e9a09f9db86..8863d819db2 100644 --- a/homeassistant/components/homewizard/sensor.py +++ b/homeassistant/components/homewizard/sensor.py @@ -142,7 +142,7 @@ async def async_setup_entry( async_add_entities(entities) -class HWEnergySensor(CoordinatorEntity[DeviceResponseEntry], SensorEntity): +class HWEnergySensor(CoordinatorEntity[HWEnergyDeviceUpdateCoordinator], SensorEntity): """Representation of a HomeWizard Sensor.""" def __init__( diff --git a/homeassistant/components/homewizard/switch.py b/homeassistant/components/homewizard/switch.py index 7860370baa7..3c6b1a1c5dc 100644 --- a/homeassistant/components/homewizard/switch.py +++ b/homeassistant/components/homewizard/switch.py @@ -31,11 +31,11 @@ async def async_setup_entry( ) -class HWEnergySwitchEntity(CoordinatorEntity, SwitchEntity): +class HWEnergySwitchEntity( + CoordinatorEntity[HWEnergyDeviceUpdateCoordinator], SwitchEntity +): """Representation switchable entity.""" - coordinator: HWEnergyDeviceUpdateCoordinator - def __init__( self, coordinator: HWEnergyDeviceUpdateCoordinator, diff --git a/homeassistant/components/homewizard/translations/el.json b/homeassistant/components/homewizard/translations/el.json index f3d7c392109..dd85c5e87f5 100644 --- a/homeassistant/components/homewizard/translations/el.json +++ b/homeassistant/components/homewizard/translations/el.json @@ -4,7 +4,7 @@ "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", "api_not_enabled": "\u03a4\u03bf API \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03bf. \u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf API \u03c3\u03c4\u03b7\u03bd \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae HomeWizard Energy App \u03c3\u03c4\u03b9\u03c2 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2", "device_not_supported": "\u0391\u03c5\u03c4\u03ae \u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b4\u03b5\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9", - "invalid_discovery_parameters": "unsupported_api_version", + "invalid_discovery_parameters": "\u0395\u03bd\u03c4\u03bf\u03c0\u03af\u03c3\u03c4\u03b7\u03ba\u03b5 \u03bc\u03b7 \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03b9\u03b6\u03cc\u03bc\u03b5\u03bd\u03b7 \u03ad\u03ba\u03b4\u03bf\u03c3\u03b7 API", "unknown_error": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" }, "step": { diff --git a/homeassistant/components/homewizard/translations/fr.json b/homeassistant/components/homewizard/translations/fr.json index 6ddc51565fb..f935f830ac9 100644 --- a/homeassistant/components/homewizard/translations/fr.json +++ b/homeassistant/components/homewizard/translations/fr.json @@ -2,14 +2,14 @@ "config": { "abort": { "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", - "api_not_enabled": "L'API n'est pas activ\u00e9e. Activer l'API dans l'application HomeWizard Energy dans les param\u00e8tres", + "api_not_enabled": "L'API n'est pas activ\u00e9e. Activez l'API dans les param\u00e8tres de l'application HomeWizard Energy", "device_not_supported": "Cet appareil n'est pas compatible", "invalid_discovery_parameters": "Version d'API non prise en charge d\u00e9tect\u00e9e", "unknown_error": "Erreur inattendue" }, "step": { "discovery_confirm": { - "description": "Voulez-vous configurer {product_type} ( {serial} ) \u00e0 {ip_address}\u00a0?", + "description": "Voulez-vous configurer {product_type} ({serial}) sur {ip_address}\u00a0?", "title": "Confirmer" }, "user": { diff --git a/homeassistant/components/honeywell/__init__.py b/homeassistant/components/honeywell/__init__.py index bafd4c470db..d141822e2ea 100644 --- a/homeassistant/components/honeywell/__init__.py +++ b/homeassistant/components/honeywell/__init__.py @@ -6,19 +6,48 @@ import somecomfort from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.util import Throttle -from .const import _LOGGER, CONF_DEV_ID, CONF_LOC_ID, DOMAIN +from .const import ( + _LOGGER, + CONF_COOL_AWAY_TEMPERATURE, + CONF_DEV_ID, + CONF_HEAT_AWAY_TEMPERATURE, + CONF_LOC_ID, + DOMAIN, +) UPDATE_LOOP_SLEEP_TIME = 5 MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=300) PLATFORMS = [Platform.CLIMATE] +MIGRATE_OPTIONS_KEYS = {CONF_COOL_AWAY_TEMPERATURE, CONF_HEAT_AWAY_TEMPERATURE} + + +@callback +def _async_migrate_data_to_options( + hass: HomeAssistant, config_entry: ConfigEntry +) -> None: + if not MIGRATE_OPTIONS_KEYS.intersection(config_entry.data): + return + hass.config_entries.async_update_entry( + config_entry, + data={ + k: v for k, v in config_entry.data.items() if k not in MIGRATE_OPTIONS_KEYS + }, + options={ + **config_entry.options, + **{k: config_entry.data.get(k) for k in MIGRATE_OPTIONS_KEYS}, + }, + ) + async def async_setup_entry(hass: HomeAssistant, config: ConfigEntry) -> bool: """Set up the Honeywell thermostat.""" + _async_migrate_data_to_options(hass, config) + username = config.data[CONF_USERNAME] password = config.data[CONF_PASSWORD] diff --git a/homeassistant/components/honeywell/climate.py b/homeassistant/components/honeywell/climate.py index f0e18953402..f8c07de7184 100644 --- a/homeassistant/components/honeywell/climate.py +++ b/homeassistant/components/honeywell/climate.py @@ -81,8 +81,8 @@ PARALLEL_UPDATES = 1 async def async_setup_entry(hass, config, async_add_entities, discovery_info=None): """Set up the Honeywell thermostat.""" - cool_away_temp = config.data.get(CONF_COOL_AWAY_TEMPERATURE) - heat_away_temp = config.data.get(CONF_HEAT_AWAY_TEMPERATURE) + cool_away_temp = config.options.get(CONF_COOL_AWAY_TEMPERATURE) + heat_away_temp = config.options.get(CONF_HEAT_AWAY_TEMPERATURE) data = hass.data[DOMAIN][config.entry_id] diff --git a/homeassistant/components/honeywell/config_flow.py b/homeassistant/components/honeywell/config_flow.py index 505a49f062b..e6fdd9b54bd 100644 --- a/homeassistant/components/honeywell/config_flow.py +++ b/homeassistant/components/honeywell/config_flow.py @@ -3,9 +3,16 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import callback from . import get_somecomfort_client -from .const import DOMAIN +from .const import ( + CONF_COOL_AWAY_TEMPERATURE, + CONF_HEAT_AWAY_TEMPERATURE, + DEFAULT_COOL_AWAY_TEMPERATURE, + DEFAULT_HEAT_AWAY_TEMPERATURE, + DOMAIN, +) class HoneywellConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @@ -42,3 +49,42 @@ class HoneywellConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) return client is not None + + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Options callback for Honeywell.""" + return HoneywellOptionsFlowHandler(config_entry) + + +class HoneywellOptionsFlowHandler(config_entries.OptionsFlow): + """Config flow options for Honeywell.""" + + def __init__(self, entry: config_entries.ConfigEntry) -> None: + """Initialize Honeywell options flow.""" + self.config_entry = 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=DOMAIN, data=user_input) + + return self.async_show_form( + step_id="init", + data_schema=vol.Schema( + { + vol.Required( + CONF_COOL_AWAY_TEMPERATURE, + default=self.config_entry.options.get( + CONF_COOL_AWAY_TEMPERATURE, DEFAULT_COOL_AWAY_TEMPERATURE + ), + ): int, + vol.Required( + CONF_HEAT_AWAY_TEMPERATURE, + default=self.config_entry.options.get( + CONF_HEAT_AWAY_TEMPERATURE, DEFAULT_HEAT_AWAY_TEMPERATURE + ), + ): int, + } + ), + ) diff --git a/homeassistant/components/honeywell/const.py b/homeassistant/components/honeywell/const.py index 2dce56046a3..08e7c3fc14e 100644 --- a/homeassistant/components/honeywell/const.py +++ b/homeassistant/components/honeywell/const.py @@ -5,6 +5,8 @@ DOMAIN = "honeywell" CONF_COOL_AWAY_TEMPERATURE = "away_cool_temperature" CONF_HEAT_AWAY_TEMPERATURE = "away_heat_temperature" +DEFAULT_COOL_AWAY_TEMPERATURE = 88 +DEFAULT_HEAT_AWAY_TEMPERATURE = 61 CONF_DEV_ID = "thermostat" CONF_LOC_ID = "location" diff --git a/homeassistant/components/honeywell/strings.json b/homeassistant/components/honeywell/strings.json index ce76b571996..8e085ad7e86 100644 --- a/homeassistant/components/honeywell/strings.json +++ b/homeassistant/components/honeywell/strings.json @@ -2,7 +2,6 @@ "config": { "step": { "user": { - "title": "Honeywell Total Connect Comfort (US)", "description": "Please enter the credentials used to log into mytotalconnectcomfort.com.", "data": { "username": "[%key:common::config_flow::data::username%]", @@ -13,5 +12,16 @@ "error": { "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" } + }, + "options": { + "step": { + "init": { + "description": "Additional Honeywell config options. Temperatures are set in Fahrenheit.", + "data": { + "away_cool_temperature": "Away cool temperature", + "away_heat_temperature": "Away heat temperature" + } + } + } } } diff --git a/homeassistant/components/honeywell/translations/en.json b/homeassistant/components/honeywell/translations/en.json index 454093c5b3e..168d3a5b93d 100644 --- a/homeassistant/components/honeywell/translations/en.json +++ b/homeassistant/components/honeywell/translations/en.json @@ -13,5 +13,17 @@ "title": "Honeywell Total Connect Comfort (US)" } } + }, + "options": { + "step": { + "init": { + "data": { + "away_cool_temperature": "Away cool temperature", + "away_heat_temperature": "Away heat temperature" + }, + "description": "Additional Honeywell config options. Temperatures are set in Fahrenheit.", + "title": "Honeywell Options" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/honeywell/translations/fr.json b/homeassistant/components/honeywell/translations/fr.json index fbe3def3113..ac11cf20576 100644 --- a/homeassistant/components/honeywell/translations/fr.json +++ b/homeassistant/components/honeywell/translations/fr.json @@ -1,7 +1,7 @@ { "config": { "error": { - "invalid_auth": "Authentification invalide" + "invalid_auth": "Authentification non valide" }, "step": { "user": { diff --git a/homeassistant/components/horizon/media_player.py b/homeassistant/components/horizon/media_player.py index 31e905b7864..fd6b7b3d3cc 100644 --- a/homeassistant/components/horizon/media_player.py +++ b/homeassistant/components/horizon/media_player.py @@ -202,12 +202,12 @@ class HorizonDevice(MediaPlayerEntity): try: self._client.connect() self._client.authorize() - except AuthenticationError as msg: - _LOGGER.error("Authentication to %s failed: %s", self._name, msg) + except AuthenticationError as msg2: + _LOGGER.error("Authentication to %s failed: %s", self._name, msg2) return - except OSError as msg: + except OSError as msg2: # occurs if horizon box is offline - _LOGGER.error("Reconnect to %s failed: %s", self._name, msg) + _LOGGER.error("Reconnect to %s failed: %s", self._name, msg2) return self._send(key=key, channel=channel) diff --git a/homeassistant/components/html5/notify.py b/homeassistant/components/html5/notify.py index 0a6cc689292..a6ca769250f 100644 --- a/homeassistant/components/html5/notify.py +++ b/homeassistant/components/html5/notify.py @@ -533,7 +533,9 @@ class HTML5NotificationService(BaseNotificationService): if response.status_code == 410: _LOGGER.info("Notification channel has expired") reg = self.registrations.pop(target) - if not save_json(self.registrations_json_path, self.registrations): + try: + save_json(self.registrations_json_path, self.registrations) + except HomeAssistantError: self.registrations[target] = reg _LOGGER.error("Error saving registration") else: diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index a41329a1548..374e69975ce 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -365,10 +365,10 @@ class HomeAssistantHTTP: ) try: context = self._create_emergency_ssl_context() - except OSError as error: + except OSError as error2: _LOGGER.error( "Could not create an emergency self signed ssl certificate: %s", - error, + error2, ) context = None else: diff --git a/homeassistant/components/htu21d/__init__.py b/homeassistant/components/htu21d/__init__.py deleted file mode 100644 index c36c8bfcffb..00000000000 --- a/homeassistant/components/htu21d/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The htu21d component.""" diff --git a/homeassistant/components/htu21d/manifest.json b/homeassistant/components/htu21d/manifest.json deleted file mode 100644 index c554c775079..00000000000 --- a/homeassistant/components/htu21d/manifest.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "domain": "htu21d", - "name": "HTU21D(F) Sensor", - "documentation": "https://www.home-assistant.io/integrations/htu21d", - "requirements": ["i2csense==0.0.4", "smbus-cffi==0.5.1"], - "codeowners": [], - "iot_class": "local_push", - "loggers": ["i2csense", "smbus"] -} diff --git a/homeassistant/components/htu21d/sensor.py b/homeassistant/components/htu21d/sensor.py deleted file mode 100644 index e0f7e6d6fbc..00000000000 --- a/homeassistant/components/htu21d/sensor.py +++ /dev/null @@ -1,124 +0,0 @@ -"""Support for HTU21D temperature and humidity sensor.""" -from __future__ import annotations - -from datetime import timedelta -from functools import partial -import logging - -from i2csense.htu21d import HTU21D # pylint: disable=import-error -import smbus -import voluptuous as vol - -from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, - SensorDeviceClass, - SensorEntity, - SensorEntityDescription, -) -from homeassistant.const import CONF_NAME, PERCENTAGE, TEMP_CELSIUS -from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from homeassistant.util import Throttle - -_LOGGER = logging.getLogger(__name__) - -CONF_I2C_BUS = "i2c_bus" -DEFAULT_I2C_BUS = 1 - -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=5) - -DEFAULT_NAME = "HTU21D Sensor" - -SENSOR_TEMPERATURE = "temperature" -SENSOR_HUMIDITY = "humidity" - -SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( - SensorEntityDescription( - key=SENSOR_TEMPERATURE, - native_unit_of_measurement=TEMP_CELSIUS, - device_class=SensorDeviceClass.TEMPERATURE, - ), - SensorEntityDescription( - key=SENSOR_HUMIDITY, - native_unit_of_measurement=PERCENTAGE, - device_class=SensorDeviceClass.HUMIDITY, - ), -) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_I2C_BUS, default=DEFAULT_I2C_BUS): vol.Coerce(int), - } -) - - -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the HTU21D sensor.""" - _LOGGER.warning( - "The HTU21D(F) Sensor integration is deprecated and will be removed " - "in Home Assistant Core 2022.4; this integration is removed under " - "Architectural Decision Record 0019, more information can be found here: " - "https://github.com/home-assistant/architecture/blob/master/adr/0019-GPIO.md" - ) - - name = config.get(CONF_NAME) - bus_number = config.get(CONF_I2C_BUS) - - bus = smbus.SMBus(config.get(CONF_I2C_BUS)) - sensor = await hass.async_add_executor_job(partial(HTU21D, bus, logger=_LOGGER)) - if not sensor.sample_ok: - _LOGGER.error("HTU21D sensor not detected in bus %s", bus_number) - return - - sensor_handler = await hass.async_add_executor_job(HTU21DHandler, sensor) - - entities = [ - HTU21DSensor(sensor_handler, name, description) for description in SENSOR_TYPES - ] - - async_add_entities(entities) - - -class HTU21DHandler: - """Implement HTU21D communication.""" - - def __init__(self, sensor): - """Initialize the sensor handler.""" - self.sensor = sensor - self.sensor.update() - - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self): - """Read raw data and calculate temperature and humidity.""" - self.sensor.update() - - -class HTU21DSensor(SensorEntity): - """Implementation of the HTU21D sensor.""" - - def __init__(self, htu21d_client, name, description: SensorEntityDescription): - """Initialize the sensor.""" - self.entity_description = description - self._client = htu21d_client - - self._attr_name = f"{name}_{description.key}" - - async def async_update(self): - """Get the latest data from the HTU21D sensor and update the state.""" - await self.hass.async_add_executor_job(self._client.update) - if self._client.sensor.sample_ok: - if self.entity_description.key == SENSOR_TEMPERATURE: - value = round(self._client.sensor.temperature, 1) - else: - value = round(self._client.sensor.humidity, 1) - self._attr_native_value = value - else: - _LOGGER.warning("Bad sample") diff --git a/homeassistant/components/huawei_lte/__init__.py b/homeassistant/components/huawei_lte/__init__.py index 6f1eb6c7a60..ca15731641d 100644 --- a/homeassistant/components/huawei_lte/__init__.py +++ b/homeassistant/components/huawei_lte/__init__.py @@ -19,11 +19,10 @@ from huawei_lte_api.exceptions import ( ResponseErrorNotSupportedException, ) from requests.exceptions import Timeout -from url_normalize import url_normalize import voluptuous as vol from homeassistant.components.notify import DOMAIN as NOTIFY_DOMAIN -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_HW_VERSION, ATTR_MODEL, @@ -300,50 +299,13 @@ class HuaweiLteData(NamedTuple): """Shared state.""" hass_config: ConfigType - # Our YAML config, keyed by router URL - config: dict[str, dict[str, Any]] routers: dict[str, Router] -async def async_setup_entry( # noqa: C901 - hass: HomeAssistant, entry: ConfigEntry -) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Huawei LTE component from config entry.""" url = entry.data[CONF_URL] - # Override settings from YAML config, but only if they're changed in it - # Old values are stored as *_from_yaml in the config entry - if yaml_config := hass.data[DOMAIN].config.get(url): - # Config values - new_data = {} - for key in CONF_USERNAME, CONF_PASSWORD: - if key in yaml_config: - value = yaml_config[key] - if value != entry.data.get(f"{key}_from_yaml"): - new_data[f"{key}_from_yaml"] = value - new_data[key] = value - # Options - new_options = {} - yaml_recipient = yaml_config.get(NOTIFY_DOMAIN, {}).get(CONF_RECIPIENT) - if yaml_recipient is not None and yaml_recipient != entry.options.get( - f"{CONF_RECIPIENT}_from_yaml" - ): - new_options[f"{CONF_RECIPIENT}_from_yaml"] = yaml_recipient - new_options[CONF_RECIPIENT] = yaml_recipient - yaml_notify_name = yaml_config.get(NOTIFY_DOMAIN, {}).get(CONF_NAME) - if yaml_notify_name is not None and yaml_notify_name != entry.options.get( - f"{CONF_NAME}_from_yaml" - ): - new_options[f"{CONF_NAME}_from_yaml"] = yaml_notify_name - new_options[CONF_NAME] = yaml_notify_name - # Update entry if overrides were found - if new_data or new_options: - hass.config_entries.async_update_entry( - entry, - data={**entry.data, **new_data}, - options={**entry.options, **new_options}, - ) - def get_connection() -> Connection: """Set up a connection.""" if entry.options.get(CONF_UNAUTHENTICATED_MODE): @@ -512,14 +474,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # https://github.com/quandyfactory/dicttoxml/issues/60 logging.getLogger("dicttoxml").setLevel(logging.WARNING) - # Arrange our YAML config to dict with normalized URLs as keys - domain_config: dict[str, dict[str, Any]] = {} if DOMAIN not in hass.data: - hass.data[DOMAIN] = HuaweiLteData( - hass_config=config, config=domain_config, routers={} - ) - for router_config in config.get(DOMAIN, []): - domain_config[url_normalize(router_config.pop(CONF_URL))] = router_config + hass.data[DOMAIN] = HuaweiLteData(hass_config=config, routers={}) def service_handler(service: ServiceCall) -> None: """ @@ -580,19 +536,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: schema=SERVICE_SCHEMA, ) - for url, router_config in domain_config.items(): - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={ - CONF_URL: url, - CONF_USERNAME: router_config.get(CONF_USERNAME), - CONF_PASSWORD: router_config.get(CONF_PASSWORD), - }, - ) - ) - return True @@ -612,6 +555,9 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> data[CONF_MAC] = [] hass.config_entries.async_update_entry(config_entry, data=data) _LOGGER.info("Migrated config entry to version %d", config_entry.version) + # There can be no longer needed *_from_yaml data and options things left behind + # from pre-2022.4ish; they can be removed while at it when/if we eventually bump and + # migrate to version > 3 for some other reason. return True diff --git a/homeassistant/components/huawei_lte/config_flow.py b/homeassistant/components/huawei_lte/config_flow.py index be2a149b4d5..f4ea7cd86f7 100644 --- a/homeassistant/components/huawei_lte/config_flow.py +++ b/homeassistant/components/huawei_lte/config_flow.py @@ -89,12 +89,6 @@ class ConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors=errors or {}, ) - async def async_step_import( - self, user_input: dict[str, Any] | None = None - ) -> FlowResult: - """Handle import initiated config flow.""" - return await self.async_step_user(user_input) - async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: @@ -183,15 +177,15 @@ class ConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): info, wlan_settings = await self.hass.async_add_executor_job(get_device_info) await self.hass.async_add_executor_job(logout) + user_input[CONF_MAC] = get_device_macs(info, wlan_settings) + if not self.unique_id: if serial_number := info.get("SerialNumber"): await self.async_set_unique_id(serial_number) - self._abort_if_unique_id_configured() + self._abort_if_unique_id_configured(updates=user_input) else: await self._async_handle_discovery_without_unique_id() - user_input[CONF_MAC] = get_device_macs(info, wlan_settings) - title = ( self.context.get("title_placeholders", {}).get(CONF_NAME) or info.get("DeviceName") # device.information diff --git a/homeassistant/components/huawei_lte/translations/fr.json b/homeassistant/components/huawei_lte/translations/fr.json index ca360ffc077..1f5042d3aa1 100644 --- a/homeassistant/components/huawei_lte/translations/fr.json +++ b/homeassistant/components/huawei_lte/translations/fr.json @@ -9,8 +9,8 @@ "connection_timeout": "D\u00e9lai de connexion d\u00e9pass\u00e9", "incorrect_password": "Mot de passe incorrect", "incorrect_username": "Nom d'utilisateur incorrect", - "invalid_auth": "Authentification invalide", - "invalid_url": "URL invalide", + "invalid_auth": "Authentification non valide", + "invalid_url": "URL non valide", "login_attempts_exceeded": "Nombre maximal de tentatives de connexion d\u00e9pass\u00e9, veuillez r\u00e9essayer ult\u00e9rieurement", "response_error": "Erreur inconnue de l'appareil", "unknown": "Erreur inattendue" diff --git a/homeassistant/components/hue/scene.py b/homeassistant/components/hue/scene.py index c21a96e4d9a..1504e52e33d 100644 --- a/homeassistant/components/hue/scene.py +++ b/homeassistant/components/hue/scene.py @@ -16,6 +16,8 @@ from homeassistant.components.scene import ATTR_TRANSITION, Scene as SceneEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_platform +from homeassistant.helpers.device_registry import DeviceEntryType +from homeassistant.helpers.entity import DeviceInfo from .bridge import HueBridge from .const import DOMAIN @@ -106,8 +108,7 @@ class HueSceneEntity(HueBaseEntity, SceneEntity): @property def name(self) -> str: """Return default entity name.""" - group = self.controller.get_group(self.resource.id) - return f"{group.metadata.name} - {self.resource.metadata.name}" + return f"{self.group.metadata.name} {self.resource.metadata.name}" @property def is_dynamic(self) -> bool: @@ -167,3 +168,18 @@ class HueSceneEntity(HueBaseEntity, SceneEntity): "brightness": brightness, "is_dynamic": self.is_dynamic, } + + @property + def device_info(self) -> DeviceInfo: + """Return device (service) info.""" + # we create a virtual service/device for Hue scenes + # so we have a parent for grouped lights and scenes + return DeviceInfo( + identifiers={(DOMAIN, self.group.id)}, + entry_type=DeviceEntryType.SERVICE, + name=self.group.metadata.name, + manufacturer=self.bridge.api.config.bridge_device.product_data.manufacturer_name, + model=self.group.type.value.title(), + suggested_area=self.group.metadata.name, + via_device=(DOMAIN, self.bridge.api.config.bridge_device.id), + ) diff --git a/homeassistant/components/hue/translations/fr.json b/homeassistant/components/hue/translations/fr.json index 418354ceb96..ec9d104aa65 100644 --- a/homeassistant/components/hue/translations/fr.json +++ b/homeassistant/components/hue/translations/fr.json @@ -51,16 +51,16 @@ "turn_on": "Allumer" }, "trigger_type": { - "double_short_release": "Les deux \" {subtype} \" ont \u00e9t\u00e9 rel\u00e2ch\u00e9s", - "initial_press": "Bouton \" {subtype} \" appuy\u00e9 initialement", - "long_release": "Bouton \" {subtype} \" rel\u00e2ch\u00e9 apr\u00e8s un appui long", + "double_short_release": "Les deux \u00ab\u00a0{subtype}\u00a0\u00bb sont rel\u00e2ch\u00e9s", + "initial_press": "D\u00e9but de l'appui du bouton \u00ab\u00a0{subtype}\u00a0\u00bb", + "long_release": "Bouton \u00ab\u00a0{subtype}\u00a0\u00bb rel\u00e2ch\u00e9 apr\u00e8s un appui long", "remote_button_long_release": "Bouton \" {subtype} \" rel\u00e2ch\u00e9 apr\u00e8s un appui long", "remote_button_short_press": "bouton \"{subtype}\" est press\u00e9", "remote_button_short_release": "Bouton \" {subtype} \" est rel\u00e2ch\u00e9", "remote_double_button_long_press": "Les deux \"{sous-type}\" ont \u00e9t\u00e9 rel\u00e2ch\u00e9s apr\u00e8s un appui long", "remote_double_button_short_press": "Les deux \" {subtype} \" ont \u00e9t\u00e9 rel\u00e2ch\u00e9s", - "repeat": "Bouton \" {subtype} \" maintenu enfonc\u00e9", - "short_release": "Bouton \" {subtype} \" rel\u00e2ch\u00e9 apr\u00e8s un appui court" + "repeat": "Bouton \u00ab\u00a0{subtype}\u00a0\u00bb maintenu enfonc\u00e9", + "short_release": "Bouton \u00ab\u00a0{subtype}\u00a0\u00bb rel\u00e2ch\u00e9 apr\u00e8s un appui court" } }, "options": { diff --git a/homeassistant/components/hue/translations/zh-Hant.json b/homeassistant/components/hue/translations/zh-Hant.json index d607baf27b5..6c37c699340 100644 --- a/homeassistant/components/hue/translations/zh-Hant.json +++ b/homeassistant/components/hue/translations/zh-Hant.json @@ -6,7 +6,7 @@ "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", "cannot_connect": "\u9023\u7dda\u5931\u6557", "discover_timeout": "\u7121\u6cd5\u641c\u5c0b\u5230 Hue Bridge", - "no_bridges": "\u672a\u641c\u5c0b\u5230 Philips Hue Bridge", + "no_bridges": "\u672a\u767c\u73fe\u5230 Philips Hue Bridge", "not_hue_bridge": "\u975e Hue Bridge \u88dd\u7f6e", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, diff --git a/homeassistant/components/hue/v2/device.py b/homeassistant/components/hue/v2/device.py index 64bdcc7a4f2..c3deee40023 100644 --- a/homeassistant/components/hue/v2/device.py +++ b/homeassistant/components/hue/v2/device.py @@ -81,6 +81,10 @@ async def async_setup_devices(bridge: "HueBridge"): dev_reg, entry.entry_id ): if device not in known_devices: + # handle case where a virtual device was created for a Hue group + hue_dev_id = next(x[1] for x in device.identifiers if x[0] == DOMAIN) + if hue_dev_id in api.groups: + continue dev_reg.async_remove_device(device.id) # add listener for updates on Hue devices controller diff --git a/homeassistant/components/hue/v2/entity.py b/homeassistant/components/hue/v2/entity.py index 721425606bc..fec14075b42 100644 --- a/homeassistant/components/hue/v2/entity.py +++ b/homeassistant/components/hue/v2/entity.py @@ -9,6 +9,7 @@ from aiohue.v2.models.resource import ResourceTypes from aiohue.v2.models.zigbee_connectivity import ConnectivityServiceStatus from homeassistant.core import callback +from homeassistant.helpers.device_registry import async_get as async_get_device_registry from homeassistant.helpers.entity import DeviceInfo, Entity from homeassistant.helpers.entity_registry import async_get as async_get_entity_registry @@ -135,17 +136,21 @@ class HueBaseEntity(Entity): @callback def _handle_event(self, event_type: EventType, resource: HueResource) -> None: """Handle status event for this resource (or it's parent).""" - if event_type == EventType.RESOURCE_DELETED and resource.id == self.resource.id: - self.logger.debug("Received delete for %s", self.entity_id) - # non-device bound entities like groups and scenes need to be removed here - # all others will be be removed by device setup in case of device removal - ent_reg = async_get_entity_registry(self.hass) - ent_reg.async_remove(self.entity_id) - else: - self.logger.debug("Received status update for %s", self.entity_id) - self._check_availability() - self.on_update() - self.async_write_ha_state() + if event_type == EventType.RESOURCE_DELETED: + # remove any services created for zones/rooms + # regular devices are removed automatically by the logic in device.py. + if resource.type in [ResourceTypes.ROOM, ResourceTypes.ZONE]: + dev_reg = async_get_device_registry(self.hass) + if device := dev_reg.async_get_device({(DOMAIN, resource.id)}): + dev_reg.async_remove_device(device.id) + if resource.type in [ResourceTypes.GROUPED_LIGHT, ResourceTypes.SCENE]: + ent_reg = async_get_entity_registry(self.hass) + ent_reg.async_remove(self.entity_id) + return + self.logger.debug("Received status update for %s", self.entity_id) + self._check_availability() + self.on_update() + self.async_write_ha_state() @callback def _check_availability(self): diff --git a/homeassistant/components/hue/v2/group.py b/homeassistant/components/hue/v2/group.py index 162ef58d320..0eac6948790 100644 --- a/homeassistant/components/hue/v2/group.py +++ b/homeassistant/components/hue/v2/group.py @@ -1,7 +1,6 @@ """Support for Hue groups (room/zone).""" from __future__ import annotations -import asyncio from typing import Any from aiohue.v2 import HueBridgeV2 @@ -26,10 +25,12 @@ from homeassistant.components.light import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.device_registry import DeviceEntryType +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from ..bridge import HueBridge -from ..const import CONF_ALLOW_HUE_GROUPS, DOMAIN +from ..const import DOMAIN from .entity import HueBaseEntity from .helpers import ( normalize_hue_brightness, @@ -47,30 +48,22 @@ async def async_setup_entry( bridge: HueBridge = hass.data[DOMAIN][config_entry.entry_id] api: HueBridgeV2 = bridge.api - # to prevent race conditions (groupedlight is created before zone/room) - # we create groupedlights from the room/zone and actually use the - # underlying grouped_light resource for control - @callback - def async_add_light(event_type: EventType, resource: Room | Zone) -> None: + def async_add_light(event_type: EventType, resource: GroupedLight) -> None: """Add Grouped Light for Hue Room/Zone.""" - if grouped_light_id := resource.grouped_light: - grouped_light = api.groups.grouped_light[grouped_light_id] - light = GroupedHueLight(bridge, grouped_light, resource) - async_add_entities([light]) + group = api.groups.grouped_light.get_zone(resource.id) + if group is None: + return + light = GroupedHueLight(bridge, resource, group) + async_add_entities([light]) # add current items - for item in api.groups.room.items + api.groups.zone.items: + for item in api.groups.grouped_light.items: async_add_light(EventType.RESOURCE_ADDED, item) - # register listener for new zones/rooms + # register listener for new grouped_light config_entry.async_on_unload( - api.groups.room.subscribe( - async_add_light, event_filter=EventType.RESOURCE_ADDED - ) - ) - config_entry.async_on_unload( - api.groups.zone.subscribe( + api.groups.grouped_light.subscribe( async_add_light, event_filter=EventType.RESOURCE_ADDED ) ) @@ -94,11 +87,6 @@ class GroupedHueLight(HueBaseEntity, LightEntity): self._attr_supported_features |= SUPPORT_FLASH self._attr_supported_features |= SUPPORT_TRANSITION - # Entities for Hue groups are disabled by default - # unless they were enabled in old version (legacy option) - self._attr_entity_registry_enabled_default = bridge.config_entry.options.get( - CONF_ALLOW_HUE_GROUPS, False - ) self._dynamic_mode_active = False self._update_values() @@ -145,8 +133,24 @@ class GroupedHueLight(HueBaseEntity, LightEntity): "dynamics": self._dynamic_mode_active, } + @property + def device_info(self) -> DeviceInfo: + """Return device (service) info.""" + # we create a virtual service/device for Hue zones/rooms + # so we have a parent for grouped lights and scenes + model = self.group.type.value.title() + return DeviceInfo( + identifiers={(DOMAIN, self.group.id)}, + entry_type=DeviceEntryType.SERVICE, + name=self.group.metadata.name, + manufacturer=self.api.config.bridge_device.product_data.manufacturer_name, + model=model, + suggested_area=self.group.metadata.name if model == "Room" else None, + via_device=(DOMAIN, self.api.config.bridge_device.id), + ) + async def async_turn_on(self, **kwargs: Any) -> None: - """Turn the light on.""" + """Turn the grouped_light on.""" transition = normalize_hue_transition(kwargs.get(ATTR_TRANSITION)) xy_color = kwargs.get(ATTR_XY_COLOR) color_temp = normalize_hue_colortemp(kwargs.get(ATTR_COLOR_TEMP)) @@ -155,38 +159,16 @@ class GroupedHueLight(HueBaseEntity, LightEntity): if flash is not None: await self.async_set_flash(flash) - # flash can not be sent with other commands at the same time return - # NOTE: a grouped_light can only handle turn on/off - # To set other features, you'll have to control the attached lights - if ( - brightness is None - and xy_color is None - and color_temp is None - and transition is None - and flash is None - ): - await self.bridge.async_request_call( - self.controller.set_state, id=self.resource.id, on=True - ) - return - - # redirect all other feature commands to underlying lights - # note that this silently ignores params sent to light that are not supported - await asyncio.gather( - *[ - self.bridge.async_request_call( - self.api.lights.set_state, - light.id, - on=True, - brightness=brightness if light.supports_dimming else None, - color_xy=xy_color if light.supports_color else None, - color_temp=color_temp if light.supports_color_temperature else None, - transition_time=transition, - ) - for light in self.controller.get_lights(self.resource.id) - ] + await self.bridge.async_request_call( + self.controller.set_state, + id=self.resource.id, + on=True, + brightness=brightness, + color_xy=xy_color, + color_temp=color_temp, + transition_time=transition, ) async def async_turn_off(self, **kwargs: Any) -> None: @@ -199,38 +181,19 @@ class GroupedHueLight(HueBaseEntity, LightEntity): # flash can not be sent with other commands at the same time return - # NOTE: a grouped_light can only handle turn on/off - # To set other features, you'll have to control the attached lights - if transition is None: - await self.bridge.async_request_call( - self.controller.set_state, id=self.resource.id, on=False - ) - return - - # redirect all other feature commands to underlying lights - await asyncio.gather( - *[ - self.bridge.async_request_call( - self.api.lights.set_state, - light.id, - on=False, - transition_time=transition, - ) - for light in self.controller.get_lights(self.resource.id) - ] + await self.bridge.async_request_call( + self.controller.set_state, + id=self.resource.id, + on=False, + transition_time=transition, ) async def async_set_flash(self, flash: str) -> None: """Send flash command to light.""" - await asyncio.gather( - *[ - self.bridge.async_request_call( - self.api.lights.set_flash, - id=light.id, - short=flash == FLASH_SHORT, - ) - for light in self.controller.get_lights(self.resource.id) - ] + await self.bridge.async_request_call( + self.controller.set_flash, + id=self.resource.id, + short=flash == FLASH_SHORT, ) @callback diff --git a/homeassistant/components/hue/v2/light.py b/homeassistant/components/hue/v2/light.py index 999da408102..af3dfa80ffc 100644 --- a/homeassistant/components/hue/v2/light.py +++ b/homeassistant/components/hue/v2/light.py @@ -6,11 +6,13 @@ from typing import Any from aiohue import HueBridgeV2 from aiohue.v2.controllers.events import EventType from aiohue.v2.controllers.lights import LightsController +from aiohue.v2.models.feature import EffectStatus, TimedEffectStatus from aiohue.v2.models.light import Light from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, + ATTR_EFFECT, ATTR_FLASH, ATTR_TRANSITION, ATTR_XY_COLOR, @@ -19,6 +21,7 @@ from homeassistant.components.light import ( COLOR_MODE_ONOFF, COLOR_MODE_XY, FLASH_SHORT, + SUPPORT_EFFECT, SUPPORT_FLASH, SUPPORT_TRANSITION, LightEntity, @@ -36,6 +39,8 @@ from .helpers import ( normalize_hue_transition, ) +EFFECT_NONE = "None" + async def async_setup_entry( hass: HomeAssistant, @@ -86,9 +91,21 @@ class HueLight(HueBaseEntity, LightEntity): self._supported_color_modes.add(COLOR_MODE_BRIGHTNESS) # support transition if brightness control self._attr_supported_features |= SUPPORT_TRANSITION - self._last_xy: tuple[float, float] | None = self.xy_color - self._last_color_temp: int = self.color_temp - self._set_color_mode() + # get list of supported effects (combine effects and timed_effects) + self._attr_effect_list = [] + if effects := resource.effects: + self._attr_effect_list = [ + x.value for x in effects.status_values if x != EffectStatus.NO_EFFECT + ] + if timed_effects := resource.timed_effects: + self._attr_effect_list += [ + x.value + for x in timed_effects.status_values + if x != TimedEffectStatus.NO_EFFECT + ] + if len(self._attr_effect_list) > 0: + self._attr_effect_list.insert(0, EFFECT_NONE) + self._attr_supported_features |= SUPPORT_EFFECT @property def brightness(self) -> int | None: @@ -103,6 +120,20 @@ class HueLight(HueBaseEntity, LightEntity): """Return true if device is on (brightness above 0).""" return self.resource.on.on + @property + def color_mode(self) -> str | None: + """Return the color mode of the light.""" + if color_temp := self.resource.color_temperature: + # Hue lights return `mired_valid` to indicate CT is active + if color_temp.mirek_valid and color_temp.mirek is not None: + return COLOR_MODE_COLOR_TEMP + if self.resource.supports_color: + return COLOR_MODE_XY + if self.resource.supports_dimming: + return COLOR_MODE_BRIGHTNESS + # fallback to on_off + return COLOR_MODE_ONOFF + @property def xy_color(self) -> tuple[float, float] | None: """Return the xy color.""" @@ -144,10 +175,16 @@ class HueLight(HueBaseEntity, LightEntity): "dynamics": self.resource.dynamics.status.value, } - @callback - def on_update(self) -> None: - """Call on update event.""" - self._set_color_mode() + @property + def effect(self) -> str | None: + """Return the current effect.""" + if effects := self.resource.effects: + if effects.status != EffectStatus.NO_EFFECT: + return effects.status.value + if timed_effects := self.resource.timed_effects: + if timed_effects.status != TimedEffectStatus.NO_EFFECT: + return timed_effects.status.value + return EFFECT_NONE async def async_turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" @@ -156,6 +193,17 @@ class HueLight(HueBaseEntity, LightEntity): color_temp = normalize_hue_colortemp(kwargs.get(ATTR_COLOR_TEMP)) brightness = normalize_hue_brightness(kwargs.get(ATTR_BRIGHTNESS)) flash = kwargs.get(ATTR_FLASH) + effect = effect_str = kwargs.get(ATTR_EFFECT) + if effect_str in (EFFECT_NONE, EFFECT_NONE.lower()): + effect = EffectStatus.NO_EFFECT + elif effect_str is not None: + # work out if we got a regular effect or timed effect + effect = EffectStatus(effect_str) + if effect == EffectStatus.UNKNOWN: + effect = TimedEffectStatus(effect_str) + if transition is None: + # a transition is required for timed effect, default to 10 minutes + transition = 600000 if flash is not None: await self.async_set_flash(flash) @@ -173,6 +221,7 @@ class HueLight(HueBaseEntity, LightEntity): color_xy=xy_color, color_temp=color_temp, transition_time=transition, + effect=effect, ) async def async_turn_off(self, **kwargs: Any) -> None: @@ -201,43 +250,3 @@ class HueLight(HueBaseEntity, LightEntity): id=self.resource.id, short=flash == FLASH_SHORT, ) - - @callback - def _set_color_mode(self) -> None: - """Set current colormode of light.""" - last_xy = self._last_xy - last_color_temp = self._last_color_temp - self._last_xy = self.xy_color - self._last_color_temp = self.color_temp - - # Certified Hue lights return `mired_valid` to indicate CT is active - if color_temp := self.resource.color_temperature: - if color_temp.mirek_valid and color_temp.mirek is not None: - self._attr_color_mode = COLOR_MODE_COLOR_TEMP - return - - # Non-certified lights do not report their current color mode correctly - # so we keep track of the color values to determine which is active - if last_color_temp != self.color_temp: - self._attr_color_mode = COLOR_MODE_COLOR_TEMP - return - if last_xy != self.xy_color: - self._attr_color_mode = COLOR_MODE_XY - return - - # if we didn't detect any changes, abort and use previous values - if self._attr_color_mode is not None: - return - - # color mode not yet determined, work it out here - # Note that for lights that do not correctly report `mirek_valid` - # we might have an invalid startup state which will be auto corrected - if self.resource.supports_color: - self._attr_color_mode = COLOR_MODE_XY - elif self.resource.supports_color_temperature: - self._attr_color_mode = COLOR_MODE_COLOR_TEMP - elif self.resource.supports_dimming: - self._attr_color_mode = COLOR_MODE_BRIGHTNESS - else: - # fallback to on_off - self._attr_color_mode = COLOR_MODE_ONOFF diff --git a/homeassistant/components/huisbaasje/manifest.json b/homeassistant/components/huisbaasje/manifest.json index 8640f126ae4..bf3155ed9b8 100644 --- a/homeassistant/components/huisbaasje/manifest.json +++ b/homeassistant/components/huisbaasje/manifest.json @@ -3,12 +3,8 @@ "name": "Huisbaasje", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/huisbaasje", - "requirements": [ - "huisbaasje-client==0.1.0" - ], - "codeowners": [ - "@dennisschroer" - ], + "requirements": ["huisbaasje-client==0.1.0"], + "codeowners": ["@dennisschroer"], "iot_class": "cloud_polling", "loggers": ["huisbaasje"] -} \ No newline at end of file +} diff --git a/homeassistant/components/huisbaasje/translations/fr.json b/homeassistant/components/huisbaasje/translations/fr.json index aa84ec33d8c..744b9c6a862 100644 --- a/homeassistant/components/huisbaasje/translations/fr.json +++ b/homeassistant/components/huisbaasje/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/humidifier/recorder.py b/homeassistant/components/humidifier/recorder.py new file mode 100644 index 00000000000..53df96605d6 --- /dev/null +++ b/homeassistant/components/humidifier/recorder.py @@ -0,0 +1,16 @@ +"""Integration platform for recorder.""" +from __future__ import annotations + +from homeassistant.core import HomeAssistant, callback + +from . import ATTR_AVAILABLE_MODES, ATTR_MAX_HUMIDITY, ATTR_MIN_HUMIDITY + + +@callback +def exclude_attributes(hass: HomeAssistant) -> set[str]: + """Exclude static attributes from being recorded in the database.""" + return { + ATTR_MIN_HUMIDITY, + ATTR_MAX_HUMIDITY, + ATTR_AVAILABLE_MODES, + } diff --git a/homeassistant/components/humidifier/services.yaml b/homeassistant/components/humidifier/services.yaml index c05dad680a3..9c1b748c9ac 100644 --- a/homeassistant/components/humidifier/services.yaml +++ b/homeassistant/components/humidifier/services.yaml @@ -10,7 +10,7 @@ set_mode: mode: description: New mode required: true - example: 'away' + example: "away" selector: text: diff --git a/homeassistant/components/humidifier/translations/fr.json b/homeassistant/components/humidifier/translations/fr.json index 3b1b60ebae3..84b24b96904 100644 --- a/homeassistant/components/humidifier/translations/fr.json +++ b/homeassistant/components/humidifier/translations/fr.json @@ -22,8 +22,8 @@ }, "state": { "_": { - "off": "Inactif", - "on": "Actif" + "off": "D\u00e9sactiv\u00e9", + "on": "Activ\u00e9" } }, "title": "Humidificateur" diff --git a/homeassistant/components/hunterdouglas_powerview/manifest.json b/homeassistant/components/hunterdouglas_powerview/manifest.json index 4e1ece9a3b0..c34f53f47b4 100644 --- a/homeassistant/components/hunterdouglas_powerview/manifest.json +++ b/homeassistant/components/hunterdouglas_powerview/manifest.json @@ -9,7 +9,7 @@ "models": ["PowerView"] }, "dhcp": [ - {"registered_devices": true}, + { "registered_devices": true }, { "hostname": "hunter*", "macaddress": "002674*" diff --git a/homeassistant/components/hunterdouglas_powerview/scene.py b/homeassistant/components/hunterdouglas_powerview/scene.py index 224d5bf7b7c..3476db4949c 100644 --- a/homeassistant/components/hunterdouglas_powerview/scene.py +++ b/homeassistant/components/hunterdouglas_powerview/scene.py @@ -4,21 +4,16 @@ from __future__ import annotations from typing import Any from aiopvapi.resources.scene import Scene as PvScene -import voluptuous as vol from homeassistant.components.scene import Scene -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import CONF_HOST, CONF_PLATFORM +from homeassistant.config_entries import ConfigEntry 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 ( COORDINATOR, DEVICE_INFO, DOMAIN, - HUB_ADDRESS, PV_API, PV_ROOM_DATA, PV_SCENE_DATA, @@ -27,27 +22,6 @@ from .const import ( ) from .entity import HDEntity -PLATFORM_SCHEMA = vol.Schema( - {vol.Required(CONF_PLATFORM): DOMAIN, vol.Required(HUB_ADDRESS): cv.string} -) - - -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Import platform from yaml.""" - - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={CONF_HOST: config[HUB_ADDRESS]}, - ) - ) - async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback diff --git a/homeassistant/components/hvv_departures/translations/fr.json b/homeassistant/components/hvv_departures/translations/fr.json index 0c7fd03f148..10a471223f4 100644 --- a/homeassistant/components/hvv_departures/translations/fr.json +++ b/homeassistant/components/hvv_departures/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "no_results": "Aucun r\u00e9sultat. Essayez avec une autre station / adresse" }, "step": { diff --git a/homeassistant/components/hyperion/camera.py b/homeassistant/components/hyperion/camera.py index 4b6492559ea..a9d78a256d6 100644 --- a/homeassistant/components/hyperion/camera.py +++ b/homeassistant/components/hyperion/camera.py @@ -254,6 +254,7 @@ class HyperionCamera(Camera): manufacturer=HYPERION_MANUFACTURER_NAME, model=HYPERION_MODEL_NAME, name=self._instance_name, + configuration_url=self._client.remote_url, ) diff --git a/homeassistant/components/hyperion/light.py b/homeassistant/components/hyperion/light.py index 753e41018d9..8b809bdec8a 100644 --- a/homeassistant/components/hyperion/light.py +++ b/homeassistant/components/hyperion/light.py @@ -244,6 +244,7 @@ class HyperionBaseLight(LightEntity): manufacturer=HYPERION_MANUFACTURER_NAME, model=HYPERION_MODEL_NAME, name=self._instance_name, + configuration_url=self._client.remote_url, ) def _get_option(self, key: str) -> Any: diff --git a/homeassistant/components/hyperion/manifest.json b/homeassistant/components/hyperion/manifest.json index 8a886053361..223c001a53c 100644 --- a/homeassistant/components/hyperion/manifest.json +++ b/homeassistant/components/hyperion/manifest.json @@ -5,7 +5,7 @@ "domain": "hyperion", "name": "Hyperion", "quality_scale": "platinum", - "requirements": ["hyperion-py==0.7.4"], + "requirements": ["hyperion-py==0.7.5"], "ssdp": [ { "manufacturer": "Hyperion Open Source Ambient Lighting", diff --git a/homeassistant/components/hyperion/switch.py b/homeassistant/components/hyperion/switch.py index ac853a32909..bf4958d845c 100644 --- a/homeassistant/components/hyperion/switch.py +++ b/homeassistant/components/hyperion/switch.py @@ -191,6 +191,7 @@ class HyperionComponentSwitch(SwitchEntity): manufacturer=HYPERION_MANUFACTURER_NAME, model=HYPERION_MODEL_NAME, name=self._instance_name, + configuration_url=self._client.remote_url, ) async def _async_send_set_component(self, value: bool) -> None: diff --git a/homeassistant/components/hyperion/translations/fr.json b/homeassistant/components/hyperion/translations/fr.json index 7bb5c02543d..13b04413f0f 100644 --- a/homeassistant/components/hyperion/translations/fr.json +++ b/homeassistant/components/hyperion/translations/fr.json @@ -20,10 +20,10 @@ "create_token": "Cr\u00e9er automatiquement un nouveau jeton", "token": "Ou fournir un jeton pr\u00e9existant" }, - "description": "Configurer l'autorisation sur votre serveur Hyperion Ambilight" + "description": "Configurez l'autorisation vers votre serveur Hyperion Ambilight" }, "confirm": { - "description": "Voulez-vous ajouter cet Hyperion Ambilight \u00e0 Home Assistant? \n\n ** H\u00f4te: ** {host}\n ** Port: ** {port}\n ** ID **: {id}", + "description": "Voulez-vous ajouter cet Hyperion Ambilight \u00e0 Home Assistant\u00a0?\n\n**H\u00f4te\u00a0:** {host}\n**Port\u00a0:** {port}\n**ID\u00a0:** {id}", "title": "Confirmer l'ajout du service Hyperion Ambilight" }, "create_token": { diff --git a/homeassistant/components/hyperion/translations/it.json b/homeassistant/components/hyperion/translations/it.json index 0d699085057..0adf9547d62 100644 --- a/homeassistant/components/hyperion/translations/it.json +++ b/homeassistant/components/hyperion/translations/it.json @@ -23,7 +23,7 @@ "description": "Configura l'autorizzazione per il tuo server Hyperion Ambilight" }, "confirm": { - "description": "Vuoi aggiungere questo Hyperion Ambilight a Home Assistant? \n\n ** Host:** {host}\n ** Porta:** {port}\n ** ID:** {id}", + "description": "Vuoi aggiungere questo Hyperion Ambilight a Home Assistant? \n\n **Host:** {host}\n **Porta:** {port}\n **ID:** {id}", "title": "Conferma l'aggiunta del servizio Hyperion Ambilight" }, "create_token": { diff --git a/homeassistant/components/iaqualink/translations/fr.json b/homeassistant/components/iaqualink/translations/fr.json index ec3e7352a63..92327990f71 100644 --- a/homeassistant/components/iaqualink/translations/fr.json +++ b/homeassistant/components/iaqualink/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide" + "invalid_auth": "Authentification non valide" }, "step": { "user": { diff --git a/homeassistant/components/icloud/__init__.py b/homeassistant/components/icloud/__init__.py index 8b6c8355e40..17cc15b195a 100644 --- a/homeassistant/components/icloud/__init__.py +++ b/homeassistant/components/icloud/__init__.py @@ -1,13 +1,10 @@ """The iCloud component.""" -import logging - import voluptuous as vol -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant, ServiceCall import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.typing import ConfigType from homeassistant.util import slugify from .account import IcloudAccount @@ -15,9 +12,6 @@ from .const import ( CONF_GPS_ACCURACY_THRESHOLD, CONF_MAX_INTERVAL, CONF_WITH_FAMILY, - DEFAULT_GPS_ACCURACY_THRESHOLD, - DEFAULT_MAX_INTERVAL, - DEFAULT_WITH_FAMILY, DOMAIN, PLATFORMS, STORAGE_KEY, @@ -69,47 +63,7 @@ SERVICE_SCHEMA_LOST_DEVICE = vol.Schema( } ) -ACCOUNT_SCHEMA = vol.Schema( - { - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_WITH_FAMILY, default=DEFAULT_WITH_FAMILY): cv.boolean, - vol.Optional(CONF_MAX_INTERVAL, default=DEFAULT_MAX_INTERVAL): cv.positive_int, - vol.Optional( - CONF_GPS_ACCURACY_THRESHOLD, default=DEFAULT_GPS_ACCURACY_THRESHOLD - ): cv.positive_int, - } -) - -CONFIG_SCHEMA = vol.Schema( - {DOMAIN: vol.Schema(vol.All(cv.ensure_list, [ACCOUNT_SCHEMA]))}, - extra=vol.ALLOW_EXTRA, -) - -_LOGGER = logging.getLogger(__name__) - - -async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up iCloud from legacy config file.""" - if (conf := config.get(DOMAIN)) is None: - return True - - # Note: need to remember to cleanup device_tracker (remove async_setup_scanner) - _LOGGER.warning( - "Configuration of the iCloud integration in YAML is deprecated and " - "will be removed in Home Assistant 2022.4; Your existing configuration " - "has been imported into the UI automatically and can be safely removed " - "from your configuration.yaml file" - ) - - for account_conf in conf: - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=account_conf - ) - ) - - return True +CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/icloud/config_flow.py b/homeassistant/components/icloud/config_flow.py index 9bfa12a76a2..f3630abfdaa 100644 --- a/homeassistant/components/icloud/config_flow.py +++ b/homeassistant/components/icloud/config_flow.py @@ -172,10 +172,6 @@ class IcloudFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return await self._validate_and_create_entry(user_input, "user") - async def async_step_import(self, user_input): - """Import a config entry.""" - return await self.async_step_user(user_input) - async def async_step_reauth(self, user_input=None): """Update password for a config entry that can't authenticate.""" # Store existing entry data so it can be used later and set unique ID @@ -288,8 +284,8 @@ class IcloudFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self._with_family, ) return await self.async_step_verification_code(None, errors) - except PyiCloudFailedLoginException as error: - _LOGGER.error("Error logging into iCloud service: %s", error) + except PyiCloudFailedLoginException as error_login: + _LOGGER.error("Error logging into iCloud service: %s", error_login) self.api = None errors = {CONF_PASSWORD: "invalid_auth"} return self._show_setup_form(user_input, errors, "user") diff --git a/homeassistant/components/icloud/translations/fr.json b/homeassistant/components/icloud/translations/fr.json index f9c0ceb3db9..8e5ec918cb0 100644 --- a/homeassistant/components/icloud/translations/fr.json +++ b/homeassistant/components/icloud/translations/fr.json @@ -6,7 +6,7 @@ "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" }, "error": { - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "send_verification_code": "\u00c9chec de l'envoi du code de v\u00e9rification", "validate_verification_code": "Impossible de v\u00e9rifier votre code de v\u00e9rification, choisissez un appareil de confiance et recommencez la v\u00e9rification" }, @@ -28,7 +28,7 @@ "user": { "data": { "password": "Mot de passe", - "username": "Email", + "username": "Courriel", "with_family": "Avec la famille" }, "description": "Entrez vos identifiants", diff --git a/homeassistant/components/ifttt/translations/fr.json b/homeassistant/components/ifttt/translations/fr.json index 4628b7bea8b..371bbd65ee0 100644 --- a/homeassistant/components/ifttt/translations/fr.json +++ b/homeassistant/components/ifttt/translations/fr.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "cloud_not_connected": "Pas connect\u00e9 \u00e0 Home Assistant Cloud", + "cloud_not_connected": "Non connect\u00e9 \u00e0 Home Assistant Cloud.", "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible.", "webhook_not_internet_accessible": "Votre installation de Home Assistant doit \u00eatre accessible depuis internet pour recevoir des messages webhook." }, diff --git a/homeassistant/components/image_processing/manifest.json b/homeassistant/components/image_processing/manifest.json index 0541f4898c9..0315a69b82a 100644 --- a/homeassistant/components/image_processing/manifest.json +++ b/homeassistant/components/image_processing/manifest.json @@ -3,6 +3,6 @@ "name": "Image Processing", "documentation": "https://www.home-assistant.io/integrations/image_processing", "dependencies": ["camera"], - "codeowners": [], + "codeowners": ["@home-assistant/core"], "quality_scale": "internal" } diff --git a/homeassistant/components/input_boolean/manifest.json b/homeassistant/components/input_boolean/manifest.json index 7a27d475e6e..589cf536253 100644 --- a/homeassistant/components/input_boolean/manifest.json +++ b/homeassistant/components/input_boolean/manifest.json @@ -1,5 +1,6 @@ { "domain": "input_boolean", + "integration_type": "helper", "name": "Input Boolean", "documentation": "https://www.home-assistant.io/integrations/input_boolean", "codeowners": ["@home-assistant/core"], diff --git a/homeassistant/components/input_boolean/translations/el.json b/homeassistant/components/input_boolean/translations/el.json index 41fcd83a349..28148a12a77 100644 --- a/homeassistant/components/input_boolean/translations/el.json +++ b/homeassistant/components/input_boolean/translations/el.json @@ -1,8 +1,8 @@ { "state": { "_": { - "off": "\u039a\u03bb\u03b5\u03b9\u03c3\u03c4\u03cc", - "on": "\u0391\u03bd\u03bf\u03b9\u03c7\u03c4\u03cc" + "off": "\u0391\u03bd\u03b5\u03bd\u03b5\u03c1\u03b3\u03cc", + "on": "\u0395\u03bd\u03b5\u03c1\u03b3\u03cc" } }, "title": "\u0395\u03b9\u03c3\u03b1\u03b3\u03c9\u03b3\u03ae \u03bb\u03bf\u03b3\u03b9\u03ba\u03ae\u03c2 \u03c0\u03c1\u03ac\u03be\u03b7\u03c2" diff --git a/homeassistant/components/input_boolean/translations/fr.json b/homeassistant/components/input_boolean/translations/fr.json index 1d83af839c1..77bd8bc9e7a 100644 --- a/homeassistant/components/input_boolean/translations/fr.json +++ b/homeassistant/components/input_boolean/translations/fr.json @@ -1,8 +1,8 @@ { "state": { "_": { - "off": "Inactif", - "on": "Actif" + "off": "D\u00e9sactiv\u00e9", + "on": "Activ\u00e9" } }, "title": "Entr\u00e9e logique" diff --git a/homeassistant/components/input_button/manifest.json b/homeassistant/components/input_button/manifest.json index 76133500d36..7e31df775c3 100644 --- a/homeassistant/components/input_button/manifest.json +++ b/homeassistant/components/input_button/manifest.json @@ -1,5 +1,6 @@ { "domain": "input_button", + "integration_type": "helper", "name": "Input Button", "documentation": "https://www.home-assistant.io/integrations/input_button", "codeowners": ["@home-assistant/core"], diff --git a/homeassistant/components/input_datetime/manifest.json b/homeassistant/components/input_datetime/manifest.json index a394b77b72e..4d1e680c12a 100644 --- a/homeassistant/components/input_datetime/manifest.json +++ b/homeassistant/components/input_datetime/manifest.json @@ -1,5 +1,6 @@ { "domain": "input_datetime", + "integration_type": "helper", "name": "Input Datetime", "documentation": "https://www.home-assistant.io/integrations/input_datetime", "codeowners": ["@home-assistant/core"], diff --git a/homeassistant/components/input_number/manifest.json b/homeassistant/components/input_number/manifest.json index 93081a7ed49..46cae513fd2 100644 --- a/homeassistant/components/input_number/manifest.json +++ b/homeassistant/components/input_number/manifest.json @@ -1,5 +1,6 @@ { "domain": "input_number", + "integration_type": "helper", "name": "Input Number", "documentation": "https://www.home-assistant.io/integrations/input_number", "codeowners": ["@home-assistant/core"], diff --git a/homeassistant/components/input_select/manifest.json b/homeassistant/components/input_select/manifest.json index 614ee18390d..1c3dc880d20 100644 --- a/homeassistant/components/input_select/manifest.json +++ b/homeassistant/components/input_select/manifest.json @@ -1,5 +1,6 @@ { "domain": "input_select", + "integration_type": "helper", "name": "Input Select", "documentation": "https://www.home-assistant.io/integrations/input_select", "codeowners": ["@home-assistant/core"], diff --git a/homeassistant/components/input_text/manifest.json b/homeassistant/components/input_text/manifest.json index 3ca9a0b961a..9cc48f745cf 100644 --- a/homeassistant/components/input_text/manifest.json +++ b/homeassistant/components/input_text/manifest.json @@ -1,5 +1,6 @@ { "domain": "input_text", + "integration_type": "helper", "name": "Input Text", "documentation": "https://www.home-assistant.io/integrations/input_text", "codeowners": ["@home-assistant/core"], diff --git a/homeassistant/components/insteon/fan.py b/homeassistant/components/insteon/fan.py index 9df11fdcc79..bea17ccaa7e 100644 --- a/homeassistant/components/insteon/fan.py +++ b/homeassistant/components/insteon/fan.py @@ -65,7 +65,6 @@ class InsteonFanEntity(InsteonEntity, FanEntity): async def async_turn_on( self, - speed: str = None, percentage: int = None, preset_mode: str = None, **kwargs, diff --git a/homeassistant/components/insteon/manifest.json b/homeassistant/components/insteon/manifest.json index b069f3b18a8..e9f5e60f9f8 100644 --- a/homeassistant/components/insteon/manifest.json +++ b/homeassistant/components/insteon/manifest.json @@ -2,16 +2,9 @@ "domain": "insteon", "name": "Insteon", "documentation": "https://www.home-assistant.io/integrations/insteon", - "requirements": [ - "pyinsteon==1.0.13" - ], - "codeowners": [ - "@teharris1" - ], + "requirements": ["pyinsteon==1.0.13"], + "codeowners": ["@teharris1"], "config_flow": true, "iot_class": "local_push", - "loggers": [ - "pyinsteon", - "pypubsub" - ] -} \ No newline at end of file + "loggers": ["pyinsteon", "pypubsub"] +} diff --git a/homeassistant/components/insteon/services.yaml b/homeassistant/components/insteon/services.yaml index 21bc0f535f6..c2cd5ee9d25 100644 --- a/homeassistant/components/insteon/services.yaml +++ b/homeassistant/components/insteon/services.yaml @@ -17,8 +17,8 @@ add_all_link: selector: select: options: - - 'controller' - - 'responder' + - "controller" + - "responder" delete_all_link: name: Delete all link description: Tells the Insteon Modem (IM) to remove an All-Link record from the All-Link Database of the IM and a device. Once the IM is set to delete the link, press the link button on the corresponding device to complete the process. @@ -73,22 +73,22 @@ x10_all_units_off: selector: select: options: - - 'a' - - 'b' - - 'c' - - 'd' - - 'e' - - 'f' - - 'g' - - 'h' - - 'i' - - 'j' - - 'k' - - 'l' - - 'm' - - 'n' - - 'o' - - 'p' + - "a" + - "b" + - "c" + - "d" + - "e" + - "f" + - "g" + - "h" + - "i" + - "j" + - "k" + - "l" + - "m" + - "n" + - "o" + - "p" x10_all_lights_on: name: X10 all lights on description: Send X10 All Lights On command @@ -100,22 +100,22 @@ x10_all_lights_on: selector: select: options: - - 'a' - - 'b' - - 'c' - - 'd' - - 'e' - - 'f' - - 'g' - - 'h' - - 'i' - - 'j' - - 'k' - - 'l' - - 'm' - - 'n' - - 'o' - - 'p' + - "a" + - "b" + - "c" + - "d" + - "e" + - "f" + - "g" + - "h" + - "i" + - "j" + - "k" + - "l" + - "m" + - "n" + - "o" + - "p" x10_all_lights_off: name: X10 all lights off description: Send X10 All Lights Off command @@ -127,22 +127,22 @@ x10_all_lights_off: selector: select: options: - - 'a' - - 'b' - - 'c' - - 'd' - - 'e' - - 'f' - - 'g' - - 'h' - - 'i' - - 'j' - - 'k' - - 'l' - - 'm' - - 'n' - - 'o' - - 'p' + - "a" + - "b" + - "c" + - "d" + - "e" + - "f" + - "g" + - "h" + - "i" + - "j" + - "k" + - "l" + - "m" + - "n" + - "o" + - "p" scene_on: name: Scene on description: Trigger an INSTEON scene to turn ON. diff --git a/homeassistant/components/insteon/strings.json b/homeassistant/components/insteon/strings.json index b0130910c5f..ca88b43956f 100644 --- a/homeassistant/components/insteon/strings.json +++ b/homeassistant/components/insteon/strings.json @@ -2,7 +2,6 @@ "config": { "step": { "user": { - "title": "Insteon", "description": "Select the Insteon modem type.", "data": { "modem_type": "Modem type." @@ -46,8 +45,6 @@ "options": { "step": { "init": { - "title": "Insteon", - "description": "Select an option to configure.", "data": { "change_hub_config": "Change the Hub configuration.", "add_override": "Add a device override.", @@ -57,7 +54,6 @@ } }, "change_hub_config": { - "title": "Insteon", "description": "Change the Insteon Hub connection information. You must restart Home Assistant after making this change. This does not change the configuration of the Hub itself. To change the configuration in the Hub use the Hub app.", "data": { "host": "[%key:common::config_flow::data::ip%]", @@ -67,7 +63,6 @@ } }, "add_override": { - "title": "Insteon", "description": "Add a device override.", "data": { "address": "Device address (i.e. 1a2b3c)", @@ -76,7 +71,6 @@ } }, "add_x10": { - "title": "Insteon", "description": "Change the Insteon Hub password.", "data": { "housecode": "Housecode (a - p)", @@ -85,15 +79,13 @@ "steps": "Dimmer steps (for light devices only, default 22)" } }, - "remove_override": { - "title": "Insteon", + "remove_override": { "description": "Remove a device override", "data": { "address": "Select a device address to remove" } }, - "remove_x10": { - "title": "Insteon", + "remove_x10": { "description": "Remove an X10 device", "data": { "address": "Select a device address to remove" diff --git a/homeassistant/components/insteon/translations/fr.json b/homeassistant/components/insteon/translations/fr.json index a0bd2f4048e..2feb3c5d9c9 100644 --- a/homeassistant/components/insteon/translations/fr.json +++ b/homeassistant/components/insteon/translations/fr.json @@ -54,9 +54,9 @@ "data": { "address": "Adresse de l'appareil (par exemple 1a2b3c)", "cat": "Cat\u00e9gorie d'appareil (c.-\u00e0-d. 0x10)", - "subcat": "Sous-cat\u00e9gorie de p\u00e9riph\u00e9rique (par exemple 0x0a)" + "subcat": "Sous-cat\u00e9gorie d'appareil (par exemple 0x0a)" }, - "description": "Ajoutez un remplacement de p\u00e9riph\u00e9rique.", + "description": "Ajouter un remplacement d'appareil.", "title": "Insteon" }, "add_x10": { @@ -81,27 +81,27 @@ }, "init": { "data": { - "add_override": "Ajoutez un remplacement de p\u00e9riph\u00e9rique.", + "add_override": "Ajouter un remplacement d'appareil.", "add_x10": "Ajouter un appareil X10.", "change_hub_config": "Modifier la configuration du Hub.", "remove_override": "Supprimer un remplacement d'appareil", - "remove_x10": "Retirez un p\u00e9riph\u00e9rique X10." + "remove_x10": "Retirer un appareil X10." }, "description": "S\u00e9lectionnez une option \u00e0 configurer.", "title": "Insteon" }, "remove_override": { "data": { - "address": "S\u00e9lectionner une adresse de p\u00e9riph\u00e9rique \u00e0 retirer" + "address": "S\u00e9lectionnez l'adresse d'un appareil \u00e0 retirer" }, "description": "Supprimer un remplacement d'appareil", "title": "Insteon" }, "remove_x10": { "data": { - "address": "S\u00e9lectionner une adresse de p\u00e9riph\u00e9rique \u00e0 retirer" + "address": "S\u00e9lectionnez l'adresse d'un appareil \u00e0 retirer" }, - "description": "Retirer un p\u00e9riph\u00e9rique X10", + "description": "Retirer un appareil X10", "title": "Insteon" } } diff --git a/homeassistant/components/insteon/translations/zh-Hant.json b/homeassistant/components/insteon/translations/zh-Hant.json index cf090f974f7..2176ea67b94 100644 --- a/homeassistant/components/insteon/translations/zh-Hant.json +++ b/homeassistant/components/insteon/translations/zh-Hant.json @@ -36,9 +36,9 @@ }, "user": { "data": { - "modem_type": "\u6578\u64da\u6a5f\u985e\u578b\u3002" + "modem_type": "\u6578\u64da\u6a5f\u985e\u5225\u3002" }, - "description": "\u9078\u64c7 Insteon \u6578\u64da\u6a5f\u985e\u578b\u3002", + "description": "\u9078\u64c7 Insteon \u6578\u64da\u6a5f\u985e\u5225\u3002", "title": "Insteon" } } diff --git a/homeassistant/components/integration/__init__.py b/homeassistant/components/integration/__init__.py index 4eea25fefe1..84bc28e3d2f 100644 --- a/homeassistant/components/integration/__init__.py +++ b/homeassistant/components/integration/__init__.py @@ -1 +1,23 @@ -"""The integration component.""" +"""The Integration integration.""" +from __future__ import annotations + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Integration from a config entry.""" + hass.config_entries.async_setup_platforms(entry, (Platform.SENSOR,)) + entry.async_on_unload(entry.add_update_listener(config_entry_update_listener)) + return True + + +async def config_entry_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Update listener, called when the config entry options are changed.""" + await hass.config_entries.async_reload(entry.entry_id) + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + return await hass.config_entries.async_unload_platforms(entry, (Platform.SENSOR,)) diff --git a/homeassistant/components/integration/config_flow.py b/homeassistant/components/integration/config_flow.py new file mode 100644 index 00000000000..3f7d39554e8 --- /dev/null +++ b/homeassistant/components/integration/config_flow.py @@ -0,0 +1,108 @@ +"""Config flow for Integration - Riemann sum integral integration.""" +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, cast + +import voluptuous as vol + +from homeassistant.const import ( + CONF_METHOD, + CONF_NAME, + CONF_UNIT_OF_MEASUREMENT, + TIME_DAYS, + TIME_HOURS, + TIME_MINUTES, + TIME_SECONDS, +) +from homeassistant.helpers import selector +from homeassistant.helpers.schema_config_entry_flow import ( + SchemaConfigFlowHandler, + SchemaFlowFormStep, + SchemaFlowMenuStep, +) + +from .const import ( + CONF_ROUND_DIGITS, + CONF_SOURCE_SENSOR, + CONF_UNIT_PREFIX, + CONF_UNIT_TIME, + DOMAIN, + METHOD_LEFT, + METHOD_RIGHT, + METHOD_TRAPEZOIDAL, +) + +UNIT_PREFIXES = [ + {"value": "none", "label": "none"}, + {"value": "k", "label": "k (kilo)"}, + {"value": "M", "label": "M (mega)"}, + {"value": "G", "label": "G (giga)"}, + {"value": "T", "label": "T (tera)"}, +] +TIME_UNITS = [ + {"value": TIME_SECONDS, "label": "s (seconds)"}, + {"value": TIME_MINUTES, "label": "min (minutes)"}, + {"value": TIME_HOURS, "label": "h (hours)"}, + {"value": TIME_DAYS, "label": "d (days)"}, +] +INTEGRATION_METHODS = [ + {"value": METHOD_TRAPEZOIDAL, "label": "Trapezoidal rule"}, + {"value": METHOD_LEFT, "label": "Left Riemann sum"}, + {"value": METHOD_RIGHT, "label": "Right Riemann sum"}, +] + +OPTIONS_SCHEMA = vol.Schema( + { + vol.Required(CONF_ROUND_DIGITS, default=2): selector.selector( + {"number": {"min": 0, "max": 6, "mode": "box"}} + ), + } +) + +CONFIG_SCHEMA = vol.Schema( + { + vol.Required(CONF_NAME): selector.selector({"text": {}}), + vol.Required(CONF_SOURCE_SENSOR): selector.selector( + {"entity": {"domain": "sensor"}}, + ), + vol.Required(CONF_METHOD, default=METHOD_TRAPEZOIDAL): selector.selector( + {"select": {"options": INTEGRATION_METHODS}} + ), + vol.Required(CONF_ROUND_DIGITS, default=2): selector.selector( + { + "number": { + "min": 0, + "max": 6, + "mode": "box", + CONF_UNIT_OF_MEASUREMENT: "decimals", + } + } + ), + vol.Required(CONF_UNIT_PREFIX, default="none"): selector.selector( + {"select": {"options": UNIT_PREFIXES}} + ), + vol.Required(CONF_UNIT_TIME, default=TIME_HOURS): selector.selector( + {"select": {"options": TIME_UNITS, "mode": "dropdown"}} + ), + } +) + +CONFIG_FLOW: dict[str, SchemaFlowFormStep | SchemaFlowMenuStep] = { + "user": SchemaFlowFormStep(CONFIG_SCHEMA) +} + +OPTIONS_FLOW: dict[str, SchemaFlowFormStep | SchemaFlowMenuStep] = { + "init": SchemaFlowFormStep(OPTIONS_SCHEMA) +} + + +class ConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN): + """Handle a config or options flow for Integration.""" + + config_flow = CONFIG_FLOW + options_flow = OPTIONS_FLOW + + def async_config_entry_title(self, options: Mapping[str, Any]) -> str: + """Return config entry title.""" + return cast(str, options["name"]) diff --git a/homeassistant/components/integration/const.py b/homeassistant/components/integration/const.py new file mode 100644 index 00000000000..b05e4e8f80b --- /dev/null +++ b/homeassistant/components/integration/const.py @@ -0,0 +1,14 @@ +"""Constants for the Integration - Riemann sum integral integration.""" + +DOMAIN = "integration" + +CONF_ROUND_DIGITS = "round" +CONF_SOURCE_SENSOR = "source" +CONF_UNIT_OF_MEASUREMENT = "unit" +CONF_UNIT_PREFIX = "unit_prefix" +CONF_UNIT_TIME = "unit_time" + +METHOD_TRAPEZOIDAL = "trapezoidal" +METHOD_LEFT = "left" +METHOD_RIGHT = "right" +INTEGRATION_METHODS = [METHOD_TRAPEZOIDAL, METHOD_LEFT, METHOD_RIGHT] diff --git a/homeassistant/components/integration/manifest.json b/homeassistant/components/integration/manifest.json index afec4dbe9ec..e20c9703dfe 100644 --- a/homeassistant/components/integration/manifest.json +++ b/homeassistant/components/integration/manifest.json @@ -1,8 +1,10 @@ { "domain": "integration", + "integration_type": "helper", "name": "Integration - Riemann sum integral", "documentation": "https://www.home-assistant.io/integrations/integration", "codeowners": ["@dgomes"], "quality_scale": "internal", - "iot_class": "local_push" + "iot_class": "local_push", + "config_flow": true } diff --git a/homeassistant/components/integration/sensor.py b/homeassistant/components/integration/sensor.py index 7a6248254d8..ba76556fc51 100644 --- a/homeassistant/components/integration/sensor.py +++ b/homeassistant/components/integration/sensor.py @@ -12,11 +12,13 @@ from homeassistant.components.sensor import ( SensorEntity, SensorStateClass, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_UNIT_OF_MEASUREMENT, CONF_METHOD, CONF_NAME, + CONF_UNIQUE_ID, STATE_UNAVAILABLE, STATE_UNKNOWN, TIME_DAYS, @@ -25,29 +27,30 @@ from homeassistant.const import ( TIME_SECONDS, ) from homeassistant.core import HomeAssistant, callback -import homeassistant.helpers.config_validation as cv +from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from .const import ( + CONF_ROUND_DIGITS, + CONF_SOURCE_SENSOR, + CONF_UNIT_OF_MEASUREMENT, + CONF_UNIT_PREFIX, + CONF_UNIT_TIME, + INTEGRATION_METHODS, + METHOD_LEFT, + METHOD_RIGHT, + METHOD_TRAPEZOIDAL, +) + # mypy: allow-untyped-defs, no-check-untyped-defs _LOGGER = logging.getLogger(__name__) ATTR_SOURCE_ID = "source" -CONF_SOURCE_SENSOR = "source" -CONF_ROUND_DIGITS = "round" -CONF_UNIT_PREFIX = "unit_prefix" -CONF_UNIT_TIME = "unit_time" -CONF_UNIT_OF_MEASUREMENT = "unit" - -TRAPEZOIDAL_METHOD = "trapezoidal" -LEFT_METHOD = "left" -RIGHT_METHOD = "right" -INTEGRATION_METHOD = [TRAPEZOIDAL_METHOD, LEFT_METHOD, RIGHT_METHOD] - # SI Metric prefixes UNIT_PREFIXES = {None: 1, "k": 10**3, "M": 10**6, "G": 10**9, "T": 10**12} @@ -68,19 +71,50 @@ PLATFORM_SCHEMA = vol.All( PLATFORM_SCHEMA.extend( { vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_UNIQUE_ID): cv.string, vol.Required(CONF_SOURCE_SENSOR): cv.entity_id, vol.Optional(CONF_ROUND_DIGITS, default=DEFAULT_ROUND): vol.Coerce(int), vol.Optional(CONF_UNIT_PREFIX, default=None): vol.In(UNIT_PREFIXES), vol.Optional(CONF_UNIT_TIME, default=TIME_HOURS): vol.In(UNIT_TIME), vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, - vol.Optional(CONF_METHOD, default=TRAPEZOIDAL_METHOD): vol.In( - INTEGRATION_METHOD + vol.Optional(CONF_METHOD, default=METHOD_TRAPEZOIDAL): vol.In( + INTEGRATION_METHODS ), } ), ) +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Initialize Integration - Riemann sum integral config entry.""" + registry = er.async_get(hass) + # Validate + resolve entity registry id to entity_id + source_entity_id = er.async_validate_entity_id( + registry, config_entry.options[CONF_SOURCE_SENSOR] + ) + + unit_prefix = config_entry.options[CONF_UNIT_PREFIX] + if unit_prefix == "none": + unit_prefix = None + + integral = IntegrationSensor( + integration_method=config_entry.options[CONF_METHOD], + name=config_entry.title, + round_digits=int(config_entry.options[CONF_ROUND_DIGITS]), + source_entity=source_entity_id, + unique_id=config_entry.entry_id, + unit_of_measurement=None, + unit_prefix=unit_prefix, + unit_time=config_entry.options[CONF_UNIT_TIME], + ) + + async_add_entities([integral]) + + async def async_setup_platform( hass: HomeAssistant, config: ConfigType, @@ -89,13 +123,14 @@ async def async_setup_platform( ) -> None: """Set up the integration sensor.""" integral = IntegrationSensor( - config[CONF_SOURCE_SENSOR], - config.get(CONF_NAME), - config[CONF_ROUND_DIGITS], - config[CONF_UNIT_PREFIX], - config[CONF_UNIT_TIME], - config.get(CONF_UNIT_OF_MEASUREMENT), - config[CONF_METHOD], + integration_method=config[CONF_METHOD], + name=config.get(CONF_NAME), + round_digits=config[CONF_ROUND_DIGITS], + source_entity=config[CONF_SOURCE_SENSOR], + unique_id=config.get(CONF_UNIQUE_ID), + unit_of_measurement=config.get(CONF_UNIT_OF_MEASUREMENT), + unit_prefix=config[CONF_UNIT_PREFIX], + unit_time=config[CONF_UNIT_TIME], ) async_add_entities([integral]) @@ -106,15 +141,18 @@ class IntegrationSensor(RestoreEntity, SensorEntity): def __init__( self, - source_entity: str, + *, + integration_method: str, name: str | None, round_digits: int, + source_entity: str, + unique_id: str | None, + unit_of_measurement: str | None, unit_prefix: str | None, unit_time: str, - unit_of_measurement: str | None, - integration_method: str, ) -> None: """Initialize the integration sensor.""" + self._attr_unique_id = unique_id self._sensor_source_id = source_entity self._round_digits = round_digits self._state = None @@ -187,15 +225,15 @@ class IntegrationSensor(RestoreEntity, SensorEntity): new_state.last_updated - old_state.last_updated ).total_seconds() - if self._method == TRAPEZOIDAL_METHOD: + if self._method == METHOD_TRAPEZOIDAL: area = ( (Decimal(new_state.state) + Decimal(old_state.state)) * Decimal(elapsed_time) / 2 ) - elif self._method == LEFT_METHOD: + elif self._method == METHOD_LEFT: area = Decimal(old_state.state) * Decimal(elapsed_time) - elif self._method == RIGHT_METHOD: + elif self._method == METHOD_RIGHT: area = Decimal(new_state.state) * Decimal(elapsed_time) integral = area / (self._unit_prefix * self._unit_time) @@ -215,8 +253,10 @@ class IntegrationSensor(RestoreEntity, SensorEntity): self._state = integral self.async_write_ha_state() - async_track_state_change_event( - self.hass, [self._sensor_source_id], calc_integration + self.async_on_remove( + async_track_state_change_event( + self.hass, [self._sensor_source_id], calc_integration + ) ) @property diff --git a/homeassistant/components/integration/strings.json b/homeassistant/components/integration/strings.json new file mode 100644 index 00000000000..4eb3b952a78 --- /dev/null +++ b/homeassistant/components/integration/strings.json @@ -0,0 +1,36 @@ +{ + "title": "Integration - Riemann sum integral sensor", + "config": { + "step": { + "user": { + "title": "Add Riemann sum integral sensor", + "description": "Create a sensor that calculates a Riemann sum to estimate the integral of a sensor.", + "data": { + "method": "Integration method", + "name": "Name", + "round": "Precision", + "source": "Input sensor", + "unit_prefix": "Metric prefix", + "unit_time": "Time unit" + }, + "data_description": { + "round": "Controls the number of decimal digits in the output.", + "unit_prefix": "The output will be scaled according to the selected metric prefix.", + "unit_time": "The output will be scaled according to the selected time unit." + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "round": "[%key:component::integration::config::step::user::data::round%]" + }, + "data_description": { + "round": "[%key:component::integration::config::step::user::data_description::round%]" + } + } + } + } +} diff --git a/homeassistant/components/integration/translations/en.json b/homeassistant/components/integration/translations/en.json new file mode 100644 index 00000000000..1ee047b447f --- /dev/null +++ b/homeassistant/components/integration/translations/en.json @@ -0,0 +1,36 @@ +{ + "config": { + "step": { + "user": { + "data": { + "method": "Integration method", + "name": "Name", + "round": "Precision", + "source": "Input sensor", + "unit_prefix": "Metric prefix", + "unit_time": "Time unit" + }, + "data_description": { + "round": "Controls the number of decimal digits in the output.", + "unit_prefix": "The output will be scaled according to the selected metric prefix.", + "unit_time": "The output will be scaled according to the selected time unit." + }, + "description": "Create a sensor that calculates a Riemann sum to estimate the integral of a sensor.", + "title": "Add Riemann sum integral sensor" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "round": "Precision" + }, + "data_description": { + "round": "Controls the number of decimal digits in the output." + } + } + } + }, + "title": "Integration - Riemann sum integral sensor" +} \ No newline at end of file diff --git a/homeassistant/components/intellifire/binary_sensor.py b/homeassistant/components/intellifire/binary_sensor.py index 747dcaa58be..837fc9e1bee 100644 --- a/homeassistant/components/intellifire/binary_sensor.py +++ b/homeassistant/components/intellifire/binary_sensor.py @@ -7,11 +7,13 @@ from dataclasses import dataclass from intellifire4py import IntellifirePollData from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, BinarySensorEntity, BinarySensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import IntellifireDataUpdateCoordinator @@ -58,6 +60,85 @@ INTELLIFIRE_BINARY_SENSORS: tuple[IntellifireBinarySensorEntityDescription, ...] icon="mdi:home-thermometer-outline", value_fn=lambda data: data.thermostat_on, ), + IntellifireBinarySensorEntityDescription( + key="error_pilot_flame", + name="Pilot Flame Error", + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda data: data.error_pilot_flame, + device_class=BinarySensorDeviceClass.PROBLEM, + ), + IntellifireBinarySensorEntityDescription( + key="error_flame", + name="Flame Error", + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda data: data.error_flame, + device_class=BinarySensorDeviceClass.PROBLEM, + ), + IntellifireBinarySensorEntityDescription( + key="error_fan_delay", + name="Fan Delay Error", + icon="mdi:fan-alert", + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda data: data.error_fan_delay, + device_class=BinarySensorDeviceClass.PROBLEM, + ), + IntellifireBinarySensorEntityDescription( + key="error_maintenance", + name="Maintenance Error", + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda data: data.error_maintenance, + device_class=BinarySensorDeviceClass.PROBLEM, + ), + IntellifireBinarySensorEntityDescription( + key="error_disabled", + name="Disabled Error", + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda data: data.error_disabled, + device_class=BinarySensorDeviceClass.PROBLEM, + ), + IntellifireBinarySensorEntityDescription( + key="error_fan", + name="Fan Error", + icon="mdi:fan-alert", + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda data: data.error_fan, + device_class=BinarySensorDeviceClass.PROBLEM, + ), + IntellifireBinarySensorEntityDescription( + key="error_lights", + name="Lights Error", + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda data: data.error_lights, + device_class=BinarySensorDeviceClass.PROBLEM, + ), + IntellifireBinarySensorEntityDescription( + key="error_accessory", + name="Accessory Error", + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda data: data.error_accessory, + device_class=BinarySensorDeviceClass.PROBLEM, + ), + IntellifireBinarySensorEntityDescription( + key="error_soft_lock_out", + name="Soft Lock Out Error", + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda data: data.error_soft_lock_out, + device_class=BinarySensorDeviceClass.PROBLEM, + ), + IntellifireBinarySensorEntityDescription( + key="error_ecm_offline", + name="ECM Offline Error", + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda data: data.error_ecm_offline, + device_class=BinarySensorDeviceClass.PROBLEM, + ), + IntellifireBinarySensorEntityDescription( + key="error_offline", + name="Offline Error", + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda data: data.error_offline, + device_class=BinarySensorDeviceClass.PROBLEM, + ), ) diff --git a/homeassistant/components/intellifire/config_flow.py b/homeassistant/components/intellifire/config_flow.py index c541c733e08..a8d9fa135d2 100644 --- a/homeassistant/components/intellifire/config_flow.py +++ b/homeassistant/components/intellifire/config_flow.py @@ -1,32 +1,44 @@ """Config flow for IntelliFire integration.""" from __future__ import annotations +from dataclasses import dataclass from typing import Any from aiohttp import ClientConnectionError -from intellifire4py import IntellifireAsync +from intellifire4py import AsyncUDPFireplaceFinder, IntellifireAsync import voluptuous as vol from homeassistant import config_entries +from homeassistant.components.dhcp import DhcpServiceInfo from homeassistant.const import CONF_HOST -from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResult -from .const import DOMAIN +from .const import DOMAIN, LOGGER STEP_USER_DATA_SCHEMA = vol.Schema({vol.Required(CONF_HOST): str}) +MANUAL_ENTRY_STRING = "IP Address" # Simplified so it does not have to be translated -async def validate_input(hass: HomeAssistant, host: str) -> str: + +@dataclass +class DiscoveredHostInfo: + """Host info for discovery.""" + + ip: str + serial: str | None + + +async def validate_host_input(host: str) -> str: """Validate the user input allows us to connect. Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user. """ api = IntellifireAsync(host) await api.poll() - + serial = api.data.serial + LOGGER.debug("Found a fireplace: %s", serial) # Return the serial number which will be used to calculate a unique ID for the device/sensors - return api.data.serial + return serial class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @@ -34,30 +46,139 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 + def __init__(self): + """Initialize the Config Flow Handler.""" + self._config_context = {} + self._not_configured_hosts: list[DiscoveredHostInfo] = [] + self._discovered_host: DiscoveredHostInfo + + async def _find_fireplaces(self): + """Perform UDP discovery.""" + fireplace_finder = AsyncUDPFireplaceFinder() + discovered_hosts = await fireplace_finder.search_fireplace(timeout=1) + configured_hosts = { + entry.data[CONF_HOST] + for entry in self._async_current_entries(include_ignore=False) + if CONF_HOST in entry.data # CONF_HOST will be missing for ignored entries + } + + self._not_configured_hosts = [ + DiscoveredHostInfo(ip, None) + for ip in discovered_hosts + if ip not in configured_hosts + ] + LOGGER.debug("Discovered Hosts: %s", discovered_hosts) + LOGGER.debug("Configured Hosts: %s", configured_hosts) + LOGGER.debug("Not Configured Hosts: %s", self._not_configured_hosts) + + async def _async_validate_and_create_entry(self, host: str) -> FlowResult: + """Validate and create the entry.""" + self._async_abort_entries_match({CONF_HOST: host}) + serial = await validate_host_input(host) + await self.async_set_unique_id(serial, raise_on_progress=False) + self._abort_if_unique_id_configured(updates={CONF_HOST: host}) + return self.async_create_entry( + title=f"Fireplace {serial}", + data={CONF_HOST: host}, + ) + + async def async_step_manual_device_entry(self, user_input=None): + """Handle manual input of local IP configuration.""" + errors = {} + host = user_input.get(CONF_HOST) if user_input else None + if user_input is not None: + try: + return await self._async_validate_and_create_entry(host) + except (ConnectionError, ClientConnectionError): + errors["base"] = "cannot_connect" + + return self.async_show_form( + step_id="manual_device_entry", + errors=errors, + data_schema=vol.Schema({vol.Required(CONF_HOST, default=host): str}), + ) + + async def async_step_pick_device( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Pick which device to configure.""" + errors = {} + + if user_input is not None: + if user_input[CONF_HOST] == MANUAL_ENTRY_STRING: + return await self.async_step_manual_device_entry() + + try: + return await self._async_validate_and_create_entry( + user_input[CONF_HOST] + ) + except (ConnectionError, ClientConnectionError): + errors["base"] = "cannot_connect" + + return self.async_show_form( + step_id="pick_device", + errors=errors, + data_schema=vol.Schema( + { + vol.Required(CONF_HOST): vol.In( + [host.ip for host in self._not_configured_hosts] + + [MANUAL_ENTRY_STRING] + ) + } + ), + ) + async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: - """Handle the initial step.""" - if user_input is None: - return self.async_show_form( - step_id="user", data_schema=STEP_USER_DATA_SCHEMA - ) - errors = {} + """Start the user flow.""" + # Launch fireplaces discovery + await self._find_fireplaces() + + if self._not_configured_hosts: + LOGGER.debug("Running Step: pick_device") + return await self.async_step_pick_device() + LOGGER.debug("Running Step: manual_device_entry") + return await self.async_step_manual_device_entry() + + async def async_step_dhcp(self, discovery_info: DhcpServiceInfo) -> FlowResult: + """Handle DHCP Discovery.""" + + # Run validation logic on ip + host = discovery_info.ip + + self._async_abort_entries_match({CONF_HOST: host}) try: - serial = await validate_input(self.hass, user_input[CONF_HOST]) + serial = await validate_host_input(host) except (ConnectionError, ClientConnectionError): - errors["base"] = "cannot_connect" - else: - await self.async_set_unique_id(serial) - self._abort_if_unique_id_configured( - updates={CONF_HOST: user_input[CONF_HOST]} + return self.async_abort(reason="not_intellifire_device") + + await self.async_set_unique_id(serial) + self._abort_if_unique_id_configured(updates={CONF_HOST: host}) + self._discovered_host = DiscoveredHostInfo(ip=host, serial=serial) + + placeholders = {CONF_HOST: host, "serial": serial} + self.context["title_placeholders"] = placeholders + self._set_confirm_only() + + return await self.async_step_dhcp_confirm() + + async def async_step_dhcp_confirm(self, user_input=None): + """Attempt to confirm.""" + + # Add the hosts one by one + host = self._discovered_host.ip + serial = self._discovered_host.serial + + if user_input is None: + # Show the confirmation dialog + return self.async_show_form( + step_id="dhcp_confirm", + description_placeholders={CONF_HOST: host, "serial": serial}, ) - return self.async_create_entry( - title="Fireplace", - data={CONF_HOST: user_input[CONF_HOST]}, - ) - return self.async_show_form( - step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors + return self.async_create_entry( + title=f"Fireplace {serial}", + data={CONF_HOST: host}, ) diff --git a/homeassistant/components/intellifire/entity.py b/homeassistant/components/intellifire/entity.py index eeb5e7b51bd..6d20c015ab9 100644 --- a/homeassistant/components/intellifire/entity.py +++ b/homeassistant/components/intellifire/entity.py @@ -7,10 +7,9 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import IntellifireDataUpdateCoordinator -class IntellifireEntity(CoordinatorEntity): +class IntellifireEntity(CoordinatorEntity[IntellifireDataUpdateCoordinator]): """Define a generic class for Intellifire entities.""" - coordinator: IntellifireDataUpdateCoordinator _attr_attribution = "Data provided by unpublished Intellifire API" def __init__( diff --git a/homeassistant/components/intellifire/manifest.json b/homeassistant/components/intellifire/manifest.json index 03862a6ef5f..388ce0c86cb 100644 --- a/homeassistant/components/intellifire/manifest.json +++ b/homeassistant/components/intellifire/manifest.json @@ -3,9 +3,9 @@ "name": "IntelliFire", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/intellifire", - "requirements": ["intellifire4py==0.9.9"], - "dependencies": [], + "requirements": ["intellifire4py==1.0.2"], "codeowners": ["@jeeftor"], "iot_class": "local_polling", - "loggers": ["intellifire4py"] + "loggers": ["intellifire4py"], + "dhcp": [{ "hostname": "zentrios-*" }] } diff --git a/homeassistant/components/intellifire/strings.json b/homeassistant/components/intellifire/strings.json index 52d57eda809..65877508db6 100644 --- a/homeassistant/components/intellifire/strings.json +++ b/homeassistant/components/intellifire/strings.json @@ -1,7 +1,17 @@ { "config": { + "flow_title": "{serial} ({host})", "step": { - "user": { + "manual_device_entry": { + "description": "Local Configuration", + "data": { + "host": "[%key:common::config_flow::data::host%]" + } + }, + "dhcp_confirm": { + "description": "Do you want to setup {host}\nSerial: {serial}?" + }, + "pick_device": { "data": { "host": "[%key:common::config_flow::data::host%]" } @@ -11,7 +21,8 @@ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" }, "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "not_intellifire_device": "Not an IntelliFire Device." } } } diff --git a/homeassistant/components/intellifire/translations/en.json b/homeassistant/components/intellifire/translations/en.json index 0a4ba36e285..844d77427ca 100644 --- a/homeassistant/components/intellifire/translations/en.json +++ b/homeassistant/components/intellifire/translations/en.json @@ -1,14 +1,24 @@ { "config": { "abort": { - "already_configured": "Device is already configured" + "already_configured": "Device is already configured", + "not_intellifire_device": "Not an IntelliFire Device." }, "error": { - "cannot_connect": "Failed to connect", - "unknown": "Unexpected error" + "cannot_connect": "Failed to connect" }, + "flow_title": "{serial} ({host})", "step": { - "user": { + "dhcp_confirm": { + "description": "Do you want to setup {host}\nSerial: {serial}?" + }, + "manual_device_entry": { + "data": { + "host": "Host" + }, + "description": "Local Configuration" + }, + "pick_device": { "data": { "host": "Host" } diff --git a/homeassistant/components/intellifire/translations/fr.json b/homeassistant/components/intellifire/translations/fr.json index 88a0aeb68c8..3b0762be825 100644 --- a/homeassistant/components/intellifire/translations/fr.json +++ b/homeassistant/components/intellifire/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "unknown": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + "unknown": "Erreur inattendue" }, "step": { "user": { diff --git a/homeassistant/components/ios/translations/fr.json b/homeassistant/components/ios/translations/fr.json index 85000f60a49..d3d7df17992 100644 --- a/homeassistant/components/ios/translations/fr.json +++ b/homeassistant/components/ios/translations/fr.json @@ -5,7 +5,7 @@ }, "step": { "confirm": { - "description": "Voulez-vous commencer la configuration ?" + "description": "Voulez-vous commencer la configuration\u00a0?" } } } diff --git a/homeassistant/components/iotawatt/manifest.json b/homeassistant/components/iotawatt/manifest.json index 5addb869994..545b07cd973 100644 --- a/homeassistant/components/iotawatt/manifest.json +++ b/homeassistant/components/iotawatt/manifest.json @@ -3,13 +3,8 @@ "name": "IoTaWatt", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/iotawatt", - "requirements": [ - "iotawattpy==0.1.0" - ], - "codeowners": [ - "@gtdiehl", - "@jyavenard" - ], + "requirements": ["iotawattpy==0.1.0"], + "codeowners": ["@gtdiehl", "@jyavenard"], "iot_class": "local_polling", "loggers": ["iotawattpy"] -} \ No newline at end of file +} diff --git a/homeassistant/components/iotawatt/sensor.py b/homeassistant/components/iotawatt/sensor.py index c3c173f778e..8bcf5bfae9a 100644 --- a/homeassistant/components/iotawatt/sensor.py +++ b/homeassistant/components/iotawatt/sensor.py @@ -24,11 +24,12 @@ from homeassistant.const import ( POWER_WATT, ) from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers import entity, entity_registry, update_coordinator +from homeassistant.helpers import entity, entity_registry from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import StateType +from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util import dt from .const import ( @@ -159,11 +160,10 @@ async def async_setup_entry( coordinator.async_add_listener(new_data_received) -class IotaWattSensor(update_coordinator.CoordinatorEntity, SensorEntity): +class IotaWattSensor(CoordinatorEntity[IotawattUpdater], SensorEntity): """Defines a IoTaWatt Energy Sensor.""" entity_description: IotaWattSensorEntityDescription - coordinator: IotawattUpdater def __init__( self, diff --git a/homeassistant/components/iotawatt/translations/fr.json b/homeassistant/components/iotawatt/translations/fr.json index 1452beb4465..2c93cf7432f 100644 --- a/homeassistant/components/iotawatt/translations/fr.json +++ b/homeassistant/components/iotawatt/translations/fr.json @@ -2,7 +2,7 @@ "config": { "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/ipma/strings.json b/homeassistant/components/ipma/strings.json index 24f142938d0..0dd013135dc 100644 --- a/homeassistant/components/ipma/strings.json +++ b/homeassistant/components/ipma/strings.json @@ -15,8 +15,8 @@ "error": { "name_exists": "Name already exists" } }, "system_health": { - "info": { - "api_endpoint_reachable": "IPMA API endpoint reachable" - } + "info": { + "api_endpoint_reachable": "IPMA API endpoint reachable" + } } } diff --git a/homeassistant/components/ipp/entity.py b/homeassistant/components/ipp/entity.py index 7bd01b4cd12..b2f3a4a1469 100644 --- a/homeassistant/components/ipp/entity.py +++ b/homeassistant/components/ipp/entity.py @@ -8,7 +8,7 @@ from .const import DOMAIN from .coordinator import IPPDataUpdateCoordinator -class IPPEntity(CoordinatorEntity): +class IPPEntity(CoordinatorEntity[IPPDataUpdateCoordinator]): """Defines a base IPP entity.""" def __init__( diff --git a/homeassistant/components/iqvia/strings.json b/homeassistant/components/iqvia/strings.json index 7e16d3ae6e3..5dc0dea53d5 100644 --- a/homeassistant/components/iqvia/strings.json +++ b/homeassistant/components/iqvia/strings.json @@ -2,7 +2,6 @@ "config": { "step": { "user": { - "title": "IQVIA", "description": "Fill out your U.S. or Canadian ZIP code.", "data": { "zip_code": "ZIP Code" diff --git a/homeassistant/components/iqvia/translations/fr.json b/homeassistant/components/iqvia/translations/fr.json index a967ff490e8..97adf4c35a6 100644 --- a/homeassistant/components/iqvia/translations/fr.json +++ b/homeassistant/components/iqvia/translations/fr.json @@ -4,7 +4,7 @@ "already_configured": "Le service est d\u00e9j\u00e0 configur\u00e9" }, "error": { - "invalid_zip_code": "Code postal invalide" + "invalid_zip_code": "Code postal non valide" }, "step": { "user": { diff --git a/homeassistant/components/iss/strings.json b/homeassistant/components/iss/strings.json index 4a2da5f6556..e0c7d85efa4 100644 --- a/homeassistant/components/iss/strings.json +++ b/homeassistant/components/iss/strings.json @@ -1,22 +1,22 @@ { - "config": { - "step": { - "user": { - "description": "Do you want to configure International Space Station (ISS)?" - } - }, - "abort": { - "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", - "latitude_longitude_not_defined": "Latitude and longitude are not defined in Home Assistant." + "config": { + "step": { + "user": { + "description": "Do you want to configure International Space Station (ISS)?" } }, - "options": { - "step": { - "init": { - "data": { - "show_on_map": "Show on map" - } + "abort": { + "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", + "latitude_longitude_not_defined": "Latitude and longitude are not defined in Home Assistant." + } + }, + "options": { + "step": { + "init": { + "data": { + "show_on_map": "Show on map" } } } } +} diff --git a/homeassistant/components/iss/translations/el.json b/homeassistant/components/iss/translations/el.json index b662dbea64c..0938fd72c09 100644 --- a/homeassistant/components/iss/translations/el.json +++ b/homeassistant/components/iss/translations/el.json @@ -9,7 +9,7 @@ "data": { "show_on_map": "\u0395\u03bc\u03c6\u03ac\u03bd\u03b9\u03c3\u03b7 \u03c3\u03c4\u03bf \u03c7\u03ac\u03c1\u03c4\u03b7;" }, - "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03ce\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03bd \u0394\u03b9\u03b5\u03b8\u03bd\u03ae \u0394\u03b9\u03b1\u03c3\u03c4\u03b7\u03bc\u03b9\u03ba\u03cc \u03a3\u03c4\u03b1\u03b8\u03bc\u03cc;" + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03c0\u03b1\u03c1\u03b1\u03bc\u03ad\u03c4\u03c1\u03bf\u03c5\u03c2 \u03c4\u03bf\u03c5 \u0394\u03b9\u03b5\u03b8\u03bd\u03bf\u03cd\u03c2 \u0394\u03b9\u03b1\u03c3\u03c4\u03b7\u03bc\u03b9\u03ba\u03bf\u03cd \u03a3\u03c4\u03b1\u03b8\u03bc\u03bf\u03cd (ISS);" } } }, diff --git a/homeassistant/components/iss/translations/es.json b/homeassistant/components/iss/translations/es.json index 91bbd571ab9..2cc46fefdb1 100644 --- a/homeassistant/components/iss/translations/es.json +++ b/homeassistant/components/iss/translations/es.json @@ -1,4 +1,16 @@ { + "config": { + "abort": { + "latitude_longitude_not_defined": "La latitud y la longitud no est\u00e1n definidas en Home Assistant." + }, + "step": { + "user": { + "data": { + "show_on_map": "\u00bfMostrar en el mapa?" + } + } + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/iss/translations/fr.json b/homeassistant/components/iss/translations/fr.json index fd0b2adba42..2f9ad5d427c 100644 --- a/homeassistant/components/iss/translations/fr.json +++ b/homeassistant/components/iss/translations/fr.json @@ -7,9 +7,9 @@ "step": { "user": { "data": { - "show_on_map": "Afficher sur la carte?" + "show_on_map": "Afficher sur la carte\u00a0?" }, - "description": "Voulez-vous configurer la Station spatiale internationale?" + "description": "Voulez-vous configurer la Station spatiale internationale (ISS)\u00a0?" } } }, @@ -17,7 +17,7 @@ "step": { "init": { "data": { - "show_on_map": "Montrer sur la carte" + "show_on_map": "Afficher sur la carte" } } } diff --git a/homeassistant/components/iss/translations/nl.json b/homeassistant/components/iss/translations/nl.json index c3e7ade05d9..08a170b1ccc 100644 --- a/homeassistant/components/iss/translations/nl.json +++ b/homeassistant/components/iss/translations/nl.json @@ -9,7 +9,7 @@ "data": { "show_on_map": "Op kaart tonen?" }, - "description": "Wilt u het International Space Station configureren?" + "description": "Wilt u International Space Station configureren?" } } }, diff --git a/homeassistant/components/isy994/fan.py b/homeassistant/components/isy994/fan.py index bf4d48ad3e8..c215eebd382 100644 --- a/homeassistant/components/isy994/fan.py +++ b/homeassistant/components/isy994/fan.py @@ -76,7 +76,6 @@ class ISYFanEntity(ISYNodeEntity, FanEntity): async def async_turn_on( self, - speed: str | None = None, percentage: int | None = None, preset_mode: str | None = None, **kwargs: Any, @@ -121,7 +120,6 @@ class ISYFanProgramEntity(ISYProgramEntity, FanEntity): async def async_turn_on( self, - speed: str | None = None, percentage: int | None = None, preset_mode: str | None = None, **kwargs: Any, diff --git a/homeassistant/components/isy994/light.py b/homeassistant/components/isy994/light.py index 640442c3f19..13df6d513b7 100644 --- a/homeassistant/components/isy994/light.py +++ b/homeassistant/components/isy994/light.py @@ -91,7 +91,6 @@ class ISYLightEntity(ISYNodeEntity, LightEntity, RestoreEntity): self._last_brightness = self._node.status super().async_on_update(event) - # pylint: disable=arguments-differ async def async_turn_on(self, brightness: int | None = None, **kwargs: Any) -> None: """Send the turn on command to the ISY994 light device.""" if self._restore_light_state and brightness is None and self._last_brightness: diff --git a/homeassistant/components/isy994/manifest.json b/homeassistant/components/isy994/manifest.json index 89e66533913..d92226a4277 100644 --- a/homeassistant/components/isy994/manifest.json +++ b/homeassistant/components/isy994/manifest.json @@ -2,7 +2,7 @@ "domain": "isy994", "name": "Universal Devices ISY994", "documentation": "https://www.home-assistant.io/integrations/isy994", - "requirements": ["pyisy==3.0.5"], + "requirements": ["pyisy==3.0.6"], "codeowners": ["@bdraco", "@shbatm"], "config_flow": true, "ssdp": [ @@ -12,8 +12,8 @@ } ], "dhcp": [ - {"registered_devices": true}, - {"hostname": "isy*", "macaddress": "0021B9*"} + { "registered_devices": true }, + { "hostname": "isy*", "macaddress": "0021B9*" } ], "iot_class": "local_push", "loggers": ["pyisy"] diff --git a/homeassistant/components/isy994/translations/fr.json b/homeassistant/components/isy994/translations/fr.json index 654df08431d..c9e53845dfb 100644 --- a/homeassistant/components/isy994/translations/fr.json +++ b/homeassistant/components/isy994/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "invalid_host": "L'entr\u00e9e d'h\u00f4te n'\u00e9tait pas au format URL complet, par exemple http://192.168.10.100:80", "unknown": "Erreur inattendue" }, @@ -16,7 +16,7 @@ "host": "URL", "password": "Mot de passe", "tls": "La version TLS du contr\u00f4leur ISY.", - "username": "Username" + "username": "Nom d'utilisateur" }, "description": "L'entr\u00e9e d'h\u00f4te doit \u00eatre au format URL complet, par exemple, http://192.168.10.100:80", "title": "Connect\u00e9 \u00e0 votre ISY994" diff --git a/homeassistant/components/izone/translations/fr.json b/homeassistant/components/izone/translations/fr.json index eb86962e11b..89abc8a4616 100644 --- a/homeassistant/components/izone/translations/fr.json +++ b/homeassistant/components/izone/translations/fr.json @@ -6,7 +6,7 @@ }, "step": { "confirm": { - "description": "Voulez-vous configurer iZone?" + "description": "Voulez-vous configurer iZone\u00a0?" } } } diff --git a/homeassistant/components/jellyfin/manifest.json b/homeassistant/components/jellyfin/manifest.json index ce00edfc108..993d2520484 100644 --- a/homeassistant/components/jellyfin/manifest.json +++ b/homeassistant/components/jellyfin/manifest.json @@ -3,12 +3,8 @@ "name": "Jellyfin", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/jellyfin", - "requirements": [ - "jellyfin-apiclient-python==1.7.2" - ], + "requirements": ["jellyfin-apiclient-python==1.7.2"], "iot_class": "local_polling", - "codeowners": [ - "@j-stienstra" - ], + "codeowners": ["@j-stienstra"], "loggers": ["jellyfin_apiclient_python"] -} \ No newline at end of file +} diff --git a/homeassistant/components/jellyfin/media_source.py b/homeassistant/components/jellyfin/media_source.py index 22bb2b3937d..dbd79612378 100644 --- a/homeassistant/components/jellyfin/media_source.py +++ b/homeassistant/components/jellyfin/media_source.py @@ -324,6 +324,9 @@ class JellyfinSource(MediaSource): def _media_mime_type(media_item: dict[str, Any]) -> str: """Return the mime type of a media item.""" + if not media_item[ITEM_KEY_MEDIA_SOURCES]: + raise BrowseError("Unable to determine mime type for item without media source") + media_source = media_item[ITEM_KEY_MEDIA_SOURCES][0] path = media_source[MEDIA_SOURCE_KEY_PATH] mime_type, _ = mimetypes.guess_type(path) diff --git a/homeassistant/components/jellyfin/strings.json b/homeassistant/components/jellyfin/strings.json index 7587c1d86d9..2c832f4003b 100644 --- a/homeassistant/components/jellyfin/strings.json +++ b/homeassistant/components/jellyfin/strings.json @@ -18,4 +18,4 @@ "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" } } -} \ No newline at end of file +} diff --git a/homeassistant/components/jellyfin/translations/fr.json b/homeassistant/components/jellyfin/translations/fr.json index c797b7e93a3..5037fd507f0 100644 --- a/homeassistant/components/jellyfin/translations/fr.json +++ b/homeassistant/components/jellyfin/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/joaoapps_join/manifest.json b/homeassistant/components/joaoapps_join/manifest.json index b56f4a091f0..35108292321 100644 --- a/homeassistant/components/joaoapps_join/manifest.json +++ b/homeassistant/components/joaoapps_join/manifest.json @@ -2,7 +2,7 @@ "domain": "joaoapps_join", "name": "Joaoapps Join", "documentation": "https://www.home-assistant.io/integrations/joaoapps_join", - "requirements": ["python-join-api==0.0.6"], + "requirements": ["python-join-api==0.0.9"], "codeowners": [], "iot_class": "cloud_push", "loggers": ["pyjoin"] diff --git a/homeassistant/components/joaoapps_join/notify.py b/homeassistant/components/joaoapps_join/notify.py index 06cad45bdde..b4ccdc9d348 100644 --- a/homeassistant/components/joaoapps_join/notify.py +++ b/homeassistant/components/joaoapps_join/notify.py @@ -72,6 +72,7 @@ class JoinNotificationService(BaseNotificationService): image=data.get("image"), sound=data.get("sound"), notification_id=data.get("notification_id"), + category=data.get("category"), url=data.get("url"), tts=data.get("tts"), tts_language=data.get("tts_language"), diff --git a/homeassistant/components/juicenet/manifest.json b/homeassistant/components/juicenet/manifest.json index 35e9414a1e6..a080ba77c4d 100644 --- a/homeassistant/components/juicenet/manifest.json +++ b/homeassistant/components/juicenet/manifest.json @@ -2,7 +2,7 @@ "domain": "juicenet", "name": "JuiceNet", "documentation": "https://www.home-assistant.io/integrations/juicenet", - "requirements": ["python-juicenet==1.0.2"], + "requirements": ["python-juicenet==1.1.0"], "codeowners": ["@jesserockz"], "config_flow": true, "iot_class": "cloud_polling", diff --git a/homeassistant/components/juicenet/translations/fr.json b/homeassistant/components/juicenet/translations/fr.json index f4e5bfa53d5..8688b5b4963 100644 --- a/homeassistant/components/juicenet/translations/fr.json +++ b/homeassistant/components/juicenet/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/kaleidescape/__init__.py b/homeassistant/components/kaleidescape/__init__.py new file mode 100644 index 00000000000..a66ae25d436 --- /dev/null +++ b/homeassistant/components/kaleidescape/__init__.py @@ -0,0 +1,96 @@ +"""The Kaleidescape integration.""" + +from __future__ import annotations + +from dataclasses import dataclass +import logging +from typing import TYPE_CHECKING + +from kaleidescape import Device as KaleidescapeDevice, KaleidescapeError + +from homeassistant.const import CONF_HOST, EVENT_HOMEASSISTANT_STOP, Platform +from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError + +from .const import DOMAIN + +if TYPE_CHECKING: + from homeassistant.config_entries import ConfigEntry + from homeassistant.core import Event, HomeAssistant + +_LOGGER = logging.getLogger(__name__) + +PLATFORMS = [Platform.MEDIA_PLAYER, Platform.REMOTE, Platform.SENSOR] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Kaleidescape from a config entry.""" + device = KaleidescapeDevice( + entry.data[CONF_HOST], timeout=5, reconnect=True, reconnect_delay=5 + ) + + try: + await device.connect() + except (KaleidescapeError, ConnectionError) as err: + await device.disconnect() + raise ConfigEntryNotReady( + f"Unable to connect to {entry.data[CONF_HOST]}: {err}" + ) from err + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = device + + async def disconnect(event: Event) -> None: + await device.disconnect() + + entry.async_on_unload( + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, disconnect) + ) + + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + await hass.data[DOMAIN][entry.entry_id].disconnect() + hass.data[DOMAIN].pop(entry.entry_id) + return unload_ok + + +@dataclass +class KaleidescapeDeviceInfo: + """Metadata for a Kaleidescape device.""" + + host: str + serial: str + name: str + model: str + server_only: bool + + +class UnsupportedError(HomeAssistantError): + """Error for unsupported device types.""" + + +async def validate_host(host: str) -> KaleidescapeDeviceInfo: + """Validate device host.""" + device = KaleidescapeDevice(host) + + try: + await device.connect() + except (KaleidescapeError, ConnectionError): + await device.disconnect() + raise + + info = KaleidescapeDeviceInfo( + host=device.host, + serial=device.system.serial_number, + name=device.system.friendly_name, + model=device.system.type, + server_only=device.is_server_only, + ) + + await device.disconnect() + + return info diff --git a/homeassistant/components/kaleidescape/config_flow.py b/homeassistant/components/kaleidescape/config_flow.py new file mode 100644 index 00000000000..5454f29f5cb --- /dev/null +++ b/homeassistant/components/kaleidescape/config_flow.py @@ -0,0 +1,111 @@ +"""Config flow for Kaleidescape.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, cast +from urllib.parse import urlparse + +import voluptuous as vol + +from homeassistant.components import ssdp +from homeassistant.config_entries import ConfigFlow +from homeassistant.const import CONF_HOST + +from . import KaleidescapeDeviceInfo, UnsupportedError, validate_host +from .const import DEFAULT_HOST, DOMAIN, NAME as KALEIDESCAPE_NAME + +if TYPE_CHECKING: + from homeassistant.data_entry_flow import FlowResult + +ERROR_CANNOT_CONNECT = "cannot_connect" +ERROR_UNSUPPORTED = "unsupported" + + +class KaleidescapeConfigFlow(ConfigFlow, domain=DOMAIN): + """Config flow for Kaleidescape integration.""" + + VERSION = 1 + + discovered_device: KaleidescapeDeviceInfo + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle user initiated device additions.""" + errors = {} + host = DEFAULT_HOST + + if user_input is not None: + host = user_input[CONF_HOST].strip() + + try: + info = await validate_host(host) + if info.server_only: + raise UnsupportedError + except ConnectionError: + errors["base"] = ERROR_CANNOT_CONNECT + except UnsupportedError: + errors["base"] = ERROR_UNSUPPORTED + else: + host = info.host + + await self.async_set_unique_id(info.serial, raise_on_progress=False) + self._abort_if_unique_id_configured(updates={CONF_HOST: host}) + + return self.async_create_entry( + title=f"{KALEIDESCAPE_NAME} ({info.name})", + data={CONF_HOST: host}, + ) + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema({vol.Required(CONF_HOST, default=host): str}), + errors=errors, + ) + + async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult: + """Handle discovered device.""" + host = cast(str, urlparse(discovery_info.ssdp_location).hostname) + serial_number = discovery_info.upnp[ssdp.ATTR_UPNP_SERIAL] + + await self.async_set_unique_id(serial_number) + self._abort_if_unique_id_configured(updates={CONF_HOST: host}) + + try: + self.discovered_device = await validate_host(host) + if self.discovered_device.server_only: + raise UnsupportedError + except ConnectionError: + return self.async_abort(reason=ERROR_CANNOT_CONNECT) + except UnsupportedError: + return self.async_abort(reason=ERROR_UNSUPPORTED) + + self.context.update( + { + "title_placeholders": { + "name": self.discovered_device.name, + "model": self.discovered_device.model, + } + } + ) + + return await self.async_step_discovery_confirm() + + async def async_step_discovery_confirm( + self, user_input: dict | None = None + ) -> FlowResult: + """Handle addition of discovered device.""" + if user_input is None: + return self.async_show_form( + step_id="discovery_confirm", + description_placeholders={ + "name": self.discovered_device.name, + "model": self.discovered_device.model, + }, + errors={}, + ) + + return self.async_create_entry( + title=f"{KALEIDESCAPE_NAME} ({self.discovered_device.name})", + data={CONF_HOST: self.discovered_device.host}, + ) diff --git a/homeassistant/components/kaleidescape/const.py b/homeassistant/components/kaleidescape/const.py new file mode 100644 index 00000000000..dc4e0195977 --- /dev/null +++ b/homeassistant/components/kaleidescape/const.py @@ -0,0 +1,5 @@ +"""Constants for the Kaleidescape integration.""" + +NAME = "Kaleidescape" +DOMAIN = "kaleidescape" +DEFAULT_HOST = "my-kaleidescape.local" diff --git a/homeassistant/components/kaleidescape/entity.py b/homeassistant/components/kaleidescape/entity.py new file mode 100644 index 00000000000..9a5e62bca94 --- /dev/null +++ b/homeassistant/components/kaleidescape/entity.py @@ -0,0 +1,47 @@ +"""Base Entity for Kaleidescape.""" + +from __future__ import annotations + +import logging +from typing import TYPE_CHECKING + +from homeassistant.core import callback +from homeassistant.helpers.entity import DeviceInfo, Entity + +from .const import DOMAIN as KALEIDESCAPE_DOMAIN, NAME as KALEIDESCAPE_NAME + +if TYPE_CHECKING: + from kaleidescape import Device as KaleidescapeDevice + +_LOGGER = logging.getLogger(__name__) + + +class KaleidescapeEntity(Entity): + """Defines a base Kaleidescape entity.""" + + def __init__(self, device: KaleidescapeDevice) -> None: + """Initialize entity.""" + self._device = device + + self._attr_should_poll = False + self._attr_unique_id = device.serial_number + self._attr_name = f"{KALEIDESCAPE_NAME} {device.system.friendly_name}" + self._attr_device_info = DeviceInfo( + identifiers={(KALEIDESCAPE_DOMAIN, self._device.serial_number)}, + name=self.name, + model=self._device.system.type, + manufacturer=KALEIDESCAPE_NAME, + sw_version=f"{self._device.system.kos_version}", + suggested_area="Theater", + configuration_url=f"http://{self._device.host}", + ) + + async def async_added_to_hass(self) -> None: + """Register update listener.""" + + @callback + def _update(event: str) -> None: + """Handle device state changes.""" + self.async_write_ha_state() + + self.async_on_remove(self._device.dispatcher.connect(_update).disconnect) diff --git a/homeassistant/components/kaleidescape/manifest.json b/homeassistant/components/kaleidescape/manifest.json new file mode 100644 index 00000000000..4d2eb18b93e --- /dev/null +++ b/homeassistant/components/kaleidescape/manifest.json @@ -0,0 +1,15 @@ +{ + "domain": "kaleidescape", + "name": "Kaleidescape", + "config_flow": true, + "ssdp": [ + { + "manufacturer": "Kaleidescape, Inc.", + "deviceType": "schemas-upnp-org:device:Basic:1" + } + ], + "documentation": "https://www.home-assistant.io/integrations/kaleidescape", + "requirements": ["pykaleidescape==1.0.1"], + "codeowners": ["@SteveEasley"], + "iot_class": "local_push" +} diff --git a/homeassistant/components/kaleidescape/media_player.py b/homeassistant/components/kaleidescape/media_player.py new file mode 100644 index 00000000000..080db5524fe --- /dev/null +++ b/homeassistant/components/kaleidescape/media_player.py @@ -0,0 +1,158 @@ +"""Kaleidescape Media Player.""" + +from __future__ import annotations + +import logging +from typing import TYPE_CHECKING + +from kaleidescape import const as kaleidescape_const + +from homeassistant.components.media_player import MediaPlayerEntity +from homeassistant.components.media_player.const import ( + SUPPORT_NEXT_TRACK, + SUPPORT_PAUSE, + SUPPORT_PLAY, + SUPPORT_PREVIOUS_TRACK, + SUPPORT_STOP, + SUPPORT_TURN_OFF, + SUPPORT_TURN_ON, +) +from homeassistant.const import STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING +from homeassistant.util.dt import utcnow + +from .const import DOMAIN as KALEIDESCAPE_DOMAIN +from .entity import KaleidescapeEntity + +if TYPE_CHECKING: + from datetime import datetime + + from kaleidescape import Device as KaleidescapeDevice + + from homeassistant.config_entries import ConfigEntry + from homeassistant.core import HomeAssistant + from homeassistant.helpers.entity_platform import AddEntitiesCallback + + +KALEIDESCAPE_PLAYING_STATES = [ + kaleidescape_const.PLAY_STATUS_PLAYING, + kaleidescape_const.PLAY_STATUS_FORWARD, + kaleidescape_const.PLAY_STATUS_REVERSE, +] + +KALEIDESCAPE_PAUSED_STATES = [kaleidescape_const.PLAY_STATUS_PAUSED] + +SUPPORTED_FEATURES = ( + SUPPORT_TURN_ON + | SUPPORT_TURN_OFF + | SUPPORT_PLAY + | SUPPORT_PAUSE + | SUPPORT_STOP + | SUPPORT_NEXT_TRACK + | SUPPORT_PREVIOUS_TRACK +) + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up the platform from a config entry.""" + entities = [KaleidescapeMediaPlayer(hass.data[KALEIDESCAPE_DOMAIN][entry.entry_id])] + async_add_entities(entities) + + +class KaleidescapeMediaPlayer(KaleidescapeEntity, MediaPlayerEntity): + """Representation of a Kaleidescape device.""" + + def __init__(self, device: KaleidescapeDevice) -> None: + """Initialize media player.""" + super().__init__(device) + self._attr_supported_features = SUPPORTED_FEATURES + + async def async_turn_on(self) -> None: + """Send leave standby command.""" + await self._device.leave_standby() + + async def async_turn_off(self) -> None: + """Send enter standby command.""" + await self._device.enter_standby() + + async def async_media_pause(self) -> None: + """Send pause command.""" + await self._device.pause() + + async def async_media_play(self) -> None: + """Send play command.""" + await self._device.play() + + async def async_media_stop(self) -> None: + """Send stop command.""" + await self._device.stop() + + async def async_media_next_track(self) -> None: + """Send track next command.""" + await self._device.next() + + async def async_media_previous_track(self) -> None: + """Send track previous command.""" + await self._device.previous() + + @property + def state(self) -> str: + """State of device.""" + if self._device.power.state == kaleidescape_const.DEVICE_POWER_STATE_STANDBY: + return STATE_OFF + if self._device.movie.play_status in KALEIDESCAPE_PLAYING_STATES: + return STATE_PLAYING + if self._device.movie.play_status in KALEIDESCAPE_PAUSED_STATES: + return STATE_PAUSED + return STATE_IDLE + + @property + def available(self) -> bool: + """Return if device is available.""" + return self._device.is_connected + + @property + def media_content_id(self) -> str | None: + """Content ID of current playing media.""" + if self._device.movie.handle: + return self._device.movie.handle + return None + + @property + def media_content_type(self) -> str | None: + """Content type of current playing media.""" + return self._device.movie.media_type + + @property + def media_duration(self) -> int | None: + """Duration of current playing media in seconds.""" + if self._device.movie.title_length: + return self._device.movie.title_length + return None + + @property + def media_position(self) -> int | None: + """Position of current playing media in seconds.""" + if self._device.movie.title_location: + return self._device.movie.title_location + return None + + @property + def media_position_updated_at(self) -> datetime | None: + """When was the position of the current playing media valid.""" + if self._device.movie.play_status in KALEIDESCAPE_PLAYING_STATES: + return utcnow() + return None + + @property + def media_image_url(self) -> str: + """Image url of current playing media.""" + return self._device.movie.cover + + @property + def media_title(self) -> str: + """Title of current playing media.""" + return self._device.movie.title diff --git a/homeassistant/components/kaleidescape/remote.py b/homeassistant/components/kaleidescape/remote.py new file mode 100644 index 00000000000..61080052ee5 --- /dev/null +++ b/homeassistant/components/kaleidescape/remote.py @@ -0,0 +1,68 @@ +"""Sensor platform for Kaleidescape integration.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from kaleidescape import const as kaleidescape_const + +from homeassistant.components.remote import RemoteEntity +from homeassistant.exceptions import HomeAssistantError + +from .const import DOMAIN as KALEIDESCAPE_DOMAIN +from .entity import KaleidescapeEntity + +if TYPE_CHECKING: + from collections.abc import Iterable + from typing import Any + + from homeassistant.config_entries import ConfigEntry + from homeassistant.core import HomeAssistant + from homeassistant.helpers.entity_platform import AddEntitiesCallback + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up the platform from a config entry.""" + entities = [KaleidescapeRemote(hass.data[KALEIDESCAPE_DOMAIN][entry.entry_id])] + async_add_entities(entities) + + +VALID_COMMANDS = { + "select", + "up", + "down", + "left", + "right", + "cancel", + "replay", + "scan_forward", + "scan_reverse", + "go_movie_covers", + "menu_toggle", +} + + +class KaleidescapeRemote(KaleidescapeEntity, RemoteEntity): + """Representation of a Kaleidescape device.""" + + @property + def is_on(self) -> bool: + """Return true if device is on.""" + return self._device.power.state == kaleidescape_const.DEVICE_POWER_STATE_ON + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the device on.""" + await self._device.leave_standby() + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the device off.""" + await self._device.enter_standby() + + async def async_send_command(self, command: Iterable[str], **kwargs: Any) -> None: + """Send a command to a device.""" + for cmd in command: + if cmd not in VALID_COMMANDS: + raise HomeAssistantError(f"{cmd} is not a known command") + await getattr(self._device, cmd)() diff --git a/homeassistant/components/kaleidescape/sensor.py b/homeassistant/components/kaleidescape/sensor.py new file mode 100644 index 00000000000..468b7ced0e8 --- /dev/null +++ b/homeassistant/components/kaleidescape/sensor.py @@ -0,0 +1,186 @@ +"""Sensor platform for Kaleidescape integration.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import TYPE_CHECKING + +from homeassistant.components.sensor import SensorEntity, SensorEntityDescription +from homeassistant.const import PERCENTAGE +from homeassistant.helpers.entity import EntityCategory + +from .const import DOMAIN as KALEIDESCAPE_DOMAIN +from .entity import KaleidescapeEntity + +if TYPE_CHECKING: + from collections.abc import Callable + + from kaleidescape import Device as KaleidescapeDevice + + from homeassistant.config_entries import ConfigEntry + from homeassistant.core import HomeAssistant + from homeassistant.helpers.entity_platform import AddEntitiesCallback + from homeassistant.helpers.typing import StateType + + +@dataclass +class BaseEntityDescriptionMixin: + """Mixin for required descriptor keys.""" + + value_fn: Callable[[KaleidescapeDevice], StateType] + + +@dataclass +class KaleidescapeSensorEntityDescription( + SensorEntityDescription, BaseEntityDescriptionMixin +): + """Describes Kaleidescape sensor entity.""" + + +SENSOR_TYPES: tuple[KaleidescapeSensorEntityDescription, ...] = ( + KaleidescapeSensorEntityDescription( + key="media_location", + name="Media Location", + icon="mdi:monitor", + value_fn=lambda device: device.automation.movie_location, + ), + KaleidescapeSensorEntityDescription( + key="play_status", + name="Play Status", + icon="mdi:monitor", + value_fn=lambda device: device.movie.play_status, + ), + KaleidescapeSensorEntityDescription( + key="play_speed", + name="Play Speed", + icon="mdi:monitor", + value_fn=lambda device: device.movie.play_speed, + ), + KaleidescapeSensorEntityDescription( + key="video_mode", + name="Video Mode", + icon="mdi:monitor-screenshot", + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda device: device.automation.video_mode, + ), + KaleidescapeSensorEntityDescription( + key="video_color_eotf", + name="Video Color EOTF", + icon="mdi:monitor-eye", + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda device: device.automation.video_color_eotf, + ), + KaleidescapeSensorEntityDescription( + key="video_color_space", + name="Video Color Space", + icon="mdi:monitor-eye", + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda device: device.automation.video_color_space, + ), + KaleidescapeSensorEntityDescription( + key="video_color_depth", + name="Video Color Depth", + icon="mdi:monitor-eye", + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda device: device.automation.video_color_depth, + ), + KaleidescapeSensorEntityDescription( + key="video_color_sampling", + name="Video Color Sampling", + icon="mdi:monitor-eye", + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda device: device.automation.video_color_sampling, + ), + KaleidescapeSensorEntityDescription( + key="screen_mask_ratio", + name="Screen Mask Ratio", + icon="mdi:monitor-screenshot", + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda device: device.automation.screen_mask_ratio, + ), + KaleidescapeSensorEntityDescription( + key="screen_mask_top_trim_rel", + name="Screen Mask Top Trim Rel", + icon="mdi:monitor-screenshot", + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=PERCENTAGE, + value_fn=lambda device: device.automation.screen_mask_top_trim_rel / 10.0, + ), + KaleidescapeSensorEntityDescription( + key="screen_mask_bottom_trim_rel", + name="Screen Mask Bottom Trim Rel", + icon="mdi:monitor-screenshot", + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=PERCENTAGE, + value_fn=lambda device: device.automation.screen_mask_bottom_trim_rel / 10.0, + ), + KaleidescapeSensorEntityDescription( + key="screen_mask_conservative_ratio", + name="Screen Mask Conservative Ratio", + icon="mdi:monitor-screenshot", + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda device: device.automation.screen_mask_conservative_ratio, + ), + KaleidescapeSensorEntityDescription( + key="screen_mask_top_mask_abs", + name="Screen Mask Top Mask Abs", + icon="mdi:monitor-screenshot", + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=PERCENTAGE, + value_fn=lambda device: device.automation.screen_mask_top_mask_abs / 10.0, + ), + KaleidescapeSensorEntityDescription( + key="screen_mask_bottom_mask_abs", + name="Screen Mask Bottom Mask Abs", + icon="mdi:monitor-screenshot", + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=PERCENTAGE, + value_fn=lambda device: device.automation.screen_mask_bottom_mask_abs / 10.0, + ), + KaleidescapeSensorEntityDescription( + key="cinemascape_mask", + name="Cinemascape Mask", + icon="mdi:monitor-star", + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda device: device.automation.cinemascape_mask, + ), + KaleidescapeSensorEntityDescription( + key="cinemascape_mode", + name="Cinemascape Mode", + icon="mdi:monitor-star", + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda device: device.automation.cinemascape_mode, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up the platform from a config entry.""" + device: KaleidescapeDevice = hass.data[KALEIDESCAPE_DOMAIN][entry.entry_id] + async_add_entities( + KaleidescapeSensor(device, description) for description in SENSOR_TYPES + ) + + +class KaleidescapeSensor(KaleidescapeEntity, SensorEntity): + """Representation of a Kaleidescape sensor.""" + + entity_description: KaleidescapeSensorEntityDescription + + def __init__( + self, + device: KaleidescapeDevice, + entity_description: KaleidescapeSensorEntityDescription, + ) -> None: + """Initialize sensor.""" + super().__init__(device) + self.entity_description = entity_description + self._attr_unique_id = f"{self._attr_unique_id}-{entity_description.key}" + self._attr_name = f"{self._attr_name} {entity_description.name}" + + @property + def native_value(self) -> StateType: + """Return value of sensor.""" + return self.entity_description.value_fn(self._device) diff --git a/homeassistant/components/kaleidescape/strings.json b/homeassistant/components/kaleidescape/strings.json new file mode 100644 index 00000000000..92b9c931acd --- /dev/null +++ b/homeassistant/components/kaleidescape/strings.json @@ -0,0 +1,25 @@ +{ + "config": { + "flow_title": "{model} ({name})", + "step": { + "user": { + "data": { + "host": "[%key:common::config_flow::data::host%]" + } + }, + "discovery_confirm": { + "description": "Do you want to set up the {model} player named {name}?" + } + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", + "unknown": "[%key:common::config_flow::error::unknown%]", + "unsupported": "Unsupported device" + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "unsupported": "Unsupported device" + } + } +} diff --git a/homeassistant/components/kaleidescape/translations/bg.json b/homeassistant/components/kaleidescape/translations/bg.json new file mode 100644 index 00000000000..cb36ec53c4b --- /dev/null +++ b/homeassistant/components/kaleidescape/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", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430", + "unsupported": "\u041d\u0435\u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430\u043d\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "unsupported": "\u041d\u0435\u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430\u043d\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" + }, + "flow_title": "{model} ({name})", + "step": { + "discovery_confirm": { + "title": "Kaleidescape" + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + }, + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043d\u0430 Kaleidescape" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kaleidescape/translations/ca.json b/homeassistant/components/kaleidescape/translations/ca.json new file mode 100644 index 00000000000..ec7eac6ef4f --- /dev/null +++ b/homeassistant/components/kaleidescape/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", + "unknown": "Error inesperat", + "unsupported": "Dispositiu no compatible" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "unsupported": "Dispositiu no compatible" + }, + "flow_title": "{model} ({name})", + "step": { + "discovery_confirm": { + "description": "Vols configurar el reproductor {name} model {model}?", + "title": "Kaleidescape" + }, + "user": { + "data": { + "host": "Amfitri\u00f3" + }, + "title": "Configuraci\u00f3 de Kaleidescape" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kaleidescape/translations/de.json b/homeassistant/components/kaleidescape/translations/de.json new file mode 100644 index 00000000000..3b353208a64 --- /dev/null +++ b/homeassistant/components/kaleidescape/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", + "unknown": "Unerwarteter Fehler", + "unsupported": "Nicht unterst\u00fctztes Ger\u00e4t" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "unsupported": "Nicht unterst\u00fctztes Ger\u00e4t" + }, + "flow_title": "{model} ({name})", + "step": { + "discovery_confirm": { + "description": "M\u00f6chtest du den Player {model} mit dem Namen {name} einrichten?", + "title": "Kaleidescape" + }, + "user": { + "data": { + "host": "Host" + }, + "title": "Kaleidescape-Setup" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kaleidescape/translations/el.json b/homeassistant/components/kaleidescape/translations/el.json new file mode 100644 index 00000000000..dc042655b02 --- /dev/null +++ b/homeassistant/components/kaleidescape/translations/el.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", + "already_in_progress": "\u0397 \u03c1\u03bf\u03ae \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7\u03c2 \u03b2\u03c1\u03af\u03c3\u03ba\u03b5\u03c4\u03b1\u03b9 \u03ae\u03b4\u03b7 \u03c3\u03b5 \u03b5\u03be\u03ad\u03bb\u03b9\u03be\u03b7", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1", + "unsupported": "\u039c\u03b7 \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03b9\u03b6\u03cc\u03bc\u03b5\u03bd\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae" + }, + "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "unsupported": "\u039c\u03b7 \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03b9\u03b6\u03cc\u03bc\u03b5\u03bd\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae" + }, + "flow_title": "{model} ({name})", + "step": { + "discovery_confirm": { + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf \u03c0\u03c1\u03cc\u03b3\u03c1\u03b1\u03bc\u03bc\u03b1 \u03b1\u03bd\u03b1\u03c0\u03b1\u03c1\u03b1\u03b3\u03c9\u03b3\u03ae\u03c2 {model} \u03bc\u03b5 \u03c4\u03bf \u03cc\u03bd\u03bf\u03bc\u03b1{name};", + "title": "Kaleidescape" + }, + "user": { + "data": { + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2" + }, + "title": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 Kaleidescape" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kaleidescape/translations/en.json b/homeassistant/components/kaleidescape/translations/en.json new file mode 100644 index 00000000000..43be9c030c0 --- /dev/null +++ b/homeassistant/components/kaleidescape/translations/en.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured", + "already_in_progress": "Configuration flow is already in progress", + "unknown": "Unexpected error", + "unsupported": "Unsupported device" + }, + "error": { + "cannot_connect": "Failed to connect", + "unsupported": "Unsupported device" + }, + "flow_title": "{model} ({name})", + "step": { + "discovery_confirm": { + "description": "Do you want to set up the {model} player named {name}?", + "title": "Kaleidescape" + }, + "user": { + "data": { + "host": "Host" + }, + "title": "Kaleidescape Setup" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kaleidescape/translations/et.json b/homeassistant/components/kaleidescape/translations/et.json new file mode 100644 index 00000000000..52ece37ad97 --- /dev/null +++ b/homeassistant/components/kaleidescape/translations/et.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "already_in_progress": "H\u00e4\u00e4lestamine juba k\u00e4ib", + "unknown": "Ootamatu t\u00f5rge", + "unsupported": "Seadet ei toetata" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "unsupported": "Seadet ei toetata" + }, + "flow_title": "{model} ({name})", + "step": { + "discovery_confirm": { + "description": "Kas seadistada m\u00e4ngijat {model} nimega {name} ?", + "title": "Kaleidescape" + }, + "user": { + "data": { + "host": "Host" + }, + "title": "Kaleidescape'i seadistamine" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kaleidescape/translations/fr.json b/homeassistant/components/kaleidescape/translations/fr.json new file mode 100644 index 00000000000..48dfd027765 --- /dev/null +++ b/homeassistant/components/kaleidescape/translations/fr.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours", + "unknown": "Erreur inattendue", + "unsupported": "Appareil non pris en charge" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "unsupported": "Appareil non pris en charge" + }, + "flow_title": "{model} ({name})", + "step": { + "discovery_confirm": { + "description": "Voulez-vous configurer le lecteur {model} nomm\u00e9 {name}\u00a0?", + "title": "Kaleidescape" + }, + "user": { + "data": { + "host": "H\u00f4te" + }, + "title": "Configuration de Kaleidescape" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kaleidescape/translations/he.json b/homeassistant/components/kaleidescape/translations/he.json new file mode 100644 index 00000000000..79d265193d5 --- /dev/null +++ b/homeassistant/components/kaleidescape/translations/he.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + }, + "flow_title": "{model} ({name})", + "step": { + "user": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kaleidescape/translations/hu.json b/homeassistant/components/kaleidescape/translations/hu.json new file mode 100644 index 00000000000..953de8119ed --- /dev/null +++ b/homeassistant/components/kaleidescape/translations/hu.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt", + "unsupported": "Nem t\u00e1mogatott eszk\u00f6z" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "unsupported": "Nem t\u00e1mogatott eszk\u00f6z" + }, + "flow_title": "{model} ({name})", + "step": { + "discovery_confirm": { + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani a {name} nev\u0171, {model} t\u00edpus\u00fa lej\u00e1tsz\u00f3t?", + "title": "Kaleidescape" + }, + "user": { + "data": { + "host": "C\u00edm" + }, + "title": "Kaleidescape be\u00e1ll\u00edt\u00e1sa" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kaleidescape/translations/id.json b/homeassistant/components/kaleidescape/translations/id.json new file mode 100644 index 00000000000..626f23354d4 --- /dev/null +++ b/homeassistant/components/kaleidescape/translations/id.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "already_in_progress": "Alur konfigurasi sedang berlangsung", + "unknown": "Kesalahan yang tidak diharapkan", + "unsupported": "Perangkat tidak didukung" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "unsupported": "Perangkat tidak didukung" + }, + "flow_title": "{model} ({name})", + "step": { + "discovery_confirm": { + "description": "Ingin menyiapkan pemutar {model} dengan nama {name}?", + "title": "Kaleidescape" + }, + "user": { + "data": { + "host": "Host" + }, + "title": "Penyiapan Kaleidescape" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kaleidescape/translations/it.json b/homeassistant/components/kaleidescape/translations/it.json new file mode 100644 index 00000000000..022b73ae0d7 --- /dev/null +++ b/homeassistant/components/kaleidescape/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", + "unknown": "Errore imprevisto", + "unsupported": "Dispositivo non supportato" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "unsupported": "Dispositivo non supportato" + }, + "flow_title": "{model} ({name})", + "step": { + "discovery_confirm": { + "description": "Vuoi configurare il lettore {model} nome {name}?", + "title": "Kaleidescape" + }, + "user": { + "data": { + "host": "Host" + }, + "title": "Configurazione di Kaleidescape" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kaleidescape/translations/ja.json b/homeassistant/components/kaleidescape/translations/ja.json new file mode 100644 index 00000000000..368a1565648 --- /dev/null +++ b/homeassistant/components/kaleidescape/translations/ja.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc", + "unsupported": "\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u306a\u3044\u30c7\u30d0\u30a4\u30b9" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", + "unsupported": "\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u306a\u3044\u30c7\u30d0\u30a4\u30b9" + }, + "flow_title": "{model} ({name})", + "step": { + "discovery_confirm": { + "description": "{model} \u30d7\u30ec\u30a4\u30e4\u30fc\u540d {name} \u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f", + "title": "\u30ab\u30ec\u30a4\u30c9\u30b9\u30b1\u30fc\u30d7(Kaleidescape)" + }, + "user": { + "data": { + "host": "\u30db\u30b9\u30c8" + }, + "title": "\u30ab\u30ec\u30a4\u30c9\u30b9\u30b1\u30fc\u30d7(Kaleidescape)\u306e\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kaleidescape/translations/nl.json b/homeassistant/components/kaleidescape/translations/nl.json new file mode 100644 index 00000000000..2b2491deb85 --- /dev/null +++ b/homeassistant/components/kaleidescape/translations/nl.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd", + "already_in_progress": "Configuratiestroom is al bezig", + "unknown": "Onverwachte fout", + "unsupported": "Niet-ondersteund apparaat" + }, + "error": { + "cannot_connect": "Verbinding mislukt", + "unsupported": "Niet-ondersteund apparaat" + }, + "flow_title": "{model} ({name})", + "step": { + "discovery_confirm": { + "description": "Wil je de speler {model} met de naam {name} instellen?", + "title": "Kaleidescape" + }, + "user": { + "data": { + "host": "Host" + }, + "title": "Kaleidescape configuratie" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kaleidescape/translations/no.json b/homeassistant/components/kaleidescape/translations/no.json new file mode 100644 index 00000000000..c07276eeeb1 --- /dev/null +++ b/homeassistant/components/kaleidescape/translations/no.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert", + "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", + "unknown": "Uventet feil", + "unsupported": "Enhet som ikke st\u00f8ttes" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "unsupported": "Enhet som ikke st\u00f8ttes" + }, + "flow_title": "{model} ( {name} )", + "step": { + "discovery_confirm": { + "description": "Vil du sette opp {model} -spilleren med navnet {name} ?", + "title": "Kaleidescape" + }, + "user": { + "data": { + "host": "Vert" + }, + "title": "Kaleidescape-oppsett" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kaleidescape/translations/pl.json b/homeassistant/components/kaleidescape/translations/pl.json new file mode 100644 index 00000000000..111dc4db7e6 --- /dev/null +++ b/homeassistant/components/kaleidescape/translations/pl.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "already_in_progress": "Konfiguracja jest ju\u017c w toku", + "unknown": "Nieoczekiwany b\u0142\u0105d", + "unsupported": "Nieobs\u0142ugiwane urz\u0105dzenie" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "unsupported": "Nieobs\u0142ugiwane urz\u0105dzenie" + }, + "flow_title": "{model} ({name})", + "step": { + "discovery_confirm": { + "description": "Czy chcesz skonfigurowa\u0107 odtwarzacz {model} o nazwie {name}?", + "title": "Kaleidescape" + }, + "user": { + "data": { + "host": "Nazwa hosta lub adres IP" + }, + "title": "Konfiguracja Kaleidescape" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kaleidescape/translations/pt-BR.json b/homeassistant/components/kaleidescape/translations/pt-BR.json new file mode 100644 index 00000000000..f87534cb88a --- /dev/null +++ b/homeassistant/components/kaleidescape/translations/pt-BR.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "O fluxo de configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", + "unknown": "Erro inesperado", + "unsupported": "Dispositivo n\u00e3o compat\u00edvel" + }, + "error": { + "cannot_connect": "Falha ao conectar", + "unsupported": "Dispositivo n\u00e3o compat\u00edvel" + }, + "flow_title": "{model} ({name})", + "step": { + "discovery_confirm": { + "description": "Deseja configurar o player {model} chamado {name}?", + "title": "Kaleidescape" + }, + "user": { + "data": { + "host": "Nome do host" + }, + "title": "Configura\u00e7\u00e3o do Kaleidescape" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kaleidescape/translations/ru.json b/homeassistant/components/kaleidescape/translations/ru.json new file mode 100644 index 00000000000..3112fdd06e5 --- /dev/null +++ b/homeassistant/components/kaleidescape/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.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430.", + "unsupported": "\u041d\u0435\u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u043e\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\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.", + "unsupported": "\u041d\u0435\u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u043e\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e." + }, + "flow_title": "{model} ({name})", + "step": { + "discovery_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 {model} ({name})?", + "title": "Kaleidescape" + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + }, + "title": "Kaleidescape" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kaleidescape/translations/sv.json b/homeassistant/components/kaleidescape/translations/sv.json new file mode 100644 index 00000000000..c5cfbbb662a --- /dev/null +++ b/homeassistant/components/kaleidescape/translations/sv.json @@ -0,0 +1,5 @@ +{ + "config": { + "flow_title": "{model} ({name})" + } +} \ No newline at end of file diff --git a/homeassistant/components/kaleidescape/translations/tr.json b/homeassistant/components/kaleidescape/translations/tr.json new file mode 100644 index 00000000000..1b891cc450c --- /dev/null +++ b/homeassistant/components/kaleidescape/translations/tr.json @@ -0,0 +1,27 @@ +{ + "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", + "unknown": "Beklenmeyen hata", + "unsupported": "Desteklenmeyen cihaz" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "unsupported": "Desteklenmeyen cihaz" + }, + "flow_title": "{model} ({name})", + "step": { + "discovery_confirm": { + "description": "{name} adl\u0131 {model} oynat\u0131c\u0131s\u0131n\u0131 kurmak istiyor musunuz?", + "title": "Kaleidescape" + }, + "user": { + "data": { + "host": "Sunucu" + }, + "title": "Kaleidescape Kurulumu" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kaleidescape/translations/zh-Hant.json b/homeassistant/components/kaleidescape/translations/zh-Hant.json new file mode 100644 index 00000000000..2fedf1ede20 --- /dev/null +++ b/homeassistant/components/kaleidescape/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", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4", + "unsupported": "\u4e0d\u652f\u63f4\u7684\u88dd\u7f6e" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "unsupported": "\u4e0d\u652f\u63f4\u7684\u88dd\u7f6e" + }, + "flow_title": "{model} ({name})", + "step": { + "discovery_confirm": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a\u540d\u70ba {name} \u7684 {model} \u64ad\u653e\u5668\uff1f", + "title": "Kaleidescape" + }, + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef" + }, + "title": "Kaleidescape \u8a2d\u5b9a" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kef/services.yaml b/homeassistant/components/kef/services.yaml index 291a8f45cdb..cf364edcf21 100644 --- a/homeassistant/components/kef/services.yaml +++ b/homeassistant/components/kef/services.yaml @@ -41,17 +41,17 @@ set_mode: selector: select: options: - - '-' - - '+' + - "-" + - "+" bass_extension: name: Base extension description: Bass extension. selector: select: options: - - 'Less' - - 'Standard' - - 'Extra' + - "Less" + - "Standard" + - "Extra" set_desk_db: name: Set desk dB diff --git a/homeassistant/components/kmtronic/strings.json b/homeassistant/components/kmtronic/strings.json index 2aaa0d2f8dd..2a3a3a40687 100644 --- a/homeassistant/components/kmtronic/strings.json +++ b/homeassistant/components/kmtronic/strings.json @@ -1,30 +1,30 @@ { - "config": { - "step": { - "user": { - "data": { - "host": "[%key:common::config_flow::data::host%]", - "username": "[%key:common::config_flow::data::username%]", - "password": "[%key:common::config_flow::data::password%]" - } - } - }, - "error": { - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", - "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", - "unknown": "[%key:common::config_flow::error::unknown%]" - }, - "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + "config": { + "step": { + "user": { + "data": { + "host": "[%key:common::config_flow::data::host%]", + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" } + } }, - "options": { - "step": { - "init": { - "data": { - "reverse": "Reverse switch logic (use NC)" - } - } - } + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } + }, + "options": { + "step": { + "init": { + "data": { + "reverse": "Reverse switch logic (use NC)" + } + } + } + } } diff --git a/homeassistant/components/kmtronic/translations/fr.json b/homeassistant/components/kmtronic/translations/fr.json index 7c0050bfad8..2cddd25f517 100644 --- a/homeassistant/components/kmtronic/translations/fr.json +++ b/homeassistant/components/kmtronic/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/knx/__init__.py b/homeassistant/components/knx/__init__.py index 5c2d7e3b68c..1910227a5a4 100644 --- a/homeassistant/components/knx/__init__.py +++ b/homeassistant/components/knx/__init__.py @@ -11,7 +11,7 @@ from xknx.core import XknxConnectionState from xknx.core.telegram_queue import TelegramQueue from xknx.dpt import DPTArray, DPTBase, DPTBinary from xknx.exceptions import ConversionError, XKNXException -from xknx.io import ConnectionConfig, ConnectionType +from xknx.io import ConnectionConfig, ConnectionType, SecureConfig from xknx.telegram import AddressFilter, Telegram from xknx.telegram.address import ( DeviceGroupAddress, @@ -21,7 +21,6 @@ from xknx.telegram.address import ( ) from xknx.telegram.apci import GroupValueRead, GroupValueResponse, GroupValueWrite -from homeassistant import config_entries from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_EVENT, @@ -37,15 +36,28 @@ from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv from homeassistant.helpers.reload import async_integration_yaml_config from homeassistant.helpers.service import async_register_admin_service +from homeassistant.helpers.storage import STORAGE_DIR from homeassistant.helpers.typing import ConfigType from .const import ( CONF_KNX_CONNECTION_TYPE, CONF_KNX_EXPOSE, CONF_KNX_INDIVIDUAL_ADDRESS, + CONF_KNX_KNXKEY_FILENAME, + CONF_KNX_KNXKEY_PASSWORD, + CONF_KNX_LOCAL_IP, + CONF_KNX_MCAST_GRP, + CONF_KNX_MCAST_PORT, + CONF_KNX_RATE_LIMIT, + CONF_KNX_ROUTE_BACK, CONF_KNX_ROUTING, + CONF_KNX_SECURE_DEVICE_AUTHENTICATION, + CONF_KNX_SECURE_USER_ID, + CONF_KNX_SECURE_USER_PASSWORD, + CONF_KNX_STATE_UPDATER, CONF_KNX_TUNNELING, CONF_KNX_TUNNELING_TCP, + CONF_KNX_TUNNELING_TCP_SECURE, DATA_HASS_CONFIG, DATA_KNX_CONFIG, DOMAIN, @@ -57,7 +69,6 @@ from .schema import ( BinarySensorSchema, ButtonSchema, ClimateSchema, - ConnectionSchema, CoverSchema, EventSchema, ExposeSchema, @@ -77,9 +88,6 @@ from .schema import ( _LOGGER = logging.getLogger(__name__) -CONF_KNX_FIRE_EVENT: Final = "fire_event" -CONF_KNX_EVENT_FILTER: Final = "event_filter" - SERVICE_KNX_SEND: Final = "send" SERVICE_KNX_ATTR_PAYLOAD: Final = "payload" SERVICE_KNX_ATTR_TYPE: Final = "type" @@ -93,26 +101,21 @@ CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.All( # deprecated since 2021.12 - cv.deprecated(ConnectionSchema.CONF_KNX_STATE_UPDATER), - cv.deprecated(ConnectionSchema.CONF_KNX_RATE_LIMIT), + cv.deprecated(CONF_KNX_STATE_UPDATER), + cv.deprecated(CONF_KNX_RATE_LIMIT), cv.deprecated(CONF_KNX_ROUTING), cv.deprecated(CONF_KNX_TUNNELING), cv.deprecated(CONF_KNX_INDIVIDUAL_ADDRESS), - cv.deprecated(ConnectionSchema.CONF_KNX_MCAST_GRP), - cv.deprecated(ConnectionSchema.CONF_KNX_MCAST_PORT), - cv.deprecated(CONF_KNX_EVENT_FILTER), + cv.deprecated(CONF_KNX_MCAST_GRP), + cv.deprecated(CONF_KNX_MCAST_PORT), + cv.deprecated("event_filter"), # deprecated since 2021.4 cv.deprecated("config_file"), # deprecated since 2021.2 - cv.deprecated(CONF_KNX_FIRE_EVENT), - cv.deprecated("fire_event_filter", replacement_key=CONF_KNX_EVENT_FILTER), + cv.deprecated("fire_event"), + cv.deprecated("fire_event_filter"), vol.Schema( { - **ConnectionSchema.SCHEMA, - vol.Optional(CONF_KNX_FIRE_EVENT): cv.boolean, - vol.Optional(CONF_KNX_EVENT_FILTER, default=[]): vol.All( - cv.ensure_list, [cv.string] - ), **EventSchema.SCHEMA, **ExposeSchema.platform_node(), **BinarySensorSchema.platform_node(), @@ -211,22 +214,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: conf = dict(conf) hass.data[DATA_KNX_CONFIG] = conf - - # Only import if we haven't before. - 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=conf - ) - ) - return True async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Load a config entry.""" - # `conf` is None when reloading the integration or no `knx` key in configuration.yaml - if (conf := hass.data.get(DATA_KNX_CONFIG)) is None: + # `config` is None when reloading the integration or no `knx` key in configuration.yaml + if (config := hass.data.get(DATA_KNX_CONFIG)) is None: _conf = await async_integration_yaml_config(hass, DOMAIN) if not _conf or DOMAIN not in _conf: _LOGGER.warning( @@ -235,18 +229,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: "for KNX entity configuration documentation" ) # generate defaults - conf = CONFIG_SCHEMA({DOMAIN: {}})[DOMAIN] + config = CONFIG_SCHEMA({DOMAIN: {}})[DOMAIN] else: - conf = _conf[DOMAIN] - config = {**conf, **entry.data} - + config = _conf[DOMAIN] try: knx_module = KNXModule(hass, config, entry) await knx_module.start() except XKNXException as ex: raise ConfigEntryNotReady from ex - hass.data[DATA_KNX_CONFIG] = conf + hass.data[DATA_KNX_CONFIG] = config hass.data[DOMAIN] = knx_module if CONF_KNX_EXPOSE in config: @@ -370,12 +362,12 @@ class KNXModule: def init_xknx(self) -> None: """Initialize XKNX object.""" self.xknx = XKNX( - own_address=self.config[CONF_KNX_INDIVIDUAL_ADDRESS], - rate_limit=self.config[ConnectionSchema.CONF_KNX_RATE_LIMIT], - multicast_group=self.config[ConnectionSchema.CONF_KNX_MCAST_GRP], - multicast_port=self.config[ConnectionSchema.CONF_KNX_MCAST_PORT], + own_address=self.entry.data[CONF_KNX_INDIVIDUAL_ADDRESS], + rate_limit=self.entry.data[CONF_KNX_RATE_LIMIT], + multicast_group=self.entry.data[CONF_KNX_MCAST_GRP], + multicast_port=self.entry.data[CONF_KNX_MCAST_PORT], connection_config=self.connection_config(), - state_updater=self.config[ConnectionSchema.CONF_KNX_STATE_UPDATER], + state_updater=self.entry.data[CONF_KNX_STATE_UPDATER], ) async def start(self) -> None: @@ -388,30 +380,61 @@ class KNXModule: def connection_config(self) -> ConnectionConfig: """Return the connection_config.""" - _conn_type: str = self.config[CONF_KNX_CONNECTION_TYPE] + _conn_type: str = self.entry.data[CONF_KNX_CONNECTION_TYPE] if _conn_type == CONF_KNX_ROUTING: return ConnectionConfig( connection_type=ConnectionType.ROUTING, - local_ip=self.config.get(ConnectionSchema.CONF_KNX_LOCAL_IP), + local_ip=self.entry.data.get(CONF_KNX_LOCAL_IP), auto_reconnect=True, + threaded=True, ) if _conn_type == CONF_KNX_TUNNELING: return ConnectionConfig( connection_type=ConnectionType.TUNNELING, - gateway_ip=self.config[CONF_HOST], - gateway_port=self.config[CONF_PORT], - local_ip=self.config.get(ConnectionSchema.CONF_KNX_LOCAL_IP), - route_back=self.config.get(ConnectionSchema.CONF_KNX_ROUTE_BACK, False), + gateway_ip=self.entry.data[CONF_HOST], + gateway_port=self.entry.data[CONF_PORT], + local_ip=self.entry.data.get(CONF_KNX_LOCAL_IP), + route_back=self.entry.data.get(CONF_KNX_ROUTE_BACK, False), auto_reconnect=True, + threaded=True, ) if _conn_type == CONF_KNX_TUNNELING_TCP: return ConnectionConfig( connection_type=ConnectionType.TUNNELING_TCP, - gateway_ip=self.config[CONF_HOST], - gateway_port=self.config[CONF_PORT], + gateway_ip=self.entry.data[CONF_HOST], + gateway_port=self.entry.data[CONF_PORT], auto_reconnect=True, + threaded=True, ) - return ConnectionConfig(auto_reconnect=True) + if _conn_type == CONF_KNX_TUNNELING_TCP_SECURE: + knxkeys_file: str | None = ( + self.hass.config.path( + STORAGE_DIR, + self.entry.data[CONF_KNX_KNXKEY_FILENAME], + ) + if self.entry.data.get(CONF_KNX_KNXKEY_FILENAME) is not None + else None + ) + return ConnectionConfig( + connection_type=ConnectionType.TUNNELING_TCP_SECURE, + gateway_ip=self.entry.data[CONF_HOST], + gateway_port=self.entry.data[CONF_PORT], + secure_config=SecureConfig( + user_id=self.entry.data.get(CONF_KNX_SECURE_USER_ID), + user_password=self.entry.data.get(CONF_KNX_SECURE_USER_PASSWORD), + device_authentication_password=self.entry.data.get( + CONF_KNX_SECURE_DEVICE_AUTHENTICATION + ), + knxkeys_password=self.entry.data.get(CONF_KNX_KNXKEY_PASSWORD), + knxkeys_file_path=knxkeys_file, + ), + auto_reconnect=True, + threaded=True, + ) + return ConnectionConfig( + auto_reconnect=True, + threaded=True, + ) async def connection_state_changed_cb(self, state: XknxConnectionState) -> None: """Call invoked after a KNX connection state change was received.""" @@ -469,9 +492,7 @@ class KNXModule: def register_event_callback(self) -> TelegramQueue.Callback: """Register callback for knx_event within XKNX TelegramQueue.""" - # backwards compatibility for deprecated CONF_KNX_EVENT_FILTER - # use `address_filters = []` when this is not needed anymore - address_filters = list(map(AddressFilter, self.config[CONF_KNX_EVENT_FILTER])) + address_filters = [] for filter_set in self.config[CONF_EVENT]: _filters = list(map(AddressFilter, filter_set[KNX_ADDRESS])) address_filters.extend(_filters) diff --git a/homeassistant/components/knx/config_flow.py b/homeassistant/components/knx/config_flow.py index 6bc6085d0e5..e45eb3a87a1 100644 --- a/homeassistant/components/knx/config_flow.py +++ b/homeassistant/components/knx/config_flow.py @@ -5,45 +5,71 @@ from typing import Any, Final import voluptuous as vol from xknx import XKNX +from xknx.exceptions.exception import InvalidSignature from xknx.io import DEFAULT_MCAST_GRP, DEFAULT_MCAST_PORT from xknx.io.gateway_scanner import GatewayDescriptor, GatewayScanner +from xknx.secure import load_key_ring from homeassistant import config_entries from homeassistant.config_entries import ConfigEntry, OptionsFlow from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult -import homeassistant.helpers.config_validation as cv +from homeassistant.helpers import selector +from homeassistant.helpers.storage import STORAGE_DIR from .const import ( CONF_KNX_AUTOMATIC, CONF_KNX_CONNECTION_TYPE, + CONF_KNX_DEFAULT_RATE_LIMIT, + CONF_KNX_DEFAULT_STATE_UPDATER, CONF_KNX_INDIVIDUAL_ADDRESS, CONF_KNX_INITIAL_CONNECTION_TYPES, + CONF_KNX_KNXKEY_FILENAME, + CONF_KNX_KNXKEY_PASSWORD, + CONF_KNX_LOCAL_IP, + CONF_KNX_MCAST_GRP, + CONF_KNX_MCAST_PORT, + CONF_KNX_RATE_LIMIT, + CONF_KNX_ROUTE_BACK, CONF_KNX_ROUTING, + CONF_KNX_SECURE_DEVICE_AUTHENTICATION, + CONF_KNX_SECURE_USER_ID, + CONF_KNX_SECURE_USER_PASSWORD, + CONF_KNX_STATE_UPDATER, CONF_KNX_TUNNELING, CONF_KNX_TUNNELING_TCP, + CONF_KNX_TUNNELING_TCP_SECURE, + CONST_KNX_STORAGE_KEY, DOMAIN, + KNXConfigEntryData, ) -from .schema import ConnectionSchema CONF_KNX_GATEWAY: Final = "gateway" CONF_MAX_RATE_LIMIT: Final = 60 CONF_DEFAULT_LOCAL_IP: Final = "0.0.0.0" -DEFAULT_ENTRY_DATA: Final = { - ConnectionSchema.CONF_KNX_STATE_UPDATER: ConnectionSchema.CONF_KNX_DEFAULT_STATE_UPDATER, - ConnectionSchema.CONF_KNX_RATE_LIMIT: ConnectionSchema.CONF_KNX_DEFAULT_RATE_LIMIT, +DEFAULT_ENTRY_DATA: KNXConfigEntryData = { + CONF_KNX_STATE_UPDATER: CONF_KNX_DEFAULT_STATE_UPDATER, + CONF_KNX_RATE_LIMIT: CONF_KNX_DEFAULT_RATE_LIMIT, CONF_KNX_INDIVIDUAL_ADDRESS: XKNX.DEFAULT_ADDRESS, - ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, - ConnectionSchema.CONF_KNX_MCAST_PORT: DEFAULT_MCAST_PORT, + CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, + CONF_KNX_MCAST_PORT: DEFAULT_MCAST_PORT, } CONF_KNX_TUNNELING_TYPE: Final = "tunneling_type" CONF_KNX_LABEL_TUNNELING_TCP: Final = "TCP" +CONF_KNX_LABEL_TUNNELING_TCP_SECURE: Final = "TCP with IP Secure" CONF_KNX_LABEL_TUNNELING_UDP: Final = "UDP" CONF_KNX_LABEL_TUNNELING_UDP_ROUTE_BACK: Final = "UDP with route back / NAT mode" +_IA_SELECTOR = selector.selector({"text": {}}) +_IP_SELECTOR = selector.selector({"text": {}}) +_PORT_SELECTOR = vol.All( + selector.selector({"number": {"min": 1, "max": 65535, "mode": "box"}}), + vol.Coerce(int), +) + class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a KNX config flow.""" @@ -52,6 +78,7 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): _found_tunnels: list[GatewayDescriptor] _selected_tunnel: GatewayDescriptor | None + _tunneling_config: KNXConfigEntryData | None @staticmethod @callback @@ -66,6 +93,7 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self._found_tunnels = [] self._selected_tunnel = None + self._tunneling_config = None return await self.async_step_type() async def async_step_type(self, user_input: dict | None = None) -> FlowResult: @@ -73,9 +101,13 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if user_input is not None: connection_type = user_input[CONF_KNX_CONNECTION_TYPE] if connection_type == CONF_KNX_AUTOMATIC: + entry_data: KNXConfigEntryData = { + **DEFAULT_ENTRY_DATA, # type: ignore[misc] + CONF_KNX_CONNECTION_TYPE: user_input[CONF_KNX_CONNECTION_TYPE], + } return self.async_create_entry( title=CONF_KNX_AUTOMATIC.capitalize(), - data={**DEFAULT_ENTRY_DATA, **user_input}, + data=entry_data, ) if connection_type == CONF_KNX_ROUTING: @@ -88,7 +120,6 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors: dict = {} supported_connection_types = CONF_KNX_INITIAL_CONNECTION_TYPES.copy() - fields = {} gateways = await scan_for_gateways() if gateways: @@ -135,33 +166,39 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Manually configure tunnel connection parameters. Fields default to preselected gateway if one was found.""" if user_input is not None: connection_type = user_input[CONF_KNX_TUNNELING_TYPE] + + entry_data: KNXConfigEntryData = { + **DEFAULT_ENTRY_DATA, # type: ignore[misc] + CONF_HOST: user_input[CONF_HOST], + CONF_PORT: user_input[CONF_PORT], + CONF_KNX_ROUTE_BACK: ( + connection_type == CONF_KNX_LABEL_TUNNELING_UDP_ROUTE_BACK + ), + CONF_KNX_LOCAL_IP: user_input.get(CONF_KNX_LOCAL_IP), + CONF_KNX_CONNECTION_TYPE: ( + CONF_KNX_TUNNELING_TCP + if connection_type == CONF_KNX_LABEL_TUNNELING_TCP + else CONF_KNX_TUNNELING + ), + } + + if connection_type == CONF_KNX_LABEL_TUNNELING_TCP_SECURE: + self._tunneling_config = entry_data + return self.async_show_menu( + step_id="secure_tunneling", + menu_options=["secure_knxkeys", "secure_manual"], + ) + return self.async_create_entry( title=f"{CONF_KNX_TUNNELING.capitalize()} @ {user_input[CONF_HOST]}", - data={ - **DEFAULT_ENTRY_DATA, - CONF_HOST: user_input[CONF_HOST], - CONF_PORT: user_input[CONF_PORT], - CONF_KNX_INDIVIDUAL_ADDRESS: user_input[ - CONF_KNX_INDIVIDUAL_ADDRESS - ], - ConnectionSchema.CONF_KNX_ROUTE_BACK: ( - connection_type == CONF_KNX_LABEL_TUNNELING_UDP_ROUTE_BACK - ), - ConnectionSchema.CONF_KNX_LOCAL_IP: user_input.get( - ConnectionSchema.CONF_KNX_LOCAL_IP - ), - CONF_KNX_CONNECTION_TYPE: ( - CONF_KNX_TUNNELING_TCP - if connection_type == CONF_KNX_LABEL_TUNNELING_TCP - else CONF_KNX_TUNNELING - ), - }, + data=entry_data, ) errors: dict = {} connection_methods: list[str] = [ CONF_KNX_LABEL_TUNNELING_TCP, CONF_KNX_LABEL_TUNNELING_UDP, + CONF_KNX_LABEL_TUNNELING_TCP_SECURE, CONF_KNX_LABEL_TUNNELING_UDP_ROUTE_BACK, ] ip_address = "" @@ -171,23 +208,107 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): port = self._selected_tunnel.port if not self._selected_tunnel.supports_tunnelling_tcp: connection_methods.remove(CONF_KNX_LABEL_TUNNELING_TCP) + connection_methods.remove(CONF_KNX_LABEL_TUNNELING_TCP_SECURE) fields = { vol.Required(CONF_KNX_TUNNELING_TYPE): vol.In(connection_methods), - vol.Required(CONF_HOST, default=ip_address): str, - vol.Required(CONF_PORT, default=port): cv.port, - vol.Required( - CONF_KNX_INDIVIDUAL_ADDRESS, default=XKNX.DEFAULT_ADDRESS - ): str, + vol.Required(CONF_HOST, default=ip_address): _IP_SELECTOR, + vol.Required(CONF_PORT, default=port): _PORT_SELECTOR, } if self.show_advanced_options: - fields[vol.Optional(ConnectionSchema.CONF_KNX_LOCAL_IP)] = str + fields[vol.Optional(CONF_KNX_LOCAL_IP)] = _IP_SELECTOR return self.async_show_form( step_id="manual_tunnel", data_schema=vol.Schema(fields), errors=errors ) + async def async_step_secure_manual( + self, user_input: dict | None = None + ) -> FlowResult: + """Configure ip secure manually.""" + errors: dict = {} + + if user_input is not None: + assert self._tunneling_config + entry_data: KNXConfigEntryData = { + **self._tunneling_config, # type: ignore[misc] + CONF_KNX_SECURE_USER_ID: user_input[CONF_KNX_SECURE_USER_ID], + CONF_KNX_SECURE_USER_PASSWORD: user_input[ + CONF_KNX_SECURE_USER_PASSWORD + ], + CONF_KNX_SECURE_DEVICE_AUTHENTICATION: user_input[ + CONF_KNX_SECURE_DEVICE_AUTHENTICATION + ], + CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING_TCP_SECURE, + } + + return self.async_create_entry( + title=f"Secure {CONF_KNX_TUNNELING.capitalize()} @ {self._tunneling_config[CONF_HOST]}", + data=entry_data, + ) + + fields = { + vol.Required(CONF_KNX_SECURE_USER_ID, default=2): vol.All( + selector.selector({"number": {"min": 1, "max": 127, "mode": "box"}}), + vol.Coerce(int), + ), + vol.Required(CONF_KNX_SECURE_USER_PASSWORD): selector.selector( + {"text": {"type": "password"}} + ), + vol.Required(CONF_KNX_SECURE_DEVICE_AUTHENTICATION): selector.selector( + {"text": {"type": "password"}} + ), + } + + return self.async_show_form( + step_id="secure_manual", data_schema=vol.Schema(fields), errors=errors + ) + + async def async_step_secure_knxkeys( + self, user_input: dict | None = None + ) -> FlowResult: + """Configure secure knxkeys used to authenticate.""" + errors = {} + + if user_input is not None: + try: + assert self._tunneling_config + storage_key: str = ( + CONST_KNX_STORAGE_KEY + user_input[CONF_KNX_KNXKEY_FILENAME] + ) + load_key_ring( + self.hass.config.path( + STORAGE_DIR, + storage_key, + ), + user_input[CONF_KNX_KNXKEY_PASSWORD], + ) + entry_data: KNXConfigEntryData = { + **self._tunneling_config, # type: ignore[misc] + CONF_KNX_KNXKEY_FILENAME: storage_key, + CONF_KNX_KNXKEY_PASSWORD: user_input[CONF_KNX_KNXKEY_PASSWORD], + CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING_TCP_SECURE, + } + + return self.async_create_entry( + title=f"Secure {CONF_KNX_TUNNELING.capitalize()} @ {self._tunneling_config[CONF_HOST]}", + data=entry_data, + ) + except InvalidSignature: + errors["base"] = "invalid_signature" + except FileNotFoundError: + errors["base"] = "file_not_found" + + fields = { + vol.Required(CONF_KNX_KNXKEY_FILENAME): selector.selector({"text": {}}), + vol.Required(CONF_KNX_KNXKEY_PASSWORD): selector.selector({"text": {}}), + } + + return self.async_show_form( + step_id="secure_knxkeys", data_schema=vol.Schema(fields), errors=errors + ) + async def async_step_routing(self, user_input: dict | None = None) -> FlowResult: """Routing setup.""" if user_input is not None: @@ -195,18 +316,12 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): title=CONF_KNX_ROUTING.capitalize(), data={ **DEFAULT_ENTRY_DATA, - ConnectionSchema.CONF_KNX_MCAST_GRP: user_input[ - ConnectionSchema.CONF_KNX_MCAST_GRP - ], - ConnectionSchema.CONF_KNX_MCAST_PORT: user_input[ - ConnectionSchema.CONF_KNX_MCAST_PORT - ], + CONF_KNX_MCAST_GRP: user_input[CONF_KNX_MCAST_GRP], + CONF_KNX_MCAST_PORT: user_input[CONF_KNX_MCAST_PORT], CONF_KNX_INDIVIDUAL_ADDRESS: user_input[ CONF_KNX_INDIVIDUAL_ADDRESS ], - ConnectionSchema.CONF_KNX_LOCAL_IP: user_input.get( - ConnectionSchema.CONF_KNX_LOCAL_IP - ), + CONF_KNX_LOCAL_IP: user_input.get(CONF_KNX_LOCAL_IP), CONF_KNX_CONNECTION_TYPE: CONF_KNX_ROUTING, }, ) @@ -215,84 +330,20 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): fields = { vol.Required( CONF_KNX_INDIVIDUAL_ADDRESS, default=XKNX.DEFAULT_ADDRESS - ): str, + ): _IA_SELECTOR, + vol.Required(CONF_KNX_MCAST_GRP, default=DEFAULT_MCAST_GRP): _IP_SELECTOR, vol.Required( - ConnectionSchema.CONF_KNX_MCAST_GRP, default=DEFAULT_MCAST_GRP - ): str, - vol.Required( - ConnectionSchema.CONF_KNX_MCAST_PORT, default=DEFAULT_MCAST_PORT - ): cv.port, + CONF_KNX_MCAST_PORT, default=DEFAULT_MCAST_PORT + ): _PORT_SELECTOR, } if self.show_advanced_options: - fields[vol.Optional(ConnectionSchema.CONF_KNX_LOCAL_IP)] = str + fields[vol.Optional(CONF_KNX_LOCAL_IP)] = _IP_SELECTOR return self.async_show_form( step_id="routing", data_schema=vol.Schema(fields), errors=errors ) - async def async_step_import(self, config: dict | None = None) -> FlowResult: - """Import a config entry. - - Performs a one time import of the YAML configuration and creates a config entry based on it - if not already done before. - """ - if self._async_current_entries() or not config: - return self.async_abort(reason="single_instance_allowed") - - data = { - ConnectionSchema.CONF_KNX_RATE_LIMIT: min( - config[ConnectionSchema.CONF_KNX_RATE_LIMIT], CONF_MAX_RATE_LIMIT - ), - ConnectionSchema.CONF_KNX_STATE_UPDATER: config[ - ConnectionSchema.CONF_KNX_STATE_UPDATER - ], - ConnectionSchema.CONF_KNX_MCAST_GRP: config[ - ConnectionSchema.CONF_KNX_MCAST_GRP - ], - ConnectionSchema.CONF_KNX_MCAST_PORT: config[ - ConnectionSchema.CONF_KNX_MCAST_PORT - ], - CONF_KNX_INDIVIDUAL_ADDRESS: config[CONF_KNX_INDIVIDUAL_ADDRESS], - } - - if CONF_KNX_TUNNELING in config: - return self.async_create_entry( - title=f"{CONF_KNX_TUNNELING.capitalize()} @ {config[CONF_KNX_TUNNELING][CONF_HOST]}", - data={ - **DEFAULT_ENTRY_DATA, - CONF_HOST: config[CONF_KNX_TUNNELING][CONF_HOST], - CONF_PORT: config[CONF_KNX_TUNNELING][CONF_PORT], - ConnectionSchema.CONF_KNX_LOCAL_IP: config[CONF_KNX_TUNNELING].get( - ConnectionSchema.CONF_KNX_LOCAL_IP - ), - ConnectionSchema.CONF_KNX_ROUTE_BACK: config[CONF_KNX_TUNNELING][ - ConnectionSchema.CONF_KNX_ROUTE_BACK - ], - CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING, - **data, - }, - ) - - if CONF_KNX_ROUTING in config: - return self.async_create_entry( - title=CONF_KNX_ROUTING.capitalize(), - data={ - **DEFAULT_ENTRY_DATA, - CONF_KNX_CONNECTION_TYPE: CONF_KNX_ROUTING, - **data, - }, - ) - - return self.async_create_entry( - title=CONF_KNX_AUTOMATIC.capitalize(), - data={ - **DEFAULT_ENTRY_DATA, - CONF_KNX_CONNECTION_TYPE: CONF_KNX_AUTOMATIC, - **data, - }, - ) - class KNXOptionsFlowHandler(OptionsFlow): """Handle KNX options.""" @@ -332,52 +383,60 @@ class KNXOptionsFlowHandler(OptionsFlow): vol.Required( CONF_KNX_INDIVIDUAL_ADDRESS, default=self.current_config[CONF_KNX_INDIVIDUAL_ADDRESS], - ): str, + ): selector.selector({"text": {}}), vol.Required( - ConnectionSchema.CONF_KNX_MCAST_GRP, - default=self.current_config.get( - ConnectionSchema.CONF_KNX_MCAST_GRP, DEFAULT_MCAST_GRP - ), - ): str, + CONF_KNX_MCAST_GRP, + default=self.current_config.get(CONF_KNX_MCAST_GRP, DEFAULT_MCAST_GRP), + ): _IP_SELECTOR, vol.Required( - ConnectionSchema.CONF_KNX_MCAST_PORT, + CONF_KNX_MCAST_PORT, default=self.current_config.get( - ConnectionSchema.CONF_KNX_MCAST_PORT, DEFAULT_MCAST_PORT + CONF_KNX_MCAST_PORT, DEFAULT_MCAST_PORT ), - ): cv.port, + ): _PORT_SELECTOR, } if self.show_advanced_options: local_ip = ( - self.current_config.get(ConnectionSchema.CONF_KNX_LOCAL_IP) - if self.current_config.get(ConnectionSchema.CONF_KNX_LOCAL_IP) - is not None + self.current_config.get(CONF_KNX_LOCAL_IP) + if self.current_config.get(CONF_KNX_LOCAL_IP) is not None else CONF_DEFAULT_LOCAL_IP ) data_schema[ vol.Required( - ConnectionSchema.CONF_KNX_LOCAL_IP, + CONF_KNX_LOCAL_IP, default=local_ip, ) - ] = str + ] = _IP_SELECTOR data_schema[ vol.Required( - ConnectionSchema.CONF_KNX_STATE_UPDATER, + CONF_KNX_STATE_UPDATER, default=self.current_config.get( - ConnectionSchema.CONF_KNX_STATE_UPDATER, - ConnectionSchema.CONF_KNX_DEFAULT_STATE_UPDATER, + CONF_KNX_STATE_UPDATER, + CONF_KNX_DEFAULT_STATE_UPDATER, ), ) - ] = bool + ] = selector.selector({"boolean": {}}) data_schema[ vol.Required( - ConnectionSchema.CONF_KNX_RATE_LIMIT, + CONF_KNX_RATE_LIMIT, default=self.current_config.get( - ConnectionSchema.CONF_KNX_RATE_LIMIT, - ConnectionSchema.CONF_KNX_DEFAULT_RATE_LIMIT, + CONF_KNX_RATE_LIMIT, + CONF_KNX_DEFAULT_RATE_LIMIT, ), ) - ] = vol.All(vol.Coerce(int), vol.Range(min=1, max=CONF_MAX_RATE_LIMIT)) + ] = vol.All( + selector.selector( + { + "number": { + "min": 1, + "max": CONF_MAX_RATE_LIMIT, + "mode": "box", + } + } + ), + vol.Coerce(int), + ) return self.async_show_form( step_id="init", @@ -409,10 +468,10 @@ class KNXOptionsFlowHandler(OptionsFlow): ): vol.In(connection_methods), vol.Required( CONF_HOST, default=self.current_config.get(CONF_HOST) - ): str, + ): _IP_SELECTOR, vol.Required( CONF_PORT, default=self.current_config.get(CONF_PORT, 3671) - ): cv.port, + ): _PORT_SELECTOR, } ), last_step=True, @@ -421,11 +480,8 @@ class KNXOptionsFlowHandler(OptionsFlow): entry_data = { **DEFAULT_ENTRY_DATA, **self.general_settings, - ConnectionSchema.CONF_KNX_LOCAL_IP: self.general_settings.get( - ConnectionSchema.CONF_KNX_LOCAL_IP - ) - if self.general_settings.get(ConnectionSchema.CONF_KNX_LOCAL_IP) - != CONF_DEFAULT_LOCAL_IP + CONF_KNX_LOCAL_IP: self.general_settings.get(CONF_KNX_LOCAL_IP) + if self.general_settings.get(CONF_KNX_LOCAL_IP) != CONF_DEFAULT_LOCAL_IP else None, CONF_HOST: self.current_config.get(CONF_HOST, ""), } @@ -436,7 +492,7 @@ class KNXOptionsFlowHandler(OptionsFlow): **entry_data, CONF_HOST: user_input[CONF_HOST], CONF_PORT: user_input[CONF_PORT], - ConnectionSchema.CONF_KNX_ROUTE_BACK: ( + CONF_KNX_ROUTE_BACK: ( connection_type == CONF_KNX_LABEL_TUNNELING_UDP_ROUTE_BACK ), CONF_KNX_CONNECTION_TYPE: ( @@ -466,7 +522,7 @@ class KNXOptionsFlowHandler(OptionsFlow): def get_knx_tunneling_type(config_entry_data: dict) -> str: """Obtain the knx tunneling type based on the data in the config entry data.""" connection_type = config_entry_data[CONF_KNX_CONNECTION_TYPE] - route_back = config_entry_data.get(ConnectionSchema.CONF_KNX_ROUTE_BACK, False) + route_back = config_entry_data.get(CONF_KNX_ROUTE_BACK, False) if route_back and connection_type == CONF_KNX_TUNNELING: return CONF_KNX_LABEL_TUNNELING_UDP_ROUTE_BACK if connection_type == CONF_KNX_TUNNELING_TCP: diff --git a/homeassistant/components/knx/const.py b/homeassistant/components/knx/const.py index ac6a156a90a..efe091f22a9 100644 --- a/homeassistant/components/knx/const.py +++ b/homeassistant/components/knx/const.py @@ -1,6 +1,6 @@ """Constants for the KNX integration.""" from enum import Enum -from typing import Final +from typing import Final, TypedDict from homeassistant.components.climate.const import ( CURRENT_HVAC_COOL, @@ -30,11 +30,38 @@ KNX_ADDRESS: Final = "address" CONF_INVERT: Final = "invert" CONF_KNX_EXPOSE: Final = "expose" CONF_KNX_INDIVIDUAL_ADDRESS: Final = "individual_address" + +## +# Connection constants +## CONF_KNX_CONNECTION_TYPE: Final = "connection_type" CONF_KNX_AUTOMATIC: Final = "automatic" CONF_KNX_ROUTING: Final = "routing" CONF_KNX_TUNNELING: Final = "tunneling" CONF_KNX_TUNNELING_TCP: Final = "tunneling_tcp" +CONF_KNX_TUNNELING_TCP_SECURE: Final = "tunneling_tcp_secure" +CONF_KNX_LOCAL_IP: Final = "local_ip" +CONF_KNX_MCAST_GRP: Final = "multicast_group" +CONF_KNX_MCAST_PORT: Final = "multicast_port" + +CONF_KNX_RATE_LIMIT: Final = "rate_limit" +CONF_KNX_ROUTE_BACK: Final = "route_back" +CONF_KNX_STATE_UPDATER: Final = "state_updater" +CONF_KNX_DEFAULT_STATE_UPDATER: Final = True +CONF_KNX_DEFAULT_RATE_LIMIT: Final = 20 + +## +# Secure constants +## +CONST_KNX_STORAGE_KEY: Final = "knx/" +CONF_KNX_KNXKEY_FILENAME: Final = "knxkeys_filename" +CONF_KNX_KNXKEY_PASSWORD: Final = "knxkeys_password" + +CONF_KNX_SECURE_USER_ID: Final = "user_id" +CONF_KNX_SECURE_USER_PASSWORD: Final = "user_password" +CONF_KNX_SECURE_DEVICE_AUTHENTICATION: Final = "device_authentication" + + CONF_PAYLOAD: Final = "payload" CONF_PAYLOAD_LENGTH: Final = "payload_length" CONF_RESET_AFTER: Final = "reset_after" @@ -52,6 +79,27 @@ ATTR_COUNTER: Final = "counter" ATTR_SOURCE: Final = "source" +class KNXConfigEntryData(TypedDict, total=False): + """Config entry for the KNX integration.""" + + connection_type: str + individual_address: str + local_ip: str + multicast_group: str + multicast_port: int + route_back: bool + state_updater: bool + rate_limit: int + host: str + port: int + + user_id: int + user_password: str + device_authentication: str + knxkeys_filename: str + knxkeys_password: str + + class ColorTempModes(Enum): """Color temperature modes for config validation.""" diff --git a/homeassistant/components/knx/diagnostics.py b/homeassistant/components/knx/diagnostics.py index 8b299bf9265..c409b4116bf 100644 --- a/homeassistant/components/knx/diagnostics.py +++ b/homeassistant/components/knx/diagnostics.py @@ -6,11 +6,23 @@ from typing import Any import voluptuous as vol from homeassistant import config as conf_util +from homeassistant.components.diagnostics import async_redact_data from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from . import CONFIG_SCHEMA -from .const import DOMAIN +from .const import ( + CONF_KNX_KNXKEY_PASSWORD, + CONF_KNX_SECURE_DEVICE_AUTHENTICATION, + CONF_KNX_SECURE_USER_PASSWORD, + DOMAIN, +) + +TO_REDACT = { + CONF_KNX_KNXKEY_PASSWORD, + CONF_KNX_SECURE_USER_PASSWORD, + CONF_KNX_SECURE_DEVICE_AUTHENTICATION, +} async def async_get_config_entry_diagnostics( @@ -24,7 +36,7 @@ async def async_get_config_entry_diagnostics( "current_address": str(knx_module.xknx.current_address), } - diag["config_entry_data"] = dict(config_entry.data) + diag["config_entry_data"] = async_redact_data(dict(config_entry.data), TO_REDACT) raw_config = await conf_util.async_hass_config_yaml(hass) diag["configuration_yaml"] = raw_config.get(DOMAIN) diff --git a/homeassistant/components/knx/fan.py b/homeassistant/components/knx/fan.py index 38c90aa149d..f5255fd25b0 100644 --- a/homeassistant/components/knx/fan.py +++ b/homeassistant/components/knx/fan.py @@ -106,7 +106,6 @@ class KNXFan(KnxEntity, FanEntity): async def async_turn_on( self, - speed: str | None = None, percentage: int | None = None, preset_mode: str | None = None, **kwargs: Any, diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json index 924fa284e93..604821ae275 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -3,14 +3,8 @@ "name": "KNX", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/knx", - "requirements": [ - "xknx==0.19.2" - ], - "codeowners": [ - "@Julius2342", - "@farmio", - "@marvin-w" - ], + "requirements": ["xknx==0.20.1"], + "codeowners": ["@Julius2342", "@farmio", "@marvin-w"], "quality_scale": "silver", "iot_class": "local_push", "loggers": ["xknx"] diff --git a/homeassistant/components/knx/schema.py b/homeassistant/components/knx/schema.py index f5f8875bb5f..555fcfc575b 100644 --- a/homeassistant/components/knx/schema.py +++ b/homeassistant/components/knx/schema.py @@ -6,11 +6,9 @@ from collections import OrderedDict from typing import Any, ClassVar, Final import voluptuous as vol -from xknx import XKNX from xknx.devices.climate import SetpointShiftMode from xknx.dpt import DPTBase, DPTNumeric from xknx.exceptions import ConversionError, CouldNotParseAddress -from xknx.io import DEFAULT_MCAST_GRP, DEFAULT_MCAST_PORT from xknx.telegram.address import IndividualAddress, parse_device_group_address from homeassistant.components.binary_sensor import ( @@ -27,10 +25,8 @@ from homeassistant.const import ( CONF_ENTITY_CATEGORY, CONF_ENTITY_ID, CONF_EVENT, - CONF_HOST, CONF_MODE, CONF_NAME, - CONF_PORT, CONF_TYPE, Platform, ) @@ -40,9 +36,6 @@ from homeassistant.helpers.entity import validate_entity_category from .const import ( CONF_INVERT, CONF_KNX_EXPOSE, - CONF_KNX_INDIVIDUAL_ADDRESS, - CONF_KNX_ROUTING, - CONF_KNX_TUNNELING, CONF_PAYLOAD, CONF_PAYLOAD_LENGTH, CONF_RESET_AFTER, @@ -196,57 +189,6 @@ sync_state_validator = vol.Any( cv.matches_regex(r"^(init|expire|every)( \d*)?$"), ) - -############## -# CONNECTION -############## - - -class ConnectionSchema: - """ - Voluptuous schema for KNX connection. - - DEPRECATED: Migrated to config and options flow. Will be removed in a future version of Home Assistant. - """ - - CONF_KNX_LOCAL_IP = "local_ip" - CONF_KNX_MCAST_GRP = "multicast_group" - CONF_KNX_MCAST_PORT = "multicast_port" - CONF_KNX_RATE_LIMIT = "rate_limit" - CONF_KNX_ROUTE_BACK = "route_back" - CONF_KNX_STATE_UPDATER = "state_updater" - - CONF_KNX_DEFAULT_STATE_UPDATER = True - CONF_KNX_DEFAULT_RATE_LIMIT = 20 - - TUNNELING_SCHEMA = vol.Schema( - { - vol.Optional(CONF_PORT, default=DEFAULT_MCAST_PORT): cv.port, - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_KNX_LOCAL_IP): cv.string, - vol.Optional(CONF_KNX_ROUTE_BACK, default=False): cv.boolean, - } - ) - - ROUTING_SCHEMA = vol.Maybe(vol.Schema({vol.Optional(CONF_KNX_LOCAL_IP): cv.string})) - - SCHEMA = { - vol.Exclusive(CONF_KNX_ROUTING, "connection_type"): ROUTING_SCHEMA, - vol.Exclusive(CONF_KNX_TUNNELING, "connection_type"): TUNNELING_SCHEMA, - vol.Optional( - CONF_KNX_INDIVIDUAL_ADDRESS, default=XKNX.DEFAULT_ADDRESS - ): ia_validator, - vol.Optional(CONF_KNX_MCAST_GRP, default=DEFAULT_MCAST_GRP): cv.string, - vol.Optional(CONF_KNX_MCAST_PORT, default=DEFAULT_MCAST_PORT): cv.port, - vol.Optional( - CONF_KNX_STATE_UPDATER, default=CONF_KNX_DEFAULT_STATE_UPDATER - ): cv.boolean, - vol.Optional(CONF_KNX_RATE_LIMIT, default=CONF_KNX_DEFAULT_RATE_LIMIT): vol.All( - vol.Coerce(int), vol.Range(min=1, max=100) - ), - } - - ######### # EVENT ######### diff --git a/homeassistant/components/knx/strings.json b/homeassistant/components/knx/strings.json index 77e18cad808..2149dd96a47 100644 --- a/homeassistant/components/knx/strings.json +++ b/homeassistant/components/knx/strings.json @@ -19,17 +19,56 @@ "tunneling_type": "KNX Tunneling Type", "port": "[%key:common::config_flow::data::port%]", "host": "[%key:common::config_flow::data::host%]", - "individual_address": "Individual address for the connection", - "local_ip": "Local IP of Home Assistant (leave empty for automatic detection)" + "local_ip": "Local IP of Home Assistant" + }, + "data_description": { + "port": "Port of the KNX/IP tunneling device.", + "host": "IP address of the KNX/IP tunneling device.", + "local_ip": "Leave blank to use auto-discovery." + } + }, + "secure_tunneling": { + "description": "Select how you want to configure KNX/IP Secure.", + "menu_options": { + "secure_knxkeys": "Use a `.knxkeys` file containing IP secure keys", + "secure_manual": "Configure IP secure keys manually" + } + }, + "secure_knxkeys": { + "description": "Please enter the information for your `.knxkeys` file.", + "data": { + "knxkeys_filename": "The filename of your `.knxkeys` file (including extension)", + "knxkeys_password": "The password to decrypt the `.knxkeys` file" + }, + "data_description": { + "knxkeys_filename": "The file is expected to be found in your config directory in `.storage/knx/`.\nIn Home Assistant OS this would be `/config/.storage/knx/`\nExample: `my_project.knxkeys`", + "knxkeys_password": "This was set when exporting the file from ETS." + } + }, + "secure_manual": { + "description": "Please enter your IP secure information.", + "data": { + "user_id": "User ID", + "user_password": "User password", + "device_authentication": "Device authentication password" + }, + "data_description": { + "user_id": "This is often tunnel number +1. So 'Tunnel 2' would have User-ID '3'.", + "user_password": "Password for the specific tunnel connection set in the 'Properties' panel of the tunnel in ETS.", + "device_authentication": "This is set in the 'IP' panel of the interface in ETS." } }, "routing": { "description": "Please configure the routing options.", "data": { - "individual_address": "Individual address for the routing connection", - "multicast_group": "The multicast group used for routing", - "multicast_port": "The multicast port used for routing", - "local_ip": "Local IP of Home Assistant (leave empty for automatic detection)" + "individual_address": "Individual address", + "multicast_group": "Multicast group used for routing", + "multicast_port": "Multicast port used for routing", + "local_ip": "Local IP of Home Assistant" + }, + "data_description": { + "individual_address": "KNX address to be used by Home Assistant, e.g. `0.0.4`", + "local_ip": "Leave blank to use auto-discovery." } } }, @@ -38,7 +77,9 @@ "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" }, "error": { - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_signature": "The password to decrypt the knxkeys file is wrong.", + "file_not_found": "The specified knxkeys file was not found in the path config/.storage/knx/" } }, "options": { @@ -47,11 +88,19 @@ "data": { "connection_type": "KNX Connection Type", "individual_address": "Default individual address", - "multicast_group": "Multicast group used for routing and discovery", - "multicast_port": "Multicast port used for routing and discovery", - "local_ip": "Local IP of Home Assistant (use 0.0.0.0 for automatic detection)", - "state_updater": "Globally enable reading states from the KNX Bus", - "rate_limit": "Maximum outgoing telegrams per second" + "multicast_group": "Multicast group", + "multicast_port": "Multicast port", + "local_ip": "Local IP of Home Assistant", + "state_updater": "State updater", + "rate_limit": "Rate limit" + }, + "data_description": { + "individual_address": "KNX address to be used by Home Assistant, e.g. `0.0.4`", + "multicast_group": "Used for routing and discovery. Default: `224.0.23.12`", + "multicast_port": "Used for routing and discovery. Default: `3671`", + "local_ip": "Use `0.0.0.0` for auto-discovery.", + "state_updater": "Globally enable or disable reading states from the KNX Bus. When disabled, Home Assistant will not actively retrieve states from the KNX Bus, `sync_state` entity options will have no effect.", + "rate_limit": "Maximum outgoing telegrams per second.\nRecommended: 20 to 40" } }, "tunnel": { @@ -59,6 +108,10 @@ "tunneling_type": "KNX Tunneling Type", "port": "[%key:common::config_flow::data::port%]", "host": "[%key:common::config_flow::data::host%]" + }, + "data_description": { + "port": "Port of the KNX/IP tunneling device.", + "host": "IP address of the KNX/IP tunneling device." } } } diff --git a/homeassistant/components/knx/translations/de.json b/homeassistant/components/knx/translations/de.json index 6716b5c8a37..2f7795153dc 100644 --- a/homeassistant/components/knx/translations/de.json +++ b/homeassistant/components/knx/translations/de.json @@ -11,8 +11,8 @@ "manual_tunnel": { "data": { "host": "Host", - "individual_address": "Individuelle Adresse f\u00fcr die Verbindung", - "local_ip": "Lokale IP des Home Assistant (f\u00fcr automatische Erkennung leer lassen)", + "individual_address": "Physikalische Adresse f\u00fcr die Verbindung", + "local_ip": "Lokale IP von Home Assistant (f\u00fcr automatische Erkennung leer lassen)", "port": "Port", "route_back": "Route Back / NAT-Modus", "tunneling_type": "KNX Tunneling Typ" @@ -21,8 +21,8 @@ }, "routing": { "data": { - "individual_address": "Individuelle Adresse f\u00fcr die Routingverbindung", - "local_ip": "Lokale IP des Home Assistant (f\u00fcr automatische Erkennung leer lassen)", + "individual_address": "Physikalische Adresse f\u00fcr die Routingverbindung", + "local_ip": "Lokale IP von Home Assistant (f\u00fcr automatische Erkennung leer lassen)", "multicast_group": "Die f\u00fcr das Routing verwendete Multicast-Gruppe", "multicast_port": "Der f\u00fcr das Routing verwendete Multicast-Port" }, @@ -47,18 +47,18 @@ "init": { "data": { "connection_type": "KNX-Verbindungstyp", - "individual_address": "Individuelle Standardadresse", - "local_ip": "Lokale IP des Home Assistant (verwende 0.0.0.0 f\u00fcr automatische Erkennung)", - "multicast_group": "Multicast-Gruppe f\u00fcr Routing und Discovery verwenden", - "multicast_port": "Multicast-Port f\u00fcr Routing und Discovery verwenden", - "rate_limit": "Maximal ausgehende Telegrams pro Sekunde", - "state_updater": "Lesen von Zust\u00e4nden aus dem KNX Bus global freigeben" + "individual_address": "Standard physikalische Adresse", + "local_ip": "Lokale IP von Home Assistant (verwende 0.0.0.0 f\u00fcr automatische Erkennung)", + "multicast_group": "Multicast-Gruppe f\u00fcr Routing und Discovery", + "multicast_port": "Multicast-Port f\u00fcr Routing und Discovery", + "rate_limit": "Maximal ausgehende Telegramme pro Sekunde", + "state_updater": "Lesen von Zust\u00e4nden von dem KNX Bus global freigeben" } }, "tunnel": { "data": { "host": "Host", - "local_ip": "Lokale IP (leer lassen, wenn unsicher)", + "local_ip": "Lokale IP (im Zweifel leer lassen)", "port": "Port", "route_back": "Route Back / NAT-Modus", "tunneling_type": "KNX Tunneling Typ" diff --git a/homeassistant/components/knx/translations/en.json b/homeassistant/components/knx/translations/en.json index b8b8cf1250e..640cb4a5358 100644 --- a/homeassistant/components/knx/translations/en.json +++ b/homeassistant/components/knx/translations/en.json @@ -5,29 +5,69 @@ "single_instance_allowed": "Already configured. Only a single configuration possible." }, "error": { - "cannot_connect": "Failed to connect" + "cannot_connect": "Failed to connect", + "file_not_found": "The specified knxkeys file was not found in the path config/.storage/knx/", + "invalid_signature": "The password to decrypt the knxkeys file is wrong." }, "step": { "manual_tunnel": { "data": { "host": "Host", - "individual_address": "Individual address for the connection", - "local_ip": "Local IP of Home Assistant (leave empty for automatic detection)", + "local_ip": "Local IP of Home Assistant", "port": "Port", - "route_back": "Route Back / NAT Mode", "tunneling_type": "KNX Tunneling Type" }, + "data_description": { + "host": "IP address of the KNX/IP tunneling device.", + "local_ip": "Leave blank to use auto-discovery.", + "port": "Port of the KNX/IP tunneling device." + }, "description": "Please enter the connection information of your tunneling device." }, "routing": { "data": { - "individual_address": "Individual address for the routing connection", - "local_ip": "Local IP of Home Assistant (leave empty for automatic detection)", - "multicast_group": "The multicast group used for routing", - "multicast_port": "The multicast port used for routing" + "individual_address": "Individual address", + "local_ip": "Local IP of Home Assistant", + "multicast_group": "Multicast group used for routing", + "multicast_port": "Multicast port used for routing" + }, + "data_description": { + "individual_address": "KNX address to be used by Home Assistant, e.g. `0.0.4`", + "local_ip": "Leave blank to use auto-discovery." }, "description": "Please configure the routing options." }, + "secure_knxkeys": { + "data": { + "knxkeys_filename": "The filename of your `.knxkeys` file (including extension)", + "knxkeys_password": "The password to decrypt the `.knxkeys` file" + }, + "data_description": { + "knxkeys_filename": "The file is expected to be found in your config directory in `.storage/knx/`.\nIn Home Assistant OS this would be `/config/.storage/knx/`\nExample: `my_project.knxkeys`", + "knxkeys_password": "This was set when exporting the file from ETS." + }, + "description": "Please enter the information for your `.knxkeys` file." + }, + "secure_manual": { + "data": { + "device_authentication": "Device authentication password", + "user_id": "User ID", + "user_password": "User password" + }, + "data_description": { + "device_authentication": "This is set in the 'IP' panel of the interface in ETS.", + "user_id": "This is often tunnel number +1. So 'Tunnel 2' would have User-ID '3'.", + "user_password": "Password for the specific tunnel connection set in the 'Properties' panel of the tunnel in ETS." + }, + "description": "Please enter your IP secure information." + }, + "secure_tunneling": { + "description": "Select how you want to configure KNX/IP Secure.", + "menu_options": { + "secure_knxkeys": "Use a `.knxkeys` file containing IP secure keys", + "secure_manual": "Configure IP secure keys manually" + } + }, "tunnel": { "data": { "gateway": "KNX Tunnel Connection" @@ -48,20 +88,30 @@ "data": { "connection_type": "KNX Connection Type", "individual_address": "Default individual address", - "local_ip": "Local IP of Home Assistant (use 0.0.0.0 for automatic detection)", - "multicast_group": "Multicast group used for routing and discovery", - "multicast_port": "Multicast port used for routing and discovery", - "rate_limit": "Maximum outgoing telegrams per second", - "state_updater": "Globally enable reading states from the KNX Bus" + "local_ip": "Local IP of Home Assistant", + "multicast_group": "Multicast group", + "multicast_port": "Multicast port", + "rate_limit": "Rate limit", + "state_updater": "State updater" + }, + "data_description": { + "individual_address": "KNX address to be used by Home Assistant, e.g. `0.0.4`", + "local_ip": "Use `0.0.0.0` for auto-discovery.", + "multicast_group": "Used for routing and discovery. Default: `224.0.23.12`", + "multicast_port": "Used for routing and discovery. Default: `3671`", + "rate_limit": "Maximum outgoing telegrams per second.\nRecommended: 20 to 40", + "state_updater": "Globally enable or disable reading states from the KNX Bus. When disabled, Home Assistant will not actively retrieve states from the KNX Bus, `sync_state` entity options will have no effect." } }, "tunnel": { "data": { "host": "Host", - "local_ip": "Local IP (leave empty if unsure)", "port": "Port", - "route_back": "Route Back / NAT Mode", "tunneling_type": "KNX Tunneling Type" + }, + "data_description": { + "host": "IP address of the KNX/IP tunneling device.", + "port": "Port of the KNX/IP tunneling device." } } } diff --git a/homeassistant/components/knx/translations/fr.json b/homeassistant/components/knx/translations/fr.json index 08e53ad8d49..31221e900cd 100644 --- a/homeassistant/components/knx/translations/fr.json +++ b/homeassistant/components/knx/translations/fr.json @@ -17,7 +17,7 @@ "route_back": "Retour/Mode NAT", "tunneling_type": "Type de tunnel KNX" }, - "description": "Veuillez saisir les informations de connexion de votre p\u00e9riph\u00e9rique de tunneling." + "description": "Veuillez saisir les informations de connexion de votre appareil de cr\u00e9ation de tunnel." }, "routing": { "data": { diff --git a/homeassistant/components/knx/translations/he.json b/homeassistant/components/knx/translations/he.json index 3c338886e22..ef11ad342ee 100644 --- a/homeassistant/components/knx/translations/he.json +++ b/homeassistant/components/knx/translations/he.json @@ -13,11 +13,21 @@ "host": "\u05de\u05d0\u05e8\u05d7", "port": "\u05e4\u05ea\u05d7\u05d4" } + }, + "routing": { + "data": { + "multicast_group": "\u05e7\u05d1\u05d5\u05e6\u05ea \u05e9\u05d9\u05d3\u05d5\u05e8 \u05de\u05e8\u05d5\u05d1\u05d4 \u05d4\u05de\u05e9\u05de\u05e9\u05ea \u05dc\u05e0\u05d9\u05ea\u05d5\u05d1" + } } } }, "options": { "step": { + "init": { + "data": { + "multicast_group": "\u05e7\u05d1\u05d5\u05e6\u05ea \u05e9\u05d9\u05d3\u05d5\u05e8 \u05de\u05e8\u05d5\u05d1\u05d4 \u05d4\u05de\u05e9\u05de\u05e9\u05ea \u05dc\u05e0\u05d9\u05ea\u05d5\u05d1 \u05d5\u05d2\u05d9\u05dc\u05d5\u05d9" + } + }, "tunnel": { "data": { "host": "\u05de\u05d0\u05e8\u05d7", diff --git a/homeassistant/components/knx/translations/zh-Hant.json b/homeassistant/components/knx/translations/zh-Hant.json index 27b167c6551..b6c09456fb3 100644 --- a/homeassistant/components/knx/translations/zh-Hant.json +++ b/homeassistant/components/knx/translations/zh-Hant.json @@ -15,7 +15,7 @@ "local_ip": "Home Assistant \u672c\u5730\u7aef IP\uff08\u4fdd\u7559\u7a7a\u767d\u4ee5\u81ea\u52d5\u5075\u6e2c\uff09", "port": "\u901a\u8a0a\u57e0", "route_back": "\u8def\u7531\u8fd4\u56de / NAT \u6a21\u5f0f", - "tunneling_type": "KNX \u901a\u9053\u985e\u578b" + "tunneling_type": "KNX \u901a\u9053\u985e\u5225" }, "description": "\u8acb\u8f38\u5165\u901a\u9053\u88dd\u7f6e\u7684\u9023\u7dda\u8cc7\u8a0a\u3002" }, @@ -36,9 +36,9 @@ }, "type": { "data": { - "connection_type": "KNX \u9023\u7dda\u985e\u578b" + "connection_type": "KNX \u9023\u7dda\u985e\u5225" }, - "description": "\u8acb\u8f38\u5165 KNX \u9023\u7dda\u6240\u4f7f\u7528\u4e4b\u9023\u7dda\u985e\u578b\u3002 \n \u81ea\u52d5\uff08AUTOMATIC\uff09 - \u6574\u5408\u81ea\u52d5\u85c9\u7531\u9598\u9053\u5668\u6383\u63cf\u5f8c\u8655\u7406\u9023\u7dda\u554f\u984c\u3002\n \u901a\u9053\uff08TUNNELING\uff09 - \u6574\u5408\u5c07\u6703\u900f\u904e\u901a\u9053\u65b9\u5f0f\u8207 KNX Bus \u9032\u884c\u9023\u7dda\u3002\n \u8def\u7531\uff08ROUTING\uff09 - \u6574\u5408\u5c07\u6703\u900f\u904e\u8def\u7531\u65b9\u5f0f\u8207 KNX Bus \u9032\u884c\u9023\u7dda\u3002" + "description": "\u8acb\u8f38\u5165 KNX \u9023\u7dda\u6240\u4f7f\u7528\u4e4b\u9023\u7dda\u985e\u5225\u3002 \n \u81ea\u52d5\uff08AUTOMATIC\uff09 - \u6574\u5408\u81ea\u52d5\u85c9\u7531\u9598\u9053\u5668\u6383\u63cf\u5f8c\u8655\u7406\u9023\u7dda\u554f\u984c\u3002\n \u901a\u9053\uff08TUNNELING\uff09 - \u6574\u5408\u5c07\u6703\u900f\u904e\u901a\u9053\u65b9\u5f0f\u8207 KNX Bus \u9032\u884c\u9023\u7dda\u3002\n \u8def\u7531\uff08ROUTING\uff09 - \u6574\u5408\u5c07\u6703\u900f\u904e\u8def\u7531\u65b9\u5f0f\u8207 KNX Bus \u9032\u884c\u9023\u7dda\u3002" } } }, @@ -46,11 +46,11 @@ "step": { "init": { "data": { - "connection_type": "KNX \u9023\u7dda\u985e\u578b", + "connection_type": "KNX \u9023\u7dda\u985e\u5225", "individual_address": "\u9810\u8a2d\u500b\u5225\u4f4d\u5740", "local_ip": "Home Assistant \u672c\u5730\u7aef IP\uff08\u586b\u5165 0.0.0.0 \u555f\u7528\u81ea\u52d5\u5075\u6e2c\uff09", - "multicast_group": "\u4f7f\u7528\u65bc\u8def\u7531\u8207\u63a2\u7d22\u7684 Multicast \u7fa4\u7d44", - "multicast_port": "\u4f7f\u7528\u65bc\u8def\u7531\u8207\u63a2\u7d22\u7684 Multicast \u901a\u8a0a\u57e0", + "multicast_group": "\u4f7f\u7528\u65bc\u8def\u7531\u8207\u641c\u7d22\u7684 Multicast \u7fa4\u7d44", + "multicast_port": "\u4f7f\u7528\u65bc\u8def\u7531\u8207\u641c\u7d22\u7684 Multicast \u901a\u8a0a\u57e0", "rate_limit": "\u6700\u5927\u6bcf\u79d2\u767c\u51fa Telegram", "state_updater": "\u7531 KNX Bus \u8b80\u53d6\u72c0\u614b\u5168\u555f\u7528" } @@ -61,7 +61,7 @@ "local_ip": "\u672c\u5730\u7aef IP\uff08\u5047\u5982\u4e0d\u78ba\u5b9a\uff0c\u4fdd\u7559\u7a7a\u767d\uff09", "port": "\u901a\u8a0a\u57e0", "route_back": "\u8def\u7531\u8fd4\u56de / NAT \u6a21\u5f0f", - "tunneling_type": "KNX \u901a\u9053\u985e\u578b" + "tunneling_type": "KNX \u901a\u9053\u985e\u5225" } } } diff --git a/homeassistant/components/kodi/media_player.py b/homeassistant/components/kodi/media_player.py index e1dae879bf8..51e242ae137 100644 --- a/homeassistant/components/kodi/media_player.py +++ b/homeassistant/components/kodi/media_player.py @@ -278,14 +278,20 @@ def cmd(func): """Wrap all command methods.""" try: await func(obj, *args, **kwargs) - except jsonrpc_base.jsonrpc.TransportError as exc: + except ( + jsonrpc_base.jsonrpc.TransportError, + jsonrpc_base.jsonrpc.ProtocolError, + ) as exc: # If Kodi is off, we expect calls to fail. if obj.state == STATE_OFF: - log_function = _LOGGER.info + log_function = _LOGGER.debug else: log_function = _LOGGER.error log_function( - "Error calling %s on entity %s: %r", func.__name__, obj.entity_id, exc + "Error calling %s on entity %s: %r", + func.__name__, + obj.entity_id, + exc, ) return wrapper @@ -306,6 +312,7 @@ class KodiEntity(MediaPlayerEntity): self._app_properties = {} self._media_position_updated_at = None self._media_position = None + self._connect_error = False def _reset_state(self, players=None): self._players = players @@ -420,6 +427,7 @@ class KodiEntity(MediaPlayerEntity): async def _on_ws_connected(self): """Call after ws is connected.""" + self._connect_error = False self._register_ws_callbacks() version = (await self._kodi.get_application_properties(["version"]))["version"] @@ -436,15 +444,23 @@ class KodiEntity(MediaPlayerEntity): await self._connection.connect() await self._on_ws_connected() except (jsonrpc_base.jsonrpc.TransportError, CannotConnectError): - _LOGGER.debug("Unable to connect to Kodi via websocket", exc_info=True) + if not self._connect_error: + self._connect_error = True + _LOGGER.warning("Unable to connect to Kodi via websocket") await self._clear_connection(False) + else: + self._connect_error = False async def _ping(self): try: await self._kodi.ping() except (jsonrpc_base.jsonrpc.TransportError, CannotConnectError): - _LOGGER.debug("Unable to ping Kodi via websocket", exc_info=True) + if not self._connect_error: + self._connect_error = True + _LOGGER.warning("Unable to ping Kodi via websocket") await self._clear_connection() + else: + self._connect_error = False async def _async_connect_websocket_if_disconnected(self, *_): """Reconnect the websocket if it fails.""" diff --git a/homeassistant/components/kodi/translations/el.json b/homeassistant/components/kodi/translations/el.json index e0a3118ee05..39d13c56856 100644 --- a/homeassistant/components/kodi/translations/el.json +++ b/homeassistant/components/kodi/translations/el.json @@ -12,7 +12,7 @@ "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2", "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" }, - "flow_title": "Kodi: {\u03cc\u03bd\u03bf\u03bc\u03b1}", + "flow_title": "{name}", "step": { "credentials": { "data": { @@ -29,7 +29,7 @@ "data": { "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2", "port": "\u0398\u03cd\u03c1\u03b1", - "ssl": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03bc\u03ad\u03c3\u03c9 SSL" + "ssl": "\u03a7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af \u03ad\u03bd\u03b1 \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03b7\u03c4\u03b9\u03ba\u03cc SSL" }, "description": "\u03a0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 Kodi. \u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03bf\u03cd\u03bc\u03b5 \u03b2\u03b5\u03b2\u03b1\u03b9\u03c9\u03b8\u03b5\u03af\u03c4\u03b5 \u03cc\u03c4\u03b9 \u03ad\u03c7\u03b5\u03c4\u03b5 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03b9 \u03c4\u03bf \"\u039d\u03b1 \u03b5\u03c0\u03b9\u03c4\u03c1\u03ad\u03c0\u03b5\u03c4\u03b1\u03b9 \u03bf \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03bf\u03c5 Kodi \u03bc\u03ad\u03c3\u03c9 HTTP\" \u03c3\u03c4\u03bf \u03a3\u03cd\u03c3\u03c4\u03b7\u03bc\u03b1/\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2/\u0394\u03af\u03ba\u03c4\u03c5\u03bf/\u03a5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b5\u03c2." }, diff --git a/homeassistant/components/kodi/translations/fr.json b/homeassistant/components/kodi/translations/fr.json index e94282fa393..34838232435 100644 --- a/homeassistant/components/kodi/translations/fr.json +++ b/homeassistant/components/kodi/translations/fr.json @@ -3,13 +3,13 @@ "abort": { "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "no_uuid": "L'instance Kodi n'a pas d'identifiant unique. Cela est probablement d\u00fb \u00e0 une ancienne version de Kodi (17.x ou inf\u00e9rieure). Vous pouvez configurer l'int\u00e9gration manuellement ou passer \u00e0 une version plus r\u00e9cente de Kodi.", "unknown": "Erreur inattendue" }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "flow_title": "{name}", diff --git a/homeassistant/components/kodi/translations/zh-Hant.json b/homeassistant/components/kodi/translations/zh-Hant.json index 32ecb4164d5..169ad92e96b 100644 --- a/homeassistant/components/kodi/translations/zh-Hant.json +++ b/homeassistant/components/kodi/translations/zh-Hant.json @@ -23,7 +23,7 @@ }, "discovery_confirm": { "description": "\u662f\u5426\u8981\u65b0\u589e Kodi (`{name}`) \u81f3 Home Assistant\uff1f", - "title": "\u5df2\u641c\u7d22\u5230\u7684 Kodi" + "title": "\u5df2\u767c\u73fe\u7684 Kodi" }, "user": { "data": { diff --git a/homeassistant/components/konnected/config_flow.py b/homeassistant/components/konnected/config_flow.py index 467f3518831..b6f80035dbe 100644 --- a/homeassistant/components/konnected/config_flow.py +++ b/homeassistant/components/konnected/config_flow.py @@ -20,6 +20,7 @@ from homeassistant.const import ( CONF_DISCOVERY, CONF_HOST, CONF_ID, + CONF_MODEL, CONF_NAME, CONF_PORT, CONF_REPEAT, @@ -38,7 +39,6 @@ from .const import ( CONF_BLINK, CONF_DEFAULT_OPTIONS, CONF_INVERSE, - CONF_MODEL, CONF_MOMENTARY, CONF_PAUSE, CONF_POLL_INTERVAL, diff --git a/homeassistant/components/konnected/const.py b/homeassistant/components/konnected/const.py index 270b2604538..c4dd67e7d39 100644 --- a/homeassistant/components/konnected/const.py +++ b/homeassistant/components/konnected/const.py @@ -13,7 +13,6 @@ CONF_INVERSE = "inverse" CONF_BLINK = "blink" CONF_DHT_SENSORS = "dht_sensors" CONF_DS18B20_SENSORS = "ds18b20_sensors" -CONF_MODEL = "model" STATE_LOW = "low" STATE_HIGH = "high" diff --git a/homeassistant/components/konnected/translations/zh-Hant.json b/homeassistant/components/konnected/translations/zh-Hant.json index 523bb10b969..9d6d522b7e1 100644 --- a/homeassistant/components/konnected/translations/zh-Hant.json +++ b/homeassistant/components/konnected/translations/zh-Hant.json @@ -40,7 +40,7 @@ "data": { "inverse": "\u53cd\u8f49\u958b\u555f/\u95dc\u9589\u72c0\u614b", "name": "\u540d\u7a31 \uff08\u9078\u9805\uff09", - "type": "\u4e8c\u9032\u4f4d\u611f\u61c9\u5668\u985e\u578b" + "type": "\u4e8c\u9032\u4f4d\u611f\u61c9\u5668\u985e\u5225" }, "description": "{zone}\u9078\u9805", "title": "\u8a2d\u5b9a\u4e8c\u9032\u4f4d\u611f\u61c9\u5668" @@ -49,7 +49,7 @@ "data": { "name": "\u540d\u7a31 \uff08\u9078\u9805\uff09", "poll_interval": "\u66f4\u65b0\u9593\u8ddd\uff08\u5206\u9418\uff09\uff08\u9078\u9805\uff09", - "type": "\u611f\u61c9\u5668\u985e\u578b" + "type": "\u611f\u61c9\u5668\u985e\u5225" }, "description": "{zone}\u9078\u9805", "title": "\u8a2d\u5b9a\u6578\u4f4d\u611f\u61c9\u5668" diff --git a/homeassistant/components/kostal_plenticore/translations/fr.json b/homeassistant/components/kostal_plenticore/translations/fr.json index a06ade90e9a..66888e8879b 100644 --- a/homeassistant/components/kostal_plenticore/translations/fr.json +++ b/homeassistant/components/kostal_plenticore/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/kraken/sensor.py b/homeassistant/components/kraken/sensor.py index d82f85e9ba9..f98a48d8dc3 100644 --- a/homeassistant/components/kraken/sensor.py +++ b/homeassistant/components/kraken/sensor.py @@ -11,7 +11,10 @@ from homeassistant.helpers import device_registry from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import CoordinatorEntity +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) from . import KrakenData from .const import ( @@ -86,7 +89,9 @@ async def async_setup_entry( ) -class KrakenSensor(CoordinatorEntity[Optional[KrakenResponse]], SensorEntity): +class KrakenSensor( + CoordinatorEntity[DataUpdateCoordinator[Optional[KrakenResponse]]], SensorEntity +): """Define a Kraken sensor.""" entity_description: KrakenSensorEntityDescription diff --git a/homeassistant/components/kraken/strings.json b/homeassistant/components/kraken/strings.json index 10257793de0..e8ad5ffb98c 100644 --- a/homeassistant/components/kraken/strings.json +++ b/homeassistant/components/kraken/strings.json @@ -1,22 +1,22 @@ { - "config": { - "abort": { - "already_configured": "[%key:common::config_flow::abort::single_instance_allowed%]" - }, - "step": { - "user": { - "description": "[%key:common::config_flow::description::confirm_setup%]" - } - } + "config": { + "abort": { + "already_configured": "[%key:common::config_flow::abort::single_instance_allowed%]" }, - "options": { - "step": { - "init": { - "data": { - "scan_interval": "Update interval", - "tracked_asset_pairs": "Tracked Asset Pairs" - } - } - } + "step": { + "user": { + "description": "[%key:common::config_flow::description::confirm_setup%]" + } } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Update interval", + "tracked_asset_pairs": "Tracked Asset Pairs" + } + } + } + } } diff --git a/homeassistant/components/kraken/translations/fr.json b/homeassistant/components/kraken/translations/fr.json index 1aa7fdfbf54..a307e16a3cc 100644 --- a/homeassistant/components/kraken/translations/fr.json +++ b/homeassistant/components/kraken/translations/fr.json @@ -3,16 +3,8 @@ "abort": { "already_configured": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." }, - "error": { - "one": "UN", - "other": "AUTRE" - }, "step": { "user": { - "data": { - "one": "UN", - "other": "AUTRE" - }, "description": "Voulez-vous commencer la configuration\u00a0?" } } diff --git a/homeassistant/components/kulersky/translations/fr.json b/homeassistant/components/kulersky/translations/fr.json index e9ae4e0b644..3d447c0cbb0 100644 --- a/homeassistant/components/kulersky/translations/fr.json +++ b/homeassistant/components/kulersky/translations/fr.json @@ -6,7 +6,7 @@ }, "step": { "confirm": { - "description": "Voulez-vous commencer la configuration ?" + "description": "Voulez-vous commencer la configuration\u00a0?" } } } diff --git a/homeassistant/components/launch_library/config_flow.py b/homeassistant/components/launch_library/config_flow.py index 1023bb84079..d57bc3b7d01 100644 --- a/homeassistant/components/launch_library/config_flow.py +++ b/homeassistant/components/launch_library/config_flow.py @@ -4,7 +4,6 @@ from __future__ import annotations from typing import Any from homeassistant import config_entries -from homeassistant.const import CONF_NAME from homeassistant.data_entry_flow import FlowResult from .const import DOMAIN @@ -27,7 +26,3 @@ class LaunchLibraryFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return self.async_create_entry(title="Launch Library", data=user_input) return self.async_show_form(step_id="user") - - async def async_step_import(self, conf: dict[str, Any]) -> FlowResult: - """Import a configuration from config.yaml.""" - return await self.async_step_user(user_input={CONF_NAME: conf[CONF_NAME]}) diff --git a/homeassistant/components/launch_library/sensor.py b/homeassistant/components/launch_library/sensor.py index d468c3a653f..85183a2d616 100644 --- a/homeassistant/components/launch_library/sensor.py +++ b/homeassistant/components/launch_library/sensor.py @@ -4,25 +4,20 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass from datetime import datetime -import logging from typing import Any from pylaunches.objects.event import Event from pylaunches.objects.launch import Launch -import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, SensorEntityDescription, ) -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME, PERCENTAGE from homeassistant.core import HomeAssistant, callback -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -34,13 +29,6 @@ from .const import DOMAIN DEFAULT_NEXT_LAUNCH_NAME = "Next launch" -_LOGGER = logging.getLogger(__name__) - - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - {vol.Optional(CONF_NAME, default=DEFAULT_NEXT_LAUNCH_NAME): cv.string} -) - @dataclass class LaunchLibrarySensorEntityDescriptionMixin: @@ -137,28 +125,6 @@ SENSOR_DESCRIPTIONS: tuple[LaunchLibrarySensorEntityDescription, ...] = ( ) -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Import Launch Library configuration from yaml.""" - _LOGGER.warning( - "Configuration of the launch_library platform in YAML is deprecated and will be " - "removed in Home Assistant 2022.4; Your existing configuration " - "has been imported into the UI automatically and can be safely removed " - "from your configuration.yaml file" - ) - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data=config, - ) - ) - - async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, @@ -179,13 +145,14 @@ async def async_setup_entry( ) -class LaunchLibrarySensor(CoordinatorEntity, SensorEntity): +class LaunchLibrarySensor( + CoordinatorEntity[DataUpdateCoordinator[LaunchLibraryData]], SensorEntity +): """Representation of the next launch sensors.""" _attr_attribution = "Data provided by Launch Library." _next_event: Launch | Event | None = None entity_description: LaunchLibrarySensorEntityDescription - coordinator: DataUpdateCoordinator[LaunchLibraryData] def __init__( self, diff --git a/homeassistant/components/launch_library/strings.json b/homeassistant/components/launch_library/strings.json index 678fc7c6f4f..5c6295e0f98 100644 --- a/homeassistant/components/launch_library/strings.json +++ b/homeassistant/components/launch_library/strings.json @@ -9,4 +9,4 @@ "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" } } -} \ No newline at end of file +} diff --git a/homeassistant/components/lcn/translations/fr.json b/homeassistant/components/lcn/translations/fr.json index 7a2202e58a7..75e7527701a 100644 --- a/homeassistant/components/lcn/translations/fr.json +++ b/homeassistant/components/lcn/translations/fr.json @@ -2,9 +2,9 @@ "device_automation": { "trigger_type": { "fingerprint": "code d'empreinte digitale re\u00e7u", - "send_keys": "code \u00e9metteur re\u00e7u", + "send_keys": "cl\u00e9s d'envoi re\u00e7ues", "transmitter": "code \u00e9metteur re\u00e7u", - "transponder": "code transpodeur re\u00e7u" + "transponder": "code transpondeur re\u00e7u" } } } \ No newline at end of file diff --git a/homeassistant/components/lg_netcast/media_player.py b/homeassistant/components/lg_netcast/media_player.py index 5faf6941aeb..a76d74481d9 100644 --- a/homeassistant/components/lg_netcast/media_player.py +++ b/homeassistant/components/lg_netcast/media_player.py @@ -8,7 +8,11 @@ from requests import RequestException import voluptuous as vol from homeassistant import util -from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity +from homeassistant.components.media_player import ( + PLATFORM_SCHEMA, + MediaPlayerDeviceClass, + MediaPlayerEntity, +) from homeassistant.components.media_player.const import ( MEDIA_TYPE_CHANNEL, SUPPORT_NEXT_TRACK, @@ -91,6 +95,8 @@ def setup_platform( class LgTVDevice(MediaPlayerEntity): """Representation of a LG TV.""" + _attr_device_class = MediaPlayerDeviceClass.TV + def __init__(self, client, name, on_action_script): """Initialize the LG TV device.""" self._client = client diff --git a/homeassistant/components/life360/strings.json b/homeassistant/components/life360/strings.json index 0d554759ee7..06ac88467ef 100644 --- a/homeassistant/components/life360/strings.json +++ b/homeassistant/components/life360/strings.json @@ -24,4 +24,4 @@ "unknown": "[%key:common::config_flow::error::unknown%]" } } -} \ No newline at end of file +} diff --git a/homeassistant/components/life360/translations/fr.json b/homeassistant/components/life360/translations/fr.json index cb86d8c6590..f58b789b267 100644 --- a/homeassistant/components/life360/translations/fr.json +++ b/homeassistant/components/life360/translations/fr.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "create_entry": { @@ -9,8 +9,8 @@ }, "error": { "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9", - "invalid_auth": "Authentification invalide", - "invalid_username": "Nom d'utilisateur invalide", + "invalid_auth": "Authentification non valide", + "invalid_username": "Nom d'utilisateur non valide", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/lifx/light.py b/homeassistant/components/lifx/light.py index a7c52424d97..a6dd643655f 100644 --- a/homeassistant/components/lifx/light.py +++ b/homeassistant/components/lifx/light.py @@ -74,6 +74,7 @@ MESSAGE_RETRIES = 8 UNAVAILABLE_GRACE = 90 FIX_MAC_FW = AwesomeVersion("3.70") +SWITCH_PRODUCT_IDS = [70, 71, 89] SERVICE_LIFX_SET_STATE = "set_state" @@ -396,14 +397,18 @@ class LIFXManager: # Read initial state ack = AwaitAioLIFX().wait - # Used to populate sw_version - # no need to wait as we do not - # need it until later - bulb.get_hostfirmware() + # Get the product info first so that LIFX Switches + # can be ignored. + version_resp = await ack(bulb.get_version) + if version_resp and bulb.product in SWITCH_PRODUCT_IDS: + _LOGGER.warning( + "(Switch) action=skip_discovery, reason=unsupported, serial=%s, ip_addr=%s, type='LIFX Switch'", + str(bulb.mac_addr).replace(":", ""), + bulb.ip_addr, + ) + return False color_resp = await ack(bulb.get_color) - if color_resp: - version_resp = await ack(bulb.get_version) if color_resp is None or version_resp is None: _LOGGER.error("Failed to initialize %s", bulb.ip_addr) diff --git a/homeassistant/components/lifx/services.yaml b/homeassistant/components/lifx/services.yaml index e5a1cf4b5d8..e499ad1b3b8 100644 --- a/homeassistant/components/lifx/services.yaml +++ b/homeassistant/components/lifx/services.yaml @@ -47,11 +47,11 @@ effect_pulse: selector: select: options: - - 'blink' - - 'breathe' - - 'ping' - - 'strobe' - - 'solid' + - "blink" + - "breathe" + - "ping" + - "strobe" + - "solid" brightness: name: Brightness description: Number indicating brightness of the temporary color. @@ -129,7 +129,7 @@ effect_colorloop: number: min: 0 max: 360 - unit_of_measurement: '°' + unit_of_measurement: "°" spread: name: Spread description: Maximum hue difference between participating lights, in degrees on a color wheel. @@ -138,7 +138,7 @@ effect_colorloop: number: min: 0 max: 360 - unit_of_measurement: '°' + unit_of_measurement: "°" power_on: name: Power on description: Powered off lights are temporarily turned on during the effect. diff --git a/homeassistant/components/light/manifest.json b/homeassistant/components/light/manifest.json index 27c504f6b91..c7cf2abc7c8 100644 --- a/homeassistant/components/light/manifest.json +++ b/homeassistant/components/light/manifest.json @@ -2,6 +2,6 @@ "domain": "light", "name": "Light", "documentation": "https://www.home-assistant.io/integrations/light", - "codeowners": [], + "codeowners": ["@home-assistant/core"], "quality_scale": "internal" } diff --git a/homeassistant/components/light/recorder.py b/homeassistant/components/light/recorder.py new file mode 100644 index 00000000000..9febb98fa41 --- /dev/null +++ b/homeassistant/components/light/recorder.py @@ -0,0 +1,22 @@ +"""Integration platform for recorder.""" +from __future__ import annotations + +from homeassistant.core import HomeAssistant, callback + +from . import ( + ATTR_EFFECT_LIST, + ATTR_MAX_MIREDS, + ATTR_MIN_MIREDS, + ATTR_SUPPORTED_COLOR_MODES, +) + + +@callback +def exclude_attributes(hass: HomeAssistant) -> set[str]: + """Exclude static attributes from being recorded in the database.""" + return { + ATTR_SUPPORTED_COLOR_MODES, + ATTR_EFFECT_LIST, + ATTR_MIN_MIREDS, + ATTR_MAX_MIREDS, + } diff --git a/homeassistant/components/light/services.yaml b/homeassistant/components/light/services.yaml index f3fe306eb90..06349c72035 100644 --- a/homeassistant/components/light/services.yaml +++ b/homeassistant/components/light/services.yaml @@ -18,12 +18,10 @@ turn_on: max: 300 unit_of_measurement: seconds rgb_color: - name: RGB-color - description: A list containing three integers between 0 and 255 representing the RGB (red, green, blue) color for the light. - advanced: true - example: "[255, 100, 100]" + name: Color + description: The color for the light (based on RGB - red, green, blue). selector: - object: + color_rgb: rgbw_color: name: RGBW-color description: A list containing four integers between 0 and 255 representing the RGBW (red, green, blue, white) color for the light. @@ -209,14 +207,12 @@ turn_on: selector: object: color_temp: - name: Color temperature (mireds) + name: Color temperature description: Color temperature for the light in mireds. - advanced: true selector: - number: - min: 153 - max: 500 - unit_of_measurement: mireds + color_temp: + min_mireds: 153 + max_mireds: 500 kelvin: name: Color temperature (Kelvin) description: Color temperature for the light in Kelvin. @@ -229,8 +225,7 @@ turn_on: unit_of_measurement: K brightness: name: Brightness value - description: - Number indicating brightness, where 0 turns the light + description: Number indicating brightness, where 0 turns the light off, 1 is the minimum brightness and 255 is the maximum brightness supported by the light. advanced: true @@ -240,8 +235,7 @@ turn_on: max: 255 brightness_pct: name: Brightness - description: - Number indicating percentage of full brightness, where 0 + description: Number indicating percentage of full brightness, where 0 turns the light off, 1 is the minimum brightness and 100 is the maximum brightness supported by the light. selector: @@ -290,8 +284,10 @@ turn_on: selector: select: options: - - long - - short + - label: "Long" + value: "long" + - label: "Short" + value: "short" effect: name: Effect description: Light effect. @@ -320,8 +316,10 @@ turn_off: selector: select: options: - - long - - short + - label: "Long" + value: "long" + - label: "Short" + value: "short" toggle: name: Toggle @@ -522,10 +520,7 @@ toggle: description: Color temperature for the light in mireds. advanced: true selector: - number: - min: 153 - max: 500 - unit_of_measurement: mireds + color_temp: kelvin: name: Color temperature (Kelvin) description: Color temperature for the light in Kelvin. @@ -556,8 +551,7 @@ toggle: max: 255 brightness_pct: name: Brightness - description: - Number indicating percentage of full brightness, where 0 + description: Number indicating percentage of full brightness, where 0 turns the light off, 1 is the minimum brightness and 100 is the maximum brightness supported by the light. selector: @@ -579,8 +573,10 @@ toggle: selector: select: options: - - long - - short + - label: "Long" + value: "long" + - label: "Short" + value: "short" effect: name: Effect description: Light effect. diff --git a/homeassistant/components/light/translations/el.json b/homeassistant/components/light/translations/el.json index 4b5c69c6101..dbba423dd2a 100644 --- a/homeassistant/components/light/translations/el.json +++ b/homeassistant/components/light/translations/el.json @@ -21,9 +21,9 @@ }, "state": { "_": { - "off": "\u039a\u03bb\u03b5\u03b9\u03c3\u03c4\u03cc", - "on": "\u0391\u03bd\u03bf\u03b9\u03c7\u03c4\u03cc" + "off": "\u0391\u03bd\u03b5\u03bd\u03b5\u03c1\u03b3\u03cc", + "on": "\u0395\u03bd\u03b5\u03c1\u03b3\u03cc" } }, - "title": "\u03a6\u03c9\u03c4\u03b9\u03c3\u03c4\u03b9\u03ba\u03ac" + "title": "\u03a6\u03c9\u03c2" } \ No newline at end of file diff --git a/homeassistant/components/light/translations/fr.json b/homeassistant/components/light/translations/fr.json index 7863f5ad5eb..bcb6cac594e 100644 --- a/homeassistant/components/light/translations/fr.json +++ b/homeassistant/components/light/translations/fr.json @@ -1,28 +1,28 @@ { "device_automation": { "action_type": { - "brightness_decrease": "Diminue la luminosit\u00e9 de {entity_name}", - "brightness_increase": "Augmentez la luminosit\u00e9 de {entity_name}", - "flash": "Flash {entity_name}", + "brightness_decrease": "Diminuer la luminosit\u00e9 de {entity_name}", + "brightness_increase": "Augmenter la luminosit\u00e9 de {entity_name}", + "flash": "Faire clignoter {entity_name}", "toggle": "Basculer {entity_name}", "turn_off": "\u00c9teindre {entity_name}", "turn_on": "Allumer {entity_name}" }, "condition_type": { - "is_off": "{entity_name} est \u00e9teint", - "is_on": "{entity_name} est allum\u00e9" + "is_off": "{entity_name} est \u00e9teinte", + "is_on": "{entity_name} est allum\u00e9e" }, "trigger_type": { - "changed_states": "{entity_name} activ\u00e9 ou d\u00e9sactiv\u00e9", - "toggled": "{entity_name} activ\u00e9 ou d\u00e9sactiv\u00e9", - "turned_off": "{entity_name} est d\u00e9sactiv\u00e9", - "turned_on": "{entity_name} activ\u00e9" + "changed_states": "{entity_name} a \u00e9t\u00e9 allum\u00e9e ou \u00e9teinte", + "toggled": "{entity_name} a \u00e9t\u00e9 allum\u00e9e ou \u00e9teinte", + "turned_off": "{entity_name} a \u00e9t\u00e9 \u00e9teinte", + "turned_on": "{entity_name} a \u00e9t\u00e9 allum\u00e9e" } }, "state": { "_": { - "off": "Inactif", - "on": "Actif" + "off": "\u00c9teinte", + "on": "Allum\u00e9e" } }, "title": "Lumi\u00e8re" diff --git a/homeassistant/components/litejet/strings.json b/homeassistant/components/litejet/strings.json index 426dcd374af..398f1a1e5aa 100644 --- a/homeassistant/components/litejet/strings.json +++ b/homeassistant/components/litejet/strings.json @@ -26,4 +26,4 @@ } } } -} \ No newline at end of file +} diff --git a/homeassistant/components/litejet/translations/fr.json b/homeassistant/components/litejet/translations/fr.json index b8ca0adb3d4..b5f533d5fce 100644 --- a/homeassistant/components/litejet/translations/fr.json +++ b/homeassistant/components/litejet/translations/fr.json @@ -11,7 +11,7 @@ "data": { "port": "Port" }, - "description": "Connectez le port RS232-2 du LiteJet \u00e0 votre ordinateur et entrez le chemin d'acc\u00e8s au p\u00e9riph\u00e9rique de port s\u00e9rie. \n\n Le LiteJet MCP doit \u00eatre configur\u00e9 pour 19,2 K bauds, 8 bits de donn\u00e9es, 1 bit d'arr\u00eat, sans parit\u00e9 et pour transmettre un \u00abCR\u00bb apr\u00e8s chaque r\u00e9ponse.", + "description": "Connectez le port RS232-2 du LiteJet \u00e0 votre ordinateur puis entrez le chemin d'acc\u00e8s au p\u00e9riph\u00e9rique sur port s\u00e9rie. \n\nLe LiteJet MCP doit \u00eatre configur\u00e9 pour 19,2\u00a0kilobauds, 8\u00a0bits de donn\u00e9es, 1\u00a0bit d'arr\u00eat, sans parit\u00e9 et pour transmettre un \u00ab\u00a0CR\u00a0\u00bb apr\u00e8s chaque r\u00e9ponse.", "title": "Connectez-vous \u00e0 LiteJet" } } diff --git a/homeassistant/components/litterrobot/manifest.json b/homeassistant/components/litterrobot/manifest.json index b404762fbf3..a07f13a47b5 100644 --- a/homeassistant/components/litterrobot/manifest.json +++ b/homeassistant/components/litterrobot/manifest.json @@ -3,12 +3,8 @@ "name": "Litter-Robot", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/litterrobot", - "requirements": [ - "pylitterbot==2021.12.0" - ], - "codeowners": [ - "@natekspencer" - ], + "requirements": ["pylitterbot==2022.3.0"], + "codeowners": ["@natekspencer"], "iot_class": "cloud_polling", "loggers": ["pylitterbot"] -} \ No newline at end of file +} diff --git a/homeassistant/components/litterrobot/translations/fr.json b/homeassistant/components/litterrobot/translations/fr.json index aa84ec33d8c..744b9c6a862 100644 --- a/homeassistant/components/litterrobot/translations/fr.json +++ b/homeassistant/components/litterrobot/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/local_ip/translations/fr.json b/homeassistant/components/local_ip/translations/fr.json index d7d0eef17c1..a527127ccdf 100644 --- a/homeassistant/components/local_ip/translations/fr.json +++ b/homeassistant/components/local_ip/translations/fr.json @@ -5,7 +5,7 @@ }, "step": { "user": { - "description": "Voulez-vous commencer la configuration ?", + "description": "Voulez-vous commencer la configuration\u00a0?", "title": "Adresse IP locale" } } diff --git a/homeassistant/components/locative/translations/fr.json b/homeassistant/components/locative/translations/fr.json index 45a6f6fe594..75dd083d5a3 100644 --- a/homeassistant/components/locative/translations/fr.json +++ b/homeassistant/components/locative/translations/fr.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "cloud_not_connected": "Pas connect\u00e9 \u00e0 Home Assistant Cloud", + "cloud_not_connected": "Non connect\u00e9 \u00e0 Home Assistant Cloud.", "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible.", "webhook_not_internet_accessible": "Votre installation de Home Assistant doit \u00eatre accessible depuis internet pour recevoir des messages webhook." }, @@ -10,7 +10,7 @@ }, "step": { "user": { - "description": "Voulez-vous commencer la configuration ?", + "description": "Voulez-vous commencer la configuration\u00a0?", "title": "Configurer le Locative Webhook" } } diff --git a/homeassistant/components/lock/manifest.json b/homeassistant/components/lock/manifest.json index b44a66613b0..f93d2962ea3 100644 --- a/homeassistant/components/lock/manifest.json +++ b/homeassistant/components/lock/manifest.json @@ -2,6 +2,6 @@ "domain": "lock", "name": "Lock", "documentation": "https://www.home-assistant.io/integrations/lock", - "codeowners": [], + "codeowners": ["@home-assistant/core"], "quality_scale": "internal" } diff --git a/homeassistant/components/lock/translations/el.json b/homeassistant/components/lock/translations/el.json index a992279f497..976a2988aa8 100644 --- a/homeassistant/components/lock/translations/el.json +++ b/homeassistant/components/lock/translations/el.json @@ -20,5 +20,5 @@ "unlocked": "\u039e\u03b5\u03ba\u03bb\u03b5\u03af\u03b4\u03c9\u03c4\u03b7" } }, - "title": "\u039a\u03bb\u03b5\u03af\u03b4\u03c9\u03bc\u03b1" + "title": "\u039a\u03bb\u03b5\u03b9\u03b4\u03b1\u03c1\u03b9\u03ac" } \ No newline at end of file diff --git a/homeassistant/components/logbook/__init__.py b/homeassistant/components/logbook/__init__.py index 28b0460ac7a..8b00311a9e7 100644 --- a/homeassistant/components/logbook/__init__.py +++ b/homeassistant/components/logbook/__init__.py @@ -1,13 +1,19 @@ """Event parser and human readable log generator.""" +from __future__ import annotations + +from collections.abc import Iterable from contextlib import suppress -from datetime import timedelta +from datetime import datetime as dt, timedelta from http import HTTPStatus from itertools import groupby import json import re +from typing import Any import sqlalchemy from sqlalchemy.orm import aliased +from sqlalchemy.orm.query import Query +from sqlalchemy.orm.session import Session from sqlalchemy.sql.expression import literal import voluptuous as vol @@ -15,8 +21,10 @@ from homeassistant.components import frontend from homeassistant.components.automation import EVENT_AUTOMATION_TRIGGERED from homeassistant.components.history import sqlalchemy_filter_from_include_exclude_conf from homeassistant.components.http import HomeAssistantView +from homeassistant.components.recorder import get_instance from homeassistant.components.recorder.models import ( Events, + StateAttributes, States, process_timestamp_to_utc_isoformat, ) @@ -56,13 +64,14 @@ from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass import homeassistant.util.dt as dt_util -ENTITY_ID_JSON_TEMPLATE = '"entity_id":"{}"' +ENTITY_ID_JSON_TEMPLATE = '%"entity_id":"{}"%' ENTITY_ID_JSON_EXTRACT = re.compile('"entity_id": ?"([^"]+)"') DOMAIN_JSON_EXTRACT = re.compile('"domain": ?"([^"]+)"') ICON_JSON_EXTRACT = re.compile('"icon": ?"([^"]+)"') ATTR_MESSAGE = "message" -CONTINUOUS_DOMAINS = ["proximity", "sensor"] +CONTINUOUS_DOMAINS = {"proximity", "sensor"} +CONTINUOUS_ENTITY_ID_LIKE = [f"{domain}.%" for domain in CONTINUOUS_DOMAINS] DOMAIN = "logbook" @@ -70,7 +79,7 @@ GROUP_BY_MINUTES = 15 EMPTY_JSON_OBJECT = "{}" UNIT_OF_MEASUREMENT_JSON = '"unit_of_measurement":' - +UNIT_OF_MEASUREMENT_JSON_LIKE = f"%{UNIT_OF_MEASUREMENT_JSON}%" HA_DOMAIN_ENTITY_ID = f"{HA_DOMAIN}._" CONFIG_SCHEMA = vol.Schema( @@ -254,7 +263,7 @@ class LogbookView(HomeAssistantView): ) ) - return await hass.async_add_executor_job(json_events) + return await get_instance(hass).async_add_executor_job(json_events) def humanify(hass, events, entity_attr_cache, context_lookup): @@ -486,42 +495,53 @@ def _get_events( ) -def _generate_events_query(session): +def _generate_events_query(session: Session) -> Query: return session.query( *EVENT_COLUMNS, States.state, States.entity_id, - States.domain, States.attributes, + StateAttributes.shared_attrs, ) -def _generate_events_query_without_states(session): +def _generate_events_query_without_states(session: Session) -> Query: return session.query( *EVENT_COLUMNS, literal(value=None, type_=sqlalchemy.String).label("state"), literal(value=None, type_=sqlalchemy.String).label("entity_id"), - literal(value=None, type_=sqlalchemy.String).label("domain"), literal(value=None, type_=sqlalchemy.Text).label("attributes"), + literal(value=None, type_=sqlalchemy.Text).label("shared_attrs"), ) -def _generate_states_query(session, start_day, end_day, old_state, entity_ids): +def _generate_states_query( + session: Session, + start_day: dt, + end_day: dt, + old_state: States, + entity_ids: Iterable[str], +) -> Query: return ( _generate_events_query(session) .outerjoin(Events, (States.event_id == Events.event_id)) .outerjoin(old_state, (States.old_state_id == old_state.state_id)) .filter(_missing_state_matcher(old_state)) - .filter(_continuous_entity_matcher()) + .filter(_not_continuous_entity_matcher()) .filter((States.last_updated > start_day) & (States.last_updated < end_day)) .filter( (States.last_updated == States.last_changed) & States.entity_id.in_(entity_ids) ) + .outerjoin( + StateAttributes, (States.attributes_id == StateAttributes.attributes_id) + ) ) -def _apply_events_types_and_states_filter(hass, query, old_state): +def _apply_events_types_and_states_filter( + hass: HomeAssistant, query: Query, old_state: States +) -> Query: events_query = ( query.outerjoin(States, (Events.event_id == States.event_id)) .outerjoin(old_state, (States.old_state_id == old_state.state_id)) @@ -530,13 +550,16 @@ def _apply_events_types_and_states_filter(hass, query, old_state): | _missing_state_matcher(old_state) ) .filter( - (Events.event_type != EVENT_STATE_CHANGED) | _continuous_entity_matcher() + (Events.event_type != EVENT_STATE_CHANGED) + | _not_continuous_entity_matcher() ) ) - return _apply_event_types_filter(hass, events_query, ALL_EVENT_TYPES) + return _apply_event_types_filter(hass, events_query, ALL_EVENT_TYPES).outerjoin( + StateAttributes, (States.attributes_id == StateAttributes.attributes_id) + ) -def _missing_state_matcher(old_state): +def _missing_state_matcher(old_state: States) -> Any: # The below removes state change events that do not have # and old_state or the old_state is missing (newly added entities) # or the new_state is missing (removed entities) @@ -547,34 +570,64 @@ def _missing_state_matcher(old_state): ) -def _continuous_entity_matcher(): - # - # Prefilter out continuous domains that have - # ATTR_UNIT_OF_MEASUREMENT as its much faster in sql. - # +def _not_continuous_entity_matcher() -> Any: + """Match non continuous entities.""" return sqlalchemy.or_( - sqlalchemy.not_(States.domain.in_(CONTINUOUS_DOMAINS)), - sqlalchemy.not_(States.attributes.contains(UNIT_OF_MEASUREMENT_JSON)), + _not_continuous_domain_matcher(), + sqlalchemy.and_( + _continuous_domain_matcher, _not_uom_attributes_matcher() + ).self_group(), ) -def _apply_event_time_filter(events_query, start_day, end_day): +def _not_continuous_domain_matcher() -> Any: + """Match not continuous domains.""" + return sqlalchemy.and_( + *[ + ~States.entity_id.like(entity_domain) + for entity_domain in CONTINUOUS_ENTITY_ID_LIKE + ], + ).self_group() + + +def _continuous_domain_matcher() -> Any: + """Match continuous domains.""" + return sqlalchemy.or_( + *[ + States.entity_id.like(entity_domain) + for entity_domain in CONTINUOUS_ENTITY_ID_LIKE + ], + ).self_group() + + +def _not_uom_attributes_matcher() -> Any: + """Prefilter ATTR_UNIT_OF_MEASUREMENT as its much faster in sql.""" + return ~StateAttributes.shared_attrs.like( + UNIT_OF_MEASUREMENT_JSON_LIKE + ) | ~States.attributes.like(UNIT_OF_MEASUREMENT_JSON_LIKE) + + +def _apply_event_time_filter(events_query: Query, start_day: dt, end_day: dt) -> Query: return events_query.filter( (Events.time_fired > start_day) & (Events.time_fired < end_day) ) -def _apply_event_types_filter(hass, query, event_types): +def _apply_event_types_filter( + hass: HomeAssistant, query: Query, event_types: list[str] +) -> Query: return query.filter( Events.event_type.in_(event_types + list(hass.data.get(DOMAIN, {}))) ) -def _apply_event_entity_id_matchers(events_query, entity_ids): +def _apply_event_entity_id_matchers( + events_query: Query, entity_ids: Iterable[str] +) -> Query: return events_query.filter( sqlalchemy.or_( *( - Events.event_data.contains(ENTITY_ID_JSON_TEMPLATE.format(entity_id)) + Events.event_data.like(ENTITY_ID_JSON_TEMPLATE.format(entity_id)) for entity_id in entity_ids ) ) @@ -681,7 +734,7 @@ class LazyEventPartialState: "event_type", "entity_id", "state", - "domain", + "_domain", "context_id", "context_user_id", "context_parent_id", @@ -694,22 +747,30 @@ class LazyEventPartialState: self._event_data = None self._time_fired_isoformat = None self._attributes = None + self._domain = None self.event_type = self._row.event_type self.entity_id = self._row.entity_id self.state = self._row.state - self.domain = self._row.domain self.context_id = self._row.context_id self.context_user_id = self._row.context_user_id self.context_parent_id = self._row.context_parent_id self.time_fired_minute = self._row.time_fired.minute + @property + def domain(self): + """Return the domain for the state.""" + if self._domain is None: + self._domain = split_entity_id(self.entity_id)[0] + return self._domain + @property def attributes_icon(self): """Extract the icon from the decoded attributes or json.""" if self._attributes: return self._attributes.get(ATTR_ICON) - - result = ICON_JSON_EXTRACT.search(self._row.attributes) + result = ICON_JSON_EXTRACT.search( + self._row.shared_attrs or self._row.attributes + ) return result and result.group(1) @property @@ -733,14 +794,12 @@ class LazyEventPartialState: @property def attributes(self): """State attributes.""" - if not self._attributes: - if ( - self._row.attributes is None - or self._row.attributes == EMPTY_JSON_OBJECT - ): + if self._attributes is None: + source = self._row.shared_attrs or self._row.attributes + if source == EMPTY_JSON_OBJECT or source is None: self._attributes = {} else: - self._attributes = json.loads(self._row.attributes) + self._attributes = json.loads(source) return self._attributes @property @@ -771,12 +830,12 @@ class EntityAttributeCache: that are expected to change state. """ - def __init__(self, hass): + def __init__(self, hass: HomeAssistant) -> None: """Init the cache.""" self._hass = hass - self._cache = {} + self._cache: dict[str, dict[str, Any]] = {} - def get(self, entity_id, attribute, event): + def get(self, entity_id: str, attribute: str, event: LazyEventPartialState) -> Any: """Lookup an attribute for an entity or get it from the cache.""" if entity_id in self._cache: if attribute in self._cache[entity_id]: diff --git a/homeassistant/components/logbook/manifest.json b/homeassistant/components/logbook/manifest.json index 58bc71959b3..66c0348a2ac 100644 --- a/homeassistant/components/logbook/manifest.json +++ b/homeassistant/components/logbook/manifest.json @@ -3,6 +3,6 @@ "name": "Logbook", "documentation": "https://www.home-assistant.io/integrations/logbook", "dependencies": ["frontend", "http", "recorder"], - "codeowners": [], + "codeowners": ["@home-assistant/core"], "quality_scale": "internal" } diff --git a/homeassistant/components/logger/services.yaml b/homeassistant/components/logger/services.yaml index 5930a4e5d9e..c20d1171bb2 100644 --- a/homeassistant/components/logger/services.yaml +++ b/homeassistant/components/logger/services.yaml @@ -8,12 +8,18 @@ set_default_level: selector: select: options: - - debug - - info - - warning - - error - - fatal - - critical + - label: "Debug" + value: "debug" + - label: "Info" + value: "info" + - label: "Warning" + value: "warning" + - label: "Error" + value: "error" + - label: "Fatal" + value: "fatal" + - label: "Critical" + value: "critical" set_level: name: Set level diff --git a/homeassistant/components/logi_circle/services.yaml b/homeassistant/components/logi_circle/services.yaml index 248701a5d45..10df6c564b4 100644 --- a/homeassistant/components/logi_circle/services.yaml +++ b/homeassistant/components/logi_circle/services.yaml @@ -18,8 +18,8 @@ set_config: selector: select: options: - - 'LED' - - 'RECORDING_MODE' + - "LED" + - "RECORDING_MODE" value: name: Value description: "Operation value." diff --git a/homeassistant/components/logi_circle/translations/fr.json b/homeassistant/components/logi_circle/translations/fr.json index 6bd22f473e7..5c4bb6fab9d 100644 --- a/homeassistant/components/logi_circle/translations/fr.json +++ b/homeassistant/components/logi_circle/translations/fr.json @@ -7,9 +7,9 @@ "missing_configuration": "Le composant n'est pas configur\u00e9. Veuillez suivre la documentation." }, "error": { - "authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration de l'URL d'authentification d\u00e9pass\u00e9.", + "authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration de l'URL d'authentification expir\u00e9.", "follow_link": "Veuillez suivre le lien et vous authentifier avant d'appuyer sur Soumettre.", - "invalid_auth": "Authentification invalide" + "invalid_auth": "Authentification non valide" }, "step": { "auth": { diff --git a/homeassistant/components/lookin/entity.py b/homeassistant/components/lookin/entity.py index 6ff167d86fe..1c641b76f32 100644 --- a/homeassistant/components/lookin/entity.py +++ b/homeassistant/components/lookin/entity.py @@ -52,11 +52,11 @@ class LookinDeviceMixIn: self._lookin_udp_subs = lookin_data.lookin_udp_subs -class LookinDeviceCoordinatorEntity(LookinDeviceMixIn, CoordinatorEntity): +class LookinDeviceCoordinatorEntity( + LookinDeviceMixIn, CoordinatorEntity[LookinDataUpdateCoordinator] +): """A lookin device entity on the device itself that uses the coordinator.""" - coordinator: LookinDataUpdateCoordinator - _attr_should_poll = False def __init__(self, lookin_data: LookinData) -> None: @@ -84,11 +84,11 @@ class LookinEntityMixIn: self._function_names = {function.name for function in self._device.functions} -class LookinCoordinatorEntity(LookinDeviceMixIn, LookinEntityMixIn, CoordinatorEntity): +class LookinCoordinatorEntity( + LookinDeviceMixIn, LookinEntityMixIn, CoordinatorEntity[LookinDataUpdateCoordinator] +): """A lookin device entity for an external device that uses the coordinator.""" - coordinator: LookinDataUpdateCoordinator - _attr_should_poll = False _attr_assumed_state = True diff --git a/homeassistant/components/lookin/strings.json b/homeassistant/components/lookin/strings.json index 1285be4abf0..641e1f45de7 100644 --- a/homeassistant/components/lookin/strings.json +++ b/homeassistant/components/lookin/strings.json @@ -28,4 +28,4 @@ "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]" } } -} \ No newline at end of file +} diff --git a/homeassistant/components/lovelace/cast.py b/homeassistant/components/lovelace/cast.py index 02280ebd182..bd06d142bd3 100644 --- a/homeassistant/components/lovelace/cast.py +++ b/homeassistant/components/lovelace/cast.py @@ -33,7 +33,7 @@ async def async_get_media_browser_root_object( return [] return [ BrowseMedia( - title="Lovelace", + title="Dashboards", media_class=MEDIA_CLASS_APP, media_content_id="", media_content_type=DOMAIN, diff --git a/homeassistant/components/lovelace/manifest.json b/homeassistant/components/lovelace/manifest.json index cc8f6ddab08..6f91a61b08c 100644 --- a/homeassistant/components/lovelace/manifest.json +++ b/homeassistant/components/lovelace/manifest.json @@ -1,6 +1,6 @@ { "domain": "lovelace", - "name": "Lovelace", + "name": "Dashboards", "documentation": "https://www.home-assistant.io/integrations/lovelace", "codeowners": ["@home-assistant/frontend"] } diff --git a/homeassistant/components/luftdaten/translations/fr.json b/homeassistant/components/luftdaten/translations/fr.json index 3acc1803fde..4eacd984ad8 100644 --- a/homeassistant/components/luftdaten/translations/fr.json +++ b/homeassistant/components/luftdaten/translations/fr.json @@ -3,7 +3,7 @@ "error": { "already_configured": "Le service est d\u00e9j\u00e0 configur\u00e9", "cannot_connect": "\u00c9chec de connexion", - "invalid_sensor": "Capteur non disponible ou invalide" + "invalid_sensor": "Capteur non disponible ou non valide" }, "step": { "user": { diff --git a/homeassistant/components/lutron_caseta/fan.py b/homeassistant/components/lutron_caseta/fan.py index 52fa1be65ed..cb00fa6f2c8 100644 --- a/homeassistant/components/lutron_caseta/fan.py +++ b/homeassistant/components/lutron_caseta/fan.py @@ -72,7 +72,6 @@ class LutronCasetaFan(LutronCasetaDevice, FanEntity): async def async_turn_on( self, - speed: str = None, percentage: int = None, preset_mode: str = None, **kwargs, diff --git a/homeassistant/components/lutron_caseta/translations/fr.json b/homeassistant/components/lutron_caseta/translations/fr.json index cdf584fcc00..c5f259167d2 100644 --- a/homeassistant/components/lutron_caseta/translations/fr.json +++ b/homeassistant/components/lutron_caseta/translations/fr.json @@ -66,11 +66,11 @@ "stop_2": "Arr\u00eat 2", "stop_3": "Arr\u00eat 3", "stop_4": "Arr\u00eat 4", - "stop_all": "Arr\u00eate tout" + "stop_all": "Arr\u00eater tout" }, "trigger_type": { - "press": "\" {subtype} \" appuy\u00e9", - "release": "\" {subtype} \" publi\u00e9" + "press": "\u00ab\u00a0{subtype}\u00a0\u00bb enfonc\u00e9", + "release": "\u00ab\u00a0{subtype}\u00a0\u00bb rel\u00e2ch\u00e9" } } } \ No newline at end of file diff --git a/homeassistant/components/lyric/climate.py b/homeassistant/components/lyric/climate.py index dfe88048ba4..56b5b6fd022 100644 --- a/homeassistant/components/lyric/climate.py +++ b/homeassistant/components/lyric/climate.py @@ -211,29 +211,35 @@ class LyricClimate(LyricDeviceEntity, ClimateEntity): """Return the temperature we try to reach.""" device = self.device if ( - not device.changeableValues.autoChangeoverActive - and HVAC_MODES[device.changeableValues.mode] != HVAC_MODE_OFF + device.changeableValues.autoChangeoverActive + or HVAC_MODES[device.changeableValues.mode] == HVAC_MODE_OFF ): - if self.hvac_mode == HVAC_MODE_COOL: - return device.changeableValues.coolSetpoint - return device.changeableValues.heatSetpoint - return None + return None + if self.hvac_mode == HVAC_MODE_COOL: + return device.changeableValues.coolSetpoint + return device.changeableValues.heatSetpoint @property def target_temperature_high(self) -> float | None: """Return the highbound target temperature we try to reach.""" device = self.device - if device.changeableValues.autoChangeoverActive: - return device.changeableValues.coolSetpoint - return None + if ( + not device.changeableValues.autoChangeoverActive + or HVAC_MODES[device.changeableValues.mode] == HVAC_MODE_OFF + ): + return None + return device.changeableValues.coolSetpoint @property def target_temperature_low(self) -> float | None: """Return the lowbound target temperature we try to reach.""" device = self.device - if device.changeableValues.autoChangeoverActive: - return device.changeableValues.heatSetpoint - return None + if ( + not device.changeableValues.autoChangeoverActive + or HVAC_MODES[device.changeableValues.mode] == HVAC_MODE_OFF + ): + return None + return device.changeableValues.heatSetpoint @property def preset_mode(self) -> str | None: @@ -269,6 +275,9 @@ class LyricClimate(LyricDeviceEntity, ClimateEntity): async def async_set_temperature(self, **kwargs) -> None: """Set new target temperature.""" + if self.hvac_mode == HVAC_MODE_OFF: + return + device = self.device target_temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW) target_temp_high = kwargs.get(ATTR_TARGET_TEMP_HIGH) diff --git a/homeassistant/components/lyric/config_flow.py b/homeassistant/components/lyric/config_flow.py index 38341113206..698e7e19a26 100644 --- a/homeassistant/components/lyric/config_flow.py +++ b/homeassistant/components/lyric/config_flow.py @@ -1,8 +1,6 @@ """Config flow for Honeywell Lyric.""" import logging -import voluptuous as vol - from homeassistant.helpers import config_entry_oauth2_flow from .const import DOMAIN @@ -27,10 +25,7 @@ class OAuth2FlowHandler( async def async_step_reauth_confirm(self, user_input=None): """Dialog that informs the user that reauth is required.""" if user_input is None: - return self.async_show_form( - step_id="reauth_confirm", - data_schema=vol.Schema({}), - ) + return self.async_show_form(step_id="reauth_confirm") return await self.async_step_user() async def async_oauth_create_entry(self, data: dict) -> dict: diff --git a/homeassistant/components/lyric/translations/fr.json b/homeassistant/components/lyric/translations/fr.json index 25992620652..694f777ca9a 100644 --- a/homeassistant/components/lyric/translations/fr.json +++ b/homeassistant/components/lyric/translations/fr.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration de l'URL d'authentification d\u00e9pass\u00e9.", + "authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration de l'URL d'authentification expir\u00e9.", "missing_configuration": "Le composant n'est pas configur\u00e9. Veuillez suivre la documentation.", "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" }, diff --git a/homeassistant/components/mailgun/translations/fr.json b/homeassistant/components/mailgun/translations/fr.json index a8d4b08ed83..80c961babba 100644 --- a/homeassistant/components/mailgun/translations/fr.json +++ b/homeassistant/components/mailgun/translations/fr.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "cloud_not_connected": "Pas connect\u00e9 \u00e0 Home Assistant Cloud", + "cloud_not_connected": "Non connect\u00e9 \u00e0 Home Assistant Cloud.", "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible.", "webhook_not_internet_accessible": "Votre installation de Home Assistant doit \u00eatre accessible depuis internet pour recevoir des messages webhook." }, diff --git a/homeassistant/components/mazda/__init__.py b/homeassistant/components/mazda/__init__.py index 8e25e08dc47..2af4e46bb1a 100644 --- a/homeassistant/components/mazda/__init__.py +++ b/homeassistant/components/mazda/__init__.py @@ -37,7 +37,14 @@ from .const import DATA_CLIENT, DATA_COORDINATOR, DATA_VEHICLES, DOMAIN, SERVICE _LOGGER = logging.getLogger(__name__) -PLATFORMS = [Platform.DEVICE_TRACKER, Platform.LOCK, Platform.SENSOR] +PLATFORMS = [ + Platform.BINARY_SENSOR, + Platform.BUTTON, + Platform.DEVICE_TRACKER, + Platform.LOCK, + Platform.SENSOR, + Platform.SWITCH, +] async def with_timeout(task, timeout_seconds=10): @@ -102,6 +109,25 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if vehicle_id == 0 or api_client is None: raise HomeAssistantError("Vehicle ID not found") + if service_call.service in ( + "start_engine", + "stop_engine", + "turn_on_hazard_lights", + "turn_off_hazard_lights", + ): + _LOGGER.warning( + "The mazda.%s service is deprecated and has been replaced by a button entity; " + "Please use the button entity instead", + service_call.service, + ) + + if service_call.service in ("start_charging", "stop_charging"): + _LOGGER.warning( + "The mazda.%s service is deprecated and has been replaced by a switch entity; " + "Please use the charging switch entity instead", + service_call.service, + ) + api_method = getattr(api_client, service_call.service) try: if service_call.service == "send_poi": @@ -195,17 +221,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Register services for service in SERVICES: - if service == "send_poi": - hass.services.async_register( - DOMAIN, - service, - async_handle_service_call, - schema=service_schema_send_poi, - ) - else: - hass.services.async_register( - DOMAIN, service, async_handle_service_call, schema=service_schema - ) + hass.services.async_register( + DOMAIN, + service, + async_handle_service_call, + schema=service_schema_send_poi if service == "send_poi" else service_schema, + ) return True diff --git a/homeassistant/components/mazda/binary_sensor.py b/homeassistant/components/mazda/binary_sensor.py new file mode 100644 index 00000000000..cc60d6318c7 --- /dev/null +++ b/homeassistant/components/mazda/binary_sensor.py @@ -0,0 +1,135 @@ +"""Platform for Mazda binary sensor integration.""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +from typing import Any + +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + BinarySensorEntity, + BinarySensorEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import MazdaEntity +from .const import DATA_CLIENT, DATA_COORDINATOR, DOMAIN + + +@dataclass +class MazdaBinarySensorRequiredKeysMixin: + """Mixin for required keys.""" + + # Suffix to be appended to the vehicle name to obtain the binary sensor name + name_suffix: str + + # Function to determine the value for this binary sensor, given the coordinator data + value_fn: Callable[[dict[str, Any]], bool] + + +@dataclass +class MazdaBinarySensorEntityDescription( + BinarySensorEntityDescription, MazdaBinarySensorRequiredKeysMixin +): + """Describes a Mazda binary sensor entity.""" + + # Function to determine whether the vehicle supports this binary sensor, given the coordinator data + is_supported: Callable[[dict[str, Any]], bool] = lambda data: True + + +def _plugged_in_supported(data): + """Determine if 'plugged in' binary sensor is supported.""" + return ( + data["isElectric"] and data["evStatus"]["chargeInfo"]["pluggedIn"] is not None + ) + + +BINARY_SENSOR_ENTITIES = [ + MazdaBinarySensorEntityDescription( + key="driver_door", + name_suffix="Driver Door", + icon="mdi:car-door", + device_class=BinarySensorDeviceClass.DOOR, + value_fn=lambda data: data["status"]["doors"]["driverDoorOpen"], + ), + MazdaBinarySensorEntityDescription( + key="passenger_door", + name_suffix="Passenger Door", + icon="mdi:car-door", + device_class=BinarySensorDeviceClass.DOOR, + value_fn=lambda data: data["status"]["doors"]["passengerDoorOpen"], + ), + MazdaBinarySensorEntityDescription( + key="rear_left_door", + name_suffix="Rear Left Door", + icon="mdi:car-door", + device_class=BinarySensorDeviceClass.DOOR, + value_fn=lambda data: data["status"]["doors"]["rearLeftDoorOpen"], + ), + MazdaBinarySensorEntityDescription( + key="rear_right_door", + name_suffix="Rear Right Door", + icon="mdi:car-door", + device_class=BinarySensorDeviceClass.DOOR, + value_fn=lambda data: data["status"]["doors"]["rearRightDoorOpen"], + ), + MazdaBinarySensorEntityDescription( + key="trunk", + name_suffix="Trunk", + icon="mdi:car-back", + device_class=BinarySensorDeviceClass.DOOR, + value_fn=lambda data: data["status"]["doors"]["trunkOpen"], + ), + MazdaBinarySensorEntityDescription( + key="hood", + name_suffix="Hood", + icon="mdi:car", + device_class=BinarySensorDeviceClass.DOOR, + value_fn=lambda data: data["status"]["doors"]["hoodOpen"], + ), + MazdaBinarySensorEntityDescription( + key="ev_plugged_in", + name_suffix="Plugged In", + device_class=BinarySensorDeviceClass.PLUG, + is_supported=_plugged_in_supported, + value_fn=lambda data: data["evStatus"]["chargeInfo"]["pluggedIn"], + ), +] + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the sensor platform.""" + client = hass.data[DOMAIN][config_entry.entry_id][DATA_CLIENT] + coordinator = hass.data[DOMAIN][config_entry.entry_id][DATA_COORDINATOR] + + async_add_entities( + MazdaBinarySensorEntity(client, coordinator, index, description) + for index, data in enumerate(coordinator.data) + for description in BINARY_SENSOR_ENTITIES + if description.is_supported(data) + ) + + +class MazdaBinarySensorEntity(MazdaEntity, BinarySensorEntity): + """Representation of a Mazda vehicle binary sensor.""" + + entity_description: MazdaBinarySensorEntityDescription + + def __init__(self, client, coordinator, index, description): + """Initialize Mazda binary sensor.""" + super().__init__(client, coordinator, index) + self.entity_description = description + + self._attr_name = f"{self.vehicle_name} {description.name_suffix}" + self._attr_unique_id = f"{self.vin}_{description.key}" + + @property + def is_on(self): + """Return the state of the binary sensor.""" + return self.entity_description.value_fn(self.data) diff --git a/homeassistant/components/mazda/button.py b/homeassistant/components/mazda/button.py new file mode 100644 index 00000000000..e747cb33dc2 --- /dev/null +++ b/homeassistant/components/mazda/button.py @@ -0,0 +1,156 @@ +"""Platform for Mazda button integration.""" +from __future__ import annotations + +from collections.abc import Awaitable, Callable +from dataclasses import dataclass +from typing import Any + +from pymazda import ( + Client as MazdaAPIClient, + MazdaAccountLockedException, + MazdaAPIEncryptionException, + MazdaAuthenticationException, + MazdaException, + MazdaLoginFailedException, + MazdaTokenExpiredException, +) + +from homeassistant.components.button import ButtonEntity, ButtonEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from . import MazdaEntity +from .const import DATA_CLIENT, DATA_COORDINATOR, DOMAIN + + +async def handle_button_press( + client: MazdaAPIClient, + key: str, + vehicle_id: int, + coordinator: DataUpdateCoordinator, +) -> None: + """Handle a press for a Mazda button entity.""" + api_method = getattr(client, key) + + try: + await api_method(vehicle_id) + except ( + MazdaException, + MazdaAuthenticationException, + MazdaAccountLockedException, + MazdaTokenExpiredException, + MazdaAPIEncryptionException, + MazdaLoginFailedException, + ) as ex: + raise HomeAssistantError(ex) from ex + + +async def handle_refresh_vehicle_status( + client: MazdaAPIClient, + key: str, + vehicle_id: int, + coordinator: DataUpdateCoordinator, +) -> None: + """Handle a request to refresh the vehicle status.""" + await handle_button_press(client, key, vehicle_id, coordinator) + + await coordinator.async_request_refresh() + + +@dataclass +class MazdaButtonRequiredKeysMixin: + """Mixin for required keys.""" + + # Suffix to be appended to the vehicle name to obtain the button name + name_suffix: str + + +@dataclass +class MazdaButtonEntityDescription( + ButtonEntityDescription, MazdaButtonRequiredKeysMixin +): + """Describes a Mazda button entity.""" + + # Function to determine whether the vehicle supports this button, given the coordinator data + is_supported: Callable[[dict[str, Any]], bool] = lambda data: True + + async_press: Callable[ + [MazdaAPIClient, str, int, DataUpdateCoordinator], Awaitable + ] = handle_button_press + + +BUTTON_ENTITIES = [ + MazdaButtonEntityDescription( + key="start_engine", + name_suffix="Start Engine", + icon="mdi:engine", + ), + MazdaButtonEntityDescription( + key="stop_engine", + name_suffix="Stop Engine", + icon="mdi:engine-off", + ), + MazdaButtonEntityDescription( + key="turn_on_hazard_lights", + name_suffix="Turn On Hazard Lights", + icon="mdi:hazard-lights", + ), + MazdaButtonEntityDescription( + key="turn_off_hazard_lights", + name_suffix="Turn Off Hazard Lights", + icon="mdi:hazard-lights", + ), + MazdaButtonEntityDescription( + key="refresh_vehicle_status", + name_suffix="Refresh Status", + icon="mdi:refresh", + async_press=handle_refresh_vehicle_status, + is_supported=lambda data: data["isElectric"], + ), +] + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the button platform.""" + client = hass.data[DOMAIN][config_entry.entry_id][DATA_CLIENT] + coordinator = hass.data[DOMAIN][config_entry.entry_id][DATA_COORDINATOR] + + async_add_entities( + MazdaButtonEntity(client, coordinator, index, description) + for index, data in enumerate(coordinator.data) + for description in BUTTON_ENTITIES + if description.is_supported(data) + ) + + +class MazdaButtonEntity(MazdaEntity, ButtonEntity): + """Representation of a Mazda button.""" + + entity_description: MazdaButtonEntityDescription + + def __init__( + self, + client: MazdaAPIClient, + coordinator: DataUpdateCoordinator, + index: int, + description: MazdaButtonEntityDescription, + ) -> None: + """Initialize Mazda button.""" + super().__init__(client, coordinator, index) + self.entity_description = description + + self._attr_name = f"{self.vehicle_name} {description.name_suffix}" + self._attr_unique_id = f"{self.vin}_{description.key}" + + async def async_press(self) -> None: + """Press the button.""" + await self.entity_description.async_press( + self.client, self.entity_description.key, self.vehicle_id, self.coordinator + ) diff --git a/homeassistant/components/mazda/sensor.py b/homeassistant/components/mazda/sensor.py index d94a4798630..99f8c74d64d 100644 --- a/homeassistant/components/mazda/sensor.py +++ b/homeassistant/components/mazda/sensor.py @@ -3,6 +3,7 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass +from typing import Any from homeassistant.components.sensor import ( SensorDeviceClass, @@ -35,7 +36,7 @@ class MazdaSensorRequiredKeysMixin: name_suffix: str # Function to determine the value for this sensor, given the coordinator data and the configured unit system - value: Callable[[dict, UnitSystem], StateType] + value: Callable[[dict[str, Any], UnitSystem], StateType] @dataclass @@ -45,7 +46,7 @@ class MazdaSensorEntityDescription( """Describes a Mazda sensor entity.""" # Function to determine whether the vehicle supports this sensor, given the coordinator data - is_supported: Callable[[dict], bool] = lambda data: True + is_supported: Callable[[dict[str, Any]], bool] = lambda data: True # Function to determine the unit of measurement for this sensor, given the configured unit system # Falls back to description.native_unit_of_measurement if it is not provided diff --git a/homeassistant/components/mazda/switch.py b/homeassistant/components/mazda/switch.py new file mode 100644 index 00000000000..3ab3028425f --- /dev/null +++ b/homeassistant/components/mazda/switch.py @@ -0,0 +1,70 @@ +"""Platform for Mazda switch integration.""" +from pymazda import Client as MazdaAPIClient + +from homeassistant.components.switch import SwitchEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from . import MazdaEntity +from .const import DATA_CLIENT, DATA_COORDINATOR, DOMAIN + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the switch platform.""" + client = hass.data[DOMAIN][config_entry.entry_id][DATA_CLIENT] + coordinator = hass.data[DOMAIN][config_entry.entry_id][DATA_COORDINATOR] + + async_add_entities( + MazdaChargingSwitch(client, coordinator, index) + for index, data in enumerate(coordinator.data) + if data["isElectric"] + ) + + +class MazdaChargingSwitch(MazdaEntity, SwitchEntity): + """Class for the charging switch.""" + + _attr_icon = "mdi:ev-station" + + def __init__( + self, + client: MazdaAPIClient, + coordinator: DataUpdateCoordinator, + index: int, + ) -> None: + """Initialize Mazda charging switch.""" + super().__init__(client, coordinator, index) + + self._attr_name = f"{self.vehicle_name} Charging" + self._attr_unique_id = self.vin + + @property + def is_on(self): + """Return true if the vehicle is charging.""" + return self.data["evStatus"]["chargeInfo"]["charging"] + + async def refresh_status_and_write_state(self): + """Request a status update, retrieve it through the coordinator, and write the state.""" + await self.client.refresh_vehicle_status(self.vehicle_id) + + await self.coordinator.async_request_refresh() + + self.async_write_ha_state() + + async def async_turn_on(self, **kwargs): + """Start charging the vehicle.""" + await self.client.start_charging(self.vehicle_id) + + await self.refresh_status_and_write_state() + + async def async_turn_off(self, **kwargs): + """Stop charging the vehicle.""" + await self.client.stop_charging(self.vehicle_id) + + await self.refresh_status_and_write_state() diff --git a/homeassistant/components/mazda/translations/fr.json b/homeassistant/components/mazda/translations/fr.json index 1f6f442a3ae..8024852de46 100644 --- a/homeassistant/components/mazda/translations/fr.json +++ b/homeassistant/components/mazda/translations/fr.json @@ -7,13 +7,13 @@ "error": { "account_locked": "Compte bloqu\u00e9. Veuillez r\u00e9essayer plus tard.", "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "step": { "user": { "data": { - "email": "Email", + "email": "Courriel", "password": "Mot de passe", "region": "R\u00e9gion" }, diff --git a/homeassistant/components/mcp23017/__init__.py b/homeassistant/components/mcp23017/__init__.py deleted file mode 100644 index 14799217a6b..00000000000 --- a/homeassistant/components/mcp23017/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -"""Support for I2C MCP23017 chip.""" - -DOMAIN = "mcp23017" diff --git a/homeassistant/components/mcp23017/binary_sensor.py b/homeassistant/components/mcp23017/binary_sensor.py deleted file mode 100644 index 161b1ceaac8..00000000000 --- a/homeassistant/components/mcp23017/binary_sensor.py +++ /dev/null @@ -1,104 +0,0 @@ -"""Support for binary sensor using I2C MCP23017 chip.""" -from __future__ import annotations - -import logging - -from adafruit_mcp230xx.mcp23017 import MCP23017 -import board -import busio -import digitalio -import voluptuous as vol - -from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity -from homeassistant.const import DEVICE_DEFAULT_NAME -from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType - -CONF_INVERT_LOGIC = "invert_logic" -CONF_I2C_ADDRESS = "i2c_address" -CONF_PINS = "pins" -CONF_PULL_MODE = "pull_mode" - -MODE_UP = "UP" -MODE_DOWN = "DOWN" - -DEFAULT_INVERT_LOGIC = False -DEFAULT_I2C_ADDRESS = 0x20 -DEFAULT_PULL_MODE = MODE_UP - -_SENSORS_SCHEMA = vol.Schema({cv.positive_int: cv.string}) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_PINS): _SENSORS_SCHEMA, - vol.Optional(CONF_INVERT_LOGIC, default=DEFAULT_INVERT_LOGIC): cv.boolean, - vol.Optional(CONF_PULL_MODE, default=DEFAULT_PULL_MODE): vol.All( - vol.Upper, vol.In([MODE_UP, MODE_DOWN]) - ), - vol.Optional(CONF_I2C_ADDRESS, default=DEFAULT_I2C_ADDRESS): vol.Coerce(int), - } -) - -_LOGGER = logging.getLogger(__name__) - - -def setup_platform( - hass: HomeAssistant, - config: ConfigType, - add_devices: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the MCP23017 binary sensors.""" - _LOGGER.warning( - "The MCP23017 I/O Expander integration is deprecated and will be removed " - "in Home Assistant Core 2022.4; this integration is removed under " - "Architectural Decision Record 0019, more information can be found here: " - "https://github.com/home-assistant/architecture/blob/master/adr/0019-GPIO.md" - ) - pull_mode = config[CONF_PULL_MODE] - invert_logic = config[CONF_INVERT_LOGIC] - i2c_address = config[CONF_I2C_ADDRESS] - - i2c = busio.I2C(board.SCL, board.SDA) - mcp = MCP23017(i2c, address=i2c_address) - - binary_sensors = [] - pins = config[CONF_PINS] - - for pin_num, pin_name in pins.items(): - pin = mcp.get_pin(pin_num) - binary_sensors.append( - MCP23017BinarySensor(pin_name, pin, pull_mode, invert_logic) - ) - - add_devices(binary_sensors, True) - - -class MCP23017BinarySensor(BinarySensorEntity): - """Represent a binary sensor that uses MCP23017.""" - - def __init__(self, name, pin, pull_mode, invert_logic): - """Initialize the MCP23017 binary sensor.""" - self._name = name or DEVICE_DEFAULT_NAME - self._pin = pin - self._pull_mode = pull_mode - self._invert_logic = invert_logic - self._state = None - self._pin.direction = digitalio.Direction.INPUT - self._pin.pull = digitalio.Pull.UP - - @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 - - def update(self): - """Update the GPIO state.""" - self._state = self._pin.value diff --git a/homeassistant/components/mcp23017/manifest.json b/homeassistant/components/mcp23017/manifest.json deleted file mode 100644 index e6f04ad1171..00000000000 --- a/homeassistant/components/mcp23017/manifest.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "domain": "mcp23017", - "name": "MCP23017 I/O Expander", - "documentation": "https://www.home-assistant.io/integrations/mcp23017", - "requirements": [ - "RPi.GPIO==0.7.1a4", - "adafruit-circuitpython-mcp230xx==2.2.2" - ], - "codeowners": ["@jardiamj"], - "iot_class": "local_polling", - "loggers": ["adafruit_mcp230xx"] -} diff --git a/homeassistant/components/mcp23017/switch.py b/homeassistant/components/mcp23017/switch.py deleted file mode 100644 index b67f20e3bf6..00000000000 --- a/homeassistant/components/mcp23017/switch.py +++ /dev/null @@ -1,110 +0,0 @@ -"""Support for switch sensor using I2C MCP23017 chip.""" -from __future__ import annotations - -import logging - -from adafruit_mcp230xx.mcp23017 import MCP23017 -import board -import busio -import digitalio -import voluptuous as vol - -from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity -from homeassistant.const import DEVICE_DEFAULT_NAME -from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType - -CONF_INVERT_LOGIC = "invert_logic" -CONF_I2C_ADDRESS = "i2c_address" -CONF_PINS = "pins" -CONF_PULL_MODE = "pull_mode" - -DEFAULT_INVERT_LOGIC = False -DEFAULT_I2C_ADDRESS = 0x20 - -_SWITCHES_SCHEMA = vol.Schema({cv.positive_int: cv.string}) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_PINS): _SWITCHES_SCHEMA, - vol.Optional(CONF_INVERT_LOGIC, default=DEFAULT_INVERT_LOGIC): cv.boolean, - vol.Optional(CONF_I2C_ADDRESS, default=DEFAULT_I2C_ADDRESS): vol.Coerce(int), - } -) - -_LOGGER = logging.getLogger(__name__) - - -def setup_platform( - hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the MCP23017 devices.""" - _LOGGER.warning( - "The MCP23017 I/O Expander integration is deprecated and will be removed " - "in Home Assistant Core 2022.4; this integration is removed under " - "Architectural Decision Record 0019, more information can be found here: " - "https://github.com/home-assistant/architecture/blob/master/adr/0019-GPIO.md" - ) - invert_logic = config.get(CONF_INVERT_LOGIC) - i2c_address = config.get(CONF_I2C_ADDRESS) - - i2c = busio.I2C(board.SCL, board.SDA) - mcp = MCP23017(i2c, address=i2c_address) - - switches = [] - pins = config[CONF_PINS] - for pin_num, pin_name in pins.items(): - pin = mcp.get_pin(pin_num) - switches.append(MCP23017Switch(pin_name, pin, invert_logic)) - add_entities(switches) - - -class MCP23017Switch(SwitchEntity): - """Representation of a MCP23017 output pin.""" - - def __init__(self, name, pin, invert_logic): - """Initialize the pin.""" - self._name = name or DEVICE_DEFAULT_NAME - self._pin = pin - self._invert_logic = invert_logic - self._state = False - - self._pin.direction = digitalio.Direction.OUTPUT - self._pin.value = self._invert_logic - - @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 - - @property - def assumed_state(self): - """Return true if optimistic updates are used.""" - return True - - def turn_on(self, **kwargs): - """Turn the device on.""" - self._pin.value = not self._invert_logic - self._state = True - self.schedule_update_ha_state() - - def turn_off(self, **kwargs): - """Turn the device off.""" - self._pin.value = self._invert_logic - self._state = False - self.schedule_update_ha_state() diff --git a/homeassistant/components/media_extractor/services.yaml b/homeassistant/components/media_extractor/services.yaml index 5b1f86e9dfe..0b7295bd7bf 100644 --- a/homeassistant/components/media_extractor/services.yaml +++ b/homeassistant/components/media_extractor/services.yaml @@ -19,9 +19,9 @@ play_media: selector: select: options: - - 'CHANNEL' - - 'EPISODE' - - 'PLAYLIST MUSIC' - - 'MUSIC' - - 'TVSHOW' - - 'VIDEO' + - "CHANNEL" + - "EPISODE" + - "PLAYLIST MUSIC" + - "MUSIC" + - "TVSHOW" + - "VIDEO" diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 99d493a75c4..1a2ea50d306 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -70,6 +70,7 @@ from .browse_media import BrowseMedia, async_process_play_media_url # noqa: F40 from .const import ( # noqa: F401 ATTR_APP_ID, ATTR_APP_NAME, + ATTR_ENTITY_PICTURE_LOCAL, ATTR_GROUP_MEMBERS, ATTR_INPUT_SOURCE, ATTR_INPUT_SOURCE_LIST, @@ -936,7 +937,7 @@ class MediaPlayerEntity(Entity): state_attr[attr] = value if self.media_image_remotely_accessible: - state_attr["entity_picture_local"] = self.media_image_local + state_attr[ATTR_ENTITY_PICTURE_LOCAL] = self.media_image_local return state_attr diff --git a/homeassistant/components/media_player/browse_media.py b/homeassistant/components/media_player/browse_media.py index fa825042817..e4cad5c3201 100644 --- a/homeassistant/components/media_player/browse_media.py +++ b/homeassistant/components/media_player/browse_media.py @@ -10,21 +10,35 @@ import yarl from homeassistant.components.http.auth import async_sign_path from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.network import get_url, is_hass_url +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.network import ( + NoURLAvailableError, + get_supervisor_network_url, + get_url, + is_hass_url, +) from .const import CONTENT_AUTH_EXPIRY_TIME, MEDIA_CLASS_DIRECTORY @callback def async_process_play_media_url( - hass: HomeAssistant, media_content_id: str, *, allow_relative_url: bool = False + hass: HomeAssistant, + media_content_id: str, + *, + allow_relative_url: bool = False, + for_supervisor_network: bool = False, ) -> str: """Update a media URL with authentication if it points at Home Assistant.""" - if media_content_id[0] != "/" and not is_hass_url(hass, media_content_id): - return media_content_id - parsed = yarl.URL(media_content_id) + if parsed.is_absolute(): + if not is_hass_url(hass, media_content_id): + return media_content_id + else: + if media_content_id[0] != "/": + raise ValueError("URL is relative, but does not start with a /") + if parsed.query: logging.getLogger(__name__).debug( "Not signing path for content with query param" @@ -38,8 +52,25 @@ def async_process_play_media_url( media_content_id = str(parsed.join(yarl.URL(signed_path))) # convert relative URL to absolute URL - if media_content_id[0] == "/" and not allow_relative_url: - media_content_id = f"{get_url(hass)}{media_content_id}" + if not parsed.is_absolute() and not allow_relative_url: + base_url = None + if for_supervisor_network: + base_url = get_supervisor_network_url(hass) + + if not base_url: + try: + base_url = get_url(hass) + except NoURLAvailableError as err: + msg = "Unable to determine Home Assistant URL to send to device" + if ( + hass.config.api + and hass.config.api.use_ssl + and (not hass.config.external_url or not hass.config.internal_url) + ): + msg += ". Configure internal and external URL in general settings." + raise HomeAssistantError(msg) from err + + media_content_id = f"{base_url}{media_content_id}" return media_content_id diff --git a/homeassistant/components/media_player/const.py b/homeassistant/components/media_player/const.py index e7b16f6ac88..fec2cfb3d0a 100644 --- a/homeassistant/components/media_player/const.py +++ b/homeassistant/components/media_player/const.py @@ -4,6 +4,7 @@ CONTENT_AUTH_EXPIRY_TIME = 3600 * 24 ATTR_APP_ID = "app_id" ATTR_APP_NAME = "app_name" +ATTR_ENTITY_PICTURE_LOCAL = "entity_picture_local" ATTR_GROUP_MEMBERS = "group_members" ATTR_INPUT_SOURCE = "source" ATTR_INPUT_SOURCE_LIST = "source_list" diff --git a/homeassistant/components/media_player/manifest.json b/homeassistant/components/media_player/manifest.json index 7a8e47adf20..118d05036cc 100644 --- a/homeassistant/components/media_player/manifest.json +++ b/homeassistant/components/media_player/manifest.json @@ -3,6 +3,6 @@ "name": "Media Player", "documentation": "https://www.home-assistant.io/integrations/media_player", "dependencies": ["http"], - "codeowners": [], + "codeowners": ["@home-assistant/core"], "quality_scale": "internal" } diff --git a/homeassistant/components/media_player/recorder.py b/homeassistant/components/media_player/recorder.py new file mode 100644 index 00000000000..8ced833ebec --- /dev/null +++ b/homeassistant/components/media_player/recorder.py @@ -0,0 +1,26 @@ +"""Integration platform for recorder.""" +from __future__ import annotations + +from homeassistant.const import ATTR_ENTITY_PICTURE +from homeassistant.core import HomeAssistant, callback + +from . import ( + ATTR_ENTITY_PICTURE_LOCAL, + ATTR_INPUT_SOURCE_LIST, + ATTR_MEDIA_POSITION, + ATTR_MEDIA_POSITION_UPDATED_AT, + ATTR_SOUND_MODE_LIST, +) + + +@callback +def exclude_attributes(hass: HomeAssistant) -> set[str]: + """Exclude static and token attributes from being recorded in the database.""" + return { + ATTR_ENTITY_PICTURE_LOCAL, + ATTR_ENTITY_PICTURE, + ATTR_INPUT_SOURCE_LIST, + ATTR_MEDIA_POSITION_UPDATED_AT, + ATTR_MEDIA_POSITION, + ATTR_SOUND_MODE_LIST, + } diff --git a/homeassistant/components/media_player/services.yaml b/homeassistant/components/media_player/services.yaml index f897ae8ad7f..2e8585d0127 100644 --- a/homeassistant/components/media_player/services.yaml +++ b/homeassistant/components/media_player/services.yaml @@ -215,9 +215,12 @@ repeat_set: selector: select: options: - - "off" - - "all" - - "one" + - label: "Off" + value: "off" + - label: "Repeat all" + value: "all" + - label: "Repeat one" + value: "one" join: name: Join diff --git a/homeassistant/components/media_player/translations/el.json b/homeassistant/components/media_player/translations/el.json index e3a300af77d..242de3e829a 100644 --- a/homeassistant/components/media_player/translations/el.json +++ b/homeassistant/components/media_player/translations/el.json @@ -19,12 +19,12 @@ "state": { "_": { "idle": "\u03a3\u03b5 \u03b1\u03b4\u03c1\u03ac\u03bd\u03b5\u03b9\u03b1", - "off": "\u0391\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7", - "on": "\u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7", + "off": "\u0391\u03bd\u03b5\u03bd\u03b5\u03c1\u03b3\u03cc", + "on": "\u0395\u03bd\u03b5\u03c1\u03b3\u03cc", "paused": "\u03a3\u03b5 \u03a0\u03b1\u03cd\u03c3\u03b7", - "playing": "\u039a\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7 \u0391\u03bd\u03b1\u03c0\u03b1\u03c1\u03b1\u03b3\u03c9\u03b3\u03ae\u03c2", + "playing": "\u03a0\u03b1\u03af\u03b6\u03b5\u03b9", "standby": "\u039a\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7 \u03b1\u03bd\u03b1\u03bc\u03bf\u03bd\u03ae\u03c2" } }, - "title": "\u03a3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b1\u03bd\u03b1\u03c0\u03b1\u03c1\u03b1\u03b3\u03c9\u03b3\u03ae\u03c2 \u03c0\u03bf\u03bb\u03c5\u03bc\u03ad\u03c3\u03c9\u03bd" + "title": "\u03a0\u03c1\u03cc\u03b3\u03c1\u03b1\u03bc\u03bc\u03b1 \u03b1\u03bd\u03b1\u03c0\u03b1\u03c1\u03b1\u03b3\u03c9\u03b3\u03ae\u03c2 \u03c0\u03bf\u03bb\u03c5\u03bc\u03ad\u03c3\u03c9\u03bd" } \ No newline at end of file diff --git a/homeassistant/components/media_player/translations/fr.json b/homeassistant/components/media_player/translations/fr.json index bcda6d770a3..17a6cbf92b3 100644 --- a/homeassistant/components/media_player/translations/fr.json +++ b/homeassistant/components/media_player/translations/fr.json @@ -12,15 +12,15 @@ "idle": "{entity_name} devient inactif", "paused": "{entity_name} est mis en pause", "playing": "{entity_name} commence \u00e0 jouer", - "turned_off": "{entity_name} d\u00e9sactiv\u00e9", - "turned_on": "{entity_name} activ\u00e9" + "turned_off": "{entity_name} a \u00e9t\u00e9 \u00e9teint", + "turned_on": "{entity_name} a \u00e9t\u00e9 allum\u00e9" } }, "state": { "_": { - "idle": "En veille", - "off": "Inactif", - "on": "Actif", + "idle": "Inactif", + "off": "D\u00e9sactiv\u00e9", + "on": "Activ\u00e9", "paused": "En pause", "playing": "Lecture en cours", "standby": "En veille" diff --git a/homeassistant/components/melcloud/translations/fr.json b/homeassistant/components/melcloud/translations/fr.json index f7f40c68bc5..95d3433fb88 100644 --- a/homeassistant/components/melcloud/translations/fr.json +++ b/homeassistant/components/melcloud/translations/fr.json @@ -5,14 +5,14 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "step": { "user": { "data": { "password": "Mot de passe", - "username": "Email" + "username": "Courriel" }, "description": "Se connecter en utilisant votre MELCloud compte.", "title": "Se connecter \u00e0 MELCloud" diff --git a/homeassistant/components/met/__init__.py b/homeassistant/components/met/__init__.py index a66c974f415..536cf03bde2 100644 --- a/homeassistant/components/met/__init__.py +++ b/homeassistant/components/met/__init__.py @@ -82,12 +82,12 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> return unload_ok -class MetDataUpdateCoordinator(DataUpdateCoordinator): +class MetDataUpdateCoordinator(DataUpdateCoordinator["MetWeatherData"]): """Class to manage fetching Met data.""" def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None: """Initialize global Met data updater.""" - self._unsub_track_home: Callable | None = None + self._unsub_track_home: Callable[[], None] | None = None self.weather = MetWeatherData( hass, config_entry.data, hass.config.units.is_metric ) @@ -137,8 +137,8 @@ class MetWeatherData: self._is_metric = is_metric self._weather_data: metno.MetWeatherData self.current_weather_data: dict = {} - self.daily_forecast = None - self.hourly_forecast = None + self.daily_forecast: list[dict] = [] + self.hourly_forecast: list[dict] = [] self._coordinates: dict[str, str] | None = None def set_coordinates(self) -> bool: diff --git a/homeassistant/components/met/config_flow.py b/homeassistant/components/met/config_flow.py index 42186a60bb9..6c4c4d33d5b 100644 --- a/homeassistant/components/met/config_flow.py +++ b/homeassistant/components/met/config_flow.py @@ -6,7 +6,6 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult import homeassistant.helpers.config_validation as cv from .const import ( @@ -79,10 +78,6 @@ class MetFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors=self._errors, ) - async def async_step_import(self, user_input: dict | None = None) -> FlowResult: - """Handle configuration by yaml file.""" - return await self.async_step_user(user_input) - async def async_step_onboarding(self, data=None): """Handle a flow initialized by onboarding.""" # Don't create entry if latitude or longitude isn't set. diff --git a/homeassistant/components/met/weather.py b/homeassistant/components/met/weather.py index 53c372030f1..251d99ad295 100644 --- a/homeassistant/components/met/weather.py +++ b/homeassistant/components/met/weather.py @@ -1,12 +1,9 @@ """Support for Met.no weather service.""" from __future__ import annotations -import logging from types import MappingProxyType from typing import Any -import voluptuous as vol - from homeassistant.components.weather import ( ATTR_FORECAST_CONDITION, ATTR_FORECAST_TEMP, @@ -16,13 +13,11 @@ from homeassistant.components.weather import ( ATTR_WEATHER_TEMPERATURE, ATTR_WEATHER_WIND_BEARING, ATTR_WEATHER_WIND_SPEED, - PLATFORM_SCHEMA, Forecast, WeatherEntity, ) -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, @@ -35,20 +30,15 @@ from homeassistant.const import ( TEMP_CELSIUS, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers import config_validation as cv from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from homeassistant.helpers.update_coordinator import ( - CoordinatorEntity, - DataUpdateCoordinator, - T, -) +from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util.distance import convert as convert_distance from homeassistant.util.pressure import convert as convert_pressure from homeassistant.util.speed import convert as convert_speed +from . import MetDataUpdateCoordinator from .const import ( ATTR_FORECAST_PRECIPITATION, ATTR_MAP, @@ -58,8 +48,6 @@ from .const import ( FORECAST_MAP, ) -_LOGGER = logging.getLogger(__name__) - ATTRIBUTION = ( "Weather forecast from met.no, delivered by the Norwegian " "Meteorological Institute." @@ -67,49 +55,13 @@ ATTRIBUTION = ( DEFAULT_NAME = "Met.no" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Inclusive( - CONF_LATITUDE, "coordinates", "Latitude and longitude must exist together" - ): cv.latitude, - vol.Inclusive( - CONF_LONGITUDE, "coordinates", "Latitude and longitude must exist together" - ): cv.longitude, - vol.Optional(CONF_ELEVATION): int, - } -) - - -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the Met.no weather platform.""" - _LOGGER.warning("Loading Met.no via platform config is deprecated") - - # Add defaults. - config = {CONF_ELEVATION: hass.config.elevation, **config} - - if config.get(CONF_LATITUDE) is None: - config[CONF_TRACK_HOME] = True - - 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: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Add a weather entity from a config_entry.""" - coordinator = hass.data[DOMAIN][config_entry.entry_id] + coordinator: MetDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] async_add_entities( [ MetWeather( @@ -130,12 +82,12 @@ def format_condition(condition: str) -> str: return condition -class MetWeather(CoordinatorEntity, WeatherEntity): +class MetWeather(CoordinatorEntity[MetDataUpdateCoordinator], WeatherEntity): """Implementation of a Met.no weather condition.""" def __init__( self, - coordinator: DataUpdateCoordinator[T], + coordinator: MetDataUpdateCoordinator, config: MappingProxyType[str, Any], is_metric: bool, hourly: bool, @@ -187,6 +139,8 @@ class MetWeather(CoordinatorEntity, WeatherEntity): def condition(self) -> str | None: """Return the current condition.""" condition = self.coordinator.data.current_weather_data.get("condition") + if condition is None: + return None return format_condition(condition) @property diff --git a/homeassistant/components/met_eireann/strings.json b/homeassistant/components/met_eireann/strings.json index 687631f2cae..984f46d71d6 100644 --- a/homeassistant/components/met_eireann/strings.json +++ b/homeassistant/components/met_eireann/strings.json @@ -12,6 +12,8 @@ } } }, - "error": { "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" } + "error": { + "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" + } } } diff --git a/homeassistant/components/meteo_france/strings.json b/homeassistant/components/meteo_france/strings.json index 9cb68a0ca73..3ff8d4308a3 100644 --- a/homeassistant/components/meteo_france/strings.json +++ b/homeassistant/components/meteo_france/strings.json @@ -2,14 +2,12 @@ "config": { "step": { "user": { - "title": "M\u00e9t\u00e9o-France", "description": "Enter the postal code (only for France, recommended) or city name", "data": { "city": "City" } }, "cities": { - "title": "M\u00e9t\u00e9o-France", "description": "Choose your city from the list", "data": { "city": "City" diff --git a/homeassistant/components/meteoclimatic/manifest.json b/homeassistant/components/meteoclimatic/manifest.json index 6c573b0c0d4..5dd0217ef5a 100644 --- a/homeassistant/components/meteoclimatic/manifest.json +++ b/homeassistant/components/meteoclimatic/manifest.json @@ -3,12 +3,8 @@ "name": "Meteoclimatic", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/meteoclimatic", - "requirements": [ - "pymeteoclimatic==0.0.6" - ], - "codeowners": [ - "@adrianmo" - ], + "requirements": ["pymeteoclimatic==0.0.6"], + "codeowners": ["@adrianmo"], "iot_class": "cloud_polling", "loggers": ["meteoclimatic"] } diff --git a/homeassistant/components/meteoclimatic/strings.json b/homeassistant/components/meteoclimatic/strings.json index 2353c22c7cc..5aedf7da01a 100644 --- a/homeassistant/components/meteoclimatic/strings.json +++ b/homeassistant/components/meteoclimatic/strings.json @@ -2,10 +2,11 @@ "config": { "step": { "user": { - "title": "Meteoclimatic", - "description": "Enter the Meteoclimatic station code (e.g., ESCAT4300000043206B)", "data": { "code": "Station code" + }, + "data_description": { + "code": "Looks like ESCAT4300000043206B" } } }, @@ -17,4 +18,4 @@ "not_found": "[%key:common::config_flow::abort::no_devices_found%]" } } -} \ No newline at end of file +} diff --git a/homeassistant/components/mfi/switch.py b/homeassistant/components/mfi/switch.py index 7564229d526..76a55e46ba1 100644 --- a/homeassistant/components/mfi/switch.py +++ b/homeassistant/components/mfi/switch.py @@ -110,16 +110,3 @@ class MfiSwitch(SwitchEntity): """Turn the switch off.""" self._port.control(False) self._target_state = False - - @property - def current_power_w(self): - """Return the current power usage in W.""" - return int(self._port.data.get("active_pwr", 0)) - - @property - def extra_state_attributes(self): - """Return the state attributes for the device.""" - return { - "volts": round(self._port.data.get("v_rms", 0), 1), - "amps": round(self._port.data.get("i_rms", 0), 1), - } diff --git a/homeassistant/components/mhz19/__init__.py b/homeassistant/components/mhz19/__init__.py deleted file mode 100644 index 5fa9bbb69e8..00000000000 --- a/homeassistant/components/mhz19/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The mhz19 component.""" diff --git a/homeassistant/components/mhz19/manifest.json b/homeassistant/components/mhz19/manifest.json deleted file mode 100644 index 349fba8c7a2..00000000000 --- a/homeassistant/components/mhz19/manifest.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "domain": "mhz19", - "name": "MH-Z19 CO2 Sensor", - "documentation": "https://www.home-assistant.io/integrations/mhz19", - "requirements": ["pmsensor==0.4"], - "codeowners": [], - "iot_class": "local_polling", - "loggers": ["pmsensor"] -} diff --git a/homeassistant/components/mhz19/sensor.py b/homeassistant/components/mhz19/sensor.py deleted file mode 100644 index f9237fd2c5b..00000000000 --- a/homeassistant/components/mhz19/sensor.py +++ /dev/null @@ -1,169 +0,0 @@ -"""Support for CO2 sensor connected to a serial port.""" -from __future__ import annotations - -from datetime import timedelta -import logging - -from pmsensor import co2sensor -import voluptuous as vol - -from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, - SensorDeviceClass, - SensorEntity, - SensorEntityDescription, -) -from homeassistant.const import ( - ATTR_TEMPERATURE, - CONCENTRATION_PARTS_PER_MILLION, - CONF_MONITORED_CONDITIONS, - CONF_NAME, - TEMP_CELSIUS, -) -from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from homeassistant.util import Throttle - -_LOGGER = logging.getLogger(__name__) - -CONF_SERIAL_DEVICE = "serial_device" -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=10) - -DEFAULT_NAME = "CO2 Sensor" - -ATTR_CO2_CONCENTRATION = "co2_concentration" - -SENSOR_TEMPERATURE = "temperature" -SENSOR_CO2 = "co2" -SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( - SensorEntityDescription( - key=SENSOR_TEMPERATURE, - name="Temperature", - native_unit_of_measurement=TEMP_CELSIUS, - device_class=SensorDeviceClass.TEMPERATURE, - ), - SensorEntityDescription( - key=SENSOR_CO2, - name="CO2", - native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, - device_class=SensorDeviceClass.CO2, - ), -) -SENSOR_KEYS: list[str] = [desc.key for desc in SENSOR_TYPES] -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Required(CONF_SERIAL_DEVICE): cv.string, - vol.Optional(CONF_MONITORED_CONDITIONS, default=[SENSOR_CO2]): vol.All( - cv.ensure_list, [vol.In(SENSOR_KEYS)] - ), - } -) - - -def setup_platform( - hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the available CO2 sensors.""" - _LOGGER.warning( - "The MH-Z19 CO2 Sensor integration is deprecated and will be removed " - "in Home Assistant Core 2022.4; this integration is removed under " - "Architectural Decision Record 0019, more information can be found here: " - "https://github.com/home-assistant/architecture/blob/master/adr/0019-GPIO.md" - ) - - try: - co2sensor.read_mh_z19(config.get(CONF_SERIAL_DEVICE)) - except OSError as err: - _LOGGER.error( - "Could not open serial connection to %s (%s)", - config.get(CONF_SERIAL_DEVICE), - err, - ) - return - - data = MHZClient(co2sensor, config.get(CONF_SERIAL_DEVICE)) - name = config[CONF_NAME] - - monitored_conditions = config[CONF_MONITORED_CONDITIONS] - entities = [ - MHZ19Sensor(data, name, description) - for description in SENSOR_TYPES - if description.key in monitored_conditions - ] - - add_entities(entities, True) - - -class MHZ19Sensor(SensorEntity): - """Representation of an CO2 sensor.""" - - def __init__(self, mhz_client, name, description: SensorEntityDescription): - """Initialize a new PM sensor.""" - self.entity_description = description - self._mhz_client = mhz_client - self._ppm = None - self._temperature = None - - self._attr_name = f"{name}: {description.name}" - - @property - def native_value(self): - """Return the state of the sensor.""" - if self.entity_description.key == SENSOR_CO2: - return self._ppm - return self._temperature - - def update(self): - """Read from sensor and update the state.""" - self._mhz_client.update() - data = self._mhz_client.data - self._temperature = data.get(SENSOR_TEMPERATURE) - self._ppm = data.get(SENSOR_CO2) - - @property - def extra_state_attributes(self): - """Return the state attributes.""" - result = {} - sensor_type = self.entity_description.key - if sensor_type == SENSOR_TEMPERATURE and self._ppm is not None: - result[ATTR_CO2_CONCENTRATION] = self._ppm - elif sensor_type == SENSOR_CO2 and self._temperature is not None: - result[ATTR_TEMPERATURE] = self._temperature - return result - - -class MHZClient: - """Get the latest data from the MH-Z sensor.""" - - def __init__(self, co2sens, serial): - """Initialize the sensor.""" - self.co2sensor = co2sens - self._serial = serial - self.data = {} - - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self): - """Get the latest data the MH-Z19 sensor.""" - self.data = {} - try: - result = self.co2sensor.read_mh_z19_with_temperature(self._serial) - if result is None: - return - co2, temperature = result - - except OSError as err: - _LOGGER.error( - "Could not open serial connection to %s (%s)", self._serial, err - ) - return - - if temperature is not None: - self.data[SENSOR_TEMPERATURE] = temperature - if co2 is not None and 0 < co2 <= 5000: - self.data[SENSOR_CO2] = co2 diff --git a/homeassistant/components/microsoft_face/__init__.py b/homeassistant/components/microsoft_face/__init__.py index 19bfef1349c..6b8fcde63f1 100644 --- a/homeassistant/components/microsoft_face/__init__.py +++ b/homeassistant/components/microsoft_face/__init__.py @@ -10,6 +10,7 @@ from aiohttp.hdrs import CONTENT_TYPE import async_timeout import voluptuous as vol +from homeassistant.components import camera from homeassistant.const import ATTR_NAME, CONF_API_KEY, CONF_TIMEOUT, CONTENT_TYPE_JSON from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.exceptions import HomeAssistantError @@ -181,7 +182,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: p_id = face.store[g_id].get(service.data[ATTR_PERSON]) camera_entity = service.data[ATTR_CAMERA_ENTITY] - camera = hass.components.camera try: image = await camera.async_get_image(hass, camera_entity) diff --git a/homeassistant/components/mikrotik/translations/fr.json b/homeassistant/components/mikrotik/translations/fr.json index 5632ab5b8dd..5d0f2786eff 100644 --- a/homeassistant/components/mikrotik/translations/fr.json +++ b/homeassistant/components/mikrotik/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "name_exists": "Le nom existe" }, "step": { diff --git a/homeassistant/components/mill/services.yaml b/homeassistant/components/mill/services.yaml index 6d475243dd5..ad5cff4a5ff 100644 --- a/homeassistant/components/mill/services.yaml +++ b/homeassistant/components/mill/services.yaml @@ -16,7 +16,7 @@ set_room_temperature: number: min: 0 max: 100 - unit_of_measurement: '°' + unit_of_measurement: "°" comfort_temp: name: Comfort temperature description: Comfort temp. @@ -24,7 +24,7 @@ set_room_temperature: number: min: 0 max: 100 - unit_of_measurement: '°' + unit_of_measurement: "°" sleep_temp: name: Sleep temperature description: Sleep temp. @@ -32,4 +32,4 @@ set_room_temperature: number: min: 0 max: 100 - unit_of_measurement: '°' + unit_of_measurement: "°" diff --git a/homeassistant/components/mill/translations/zh-Hant.json b/homeassistant/components/mill/translations/zh-Hant.json index a77203a0495..5c4745b23ae 100644 --- a/homeassistant/components/mill/translations/zh-Hant.json +++ b/homeassistant/components/mill/translations/zh-Hant.json @@ -21,11 +21,11 @@ }, "user": { "data": { - "connection_type": "\u9078\u64c7\u9023\u7dda\u985e\u578b", + "connection_type": "\u9078\u64c7\u9023\u7dda\u985e\u5225", "password": "\u5bc6\u78bc", "username": "\u4f7f\u7528\u8005\u540d\u7a31" }, - "description": "\u9078\u64c7\u9023\u7dda\u985e\u578b\u3002\u672c\u5730\u7aef\u5c07\u9700\u8981\u7b2c\u4e09\u4ee3\u52a0\u71b1\u5668" + "description": "\u9078\u64c7\u9023\u7dda\u985e\u5225\u3002\u672c\u5730\u7aef\u5c07\u9700\u8981\u7b2c\u4e09\u4ee3\u52a0\u71b1\u5668" } } } diff --git a/homeassistant/components/min_max/__init__.py b/homeassistant/components/min_max/__init__.py index 84b7c0a2cf5..db80473f90a 100644 --- a/homeassistant/components/min_max/__init__.py +++ b/homeassistant/components/min_max/__init__.py @@ -1,6 +1,26 @@ """The min_max component.""" +from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform +from homeassistant.core import HomeAssistant -DOMAIN = "min_max" PLATFORMS = [Platform.SENSOR] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Min/Max from a config entry.""" + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + + entry.async_on_unload(entry.add_update_listener(config_entry_update_listener)) + + return True + + +async def config_entry_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Update listener, called when the config entry options are changed.""" + await hass.config_entries.async_reload(entry.entry_id) + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/min_max/config_flow.py b/homeassistant/components/min_max/config_flow.py new file mode 100644 index 00000000000..6dab7cc000c --- /dev/null +++ b/homeassistant/components/min_max/config_flow.py @@ -0,0 +1,65 @@ +"""Config flow for Min/Max integration.""" +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, cast + +import voluptuous as vol + +from homeassistant.const import CONF_TYPE +from homeassistant.helpers import selector +from homeassistant.helpers.schema_config_entry_flow import ( + SchemaConfigFlowHandler, + SchemaFlowFormStep, + SchemaFlowMenuStep, +) + +from .const import CONF_ENTITY_IDS, CONF_ROUND_DIGITS, DOMAIN + +_STATISTIC_MEASURES = [ + {"value": "min", "label": "Minimum"}, + {"value": "max", "label": "Maximum"}, + {"value": "mean", "label": "Arithmetic mean"}, + {"value": "median", "label": "Median"}, + {"value": "last", "label": "Most recently updated"}, +] + + +OPTIONS_SCHEMA = vol.Schema( + { + vol.Required(CONF_ENTITY_IDS): selector.selector( + {"entity": {"domain": "sensor", "multiple": True}} + ), + vol.Required(CONF_TYPE): selector.selector( + {"select": {"options": _STATISTIC_MEASURES}} + ), + vol.Required(CONF_ROUND_DIGITS, default=2): selector.selector( + {"number": {"min": 0, "max": 6, "mode": "box"}} + ), + } +) + +CONFIG_SCHEMA = vol.Schema( + { + vol.Required("name"): selector.selector({"text": {}}), + } +).extend(OPTIONS_SCHEMA.schema) + +CONFIG_FLOW: dict[str, SchemaFlowFormStep | SchemaFlowMenuStep] = { + "user": SchemaFlowFormStep(CONFIG_SCHEMA) +} + +OPTIONS_FLOW: dict[str, SchemaFlowFormStep | SchemaFlowMenuStep] = { + "init": SchemaFlowFormStep(OPTIONS_SCHEMA) +} + + +class ConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN): + """Handle a config or options flow for Min/Max.""" + + config_flow = CONFIG_FLOW + options_flow = OPTIONS_FLOW + + def async_config_entry_title(self, options: Mapping[str, Any]) -> str: + """Return config entry title.""" + return cast(str, options["name"]) if "name" in options else "" diff --git a/homeassistant/components/min_max/const.py b/homeassistant/components/min_max/const.py new file mode 100644 index 00000000000..d738eff4774 --- /dev/null +++ b/homeassistant/components/min_max/const.py @@ -0,0 +1,6 @@ +"""Constants for the Min/Max integration.""" + +DOMAIN = "min_max" + +CONF_ENTITY_IDS = "entity_ids" +CONF_ROUND_DIGITS = "round_digits" diff --git a/homeassistant/components/min_max/manifest.json b/homeassistant/components/min_max/manifest.json index cf8c78d46ac..dd3e846aa84 100644 --- a/homeassistant/components/min_max/manifest.json +++ b/homeassistant/components/min_max/manifest.json @@ -1,8 +1,10 @@ { "domain": "min_max", + "integration_type": "helper", "name": "Min/Max", "documentation": "https://www.home-assistant.io/integrations/min_max", "codeowners": ["@fabaff"], "quality_scale": "internal", - "iot_class": "local_push" + "iot_class": "local_push", + "config_flow": true } diff --git a/homeassistant/components/min_max/sensor.py b/homeassistant/components/min_max/sensor.py index cb6463ba5c9..bfc6b99d150 100644 --- a/homeassistant/components/min_max/sensor.py +++ b/homeassistant/components/min_max/sensor.py @@ -6,6 +6,7 @@ import logging import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, CONF_NAME, @@ -13,14 +14,15 @@ from homeassistant.const import ( STATE_UNAVAILABLE, STATE_UNKNOWN, ) -from homeassistant.core import HomeAssistant, callback -import homeassistant.helpers.config_validation as cv +from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import DOMAIN, PLATFORMS +from . import PLATFORMS +from .const import CONF_ENTITY_IDS, CONF_ROUND_DIGITS, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -46,9 +48,6 @@ ATTR_TO_PROPERTY = [ ATTR_LAST_ENTITY_ID, ] -CONF_ENTITY_IDS = "entity_ids" -CONF_ROUND_DIGITS = "round_digits" - ICON = "mdi:calculator" SENSOR_TYPES = { @@ -71,6 +70,32 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Initialize min/max/mean config entry.""" + registry = er.async_get(hass) + entity_ids = er.async_validate_entity_ids( + registry, config_entry.options[CONF_ENTITY_IDS] + ) + sensor_type = config_entry.options[CONF_TYPE] + round_digits = int(config_entry.options[CONF_ROUND_DIGITS]) + + async_add_entities( + [ + MinMaxSensor( + entity_ids, + config_entry.title, + sensor_type, + round_digits, + config_entry.entry_id, + ) + ] + ) + + async def async_setup_platform( hass: HomeAssistant, config: ConfigType, @@ -85,7 +110,9 @@ async def async_setup_platform( await async_setup_reload_service(hass, DOMAIN, PLATFORMS) - async_add_entities([MinMaxSensor(entity_ids, name, sensor_type, round_digits)]) + async_add_entities( + [MinMaxSensor(entity_ids, name, sensor_type, round_digits, None)] + ) def calc_min(sensor_values): @@ -148,8 +175,9 @@ def calc_median(sensor_values, round_digits): class MinMaxSensor(SensorEntity): """Representation of a min/max sensor.""" - def __init__(self, entity_ids, name, sensor_type, round_digits): + def __init__(self, entity_ids, name, sensor_type, round_digits, unique_id): """Initialize the min/max sensor.""" + self._attr_unique_id = unique_id self._entity_ids = entity_ids self._sensor_type = sensor_type self._round_digits = round_digits @@ -173,6 +201,12 @@ class MinMaxSensor(SensorEntity): ) ) + # Replay current state of source entities + for entity_id in self._entity_ids: + state = self.hass.states.get(entity_id) + state_event = Event("", {"entity_id": entity_id, "new_state": state}) + self._async_min_max_sensor_state_listener(state_event, update_state=False) + self._calc_values() @property @@ -216,16 +250,24 @@ class MinMaxSensor(SensorEntity): return ICON @callback - def _async_min_max_sensor_state_listener(self, event): + def _async_min_max_sensor_state_listener(self, event, update_state=True): """Handle the sensor state changes.""" new_state = event.data.get("new_state") entity = event.data.get("entity_id") - if new_state.state is None or new_state.state in [ - STATE_UNKNOWN, - STATE_UNAVAILABLE, - ]: + if ( + new_state is None + or new_state.state is None + or new_state.state + in [ + STATE_UNKNOWN, + STATE_UNAVAILABLE, + ] + ): self.states[entity] = STATE_UNKNOWN + if not update_state: + return + self._calc_values() self.async_write_ha_state() return @@ -252,6 +294,9 @@ class MinMaxSensor(SensorEntity): "Unable to store state. Only numerical states are supported" ) + if not update_state: + return + self._calc_values() self.async_write_ha_state() diff --git a/homeassistant/components/min_max/strings.json b/homeassistant/components/min_max/strings.json new file mode 100644 index 00000000000..596dd2250eb --- /dev/null +++ b/homeassistant/components/min_max/strings.json @@ -0,0 +1,34 @@ +{ + "title": "Min / max / mean / median sensor", + "config": { + "step": { + "user": { + "title": "Add min / max / mean / median sensor", + "description": "Create a sensor that calculates a min, max, mean or median value from a list of input sensors.", + "data": { + "entity_ids": "Input entities", + "name": "Name", + "round_digits": "Precision", + "type": "Statistic characteristic" + }, + "data_description": { + "round_digits": "Controls the number of decimal digits in the output when the statistics characteristic is mean or median." + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "entity_ids": "[%key:component::min_max::config::step::user::data::entity_ids%]", + "round_digits": "[%key:component::min_max::config::step::user::data::round_digits%]", + "type": "[%key:component::min_max::config::step::user::data::type%]" + }, + "data_description": { + "round_digits": "[%key:component::min_max::config::step::user::data_description::round_digits%]" + } + } + } + } +} diff --git a/homeassistant/components/min_max/translations/en.json b/homeassistant/components/min_max/translations/en.json new file mode 100644 index 00000000000..8cc0d41c419 --- /dev/null +++ b/homeassistant/components/min_max/translations/en.json @@ -0,0 +1,34 @@ +{ + "config": { + "step": { + "user": { + "data": { + "entity_ids": "Input entities", + "name": "Name", + "round_digits": "Precision", + "type": "Statistic characteristic" + }, + "data_description": { + "round_digits": "Controls the number of decimal digits in the output when the statistics characteristic is mean or median." + }, + "description": "Create a sensor that calculates a min, max, mean or median value from a list of input sensors.", + "title": "Add min / max / mean / median sensor" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "entity_ids": "Input entities", + "round_digits": "Precision", + "type": "Statistic characteristic" + }, + "data_description": { + "round_digits": "Controls the number of decimal digits in the output when the statistics characteristic is mean or median." + } + } + } + }, + "title": "Min / max / mean / median sensor" +} \ No newline at end of file diff --git a/homeassistant/components/minecraft_server/__init__.py b/homeassistant/components/minecraft_server/__init__.py index d8f42454498..b45cc0fb2e3 100644 --- a/homeassistant/components/minecraft_server/__init__.py +++ b/homeassistant/components/minecraft_server/__init__.py @@ -95,6 +95,7 @@ class MinecraftServer: self.players_online = None self.players_max = None self.players_list = None + self.motd = None # Dispatcher signal name self.signal_name = f"{SIGNAL_NAME_PREFIX}_{self.unique_id}" @@ -179,6 +180,7 @@ class MinecraftServer: self.players_online = status_response.players.online self.players_max = status_response.players.max self.latency_time = status_response.latency + self.motd = (status_response.description).get("text") self.players_list = [] if status_response.players.sample is not None: for player in status_response.players.sample: @@ -201,6 +203,7 @@ class MinecraftServer: self.players_max = None self.latency_time = None self.players_list = None + self.motd = None # Inform user once about failed update if necessary. if not self._last_status_request_failed: diff --git a/homeassistant/components/minecraft_server/const.py b/homeassistant/components/minecraft_server/const.py index 52e6ae8fd5e..ab5d67dc426 100644 --- a/homeassistant/components/minecraft_server/const.py +++ b/homeassistant/components/minecraft_server/const.py @@ -14,6 +14,7 @@ ICON_PLAYERS_ONLINE = "mdi:account-multiple" ICON_PROTOCOL_VERSION = "mdi:numeric" ICON_STATUS = "mdi:lan" ICON_VERSION = "mdi:numeric" +ICON_MOTD = "mdi:minecraft" KEY_SERVERS = "servers" @@ -25,6 +26,7 @@ NAME_PLAYERS_ONLINE = "Players Online" NAME_PROTOCOL_VERSION = "Protocol Version" NAME_STATUS = "Status" NAME_VERSION = "Version" +NAME_MOTD = "World Message" SCAN_INTERVAL = 60 @@ -36,3 +38,4 @@ UNIT_PLAYERS_MAX = "players" UNIT_PLAYERS_ONLINE = "players" UNIT_PROTOCOL_VERSION = None UNIT_VERSION = None +UNIT_MOTD = None diff --git a/homeassistant/components/minecraft_server/sensor.py b/homeassistant/components/minecraft_server/sensor.py index a59ef3db363..d7ca73d1411 100644 --- a/homeassistant/components/minecraft_server/sensor.py +++ b/homeassistant/components/minecraft_server/sensor.py @@ -14,15 +14,18 @@ from .const import ( ATTR_PLAYERS_LIST, DOMAIN, ICON_LATENCY_TIME, + ICON_MOTD, ICON_PLAYERS_MAX, ICON_PLAYERS_ONLINE, ICON_PROTOCOL_VERSION, ICON_VERSION, NAME_LATENCY_TIME, + NAME_MOTD, NAME_PLAYERS_MAX, NAME_PLAYERS_ONLINE, NAME_PROTOCOL_VERSION, NAME_VERSION, + UNIT_MOTD, UNIT_PLAYERS_MAX, UNIT_PLAYERS_ONLINE, UNIT_PROTOCOL_VERSION, @@ -45,6 +48,7 @@ async def async_setup_entry( MinecraftServerLatencyTimeSensor(server), MinecraftServerPlayersOnlineSensor(server), MinecraftServerPlayersMaxSensor(server), + MinecraftServerMOTDSensor(server), ] # Add sensor entities. @@ -176,3 +180,20 @@ class MinecraftServerPlayersMaxSensor(MinecraftServerSensorEntity): async def async_update(self) -> None: """Update maximum number of players.""" self._state = self._server.players_max + + +class MinecraftServerMOTDSensor(MinecraftServerSensorEntity): + """Representation of a Minecraft Server MOTD sensor.""" + + def __init__(self, server: MinecraftServer) -> None: + """Initialize MOTD sensor.""" + super().__init__( + server=server, + type_name=NAME_MOTD, + icon=ICON_MOTD, + unit=UNIT_MOTD, + ) + + async def async_update(self) -> None: + """Update MOTD.""" + self._state = self._server.motd diff --git a/homeassistant/components/mjpeg/translations/fr.json b/homeassistant/components/mjpeg/translations/fr.json new file mode 100644 index 00000000000..f54c60631a6 --- /dev/null +++ b/homeassistant/components/mjpeg/translations/fr.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "invalid_auth": "Authentification non valide" + }, + "step": { + "user": { + "data": { + "mjpeg_url": "URL MJPEG", + "name": "Nom", + "password": "Mot de passe", + "username": "Nom d'utilisateur", + "verify_ssl": "V\u00e9rifier le certificat SSL" + } + } + } + }, + "options": { + "error": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "cannot_connect": "\u00c9chec de connexion", + "invalid_auth": "Authentification non valide" + }, + "step": { + "init": { + "data": { + "mjpeg_url": "URL MJPEG", + "name": "Nom", + "password": "Mot de passe", + "username": "Nom d'utilisateur", + "verify_ssl": "V\u00e9rifier le certificat SSL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mjpeg/translations/pt-BR.json b/homeassistant/components/mjpeg/translations/pt-BR.json index f54828ea224..ebd643a94bd 100644 --- a/homeassistant/components/mjpeg/translations/pt-BR.json +++ b/homeassistant/components/mjpeg/translations/pt-BR.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" }, "error": { - "cannot_connect": "Falhou ao conectar", + "cannot_connect": "Falha ao conectar", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" }, "step": { @@ -14,16 +14,16 @@ "name": "Nome", "password": "Senha", "still_image_url": "URL da imagem est\u00e1tica", - "username": "Nome de usu\u00e1rio", - "verify_ssl": "Verificar certificado SSL" + "username": "Usu\u00e1rio", + "verify_ssl": "Verifique o certificado SSL" } } } }, "options": { "error": { - "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", - "cannot_connect": "Falhou ao conectar", + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "cannot_connect": "Falha ao conectar", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" }, "step": { @@ -33,8 +33,8 @@ "name": "Nome", "password": "Senha", "still_image_url": "URL da imagem est\u00e1tica", - "username": "Nome de usu\u00e1rio", - "verify_ssl": "Verificar certificado SSL" + "username": "Usu\u00e1rio", + "verify_ssl": "Verifique o certificado SSL" } } } diff --git a/homeassistant/components/mobile_app/device_tracker.py b/homeassistant/components/mobile_app/device_tracker.py index 5e2ae23af16..0f6f0835c3b 100644 --- a/homeassistant/components/mobile_app/device_tracker.py +++ b/homeassistant/components/mobile_app/device_tracker.py @@ -15,6 +15,7 @@ from homeassistant.const import ( ATTR_LONGITUDE, ) from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity @@ -114,8 +115,10 @@ class MobileAppEntity(TrackerEntity, RestoreEntity): async def async_added_to_hass(self): """Call when entity about to be added to Home Assistant.""" await super().async_added_to_hass() - self._dispatch_unsub = self.hass.helpers.dispatcher.async_dispatcher_connect( - SIGNAL_LOCATION_UPDATE.format(self._entry.entry_id), self.update_data + self._dispatch_unsub = async_dispatcher_connect( + self.hass, + SIGNAL_LOCATION_UPDATE.format(self._entry.entry_id), + self.update_data, ) # Don't restore if we got set up with data. diff --git a/homeassistant/components/mobile_app/manifest.json b/homeassistant/components/mobile_app/manifest.json index 6cb4e964c9b..eb0bf100aee 100644 --- a/homeassistant/components/mobile_app/manifest.json +++ b/homeassistant/components/mobile_app/manifest.json @@ -3,7 +3,7 @@ "name": "Mobile App", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/mobile_app", - "requirements": ["PyNaCl==1.4.0"], + "requirements": ["PyNaCl==1.5.0"], "dependencies": ["http", "webhook", "person", "tag", "websocket_api"], "after_dependencies": ["cloud", "camera", "notify"], "codeowners": ["@home-assistant/core"], diff --git a/homeassistant/components/mobile_app/translations/he.json b/homeassistant/components/mobile_app/translations/he.json index e213f54137c..022accb4abc 100644 --- a/homeassistant/components/mobile_app/translations/he.json +++ b/homeassistant/components/mobile_app/translations/he.json @@ -1,10 +1,18 @@ { "config": { + "abort": { + "install_app": "\u05d9\u05e9 \u05dc\u05e4\u05ea\u05d5\u05d7 \u05d0\u05ea \u05d4\u05d9\u05d9\u05e9\u05d5\u05dd \u05dc\u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05e0\u05d9\u05d9\u05d3\u05d9\u05dd \u05db\u05d3\u05d9 \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d0\u05ea \u05d4\u05e9\u05d9\u05dc\u05d5\u05d1 \u05e2\u05dd Home Assistant. \u05d9\u05e9 \u05dc\u05e2\u05d9\u05d9\u05df [\u05d1\u05de\u05e1\u05de\u05db\u05d9\u05dd]({apps_url}) \u05dc\u05e7\u05d1\u05dc\u05ea \u05e8\u05e9\u05d9\u05de\u05d4 \u05e9\u05dc \u05d9\u05d9\u05e9\u05d5\u05de\u05d9\u05dd \u05ea\u05d5\u05d0\u05de\u05d9\u05dd." + }, "step": { "confirm": { "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d0\u05ea \u05e8\u05db\u05d9\u05d1 \u05d4\u05d9\u05d9\u05e9\u05d5\u05dd \u05dc\u05e0\u05d9\u05d9\u05d3?" } } }, + "device_automation": { + "action_type": { + "notify": "\u05e9\u05dc\u05d9\u05d7\u05ea \u05d4\u05d5\u05d3\u05e2\u05d4" + } + }, "title": "\u05d9\u05d9\u05e9\u05d5\u05dd \u05dc\u05e0\u05d9\u05d9\u05d3" } \ No newline at end of file diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py index a5ad05a4711..17a4acc1742 100644 --- a/homeassistant/components/modbus/__init__.py +++ b/homeassistant/components/modbus/__init__.py @@ -1,6 +1,7 @@ """Support for Modbus.""" from __future__ import annotations +import logging from typing import cast import voluptuous as vol @@ -60,7 +61,6 @@ from .const import ( CONF_BYTESIZE, CONF_CLIMATES, CONF_CLOSE_COMM_ON_ERROR, - CONF_DATA_COUNT, CONF_DATA_TYPE, CONF_FANS, CONF_INPUT_TYPE, @@ -72,7 +72,6 @@ from .const import ( CONF_PRECISION, CONF_RETRIES, CONF_RETRY_ON_EMPTY, - CONF_REVERSE_ORDER, CONF_SCALE, CONF_SLAVE_COUNT, CONF_STATE_CLOSED, @@ -112,6 +111,9 @@ from .validators import ( struct_validator, ) +_LOGGER = logging.getLogger(__name__) + + BASE_SCHEMA = vol.Schema({vol.Optional(CONF_NAME, default=DEFAULT_HUB): cv.string}) @@ -138,7 +140,7 @@ BASE_STRUCT_SCHEMA = BASE_COMPONENT_SCHEMA.extend( ] ), vol.Optional(CONF_COUNT): cv.positive_int, - vol.Optional(CONF_DATA_TYPE, default=DataType.INT): vol.In( + vol.Optional(CONF_DATA_TYPE, default=DataType.INT16): vol.In( [ DataType.INT8, DataType.INT16, @@ -152,9 +154,6 @@ BASE_STRUCT_SCHEMA = BASE_COMPONENT_SCHEMA.extend( DataType.FLOAT32, DataType.FLOAT64, DataType.STRING, - DataType.INT, - DataType.UINT, - DataType.FLOAT, DataType.STRING, DataType.CUSTOM, ] @@ -210,7 +209,6 @@ BASE_SWITCH_SCHEMA = BASE_COMPONENT_SCHEMA.extend( CLIMATE_SCHEMA = vol.All( - cv.deprecated(CONF_DATA_COUNT, replacement_key=CONF_COUNT), BASE_STRUCT_SCHEMA.extend( { vol.Required(CONF_TARGET_TEMP): cv.positive_int, @@ -254,13 +252,12 @@ LIGHT_SCHEMA = BASE_SWITCH_SCHEMA.extend({}) FAN_SCHEMA = BASE_SWITCH_SCHEMA.extend({}) SENSOR_SCHEMA = vol.All( - cv.deprecated(CONF_REVERSE_ORDER), BASE_STRUCT_SCHEMA.extend( { vol.Optional(CONF_DEVICE_CLASS): SENSOR_DEVICE_CLASSES_SCHEMA, vol.Optional(CONF_STATE_CLASS): SENSOR_STATE_CLASSES_SCHEMA, vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, - vol.Optional(CONF_REVERSE_ORDER): cv.boolean, + vol.Optional(CONF_SLAVE_COUNT, default=0): cv.positive_int, } ), ) @@ -343,7 +340,18 @@ def get_hub(hass: HomeAssistant, name: str) -> ModbusHub: async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up Modbus component.""" + if DOMAIN not in config: + return True return await async_modbus_setup( hass, config, ) + + +async def async_reset_platform(hass: HomeAssistant, integration_name: str) -> None: + """Release modbus resources.""" + _LOGGER.info("Modbus reloading") + hubs = hass.data[DOMAIN] + for name in hubs: + await hubs[name].async_close() + del hass.data[DOMAIN] diff --git a/homeassistant/components/modbus/binary_sensor.py b/homeassistant/components/modbus/binary_sensor.py index 50281bd2b29..c432c102492 100644 --- a/homeassistant/components/modbus/binary_sensor.py +++ b/homeassistant/components/modbus/binary_sensor.py @@ -39,7 +39,7 @@ async def async_setup_platform( ) -> None: """Set up the Modbus binary sensors.""" - if discovery_info is None: # pragma: no cover + if discovery_info is None: return sensors: list[ModbusBinarySensor | SlaveSensor] = [] diff --git a/homeassistant/components/modbus/climate.py b/homeassistant/components/modbus/climate.py index 37ff9504b35..3a835f79c55 100644 --- a/homeassistant/components/modbus/climate.py +++ b/homeassistant/components/modbus/climate.py @@ -11,6 +11,7 @@ from homeassistant.components.climate.const import ( SUPPORT_TARGET_TEMPERATURE, ) from homeassistant.const import ( + ATTR_TEMPERATURE, CONF_NAME, CONF_TEMPERATURE_UNIT, PRECISION_TENTHS, @@ -26,8 +27,8 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import get_hub from .base_platform import BaseStructPlatform from .const import ( - ATTR_TEMPERATURE, CALL_TYPE_REGISTER_HOLDING, + CALL_TYPE_WRITE_REGISTER, CALL_TYPE_WRITE_REGISTERS, CONF_CLIMATES, CONF_MAX_TEMP, @@ -102,8 +103,6 @@ class ModbusThermostat(BaseStructPlatform, RestoreEntity, ClimateEntity): async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" - if ATTR_TEMPERATURE not in kwargs: - return target_temperature = ( float(kwargs[ATTR_TEMPERATURE]) - self._offset ) / self._scale @@ -122,12 +121,24 @@ class ModbusThermostat(BaseStructPlatform, RestoreEntity, ClimateEntity): for i in range(0, len(as_bytes), 2) ] registers = self._swap_registers(raw_regs) - result = await self._hub.async_pymodbus_call( - self._slave, - self._target_temperature_register, - registers, - CALL_TYPE_WRITE_REGISTERS, - ) + + if self._data_type in ( + DataType.INT16, + DataType.UINT16, + ): + result = await self._hub.async_pymodbus_call( + self._slave, + self._target_temperature_register, + int(float(registers[0])), + CALL_TYPE_WRITE_REGISTER, + ) + else: + result = await self._hub.async_pymodbus_call( + self._slave, + self._target_temperature_register, + [int(float(i)) for i in registers], + CALL_TYPE_WRITE_REGISTERS, + ) self._attr_available = result is not None await self.async_update() diff --git a/homeassistant/components/modbus/const.py b/homeassistant/components/modbus/const.py index 934d14012f8..b09d75f27e0 100644 --- a/homeassistant/components/modbus/const.py +++ b/homeassistant/components/modbus/const.py @@ -2,6 +2,7 @@ from enum import Enum from homeassistant.const import ( + CONF_ADDRESS, CONF_BINARY_SENSORS, CONF_COVERS, CONF_LIGHTS, @@ -18,7 +19,6 @@ CONF_CLOSE_COMM_ON_ERROR = "close_comm_on_error" CONF_COILS = "coils" CONF_CURRENT_TEMP = "current_temp_register" CONF_CURRENT_TEMP_REGISTER_TYPE = "current_temp_register_type" -CONF_DATA_COUNT = "data_count" CONF_DATA_TYPE = "data_type" CONF_FANS = "fans" CONF_HUB = "hub" @@ -34,7 +34,6 @@ CONF_REGISTER_TYPE = "register_type" CONF_REGISTERS = "registers" CONF_RETRIES = "retries" CONF_RETRY_ON_EMPTY = "retry_on_empty" -CONF_REVERSE_ORDER = "reverse_order" CONF_PRECISION = "precision" CONF_SCALE = "scale" CONF_SLAVE_COUNT = "slave_count" @@ -66,21 +65,17 @@ UDP = "udp" # service call attributes -ATTR_ADDRESS = "address" -ATTR_HUB = "hub" +ATTR_ADDRESS = CONF_ADDRESS +ATTR_HUB = CONF_HUB ATTR_UNIT = "unit" +ATTR_SLAVE = "slave" ATTR_VALUE = "value" -ATTR_STATE = "state" -ATTR_TEMPERATURE = "temperature" class DataType(str, Enum): """Data types used by sensor etc.""" CUSTOM = "custom" - FLOAT = "float" # deprecated - INT = "int" # deprecated - UINT = "uint" # deprecated STRING = "string" INT8 = "int8" INT16 = "int16" diff --git a/homeassistant/components/modbus/cover.py b/homeassistant/components/modbus/cover.py index 1a9a7a82e9c..a01f732ef13 100644 --- a/homeassistant/components/modbus/cover.py +++ b/homeassistant/components/modbus/cover.py @@ -45,7 +45,7 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Read configuration and create Modbus cover.""" - if discovery_info is None: # pragma: no cover + if discovery_info is None: return covers = [] diff --git a/homeassistant/components/modbus/fan.py b/homeassistant/components/modbus/fan.py index afa26ac1638..a986b243c1b 100644 --- a/homeassistant/components/modbus/fan.py +++ b/homeassistant/components/modbus/fan.py @@ -24,7 +24,7 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Read configuration and create Modbus fans.""" - if discovery_info is None: # pragma: no cover + if discovery_info is None: return fans = [] @@ -39,7 +39,6 @@ class ModbusFan(BaseSwitch, FanEntity): async def async_turn_on( self, - speed: str | None = None, percentage: int | None = None, preset_mode: str | None = None, **kwargs: Any, diff --git a/homeassistant/components/modbus/light.py b/homeassistant/components/modbus/light.py index cc5936050e8..2313dd9bacb 100644 --- a/homeassistant/components/modbus/light.py +++ b/homeassistant/components/modbus/light.py @@ -23,7 +23,7 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Read configuration and create Modbus lights.""" - if discovery_info is None: # pragma: no cover + if discovery_info is None: return lights = [] diff --git a/homeassistant/components/modbus/modbus.py b/homeassistant/components/modbus/modbus.py index 20083bb3d1c..5e15f65035a 100644 --- a/homeassistant/components/modbus/modbus.py +++ b/homeassistant/components/modbus/modbus.py @@ -20,6 +20,7 @@ from pymodbus.transaction import ModbusRtuFramer import voluptuous as vol from homeassistant.const import ( + ATTR_STATE, CONF_DELAY, CONF_HOST, CONF_METHOD, @@ -34,12 +35,13 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_call_later +from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.typing import ConfigType from .const import ( ATTR_ADDRESS, ATTR_HUB, - ATTR_STATE, + ATTR_SLAVE, ATTR_UNIT, ATTR_VALUE, CALL_TYPE_COIL, @@ -128,6 +130,8 @@ async def async_modbus_setup( ) -> bool: """Set up Modbus component.""" + await async_setup_reload_service(hass, DOMAIN, [DOMAIN]) + hass.data[DOMAIN] = hub_collect = {} for conf_hub in config[DOMAIN]: my_hub = ModbusHub(hass, conf_hub) @@ -156,7 +160,11 @@ async def async_modbus_setup( async def async_write_register(service: ServiceCall) -> None: """Write Modbus registers.""" - unit = int(float(service.data[ATTR_UNIT])) + unit = 0 + if ATTR_UNIT in service.data: + unit = int(float(service.data[ATTR_UNIT])) + if ATTR_SLAVE in service.data: + unit = int(float(service.data[ATTR_SLAVE])) address = int(float(service.data[ATTR_ADDRESS])) value = service.data[ATTR_VALUE] hub = hub_collect[ @@ -173,7 +181,11 @@ async def async_modbus_setup( async def async_write_coil(service: ServiceCall) -> None: """Write Modbus coil.""" - unit = service.data[ATTR_UNIT] + unit = 0 + if ATTR_UNIT in service.data: + unit = int(float(service.data[ATTR_UNIT])) + if ATTR_SLAVE in service.data: + unit = int(float(service.data[ATTR_SLAVE])) address = service.data[ATTR_ADDRESS] state = service.data[ATTR_STATE] hub = hub_collect[ @@ -195,7 +207,8 @@ async def async_modbus_setup( schema=vol.Schema( { vol.Optional(ATTR_HUB, default=DEFAULT_HUB): cv.string, - vol.Required(ATTR_UNIT): cv.positive_int, + vol.Exclusive(ATTR_SLAVE, "unit"): cv.positive_int, + vol.Exclusive(ATTR_UNIT, "unit"): cv.positive_int, vol.Required(ATTR_ADDRESS): cv.positive_int, vol.Required(x_write[2]): vol.Any( cv.positive_int, vol.All(cv.ensure_list, [x_write[3]]) @@ -386,7 +399,7 @@ class ModbusHub: return None async with self._lock: if not self._client: - return None # pragma: no cover + return None result = await self.hass.async_add_executor_job( self._pymodbus_call, unit, address, value, use_call ) diff --git a/homeassistant/components/modbus/sensor.py b/homeassistant/components/modbus/sensor.py index e4249594940..7e9295fdb14 100644 --- a/homeassistant/components/modbus/sensor.py +++ b/homeassistant/components/modbus/sensor.py @@ -2,19 +2,27 @@ from __future__ import annotations from datetime import datetime +import logging from typing import Any from homeassistant.components.sensor import CONF_STATE_CLASS, SensorEntity from homeassistant.const import CONF_NAME, CONF_SENSORS, CONF_UNIT_OF_MEASUREMENT -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) from . import get_hub from .base_platform import BaseStructPlatform +from .const import CONF_SLAVE_COUNT from .modbus import ModbusHub +_LOGGER = logging.getLogger(__name__) + PARALLEL_UPDATES = 1 @@ -25,15 +33,18 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the Modbus sensors.""" - sensors = [] - if discovery_info is None: # pragma: no cover + if discovery_info is None: return + sensors: list[ModbusRegisterSensor | SlaveSensor] = [] + hub = get_hub(hass, discovery_info[CONF_NAME]) for entry in discovery_info[CONF_SENSORS]: - hub = get_hub(hass, discovery_info[CONF_NAME]) - sensors.append(ModbusRegisterSensor(hub, entry)) - + slave_count = entry.get(CONF_SLAVE_COUNT, 0) + sensor = ModbusRegisterSensor(hub, entry) + if slave_count > 0: + sensors.extend(await sensor.async_setup_slaves(hass, slave_count, entry)) + sensors.append(sensor) async_add_entities(sensors) @@ -47,9 +58,30 @@ class ModbusRegisterSensor(BaseStructPlatform, RestoreEntity, SensorEntity): ) -> None: """Initialize the modbus register sensor.""" super().__init__(hub, entry) + self._coordinator: DataUpdateCoordinator[Any] | None = None self._attr_native_unit_of_measurement = entry.get(CONF_UNIT_OF_MEASUREMENT) self._attr_state_class = entry.get(CONF_STATE_CLASS) + async def async_setup_slaves( + self, hass: HomeAssistant, slave_count: int, entry: dict[str, Any] + ) -> list[SlaveSensor]: + """Add slaves as needed (1 read for multiple sensors).""" + + # Add a dataCoordinator for each sensor that have slaves + # this ensures that idx = bit position of value in result + # polling is done with the base class + name = self._attr_name if self._attr_name else "modbus_sensor" + self._coordinator = DataUpdateCoordinator( + hass, + _LOGGER, + name=name, + ) + + slaves: list[SlaveSensor] = [] + for idx in range(0, slave_count): + slaves.append(SlaveSensor(self._coordinator, idx, entry)) + return slaves + async def async_added_to_hass(self) -> None: """Handle entity which will be added.""" await self.async_base_added_to_hass() @@ -60,22 +92,63 @@ class ModbusRegisterSensor(BaseStructPlatform, RestoreEntity, SensorEntity): """Update the state of the sensor.""" # remark "now" is a dummy parameter to avoid problems with # async_track_time_interval - result = await self._hub.async_pymodbus_call( + raw_result = await self._hub.async_pymodbus_call( self._slave, self._address, self._count, self._input_type ) - if result is None: + if raw_result is None: if self._lazy_errors: self._lazy_errors -= 1 return self._lazy_errors = self._lazy_error_count self._attr_available = False + self._attr_native_value = None + if self._coordinator: + self._coordinator.async_set_updated_data(None) self.async_write_ha_state() return - self._attr_native_value = self.unpack_structure_result(result.registers) + result = self.unpack_structure_result(raw_result.registers) + if self._coordinator: + if result: + result_array = result.split(",") + self._attr_native_value = result_array[0] + self._coordinator.async_set_updated_data(result_array) + else: + self._attr_native_value = None + self._coordinator.async_set_updated_data(None) + else: + self._attr_native_value = result if self._attr_native_value is None: self._attr_available = False else: self._attr_available = True self._lazy_errors = self._lazy_error_count self.async_write_ha_state() + + +class SlaveSensor(CoordinatorEntity, RestoreEntity, SensorEntity): + """Modbus slave binary sensor.""" + + def __init__( + self, coordinator: DataUpdateCoordinator[Any], idx: int, entry: dict[str, Any] + ) -> None: + """Initialize the Modbus binary sensor.""" + idx += 1 + self._idx = idx + self._attr_name = f"{entry[CONF_NAME]} {idx}" + self._attr_available = False + super().__init__(coordinator) + + async def async_added_to_hass(self) -> None: + """Handle entity which will be added.""" + if state := await self.async_get_last_state(): + self._attr_native_value = state.state + await super().async_added_to_hass() + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + result = self.coordinator.data + if result: + self._attr_native_value = result[self._idx] + super()._handle_coordinator_update() diff --git a/homeassistant/components/modbus/services.yaml b/homeassistant/components/modbus/services.yaml index 87e8b98fa21..07acf0a72df 100644 --- a/homeassistant/components/modbus/services.yaml +++ b/homeassistant/components/modbus/services.yaml @@ -1,3 +1,6 @@ +reload: + name: Reload + description: Reload all modbus entities. write_coil: name: Write coil description: Write to a modbus coil. @@ -14,13 +17,13 @@ write_coil: name: State description: State to write. required: true - example: false + example: "0 or [1,0]" selector: object: - unit: - name: Unit - description: Address of the modbus unit. - required: true + slave: + name: Slave + description: Address of the modbus unit/slave. + required: false selector: number: min: 1 @@ -44,10 +47,10 @@ write_register: number: min: 0 max: 65535 - unit: - name: Unit - description: Address of the modbus unit. - required: true + slave: + name: Slave + description: Address of the modbus unit/slave. + required: false selector: number: min: 1 diff --git a/homeassistant/components/modbus/switch.py b/homeassistant/components/modbus/switch.py index 5844daf648e..beb84096006 100644 --- a/homeassistant/components/modbus/switch.py +++ b/homeassistant/components/modbus/switch.py @@ -25,7 +25,7 @@ async def async_setup_platform( """Read configuration and create Modbus switches.""" switches = [] - if discovery_info is None: # pragma: no cover + if discovery_info is None: return for entry in discovery_info[CONF_SWITCHES]: diff --git a/homeassistant/components/modbus/validators.py b/homeassistant/components/modbus/validators.py index 347e8d3fc72..315e138e130 100644 --- a/homeassistant/components/modbus/validators.py +++ b/homeassistant/components/modbus/validators.py @@ -26,6 +26,7 @@ from homeassistant.const import ( from .const import ( CONF_DATA_TYPE, CONF_INPUT_TYPE, + CONF_SLAVE_COUNT, CONF_SWAP, CONF_SWAP_BYTE, CONF_SWAP_NONE, @@ -39,23 +40,6 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -OLD_DATA_TYPES = { - DataType.INT: { - 1: DataType.INT16, - 2: DataType.INT32, - 4: DataType.INT64, - }, - DataType.UINT: { - 1: DataType.UINT16, - 2: DataType.UINT32, - 4: DataType.UINT64, - }, - DataType.FLOAT: { - 1: DataType.FLOAT16, - 2: DataType.FLOAT32, - 4: DataType.FLOAT64, - }, -} ENTRY = namedtuple("ENTRY", ["struct_id", "register_count"]) DEFAULT_STRUCT_FORMAT = { DataType.INT8: ENTRY("b", 1), @@ -80,24 +64,27 @@ def struct_validator(config: dict[str, Any]) -> dict[str, Any]: count = config.get(CONF_COUNT, 1) name = config[CONF_NAME] structure = config.get(CONF_STRUCTURE) + slave_count = config.get(CONF_SLAVE_COUNT, 0) + 1 swap_type = config.get(CONF_SWAP) - if data_type in (DataType.INT, DataType.UINT, DataType.FLOAT): - error = f"{name} with {data_type} is not valid, trying to convert" - _LOGGER.warning(error) - try: - data_type = OLD_DATA_TYPES[data_type][config.get(CONF_COUNT, 1)] - config[CONF_DATA_TYPE] = data_type - except KeyError as exp: - error = f"{name} cannot convert automatically {data_type}" - raise vol.Invalid(error) from exp if config[CONF_DATA_TYPE] != DataType.CUSTOM: if structure: error = f"{name} structure: cannot be mixed with {data_type}" raise vol.Invalid(error) + if data_type not in DEFAULT_STRUCT_FORMAT: + error = f"Error in sensor {name}. data_type `{data_type}` not supported" + raise vol.Invalid(error) + structure = f">{DEFAULT_STRUCT_FORMAT[data_type].struct_id}" if CONF_COUNT not in config: config[CONF_COUNT] = DEFAULT_STRUCT_FORMAT[data_type].register_count + if slave_count > 1: + structure = f">{slave_count}{DEFAULT_STRUCT_FORMAT[data_type].struct_id}" + else: + structure = f">{DEFAULT_STRUCT_FORMAT[data_type].struct_id}" else: + if slave_count > 1: + error = f"{name} structure: cannot be mixed with {CONF_SLAVE_COUNT}" + raise vol.Invalid(error) if not structure: error = ( f"Error in sensor {name}. The `{CONF_STRUCTURE}` field can not be empty" diff --git a/homeassistant/components/modem_callerid/manifest.json b/homeassistant/components/modem_callerid/manifest.json index ae66e72bfcb..024759791f4 100644 --- a/homeassistant/components/modem_callerid/manifest.json +++ b/homeassistant/components/modem_callerid/manifest.json @@ -7,6 +7,6 @@ "codeowners": ["@tkdrob"], "dependencies": ["usb"], "iot_class": "local_polling", - "usb": [{"vid":"0572","pid":"1340"}], + "usb": [{ "vid": "0572", "pid": "1340" }], "loggers": ["phone_modem"] } diff --git a/homeassistant/components/modem_callerid/strings.json b/homeassistant/components/modem_callerid/strings.json index 17359128528..bb6ac1879da 100644 --- a/homeassistant/components/modem_callerid/strings.json +++ b/homeassistant/components/modem_callerid/strings.json @@ -1,26 +1,24 @@ { - "config": { - "step": { - "user": { - "title": "Phone Modem", - "description": "This is an integration for landline calls using a CX93001 voice modem. This can retrieve caller ID information with an option to reject an incoming call.", - "data": { - "name": "[%key:common::config_flow::data::name%]", - "port": "[%key:common::config_flow::data::port%]" - } - }, - "usb_confirm": { - "title": "Phone Modem", - "description": "This is an integration for landline calls using a CX93001 voice modem. This can retrieve caller ID information with an option to reject an incoming call." + "config": { + "step": { + "user": { + "description": "This is an integration for landline calls using a CX93001 voice modem. This can retrieve caller ID information with an option to reject an incoming call.", + "data": { + "name": "[%key:common::config_flow::data::name%]", + "port": "[%key:common::config_flow::data::port%]" } }, - "error": { - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" - }, - "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", - "no_devices_found": "No remaining devices found" + "usb_confirm": { + "description": "This is an integration for landline calls using a CX93001 voice modem. This can retrieve caller ID information with an option to reject an incoming call." } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", + "no_devices_found": "No remaining devices found" } - } \ No newline at end of file + } +} diff --git a/homeassistant/components/modern_forms/__init__.py b/homeassistant/components/modern_forms/__init__.py index dfd6a9a8807..af4f05a1536 100644 --- a/homeassistant/components/modern_forms/__init__.py +++ b/homeassistant/components/modern_forms/__init__.py @@ -126,8 +126,6 @@ class ModernFormsDataUpdateCoordinator(DataUpdateCoordinator[ModernFormsDeviceSt class ModernFormsDeviceEntity(CoordinatorEntity[ModernFormsDataUpdateCoordinator]): """Defines a Modern Forms device entity.""" - coordinator: ModernFormsDataUpdateCoordinator - def __init__( self, *, diff --git a/homeassistant/components/modern_forms/fan.py b/homeassistant/components/modern_forms/fan.py index 7c1da064b18..8ebb706096a 100644 --- a/homeassistant/components/modern_forms/fan.py +++ b/homeassistant/components/modern_forms/fan.py @@ -129,7 +129,6 @@ class ModernFormsFanEntity(FanEntity, ModernFormsDeviceEntity): @modernforms_exception_handler async def async_turn_on( self, - speed: int | None = None, percentage: int | None = None, preset_mode: int | None = None, **kwargs: Any, diff --git a/homeassistant/components/modern_forms/manifest.json b/homeassistant/components/modern_forms/manifest.json index 67a7581e897..3da5f88a665 100644 --- a/homeassistant/components/modern_forms/manifest.json +++ b/homeassistant/components/modern_forms/manifest.json @@ -3,15 +3,9 @@ "name": "Modern Forms", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/modern_forms", - "requirements": [ - "aiomodernforms==0.1.8" - ], - "zeroconf": [ - {"type":"_easylink._tcp.local.", "name":"wac*"} - ], - "codeowners": [ - "@wonderslug" - ], + "requirements": ["aiomodernforms==0.1.8"], + "zeroconf": [{ "type": "_easylink._tcp.local.", "name": "wac*" }], + "codeowners": ["@wonderslug"], "iot_class": "local_polling", "loggers": ["aiomodernforms"] } diff --git a/homeassistant/components/modern_forms/translations/fr.json b/homeassistant/components/modern_forms/translations/fr.json index cde37b5251b..d68f4a7f680 100644 --- a/homeassistant/components/modern_forms/translations/fr.json +++ b/homeassistant/components/modern_forms/translations/fr.json @@ -10,7 +10,7 @@ "flow_title": "{name}", "step": { "confirm": { - "description": "Voulez-vous commencer la configuration ?" + "description": "Voulez-vous commencer la configuration\u00a0?" }, "user": { "data": { diff --git a/homeassistant/components/moehlenhoff_alpha2/climate.py b/homeassistant/components/moehlenhoff_alpha2/climate.py index d99eb0e4c8c..783e2df5967 100644 --- a/homeassistant/components/moehlenhoff_alpha2/climate.py +++ b/homeassistant/components/moehlenhoff_alpha2/climate.py @@ -38,10 +38,9 @@ async def async_setup_entry( # https://developers.home-assistant.io/docs/core/entity/climate/ -class Alpha2Climate(CoordinatorEntity, ClimateEntity): +class Alpha2Climate(CoordinatorEntity[Alpha2BaseCoordinator], ClimateEntity): """Alpha2 ClimateEntity.""" - coordinator: Alpha2BaseCoordinator target_temperature_step = 0.2 _attr_supported_features = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE diff --git a/homeassistant/components/moehlenhoff_alpha2/manifest.json b/homeassistant/components/moehlenhoff_alpha2/manifest.json index b755b28f826..db362163e72 100644 --- a/homeassistant/components/moehlenhoff_alpha2/manifest.json +++ b/homeassistant/components/moehlenhoff_alpha2/manifest.json @@ -5,7 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/moehlenhoff_alpha2", "requirements": ["moehlenhoff-alpha2==1.1.2"], "iot_class": "local_push", - "codeowners": [ - "@j-a-n" - ] + "codeowners": ["@j-a-n"] } diff --git a/homeassistant/components/moehlenhoff_alpha2/translations/fr.json b/homeassistant/components/moehlenhoff_alpha2/translations/fr.json index 205436aa03d..27fc4e30675 100644 --- a/homeassistant/components/moehlenhoff_alpha2/translations/fr.json +++ b/homeassistant/components/moehlenhoff_alpha2/translations/fr.json @@ -4,7 +4,7 @@ "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" }, "error": { - "cannot_connect": "Impossible de se connecter", + "cannot_connect": "\u00c9chec de connexion", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/moon/__init__.py b/homeassistant/components/moon/__init__.py index f7049608dda..0b36ba59198 100644 --- a/homeassistant/components/moon/__init__.py +++ b/homeassistant/components/moon/__init__.py @@ -1 +1,16 @@ -"""The moon component.""" +"""The Moon integration.""" +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from .const import PLATFORMS + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up from a config entry.""" + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/moon/config_flow.py b/homeassistant/components/moon/config_flow.py new file mode 100644 index 00000000000..abdd60c7b65 --- /dev/null +++ b/homeassistant/components/moon/config_flow.py @@ -0,0 +1,35 @@ +"""Config flow to configure the Moon integration.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.config_entries import ConfigFlow +from homeassistant.const import CONF_NAME +from homeassistant.data_entry_flow import FlowResult + +from .const import DEFAULT_NAME, DOMAIN + + +class MoonConfigFlow(ConfigFlow, domain=DOMAIN): + """Config flow for Moon.""" + + VERSION = 1 + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle a flow initialized by the user.""" + if self._async_current_entries(): + return self.async_abort(reason="single_instance_allowed") + + if user_input is not None: + return self.async_create_entry( + title=user_input.get(CONF_NAME, DEFAULT_NAME), + data={}, + ) + + return self.async_show_form(step_id="user") + + async def async_step_import(self, user_input: dict[str, Any]) -> FlowResult: + """Handle import from configuration.yaml.""" + return await self.async_step_user(user_input) diff --git a/homeassistant/components/moon/const.py b/homeassistant/components/moon/const.py new file mode 100644 index 00000000000..87c525758b9 --- /dev/null +++ b/homeassistant/components/moon/const.py @@ -0,0 +1,9 @@ +"""Constants for the Moon integration.""" +from typing import Final + +from homeassistant.const import Platform + +DOMAIN: Final = "moon" +PLATFORMS: Final = [Platform.SENSOR] + +DEFAULT_NAME: Final = "Moon" diff --git a/homeassistant/components/moon/manifest.json b/homeassistant/components/moon/manifest.json index 19fb952f59f..0402a87cf1a 100644 --- a/homeassistant/components/moon/manifest.json +++ b/homeassistant/components/moon/manifest.json @@ -2,7 +2,8 @@ "domain": "moon", "name": "Moon", "documentation": "https://www.home-assistant.io/integrations/moon", - "codeowners": ["@fabaff"], + "codeowners": ["@fabaff", "@frenck"], "quality_scale": "internal", - "iot_class": "local_polling" + "iot_class": "local_polling", + "config_flow": true } diff --git a/homeassistant/components/moon/sensor.py b/homeassistant/components/moon/sensor.py index cd10d9168d9..c5078771af8 100644 --- a/homeassistant/components/moon/sensor.py +++ b/homeassistant/components/moon/sensor.py @@ -8,6 +8,7 @@ from homeassistant.components.sensor import ( PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, SensorEntity, ) +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -15,7 +16,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType import homeassistant.util.dt as dt_util -DEFAULT_NAME = "Moon" +from .const import DEFAULT_NAME, DOMAIN STATE_FIRST_QUARTER = "first_quarter" STATE_FULL_MOON = "full_moon" @@ -49,23 +50,37 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the Moon sensor.""" - name: str = config[CONF_NAME] - - async_add_entities([MoonSensor(name)], True) + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=config, + ) + ) -class MoonSensor(SensorEntity): +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the platform from config_entry.""" + async_add_entities([MoonSensorEntity(entry)], True) + + +class MoonSensorEntity(SensorEntity): """Representation of a Moon sensor.""" _attr_device_class = "moon__phase" - def __init__(self, name: str) -> None: + def __init__(self, entry: ConfigEntry) -> None: """Initialize the moon sensor.""" - self._attr_name = name + self._attr_name = entry.title + self._attr_unique_id = entry.entry_id - async def async_update(self): + async def async_update(self) -> None: """Get the time and updates the states.""" - today = dt_util.as_local(dt_util.utcnow()).date() + today = dt_util.now().date() state = moon.phase(today) if state < 0.5 or state > 27.5: diff --git a/homeassistant/components/moon/strings.json b/homeassistant/components/moon/strings.json new file mode 100644 index 00000000000..d5bb204a740 --- /dev/null +++ b/homeassistant/components/moon/strings.json @@ -0,0 +1,13 @@ +{ + "title": "Moon", + "config": { + "step": { + "user": { + "description": "[%key:common::config_flow::description::confirm_setup%]" + } + }, + "abort": { + "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" + } + } +} diff --git a/homeassistant/components/moon/translations/bg.json b/homeassistant/components/moon/translations/bg.json new file mode 100644 index 00000000000..71462a123f9 --- /dev/null +++ b/homeassistant/components/moon/translations/bg.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u0412\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e. \u0412\u044a\u0437\u043c\u043e\u0436\u043d\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f." + }, + "step": { + "user": { + "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u0437\u0430\u043f\u043e\u0447\u043d\u0435\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u0432\u0430\u043d\u0435\u0442\u043e?" + } + } + }, + "title": "\u041b\u0443\u043d\u0430" +} \ No newline at end of file diff --git a/homeassistant/components/moon/translations/ca.json b/homeassistant/components/moon/translations/ca.json new file mode 100644 index 00000000000..085de62df8c --- /dev/null +++ b/homeassistant/components/moon/translations/ca.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3." + }, + "step": { + "user": { + "description": "Vols comen\u00e7ar la configuraci\u00f3?" + } + } + }, + "title": "Lluna" +} \ No newline at end of file diff --git a/homeassistant/components/moon/translations/de.json b/homeassistant/components/moon/translations/de.json new file mode 100644 index 00000000000..4d8d2d45284 --- /dev/null +++ b/homeassistant/components/moon/translations/de.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." + }, + "step": { + "user": { + "description": "M\u00f6chtest Du mit der Einrichtung beginnen?" + } + } + }, + "title": "Mond" +} \ No newline at end of file diff --git a/homeassistant/components/moon/translations/el.json b/homeassistant/components/moon/translations/el.json new file mode 100644 index 00000000000..51cde4e9c65 --- /dev/null +++ b/homeassistant/components/moon/translations/el.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u0388\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af. \u039c\u03cc\u03bd\u03bf \u03bc\u03af\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae." + }, + "step": { + "user": { + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03be\u03b5\u03ba\u03b9\u03bd\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7;" + } + } + }, + "title": "\u03a6\u03b5\u03b3\u03b3\u03ac\u03c1\u03b9" +} \ No newline at end of file diff --git a/homeassistant/components/moon/translations/en.json b/homeassistant/components/moon/translations/en.json new file mode 100644 index 00000000000..0f324f7b64b --- /dev/null +++ b/homeassistant/components/moon/translations/en.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Already configured. Only a single configuration possible." + }, + "step": { + "user": { + "description": "Do you want to start set up?" + } + } + }, + "title": "Moon" +} \ No newline at end of file diff --git a/homeassistant/components/moon/translations/es.json b/homeassistant/components/moon/translations/es.json new file mode 100644 index 00000000000..d23683ceb5d --- /dev/null +++ b/homeassistant/components/moon/translations/es.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Ya configurado. Solo una configuraci\u00f3n es posible." + }, + "step": { + "user": { + "description": "\u00bfQuieres empezar la configuraci\u00f3n?" + } + } + }, + "title": "Luna" +} \ No newline at end of file diff --git a/homeassistant/components/moon/translations/et.json b/homeassistant/components/moon/translations/et.json new file mode 100644 index 00000000000..28eccf25439 --- /dev/null +++ b/homeassistant/components/moon/translations/et.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Juba seadistatud. Lubatud on ainult \u00fcks sidumine." + }, + "step": { + "user": { + "description": "Kas alustada seadistamist?" + } + } + }, + "title": "Kuu" +} \ No newline at end of file diff --git a/homeassistant/components/moon/translations/fr.json b/homeassistant/components/moon/translations/fr.json new file mode 100644 index 00000000000..0b67320b311 --- /dev/null +++ b/homeassistant/components/moon/translations/fr.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." + }, + "step": { + "user": { + "description": "Voulez-vous commencer la configuration\u00a0?" + } + } + }, + "title": "Lune" +} \ No newline at end of file diff --git a/homeassistant/components/moon/translations/he.json b/homeassistant/components/moon/translations/he.json new file mode 100644 index 00000000000..e5602d2dc15 --- /dev/null +++ b/homeassistant/components/moon/translations/he.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." + }, + "step": { + "user": { + "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05ea\u05d7\u05d9\u05dc \u05d1\u05d4\u05d2\u05d3\u05e8\u05d4?" + } + } + }, + "title": "\u05d9\u05e8\u05d7" +} \ No newline at end of file diff --git a/homeassistant/components/moon/translations/hu.json b/homeassistant/components/moon/translations/hu.json new file mode 100644 index 00000000000..8c6a9f42071 --- /dev/null +++ b/homeassistant/components/moon/translations/hu.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." + }, + "step": { + "user": { + "description": "El szeretn\u00e9 kezdeni a be\u00e1ll\u00edt\u00e1st?" + } + } + }, + "title": "Hold" +} \ No newline at end of file diff --git a/homeassistant/components/moon/translations/id.json b/homeassistant/components/moon/translations/id.json new file mode 100644 index 00000000000..42b208bfb7e --- /dev/null +++ b/homeassistant/components/moon/translations/id.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan." + }, + "step": { + "user": { + "description": "Ingin memulai penyiapan?" + } + } + }, + "title": "Bulan" +} \ No newline at end of file diff --git a/homeassistant/components/moon/translations/it.json b/homeassistant/components/moon/translations/it.json new file mode 100644 index 00000000000..af891532233 --- /dev/null +++ b/homeassistant/components/moon/translations/it.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione." + }, + "step": { + "user": { + "description": "Vuoi iniziare la configurazione?" + } + } + }, + "title": "Luna" +} \ No newline at end of file diff --git a/homeassistant/components/moon/translations/ja.json b/homeassistant/components/moon/translations/ja.json new file mode 100644 index 00000000000..6544580781f --- /dev/null +++ b/homeassistant/components/moon/translations/ja.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + }, + "step": { + "user": { + "description": "\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3092\u958b\u59cb\u3057\u307e\u3059\u304b\uff1f" + } + } + }, + "title": "\u6708" +} \ No newline at end of file diff --git a/homeassistant/components/moon/translations/nl.json b/homeassistant/components/moon/translations/nl.json new file mode 100644 index 00000000000..ebcef695e04 --- /dev/null +++ b/homeassistant/components/moon/translations/nl.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk." + }, + "step": { + "user": { + "description": "Wilt u beginnen met instellen?" + } + } + }, + "title": "Maan" +} \ No newline at end of file diff --git a/homeassistant/components/moon/translations/no.json b/homeassistant/components/moon/translations/no.json new file mode 100644 index 00000000000..c86dae4f615 --- /dev/null +++ b/homeassistant/components/moon/translations/no.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig." + }, + "step": { + "user": { + "description": "Vil du starte oppsettet?" + } + } + }, + "title": "M\u00e5ne" +} \ No newline at end of file diff --git a/homeassistant/components/moon/translations/pl.json b/homeassistant/components/moon/translations/pl.json new file mode 100644 index 00000000000..fe01b71dadf --- /dev/null +++ b/homeassistant/components/moon/translations/pl.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja." + }, + "step": { + "user": { + "description": "Czy chcesz rozpocz\u0105\u0107 konfiguracj\u0119?" + } + } + }, + "title": "Ksi\u0119\u017cyc" +} \ No newline at end of file diff --git a/homeassistant/components/moon/translations/pt-BR.json b/homeassistant/components/moon/translations/pt-BR.json new file mode 100644 index 00000000000..1570f4110ec --- /dev/null +++ b/homeassistant/components/moon/translations/pt-BR.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, + "step": { + "user": { + "description": "Deseja iniciar a configura\u00e7\u00e3o?" + } + } + }, + "title": "Moon" +} \ No newline at end of file diff --git a/homeassistant/components/moon/translations/ru.json b/homeassistant/components/moon/translations/ru.json new file mode 100644 index 00000000000..90f93873205 --- /dev/null +++ b/homeassistant/components/moon/translations/ru.json @@ -0,0 +1,13 @@ +{ + "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." + }, + "step": { + "user": { + "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0447\u0430\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443?" + } + } + }, + "title": "\u041b\u0443\u043d\u0430" +} \ No newline at end of file diff --git a/homeassistant/components/moon/translations/tr.json b/homeassistant/components/moon/translations/tr.json new file mode 100644 index 00000000000..0abcd94692c --- /dev/null +++ b/homeassistant/components/moon/translations/tr.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + }, + "step": { + "user": { + "description": "Kuruluma ba\u015flamak ister misiniz?" + } + } + }, + "title": "Ay" +} \ No newline at end of file diff --git a/homeassistant/components/moon/translations/zh-Hant.json b/homeassistant/components/moon/translations/zh-Hant.json new file mode 100644 index 00000000000..c84da0f79f2 --- /dev/null +++ b/homeassistant/components/moon/translations/zh-Hant.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" + }, + "step": { + "user": { + "description": "\u662f\u5426\u8981\u958b\u59cb\u8a2d\u5b9a\uff1f" + } + } + }, + "title": "\u6708\u76f8" +} \ No newline at end of file diff --git a/homeassistant/components/motion_blinds/__init__.py b/homeassistant/components/motion_blinds/__init__.py index b3712e6e832..508851e6dad 100644 --- a/homeassistant/components/motion_blinds/__init__.py +++ b/homeassistant/components/motion_blinds/__init__.py @@ -1,10 +1,11 @@ """The motion_blinds component.""" +import asyncio from datetime import timedelta import logging from socket import timeout from typing import TYPE_CHECKING -from motionblinds import AsyncMotionMulticast, ParseException +from motionblinds import DEVICE_TYPES_WIFI, AsyncMotionMulticast, ParseException from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY, CONF_HOST, EVENT_HOMEASSISTANT_STOP @@ -20,9 +21,11 @@ from .const import ( DEFAULT_INTERFACE, DEFAULT_WAIT_FOR_PUSH, DOMAIN, + KEY_API_LOCK, KEY_COORDINATOR, KEY_GATEWAY, KEY_MULTICAST_LISTENER, + KEY_VERSION, MANUFACTURER, PLATFORMS, UPDATE_INTERVAL, @@ -55,6 +58,7 @@ class DataUpdateCoordinatorMotionBlinds(DataUpdateCoordinator): update_interval=update_interval, ) + self.api_lock = coordinator_info[KEY_API_LOCK] self._gateway = coordinator_info[KEY_GATEWAY] self._wait_for_push = coordinator_info[CONF_WAIT_FOR_PUSH] @@ -87,7 +91,8 @@ class DataUpdateCoordinatorMotionBlinds(DataUpdateCoordinator): async def _async_update_data(self): """Fetch the latest data from the gateway and blinds.""" - data = await self.hass.async_add_executor_job(self.update_gateway) + async with self.api_lock: + data = await self.hass.async_add_executor_job(self.update_gateway) all_available = all(device[ATTR_AVAILABLE] for device in data.values()) if all_available: @@ -129,8 +134,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if not await connect_gateway_class.async_connect_gateway(host, key): raise ConfigEntryNotReady motion_gateway = connect_gateway_class.gateway_device + api_lock = asyncio.Lock() coordinator_info = { KEY_GATEWAY: motion_gateway, + KEY_API_LOCK: api_lock, CONF_WAIT_FOR_PUSH: wait_for_push, } @@ -147,29 +154,31 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Fetch initial data so we have data when entities subscribe await coordinator.async_config_entry_first_refresh() - hass.data[DOMAIN][entry.entry_id] = { - KEY_GATEWAY: motion_gateway, - KEY_COORDINATOR: coordinator, - } - if motion_gateway.firmware is not None: version = f"{motion_gateway.firmware}, protocol: {motion_gateway.protocol}" else: version = f"Protocol: {motion_gateway.protocol}" + hass.data[DOMAIN][entry.entry_id] = { + KEY_GATEWAY: motion_gateway, + KEY_COORDINATOR: coordinator, + KEY_VERSION: version, + } + if TYPE_CHECKING: assert entry.unique_id is not None - device_registry = dr.async_get(hass) - device_registry.async_get_or_create( - config_entry_id=entry.entry_id, - connections={(dr.CONNECTION_NETWORK_MAC, motion_gateway.mac)}, - identifiers={(DOMAIN, motion_gateway.mac)}, - manufacturer=MANUFACTURER, - name=entry.title, - model="Wi-Fi bridge", - sw_version=version, - ) + if motion_gateway.device_type not in DEVICE_TYPES_WIFI: + device_registry = dr.async_get(hass) + device_registry.async_get_or_create( + config_entry_id=entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, motion_gateway.mac)}, + identifiers={(DOMAIN, motion_gateway.mac)}, + manufacturer=MANUFACTURER, + name=entry.title, + model="Wi-Fi bridge", + sw_version=version, + ) hass.config_entries.async_setup_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/motion_blinds/config_flow.py b/homeassistant/components/motion_blinds/config_flow.py index 90ee92ccff3..a956289d72e 100644 --- a/homeassistant/components/motion_blinds/config_flow.py +++ b/homeassistant/components/motion_blinds/config_flow.py @@ -5,9 +5,11 @@ from motionblinds import AsyncMotionMulticast, MotionDiscovery import voluptuous as vol from homeassistant import config_entries -from homeassistant.components import network +from homeassistant.components import dhcp, network from homeassistant.const import CONF_API_KEY, CONF_HOST from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.device_registry import format_mac from .const import ( CONF_INTERFACE, @@ -72,6 +74,21 @@ class MotionBlindsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Get the options flow.""" return OptionsFlowHandler(config_entry) + async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: + """Handle discovery via dhcp.""" + mac_address = format_mac(discovery_info.macaddress).replace(":", "") + await self.async_set_unique_id(mac_address) + self._abort_if_unique_id_configured(updates={CONF_HOST: discovery_info.ip}) + + short_mac = mac_address[-6:].upper() + self.context["title_placeholders"] = { + "short_mac": short_mac, + "ip_address": discovery_info.ip, + } + + self._host = discovery_info.ip + return await self.async_step_connect() + async def async_step_user(self, user_input=None): """Handle a flow initialized by the user.""" errors = {} @@ -137,8 +154,14 @@ class MotionBlindsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): mac_address = motion_gateway.mac - await self.async_set_unique_id(mac_address) - self._abort_if_unique_id_configured() + await self.async_set_unique_id(mac_address, raise_on_progress=False) + self._abort_if_unique_id_configured( + updates={ + CONF_HOST: self._host, + CONF_API_KEY: key, + CONF_INTERFACE: multicast_interface, + } + ) return self.async_create_entry( title=DEFAULT_GATEWAY_NAME, diff --git a/homeassistant/components/motion_blinds/const.py b/homeassistant/components/motion_blinds/const.py index 01f74c4ef4d..a35aeb6cd89 100644 --- a/homeassistant/components/motion_blinds/const.py +++ b/homeassistant/components/motion_blinds/const.py @@ -13,8 +13,10 @@ DEFAULT_WAIT_FOR_PUSH = False DEFAULT_INTERFACE = "any" KEY_GATEWAY = "gateway" +KEY_API_LOCK = "api_lock" KEY_COORDINATOR = "coordinator" KEY_MULTICAST_LISTENER = "multicast_listener" +KEY_VERSION = "version" ATTR_WIDTH = "width" ATTR_ABSOLUTE_POSITION = "absolute_position" @@ -24,3 +26,4 @@ SERVICE_SET_ABSOLUTE_POSITION = "set_absolute_position" UPDATE_INTERVAL = 600 UPDATE_INTERVAL_FAST = 60 +UPDATE_INTERVAL_MOVING = 5 diff --git a/homeassistant/components/motion_blinds/cover.py b/homeassistant/components/motion_blinds/cover.py index 9bc952a21ea..80f5b4d60c4 100644 --- a/homeassistant/components/motion_blinds/cover.py +++ b/homeassistant/components/motion_blinds/cover.py @@ -1,7 +1,7 @@ """Support for Motion Blinds using their WLAN API.""" import logging -from motionblinds import BlindType +from motionblinds import DEVICE_TYPES_WIFI, BlindType import voluptuous as vol from homeassistant.components.cover import ( @@ -12,9 +12,14 @@ from homeassistant.components.cover import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers import config_validation as cv, entity_platform +from homeassistant.helpers import ( + config_validation as cv, + device_registry as dr, + entity_platform, +) from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.event import async_call_later from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( @@ -24,8 +29,10 @@ from .const import ( DOMAIN, KEY_COORDINATOR, KEY_GATEWAY, + KEY_VERSION, MANUFACTURER, SERVICE_SET_ABSOLUTE_POSITION, + UPDATE_INTERVAL_MOVING, ) _LOGGER = logging.getLogger(__name__) @@ -44,6 +51,7 @@ POSITION_DEVICE_MAP = { BlindType.Curtain: CoverDeviceClass.CURTAIN, BlindType.CurtainLeft: CoverDeviceClass.CURTAIN, BlindType.CurtainRight: CoverDeviceClass.CURTAIN, + BlindType.SkylightBlind: CoverDeviceClass.SHADE, } TILT_DEVICE_MAP = { @@ -52,6 +60,7 @@ TILT_DEVICE_MAP = { BlindType.DoubleRoller: CoverDeviceClass.SHADE, BlindType.VerticalBlind: CoverDeviceClass.BLIND, BlindType.VerticalBlindLeft: CoverDeviceClass.BLIND, + BlindType.VerticalBlindRight: CoverDeviceClass.BLIND, } TDBU_DEVICE_MAP = { @@ -74,26 +83,37 @@ async def async_setup_entry( entities = [] motion_gateway = hass.data[DOMAIN][config_entry.entry_id][KEY_GATEWAY] coordinator = hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR] + sw_version = hass.data[DOMAIN][config_entry.entry_id][KEY_VERSION] for blind in motion_gateway.device_list.values(): if blind.type in POSITION_DEVICE_MAP: entities.append( MotionPositionDevice( - coordinator, blind, POSITION_DEVICE_MAP[blind.type], config_entry + coordinator, + blind, + POSITION_DEVICE_MAP[blind.type], + sw_version, ) ) elif blind.type in TILT_DEVICE_MAP: entities.append( MotionTiltDevice( - coordinator, blind, TILT_DEVICE_MAP[blind.type], config_entry + coordinator, + blind, + TILT_DEVICE_MAP[blind.type], + sw_version, ) ) elif blind.type in TDBU_DEVICE_MAP: entities.append( MotionTDBUDevice( - coordinator, blind, TDBU_DEVICE_MAP[blind.type], config_entry, "Top" + coordinator, + blind, + TDBU_DEVICE_MAP[blind.type], + sw_version, + "Top", ) ) entities.append( @@ -101,7 +121,7 @@ async def async_setup_entry( coordinator, blind, TDBU_DEVICE_MAP[blind.type], - config_entry, + sw_version, "Bottom", ) ) @@ -110,13 +130,24 @@ async def async_setup_entry( coordinator, blind, TDBU_DEVICE_MAP[blind.type], - config_entry, + sw_version, "Combined", ) ) else: - _LOGGER.warning("Blind type '%s' not yet supported", blind.blind_type) + _LOGGER.warning( + "Blind type '%s' not yet supported, assuming RollerBlind", + blind.blind_type, + ) + entities.append( + MotionPositionDevice( + coordinator, + blind, + POSITION_DEVICE_MAP[BlindType.RollerBlind], + sw_version, + ) + ) async_add_entities(entities) @@ -131,22 +162,36 @@ async def async_setup_entry( class MotionPositionDevice(CoordinatorEntity, CoverEntity): """Representation of a Motion Blind Device.""" - def __init__(self, coordinator, blind, device_class, config_entry): + def __init__(self, coordinator, blind, device_class, sw_version): """Initialize the blind.""" super().__init__(coordinator) self._blind = blind - self._config_entry = config_entry + self._api_lock = coordinator.api_lock + self._requesting_position = False + self._previous_positions = [] + + if blind.device_type in DEVICE_TYPES_WIFI: + via_device = () + connections = {(dr.CONNECTION_NETWORK_MAC, blind.mac)} + name = blind.blind_type + else: + via_device = (DOMAIN, blind._gateway.mac) + connections = {} + name = f"{blind.blind_type}-{blind.mac[12:]}" + sw_version = None self._attr_device_class = device_class - self._attr_name = f"{blind.blind_type}-{blind.mac[12:]}" + self._attr_name = name self._attr_unique_id = blind.mac self._attr_device_info = DeviceInfo( + connections=connections, identifiers={(DOMAIN, blind.mac)}, manufacturer=MANUFACTURER, model=blind.blind_type, - name=f"{blind.blind_type}-{blind.mac[12:]}", - via_device=(DOMAIN, blind._gateway.mac), + name=name, + via_device=via_device, + sw_version=sw_version, hw_version=blind.wireless_name, ) @@ -189,27 +234,75 @@ class MotionPositionDevice(CoordinatorEntity, CoverEntity): self._blind.Remove_callback(self.unique_id) await super().async_will_remove_from_hass() - def open_cover(self, **kwargs): + async def async_scheduled_update_request(self, *_): + """Request a state update from the blind at a scheduled point in time.""" + # add the last position to the list and keep the list at max 2 items + self._previous_positions.append(self.current_cover_position) + if len(self._previous_positions) > 2: + del self._previous_positions[: len(self._previous_positions) - 2] + + async with self._api_lock: + await self.hass.async_add_executor_job(self._blind.Update_trigger) + + self.async_write_ha_state() + + if len(self._previous_positions) < 2 or not all( + self.current_cover_position == prev_position + for prev_position in self._previous_positions + ): + # keep updating the position @UPDATE_INTERVAL_MOVING until the position does not change. + async_call_later( + self.hass, UPDATE_INTERVAL_MOVING, self.async_scheduled_update_request + ) + else: + self._previous_positions = [] + self._requesting_position = False + + async def async_request_position_till_stop(self): + """Request the position of the blind every UPDATE_INTERVAL_MOVING seconds until it stops moving.""" + self._previous_positions = [] + if self._requesting_position or self.current_cover_position is None: + return + + self._requesting_position = True + async_call_later( + self.hass, UPDATE_INTERVAL_MOVING, self.async_scheduled_update_request + ) + + async def async_open_cover(self, **kwargs): """Open the cover.""" - self._blind.Open() + async with self._api_lock: + await self.hass.async_add_executor_job(self._blind.Open) + await self.async_request_position_till_stop() - def close_cover(self, **kwargs): + async def async_close_cover(self, **kwargs): """Close cover.""" - self._blind.Close() + async with self._api_lock: + await self.hass.async_add_executor_job(self._blind.Close) + await self.async_request_position_till_stop() - def set_cover_position(self, **kwargs): + async def async_set_cover_position(self, **kwargs): """Move the cover to a specific position.""" position = kwargs[ATTR_POSITION] - self._blind.Set_position(100 - position) + async with self._api_lock: + await self.hass.async_add_executor_job( + self._blind.Set_position, 100 - position + ) + await self.async_request_position_till_stop() - def set_absolute_position(self, **kwargs): + async def async_set_absolute_position(self, **kwargs): """Move the cover to a specific absolute position (see TDBU).""" position = kwargs[ATTR_ABSOLUTE_POSITION] - self._blind.Set_position(100 - position) + async with self._api_lock: + await self.hass.async_add_executor_job( + self._blind.Set_position, 100 - position + ) + await self.async_request_position_till_stop() - def stop_cover(self, **kwargs): + async def async_stop_cover(self, **kwargs): """Stop the cover.""" - self._blind.Stop() + async with self._api_lock: + await self.hass.async_add_executor_job(self._blind.Stop) class MotionTiltDevice(MotionPositionDevice): @@ -226,30 +319,34 @@ class MotionTiltDevice(MotionPositionDevice): return None return self._blind.angle * 100 / 180 - def open_cover_tilt(self, **kwargs): + async def async_open_cover_tilt(self, **kwargs): """Open the cover tilt.""" - self._blind.Set_angle(180) + async with self._api_lock: + await self.hass.async_add_executor_job(self._blind.Set_angle, 180) - def close_cover_tilt(self, **kwargs): + async def async_close_cover_tilt(self, **kwargs): """Close the cover tilt.""" - self._blind.Set_angle(0) + async with self._api_lock: + await self.hass.async_add_executor_job(self._blind.Set_angle, 0) - def set_cover_tilt_position(self, **kwargs): + async def async_set_cover_tilt_position(self, **kwargs): """Move the cover tilt to a specific position.""" angle = kwargs[ATTR_TILT_POSITION] * 180 / 100 - self._blind.Set_angle(angle) + async with self._api_lock: + await self.hass.async_add_executor_job(self._blind.Set_angle, angle) - def stop_cover_tilt(self, **kwargs): + async def async_stop_cover_tilt(self, **kwargs): """Stop the cover.""" - self._blind.Stop() + async with self._api_lock: + await self.hass.async_add_executor_job(self._blind.Stop) class MotionTDBUDevice(MotionPositionDevice): """Representation of a Motion Top Down Bottom Up blind Device.""" - def __init__(self, coordinator, blind, device_class, config_entry, motor): + def __init__(self, coordinator, blind, device_class, sw_version, motor): """Initialize the blind.""" - super().__init__(coordinator, blind, device_class, config_entry) + super().__init__(coordinator, blind, device_class, sw_version) self._motor = motor self._motor_key = motor[0] self._attr_name = f"{blind.blind_type}-{motor}-{blind.mac[12:]}" @@ -293,28 +390,40 @@ class MotionTDBUDevice(MotionPositionDevice): attributes[ATTR_WIDTH] = self._blind.width return attributes - def open_cover(self, **kwargs): + async def async_open_cover(self, **kwargs): """Open the cover.""" - self._blind.Open(motor=self._motor_key) + async with self._api_lock: + await self.hass.async_add_executor_job(self._blind.Open, self._motor_key) + await self.async_request_position_till_stop() - def close_cover(self, **kwargs): + async def async_close_cover(self, **kwargs): """Close cover.""" - self._blind.Close(motor=self._motor_key) + async with self._api_lock: + await self.hass.async_add_executor_job(self._blind.Close, self._motor_key) + await self.async_request_position_till_stop() - def set_cover_position(self, **kwargs): + async def async_set_cover_position(self, **kwargs): """Move the cover to a specific scaled position.""" position = kwargs[ATTR_POSITION] - self._blind.Set_scaled_position(100 - position, motor=self._motor_key) + async with self._api_lock: + await self.hass.async_add_executor_job( + self._blind.Set_scaled_position, 100 - position, self._motor_key + ) + await self.async_request_position_till_stop() - def set_absolute_position(self, **kwargs): + async def async_set_absolute_position(self, **kwargs): """Move the cover to a specific absolute position.""" position = kwargs[ATTR_ABSOLUTE_POSITION] target_width = kwargs.get(ATTR_WIDTH, None) - self._blind.Set_position( - 100 - position, motor=self._motor_key, width=target_width - ) + async with self._api_lock: + await self.hass.async_add_executor_job( + self._blind.Set_position, 100 - position, self._motor_key, target_width + ) - def stop_cover(self, **kwargs): + await self.async_request_position_till_stop() + + async def async_stop_cover(self, **kwargs): """Stop the cover.""" - self._blind.Stop(motor=self._motor_key) + async with self._api_lock: + await self.hass.async_add_executor_job(self._blind.Stop, self._motor_key) diff --git a/homeassistant/components/motion_blinds/manifest.json b/homeassistant/components/motion_blinds/manifest.json index c904320d9af..4f4575ae6dd 100644 --- a/homeassistant/components/motion_blinds/manifest.json +++ b/homeassistant/components/motion_blinds/manifest.json @@ -3,8 +3,14 @@ "name": "Motion Blinds", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/motion_blinds", - "requirements": ["motionblinds==0.5.13"], + "requirements": ["motionblinds==0.6.2"], "dependencies": ["network"], + "dhcp": [ + { "registered_devices": true }, + { + "hostname": "motion_*" + } + ], "codeowners": ["@starkillerOG"], "iot_class": "local_push", "loggers": ["motionblinds"] diff --git a/homeassistant/components/motion_blinds/sensor.py b/homeassistant/components/motion_blinds/sensor.py index 4c0b4251bf2..03e3a6e7618 100644 --- a/homeassistant/components/motion_blinds/sensor.py +++ b/homeassistant/components/motion_blinds/sensor.py @@ -1,5 +1,5 @@ """Support for Motion Blinds sensors.""" -from motionblinds import BlindType +from motionblinds import DEVICE_TYPES_WIFI, BlindType from homeassistant.components.sensor import SensorDeviceClass, SensorEntity from homeassistant.config_entries import ConfigEntry @@ -31,13 +31,15 @@ async def async_setup_entry( if blind.type == BlindType.TopDownBottomUp: entities.append(MotionTDBUBatterySensor(coordinator, blind, "Bottom")) entities.append(MotionTDBUBatterySensor(coordinator, blind, "Top")) - elif blind.battery_voltage > 0: + elif blind.battery_voltage is not None and blind.battery_voltage > 0: # Only add battery powered blinds entities.append(MotionBatterySensor(coordinator, blind)) - entities.append( - MotionSignalStrengthSensor(coordinator, motion_gateway, TYPE_GATEWAY) - ) + # Do not add signal sensor twice for direct WiFi blinds + if motion_gateway.device_type not in DEVICE_TYPES_WIFI: + entities.append( + MotionSignalStrengthSensor(coordinator, motion_gateway, TYPE_GATEWAY) + ) async_add_entities(entities) @@ -52,9 +54,14 @@ class MotionBatterySensor(CoordinatorEntity, SensorEntity): """Initialize the Motion Battery Sensor.""" super().__init__(coordinator) + if blind.device_type in DEVICE_TYPES_WIFI: + name = f"{blind.blind_type}-battery" + else: + name = f"{blind.blind_type}-battery-{blind.mac[12:]}" + self._blind = blind self._attr_device_info = DeviceInfo(identifiers={(DOMAIN, blind.mac)}) - self._attr_name = f"{blind.blind_type}-battery-{blind.mac[12:]}" + self._attr_name = name self._attr_unique_id = f"{blind.mac}-battery" @property @@ -96,9 +103,14 @@ class MotionTDBUBatterySensor(MotionBatterySensor): """Initialize the Motion Battery Sensor.""" super().__init__(coordinator, blind) + if blind.device_type in DEVICE_TYPES_WIFI: + name = f"{blind.blind_type}-{motor}-battery" + else: + name = f"{blind.blind_type}-{motor}-battery-{blind.mac[12:]}" + self._motor = motor self._attr_unique_id = f"{blind.mac}-{motor}-battery" - self._attr_name = f"{blind.blind_type}-{motor}-battery-{blind.mac[12:]}" + self._attr_name = name @property def native_value(self): @@ -130,17 +142,18 @@ class MotionSignalStrengthSensor(CoordinatorEntity, SensorEntity): """Initialize the Motion Signal Strength Sensor.""" super().__init__(coordinator) + if device_type == TYPE_GATEWAY: + name = "Motion gateway signal strength" + elif device.device_type in DEVICE_TYPES_WIFI: + name = f"{device.blind_type} signal strength" + else: + name = f"{device.blind_type} signal strength - {device.mac[12:]}" + self._device = device self._device_type = device_type self._attr_device_info = DeviceInfo(identifiers={(DOMAIN, device.mac)}) self._attr_unique_id = f"{device.mac}-RSSI" - - @property - def name(self): - """Return the name of the blind signal strength sensor.""" - if self._device_type == TYPE_GATEWAY: - return "Motion gateway signal strength" - return f"{self._device.blind_type} signal strength - {self._device.mac[12:]}" + self._attr_name = name @property def available(self): diff --git a/homeassistant/components/motion_blinds/strings.json b/homeassistant/components/motion_blinds/strings.json index e5c86c2a45e..c62c6dc2873 100644 --- a/homeassistant/components/motion_blinds/strings.json +++ b/homeassistant/components/motion_blinds/strings.json @@ -1,15 +1,14 @@ { "config": { + "flow_title": "{short_mac} ({ip_address})", "step": { "user": { - "title": "Motion Blinds", "description": "Connect to your Motion Gateway, if the IP address is not set, auto-discovery is used", "data": { "host": "[%key:common::config_flow::data::ip%]" } }, "connect": { - "title": "Motion Blinds", "description": "You will need the 16 character API Key, see https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key for instructions", "data": { "api_key": "[%key:common::config_flow::data::api_key%]", @@ -29,7 +28,7 @@ "invalid_interface": "Invalid network interface" }, "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "already_configured": "[%key:common::config_flow::abort::already_configured_device%], connection settings are updated", "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "connection_error": "[%key:common::config_flow::error::cannot_connect%]" } @@ -37,8 +36,6 @@ "options": { "step": { "init": { - "title": "Motion Blinds", - "description": "Specify optional settings", "data": { "wait_for_push": "Wait for multicast push on update" } @@ -46,6 +43,3 @@ } } } - - - diff --git a/homeassistant/components/motion_blinds/translations/en.json b/homeassistant/components/motion_blinds/translations/en.json index 8f0d21addce..92931ee27ab 100644 --- a/homeassistant/components/motion_blinds/translations/en.json +++ b/homeassistant/components/motion_blinds/translations/en.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Device is already configured", + "already_configured": "Device is already configured, connection settings are updated", "already_in_progress": "Configuration flow is already in progress", "connection_error": "Failed to connect" }, @@ -9,7 +9,7 @@ "discovery_error": "Failed to discover a Motion Gateway", "invalid_interface": "Invalid network interface" }, - "flow_title": "Motion Blinds", + "flow_title": "{short_mac} ({ip_address})", "step": { "connect": { "data": { diff --git a/homeassistant/components/motion_blinds/translations/zh-Hant.json b/homeassistant/components/motion_blinds/translations/zh-Hant.json index 7aeb111ed3f..df300e0511f 100644 --- a/homeassistant/components/motion_blinds/translations/zh-Hant.json +++ b/homeassistant/components/motion_blinds/translations/zh-Hant.json @@ -6,7 +6,7 @@ "connection_error": "\u9023\u7dda\u5931\u6557" }, "error": { - "discovery_error": "\u63a2\u7d22 Motion \u9598\u9053\u5668\u5931\u6557", + "discovery_error": "\u641c\u7d22 Motion \u9598\u9053\u5668\u5931\u6557", "invalid_interface": "\u7db2\u8def\u4ecb\u9762\u7121\u6548" }, "flow_title": "Motion Blinds", @@ -31,7 +31,7 @@ "api_key": "API \u91d1\u9470", "host": "IP \u4f4d\u5740" }, - "description": "\u9023\u7dda\u81f3 Motion \u9598\u9053\u5668\uff0c\u5047\u5982\u672a\u63d0\u4f9b IP \u4f4d\u5740\uff0c\u5c07\u4f7f\u7528\u81ea\u52d5\u63a2\u7d22", + "description": "\u9023\u7dda\u81f3 Motion \u9598\u9053\u5668\uff0c\u5047\u5982\u672a\u63d0\u4f9b IP \u4f4d\u5740\uff0c\u5c07\u4f7f\u7528\u81ea\u52d5\u641c\u7d22", "title": "Motion Blinds" } } diff --git a/homeassistant/components/motioneye/strings.json b/homeassistant/components/motioneye/strings.json index 0f17699e652..742a7ec59a8 100644 --- a/homeassistant/components/motioneye/strings.json +++ b/homeassistant/components/motioneye/strings.json @@ -37,4 +37,4 @@ } } } -} \ No newline at end of file +} diff --git a/homeassistant/components/motioneye/translations/fr.json b/homeassistant/components/motioneye/translations/fr.json index db844987d88..e498cf9987a 100644 --- a/homeassistant/components/motioneye/translations/fr.json +++ b/homeassistant/components/motioneye/translations/fr.json @@ -6,8 +6,8 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", - "invalid_url": "URL invalide", + "invalid_auth": "Authentification non valide", + "invalid_url": "URL non valide", "unknown": "Erreur inattendue" }, "step": { @@ -17,10 +17,10 @@ }, "user": { "data": { - "admin_password": "Admin Mot de passe", - "admin_username": "Admin Nom d'utilisateur", - "surveillance_password": "Surveillance Mot de passe", - "surveillance_username": "Surveillance Nom d'utilisateur", + "admin_password": "Mot de passe Admin", + "admin_username": "Nom d'utilisateur Admin", + "surveillance_password": "Mot de passe Surveillance", + "surveillance_username": "Nom d'utilisateur Surveillance", "url": "URL" } } diff --git a/homeassistant/components/mpchc/__init__.py b/homeassistant/components/mpchc/__init__.py deleted file mode 100644 index e8a0057a9b6..00000000000 --- a/homeassistant/components/mpchc/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The mpchc component.""" diff --git a/homeassistant/components/mpchc/manifest.json b/homeassistant/components/mpchc/manifest.json deleted file mode 100644 index a1a9e769be6..00000000000 --- a/homeassistant/components/mpchc/manifest.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "domain": "mpchc", - "name": "Media Player Classic Home Cinema (MPC-HC)", - "documentation": "https://www.home-assistant.io/integrations/mpchc", - "codeowners": [], - "iot_class": "local_polling" -} diff --git a/homeassistant/components/mpchc/media_player.py b/homeassistant/components/mpchc/media_player.py deleted file mode 100644 index c2d92da7eac..00000000000 --- a/homeassistant/components/mpchc/media_player.py +++ /dev/null @@ -1,199 +0,0 @@ -"""Support to interface with the MPC-HC Web API.""" -from __future__ import annotations - -import logging -import re - -import requests -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_STOP, - SUPPORT_VOLUME_MUTE, - SUPPORT_VOLUME_STEP, -) -from homeassistant.const import ( - CONF_HOST, - CONF_NAME, - CONF_PORT, - STATE_IDLE, - STATE_OFF, - STATE_PAUSED, - STATE_PLAYING, -) -from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType - -_LOGGER = logging.getLogger(__name__) - -DEFAULT_NAME = "MPC-HC" -DEFAULT_PORT = 13579 - -SUPPORT_MPCHC = ( - SUPPORT_VOLUME_MUTE - | SUPPORT_PAUSE - | SUPPORT_STOP - | SUPPORT_PREVIOUS_TRACK - | SUPPORT_NEXT_TRACK - | SUPPORT_VOLUME_STEP - | 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, - } -) - - -def setup_platform( - hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the MPC-HC platform.""" - _LOGGER.warning( - "The Media Player Classic Home Cinema integration is now deprecated " - "and will be removed in Home Assistant Core 2022.4; " - "this integration is removed under Architectural Decision Record 0004, " - "more information can be found here: " - "https://github.com/home-assistant/architecture/blob/master/adr/0004-webscraping.md" - ) - - name = config.get(CONF_NAME) - host = config.get(CONF_HOST) - port = config.get(CONF_PORT) - - url = f"{host}:{port}" - - add_entities([MpcHcDevice(name, url)], True) - - -class MpcHcDevice(MediaPlayerEntity): - """Representation of a MPC-HC server.""" - - def __init__(self, name, url): - """Initialize the MPC-HC device.""" - self._name = name - self._url = url - self._player_variables = {} - self._available = False - - def update(self): - """Get the latest details.""" - try: - response = requests.get(f"{self._url}/variables.html", data=None, timeout=3) - - mpchc_variables = re.findall(r'

(.+?)

', response.text) - - for var in mpchc_variables: - self._player_variables[var[0]] = var[1].lower() - self._available = True - except requests.exceptions.RequestException: - if self.available: - _LOGGER.error("Could not connect to MPC-HC at: %s", self._url) - - self._player_variables = {} - self._available = False - - def _send_command(self, command_id): - """Send a command to MPC-HC via its window message ID.""" - try: - params = {"wm_command": command_id} - requests.get(f"{self._url}/command.html", params=params, timeout=3) - except requests.exceptions.RequestException: - _LOGGER.error( - "Could not send command %d to MPC-HC at: %s", command_id, self._url - ) - - @property - def name(self): - """Return the name of the device.""" - return self._name - - @property - def state(self): - """Return the state of the device.""" - state = self._player_variables.get("statestring", None) - - if state is None: - return STATE_OFF - if state == "playing": - return STATE_PLAYING - if state == "paused": - return STATE_PAUSED - - return STATE_IDLE - - @property - def available(self): - """Return True if entity is available.""" - return self._available - - @property - def media_title(self): - """Return the title of current playing media.""" - return self._player_variables.get("file", None) - - @property - def volume_level(self): - """Return the volume level of the media player (0..1).""" - return int(self._player_variables.get("volumelevel", 0)) / 100.0 - - @property - def is_volume_muted(self): - """Return boolean if volume is currently muted.""" - return self._player_variables.get("muted", "0") == "1" - - @property - def media_duration(self): - """Return the duration of the current playing media in seconds.""" - duration = self._player_variables.get("durationstring", "00:00:00").split(":") - return int(duration[0]) * 3600 + int(duration[1]) * 60 + int(duration[2]) - - @property - def supported_features(self): - """Flag media player features that are supported.""" - return SUPPORT_MPCHC - - def volume_up(self): - """Volume up the media player.""" - self._send_command(907) - - def volume_down(self): - """Volume down media player.""" - self._send_command(908) - - def mute_volume(self, mute): - """Mute the volume.""" - self._send_command(909) - - def media_play(self): - """Send play command.""" - self._send_command(887) - - def media_pause(self): - """Send pause command.""" - self._send_command(888) - - def media_stop(self): - """Send stop command.""" - self._send_command(890) - - def media_next_track(self): - """Send next track command.""" - self._send_command(920) - - def media_previous_track(self): - """Send previous track command.""" - self._send_command(919) diff --git a/homeassistant/components/mpd/media_player.py b/homeassistant/components/mpd/media_player.py index 4d3df8f00ca..fca3e8bec0d 100644 --- a/homeassistant/components/mpd/media_player.py +++ b/homeassistant/components/mpd/media_player.py @@ -11,13 +11,18 @@ import mpd from mpd.asyncio import MPDClient import voluptuous as vol +from homeassistant.components import media_source from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity +from homeassistant.components.media_player.browse_media import ( + async_process_play_media_url, +) from homeassistant.components.media_player.const import ( MEDIA_TYPE_MUSIC, MEDIA_TYPE_PLAYLIST, REPEAT_MODE_ALL, REPEAT_MODE_OFF, REPEAT_MODE_ONE, + SUPPORT_BROWSE_MEDIA, SUPPORT_CLEAR_PLAYLIST, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, @@ -71,6 +76,7 @@ SUPPORT_MPD = ( | SUPPORT_STOP | SUPPORT_TURN_OFF | SUPPORT_TURN_ON + | SUPPORT_BROWSE_MEDIA ) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( @@ -221,8 +227,14 @@ class MpdDevice(MediaPlayerEntity): @property def media_duration(self): """Return the duration of current playing media in seconds.""" - # Time does not exist for streams - return self._currentsong.get("time") + if currentsong_time := self._currentsong.get("time"): + return currentsong_time + + time_from_status = self._status.get("time") + if isinstance(time_from_status, str) and ":" in time_from_status: + return time_from_status.split(":")[1] + + return None @property def media_position(self): @@ -259,7 +271,10 @@ class MpdDevice(MediaPlayerEntity): @property def media_artist(self): """Return the artist of current playing media (Music track only).""" - return self._currentsong.get("artist") + artists = self._currentsong.get("artist") + if isinstance(artists, list): + return ", ".join(artists) + return artists @property def media_album_name(self): @@ -445,8 +460,13 @@ class MpdDevice(MediaPlayerEntity): async def async_play_media(self, media_type, media_id, **kwargs): """Send the media player the command for playing a playlist.""" - _LOGGER.debug("Playing playlist: %s", media_id) + if media_source.is_media_source_id(media_id): + media_type = MEDIA_TYPE_MUSIC + play_item = await media_source.async_resolve_media(self.hass, media_id) + media_id = play_item.url + if media_type == MEDIA_TYPE_PLAYLIST: + _LOGGER.debug("Playing playlist: %s", media_id) if media_id in self._playlists: self._currentplaylist = media_id else: @@ -456,6 +476,8 @@ class MpdDevice(MediaPlayerEntity): await self._client.load(media_id) await self._client.play() else: + media_id = async_process_play_media_url(self.hass, media_id) + await self._client.clear() self._currentplaylist = None await self._client.add(media_id) @@ -507,3 +529,11 @@ class MpdDevice(MediaPlayerEntity): async def async_media_seek(self, position): """Send seek command.""" await self._client.seekcur(position) + + async def async_browse_media(self, media_content_type=None, media_content_id=None): + """Implement the websocket media browsing helper.""" + return await media_source.async_browse_media( + self.hass, + media_content_id, + content_filter=lambda item: item.media_content_type.startswith("audio/"), + ) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 1bb318f11c5..7c16da7f2aa 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -50,12 +50,7 @@ from homeassistant.core import ( ) from homeassistant.data_entry_flow import BaseServiceInfo from homeassistant.exceptions import HomeAssistantError, TemplateError, Unauthorized -from homeassistant.helpers import ( - config_validation as cv, - device_registry as dr, - event, - template, -) +from homeassistant.helpers import config_validation as cv, event, template from homeassistant.helpers.device_registry import DeviceEntry from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send from homeassistant.helpers.entity import Entity @@ -116,7 +111,7 @@ from .util import _VALID_QOS_SCHEMA, valid_publish_topic, valid_subscribe_topic if TYPE_CHECKING: # Only import for paho-mqtt type checking here, imports are done locally # because integrations should be able to optionally rely on MQTT. - import paho.mqtt.client as mqtt # pylint: disable=import-outside-toplevel + import paho.mqtt.client as mqtt _LOGGER = logging.getLogger(__name__) @@ -136,12 +131,15 @@ DEFAULT_PROTOCOL = PROTOCOL_311 DEFAULT_TLS_PROTOCOL = "auto" DEFAULT_VALUES = { - CONF_PORT: DEFAULT_PORT, - CONF_WILL_MESSAGE: DEFAULT_WILL, CONF_BIRTH_MESSAGE: DEFAULT_BIRTH, CONF_DISCOVERY: DEFAULT_DISCOVERY, + CONF_PORT: DEFAULT_PORT, + CONF_TLS_VERSION: DEFAULT_TLS_PROTOCOL, + CONF_WILL_MESSAGE: DEFAULT_WILL, } +MANDATORY_DEFAULT_VALUES = (CONF_PORT,) + ATTR_TOPIC_TEMPLATE = "topic_template" ATTR_PAYLOAD_TEMPLATE = "payload_template" @@ -208,9 +206,7 @@ CONFIG_SCHEMA_BASE = vol.Schema( CONF_CLIENT_CERT, "client_key_auth", msg=CLIENT_KEY_AUTH_MSG ): cv.isfile, vol.Optional(CONF_TLS_INSECURE): cv.boolean, - vol.Optional(CONF_TLS_VERSION, default=DEFAULT_TLS_PROTOCOL): vol.Any( - "auto", "1.0", "1.1", "1.2" - ), + vol.Optional(CONF_TLS_VERSION): vol.Any("auto", "1.0", "1.1", "1.2"), vol.Optional(CONF_PROTOCOL, default=DEFAULT_PROTOCOL): vol.All( cv.string, vol.In([PROTOCOL_31, PROTOCOL_311]) ), @@ -225,6 +221,17 @@ CONFIG_SCHEMA_BASE = vol.Schema( } ) +DEPRECATED_CONFIG_KEYS = [ + CONF_BIRTH_MESSAGE, + CONF_BROKER, + CONF_DISCOVERY, + CONF_PASSWORD, + CONF_PORT, + CONF_TLS_VERSION, + CONF_USERNAME, + CONF_WILL_MESSAGE, +] + CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.All( @@ -599,7 +606,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: conf: ConfigType | None = config.get(DOMAIN) websocket_api.async_register_command(hass, websocket_subscribe) - websocket_api.async_register_command(hass, websocket_remove_device) websocket_api.async_register_command(hass, websocket_mqtt_info) debug_info.initialize(hass) @@ -608,6 +614,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: hass.data[DATA_MQTT_CONFIG] = conf if not bool(hass.config_entries.async_entries(DOMAIN)): + # Create an import flow if the user has yaml configured entities etc. + # but no broker configuration. Note: The intention is not for this to + # import broker configuration from YAML because that has been deprecated. hass.async_create_task( hass.config_entries.flow.async_init( DOMAIN, @@ -618,31 +627,66 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return True -def _merge_config(entry, conf): - """Merge configuration.yaml config with config entry.""" - # Base config on default values +def _merge_basic_config( + hass: HomeAssistant, entry: ConfigEntry, yaml_config: dict[str, Any] +) -> None: + """Merge basic options in configuration.yaml config with config entry. + + This mends incomplete migration from old version of HA Core. + """ + + entry_updated = False + entry_config = {**entry.data} + for key in DEPRECATED_CONFIG_KEYS: + if key in yaml_config and key not in entry_config: + entry_config[key] = yaml_config[key] + entry_updated = True + + for key in MANDATORY_DEFAULT_VALUES: + if key not in entry_config: + entry_config[key] = DEFAULT_VALUES[key] + entry_updated = True + + if entry_updated: + hass.config_entries.async_update_entry(entry, data=entry_config) + + +def _merge_extended_config(entry, conf): + """Merge advanced options in configuration.yaml config with config entry.""" + # Add default values conf = {**DEFAULT_VALUES, **conf} return {**conf, **entry.data} async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Load a config entry.""" - # If user didn't have configuration.yaml config, generate defaults + # Merge basic configuration, and add missing defaults for basic options + _merge_basic_config(hass, entry, hass.data.get(DATA_MQTT_CONFIG, {})) + + # Bail out if broker setting is missing + if CONF_BROKER not in entry.data: + _LOGGER.error("MQTT broker is not configured, please configure it") + return False + + # If user doesn't have configuration.yaml config, generate default values + # for options not in config entry data if (conf := hass.data.get(DATA_MQTT_CONFIG)) is None: conf = CONFIG_SCHEMA_BASE(dict(entry.data)) + + # User has configuration.yaml config, warn about config entry overrides elif any(key in conf for key in entry.data): shared_keys = conf.keys() & entry.data.keys() override = {k: entry.data[k] for k in shared_keys} if CONF_PASSWORD in override: override[CONF_PASSWORD] = "********" - _LOGGER.info( - "Data in your configuration entry is going to override your " - "configuration.yaml: %s", + _LOGGER.warning( + "Deprecated configuration settings found in configuration.yaml. " + "These settings from your configuration entry will override: %s", override, ) - # Merge the configuration values from configuration.yaml - conf = _merge_config(entry, conf) + # Merge advanced configuration values from configuration.yaml + conf = _merge_extended_config(entry, conf) hass.data[DATA_MQTT] = MQTT( hass, @@ -876,7 +920,7 @@ class MQTT: if (conf := hass.data.get(DATA_MQTT_CONFIG)) is None: conf = CONFIG_SCHEMA_BASE(dict(entry.data)) - self.conf = _merge_config(entry, conf) + self.conf = _merge_extended_config(entry, conf) await self.async_disconnect() self.init_client() await self.async_connect() @@ -1225,37 +1269,6 @@ def websocket_mqtt_info(hass, connection, msg): connection.send_result(msg["id"], mqtt_info) -@websocket_api.websocket_command( - {vol.Required("type"): "mqtt/device/remove", vol.Required("device_id"): str} -) -@websocket_api.async_response -async def websocket_remove_device(hass, connection, msg): - """Delete device.""" - device_id = msg["device_id"] - device_registry = dr.async_get(hass) - - if not (device := device_registry.async_get(device_id)): - connection.send_error( - msg["id"], websocket_api.const.ERR_NOT_FOUND, "Device not found" - ) - return - - for config_entry in device.config_entries: - config_entry = hass.config_entries.async_get_entry(config_entry) - # Only delete the device if it belongs to an MQTT device entry - if config_entry.domain == DOMAIN: - await async_remove_config_entry_device(hass, config_entry, device) - device_registry.async_update_device( - device_id, remove_config_entry_id=config_entry.entry_id - ) - connection.send_message(websocket_api.result_message(msg["id"])) - return - - connection.send_error( - msg["id"], websocket_api.const.ERR_NOT_FOUND, "Non MQTT device" - ) - - @websocket_api.websocket_command( { vol.Required("type"): "mqtt/subscribe", @@ -1270,20 +1283,29 @@ async def websocket_subscribe(hass, connection, msg): async def forward_messages(mqttmsg: ReceiveMessage): """Forward events to websocket.""" + try: + payload = cast(bytes, mqttmsg.payload).decode( + DEFAULT_ENCODING + ) # not str because encoding is set to None + except (AttributeError, UnicodeDecodeError): + # Convert non UTF-8 payload to a string presentation + payload = str(mqttmsg.payload) + connection.send_message( websocket_api.event_message( msg["id"], { "topic": mqttmsg.topic, - "payload": mqttmsg.payload, + "payload": payload, "qos": mqttmsg.qos, "retain": mqttmsg.retain, }, ) ) + # Perform UTF-8 decoding directly in callback routine connection.subscriptions[msg["id"]] = await async_subscribe( - hass, msg["topic"], forward_messages + hass, msg["topic"], forward_messages, encoding=None ) connection.send_message(websocket_api.result_message(msg["id"])) diff --git a/homeassistant/components/mqtt/binary_sensor.py b/homeassistant/components/mqtt/binary_sensor.py index 40d5c876c11..5d0da99d786 100644 --- a/homeassistant/components/mqtt/binary_sensor.py +++ b/homeassistant/components/mqtt/binary_sensor.py @@ -118,16 +118,15 @@ class MqttBinarySensor(MqttEntity, BinarySensorEntity, RestoreEntity): MqttEntity.__init__(self, hass, config, config_entry, discovery_data) - async def async_added_to_hass(self) -> None: + async def mqtt_async_added_to_hass(self) -> None: """Restore state for entities with expire_after set.""" - await super().async_added_to_hass() if ( (expire_after := self._config.get(CONF_EXPIRE_AFTER)) is not None and expire_after > 0 and (last_state := await self.async_get_last_state()) is not None and last_state.state not in [STATE_UNKNOWN, STATE_UNAVAILABLE] # We might have set up a trigger already after subscribing from - # super().async_added_to_hass(), then we should not restore state + # MqttEntity.async_added_to_hass(), then we should not restore state and not self._expiration_trigger ): expiration_at = last_state.last_changed + timedelta(seconds=expire_after) diff --git a/homeassistant/components/mqtt/fan.py b/homeassistant/components/mqtt/fan.py index bedc3467c3b..89ea7dbdd4b 100644 --- a/homeassistant/components/mqtt/fan.py +++ b/homeassistant/components/mqtt/fan.py @@ -12,10 +12,6 @@ from homeassistant.components.fan import ( ATTR_OSCILLATING, ATTR_PERCENTAGE, ATTR_PRESET_MODE, - SPEED_HIGH, - SPEED_LOW, - SPEED_MEDIUM, - SPEED_OFF, SUPPORT_OSCILLATE, SUPPORT_PRESET_MODE, SUPPORT_SET_SPEED, @@ -163,10 +159,6 @@ _PLATFORM_SCHEMA_BASE = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend( vol.Optional( CONF_PAYLOAD_RESET_PRESET_MODE, default=DEFAULT_PAYLOAD_RESET ): cv.string, - vol.Optional(CONF_PAYLOAD_HIGH_SPEED, default=SPEED_HIGH): cv.string, - vol.Optional(CONF_PAYLOAD_LOW_SPEED, default=SPEED_LOW): cv.string, - vol.Optional(CONF_PAYLOAD_MEDIUM_SPEED, default=SPEED_MEDIUM): cv.string, - vol.Optional(CONF_PAYLOAD_OFF_SPEED, default=SPEED_OFF): cv.string, vol.Optional(CONF_PAYLOAD_OFF, default=DEFAULT_PAYLOAD_OFF): cv.string, vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string, vol.Optional( @@ -176,10 +168,6 @@ _PLATFORM_SCHEMA_BASE = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend( CONF_PAYLOAD_OSCILLATION_ON, default=OSCILLATE_ON_PAYLOAD ): cv.string, vol.Optional(CONF_SPEED_COMMAND_TOPIC): mqtt.valid_publish_topic, - vol.Optional( - CONF_SPEED_LIST, - default=[SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH], - ): cv.ensure_list, vol.Optional(CONF_SPEED_STATE_TOPIC): mqtt.valid_subscribe_topic, vol.Optional(CONF_SPEED_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_STATE_VALUE_TEMPLATE): cv.template, @@ -537,7 +525,6 @@ class MqttFan(MqttEntity, FanEntity): # The speed attribute deprecated in the schema, support will be removed after a quarter (2021.7) async def async_turn_on( self, - speed: str = None, percentage: int = None, preset_mode: str = None, **kwargs, @@ -605,9 +592,7 @@ class MqttFan(MqttEntity, FanEntity): This method is a coroutine. """ - if preset_mode not in self.preset_modes: - _LOGGER.warning("'%s'is not a valid preset mode", preset_mode) - return + self._valid_preset_mode_or_raise(preset_mode) mqtt_payload = self._command_templates[ATTR_PRESET_MODE](preset_mode) diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index 9f3722a8f31..09efb384196 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -5,7 +5,7 @@ from abc import abstractmethod from collections.abc import Callable import json import logging -from typing import Any, Protocol +from typing import Any, Protocol, final import voluptuous as vol @@ -21,6 +21,7 @@ from homeassistant.const import ( CONF_DEVICE, CONF_ENTITY_CATEGORY, CONF_ICON, + CONF_MODEL, CONF_NAME, CONF_UNIQUE_ID, CONF_VALUE_TEMPLATE, @@ -106,7 +107,6 @@ CONF_JSON_ATTRS_TEMPLATE = "json_attributes_template" CONF_IDENTIFIERS = "identifiers" CONF_CONNECTIONS = "connections" CONF_MANUFACTURER = "manufacturer" -CONF_MODEL = "model" CONF_SW_VERSION = "sw_version" CONF_VIA_DEVICE = "via_device" CONF_DEPRECATED_VIA_HUB = "via_hub" @@ -572,9 +572,7 @@ class MqttDiscoveryUpdate(Entity): else: # Non-empty, unchanged payload: Ignore to avoid changing states _LOGGER.info("Ignoring unchanged update for: %s", self.entity_id) - async_dispatcher_send( - self.hass, MQTT_DISCOVERY_DONE.format(discovery_hash), None - ) + self.async_send_discovery_done() if discovery_hash: debug_info.add_entity_discovery_data( @@ -587,9 +585,18 @@ class MqttDiscoveryUpdate(Entity): MQTT_DISCOVERY_UPDATED.format(discovery_hash), discovery_callback, ) - async_dispatcher_send( - self.hass, MQTT_DISCOVERY_DONE.format(discovery_hash), None - ) + + @callback + def async_send_discovery_done(self) -> None: + """Acknowledge a discovery message has been handled.""" + discovery_hash = ( + self._discovery_data[ATTR_DISCOVERY_HASH] if self._discovery_data else None + ) + if not discovery_hash: + return + async_dispatcher_send( + self.hass, MQTT_DISCOVERY_DONE.format(discovery_hash), None + ) async def async_removed_from_registry(self) -> None: """Clear retained discovery topic in broker.""" @@ -723,11 +730,20 @@ class MqttEntity( self.hass, self, self._config, self._entity_id_format ) + @final async def async_added_to_hass(self): - """Subscribe mqtt events.""" + """Subscribe to MQTT events.""" await super().async_added_to_hass() self._prepare_subscribe_topics() await self._subscribe_topics() + await self.mqtt_async_added_to_hass() + self.async_send_discovery_done() + + async def mqtt_async_added_to_hass(self): + """Call before the discovery message is acknowledged. + + To be extended by subclasses. + """ async def discovery_update(self, discovery_payload): """Handle updated discovery message.""" @@ -798,7 +814,7 @@ class MqttEntity( return self._config[CONF_ENABLED_BY_DEFAULT] @property - def entity_category(self) -> EntityCategory | str | None: + def entity_category(self) -> EntityCategory | None: """Return the entity category if any.""" return self._config.get(CONF_ENTITY_CATEGORY) diff --git a/homeassistant/components/mqtt/scene.py b/homeassistant/components/mqtt/scene.py index c44ea1dca53..c9bcae2dac1 100644 --- a/homeassistant/components/mqtt/scene.py +++ b/homeassistant/components/mqtt/scene.py @@ -17,13 +17,12 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .. import mqtt from .const import CONF_COMMAND_TOPIC, CONF_ENCODING, CONF_QOS, CONF_RETAIN from .mixins import ( + CONF_ENABLED_BY_DEFAULT, CONF_OBJECT_ID, MQTT_AVAILABILITY_SCHEMA, - MqttAvailability, - MqttDiscoveryUpdate, + MqttEntity, async_setup_entry_helper, async_setup_platform_helper, - init_entity_id_from_config, ) DEFAULT_NAME = "MQTT Scene" @@ -38,6 +37,8 @@ PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend( vol.Optional(CONF_UNIQUE_ID): cv.string, vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean, vol.Optional(CONF_OBJECT_ID): cv.string, + # CONF_ENABLED_BY_DEFAULT is not added by default because we are not using the common schema here + vol.Optional(CONF_ENABLED_BY_DEFAULT, default=True): cv.boolean, } ).extend(MQTT_AVAILABILITY_SCHEMA.schema) @@ -76,8 +77,7 @@ async def _async_setup_entity( class MqttScene( - MqttAvailability, - MqttDiscoveryUpdate, + MqttEntity, Scene, ): """Representation of a scene that can be activated using MQTT.""" @@ -86,61 +86,22 @@ class MqttScene( def __init__(self, hass, config, config_entry, discovery_data): """Initialize the MQTT scene.""" - self.hass = hass - self._state = False - self._sub_state = None + MqttEntity.__init__(self, hass, config, config_entry, discovery_data) - self._unique_id = config.get(CONF_UNIQUE_ID) - - # Load config - self._setup_from_config(config) - - # Initialize entity_id from config - self._init_entity_id() - - MqttAvailability.__init__(self, config) - MqttDiscoveryUpdate.__init__(self, discovery_data, self.discovery_update) - - def _init_entity_id(self): - """Set entity_id from object_id if defined in config.""" - init_entity_id_from_config( - self.hass, self, self._config, self._entity_id_format - ) - - async def async_added_to_hass(self): - """Subscribe to MQTT events.""" - await super().async_added_to_hass() - - async def discovery_update(self, discovery_payload): - """Handle updated discovery message.""" - config = DISCOVERY_SCHEMA(discovery_payload) - self._setup_from_config(config) - await self.availability_discovery_update(config) - self.async_write_ha_state() + @staticmethod + def config_schema(): + """Return the config schema.""" + return DISCOVERY_SCHEMA def _setup_from_config(self, config): """(Re)Setup the entity.""" self._config = config - async def async_will_remove_from_hass(self): - """Unsubscribe when removed.""" - await MqttAvailability.async_will_remove_from_hass(self) - await MqttDiscoveryUpdate.async_will_remove_from_hass(self) + def _prepare_subscribe_topics(self): + """(Re)Subscribe to topics.""" - @property - def name(self): - """Return the name of the scene.""" - return self._config[CONF_NAME] - - @property - def unique_id(self): - """Return a unique ID.""" - return self._unique_id - - @property - def icon(self): - """Return the icon.""" - return self._config.get(CONF_ICON) + async def _subscribe_topics(self): + """(Re)Subscribe to topics.""" async def async_activate(self, **kwargs): """Activate the scene. diff --git a/homeassistant/components/mqtt/sensor.py b/homeassistant/components/mqtt/sensor.py index c24535ebd1f..a13f58f95ea 100644 --- a/homeassistant/components/mqtt/sensor.py +++ b/homeassistant/components/mqtt/sensor.py @@ -163,9 +163,8 @@ class MqttSensor(MqttEntity, RestoreSensor): MqttEntity.__init__(self, hass, config, config_entry, discovery_data) - async def async_added_to_hass(self) -> None: + async def mqtt_async_added_to_hass(self) -> None: """Restore state for entities with expire_after set.""" - await super().async_added_to_hass() if ( (expire_after := self._config.get(CONF_EXPIRE_AFTER)) is not None and expire_after > 0 @@ -174,7 +173,7 @@ class MqttSensor(MqttEntity, RestoreSensor): and (last_sensor_data := await self.async_get_last_sensor_data()) is not None # We might have set up a trigger already after subscribing from - # super().async_added_to_hass(), then we should not restore state + # MqttEntity.async_added_to_hass(), then we should not restore state and not self._expiration_trigger ): expiration_at = last_state.last_changed + timedelta(seconds=expire_after) diff --git a/homeassistant/components/mqtt/services.yaml b/homeassistant/components/mqtt/services.yaml index 7b57570a3ef..07507035c57 100644 --- a/homeassistant/components/mqtt/services.yaml +++ b/homeassistant/components/mqtt/services.yaml @@ -19,8 +19,7 @@ publish: text: payload_template: name: Payload Template - description: - Template to render as payload value. Ignored if payload given. + description: Template to render as payload value. Ignored if payload given. advanced: true example: "{{ states('sensor.temperature') }}" selector: diff --git a/homeassistant/components/mqtt/switch.py b/homeassistant/components/mqtt/switch.py index 1a471ea2ad0..db54ae08953 100644 --- a/homeassistant/components/mqtt/switch.py +++ b/homeassistant/components/mqtt/switch.py @@ -41,13 +41,6 @@ from .mixins import ( async_setup_platform_helper, ) -MQTT_SWITCH_ATTRIBUTES_BLOCKED = frozenset( - { - switch.ATTR_CURRENT_POWER_W, - switch.ATTR_TODAY_ENERGY_KWH, - } -) - DEFAULT_NAME = "MQTT Switch" DEFAULT_PAYLOAD_ON = "ON" DEFAULT_PAYLOAD_OFF = "OFF" @@ -106,7 +99,6 @@ class MqttSwitch(MqttEntity, SwitchEntity, RestoreEntity): """Representation of a switch that can be toggled using MQTT.""" _entity_id_format = switch.ENTITY_ID_FORMAT - _attributes_extra_blocked = MQTT_SWITCH_ATTRIBUTES_BLOCKED def __init__(self, hass, config, config_entry, discovery_data): """Initialize the MQTT switch.""" diff --git a/homeassistant/components/mqtt/translations/fr.json b/homeassistant/components/mqtt/translations/fr.json index 13bfce8dd5e..b84aec15a80 100644 --- a/homeassistant/components/mqtt/translations/fr.json +++ b/homeassistant/components/mqtt/translations/fr.json @@ -44,15 +44,15 @@ "button_long_release": "\"{subtype}\" relach\u00e9 apr\u00e8s un appui long", "button_quadruple_press": "\"{subtype}\" quadruple cliqu\u00e9", "button_quintuple_press": "\"{subtype}\" quintuple cliqu\u00e9", - "button_short_press": "\" {subtype} \" press\u00e9", - "button_short_release": "\"{subtype}\" relach\u00e9", - "button_triple_press": "\"{subtype}\" triple-cliqu\u00e9" + "button_short_press": "\u00ab\u00a0{subtype}\u00a0\u00bb enfonc\u00e9", + "button_short_release": "\u00ab\u00a0{subtype}\u00a0\u00bb rel\u00e2ch\u00e9", + "button_triple_press": "\u00ab\u00a0{subtype}\u00a0\u00bb triple-cliqu\u00e9" } }, "options": { "error": { - "bad_birth": "Topic de naissance invalide", - "bad_will": "Topic de testament invalide", + "bad_birth": "Sujet de la naissance non valide.", + "bad_will": "Sujet du testament non valide.", "cannot_connect": "\u00c9chec de connexion" }, "step": { diff --git a/homeassistant/components/mqtt/translations/nl.json b/homeassistant/components/mqtt/translations/nl.json index 542ae467e7a..40448942a9f 100644 --- a/homeassistant/components/mqtt/translations/nl.json +++ b/homeassistant/components/mqtt/translations/nl.json @@ -71,14 +71,14 @@ "birth_enable": "Geboortebericht inschakelen", "birth_payload": "Birth message payload", "birth_qos": "Birth message QoS", - "birth_retain": "Birth message behouden", + "birth_retain": "Verbind bericht onthouden", "birth_topic": "Birth message onderwerp", "discovery": "Discovery inschakelen", - "will_enable": "Will message inschakelen", - "will_payload": "Will message payload", - "will_qos": "Will message QoS", - "will_retain": "Will message behouden", - "will_topic": "Will message topic" + "will_enable": "Offline bericht inschakelen", + "will_payload": "Offline bericht inhoud", + "will_qos": "Offline bericht QoS", + "will_retain": "Offline bericht onthouden", + "will_topic": "Offline bericht topic" }, "description": "Detectie - Als detectie is ingeschakeld (aanbevolen), zal Home Assistant automatisch apparaten en entiteiten detecteren die hun configuratie publiceren op de MQTT-broker. Als detectie is uitgeschakeld, moet alle configuratie handmatig worden uitgevoerd.\n Birth message - Het birth message wordt elke keer dat Home Assistant (opnieuw) verbinding maakt met de MQTT-broker, verzonden.\n Will message - Het will message wordt telkens verzonden wanneer Home Assistant de verbinding met de broker verliest, zowel in het geval van een schone (bijv. Home Assistant wordt uitgeschakeld) als in geval van een onjuiste (bijv. Home Assistant crasht of verliest de netwerkverbinding) verbroken verbinding.", "title": "MQTT-opties" diff --git a/homeassistant/components/mqtt/translations/pt-BR.json b/homeassistant/components/mqtt/translations/pt-BR.json index 526fe072cf7..2e1bb5bebfd 100644 --- a/homeassistant/components/mqtt/translations/pt-BR.json +++ b/homeassistant/components/mqtt/translations/pt-BR.json @@ -10,7 +10,7 @@ "step": { "broker": { "data": { - "broker": "Broker", + "broker": "Endere\u00e7o do Broker", "discovery": "Ativar descoberta", "password": "Senha", "port": "Porta", diff --git a/homeassistant/components/mqtt/translations/zh-Hant.json b/homeassistant/components/mqtt/translations/zh-Hant.json index 43d6a5f0b4e..b34b4813499 100644 --- a/homeassistant/components/mqtt/translations/zh-Hant.json +++ b/homeassistant/components/mqtt/translations/zh-Hant.json @@ -80,7 +80,7 @@ "will_retain": "Will \u8a0a\u606f Retain", "will_topic": "Will \u8a0a\u606f\u4e3b\u984c" }, - "description": "Discovery - \u5047\u5982\u63a2\u7d22\uff08Discovery\uff09\u529f\u80fd\u958b\u555f\uff08\u5efa\u8b70\uff09\uff0cHome Assistant \u5c07\u6703\u81ea\u52d5\u767c\u73fe\u88dd\u7f6e\u8207\u5be6\u9ad4\u3001\u4e26\u767c\u5e03\u5176\u8a2d\u5b9a\u81f3 MQTT Broker\u3002\u5047\u5982\u63a2\u7d22\u95dc\u9589\u7684\u8a71\uff0c\u6240\u6709\u8a2d\u5b9a\u5fc5\u9808\u624b\u52d5\u9032\u884c\u3002\nBirth \u8a0a\u606f - Birth \u8a0a\u606f\u5c07\u6703\u65bc\u6bcf\u6b21 Home Assistant \u9023\u7dda\u81f3 MQTT Broker \u6642\u50b3\u9001\u3002\nWill \u8a0a\u606f - Will \u8a0a\u606f\u5c07\u6703\u65bc\u6bcf\u6b21 Home Assistant \u81ea Broker \u65b7\u7dda\u6642\u50b3\u9001\u3001\u540c\u6642\u5305\u542b\u5b89\u5168\u65b7\u7dda\uff08\u4f8b\u5982 Home Assistant \u95dc\u6a5f\uff09\u53ca\u975e\u5b89\u5168\u65b7\u7dda\uff08\u4f8b\u5982 Home Assistant \u7576\u6a5f\u6216\u65b7\u7dda\uff09\u72c0\u6cc1\u3002", + "description": "Discovery - \u5047\u5982\u641c\u7d22\uff08Discovery\uff09\u529f\u80fd\u958b\u555f\uff08\u5efa\u8b70\uff09\uff0cHome Assistant \u5c07\u6703\u81ea\u52d5\u767c\u73fe\u88dd\u7f6e\u8207\u5be6\u9ad4\u3001\u4e26\u767c\u5e03\u5176\u8a2d\u5b9a\u81f3 MQTT Broker\u3002\u5047\u5982\u641c\u7d22\u95dc\u9589\u7684\u8a71\uff0c\u6240\u6709\u8a2d\u5b9a\u5fc5\u9808\u624b\u52d5\u9032\u884c\u3002\nBirth \u8a0a\u606f - Birth \u8a0a\u606f\u5c07\u6703\u65bc\u6bcf\u6b21 Home Assistant \u9023\u7dda\u81f3 MQTT Broker \u6642\u50b3\u9001\u3002\nWill \u8a0a\u606f - Will \u8a0a\u606f\u5c07\u6703\u65bc\u6bcf\u6b21 Home Assistant \u81ea Broker \u65b7\u7dda\u6642\u50b3\u9001\u3001\u540c\u6642\u5305\u542b\u5b89\u5168\u65b7\u7dda\uff08\u4f8b\u5982 Home Assistant \u95dc\u6a5f\uff09\u53ca\u975e\u5b89\u5168\u65b7\u7dda\uff08\u4f8b\u5982 Home Assistant \u7576\u6a5f\u6216\u65b7\u7dda\uff09\u72c0\u6cc1\u3002", "title": "MQTT \u9078\u9805" } } diff --git a/homeassistant/components/mutesync/manifest.json b/homeassistant/components/mutesync/manifest.json index 1498c695505..2f0a467e9e2 100644 --- a/homeassistant/components/mutesync/manifest.json +++ b/homeassistant/components/mutesync/manifest.json @@ -5,8 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/mutesync", "requirements": ["mutesync==0.0.1"], "iot_class": "local_polling", - "codeowners": [ - "@currentoor" - ], + "codeowners": ["@currentoor"], "loggers": ["mutesync"] } diff --git a/homeassistant/components/myq/manifest.json b/homeassistant/components/myq/manifest.json index 0506e589d54..c875f8e7604 100644 --- a/homeassistant/components/myq/manifest.json +++ b/homeassistant/components/myq/manifest.json @@ -3,7 +3,7 @@ "name": "MyQ", "documentation": "https://www.home-assistant.io/integrations/myq", "requirements": ["pymyq==3.1.4"], - "codeowners": ["@bdraco","@ehendrix23"], + "codeowners": ["@bdraco", "@ehendrix23"], "config_flow": true, "homekit": { "models": ["819LMB", "MYQ"] diff --git a/homeassistant/components/myq/strings.json b/homeassistant/components/myq/strings.json index e8a0baa85ff..c986b8a8997 100644 --- a/homeassistant/components/myq/strings.json +++ b/homeassistant/components/myq/strings.json @@ -14,7 +14,7 @@ "data": { "password": "[%key:common::config_flow::data::password%]" } - } + } }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", diff --git a/homeassistant/components/myq/translations/fr.json b/homeassistant/components/myq/translations/fr.json index 6aa94c54577..27f57ff60b1 100644 --- a/homeassistant/components/myq/translations/fr.json +++ b/homeassistant/components/myq/translations/fr.json @@ -6,7 +6,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/mysensors/__init__.py b/homeassistant/components/mysensors/__init__.py index 4d3c3046a89..d392624bbe4 100644 --- a/homeassistant/components/mysensors/__init__.py +++ b/homeassistant/components/mysensors/__init__.py @@ -43,7 +43,7 @@ from .const import ( SensorType, ) from .device import MySensorsDevice, get_mysensors_devices -from .gateway import finish_setup, get_mysensors_gateway, gw_stop, setup_gateway +from .gateway import finish_setup, gw_stop, setup_gateway from .helpers import on_unload _LOGGER = logging.getLogger(__name__) @@ -124,27 +124,30 @@ GATEWAY_SCHEMA = vol.Schema( ) CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - vol.All( - deprecated(CONF_DEBUG), - deprecated(CONF_OPTIMISTIC), - deprecated(CONF_PERSISTENCE), - { - vol.Required(CONF_GATEWAYS): vol.All( - cv.ensure_list, - set_default_persistence_file, - has_all_unique_files, - [GATEWAY_SCHEMA], - ), - vol.Optional(CONF_RETAIN, default=True): cv.boolean, - vol.Optional(CONF_VERSION, default=DEFAULT_VERSION): cv.string, - vol.Optional(CONF_OPTIMISTIC, default=False): cv.boolean, - vol.Optional(CONF_PERSISTENCE, default=True): cv.boolean, - }, + vol.All( + cv.deprecated(DOMAIN), + { + DOMAIN: vol.Schema( + vol.All( + deprecated(CONF_DEBUG), + deprecated(CONF_OPTIMISTIC), + deprecated(CONF_PERSISTENCE), + { + vol.Required(CONF_GATEWAYS): vol.All( + cv.ensure_list, + set_default_persistence_file, + has_all_unique_files, + [GATEWAY_SCHEMA], + ), + vol.Optional(CONF_RETAIN, default=True): cv.boolean, + vol.Optional(CONF_VERSION, default=DEFAULT_VERSION): cv.string, + vol.Optional(CONF_OPTIMISTIC, default=False): cv.boolean, + vol.Optional(CONF_PERSISTENCE, default=True): cv.boolean, + }, + ) ) - ) - }, + }, + ), extra=vol.ALLOW_EXTRA, ) @@ -244,7 +247,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Remove an instance of the MySensors integration.""" - gateway = get_mysensors_gateway(hass, entry.entry_id) + gateway: BaseAsyncGateway = hass.data[DOMAIN][MYSENSORS_GATEWAYS][entry.entry_id] unload_ok = await hass.config_entries.async_unload_platforms( entry, PLATFORMS_WITH_ENTRY_SUPPORT @@ -314,10 +317,7 @@ def setup_mysensors_platform( ) continue gateway_id, node_id, child_id, value_type = dev_id - gateway: BaseAsyncGateway | None = get_mysensors_gateway(hass, gateway_id) - if not gateway: - _LOGGER.warning("Skipping setup of %s, no gateway found", dev_id) - continue + gateway: BaseAsyncGateway = hass.data[DOMAIN][MYSENSORS_GATEWAYS][gateway_id] if isinstance(device_class, dict): child = gateway.sensors[node_id].children[child_id] diff --git a/homeassistant/components/mysensors/const.py b/homeassistant/components/mysensors/const.py index ad25e601bbd..5f3cb6aed96 100644 --- a/homeassistant/components/mysensors/const.py +++ b/homeassistant/components/mysensors/const.py @@ -62,8 +62,6 @@ ValueType = str GatewayId = str # a unique id generated by config_flow.py and stored in the ConfigEntry as the entry id. -# -# Gateway may be fetched by giving the gateway id to get_mysensors_gateway() DevId = tuple[GatewayId, int, int, int] # describes the backend of a hass entity. Contents are: GatewayId, node_id, child_id, v_type as int diff --git a/homeassistant/components/mysensors/gateway.py b/homeassistant/components/mysensors/gateway.py index be0381ab74e..7f13035f55c 100644 --- a/homeassistant/components/mysensors/gateway.py +++ b/homeassistant/components/mysensors/gateway.py @@ -37,7 +37,6 @@ from .const import ( CONF_VERSION, DOMAIN, MYSENSORS_GATEWAY_START_TASK, - MYSENSORS_GATEWAYS, ConfGatewayType, GatewayId, ) @@ -122,16 +121,6 @@ async def try_connect( return False -def get_mysensors_gateway( - hass: HomeAssistant, gateway_id: GatewayId -) -> BaseAsyncGateway | None: - """Return the Gateway for a given GatewayId.""" - if MYSENSORS_GATEWAYS not in hass.data[DOMAIN]: - hass.data[DOMAIN][MYSENSORS_GATEWAYS] = {} - gateways = hass.data[DOMAIN].get(MYSENSORS_GATEWAYS) - return gateways.get(gateway_id) - - async def setup_gateway( hass: HomeAssistant, entry: ConfigEntry ) -> BaseAsyncGateway | None: diff --git a/homeassistant/components/mysensors/translations/fr.json b/homeassistant/components/mysensors/translations/fr.json index e104c69e815..c9d64ee73e6 100644 --- a/homeassistant/components/mysensors/translations/fr.json +++ b/homeassistant/components/mysensors/translations/fr.json @@ -5,7 +5,7 @@ "cannot_connect": "\u00c9chec de connexion", "duplicate_persistence_file": "Fichier de persistance d\u00e9j\u00e0 utilis\u00e9", "duplicate_topic": "Sujet d\u00e9j\u00e0 utilis\u00e9", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "invalid_device": "Appareil non valide", "invalid_ip": "Adresse IP non valide", "invalid_persistence_file": "Fichier de persistance non valide", @@ -24,7 +24,7 @@ "cannot_connect": "\u00c9chec de connexion", "duplicate_persistence_file": "Fichier de persistance d\u00e9j\u00e0 utilis\u00e9", "duplicate_topic": "Sujet d\u00e9j\u00e0 utilis\u00e9", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "invalid_device": "Appareil non valide", "invalid_ip": "Adresse IP non valide", "invalid_persistence_file": "Fichier de persistance non valide", diff --git a/homeassistant/components/mysensors/translations/it.json b/homeassistant/components/mysensors/translations/it.json index 95837352502..56ce395aa4c 100644 --- a/homeassistant/components/mysensors/translations/it.json +++ b/homeassistant/components/mysensors/translations/it.json @@ -14,7 +14,7 @@ "invalid_serial": "Porta seriale non valida", "invalid_subscribe_topic": "Argomento di sottoscrizione non valido", "invalid_version": "Versione di MySensors non valida", - "not_a_number": "Per favore inserisci un numero", + "not_a_number": "Digita un numero", "port_out_of_range": "Il numero di porta deve essere almeno 1 e al massimo 65535", "same_topic": "Gli argomenti di sottoscrizione e pubblicazione sono gli stessi", "unknown": "Errore imprevisto" @@ -34,7 +34,7 @@ "invalid_subscribe_topic": "Argomento di sottoscrizione non valido", "invalid_version": "Versione di MySensors non valida", "mqtt_required": "L'integrazione MQTT non \u00e8 configurata", - "not_a_number": "Per favore inserisci un numero", + "not_a_number": "Digita un numero", "port_out_of_range": "Il numero di porta deve essere almeno 1 e al massimo 65535", "same_topic": "Gli argomenti di sottoscrizione e pubblicazione sono gli stessi", "unknown": "Errore imprevisto" diff --git a/homeassistant/components/mysensors/translations/zh-Hant.json b/homeassistant/components/mysensors/translations/zh-Hant.json index 234a2bd0b30..5774df64b78 100644 --- a/homeassistant/components/mysensors/translations/zh-Hant.json +++ b/homeassistant/components/mysensors/translations/zh-Hant.json @@ -14,7 +14,7 @@ "invalid_serial": "\u5e8f\u5217\u57e0\u7121\u6548", "invalid_subscribe_topic": "\u8a02\u95b1\u4e3b\u984c\u7121\u6548", "invalid_version": "MySensors \u7248\u672c\u7121\u6548", - "not_a_number": "\u8acb\u8f38\u5165\u865f\u78bc", + "not_a_number": "\u8acb\u8f38\u5165\u6578\u5b57", "port_out_of_range": "\u8acb\u8f38\u5165\u4ecb\u65bc 1 \u81f3 65535 \u4e4b\u9593\u7684\u865f\u78bc", "same_topic": "\u8a02\u95b1\u8207\u767c\u4f48\u4e3b\u984c\u76f8\u540c", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" @@ -34,7 +34,7 @@ "invalid_subscribe_topic": "\u8a02\u95b1\u4e3b\u984c\u7121\u6548", "invalid_version": "MySensors \u7248\u672c\u7121\u6548", "mqtt_required": "MQTT \u6574\u5408\u5c1a\u672a\u8a2d\u5b9a", - "not_a_number": "\u8acb\u8f38\u5165\u865f\u78bc", + "not_a_number": "\u8acb\u8f38\u5165\u6578\u5b57", "port_out_of_range": "\u8acb\u8f38\u5165\u4ecb\u65bc 1 \u81f3 65535 \u4e4b\u9593\u7684\u865f\u78bc", "same_topic": "\u8a02\u95b1\u8207\u767c\u4f48\u4e3b\u984c\u76f8\u540c", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" @@ -70,7 +70,7 @@ }, "user": { "data": { - "gateway_type": "\u9598\u9053\u5668\u985e\u578b" + "gateway_type": "\u9598\u9053\u5668\u985e\u5225" }, "description": "\u9078\u64c7\u9598\u9053\u5668\u9023\u7dda\u65b9\u5f0f" } diff --git a/homeassistant/components/mystrom/switch.py b/homeassistant/components/mystrom/switch.py index b8089eb116f..41303b25a9f 100644 --- a/homeassistant/components/mystrom/switch.py +++ b/homeassistant/components/mystrom/switch.py @@ -72,11 +72,6 @@ class MyStromSwitch(SwitchEntity): """Return a unique ID.""" return self.plug._mac # pylint: disable=protected-access - @property - def current_power_w(self): - """Return the current power consumption in W.""" - return self.plug.consumption - @property def available(self): """Could the device be accessed during the last update call.""" @@ -103,5 +98,6 @@ class MyStromSwitch(SwitchEntity): self.relay = self.plug.relay self._available = True except MyStromConnectionError: - self._available = False - _LOGGER.error("No route to myStrom plug") + if self._available: + self._available = False + _LOGGER.error("No route to myStrom plug") diff --git a/homeassistant/components/nam/button.py b/homeassistant/components/nam/button.py index f73307d09ce..db5474ec925 100644 --- a/homeassistant/components/nam/button.py +++ b/homeassistant/components/nam/button.py @@ -41,11 +41,9 @@ async def async_setup_entry( async_add_entities(buttons, False) -class NAMButton(CoordinatorEntity, ButtonEntity): +class NAMButton(CoordinatorEntity[NAMDataUpdateCoordinator], ButtonEntity): """Define an Nettigo Air Monitor button.""" - coordinator: NAMDataUpdateCoordinator - def __init__( self, coordinator: NAMDataUpdateCoordinator, diff --git a/homeassistant/components/nam/manifest.json b/homeassistant/components/nam/manifest.json index d8cda2f16c7..64c3d2fb0f7 100644 --- a/homeassistant/components/nam/manifest.json +++ b/homeassistant/components/nam/manifest.json @@ -11,7 +11,7 @@ }, { "type": "_http._tcp.local.", - "properties": {"manufacturer": "nettigo"} + "properties": { "manufacturer": "nettigo" } } ], "config_flow": true, diff --git a/homeassistant/components/nam/sensor.py b/homeassistant/components/nam/sensor.py index 88f6008b45f..af729cf9066 100644 --- a/homeassistant/components/nam/sensor.py +++ b/homeassistant/components/nam/sensor.py @@ -58,11 +58,9 @@ async def async_setup_entry( async_add_entities(sensors, False) -class NAMSensor(CoordinatorEntity, SensorEntity): +class NAMSensor(CoordinatorEntity[NAMDataUpdateCoordinator], SensorEntity): """Define an Nettigo Air Monitor sensor.""" - coordinator: NAMDataUpdateCoordinator - def __init__( self, coordinator: NAMDataUpdateCoordinator, diff --git a/homeassistant/components/nam/translations/fr.json b/homeassistant/components/nam/translations/fr.json index 58a626f4071..8c5331d0e1c 100644 --- a/homeassistant/components/nam/translations/fr.json +++ b/homeassistant/components/nam/translations/fr.json @@ -8,7 +8,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "flow_title": "{host}", diff --git a/homeassistant/components/nam/translations/he.json b/homeassistant/components/nam/translations/he.json index dd6dcc60585..80ebc5ae4fd 100644 --- a/homeassistant/components/nam/translations/he.json +++ b/homeassistant/components/nam/translations/he.json @@ -9,7 +9,7 @@ "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" }, - "flow_title": "{name}", + "flow_title": "{host}", "step": { "confirm_discovery": { "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d0\u05ea Nettigo Air Monitor \u05d1-{host}?" diff --git a/homeassistant/components/nanoleaf/config_flow.py b/homeassistant/components/nanoleaf/config_flow.py index 6ae70b32d8e..ed63754697a 100644 --- a/homeassistant/components/nanoleaf/config_flow.py +++ b/homeassistant/components/nanoleaf/config_flow.py @@ -184,17 +184,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_setup_finish() - async def async_step_import(self, config: dict[str, Any]) -> FlowResult: - """Handle Nanoleaf configuration import.""" - self._async_abort_entries_match({CONF_HOST: config[CONF_HOST]}) - _LOGGER.debug( - "Importing Nanoleaf on %s from your configuration.yaml", config[CONF_HOST] - ) - self.nanoleaf = Nanoleaf( - async_get_clientsession(self.hass), config[CONF_HOST], config[CONF_TOKEN] - ) - return await self.async_setup_finish() - async def async_setup_finish( self, discovery_integration_import: bool = False ) -> FlowResult: diff --git a/homeassistant/components/nanoleaf/light.py b/homeassistant/components/nanoleaf/light.py index ed3476c4576..1cf6bd4d8bf 100644 --- a/homeassistant/components/nanoleaf/light.py +++ b/homeassistant/components/nanoleaf/light.py @@ -1,12 +1,10 @@ """Support for Nanoleaf Lights.""" from __future__ import annotations -import logging import math from typing import Any from aionanoleaf import Nanoleaf -import voluptuous as vol from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -14,7 +12,6 @@ from homeassistant.components.light import ( ATTR_EFFECT, ATTR_HS_COLOR, ATTR_TRANSITION, - PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, @@ -22,12 +19,9 @@ from homeassistant.components.light import ( SUPPORT_TRANSITION, LightEntity, ) -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import CONF_HOST, CONF_NAME, CONF_TOKEN +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.util.color import ( color_temperature_kelvin_to_mired as kelvin_to_mired, @@ -41,38 +35,6 @@ from .entity import NanoleafEntity RESERVED_EFFECTS = ("*Solid*", "*Static*", "*Dynamic*") DEFAULT_NAME = "Nanoleaf" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_TOKEN): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - } -) - -_LOGGER = logging.getLogger(__name__) - - -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Import Nanoleaf light platform.""" - _LOGGER.warning( - "Configuration of the Nanoleaf integration in YAML is deprecated and " - "will be removed in Home Assistant 2022.4; Your existing configuration " - "has been imported into the UI automatically and can be safely removed " - "from your configuration.yaml file" - ) - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={CONF_HOST: config[CONF_HOST], CONF_TOKEN: config[CONF_TOKEN]}, - ) - ) - async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback diff --git a/homeassistant/components/nanoleaf/manifest.json b/homeassistant/components/nanoleaf/manifest.json index 8d7d76dc3db..1ce3210a206 100644 --- a/homeassistant/components/nanoleaf/manifest.json +++ b/homeassistant/components/nanoleaf/manifest.json @@ -5,10 +5,8 @@ "documentation": "https://www.home-assistant.io/integrations/nanoleaf", "requirements": ["aionanoleaf==0.2.0"], "zeroconf": ["_nanoleafms._tcp.local.", "_nanoleafapi._tcp.local."], - "homekit" : { - "models": [ - "NL29", "NL42", "NL47", "NL48", "NL52", "NL59" - ] + "homekit": { + "models": ["NL29", "NL42", "NL47", "NL48", "NL52", "NL59"] }, "ssdp": [ { @@ -27,4 +25,4 @@ "codeowners": ["@milanmeu"], "iot_class": "local_push", "loggers": ["aionanoleaf"] -} \ No newline at end of file +} diff --git a/homeassistant/components/nanoleaf/translations/ca.json b/homeassistant/components/nanoleaf/translations/ca.json index d040dac3e6b..e0e1df01c0c 100644 --- a/homeassistant/components/nanoleaf/translations/ca.json +++ b/homeassistant/components/nanoleaf/translations/ca.json @@ -24,5 +24,13 @@ } } } + }, + "device_automation": { + "trigger_type": { + "swipe_down": "Llisca cap avall", + "swipe_left": "Llisca cap a l'esquerra", + "swipe_right": "Llisca cap a la dreta", + "swipe_up": "Llisca cap amunt" + } } } \ No newline at end of file diff --git a/homeassistant/components/nanoleaf/translations/de.json b/homeassistant/components/nanoleaf/translations/de.json index fa1b9d98057..e29b3b76a46 100644 --- a/homeassistant/components/nanoleaf/translations/de.json +++ b/homeassistant/components/nanoleaf/translations/de.json @@ -24,5 +24,13 @@ } } } + }, + "device_automation": { + "trigger_type": { + "swipe_down": "Nach unten wischen", + "swipe_left": "Nach links wischen", + "swipe_right": "Nach rechts wischen", + "swipe_up": "Nach oben wischen" + } } } \ No newline at end of file diff --git a/homeassistant/components/nanoleaf/translations/el.json b/homeassistant/components/nanoleaf/translations/el.json index bf3e406a861..a81f0fcf993 100644 --- a/homeassistant/components/nanoleaf/translations/el.json +++ b/homeassistant/components/nanoleaf/translations/el.json @@ -24,5 +24,13 @@ } } } + }, + "device_automation": { + "trigger_type": { + "swipe_down": "\u03a3\u03cd\u03c1\u03b5\u03c4\u03b5 \u03c0\u03c1\u03bf\u03c2 \u03c4\u03b1 \u03ba\u03ac\u03c4\u03c9", + "swipe_left": "\u03a3\u03cd\u03c1\u03b5\u03c4\u03b5 \u03c0\u03c1\u03bf\u03c2 \u03c4\u03b1 \u03b1\u03c1\u03b9\u03c3\u03c4\u03b5\u03c1\u03ac", + "swipe_right": "\u03a3\u03cd\u03c1\u03b5\u03c4\u03b5 \u03c0\u03c1\u03bf\u03c2 \u03c4\u03b1 \u03b4\u03b5\u03be\u03b9\u03ac", + "swipe_up": "\u03a3\u03cd\u03c1\u03b5\u03c4\u03b5 \u03c0\u03c1\u03bf\u03c2 \u03c4\u03b1 \u03b5\u03c0\u03ac\u03bd\u03c9" + } } } \ No newline at end of file diff --git a/homeassistant/components/nanoleaf/translations/et.json b/homeassistant/components/nanoleaf/translations/et.json index 15690ab3072..d080167306a 100644 --- a/homeassistant/components/nanoleaf/translations/et.json +++ b/homeassistant/components/nanoleaf/translations/et.json @@ -24,5 +24,13 @@ } } } + }, + "device_automation": { + "trigger_type": { + "swipe_down": "Nipsa alla", + "swipe_left": "Nipsa vasakule", + "swipe_right": "Nipsa paremale", + "swipe_up": "Nipsa \u00fcles" + } } } \ No newline at end of file diff --git a/homeassistant/components/nanoleaf/translations/hu.json b/homeassistant/components/nanoleaf/translations/hu.json index 176d47cc38f..c67c4f958de 100644 --- a/homeassistant/components/nanoleaf/translations/hu.json +++ b/homeassistant/components/nanoleaf/translations/hu.json @@ -24,5 +24,13 @@ } } } + }, + "device_automation": { + "trigger_type": { + "swipe_down": "P\u00f6ccint\u00e9s lefel\u00e9", + "swipe_left": "P\u00f6ccint\u00e9s balra", + "swipe_right": "P\u00f6ccint\u00e9s jobbra", + "swipe_up": "P\u00f6ccint\u00e9s felfel\u00e9" + } } } \ No newline at end of file diff --git a/homeassistant/components/nanoleaf/translations/id.json b/homeassistant/components/nanoleaf/translations/id.json index f17c0de7209..a638cc19fbc 100644 --- a/homeassistant/components/nanoleaf/translations/id.json +++ b/homeassistant/components/nanoleaf/translations/id.json @@ -24,5 +24,13 @@ } } } + }, + "device_automation": { + "trigger_type": { + "swipe_down": "Usap ke bawah", + "swipe_left": "Usap ke Kiri", + "swipe_right": "Usap ke Kanan", + "swipe_up": "Usap ke Atas" + } } } \ No newline at end of file diff --git a/homeassistant/components/nanoleaf/translations/it.json b/homeassistant/components/nanoleaf/translations/it.json index 1e7517d8ada..ccd36acd887 100644 --- a/homeassistant/components/nanoleaf/translations/it.json +++ b/homeassistant/components/nanoleaf/translations/it.json @@ -24,5 +24,13 @@ } } } + }, + "device_automation": { + "trigger_type": { + "swipe_down": "Scorri verso il basso", + "swipe_left": "Scorri verso sinistra", + "swipe_right": "Scorri verso destra", + "swipe_up": "Scorri verso l'alto" + } } } \ No newline at end of file diff --git a/homeassistant/components/nanoleaf/translations/ja.json b/homeassistant/components/nanoleaf/translations/ja.json index 824c4193f87..a72b64a991c 100644 --- a/homeassistant/components/nanoleaf/translations/ja.json +++ b/homeassistant/components/nanoleaf/translations/ja.json @@ -24,5 +24,13 @@ } } } + }, + "device_automation": { + "trigger_type": { + "swipe_down": "\u4e0b\u306b\u30b9\u30ef\u30a4\u30d7", + "swipe_left": "\u5de6\u306b\u30b9\u30ef\u30a4\u30d7", + "swipe_right": "\u53f3\u306b\u30b9\u30ef\u30a4\u30d7", + "swipe_up": "\u4e0a\u306b\u30b9\u30ef\u30a4\u30d7" + } } } \ No newline at end of file diff --git a/homeassistant/components/nanoleaf/translations/nl.json b/homeassistant/components/nanoleaf/translations/nl.json index 29a9f7ac58b..76c471769e3 100644 --- a/homeassistant/components/nanoleaf/translations/nl.json +++ b/homeassistant/components/nanoleaf/translations/nl.json @@ -24,5 +24,13 @@ } } } + }, + "device_automation": { + "trigger_type": { + "swipe_down": "Veeg omlaag", + "swipe_left": "Veeg naar links", + "swipe_right": "Veeg naar rechts", + "swipe_up": "Veeg omhoog" + } } } \ No newline at end of file diff --git a/homeassistant/components/nanoleaf/translations/no.json b/homeassistant/components/nanoleaf/translations/no.json index 5c06bc4b811..37240468676 100644 --- a/homeassistant/components/nanoleaf/translations/no.json +++ b/homeassistant/components/nanoleaf/translations/no.json @@ -24,5 +24,13 @@ } } } + }, + "device_automation": { + "trigger_type": { + "swipe_down": "Sveip ned", + "swipe_left": "Sveip til venstre", + "swipe_right": "Sveip til h\u00f8yre", + "swipe_up": "Sveip opp" + } } } \ No newline at end of file diff --git a/homeassistant/components/nanoleaf/translations/pl.json b/homeassistant/components/nanoleaf/translations/pl.json index 66c7ecd23f1..1419ccd650b 100644 --- a/homeassistant/components/nanoleaf/translations/pl.json +++ b/homeassistant/components/nanoleaf/translations/pl.json @@ -24,5 +24,13 @@ } } } + }, + "device_automation": { + "trigger_type": { + "swipe_down": "przeci\u0105gni\u0119to w d\u00f3\u0142", + "swipe_left": "przeci\u0105gni\u0119to w lewo", + "swipe_right": "przeci\u0105gni\u0119to w prawo", + "swipe_up": "przeci\u0105gni\u0119to do g\u00f3ry" + } } } \ No newline at end of file diff --git a/homeassistant/components/nanoleaf/translations/pt-BR.json b/homeassistant/components/nanoleaf/translations/pt-BR.json index 3946a794711..98e60a4bf27 100644 --- a/homeassistant/components/nanoleaf/translations/pt-BR.json +++ b/homeassistant/components/nanoleaf/translations/pt-BR.json @@ -24,5 +24,13 @@ } } } + }, + "device_automation": { + "trigger_type": { + "swipe_down": "Deslize para baixo", + "swipe_left": "Deslize para a esquerda", + "swipe_right": "Desliza para a direita", + "swipe_up": "Deslize para cima" + } } } \ No newline at end of file diff --git a/homeassistant/components/nanoleaf/translations/ru.json b/homeassistant/components/nanoleaf/translations/ru.json index 884ace3dedc..93f40d654d9 100644 --- a/homeassistant/components/nanoleaf/translations/ru.json +++ b/homeassistant/components/nanoleaf/translations/ru.json @@ -24,5 +24,13 @@ } } } + }, + "device_automation": { + "trigger_type": { + "swipe_down": "\u0421\u0432\u0430\u0439\u043f \u0432\u043d\u0438\u0437", + "swipe_left": "\u0421\u0432\u0430\u0439\u043f \u0432\u043b\u0435\u0432\u043e", + "swipe_right": "\u0421\u0432\u0430\u0439\u043f \u0432\u043f\u0440\u0430\u0432\u043e", + "swipe_up": "\u0421\u0432\u0430\u0439\u043f \u0432\u0432\u0435\u0440\u0445" + } } } \ No newline at end of file diff --git a/homeassistant/components/nanoleaf/translations/tr.json b/homeassistant/components/nanoleaf/translations/tr.json index d3c3969dc48..847b95a137c 100644 --- a/homeassistant/components/nanoleaf/translations/tr.json +++ b/homeassistant/components/nanoleaf/translations/tr.json @@ -24,5 +24,13 @@ } } } + }, + "device_automation": { + "trigger_type": { + "swipe_down": "A\u015fa\u011f\u0131 Kayd\u0131r", + "swipe_left": "Sola Kayd\u0131r", + "swipe_right": "Sa\u011fa Kayd\u0131r", + "swipe_up": "Yukar\u0131 Kayd\u0131r" + } } } \ No newline at end of file diff --git a/homeassistant/components/nanoleaf/translations/zh-Hant.json b/homeassistant/components/nanoleaf/translations/zh-Hant.json index cc5d1c08a8b..c18331f858c 100644 --- a/homeassistant/components/nanoleaf/translations/zh-Hant.json +++ b/homeassistant/components/nanoleaf/translations/zh-Hant.json @@ -24,5 +24,13 @@ } } } + }, + "device_automation": { + "trigger_type": { + "swipe_down": "\u5411\u4e0b\u6ed1", + "swipe_left": "\u5411\u5de6\u6ed1", + "swipe_right": "\u5411\u53f3\u6ed1", + "swipe_up": "\u5411\u4e0a\u6ed1" + } } } \ No newline at end of file diff --git a/homeassistant/components/neato/config_flow.py b/homeassistant/components/neato/config_flow.py index 07aea0a7e9c..15544371b2e 100644 --- a/homeassistant/components/neato/config_flow.py +++ b/homeassistant/components/neato/config_flow.py @@ -5,8 +5,6 @@ import logging from types import MappingProxyType from typing import Any -import voluptuous as vol - from homeassistant.config_entries import SOURCE_REAUTH from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import config_entry_oauth2_flow @@ -46,9 +44,7 @@ class OAuth2FlowHandler( ) -> FlowResult: """Confirm reauth upon migration of old entries.""" if user_input is None: - return self.async_show_form( - step_id="reauth_confirm", data_schema=vol.Schema({}) - ) + return self.async_show_form(step_id="reauth_confirm") return await self.async_step_user() async def async_oauth_create_entry(self, data: dict[str, Any]) -> FlowResult: diff --git a/homeassistant/components/neato/switch.py b/homeassistant/components/neato/switch.py index 8817e280f43..52226292248 100644 --- a/homeassistant/components/neato/switch.py +++ b/homeassistant/components/neato/switch.py @@ -108,7 +108,7 @@ class NeatoConnectedSwitch(SwitchEntity): ) @property - def entity_category(self) -> str: + def entity_category(self) -> EntityCategory: """Device entity category.""" return EntityCategory.CONFIG diff --git a/homeassistant/components/neato/translations/fr.json b/homeassistant/components/neato/translations/fr.json index 4348ce3d6f5..df805121f6b 100644 --- a/homeassistant/components/neato/translations/fr.json +++ b/homeassistant/components/neato/translations/fr.json @@ -2,9 +2,9 @@ "config": { "abort": { "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", - "authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration de l'URL d'authentification d\u00e9pass\u00e9.", + "authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration de l'URL d'authentification expir\u00e9.", "missing_configuration": "Le composant n'est pas configur\u00e9. Veuillez suivre la documentation.", - "no_url_available": "Aucune URL disponible. Pour plus d'informations sur cette erreur, [consultez la section d'aide] ( {docs_url} )", + "no_url_available": "Aucune URL disponible. Pour plus d'informations sur cette erreur, [consultez la section d'aide]({docs_url})", "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" }, "create_entry": { @@ -15,7 +15,7 @@ "title": "S\u00e9lectionner une m\u00e9thode d'authentification" }, "reauth_confirm": { - "title": "Voulez-vous commencer la configuration ?" + "title": "Voulez-vous commencer la configuration\u00a0?" } } }, diff --git a/homeassistant/components/nest/config_flow.py b/homeassistant/components/nest/config_flow.py index b257c7b51bb..aeebd48abb4 100644 --- a/homeassistant/components/nest/config_flow.py +++ b/homeassistant/components/nest/config_flow.py @@ -232,10 +232,7 @@ class NestFlowHandler( """Confirm reauth dialog.""" assert self.config_mode != ConfigMode.LEGACY, "Step only supported for SDM API" if user_input is None: - return self.async_show_form( - step_id="reauth_confirm", - data_schema=vol.Schema({}), - ) + return self.async_show_form(step_id="reauth_confirm") existing_entries = self._async_current_entries() if existing_entries: # Pick an existing auth implementation for Reauth if present. Note diff --git a/homeassistant/components/nest/diagnostics.py b/homeassistant/components/nest/diagnostics.py index 859aa834581..2840c34378b 100644 --- a/homeassistant/components/nest/diagnostics.py +++ b/homeassistant/components/nest/diagnostics.py @@ -2,8 +2,6 @@ from __future__ import annotations -from typing import Any - from google_nest_sdm import diagnostics from google_nest_sdm.device import Device from google_nest_sdm.device_traits import InfoTrait @@ -11,37 +9,57 @@ from google_nest_sdm.exceptions import ApiException from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceEntry from .const import DATA_SDM, DATA_SUBSCRIBER, DOMAIN REDACT_DEVICE_TRAITS = {InfoTrait.NAME} -async def async_get_config_entry_diagnostics( +async def _get_nest_devices( hass: HomeAssistant, config_entry: ConfigEntry -) -> dict: - """Return diagnostics for a config entry.""" +) -> dict[str, Device]: + """Return dict of available devices.""" if DATA_SDM not in config_entry.data: return {} if DATA_SUBSCRIBER not in hass.data[DOMAIN]: - return {"error": "No subscriber configured"} + return {} subscriber = hass.data[DOMAIN][DATA_SUBSCRIBER] + device_manager = await subscriber.async_get_device_manager() + devices: dict[str, Device] = device_manager.devices + return devices + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, config_entry: ConfigEntry +) -> dict: + """Return diagnostics for a config entry.""" try: - device_manager = await subscriber.async_get_device_manager() + nest_devices = await _get_nest_devices(hass, config_entry) except ApiException as err: return {"error": str(err)} - + if not nest_devices: + return {} return { **diagnostics.get_diagnostics(), "devices": [ - get_device_data(device) for device in device_manager.devices.values() + nest_device.get_diagnostics() for nest_device in nest_devices.values() ], } -def get_device_data(device: Device) -> dict[str, Any]: - """Return diagnostic information about a device.""" - # Library performs its own redaction for device data - return device.get_diagnostics() +async def async_get_device_diagnostics( + hass: HomeAssistant, + config_entry: ConfigEntry, + device: DeviceEntry, +) -> dict: + """Return diagnostics for a device.""" + try: + nest_devices = await _get_nest_devices(hass, config_entry) + except ApiException as err: + return {"error": str(err)} + nest_device_id = next(iter(device.identifiers))[1] + nest_device = nest_devices.get(nest_device_id) + return nest_device.get_diagnostics() if nest_device else {} diff --git a/homeassistant/components/nest/legacy/__init__.py b/homeassistant/components/nest/legacy/__init__.py index e0202c63567..79579a1c3df 100644 --- a/homeassistant/components/nest/legacy/__init__.py +++ b/homeassistant/components/nest/legacy/__init__.py @@ -109,12 +109,6 @@ async def async_setup_legacy(hass: HomeAssistant, config: dict) -> bool: if DOMAIN not in config: return True - _LOGGER.warning( - "The Legacy Works With Nest API is deprecated and support will be removed " - "in Home Assistant Core 2022.5; See instructions for using the Smart Device " - "Management API at https://www.home-assistant.io/integrations/nest/" - ) - conf = config[DOMAIN] local_auth.initialize(hass, conf[CONF_CLIENT_ID], conf[CONF_CLIENT_SECRET]) diff --git a/homeassistant/components/nest/manifest.json b/homeassistant/components/nest/manifest.json index 6968b401561..f59a8e6ac31 100644 --- a/homeassistant/components/nest/manifest.json +++ b/homeassistant/components/nest/manifest.json @@ -5,7 +5,7 @@ "dependencies": ["ffmpeg", "http"], "after_dependencies": ["media_source"], "documentation": "https://www.home-assistant.io/integrations/nest", - "requirements": ["python-nest==4.2.0", "google-nest-sdm==1.7.1"], + "requirements": ["python-nest==4.2.0", "google-nest-sdm==1.8.0"], "codeowners": ["@allenporter"], "quality_scale": "platinum", "dhcp": [ diff --git a/homeassistant/components/nest/media_source.py b/homeassistant/components/nest/media_source.py index af8a4af8ba5..7a8ae49bdbc 100644 --- a/homeassistant/components/nest/media_source.py +++ b/homeassistant/components/nest/media_source.py @@ -339,15 +339,21 @@ class NestMediaSource(MediaSource): media_id: MediaId | None = parse_media_id(item.identifier) if not media_id: raise Unresolvable("No identifier specified for MediaSourceItem") - if not media_id.event_token: - raise Unresolvable( - "Identifier missing an event_token: %s" % item.identifier - ) devices = await self.devices() if not (device := devices.get(media_id.device_id)): raise Unresolvable( "Unable to find device with identifier: %s" % item.identifier ) + if not media_id.event_token: + # The device resolves to the most recent event if available + if not ( + last_event_id := await _async_get_recent_event_id(media_id, device) + ): + raise Unresolvable( + "Unable to resolve recent event for device: %s" % item.identifier + ) + media_id = last_event_id + # Infer content type from the device, since it only supports one # snapshot type (either jpg or mp4 clip) content_type = EventImageType.IMAGE.content_type @@ -384,6 +390,7 @@ class NestMediaSource(MediaSource): device_id=last_event_id.device_id, event_token=last_event_id.event_token, ) + browse_device.can_play = True browse_root.children.append(browse_device) return browse_root diff --git a/homeassistant/components/nest/services.yaml b/homeassistant/components/nest/services.yaml index 98aacf60524..24b7290668f 100644 --- a/homeassistant/components/nest/services.yaml +++ b/homeassistant/components/nest/services.yaml @@ -11,8 +11,8 @@ set_away_mode: selector: select: options: - - 'away' - - 'home' + - "away" + - "home" structure: name: Structure description: Name(s) of structure(s) to change. Defaults to all structures if not specified. diff --git a/homeassistant/components/nest/translations/fr.json b/homeassistant/components/nest/translations/fr.json index adb7999d772..d639009dff8 100644 --- a/homeassistant/components/nest/translations/fr.json +++ b/homeassistant/components/nest/translations/fr.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration de l'URL d'authentification d\u00e9pass\u00e9.", + "authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration de l'URL d'authentification expir\u00e9.", "invalid_access_token": "Jeton d'acc\u00e8s non valide", "missing_configuration": "Le composant n'est pas configur\u00e9. Veuillez suivre la documentation.", - "no_url_available": "Aucune URL disponible. Pour plus d'informations sur cette erreur, [consultez la section d'aide] ( {docs_url} )", + "no_url_available": "Aucune URL disponible. Pour plus d'informations sur cette erreur, [consultez la section d'aide]({docs_url})", "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi", "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible.", "unknown_authorize_url_generation": "Erreur inconnue lors de la g\u00e9n\u00e9ration d'une URL d'autorisation." @@ -15,7 +15,7 @@ "error": { "bad_project_id": "Veuillez saisir un ID de projet Cloud valide (v\u00e9rifiez Cloud\u00a0Console)", "internal_error": "Erreur interne lors de la validation du code", - "invalid_pin": "Code PIN invalide", + "invalid_pin": "Code PIN non valide", "subscriber_error": "Erreur d'abonn\u00e9 inconnue, voir les journaux", "timeout": "D\u00e9lai de la validation du code expir\u00e9", "unknown": "Erreur inattendue", diff --git a/homeassistant/components/netatmo/config_flow.py b/homeassistant/components/netatmo/config_flow.py index ad8f75f5d45..bbd28e8398f 100644 --- a/homeassistant/components/netatmo/config_flow.py +++ b/homeassistant/components/netatmo/config_flow.py @@ -75,10 +75,7 @@ class NetatmoFlowHandler( ) -> FlowResult: """Dialog that informs the user that reauth is required.""" if user_input is None: - return self.async_show_form( - step_id="reauth_confirm", - data_schema=vol.Schema({}), - ) + return self.async_show_form(step_id="reauth_confirm") return await self.async_step_user() diff --git a/homeassistant/components/netatmo/manifest.json b/homeassistant/components/netatmo/manifest.json index e801d941a74..953ead88f33 100644 --- a/homeassistant/components/netatmo/manifest.json +++ b/homeassistant/components/netatmo/manifest.json @@ -2,28 +2,14 @@ "domain": "netatmo", "name": "Netatmo", "documentation": "https://www.home-assistant.io/integrations/netatmo", - "requirements": [ - "pyatmo==6.2.4" - ], - "after_dependencies": [ - "cloud", - "media_source" - ], - "dependencies": [ - "webhook" - ], - "codeowners": [ - "@cgtobi" - ], + "requirements": ["pyatmo==6.2.4"], + "after_dependencies": ["cloud", "media_source"], + "dependencies": ["webhook"], + "codeowners": ["@cgtobi"], "config_flow": true, "homekit": { - "models": [ - "Healty Home Coach", - "Netatmo Relay", - "Presence", - "Welcome" - ] + "models": ["Healty Home Coach", "Netatmo Relay", "Presence", "Welcome"] }, "iot_class": "cloud_polling", "loggers": ["pyatmo"] -} \ No newline at end of file +} diff --git a/homeassistant/components/netatmo/translations/fr.json b/homeassistant/components/netatmo/translations/fr.json index 5de581d301b..7fefb68f629 100644 --- a/homeassistant/components/netatmo/translations/fr.json +++ b/homeassistant/components/netatmo/translations/fr.json @@ -1,9 +1,9 @@ { "config": { "abort": { - "authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration de l'URL d'authentification d\u00e9pass\u00e9.", + "authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration de l'URL d'authentification expir\u00e9.", "missing_configuration": "Le composant n'est pas configur\u00e9. Veuillez suivre la documentation.", - "no_url_available": "Aucune URL disponible. Pour plus d'informations sur cette erreur, [consultez la section d'aide] ( {docs_url} )", + "no_url_available": "Aucune URL disponible. Pour plus d'informations sur cette erreur, [consultez la section d'aide]({docs_url})", "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi", "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." }, @@ -23,8 +23,8 @@ "device_automation": { "trigger_subtype": { "away": "absent", - "hg": "garde-gel", - "schedule": "horaire" + "hg": "hors-gel", + "schedule": "programm\u00e9" }, "trigger_type": { "alarm_started": "{entity_name} a d\u00e9tect\u00e9 une alarme", @@ -47,10 +47,10 @@ "public_weather": { "data": { "area_name": "Nom de la zone", - "lat_ne": "Latitude Nord-Est", - "lat_sw": "Latitude Sud-Ouest", - "lon_ne": "Longitude Nord-Est", - "lon_sw": "Longitude Sud-Ouest", + "lat_ne": "Latitude du coin nord-est", + "lat_sw": "Latitude du coin sud-ouest", + "lon_ne": "Longitude du coin nord-est", + "lon_sw": "Longitude du coin sud-ouest", "mode": "Calcul", "show_on_map": "Montrer sur la carte" }, diff --git a/homeassistant/components/netatmo/translations/it.json b/homeassistant/components/netatmo/translations/it.json index c1bfbca3330..e26cd64705f 100644 --- a/homeassistant/components/netatmo/translations/it.json +++ b/homeassistant/components/netatmo/translations/it.json @@ -47,10 +47,10 @@ "public_weather": { "data": { "area_name": "Nome dell'area", - "lat_ne": "Latitudine angolo Nord-Est", - "lat_sw": "Latitudine angolo Sud-Ovest", - "lon_ne": "Logitudine angolo Nord-Est", - "lon_sw": "Logitudine angolo Sud-Ovest", + "lat_ne": "Latitudine angolo nord-est", + "lat_sw": "Latitudine angolo sud-ovest", + "lon_ne": "Logitudine angolo norde-st", + "lon_sw": "Logitudine angolo sud-ovest", "mode": "Calcolo", "show_on_map": "Mostra sulla mappa" }, diff --git a/homeassistant/components/netatmo/translations/pl.json b/homeassistant/components/netatmo/translations/pl.json index 4b7ac6490cc..39137e1ae83 100644 --- a/homeassistant/components/netatmo/translations/pl.json +++ b/homeassistant/components/netatmo/translations/pl.json @@ -54,16 +54,16 @@ "mode": "Obliczenia", "show_on_map": "Poka\u017c na mapie" }, - "description": "Skonfiguruj publiczny czujnik pogody dla obszaru.", - "title": "Netatmo publiczny czujnik pogody" + "description": "Skonfiguruj publiczny sensor pogody dla obszaru.", + "title": "Publiczny sensor pogody Netatmo" }, "public_weather_areas": { "data": { "new_area": "Nazwa obszaru", "weather_areas": "Obszary pogodowe" }, - "description": "Skonfiguruj publiczne czujniki pogody.", - "title": "Netatmo publiczny czujnik pogody" + "description": "Skonfiguruj publiczne sensory pogody.", + "title": "Publiczny sensor pogody Netatmo" } } } diff --git a/homeassistant/components/netgear/__init__.py b/homeassistant/components/netgear/__init__.py index 2842157f578..72a56427e17 100644 --- a/homeassistant/components/netgear/__init__.py +++ b/homeassistant/components/netgear/__init__.py @@ -1,6 +1,9 @@ """Support for Netgear routers.""" +from __future__ import annotations + from datetime import timedelta import logging +from typing import Any from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PORT, CONF_SSL @@ -51,6 +54,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry.async_on_unload(entry.add_update_listener(update_listener)) + assert entry.unique_id device_registry = dr.async_get(hass) device_registry.async_get_or_create( config_entry_id=entry.entry_id, @@ -67,7 +71,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Fetch data from the router.""" return await router.async_update_device_trackers() - async def async_update_traffic_meter() -> dict: + async def async_update_traffic_meter() -> dict[str, Any] | None: """Fetch data from the router.""" return await router.async_get_traffic_meter() diff --git a/homeassistant/components/netgear/config_flow.py b/homeassistant/components/netgear/config_flow.py index 791fbd26bd3..85c206ce463 100644 --- a/homeassistant/components/netgear/config_flow.py +++ b/homeassistant/components/netgear/config_flow.py @@ -1,5 +1,8 @@ """Config flow to configure the Netgear integration.""" +from __future__ import annotations + import logging +from typing import cast from urllib.parse import urlparse from pynetgear import DEFAULT_HOST, DEFAULT_PORT, DEFAULT_USER @@ -119,11 +122,12 @@ class NetgearFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult: """Initialize flow from ssdp.""" - updated_data = {} + updated_data: dict[str, str | int | bool] = {} device_url = urlparse(discovery_info.ssdp_location) - if device_url.hostname: - updated_data[CONF_HOST] = device_url.hostname + if hostname := device_url.hostname: + hostname = cast(str, hostname) + updated_data[CONF_HOST] = hostname _LOGGER.debug("Netgear ssdp discovery info: %s", discovery_info) diff --git a/homeassistant/components/netgear/device_tracker.py b/homeassistant/components/netgear/device_tracker.py index 72699768f84..0f7f5cffb10 100644 --- a/homeassistant/components/netgear/device_tracker.py +++ b/homeassistant/components/netgear/device_tracker.py @@ -59,7 +59,7 @@ class NetgearScannerEntity(NetgearBaseEntity, ScannerEntity): self._hostname = self.get_hostname() self._icon = DEVICE_ICONS.get(device["device_type"], "mdi:help-network") - def get_hostname(self): + def get_hostname(self) -> str | None: """Return the hostname of the given device or None if we don't know.""" if (hostname := self._device["name"]) == "--": return None @@ -74,7 +74,7 @@ class NetgearScannerEntity(NetgearBaseEntity, ScannerEntity): self._icon = DEVICE_ICONS.get(self._device["device_type"], "mdi:help-network") @property - def is_connected(self): + def is_connected(self) -> bool: """Return true if the device is connected to the router.""" return self._active @@ -94,7 +94,7 @@ class NetgearScannerEntity(NetgearBaseEntity, ScannerEntity): return self._mac @property - def hostname(self) -> str: + def hostname(self) -> str | None: """Return the hostname.""" return self._hostname diff --git a/homeassistant/components/netgear/manifest.json b/homeassistant/components/netgear/manifest.json index b2c7ddf6be2..a5374f4c315 100644 --- a/homeassistant/components/netgear/manifest.json +++ b/homeassistant/components/netgear/manifest.json @@ -2,7 +2,7 @@ "domain": "netgear", "name": "NETGEAR", "documentation": "https://www.home-assistant.io/integrations/netgear", - "requirements": ["pynetgear==0.9.1"], + "requirements": ["pynetgear==0.9.4"], "codeowners": ["@hacf-fr", "@Quentame", "@starkillerOG"], "iot_class": "local_polling", "config_flow": true, diff --git a/homeassistant/components/netgear/router.py b/homeassistant/components/netgear/router.py index 9e44495aa62..f543ba8a5f3 100644 --- a/homeassistant/components/netgear/router.py +++ b/homeassistant/components/netgear/router.py @@ -5,6 +5,7 @@ from abc import abstractmethod import asyncio from datetime import timedelta import logging +from typing import Any from pynetgear import Netgear @@ -59,13 +60,14 @@ class NetgearRouter: def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: """Initialize a Netgear router.""" + assert entry.unique_id self.hass = hass self.entry = entry self.entry_id = entry.entry_id self.unique_id = entry.unique_id - self._host = entry.data.get(CONF_HOST) - self._port = entry.data.get(CONF_PORT) - self._ssl = entry.data.get(CONF_SSL) + self._host: str = entry.data[CONF_HOST] + self._port: int = entry.data[CONF_PORT] + self._ssl: bool = entry.data[CONF_SSL] self._username = entry.data.get(CONF_USERNAME) self._password = entry.data[CONF_PASSWORD] @@ -85,9 +87,9 @@ class NetgearRouter: self._api: Netgear = None self._api_lock = asyncio.Lock() - self.devices = {} + self.devices: dict[str, Any] = {} - def _setup(self) -> None: + def _setup(self) -> bool: """Set up a Netgear router sync portion.""" self._api = get_api( self._password, @@ -134,7 +136,7 @@ class NetgearRouter: if device_entry.via_device_id is None: continue # do not add the router itself - device_mac = dict(device_entry.connections).get(dr.CONNECTION_NETWORK_MAC) + device_mac = dict(device_entry.connections)[dr.CONNECTION_NETWORK_MAC] self.devices[device_mac] = { "mac": device_mac, "name": device_entry.name, @@ -166,14 +168,14 @@ class NetgearRouter: self._api.get_attached_devices_2 ) - async def async_update_device_trackers(self, now=None) -> None: + async def async_update_device_trackers(self, now=None) -> bool: """Update Netgear devices.""" new_device = False ntg_devices = await self.async_get_attached_devices() now = dt_util.utcnow() if ntg_devices is None: - return + return new_device if _LOGGER.isEnabledFor(logging.DEBUG): _LOGGER.debug("Netgear scan result: \n%s", ntg_devices) @@ -197,7 +199,7 @@ class NetgearRouter: return new_device - async def async_get_traffic_meter(self) -> None: + async def async_get_traffic_meter(self) -> dict[str, Any] | None: """Get the traffic meter data of the router.""" async with self._api_lock: return await self.hass.async_add_executor_job(self._api.get_traffic_meter) diff --git a/homeassistant/components/netgear/strings.json b/homeassistant/components/netgear/strings.json index 7a81d414e2f..3585d1e613b 100644 --- a/homeassistant/components/netgear/strings.json +++ b/homeassistant/components/netgear/strings.json @@ -27,4 +27,4 @@ } } } -} \ No newline at end of file +} diff --git a/homeassistant/components/netgear/translations/el.json b/homeassistant/components/netgear/translations/el.json index 141f8f31ddd..0ac5cb328bc 100644 --- a/homeassistant/components/netgear/translations/el.json +++ b/homeassistant/components/netgear/translations/el.json @@ -15,7 +15,7 @@ "ssl": "\u03a7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af \u03ad\u03bd\u03b1 \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03b7\u03c4\u03b9\u03ba\u03cc SSL", "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7 (\u03a0\u03c1\u03bf\u03b1\u03b9\u03c1\u03b5\u03c4\u03b9\u03ba\u03cc)" }, - "description": "\u03a0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03bf\u03c2 \u03ba\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2: {host}\n\u03a0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03b7 \u03b8\u03cd\u03c1\u03b1: {port}\n\u03a0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03bf \u03cc\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7: {username}", + "description": "\u03a0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03bf\u03c2 \u03ba\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2: {host}\n \u03a0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03bf \u03cc\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7: {username}", "title": "Netgear" } } diff --git a/homeassistant/components/netgear/translations/fr.json b/homeassistant/components/netgear/translations/fr.json index 3230b8df45f..fe507462e8d 100644 --- a/homeassistant/components/netgear/translations/fr.json +++ b/homeassistant/components/netgear/translations/fr.json @@ -13,7 +13,7 @@ "password": "Mot de passe", "port": "Port (facultatif)", "ssl": "Utilise un certificat SSL", - "username": "Nom d'utilisateur (Optional)" + "username": "Nom d'utilisateur (facultatif)" }, "description": "H\u00f4te par d\u00e9faut\u00a0: {host}\nNom d'utilisateur par d\u00e9faut\u00a0: {username}", "title": "Netgear" diff --git a/homeassistant/components/netgear/translations/nl.json b/homeassistant/components/netgear/translations/nl.json index 7333a0cd0ee..1d0ae357a8f 100644 --- a/homeassistant/components/netgear/translations/nl.json +++ b/homeassistant/components/netgear/translations/nl.json @@ -24,7 +24,7 @@ "step": { "init": { "data": { - "consider_home": "Overweeg thuis tijd (seconden)" + "consider_home": "Aantal seconden dat wordt gewacht voordat een apparaat als afwezig wordt beschouwd" }, "description": "Optionele instellingen opgeven", "title": "Netgear" diff --git a/homeassistant/components/netgear_lte/services.yaml b/homeassistant/components/netgear_lte/services.yaml index a708287612b..bed9647a1b7 100644 --- a/homeassistant/components/netgear_lte/services.yaml +++ b/homeassistant/components/netgear_lte/services.yaml @@ -32,18 +32,18 @@ set_option: selector: select: options: - - 'auto' - - 'mobile' - - 'wire' + - "auto" + - "mobile" + - "wire" autoconnect: name: Auto-connect description: Auto-connect mode. selector: select: options: - - 'always' - - 'home' - - 'never' + - "always" + - "home" + - "never" connect_lte: name: Connect LTE diff --git a/homeassistant/components/netio/switch.py b/homeassistant/components/netio/switch.py index 2ce3fdf5d85..cfd736c9538 100644 --- a/homeassistant/components/netio/switch.py +++ b/homeassistant/components/netio/switch.py @@ -175,26 +175,3 @@ class NetioSwitch(SwitchEntity): def update(self): """Update the state.""" self.netio.update() - - @property - def extra_state_attributes(self): - """Return optional state attributes.""" - return { - ATTR_TOTAL_CONSUMPTION_KWH: self.cumulated_consumption_kwh, - ATTR_START_DATE: self.start_date.split("|")[0], - } - - @property - def current_power_w(self): - """Return actual power.""" - return self.netio.consumptions[int(self.outlet) - 1] - - @property - def cumulated_consumption_kwh(self): - """Return the total enerygy consumption since start_date.""" - return self.netio.cumulated_consumptions[int(self.outlet) - 1] - - @property - def start_date(self): - """Point in time when the energy accumulation started.""" - return self.netio.start_dates[int(self.outlet) - 1] diff --git a/homeassistant/components/network/__init__.py b/homeassistant/components/network/__init__.py index b3ef88e7ab2..1ad975e1d85 100644 --- a/homeassistant/components/network/__init__.py +++ b/homeassistant/components/network/__init__.py @@ -6,11 +6,16 @@ import logging from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import UNDEFINED, ConfigType, UndefinedType from homeassistant.loader import bind_hass from . import util -from .const import IPV4_BROADCAST_ADDR, PUBLIC_TARGET_IP +from .const import ( + IPV4_BROADCAST_ADDR, + LOOPBACK_TARGET_IP, + MDNS_TARGET_IP, + PUBLIC_TARGET_IP, +) from .models import Adapter from .network import Network, async_get_network @@ -26,7 +31,7 @@ async def async_get_adapters(hass: HomeAssistant) -> list[Adapter]: @bind_hass async def async_get_source_ip( - hass: HomeAssistant, target_ip: str = PUBLIC_TARGET_IP + hass: HomeAssistant, target_ip: str | UndefinedType = UNDEFINED ) -> str: """Get the source ip for a target ip.""" adapters = await async_get_adapters(hass) @@ -35,7 +40,15 @@ async def async_get_source_ip( if adapter["enabled"] and (ipv4s := adapter["ipv4"]): all_ipv4s.extend([ipv4["address"] for ipv4 in ipv4s]) - source_ip = util.async_get_source_ip(target_ip) + if target_ip is UNDEFINED: + source_ip = ( + util.async_get_source_ip(PUBLIC_TARGET_IP) + or util.async_get_source_ip(MDNS_TARGET_IP) + or util.async_get_source_ip(LOOPBACK_TARGET_IP) + ) + else: + source_ip = util.async_get_source_ip(target_ip) + if not all_ipv4s: _LOGGER.warning( "Because the system does not have any enabled IPv4 addresses, source address detection may be inaccurate" @@ -60,12 +73,14 @@ async def async_get_enabled_source_ips( if not adapter["enabled"]: continue if adapter["ipv4"]: - sources.extend(IPv4Address(ipv4["address"]) for ipv4 in adapter["ipv4"]) + addrs_ipv4 = [IPv4Address(ipv4["address"]) for ipv4 in adapter["ipv4"]] + sources.extend(addrs_ipv4) if adapter["ipv6"]: - # With python 3.9 add scope_ids can be - # added by enumerating adapter["ipv6"]s - # IPv6Address(f"::%{ipv6['scope_id']}") - sources.extend(IPv6Address(ipv6["address"]) for ipv6 in adapter["ipv6"]) + addrs_ipv6 = [ + IPv6Address(f"{ipv6['address']}%{ipv6['scope_id']}") + for ipv6 in adapter["ipv6"] + ] + sources.extend(addrs_ipv6) return sources diff --git a/homeassistant/components/network/const.py b/homeassistant/components/network/const.py index 07c12e63a10..3a166189b85 100644 --- a/homeassistant/components/network/const.py +++ b/homeassistant/components/network/const.py @@ -17,6 +17,7 @@ ATTR_ADAPTERS: Final = "adapters" ATTR_CONFIGURED_ADAPTERS: Final = "configured_adapters" DEFAULT_CONFIGURED_ADAPTERS: list[str] = [] +LOOPBACK_TARGET_IP: Final = "127.0.0.1" MDNS_TARGET_IP: Final = "224.0.0.251" PUBLIC_TARGET_IP: Final = "8.8.8.8" IPV4_BROADCAST_ADDR: Final = "255.255.255.255" diff --git a/homeassistant/components/network/manifest.json b/homeassistant/components/network/manifest.json index 84e86014036..9f2fa7849f0 100644 --- a/homeassistant/components/network/manifest.json +++ b/homeassistant/components/network/manifest.json @@ -3,7 +3,7 @@ "name": "Network Configuration", "documentation": "https://www.home-assistant.io/integrations/network", "requirements": ["ifaddr==0.1.7"], - "codeowners": [], + "codeowners": ["@home-assistant/core"], "dependencies": ["websocket_api"], "quality_scale": "internal", "iot_class": "local_push" diff --git a/homeassistant/components/nexia/diagnostics.py b/homeassistant/components/nexia/diagnostics.py new file mode 100644 index 00000000000..9b3f518217b --- /dev/null +++ b/homeassistant/components/nexia/diagnostics.py @@ -0,0 +1,32 @@ +"""Diagnostics support for nexia.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.components.diagnostics import async_redact_data +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from .const import CONF_BRAND, DOMAIN +from .coordinator import NexiaDataUpdateCoordinator + +TO_REDACT = { + "dealer_contact_info", +} + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, entry: ConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + coordinator: NexiaDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + nexia_home = coordinator.nexia_home + + return { + "entry": { + "title": entry.title, + "brand": entry.data.get(CONF_BRAND), + }, + "automations": async_redact_data(nexia_home.automations_json, TO_REDACT), + "devices": async_redact_data(nexia_home.devices_json, TO_REDACT), + } diff --git a/homeassistant/components/nexia/services.yaml b/homeassistant/components/nexia/services.yaml index 78cf889f978..0deb5225cd3 100644 --- a/homeassistant/components/nexia/services.yaml +++ b/homeassistant/components/nexia/services.yaml @@ -8,14 +8,14 @@ set_aircleaner_mode: fields: aircleaner_mode: name: Air cleaner mode - description: 'The air cleaner mode to set.' + description: "The air cleaner mode to set." required: true selector: select: options: - - 'allergy' - - 'auto' - - 'quick' + - "allergy" + - "auto" + - "quick" set_humidify_setpoint: name: Set humidify set point @@ -33,7 +33,7 @@ set_humidify_setpoint: number: min: 35 max: 65 - unit_of_measurement: '%' + unit_of_measurement: "%" set_hvac_run_mode: name: Set hvac run mode @@ -45,20 +45,20 @@ set_hvac_run_mode: fields: run_mode: name: Run mode - description: 'Run the schedule or hold. If not specified, the current run mode will be used.' + description: "Run the schedule or hold. If not specified, the current run mode will be used." required: false selector: select: options: - - 'permanent_hold' - - 'run_schedule' + - "permanent_hold" + - "run_schedule" hvac_mode: name: Hvac mode - description: 'The hvac mode to use for the schedule or hold. If not specified, the current hvac mode will be used.' + description: "The hvac mode to use for the schedule or hold. If not specified, the current hvac mode will be used." required: false selector: select: options: - - 'auto' - - 'cool' - - 'heat' + - "auto" + - "cool" + - "heat" diff --git a/homeassistant/components/nexia/translations/fr.json b/homeassistant/components/nexia/translations/fr.json index b76672cd017..3955d07ff7a 100644 --- a/homeassistant/components/nexia/translations/fr.json +++ b/homeassistant/components/nexia/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/nfandroidtv/manifest.json b/homeassistant/components/nfandroidtv/manifest.json index 75163f3a92f..df285bea228 100644 --- a/homeassistant/components/nfandroidtv/manifest.json +++ b/homeassistant/components/nfandroidtv/manifest.json @@ -2,7 +2,7 @@ "domain": "nfandroidtv", "name": "Notifications for Android TV / Fire TV", "documentation": "https://www.home-assistant.io/integrations/nfandroidtv", - "requirements": ["notifications-android-tv==0.1.3"], + "requirements": ["notifications-android-tv==0.1.5"], "codeowners": ["@tkdrob"], "config_flow": true, "iot_class": "local_push", diff --git a/homeassistant/components/nfandroidtv/notify.py b/homeassistant/components/nfandroidtv/notify.py index 479bbc40891..b5e4962e9be 100644 --- a/homeassistant/components/nfandroidtv/notify.py +++ b/homeassistant/components/nfandroidtv/notify.py @@ -1,8 +1,9 @@ """Notifications for Android TV notification service.""" from __future__ import annotations +from io import BufferedReader import logging -from typing import Any, BinaryIO +from typing import Any from notifications_android_tv import Notifications import requests @@ -116,7 +117,7 @@ class NFAndroidTVNotificationService(BaseNotificationService): position = None transparency = None bkgcolor = None - interrupt = None + interrupt = False icon = None image_file = None if data: @@ -203,7 +204,7 @@ class NFAndroidTVNotificationService(BaseNotificationService): username: str | None = None, password: str | None = None, auth: str | None = None, - ) -> bytes | BinaryIO | None: + ) -> BufferedReader | bytes | None: """Load image/document/etc from a local path or URL.""" try: if url is not None: diff --git a/homeassistant/components/nfandroidtv/strings.json b/homeassistant/components/nfandroidtv/strings.json index 5940f86a406..fdc9f01d343 100644 --- a/homeassistant/components/nfandroidtv/strings.json +++ b/homeassistant/components/nfandroidtv/strings.json @@ -2,8 +2,7 @@ "config": { "step": { "user": { - "title": "Notifications for Android TV / Fire TV", - "description": "This integration requires the Notifications for Android TV app.\n\nFor Android TV: https://play.google.com/store/apps/details?id=de.cyberdream.androidtv.notifications.google\nFor Fire TV: https://www.amazon.com/Christian-Fees-Notifications-for-Fire/dp/B00OESCXEK\n\nYou should set up either DHCP reservation on your router (refer to your router's user manual) or a static IP address on the device. If not, the device will eventually become unavailable.", + "description": "Please refer to the documentation to make sure all requirements are met.", "data": { "host": "[%key:common::config_flow::data::host%]", "name": "[%key:common::config_flow::data::name%]" diff --git a/homeassistant/components/nightscout/strings.json b/homeassistant/components/nightscout/strings.json index b3b99485587..fd1d81e1273 100644 --- a/homeassistant/components/nightscout/strings.json +++ b/homeassistant/components/nightscout/strings.json @@ -1,22 +1,22 @@ { - "config": { - "step": { - "user": { - "title": "Enter your Nightscout server information.", - "description": "- URL: the address of your nightscout instance. I.e.: https://myhomeassistant.duckdns.org:5423\n- API Key (optional): Only use if your instance is protected (auth_default_roles != readable).", - "data": { - "url": "[%key:common::config_flow::data::url%]", - "api_key": "[%key:common::config_flow::data::api_key%]" - } - } - }, - "error": { - "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", - "unknown": "[%key:common::config_flow::error::unknown%]" - }, - "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + "config": { + "step": { + "user": { + "title": "Enter your Nightscout server information.", + "description": "- URL: the address of your nightscout instance. I.e.: https://myhomeassistant.duckdns.org:5423\n- API Key (optional): Only use if your instance is protected (auth_default_roles != readable).", + "data": { + "url": "[%key:common::config_flow::data::url%]", + "api_key": "[%key:common::config_flow::data::api_key%]" } + } + }, + "error": { + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } + } } diff --git a/homeassistant/components/nightscout/translations/el.json b/homeassistant/components/nightscout/translations/el.json index aaa8db2b70d..c484d715e76 100644 --- a/homeassistant/components/nightscout/translations/el.json +++ b/homeassistant/components/nightscout/translations/el.json @@ -15,7 +15,7 @@ "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API", "url": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL" }, - "description": "- \u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL: \u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03c0\u03b1\u03c1\u03bf\u03c5\u03c3\u03af\u03b1\u03c2 \u03c4\u03bf\u03c5 nightcout. \u0394\u03b7\u03bb\u03b1\u03b4\u03ae: https://myhomeassistant.duckdns.org:5423\n - \u039a\u03bb\u03b5\u03b9\u03b4\u03af API (\u03a0\u03c1\u03bf\u03b1\u03b9\u03c1\u03b5\u03c4\u03b9\u03ba\u03cc): \u03a7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03bc\u03cc\u03bd\u03bf \u03b5\u03ac\u03bd \u03b7 \u03c0\u03b1\u03c1\u03bf\u03c5\u03c3\u03af\u03b1 \u03c3\u03b1\u03c2 \u03c0\u03c1\u03bf\u03c3\u03c4\u03b1\u03c4\u03b5\u03cd\u03b5\u03c4\u03b1\u03b9 (auth_default_roles! = readable).", + "description": "- URL: \u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03c0\u03b1\u03c1\u03bf\u03c5\u03c3\u03af\u03b1\u03c2 nightscout \u03c3\u03b1\u03c2. \u0394\u03b7\u03bb\u03b1\u03b4\u03ae: https://myhomeassistant.duckdns.org:5423\n- \u039a\u03bb\u03b5\u03b9\u03b4\u03af API (\u03c0\u03c1\u03bf\u03b1\u03b9\u03c1\u03b5\u03c4\u03b9\u03ba\u03cc): \u03a7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf \u03bc\u03cc\u03bd\u03bf \u03b5\u03ac\u03bd \u03b7 \u03c0\u03b5\u03c1\u03af\u03c0\u03c4\u03c9\u03c3\u03ae \u03c3\u03b1\u03c2 \u03b5\u03af\u03bd\u03b1\u03b9 \u03c0\u03c1\u03bf\u03c3\u03c4\u03b1\u03c4\u03b5\u03c5\u03bc\u03ad\u03bd\u03b7 (auth_default_roles != readable).", "title": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 \u03c4\u03bf\u03c5 \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae Nightscout." } } diff --git a/homeassistant/components/nightscout/translations/fr.json b/homeassistant/components/nightscout/translations/fr.json index bec90374906..0b99785652d 100644 --- a/homeassistant/components/nightscout/translations/fr.json +++ b/homeassistant/components/nightscout/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "flow_title": "Nightscout", diff --git a/homeassistant/components/nina/__init__.py b/homeassistant/components/nina/__init__.py index 6fa5cab6f7a..16b6d01b8c2 100644 --- a/homeassistant/components/nina/__init__.py +++ b/homeassistant/components/nina/__init__.py @@ -1,7 +1,6 @@ """The Nina integration.""" from __future__ import annotations -import datetime as dt from typing import Any from async_timeout import timeout @@ -12,14 +11,16 @@ from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from homeassistant.util import dt as dt_util from .const import ( _LOGGER, + ATTR_DESCRIPTION, ATTR_EXPIRES, ATTR_HEADLINE, ATTR_ID, + ATTR_SENDER, ATTR_SENT, + ATTR_SEVERITY, ATTR_START, CONF_FILTER_CORONA, CONF_REGIONS, @@ -89,22 +90,15 @@ class NINADataUpdateCoordinator(DataUpdateCoordinator): warn_obj: dict[str, Any] = { ATTR_ID: raw_warn.id, ATTR_HEADLINE: raw_warn.headline, - ATTR_SENT: self._to_utc(raw_warn.sent), - ATTR_START: self._to_utc(raw_warn.start), - ATTR_EXPIRES: self._to_utc(raw_warn.expires), + ATTR_DESCRIPTION: raw_warn.description, + ATTR_SENDER: raw_warn.sender, + ATTR_SEVERITY: raw_warn.severity, + ATTR_SENT: raw_warn.sent or "", + ATTR_START: raw_warn.start or "", + ATTR_EXPIRES: raw_warn.expires or "", } warnings_for_regions.append(warn_obj) return_data[region_id] = warnings_for_regions return return_data - - @staticmethod - def _to_utc(input_time: str) -> str | None: - if input_time: - return ( - dt.datetime.fromisoformat(input_time) - .astimezone(dt_util.UTC) - .isoformat() - ) - return None diff --git a/homeassistant/components/nina/binary_sensor.py b/homeassistant/components/nina/binary_sensor.py index eca6668d0f5..29f985df618 100644 --- a/homeassistant/components/nina/binary_sensor.py +++ b/homeassistant/components/nina/binary_sensor.py @@ -14,10 +14,13 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import NINADataUpdateCoordinator from .const import ( + ATTR_DESCRIPTION, ATTR_EXPIRES, ATTR_HEADLINE, ATTR_ID, + ATTR_SENDER, ATTR_SENT, + ATTR_SEVERITY, ATTR_START, CONF_MESSAGE_SLOTS, CONF_REGIONS, @@ -46,7 +49,7 @@ async def async_setup_entry( async_add_entities(entities) -class NINAMessage(CoordinatorEntity, BinarySensorEntity): +class NINAMessage(CoordinatorEntity[NINADataUpdateCoordinator], BinarySensorEntity): """Representation of an NINA warning.""" def __init__( @@ -83,6 +86,9 @@ class NINAMessage(CoordinatorEntity, BinarySensorEntity): return { ATTR_HEADLINE: data[ATTR_HEADLINE], + ATTR_DESCRIPTION: data[ATTR_DESCRIPTION], + ATTR_SENDER: data[ATTR_SENDER], + ATTR_SEVERITY: data[ATTR_SEVERITY], ATTR_ID: data[ATTR_ID], ATTR_SENT: data[ATTR_SENT], ATTR_START: data[ATTR_START], diff --git a/homeassistant/components/nina/const.py b/homeassistant/components/nina/const.py index 18af5021544..76881501894 100644 --- a/homeassistant/components/nina/const.py +++ b/homeassistant/components/nina/const.py @@ -15,11 +15,14 @@ CONF_REGIONS: str = "regions" CONF_MESSAGE_SLOTS: str = "slots" CONF_FILTER_CORONA: str = "corona_filter" -ATTR_HEADLINE: str = "Headline" -ATTR_ID: str = "ID" -ATTR_SENT: str = "Sent" -ATTR_START: str = "Start" -ATTR_EXPIRES: str = "Expires" +ATTR_HEADLINE: str = "headline" +ATTR_DESCRIPTION: str = "description" +ATTR_SENDER: str = "sender" +ATTR_SEVERITY: str = "severity" +ATTR_ID: str = "id" +ATTR_SENT: str = "sent" +ATTR_START: str = "start" +ATTR_EXPIRES: str = "expires" CONST_LIST_A_TO_D: list[str] = ["A", "Ä", "B", "C", "D"] CONST_LIST_E_TO_H: list[str] = ["E", "F", "G", "H"] diff --git a/homeassistant/components/nina/manifest.json b/homeassistant/components/nina/manifest.json index 7c45d193bbc..0797076917d 100644 --- a/homeassistant/components/nina/manifest.json +++ b/homeassistant/components/nina/manifest.json @@ -3,13 +3,9 @@ "name": "NINA", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/nina", - "requirements": [ - "pynina==0.1.7" - ], + "requirements": ["pynina==0.1.7"], "dependencies": [], - "codeowners": [ - "@DeerMaximum" - ], + "codeowners": ["@DeerMaximum"], "iot_class": "cloud_polling", "loggers": ["pynina"] -} \ No newline at end of file +} diff --git a/homeassistant/components/nina/strings.json b/homeassistant/components/nina/strings.json index 1d9bb83def3..49ecf7fa7fa 100644 --- a/homeassistant/components/nina/strings.json +++ b/homeassistant/components/nina/strings.json @@ -1,27 +1,27 @@ { - "config": { - "step":{ - "user": { - "title": "Select city/county", - "data" : { - "_a_to_d": "City/county (A-D)", - "_e_to_h": "City/county (E-H)", - "_i_to_l": "City/county (I-L)", - "_m_to_q": "City/county (M-Q)", - "_r_to_u": "City/county (R-U)", - "_v_to_z": "City/county (V-Z)", - "slots": "Maximum warnings per city/county", - "corona_filter": "Remove Corona Warnings" - } - } - }, - "abort": { - "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" - }, - "error": { - "no_selection": "Please select at least one city/county", - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", - "unknown": "[%key:common::config_flow::error::unknown%]" + "config": { + "step": { + "user": { + "title": "Select city/county", + "data": { + "_a_to_d": "City/county (A-D)", + "_e_to_h": "City/county (E-H)", + "_i_to_l": "City/county (I-L)", + "_m_to_q": "City/county (M-Q)", + "_r_to_u": "City/county (R-U)", + "_v_to_z": "City/county (V-Z)", + "slots": "Maximum warnings per city/county", + "corona_filter": "Remove Corona Warnings" } + } + }, + "abort": { + "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" + }, + "error": { + "no_selection": "Please select at least one city/county", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "unknown": "[%key:common::config_flow::error::unknown%]" } -} \ No newline at end of file + } +} diff --git a/homeassistant/components/nmap_tracker/strings.json b/homeassistant/components/nmap_tracker/strings.json index ed5a8cb0b05..ef660c7e991 100644 --- a/homeassistant/components/nmap_tracker/strings.json +++ b/homeassistant/components/nmap_tracker/strings.json @@ -2,17 +2,17 @@ "title": "Nmap Tracker", "options": { "step": { - "init": { - "description": "[%key:component::nmap_tracker::config::step::user::description%]", - "data": { - "hosts": "[%key:component::nmap_tracker::config::step::user::data::hosts%]", - "home_interval": "[%key:component::nmap_tracker::config::step::user::data::home_interval%]", - "consider_home": "Seconds to wait till marking a device tracker as not home after not being seen.", - "exclude": "[%key:component::nmap_tracker::config::step::user::data::exclude%]", - "scan_options": "[%key:component::nmap_tracker::config::step::user::data::scan_options%]", - "interval_seconds": "Scan interval" - } + "init": { + "description": "[%key:component::nmap_tracker::config::step::user::description%]", + "data": { + "hosts": "[%key:component::nmap_tracker::config::step::user::data::hosts%]", + "home_interval": "[%key:component::nmap_tracker::config::step::user::data::home_interval%]", + "consider_home": "Seconds to wait till marking a device tracker as not home after not being seen.", + "exclude": "[%key:component::nmap_tracker::config::step::user::data::exclude%]", + "scan_options": "[%key:component::nmap_tracker::config::step::user::data::scan_options%]", + "interval_seconds": "Scan interval" } + } }, "error": { "invalid_hosts": "[%key:component::nmap_tracker::config::error::invalid_hosts%]" @@ -21,11 +21,11 @@ "config": { "step": { "user": { - "description":"Configure hosts to be scanned by Nmap. Network address and excludes can be IP Addresses (192.168.1.1), IP Networks (192.168.0.0/24) or IP Ranges (192.168.1.0-32).", + "description": "Configure hosts to be scanned by Nmap. Network address and excludes can be IP Addresses (192.168.1.1), IP Networks (192.168.0.0/24) or IP Ranges (192.168.1.0-32).", "data": { - "hosts": "Network addresses (comma seperated) to scan", + "hosts": "Network addresses (comma separated) to scan", "home_interval": "Minimum number of minutes between scans of active devices (preserve battery)", - "exclude": "Network addresses (comma seperated) to exclude from scanning", + "exclude": "Network addresses (comma separated) to exclude from scanning", "scan_options": "Raw configurable scan options for Nmap" } } @@ -37,4 +37,4 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_location%]" } } -} \ No newline at end of file +} diff --git a/homeassistant/components/nmap_tracker/translations/en.json b/homeassistant/components/nmap_tracker/translations/en.json index 9ded6eae4c2..ae4175e0f14 100644 --- a/homeassistant/components/nmap_tracker/translations/en.json +++ b/homeassistant/components/nmap_tracker/translations/en.json @@ -9,9 +9,9 @@ "step": { "user": { "data": { - "exclude": "Network addresses (comma seperated) to exclude from scanning", + "exclude": "Network addresses (comma separated) to exclude from scanning", "home_interval": "Minimum number of minutes between scans of active devices (preserve battery)", - "hosts": "Network addresses (comma seperated) to scan", + "hosts": "Network addresses (comma separated) to scan", "scan_options": "Raw configurable scan options for Nmap" }, "description": "Configure hosts to be scanned by Nmap. Network address and excludes can be IP Addresses (192.168.1.1), IP Networks (192.168.0.0/24) or IP Ranges (192.168.1.0-32)." @@ -26,12 +26,11 @@ "init": { "data": { "consider_home": "Seconds to wait till marking a device tracker as not home after not being seen.", - "exclude": "Network addresses (comma seperated) to exclude from scanning", + "exclude": "Network addresses (comma separated) to exclude from scanning", "home_interval": "Minimum number of minutes between scans of active devices (preserve battery)", - "hosts": "Network addresses (comma seperated) to scan", + "hosts": "Network addresses (comma separated) to scan", "interval_seconds": "Scan interval", - "scan_options": "Raw configurable scan options for Nmap", - "track_new_devices": "Track new devices" + "scan_options": "Raw configurable scan options for Nmap" }, "description": "Configure hosts to be scanned by Nmap. Network address and excludes can be IP Addresses (192.168.1.1), IP Networks (192.168.0.0/24) or IP Ranges (192.168.1.0-32)." } diff --git a/homeassistant/components/nmap_tracker/translations/fr.json b/homeassistant/components/nmap_tracker/translations/fr.json index 59e02ea14cc..0a4e75a9813 100644 --- a/homeassistant/components/nmap_tracker/translations/fr.json +++ b/homeassistant/components/nmap_tracker/translations/fr.json @@ -4,7 +4,7 @@ "already_configured": "L'emplacement est d\u00e9j\u00e0 configur\u00e9" }, "error": { - "invalid_hosts": "H\u00f4tes invalides" + "invalid_hosts": "H\u00f4tes non valides" }, "step": { "user": { @@ -20,7 +20,7 @@ }, "options": { "error": { - "invalid_hosts": "H\u00f4tes invalides" + "invalid_hosts": "H\u00f4tes non valides" }, "step": { "init": { diff --git a/homeassistant/components/notify/legacy.py b/homeassistant/components/notify/legacy.py index af29a9fba99..50b02324827 100644 --- a/homeassistant/components/notify/legacy.py +++ b/homeassistant/components/notify/legacy.py @@ -29,11 +29,13 @@ from .const import ( CONF_FIELDS = "fields" NOTIFY_SERVICES = "notify_services" +NOTIFY_DISCOVERY_DISPATCHER = "notify_discovery_dispatcher" async def async_setup_legacy(hass: HomeAssistant, config: ConfigType) -> None: """Set up legacy notify services.""" hass.data.setdefault(NOTIFY_SERVICES, {}) + hass.data.setdefault(NOTIFY_DISCOVERY_DISPATCHER, None) async def async_setup_platform( integration_name: str, @@ -114,7 +116,9 @@ async def async_setup_legacy(hass: HomeAssistant, config: ConfigType) -> None: """Handle for discovered platform.""" await async_setup_platform(platform, discovery_info=info) - discovery.async_listen_platform(hass, DOMAIN, async_platform_discovered) + hass.data[NOTIFY_DISCOVERY_DISPATCHER] = discovery.async_listen_platform( + hass, DOMAIN, async_platform_discovered + ) @callback @@ -147,6 +151,9 @@ async def async_reload(hass: HomeAssistant, integration_name: str) -> None: @bind_hass async def async_reset_platform(hass: HomeAssistant, integration_name: str) -> None: """Unregister notify services for an integration.""" + if NOTIFY_DISCOVERY_DISPATCHER in hass.data: + hass.data[NOTIFY_DISCOVERY_DISPATCHER]() + hass.data[NOTIFY_DISCOVERY_DISPATCHER] = None if not _async_integration_has_notify_services(hass, integration_name): return diff --git a/homeassistant/components/notify/services.yaml b/homeassistant/components/notify/services.yaml index 7284cb68eb6..bc31aef1a6e 100644 --- a/homeassistant/components/notify/services.yaml +++ b/homeassistant/components/notify/services.yaml @@ -51,25 +51,3 @@ persistent_notification: example: "Your Garage Door Friend" selector: text: - -apns_register: - name: Register APNS device - description: - Registers a device to receive push notifications via APNS (Apple Push - Notification Service). - fields: - push_id: - name: Push ID - description: - The device token, a 64 character hex string (256 bits). The device token - is provided to you by your client app, which receives the token after - registering itself with the remote notification service. - example: "72f2a8633655c5ce574fdc9b2b34ff8abdfc3b739b6ceb7a9ff06c1cbbf99f62" - selector: - text: - name: - name: Name - description: A friendly name for the device. - example: "Sam's iPhone" - selector: - text: diff --git a/homeassistant/components/notify/translations/el.json b/homeassistant/components/notify/translations/el.json index e95012f3183..0b85b3aeabf 100644 --- a/homeassistant/components/notify/translations/el.json +++ b/homeassistant/components/notify/translations/el.json @@ -1,3 +1,3 @@ { - "title": "\u039a\u03bf\u03b9\u03bd\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7" + "title": "\u0395\u03b9\u03b4\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03b9\u03c2" } \ No newline at end of file diff --git a/homeassistant/components/notion/translations/fr.json b/homeassistant/components/notion/translations/fr.json index 111fc918818..c5c0145e972 100644 --- a/homeassistant/components/notion/translations/fr.json +++ b/homeassistant/components/notion/translations/fr.json @@ -5,7 +5,7 @@ "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" }, "error": { - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "no_devices": "Aucun appareil trouv\u00e9 sur le compte", "unknown": "Erreur inattendue" }, diff --git a/homeassistant/components/nsw_fuel_station/sensor.py b/homeassistant/components/nsw_fuel_station/sensor.py index 2e022a7ec13..82a5e3a59a8 100644 --- a/homeassistant/components/nsw_fuel_station/sensor.py +++ b/homeassistant/components/nsw_fuel_station/sensor.py @@ -83,7 +83,9 @@ def setup_platform( add_entities(entities) -class StationPriceSensor(CoordinatorEntity, SensorEntity): +class StationPriceSensor( + CoordinatorEntity[DataUpdateCoordinator[StationPriceData]], SensorEntity +): """Implementation of a sensor that reports the fuel price for a station.""" def __init__( diff --git a/homeassistant/components/nuheat/translations/fr.json b/homeassistant/components/nuheat/translations/fr.json index 6c50dae47d5..9c0c76d6553 100644 --- a/homeassistant/components/nuheat/translations/fr.json +++ b/homeassistant/components/nuheat/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "invalid_thermostat": "Le num\u00e9ro de s\u00e9rie du thermostat n'est pas valide.", "unknown": "Erreur inattendue" }, diff --git a/homeassistant/components/nuki/strings.json b/homeassistant/components/nuki/strings.json index 3f6de25122a..6552f08721e 100644 --- a/homeassistant/components/nuki/strings.json +++ b/homeassistant/components/nuki/strings.json @@ -1,28 +1,28 @@ { - "config": { - "step": { - "user": { - "data": { - "host": "[%key:common::config_flow::data::host%]", - "port": "[%key:common::config_flow::data::port%]", - "token": "[%key:common::config_flow::data::access_token%]" - } - }, - "reauth_confirm": { - "title": "[%key:common::config_flow::title::reauth%]", - "description": "The Nuki integration needs to re-authenticate with your bridge.", - "data": { - "token": "[%key:common::config_flow::data::access_token%]" - } - } - }, - "error": { - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", - "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", - "unknown": "[%key:common::config_flow::error::unknown%]" - }, - "abort": { - "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" + "config": { + "step": { + "user": { + "data": { + "host": "[%key:common::config_flow::data::host%]", + "port": "[%key:common::config_flow::data::port%]", + "token": "[%key:common::config_flow::data::access_token%]" } + }, + "reauth_confirm": { + "title": "[%key:common::config_flow::title::reauth%]", + "description": "The Nuki integration needs to re-authenticate with your bridge.", + "data": { + "token": "[%key:common::config_flow::data::access_token%]" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } -} \ No newline at end of file + } +} diff --git a/homeassistant/components/nuki/translations/fr.json b/homeassistant/components/nuki/translations/fr.json index 360e374888c..e6950517033 100644 --- a/homeassistant/components/nuki/translations/fr.json +++ b/homeassistant/components/nuki/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/number/recorder.py b/homeassistant/components/number/recorder.py new file mode 100644 index 00000000000..39418a48878 --- /dev/null +++ b/homeassistant/components/number/recorder.py @@ -0,0 +1,17 @@ +"""Integration platform for recorder.""" +from __future__ import annotations + +from homeassistant.core import HomeAssistant, callback + +from . import ATTR_MAX, ATTR_MIN, ATTR_MODE, ATTR_STEP + + +@callback +def exclude_attributes(hass: HomeAssistant) -> set[str]: + """Exclude static attributes from being recorded in the database.""" + return { + ATTR_MIN, + ATTR_MAX, + ATTR_STEP, + ATTR_MODE, + } diff --git a/homeassistant/components/number/translations/zh-Hant.json b/homeassistant/components/number/translations/zh-Hant.json index d36f751682d..d724acc8f5e 100644 --- a/homeassistant/components/number/translations/zh-Hant.json +++ b/homeassistant/components/number/translations/zh-Hant.json @@ -4,5 +4,5 @@ "set_value": "{entity_name} \u8a2d\u5b9a\u503c" } }, - "title": "\u865f\u78bc" + "title": "\u6578\u5b57" } \ No newline at end of file diff --git a/homeassistant/components/nzbget/__init__.py b/homeassistant/components/nzbget/__init__.py index d9a41c90535..cb906495d58 100644 --- a/homeassistant/components/nzbget/__init__.py +++ b/homeassistant/components/nzbget/__init__.py @@ -154,7 +154,7 @@ async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> Non await hass.config_entries.async_reload(entry.entry_id) -class NZBGetEntity(CoordinatorEntity): +class NZBGetEntity(CoordinatorEntity[NZBGetDataUpdateCoordinator]): """Defines a base NZBGet entity.""" def __init__( diff --git a/homeassistant/components/nzbget/services.yaml b/homeassistant/components/nzbget/services.yaml index 8fe8780dce9..46439b761e1 100644 --- a/homeassistant/components/nzbget/services.yaml +++ b/homeassistant/components/nzbget/services.yaml @@ -20,4 +20,4 @@ set_speed: number: min: 0 max: 1000000 - unit_of_measurement: 'kB/s' + unit_of_measurement: "kB/s" diff --git a/homeassistant/components/nzbget/translations/el.json b/homeassistant/components/nzbget/translations/el.json index c2d086e8be1..9f2f0612071 100644 --- a/homeassistant/components/nzbget/translations/el.json +++ b/homeassistant/components/nzbget/translations/el.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" }, - "flow_title": "NZBGet: {\u03cc\u03bd\u03bf\u03bc\u03b1}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/octoprint/binary_sensor.py b/homeassistant/components/octoprint/binary_sensor.py index e1db7a95136..b0e43bd74e0 100644 --- a/homeassistant/components/octoprint/binary_sensor.py +++ b/homeassistant/components/octoprint/binary_sensor.py @@ -36,11 +36,11 @@ async def async_setup_entry( async_add_entities(entities) -class OctoPrintBinarySensorBase(CoordinatorEntity, BinarySensorEntity): +class OctoPrintBinarySensorBase( + CoordinatorEntity[OctoprintDataUpdateCoordinator], BinarySensorEntity +): """Representation an OctoPrint binary sensor.""" - coordinator: OctoprintDataUpdateCoordinator - def __init__( self, coordinator: OctoprintDataUpdateCoordinator, diff --git a/homeassistant/components/octoprint/button.py b/homeassistant/components/octoprint/button.py index 97676592f47..e16f123a73a 100644 --- a/homeassistant/components/octoprint/button.py +++ b/homeassistant/components/octoprint/button.py @@ -34,10 +34,9 @@ async def async_setup_entry( ) -class OctoprintButton(CoordinatorEntity, ButtonEntity): +class OctoprintButton(CoordinatorEntity[OctoprintDataUpdateCoordinator], ButtonEntity): """Represent an OctoPrint binary sensor.""" - coordinator: OctoprintDataUpdateCoordinator client: OctoprintClient def __init__( diff --git a/homeassistant/components/octoprint/manifest.json b/homeassistant/components/octoprint/manifest.json index 385ab88428a..4086f9fbe20 100644 --- a/homeassistant/components/octoprint/manifest.json +++ b/homeassistant/components/octoprint/manifest.json @@ -3,7 +3,7 @@ "name": "OctoPrint", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/octoprint", - "requirements": ["pyoctoprintapi==0.1.7"], + "requirements": ["pyoctoprintapi==0.1.8"], "codeowners": ["@rfleming71"], "zeroconf": ["_octoprint._tcp.local."], "ssdp": [ diff --git a/homeassistant/components/octoprint/sensor.py b/homeassistant/components/octoprint/sensor.py index 5a094c10987..4efc094c297 100644 --- a/homeassistant/components/octoprint/sensor.py +++ b/homeassistant/components/octoprint/sensor.py @@ -89,11 +89,11 @@ async def async_setup_entry( async_add_entities(entities) -class OctoPrintSensorBase(CoordinatorEntity, SensorEntity): +class OctoPrintSensorBase( + CoordinatorEntity[OctoprintDataUpdateCoordinator], SensorEntity +): """Representation of an OctoPrint sensor.""" - coordinator: OctoprintDataUpdateCoordinator - def __init__( self, coordinator: OctoprintDataUpdateCoordinator, diff --git a/homeassistant/components/octoprint/translations/fr.json b/homeassistant/components/octoprint/translations/fr.json index da1389f619a..be78ad8b877 100644 --- a/homeassistant/components/octoprint/translations/fr.json +++ b/homeassistant/components/octoprint/translations/fr.json @@ -22,7 +22,7 @@ "port": "Num\u00e9ro de port", "ssl": "Utiliser SSL", "username": "Nom d'utilisateur", - "verify_ssl": "V\u00e9rifier certificat SSL" + "verify_ssl": "V\u00e9rifier le certificat SSL" } } } diff --git a/homeassistant/components/octoprint/translations/it.json b/homeassistant/components/octoprint/translations/it.json index 3b5950adee3..639b304417d 100644 --- a/homeassistant/components/octoprint/translations/it.json +++ b/homeassistant/components/octoprint/translations/it.json @@ -22,7 +22,7 @@ "port": "Numero porta", "ssl": "Utilizza SSL", "username": "Nome utente", - "verify_ssl": "Verifica certificato SSL" + "verify_ssl": "Verifica il certificato SSL" } } } diff --git a/homeassistant/components/ombi/services.yaml b/homeassistant/components/ombi/services.yaml index 5a44c7bba02..d7e7068e84c 100644 --- a/homeassistant/components/ombi/services.yaml +++ b/homeassistant/components/ombi/services.yaml @@ -29,9 +29,9 @@ submit_tv_request: selector: select: options: - - 'all' - - 'first' - - 'latest' + - "all" + - "first" + - "latest" submit_music_request: name: Submit music request diff --git a/homeassistant/components/omnilogic/common.py b/homeassistant/components/omnilogic/common.py index 78685036e06..4c92420972b 100644 --- a/homeassistant/components/omnilogic/common.py +++ b/homeassistant/components/omnilogic/common.py @@ -73,7 +73,7 @@ class OmniLogicUpdateCoordinator(DataUpdateCoordinator): return parsed_data -class OmniLogicEntity(CoordinatorEntity): +class OmniLogicEntity(CoordinatorEntity[OmniLogicUpdateCoordinator]): """Defines the base OmniLogic entity.""" def __init__( diff --git a/homeassistant/components/omnilogic/translations/fr.json b/homeassistant/components/omnilogic/translations/fr.json index 4a8b293aebf..3567530d77e 100644 --- a/homeassistant/components/omnilogic/translations/fr.json +++ b/homeassistant/components/omnilogic/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/oncue/manifest.json b/homeassistant/components/oncue/manifest.json index 190b5c790cc..9991d35ed18 100644 --- a/homeassistant/components/oncue/manifest.json +++ b/homeassistant/components/oncue/manifest.json @@ -2,10 +2,12 @@ "domain": "oncue", "name": "Oncue by Kohler", "config_flow": true, - "dhcp": [{ - "hostname": "kohlergen*", - "macaddress": "00146F*" - }], + "dhcp": [ + { + "hostname": "kohlergen*", + "macaddress": "00146F*" + } + ], "documentation": "https://www.home-assistant.io/integrations/oncue", "requirements": ["aiooncue==0.3.2"], "codeowners": ["@bdraco"], diff --git a/homeassistant/components/oncue/strings.json b/homeassistant/components/oncue/strings.json index cdf281b4fea..f7a539fe0e6 100644 --- a/homeassistant/components/oncue/strings.json +++ b/homeassistant/components/oncue/strings.json @@ -17,4 +17,4 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_account%]" } } -} \ No newline at end of file +} diff --git a/homeassistant/components/oncue/translations/fr.json b/homeassistant/components/oncue/translations/fr.json index 1114bc4069e..dc122998917 100644 --- a/homeassistant/components/oncue/translations/fr.json +++ b/homeassistant/components/oncue/translations/fr.json @@ -4,9 +4,9 @@ "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9" }, "error": { - "cannot_connect": "Erreur de connexion", - "invalid_auth": "Erreur d'authentification", - "unknown": "Erreur" + "cannot_connect": "\u00c9chec de connexion", + "invalid_auth": "Authentification non valide", + "unknown": "Erreur inattendue" }, "step": { "user": { diff --git a/homeassistant/components/ondilo_ico/strings.json b/homeassistant/components/ondilo_ico/strings.json index 7350cc18236..4e5f2330840 100644 --- a/homeassistant/components/ondilo_ico/strings.json +++ b/homeassistant/components/ondilo_ico/strings.json @@ -13,4 +13,4 @@ "default": "[%key:common::config_flow::create_entry::authenticated%]" } } -} \ No newline at end of file +} diff --git a/homeassistant/components/ondilo_ico/translations/fr.json b/homeassistant/components/ondilo_ico/translations/fr.json index 540d3e1e6c2..f84926d676f 100644 --- a/homeassistant/components/ondilo_ico/translations/fr.json +++ b/homeassistant/components/ondilo_ico/translations/fr.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration de l'URL d'authentification d\u00e9pass\u00e9.", + "authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration de l'URL d'authentification expir\u00e9.", "missing_configuration": "Le composant n'est pas configur\u00e9. Veuillez suivre la documentation." }, "create_entry": { diff --git a/homeassistant/components/onewire/__init__.py b/homeassistant/components/onewire/__init__.py index 70a0a5fc856..c6f3d7dfa3f 100644 --- a/homeassistant/components/onewire/__init__.py +++ b/homeassistant/components/onewire/__init__.py @@ -30,6 +30,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.config_entries.async_setup_platforms(entry, PLATFORMS) + entry.async_on_unload(entry.add_update_listener(options_update_listener)) + return True @@ -41,3 +43,9 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> if unload_ok: hass.data[DOMAIN].pop(config_entry.entry_id) return unload_ok + + +async def options_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Handle options update.""" + _LOGGER.debug("Configuration options updated, reloading OneWire integration") + await hass.config_entries.async_reload(entry.entry_id) diff --git a/homeassistant/components/onewire/config_flow.py b/homeassistant/components/onewire/config_flow.py index 20b76ff236b..25604473bcf 100644 --- a/homeassistant/components/onewire/config_flow.py +++ b/homeassistant/components/onewire/config_flow.py @@ -5,10 +5,12 @@ from typing import Any import voluptuous as vol -from homeassistant.config_entries import ConfigFlow +from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TYPE -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers import config_validation as cv, device_registry as dr +from homeassistant.helpers.device_registry import DeviceRegistry from .const import ( CONF_MOUNT_DIR, @@ -17,8 +19,15 @@ from .const import ( DEFAULT_OWSERVER_HOST, DEFAULT_OWSERVER_PORT, DEFAULT_SYSBUS_MOUNT_DIR, + DEVICE_SUPPORT_OPTIONS, DOMAIN, + INPUT_ENTRY_CLEAR_OPTIONS, + INPUT_ENTRY_DEVICE_SELECTION, + OPTION_ENTRY_DEVICE_OPTIONS, + OPTION_ENTRY_SENSOR_PRECISION, + PRECISION_MAPPING_FAMILY_28, ) +from .model import OWServerDeviceDescription from .onewirehub import CannotConnect, InvalidPath, OneWireHub DATA_SCHEMA_USER = vol.Schema( @@ -164,3 +173,161 @@ class OneWireFlowHandler(ConfigFlow, domain=DOMAIN): data_schema=DATA_SCHEMA_MOUNTDIR, errors=errors, ) + + @staticmethod + @callback + def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlow: + """Get the options flow for this handler.""" + return OnewireOptionsFlowHandler(config_entry) + + +class OnewireOptionsFlowHandler(OptionsFlow): + """Handle OneWire Config options.""" + + def __init__(self, config_entry: ConfigEntry) -> None: + """Initialize OneWire Network options flow.""" + self.entry_id = config_entry.entry_id + self.options = dict(config_entry.options) + self.configurable_devices: dict[str, OWServerDeviceDescription] = {} + self.devices_to_configure: dict[str, OWServerDeviceDescription] = {} + self.current_device: str = "" + + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Manage the options.""" + controller: OneWireHub = self.hass.data[DOMAIN][self.entry_id] + if controller.type == CONF_TYPE_SYSBUS: + return self.async_abort( + reason="SysBus setup does not have any config options." + ) + + all_devices: list[OWServerDeviceDescription] = controller.devices # type: ignore[assignment] + if not all_devices: + return self.async_abort(reason="No configurable devices found.") + + device_registry = dr.async_get(self.hass) + self.configurable_devices = { + self._get_device_long_name(device_registry, device.id): device + for device in all_devices + if device.family in DEVICE_SUPPORT_OPTIONS + } + + return await self.async_step_device_selection(user_input=None) + + async def async_step_device_selection( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Select what devices to configure.""" + errors = {} + if user_input is not None: + if user_input.get(INPUT_ENTRY_CLEAR_OPTIONS): + # Reset all options + self.options = {} + return await self._update_options() + + selected_devices: list[str] = ( + user_input.get(INPUT_ENTRY_DEVICE_SELECTION) or [] + ) + if selected_devices: + self.devices_to_configure = { + device_name: self.configurable_devices[device_name] + for device_name in selected_devices + } + + return await self.async_step_configure_device(user_input=None) + errors["base"] = "device_not_selected" + + return self.async_show_form( + step_id="device_selection", + data_schema=vol.Schema( + { + vol.Optional( + INPUT_ENTRY_CLEAR_OPTIONS, + default=False, + ): bool, + vol.Optional( + INPUT_ENTRY_DEVICE_SELECTION, + default=self._get_current_configured_sensors(), + description="Multiselect with list of devices to choose from", + ): cv.multi_select( + {device: False for device in self.configurable_devices} + ), + } + ), + errors=errors, + ) + + async def async_step_configure_device( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Config precision option for device.""" + if user_input is not None: + self._update_device_options(user_input) + if self.devices_to_configure: + return await self.async_step_configure_device(user_input=None) + return await self._update_options() + + self.current_device, description = self.devices_to_configure.popitem() + data_schema = vol.Schema( + { + vol.Required( + OPTION_ENTRY_SENSOR_PRECISION, + default=self._get_current_setting( + description.id, OPTION_ENTRY_SENSOR_PRECISION, "temperature" + ), + ): vol.In(PRECISION_MAPPING_FAMILY_28), + } + ) + + return self.async_show_form( + step_id="configure_device", + data_schema=data_schema, + description_placeholders={"sensor_id": self.current_device}, + ) + + async def _update_options(self) -> FlowResult: + """Update config entry options.""" + return self.async_create_entry(title="", data=self.options) + + @staticmethod + def _get_device_long_name( + device_registry: DeviceRegistry, current_device: str + ) -> str: + device = device_registry.async_get_device({(DOMAIN, current_device)}) + if device and device.name_by_user: + return f"{device.name_by_user} ({current_device})" + return current_device + + def _get_current_configured_sensors(self) -> list[str]: + """Get current list of sensors that are configured.""" + configured_sensors = self.options.get(OPTION_ENTRY_DEVICE_OPTIONS) + if not configured_sensors: + return [] + return [ + device_name + for device_name, description in self.configurable_devices.items() + if description.id in configured_sensors + ] + + def _get_current_setting(self, device_id: str, setting: str, default: Any) -> Any: + """Get current value for setting.""" + if entry_device_options := self.options.get(OPTION_ENTRY_DEVICE_OPTIONS): + if device_options := entry_device_options.get(device_id): + return device_options.get(setting) + return default + + def _update_device_options(self, user_input: dict[str, Any]) -> None: + """Update the global config with the new options for the current device.""" + options: dict[str, dict[str, Any]] = self.options.setdefault( + OPTION_ENTRY_DEVICE_OPTIONS, {} + ) + + description = self.configurable_devices[self.current_device] + device_options: dict[str, Any] = options.setdefault(description.id, {}) + if description.family == "28": + device_options[OPTION_ENTRY_SENSOR_PRECISION] = user_input[ + OPTION_ENTRY_SENSOR_PRECISION + ] + + self.options.update({OPTION_ENTRY_DEVICE_OPTIONS: options}) diff --git a/homeassistant/components/onewire/const.py b/homeassistant/components/onewire/const.py index 285b2c51be5..7fce90cc012 100644 --- a/homeassistant/components/onewire/const.py +++ b/homeassistant/components/onewire/const.py @@ -37,6 +37,20 @@ DEVICE_SUPPORT_OWSERVER = { } DEVICE_SUPPORT_SYSBUS = ["10", "22", "28", "3B", "42"] +DEVICE_SUPPORT_OPTIONS = ["28"] + +PRECISION_MAPPING_FAMILY_28 = { + "temperature": "Default", + "temperature9": "9 Bits", + "temperature10": "10 Bits", + "temperature11": "11 Bits", + "temperature12": "12 Bits", +} + +OPTION_ENTRY_DEVICE_OPTIONS = "device_options" +OPTION_ENTRY_SENSOR_PRECISION = "precision" +INPUT_ENTRY_CLEAR_OPTIONS = "clear_device_options" +INPUT_ENTRY_DEVICE_SELECTION = "device_selection" MANUFACTURER_MAXIM = "Maxim Integrated" MANUFACTURER_HOBBYBOARDS = "Hobby Boards" diff --git a/homeassistant/components/onewire/sensor.py b/homeassistant/components/onewire/sensor.py index cac3630d473..759b1f9eccf 100644 --- a/homeassistant/components/onewire/sensor.py +++ b/homeassistant/components/onewire/sensor.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio +from collections.abc import Callable, Mapping import copy from dataclasses import dataclass import logging @@ -38,6 +39,9 @@ from .const import ( DEVICE_KEYS_0_3, DEVICE_KEYS_A_B, DOMAIN, + OPTION_ENTRY_DEVICE_OPTIONS, + OPTION_ENTRY_SENSOR_PRECISION, + PRECISION_MAPPING_FAMILY_28, READ_MODE_FLOAT, READ_MODE_INT, ) @@ -54,7 +58,24 @@ from .onewirehub import OneWireHub class OneWireSensorEntityDescription(OneWireEntityDescription, SensorEntityDescription): """Class describing OneWire sensor entities.""" - override_key: str | None = None + override_key: Callable[[str, Mapping[str, Any]], str] | None = None + + +def _get_sensor_precision_family_28(device_id: str, options: Mapping[str, Any]) -> str: + """Get precision form config flow options.""" + precision: str = ( + options.get(OPTION_ENTRY_DEVICE_OPTIONS, {}) + .get(device_id, {}) + .get(OPTION_ENTRY_SENSOR_PRECISION, "temperature") + ) + if precision in PRECISION_MAPPING_FAMILY_28: + return precision + _LOGGER.warning( + "Invalid sensor precision `%s` for device `%s`: reverting to default", + precision, + device_id, + ) + return "temperature" SIMPLE_TEMPERATURE_SENSOR_DESCRIPTION = OneWireSensorEntityDescription( @@ -185,7 +206,17 @@ DEVICE_SENSORS: dict[str, tuple[OneWireSensorEntityDescription, ...]] = { state_class=SensorStateClass.MEASUREMENT, ), ), - "28": (SIMPLE_TEMPERATURE_SENSOR_DESCRIPTION,), + "28": ( + OneWireSensorEntityDescription( + key="temperature", + device_class=SensorDeviceClass.TEMPERATURE, + name="Temperature", + native_unit_of_measurement=TEMP_CELSIUS, + override_key=_get_sensor_precision_family_28, + read_mode=READ_MODE_FLOAT, + state_class=SensorStateClass.MEASUREMENT, + ), + ), "30": ( SIMPLE_TEMPERATURE_SENSOR_DESCRIPTION, OneWireSensorEntityDescription( @@ -195,7 +226,7 @@ DEVICE_SENSORS: dict[str, tuple[OneWireSensorEntityDescription, ...]] = { name="Thermocouple temperature", native_unit_of_measurement=TEMP_CELSIUS, read_mode=READ_MODE_FLOAT, - override_key="typeK/temperature", + override_key=lambda d, o: "typeK/temperature", state_class=SensorStateClass.MEASUREMENT, ), OneWireSensorEntityDescription( @@ -352,13 +383,15 @@ async def async_setup_entry( """Set up 1-Wire platform.""" onewirehub = hass.data[DOMAIN][config_entry.entry_id] entities = await hass.async_add_executor_job( - get_entities, onewirehub, config_entry.data + get_entities, onewirehub, config_entry.data, config_entry.options ) async_add_entities(entities, True) def get_entities( - onewirehub: OneWireHub, config: MappingProxyType[str, Any] + onewirehub: OneWireHub, + config: MappingProxyType[str, Any], + options: MappingProxyType[str, Any], ) -> list[SensorEntity]: """Get a list of entities.""" if not onewirehub.devices: @@ -400,9 +433,12 @@ def get_entities( description.device_class = SensorDeviceClass.HUMIDITY description.native_unit_of_measurement = PERCENTAGE description.name = f"Wetness {s_id}" + override_key = None + if description.override_key: + override_key = description.override_key(device_id, options) device_file = os.path.join( os.path.split(device.path)[0], - description.override_key or description.key, + override_key or description.key, ) name = f"{device_id} {description.name}" entities.append( diff --git a/homeassistant/components/onewire/strings.json b/homeassistant/components/onewire/strings.json index 928907b319a..b685479d359 100644 --- a/homeassistant/components/onewire/strings.json +++ b/homeassistant/components/onewire/strings.json @@ -22,5 +22,32 @@ "title": "Set up 1-Wire" } } + }, + "options": { + "error": { + "device_not_selected": "Select devices to configure" + }, + "step": { + "ack_no_options": { + "data": {}, + "description": "There are no options for the SysBus implementation", + "title": "OneWire SysBus Options" + }, + "device_selection": { + "data": { + "clear_device_options": "Clear all device configurations", + "device_selection": "Select devices to configure" + }, + "description": "Select what configuration steps to process", + "title": "OneWire Device Options" + }, + "configure_device": { + "data": { + "precision": "Sensor Precision" + }, + "description": "Select sensor precision for {sensor_id}", + "title": "OneWire Sensor Precision" + } + } } } diff --git a/homeassistant/components/onewire/translations/bg.json b/homeassistant/components/onewire/translations/bg.json index 353a6523eed..e45a2db7197 100644 --- a/homeassistant/components/onewire/translations/bg.json +++ b/homeassistant/components/onewire/translations/bg.json @@ -19,5 +19,17 @@ } } } + }, + "options": { + "error": { + "device_not_selected": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430, \u043a\u043e\u0438\u0442\u043e \u0434\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u0442\u0435" + }, + "step": { + "device_selection": { + "data": { + "device_selection": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430, \u043a\u043e\u0438\u0442\u043e \u0434\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u0442\u0435" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/onewire/translations/ca.json b/homeassistant/components/onewire/translations/ca.json index 73c6cb0991e..b50e3abc1f6 100644 --- a/homeassistant/components/onewire/translations/ca.json +++ b/homeassistant/components/onewire/translations/ca.json @@ -22,5 +22,31 @@ "title": "Configuraci\u00f3 d'1-Wire" } } + }, + "options": { + "error": { + "device_not_selected": "Selecciona els dispositius a configurar" + }, + "step": { + "ack_no_options": { + "description": "No hi ha opcions per a la implementaci\u00f3 de SysBus", + "title": "Opcions SysBus de OneWire" + }, + "configure_device": { + "data": { + "precision": "Precisi\u00f3 del sensor" + }, + "description": "Selecciona la precisi\u00f3 del sensor {sensor_id}", + "title": "Precisi\u00f3 del sensor OneWire" + }, + "device_selection": { + "data": { + "clear_device_options": "Esborra totes les configuracions de dispositiu", + "device_selection": "Selecciona els dispositius a configurar" + }, + "description": "Seleccioneu els passos de configuraci\u00f3 a processar", + "title": "Opcions de dispositiu OneWire" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/onewire/translations/de.json b/homeassistant/components/onewire/translations/de.json index 2b0630db22c..a6347b12d75 100644 --- a/homeassistant/components/onewire/translations/de.json +++ b/homeassistant/components/onewire/translations/de.json @@ -22,5 +22,31 @@ "title": "1-Wire einrichten" } } + }, + "options": { + "error": { + "device_not_selected": "Zu konfigurierende Ger\u00e4te ausw\u00e4hlen" + }, + "step": { + "ack_no_options": { + "description": "Es gibt keine Optionen f\u00fcr die SysBus-Implementierung", + "title": "OneWire SysBus-Optionen" + }, + "configure_device": { + "data": { + "precision": "Sensorgenauigkeit" + }, + "description": "Sensorgenauigkeit f\u00fcr {sensor_id} ausw\u00e4hlen", + "title": "OneWire-Sensorpr\u00e4zision" + }, + "device_selection": { + "data": { + "clear_device_options": "Alle Ger\u00e4tekonfigurationen l\u00f6schen", + "device_selection": "Zu konfigurierende Ger\u00e4te ausw\u00e4hlen" + }, + "description": "W\u00e4hle die zu verarbeitenden Konfigurationsschritte aus", + "title": "OneWire-Ger\u00e4teoptionen" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/onewire/translations/el.json b/homeassistant/components/onewire/translations/el.json index 35b39838f5d..3d358decfe5 100644 --- a/homeassistant/components/onewire/translations/el.json +++ b/homeassistant/components/onewire/translations/el.json @@ -22,5 +22,31 @@ "title": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 1-Wire" } } + }, + "options": { + "error": { + "device_not_selected": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03b3\u03b9\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7" + }, + "step": { + "ack_no_options": { + "description": "\u0394\u03b5\u03bd \u03c5\u03c0\u03ac\u03c1\u03c7\u03bf\u03c5\u03bd \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd \u03c5\u03bb\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03c4\u03bf\u03c5 SysBus", + "title": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 SysBus OneWire" + }, + "configure_device": { + "data": { + "precision": "\u0391\u03ba\u03c1\u03af\u03b2\u03b5\u03b9\u03b1 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1" + }, + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03b1\u03ba\u03c1\u03af\u03b2\u03b5\u03b9\u03b1 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03b3\u03b9\u03b1 {sensor_id}", + "title": "\u0391\u03ba\u03c1\u03af\u03b2\u03b5\u03b9\u03b1 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 OneWire" + }, + "device_selection": { + "data": { + "clear_device_options": "\u0394\u03b9\u03b1\u03b3\u03c1\u03b1\u03c6\u03ae \u03cc\u03bb\u03c9\u03bd \u03c4\u03c9\u03bd \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c9\u03bd \u03c4\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2", + "device_selection": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03b3\u03b9\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7" + }, + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c0\u03bf\u03b9\u03b1 \u03b2\u03ae\u03bc\u03b1\u03c4\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7\u03c2 \u03b8\u03b1 \u03b5\u03c0\u03b5\u03be\u03b5\u03c1\u03b3\u03b1\u03c3\u03c4\u03b5\u03af\u03c4\u03b5", + "title": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 OneWire" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/onewire/translations/en.json b/homeassistant/components/onewire/translations/en.json index ff4d1cb53b9..df61b436f65 100644 --- a/homeassistant/components/onewire/translations/en.json +++ b/homeassistant/components/onewire/translations/en.json @@ -22,5 +22,31 @@ "title": "Set up 1-Wire" } } + }, + "options": { + "error": { + "device_not_selected": "Select devices to configure" + }, + "step": { + "ack_no_options": { + "description": "There are no options for the SysBus implementation", + "title": "OneWire SysBus Options" + }, + "configure_device": { + "data": { + "precision": "Sensor Precision" + }, + "description": "Select sensor precision for {sensor_id}", + "title": "OneWire Sensor Precision" + }, + "device_selection": { + "data": { + "clear_device_options": "Clear all device configurations", + "device_selection": "Select devices to configure" + }, + "description": "Select what configuration steps to process", + "title": "OneWire Device Options" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/onewire/translations/et.json b/homeassistant/components/onewire/translations/et.json index 175c26cebb5..bb00ce8fbbc 100644 --- a/homeassistant/components/onewire/translations/et.json +++ b/homeassistant/components/onewire/translations/et.json @@ -22,5 +22,31 @@ "title": "Seadista 1-wire sidumine" } } + }, + "options": { + "error": { + "device_not_selected": "Vali seadistatav seade" + }, + "step": { + "ack_no_options": { + "description": "SysBusi rakendamiseks pole v\u00f5imalusi", + "title": "OneWire SysBusi valikud" + }, + "configure_device": { + "data": { + "precision": "Anduri t\u00e4psus" + }, + "description": "Vali {sensor_id} anduri t\u00e4psus.", + "title": "OneWire anduri t\u00e4psus" + }, + "device_selection": { + "data": { + "clear_device_options": "Eemalda k\u00f5ik seadmete s\u00e4tted", + "device_selection": "Vali seadistatav seade" + }, + "description": "Vali milliseid konfiguratsioonietappe t\u00f6\u00f6delda", + "title": "OneWire'i seadme valikud" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/onewire/translations/fr.json b/homeassistant/components/onewire/translations/fr.json index 13a5438b1a9..0c75fb23ad1 100644 --- a/homeassistant/components/onewire/translations/fr.json +++ b/homeassistant/components/onewire/translations/fr.json @@ -22,5 +22,30 @@ "title": "Configurer 1-Wire" } } + }, + "options": { + "error": { + "device_not_selected": "S\u00e9lectionnez les appareils \u00e0 configurer" + }, + "step": { + "ack_no_options": { + "description": "Il n'y a pas d'option pour l'impl\u00e9mentation de SysBus" + }, + "configure_device": { + "data": { + "precision": "Pr\u00e9cision du capteur" + }, + "description": "S\u00e9lectionnez la pr\u00e9cision du capteur pour {sensor_id}", + "title": "Pr\u00e9cision du capteur OneWire" + }, + "device_selection": { + "data": { + "clear_device_options": "Effacer toutes les configurations de l'appareil", + "device_selection": "S\u00e9lectionnez les appareils \u00e0 configurer" + }, + "description": "S\u00e9lectionnez les \u00e9tapes de configuration \u00e0 traiter", + "title": "Options de l'appareil OneWire" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/onewire/translations/hu.json b/homeassistant/components/onewire/translations/hu.json index 4d53659788d..7034f2eaa29 100644 --- a/homeassistant/components/onewire/translations/hu.json +++ b/homeassistant/components/onewire/translations/hu.json @@ -22,5 +22,35 @@ "title": "A 1-Wire be\u00e1ll\u00edt\u00e1sa" } } + }, + "options": { + "error": { + "device_not_selected": "V\u00e1lassza ki a konfigur\u00e1lni k\u00edv\u00e1nt eszk\u00f6z\u00f6ket" + }, + "step": { + "ack_no_options": { + "data": { + "one": "\u00dcres", + "other": "\u00dcres" + }, + "description": "A SysBus implement\u00e1ci\u00f3j\u00e1nak nincsenek opci\u00f3i.", + "title": "OneWire SysBus opci\u00f3k" + }, + "configure_device": { + "data": { + "precision": "\u00c9rz\u00e9kel\u0151 pontoss\u00e1ga" + }, + "description": "V\u00e1lassza ki az \u00e9rz\u00e9kel\u0151 pontoss\u00e1g\u00e1t a k\u00f6vetkez\u0151h\u00f6z: {sensor_id}", + "title": "OneWire \u00e9rz\u00e9kel\u0151 pontoss\u00e1ga" + }, + "device_selection": { + "data": { + "clear_device_options": "Az \u00f6sszes eszk\u00f6zkonfigur\u00e1ci\u00f3 t\u00f6rl\u00e9se", + "device_selection": "V\u00e1lassza ki a konfigur\u00e1lni k\u00edv\u00e1nt eszk\u00f6z\u00f6ket" + }, + "description": "V\u00e1lassza ki a konfigur\u00e1ci\u00f3s l\u00e9p\u00e9seket", + "title": "OneWire eszk\u00f6z be\u00e1ll\u00edt\u00e1sai" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/onewire/translations/id.json b/homeassistant/components/onewire/translations/id.json index 5de8e2eee3e..a1cde1b35bd 100644 --- a/homeassistant/components/onewire/translations/id.json +++ b/homeassistant/components/onewire/translations/id.json @@ -22,5 +22,31 @@ "title": "Siapkan 1-Wire" } } + }, + "options": { + "error": { + "device_not_selected": "Pilih perangkat untuk dikonfigurasi" + }, + "step": { + "ack_no_options": { + "description": "Tidak ada opsi untuk implementasi SysBus", + "title": "Opsi OneWire SysBus" + }, + "configure_device": { + "data": { + "precision": "Presisi Sensor" + }, + "description": "Pilih presisi sensor untuk {sensor_id}", + "title": "Presisi Sensor OneWire" + }, + "device_selection": { + "data": { + "clear_device_options": "Hapus semua konfigurasi perangkat", + "device_selection": "Pilih perangkat untuk dikonfigurasi" + }, + "description": "Pilih langkah konfigurasi apa yang akan diproses", + "title": "Opsi Perangkat OneWire" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/onewire/translations/it.json b/homeassistant/components/onewire/translations/it.json index 118cee0de4a..ec2edddc275 100644 --- a/homeassistant/components/onewire/translations/it.json +++ b/homeassistant/components/onewire/translations/it.json @@ -22,5 +22,35 @@ "title": "Configurazione 1-Wire" } } + }, + "options": { + "error": { + "device_not_selected": "Seleziona i dispositivi da configurare" + }, + "step": { + "ack_no_options": { + "data": { + "one": "Vuoto", + "other": "Vuoti" + }, + "description": "Non ci sono opzioni per l'implementazione SysBus", + "title": "Opzioni SysBus OneWire" + }, + "configure_device": { + "data": { + "precision": "Precisione del sensore" + }, + "description": "Seleziona la precisione del sensore per {sensor_id}", + "title": "Precisione del sensore OneWire" + }, + "device_selection": { + "data": { + "clear_device_options": "Cancella tutte le configurazioni del dispositivo", + "device_selection": "Seleziona i dispositivi da configurare" + }, + "description": "Seleziona quali passaggi di configurazione elaborare", + "title": "Opzioni del dispositivo OneWire" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/onewire/translations/ja.json b/homeassistant/components/onewire/translations/ja.json index 2aa624fbfb6..c948a9567b0 100644 --- a/homeassistant/components/onewire/translations/ja.json +++ b/homeassistant/components/onewire/translations/ja.json @@ -22,5 +22,31 @@ "title": "1-Wire\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" } } + }, + "options": { + "error": { + "device_not_selected": "\u8a2d\u5b9a\u3059\u308b\u30c7\u30d0\u30a4\u30b9\u3092\u9078\u629e" + }, + "step": { + "ack_no_options": { + "description": "SysBus\u306e\u5b9f\u88c5\u306b\u95a2\u3059\u308b\u30aa\u30d7\u30b7\u30e7\u30f3\u306f\u3042\u308a\u307e\u305b\u3093", + "title": "OneWire SysBus\u306e\u30aa\u30d7\u30b7\u30e7\u30f3" + }, + "configure_device": { + "data": { + "precision": "\u30bb\u30f3\u30b5\u30fc\u306e\u7cbe\u5ea6" + }, + "description": "{sensor_id} \u306e\u30bb\u30f3\u30b5\u30fc\u7cbe\u5ea6\u3092\u9078\u629e", + "title": "OneWire\u30bb\u30f3\u30b5\u30fc\u306e\u7cbe\u5ea6" + }, + "device_selection": { + "data": { + "clear_device_options": "\u5168\u30c7\u30d0\u30a4\u30b9\u306e\u30b3\u30f3\u30d5\u30a3\u30ae\u30e5\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u30af\u30ea\u30a2\u3059\u308b", + "device_selection": "\u8a2d\u5b9a\u3059\u308b\u30c7\u30d0\u30a4\u30b9\u3092\u9078\u629e" + }, + "description": "\u3069\u306e\u3088\u3046\u306a\u51e6\u7406\u30b9\u30c6\u30c3\u30d7\u3092\u8e0f\u3080\u306e\u304b\u9078\u629e", + "title": "OneWire\u30c7\u30d0\u30a4\u30b9\u306e\u30aa\u30d7\u30b7\u30e7\u30f3" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/onewire/translations/nl.json b/homeassistant/components/onewire/translations/nl.json index 77ac79c1597..cc310900d92 100644 --- a/homeassistant/components/onewire/translations/nl.json +++ b/homeassistant/components/onewire/translations/nl.json @@ -22,5 +22,31 @@ "title": "Stel 1-Wire in" } } + }, + "options": { + "error": { + "device_not_selected": "Selecteer apparaten om te configureren" + }, + "step": { + "ack_no_options": { + "description": "Er zijn geen opties voor de SysBus implementatie", + "title": "OneWire SysBus opties" + }, + "configure_device": { + "data": { + "precision": "Sensor nauwkeurigheid" + }, + "description": "Selecteer sensor nauwkeurigheid voor {sensor_id}", + "title": "OneWire sensor nauwkeurigheid" + }, + "device_selection": { + "data": { + "clear_device_options": "Wis alle apparaatconfiguraties", + "device_selection": "Selecteer apparaten om te configureren" + }, + "description": "Selecteer welke configuratiestappen moeten worden doorlopen", + "title": "OneWire-apparaatopties" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/onewire/translations/no.json b/homeassistant/components/onewire/translations/no.json index b126349fb81..62bd0ac4634 100644 --- a/homeassistant/components/onewire/translations/no.json +++ b/homeassistant/components/onewire/translations/no.json @@ -22,5 +22,31 @@ "title": "Sett opp 1-Wire" } } + }, + "options": { + "error": { + "device_not_selected": "Velg enheter som skal konfigureres" + }, + "step": { + "ack_no_options": { + "description": "Det er ingen alternativer for SysBus-implementeringen", + "title": "OneWire SysBus-alternativer" + }, + "configure_device": { + "data": { + "precision": "Sensorpresisjon" + }, + "description": "Velg sensorpresisjon for {sensor_id}", + "title": "OneWire-sensorpresisjon" + }, + "device_selection": { + "data": { + "clear_device_options": "Fjern alle enhetskonfigurasjoner", + "device_selection": "Velg enheter som skal konfigureres" + }, + "description": "Velg hvilke konfigurasjonstrinn som skal behandles", + "title": "OneWire-enhetsalternativer" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/onewire/translations/pl.json b/homeassistant/components/onewire/translations/pl.json index e1200b968b8..fe0e1d54eb5 100644 --- a/homeassistant/components/onewire/translations/pl.json +++ b/homeassistant/components/onewire/translations/pl.json @@ -22,5 +22,31 @@ "title": "Konfiguracja 1-Wire" } } + }, + "options": { + "error": { + "device_not_selected": "Wybierz urz\u0105dzenia do skonfigurowania" + }, + "step": { + "ack_no_options": { + "description": "Nie ma opcji dla implementacji magistrali SysBus", + "title": "Opcje magistrali OneWire SysBus" + }, + "configure_device": { + "data": { + "precision": "Dok\u0142adno\u015b\u0107 sensora" + }, + "description": "Wybierz dok\u0142adno\u015b\u0107 dla sensora {sensor_id}", + "title": "Dok\u0142adno\u015b\u0107 sensora OneWire" + }, + "device_selection": { + "data": { + "clear_device_options": "Wyczy\u015b\u0107 wszystkie konfiguracje urz\u0105dze\u0144", + "device_selection": "Wybierz urz\u0105dzenia do skonfigurowania" + }, + "description": "Wybierz, kt\u00f3re kroki konfiguracji maj\u0105 by\u0107 przetworzone", + "title": "Opcje urz\u0105dze\u0144 OneWire" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/onewire/translations/pt-BR.json b/homeassistant/components/onewire/translations/pt-BR.json index 401452bcaf5..61405f5089a 100644 --- a/homeassistant/components/onewire/translations/pt-BR.json +++ b/homeassistant/components/onewire/translations/pt-BR.json @@ -22,5 +22,31 @@ "title": "Configurar 1-Wire" } } + }, + "options": { + "error": { + "device_not_selected": "Selecione os dispositivos para configurar" + }, + "step": { + "ack_no_options": { + "description": "N\u00e3o h\u00e1 op\u00e7\u00f5es para a implementa\u00e7\u00e3o do SysBus", + "title": "Op\u00e7\u00f5es de OneWire SysBus" + }, + "configure_device": { + "data": { + "precision": "Precis\u00e3o do Sensor" + }, + "description": "Selecione a precis\u00e3o do sensor para {sensor_id}", + "title": "Precis\u00e3o do Sensor OneWire" + }, + "device_selection": { + "data": { + "clear_device_options": "Limpar todas as configura\u00e7\u00f5es do dispositivo", + "device_selection": "Selecione os dispositivos para configurar" + }, + "description": "Selecione as etapas de configura\u00e7\u00e3o a serem processadas", + "title": "Op\u00e7\u00f5es de dispositivo OneWire" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/onewire/translations/ru.json b/homeassistant/components/onewire/translations/ru.json index 4459607604b..4bc24f85f19 100644 --- a/homeassistant/components/onewire/translations/ru.json +++ b/homeassistant/components/onewire/translations/ru.json @@ -22,5 +22,31 @@ "title": "1-Wire" } } + }, + "options": { + "error": { + "device_not_selected": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438" + }, + "step": { + "ack_no_options": { + "description": "\u0414\u043b\u044f \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438 \u0447\u0435\u0440\u0435\u0437 SysBus \u043e\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u044e\u0442 \u043a\u0430\u043a\u0438\u0435-\u043b\u0438\u0431\u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438.", + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 OneWire SysBus" + }, + "configure_device": { + "data": { + "precision": "\u0422\u043e\u0447\u043d\u043e\u0441\u0442\u044c \u0434\u0430\u0442\u0447\u0438\u043a\u0430" + }, + "description": "\u0423\u043a\u0430\u0436\u0438\u0442\u0435 \u0442\u0440\u0435\u0431\u0443\u0435\u043c\u0443\u044e \u0442\u043e\u0447\u043d\u043e\u0441\u0442\u044c \u0434\u043b\u044f \u0434\u0430\u0442\u0447\u0438\u043a\u0430 {sensor_id}.", + "title": "\u0422\u043e\u0447\u043d\u043e\u0441\u0442\u044c \u0434\u0430\u0442\u0447\u0438\u043a\u0430 OneWire" + }, + "device_selection": { + "data": { + "clear_device_options": "\u041e\u0447\u0438\u0441\u0442\u0438\u0442\u044c \u0432\u0441\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430", + "device_selection": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435, \u043a\u0430\u043a\u0438\u0435 \u0448\u0430\u0433\u0438 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c", + "title": "\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 OneWire" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/onewire/translations/tr.json b/homeassistant/components/onewire/translations/tr.json index e3ed5f7ce45..6fea23cfc76 100644 --- a/homeassistant/components/onewire/translations/tr.json +++ b/homeassistant/components/onewire/translations/tr.json @@ -22,5 +22,35 @@ "title": "1-Wire'\u0131 kurun" } } + }, + "options": { + "error": { + "device_not_selected": "Yap\u0131land\u0131r\u0131lacak cihazlar\u0131 se\u00e7in" + }, + "step": { + "ack_no_options": { + "data": { + "one": "Bo\u015f", + "other": "Bo\u015f" + }, + "description": "SysBus uygulamas\u0131 i\u00e7in se\u00e7enek yok", + "title": "OneWire SysBus Se\u00e7enekleri" + }, + "configure_device": { + "data": { + "precision": "Sens\u00f6r Hassasiyeti" + }, + "description": "{sensor_id} i\u00e7in sens\u00f6r hassasiyetini se\u00e7in", + "title": "OneWire Sens\u00f6r Hassasiyeti" + }, + "device_selection": { + "data": { + "clear_device_options": "T\u00fcm cihaz yap\u0131land\u0131rmalar\u0131n\u0131 temizle", + "device_selection": "Yap\u0131land\u0131r\u0131lacak cihazlar\u0131 se\u00e7in" + }, + "description": "Hangi yap\u0131land\u0131rma ad\u0131mlar\u0131n\u0131n i\u015flenece\u011fini se\u00e7in", + "title": "OneWire Cihaz Se\u00e7enekleri" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/onewire/translations/zh-Hant.json b/homeassistant/components/onewire/translations/zh-Hant.json index f9ee1b5e2c2..8b11692a3f5 100644 --- a/homeassistant/components/onewire/translations/zh-Hant.json +++ b/homeassistant/components/onewire/translations/zh-Hant.json @@ -17,10 +17,36 @@ }, "user": { "data": { - "type": "\u9023\u7dda\u985e\u578b" + "type": "\u9023\u7dda\u985e\u5225" }, "title": "\u8a2d\u5b9a 1-Wire" } } + }, + "options": { + "error": { + "device_not_selected": "\u9078\u64c7\u6240\u8981\u8a2d\u5b9a\u7684\u88dd\u7f6e" + }, + "step": { + "ack_no_options": { + "description": "SysBus implementation \u6c92\u6709\u8a2d\u5b9a\u9078\u9805", + "title": "OneWire SysBus \u9078\u9805" + }, + "configure_device": { + "data": { + "precision": "\u611f\u6e2c\u5668\u7cbe\u6e96\u5ea6" + }, + "description": "\u8a2d\u5b9a {sensor_id} \u50b3\u611f\u5668\u7cbe\u6e96\u5ea6", + "title": "OneWire \u611f\u6e2c\u5668\u7cbe\u6e96\u5ea6" + }, + "device_selection": { + "data": { + "clear_device_options": "\u6e05\u9664\u6240\u6709\u88dd\u7f6e\u8a2d\u5b9a", + "device_selection": "\u9078\u64c7\u6240\u8981\u8a2d\u5b9a\u7684\u88dd\u7f6e" + }, + "description": "\u9078\u64c7\u8a2d\u5b9a\u6b65\u9a5f\u9032\u884c", + "title": "OneWire \u88dd\u7f6e\u9078\u9805" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/onvif/services.yaml b/homeassistant/components/onvif/services.yaml index 83b2d112d2d..9d753b2fe77 100644 --- a/homeassistant/components/onvif/services.yaml +++ b/homeassistant/components/onvif/services.yaml @@ -12,24 +12,24 @@ ptz: selector: select: options: - - 'DOWN' - - 'UP' + - "DOWN" + - "UP" pan: name: Pan description: "Pan direction." selector: select: options: - - 'LEFT' - - 'RIGHT' + - "LEFT" + - "RIGHT" zoom: name: Zoom description: "Zoom." selector: select: options: - - 'ZOOM_IN' - - 'ZOOM_OUT' + - "ZOOM_IN" + - "ZOOM_OUT" distance: name: Distance description: "Distance coefficient. Sets how much PTZ should be executed in one request." @@ -71,8 +71,8 @@ ptz: selector: select: options: - - 'AbsoluteMove' - - 'ContinuousMove' - - 'GotoPreset' - - 'RelativeMove' - - 'Stop' + - "AbsoluteMove" + - "ContinuousMove" + - "GotoPreset" + - "RelativeMove" + - "Stop" diff --git a/homeassistant/components/onvif/translations/fr.json b/homeassistant/components/onvif/translations/fr.json index 4ea0ae566cc..ab772fcec2d 100644 --- a/homeassistant/components/onvif/translations/fr.json +++ b/homeassistant/components/onvif/translations/fr.json @@ -26,7 +26,7 @@ "port": "Port", "username": "Nom d'utilisateur" }, - "title": "Configurer le p\u00e9riph\u00e9rique ONVIF" + "title": "Configurer l'appareil ONVIF" }, "configure_profile": { "data": { diff --git a/homeassistant/components/onvif/translations/zh-Hant.json b/homeassistant/components/onvif/translations/zh-Hant.json index fa4d7d632da..23d0f2d12f0 100644 --- a/homeassistant/components/onvif/translations/zh-Hant.json +++ b/homeassistant/components/onvif/translations/zh-Hant.json @@ -37,7 +37,7 @@ }, "device": { "data": { - "host": "\u9078\u64c7\u6240\u63a2\u7d22\u5230\u7684 ONVIF \u88dd\u7f6e" + "host": "\u9078\u64c7\u6240\u767c\u73fe\u7684 ONVIF \u88dd\u7f6e" }, "title": "\u9078\u64c7 ONVIF \u88dd\u7f6e" }, diff --git a/homeassistant/components/open_meteo/weather.py b/homeassistant/components/open_meteo/weather.py index bb7170bb5da..40b52248a52 100644 --- a/homeassistant/components/open_meteo/weather.py +++ b/homeassistant/components/open_meteo/weather.py @@ -28,14 +28,18 @@ async def async_setup_entry( async_add_entities([OpenMeteoWeatherEntity(entry=entry, coordinator=coordinator)]) -class OpenMeteoWeatherEntity(CoordinatorEntity, WeatherEntity): +class OpenMeteoWeatherEntity( + CoordinatorEntity[DataUpdateCoordinator[OpenMeteoForecast]], WeatherEntity +): """Defines an Open-Meteo weather entity.""" _attr_temperature_unit = TEMP_CELSIUS - coordinator: DataUpdateCoordinator[OpenMeteoForecast] def __init__( - self, *, entry: ConfigEntry, coordinator: DataUpdateCoordinator + self, + *, + entry: ConfigEntry, + coordinator: DataUpdateCoordinator[OpenMeteoForecast], ) -> None: """Initialize Open-Meteo weather entity.""" super().__init__(coordinator=coordinator) diff --git a/homeassistant/components/opengarage/manifest.json b/homeassistant/components/opengarage/manifest.json index a76c7d16d74..d0d0437d993 100644 --- a/homeassistant/components/opengarage/manifest.json +++ b/homeassistant/components/opengarage/manifest.json @@ -2,13 +2,9 @@ "domain": "opengarage", "name": "OpenGarage", "documentation": "https://www.home-assistant.io/integrations/opengarage", - "codeowners": [ - "@danielhiversen" - ], - "requirements": [ - "open-garage==0.2.0" - ], + "codeowners": ["@danielhiversen"], + "requirements": ["open-garage==0.2.0"], "iot_class": "local_polling", "config_flow": true, "loggers": ["opengarage"] -} \ No newline at end of file +} diff --git a/homeassistant/components/opengarage/strings.json b/homeassistant/components/opengarage/strings.json index 20e90386b45..26f2f94ff9f 100644 --- a/homeassistant/components/opengarage/strings.json +++ b/homeassistant/components/opengarage/strings.json @@ -19,4 +19,4 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } } -} \ No newline at end of file +} diff --git a/homeassistant/components/opengarage/translations/fr.json b/homeassistant/components/opengarage/translations/fr.json index 571a2c68b22..3601e6d46bd 100644 --- a/homeassistant/components/opengarage/translations/fr.json +++ b/homeassistant/components/opengarage/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/openhome/media_player.py b/homeassistant/components/openhome/media_player.py index 1bda8b077a9..21c3c465775 100644 --- a/homeassistant/components/openhome/media_player.py +++ b/homeassistant/components/openhome/media_player.py @@ -10,9 +10,14 @@ from async_upnp_client.client import UpnpError from openhomedevice.device import Device import voluptuous as vol +from homeassistant.components import media_source from homeassistant.components.media_player import MediaPlayerEntity +from homeassistant.components.media_player.browse_media import ( + async_process_play_media_url, +) from homeassistant.components.media_player.const import ( MEDIA_TYPE_MUSIC, + SUPPORT_BROWSE_MEDIA, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, @@ -149,7 +154,10 @@ class OpenhomeDevice(MediaPlayerEntity): if self._source["type"] == "Radio": self._supported_features |= ( - SUPPORT_STOP | SUPPORT_PLAY | SUPPORT_PLAY_MEDIA + SUPPORT_STOP + | SUPPORT_PLAY + | SUPPORT_PLAY_MEDIA + | SUPPORT_BROWSE_MEDIA ) if self._source["type"] in ("Playlist", "Spotify"): self._supported_features |= ( @@ -158,6 +166,7 @@ class OpenhomeDevice(MediaPlayerEntity): | SUPPORT_PAUSE | SUPPORT_PLAY | SUPPORT_PLAY_MEDIA + | SUPPORT_BROWSE_MEDIA ) if self._in_standby: @@ -189,6 +198,11 @@ class OpenhomeDevice(MediaPlayerEntity): @catch_request_errors() async def async_play_media(self, media_type, media_id, **kwargs): """Send the play_media command to the media player.""" + if media_source.is_media_source_id(media_id): + media_type = MEDIA_TYPE_MUSIC + play_item = await media_source.async_resolve_media(self.hass, media_id) + media_id = play_item.url + if media_type != MEDIA_TYPE_MUSIC: _LOGGER.error( "Invalid media type %s. Only %s is supported", @@ -196,6 +210,9 @@ class OpenhomeDevice(MediaPlayerEntity): MEDIA_TYPE_MUSIC, ) return + + media_id = async_process_play_media_url(self.hass, media_id) + track_details = {"title": "Home Assistant", "uri": media_id} await self._device.play_media(track_details) @@ -320,3 +337,11 @@ class OpenhomeDevice(MediaPlayerEntity): async def async_mute_volume(self, mute): """Mute (true) or unmute (false) media player.""" await self._device.set_mute(mute) + + async def async_browse_media(self, media_content_type=None, media_content_id=None): + """Implement the websocket media browsing helper.""" + return await media_source.async_browse_media( + self.hass, + media_content_id, + content_filter=lambda item: item.media_content_type.startswith("audio/"), + ) diff --git a/homeassistant/components/opentherm_gw/services.yaml b/homeassistant/components/opentherm_gw/services.yaml index fc0b0011d7c..77ef501f9d8 100644 --- a/homeassistant/components/opentherm_gw/services.yaml +++ b/homeassistant/components/opentherm_gw/services.yaml @@ -85,7 +85,7 @@ set_control_setpoint: min: 0 max: 90 step: 0.1 - unit_of_measurement: '°' + unit_of_measurement: "°" set_hot_water_ovrd: name: Set hot water override @@ -136,7 +136,7 @@ set_hot_water_setpoint: min: 0 max: 90 step: 0.1 - unit_of_measurement: '°' + unit_of_measurement: "°" set_gpio_mode: name: Set gpio mode @@ -156,8 +156,8 @@ set_gpio_mode: selector: select: options: - - 'A' - - 'B' + - "A" + - "B" mode: name: Mode description: > @@ -187,12 +187,12 @@ set_led_mode: selector: select: options: - - 'A' - - 'B' - - 'C' - - 'D' - - 'E' - - 'F' + - "A" + - "B" + - "C" + - "D" + - "E" + - "F" mode: name: Mode description: > @@ -202,18 +202,18 @@ set_led_mode: selector: select: options: - - 'B' - - 'C' - - 'E' - - 'F' - - 'H' - - 'M' - - 'O' - - 'P' - - 'R' - - 'T' - - 'W' - - 'X' + - "B" + - "C" + - "E" + - "F" + - "H" + - "M" + - "O" + - "P" + - "R" + - "T" + - "W" + - "X" set_max_modulation: name: Set max modulation diff --git a/homeassistant/components/opentherm_gw/strings.json b/homeassistant/components/opentherm_gw/strings.json index ed9cf05cae8..f53ffeda6f6 100644 --- a/homeassistant/components/opentherm_gw/strings.json +++ b/homeassistant/components/opentherm_gw/strings.json @@ -2,7 +2,6 @@ "config": { "step": { "init": { - "title": "OpenTherm Gateway", "data": { "name": "[%key:common::config_flow::data::name%]", "device": "Path or URL", @@ -19,7 +18,6 @@ "options": { "step": { "init": { - "description": "Options for the OpenTherm Gateway", "data": { "floor_temperature": "Floor Temperature", "read_precision": "Read Precision", diff --git a/homeassistant/components/openuv/translations/fr.json b/homeassistant/components/openuv/translations/fr.json index 6acd3144a10..99a616427a7 100644 --- a/homeassistant/components/openuv/translations/fr.json +++ b/homeassistant/components/openuv/translations/fr.json @@ -4,7 +4,7 @@ "already_configured": "L'emplacement est d\u00e9j\u00e0 configur\u00e9" }, "error": { - "invalid_api_key": "Cl\u00e9 API invalide" + "invalid_api_key": "Cl\u00e9 d'API non valide" }, "step": { "user": { diff --git a/homeassistant/components/openweathermap/strings.json b/homeassistant/components/openweathermap/strings.json index 520cf1181ff..12d5c3e21f6 100644 --- a/homeassistant/components/openweathermap/strings.json +++ b/homeassistant/components/openweathermap/strings.json @@ -1,35 +1,34 @@ { - "config": { - "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_location%]" - }, - "error": { - "invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]", - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" - }, - "step": { - "user": { - "data": { - "api_key": "[%key:common::config_flow::data::api_key%]", - "language": "Language", - "latitude": "[%key:common::config_flow::data::latitude%]", - "longitude": "[%key:common::config_flow::data::longitude%]", - "mode": "[%key:common::config_flow::data::mode%]", - "name": "Name of the integration" - }, - "description": "Set up OpenWeatherMap integration. To generate API key go to https://openweathermap.org/appid", - "title": "OpenWeatherMap" - } - } + "config": { + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_location%]" }, - "options": { - "step": { - "init": { - "data": { - "language": "Language", - "mode": "[%key:common::config_flow::data::mode%]" - } - } - } + "error": { + "invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + }, + "step": { + "user": { + "data": { + "api_key": "[%key:common::config_flow::data::api_key%]", + "language": "Language", + "latitude": "[%key:common::config_flow::data::latitude%]", + "longitude": "[%key:common::config_flow::data::longitude%]", + "mode": "[%key:common::config_flow::data::mode%]", + "name": "Name" + }, + "description": "To generate API key go to https://openweathermap.org/appid" + } } + }, + "options": { + "step": { + "init": { + "data": { + "language": "Language", + "mode": "[%key:common::config_flow::data::mode%]" + } + } + } + } } diff --git a/homeassistant/components/openweathermap/translations/el.json b/homeassistant/components/openweathermap/translations/el.json index 1455ed91a53..90b876033f6 100644 --- a/homeassistant/components/openweathermap/translations/el.json +++ b/homeassistant/components/openweathermap/translations/el.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 OpenWeatherMap \u03b3\u03b9\u03b1 \u03b1\u03c5\u03c4\u03ad\u03c2 \u03c4\u03b9\u03c2 \u03c3\u03c5\u03bd\u03c4\u03b5\u03c4\u03b1\u03b3\u03bc\u03ad\u03bd\u03b5\u03c2 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af." + "already_configured": "\u0397 \u03c4\u03bf\u03c0\u03bf\u03b8\u03b5\u03c3\u03af\u03b1 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af" }, "error": { "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API OpenWeatherMap", + "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API", "language": "\u0393\u03bb\u03ce\u03c3\u03c3\u03b1", "latitude": "\u0393\u03b5\u03c9\u03b3\u03c1\u03b1\u03c6\u03b9\u03ba\u03cc \u03c0\u03bb\u03ac\u03c4\u03bf\u03c2", "longitude": "\u0393\u03b5\u03c9\u03b3\u03c1\u03b1\u03c6\u03b9\u03ba\u03cc \u03bc\u03ae\u03ba\u03bf\u03c2", diff --git a/homeassistant/components/openweathermap/translations/fr.json b/homeassistant/components/openweathermap/translations/fr.json index f8879a04d32..efa76d4d7e4 100644 --- a/homeassistant/components/openweathermap/translations/fr.json +++ b/homeassistant/components/openweathermap/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_api_key": "Cl\u00e9 API invalide" + "invalid_api_key": "Cl\u00e9 d'API non valide" }, "step": { "user": { diff --git a/homeassistant/components/orangepi_gpio/__init__.py b/homeassistant/components/orangepi_gpio/__init__.py deleted file mode 100644 index 1b0d7a46648..00000000000 --- a/homeassistant/components/orangepi_gpio/__init__.py +++ /dev/null @@ -1,59 +0,0 @@ -"""Support for controlling GPIO pins of a Orange Pi.""" -import logging - -from OPi import GPIO - -from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP -from homeassistant.core import HomeAssistant -from homeassistant.helpers.typing import ConfigType - -from .const import PIN_MODES - -_LOGGER = logging.getLogger(__name__) - -DOMAIN = "orangepi_gpio" - - -async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up the Orange Pi GPIO component.""" - _LOGGER.warning( - "The Orange Pi GPIO integration is deprecated and will be removed " - "in Home Assistant Core 2022.4; this integration is removed under " - "Architectural Decision Record 0019, more information can be found here: " - "https://github.com/home-assistant/architecture/blob/master/adr/0019-GPIO.md" - ) - - def cleanup_gpio(event): - """Stuff to do before stopping.""" - GPIO.cleanup() - - def prepare_gpio(event): - """Stuff to do when Home Assistant starts.""" - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, cleanup_gpio) - - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, prepare_gpio) - return True - - -def setup_mode(mode): - """Set GPIO pin mode.""" - _LOGGER.debug("Setting GPIO pin mode as %s", PIN_MODES[mode]) - GPIO.setmode(PIN_MODES[mode]) - - -def setup_input(port): - """Set up a GPIO as input.""" - _LOGGER.debug("Setting up GPIO pin %i as input", port) - GPIO.setup(port, GPIO.IN) - - -def read_input(port): - """Read a value from a GPIO.""" - _LOGGER.debug("Reading GPIO pin %i", port) - return GPIO.input(port) - - -def edge_detect(port, event_callback): - """Add detection for RISING and FALLING events.""" - _LOGGER.debug("Add callback for GPIO pin %i", port) - GPIO.add_event_detect(port, GPIO.BOTH, callback=event_callback) diff --git a/homeassistant/components/orangepi_gpio/binary_sensor.py b/homeassistant/components/orangepi_gpio/binary_sensor.py deleted file mode 100644 index 0be4ec48394..00000000000 --- a/homeassistant/components/orangepi_gpio/binary_sensor.py +++ /dev/null @@ -1,77 +0,0 @@ -"""Support for binary sensor using Orange Pi GPIO.""" -from __future__ import annotations - -from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity -from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType - -from . import edge_detect, read_input, setup_input, setup_mode -from .const import CONF_INVERT_LOGIC, CONF_PIN_MODE, CONF_PORTS, PORT_SCHEMA - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(PORT_SCHEMA) - - -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the Orange Pi GPIO platform.""" - binary_sensors = [] - invert_logic = config[CONF_INVERT_LOGIC] - pin_mode = config[CONF_PIN_MODE] - ports = config[CONF_PORTS] - - setup_mode(pin_mode) - - for port_num, port_name in ports.items(): - binary_sensors.append( - OPiGPIOBinarySensor(hass, port_name, port_num, invert_logic) - ) - async_add_entities(binary_sensors) - - -class OPiGPIOBinarySensor(BinarySensorEntity): - """Represent a binary sensor that uses Orange Pi GPIO.""" - - def __init__(self, hass, name, port, invert_logic): - """Initialize the Orange Pi binary sensor.""" - self._name = name - self._port = port - self._invert_logic = invert_logic - self._state = None - - async def async_added_to_hass(self): - """Run when entity about to be added to hass.""" - - def gpio_edge_listener(port): - """Update GPIO when edge change is detected.""" - self.schedule_update_ha_state(True) - - def setup_entity(): - setup_input(self._port) - edge_detect(self._port, gpio_edge_listener) - self.schedule_update_ha_state(True) - - await self.hass.async_add_executor_job(setup_entity) - - @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 - - def update(self): - """Update state with new GPIO data.""" - self._state = read_input(self._port) diff --git a/homeassistant/components/orangepi_gpio/const.py b/homeassistant/components/orangepi_gpio/const.py deleted file mode 100644 index f663fbc1ef4..00000000000 --- a/homeassistant/components/orangepi_gpio/const.py +++ /dev/null @@ -1,59 +0,0 @@ -"""Constants for Orange Pi GPIO.""" - -from nanopi import duo, neocore2 -from orangepi import ( - lite, - lite2, - one, - oneplus, - pc, - pc2, - pcplus, - pi3, - pi4, - pi4B, - plus2e, - prime, - r1, - winplus, - zero, - zeroplus, - zeroplus2, -) -import voluptuous as vol - -from homeassistant.helpers import config_validation as cv - -CONF_INVERT_LOGIC = "invert_logic" -CONF_PIN_MODE = "pin_mode" -CONF_PORTS = "ports" -DEFAULT_INVERT_LOGIC = False -PIN_MODES = { - "duo": duo.BOARD, - "lite": lite.BOARD, - "lite2": lite2.BOARD, - "neocore2": neocore2.BOARD, - "one": one.BOARD, - "oneplus": oneplus.BOARD, - "pc": pc.BOARD, - "pc2": pc2.BOARD, - "pcplus": pcplus.BOARD, - "pi3": pi3.BOARD, - "pi4": pi4.BOARD, - "pi4B": pi4B.BOARD, - "plus2e": plus2e.BOARD, - "prime": prime.BOARD, - "r1": r1.BOARD, - "winplus": winplus.BOARD, - "zero": zero.BOARD, - "zeroplus": zeroplus.BOARD, - "zeroplus2": zeroplus2.BOARD, -} - -_SENSORS_SCHEMA = vol.Schema({cv.positive_int: cv.string}) - -PORT_SCHEMA = { - vol.Required(CONF_PORTS): _SENSORS_SCHEMA, - vol.Required(CONF_PIN_MODE): vol.In(PIN_MODES.keys()), - vol.Optional(CONF_INVERT_LOGIC, default=DEFAULT_INVERT_LOGIC): cv.boolean, -} diff --git a/homeassistant/components/orangepi_gpio/manifest.json b/homeassistant/components/orangepi_gpio/manifest.json deleted file mode 100644 index b4cda33ee80..00000000000 --- a/homeassistant/components/orangepi_gpio/manifest.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "domain": "orangepi_gpio", - "name": "Orange Pi GPIO", - "documentation": "https://www.home-assistant.io/integrations/orangepi_gpio", - "requirements": ["OPi.GPIO==0.5.2"], - "codeowners": ["@pascallj"], - "iot_class": "local_push", - "loggers": ["OPi", "nanopi", "orangepi"] -} diff --git a/homeassistant/components/overkiz/alarm_control_panel.py b/homeassistant/components/overkiz/alarm_control_panel.py new file mode 100644 index 00000000000..059f2562544 --- /dev/null +++ b/homeassistant/components/overkiz/alarm_control_panel.py @@ -0,0 +1,311 @@ +"""Support for Overkiz alarm control panel.""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +from typing import Any, cast + +from pyoverkiz.enums import OverkizCommand, OverkizCommandParam, OverkizState +from pyoverkiz.enums.ui import UIWidget +from pyoverkiz.types import StateType as OverkizStateType + +from homeassistant.components.alarm_control_panel import ( + AlarmControlPanelEntity, + AlarmControlPanelEntityDescription, +) +from homeassistant.components.alarm_control_panel.const import ( + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_HOME, + SUPPORT_ALARM_ARM_NIGHT, + SUPPORT_ALARM_TRIGGER, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_CUSTOM_BYPASS, + STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_DISARMED, + STATE_ALARM_PENDING, + STATE_ALARM_TRIGGERED, + Platform, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityDescription +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import HomeAssistantOverkizData +from .const import DOMAIN +from .coordinator import OverkizDataUpdateCoordinator +from .entity import OverkizDescriptiveEntity + + +@dataclass +class OverkizAlarmDescriptionMixin: + """Define an entity description mixin for switch entities.""" + + supported_features: int + fn_state: Callable[[Callable[[str], OverkizStateType]], str] + + +@dataclass +class OverkizAlarmDescription( + AlarmControlPanelEntityDescription, OverkizAlarmDescriptionMixin +): + """Class to describe an Overkiz alarm control panel.""" + + alarm_disarm: str | None = None + alarm_disarm_args: OverkizStateType | list[OverkizStateType] = None + alarm_arm_home: str | None = None + alarm_arm_home_args: OverkizStateType | list[OverkizStateType] = None + alarm_arm_night: str | None = None + alarm_arm_night_args: OverkizStateType | list[OverkizStateType] = None + alarm_arm_away: str | None = None + alarm_arm_away_args: OverkizStateType | list[OverkizStateType] = None + alarm_trigger: str | None = None + alarm_trigger_args: OverkizStateType | list[OverkizStateType] = None + + +MAP_INTERNAL_STATUS_STATE: dict[str, str] = { + OverkizCommandParam.OFF: STATE_ALARM_DISARMED, + OverkizCommandParam.ZONE_1: STATE_ALARM_ARMED_HOME, + OverkizCommandParam.ZONE_2: STATE_ALARM_ARMED_NIGHT, + OverkizCommandParam.TOTAL: STATE_ALARM_ARMED_AWAY, +} + + +def _state_tsk_alarm_controller(select_state: Callable[[str], OverkizStateType]) -> str: + """Return the state of the device.""" + if ( + cast(str, select_state(OverkizState.INTERNAL_INTRUSION_DETECTED)) + == OverkizCommandParam.DETECTED + ): + return STATE_ALARM_TRIGGERED + + if cast(str, select_state(OverkizState.INTERNAL_CURRENT_ALARM_MODE)) != cast( + str, select_state(OverkizState.INTERNAL_TARGET_ALARM_MODE) + ): + return STATE_ALARM_PENDING + + return MAP_INTERNAL_STATUS_STATE[ + cast(str, select_state(OverkizState.INTERNAL_TARGET_ALARM_MODE)) + ] + + +MAP_CORE_ACTIVE_ZONES: dict[str, str] = { + OverkizCommandParam.A: STATE_ALARM_ARMED_HOME, + f"{OverkizCommandParam.A},{OverkizCommandParam.B}": STATE_ALARM_ARMED_NIGHT, + f"{OverkizCommandParam.A},{OverkizCommandParam.B},{OverkizCommandParam.C}": STATE_ALARM_ARMED_AWAY, +} + + +def _state_stateful_alarm_controller( + select_state: Callable[[str], OverkizStateType] +) -> str: + """Return the state of the device.""" + if state := cast(str, select_state(OverkizState.CORE_ACTIVE_ZONES)): + # The Stateful Alarm Controller has 3 zones with the following options: + # (A, B, C, A,B, B,C, A,C, A,B,C). Since it is not possible to map this to AlarmControlPanel entity, + # only the most important zones are mapped, other zones can only be disarmed. + if state in MAP_CORE_ACTIVE_ZONES: + return MAP_CORE_ACTIVE_ZONES[state] + + return STATE_ALARM_ARMED_CUSTOM_BYPASS + + return STATE_ALARM_DISARMED + + +MAP_MYFOX_STATUS_STATE: dict[str, str] = { + OverkizCommandParam.ARMED: STATE_ALARM_ARMED_AWAY, + OverkizCommandParam.DISARMED: STATE_ALARM_DISARMED, + OverkizCommandParam.PARTIAL: STATE_ALARM_ARMED_NIGHT, +} + + +def _state_myfox_alarm_controller( + select_state: Callable[[str], OverkizStateType] +) -> str: + """Return the state of the device.""" + if ( + cast(str, select_state(OverkizState.CORE_INTRUSION)) + == OverkizCommandParam.DETECTED + ): + return STATE_ALARM_TRIGGERED + + return MAP_MYFOX_STATUS_STATE[ + cast(str, select_state(OverkizState.MYFOX_ALARM_STATUS)) + ] + + +MAP_ARM_TYPE: dict[str, str] = { + OverkizCommandParam.DISARMED: STATE_ALARM_DISARMED, + OverkizCommandParam.ARMED_DAY: STATE_ALARM_ARMED_HOME, + OverkizCommandParam.ARMED_NIGHT: STATE_ALARM_ARMED_NIGHT, + OverkizCommandParam.ARMED: STATE_ALARM_ARMED_AWAY, +} + + +def _state_alarm_panel_controller( + select_state: Callable[[str], OverkizStateType] +) -> str: + """Return the state of the device.""" + return MAP_ARM_TYPE[ + cast(str, select_state(OverkizState.VERISURE_ALARM_PANEL_MAIN_ARM_TYPE)) + ] + + +ALARM_DESCRIPTIONS: list[OverkizAlarmDescription] = [ + # TSKAlarmController + # Disabled by default since all Overkiz hubs have this + # virtual device, but only a few users actually use this. + OverkizAlarmDescription( + key=UIWidget.TSKALARM_CONTROLLER, + entity_registry_enabled_default=False, + supported_features=( + SUPPORT_ALARM_ARM_AWAY + | SUPPORT_ALARM_ARM_HOME + | SUPPORT_ALARM_ARM_NIGHT + | SUPPORT_ALARM_TRIGGER + ), + fn_state=_state_tsk_alarm_controller, + alarm_disarm=OverkizCommand.ALARM_OFF, + alarm_arm_home=OverkizCommand.SET_TARGET_ALARM_MODE, + alarm_arm_home_args=OverkizCommandParam.PARTIAL_1, + alarm_arm_night=OverkizCommand.SET_TARGET_ALARM_MODE, + alarm_arm_night_args=OverkizCommandParam.PARTIAL_2, + alarm_arm_away=OverkizCommand.SET_TARGET_ALARM_MODE, + alarm_arm_away_args=OverkizCommandParam.TOTAL, + alarm_trigger=OverkizCommand.ALARM_ON, + ), + # StatefulAlarmController + OverkizAlarmDescription( + key=UIWidget.STATEFUL_ALARM_CONTROLLER, + supported_features=( + SUPPORT_ALARM_ARM_AWAY | SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_NIGHT + ), + fn_state=_state_stateful_alarm_controller, + alarm_disarm=OverkizCommand.ALARM_OFF, + alarm_arm_home=OverkizCommand.ALARM_ZONE_ON, + alarm_arm_home_args=OverkizCommandParam.A, + alarm_arm_night=OverkizCommand.ALARM_ZONE_ON, + alarm_arm_night_args=f"{OverkizCommandParam.A}, {OverkizCommandParam.B}", + alarm_arm_away=OverkizCommand.ALARM_ZONE_ON, + alarm_arm_away_args=f"{OverkizCommandParam.A},{OverkizCommandParam.B},{OverkizCommandParam.C}", + ), + # MyFoxAlarmController + OverkizAlarmDescription( + key=UIWidget.MY_FOX_ALARM_CONTROLLER, + supported_features=SUPPORT_ALARM_ARM_AWAY | SUPPORT_ALARM_ARM_NIGHT, + fn_state=_state_myfox_alarm_controller, + alarm_disarm=OverkizCommand.DISARM, + alarm_arm_night=OverkizCommand.PARTIAL, + alarm_arm_away=OverkizCommand.ARM, + ), + # AlarmPanelController + OverkizAlarmDescription( + key=UIWidget.ALARM_PANEL_CONTROLLER, + supported_features=( + SUPPORT_ALARM_ARM_AWAY | SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_NIGHT + ), + fn_state=_state_alarm_panel_controller, + alarm_disarm=OverkizCommand.DISARM, + alarm_arm_home=OverkizCommand.ARM_PARTIAL_DAY, + alarm_arm_night=OverkizCommand.ARM_PARTIAL_NIGHT, + alarm_arm_away=OverkizCommand.ARM, + ), +] + +SUPPORTED_DEVICES = {description.key: description for description in ALARM_DESCRIPTIONS} + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Overkiz alarm control panel from a config entry.""" + data: HomeAssistantOverkizData = hass.data[DOMAIN][entry.entry_id] + entities: list[OverkizAlarmControlPanel] = [] + + for device in data.platforms[Platform.ALARM_CONTROL_PANEL]: + if description := SUPPORTED_DEVICES.get(device.widget) or SUPPORTED_DEVICES.get( + device.ui_class + ): + entities.append( + OverkizAlarmControlPanel( + device.device_url, + data.coordinator, + description, + ) + ) + + async_add_entities(entities) + + +class OverkizAlarmControlPanel(OverkizDescriptiveEntity, AlarmControlPanelEntity): + """Representation of an Overkiz Alarm Control Panel.""" + + entity_description: OverkizAlarmDescription + + def __init__( + self, + device_url: str, + coordinator: OverkizDataUpdateCoordinator, + description: EntityDescription, + ) -> None: + """Initialize the device.""" + super().__init__(device_url, coordinator, description) + + self._attr_supported_features = self.entity_description.supported_features + + @property + def state(self) -> str: + """Return the state of the device.""" + return self.entity_description.fn_state(self.executor.select_state) + + async def async_alarm_disarm(self, code: str | None = None) -> None: + """Send disarm command.""" + assert self.entity_description.alarm_disarm + await self.async_execute_command( + self.entity_description.alarm_disarm, + self.entity_description.alarm_disarm_args, + ) + + async def async_alarm_arm_home(self, code: str | None = None) -> None: + """Send arm home command.""" + assert self.entity_description.alarm_arm_home + await self.async_execute_command( + self.entity_description.alarm_arm_home, + self.entity_description.alarm_arm_home_args, + ) + + async def async_alarm_arm_night(self, code: str | None = None) -> None: + """Send arm night command.""" + assert self.entity_description.alarm_arm_night + await self.async_execute_command( + self.entity_description.alarm_arm_night, + self.entity_description.alarm_arm_night_args, + ) + + async def async_alarm_arm_away(self, code: str | None = None) -> None: + """Send arm away command.""" + assert self.entity_description.alarm_arm_away + await self.async_execute_command( + self.entity_description.alarm_arm_away, + self.entity_description.alarm_arm_away_args, + ) + + async def async_alarm_trigger(self, code: str | None = None) -> None: + """Send alarm trigger command.""" + assert self.entity_description.alarm_trigger + await self.async_execute_command( + self.entity_description.alarm_trigger, + self.entity_description.alarm_trigger_args, + ) + + async def async_execute_command(self, command_name: str, args: Any) -> None: + """Execute device command in async context.""" + if args: + await self.executor.async_execute_command(command_name, args) + else: + await self.executor.async_execute_command(command_name) diff --git a/homeassistant/components/overkiz/config_flow.py b/homeassistant/components/overkiz/config_flow.py index f35941d6773..479e212e317 100644 --- a/homeassistant/components/overkiz/config_flow.py +++ b/homeassistant/components/overkiz/config_flow.py @@ -9,6 +9,7 @@ from pyoverkiz.const import SUPPORTED_SERVERS from pyoverkiz.exceptions import ( BadCredentialsException, MaintenanceException, + TooManyAttemptsBannedException, TooManyRequestsException, ) from pyoverkiz.models import obfuscate_id @@ -48,15 +49,16 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): server = SUPPORTED_SERVERS[user_input[CONF_HUB]] session = async_create_clientsession(self.hass) - async with OverkizClient( + client = OverkizClient( username=username, password=password, server=server, session=session - ) as client: - await client.login() + ) - # Set first gateway id as unique id - if gateways := await client.get_gateways(): - gateway_id = gateways[0].id - await self.async_set_unique_id(gateway_id) + await client.login(register_event_listener=False) + + # Set first gateway id as unique id + if gateways := await client.get_gateways(): + gateway_id = gateways[0].id + await self.async_set_unique_id(gateway_id) async def async_step_user( self, user_input: dict[str, Any] | None = None @@ -78,6 +80,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors["base"] = "cannot_connect" except MaintenanceException: errors["base"] = "server_in_maintenance" + except TooManyAttemptsBannedException: + errors["base"] = "too_many_attempts" except Exception as exception: # pylint: disable=broad-except errors["base"] = "unknown" LOGGER.exception(exception) @@ -136,8 +140,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self, discovery_info: zeroconf.ZeroconfServiceInfo ) -> FlowResult: """Handle ZeroConf discovery.""" - - # abort if we already have exactly this bridge id/host properties = discovery_info.properties gateway_id = properties["gateway_pin"] @@ -161,6 +163,10 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self.hass.config_entries.async_get_entry(self.context["entry_id"]), ) + self.context["title_placeholders"] = { + "gateway_id": self._config_entry.unique_id + } + self._default_user = self._config_entry.data[CONF_USERNAME] self._default_hub = self._config_entry.data[CONF_HUB] diff --git a/homeassistant/components/overkiz/const.py b/homeassistant/components/overkiz/const.py index 7f031ef3b6a..8488103a238 100644 --- a/homeassistant/components/overkiz/const.py +++ b/homeassistant/components/overkiz/const.py @@ -19,6 +19,7 @@ UPDATE_INTERVAL: Final = timedelta(seconds=30) UPDATE_INTERVAL_ALL_ASSUMED_STATE: Final = timedelta(minutes=60) PLATFORMS: list[Platform] = [ + Platform.ALARM_CONTROL_PANEL, Platform.BINARY_SENSOR, Platform.BUTTON, Platform.CLIMATE, @@ -27,6 +28,7 @@ PLATFORMS: list[Platform] = [ Platform.LOCK, Platform.NUMBER, Platform.SCENE, + Platform.SELECT, Platform.SENSOR, Platform.SIREN, Platform.SWITCH, @@ -58,15 +60,19 @@ OVERKIZ_DEVICE_TO_PLATFORM: dict[UIClass | UIWidget, Platform | None] = { UIClass.SWINGING_SHUTTER: Platform.COVER, UIClass.VENETIAN_BLIND: Platform.COVER, UIClass.WINDOW: Platform.COVER, + UIWidget.ALARM_PANEL_CONTROLLER: Platform.ALARM_CONTROL_PANEL, # widgetName, uiClass is Alarm (not supported) UIWidget.ATLANTIC_ELECTRICAL_HEATER: Platform.CLIMATE, # widgetName, uiClass is HeatingSystem (not supported) UIWidget.DOMESTIC_HOT_WATER_TANK: Platform.SWITCH, # widgetName, uiClass is WaterHeatingSystem (not supported) + UIWidget.MY_FOX_ALARM_CONTROLLER: Platform.ALARM_CONTROL_PANEL, # widgetName, uiClass is Alarm (not supported) UIWidget.MY_FOX_SECURITY_CAMERA: Platform.SWITCH, # widgetName, uiClass is Camera (not supported) UIWidget.RTD_INDOOR_SIREN: Platform.SWITCH, # widgetName, uiClass is Siren (not supported) UIWidget.RTD_OUTDOOR_SIREN: Platform.SWITCH, # widgetName, uiClass is Siren (not supported) UIWidget.RTS_GENERIC: Platform.COVER, # widgetName, uiClass is Generic (not supported) UIWidget.SIREN_STATUS: None, # widgetName, uiClass is Siren (siren) UIWidget.STATELESS_ALARM_CONTROLLER: Platform.SWITCH, # widgetName, uiClass is Alarm (not supported) + UIWidget.STATEFUL_ALARM_CONTROLLER: Platform.ALARM_CONTROL_PANEL, # widgetName, uiClass is Alarm (not supported) UIWidget.STATELESS_EXTERIOR_HEATING: Platform.SWITCH, # widgetName, uiClass is ExteriorHeatingSystem (not supported) + UIWidget.TSKALARM_CONTROLLER: Platform.ALARM_CONTROL_PANEL, # widgetName, uiClass is Alarm (not supported) } # Map Overkiz camelCase to Home Assistant snake_case for translation diff --git a/homeassistant/components/overkiz/diagnostics.py b/homeassistant/components/overkiz/diagnostics.py index c5596a9cf3d..77ca0227579 100644 --- a/homeassistant/components/overkiz/diagnostics.py +++ b/homeassistant/components/overkiz/diagnostics.py @@ -1,21 +1,59 @@ """Provides diagnostics for Overkiz.""" from __future__ import annotations -from typing import Any, cast +from typing import Any + +from pyoverkiz.obfuscate import obfuscate_id from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceEntry from . import HomeAssistantOverkizData -from .const import DOMAIN +from .const import CONF_HUB, DOMAIN async def async_get_config_entry_diagnostics( hass: HomeAssistant, entry: ConfigEntry ) -> dict[str, Any]: """Return diagnostics for a config entry.""" - data: HomeAssistantOverkizData = hass.data[DOMAIN][entry.entry_id] - client = data.coordinator.client - setup = await client.get_diagnostic_data() + entry_data: HomeAssistantOverkizData = hass.data[DOMAIN][entry.entry_id] + client = entry_data.coordinator.client - return cast(dict, setup) + data = { + "setup": await client.get_diagnostic_data(), + "server": entry.data[CONF_HUB], + "execution_history": [ + repr(execution) for execution in await client.get_execution_history() + ], + } + + return data + + +async def async_get_device_diagnostics( + hass: HomeAssistant, entry: ConfigEntry, device: DeviceEntry +) -> dict[str, Any]: + """Return diagnostics for a device entry.""" + entry_data: HomeAssistantOverkizData = hass.data[DOMAIN][entry.entry_id] + client = entry_data.coordinator.client + + device_url = min(device.identifiers)[1] + + data = { + "device": { + "controllable_name": device.hw_version, + "firmware": device.sw_version, + "device_url": obfuscate_id(device_url), + "model": device.model, + }, + "setup": await client.get_diagnostic_data(), + "server": entry.data[CONF_HUB], + "execution_history": [ + repr(execution) + for execution in await client.get_execution_history() + if any(command.device_url == device_url for command in execution.commands) + ], + } + + return data diff --git a/homeassistant/components/overkiz/entity.py b/homeassistant/components/overkiz/entity.py index 72ef793c2b4..7c42f415a65 100644 --- a/homeassistant/components/overkiz/entity.py +++ b/homeassistant/components/overkiz/entity.py @@ -16,11 +16,9 @@ from .coordinator import OverkizDataUpdateCoordinator from .executor import OverkizExecutor -class OverkizEntity(CoordinatorEntity): +class OverkizEntity(CoordinatorEntity[OverkizDataUpdateCoordinator]): """Representation of an Overkiz device entity.""" - coordinator: OverkizDataUpdateCoordinator - def __init__( self, device_url: str, coordinator: OverkizDataUpdateCoordinator ) -> None: diff --git a/homeassistant/components/overkiz/manifest.json b/homeassistant/components/overkiz/manifest.json index 5e8fe27e21e..3409c06be26 100644 --- a/homeassistant/components/overkiz/manifest.json +++ b/homeassistant/components/overkiz/manifest.json @@ -3,9 +3,7 @@ "name": "Overkiz (by Somfy)", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/overkiz", - "requirements": [ - "pyoverkiz==1.3.9" - ], + "requirements": ["pyoverkiz==1.3.14"], "zeroconf": [ { "type": "_kizbox._tcp.local.", @@ -18,17 +16,14 @@ "macaddress": "F8811A*" } ], - "codeowners": [ - "@imicknl", - "@vlebourl", - "@tetienne" - ], + "codeowners": ["@imicknl", "@vlebourl", "@tetienne"], "iot_class": "cloud_polling", - "loggers": [ - "boto3", - "botocore", - "pyhumps", - "pyoverkiz", - "s3transfer" - ] -} \ No newline at end of file + "loggers": ["boto3", "botocore", "pyhumps", "pyoverkiz", "s3transfer"], + "supported_brands": { + "cozytouch": "Atlantic Cozytouch", + "flexom": "Bouygues Flexom", + "hi_kumo": "Hitachi Hi Kumo", + "nexity": "Nexity Eugénie", + "rexel": "Rexel Energeasy Connect" + } +} diff --git a/homeassistant/components/overkiz/strings.json b/homeassistant/components/overkiz/strings.json index 87487d53c66..9c64311a73e 100644 --- a/homeassistant/components/overkiz/strings.json +++ b/homeassistant/components/overkiz/strings.json @@ -16,6 +16,7 @@ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", "server_in_maintenance": "Server is down for maintenance", + "too_many_attempts": "Too many attempts with an invalid token, temporarily banned", "too_many_requests": "Too many requests, try again later", "unknown": "[%key:common::config_flow::error::unknown%]" }, @@ -25,4 +26,4 @@ "reauth_wrong_account": "You can only reauthenticate this entry with the same Overkiz account and hub" } } -} \ No newline at end of file +} diff --git a/homeassistant/components/overkiz/strings.select.json b/homeassistant/components/overkiz/strings.select.json index 02a47e47a05..65abfc3d93b 100644 --- a/homeassistant/components/overkiz/strings.select.json +++ b/homeassistant/components/overkiz/strings.select.json @@ -1,13 +1,13 @@ { - "state": { - "overkiz__open_closed_pedestrian": { - "open": "Open", - "pedestrian": "Pedestrian", - "closed": "Closed" - }, - "overkiz__memorized_simple_volume": { - "highest": "Highest", - "standard": "Standard" - } + "state": { + "overkiz__open_closed_pedestrian": { + "open": "Open", + "pedestrian": "Pedestrian", + "closed": "Closed" + }, + "overkiz__memorized_simple_volume": { + "highest": "Highest", + "standard": "Standard" } -} \ No newline at end of file + } +} diff --git a/homeassistant/components/overkiz/strings.sensor.json b/homeassistant/components/overkiz/strings.sensor.json index 23b2256c51c..4df83bcad77 100644 --- a/homeassistant/components/overkiz/strings.sensor.json +++ b/homeassistant/components/overkiz/strings.sensor.json @@ -1,41 +1,41 @@ { - "state": { - "overkiz__battery": { - "full": "Full", - "low": "Low", - "normal": "Normal", - "verylow": "Very low" - }, - "overkiz__discrete_rssi_level": { - "good": "Good", - "low": "Low", - "normal": "Normal", - "verylow": "Very low" - }, - "overkiz__priority_lock_originator": { - "lsc": "LSC", - "saac": "SAAC", - "sfc": "SFC", - "ups": "UPS", - "external_gateway": "External gateway", - "local_user": "Local user", - "myself": "Myself", - "rain": "Rain", - "security": "Security", - "temperature": "Temperature", - "timer": "Timer", - "user": "User", - "wind": "Wind" - }, - "overkiz__sensor_room": { - "clean": "Clean", - "dirty": "Dirty" - }, - "overkiz__sensor_defect": { - "dead": "Dead", - "low_battery": "Low battery", - "maintenance_required": "Maintenance required", - "no_defect": "No defect" - } + "state": { + "overkiz__battery": { + "full": "Full", + "low": "Low", + "normal": "Normal", + "verylow": "Very low" + }, + "overkiz__discrete_rssi_level": { + "good": "Good", + "low": "Low", + "normal": "Normal", + "verylow": "Very low" + }, + "overkiz__priority_lock_originator": { + "lsc": "LSC", + "saac": "SAAC", + "sfc": "SFC", + "ups": "UPS", + "external_gateway": "External gateway", + "local_user": "Local user", + "myself": "Myself", + "rain": "Rain", + "security": "Security", + "temperature": "Temperature", + "timer": "Timer", + "user": "User", + "wind": "Wind" + }, + "overkiz__sensor_room": { + "clean": "Clean", + "dirty": "Dirty" + }, + "overkiz__sensor_defect": { + "dead": "Dead", + "low_battery": "Low battery", + "maintenance_required": "Maintenance required", + "no_defect": "No defect" } -} \ No newline at end of file + } +} diff --git a/homeassistant/components/overkiz/switch.py b/homeassistant/components/overkiz/switch.py index 8fd38816bcd..a198964b5d3 100644 --- a/homeassistant/components/overkiz/switch.py +++ b/homeassistant/components/overkiz/switch.py @@ -1,7 +1,7 @@ """Support for Overkiz switches.""" from __future__ import annotations -from collections.abc import Awaitable, Callable +from collections.abc import Callable from dataclasses import dataclass from typing import Any @@ -29,8 +29,8 @@ from .entity import OverkizDescriptiveEntity class OverkizSwitchDescriptionMixin: """Define an entity description mixin for switch entities.""" - turn_on: Callable[[Callable[..., Awaitable[None]]], Awaitable[None]] - turn_off: Callable[[Callable[..., Awaitable[None]]], Awaitable[None]] + turn_on: str + turn_off: str @dataclass @@ -38,17 +38,17 @@ class OverkizSwitchDescription(SwitchEntityDescription, OverkizSwitchDescription """Class to describe an Overkiz switch.""" is_on: Callable[[Callable[[str], OverkizStateType]], bool] | None = None + turn_on_args: OverkizStateType | list[OverkizStateType] | None = None + turn_off_args: OverkizStateType | list[OverkizStateType] | None = None SWITCH_DESCRIPTIONS: list[OverkizSwitchDescription] = [ OverkizSwitchDescription( key=UIWidget.DOMESTIC_HOT_WATER_TANK, - turn_on=lambda execute_command: execute_command( - OverkizCommand.SET_FORCE_HEATING, OverkizCommandParam.ON - ), - turn_off=lambda execute_command: execute_command( - OverkizCommand.SET_FORCE_HEATING, OverkizCommandParam.OFF - ), + turn_on=OverkizCommand.SET_FORCE_HEATING, + turn_on_args=OverkizCommandParam.ON, + turn_off=OverkizCommand.SET_FORCE_HEATING, + turn_off_args=OverkizCommandParam.OFF, is_on=lambda select_state: ( select_state(OverkizState.IO_FORCE_HEATING) == OverkizCommandParam.ON ), @@ -56,8 +56,8 @@ SWITCH_DESCRIPTIONS: list[OverkizSwitchDescription] = [ ), OverkizSwitchDescription( key=UIClass.ON_OFF, - turn_on=lambda execute_command: execute_command(OverkizCommand.ON), - turn_off=lambda execute_command: execute_command(OverkizCommand.OFF), + turn_on=OverkizCommand.ON, + turn_off=OverkizCommand.OFF, is_on=lambda select_state: ( select_state(OverkizState.CORE_ON_OFF) == OverkizCommandParam.ON ), @@ -65,8 +65,8 @@ SWITCH_DESCRIPTIONS: list[OverkizSwitchDescription] = [ ), OverkizSwitchDescription( key=UIClass.SWIMMING_POOL, - turn_on=lambda execute_command: execute_command(OverkizCommand.ON), - turn_off=lambda execute_command: execute_command(OverkizCommand.OFF), + turn_on=OverkizCommand.ON, + turn_off=OverkizCommand.OFF, is_on=lambda select_state: ( select_state(OverkizState.CORE_ON_OFF) == OverkizCommandParam.ON ), @@ -74,33 +74,33 @@ SWITCH_DESCRIPTIONS: list[OverkizSwitchDescription] = [ ), OverkizSwitchDescription( key=UIWidget.RTD_INDOOR_SIREN, - turn_on=lambda execute_command: execute_command(OverkizCommand.ON), - turn_off=lambda execute_command: execute_command(OverkizCommand.OFF), + turn_on=OverkizCommand.ON, + turn_off=OverkizCommand.OFF, icon="mdi:bell", ), OverkizSwitchDescription( key=UIWidget.RTD_OUTDOOR_SIREN, - turn_on=lambda execute_command: execute_command(OverkizCommand.ON), - turn_off=lambda execute_command: execute_command(OverkizCommand.OFF), + turn_on=OverkizCommand.ON, + turn_off=OverkizCommand.OFF, icon="mdi:bell", ), OverkizSwitchDescription( key=UIWidget.STATELESS_ALARM_CONTROLLER, - turn_on=lambda execute_command: execute_command(OverkizCommand.ALARM_ON), - turn_off=lambda execute_command: execute_command(OverkizCommand.ALARM_OFF), + turn_on=OverkizCommand.ALARM_ON, + turn_off=OverkizCommand.ALARM_OFF, icon="mdi:shield-lock", ), OverkizSwitchDescription( key=UIWidget.STATELESS_EXTERIOR_HEATING, - turn_on=lambda execute_command: execute_command(OverkizCommand.ON), - turn_off=lambda execute_command: execute_command(OverkizCommand.OFF), + turn_on=OverkizCommand.ON, + turn_off=OverkizCommand.OFF, icon="mdi:radiator", ), OverkizSwitchDescription( key=UIWidget.MY_FOX_SECURITY_CAMERA, name="Camera Shutter", - turn_on=lambda execute_command: execute_command(OverkizCommand.OPEN), - turn_off=lambda execute_command: execute_command(OverkizCommand.CLOSE), + turn_on=OverkizCommand.OPEN, + turn_off=OverkizCommand.CLOSE, icon="mdi:camera-lock", is_on=lambda select_state: ( select_state(OverkizState.MYFOX_SHUTTER_STATUS) @@ -154,8 +154,14 @@ class OverkizSwitch(OverkizDescriptiveEntity, SwitchEntity): async def async_turn_on(self, **kwargs: Any) -> None: """Turn the entity on.""" - await self.entity_description.turn_on(self.executor.async_execute_command) + await self.executor.async_execute_command( + self.entity_description.turn_on, + self.entity_description.turn_on_args, + ) async def async_turn_off(self, **kwargs: Any) -> None: """Turn the entity off.""" - await self.entity_description.turn_off(self.executor.async_execute_command) + await self.executor.async_execute_command( + self.entity_description.turn_off, + self.entity_description.turn_off_args, + ) diff --git a/homeassistant/components/overkiz/translations/el.json b/homeassistant/components/overkiz/translations/el.json index 2ba577de4c5..9f7ad60cb09 100644 --- a/homeassistant/components/overkiz/translations/el.json +++ b/homeassistant/components/overkiz/translations/el.json @@ -9,7 +9,7 @@ "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\u03bf\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2", "server_in_maintenance": "\u039f \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae\u03c2 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03ba\u03c4\u03cc\u03c2 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1\u03c2 \u03b3\u03b9\u03b1 \u03c3\u03c5\u03bd\u03c4\u03ae\u03c1\u03b7\u03c3\u03b7", - "too_many_requests": "\u03a0\u03ac\u03c1\u03b1 \u03c0\u03bf\u03bb\u03bb\u03ac \u03b1\u03b9\u03c4\u03ae\u03bc\u03b1\u03c4\u03b1, \u03c0\u03c1\u03bf\u03c3\u03c0\u03b1\u03b8\u03ae\u03c3\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac \u03b1\u03c1\u03b3\u03cc\u03c4\u03b5\u03c1\u03b1.", + "too_many_requests": "\u03a0\u03ac\u03c1\u03b1 \u03c0\u03bf\u03bb\u03bb\u03ac \u03b1\u03b9\u03c4\u03ae\u03bc\u03b1\u03c4\u03b1, \u03c0\u03c1\u03bf\u03c3\u03c0\u03b1\u03b8\u03ae\u03c3\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac \u03b1\u03c1\u03b3\u03cc\u03c4\u03b5\u03c1\u03b1", "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" }, "flow_title": "\u03a0\u03cd\u03bb\u03b7: {gateway_id}", diff --git a/homeassistant/components/overkiz/translations/en.json b/homeassistant/components/overkiz/translations/en.json index c9551aa555c..9e24a9d3cb3 100644 --- a/homeassistant/components/overkiz/translations/en.json +++ b/homeassistant/components/overkiz/translations/en.json @@ -9,6 +9,7 @@ "cannot_connect": "Failed to connect", "invalid_auth": "Invalid authentication", "server_in_maintenance": "Server is down for maintenance", + "too_many_attempts": "Too many attempts with an invalid token, temporarily banned", "too_many_requests": "Too many requests, try again later", "unknown": "Unexpected error" }, diff --git a/homeassistant/components/overkiz/translations/fr.json b/homeassistant/components/overkiz/translations/fr.json index 790aafc0796..f6a56db80a5 100644 --- a/homeassistant/components/overkiz/translations/fr.json +++ b/homeassistant/components/overkiz/translations/fr.json @@ -7,17 +7,17 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "server_in_maintenance": "Le serveur est ferm\u00e9 pour maintenance", "too_many_requests": "Trop de demandes, r\u00e9essayez plus tard.", "unknown": "Erreur inattendue" }, - "flow_title": "Passerelle : {gateway_id}", + "flow_title": "Passerelle\u00a0: {gateway_id}", "step": { "user": { "data": { "host": "H\u00f4te", - "hub": "Moyeu", + "hub": "Hub", "password": "Mot de passe", "username": "Nom d'utilisateur" }, diff --git a/homeassistant/components/ovo_energy/strings.json b/homeassistant/components/ovo_energy/strings.json index 87605e2b3c4..69392295899 100644 --- a/homeassistant/components/ovo_energy/strings.json +++ b/homeassistant/components/ovo_energy/strings.json @@ -1,27 +1,27 @@ { - "config": { - "flow_title": "{username}", - "error": { - "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", - "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + "config": { + "flow_title": "{username}", + "error": { + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + }, + "step": { + "user": { + "data": { + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" }, - "step": { - "user": { - "data": { - "username": "[%key:common::config_flow::data::username%]", - "password": "[%key:common::config_flow::data::password%]" - }, - "description": "Set up an OVO Energy instance to access your energy usage.", - "title": "Add OVO Energy Account" - }, - "reauth": { - "data": { - "password": "[%key:common::config_flow::data::password%]" - }, - "description": "Authentication failed for OVO Energy. Please enter your current credentials.", - "title": "Reauthentication" - } - } + "description": "Set up an OVO Energy instance to access your energy usage.", + "title": "Add OVO Energy Account" + }, + "reauth": { + "data": { + "password": "[%key:common::config_flow::data::password%]" + }, + "description": "Authentication failed for OVO Energy. Please enter your current credentials.", + "title": "Reauthentication" + } } + } } diff --git a/homeassistant/components/ovo_energy/translations/fr.json b/homeassistant/components/ovo_energy/translations/fr.json index 2c2482d9f5f..3a915229883 100644 --- a/homeassistant/components/ovo_energy/translations/fr.json +++ b/homeassistant/components/ovo_energy/translations/fr.json @@ -3,7 +3,7 @@ "error": { "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9", "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide" + "invalid_auth": "Authentification non valide" }, "flow_title": "{username}", "step": { diff --git a/homeassistant/components/owntracks/__init__.py b/homeassistant/components/owntracks/__init__.py index 1ac454d0c8c..a9f89d26238 100644 --- a/homeassistant/components/owntracks/__init__.py +++ b/homeassistant/components/owntracks/__init__.py @@ -18,6 +18,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.typing import ConfigType from homeassistant.setup import async_when_setup @@ -101,8 +102,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.config_entries.async_setup_platforms(entry, PLATFORMS) - hass.data[DOMAIN]["unsub"] = hass.helpers.dispatcher.async_dispatcher_connect( - DOMAIN, async_handle_message + hass.data[DOMAIN]["unsub"] = async_dispatcher_connect( + hass, DOMAIN, async_handle_message ) return True diff --git a/homeassistant/components/owntracks/helper.py b/homeassistant/components/owntracks/helper.py index b6ed307112c..a9499059ba0 100644 --- a/homeassistant/components/owntracks/helper.py +++ b/homeassistant/components/owntracks/helper.py @@ -2,7 +2,7 @@ try: import nacl except ImportError: - nacl = None + nacl = None # type: ignore[assignment] def supports_encryption() -> bool: diff --git a/homeassistant/components/owntracks/manifest.json b/homeassistant/components/owntracks/manifest.json index 1b502481764..4a84f6a706c 100644 --- a/homeassistant/components/owntracks/manifest.json +++ b/homeassistant/components/owntracks/manifest.json @@ -3,7 +3,7 @@ "name": "OwnTracks", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/owntracks", - "requirements": ["PyNaCl==1.4.0"], + "requirements": ["PyNaCl==1.5.0"], "dependencies": ["webhook"], "after_dependencies": ["mqtt", "cloud"], "codeowners": [], diff --git a/homeassistant/components/owntracks/translations/fr.json b/homeassistant/components/owntracks/translations/fr.json index cecdab86436..9d651c66e94 100644 --- a/homeassistant/components/owntracks/translations/fr.json +++ b/homeassistant/components/owntracks/translations/fr.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "cloud_not_connected": "Pas connect\u00e9 \u00e0 Home Assistant Cloud", + "cloud_not_connected": "Non connect\u00e9 \u00e0 Home Assistant Cloud.", "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." }, "create_entry": { - "default": "\n\nSous Android, ouvrez [l'application OwnTracks]({android_url}), acc\u00e9dez \u00e0 Pr\u00e9f\u00e9rences - > Connexion. Modifiez les param\u00e8tres suivants: \n - Mode: HTTP priv\u00e9 \n - H\u00f4te: {webhook_url} \n - Identification: \n - Nom d'utilisateur: `''` \n - ID de p\u00e9riph\u00e9rique: `''` \n\n Sur iOS, ouvrez [l'application OwnTracks]({ios_url}), appuyez sur l'ic\u00f4ne (i) en haut \u00e0 gauche - > param\u00e8tres. Modifiez les param\u00e8tres suivants: \n - Mode: HTTP \n - URL: {webhook_url} \n - Activer l'authentification \n - ID utilisateur: `''` \n\n {secret} \n \n Voir [la documentation]({docs_url}) pour plus d'informations." + "default": "\n\nSous Android, ouvrez [l'application OwnTracks]({android_url}), acc\u00e9dez \u00e0 Pr\u00e9f\u00e9rences -> Connexion. Modifiez les param\u00e8tres suivants\u00a0:\n - Mode\u00a0: HTTP priv\u00e9 \n - H\u00f4te\u00a0: {webhook_url} \n - Identification\u00a0: \n - Nom d'utilisateur\u00a0: `''` \n - ID de p\u00e9riph\u00e9rique\u00a0: `''` \n\nSous iOS, ouvrez [l'application OwnTracks]({ios_url}), appuyez sur l'ic\u00f4ne (i) en haut \u00e0 gauche -> param\u00e8tres. Modifiez les param\u00e8tres suivants\u00a0:\n - Mode\u00a0: HTTP \n - URL\u00a0: {webhook_url} \n - Activer l'authentification \n - ID utilisateur\u00a0: `''` \n\n{secret}\n \nConsultez [la documentation]({docs_url}) pour plus d'informations." }, "step": { "user": { diff --git a/homeassistant/components/ozw/__init__.py b/homeassistant/components/ozw/__init__.py deleted file mode 100644 index 6aa3bbbcf1c..00000000000 --- a/homeassistant/components/ozw/__init__.py +++ /dev/null @@ -1,418 +0,0 @@ -"""The ozw integration.""" -import asyncio -from contextlib import suppress -import json -import logging - -from openzwavemqtt import OZWManager, OZWOptions -from openzwavemqtt.const import ( - EVENT_INSTANCE_EVENT, - EVENT_NODE_ADDED, - EVENT_NODE_CHANGED, - EVENT_NODE_REMOVED, - EVENT_VALUE_ADDED, - EVENT_VALUE_CHANGED, - EVENT_VALUE_REMOVED, - CommandClass, - ValueType, -) -from openzwavemqtt.models.node import OZWNode -from openzwavemqtt.models.value import OZWValue -from openzwavemqtt.util.mqtt_client import MQTTClient - -from homeassistant.components import hassio, mqtt -from homeassistant.components.hassio.handler import HassioAPIError -from homeassistant.config_entries import ConfigEntry, ConfigEntryState -from homeassistant.const import EVENT_HOMEASSISTANT_STOP -from homeassistant.core import HomeAssistant, callback -from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers.device_registry import async_get_registry as get_dev_reg -from homeassistant.helpers.dispatcher import async_dispatcher_send - -from . import const -from .const import ( - CONF_INTEGRATION_CREATED_ADDON, - CONF_USE_ADDON, - DATA_UNSUBSCRIBE, - DOMAIN, - MANAGER, - NODES_VALUES, - PLATFORMS, - TOPIC_OPENZWAVE, -) -from .discovery import DISCOVERY_SCHEMAS, check_node_schema, check_value_schema -from .entity import ( - ZWaveDeviceEntityValues, - create_device_id, - create_device_name, - create_value_id, -) -from .services import ZWaveServices -from .websocket_api import async_register_api - -_LOGGER = logging.getLogger(__name__) - -DATA_DEVICES = "zwave-mqtt-devices" -DATA_STOP_MQTT_CLIENT = "ozw_stop_mqtt_client" - - -async def async_setup_entry( # noqa: C901 - hass: HomeAssistant, entry: ConfigEntry -) -> bool: - """Set up ozw from a config entry.""" - hass.data.setdefault(DOMAIN, {}) - ozw_data = hass.data[DOMAIN][entry.entry_id] = {} - ozw_data[DATA_UNSUBSCRIBE] = [] - - data_nodes = {} - hass.data[DOMAIN][NODES_VALUES] = data_values = {} - removed_nodes = [] - manager_options = {"topic_prefix": f"{TOPIC_OPENZWAVE}/"} - - if entry.unique_id is None: - hass.config_entries.async_update_entry(entry, unique_id=DOMAIN) - - if entry.data.get(CONF_USE_ADDON): - # Do not use MQTT integration. Use own MQTT client. - # Retrieve discovery info from the OpenZWave add-on. - discovery_info = await hassio.async_get_addon_discovery_info(hass, "core_zwave") - - if not discovery_info: - _LOGGER.error("Failed to get add-on discovery info") - raise ConfigEntryNotReady - - discovery_info_config = discovery_info["config"] - - host = discovery_info_config["host"] - port = discovery_info_config["port"] - username = discovery_info_config["username"] - password = discovery_info_config["password"] - mqtt_client = MQTTClient(host, port, username=username, password=password) - manager_options["send_message"] = mqtt_client.send_message - - else: - mqtt_entries = hass.config_entries.async_entries("mqtt") - if not mqtt_entries or mqtt_entries[0].state is not ConfigEntryState.LOADED: - _LOGGER.error("MQTT integration is not set up") - return False - - mqtt_entry = mqtt_entries[0] # MQTT integration only has one entry. - - @callback - def send_message(topic, payload): - if mqtt_entry.state is not ConfigEntryState.LOADED: - _LOGGER.error("MQTT integration is not set up") - return - - hass.async_create_task(mqtt.async_publish(hass, topic, json.dumps(payload))) - - manager_options["send_message"] = send_message - - options = OZWOptions(**manager_options) - manager = OZWManager(options) - - hass.data[DOMAIN][MANAGER] = manager - - @callback - def async_node_added(node): - # Caution: This is also called on (re)start. - _LOGGER.debug("[NODE ADDED] node_id: %s", node.id) - data_nodes[node.id] = node - if node.id not in data_values: - data_values[node.id] = [] - - @callback - def async_node_changed(node): - _LOGGER.debug("[NODE CHANGED] node_id: %s", node.id) - data_nodes[node.id] = node - # notify devices about the node change - if node.id not in removed_nodes: - hass.async_create_task(async_handle_node_update(hass, node)) - - @callback - def async_node_removed(node): - _LOGGER.debug("[NODE REMOVED] node_id: %s", node.id) - data_nodes.pop(node.id) - # node added/removed events also happen on (re)starts of hass/mqtt/ozw - # cleanup device/entity registry if we know this node is permanently deleted - # entities itself are removed by the values logic - if node.id in removed_nodes: - hass.async_create_task(async_handle_remove_node(hass, node)) - removed_nodes.remove(node.id) - - @callback - def async_instance_event(message): - event = message["event"] - event_data = message["data"] - _LOGGER.debug("[INSTANCE EVENT]: %s - data: %s", event, event_data) - # The actual removal action of a Z-Wave node is reported as instance event - # Only when this event is detected we cleanup the device and entities from hass - # Note: Find a more elegant way of doing this, e.g. a notification of this event from OZW - if event in ("removenode", "removefailednode") and "Node" in event_data: - removed_nodes.append(event_data["Node"]) - - @callback - def async_value_added(value): - node = value.node - # Clean up node.node_id and node.id use. They are the same. - node_id = value.node.node_id - - # Filter out CommandClasses we're definitely not interested in. - if value.command_class in (CommandClass.MANUFACTURER_SPECIFIC,): - return - - _LOGGER.debug( - "[VALUE ADDED] node_id: %s - label: %s - value: %s - value_id: %s - CC: %s", - value.node.id, - value.label, - value.value, - value.value_id_key, - value.command_class, - ) - - node_data_values = data_values[node_id] - - # Check if this value should be tracked by an existing entity - value_unique_id = create_value_id(value) - for values in node_data_values: - values.async_check_value(value) - if values.values_id == value_unique_id: - return # this value already has an entity - - # Run discovery on it and see if any entities need created - for schema in DISCOVERY_SCHEMAS: - if not check_node_schema(node, schema): - continue - if not check_value_schema( - value, schema[const.DISC_VALUES][const.DISC_PRIMARY] - ): - continue - - values = ZWaveDeviceEntityValues(hass, options, schema, value) - values.async_setup() - - # This is legacy and can be cleaned up since we are in the main thread: - # We create a new list and update the reference here so that - # the list can be safely iterated over in the main thread - data_values[node_id] = node_data_values + [values] - - @callback - def async_value_changed(value): - # if an entity belonging to this value needs updating, - # it's handled within the entity logic - _LOGGER.debug( - "[VALUE CHANGED] node_id: %s - label: %s - value: %s - value_id: %s - CC: %s", - value.node.id, - value.label, - value.value, - value.value_id_key, - value.command_class, - ) - # Handle a scene activation message - if value.command_class in ( - CommandClass.SCENE_ACTIVATION, - CommandClass.CENTRAL_SCENE, - ): - async_handle_scene_activated(hass, value) - return - - @callback - def async_value_removed(value): - _LOGGER.debug( - "[VALUE REMOVED] node_id: %s - label: %s - value: %s - value_id: %s - CC: %s", - value.node.id, - value.label, - value.value, - value.value_id_key, - value.command_class, - ) - # signal all entities using this value for removal - value_unique_id = create_value_id(value) - async_dispatcher_send(hass, const.SIGNAL_DELETE_ENTITY, value_unique_id) - # remove value from our local list - node_data_values = data_values[value.node.id] - node_data_values[:] = [ - item for item in node_data_values if item.values_id != value_unique_id - ] - - # Listen to events for node and value changes - for event, event_callback in ( - (EVENT_NODE_ADDED, async_node_added), - (EVENT_NODE_CHANGED, async_node_changed), - (EVENT_NODE_REMOVED, async_node_removed), - (EVENT_VALUE_ADDED, async_value_added), - (EVENT_VALUE_CHANGED, async_value_changed), - (EVENT_VALUE_REMOVED, async_value_removed), - (EVENT_INSTANCE_EVENT, async_instance_event), - ): - ozw_data[DATA_UNSUBSCRIBE].append(options.listen(event, event_callback)) - - # Register Services - services = ZWaveServices(hass, manager) - services.async_register() - - # Register WebSocket API - async_register_api(hass) - - @callback - def async_receive_message(msg): - manager.receive_message(msg.topic, msg.payload) - - async def start_platforms(): - await asyncio.gather( - *( - hass.config_entries.async_forward_entry_setup(entry, platform) - for platform in PLATFORMS - ) - ) - if entry.data.get(CONF_USE_ADDON): - mqtt_client_task = asyncio.create_task(mqtt_client.start_client(manager)) - - async def async_stop_mqtt_client(event=None): - """Stop the mqtt client. - - Do not unsubscribe the manager topic. - """ - mqtt_client_task.cancel() - with suppress(asyncio.CancelledError): - await mqtt_client_task - - ozw_data[DATA_UNSUBSCRIBE].append( - hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STOP, async_stop_mqtt_client - ) - ) - ozw_data[DATA_STOP_MQTT_CLIENT] = async_stop_mqtt_client - - else: - ozw_data[DATA_UNSUBSCRIBE].append( - await mqtt.async_subscribe( - hass, f"{manager.options.topic_prefix}#", async_receive_message - ) - ) - - hass.async_create_task(start_platforms()) - - return True - - -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Unload a config entry.""" - # cleanup platforms - unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - if not unload_ok: - return False - - # unsubscribe all listeners - for unsubscribe_listener in hass.data[DOMAIN][entry.entry_id][DATA_UNSUBSCRIBE]: - unsubscribe_listener() - - if entry.data.get(CONF_USE_ADDON): - async_stop_mqtt_client = hass.data[DOMAIN][entry.entry_id][ - DATA_STOP_MQTT_CLIENT - ] - await async_stop_mqtt_client() - - hass.data[DOMAIN].pop(entry.entry_id) - - return True - - -async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: - """Remove a config entry.""" - if not entry.data.get(CONF_INTEGRATION_CREATED_ADDON): - return - - try: - await hassio.async_stop_addon(hass, "core_zwave") - except HassioAPIError as err: - _LOGGER.error("Failed to stop the OpenZWave add-on: %s", err) - return - try: - await hassio.async_uninstall_addon(hass, "core_zwave") - except HassioAPIError as err: - _LOGGER.error("Failed to uninstall the OpenZWave add-on: %s", err) - - -async def async_handle_remove_node(hass: HomeAssistant, node: OZWNode): - """Handle the removal of a Z-Wave node, removing all traces in device/entity registry.""" - dev_registry = await get_dev_reg(hass) - # grab device in device registry attached to this node - dev_id = create_device_id(node) - device = dev_registry.async_get_device({(DOMAIN, dev_id)}) - if not device: - return - devices_to_remove = [device.id] - # also grab slave devices (node instances) - for item in dev_registry.devices.values(): - if item.via_device_id == device.id: - devices_to_remove.append(item.id) - # remove all devices in registry related to this node - # note: removal of entity registry is handled by core - for dev_id in devices_to_remove: - dev_registry.async_remove_device(dev_id) - - -async def async_handle_node_update(hass: HomeAssistant, node: OZWNode): - """ - Handle a node updated event from OZW. - - Meaning some of the basic info like name/model is updated. - We want these changes to be pushed to the device registry. - """ - dev_registry = await get_dev_reg(hass) - # grab device in device registry attached to this node - dev_id = create_device_id(node) - device = dev_registry.async_get_device({(DOMAIN, dev_id)}) - if not device: - return - # update device in device registry with (updated) info - for item in dev_registry.devices.values(): - if device.id not in (item.id, item.via_device_id): - continue - dev_name = create_device_name(node) - dev_registry.async_update_device( - item.id, - manufacturer=node.node_manufacturer_name, - model=node.node_product_name, - name=dev_name, - ) - - -@callback -def async_handle_scene_activated(hass: HomeAssistant, scene_value: OZWValue): - """Handle a (central) scene activation message.""" - node_id = scene_value.node.id - ozw_instance_id = scene_value.ozw_instance.id - scene_id = scene_value.index - scene_label = scene_value.label - if scene_value.command_class == CommandClass.SCENE_ACTIVATION: - # legacy/network scene - scene_value_id = scene_value.value - scene_value_label = scene_value.label - else: - # central scene command - if scene_value.type != ValueType.LIST: - return - scene_value_label = scene_value.value["Selected"] - scene_value_id = scene_value.value["Selected_id"] - - _LOGGER.debug( - "[SCENE_ACTIVATED] ozw_instance: %s - node_id: %s - scene_id: %s - scene_value_id: %s", - ozw_instance_id, - node_id, - scene_id, - scene_value_id, - ) - # Simply forward it to the hass event bus - hass.bus.async_fire( - const.EVENT_SCENE_ACTIVATED, - { - const.ATTR_INSTANCE_ID: ozw_instance_id, - const.ATTR_NODE_ID: node_id, - const.ATTR_SCENE_ID: scene_id, - const.ATTR_SCENE_LABEL: scene_label, - const.ATTR_SCENE_VALUE_ID: scene_value_id, - const.ATTR_SCENE_VALUE_LABEL: scene_value_label, - }, - ) diff --git a/homeassistant/components/ozw/binary_sensor.py b/homeassistant/components/ozw/binary_sensor.py deleted file mode 100644 index c234d30c5ca..00000000000 --- a/homeassistant/components/ozw/binary_sensor.py +++ /dev/null @@ -1,396 +0,0 @@ -"""Representation of Z-Wave binary_sensors.""" -from openzwavemqtt.const import CommandClass, ValueIndex, ValueType - -from homeassistant.components.binary_sensor import ( - DOMAIN as BINARY_SENSOR_DOMAIN, - BinarySensorDeviceClass, - BinarySensorEntity, -) -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity_platform import AddEntitiesCallback - -from .const import DATA_UNSUBSCRIBE, DOMAIN -from .entity import ZWaveDeviceEntity - -NOTIFICATION_TYPE = "index" -NOTIFICATION_VALUES = "values" -NOTIFICATION_DEVICE_CLASS = "device_class" -NOTIFICATION_SENSOR_ENABLED = "enabled" -NOTIFICATION_OFF_VALUE = "off_value" - -NOTIFICATION_VALUE_CLEAR = 0 - -# Translation from values in Notification CC to binary sensors -# https://github.com/OpenZWave/open-zwave/blob/master/config/NotificationCCTypes.xml -NOTIFICATION_SENSORS = [ - { - # Index 1: Smoke Alarm - Value Id's 1 and 2 - # Assuming here that Value 1 and 2 are not present at the same time - NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_SMOKE_ALARM, - NOTIFICATION_VALUES: [1, 2], - NOTIFICATION_DEVICE_CLASS: BinarySensorDeviceClass.SMOKE, - }, - { - # Index 1: Smoke Alarm - All other Value Id's - # Create as disabled sensors - NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_SMOKE_ALARM, - NOTIFICATION_VALUES: [3, 4, 5, 6, 7, 8], - NOTIFICATION_DEVICE_CLASS: BinarySensorDeviceClass.SMOKE, - NOTIFICATION_SENSOR_ENABLED: False, - }, - { - # Index 2: Carbon Monoxide - Value Id's 1 and 2 - NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_CARBON_MONOOXIDE, - NOTIFICATION_VALUES: [1, 2], - NOTIFICATION_DEVICE_CLASS: BinarySensorDeviceClass.GAS, - }, - { - # Index 2: Carbon Monoxide - All other Value Id's - NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_CARBON_MONOOXIDE, - NOTIFICATION_VALUES: [4, 5, 7], - NOTIFICATION_DEVICE_CLASS: BinarySensorDeviceClass.GAS, - NOTIFICATION_SENSOR_ENABLED: False, - }, - { - # Index 3: Carbon Dioxide - Value Id's 1 and 2 - NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_CARBON_DIOXIDE, - NOTIFICATION_VALUES: [1, 2], - NOTIFICATION_DEVICE_CLASS: BinarySensorDeviceClass.GAS, - }, - { - # Index 3: Carbon Dioxide - All other Value Id's - NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_CARBON_DIOXIDE, - NOTIFICATION_VALUES: [4, 5, 7], - NOTIFICATION_DEVICE_CLASS: BinarySensorDeviceClass.GAS, - NOTIFICATION_SENSOR_ENABLED: False, - }, - { - # Index 4: Heat - Value Id's 1, 2, 5, 6 (heat/underheat) - NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_HEAT, - NOTIFICATION_VALUES: [1, 2, 5, 6], - NOTIFICATION_DEVICE_CLASS: BinarySensorDeviceClass.HEAT, - }, - { - # Index 4: Heat - All other Value Id's - NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_HEAT, - NOTIFICATION_VALUES: [3, 4, 8, 10, 11], - NOTIFICATION_DEVICE_CLASS: BinarySensorDeviceClass.HEAT, - NOTIFICATION_SENSOR_ENABLED: False, - }, - { - # Index 5: Water - Value Id's 1, 2, 3, 4 - NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_WATER, - NOTIFICATION_VALUES: [1, 2, 3, 4], - NOTIFICATION_DEVICE_CLASS: BinarySensorDeviceClass.MOISTURE, - }, - { - # Index 5: Water - All other Value Id's - NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_WATER, - NOTIFICATION_VALUES: [5], - NOTIFICATION_DEVICE_CLASS: BinarySensorDeviceClass.MOISTURE, - NOTIFICATION_SENSOR_ENABLED: False, - }, - { - # Index 6: Access Control - Value Id's 1, 2, 3, 4 (Lock) - NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_ACCESS_CONTROL, - NOTIFICATION_VALUES: [1, 2, 3, 4], - NOTIFICATION_DEVICE_CLASS: BinarySensorDeviceClass.LOCK, - }, - { - # Index 6: Access Control - Value Id 22 (door/window open) - NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_ACCESS_CONTROL, - NOTIFICATION_VALUES: [22], - NOTIFICATION_DEVICE_CLASS: BinarySensorDeviceClass.DOOR, - NOTIFICATION_OFF_VALUE: 23, - }, - { - # Index 7: Home Security - Value Id's 1, 2 (intrusion) - # Assuming that value 1 and 2 are not present at the same time - NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_HOME_SECURITY, - NOTIFICATION_VALUES: [1, 2], - NOTIFICATION_DEVICE_CLASS: BinarySensorDeviceClass.SAFETY, - }, - { - # Index 7: Home Security - Value Id's 3, 4, 9 (tampering) - NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_HOME_SECURITY, - NOTIFICATION_VALUES: [3, 4, 9], - NOTIFICATION_DEVICE_CLASS: BinarySensorDeviceClass.SAFETY, - }, - { - # Index 7: Home Security - Value Id's 5, 6 (glass breakage) - # Assuming that value 5 and 6 are not present at the same time - NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_HOME_SECURITY, - NOTIFICATION_VALUES: [5, 6], - NOTIFICATION_DEVICE_CLASS: BinarySensorDeviceClass.SAFETY, - }, - { - # Index 7: Home Security - Value Id's 7, 8 (motion) - NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_HOME_SECURITY, - NOTIFICATION_VALUES: [7, 8], - NOTIFICATION_DEVICE_CLASS: BinarySensorDeviceClass.MOTION, - }, - { - # Index 8: Power management - Values 1...9 - NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_POWER_MANAGEMENT, - NOTIFICATION_VALUES: [1, 2, 3, 4, 5, 6, 7, 8, 9], - NOTIFICATION_DEVICE_CLASS: BinarySensorDeviceClass.POWER, - NOTIFICATION_SENSOR_ENABLED: False, - }, - { - # Index 8: Power management - Values 10...15 - # Battery values (mutually exclusive) - NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_POWER_MANAGEMENT, - NOTIFICATION_VALUES: [10, 11, 12, 13, 14, 15], - NOTIFICATION_DEVICE_CLASS: BinarySensorDeviceClass.POWER, - NOTIFICATION_SENSOR_ENABLED: False, - NOTIFICATION_OFF_VALUE: None, - }, - { - # Index 9: System - Value Id's 1, 2, 6, 7 - NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_SYSTEM, - NOTIFICATION_VALUES: [1, 2, 6, 7], - NOTIFICATION_DEVICE_CLASS: BinarySensorDeviceClass.PROBLEM, - NOTIFICATION_SENSOR_ENABLED: False, - }, - { - # Index 10: Emergency - Value Id's 1, 2, 3 - NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_EMERGENCY, - NOTIFICATION_VALUES: [1, 2, 3], - NOTIFICATION_DEVICE_CLASS: BinarySensorDeviceClass.PROBLEM, - }, - { - # Index 11: Clock - Value Id's 1, 2 - NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_CLOCK, - NOTIFICATION_VALUES: [1, 2], - NOTIFICATION_DEVICE_CLASS: None, - NOTIFICATION_SENSOR_ENABLED: False, - }, - { - # Index 12: Appliance - All Value Id's - NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_APPLIANCE, - NOTIFICATION_VALUES: [ - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9, - 10, - 11, - 12, - 13, - 14, - 15, - 16, - 17, - 18, - 19, - 20, - 21, - ], - NOTIFICATION_DEVICE_CLASS: None, - }, - { - # Index 13: Home Health - Value Id's 1,2,3,4,5 - NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_APPLIANCE, - NOTIFICATION_VALUES: [1, 2, 3, 4, 5], - NOTIFICATION_DEVICE_CLASS: None, - }, - { - # Index 14: Siren - NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_SIREN, - NOTIFICATION_VALUES: [1], - NOTIFICATION_DEVICE_CLASS: BinarySensorDeviceClass.SOUND, - }, - { - # Index 15: Water valve - # ignore non-boolean values - NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_WATER_VALVE, - NOTIFICATION_VALUES: [3, 4], - NOTIFICATION_DEVICE_CLASS: BinarySensorDeviceClass.PROBLEM, - }, - { - # Index 16: Weather - NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_WEATHER, - NOTIFICATION_VALUES: [1, 2], - NOTIFICATION_DEVICE_CLASS: BinarySensorDeviceClass.PROBLEM, - }, - { - # Index 17: Irrigation - # ignore non-boolean values - NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_IRRIGATION, - NOTIFICATION_VALUES: [1, 2, 3, 4, 5], - NOTIFICATION_DEVICE_CLASS: None, - }, - { - # Index 18: Gas - NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_GAS, - NOTIFICATION_VALUES: [1, 2, 3, 4], - NOTIFICATION_DEVICE_CLASS: BinarySensorDeviceClass.GAS, - }, - { - # Index 18: Gas - NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_GAS, - NOTIFICATION_VALUES: [6], - NOTIFICATION_DEVICE_CLASS: BinarySensorDeviceClass.PROBLEM, - }, -] - - -async def async_setup_entry( - hass: HomeAssistant, - config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up Z-Wave binary_sensor from config entry.""" - - @callback - def async_add_binary_sensor(values): - """Add Z-Wave Binary Sensor(s).""" - async_add_entities(VALUE_TYPE_SENSORS[values.primary.type](values)) - - hass.data[DOMAIN][config_entry.entry_id][DATA_UNSUBSCRIBE].append( - async_dispatcher_connect( - hass, f"{DOMAIN}_new_{BINARY_SENSOR_DOMAIN}", async_add_binary_sensor - ) - ) - - -@callback -def async_get_legacy_binary_sensors(values): - """Add Legacy/classic Z-Wave Binary Sensor.""" - return [ZWaveBinarySensor(values)] - - -@callback -def async_get_notification_sensors(values): - """Convert Notification values into binary sensors.""" - sensors_to_add = [] - for list_value in values.primary.value["List"]: - # check if we have a mapping for this value - for item in NOTIFICATION_SENSORS: - if item[NOTIFICATION_TYPE] != values.primary.index: - continue - if list_value["Value"] not in item[NOTIFICATION_VALUES]: - continue - sensors_to_add.append( - ZWaveListValueSensor( - # required values - values, - list_value["Value"], - item[NOTIFICATION_DEVICE_CLASS], - # optional values - item.get(NOTIFICATION_SENSOR_ENABLED, True), - item.get(NOTIFICATION_OFF_VALUE, NOTIFICATION_VALUE_CLEAR), - ) - ) - return sensors_to_add - - -VALUE_TYPE_SENSORS = { - ValueType.BOOL: async_get_legacy_binary_sensors, - ValueType.LIST: async_get_notification_sensors, -} - - -class ZWaveBinarySensor(ZWaveDeviceEntity, BinarySensorEntity): - """Representation of a Z-Wave binary_sensor.""" - - @property - def is_on(self): - """Return if the sensor is on or off.""" - return self.values.primary.value - - @property - def entity_registry_enabled_default(self) -> bool: - """Return if the entity should be enabled when first added to the entity registry.""" - # Legacy binary sensors are phased out (replaced by notification sensors) - # Disable by default to not confuse users - for item in self.values.primary.node.values(): - if item.command_class == CommandClass.NOTIFICATION: - # This device properly implements the Notification CC, legacy sensor can be disabled - return False - return True - - -class ZWaveListValueSensor(ZWaveDeviceEntity, BinarySensorEntity): - """Representation of a binary_sensor from values in the Z-Wave Notification CommandClass.""" - - def __init__( - self, - values, - on_value, - device_class=None, - default_enabled=True, - off_value=NOTIFICATION_VALUE_CLEAR, - ): - """Initialize a ZWaveListValueSensor entity.""" - super().__init__(values) - self._on_value = on_value - self._device_class = device_class - self._default_enabled = default_enabled - self._off_value = off_value - # make sure the correct value is selected at startup - self._state = False - self.on_value_update() - - @callback - def on_value_update(self): - """Call when a value is added/updated in the underlying EntityValues Collection.""" - if self.values.primary.value["Selected_id"] == self._on_value: - # Only when the active ID exactly matches our watched ON value, set sensor state to ON - self._state = True - elif self.values.primary.value["Selected_id"] == self._off_value: - # Only when the active ID exactly matches our watched OFF value, set sensor state to OFF - self._state = False - elif ( - self._off_value is None - and self.values.primary.value["Selected_id"] != self._on_value - ): - # Off value not explicitly specified - # Some values are reset by the simple fact they're overruled by another value coming in - # For example the battery charging values in Power Management Index - self._state = False - - @property - def name(self): - """Return the name of the entity.""" - # Append value label to base name - base_name = super().name - value_label = "" - for item in self.values.primary.value["List"]: - if item["Value"] == self._on_value: - value_label = item["Label"] - break - # Strip "on location" / "at location" from name - # Note: We're assuming that we don't retrieve 2 values with different location - value_label = value_label.split(" on ")[0] - value_label = value_label.split(" at ")[0] - return f"{base_name}: {value_label}" - - @property - def unique_id(self): - """Return the unique_id of the entity.""" - unique_id = super().unique_id - return f"{unique_id}.{self._on_value}" - - @property - def is_on(self): - """Return if the sensor is on or off.""" - return self._state - - @property - def device_class(self): - """Return the class of this device, from component DEVICE_CLASSES.""" - return self._device_class - - @property - def entity_registry_enabled_default(self) -> bool: - """Return if the entity should be enabled when first added to the entity registry.""" - # We hide the more advanced sensors by default to not overwhelm users - return self._default_enabled diff --git a/homeassistant/components/ozw/climate.py b/homeassistant/components/ozw/climate.py deleted file mode 100644 index 334f851d6b0..00000000000 --- a/homeassistant/components/ozw/climate.py +++ /dev/null @@ -1,379 +0,0 @@ -"""Support for Z-Wave climate devices.""" -from __future__ import annotations - -from enum import IntEnum -import logging - -from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN, ClimateEntity -from homeassistant.components.climate.const import ( - ATTR_HVAC_MODE, - ATTR_TARGET_TEMP_HIGH, - ATTR_TARGET_TEMP_LOW, - CURRENT_HVAC_COOL, - CURRENT_HVAC_FAN, - CURRENT_HVAC_HEAT, - CURRENT_HVAC_IDLE, - CURRENT_HVAC_OFF, - HVAC_MODE_COOL, - HVAC_MODE_DRY, - HVAC_MODE_FAN_ONLY, - HVAC_MODE_HEAT, - HVAC_MODE_HEAT_COOL, - HVAC_MODE_OFF, - PRESET_NONE, - SUPPORT_FAN_MODE, - SUPPORT_PRESET_MODE, - SUPPORT_TARGET_TEMPERATURE, - SUPPORT_TARGET_TEMPERATURE_RANGE, -) -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity_platform import AddEntitiesCallback - -from .const import DATA_UNSUBSCRIBE, DOMAIN -from .entity import ZWaveDeviceEntity - -VALUE_LIST = "List" -VALUE_ID = "Value" -VALUE_LABEL = "Label" -VALUE_SELECTED_ID = "Selected_id" -VALUE_SELECTED_LABEL = "Selected" - -ATTR_FAN_ACTION = "fan_action" -ATTR_VALVE_POSITION = "valve_position" -_LOGGER = logging.getLogger(__name__) - - -class ThermostatMode(IntEnum): - """Enum with all (known/used) Z-Wave ThermostatModes.""" - - # https://github.com/OpenZWave/open-zwave/blob/master/cpp/src/command_classes/ThermostatMode.cpp - OFF = 0 - HEAT = 1 - COOL = 2 - AUTO = 3 - AUXILIARY = 4 - RESUME_ON = 5 - FAN = 6 - FURNANCE = 7 - DRY = 8 - MOIST = 9 - AUTO_CHANGE_OVER = 10 - HEATING_ECON = 11 - COOLING_ECON = 12 - AWAY = 13 - FULL_POWER = 15 - MANUFACTURER_SPECIFIC = 31 - - -# In Z-Wave the modes and presets are both in ThermostatMode. -# This list contains thermostatmodes we should consider a mode only -MODES_LIST = [ - ThermostatMode.OFF, - ThermostatMode.HEAT, - ThermostatMode.COOL, - ThermostatMode.AUTO, - ThermostatMode.AUTO_CHANGE_OVER, -] - -MODE_SETPOINT_MAPPINGS = { - ThermostatMode.OFF: (), - ThermostatMode.HEAT: ("setpoint_heating",), - ThermostatMode.COOL: ("setpoint_cooling",), - ThermostatMode.AUTO: ("setpoint_heating", "setpoint_cooling"), - ThermostatMode.AUXILIARY: ("setpoint_heating",), - ThermostatMode.FURNANCE: ("setpoint_furnace",), - ThermostatMode.DRY: ("setpoint_dry_air",), - ThermostatMode.MOIST: ("setpoint_moist_air",), - ThermostatMode.AUTO_CHANGE_OVER: ("setpoint_auto_changeover",), - ThermostatMode.HEATING_ECON: ("setpoint_eco_heating",), - ThermostatMode.COOLING_ECON: ("setpoint_eco_cooling",), - ThermostatMode.AWAY: ("setpoint_away_heating", "setpoint_away_cooling"), - ThermostatMode.FULL_POWER: ("setpoint_full_power",), -} - - -# strings, OZW and/or qt-ozw does not send numeric values -# https://github.com/OpenZWave/open-zwave/blob/master/cpp/src/command_classes/ThermostatOperatingState.cpp -HVAC_CURRENT_MAPPINGS = { - "idle": CURRENT_HVAC_IDLE, - "heat": CURRENT_HVAC_HEAT, - "pending heat": CURRENT_HVAC_IDLE, - "heating": CURRENT_HVAC_HEAT, - "cool": CURRENT_HVAC_COOL, - "pending cool": CURRENT_HVAC_IDLE, - "cooling": CURRENT_HVAC_COOL, - "fan only": CURRENT_HVAC_FAN, - "vent / economiser": CURRENT_HVAC_FAN, - "off": CURRENT_HVAC_OFF, -} - - -# Map Z-Wave HVAC Mode to Home Assistant value -# Note: We treat "auto" as "heat_cool" as most Z-Wave devices -# report auto_changeover as auto without schedule support. -ZW_HVAC_MODE_MAPPINGS = { - ThermostatMode.OFF: HVAC_MODE_OFF, - ThermostatMode.HEAT: HVAC_MODE_HEAT, - ThermostatMode.COOL: HVAC_MODE_COOL, - # Z-Wave auto mode is actually heat/cool in the hass world - ThermostatMode.AUTO: HVAC_MODE_HEAT_COOL, - ThermostatMode.AUXILIARY: HVAC_MODE_HEAT, - ThermostatMode.FAN: HVAC_MODE_FAN_ONLY, - ThermostatMode.FURNANCE: HVAC_MODE_HEAT, - ThermostatMode.DRY: HVAC_MODE_DRY, - ThermostatMode.AUTO_CHANGE_OVER: HVAC_MODE_HEAT_COOL, - ThermostatMode.HEATING_ECON: HVAC_MODE_HEAT, - ThermostatMode.COOLING_ECON: HVAC_MODE_COOL, - ThermostatMode.AWAY: HVAC_MODE_HEAT_COOL, - ThermostatMode.FULL_POWER: HVAC_MODE_HEAT, -} - -# Map Home Assistant HVAC Mode to Z-Wave value -HVAC_MODE_ZW_MAPPINGS = { - HVAC_MODE_OFF: ThermostatMode.OFF, - HVAC_MODE_HEAT: ThermostatMode.HEAT, - HVAC_MODE_COOL: ThermostatMode.COOL, - HVAC_MODE_FAN_ONLY: ThermostatMode.FAN, - HVAC_MODE_DRY: ThermostatMode.DRY, - HVAC_MODE_HEAT_COOL: ThermostatMode.AUTO_CHANGE_OVER, -} - - -async def async_setup_entry( - hass: HomeAssistant, - config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up Z-Wave Climate from Config Entry.""" - - @callback - def async_add_climate(values): - """Add Z-Wave Climate.""" - async_add_entities([ZWaveClimateEntity(values)]) - - hass.data[DOMAIN][config_entry.entry_id][DATA_UNSUBSCRIBE].append( - async_dispatcher_connect( - hass, f"{DOMAIN}_new_{CLIMATE_DOMAIN}", async_add_climate - ) - ) - - -class ZWaveClimateEntity(ZWaveDeviceEntity, ClimateEntity): - """Representation of a Z-Wave Climate device.""" - - def __init__(self, values): - """Initialize the entity.""" - super().__init__(values) - self._hvac_modes = {} - self._hvac_presets = {} - self.on_value_update() - - @callback - def on_value_update(self): - """Call when the underlying values object changes.""" - self._current_mode_setpoint_values = self._get_current_mode_setpoint_values() - if not self._hvac_modes: - self._set_modes_and_presets() - - @property - def hvac_mode(self): - """Return hvac operation ie. heat, cool mode.""" - if not self.values.mode: - # Thermostat(valve) with no support for setting a mode is considered heating-only - return HVAC_MODE_HEAT - return ZW_HVAC_MODE_MAPPINGS.get( - self.values.mode.value[VALUE_SELECTED_ID], HVAC_MODE_HEAT_COOL - ) - - @property - def hvac_modes(self): - """Return the list of available hvac operation modes.""" - return list(self._hvac_modes) - - @property - def fan_mode(self): - """Return the fan speed set.""" - return self.values.fan_mode.value[VALUE_SELECTED_LABEL] - - @property - def fan_modes(self): - """Return a list of available fan modes.""" - return [entry[VALUE_LABEL] for entry in self.values.fan_mode.value[VALUE_LIST]] - - @property - def temperature_unit(self): - """Return the unit of measurement.""" - if self.values.temperature is not None and self.values.temperature.units == "F": - return TEMP_FAHRENHEIT - return TEMP_CELSIUS - - @property - def current_temperature(self): - """Return the current temperature.""" - if not self.values.temperature: - return None - return self.values.temperature.value - - @property - def hvac_action(self): - """Return the current running hvac operation if supported.""" - if not self.values.operating_state: - return None - cur_state = self.values.operating_state.value.lower() - return HVAC_CURRENT_MAPPINGS.get(cur_state) - - @property - def preset_mode(self): - """Return preset operation ie. eco, away.""" - # A Zwave mode that can not be translated to a hass mode is considered a preset - if not self.values.mode: - return None - if self.values.mode.value[VALUE_SELECTED_ID] not in MODES_LIST: - return self.values.mode.value[VALUE_SELECTED_LABEL] - return PRESET_NONE - - @property - def preset_modes(self): - """Return the list of available preset operation modes.""" - return list(self._hvac_presets) - - @property - def target_temperature(self): - """Return the temperature we try to reach.""" - return self._current_mode_setpoint_values[0].value - - @property - def target_temperature_low(self) -> float | None: - """Return the lowbound target temperature we try to reach.""" - return self._current_mode_setpoint_values[0].value - - @property - def target_temperature_high(self) -> float | None: - """Return the highbound target temperature we try to reach.""" - return self._current_mode_setpoint_values[1].value - - async def async_set_temperature(self, **kwargs): - """Set new target temperature. - - Must know if single or double setpoint. - """ - if (hvac_mode := kwargs.get(ATTR_HVAC_MODE)) is not None: - await self.async_set_hvac_mode(hvac_mode) - - if len(self._current_mode_setpoint_values) == 1: - setpoint = self._current_mode_setpoint_values[0] - target_temp = kwargs.get(ATTR_TEMPERATURE) - if setpoint is not None and target_temp is not None: - setpoint.send_value(target_temp) - elif len(self._current_mode_setpoint_values) == 2: - (setpoint_low, setpoint_high) = self._current_mode_setpoint_values - target_temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW) - target_temp_high = kwargs.get(ATTR_TARGET_TEMP_HIGH) - if setpoint_low is not None and target_temp_low is not None: - setpoint_low.send_value(target_temp_low) - if setpoint_high is not None and target_temp_high is not None: - setpoint_high.send_value(target_temp_high) - - async def async_set_fan_mode(self, fan_mode): - """Set new target fan mode.""" - # get id for this fan_mode - fan_mode_value = _get_list_id(self.values.fan_mode.value[VALUE_LIST], fan_mode) - if fan_mode_value is None: - _LOGGER.warning("Received an invalid fan mode: %s", fan_mode) - return - self.values.fan_mode.send_value(fan_mode_value) - - async def async_set_hvac_mode(self, hvac_mode): - """Set new target hvac mode.""" - if not self.values.mode: - # Thermostat(valve) with no support for setting a mode - _LOGGER.warning( - "Thermostat %s does not support setting a mode", self.entity_id - ) - return - if (hvac_mode_value := self._hvac_modes.get(hvac_mode)) is None: - _LOGGER.warning("Received an invalid hvac mode: %s", hvac_mode) - return - self.values.mode.send_value(hvac_mode_value) - - async def async_set_preset_mode(self, preset_mode): - """Set new target preset mode.""" - if preset_mode == PRESET_NONE: - # try to restore to the (translated) main hvac mode - await self.async_set_hvac_mode(self.hvac_mode) - return - preset_mode_value = self._hvac_presets.get(preset_mode) - if preset_mode_value is None: - _LOGGER.warning("Received an invalid preset mode: %s", preset_mode) - return - self.values.mode.send_value(preset_mode_value) - - @property - def extra_state_attributes(self): - """Return the optional state attributes.""" - data = super().extra_state_attributes - if self.values.fan_action: - data[ATTR_FAN_ACTION] = self.values.fan_action.value - if self.values.valve_position: - data[ - ATTR_VALVE_POSITION - ] = f"{self.values.valve_position.value} {self.values.valve_position.units}" - return data - - @property - def supported_features(self): - """Return the list of supported features.""" - support = 0 - if len(self._current_mode_setpoint_values) == 1: - support |= SUPPORT_TARGET_TEMPERATURE - if len(self._current_mode_setpoint_values) > 1: - support |= SUPPORT_TARGET_TEMPERATURE_RANGE - if self.values.fan_mode: - support |= SUPPORT_FAN_MODE - if self.values.mode: - support |= SUPPORT_PRESET_MODE - return support - - def _get_current_mode_setpoint_values(self) -> tuple: - """Return a tuple of current setpoint Z-Wave value(s).""" - if not self.values.mode: - setpoint_names = ("setpoint_heating",) - else: - current_mode = self.values.mode.value[VALUE_SELECTED_ID] - setpoint_names = MODE_SETPOINT_MAPPINGS.get(current_mode, ()) - # we do not want None values in our tuple so check if the value exists - return tuple( - getattr(self.values, value_name) - for value_name in setpoint_names - if getattr(self.values, value_name, None) - ) - - def _set_modes_and_presets(self): - """Convert Z-Wave Thermostat modes into Home Assistant modes and presets.""" - all_modes = {} - all_presets = {PRESET_NONE: None} - if self.values.mode: - # Z-Wave uses one list for both modes and presets. - # Iterate over all Z-Wave ThermostatModes and extract the hvac modes and presets. - for val in self.values.mode.value[VALUE_LIST]: - if val[VALUE_ID] in MODES_LIST: - # treat value as hvac mode - hass_mode = ZW_HVAC_MODE_MAPPINGS.get(val[VALUE_ID]) - all_modes[hass_mode] = val[VALUE_ID] - else: - # treat value as hvac preset - all_presets[val[VALUE_LABEL]] = val[VALUE_ID] - else: - all_modes[HVAC_MODE_HEAT] = None - self._hvac_modes = all_modes - self._hvac_presets = all_presets - - -def _get_list_id(value_lst, value_lbl): - """Return the id for the value in the list.""" - return next( - (val[VALUE_ID] for val in value_lst if val[VALUE_LABEL] == value_lbl), None - ) diff --git a/homeassistant/components/ozw/config_flow.py b/homeassistant/components/ozw/config_flow.py deleted file mode 100644 index 5e745a123f4..00000000000 --- a/homeassistant/components/ozw/config_flow.py +++ /dev/null @@ -1,228 +0,0 @@ -"""Config flow for ozw integration.""" -import logging - -import voluptuous as vol - -from homeassistant import config_entries -from homeassistant.components import hassio -from homeassistant.components.hassio import HassioServiceInfo -from homeassistant.core import callback -from homeassistant.data_entry_flow import AbortFlow, FlowResult - -from .const import CONF_INTEGRATION_CREATED_ADDON, CONF_USE_ADDON, DOMAIN - -_LOGGER = logging.getLogger(__name__) - -CONF_ADDON_DEVICE = "device" -CONF_ADDON_NETWORK_KEY = "network_key" -CONF_NETWORK_KEY = "network_key" -CONF_USB_PATH = "usb_path" -TITLE = "OpenZWave" - -ON_SUPERVISOR_SCHEMA = vol.Schema({vol.Optional(CONF_USE_ADDON, default=False): bool}) - - -class DomainConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): - """Handle a config flow for ozw.""" - - VERSION = 1 - - def __init__(self): - """Set up flow instance.""" - self.addon_config = None - self.network_key = None - self.usb_path = None - self.use_addon = False - # If we install the add-on we should uninstall it on entry remove. - self.integration_created_addon = False - self.install_task = None - - async def async_step_user(self, user_input=None): - """Handle the initial step.""" - if self._async_current_entries(): - return self.async_abort(reason="single_instance_allowed") - - # Set a unique_id to make sure discovery flow is aborted on progress. - await self.async_set_unique_id(DOMAIN, raise_on_progress=False) - - if not hassio.is_hassio(self.hass): - return self._async_use_mqtt_integration() - - return await self.async_step_on_supervisor() - - async def async_step_hassio(self, discovery_info: HassioServiceInfo) -> FlowResult: - """Receive configuration from add-on discovery info. - - This flow is triggered by the OpenZWave add-on. - """ - await self.async_set_unique_id(DOMAIN) - self._abort_if_unique_id_configured() - - return await self.async_step_hassio_confirm() - - async def async_step_hassio_confirm(self, user_input=None): - """Confirm the add-on discovery.""" - if user_input is not None: - return await self.async_step_on_supervisor( - user_input={CONF_USE_ADDON: True} - ) - - return self.async_show_form(step_id="hassio_confirm") - - def _async_create_entry_from_vars(self): - """Return a config entry for the flow.""" - return self.async_create_entry( - title=TITLE, - data={ - CONF_USB_PATH: self.usb_path, - CONF_NETWORK_KEY: self.network_key, - CONF_USE_ADDON: self.use_addon, - CONF_INTEGRATION_CREATED_ADDON: self.integration_created_addon, - }, - ) - - @callback - def _async_use_mqtt_integration(self): - """Handle logic when using the MQTT integration. - - This is the entry point for the logic that is needed - when this integration will depend on the MQTT integration. - """ - mqtt_entries = self.hass.config_entries.async_entries("mqtt") - if ( - not mqtt_entries - or mqtt_entries[0].state is not config_entries.ConfigEntryState.LOADED - ): - return self.async_abort(reason="mqtt_required") - return self._async_create_entry_from_vars() - - async def async_step_on_supervisor(self, user_input=None): - """Handle logic when on Supervisor host.""" - if user_input is None: - return self.async_show_form( - step_id="on_supervisor", data_schema=ON_SUPERVISOR_SCHEMA - ) - if not user_input[CONF_USE_ADDON]: - return self._async_create_entry_from_vars() - - self.use_addon = True - - if await self._async_is_addon_running(): - addon_config = await self._async_get_addon_config() - self.usb_path = addon_config[CONF_ADDON_DEVICE] - self.network_key = addon_config.get(CONF_ADDON_NETWORK_KEY, "") - return self._async_create_entry_from_vars() - - if await self._async_is_addon_installed(): - return await self.async_step_start_addon() - - return await self.async_step_install_addon() - - async def async_step_install_addon(self, user_input=None): - """Install OpenZWave add-on.""" - if not self.install_task: - self.install_task = self.hass.async_create_task(self._async_install_addon()) - return self.async_show_progress( - step_id="install_addon", progress_action="install_addon" - ) - - try: - await self.install_task - except hassio.HassioAPIError as err: - _LOGGER.error("Failed to install OpenZWave add-on: %s", err) - return self.async_show_progress_done(next_step_id="install_failed") - - self.integration_created_addon = True - - return self.async_show_progress_done(next_step_id="start_addon") - - async def async_step_install_failed(self, user_input=None): - """Add-on installation failed.""" - return self.async_abort(reason="addon_install_failed") - - async def async_step_start_addon(self, user_input=None): - """Ask for config and start OpenZWave add-on.""" - if self.addon_config is None: - self.addon_config = await self._async_get_addon_config() - - errors = {} - - if user_input is not None: - self.network_key = user_input[CONF_NETWORK_KEY] - self.usb_path = user_input[CONF_USB_PATH] - - new_addon_config = { - CONF_ADDON_DEVICE: self.usb_path, - CONF_ADDON_NETWORK_KEY: self.network_key, - } - - if new_addon_config != self.addon_config: - await self._async_set_addon_config(new_addon_config) - - try: - await hassio.async_start_addon(self.hass, "core_zwave") - except hassio.HassioAPIError as err: - _LOGGER.error("Failed to start OpenZWave add-on: %s", err) - errors["base"] = "addon_start_failed" - else: - return self._async_create_entry_from_vars() - - usb_path = self.addon_config.get(CONF_ADDON_DEVICE, self.usb_path or "") - network_key = self.addon_config.get( - CONF_ADDON_NETWORK_KEY, self.network_key or "" - ) - - data_schema = vol.Schema( - { - vol.Required(CONF_USB_PATH, default=usb_path): str, - vol.Optional(CONF_NETWORK_KEY, default=network_key): str, - } - ) - - return self.async_show_form( - step_id="start_addon", data_schema=data_schema, errors=errors - ) - - async def _async_get_addon_info(self): - """Return and cache OpenZWave add-on info.""" - try: - addon_info = await hassio.async_get_addon_info(self.hass, "core_zwave") - except hassio.HassioAPIError as err: - _LOGGER.error("Failed to get OpenZWave add-on info: %s", err) - raise AbortFlow("addon_info_failed") from err - - return addon_info - - async def _async_is_addon_running(self): - """Return True if OpenZWave add-on is running.""" - addon_info = await self._async_get_addon_info() - return addon_info["state"] == "started" - - async def _async_is_addon_installed(self): - """Return True if OpenZWave add-on is installed.""" - addon_info = await self._async_get_addon_info() - return addon_info["version"] is not None - - async def _async_get_addon_config(self): - """Get OpenZWave add-on config.""" - addon_info = await self._async_get_addon_info() - return addon_info["options"] - - async def _async_set_addon_config(self, config): - """Set OpenZWave add-on config.""" - options = {"options": config} - try: - await hassio.async_set_addon_options(self.hass, "core_zwave", options) - except hassio.HassioAPIError as err: - _LOGGER.error("Failed to set OpenZWave add-on config: %s", err) - raise AbortFlow("addon_set_config_failed") from err - - async def _async_install_addon(self): - """Install the OpenZWave add-on.""" - try: - await hassio.async_install_addon(self.hass, "core_zwave") - finally: - # Continue the flow after show progress when the task is done. - self.hass.async_create_task( - self.hass.config_entries.flow.async_configure(flow_id=self.flow_id) - ) diff --git a/homeassistant/components/ozw/const.py b/homeassistant/components/ozw/const.py deleted file mode 100644 index 68eaf9f7c8a..00000000000 --- a/homeassistant/components/ozw/const.py +++ /dev/null @@ -1,71 +0,0 @@ -"""Constants for the ozw integration.""" -from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN -from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN -from homeassistant.components.cover import DOMAIN as COVER_DOMAIN -from homeassistant.components.fan import DOMAIN as FAN_DOMAIN -from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN -from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN -from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN - -DOMAIN = "ozw" -DATA_UNSUBSCRIBE = "unsubscribe" - -CONF_INTEGRATION_CREATED_ADDON = "integration_created_addon" -CONF_USE_ADDON = "use_addon" - -PLATFORMS = [ - BINARY_SENSOR_DOMAIN, - COVER_DOMAIN, - CLIMATE_DOMAIN, - FAN_DOMAIN, - LIGHT_DOMAIN, - LOCK_DOMAIN, - SENSOR_DOMAIN, - SWITCH_DOMAIN, -] -MANAGER = "manager" -NODES_VALUES = "nodes_values" - -# MQTT Topics -TOPIC_OPENZWAVE = "OpenZWave" - -# Common Attributes -ATTR_CONFIG_PARAMETER = "parameter" -ATTR_CONFIG_VALUE = "value" -ATTR_INSTANCE_ID = "instance_id" -ATTR_SECURE = "secure" -ATTR_NODE_ID = "node_id" -ATTR_SCENE_ID = "scene_id" -ATTR_SCENE_LABEL = "scene_label" -ATTR_SCENE_VALUE_ID = "scene_value_id" -ATTR_SCENE_VALUE_LABEL = "scene_value_label" - -# Config entry data and options -MIGRATED = "migrated" - -# Service specific -SERVICE_ADD_NODE = "add_node" -SERVICE_REMOVE_NODE = "remove_node" -SERVICE_CANCEL_COMMAND = "cancel_command" -SERVICE_SET_CONFIG_PARAMETER = "set_config_parameter" - -# Home Assistant Events -EVENT_SCENE_ACTIVATED = f"{DOMAIN}.scene_activated" - -# Signals -SIGNAL_DELETE_ENTITY = f"{DOMAIN}_delete_entity" - -# Discovery Information -DISC_COMMAND_CLASS = "command_class" -DISC_COMPONENT = "component" -DISC_GENERIC_DEVICE_CLASS = "generic_device_class" -DISC_GENRE = "genre" -DISC_INDEX = "index" -DISC_INSTANCE = "instance" -DISC_NODE_ID = "node_id" -DISC_OPTIONAL = "optional" -DISC_PRIMARY = "primary" -DISC_SPECIFIC_DEVICE_CLASS = "specific_device_class" -DISC_TYPE = "type" -DISC_VALUES = "values" diff --git a/homeassistant/components/ozw/cover.py b/homeassistant/components/ozw/cover.py deleted file mode 100644 index 780e19c2ccd..00000000000 --- a/homeassistant/components/ozw/cover.py +++ /dev/null @@ -1,127 +0,0 @@ -"""Support for Z-Wave cover devices.""" -from openzwavemqtt.const import CommandClass - -from homeassistant.components.cover import ( - ATTR_POSITION, - DOMAIN as COVER_DOMAIN, - SUPPORT_CLOSE, - SUPPORT_OPEN, - CoverDeviceClass, - CoverEntity, -) -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity_platform import AddEntitiesCallback - -from .const import DATA_UNSUBSCRIBE, DOMAIN -from .entity import ZWaveDeviceEntity - -SUPPORT_GARAGE = SUPPORT_OPEN | SUPPORT_CLOSE -VALUE_SELECTED_ID = "Selected_id" -PRESS_BUTTON = True -RELEASE_BUTTON = False - - -async def async_setup_entry( - hass: HomeAssistant, - config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up Z-Wave Cover from Config Entry.""" - - @callback - def async_add_cover(values): - """Add Z-Wave Cover.""" - if values.primary.command_class == CommandClass.BARRIER_OPERATOR: - cover = ZwaveGarageDoorBarrier(values) - else: - cover = ZWaveCoverEntity(values) - - async_add_entities([cover]) - - hass.data[DOMAIN][config_entry.entry_id][DATA_UNSUBSCRIBE].append( - async_dispatcher_connect(hass, f"{DOMAIN}_new_{COVER_DOMAIN}", async_add_cover) - ) - - -def percent_to_zwave_position(value): - """Convert position in 0-100 scale to 0-99 scale. - - `value` -- (int) Position byte value from 0-100. - """ - if value > 0: - return max(1, round((value / 100) * 99)) - return 0 - - -class ZWaveCoverEntity(ZWaveDeviceEntity, CoverEntity): - """Representation of a Z-Wave Cover device.""" - - @property - def is_closed(self): - """Return true if cover is closed.""" - return self.values.primary.value == 0 - - @property - def current_cover_position(self): - """Return the current position of cover where 0 means closed and 100 is fully open.""" - return round((self.values.primary.value / 99) * 100) - - async def async_set_cover_position(self, **kwargs): - """Move the cover to a specific position.""" - self.values.primary.send_value(percent_to_zwave_position(kwargs[ATTR_POSITION])) - - async def async_open_cover(self, **kwargs): - """Open the cover.""" - self.values.open.send_value(PRESS_BUTTON) - - async def async_close_cover(self, **kwargs): - """Close cover.""" - self.values.close.send_value(PRESS_BUTTON) - - async def async_stop_cover(self, **kwargs): - """Stop cover.""" - # Need to issue both buttons release since qt-openzwave implements idempotency - # keeping internal state of model to trigger actual updates. We could also keep - # another state in Home Assistant to know which button to release, - # but this implementation is simpler. - self.values.open.send_value(RELEASE_BUTTON) - self.values.close.send_value(RELEASE_BUTTON) - - -class ZwaveGarageDoorBarrier(ZWaveDeviceEntity, CoverEntity): - """Representation of a barrier operator Zwave garage door device.""" - - @property - def supported_features(self): - """Flag supported features.""" - return SUPPORT_GARAGE - - @property - def device_class(self): - """Return the class of this device, from CoverDeviceClass.""" - return CoverDeviceClass.GARAGE - - @property - def is_opening(self): - """Return true if cover is in an opening state.""" - return self.values.primary.value[VALUE_SELECTED_ID] == 3 - - @property - def is_closing(self): - """Return true if cover is in a closing state.""" - return self.values.primary.value[VALUE_SELECTED_ID] == 1 - - @property - def is_closed(self): - """Return the current position of Zwave garage door.""" - return self.values.primary.value[VALUE_SELECTED_ID] == 0 - - async def async_close_cover(self, **kwargs): - """Close the garage door.""" - self.values.primary.send_value(0) - - async def async_open_cover(self, **kwargs): - """Open the garage door.""" - self.values.primary.send_value(4) diff --git a/homeassistant/components/ozw/discovery.py b/homeassistant/components/ozw/discovery.py deleted file mode 100644 index 67e3442cf5f..00000000000 --- a/homeassistant/components/ozw/discovery.py +++ /dev/null @@ -1,356 +0,0 @@ -"""Map Z-Wave nodes and values to Home Assistant entities.""" -import openzwavemqtt.const as const_ozw -from openzwavemqtt.const import CommandClass, ValueGenre, ValueIndex, ValueType - -from . import const - -DISCOVERY_SCHEMAS = ( - { # Binary sensors - const.DISC_COMPONENT: "binary_sensor", - const.DISC_VALUES: { - const.DISC_PRIMARY: { - const.DISC_COMMAND_CLASS: CommandClass.SENSOR_BINARY, - const.DISC_TYPE: ValueType.BOOL, - const.DISC_GENRE: ValueGenre.USER, - }, - "off_delay": { - const.DISC_COMMAND_CLASS: CommandClass.CONFIGURATION, - const.DISC_INDEX: 9, - const.DISC_OPTIONAL: True, - }, - }, - }, - { # Notification CommandClass translates to binary_sensor - const.DISC_COMPONENT: "binary_sensor", - const.DISC_VALUES: { - const.DISC_PRIMARY: { - const.DISC_COMMAND_CLASS: CommandClass.NOTIFICATION, - const.DISC_GENRE: ValueGenre.USER, - const.DISC_TYPE: (ValueType.BOOL, ValueType.LIST), - } - }, - }, - { # Z-Wave Thermostat device translates to Climate entity - const.DISC_COMPONENT: "climate", - const.DISC_GENERIC_DEVICE_CLASS: ( - const_ozw.GENERIC_TYPE_THERMOSTAT, - const_ozw.GENERIC_TYPE_SENSOR_MULTILEVEL, - ), - const.DISC_SPECIFIC_DEVICE_CLASS: ( - const_ozw.SPECIFIC_TYPE_THERMOSTAT_GENERAL, - const_ozw.SPECIFIC_TYPE_THERMOSTAT_GENERAL_V2, - const_ozw.SPECIFIC_TYPE_SETBACK_THERMOSTAT, - const_ozw.SPECIFIC_TYPE_THERMOSTAT_HEATING, - const_ozw.SPECIFIC_TYPE_SETPOINT_THERMOSTAT, - const_ozw.SPECIFIC_TYPE_NOT_USED, - ), - const.DISC_VALUES: { - const.DISC_PRIMARY: { - const.DISC_COMMAND_CLASS: (CommandClass.THERMOSTAT_MODE,) - }, - "mode": { - const.DISC_COMMAND_CLASS: (CommandClass.THERMOSTAT_MODE,), - const.DISC_OPTIONAL: True, - }, - "temperature": { - const.DISC_COMMAND_CLASS: (CommandClass.SENSOR_MULTILEVEL,), - const.DISC_INDEX: (1,), - const.DISC_OPTIONAL: True, - }, - "fan_mode": { - const.DISC_COMMAND_CLASS: (CommandClass.THERMOSTAT_FAN_MODE,), - const.DISC_OPTIONAL: True, - }, - "operating_state": { - const.DISC_COMMAND_CLASS: (CommandClass.THERMOSTAT_OPERATING_STATE,), - const.DISC_OPTIONAL: True, - }, - "fan_action": { - const.DISC_COMMAND_CLASS: (CommandClass.THERMOSTAT_FAN_STATE,), - const.DISC_OPTIONAL: True, - }, - "valve_position": { - const.DISC_COMMAND_CLASS: (CommandClass.SWITCH_MULTILEVEL,), - const.DISC_INDEX: (0,), - const.DISC_OPTIONAL: True, - }, - "setpoint_heating": { - const.DISC_COMMAND_CLASS: (CommandClass.THERMOSTAT_SETPOINT,), - const.DISC_INDEX: (1,), - const.DISC_OPTIONAL: True, - }, - "setpoint_cooling": { - const.DISC_COMMAND_CLASS: (CommandClass.THERMOSTAT_SETPOINT,), - const.DISC_INDEX: (2,), - const.DISC_OPTIONAL: True, - }, - "setpoint_furnace": { - const.DISC_COMMAND_CLASS: (CommandClass.THERMOSTAT_SETPOINT,), - const.DISC_INDEX: (7,), - const.DISC_OPTIONAL: True, - }, - "setpoint_dry_air": { - const.DISC_COMMAND_CLASS: (CommandClass.THERMOSTAT_SETPOINT,), - const.DISC_INDEX: (8,), - const.DISC_OPTIONAL: True, - }, - "setpoint_moist_air": { - const.DISC_COMMAND_CLASS: (CommandClass.THERMOSTAT_SETPOINT,), - const.DISC_INDEX: (9,), - const.DISC_OPTIONAL: True, - }, - "setpoint_auto_changeover": { - const.DISC_COMMAND_CLASS: (CommandClass.THERMOSTAT_SETPOINT,), - const.DISC_INDEX: (10,), - const.DISC_OPTIONAL: True, - }, - "setpoint_eco_heating": { - const.DISC_COMMAND_CLASS: (CommandClass.THERMOSTAT_SETPOINT,), - const.DISC_INDEX: (11,), - const.DISC_OPTIONAL: True, - }, - "setpoint_eco_cooling": { - const.DISC_COMMAND_CLASS: (CommandClass.THERMOSTAT_SETPOINT,), - const.DISC_INDEX: (12,), - const.DISC_OPTIONAL: True, - }, - "setpoint_away_heating": { - const.DISC_COMMAND_CLASS: (CommandClass.THERMOSTAT_SETPOINT,), - const.DISC_INDEX: (13,), - const.DISC_OPTIONAL: True, - }, - "setpoint_away_cooling": { - const.DISC_COMMAND_CLASS: (CommandClass.THERMOSTAT_SETPOINT,), - const.DISC_INDEX: (14,), - const.DISC_OPTIONAL: True, - }, - "setpoint_full_power": { - const.DISC_COMMAND_CLASS: (CommandClass.THERMOSTAT_SETPOINT,), - const.DISC_INDEX: (15,), - const.DISC_OPTIONAL: True, - }, - }, - }, - { # Z-Wave Thermostat device without mode support - const.DISC_COMPONENT: "climate", - const.DISC_GENERIC_DEVICE_CLASS: (const_ozw.GENERIC_TYPE_THERMOSTAT,), - const.DISC_SPECIFIC_DEVICE_CLASS: ( - const_ozw.SPECIFIC_TYPE_SETPOINT_THERMOSTAT, - const_ozw.SPECIFIC_TYPE_NOT_USED, - ), - const.DISC_VALUES: { - const.DISC_PRIMARY: { - const.DISC_COMMAND_CLASS: (CommandClass.THERMOSTAT_SETPOINT,) - }, - "temperature": { - const.DISC_COMMAND_CLASS: (CommandClass.SENSOR_MULTILEVEL,), - const.DISC_INDEX: (1,), - const.DISC_OPTIONAL: True, - }, - "operating_state": { - const.DISC_COMMAND_CLASS: (CommandClass.THERMOSTAT_OPERATING_STATE,), - const.DISC_OPTIONAL: True, - }, - "valve_position": { - const.DISC_COMMAND_CLASS: (CommandClass.SWITCH_MULTILEVEL,), - const.DISC_INDEX: (0,), - const.DISC_OPTIONAL: True, - }, - "setpoint_heating": { - const.DISC_COMMAND_CLASS: (CommandClass.THERMOSTAT_SETPOINT,), - const.DISC_INDEX: (1,), - const.DISC_OPTIONAL: True, - }, - }, - }, - { # Rollershutter - const.DISC_COMPONENT: "cover", - const.DISC_GENERIC_DEVICE_CLASS: (const_ozw.GENERIC_TYPE_SWITCH_MULTILEVEL,), - const.DISC_SPECIFIC_DEVICE_CLASS: ( - const_ozw.SPECIFIC_TYPE_CLASS_A_MOTOR_CONTROL, - const_ozw.SPECIFIC_TYPE_CLASS_B_MOTOR_CONTROL, - const_ozw.SPECIFIC_TYPE_CLASS_C_MOTOR_CONTROL, - const_ozw.SPECIFIC_TYPE_MOTOR_MULTIPOSITION, - const_ozw.SPECIFIC_TYPE_SECURE_BARRIER_ADDON, - const_ozw.SPECIFIC_TYPE_SECURE_DOOR, - ), - const.DISC_VALUES: { - const.DISC_PRIMARY: { - const.DISC_COMMAND_CLASS: CommandClass.SWITCH_MULTILEVEL, - const.DISC_INDEX: ValueIndex.SWITCH_MULTILEVEL_LEVEL, - const.DISC_GENRE: ValueGenre.USER, - }, - "open": { - const.DISC_COMMAND_CLASS: CommandClass.SWITCH_MULTILEVEL, - const.DISC_INDEX: ValueIndex.SWITCH_MULTILEVEL_BRIGHT, - const.DISC_OPTIONAL: True, - }, - "close": { - const.DISC_COMMAND_CLASS: CommandClass.SWITCH_MULTILEVEL, - const.DISC_INDEX: ValueIndex.SWITCH_MULTILEVEL_DIM, - const.DISC_OPTIONAL: True, - }, - }, - }, - { # Garage Door Barrier - const.DISC_COMPONENT: "cover", - const.DISC_GENERIC_DEVICE_CLASS: (const_ozw.GENERIC_TYPE_ENTRY_CONTROL,), - const.DISC_SPECIFIC_DEVICE_CLASS: ( - const_ozw.SPECIFIC_TYPE_SECURE_BARRIER_ADDON, - ), - const.DISC_VALUES: { - const.DISC_PRIMARY: { - const.DISC_COMMAND_CLASS: CommandClass.BARRIER_OPERATOR, - const.DISC_INDEX: ValueIndex.BARRIER_OPERATOR_LABEL, - }, - }, - }, - { # Fan - const.DISC_COMPONENT: "fan", - const.DISC_GENERIC_DEVICE_CLASS: const_ozw.GENERIC_TYPE_SWITCH_MULTILEVEL, - const.DISC_SPECIFIC_DEVICE_CLASS: const_ozw.SPECIFIC_TYPE_FAN_SWITCH, - const.DISC_VALUES: { - const.DISC_PRIMARY: { - const.DISC_COMMAND_CLASS: CommandClass.SWITCH_MULTILEVEL, - const.DISC_INDEX: ValueIndex.SWITCH_MULTILEVEL_LEVEL, - const.DISC_TYPE: ValueType.BYTE, - }, - }, - }, - { # Light - const.DISC_COMPONENT: "light", - const.DISC_GENERIC_DEVICE_CLASS: ( - const_ozw.GENERIC_TYPE_SWITCH_MULTILEVEL, - const_ozw.GENERIC_TYPE_SWITCH_REMOTE, - ), - const.DISC_SPECIFIC_DEVICE_CLASS: ( - const_ozw.SPECIFIC_TYPE_POWER_SWITCH_MULTILEVEL, - const_ozw.SPECIFIC_TYPE_SCENE_SWITCH_MULTILEVEL, - const_ozw.SPECIFIC_TYPE_COLOR_TUNABLE_BINARY, - const_ozw.SPECIFIC_TYPE_COLOR_TUNABLE_MULTILEVEL, - const_ozw.SPECIFIC_TYPE_NOT_USED, - ), - const.DISC_VALUES: { - const.DISC_PRIMARY: { - const.DISC_COMMAND_CLASS: (CommandClass.SWITCH_MULTILEVEL,), - const.DISC_INDEX: ValueIndex.SWITCH_MULTILEVEL_LEVEL, - const.DISC_TYPE: ValueType.BYTE, - }, - "dimming_duration": { - const.DISC_COMMAND_CLASS: (CommandClass.SWITCH_MULTILEVEL,), - const.DISC_INDEX: ValueIndex.SWITCH_MULTILEVEL_DURATION, - const.DISC_OPTIONAL: True, - }, - "color": { - const.DISC_COMMAND_CLASS: (CommandClass.SWITCH_COLOR,), - const.DISC_INDEX: ValueIndex.SWITCH_COLOR_COLOR, - const.DISC_OPTIONAL: True, - }, - "color_channels": { - const.DISC_COMMAND_CLASS: (CommandClass.SWITCH_COLOR,), - const.DISC_INDEX: ValueIndex.SWITCH_COLOR_CHANNELS, - const.DISC_OPTIONAL: True, - }, - "min_kelvin": { - const.DISC_COMMAND_CLASS: (CommandClass.CONFIGURATION,), - const.DISC_INDEX: 81, # PR for upstream to add SWITCH_COLOR_CT_WARM - const.DISC_TYPE: ValueType.INT, - const.DISC_OPTIONAL: True, - }, - "max_kelvin": { - const.DISC_COMMAND_CLASS: (CommandClass.CONFIGURATION,), - const.DISC_INDEX: 82, # PR for upstream to add SWITCH_COLOR_CT_COLD - const.DISC_TYPE: ValueType.INT, - const.DISC_OPTIONAL: True, - }, - }, - }, - { # All other text/numeric sensors - const.DISC_COMPONENT: "sensor", - const.DISC_VALUES: { - const.DISC_PRIMARY: { - const.DISC_COMMAND_CLASS: ( - CommandClass.SENSOR_MULTILEVEL, - CommandClass.METER, - CommandClass.ALARM, - CommandClass.SENSOR_ALARM, - CommandClass.INDICATOR, - CommandClass.BATTERY, - CommandClass.NOTIFICATION, - CommandClass.BASIC, - ), - const.DISC_TYPE: ( - ValueType.DECIMAL, - ValueType.INT, - ValueType.STRING, - ValueType.BYTE, - ValueType.LIST, - ), - } - }, - }, - { # Switch platform - const.DISC_COMPONENT: "switch", - const.DISC_VALUES: { - const.DISC_PRIMARY: { - const.DISC_COMMAND_CLASS: (CommandClass.SWITCH_BINARY,), - const.DISC_TYPE: ValueType.BOOL, - const.DISC_GENRE: ValueGenre.USER, - } - }, - }, - { # Lock platform - const.DISC_COMPONENT: "lock", - const.DISC_VALUES: { - const.DISC_PRIMARY: { - const.DISC_COMMAND_CLASS: (CommandClass.DOOR_LOCK,), - const.DISC_TYPE: ValueType.BOOL, - const.DISC_GENRE: ValueGenre.USER, - } - }, - }, -) - - -def check_node_schema(node, schema): - """Check if node matches the passed node schema.""" - if const.DISC_NODE_ID in schema and node.node_id not in schema[const.DISC_NODE_ID]: - return False - if const.DISC_GENERIC_DEVICE_CLASS in schema and not eq_or_in( - node.node_generic, schema[const.DISC_GENERIC_DEVICE_CLASS] - ): - return False - if const.DISC_SPECIFIC_DEVICE_CLASS in schema and not eq_or_in( - node.node_specific, schema[const.DISC_SPECIFIC_DEVICE_CLASS] - ): - return False - return True - - -def check_value_schema(value, schema): - """Check if the value matches the passed value schema.""" - if const.DISC_COMMAND_CLASS in schema and not eq_or_in( - value.parent.command_class_id, schema[const.DISC_COMMAND_CLASS] - ): - return False - if const.DISC_TYPE in schema and not eq_or_in(value.type, schema[const.DISC_TYPE]): - return False - if const.DISC_GENRE in schema and not eq_or_in( - value.genre, schema[const.DISC_GENRE] - ): - return False - if const.DISC_INDEX in schema and not eq_or_in( - value.index, schema[const.DISC_INDEX] - ): - return False - if const.DISC_INSTANCE in schema and not eq_or_in( - value.instance, schema[const.DISC_INSTANCE] - ): - return False - - return True - - -def eq_or_in(val, options): - """Return True if options contains value or if value is equal to options.""" - return val in options if isinstance(options, tuple) else val == options diff --git a/homeassistant/components/ozw/entity.py b/homeassistant/components/ozw/entity.py deleted file mode 100644 index 790b60aed69..00000000000 --- a/homeassistant/components/ozw/entity.py +++ /dev/null @@ -1,304 +0,0 @@ -"""Generic Z-Wave Entity Classes.""" - -import copy -import logging - -from openzwavemqtt.const import ( - EVENT_INSTANCE_STATUS_CHANGED, - EVENT_VALUE_CHANGED, - OZW_READY_STATES, - CommandClass, - ValueIndex, -) -from openzwavemqtt.models.node import OZWNode -from openzwavemqtt.models.value import OZWValue - -from homeassistant.const import ATTR_NAME, ATTR_SW_VERSION, ATTR_VIA_DEVICE -from homeassistant.core import callback -from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, - async_dispatcher_send, -) -from homeassistant.helpers.entity import DeviceInfo, Entity - -from . import const -from .const import DOMAIN, PLATFORMS -from .discovery import check_node_schema, check_value_schema - -_LOGGER = logging.getLogger(__name__) -OZW_READY_STATES_VALUES = {st.value for st in OZW_READY_STATES} - - -class ZWaveDeviceEntityValues: - """Manages entity access to the underlying Z-Wave value objects.""" - - def __init__(self, hass, options, schema, primary_value): - """Initialize the values object with the passed entity schema.""" - self._hass = hass - self._entity_created = False - self._schema = copy.deepcopy(schema) - self._values = {} - self.options = options - - # Go through values listed in the discovery schema, initialize them, - # and add a check to the schema to make sure the Instance matches. - for name, disc_settings in self._schema[const.DISC_VALUES].items(): - self._values[name] = None - disc_settings[const.DISC_INSTANCE] = (primary_value.instance,) - - self._values[const.DISC_PRIMARY] = primary_value - self._node = primary_value.node - self._schema[const.DISC_NODE_ID] = [self._node.node_id] - - def async_setup(self): - """Set up values instance.""" - # Check values that have already been discovered for node - # and see if they match the schema and need added to the entity. - for value in self._node.values(): - self.async_check_value(value) - - # Check if all the _required_ values in the schema are present and - # create the entity. - self._async_check_entity_ready() - - def __getattr__(self, name): - """Get the specified value for this entity.""" - return self._values.get(name, None) - - def __iter__(self): - """Allow iteration over all values.""" - return iter(self._values.values()) - - def __contains__(self, name): - """Check if the specified name/key exists in the values.""" - return name in self._values - - @callback - def async_check_value(self, value): - """Check if the new value matches a missing value for this entity. - - If a match is found, it is added to the values mapping. - """ - # Make sure the node matches the schema for this entity. - if not check_node_schema(value.node, self._schema): - return - - # Go through the possible values for this entity defined by the schema. - for name, name_value in self._values.items(): - # Skip if it's already been added. - if name_value is not None: - continue - # Skip if the value doesn't match the schema. - if not check_value_schema(value, self._schema[const.DISC_VALUES][name]): - continue - - # Add value to mapping. - self._values[name] = value - - # If the entity has already been created, notify it of the new value. - if self._entity_created: - async_dispatcher_send( - self._hass, f"{DOMAIN}_{self.values_id}_value_added" - ) - - # Check if entity has all required values and create the entity if needed. - self._async_check_entity_ready() - - @callback - def _async_check_entity_ready(self): - """Check if all required values are discovered and create entity.""" - # Abort if the entity has already been created - if self._entity_created: - return - - # Go through values defined in the schema and abort if a required value is missing. - for name, disc_settings in self._schema[const.DISC_VALUES].items(): - if self._values[name] is None and not disc_settings.get( - const.DISC_OPTIONAL - ): - return - - # We have all the required values, so create the entity. - component = self._schema[const.DISC_COMPONENT] - - _LOGGER.debug( - "Adding Node_id=%s Generic_command_class=%s, " - "Specific_command_class=%s, " - "Command_class=%s, Index=%s, Value type=%s, " - "Genre=%s as %s", - self._node.node_id, - self._node.node_generic, - self._node.node_specific, - self.primary.command_class, - self.primary.index, - self.primary.type, - self.primary.genre, - component, - ) - self._entity_created = True - - if component in PLATFORMS: - async_dispatcher_send(self._hass, f"{DOMAIN}_new_{component}", self) - - @property - def values_id(self): - """Identification for this values collection.""" - return create_value_id(self.primary) - - -class ZWaveDeviceEntity(Entity): - """Generic Entity Class for a Z-Wave Device.""" - - def __init__(self, values): - """Initialize a generic Z-Wave device entity.""" - self.values = values - self.options = values.options - - @callback - def on_value_update(self): - """Call when a value is added/updated in the entity EntityValues Collection. - - To be overridden by platforms needing this event. - """ - - async def async_added_to_hass(self): - """Call when entity is added.""" - # Add dispatcher and OZW listeners callbacks. - # Add to on_remove so they will be cleaned up on entity removal. - self.async_on_remove( - self.options.listen(EVENT_VALUE_CHANGED, self._value_changed) - ) - self.async_on_remove( - self.options.listen(EVENT_INSTANCE_STATUS_CHANGED, self._instance_updated) - ) - self.async_on_remove( - async_dispatcher_connect( - self.hass, const.SIGNAL_DELETE_ENTITY, self._delete_callback - ) - ) - self.async_on_remove( - async_dispatcher_connect( - self.hass, - f"{DOMAIN}_{self.values.values_id}_value_added", - self._value_added, - ) - ) - - @property - def device_info(self) -> DeviceInfo: - """Return device information for the device registry.""" - node = self.values.primary.node - node_instance = self.values.primary.instance - dev_id = create_device_id(node, self.values.primary.instance) - node_firmware = node.get_value( - CommandClass.VERSION, ValueIndex.VERSION_APPLICATION - ) - device_info = DeviceInfo( - identifiers={(DOMAIN, dev_id)}, - name=create_device_name(node), - manufacturer=node.node_manufacturer_name, - model=node.node_product_name, - ) - if node_firmware is not None: - device_info[ATTR_SW_VERSION] = node_firmware.value - - # device with multiple instances is split up into virtual devices for each instance - if node_instance > 1: - parent_dev_id = create_device_id(node) - device_info[ATTR_NAME] += f" - Instance {node_instance}" - device_info[ATTR_VIA_DEVICE] = (DOMAIN, parent_dev_id) - return device_info - - @property - def extra_state_attributes(self): - """Return the device specific state attributes.""" - return {const.ATTR_NODE_ID: self.values.primary.node.node_id} - - @property - def name(self): - """Return the name of the entity.""" - node = self.values.primary.node - return f"{create_device_name(node)}: {self.values.primary.label}" - - @property - def unique_id(self): - """Return the unique_id of the entity.""" - return self.values.values_id - - @property - def available(self) -> bool: - """Return entity availability.""" - # Use OZW Daemon status for availability. - instance_status = self.values.primary.ozw_instance.get_status() - return instance_status and instance_status.status in OZW_READY_STATES_VALUES - - @callback - def _value_changed(self, value): - """Call when a value from ZWaveDeviceEntityValues is changed. - - Should not be overridden by subclasses. - """ - if value.value_id_key in (v.value_id_key for v in self.values if v): - self.on_value_update() - self.async_write_ha_state() - - @callback - def _value_added(self): - """Call when a value from ZWaveDeviceEntityValues is added. - - Should not be overridden by subclasses. - """ - self.on_value_update() - - @callback - def _instance_updated(self, new_status): - """Call when the instance status changes. - - Should not be overridden by subclasses. - """ - self.on_value_update() - self.async_write_ha_state() - - @property - def should_poll(self): - """No polling needed.""" - return False - - async def _delete_callback(self, values_id): - """Remove this entity.""" - if not self.values: - return # race condition: delete already requested - if values_id == self.values.values_id: - await self.async_remove(force_remove=True) - - -def create_device_name(node: OZWNode): - """Generate sensible (short) default device name from a OZWNode.""" - # Prefer custom name set by OZWAdmin if present - if node.node_name: - return node.node_name - # Prefer short devicename from metadata if present - if node.meta_data and node.meta_data.get("Name"): - return node.meta_data["Name"] - # Fallback to productname or devicetype strings - if node.node_product_name: - return node.node_product_name - if node.node_device_type_string: - return node.node_device_type_string - if node.node_specific_string: - return node.node_specific_string - # Last resort: use Node id (should never happen, but just in case) - return f"Node {node.id}" - - -def create_device_id(node: OZWNode, node_instance: int = 1): - """Generate unique device_id from a OZWNode.""" - ozw_instance = node.parent.id - dev_id = f"{ozw_instance}.{node.node_id}.{node_instance}" - return dev_id - - -def create_value_id(value: OZWValue): - """Generate unique value_id from an OZWValue.""" - # [OZW_INSTANCE_ID]-[NODE_ID]-[VALUE_ID_KEY] - return f"{value.node.parent.id}-{value.node.id}-{value.value_id_key}" diff --git a/homeassistant/components/ozw/fan.py b/homeassistant/components/ozw/fan.py deleted file mode 100644 index 8f6374cc8e5..00000000000 --- a/homeassistant/components/ozw/fan.py +++ /dev/null @@ -1,90 +0,0 @@ -"""Support for Z-Wave fans.""" -import math - -from homeassistant.components.fan import ( - DOMAIN as FAN_DOMAIN, - SUPPORT_SET_SPEED, - FanEntity, -) -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.util.percentage import ( - int_states_in_range, - percentage_to_ranged_value, - ranged_value_to_percentage, -) - -from .const import DATA_UNSUBSCRIBE, DOMAIN -from .entity import ZWaveDeviceEntity - -SUPPORTED_FEATURES = SUPPORT_SET_SPEED -SPEED_RANGE = (1, 99) # off is not included - - -async def async_setup_entry( - hass: HomeAssistant, - config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up Z-Wave Fan from Config Entry.""" - - @callback - def async_add_fan(values): - """Add Z-Wave Fan.""" - fan = ZwaveFan(values) - async_add_entities([fan]) - - hass.data[DOMAIN][config_entry.entry_id][DATA_UNSUBSCRIBE].append( - async_dispatcher_connect(hass, f"{DOMAIN}_new_{FAN_DOMAIN}", async_add_fan) - ) - - -class ZwaveFan(ZWaveDeviceEntity, FanEntity): - """Representation of a Z-Wave fan.""" - - async def async_set_percentage(self, percentage): - """Set the speed percentage of the fan.""" - if percentage is None: - # Value 255 tells device to return to previous value - zwave_speed = 255 - elif percentage == 0: - zwave_speed = 0 - else: - zwave_speed = math.ceil(percentage_to_ranged_value(SPEED_RANGE, percentage)) - self.values.primary.send_value(zwave_speed) - - async def async_turn_on( - self, speed=None, percentage=None, preset_mode=None, **kwargs - ): - """Turn the device on.""" - await self.async_set_percentage(percentage) - - async def async_turn_off(self, **kwargs): - """Turn the device off.""" - self.values.primary.send_value(0) - - @property - def is_on(self): - """Return true if device is on (speed above 0).""" - return self.values.primary.value > 0 - - @property - def percentage(self): - """Return the current speed. - - The Z-Wave speed value is a byte 0-255. 255 means previous value. - The normal range of the speed is 0-99. 0 means off. - """ - return ranged_value_to_percentage(SPEED_RANGE, self.values.primary.value) - - @property - def speed_count(self) -> int: - """Return the number of speeds the fan supports.""" - return int_states_in_range(SPEED_RANGE) - - @property - def supported_features(self): - """Flag supported features.""" - return SUPPORTED_FEATURES diff --git a/homeassistant/components/ozw/light.py b/homeassistant/components/ozw/light.py deleted file mode 100644 index a3c6798f40f..00000000000 --- a/homeassistant/components/ozw/light.py +++ /dev/null @@ -1,343 +0,0 @@ -"""Support for Z-Wave lights.""" -import logging - -from homeassistant.components.light import ( - ATTR_BRIGHTNESS, - ATTR_COLOR_TEMP, - ATTR_HS_COLOR, - ATTR_RGBW_COLOR, - ATTR_TRANSITION, - COLOR_MODE_BRIGHTNESS, - COLOR_MODE_COLOR_TEMP, - COLOR_MODE_HS, - COLOR_MODE_RGBW, - DOMAIN as LIGHT_DOMAIN, - SUPPORT_TRANSITION, - LightEntity, -) -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity_platform import AddEntitiesCallback -import homeassistant.util.color as color_util - -from .const import DATA_UNSUBSCRIBE, DOMAIN -from .entity import ZWaveDeviceEntity - -_LOGGER = logging.getLogger(__name__) - -ATTR_VALUE = "Value" -COLOR_CHANNEL_WARM_WHITE = 0x01 -COLOR_CHANNEL_COLD_WHITE = 0x02 -COLOR_CHANNEL_RED = 0x04 -COLOR_CHANNEL_GREEN = 0x08 -COLOR_CHANNEL_BLUE = 0x10 - - -async def async_setup_entry( - hass: HomeAssistant, - config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up Z-Wave Light from Config Entry.""" - - @callback - def async_add_light(values): - """Add Z-Wave Light.""" - light = ZwaveLight(values) - - async_add_entities([light]) - - hass.data[DOMAIN][config_entry.entry_id][DATA_UNSUBSCRIBE].append( - async_dispatcher_connect(hass, f"{DOMAIN}_new_{LIGHT_DOMAIN}", async_add_light) - ) - - -def byte_to_zwave_brightness(value): - """Convert brightness in 0-255 scale to 0-99 scale. - - `value` -- (int) Brightness byte value from 0-255. - """ - if value > 0: - return max(1, round((value / 255) * 99)) - return 0 - - -class ZwaveLight(ZWaveDeviceEntity, LightEntity): - """Representation of a Z-Wave light.""" - - def __init__(self, values): - """Initialize the light.""" - super().__init__(values) - self._color_channels = None - self._hs = None - self._rgbw_color = None - self._ct = None - self._attr_color_mode = None - self._attr_supported_features = 0 - self._attr_supported_color_modes = set() - self._min_mireds = 153 # 6500K as a safe default - self._max_mireds = 370 # 2700K as a safe default - - # make sure that supported features is correctly set - self.on_value_update() - - @callback - def on_value_update(self): - """Call when the underlying value(s) is added or updated.""" - if self.values.dimming_duration is not None: - self._attr_supported_features |= SUPPORT_TRANSITION - - if self.values.color_channels is not None: - # Support Color Temp if both white channels - if (self.values.color_channels.value & COLOR_CHANNEL_WARM_WHITE) and ( - self.values.color_channels.value & COLOR_CHANNEL_COLD_WHITE - ): - self._attr_supported_color_modes.add(COLOR_MODE_COLOR_TEMP) - self._attr_supported_color_modes.add(COLOR_MODE_HS) - - # Support White value if only a single white channel - if ((self.values.color_channels.value & COLOR_CHANNEL_WARM_WHITE) != 0) ^ ( - (self.values.color_channels.value & COLOR_CHANNEL_COLD_WHITE) != 0 - ): - self._attr_supported_color_modes.add(COLOR_MODE_RGBW) - - if not self._attr_supported_color_modes and self.values.color is not None: - self._attr_supported_color_modes.add(COLOR_MODE_HS) - - if not self._attr_supported_color_modes: - self._attr_supported_color_modes.add(COLOR_MODE_BRIGHTNESS) - # Default: Brightness (no color) - self._attr_color_mode = COLOR_MODE_BRIGHTNESS - - if self.values.color is not None: - self._calculate_color_values() - - @property - def brightness(self): - """Return the brightness of this light between 0..255. - - Zwave multilevel switches use a range of [0, 99] to control brightness. - """ - if "target" in self.values: - return round((self.values.target.value / 99) * 255) - return round((self.values.primary.value / 99) * 255) - - @property - def is_on(self): - """Return true if device is on (brightness above 0).""" - if "target" in self.values: - return self.values.target.value > 0 - return self.values.primary.value > 0 - - @property - def hs_color(self): - """Return the hs color.""" - return self._hs - - @property - def rgbw_color(self): - """Return the rgbw color.""" - return self._rgbw_color - - @property - def color_temp(self): - """Return the color temperature.""" - return self._ct - - @property - def min_mireds(self): - """Return the coldest color_temp that this light supports.""" - return self._min_mireds - - @property - def max_mireds(self): - """Return the warmest color_temp that this light supports.""" - return self._max_mireds - - @callback - def async_set_duration(self, **kwargs): - """Set the transition time for the brightness value. - - Zwave Dimming Duration values now use seconds as an - integer (max: 7620 seconds or 127 mins) - Build 1205 https://github.com/OpenZWave/open-zwave/commit/f81bc04 - """ - if self.values.dimming_duration is None: - return - - ozw_version = tuple( - int(x) - for x in self.values.primary.ozw_instance.get_status().openzwave_version.split( - "." - ) - ) - - if ATTR_TRANSITION not in kwargs: - # no transition specified by user, use defaults - new_value = 7621 # anything over 7620 uses the factory default - if ozw_version < (1, 6, 1205): - new_value = 255 # default for older version - - else: - # transition specified by user - new_value = int(max(0, min(7620, kwargs[ATTR_TRANSITION]))) - if ozw_version < (1, 6, 1205): - if (transition := kwargs[ATTR_TRANSITION]) <= 127: - new_value = int(transition) - else: - minutes = int(transition / 60) - _LOGGER.debug( - "Transition rounded to %d minutes for %s", - minutes, - self.entity_id, - ) - new_value = minutes + 128 - - # only send value if it differs from current - # this prevents a command for nothing - if self.values.dimming_duration.value != new_value: - self.values.dimming_duration.send_value(new_value) - - async def async_turn_on(self, **kwargs): - """Turn the device on.""" - self.async_set_duration(**kwargs) - - rgbw = None - hs_color = kwargs.get(ATTR_HS_COLOR) - rgbw_color = kwargs.get(ATTR_RGBW_COLOR) - color_temp = kwargs.get(ATTR_COLOR_TEMP) - - if hs_color is not None: - rgbw = "#" - for colorval in color_util.color_hs_to_RGB(*hs_color): - rgbw += f"{colorval:02x}" - if self._color_channels and self._color_channels & COLOR_CHANNEL_COLD_WHITE: - rgbw += "0000" - else: - # trim the CW value or it will not work correctly - rgbw += "00" - # white LED must be off in order for color to work - - elif rgbw_color is not None: - red = rgbw_color[0] - green = rgbw_color[1] - blue = rgbw_color[2] - white = rgbw_color[3] - if self._color_channels & COLOR_CHANNEL_WARM_WHITE: - # trim the CW value or it will not work correctly - rgbw = f"#{red:02x}{green:02x}{blue:02x}{white:02x}" - else: - rgbw = f"#{red:02x}{green:02x}{blue:02x}00{white:02x}" - - elif color_temp is not None: - # Limit color temp to min/max values - cold = max( - 0, - min( - 255, - round( - (self._max_mireds - color_temp) - / (self._max_mireds - self._min_mireds) - * 255 - ), - ), - ) - warm = 255 - cold - rgbw = f"#000000{warm:02x}{cold:02x}" - - if rgbw and self.values.color: - self.values.color.send_value(rgbw) - - # Zwave multilevel switches use a range of [0, 99] to control - # brightness. Level 255 means to set it to previous value. - if ATTR_BRIGHTNESS in kwargs: - brightness = kwargs[ATTR_BRIGHTNESS] - brightness = byte_to_zwave_brightness(brightness) - else: - brightness = 255 - - self.values.primary.send_value(brightness) - - async def async_turn_off(self, **kwargs): - """Turn the device off.""" - self.async_set_duration(**kwargs) - - self.values.primary.send_value(0) - - def _calculate_color_values(self): - """Parse color rgb and color temperature data.""" - # Color Data String - data = self.values.color.data[ATTR_VALUE] - - # RGB is always present in the OpenZWave color data string. - rgb = [int(data[1:3], 16), int(data[3:5], 16), int(data[5:7], 16)] - self._hs = color_util.color_RGB_to_hs(*rgb) - - # Light supports color, set color mode to hs - self._attr_color_mode = COLOR_MODE_HS - - if self.values.color_channels is None: - return - - # Color Channels - self._color_channels = self.values.color_channels.data[ATTR_VALUE] - - # Parse remaining color channels. OpenZWave appends white channels - # that are present. - index = 7 - temp_warm = 0 - temp_cold = 0 - - # Update color temp limits. - if self.values.min_kelvin: - self._max_mireds = color_util.color_temperature_kelvin_to_mired( - self.values.min_kelvin.data[ATTR_VALUE] - ) - if self.values.max_kelvin: - self._min_mireds = color_util.color_temperature_kelvin_to_mired( - self.values.max_kelvin.data[ATTR_VALUE] - ) - - # Warm white - if self._color_channels & COLOR_CHANNEL_WARM_WHITE: - white = int(data[index : index + 2], 16) - self._rgbw_color = [rgb[0], rgb[1], rgb[2], white] - temp_warm = white - # Light supports rgbw, set color mode to rgbw - self._attr_color_mode = COLOR_MODE_RGBW - - index += 2 - - # Cold white - if self._color_channels & COLOR_CHANNEL_COLD_WHITE: - white = int(data[index : index + 2], 16) - self._rgbw_color = [rgb[0], rgb[1], rgb[2], white] - temp_cold = white - # Light supports rgbw, set color mode to rgbw - self._attr_color_mode = COLOR_MODE_RGBW - - # Calculate color temps based on white LED status - if temp_cold or temp_warm: - self._ct = round( - self._max_mireds - - ((temp_cold / 255) * (self._max_mireds - self._min_mireds)) - ) - - if ( - self._color_channels & COLOR_CHANNEL_WARM_WHITE - and self._color_channels & COLOR_CHANNEL_COLD_WHITE - ): - # Light supports 5 channels, set color_mode to color_temp or hs - if rgb[0] == 0 and rgb[1] == 0 and rgb[2] == 0: - # Color channels turned off, set color mode to color_temp - self._attr_color_mode = COLOR_MODE_COLOR_TEMP - else: - self._attr_color_mode = COLOR_MODE_HS - - if not ( - self._color_channels & COLOR_CHANNEL_RED - or self._color_channels & COLOR_CHANNEL_GREEN - or self._color_channels & COLOR_CHANNEL_BLUE - ): - self._hs = None diff --git a/homeassistant/components/ozw/lock.py b/homeassistant/components/ozw/lock.py deleted file mode 100644 index ea6782c5c79..00000000000 --- a/homeassistant/components/ozw/lock.py +++ /dev/null @@ -1,105 +0,0 @@ -"""Representation of Z-Wave locks.""" -import logging - -from openzwavemqtt.const import ATTR_CODE_SLOT -from openzwavemqtt.exceptions import BaseOZWError -from openzwavemqtt.util.lock import clear_usercode, set_usercode -import voluptuous as vol - -from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN, LockEntity -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers import config_validation as cv, entity_platform -from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity_platform import AddEntitiesCallback - -from .const import DATA_UNSUBSCRIBE, DOMAIN -from .entity import ZWaveDeviceEntity - -ATTR_USERCODE = "usercode" - -SERVICE_SET_USERCODE = "set_usercode" -SERVICE_GET_USERCODE = "get_usercode" -SERVICE_CLEAR_USERCODE = "clear_usercode" - -_LOGGER = logging.getLogger(__name__) - - -async def async_setup_entry( - hass: HomeAssistant, - config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up Z-Wave lock from config entry.""" - - @callback - def async_add_lock(value): - """Add Z-Wave Lock.""" - lock = ZWaveLock(value) - - async_add_entities([lock]) - - hass.data[DOMAIN][config_entry.entry_id][DATA_UNSUBSCRIBE].append( - async_dispatcher_connect(hass, f"{DOMAIN}_new_{LOCK_DOMAIN}", async_add_lock) - ) - - platform = entity_platform.async_get_current_platform() - - platform.async_register_entity_service( - SERVICE_SET_USERCODE, - { - vol.Required(ATTR_CODE_SLOT): vol.Coerce(int), - vol.Required(ATTR_USERCODE): cv.string, - }, - "async_set_usercode", - ) - - platform.async_register_entity_service( - SERVICE_CLEAR_USERCODE, - {vol.Required(ATTR_CODE_SLOT): vol.Coerce(int)}, - "async_clear_usercode", - ) - - -def _call_util_lock_function(function, *args): - """Call an openzwavemqtt.util.lock function and return success of call.""" - try: - function(*args) - except BaseOZWError as err: - _LOGGER.error("%s: %s", type(err), err.args[0]) - return False - - return True - - -class ZWaveLock(ZWaveDeviceEntity, LockEntity): - """Representation of a Z-Wave lock.""" - - @property - def is_locked(self): - """Return a boolean for the state of the lock.""" - return bool(self.values.primary.value) - - async def async_lock(self, **kwargs): - """Lock the lock.""" - self.values.primary.send_value(True) - - async def async_unlock(self, **kwargs): - """Unlock the lock.""" - self.values.primary.send_value(False) - - @callback - def async_set_usercode(self, code_slot, usercode): - """Set the usercode to index X on the lock.""" - if _call_util_lock_function( - set_usercode, self.values.primary.node, code_slot, usercode - ): - _LOGGER.debug("User code at slot %s set", code_slot) - - @callback - def async_clear_usercode(self, code_slot): - """Clear usercode in slot X on the lock.""" - if _call_util_lock_function( - clear_usercode, self.values.primary.node, code_slot - ): - _LOGGER.info("Usercode at slot %s is cleared", code_slot) diff --git a/homeassistant/components/ozw/manifest.json b/homeassistant/components/ozw/manifest.json deleted file mode 100644 index 997fbbc5a70..00000000000 --- a/homeassistant/components/ozw/manifest.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "domain": "ozw", - "name": "OpenZWave (deprecated)", - "config_flow": true, - "documentation": "https://www.home-assistant.io/integrations/ozw", - "requirements": ["python-openzwave-mqtt[mqtt-client]==1.4.0"], - "after_dependencies": ["mqtt"], - "codeowners": ["@cgarwood", "@marcelveldt", "@MartinHjelmare"], - "iot_class": "local_push", - "loggers": ["openzwavemqtt"] -} diff --git a/homeassistant/components/ozw/sensor.py b/homeassistant/components/ozw/sensor.py deleted file mode 100644 index 0a109118153..00000000000 --- a/homeassistant/components/ozw/sensor.py +++ /dev/null @@ -1,164 +0,0 @@ -"""Representation of Z-Wave sensors.""" -import logging - -from openzwavemqtt.const import CommandClass, ValueType - -from homeassistant.components.sensor import ( - DOMAIN as SENSOR_DOMAIN, - SensorDeviceClass, - SensorEntity, -) -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity_platform import AddEntitiesCallback - -from .const import DATA_UNSUBSCRIBE, DOMAIN -from .entity import ZWaveDeviceEntity - -_LOGGER = logging.getLogger(__name__) - - -async def async_setup_entry( - hass: HomeAssistant, - config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up Z-Wave sensor from config entry.""" - - @callback - def async_add_sensor(value): - """Add Z-Wave Sensor.""" - # Basic Sensor types - if value.primary.type in ( - ValueType.BYTE, - ValueType.INT, - ValueType.SHORT, - ValueType.DECIMAL, - ): - sensor = ZWaveNumericSensor(value) - - elif value.primary.type == ValueType.LIST: - sensor = ZWaveListSensor(value) - - elif value.primary.type == ValueType.STRING: - sensor = ZWaveStringSensor(value) - - else: - _LOGGER.warning("Sensor not implemented for value %s", value.primary.label) - return - - async_add_entities([sensor]) - - hass.data[DOMAIN][config_entry.entry_id][DATA_UNSUBSCRIBE].append( - async_dispatcher_connect( - hass, f"{DOMAIN}_new_{SENSOR_DOMAIN}", async_add_sensor - ) - ) - - -class ZwaveSensorBase(ZWaveDeviceEntity, SensorEntity): - """Basic Representation of a Z-Wave sensor.""" - - @property - def device_class(self): - """Return the device class of the sensor.""" - if self.values.primary.command_class == CommandClass.BATTERY: - return SensorDeviceClass.BATTERY - if self.values.primary.command_class == CommandClass.METER: - return SensorDeviceClass.POWER - if "Temperature" in self.values.primary.label: - return SensorDeviceClass.TEMPERATURE - if "Illuminance" in self.values.primary.label: - return SensorDeviceClass.ILLUMINANCE - if "Humidity" in self.values.primary.label: - return SensorDeviceClass.HUMIDITY - if "Power" in self.values.primary.label: - return SensorDeviceClass.POWER - if "Energy" in self.values.primary.label: - return SensorDeviceClass.POWER - if "Electric" in self.values.primary.label: - return SensorDeviceClass.POWER - if "Pressure" in self.values.primary.label: - return SensorDeviceClass.PRESSURE - return None - - @property - def entity_registry_enabled_default(self) -> bool: - """Return if the entity should be enabled when first added to the entity registry.""" - # We hide some of the more advanced sensors by default to not overwhelm users - if self.values.primary.command_class in ( - CommandClass.BASIC, - CommandClass.INDICATOR, - CommandClass.NOTIFICATION, - ): - return False - return True - - @property - def force_update(self) -> bool: - """Force updates.""" - return True - - -class ZWaveStringSensor(ZwaveSensorBase): - """Representation of a Z-Wave sensor.""" - - @property - def native_value(self): - """Return state of the sensor.""" - return self.values.primary.value - - @property - def native_unit_of_measurement(self): - """Return unit of measurement the value is expressed in.""" - return self.values.primary.units - - @property - def entity_registry_enabled_default(self): - """Return if the entity should be enabled when first added to the entity registry.""" - return False - - -class ZWaveNumericSensor(ZwaveSensorBase): - """Representation of a Z-Wave sensor.""" - - @property - def native_value(self): - """Return state of the sensor.""" - return round(self.values.primary.value, 2) - - @property - def native_unit_of_measurement(self): - """Return unit of measurement the value is expressed in.""" - if self.values.primary.units == "C": - return TEMP_CELSIUS - if self.values.primary.units == "F": - return TEMP_FAHRENHEIT - - return self.values.primary.units - - -class ZWaveListSensor(ZwaveSensorBase): - """Representation of a Z-Wave list sensor.""" - - @property - def native_value(self): - """Return the state of the sensor.""" - # We use the id as value for backwards compatibility - return self.values.primary.value["Selected_id"] - - @property - def extra_state_attributes(self): - """Return the device specific state attributes.""" - attributes = super().extra_state_attributes - # add the value's label as property - attributes["label"] = self.values.primary.value["Selected"] - return attributes - - @property - def entity_registry_enabled_default(self) -> bool: - """Return if the entity should be enabled when first added to the entity registry.""" - # these sensors are only here for backwards compatibility, disable them by default - return False diff --git a/homeassistant/components/ozw/services.py b/homeassistant/components/ozw/services.py deleted file mode 100644 index b6e54b4baa1..00000000000 --- a/homeassistant/components/ozw/services.py +++ /dev/null @@ -1,134 +0,0 @@ -"""Methods and classes related to executing Z-Wave commands and publishing these to hass.""" -import logging - -from openzwavemqtt.const import ATTR_LABEL, ATTR_POSITION, ATTR_VALUE -from openzwavemqtt.util.node import get_node_from_manager, set_config_parameter -import voluptuous as vol - -from homeassistant.core import ServiceCall, callback -import homeassistant.helpers.config_validation as cv - -from . import const - -_LOGGER = logging.getLogger(__name__) - - -class ZWaveServices: - """Class that holds our services ( Zwave Commands) that should be published to hass.""" - - def __init__(self, hass, manager): - """Initialize with both hass and ozwmanager objects.""" - self._hass = hass - self._manager = manager - - @callback - def async_register(self): - """Register all our services.""" - self._hass.services.async_register( - const.DOMAIN, - const.SERVICE_ADD_NODE, - self.async_add_node, - schema=vol.Schema( - { - vol.Optional(const.ATTR_INSTANCE_ID, default=1): vol.Coerce(int), - vol.Optional(const.ATTR_SECURE, default=False): vol.Coerce(bool), - } - ), - ) - self._hass.services.async_register( - const.DOMAIN, - const.SERVICE_REMOVE_NODE, - self.async_remove_node, - schema=vol.Schema( - {vol.Optional(const.ATTR_INSTANCE_ID, default=1): vol.Coerce(int)} - ), - ) - self._hass.services.async_register( - const.DOMAIN, - const.SERVICE_CANCEL_COMMAND, - self.async_cancel_command, - schema=vol.Schema( - {vol.Optional(const.ATTR_INSTANCE_ID, default=1): vol.Coerce(int)} - ), - ) - - self._hass.services.async_register( - const.DOMAIN, - const.SERVICE_SET_CONFIG_PARAMETER, - self.async_set_config_parameter, - schema=vol.Schema( - { - vol.Optional(const.ATTR_INSTANCE_ID, default=1): vol.Coerce(int), - vol.Required(const.ATTR_NODE_ID): vol.Coerce(int), - vol.Required(const.ATTR_CONFIG_PARAMETER): vol.Coerce(int), - vol.Required(const.ATTR_CONFIG_VALUE): vol.Any( - vol.All( - cv.ensure_list, - [ - vol.All( - { - vol.Exclusive(ATTR_LABEL, "bit"): cv.string, - vol.Exclusive(ATTR_POSITION, "bit"): vol.Coerce( - int - ), - vol.Required(ATTR_VALUE): bool, - }, - cv.has_at_least_one_key(ATTR_LABEL, ATTR_POSITION), - ) - ], - ), - vol.Coerce(int), - bool, - cv.string, - ), - } - ), - ) - - @callback - def async_set_config_parameter(self, service: ServiceCall) -> None: - """Set a config parameter to a node.""" - instance_id = service.data[const.ATTR_INSTANCE_ID] - node_id = service.data[const.ATTR_NODE_ID] - param = service.data[const.ATTR_CONFIG_PARAMETER] - selection = service.data[const.ATTR_CONFIG_VALUE] - - # These function calls may raise an exception but that's ok because - # the exception will show in the UI to the user - node = get_node_from_manager(self._manager, instance_id, node_id) - payload = set_config_parameter(node, param, selection) - - _LOGGER.info( - "Setting configuration parameter %s on Node %s with value %s", - param, - node_id, - payload, - ) - - @callback - def async_add_node(self, service: ServiceCall) -> None: - """Enter inclusion mode on the controller.""" - instance_id = service.data[const.ATTR_INSTANCE_ID] - secure = service.data[const.ATTR_SECURE] - instance = self._manager.get_instance(instance_id) - if instance is None: - raise ValueError(f"No OpenZWave Instance with ID {instance_id}") - instance.add_node(secure) - - @callback - def async_remove_node(self, service: ServiceCall) -> None: - """Enter exclusion mode on the controller.""" - instance_id = service.data[const.ATTR_INSTANCE_ID] - instance = self._manager.get_instance(instance_id) - if instance is None: - raise ValueError(f"No OpenZWave Instance with ID {instance_id}") - instance.remove_node() - - @callback - def async_cancel_command(self, service: ServiceCall) -> None: - """Tell the controller to cancel an add or remove command.""" - instance_id = service.data[const.ATTR_INSTANCE_ID] - instance = self._manager.get_instance(instance_id) - if instance is None: - raise ValueError(f"No OpenZWave Instance with ID {instance_id}") - instance.cancel_controller_command() diff --git a/homeassistant/components/ozw/services.yaml b/homeassistant/components/ozw/services.yaml deleted file mode 100644 index c9c23023134..00000000000 --- a/homeassistant/components/ozw/services.yaml +++ /dev/null @@ -1,121 +0,0 @@ -# Describes the format for available Z-Wave services -add_node: - name: Add node - description: Add a new node to the Z-Wave network. - fields: - secure: - name: Secure - description: Add the new node with secure communications. Secure network key must be set, this process will fallback to add_node (unsecure) for unsupported devices. Note that unsecure devices can't directly talk to secure devices. - default: false - selector: - boolean: - instance_id: - name: Instance ID - description: The OZW Instance/Controller to use. - selector: - number: - min: 1 - max: 255 - -remove_node: - name: Remove node - description: Remove a node from the Z-Wave network. Will set the controller into exclusion mode. - fields: - instance_id: - name: Instance ID - description: The OZW Instance/Controller to use. - default: 1 - selector: - number: - min: 1 - max: 255 - -cancel_command: - name: Cancel command - description: Cancel a pending add or remove node command. - fields: - instance_id: - name: Instance ID - description: The OZW Instance/Controller to use. - default: 1 - selector: - number: - min: 1 - max: 255 - -set_config_parameter: - name: Set config parameter - description: Set a config parameter to a node on the Z-Wave network. - fields: - node_id: - name: Node ID - description: Node id of the device to set config parameter to. - required: true - selector: - number: - min: 1 - max: 255 - parameter: - name: Parameter - description: Parameter number to set. - required: true - selector: - number: - min: 1 - max: 255 - value: - name: Value - description: Value to set for parameter. (String value for list and bool parameters, integer for others). - required: true - example: 50268673 - selector: - text: - instance_id: - name: Instance ID - description: The OZW Instance/Controller to use. - default: 1 - selector: - number: - min: 1 - max: 255 - -clear_usercode: - name: Clear usercode - description: Clear a usercode from lock. - target: - entity: - integration: ozw - domain: lock - fields: - code_slot: - name: Code slot - description: Code slot to clear code from. - required: true - selector: - number: - min: 1 - max: 255 - -set_usercode: - name: Set usercode - description: Set a usercode to lock. - target: - entity: - integration: ozw - domain: lock - fields: - code_slot: - name: Code slot - description: Code slot to set the code. - required: true - selector: - number: - min: 1 - max: 255 - usercode: - name: Usercode - description: Code to set. - required: true - example: 1234 - selector: - text: diff --git a/homeassistant/components/ozw/strings.json b/homeassistant/components/ozw/strings.json deleted file mode 100644 index ed9816c57f2..00000000000 --- a/homeassistant/components/ozw/strings.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "config": { - "step": { - "on_supervisor": { - "title": "Select connection method", - "description": "Do you want to use the OpenZWave Supervisor add-on?", - "data": { "use_addon": "Use the OpenZWave Supervisor add-on" } - }, - "install_addon": { - "title": "The OpenZWave add-on installation has started" - }, - "start_addon": { - "title": "Enter the OpenZWave add-on configuration", - "data": { - "usb_path": "[%key:common::config_flow::data::usb_path%]", - "network_key": "Network Key" - } - }, - "hassio_confirm": { - "title": "Set up OpenZWave integration with the OpenZWave add-on" - } - }, - "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", - "addon_info_failed": "Failed to get OpenZWave add-on info.", - "addon_install_failed": "Failed to install the OpenZWave add-on.", - "addon_set_config_failed": "Failed to set OpenZWave configuration.", - "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", - "mqtt_required": "The MQTT integration is not set up" - }, - "error": { - "addon_start_failed": "Failed to start the OpenZWave add-on. Check the configuration." - }, - "progress": { - "install_addon": "Please wait while the OpenZWave add-on installation finishes. This can take several minutes." - } - } -} diff --git a/homeassistant/components/ozw/switch.py b/homeassistant/components/ozw/switch.py deleted file mode 100644 index 69ae1dfb379..00000000000 --- a/homeassistant/components/ozw/switch.py +++ /dev/null @@ -1,47 +0,0 @@ -"""Representation of Z-Wave switches.""" -from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN, SwitchEntity -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity_platform import AddEntitiesCallback - -from .const import DATA_UNSUBSCRIBE, DOMAIN -from .entity import ZWaveDeviceEntity - - -async def async_setup_entry( - hass: HomeAssistant, - config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up Z-Wave switch from config entry.""" - - @callback - def async_add_switch(value): - """Add Z-Wave Switch.""" - switch = ZWaveSwitch(value) - - async_add_entities([switch]) - - hass.data[DOMAIN][config_entry.entry_id][DATA_UNSUBSCRIBE].append( - async_dispatcher_connect( - hass, f"{DOMAIN}_new_{SWITCH_DOMAIN}", async_add_switch - ) - ) - - -class ZWaveSwitch(ZWaveDeviceEntity, SwitchEntity): - """Representation of a Z-Wave switch.""" - - @property - def is_on(self): - """Return a boolean for the state of the switch.""" - return bool(self.values.primary.value) - - async def async_turn_on(self, **kwargs): - """Turn the switch on.""" - self.values.primary.send_value(True) - - async def async_turn_off(self, **kwargs): - """Turn the switch off.""" - self.values.primary.send_value(False) diff --git a/homeassistant/components/ozw/translations/ca.json b/homeassistant/components/ozw/translations/ca.json deleted file mode 100644 index 9c9fc17e58e..00000000000 --- a/homeassistant/components/ozw/translations/ca.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "config": { - "abort": { - "addon_info_failed": "No s'ha pogut obtenir la informaci\u00f3 del complement OpenZWave.", - "addon_install_failed": "No s'ha pogut instal\u00b7lar el complement OpenZWave.", - "addon_set_config_failed": "No s'ha pogut establir la configuraci\u00f3 d'OpenZWave.", - "already_configured": "El dispositiu ja est\u00e0 configurat", - "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs", - "mqtt_required": "La integraci\u00f3 MQTT no est\u00e0 configurada", - "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3." - }, - "error": { - "addon_start_failed": "No s'ha pogut iniciar el complement OpenZWave. Comprova la configuraci\u00f3." - }, - "progress": { - "install_addon": "Espera mentre finalitza la instal\u00b7laci\u00f3 del complement OpenZWave. Pot tardar uns quants minuts." - }, - "step": { - "hassio_confirm": { - "title": "Configuraci\u00f3 de la integraci\u00f3 d'OpenZWave amb el complement OpenZWave" - }, - "install_addon": { - "title": "Ha comen\u00e7at la instal\u00b7laci\u00f3 del complement OpenZWave" - }, - "on_supervisor": { - "data": { - "use_addon": "Utilitza el complement OpenZWave Supervisor" - }, - "description": "Vols utilitzar el complement Supervisor d'OpenZWave?", - "title": "Selecciona el m\u00e8tode de connexi\u00f3" - }, - "start_addon": { - "data": { - "network_key": "Clau de xarxa", - "usb_path": "Ruta del dispositiu USB" - }, - "title": "Introdueix la configuraci\u00f3 del complement OpenZWave" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ozw/translations/cs.json b/homeassistant/components/ozw/translations/cs.json deleted file mode 100644 index d479efdf95f..00000000000 --- a/homeassistant/components/ozw/translations/cs.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "config": { - "abort": { - "addon_info_failed": "Nepoda\u0159ilo se z\u00edskat informace o dopl\u0148ku OpenZWave.", - "addon_install_failed": "Instalace dopl\u0148ku OpenZWave se nezda\u0159ila.", - "addon_set_config_failed": "Nepoda\u0159ilo se nastavit OpenZWave.", - "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", - "already_in_progress": "Konfigurace ji\u017e prob\u00edh\u00e1", - "mqtt_required": "Integrace MQTT nen\u00ed nastavena", - "single_instance_allowed": "Ji\u017e nastaveno. Je mo\u017en\u00e1 pouze jedin\u00e1 konfigurace." - }, - "error": { - "addon_start_failed": "Spu\u0161t\u011bn\u00ed dopl\u0148ku OpenZWave se nezda\u0159ilo. Zkontrolujte konfiguraci." - }, - "step": { - "hassio_confirm": { - "title": "Nastaven\u00ed integrace OpenZWave s dopl\u0148kem OpenZWave" - }, - "install_addon": { - "title": "Instalace dopl\u0148ku OpenZWave byla zah\u00e1jena." - }, - "on_supervisor": { - "data": { - "use_addon": "Pou\u017e\u00edt dopln\u011bk OpenZWave pro Supervisor" - }, - "description": "Chcete pou\u017e\u00edt dopln\u011bk OpenZWave pro Supervisor?", - "title": "Vyberte metodu p\u0159ipojen\u00ed" - }, - "start_addon": { - "data": { - "network_key": "S\u00ed\u0165ov\u00fd kl\u00ed\u010d", - "usb_path": "Cesta k USB za\u0159\u00edzen\u00ed" - }, - "title": "Zadejte konfiguraci dopl\u0148ku OpenZWave" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ozw/translations/de.json b/homeassistant/components/ozw/translations/de.json deleted file mode 100644 index c58c55c49ad..00000000000 --- a/homeassistant/components/ozw/translations/de.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "config": { - "abort": { - "addon_info_failed": "Fehler beim Abrufen von OpenZWave Add-on Informationen.", - "addon_install_failed": "Installation des OpenZWave Add-ons fehlgeschlagen.", - "addon_set_config_failed": "Setzen der OpenZWave Konfiguration fehlgeschlagen.", - "already_configured": "Ger\u00e4t ist bereits konfiguriert", - "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", - "mqtt_required": "Die MQTT-Integration ist nicht eingerichtet", - "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." - }, - "error": { - "addon_start_failed": "Fehler beim Starten des OpenZWave Add-ons. \u00dcberpr\u00fcfe die Konfiguration." - }, - "progress": { - "install_addon": "Bitte warten, bis die Installation des OpenZWave-Add-Ons abgeschlossen ist. Dies kann einige Minuten dauern." - }, - "step": { - "hassio_confirm": { - "title": "Richte die OpenZWave Integration mit dem OpenZWave Add-On ein" - }, - "install_addon": { - "title": "Die Installation des OpenZWave-Add-On wurde gestartet" - }, - "on_supervisor": { - "data": { - "use_addon": "Verwende das OpenZWave Supervisor Add-on" - }, - "description": "M\u00f6chtest du das OpenZWave Supervisor Add-on verwenden?", - "title": "Verbindungstyp ausw\u00e4hlen" - }, - "start_addon": { - "data": { - "network_key": "Netzwerk-Schl\u00fcssel", - "usb_path": "USB-Ger\u00e4te-Pfad" - }, - "title": "Gib die Konfiguration des OpenZWave Add-ons ein" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ozw/translations/el.json b/homeassistant/components/ozw/translations/el.json deleted file mode 100644 index 6708cec1358..00000000000 --- a/homeassistant/components/ozw/translations/el.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "config": { - "abort": { - "addon_info_failed": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03bb\u03ae\u03c8\u03b7\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03b9\u03ce\u03bd \u03c0\u03c1\u03cc\u03c3\u03b8\u03b5\u03c4\u03bf\u03c5 OpenZWave.", - "addon_install_failed": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03b5\u03b3\u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7\u03c2 \u03c4\u03bf\u03c5 \u03c0\u03c1\u03cc\u03c3\u03b8\u03b5\u03c4\u03bf\u03c5 OpenZWave.", - "addon_set_config_failed": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7\u03c2 \u03c0\u03b1\u03c1\u03b1\u03bc\u03ad\u03c4\u03c1\u03c9\u03bd OpenZWave.", - "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", - "already_in_progress": "\u0397 \u03c1\u03bf\u03ae \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7\u03c2 \u03b2\u03c1\u03af\u03c3\u03ba\u03b5\u03c4\u03b1\u03b9 \u03ae\u03b4\u03b7 \u03c3\u03b5 \u03b5\u03be\u03ad\u03bb\u03b9\u03be\u03b7", - "mqtt_required": "\u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 MQTT \u03b4\u03b5\u03bd \u03ad\u03c7\u03b5\u03b9 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af", - "single_instance_allowed": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b7\u03ba\u03b5 \u03ae\u03b4\u03b7. \u039c\u03cc\u03bd\u03bf \u03bc\u03af\u03b1 \u03c0\u03b1\u03c1\u03b1\u03bc\u03b5\u03c4\u03c1\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae." - }, - "error": { - "addon_start_failed": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03b5\u03ba\u03ba\u03af\u03bd\u03b7\u03c3\u03b7\u03c2 \u03c4\u03bf\u03c5 \u03c0\u03c1\u03cc\u03c3\u03b8\u03b5\u03c4\u03bf\u03c5 OpenZWave. \u0395\u03bb\u03ad\u03b3\u03be\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7." - }, - "progress": { - "install_addon": "\u03a0\u03b5\u03c1\u03b9\u03bc\u03ad\u03bd\u03b5\u03c4\u03b5 \u03bc\u03ad\u03c7\u03c1\u03b9 \u03bd\u03b1 \u03bf\u03bb\u03bf\u03ba\u03bb\u03b7\u03c1\u03c9\u03b8\u03b5\u03af \u03b7 \u03b5\u03b3\u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03c0\u03c1\u03cc\u03c3\u03b8\u03b5\u03c4\u03bf\u03c5 OpenZWave. \u0391\u03c5\u03c4\u03cc \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03b4\u03b9\u03b1\u03c1\u03ba\u03ad\u03c3\u03b5\u03b9 \u03b1\u03c1\u03ba\u03b5\u03c4\u03ac \u03bb\u03b5\u03c0\u03c4\u03ac." - }, - "step": { - "hassio_confirm": { - "title": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2 OpenZWave \u03bc\u03b5 \u03c4\u03bf \u03c0\u03c1\u03cc\u03c3\u03b8\u03b5\u03c4\u03bf OpenZWave" - }, - "install_addon": { - "title": "\u0397 \u03b5\u03b3\u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03c0\u03c1\u03cc\u03c3\u03b8\u03b5\u03c4\u03bf\u03c5 OpenZWave \u03ad\u03c7\u03b5\u03b9 \u03be\u03b5\u03ba\u03b9\u03bd\u03ae\u03c3\u03b5\u03b9" - }, - "on_supervisor": { - "data": { - "use_addon": "\u03a7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf \u03c0\u03c1\u03cc\u03c3\u03b8\u03b5\u03c4\u03bf OpenZWave Supervisor" - }, - "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf \u03c0\u03c1\u03cc\u03c3\u03b8\u03b5\u03c4\u03bf OpenZWave Supervisor;", - "title": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03bc\u03ad\u03b8\u03bf\u03b4\u03bf \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" - }, - "start_addon": { - "data": { - "network_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af \u03b4\u03b9\u03ba\u03c4\u03cd\u03bf\u03c5", - "usb_path": "\u0394\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 USB" - }, - "title": "\u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03c0\u03c1\u03cc\u03c3\u03b8\u03b5\u03c4\u03bf\u03c5 OpenZWave" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ozw/translations/en.json b/homeassistant/components/ozw/translations/en.json deleted file mode 100644 index 1c837e0bde5..00000000000 --- a/homeassistant/components/ozw/translations/en.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "config": { - "abort": { - "addon_info_failed": "Failed to get OpenZWave add-on info.", - "addon_install_failed": "Failed to install the OpenZWave add-on.", - "addon_set_config_failed": "Failed to set OpenZWave configuration.", - "already_configured": "Device is already configured", - "already_in_progress": "Configuration flow is already in progress", - "mqtt_required": "The MQTT integration is not set up", - "single_instance_allowed": "Already configured. Only a single configuration possible." - }, - "error": { - "addon_start_failed": "Failed to start the OpenZWave add-on. Check the configuration." - }, - "progress": { - "install_addon": "Please wait while the OpenZWave add-on installation finishes. This can take several minutes." - }, - "step": { - "hassio_confirm": { - "title": "Set up OpenZWave integration with the OpenZWave add-on" - }, - "install_addon": { - "title": "The OpenZWave add-on installation has started" - }, - "on_supervisor": { - "data": { - "use_addon": "Use the OpenZWave Supervisor add-on" - }, - "description": "Do you want to use the OpenZWave Supervisor add-on?", - "title": "Select connection method" - }, - "start_addon": { - "data": { - "network_key": "Network Key", - "usb_path": "USB Device Path" - }, - "title": "Enter the OpenZWave add-on configuration" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ozw/translations/es.json b/homeassistant/components/ozw/translations/es.json deleted file mode 100644 index f06c2896bc8..00000000000 --- a/homeassistant/components/ozw/translations/es.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "config": { - "abort": { - "addon_info_failed": "No se pudo obtener la informaci\u00f3n del complemento de OpenZWave.", - "addon_install_failed": "No se pudo instalar el complemento de OpenZWave.", - "addon_set_config_failed": "No se pudo establecer la configuraci\u00f3n de OpenZWave.", - "already_configured": "El dispositivo ya est\u00e1 configurado", - "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en proceso", - "mqtt_required": "La integraci\u00f3n de MQTT no est\u00e1 configurada", - "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." - }, - "error": { - "addon_start_failed": "No se pudo iniciar el complemento OpenZWave. Verifica la configuraci\u00f3n." - }, - "progress": { - "install_addon": "Espera mientras finaliza la instalaci\u00f3n del complemento OpenZWave. Esto puede tardar varios minutos." - }, - "step": { - "hassio_confirm": { - "title": "Configurar la integraci\u00f3n de OpenZWave con el complemento OpenZWave" - }, - "install_addon": { - "title": "La instalaci\u00f3n del complemento OpenZWave se ha iniciado" - }, - "on_supervisor": { - "data": { - "use_addon": "Usar el complemento de supervisor de OpenZWave" - }, - "description": "\u00bfQuiere utilizar el complemento de Supervisor de OpenZWave?", - "title": "Selecciona el m\u00e9todo de conexi\u00f3n" - }, - "start_addon": { - "data": { - "network_key": "Clave de red", - "usb_path": "Ruta del dispositivo USB" - }, - "title": "Introduce la configuraci\u00f3n del complemento OpenZWave" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ozw/translations/et.json b/homeassistant/components/ozw/translations/et.json deleted file mode 100644 index 6ddd2e7ab96..00000000000 --- a/homeassistant/components/ozw/translations/et.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "config": { - "abort": { - "addon_info_failed": "OpenZWave'i lisandmooduli teabe hankimine nurjus.", - "addon_install_failed": "OpenZWave'i lisandmooduli paigaldamine nurjus.", - "addon_set_config_failed": "OpenZWave'i konfiguratsiooni seadistamine eba\u00f5nnestus.", - "already_configured": "Seade on juba h\u00e4\u00e4lestatud", - "already_in_progress": "Seadistamine on juba k\u00e4imas", - "mqtt_required": "MQTT sidumine pole seadistatud", - "single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks seadistamine." - }, - "error": { - "addon_start_failed": "OpenZWave'i lisandmooduli k\u00e4ivitamine nurjus. Kontrolli s\u00e4tteid." - }, - "progress": { - "install_addon": "Palun oota kuni OpenZWave lisandmooduli paigaldus l\u00f5peb. See v\u00f5ib v\u00f5tta mitu minutit." - }, - "step": { - "hassio_confirm": { - "title": "Seadista OpenZWave'i sidumine OpenZWave lisandmooduli abil" - }, - "install_addon": { - "title": "OpenZWave lisandmooduli paigaldamine on alanud" - }, - "on_supervisor": { - "data": { - "use_addon": "Kasuta OpenZWave Supervisori lisandmoodulit" - }, - "description": "Kas soovid kasutada OpenZWave'i halduri lisandmoodulit?", - "title": "Vali \u00fchendusviis" - }, - "start_addon": { - "data": { - "network_key": "V\u00f5rgu v\u00f5ti", - "usb_path": "USB seadme rada" - }, - "title": "Sisesta OpenZWave'i lisandmooduli seaded" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ozw/translations/fr.json b/homeassistant/components/ozw/translations/fr.json deleted file mode 100644 index 0a0232dd91d..00000000000 --- a/homeassistant/components/ozw/translations/fr.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "config": { - "abort": { - "addon_info_failed": "Impossible d'obtenir les informations sur le module compl\u00e9mentaire OpenZWave.", - "addon_install_failed": "\u00c9chec de l'installation du module compl\u00e9mentaire OpenZWave.", - "addon_set_config_failed": "\u00c9chec de la configuration OpenZWave.", - "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", - "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours\u00e0", - "mqtt_required": "L'int\u00e9gration MQTT n'est pas configur\u00e9e", - "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." - }, - "error": { - "addon_start_failed": "\u00c9chec du d\u00e9marrage du module compl\u00e9mentaire OpenZWave. V\u00e9rifiez la configuration." - }, - "progress": { - "install_addon": "Veuillez patienter pendant que l'installation du module OpenZWave se termine. Cela peut prendre plusieurs minutes." - }, - "step": { - "hassio_confirm": { - "title": "Configurer l'int\u00e9gration d'OpenZWave avec le module compl\u00e9mentaire OpenZWave" - }, - "install_addon": { - "title": "L'installation du module compl\u00e9mentaire OpenZWave a commenc\u00e9" - }, - "on_supervisor": { - "data": { - "use_addon": "Utilisez le module compl\u00e9mentaire OpenZWave du Supervisor" - }, - "description": "Voulez-vous utiliser le module compl\u00e9mentaire OpenZWave du Supervisor?", - "title": "S\u00e9lectionner la m\u00e9thode de connexion" - }, - "start_addon": { - "data": { - "network_key": "Cl\u00e9 r\u00e9seau", - "usb_path": "Chemin du p\u00e9riph\u00e9rique USB" - }, - "title": "Entrez dans la configuration du module compl\u00e9mentaire OpenZWave" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ozw/translations/he.json b/homeassistant/components/ozw/translations/he.json deleted file mode 100644 index 021d34db25a..00000000000 --- a/homeassistant/components/ozw/translations/he.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", - "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", - "mqtt_required": "\u05e9\u05d9\u05dc\u05d5\u05d1 MQTT \u05d0\u05d9\u05e0\u05d5 \u05de\u05d5\u05d2\u05d3\u05e8", - "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." - }, - "step": { - "on_supervisor": { - "data": { - "use_addon": "\u05e9\u05d9\u05de\u05d5\u05e9 \u05d1\u05d4\u05e8\u05d7\u05d1\u05d4 '\u05de\u05e4\u05e7\u05d7 OpenZWave'" - }, - "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05e9\u05ea\u05de\u05e9 \u05d1\u05d4\u05e8\u05d7\u05d1\u05d4 \u05e9\u05dc \u05de\u05e4\u05e7\u05d7 OpenZWave?", - "title": "\u05d1\u05d7\u05e8 \u05e9\u05d9\u05d8\u05ea \u05d7\u05d9\u05d1\u05d5\u05e8" - }, - "start_addon": { - "data": { - "usb_path": "\u05e0\u05ea\u05d9\u05d1 \u05d4\u05ea\u05e7\u05df USB" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ozw/translations/hu.json b/homeassistant/components/ozw/translations/hu.json deleted file mode 100644 index 1c1d5a9f87d..00000000000 --- a/homeassistant/components/ozw/translations/hu.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "config": { - "abort": { - "addon_info_failed": "Nem siker\u00fclt leh\u00edvni az OpenZWave b\u0151v\u00edtm\u00e9ny inform\u00e1ci\u00f3kat.", - "addon_install_failed": "Nem siker\u00fclt telep\u00edteni az OpenZWave b\u0151v\u00edtm\u00e9nyt.", - "addon_set_config_failed": "Nem siker\u00fclt be\u00e1ll\u00edtani az OpenZWave konfigur\u00e1ci\u00f3t.", - "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", - "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van", - "mqtt_required": "Az MQTT integr\u00e1ci\u00f3 nincs be\u00e1ll\u00edtva", - "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." - }, - "error": { - "addon_start_failed": "Nem siker\u00fclt elind\u00edtani az OpenZWave b\u0151v\u00edtm\u00e9nyt. Ellen\u0151rizze a konfigur\u00e1ci\u00f3t." - }, - "progress": { - "install_addon": "V\u00e1rjon, am\u00edg az OpenZWave b\u0151v\u00edtm\u00e9ny telep\u00edt\u00e9se befejez\u0151dik. Ez t\u00f6bb percig is eltarthat." - }, - "step": { - "hassio_confirm": { - "title": "\u00c1ll\u00edtsa be az OpenZWave integr\u00e1ci\u00f3t az OpenZWave b\u0151v\u00edtm\u00e9nnyel" - }, - "install_addon": { - "title": "Elindult az OpenZWave b\u0151v\u00edtm\u00e9ny telep\u00edt\u00e9se" - }, - "on_supervisor": { - "data": { - "use_addon": "OpenZWave Supervisor b\u0151v\u00edtm\u00e9ny haszn\u00e1lata" - }, - "description": "Szeretn\u00e9 haszn\u00e1lni az OpenZWave Supervisor b\u0151v\u00edtm\u00e9nyt?", - "title": "V\u00e1lassza ki a csatlakoz\u00e1si m\u00f3dot" - }, - "start_addon": { - "data": { - "network_key": "H\u00e1l\u00f3zati kulcs", - "usb_path": "USB eszk\u00f6z el\u00e9r\u00e9si \u00fat" - }, - "title": "Adja meg az OpenZWave b\u0151v\u00edtm\u00e9ny konfigur\u00e1ci\u00f3j\u00e1t" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ozw/translations/id.json b/homeassistant/components/ozw/translations/id.json deleted file mode 100644 index ef47e12f12d..00000000000 --- a/homeassistant/components/ozw/translations/id.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "config": { - "abort": { - "addon_info_failed": "Gagal mendapatkan info add-on OpenZWave.", - "addon_install_failed": "Gagal menginstal add-on OpenZWave.", - "addon_set_config_failed": "Gagal menyetel konfigurasi OpenZWave.", - "already_configured": "Perangkat sudah dikonfigurasi", - "already_in_progress": "Alur konfigurasi sedang berlangsung", - "mqtt_required": "Integrasi MQTT belum disiapkan", - "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan." - }, - "error": { - "addon_start_failed": "Gagal memulai add-on OpenZWave. Periksa konfigurasi." - }, - "progress": { - "install_addon": "Harap tunggu hingga penginstalan add-on OpenZWave selesai. Ini bisa memakan waktu beberapa saat." - }, - "step": { - "hassio_confirm": { - "title": "Siapkan integrasi OpenZWave dengan add-on OpenZWave" - }, - "install_addon": { - "title": "Instalasi add-on OpenZWave telah dimulai" - }, - "on_supervisor": { - "data": { - "use_addon": "Gunakan add-on Supervisor OpenZWave" - }, - "description": "Ingin menggunakan add-on Supervisor OpenZWave?", - "title": "Pilih metode koneksi" - }, - "start_addon": { - "data": { - "network_key": "Kunci Jaringan", - "usb_path": "Jalur Perangkat USB" - }, - "title": "Masukkan konfigurasi add-on OpenZWave" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ozw/translations/it.json b/homeassistant/components/ozw/translations/it.json deleted file mode 100644 index ff3e0a711c5..00000000000 --- a/homeassistant/components/ozw/translations/it.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "config": { - "abort": { - "addon_info_failed": "Impossibile ottenere le informazioni sul componente aggiuntivo OpenZWave.", - "addon_install_failed": "Impossibile installare il componente aggiuntivo OpenZWave.", - "addon_set_config_failed": "Impossibile impostare la configurazione di OpenZWave.", - "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", - "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso", - "mqtt_required": "L'integrazione MQTT non \u00e8 impostata", - "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione." - }, - "error": { - "addon_start_failed": "Impossibile avviare il componente aggiuntivo OpenZWave. Controlla la configurazione." - }, - "progress": { - "install_addon": "Attendi il termine dell'installazione del componente aggiuntivo OpenZWave. Questa operazione pu\u00f2 richiedere diversi minuti." - }, - "step": { - "hassio_confirm": { - "title": "Configura l'integrazione di OpenZWave con il componente aggiuntivo OpenZWave" - }, - "install_addon": { - "title": "L'installazione del componente aggiuntivo OpenZWave \u00e8 iniziata" - }, - "on_supervisor": { - "data": { - "use_addon": "Usa il componente aggiuntivo OpenZWave Supervisor" - }, - "description": "Vuoi usare il componente aggiuntivo OpenZWave Supervisor?", - "title": "Seleziona il metodo di connessione" - }, - "start_addon": { - "data": { - "network_key": "Chiave di rete", - "usb_path": "Percorso del dispositivo USB" - }, - "title": "Accedi alla configurazione dell'add-on OpenZWave" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ozw/translations/ja.json b/homeassistant/components/ozw/translations/ja.json deleted file mode 100644 index d3ef9f7d17f..00000000000 --- a/homeassistant/components/ozw/translations/ja.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "config": { - "abort": { - "addon_info_failed": "OpenZWave\u306e\u30a2\u30c9\u30aa\u30f3\u60c5\u5831\u306e\u53d6\u5f97\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002", - "addon_install_failed": "OpenZWave\u30a2\u30c9\u30aa\u30f3\u306e\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002", - "addon_set_config_failed": "OpenZWave\u306e\u8a2d\u5b9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002", - "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", - "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059", - "mqtt_required": "MQTT\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" - }, - "error": { - "addon_start_failed": "OpenZWave\u30a2\u30c9\u30aa\u30f3\u306e\u8d77\u52d5\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002\u8a2d\u5b9a\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002" - }, - "progress": { - "install_addon": "OpenZWave\u30a2\u30c9\u30aa\u30f3\u306e\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u304c\u5b8c\u4e86\u3059\u308b\u307e\u3067\u304a\u5f85\u3061\u304f\u3060\u3055\u3044\u3002\u3053\u308c\u306b\u306f\u6570\u5206\u304b\u304b\u308b\u5834\u5408\u304c\u3042\u308a\u307e\u3059\u3002" - }, - "step": { - "hassio_confirm": { - "title": "OpenZWave\u30a2\u30c9\u30aa\u30f3\u3068OpenZWave\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" - }, - "install_addon": { - "title": "OpenZWave\u30a2\u30c9\u30aa\u30f3\u306e\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u304c\u958b\u59cb\u3055\u308c\u307e\u3057\u305f" - }, - "on_supervisor": { - "data": { - "use_addon": "OpenZWave Supervisor\u30a2\u30c9\u30aa\u30f3\u3092\u4f7f\u7528\u3059\u308b" - }, - "description": "OpenZWave Supervisor\u30a2\u30c9\u30aa\u30f3\u3092\u4f7f\u7528\u3057\u307e\u3059\u304b\uff1f", - "title": "\u63a5\u7d9a\u65b9\u6cd5\u306e\u9078\u629e" - }, - "start_addon": { - "data": { - "network_key": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u30ad\u30fc", - "usb_path": "USB\u30c7\u30d0\u30a4\u30b9\u306e\u30d1\u30b9" - }, - "title": "OpenZWave\u30a2\u30c9\u30aa\u30f3\u306e\u8a2d\u5b9a\u3092\u5165\u529b\u3059\u308b" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ozw/translations/ka.json b/homeassistant/components/ozw/translations/ka.json deleted file mode 100644 index da587087c92..00000000000 --- a/homeassistant/components/ozw/translations/ka.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "config": { - "abort": { - "addon_info_failed": "\u10d5\u10d4\u10e0 \u10db\u10dd\u10ee\u10d4\u10e0\u10ee\u10d3\u10d0 OpenZWave \u10d3\u10d0\u10dc\u10d0\u10db\u10d0\u10e2\u10d8\u10e1 \u10d8\u10dc\u10e4\u10dd\u10e1 \u10db\u10d8\u10e6\u10d4\u10d1\u10d0.", - "addon_install_failed": "\u10d5\u10d4\u10e0 \u10db\u10dd\u10ee\u10d4\u10e0\u10ee\u10d3\u10d0 OpenZWave \u10d3\u10d0\u10dc\u10d0\u10db\u10d0\u10e2\u10d8\u10e1 \u10d8\u10dc\u10e1\u10e2\u10d0\u10da\u10d8\u10e0\u10d4\u10d1\u10d0.", - "addon_set_config_failed": "\u10d5\u10d4\u10e0 \u10db\u10dd\u10ee\u10d4\u10e0\u10ee\u10d3\u10d0 OpenZWave \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d0\u10ea\u10d8\u10d8\u10e1 \u10d3\u10d0\u10e7\u10d4\u10dc\u10d4\u10d1\u10d0.", - "single_instance_allowed": "\u10e3\u10d9\u10d5\u10d4 \u10d3\u10d0\u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d8\u10e0\u10d4\u10d1\u10e3\u10da\u10d8\u10d0. \u10e8\u10d4\u10e1\u10d0\u10eb\u10da\u10d4\u10d1\u10d4\u10da\u10d8\u10d0 \u10db\u10ee\u10dd\u10da\u10dd\u10d3 \u10d4\u10e0\u10d7\u10d8 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d0\u10ea\u10d8\u10d0." - }, - "error": { - "addon_start_failed": "OpenZWave \u10d3\u10d0\u10dc\u10d0\u10db\u10d0\u10e2\u10d8 \u10d0\u10e0 \u10d3\u10d0\u10d8\u10e1\u10e2\u10d0\u10e0\u10e2\u10d0. \u10e8\u10d4\u10d0\u10db\u10dd\u10ec\u10db\u10d4\u10d7 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d0\u10ea\u10d8\u10d0." - }, - "step": { - "on_supervisor": { - "data": { - "use_addon": "\u10d2\u10d0\u10db\u10dd\u10d8\u10e7\u10d4\u10dc\u10d4\u10d7 OpenZWave Supervisor \u10d3\u10d0\u10dc\u10d0\u10db\u10d0\u10e2\u10d8" - }, - "description": "\u10d2\u10e1\u10e3\u10e0\u10d7 \u10d2\u10d0\u10db\u10dd\u10d8\u10e7\u10d4\u10dc\u10dd\u10d7 OpenZWave Supervisor-\u10d8\u10e1 \u10d3\u10d0\u10dc\u10d0\u10db\u10d0\u10e2\u10d8?", - "title": "\u10d0\u10d8\u10e0\u10e9\u10d8\u10d4\u10d7 \u10d9\u10d0\u10d5\u10e8\u10d8\u10e0\u10d8\u10e1 \u10db\u10d4\u10d7\u10dd\u10d3\u10d8" - }, - "start_addon": { - "data": { - "network_key": "\u10e5\u10e1\u10d4\u10da\u10d8\u10e1 \u10d2\u10d0\u10e1\u10d0\u10e6\u10d4\u10d1\u10d8", - "usb_path": "USB \u10db\u10dd\u10ec\u10e7\u10dd\u10d1\u10da\u10dd\u10d1\u10d8\u10e1 \u10d2\u10d6\u10d0" - }, - "title": "\u10e8\u10d4\u10d8\u10e7\u10d5\u10d0\u10dc\u10d4\u10d7 OpenZWave \u10d3\u10d0\u10dc\u10d0\u10db\u10d0\u10e2\u10d8\u10e1 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d0\u10ea\u10d8\u10d0" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ozw/translations/ko.json b/homeassistant/components/ozw/translations/ko.json deleted file mode 100644 index f6dddf5c96a..00000000000 --- a/homeassistant/components/ozw/translations/ko.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "config": { - "abort": { - "addon_info_failed": "OpenZWave \uc560\ub4dc\uc628\uc758 \uc815\ubcf4\ub97c \uac00\uc838\uc624\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4.", - "addon_install_failed": "OpenZWave \uc560\ub4dc\uc628\uc744 \uc124\uce58\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4.", - "addon_set_config_failed": "OpenZWave \uad6c\uc131\uc744 \uc124\uc815\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4.", - "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", - "mqtt_required": "MQTT \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\uac00 \uc124\uc815\ub418\uc9c0 \uc54a\uc558\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": { - "addon_start_failed": "OpenZWave \uc560\ub4dc\uc628\uc744 \uc2dc\uc791\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \uad6c\uc131 \ub0b4\uc6a9\uc744 \ud655\uc778\ud574\uc8fc\uc138\uc694." - }, - "progress": { - "install_addon": "Openzwave \uc560\ub4dc\uc628\uc758 \uc124\uce58\uac00 \uc644\ub8cc\ub418\ub294 \ub3d9\uc548 \uc7a0\uc2dc \uae30\ub2e4\ub824\uc8fc\uc138\uc694. \uba87 \ubd84 \uc815\ub3c4 \uac78\ub9b4 \uc218 \uc788\uc2b5\ub2c8\ub2e4." - }, - "step": { - "hassio_confirm": { - "title": "OpenZWave \uc560\ub4dc\uc628\uc73c\ub85c OpenZWave \ud1b5\ud569 \uad6c\uc131\uc694\uc18c \uc124\uc815\ud558\uae30" - }, - "install_addon": { - "title": "Openzwave \uc560\ub4dc\uc628 \uc124\uce58\uac00 \uc2dc\uc791\ub418\uc5c8\uc2b5\ub2c8\ub2e4" - }, - "on_supervisor": { - "data": { - "use_addon": "OpenZWave Supervisor \uc560\ub4dc\uc628\uc744 \uc0ac\uc6a9\ud558\uae30" - }, - "description": "OpenZWave Supervisor \uc560\ub4dc\uc628\uc744 \uc0ac\uc6a9\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", - "title": "\uc5f0\uacb0 \ubc29\ubc95 \uc120\ud0dd\ud558\uae30" - }, - "start_addon": { - "data": { - "network_key": "\ub124\ud2b8\uc6cc\ud06c \ud0a4", - "usb_path": "USB \uc7a5\uce58 \uacbd\ub85c" - }, - "title": "OpenZWave \uc560\ub4dc\uc628\uc758 \uad6c\uc131\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ozw/translations/lb.json b/homeassistant/components/ozw/translations/lb.json deleted file mode 100644 index 33de9a44953..00000000000 --- a/homeassistant/components/ozw/translations/lb.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Apparat ass scho konfigur\u00e9iert", - "already_in_progress": "Konfiguratioun's Oflaf ass schon am gaang", - "mqtt_required": "MQTT Integratioun ass net ageriicht", - "single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun ass m\u00e9iglech." - }, - "step": { - "start_addon": { - "data": { - "network_key": "Netzwierk Schl\u00ebssel" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ozw/translations/nl.json b/homeassistant/components/ozw/translations/nl.json deleted file mode 100644 index 7392f9b63eb..00000000000 --- a/homeassistant/components/ozw/translations/nl.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "config": { - "abort": { - "addon_info_failed": "Mislukt om OpenZWave add-on info te krijgen.", - "addon_install_failed": "De installatie van de OpenZWave add-on is mislukt.", - "addon_set_config_failed": "Mislukt om OpenZWave configuratie in te stellen.", - "already_configured": "Apparaat is al geconfigureerd", - "already_in_progress": "De configuratiestroom is al aan de gang", - "mqtt_required": "De MQTT-integratie is niet ingesteld", - "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk." - }, - "error": { - "addon_start_failed": "Het starten van de OpenZWave-add-on is mislukt. Controleer de configuratie." - }, - "progress": { - "install_addon": "Wacht even terwijl de installatie van de OpenZWave add-on wordt voltooid. Dit kan enkele minuten duren." - }, - "step": { - "hassio_confirm": { - "title": "OpenZWave integratie instellen met de OpenZWave add-on" - }, - "install_addon": { - "title": "De OpenZWave add-on installatie is gestart" - }, - "on_supervisor": { - "data": { - "use_addon": "Gebruik de OpenZWave Supervisor add-on" - }, - "description": "Wilt u de OpenZWave Supervisor add-on gebruiken?", - "title": "Selecteer een verbindingsmethode" - }, - "start_addon": { - "data": { - "network_key": "Netwerksleutel", - "usb_path": "USB-apparaatpad" - }, - "title": "Voer de OpenZWave add-on configuratie in" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ozw/translations/no.json b/homeassistant/components/ozw/translations/no.json deleted file mode 100644 index 652e28fe3fc..00000000000 --- a/homeassistant/components/ozw/translations/no.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "config": { - "abort": { - "addon_info_failed": "Kunne ikke hente informasjon om OpenZWave-tillegg", - "addon_install_failed": "Kunne ikke installere OpenZWave-tillegg", - "addon_set_config_failed": "Kunne ikke angi OpenZWave-konfigurasjon", - "already_configured": "Enheten er allerede konfigurert", - "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", - "mqtt_required": "MQTT-integrasjonen er ikke satt opp", - "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig." - }, - "error": { - "addon_start_failed": "Kunne ikke starte OpenZWave-tillegg. Sjekk konfigurasjonen." - }, - "progress": { - "install_addon": "Vent mens installasjonen av OpenZWave-tillegg er ferdig. Dette kan ta flere minutter." - }, - "step": { - "hassio_confirm": { - "title": "Sett opp OpenZWave-integrasjon med OpenZWave-tillegg" - }, - "install_addon": { - "title": "Installasjonen av OpenZWave-tillegg har startet" - }, - "on_supervisor": { - "data": { - "use_addon": "Bruk OpenZWave Supervisor-tillegg" - }, - "description": "\u00d8nsker du \u00e5 bruke OpenZWave Supervisor-tillegg?", - "title": "Velg tilkoblingsmetode" - }, - "start_addon": { - "data": { - "network_key": "Nettverksn\u00f8kkel", - "usb_path": "USB enhetsbane" - }, - "title": "Angi konfigurasjon for OpenZWave-tillegg" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ozw/translations/pl.json b/homeassistant/components/ozw/translations/pl.json deleted file mode 100644 index c9fd17c59bd..00000000000 --- a/homeassistant/components/ozw/translations/pl.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "config": { - "abort": { - "addon_info_failed": "Nie uda\u0142o si\u0119 pobra\u0107 informacji o dodatku OpenZWave", - "addon_install_failed": "Nie uda\u0142o si\u0119 zainstalowa\u0107 dodatku OpenZWave", - "addon_set_config_failed": "Nie uda\u0142o si\u0119 ustawi\u0107 konfiguracji OpenZWave", - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", - "already_in_progress": "Konfiguracja jest ju\u017c w toku", - "mqtt_required": "Integracja MQTT nie jest skonfigurowana", - "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja." - }, - "error": { - "addon_start_failed": "Nie uda\u0142o si\u0119 uruchomi\u0107 dodatku OpenZWave. Sprawd\u017a konfiguracj\u0119." - }, - "progress": { - "install_addon": "Poczekaj, a\u017c zako\u0144czy si\u0119 instalacja dodatku OpenZWave. Mo\u017ce to potrwa\u0107 kilka minut." - }, - "step": { - "hassio_confirm": { - "title": "Konfiguracja integracji OpenZWave z dodatkiem OpenZWave" - }, - "install_addon": { - "title": "Rozpocz\u0119\u0142a si\u0119 instalacja dodatku OpenZWave" - }, - "on_supervisor": { - "data": { - "use_addon": "U\u017cyj dodatku OpenZWave Supervisor" - }, - "description": "Czy chcesz u\u017cy\u0107 dodatku OpenZWave Supervisor?", - "title": "Wybierz metod\u0119 po\u0142\u0105czenia" - }, - "start_addon": { - "data": { - "network_key": "Klucz sieci", - "usb_path": "\u015acie\u017cka urz\u0105dzenia USB" - }, - "title": "Wprowad\u017a konfiguracj\u0119 dodatku OpenZWave" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ozw/translations/pt-BR.json b/homeassistant/components/ozw/translations/pt-BR.json deleted file mode 100644 index 8ec256d1d75..00000000000 --- a/homeassistant/components/ozw/translations/pt-BR.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "config": { - "abort": { - "addon_info_failed": "Falha ao obter informa\u00e7\u00f5es do add-on OpenZWave.", - "addon_install_failed": "Falha ao instalar o add-on OpenZWave.", - "addon_set_config_failed": "Falha ao definir a configura\u00e7\u00e3o do OpenZWave.", - "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", - "already_in_progress": "O fluxo de configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", - "mqtt_required": "A integra\u00e7\u00e3o do MQTT n\u00e3o est\u00e1 configurada", - "single_instance_allowed": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel." - }, - "error": { - "addon_start_failed": "Falha ao iniciar o add-on OpenZWave. Verifique a configura\u00e7\u00e3o." - }, - "progress": { - "install_addon": "Aguarde enquanto a instala\u00e7\u00e3o do add-on OpenZWave termina. Isso pode levar v\u00e1rios minutos." - }, - "step": { - "hassio_confirm": { - "title": "Configure a integra\u00e7\u00e3o do OpenZWave com o add-on OpenZWave" - }, - "install_addon": { - "title": "A instala\u00e7\u00e3o do add-on OpenZWave foi iniciada" - }, - "on_supervisor": { - "data": { - "use_addon": "Use o add-on OpenZWave Supervisor" - }, - "description": "Deseja usar o add-on OpenZWave Supervisor?", - "title": "Selecione o m\u00e9todo de conex\u00e3o" - }, - "start_addon": { - "data": { - "network_key": "Chave de rede", - "usb_path": "Caminho do Dispositivo USB" - }, - "title": "Digite a configura\u00e7\u00e3o do add-on OpenZWave" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ozw/translations/pt.json b/homeassistant/components/ozw/translations/pt.json deleted file mode 100644 index 75d85097874..00000000000 --- a/homeassistant/components/ozw/translations/pt.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "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", - "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." - }, - "step": { - "start_addon": { - "data": { - "usb_path": "Caminho do Dispositivo USB" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ozw/translations/ru.json b/homeassistant/components/ozw/translations/ru.json deleted file mode 100644 index 07dc84eae07..00000000000 --- a/homeassistant/components/ozw/translations/ru.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "config": { - "abort": { - "addon_info_failed": "\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 \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0438 OpenZWave.", - "addon_install_failed": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c OpenZWave.", - "addon_set_config_failed": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e OpenZWave.", - "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.", - "mqtt_required": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f MQTT \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u0430.", - "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": { - "addon_start_failed": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u044c OpenZWave. \u041f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e." - }, - "progress": { - "install_addon": "\u041f\u043e\u0434\u043e\u0436\u0434\u0438\u0442\u0435, \u043f\u043e\u043a\u0430 \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u0441\u044f \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0430 \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f OpenZWave. \u042d\u0442\u043e \u043c\u043e\u0436\u0435\u0442 \u0437\u0430\u043d\u044f\u0442\u044c \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u043c\u0438\u043d\u0443\u0442." - }, - "step": { - "hassio_confirm": { - "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0441 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435\u043c \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f OpenZWave" - }, - "install_addon": { - "title": "\u041d\u0430\u0447\u0430\u043b\u0430\u0441\u044c \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0430 \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f OpenZWave" - }, - "on_supervisor": { - "data": { - "use_addon": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 Supervisor OpenZWave" - }, - "description": "\u0412\u044b \u0445\u043e\u0442\u0438\u0442\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 Supervisor OpenZWave?", - "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u043f\u043e\u0441\u043e\u0431 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f" - }, - "start_addon": { - "data": { - "network_key": "\u041a\u043b\u044e\u0447 \u0441\u0435\u0442\u0438", - "usb_path": "\u041f\u0443\u0442\u044c \u043a USB-\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443" - }, - "title": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f OpenZWave" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ozw/translations/sk.json b/homeassistant/components/ozw/translations/sk.json deleted file mode 100644 index bee0999420f..00000000000 --- a/homeassistant/components/ozw/translations/sk.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "config": { - "abort": { - "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ozw/translations/sl.json b/homeassistant/components/ozw/translations/sl.json deleted file mode 100644 index 8da77910c38..00000000000 --- a/homeassistant/components/ozw/translations/sl.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Naprava je \u017ee name\u0161\u010dena", - "already_in_progress": "Name\u0161\u010danje se \u017ee izvaja", - "mqtt_required": "Integracija MQTT ni nastavljena" - }, - "progress": { - "install_addon": "Po\u010dakajte, da se namestitev dodatka OpenZWave zaklju\u010di. To lahko traja ve\u010d minut." - }, - "step": { - "hassio_confirm": { - "title": "Namestite OpenZWave integracijo z OpenZWave dodatkom." - }, - "install_addon": { - "title": "Namestitev dodatka OpenZWave se je za\u010dela" - }, - "on_supervisor": { - "title": "Izberite na\u010din povezave" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ozw/translations/tr.json b/homeassistant/components/ozw/translations/tr.json deleted file mode 100644 index e9e8643fa94..00000000000 --- a/homeassistant/components/ozw/translations/tr.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "config": { - "abort": { - "addon_info_failed": "OpenZWave eklenti bilgileri al\u0131namad\u0131.", - "addon_install_failed": "OpenZWave eklentisi y\u00fcklenemedi.", - "addon_set_config_failed": "OpenZWave yap\u0131land\u0131rmas\u0131 ayarlanamad\u0131.", - "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", - "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", - "mqtt_required": "MQTT entegrasyonu kurulmam\u0131\u015f", - "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." - }, - "error": { - "addon_start_failed": "OpenZWave eklentisi ba\u015flat\u0131lamad\u0131. Yap\u0131land\u0131rmay\u0131 kontrol edin." - }, - "progress": { - "install_addon": "OpenZWave eklenti kurulumu bitene kadar l\u00fctfen bekleyin. Bu birka\u00e7 dakika s\u00fcrebilir." - }, - "step": { - "hassio_confirm": { - "title": "OpenZWave eklentisi ile OpenZWave entegrasyonunu kurun" - }, - "install_addon": { - "title": "OpenZWave eklenti kurulumu ba\u015flad\u0131" - }, - "on_supervisor": { - "data": { - "use_addon": "OpenZWave Supervisor eklentisini kullan\u0131n" - }, - "description": "OpenZWave Supervisor eklentisini kullanmak istiyor musunuz?", - "title": "Ba\u011flant\u0131 y\u00f6ntemini se\u00e7in" - }, - "start_addon": { - "data": { - "network_key": "A\u011f Anahtar\u0131", - "usb_path": "USB Cihaz Yolu" - }, - "title": "OpenZWave eklenti yap\u0131land\u0131rmas\u0131n\u0131 girin" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ozw/translations/uk.json b/homeassistant/components/ozw/translations/uk.json deleted file mode 100644 index f662bc978ae..00000000000 --- a/homeassistant/components/ozw/translations/uk.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "config": { - "abort": { - "addon_info_failed": "\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 \u0434\u043e\u043f\u043e\u0432\u043d\u0435\u043d\u043d\u044f OpenZWave.", - "addon_install_failed": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0432\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u0438 OpenZWave.", - "addon_set_config_failed": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0432\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u0438 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e OpenZWave.", - "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.", - "mqtt_required": "\u0406\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u044f MQTT \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u0430.", - "single_instance_allowed": "\u0412\u0436\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u043e. \u041c\u043e\u0436\u043b\u0438\u0432\u0430 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044f." - }, - "error": { - "addon_start_failed": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0438 OpenZWave. \u041f\u0435\u0440\u0435\u0432\u0456\u0440\u0442\u0435 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e." - }, - "progress": { - "install_addon": "\u0417\u0430\u0447\u0435\u043a\u0430\u0439\u0442\u0435, \u043f\u043e\u043a\u0438 \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c\u0441\u044f \u0432\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u044f \u0434\u043e\u043f\u043e\u0432\u043d\u0435\u043d\u043d\u044f OpenZWave. \u0426\u0435 \u043c\u043e\u0436\u0435 \u0437\u0430\u0439\u043d\u044f\u0442\u0438 \u043a\u0456\u043b\u044c\u043a\u0430 \u0445\u0432\u0438\u043b\u0438\u043d." - }, - "step": { - "hassio_confirm": { - "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0439\u0442\u0435 \u0437\u0430 \u0434\u043e\u043f\u043e\u043c\u043e\u0433\u043e\u044e \u0434\u043e\u043f\u043e\u0432\u043d\u0435\u043d\u043d\u044f OpenZWave" - }, - "install_addon": { - "title": "\u0420\u043e\u0437\u043f\u043e\u0447\u0430\u0442\u043e \u0432\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u044f \u0434\u043e\u043f\u043e\u0432\u043d\u0435\u043d\u043d\u044f Open'Wave" - }, - "on_supervisor": { - "data": { - "use_addon": "\u0412\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0439\u0442\u0435 \u0434\u043e\u0434\u0430\u0442\u043e\u043a Supervisor OpenZWave" - }, - "description": "\u0412\u0438 \u0445\u043e\u0447\u0435\u0442\u0435 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0432\u0430\u0442\u0438 \u0434\u043e\u0434\u0430\u0442\u043e\u043a Supervisor OpenZWave?", - "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0441\u043f\u043e\u0441\u0456\u0431 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f" - }, - "start_addon": { - "data": { - "network_key": "\u041a\u043b\u044e\u0447 \u043c\u0435\u0440\u0435\u0436\u0456", - "usb_path": "\u0428\u043b\u044f\u0445 \u0434\u043e USB-\u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e" - }, - "title": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0434\u043e\u0434\u0430\u0442\u043a\u043e\u0432\u043e\u0433\u043e \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 Open'Wave" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ozw/translations/zh-Hans.json b/homeassistant/components/ozw/translations/zh-Hans.json deleted file mode 100644 index cf0c8771863..00000000000 --- a/homeassistant/components/ozw/translations/zh-Hans.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "config": { - "abort": { - "mqtt_required": "\u672a\u8bbe\u7f6e MQTT \u96c6\u6210" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ozw/translations/zh-Hant.json b/homeassistant/components/ozw/translations/zh-Hant.json deleted file mode 100644 index 0e51da481d7..00000000000 --- a/homeassistant/components/ozw/translations/zh-Hant.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "config": { - "abort": { - "addon_info_failed": "\u53d6\u5f97 OpenZWave \u9644\u52a0\u5143\u4ef6\u8cc7\u8a0a\u5931\u6557\u3002", - "addon_install_failed": "OpenZWave \u9644\u52a0\u5143\u4ef6\u5b89\u88dd\u5931\u6557\u3002", - "addon_set_config_failed": "OpenZWave a\u9644\u52a0\u5143\u4ef6\u8a2d\u5b9a\u5931\u6557\u3002", - "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", - "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", - "mqtt_required": "MQTT \u6574\u5408\u5c1a\u672a\u8a2d\u5b9a", - "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" - }, - "error": { - "addon_start_failed": "OpenZWave \u9644\u52a0\u5143\u4ef6\u555f\u52d5\u5931\u6557\uff0c\u8acb\u6aa2\u67e5\u8a2d\u5b9a\u3002" - }, - "progress": { - "install_addon": "\u8acb\u7a0d\u7b49 OpenZWave \u9644\u52a0\u5143\u4ef6\u5b89\u88dd\u5b8c\u6210\uff0c\u53ef\u80fd\u6703\u9700\u8981\u5e7e\u5206\u9418\u3002" - }, - "step": { - "hassio_confirm": { - "title": "\u4ee5 OpenZWave \u9644\u52a0\u5143\u4ef6\u8a2d\u5b9a OpenZwave \u6574\u5408" - }, - "install_addon": { - "title": "OpenZWave \u9644\u52a0\u5143\u4ef6\u5b89\u88dd\u5df2\u555f\u52d5" - }, - "on_supervisor": { - "data": { - "use_addon": "\u4f7f\u7528 OpenZWave Supervisor \u9644\u52a0\u5143\u4ef6" - }, - "description": "\u662f\u5426\u8981\u4f7f\u7528 OpenZWave Supervisor \u9644\u52a0\u5143\u4ef6\uff1f", - "title": "\u9078\u64c7\u9023\u7dda\u985e\u578b" - }, - "start_addon": { - "data": { - "network_key": "\u7db2\u8def\u91d1\u9470", - "usb_path": "USB \u88dd\u7f6e\u8def\u5f91" - }, - "title": "\u8acb\u8f38\u5165 OpenZWave \u9644\u52a0\u5143\u4ef6\u8a2d\u5b9a\u3002" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ozw/websocket_api.py b/homeassistant/components/ozw/websocket_api.py deleted file mode 100644 index 45c0a113841..00000000000 --- a/homeassistant/components/ozw/websocket_api.py +++ /dev/null @@ -1,493 +0,0 @@ -"""Web socket API for OpenZWave.""" -from openzwavemqtt.const import ( - ATTR_CODE_SLOT, - ATTR_LABEL, - ATTR_POSITION, - ATTR_VALUE, - EVENT_NODE_ADDED, - EVENT_NODE_CHANGED, -) -from openzwavemqtt.exceptions import NotFoundError, NotSupportedError -from openzwavemqtt.util.lock import clear_usercode, get_code_slots, set_usercode -from openzwavemqtt.util.node import ( - get_config_parameters, - get_node_from_manager, - set_config_parameter, -) -import voluptuous as vol -import voluptuous_serialize - -from homeassistant.components import websocket_api -from homeassistant.core import callback -from homeassistant.helpers import config_validation as cv - -from .const import ATTR_CONFIG_PARAMETER, ATTR_CONFIG_VALUE, DOMAIN, MANAGER -from .lock import ATTR_USERCODE - -DRY_RUN = "dry_run" -TYPE = "type" -ID = "id" -OZW_INSTANCE = "ozw_instance" -NODE_ID = "node_id" -PARAMETER = ATTR_CONFIG_PARAMETER -VALUE = ATTR_CONFIG_VALUE -SCHEMA = "schema" - -ATTR_NODE_QUERY_STAGE = "node_query_stage" -ATTR_IS_ZWAVE_PLUS = "is_zwave_plus" -ATTR_IS_AWAKE = "is_awake" -ATTR_IS_FAILED = "is_failed" -ATTR_NODE_BAUD_RATE = "node_baud_rate" -ATTR_IS_BEAMING = "is_beaming" -ATTR_IS_FLIRS = "is_flirs" -ATTR_IS_ROUTING = "is_routing" -ATTR_IS_SECURITYV1 = "is_securityv1" -ATTR_NODE_BASIC_STRING = "node_basic_string" -ATTR_NODE_GENERIC_STRING = "node_generic_string" -ATTR_NODE_SPECIFIC_STRING = "node_specific_string" -ATTR_NODE_MANUFACTURER_NAME = "node_manufacturer_name" -ATTR_NODE_PRODUCT_NAME = "node_product_name" -ATTR_NEIGHBORS = "neighbors" - - -@callback -def async_register_api(hass): - """Register all of our api endpoints.""" - websocket_api.async_register_command(hass, websocket_get_instances) - websocket_api.async_register_command(hass, websocket_get_nodes) - websocket_api.async_register_command(hass, websocket_network_status) - websocket_api.async_register_command(hass, websocket_network_statistics) - websocket_api.async_register_command(hass, websocket_node_metadata) - websocket_api.async_register_command(hass, websocket_node_status) - websocket_api.async_register_command(hass, websocket_node_statistics) - websocket_api.async_register_command(hass, websocket_refresh_node_info) - websocket_api.async_register_command(hass, websocket_get_config_parameters) - websocket_api.async_register_command(hass, websocket_set_config_parameter) - websocket_api.async_register_command(hass, websocket_set_usercode) - websocket_api.async_register_command(hass, websocket_clear_usercode) - websocket_api.async_register_command(hass, websocket_get_code_slots) - - -def _call_util_function(hass, connection, msg, send_result, function, *args): - """Call an openzwavemqtt.util function.""" - try: - node = get_node_from_manager( - hass.data[DOMAIN][MANAGER], msg[OZW_INSTANCE], msg[NODE_ID] - ) - except NotFoundError as err: - connection.send_error( - msg[ID], - websocket_api.const.ERR_NOT_FOUND, - err.args[0], - ) - return - - try: - payload = function(node, *args) - except NotFoundError as err: - connection.send_error( - msg[ID], - websocket_api.const.ERR_NOT_FOUND, - err.args[0], - ) - return - except NotSupportedError as err: - connection.send_error( - msg[ID], - websocket_api.const.ERR_NOT_SUPPORTED, - err.args[0], - ) - return - - if send_result: - connection.send_result( - msg[ID], - payload, - ) - return - - connection.send_result(msg[ID]) - - -def _get_config_params(node, *args): - raw_values = get_config_parameters(node) - config_params = [] - - for param in raw_values: - schema = {} - - if param["type"] in ("Byte", "Int", "Short"): - schema = vol.Schema( - { - vol.Required(param["label"], default=param["value"]): vol.All( - vol.Coerce(int), vol.Range(min=param["min"], max=param["max"]) - ) - } - ) - data = {param["label"]: param["value"]} - - if param["type"] == "List": - - for options in param["options"]: - if options["Label"] == param["value"]: - selected = options - break - - schema = vol.Schema( - { - vol.Required(param["label"],): vol.In( - { - option["Value"]: option["Label"] - for option in param["options"] - } - ) - } - ) - data = {param["label"]: selected["Value"]} - - config_params.append( - { - "type": param["type"], - "label": param["label"], - "parameter": param["parameter"], - "help": param["help"], - "value": param["value"], - "schema": voluptuous_serialize.convert( - schema, custom_serializer=cv.custom_serializer - ), - "data": data, - } - ) - - return config_params - - -@websocket_api.websocket_command({vol.Required(TYPE): "ozw/get_instances"}) -def websocket_get_instances(hass, connection, msg): - """Get a list of OZW instances.""" - manager = hass.data[DOMAIN][MANAGER] - instances = [] - - for instance in manager.collections["instance"]: - instances.append(dict(instance.get_status().data, ozw_instance=instance.id)) - - connection.send_result( - msg[ID], - instances, - ) - - -@websocket_api.websocket_command( - { - vol.Required(TYPE): "ozw/get_nodes", - vol.Optional(OZW_INSTANCE, default=1): vol.Coerce(int), - } -) -def websocket_get_nodes(hass, connection, msg): - """Get a list of nodes for an OZW instance.""" - manager = hass.data[DOMAIN][MANAGER] - nodes = [] - - for node in manager.get_instance(msg[OZW_INSTANCE]).collections["node"]: - nodes.append( - { - ATTR_NODE_QUERY_STAGE: node.node_query_stage, - NODE_ID: node.node_id, - ATTR_IS_ZWAVE_PLUS: node.is_zwave_plus, - ATTR_IS_AWAKE: node.is_awake, - ATTR_IS_FAILED: node.is_failed, - ATTR_NODE_BAUD_RATE: node.node_baud_rate, - ATTR_IS_BEAMING: node.is_beaming, - ATTR_IS_FLIRS: node.is_flirs, - ATTR_IS_ROUTING: node.is_routing, - ATTR_IS_SECURITYV1: node.is_securityv1, - ATTR_NODE_BASIC_STRING: node.node_basic_string, - ATTR_NODE_GENERIC_STRING: node.node_generic_string, - ATTR_NODE_SPECIFIC_STRING: node.node_specific_string, - ATTR_NODE_MANUFACTURER_NAME: node.node_manufacturer_name, - ATTR_NODE_PRODUCT_NAME: node.node_product_name, - ATTR_NEIGHBORS: node.neighbors, - OZW_INSTANCE: msg[OZW_INSTANCE], - } - ) - - connection.send_result( - msg[ID], - nodes, - ) - - -@websocket_api.websocket_command( - { - vol.Required(TYPE): "ozw/set_usercode", - vol.Required(NODE_ID): vol.Coerce(int), - vol.Optional(OZW_INSTANCE, default=1): vol.Coerce(int), - vol.Required(ATTR_CODE_SLOT): vol.Coerce(int), - vol.Required(ATTR_USERCODE): cv.string, - } -) -def websocket_set_usercode(hass, connection, msg): - """Set a usercode to a node code slot.""" - _call_util_function( - hass, connection, msg, False, set_usercode, msg[ATTR_CODE_SLOT], ATTR_USERCODE - ) - - -@websocket_api.websocket_command( - { - vol.Required(TYPE): "ozw/clear_usercode", - vol.Required(NODE_ID): vol.Coerce(int), - vol.Optional(OZW_INSTANCE, default=1): vol.Coerce(int), - vol.Required(ATTR_CODE_SLOT): vol.Coerce(int), - } -) -def websocket_clear_usercode(hass, connection, msg): - """Clear a node code slot.""" - _call_util_function( - hass, connection, msg, False, clear_usercode, msg[ATTR_CODE_SLOT] - ) - - -@websocket_api.websocket_command( - { - vol.Required(TYPE): "ozw/get_code_slots", - vol.Required(NODE_ID): vol.Coerce(int), - vol.Optional(OZW_INSTANCE, default=1): vol.Coerce(int), - } -) -def websocket_get_code_slots(hass, connection, msg): - """Get status of node's code slots.""" - _call_util_function(hass, connection, msg, True, get_code_slots) - - -@websocket_api.websocket_command( - { - vol.Required(TYPE): "ozw/get_config_parameters", - vol.Required(NODE_ID): vol.Coerce(int), - vol.Optional(OZW_INSTANCE, default=1): vol.Coerce(int), - } -) -def websocket_get_config_parameters(hass, connection, msg): - """Get a list of configuration parameters for an OZW node instance.""" - _call_util_function(hass, connection, msg, True, _get_config_params) - - -@websocket_api.websocket_command( - { - vol.Required(TYPE): "ozw/set_config_parameter", - vol.Required(NODE_ID): vol.Coerce(int), - vol.Optional(OZW_INSTANCE, default=1): vol.Coerce(int), - vol.Required(PARAMETER): vol.Coerce(int), - vol.Required(VALUE): vol.Any( - vol.All( - cv.ensure_list, - [ - vol.All( - { - vol.Exclusive(ATTR_LABEL, "bit"): cv.string, - vol.Exclusive(ATTR_POSITION, "bit"): vol.Coerce(int), - vol.Required(ATTR_VALUE): bool, - }, - cv.has_at_least_one_key(ATTR_LABEL, ATTR_POSITION), - ) - ], - ), - vol.Coerce(int), - bool, - cv.string, - ), - } -) -def websocket_set_config_parameter(hass, connection, msg): - """Set a config parameter to a node.""" - _call_util_function( - hass, connection, msg, True, set_config_parameter, msg[PARAMETER], msg[VALUE] - ) - - -@websocket_api.websocket_command( - { - vol.Required(TYPE): "ozw/network_status", - vol.Optional(OZW_INSTANCE, default=1): vol.Coerce(int), - } -) -def websocket_network_status(hass, connection, msg): - """Get Z-Wave network status.""" - - manager = hass.data[DOMAIN][MANAGER] - status = manager.get_instance(msg[OZW_INSTANCE]).get_status().data - connection.send_result( - msg[ID], - dict(status, ozw_instance=msg[OZW_INSTANCE]), - ) - - -@websocket_api.websocket_command( - { - vol.Required(TYPE): "ozw/network_statistics", - vol.Optional(OZW_INSTANCE, default=1): vol.Coerce(int), - } -) -def websocket_network_statistics(hass, connection, msg): - """Get Z-Wave network statistics.""" - - manager = hass.data[DOMAIN][MANAGER] - statistics = manager.get_instance(msg[OZW_INSTANCE]).get_statistics().data - node_count = len( - manager.get_instance(msg[OZW_INSTANCE]).collections["node"].collection - ) - connection.send_result( - msg[ID], - dict(statistics, ozw_instance=msg[OZW_INSTANCE], node_count=node_count), - ) - - -@websocket_api.websocket_command( - { - vol.Required(TYPE): "ozw/node_status", - vol.Required(NODE_ID): vol.Coerce(int), - vol.Optional(OZW_INSTANCE, default=1): vol.Coerce(int), - } -) -def websocket_node_status(hass, connection, msg): - """Get the status for a Z-Wave node.""" - try: - node = get_node_from_manager( - hass.data[DOMAIN][MANAGER], msg[OZW_INSTANCE], msg[NODE_ID] - ) - except NotFoundError as err: - connection.send_error( - msg[ID], - websocket_api.const.ERR_NOT_FOUND, - err.args[0], - ) - return - - connection.send_result( - msg[ID], - { - ATTR_NODE_QUERY_STAGE: node.node_query_stage, - NODE_ID: node.node_id, - ATTR_IS_ZWAVE_PLUS: node.is_zwave_plus, - ATTR_IS_AWAKE: node.is_awake, - ATTR_IS_FAILED: node.is_failed, - ATTR_NODE_BAUD_RATE: node.node_baud_rate, - ATTR_IS_BEAMING: node.is_beaming, - ATTR_IS_FLIRS: node.is_flirs, - ATTR_IS_ROUTING: node.is_routing, - ATTR_IS_SECURITYV1: node.is_securityv1, - ATTR_NODE_BASIC_STRING: node.node_basic_string, - ATTR_NODE_GENERIC_STRING: node.node_generic_string, - ATTR_NODE_SPECIFIC_STRING: node.node_specific_string, - ATTR_NODE_MANUFACTURER_NAME: node.node_manufacturer_name, - ATTR_NODE_PRODUCT_NAME: node.node_product_name, - ATTR_NEIGHBORS: node.neighbors, - OZW_INSTANCE: msg[OZW_INSTANCE], - }, - ) - - -@websocket_api.websocket_command( - { - vol.Required(TYPE): "ozw/node_metadata", - vol.Required(NODE_ID): vol.Coerce(int), - vol.Optional(OZW_INSTANCE, default=1): vol.Coerce(int), - } -) -def websocket_node_metadata(hass, connection, msg): - """Get the metadata for a Z-Wave node.""" - try: - node = get_node_from_manager( - hass.data[DOMAIN][MANAGER], msg[OZW_INSTANCE], msg[NODE_ID] - ) - except NotFoundError as err: - connection.send_error( - msg[ID], - websocket_api.const.ERR_NOT_FOUND, - err.args[0], - ) - return - - connection.send_result( - msg[ID], - { - "metadata": node.meta_data, - NODE_ID: node.node_id, - OZW_INSTANCE: msg[OZW_INSTANCE], - }, - ) - - -@websocket_api.websocket_command( - { - vol.Required(TYPE): "ozw/node_statistics", - vol.Required(NODE_ID): vol.Coerce(int), - vol.Optional(OZW_INSTANCE, default=1): vol.Coerce(int), - } -) -def websocket_node_statistics(hass, connection, msg): - """Get the statistics for a Z-Wave node.""" - manager = hass.data[DOMAIN][MANAGER] - stats = ( - manager.get_instance(msg[OZW_INSTANCE]).get_node(msg[NODE_ID]).get_statistics() - ) - connection.send_result( - msg[ID], - { - NODE_ID: msg[NODE_ID], - "send_count": stats.send_count, - "sent_failed": stats.sent_failed, - "retries": stats.retries, - "last_request_rtt": stats.last_request_rtt, - "last_response_rtt": stats.last_response_rtt, - "average_request_rtt": stats.average_request_rtt, - "average_response_rtt": stats.average_response_rtt, - "received_packets": stats.received_packets, - "received_dup_packets": stats.received_dup_packets, - "received_unsolicited": stats.received_unsolicited, - OZW_INSTANCE: msg[OZW_INSTANCE], - }, - ) - - -@websocket_api.require_admin -@websocket_api.websocket_command( - { - vol.Required(TYPE): "ozw/refresh_node_info", - vol.Optional(OZW_INSTANCE, default=1): vol.Coerce(int), - vol.Required(NODE_ID): vol.Coerce(int), - } -) -def websocket_refresh_node_info(hass, connection, msg): - """Tell OpenZWave to re-interview a node.""" - - manager = hass.data[DOMAIN][MANAGER] - options = manager.options - - @callback - def forward_node(node): - """Forward node events to websocket.""" - if node.node_id != msg[NODE_ID]: - return - - forward_data = { - "type": "node_updated", - ATTR_NODE_QUERY_STAGE: node.node_query_stage, - } - connection.send_message(websocket_api.event_message(msg["id"], forward_data)) - - @callback - def async_cleanup() -> None: - """Remove signal listeners.""" - for unsub in unsubs: - unsub() - - connection.subscriptions[msg["id"]] = async_cleanup - unsubs = [ - options.listen(EVENT_NODE_CHANGED, forward_node), - options.listen(EVENT_NODE_ADDED, forward_node), - ] - - instance = manager.get_instance(msg[OZW_INSTANCE]) - instance.refresh_node(msg[NODE_ID]) - connection.send_result(msg["id"]) diff --git a/homeassistant/components/p1_monitor/sensor.py b/homeassistant/components/p1_monitor/sensor.py index edc076382ec..57f6b0ad99c 100644 --- a/homeassistant/components/p1_monitor/sensor.py +++ b/homeassistant/components/p1_monitor/sensor.py @@ -235,11 +235,11 @@ async def async_setup_entry( ) -class P1MonitorSensorEntity(CoordinatorEntity, SensorEntity): +class P1MonitorSensorEntity( + CoordinatorEntity[P1MonitorDataUpdateCoordinator], SensorEntity +): """Defines an P1 Monitor sensor.""" - coordinator: P1MonitorDataUpdateCoordinator - def __init__( self, *, diff --git a/homeassistant/components/p1_monitor/strings.json b/homeassistant/components/p1_monitor/strings.json index fba7973528e..b088bf7adce 100644 --- a/homeassistant/components/p1_monitor/strings.json +++ b/homeassistant/components/p1_monitor/strings.json @@ -13,4 +13,4 @@ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" } } -} \ No newline at end of file +} diff --git a/homeassistant/components/panasonic_viera/media_player.py b/homeassistant/components/panasonic_viera/media_player.py index 4b16d9361a1..f9cff28f6ff 100644 --- a/homeassistant/components/panasonic_viera/media_player.py +++ b/homeassistant/components/panasonic_viera/media_player.py @@ -5,12 +5,17 @@ import logging from panasonic_viera import Keys +from homeassistant.components import media_source from homeassistant.components.media_player import ( MediaPlayerDeviceClass, MediaPlayerEntity, ) +from homeassistant.components.media_player.browse_media import ( + async_process_play_media_url, +) from homeassistant.components.media_player.const import ( MEDIA_TYPE_URL, + SUPPORT_BROWSE_MEDIA, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, @@ -52,6 +57,7 @@ SUPPORT_VIERATV = ( | SUPPORT_PLAY | SUPPORT_PLAY_MEDIA | SUPPORT_STOP + | SUPPORT_BROWSE_MEDIA ) _LOGGER = logging.getLogger(__name__) @@ -198,8 +204,18 @@ class PanasonicVieraTVEntity(MediaPlayerEntity): async def async_play_media(self, media_type, media_id, **kwargs): """Play media.""" + if media_source.is_media_source_id(media_id): + media_type = MEDIA_TYPE_URL + play_item = await media_source.async_resolve_media(self.hass, media_id) + media_id = play_item.url + if media_type != MEDIA_TYPE_URL: _LOGGER.warning("Unsupported media_type: %s", media_type) return + media_id = async_process_play_media_url(self.hass, media_id) await self._remote.async_play_media(media_type, media_id) + + async def async_browse_media(self, media_content_type=None, media_content_id=None): + """Implement the websocket media browsing helper.""" + return await media_source.async_browse_media(self.hass, media_content_id) diff --git a/homeassistant/components/panasonic_viera/translations/es.json b/homeassistant/components/panasonic_viera/translations/es.json index cb8021f36d5..d14cc6b878b 100644 --- a/homeassistant/components/panasonic_viera/translations/es.json +++ b/homeassistant/components/panasonic_viera/translations/es.json @@ -7,14 +7,14 @@ }, "error": { "cannot_connect": "No se pudo conectar", - "invalid_pin_code": "El c\u00f3digo PIN que ha introducido no es v\u00e1lido" + "invalid_pin_code": "El c\u00f3digo PIN que has introducido no es v\u00e1lido" }, "step": { "pairing": { "data": { "pin": "C\u00f3digo PIN" }, - "description": "Introduzca el PIN que aparece en su Televisor", + "description": "Introduce el PIN que aparece en tu Televisor", "title": "Emparejamiento" }, "user": { diff --git a/homeassistant/components/pcal9535a/__init__.py b/homeassistant/components/pcal9535a/__init__.py deleted file mode 100644 index fa1295939be..00000000000 --- a/homeassistant/components/pcal9535a/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -"""Support for I2C PCAL9535A chip.""" - -DOMAIN = "pcal9535a" diff --git a/homeassistant/components/pcal9535a/binary_sensor.py b/homeassistant/components/pcal9535a/binary_sensor.py deleted file mode 100644 index 729bc534402..00000000000 --- a/homeassistant/components/pcal9535a/binary_sensor.py +++ /dev/null @@ -1,110 +0,0 @@ -"""Support for binary sensor using I2C PCAL9535A chip.""" -from __future__ import annotations - -import logging - -from pcal9535a import PCAL9535A -import voluptuous as vol - -from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity -from homeassistant.const import DEVICE_DEFAULT_NAME -from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType - -CONF_INVERT_LOGIC = "invert_logic" -CONF_I2C_ADDRESS = "i2c_address" -CONF_I2C_BUS = "i2c_bus" -CONF_PINS = "pins" -CONF_PULL_MODE = "pull_mode" - -MODE_UP = "UP" -MODE_DOWN = "DOWN" -MODE_DISABLED = "DISABLED" - -DEFAULT_INVERT_LOGIC = False -DEFAULT_I2C_ADDRESS = 0x20 -DEFAULT_I2C_BUS = 1 -DEFAULT_PULL_MODE = MODE_DISABLED - -_SENSORS_SCHEMA = vol.Schema({cv.positive_int: cv.string}) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_PINS): _SENSORS_SCHEMA, - vol.Optional(CONF_INVERT_LOGIC, default=DEFAULT_INVERT_LOGIC): cv.boolean, - vol.Optional(CONF_PULL_MODE, default=DEFAULT_PULL_MODE): vol.All( - vol.Upper, vol.In([MODE_UP, MODE_DOWN, MODE_DISABLED]) - ), - vol.Optional(CONF_I2C_ADDRESS, default=DEFAULT_I2C_ADDRESS): vol.Coerce(int), - vol.Optional(CONF_I2C_BUS, default=DEFAULT_I2C_BUS): cv.positive_int, - } -) - -_LOGGER = logging.getLogger(__name__) - - -def setup_platform( - hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the PCAL9535A binary sensors.""" - _LOGGER.warning( - "The PCAL9535A I/O Expander integration is deprecated and will be removed " - "in Home Assistant Core 2022.4; this integration is removed under " - "Architectural Decision Record 0019, more information can be found here: " - "https://github.com/home-assistant/architecture/blob/master/adr/0019-GPIO.md" - ) - - pull_mode = config[CONF_PULL_MODE] - invert_logic = config[CONF_INVERT_LOGIC] - i2c_address = config[CONF_I2C_ADDRESS] - bus = config[CONF_I2C_BUS] - - pcal = PCAL9535A(bus, i2c_address) - - binary_sensors = [] - pins = config[CONF_PINS] - - for pin_num, pin_name in pins.items(): - pin = pcal.get_pin(pin_num // 8, pin_num % 8) - binary_sensors.append( - PCAL9535ABinarySensor(pin_name, pin, pull_mode, invert_logic) - ) - - add_entities(binary_sensors, True) - - -class PCAL9535ABinarySensor(BinarySensorEntity): - """Represent a binary sensor that uses PCAL9535A.""" - - def __init__(self, name, pin, pull_mode, invert_logic): - """Initialize the PCAL9535A binary sensor.""" - self._name = name or DEVICE_DEFAULT_NAME - self._pin = pin - self._pin.input = True - self._pin.inverted = invert_logic - if pull_mode == "DISABLED": - self._pin.pullup = 0 - elif pull_mode == "DOWN": - self._pin.pullup = -1 - else: - self._pin.pullup = 1 - self._state = None - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def is_on(self): - """Return the cached state of the entity.""" - return self._state - - def update(self): - """Update the GPIO state.""" - self._state = self._pin.level diff --git a/homeassistant/components/pcal9535a/manifest.json b/homeassistant/components/pcal9535a/manifest.json deleted file mode 100644 index fc821426542..00000000000 --- a/homeassistant/components/pcal9535a/manifest.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "domain": "pcal9535a", - "name": "PCAL9535A I/O Expander", - "documentation": "https://www.home-assistant.io/integrations/pcal9535a", - "requirements": ["pcal9535a==0.7"], - "codeowners": ["@Shulyaka"], - "iot_class": "local_polling", - "loggers": ["pcal9535a", "smbus_cffi"] -} diff --git a/homeassistant/components/pcal9535a/switch.py b/homeassistant/components/pcal9535a/switch.py deleted file mode 100644 index 70da61a597a..00000000000 --- a/homeassistant/components/pcal9535a/switch.py +++ /dev/null @@ -1,119 +0,0 @@ -"""Support for switch sensor using I2C PCAL9535A chip.""" -from __future__ import annotations - -import logging - -from pcal9535a import PCAL9535A -import voluptuous as vol - -from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity -from homeassistant.const import DEVICE_DEFAULT_NAME -from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType - -CONF_INVERT_LOGIC = "invert_logic" -CONF_I2C_ADDRESS = "i2c_address" -CONF_I2C_BUS = "i2c_bus" -CONF_PINS = "pins" -CONF_STRENGTH = "strength" - -STRENGTH_025 = "0.25" -STRENGTH_050 = "0.5" -STRENGTH_075 = "0.75" -STRENGTH_100 = "1.0" - -DEFAULT_INVERT_LOGIC = False -DEFAULT_I2C_ADDRESS = 0x20 -DEFAULT_I2C_BUS = 1 -DEFAULT_STRENGTH = STRENGTH_100 - -_SWITCHES_SCHEMA = vol.Schema({cv.positive_int: cv.string}) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_PINS): _SWITCHES_SCHEMA, - vol.Optional(CONF_INVERT_LOGIC, default=DEFAULT_INVERT_LOGIC): cv.boolean, - vol.Optional(CONF_STRENGTH, default=DEFAULT_STRENGTH): vol.In( - [STRENGTH_025, STRENGTH_050, STRENGTH_075, STRENGTH_100] - ), - vol.Optional(CONF_I2C_ADDRESS, default=DEFAULT_I2C_ADDRESS): vol.Coerce(int), - vol.Optional(CONF_I2C_BUS, default=DEFAULT_I2C_BUS): cv.positive_int, - } -) - -_LOGGER = logging.getLogger(__name__) - - -def setup_platform( - hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the PCAL9535A devices.""" - _LOGGER.warning( - "The PCAL9535A I/O Expander integration is deprecated and will be removed " - "in Home Assistant Core 2022.4; this integration is removed under " - "Architectural Decision Record 0019, more information can be found here: " - "https://github.com/home-assistant/architecture/blob/master/adr/0019-GPIO.md" - ) - - invert_logic = config[CONF_INVERT_LOGIC] - i2c_address = config[CONF_I2C_ADDRESS] - bus = config[CONF_I2C_BUS] - - pcal = PCAL9535A(bus, i2c_address) - - switches = [] - pins = config[CONF_PINS] - for pin_num, pin_name in pins.items(): - pin = pcal.get_pin(pin_num // 8, pin_num % 8) - switches.append(PCAL9535ASwitch(pin_name, pin, invert_logic)) - - add_entities(switches) - - -class PCAL9535ASwitch(SwitchEntity): - """Representation of a PCAL9535A output pin.""" - - def __init__(self, name, pin, invert_logic): - """Initialize the pin.""" - self._name = name or DEVICE_DEFAULT_NAME - self._pin = pin - self._pin.inverted = invert_logic - self._pin.input = False - self._state = self._pin.level - - @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 - - @property - def assumed_state(self): - """Return true if optimistic updates are used.""" - return True - - def turn_on(self, **kwargs): - """Turn the device on.""" - self._pin.level = True - self._state = True - self.schedule_update_ha_state() - - def turn_off(self, **kwargs): - """Turn the device off.""" - self._pin.level = False - self._state = False - self.schedule_update_ha_state() diff --git a/homeassistant/components/peco/__init__.py b/homeassistant/components/peco/__init__.py new file mode 100644 index 00000000000..068a56c9bb8 --- /dev/null +++ b/homeassistant/components/peco/__init__.py @@ -0,0 +1,65 @@ +"""The PECO Outage Counter integration.""" +from __future__ import annotations + +import asyncio +from datetime import timedelta +from typing import Final + +from peco import BadJSONError, HttpError, OutageResults, PecoOutageApi + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import CONF_COUNTY, DOMAIN, LOGGER, SCAN_INTERVAL + +PLATFORMS: Final = [Platform.SENSOR] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up PECO Outage Counter from a config entry.""" + + websession = async_get_clientsession(hass) + api = PecoOutageApi() + county: str = entry.data[CONF_COUNTY] + + async def async_update_data() -> OutageResults: + """Fetch data from API.""" + try: + data: OutageResults = ( + await api.get_outage_totals(websession) + if county == "TOTAL" + else await api.get_outage_count(county, websession) + ) + except HttpError as err: + raise UpdateFailed(f"Error fetching data: {err}") from err + except BadJSONError as err: + raise UpdateFailed(f"Error parsing data: {err}") from err + except asyncio.TimeoutError as err: + raise UpdateFailed(f"Timeout fetching data: {err}") from err + return data + + coordinator = DataUpdateCoordinator( + hass, + LOGGER, + name="PECO Outage Count", + update_method=async_update_data, + update_interval=timedelta(minutes=SCAN_INTERVAL), + ) + + await coordinator.async_config_entry_first_refresh() + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/peco/config_flow.py b/homeassistant/components/peco/config_flow.py new file mode 100644 index 00000000000..63ca7f3291a --- /dev/null +++ b/homeassistant/components/peco/config_flow.py @@ -0,0 +1,41 @@ +"""Config flow for PECO Outage Counter integration.""" +from __future__ import annotations + +from typing import Any + +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.data_entry_flow import FlowResult + +from .const import CONF_COUNTY, COUNTY_LIST, DOMAIN + +STEP_USER_DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_COUNTY): vol.In(COUNTY_LIST), + } +) + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for PECO Outage Counter.""" + + VERSION = 1 + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the initial step.""" + if user_input is None: + return self.async_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA + ) + + county = user_input[CONF_COUNTY] + + await self.async_set_unique_id(county) + self._abort_if_unique_id_configured() + + return self.async_create_entry( + title=f"{county.capitalize()} Outage Count", data=user_input + ) diff --git a/homeassistant/components/peco/const.py b/homeassistant/components/peco/const.py new file mode 100644 index 00000000000..18ed2c28b35 --- /dev/null +++ b/homeassistant/components/peco/const.py @@ -0,0 +1,18 @@ +"""Constants for the PECO Outage Counter integration.""" +import logging +from typing import Final + +DOMAIN: Final = "peco" +LOGGER: Final = logging.getLogger(__package__) +COUNTY_LIST: Final = [ + "BUCKS", + "CHESTER", + "DELAWARE", + "MONTGOMERY", + "PHILADELPHIA", + "YORK", + "TOTAL", +] +CONFIG_FLOW_COUNTIES: Final = [{county: county.capitalize()} for county in COUNTY_LIST] +SCAN_INTERVAL: Final = 9 +CONF_COUNTY: Final = "county" diff --git a/homeassistant/components/peco/manifest.json b/homeassistant/components/peco/manifest.json new file mode 100644 index 00000000000..7f41f2c0417 --- /dev/null +++ b/homeassistant/components/peco/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "peco", + "name": "PECO Outage Counter", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/peco", + "codeowners": ["@IceBotYT"], + "iot_class": "cloud_polling", + "requirements": ["peco==0.0.25"] +} diff --git a/homeassistant/components/peco/sensor.py b/homeassistant/components/peco/sensor.py new file mode 100644 index 00000000000..bef5040a6f6 --- /dev/null +++ b/homeassistant/components/peco/sensor.py @@ -0,0 +1,105 @@ +"""Sensor component for PECO outage counter.""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +from typing import Final + +from peco import OutageResults + +from homeassistant.components.sensor import ( + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import PERCENTAGE +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) + +from .const import CONF_COUNTY, DOMAIN + + +@dataclass +class PECOSensorEntityDescriptionMixin: + """Mixin for required keys.""" + + value_fn: Callable[[OutageResults], int] + + +@dataclass +class PECOSensorEntityDescription( + SensorEntityDescription, PECOSensorEntityDescriptionMixin +): + """Description for PECO sensor.""" + + +PARALLEL_UPDATES: Final = 0 +SENSOR_LIST: tuple[PECOSensorEntityDescription, ...] = ( + PECOSensorEntityDescription( + key="customers_out", + name="Customers Out", + value_fn=lambda data: int(data.customers_out), + ), + PECOSensorEntityDescription( + key="percent_customers_out", + name="Percent Customers Out", + native_unit_of_measurement=PERCENTAGE, + value_fn=lambda data: int(data.percent_customers_out), + ), + PECOSensorEntityDescription( + key="outage_count", + name="Outage Count", + value_fn=lambda data: int(data.outage_count), + ), + PECOSensorEntityDescription( + key="customers_served", + name="Customers Served", + value_fn=lambda data: int(data.customers_served), + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the sensor platform.""" + county: str = config_entry.data[CONF_COUNTY] + coordinator = hass.data[DOMAIN][config_entry.entry_id] + + async_add_entities( + [PecoSensor(sensor, county, coordinator) for sensor in SENSOR_LIST], + True, + ) + return + + +class PecoSensor(CoordinatorEntity[DataUpdateCoordinator[OutageResults]], SensorEntity): + """PECO outage counter sensor.""" + + _attr_state_class = SensorStateClass.MEASUREMENT + _attr_icon: str = "mdi:power-plug-off" + entity_description: PECOSensorEntityDescription + + def __init__( + self, + description: PECOSensorEntityDescription, + county: str, + coordinator: DataUpdateCoordinator[OutageResults], + ) -> None: + """Initialize the sensor.""" + super().__init__(coordinator) + self._attr_name = f"{county.capitalize()} {description.name}" + self._attr_unique_id = f"{county}-{description.key}" + self.entity_description = description + + @property + def native_value(self) -> int: + """Return the value of the sensor.""" + return self.entity_description.value_fn(self.coordinator.data) diff --git a/homeassistant/components/peco/strings.json b/homeassistant/components/peco/strings.json new file mode 100644 index 00000000000..54208b12d93 --- /dev/null +++ b/homeassistant/components/peco/strings.json @@ -0,0 +1,14 @@ +{ + "config": { + "step": { + "user": { + "data": { + "county": "County" + } + } + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" + } + } +} diff --git a/homeassistant/components/smarthab/translations/sk.json b/homeassistant/components/peco/translations/en.json similarity index 53% rename from homeassistant/components/smarthab/translations/sk.json rename to homeassistant/components/peco/translations/en.json index 72b0304f1c3..6f7ff2b0b12 100644 --- a/homeassistant/components/smarthab/translations/sk.json +++ b/homeassistant/components/peco/translations/en.json @@ -1,12 +1,12 @@ { "config": { - "error": { - "invalid_auth": "Neplatn\u00e9 overenie" + "abort": { + "already_configured": "Service is already configured" }, "step": { "user": { "data": { - "email": "Email" + "county": "County" } } } diff --git a/homeassistant/components/persistent_notification/__init__.py b/homeassistant/components/persistent_notification/__init__.py index 88a61b52038..c247a326036 100644 --- a/homeassistant/components/persistent_notification/__init__.py +++ b/homeassistant/components/persistent_notification/__init__.py @@ -3,17 +3,15 @@ from __future__ import annotations from collections.abc import Mapping import logging -from typing import Any, cast +from typing import Any import voluptuous as vol from homeassistant.components import websocket_api from homeassistant.const import ATTR_FRIENDLY_NAME from homeassistant.core import Context, HomeAssistant, ServiceCall, callback -from homeassistant.exceptions import TemplateError from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity import async_generate_entity_id -from homeassistant.helpers.template import Template, is_template_string from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass from homeassistant.util import slugify @@ -82,36 +80,11 @@ def async_create( ) notification_id = entity_id.split(".")[1] - warn = False - - attr: dict[str, str] = {} + attr: dict[str, str] = {ATTR_MESSAGE: message} if title is not None: - if is_template_string(title): - warn = True - try: - title = cast( - str, Template(title, hass).async_render(parse_result=False) # type: ignore[no-untyped-call] - ) - except TemplateError as ex: - _LOGGER.error("Error rendering title %s: %s", title, ex) - attr[ATTR_TITLE] = title attr[ATTR_FRIENDLY_NAME] = title - if is_template_string(message): - warn = True - try: - message = Template(message, hass).async_render(parse_result=False) # type: ignore[no-untyped-call] - except TemplateError as ex: - _LOGGER.error("Error rendering message %s: %s", message, ex) - - attr[ATTR_MESSAGE] = message - - if warn: - _LOGGER.warning( - "Passing a template string to persistent_notification.async_create function is deprecated" - ) - hass.states.async_set(entity_id, STATE, attr, context=context) # Store notification and fire event diff --git a/homeassistant/components/philips_js/light.py b/homeassistant/components/philips_js/light.py index fc26fc3ed6a..21bb7199269 100644 --- a/homeassistant/components/philips_js/light.py +++ b/homeassistant/components/philips_js/light.py @@ -123,7 +123,9 @@ def _average_pixels(data): return 0.0, 0.0, 0.0 -class PhilipsTVLightEntity(CoordinatorEntity, LightEntity): +class PhilipsTVLightEntity( + CoordinatorEntity[PhilipsTVDataUpdateCoordinator], LightEntity +): """Representation of a Philips TV exposing the JointSpace API.""" def __init__( diff --git a/homeassistant/components/philips_js/media_player.py b/homeassistant/components/philips_js/media_player.py index be24be632fc..77a1d9dccd9 100644 --- a/homeassistant/components/philips_js/media_player.py +++ b/homeassistant/components/philips_js/media_player.py @@ -79,10 +79,11 @@ async def async_setup_entry( ) -class PhilipsTVMediaPlayer(CoordinatorEntity, MediaPlayerEntity): +class PhilipsTVMediaPlayer( + CoordinatorEntity[PhilipsTVDataUpdateCoordinator], MediaPlayerEntity +): """Representation of a Philips TV exposing the JointSpace API.""" - coordinator: PhilipsTVDataUpdateCoordinator _attr_device_class = MediaPlayerDeviceClass.TV def __init__( @@ -424,7 +425,7 @@ class PhilipsTVMediaPlayer(CoordinatorEntity, MediaPlayerEntity): """Return root media objects.""" return BrowseMedia( - title="Library", + title="Philips TV", media_class=MEDIA_CLASS_DIRECTORY, media_content_id="", media_content_type="", diff --git a/homeassistant/components/philips_js/remote.py b/homeassistant/components/philips_js/remote.py index 09fe16215b6..38851964427 100644 --- a/homeassistant/components/philips_js/remote.py +++ b/homeassistant/components/philips_js/remote.py @@ -27,11 +27,9 @@ async def async_setup_entry( async_add_entities([PhilipsTVRemote(coordinator)]) -class PhilipsTVRemote(CoordinatorEntity, RemoteEntity): +class PhilipsTVRemote(CoordinatorEntity[PhilipsTVDataUpdateCoordinator], RemoteEntity): """Device that sends commands.""" - coordinator: PhilipsTVDataUpdateCoordinator - def __init__( self, coordinator: PhilipsTVDataUpdateCoordinator, diff --git a/homeassistant/components/philips_js/strings.json b/homeassistant/components/philips_js/strings.json index 3e6d4f494d3..dc258385804 100644 --- a/homeassistant/components/philips_js/strings.json +++ b/homeassistant/components/philips_js/strings.json @@ -10,7 +10,7 @@ "pair": { "title": "Pair", "description": "Enter the PIN displayed on your TV", - "data":{ + "data": { "pin": "[%key:common::config_flow::data::pin%]" } } diff --git a/homeassistant/components/philips_js/switch.py b/homeassistant/components/philips_js/switch.py index 15f72a8aaff..a89c22f1850 100644 --- a/homeassistant/components/philips_js/switch.py +++ b/homeassistant/components/philips_js/switch.py @@ -27,11 +27,11 @@ async def async_setup_entry( async_add_entities([PhilipsTVScreenSwitch(coordinator)]) -class PhilipsTVScreenSwitch(CoordinatorEntity, SwitchEntity): +class PhilipsTVScreenSwitch( + CoordinatorEntity[PhilipsTVDataUpdateCoordinator], SwitchEntity +): """A Philips TV screen state switch.""" - coordinator: PhilipsTVDataUpdateCoordinator - def __init__( self, coordinator: PhilipsTVDataUpdateCoordinator, diff --git a/homeassistant/components/philips_js/translations/fr.json b/homeassistant/components/philips_js/translations/fr.json index 8cc5d187743..3e5d451bbc3 100644 --- a/homeassistant/components/philips_js/translations/fr.json +++ b/homeassistant/components/philips_js/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_pin": "NIP invalide", + "invalid_pin": "PIN non valide", "pairing_failure": "Association impossible: {error_id}", "unknown": "Erreur inattendue" }, diff --git a/homeassistant/components/pi4ioe5v9xxxx/__init__.py b/homeassistant/components/pi4ioe5v9xxxx/__init__.py deleted file mode 100644 index 516cfc32575..00000000000 --- a/homeassistant/components/pi4ioe5v9xxxx/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Support for controlling IO expanders from Digital.com (PI4IOE5V9570, PI4IOE5V9674, PI4IOE5V9673, PI4IOE5V96224, PI4IOE5V96248).""" diff --git a/homeassistant/components/pi4ioe5v9xxxx/binary_sensor.py b/homeassistant/components/pi4ioe5v9xxxx/binary_sensor.py deleted file mode 100644 index c1a82c3eb5a..00000000000 --- a/homeassistant/components/pi4ioe5v9xxxx/binary_sensor.py +++ /dev/null @@ -1,97 +0,0 @@ -"""Support for binary sensor using RPi GPIO.""" -from __future__ import annotations - -import logging - -from pi4ioe5v9xxxx import pi4ioe5v9xxxx -import voluptuous as vol - -from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity -from homeassistant.const import DEVICE_DEFAULT_NAME -from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType - -CONF_INVERT_LOGIC = "invert_logic" -CONF_PINS = "pins" -CONF_I2CBUS = "i2c_bus" -CONF_I2CADDR = "i2c_address" -CONF_BITS = "bits" - -DEFAULT_INVERT_LOGIC = False -DEFAULT_BITS = 24 -DEFAULT_BUS = 1 -DEFAULT_ADDR = 0x20 - - -_SENSORS_SCHEMA = vol.Schema({cv.positive_int: cv.string}) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_PINS): _SENSORS_SCHEMA, - vol.Optional(CONF_I2CBUS, default=DEFAULT_BUS): cv.positive_int, - vol.Optional(CONF_I2CADDR, default=DEFAULT_ADDR): cv.positive_int, - vol.Optional(CONF_BITS, default=DEFAULT_BITS): cv.positive_int, - vol.Optional(CONF_INVERT_LOGIC, default=DEFAULT_INVERT_LOGIC): cv.boolean, - } -) - -_LOGGER = logging.getLogger(__name__) - - -def setup_platform( - hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the IO expander devices.""" - _LOGGER.warning( - "The pi4ioe5v9xxxx IO Expander integration is deprecated and will be removed " - "in Home Assistant Core 2022.4; this integration is removed under " - "Architectural Decision Record 0019, more information can be found here: " - "https://github.com/home-assistant/architecture/blob/master/adr/0019-GPIO.md" - ) - - pins = config[CONF_PINS] - binary_sensors = [] - - pi4ioe5v9xxxx.setup( - i2c_bus=config[CONF_I2CBUS], - i2c_addr=config[CONF_I2CADDR], - bits=config[CONF_BITS], - read_mode=True, - invert=False, - ) - for pin_num, pin_name in pins.items(): - binary_sensors.append( - Pi4ioe5v9BinarySensor(pin_name, pin_num, config[CONF_INVERT_LOGIC]) - ) - add_entities(binary_sensors, True) - - -class Pi4ioe5v9BinarySensor(BinarySensorEntity): - """Represent a binary sensor that uses pi4ioe5v9xxxx IO expander in read mode.""" - - def __init__(self, name, pin, invert_logic): - """Initialize the pi4ioe5v9xxxx sensor.""" - self._name = name or DEVICE_DEFAULT_NAME - self._pin = pin - self._invert_logic = invert_logic - self._state = pi4ioe5v9xxxx.pin_from_memory(self._pin) - - @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 - - def update(self): - """Update the IO state.""" - pi4ioe5v9xxxx.hw_to_memory() - self._state = pi4ioe5v9xxxx.pin_from_memory(self._pin) diff --git a/homeassistant/components/pi4ioe5v9xxxx/manifest.json b/homeassistant/components/pi4ioe5v9xxxx/manifest.json deleted file mode 100644 index 3ea322a6c63..00000000000 --- a/homeassistant/components/pi4ioe5v9xxxx/manifest.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "domain": "pi4ioe5v9xxxx", - "name": "pi4ioe5v9xxxx IO Expander", - "documentation": "https://www.home-assistant.io/integrations/pi4ioe5v9xxxx", - "requirements": ["pi4ioe5v9xxxx==0.0.2"], - "codeowners": ["@antonverburg"], - "iot_class": "local_polling", - "loggers": ["pi4ioe5v9xxxx", "smbus2"] -} diff --git a/homeassistant/components/pi4ioe5v9xxxx/switch.py b/homeassistant/components/pi4ioe5v9xxxx/switch.py deleted file mode 100644 index 28a963629e9..00000000000 --- a/homeassistant/components/pi4ioe5v9xxxx/switch.py +++ /dev/null @@ -1,108 +0,0 @@ -"""Allows to configure a switch using RPi GPIO.""" -from __future__ import annotations - -import logging - -from pi4ioe5v9xxxx import pi4ioe5v9xxxx -import voluptuous as vol - -from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity -from homeassistant.const import DEVICE_DEFAULT_NAME -from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType - -CONF_PINS = "pins" -CONF_INVERT_LOGIC = "invert_logic" -CONF_I2CBUS = "i2c_bus" -CONF_I2CADDR = "i2c_address" -CONF_BITS = "bits" - -DEFAULT_INVERT_LOGIC = False -DEFAULT_BITS = 24 -DEFAULT_BUS = 1 -DEFAULT_ADDR = 0x20 - -_SWITCHES_SCHEMA = vol.Schema({cv.positive_int: cv.string}) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_PINS): _SWITCHES_SCHEMA, - vol.Optional(CONF_I2CBUS, default=DEFAULT_BUS): cv.positive_int, - vol.Optional(CONF_I2CADDR, default=DEFAULT_ADDR): cv.positive_int, - vol.Optional(CONF_BITS, default=DEFAULT_BITS): cv.positive_int, - vol.Optional(CONF_INVERT_LOGIC, default=DEFAULT_INVERT_LOGIC): cv.boolean, - } -) - -_LOGGER = logging.getLogger(__name__) - - -def setup_platform( - hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the swiches devices.""" - _LOGGER.warning( - "The pi4ioe5v9xxxx IO Expander integration is deprecated and will be removed " - "in Home Assistant Core 2022.4; this integration is removed under " - "Architectural Decision Record 0019, more information can be found here: " - "https://github.com/home-assistant/architecture/blob/master/adr/0019-GPIO.md" - ) - - pins = config[CONF_PINS] - switches = [] - - pi4ioe5v9xxxx.setup( - i2c_bus=config[CONF_I2CBUS], - i2c_addr=config[CONF_I2CADDR], - bits=config[CONF_BITS], - read_mode=False, - invert=False, - ) - for pin, name in pins.items(): - switches.append(Pi4ioe5v9Switch(name, pin, config[CONF_INVERT_LOGIC])) - add_entities(switches) - - -class Pi4ioe5v9Switch(SwitchEntity): - """Representation of a pi4ioe5v9 IO expansion IO.""" - - def __init__(self, name, pin, invert_logic): - """Initialize the pin.""" - self._name = name or DEVICE_DEFAULT_NAME - self._pin = pin - self._invert_logic = invert_logic - self._state = not invert_logic - - @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.""" - pi4ioe5v9xxxx.pin_to_memory(self._pin, not self._invert_logic) - pi4ioe5v9xxxx.memory_to_hw() - self._state = True - self.schedule_update_ha_state() - - def turn_off(self, **kwargs): - """Turn the device off.""" - pi4ioe5v9xxxx.pin_to_memory(self._pin, self._invert_logic) - pi4ioe5v9xxxx.memory_to_hw() - self._state = False - self.schedule_update_ha_state() diff --git a/homeassistant/components/pi_hole/__init__.py b/homeassistant/components/pi_hole/__init__.py index 3e18953af84..df156353b88 100644 --- a/homeassistant/components/pi_hole/__init__.py +++ b/homeassistant/components/pi_hole/__init__.py @@ -11,6 +11,7 @@ from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( CONF_API_KEY, CONF_HOST, + CONF_LOCATION, CONF_NAME, CONF_SSL, CONF_VERIFY_SSL, @@ -29,7 +30,6 @@ from homeassistant.helpers.update_coordinator import ( ) from .const import ( - CONF_LOCATION, CONF_STATISTICS_ONLY, DATA_KEY_API, DATA_KEY_COORDINATOR, @@ -153,7 +153,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @callback def _async_platforms(entry: ConfigEntry) -> list[Platform]: """Return platforms to be loaded / unloaded.""" - platforms = [Platform.BINARY_SENSOR, Platform.SENSOR] + platforms = [Platform.BINARY_SENSOR, Platform.UPDATE, Platform.SENSOR] if not entry.data[CONF_STATISTICS_ONLY]: platforms.append(Platform.SWITCH) return platforms diff --git a/homeassistant/components/pi_hole/config_flow.py b/homeassistant/components/pi_hole/config_flow.py index 39d47ecdd74..40f4555e7d2 100644 --- a/homeassistant/components/pi_hole/config_flow.py +++ b/homeassistant/components/pi_hole/config_flow.py @@ -12,6 +12,7 @@ from homeassistant import config_entries from homeassistant.const import ( CONF_API_KEY, CONF_HOST, + CONF_LOCATION, CONF_NAME, CONF_PORT, CONF_SSL, @@ -21,7 +22,6 @@ from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import ( - CONF_LOCATION, CONF_STATISTICS_ONLY, DEFAULT_LOCATION, DEFAULT_NAME, diff --git a/homeassistant/components/pi_hole/const.py b/homeassistant/components/pi_hole/const.py index 38819d29df4..c73660faedb 100644 --- a/homeassistant/components/pi_hole/const.py +++ b/homeassistant/components/pi_hole/const.py @@ -17,7 +17,6 @@ from homeassistant.const import PERCENTAGE DOMAIN = "pi_hole" -CONF_LOCATION = "location" CONF_STATISTICS_ONLY = "statistics_only" DEFAULT_LOCATION = "admin" @@ -120,8 +119,10 @@ class PiHoleBinarySensorEntityDescription( BINARY_SENSOR_TYPES: tuple[PiHoleBinarySensorEntityDescription, ...] = ( PiHoleBinarySensorEntityDescription( + # Deprecated, scheduled to be removed in 2022.6 key="core_update_available", name="Core Update Available", + entity_registry_enabled_default=False, device_class=BinarySensorDeviceClass.UPDATE, extra_value=lambda api: { "current_version": api.versions["core_current"], @@ -130,8 +131,10 @@ BINARY_SENSOR_TYPES: tuple[PiHoleBinarySensorEntityDescription, ...] = ( state_value=lambda api: bool(api.versions["core_update"]), ), PiHoleBinarySensorEntityDescription( + # Deprecated, scheduled to be removed in 2022.6 key="web_update_available", name="Web Update Available", + entity_registry_enabled_default=False, device_class=BinarySensorDeviceClass.UPDATE, extra_value=lambda api: { "current_version": api.versions["web_current"], @@ -140,8 +143,10 @@ BINARY_SENSOR_TYPES: tuple[PiHoleBinarySensorEntityDescription, ...] = ( state_value=lambda api: bool(api.versions["web_update"]), ), PiHoleBinarySensorEntityDescription( + # Deprecated, scheduled to be removed in 2022.6 key="ftl_update_available", name="FTL Update Available", + entity_registry_enabled_default=False, device_class=BinarySensorDeviceClass.UPDATE, extra_value=lambda api: { "current_version": api.versions["FTL_current"], diff --git a/homeassistant/components/pi_hole/update.py b/homeassistant/components/pi_hole/update.py new file mode 100644 index 00000000000..6bb424f15b1 --- /dev/null +++ b/homeassistant/components/pi_hole/update.py @@ -0,0 +1,121 @@ +"""Support for update entities of a Pi-hole system.""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass + +from hole import Hole + +from homeassistant.components.update import UpdateEntity, UpdateEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_NAME +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from . import PiHoleEntity +from .const import DATA_KEY_API, DATA_KEY_COORDINATOR, DOMAIN + + +@dataclass +class PiHoleUpdateEntityDescription(UpdateEntityDescription): + """Describes PiHole update entity.""" + + installed_version: Callable[[dict], str | None] = lambda api: None + latest_version: Callable[[dict], str | None] = lambda api: None + release_base_url: str | None = None + title: str | None = None + + +UPDATE_ENTITY_TYPES: tuple[PiHoleUpdateEntityDescription, ...] = ( + PiHoleUpdateEntityDescription( + key="core_update_available", + name="Core Update Available", + title="Pi-hole Core", + entity_category=EntityCategory.DIAGNOSTIC, + installed_version=lambda versions: versions.get("core_current"), + latest_version=lambda versions: versions.get("core_latest"), + release_base_url="https://github.com/pi-hole/pi-hole/releases/tag", + ), + PiHoleUpdateEntityDescription( + key="web_update_available", + name="Web Update Available", + title="Pi-hole Web interface", + entity_category=EntityCategory.DIAGNOSTIC, + installed_version=lambda versions: versions.get("web_current"), + latest_version=lambda versions: versions.get("web_latest"), + release_base_url="https://github.com/pi-hole/AdminLTE/releases/tag", + ), + PiHoleUpdateEntityDescription( + key="ftl_update_available", + name="FTL Update Available", + title="Pi-hole FTL DNS", + entity_category=EntityCategory.DIAGNOSTIC, + installed_version=lambda versions: versions.get("FTL_current"), + latest_version=lambda versions: versions.get("FTL_latest"), + release_base_url="https://github.com/pi-hole/FTL/releases/tag", + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up the Pi-hole update entities.""" + name = entry.data[CONF_NAME] + hole_data = hass.data[DOMAIN][entry.entry_id] + + async_add_entities( + PiHoleUpdateEntity( + hole_data[DATA_KEY_API], + hole_data[DATA_KEY_COORDINATOR], + name, + entry.entry_id, + description, + ) + for description in UPDATE_ENTITY_TYPES + ) + + +class PiHoleUpdateEntity(PiHoleEntity, UpdateEntity): + """Representation of a Pi-hole update entity.""" + + entity_description: PiHoleUpdateEntityDescription + + def __init__( + self, + api: Hole, + coordinator: DataUpdateCoordinator, + name: str, + server_unique_id: str, + description: PiHoleUpdateEntityDescription, + ) -> None: + """Initialize a Pi-hole update entity.""" + super().__init__(api, coordinator, name, server_unique_id) + self.entity_description = description + + self._attr_name = f"{name} {description.name}" + self._attr_unique_id = f"{self._server_unique_id}/{description.name}" + self._attr_title = description.title + + @property + def installed_version(self) -> str | None: + """Version installed and in use.""" + if isinstance(self.api.versions, dict): + return self.entity_description.installed_version(self.api.versions) + return None + + @property + def latest_version(self) -> str | None: + """Latest version available for install.""" + if isinstance(self.api.versions, dict): + return self.entity_description.latest_version(self.api.versions) + return None + + @property + def release_url(self) -> str | None: + """URL to the full release notes of the latest version available.""" + if self.latest_version: + return f"{self.entity_description.release_base_url}/{self.latest_version}" + return None diff --git a/homeassistant/components/picnic/manifest.json b/homeassistant/components/picnic/manifest.json index 54bcab8e3fd..8012bf548c8 100644 --- a/homeassistant/components/picnic/manifest.json +++ b/homeassistant/components/picnic/manifest.json @@ -7,4 +7,4 @@ "requirements": ["python-picnic-api==1.1.0"], "codeowners": ["@corneyl"], "loggers": ["python_picnic_api"] -} \ No newline at end of file +} diff --git a/homeassistant/components/picnic/translations/fr.json b/homeassistant/components/picnic/translations/fr.json index 794a33ffe75..77d9756b5b8 100644 --- a/homeassistant/components/picnic/translations/fr.json +++ b/homeassistant/components/picnic/translations/fr.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "\u00c9chec de connexion", "different_account": "Le compte doit \u00eatre le m\u00eame que celui utilis\u00e9 pour configurer l'int\u00e9gration", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/piglow/__init__.py b/homeassistant/components/piglow/__init__.py deleted file mode 100644 index e6d4bbd3ec2..00000000000 --- a/homeassistant/components/piglow/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The piglow component.""" diff --git a/homeassistant/components/piglow/light.py b/homeassistant/components/piglow/light.py deleted file mode 100644 index f66121ec413..00000000000 --- a/homeassistant/components/piglow/light.py +++ /dev/null @@ -1,129 +0,0 @@ -"""Support for Piglow LED's.""" -from __future__ import annotations - -import logging -import subprocess - -import piglow -import voluptuous as vol - -from homeassistant.components.light import ( - ATTR_BRIGHTNESS, - ATTR_HS_COLOR, - PLATFORM_SCHEMA, - SUPPORT_BRIGHTNESS, - SUPPORT_COLOR, - LightEntity, -) -from homeassistant.const import CONF_NAME -from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -import homeassistant.util.color as color_util - -_LOGGER = logging.getLogger(__name__) - -SUPPORT_PIGLOW = SUPPORT_BRIGHTNESS | SUPPORT_COLOR - -DEFAULT_NAME = "Piglow" - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - {vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string} -) - - -def setup_platform( - hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the Piglow Light platform.""" - _LOGGER.warning( - "The Piglow integration is deprecated and will be removed " - "in Home Assistant Core 2022.4; this integration is removed under " - "Architectural Decision Record 0019, more information can be found here: " - "https://github.com/home-assistant/architecture/blob/master/adr/0019-GPIO.md" - ) - - if subprocess.getoutput("i2cdetect -q -y 1 | grep -o 54") != "54": - _LOGGER.error("A Piglow device was not found") - return - - name = config.get(CONF_NAME) - - add_entities([PiglowLight(name)]) - - -class PiglowLight(LightEntity): - """Representation of an Piglow Light.""" - - def __init__(self, name): - """Initialize an PiglowLight.""" - self._name = name - self._is_on = False - self._brightness = 255 - self._hs_color = [0, 0] - - @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 hs_color(self): - """Read back the color of the light.""" - return self._hs_color - - @property - def supported_features(self): - """Flag supported features.""" - return SUPPORT_PIGLOW - - @property - def should_poll(self): - """Return if we should poll this device.""" - return False - - @property - def assumed_state(self) -> bool: - """Return True if unable to access real state of the entity.""" - return True - - @property - def is_on(self): - """Return true if light is on.""" - return self._is_on - - def turn_on(self, **kwargs): - """Instruct the light to turn on.""" - piglow.clear() - - if ATTR_BRIGHTNESS in kwargs: - self._brightness = kwargs[ATTR_BRIGHTNESS] - - if ATTR_HS_COLOR in kwargs: - self._hs_color = kwargs[ATTR_HS_COLOR] - - rgb = color_util.color_hsv_to_RGB( - self._hs_color[0], self._hs_color[1], self._brightness / 255 * 100 - ) - piglow.red(rgb[0]) - piglow.green(rgb[1]) - piglow.blue(rgb[2]) - piglow.show() - self._is_on = True - self.schedule_update_ha_state() - - def turn_off(self, **kwargs): - """Instruct the light to turn off.""" - piglow.clear() - piglow.show() - self._is_on = False - self.schedule_update_ha_state() diff --git a/homeassistant/components/piglow/manifest.json b/homeassistant/components/piglow/manifest.json deleted file mode 100644 index f4b869aacf8..00000000000 --- a/homeassistant/components/piglow/manifest.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "domain": "piglow", - "name": "Piglow", - "documentation": "https://www.home-assistant.io/integrations/piglow", - "requirements": ["piglow==1.2.4"], - "codeowners": [], - "iot_class": "local_polling" -} diff --git a/homeassistant/components/plaato/entity.py b/homeassistant/components/plaato/entity.py index b0297ec3024..33ac5d910aa 100644 --- a/homeassistant/components/plaato/entity.py +++ b/homeassistant/components/plaato/entity.py @@ -2,6 +2,7 @@ from pyplaato.models.device import PlaatoDevice from homeassistant.helpers import entity +from homeassistant.helpers.dispatcher import async_dispatcher_connect from .const import ( DEVICE, @@ -95,7 +96,8 @@ class PlaatoEntity(entity.Entity): ) else: self.async_on_remove( - self.hass.helpers.dispatcher.async_dispatcher_connect( + async_dispatcher_connect( + self.hass, SENSOR_SIGNAL % (self._device_id, self._sensor_type), self.async_write_ha_state, ) diff --git a/homeassistant/components/plaato/manifest.json b/homeassistant/components/plaato/manifest.json index ddb3d4474a3..5335a79fe15 100644 --- a/homeassistant/components/plaato/manifest.json +++ b/homeassistant/components/plaato/manifest.json @@ -6,7 +6,7 @@ "dependencies": ["webhook"], "after_dependencies": ["cloud"], "codeowners": ["@JohNan"], - "requirements": ["pyplaato==0.0.15"], + "requirements": ["pyplaato==0.0.16"], "iot_class": "cloud_push", "loggers": ["pyplaato"] } diff --git a/homeassistant/components/plaato/translations/fr.json b/homeassistant/components/plaato/translations/fr.json index 370796c18f3..39cd68d9362 100644 --- a/homeassistant/components/plaato/translations/fr.json +++ b/homeassistant/components/plaato/translations/fr.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9", - "cloud_not_connected": "Pas connect\u00e9 \u00e0 Home Assistant Cloud", + "cloud_not_connected": "Non connect\u00e9 \u00e0 Home Assistant Cloud.", "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible.", "webhook_not_internet_accessible": "Votre installation de Home Assistant doit \u00eatre accessible depuis internet pour recevoir des messages webhook." }, @@ -28,7 +28,7 @@ "device_name": "Nommez votre appareil", "device_type": "Type d'appareil Plaato" }, - "description": "Voulez-vous commencer la configuration ?", + "description": "Voulez-vous commencer la configuration\u00a0?", "title": "Configurer le Webhook Plaato" }, "webhook": { diff --git a/homeassistant/components/plaato/translations/zh-Hant.json b/homeassistant/components/plaato/translations/zh-Hant.json index 8ffa238d816..09b729b3a36 100644 --- a/homeassistant/components/plaato/translations/zh-Hant.json +++ b/homeassistant/components/plaato/translations/zh-Hant.json @@ -10,7 +10,7 @@ "default": "\u540d\u7a31\u70ba **{device_name}** \u7684 Plaato {device_type} \u5df2\u6210\u529f\u8a2d\u5b9a\uff01" }, "error": { - "invalid_webhook_device": "\u6240\u9078\u64c7\u7684\u88dd\u7f6e\u4e0d\u652f\u63f4\u50b3\u9001\u8cc7\u6599\u81f3 Webhook\u3001AirLock \u50c5\u652f\u63f4\u6b64\u985e\u578b", + "invalid_webhook_device": "\u6240\u9078\u64c7\u7684\u88dd\u7f6e\u4e0d\u652f\u63f4\u50b3\u9001\u8cc7\u6599\u81f3 Webhook\u3001AirLock \u50c5\u652f\u63f4\u6b64\u985e\u5225", "no_api_method": "\u9700\u8981\u65b0\u589e\u6388\u6b0a\u6b0a\u6756\u6216\u9078\u64c7 Webhook", "no_auth_token": "\u9700\u8981\u65b0\u589e\u6388\u6b0a\u6b0a\u6756" }, @@ -26,7 +26,7 @@ "user": { "data": { "device_name": "\u88dd\u7f6e\u540d\u7a31", - "device_type": "Plaato \u88dd\u7f6e\u985e\u578b" + "device_type": "Plaato \u88dd\u7f6e\u985e\u5225" }, "description": "\u662f\u5426\u8981\u958b\u59cb\u8a2d\u5b9a\uff1f", "title": "\u8a2d\u5b9a Plaato \u88dd\u7f6e" diff --git a/homeassistant/components/plant/__init__.py b/homeassistant/components/plant/__init__.py index 37ae8422af0..75d412f7f5f 100644 --- a/homeassistant/components/plant/__init__.py +++ b/homeassistant/components/plant/__init__.py @@ -6,8 +6,7 @@ import logging import voluptuous as vol -from homeassistant.components.recorder.models import States -from homeassistant.components.recorder.util import execute, session_scope +from homeassistant.components.recorder import get_instance, history from homeassistant.const import ( ATTR_TEMPERATURE, ATTR_UNIT_OF_MEASUREMENT, @@ -28,6 +27,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.typing import ConfigType +from homeassistant.util import dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -109,11 +109,6 @@ DOMAIN = "plant" CONFIG_SCHEMA = vol.Schema({DOMAIN: {cv.string: PLANT_SCHEMA}}, extra=vol.ALLOW_EXTRA) -# Flag for enabling/disabling the loading of the history from the database. -# This feature is turned off right now as its tests are not 100% stable. -ENABLE_LOAD_HISTORY = False - - async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Plant component.""" component = EntityComponent(_LOGGER, DOMAIN, hass) @@ -281,9 +276,11 @@ class Plant(Entity): async def async_added_to_hass(self): """After being added to hass, load from history.""" - if ENABLE_LOAD_HISTORY and "recorder" in self.hass.config.components: + if "recorder" in self.hass.config.components: # only use the database if it's configured - await self.hass.async_add_executor_job(self._load_history_from_db) + await get_instance(self.hass).async_add_executor_job( + self._load_history_from_db + ) self.async_write_ha_state() async_track_state_change_event( @@ -300,7 +297,7 @@ class Plant(Entity): This only needs to be done once during startup. """ - start_date = datetime.now() - timedelta(days=self._conf_check_days) + start_date = dt_util.utcnow() - timedelta(days=self._conf_check_days) entity_id = self._readingmap.get(READING_BRIGHTNESS) if entity_id is None: _LOGGER.debug( @@ -308,26 +305,22 @@ class Plant(Entity): "there is no brightness sensor configured" ) return - _LOGGER.debug("Initializing values for %s from the database", self._name) - with session_scope(hass=self.hass) as session: - query = ( - session.query(States) - .filter( - (States.entity_id == entity_id.lower()) - and (States.last_updated > start_date) + lower_entity_id = entity_id.lower() + history_list = history.state_changes_during_period( + self.hass, + start_date, + entity_id=lower_entity_id, + no_attributes=True, + ) + for state in history_list.get(lower_entity_id, []): + # filter out all None, NaN and "unknown" states + # only keep real values + with suppress(ValueError): + self._brightness_history.add_measurement( + int(state.state), state.last_updated ) - .order_by(States.last_updated.asc()) - ) - states = execute(query, to_native=True, validate_entity_ids=False) - for state in states: - # filter out all None, NaN and "unknown" states - # only keep real values - with suppress(ValueError): - self._brightness_history.add_measurement( - int(state.state), state.last_updated - ) _LOGGER.debug("Initializing from database completed") @property diff --git a/homeassistant/components/plant/translations/el.json b/homeassistant/components/plant/translations/el.json index 9d8fb65979b..af4f00ff57b 100644 --- a/homeassistant/components/plant/translations/el.json +++ b/homeassistant/components/plant/translations/el.json @@ -5,5 +5,5 @@ "problem": "\u03a0\u03c1\u03cc\u03b2\u03bb\u03b7\u03bc\u03b1" } }, - "title": "\u03a7\u03bb\u03c9\u03c1\u03af\u03b4\u03b1" + "title": "\u03a0\u03b1\u03c1\u03b1\u03ba\u03bf\u03bb\u03bf\u03cd\u03b8\u03b7\u03c3\u03b7 \u03c6\u03c5\u03c4\u03ce\u03bd" } \ No newline at end of file diff --git a/homeassistant/components/plex/__init__.py b/homeassistant/components/plex/__init__.py index df3a4b8cd11..e0a84ced16f 100644 --- a/homeassistant/components/plex/__init__.py +++ b/homeassistant/components/plex/__init__.py @@ -69,7 +69,7 @@ async def async_browse_media(hass, media_content_type, media_content_id, platfor return await hass.async_add_executor_job( partial( browse_media, - plex_server, + hass, is_internal, media_content_type, media_content_id, diff --git a/homeassistant/components/plex/cast.py b/homeassistant/components/plex/cast.py index 59f23a681f8..dc8d791f117 100644 --- a/homeassistant/components/plex/cast.py +++ b/homeassistant/components/plex/cast.py @@ -10,8 +10,7 @@ from homeassistant.components.media_player.const import MEDIA_CLASS_APP from homeassistant.core import HomeAssistant from . import async_browse_media as async_browse_plex_media, is_plex_media_id -from .const import PLEX_URI_SCHEME -from .services import lookup_plex_media +from .services import process_plex_payload async def async_get_media_browser_root_object( @@ -51,13 +50,10 @@ def _play_media( hass: HomeAssistant, chromecast: Chromecast, media_type: str, media_id: str ) -> None: """Play media.""" - media_id = media_id[len(PLEX_URI_SCHEME) :] - media = lookup_plex_media(hass, media_type, media_id) - if media is None: - return + result = process_plex_payload(hass, media_type, media_id) controller = PlexController() chromecast.register_handler(controller) - controller.play_media(media) + controller.play_media(result.media, offset=result.offset) async def async_play_media( @@ -68,7 +64,7 @@ async def async_play_media( media_id: str, ) -> bool: """Play media.""" - if media_id and media_id.startswith(PLEX_URI_SCHEME): + if is_plex_media_id(media_id): await hass.async_add_executor_job( _play_media, hass, chromecast, media_type, media_id ) diff --git a/homeassistant/components/plex/config_flow.py b/homeassistant/components/plex/config_flow.py index dffbd7a1930..7baee63477a 100644 --- a/homeassistant/components/plex/config_flow.py +++ b/homeassistant/components/plex/config_flow.py @@ -98,9 +98,7 @@ class PlexFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self.client_id = None self._manual = False - async def async_step_user( - self, user_input=None, errors=None - ): # pylint: disable=arguments-differ + async def async_step_user(self, user_input=None, errors=None): """Handle a flow initialized by the user.""" if user_input is not None: return await self.async_step_plex_website_auth() diff --git a/homeassistant/components/plex/errors.py b/homeassistant/components/plex/errors.py index 534c553d45e..ddbc1a2ea40 100644 --- a/homeassistant/components/plex/errors.py +++ b/homeassistant/components/plex/errors.py @@ -16,3 +16,7 @@ class ServerNotSpecified(PlexException): class ShouldUpdateConfigEntry(PlexException): """Config entry data is out of date and should be updated.""" + + +class MediaNotFound(PlexException): + """Requested media was not found.""" diff --git a/homeassistant/components/plex/manifest.json b/homeassistant/components/plex/manifest.json index 85a060ae7cd..084356abf7b 100644 --- a/homeassistant/components/plex/manifest.json +++ b/homeassistant/components/plex/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/plex", "requirements": [ - "plexapi==4.10.0", + "plexapi==4.10.1", "plexauth==0.0.6", "plexwebsocket==0.0.13" ], diff --git a/homeassistant/components/plex/media_browser.py b/homeassistant/components/plex/media_browser.py index 11599086179..1df624e265b 100644 --- a/homeassistant/components/plex/media_browser.py +++ b/homeassistant/components/plex/media_browser.py @@ -1,7 +1,7 @@ """Support to interface with the Plex API.""" from __future__ import annotations -import logging +from yarl import URL from homeassistant.components.media_player import BrowseMedia from homeassistant.components.media_player.const import ( @@ -18,7 +18,8 @@ from homeassistant.components.media_player.const import ( ) from homeassistant.components.media_player.errors import BrowseError -from .const import DOMAIN, PLEX_URI_SCHEME +from .const import DOMAIN, SERVERS +from .errors import MediaNotFound from .helpers import pretty_title @@ -26,16 +27,7 @@ class UnknownMediaType(BrowseError): """Unknown media type.""" -HUB_PREFIX = "hub:" EXPANDABLES = ["album", "artist", "playlist", "season", "show"] -PLAYLISTS_BROWSE_PAYLOAD = { - "title": "Playlists", - "media_class": MEDIA_CLASS_DIRECTORY, - "media_content_id": PLEX_URI_SCHEME + "all", - "media_content_type": "playlists", - "can_play": False, - "can_expand": True, -} ITEM_TYPE_MEDIA_CLASS = { "album": MEDIA_CLASS_ALBUM, "artist": MEDIA_CLASS_ARTIST, @@ -51,25 +43,41 @@ ITEM_TYPE_MEDIA_CLASS = { "video": MEDIA_CLASS_VIDEO, } -_LOGGER = logging.getLogger(__name__) - def browse_media( # noqa: C901 - plex_server, is_internal, media_content_type, media_content_id, *, platform=None + hass, is_internal, media_content_type, media_content_id, *, platform=None ): """Implement the websocket media browsing helper.""" + server_id = None + plex_server = None + special_folder = None - def item_payload(item, short_name=False): + if media_content_id: + url = URL(media_content_id) + server_id = url.host + plex_server = hass.data[DOMAIN][SERVERS][server_id] + if media_content_type == "hub": + _, hub_location, hub_identifier = url.parts + elif media_content_type in ["library", "server"] and len(url.parts) > 2: + _, media_content_id, special_folder = url.parts + else: + media_content_id = url.name + + if media_content_type in ("plex_root", None): + return root_payload(hass, is_internal, platform=platform) + + def item_payload(item, short_name=False, extra_params=None): """Create response payload for a single media item.""" try: media_class = ITEM_TYPE_MEDIA_CLASS[item.type] except KeyError as err: - _LOGGER.debug("Unknown type received: %s", item.type) - raise UnknownMediaType from err + raise UnknownMediaType("Unknown type received: {item.type}") from err payload = { "title": pretty_title(item, short_name), "media_class": media_class, - "media_content_id": PLEX_URI_SCHEME + str(item.ratingKey), + "media_content_id": generate_plex_uri( + server_id, item.ratingKey, params=extra_params + ), "media_content_type": item.type, "can_play": True, "can_expand": item.type in EXPANDABLES, @@ -80,16 +88,41 @@ def browse_media( # noqa: C901 thumbnail = item.thumbUrl else: thumbnail = get_proxy_image_url( - plex_server.machine_identifier, + server_id, item.ratingKey, ) payload["thumbnail"] = thumbnail return BrowseMedia(**payload) - def library_payload(library_id): + def server_payload(): + """Create response payload to describe libraries of the Plex server.""" + server_info = BrowseMedia( + title=plex_server.friendly_name, + media_class=MEDIA_CLASS_DIRECTORY, + media_content_id=generate_plex_uri(server_id, "server"), + media_content_type="server", + can_play=False, + can_expand=True, + children=[], + children_media_class=MEDIA_CLASS_DIRECTORY, + thumbnail="https://brands.home-assistant.io/_/plex/logo.png", + ) + if platform != "sonos": + server_info.children.append( + special_library_payload(server_info, "Recommended") + ) + for library in plex_server.library.sections(): + if library.type == "photo": + continue + if library.type != "artist" and platform == "sonos": + continue + server_info.children.append(library_section_payload(library)) + server_info.children.append(playlists_payload()) + return server_info + + def library_contents(library): """Create response payload to describe contents of a specific library.""" - library = plex_server.library.sectionByID(library_id) library_info = library_section_payload(library) library_info.children = [special_library_payload(library_info, "Recommended")] for item in library.all(): @@ -99,9 +132,17 @@ def browse_media( # noqa: C901 continue return library_info - def playlists_payload(platform): + def playlists_payload(): """Create response payload for all available playlists.""" - playlists_info = {**PLAYLISTS_BROWSE_PAYLOAD, "children": []} + playlists_info = { + "title": "Playlists", + "media_class": MEDIA_CLASS_DIRECTORY, + "media_content_id": generate_plex_uri(server_id, "all"), + "media_content_type": "playlists", + "can_play": False, + "can_expand": True, + "children": [], + } for playlist in plex_server.playlists(): if playlist.playlistType != "audio" and platform == "sonos": continue @@ -115,9 +156,9 @@ def browse_media( # noqa: C901 def build_item_response(payload): """Create response payload for the provided media query.""" - media = plex_server.lookup_media(**payload) - - if media is None: + try: + media = plex_server.lookup_media(**payload) + except MediaNotFound: return None try: @@ -136,35 +177,29 @@ def browse_media( # noqa: C901 continue return media_info - if media_content_id: - assert media_content_id.startswith(PLEX_URI_SCHEME) - media_content_id = media_content_id[len(PLEX_URI_SCHEME) :] - - if media_content_id and media_content_id.startswith(HUB_PREFIX): - media_content_id = media_content_id[len(HUB_PREFIX) :] - location, hub_identifier = media_content_id.split(":") - if location == "server": + if media_content_type == "hub": + if hub_location == "server": hub = next( x for x in plex_server.library.hubs() if x.hubIdentifier == hub_identifier ) - media_content_id = f"{HUB_PREFIX}server:{hub.hubIdentifier}" + media_content_id = f"server/{hub.hubIdentifier}" else: - library_section = plex_server.library.sectionByID(int(location)) + library_section = plex_server.library.sectionByID(int(hub_location)) hub = next( x for x in library_section.hubs() if x.hubIdentifier == hub_identifier ) - media_content_id = f"{HUB_PREFIX}{hub.librarySectionID}:{hub.hubIdentifier}" + media_content_id = f"{hub.librarySectionID}/{hub.hubIdentifier}" try: children_media_class = ITEM_TYPE_MEDIA_CLASS[hub.type] except KeyError as err: - raise BrowseError(f"Unknown type received: {hub.type}") from err + raise UnknownMediaType(f"Unknown type received: {hub.type}") from err payload = { "title": hub.title, "media_class": MEDIA_CLASS_DIRECTORY, - "media_content_id": PLEX_URI_SCHEME + media_content_id, - "media_content_type": hub.type, + "media_content_id": generate_plex_uri(server_id, media_content_id), + "media_content_type": "hub", "can_play": False, "can_expand": True, "children": [], @@ -176,14 +211,15 @@ def browse_media( # noqa: C901 continue payload["children"].append(station_payload(item)) else: - payload["children"].append(item_payload(item)) + extra_params = None + hub_context = hub.context.split(".")[-1] + if hub_context in ("continue", "inprogress", "ondeck"): + extra_params = {"resume": 1} + payload["children"].append( + item_payload(item, extra_params=extra_params) + ) return BrowseMedia(**payload) - if media_content_id and ":" in media_content_id: - media_content_id, special_folder = media_content_id.split(":") - else: - special_folder = None - if special_folder: if media_content_type == "server": library_or_section = plex_server.library @@ -195,7 +231,7 @@ def browse_media( # noqa: C901 try: children_media_class = ITEM_TYPE_MEDIA_CLASS[library_or_section.TYPE] except KeyError as err: - raise BrowseError( + raise UnknownMediaType( f"Unknown type received: {library_or_section.TYPE}" ) from err else: @@ -206,8 +242,9 @@ def browse_media( # noqa: C901 payload = { "title": title, "media_class": MEDIA_CLASS_DIRECTORY, - "media_content_id": PLEX_URI_SCHEME - + f"{media_content_id}:{special_folder}", + "media_content_id": generate_plex_uri( + server_id, f"{media_content_id}/{special_folder}" + ), "media_content_type": media_content_type, "can_play": False, "can_expand": True, @@ -224,11 +261,13 @@ def browse_media( # noqa: C901 return BrowseMedia(**payload) try: - if media_content_type in ("server", None): - return server_payload(plex_server, platform) + if media_content_type == "server": + return server_payload() if media_content_type == "library": - return library_payload(int(media_content_id)) + library_id = int(media_content_id) + library = plex_server.library.sectionByID(library_id) + return library_contents(library) except UnknownMediaType as err: raise BrowseError( @@ -236,7 +275,7 @@ def browse_media( # noqa: C901 ) from err if media_content_type == "playlists": - return playlists_payload(platform) + return playlists_payload() payload = { "media_type": DOMAIN, @@ -248,17 +287,62 @@ def browse_media( # noqa: C901 return response +def generate_plex_uri(server_id, media_id, params=None): + """Create a media_content_id URL for playable Plex media.""" + if isinstance(media_id, int): + media_id = str(media_id) + if isinstance(media_id, str) and not media_id.startswith("/"): + media_id = f"/{media_id}" + return str( + URL.build( + scheme=DOMAIN, + host=server_id, + path=media_id, + query=params, + ) + ) + + +def root_payload(hass, is_internal, platform=None): + """Return root payload for Plex.""" + children = [] + + for server_id in hass.data[DOMAIN][SERVERS]: + children.append( + browse_media( + hass, + is_internal, + "server", + generate_plex_uri(server_id, ""), + platform=platform, + ) + ) + + if len(children) == 1: + return children[0] + + return BrowseMedia( + title="Plex", + media_class=MEDIA_CLASS_DIRECTORY, + media_content_id="", + media_content_type="plex_root", + can_play=False, + can_expand=True, + children=children, + ) + + def library_section_payload(section): """Create response payload for a single library section.""" try: children_media_class = ITEM_TYPE_MEDIA_CLASS[section.TYPE] except KeyError as err: - _LOGGER.debug("Unknown type received: %s", section.TYPE) - raise UnknownMediaType from err + raise UnknownMediaType(f"Unknown type received: {section.TYPE}") from err + server_id = section._server.machineIdentifier # pylint: disable=protected-access return BrowseMedia( title=section.title, media_class=MEDIA_CLASS_DIRECTORY, - media_content_id=PLEX_URI_SCHEME + str(section.key), + media_content_id=generate_plex_uri(server_id, section.key), media_content_type="library", can_play=False, can_expand=True, @@ -269,10 +353,11 @@ def library_section_payload(section): def special_library_payload(parent_payload, special_type): """Create response payload for special library folders.""" title = f"{special_type} ({parent_payload.title})" + special_library_id = f"{parent_payload.media_content_id}/{special_type}" return BrowseMedia( title=title, media_class=parent_payload.media_class, - media_content_id=f"{parent_payload.media_content_id}:{special_type}", + media_content_id=special_library_id, media_content_type=parent_payload.media_content_type, can_play=False, can_expand=True, @@ -280,41 +365,18 @@ def special_library_payload(parent_payload, special_type): ) -def server_payload(plex_server, platform): - """Create response payload to describe libraries of the Plex server.""" - server_info = BrowseMedia( - title=plex_server.friendly_name, - media_class=MEDIA_CLASS_DIRECTORY, - media_content_id=PLEX_URI_SCHEME + plex_server.machine_identifier, - media_content_type="server", - can_play=False, - can_expand=True, - children=[], - children_media_class=MEDIA_CLASS_DIRECTORY, - ) - if platform != "sonos": - server_info.children.append(special_library_payload(server_info, "Recommended")) - for library in plex_server.library.sections(): - if library.type == "photo": - continue - if library.type != "artist" and platform == "sonos": - continue - server_info.children.append(library_section_payload(library)) - server_info.children.append(BrowseMedia(**PLAYLISTS_BROWSE_PAYLOAD)) - return server_info - - def hub_payload(hub): """Create response payload for a hub.""" if hasattr(hub, "librarySectionID"): - media_content_id = f"{HUB_PREFIX}{hub.librarySectionID}:{hub.hubIdentifier}" + media_content_id = f"{hub.librarySectionID}/{hub.hubIdentifier}" else: - media_content_id = f"{HUB_PREFIX}server:{hub.hubIdentifier}" + media_content_id = f"server/{hub.hubIdentifier}" + server_id = hub._server.machineIdentifier # pylint: disable=protected-access payload = { "title": hub.title, "media_class": MEDIA_CLASS_DIRECTORY, - "media_content_id": PLEX_URI_SCHEME + media_content_id, - "media_content_type": hub.type, + "media_content_id": generate_plex_uri(server_id, media_content_id), + "media_content_type": "hub", "can_play": False, "can_expand": True, } @@ -323,10 +385,11 @@ def hub_payload(hub): def station_payload(station): """Create response payload for a music station.""" + server_id = station._server.machineIdentifier # pylint: disable=protected-access return BrowseMedia( title=station.title, media_class=ITEM_TYPE_MEDIA_CLASS[station.type], - media_content_id=PLEX_URI_SCHEME + station.key, + media_content_id=generate_plex_uri(server_id, station.key), media_content_type="station", can_play=True, can_expand=False, diff --git a/homeassistant/components/plex/media_player.py b/homeassistant/components/plex/media_player.py index 9b8b0df14c7..6e729618022 100644 --- a/homeassistant/components/plex/media_player.py +++ b/homeassistant/components/plex/media_player.py @@ -2,7 +2,6 @@ from __future__ import annotations from functools import wraps -import json import logging import plexapi.exceptions @@ -46,11 +45,11 @@ from .const import ( PLEX_UPDATE_MEDIA_PLAYER_SESSION_SIGNAL, PLEX_UPDATE_MEDIA_PLAYER_SIGNAL, PLEX_UPDATE_SENSOR_SIGNAL, - PLEX_URI_SCHEME, SERVERS, TRANSIENT_DEVICE_MODELS, ) from .media_browser import browse_media +from .services import process_plex_payload _LOGGER = logging.getLogger(__name__) @@ -482,54 +481,13 @@ class PlexMediaPlayer(MediaPlayerEntity): f"Client is not currently accepting playback controls: {self.name}" ) - if not self.plex_server.has_token: - _LOGGER.warning( - "Plex integration configured without a token, playback may fail" - ) - - if media_id.startswith(PLEX_URI_SCHEME): - media_id = media_id[len(PLEX_URI_SCHEME) :] - - if media_type == "station": - playqueue = self.plex_server.create_station_playqueue(media_id) - try: - self.device.playMedia(playqueue) - except requests.exceptions.ConnectTimeout as exc: - raise HomeAssistantError( - f"Request failed when playing on {self.name}" - ) from exc - return - - src = json.loads(media_id) - if isinstance(src, int): - src = {"plex_key": src} - - offset = 0 - - if playqueue_id := src.pop("playqueue_id", None): - try: - playqueue = self.plex_server.get_playqueue(playqueue_id) - except plexapi.exceptions.NotFound as err: - raise HomeAssistantError( - f"PlayQueue '{playqueue_id}' could not be found" - ) from err - else: - shuffle = src.pop("shuffle", 0) - offset = src.pop("offset", 0) * 1000 - resume = src.pop("resume", False) - media = self.plex_server.lookup_media(media_type, **src) - - if media is None: - raise HomeAssistantError(f"Media could not be found: {media_id}") - - if resume and not offset: - offset = media.viewOffset - - _LOGGER.debug("Attempting to play %s on %s", media, self.name) - playqueue = self.plex_server.create_playqueue(media, shuffle=shuffle) + result = process_plex_payload( + self.hass, media_type, media_id, default_plex_server=self.plex_server + ) + _LOGGER.debug("Attempting to play %s on %s", result.media, self.name) try: - self.device.playMedia(playqueue, offset=offset) + self.device.playMedia(result.media, offset=result.offset) except requests.exceptions.ConnectTimeout as exc: raise HomeAssistantError( f"Request failed when playing on {self.name}" @@ -580,7 +538,7 @@ class PlexMediaPlayer(MediaPlayerEntity): is_internal = is_internal_request(self.hass) return await self.hass.async_add_executor_job( browse_media, - self.plex_server, + self.hass, is_internal, media_content_type, media_content_id, diff --git a/homeassistant/components/plex/media_search.py b/homeassistant/components/plex/media_search.py index abe32f7cf4c..351ee1444c4 100644 --- a/homeassistant/components/plex/media_search.py +++ b/homeassistant/components/plex/media_search.py @@ -1,7 +1,13 @@ """Helper methods to search for Plex media.""" +from __future__ import annotations + import logging +from plexapi.base import PlexObject from plexapi.exceptions import BadRequest, NotFound +from plexapi.library import LibrarySection + +from .errors import MediaNotFound LEGACY_PARAM_MAPPING = { "show_name": "show.title", @@ -28,13 +34,19 @@ PREFERRED_LIBTYPE_ORDER = ( _LOGGER = logging.getLogger(__name__) -def search_media(media_type, library_section, allow_multiple=False, **kwargs): +def search_media( + media_type: str, + library_section: LibrarySection, + allow_multiple: bool = False, + **kwargs, +) -> PlexObject | list[PlexObject]: """Search for specified Plex media in the provided library section. - Returns a single media item or None. + Returns a media item or a list of items if `allow_multiple` is set. - If `allow_multiple` is `True`, return a list of matching items. + Raises MediaNotFound if the search was unsuccessful. """ + original_query = kwargs.copy() search_query = {} libtype = kwargs.pop("libtype", None) @@ -61,11 +73,12 @@ def search_media(media_type, library_section, allow_multiple=False, **kwargs): try: results = library_section.search(**search_query) except (BadRequest, NotFound) as exc: - _LOGGER.error("Problem in query %s: %s", search_query, exc) - return None + raise MediaNotFound(f"Problem in query {original_query}: {exc}") from exc if not results: - return None + raise MediaNotFound( + f"No {media_type} results in '{library_section.title}' for {original_query}" + ) if len(results) > 1: if allow_multiple: @@ -75,10 +88,8 @@ def search_media(media_type, library_section, allow_multiple=False, **kwargs): exact_matches = [x for x in results if x.title.lower() == title.lower()] if len(exact_matches) == 1: return exact_matches[0] - _LOGGER.warning( - "Multiple matches, make content_id more specific or use `allow_multiple`: %s", - results, + raise MediaNotFound( + f"Multiple matches, make content_id more specific or use `allow_multiple`: {results}" ) - return None return results[0] diff --git a/homeassistant/components/plex/models.py b/homeassistant/components/plex/models.py index 406c263dbff..0b77f450e7e 100644 --- a/homeassistant/components/plex/models.py +++ b/homeassistant/components/plex/models.py @@ -1,4 +1,5 @@ """Models to represent various Plex objects used in the integration.""" +from distutils.util import strtobool import logging from homeassistant.components.media_player.const import ( @@ -141,3 +142,35 @@ class PlexSession: thumb_url = media.url(media.art) return thumb_url + + +class PlexMediaSearchResult: + """Represents results from a Plex media media_content_id search. + + Results are used by media_player.play_media implementations. + """ + + def __init__(self, media, params=None) -> None: + """Initialize the result.""" + self.media = media + self._params = params or {} + + @property + def offset(self) -> int: + """Provide the appropriate offset based on payload contents.""" + if offset := self._params.get("offset", 0): + return offset * 1000 + resume = self._params.get("resume", False) + if isinstance(resume, str): + resume = bool(strtobool(resume)) + if resume: + return self.media.viewOffset + return 0 + + @property + def shuffle(self) -> bool: + """Return value of shuffle parameter.""" + shuffle = self._params.get("shuffle", False) + if isinstance(shuffle, str): + shuffle = bool(strtobool(shuffle)) + return shuffle diff --git a/homeassistant/components/plex/server.py b/homeassistant/components/plex/server.py index b4dc5755a73..b136bec73e9 100644 --- a/homeassistant/components/plex/server.py +++ b/homeassistant/components/plex/server.py @@ -42,7 +42,12 @@ from .const import ( X_PLEX_PRODUCT, X_PLEX_VERSION, ) -from .errors import NoServersFound, ServerNotSpecified, ShouldUpdateConfigEntry +from .errors import ( + MediaNotFound, + NoServersFound, + ServerNotSpecified, + ShouldUpdateConfigEntry, +) from .media_search import search_media from .models import PlexSession @@ -619,37 +624,34 @@ class PlexServer: key = kwargs["plex_key"] try: return self.fetch_item(key) - except NotFound: - _LOGGER.error("Media for key %s not found", key) - return None + except NotFound as err: + raise MediaNotFound(f"Media for key {key} not found") from err if media_type == MEDIA_TYPE_PLAYLIST: try: playlist_name = kwargs["playlist_name"] return self.playlist(playlist_name) - except KeyError: - _LOGGER.error("Must specify 'playlist_name' for this search") - return None - except NotFound: - _LOGGER.error( - "Playlist '%s' not found", - playlist_name, - ) - return None + except KeyError as err: + raise MediaNotFound( + "Must specify 'playlist_name' for this search" + ) from err + except NotFound as err: + raise MediaNotFound(f"Playlist '{playlist_name}' not found") from err try: library_name = kwargs.pop("library_name") library_section = self.library.section(library_name) - except KeyError: - _LOGGER.error("Must specify 'library_name' for this search") - return None - except NotFound: + except KeyError as err: + raise MediaNotFound("Must specify 'library_name' for this search") from err + except NotFound as err: library_sections = [section.title for section in self.library.sections()] - _LOGGER.error( - "Library '%s' not found in %s", library_name, library_sections - ) - return None + raise MediaNotFound( + f"Library '{library_name}' not found in {library_sections}" + ) from err + _LOGGER.debug( + "Searching for %s in %s using: %s", media_type, library_section, kwargs + ) return search_media(media_type, library_section, **kwargs) @property diff --git a/homeassistant/components/plex/services.py b/homeassistant/components/plex/services.py index 0433ba836cd..8cf714b8823 100644 --- a/homeassistant/components/plex/services.py +++ b/homeassistant/components/plex/services.py @@ -4,6 +4,7 @@ import logging from plexapi.exceptions import NotFound import voluptuous as vol +from yarl import URL from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.exceptions import HomeAssistantError @@ -12,10 +13,13 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send from .const import ( DOMAIN, PLEX_UPDATE_PLATFORMS_SIGNAL, + PLEX_URI_SCHEME, SERVERS, SERVICE_REFRESH_LIBRARY, SERVICE_SCAN_CLIENTS, ) +from .errors import MediaNotFound +from .models import PlexMediaSearchResult REFRESH_LIBRARY_SCHEMA = vol.Schema( {vol.Optional("server_name"): str, vol.Required("library_name"): str} @@ -72,7 +76,7 @@ def refresh_library(hass: HomeAssistant, service_call: ServiceCall) -> None: library.update() -def get_plex_server(hass, plex_server_name=None): +def get_plex_server(hass, plex_server_name=None, plex_server_id=None): """Retrieve a configured Plex server by name.""" if DOMAIN not in hass.data: raise HomeAssistantError("Plex integration not configured") @@ -80,6 +84,9 @@ def get_plex_server(hass, plex_server_name=None): if not plex_servers: raise HomeAssistantError("No Plex servers available") + if plex_server_id: + return hass.data[DOMAIN][SERVERS][plex_server_id] + if plex_server_name: plex_server = next( (x for x in plex_servers if x.friendly_name == plex_server_name), None @@ -100,32 +107,73 @@ def get_plex_server(hass, plex_server_name=None): ) -def lookup_plex_media(hass, content_type, content_id): - """Look up Plex media for other integrations using media_player.play_media service payloads.""" - content = json.loads(content_id) +def process_plex_payload( + hass, content_type, content_id, default_plex_server=None, supports_playqueues=True +) -> PlexMediaSearchResult: + """Look up Plex media using media_player.play_media service payloads.""" + plex_server = default_plex_server + extra_params = {} + + if content_id.startswith(PLEX_URI_SCHEME + "{"): + # Handle the special payload of 'plex://{}' + content_id = content_id[len(PLEX_URI_SCHEME) :] + content = json.loads(content_id) + elif content_id.startswith(PLEX_URI_SCHEME): + # Handle standard media_browser payloads + plex_url = URL(content_id) + if plex_url.name: + if len(plex_url.parts) == 2: + # The path contains a single item, will always be a ratingKey + content = int(plex_url.name) + else: + # For "special" items like radio stations + content = plex_url.path + server_id = plex_url.host + plex_server = get_plex_server(hass, plex_server_id=server_id) + else: + # Handle legacy payloads without server_id in URL host position + content = int(plex_url.host) # type: ignore[arg-type] + extra_params = dict(plex_url.query) + else: + content = json.loads(content_id) + + if isinstance(content, dict): + if plex_server_name := content.pop("plex_server", None): + plex_server = get_plex_server(hass, plex_server_name) + + if not plex_server: + plex_server = get_plex_server(hass) + + if content_type == "station": + if not supports_playqueues: + raise HomeAssistantError("Plex stations are not supported on this device") + playqueue = plex_server.create_station_playqueue(content) + return PlexMediaSearchResult(playqueue) if isinstance(content, int): content = {"plex_key": content} content_type = DOMAIN - plex_server_name = content.pop("plex_server", None) - plex_server = get_plex_server(hass, plex_server_name) + content.update(extra_params) if playqueue_id := content.pop("playqueue_id", None): + if not supports_playqueues: + raise HomeAssistantError("Plex playqueues are not supported on this device") try: playqueue = plex_server.get_playqueue(playqueue_id) except NotFound as err: - raise HomeAssistantError( + raise MediaNotFound( f"PlayQueue '{playqueue_id}' could not be found" ) from err - return playqueue + return PlexMediaSearchResult(playqueue, content) - shuffle = content.pop("shuffle", 0) - media = plex_server.lookup_media(content_type, **content) - if media is None: - raise HomeAssistantError(f"Plex media not found using payload: '{content_id}'") + search_query = content.copy() + shuffle = search_query.pop("shuffle", 0) - if shuffle: - return plex_server.create_playqueue(media, shuffle=shuffle) + media = plex_server.lookup_media(content_type, **search_query) - return media + if supports_playqueues and (isinstance(media, list) or shuffle): + playqueue = plex_server.create_playqueue(media, shuffle=shuffle) + return PlexMediaSearchResult(playqueue, content) + + return PlexMediaSearchResult(media, content) diff --git a/homeassistant/components/plex/strings.json b/homeassistant/components/plex/strings.json index c16a84b1cd8..f08b6f59862 100644 --- a/homeassistant/components/plex/strings.json +++ b/homeassistant/components/plex/strings.json @@ -3,11 +3,9 @@ "flow_title": "{name} ({host})", "step": { "user": { - "title": "Plex Media Server", "description": "Continue to [plex.tv](https://plex.tv) to link a Plex server." }, "user_advanced": { - "title": "Plex Media Server", "data": { "setup_method": "Setup method" } diff --git a/homeassistant/components/plex/translations/el.json b/homeassistant/components/plex/translations/el.json index 3f447b48d86..679ef3d3937 100644 --- a/homeassistant/components/plex/translations/el.json +++ b/homeassistant/components/plex/translations/el.json @@ -4,7 +4,7 @@ "all_configured": "\u038c\u03bb\u03bf\u03b9 \u03bf\u03b9 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b4\u03b5\u03bc\u03ad\u03bd\u03bf\u03b9 \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ad\u03c2 \u03ad\u03c7\u03bf\u03c5\u03bd \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af", "already_configured": "\u0391\u03c5\u03c4\u03cc\u03c2 \u03bf \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae\u03c2 Plex \u03b5\u03af\u03bd\u03b1\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03bc\u03ad\u03bd\u03bf\u03c2", "already_in_progress": "\u0397 \u03c1\u03bf\u03ae \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7\u03c2 \u03b2\u03c1\u03af\u03c3\u03ba\u03b5\u03c4\u03b1\u03b9 \u03ae\u03b4\u03b7 \u03c3\u03b5 \u03b5\u03be\u03ad\u03bb\u03b9\u03be\u03b7", - "reauth_successful": "\u0395\u03c0\u03b1\u03bb\u03b7\u03b8\u03b5\u03cd\u03c4\u03b7\u03ba\u03b5 \u03be\u03b1\u03bd\u03ac \u03bc\u03b5 \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03af\u03b1", + "reauth_successful": "\u039f \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03ae\u03c4\u03b1\u03bd \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2", "token_request_timeout": "\u039b\u03ae\u03be\u03b7 \u03c7\u03c1\u03bf\u03bd\u03b9\u03ba\u03bf\u03cd \u03bf\u03c1\u03af\u03bf\u03c5 \u03bb\u03ae\u03c8\u03b7\u03c2 \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03bf\u03cd", "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" }, diff --git a/homeassistant/components/plex/translations/es.json b/homeassistant/components/plex/translations/es.json index 71365b9e2f2..c31dcf26e07 100644 --- a/homeassistant/components/plex/translations/es.json +++ b/homeassistant/components/plex/translations/es.json @@ -9,7 +9,7 @@ "unknown": "Fall\u00f3 por razones desconocidas" }, "error": { - "faulty_credentials": "La autorizaci\u00f3n fall\u00f3, verifica el token", + "faulty_credentials": "La autorizaci\u00f3n ha fallado, verifica el token", "host_or_token": "Debes proporcionar al menos uno de Host o Token", "no_servers": "No hay servidores vinculados a la cuenta Plex", "not_found": "No se ha encontrado el servidor Plex", diff --git a/homeassistant/components/plugwise/climate.py b/homeassistant/components/plugwise/climate.py index e22f12c3526..1e4b972e4d0 100644 --- a/homeassistant/components/plugwise/climate.py +++ b/homeassistant/components/plugwise/climate.py @@ -12,7 +12,6 @@ from homeassistant.components.climate.const import ( HVAC_MODE_AUTO, HVAC_MODE_COOL, HVAC_MODE_HEAT, - HVAC_MODE_OFF, SUPPORT_PRESET_MODE, SUPPORT_TARGET_TEMPERATURE, ) @@ -21,18 +20,11 @@ from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP, DOMAIN +from .const import DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP, DOMAIN, THERMOSTAT_CLASSES from .coordinator import PlugwiseDataUpdateCoordinator from .entity import PlugwiseEntity from .util import plugwise_command -THERMOSTAT_CLASSES = [ - "thermostat", - "thermostatic_radiator_valve", - "zone_thermometer", - "zone_thermostat", -] - async def async_setup_entry( hass: HomeAssistant, @@ -71,7 +63,7 @@ class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity): self._attr_preset_modes = list(presets) # Determine hvac modes and current hvac mode - self._attr_hvac_modes = [HVAC_MODE_HEAT, HVAC_MODE_OFF] + self._attr_hvac_modes = [HVAC_MODE_HEAT] if self.coordinator.data.gateway.get("cooling_present"): self._attr_hvac_modes.append(HVAC_MODE_COOL) if self.device.get("available_schedules") != ["None"]: @@ -97,7 +89,7 @@ class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity): def hvac_mode(self) -> str: """Return HVAC operation ie. heat, cool mode.""" if (mode := self.device.get("mode")) is None or mode not in self.hvac_modes: - return HVAC_MODE_OFF + return HVAC_MODE_HEAT return mode @property diff --git a/homeassistant/components/plugwise/config_flow.py b/homeassistant/components/plugwise/config_flow.py index e69d92e3cd0..5b5c79ba2b8 100644 --- a/homeassistant/components/plugwise/config_flow.py +++ b/homeassistant/components/plugwise/config_flow.py @@ -3,7 +3,11 @@ from __future__ import annotations from typing import Any -from plugwise.exceptions import InvalidAuthentication, PlugwiseException +from plugwise.exceptions import ( + InvalidAuthentication, + InvalidSetupError, + PlugwiseException, +) from plugwise.smile import Smile import voluptuous as vol @@ -124,6 +128,8 @@ class PlugwiseConfigFlow(ConfigFlow, domain=DOMAIN): try: api = await validate_gw_input(self.hass, user_input) + except InvalidSetupError: + errors[CONF_BASE] = "invalid_setup" except InvalidAuthentication: errors[CONF_BASE] = "invalid_auth" except PlugwiseException: diff --git a/homeassistant/components/plugwise/const.py b/homeassistant/components/plugwise/const.py index adcd68ed50e..3bcad0b88aa 100644 --- a/homeassistant/components/plugwise/const.py +++ b/homeassistant/components/plugwise/const.py @@ -24,6 +24,7 @@ PLATFORMS_GATEWAY = [ Platform.CLIMATE, Platform.SENSOR, Platform.SWITCH, + Platform.SELECT, ] ZEROCONF_MAP = { "smile": "P1", @@ -43,3 +44,10 @@ DEFAULT_SCAN_INTERVAL = { "thermostat": timedelta(seconds=60), } DEFAULT_USERNAME = "smile" + +THERMOSTAT_CLASSES = [ + "thermostat", + "thermostatic_radiator_valve", + "zone_thermometer", + "zone_thermostat", +] diff --git a/homeassistant/components/plugwise/entity.py b/homeassistant/components/plugwise/entity.py index b172b5468b0..b0896c3cd6d 100644 --- a/homeassistant/components/plugwise/entity.py +++ b/homeassistant/components/plugwise/entity.py @@ -12,14 +12,12 @@ from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN -from .coordinator import PlugwiseData, PlugwiseDataUpdateCoordinator +from .coordinator import PlugwiseDataUpdateCoordinator -class PlugwiseEntity(CoordinatorEntity[PlugwiseData]): +class PlugwiseEntity(CoordinatorEntity[PlugwiseDataUpdateCoordinator]): """Represent a PlugWise Entity.""" - coordinator: PlugwiseDataUpdateCoordinator - def __init__( self, coordinator: PlugwiseDataUpdateCoordinator, diff --git a/homeassistant/components/plugwise/gateway.py b/homeassistant/components/plugwise/gateway.py index 71ca2af9537..f540c6d0b9c 100644 --- a/homeassistant/components/plugwise/gateway.py +++ b/homeassistant/components/plugwise/gateway.py @@ -7,22 +7,27 @@ from typing import Any from plugwise.exceptions import InvalidAuthentication, PlugwiseException from plugwise.smile import Smile -from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers import device_registry as dr +from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.entity_registry import RegistryEntry, async_migrate_entries -from .const import DEFAULT_PORT, DEFAULT_USERNAME, DOMAIN, LOGGER, PLATFORMS_GATEWAY +from .const import ( + DEFAULT_PORT, + DEFAULT_USERNAME, + DOMAIN, + LOGGER, + PLATFORMS_GATEWAY, + Platform, +) from .coordinator import PlugwiseDataUpdateCoordinator async def async_setup_entry_gw(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Plugwise Smiles from a config entry.""" - await async_migrate_entries(hass, entry.entry_id, async_migrate_entity_entry) + await er.async_migrate_entries(hass, entry.entry_id, async_migrate_entity_entry) websession = async_get_clientsession(hass, verify_ssl=False) api = Smile( @@ -57,6 +62,7 @@ async def async_setup_entry_gw(hass: HomeAssistant, entry: ConfigEntry) -> bool: coordinator = PlugwiseDataUpdateCoordinator(hass, api) await coordinator.async_config_entry_first_refresh() + migrate_sensor_entities(hass, coordinator) hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator @@ -85,13 +91,39 @@ async def async_unload_entry_gw(hass: HomeAssistant, entry: ConfigEntry): @callback -def async_migrate_entity_entry(entry: RegistryEntry) -> dict[str, Any] | None: +def async_migrate_entity_entry(entry: er.RegistryEntry) -> dict[str, Any] | None: """Migrate Plugwise entity entries. - Migrates unique ID from old relay switches to the new unique ID """ - if entry.domain == SWITCH_DOMAIN and entry.unique_id.endswith("-plug"): + if entry.domain == Platform.SWITCH and entry.unique_id.endswith("-plug"): return {"new_unique_id": entry.unique_id.replace("-plug", "-relay")} # No migration needed return None + + +def migrate_sensor_entities( + hass: HomeAssistant, + coordinator: PlugwiseDataUpdateCoordinator, +) -> None: + """Migrate Sensors if needed.""" + ent_reg = er.async_get(hass) + + # Migrating opentherm_outdoor_temperature to opentherm_outdoor_air_temperature sensor + for device_id, device in coordinator.data.devices.items(): + if device["class"] != "heater_central": + continue + + old_unique_id = f"{device_id}-outdoor_temperature" + if entity_id := ent_reg.async_get_entity_id( + Platform.SENSOR, DOMAIN, old_unique_id + ): + new_unique_id = f"{device_id}-outdoor_air_temperature" + LOGGER.debug( + "Migrating entity %s from old unique ID '%s' to new unique ID '%s'", + entity_id, + old_unique_id, + new_unique_id, + ) + ent_reg.async_update_entity(entity_id, new_unique_id=new_unique_id) diff --git a/homeassistant/components/plugwise/manifest.json b/homeassistant/components/plugwise/manifest.json index 4f1417ae018..68ed6d65647 100644 --- a/homeassistant/components/plugwise/manifest.json +++ b/homeassistant/components/plugwise/manifest.json @@ -2,7 +2,7 @@ "domain": "plugwise", "name": "Plugwise", "documentation": "https://www.home-assistant.io/integrations/plugwise", - "requirements": ["plugwise==0.16.6"], + "requirements": ["plugwise==0.17.3"], "codeowners": ["@CoMPaTech", "@bouwew", "@brefra", "@frenck"], "zeroconf": ["_plugwise._tcp.local."], "config_flow": true, diff --git a/homeassistant/components/plugwise/select.py b/homeassistant/components/plugwise/select.py new file mode 100644 index 00000000000..b3cfb366889 --- /dev/null +++ b/homeassistant/components/plugwise/select.py @@ -0,0 +1,64 @@ +"""Plugwise Select component for Home Assistant.""" +from __future__ import annotations + +from homeassistant.components.select import SelectEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import STATE_ON +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN, THERMOSTAT_CLASSES +from .coordinator import PlugwiseDataUpdateCoordinator +from .entity import PlugwiseEntity + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Smile selector from a config entry.""" + coordinator = hass.data[DOMAIN][config_entry.entry_id] + async_add_entities( + PlugwiseSelectEntity(coordinator, device_id) + for device_id, device in coordinator.data.devices.items() + if device["class"] in THERMOSTAT_CLASSES + and len(device.get("available_schedules")) > 1 + ) + + +class PlugwiseSelectEntity(PlugwiseEntity, SelectEntity): + """Represent Smile selector.""" + + def __init__( + self, + coordinator: PlugwiseDataUpdateCoordinator, + device_id: str, + ) -> None: + """Initialise the selector.""" + super().__init__(coordinator, device_id) + self._attr_unique_id = f"{device_id}-select_schedule" + self._attr_name = (f"{self.device.get('name', '')} Select Schedule").lstrip() + + @property + def current_option(self) -> str | None: + """Return the selected entity option to represent the entity state.""" + return self.device.get("selected_schedule") + + @property + def options(self) -> list[str]: + """Return a set of selectable options.""" + return self.device.get("available_schedules", []) + + async def async_select_option(self, option: str) -> None: + """Change the selected option.""" + if not ( + await self.coordinator.api.set_schedule_state( + self.device.get("location"), + option, + STATE_ON, + ) + ): + raise HomeAssistantError(f"Failed to change to schedule {option}") + await self.coordinator.async_request_refresh() diff --git a/homeassistant/components/plugwise/sensor.py b/homeassistant/components/plugwise/sensor.py index 4ee75e21a45..87c81699d10 100644 --- a/homeassistant/components/plugwise/sensor.py +++ b/homeassistant/components/plugwise/sensor.py @@ -60,6 +60,13 @@ SENSORS: tuple[SensorEntityDescription, ...] = ( device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), + SensorEntityDescription( + key="outdoor_air_temperature", + name="Outdoor Air Temperature", + native_unit_of_measurement=TEMP_CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + ), SensorEntityDescription( key="water_temperature", name="Water Temperature", @@ -267,7 +274,7 @@ async def async_setup_entry( """Set up the Smile sensors from a config entry.""" coordinator = hass.data[DOMAIN][config_entry.entry_id] - entities: list[PlugwiseSensorEnity] = [] + entities: list[PlugwiseSensorEntity] = [] for device_id, device in coordinator.data.devices.items(): for description in SENSORS: if ( @@ -277,7 +284,7 @@ async def async_setup_entry( continue entities.append( - PlugwiseSensorEnity( + PlugwiseSensorEntity( coordinator, device_id, description, @@ -287,7 +294,7 @@ async def async_setup_entry( async_add_entities(entities) -class PlugwiseSensorEnity(PlugwiseEntity, SensorEntity): +class PlugwiseSensorEntity(PlugwiseEntity, SensorEntity): """Represent Plugwise Sensors.""" def __init__( diff --git a/homeassistant/components/plugwise/strings.json b/homeassistant/components/plugwise/strings.json index e5a7ab5a6a9..a350543ee07 100644 --- a/homeassistant/components/plugwise/strings.json +++ b/homeassistant/components/plugwise/strings.json @@ -25,13 +25,14 @@ "password": "Smile ID", "host": "[%key:common::config_flow::data::ip%]", "port": "[%key:common::config_flow::data::port%]", - "username" : "Smile Username" + "username": "Smile Username" } } }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "invalid_setup": "Add your Adam instead of your Anna, see the Home Assistant Plugwise integration documentation for more information", "unknown": "[%key:common::config_flow::error::unknown%]" }, "abort": { diff --git a/homeassistant/components/plugwise/translations/fr.json b/homeassistant/components/plugwise/translations/fr.json index 5370a18ba56..c6098db43f0 100644 --- a/homeassistant/components/plugwise/translations/fr.json +++ b/homeassistant/components/plugwise/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "flow_title": "{name}", diff --git a/homeassistant/components/plugwise/translations/zh-Hant.json b/homeassistant/components/plugwise/translations/zh-Hant.json index edd67dd41cb..0a1e21a12ed 100644 --- a/homeassistant/components/plugwise/translations/zh-Hant.json +++ b/homeassistant/components/plugwise/translations/zh-Hant.json @@ -12,10 +12,10 @@ "step": { "user": { "data": { - "flow_type": "\u9023\u7dda\u985e\u578b" + "flow_type": "\u9023\u7dda\u985e\u5225" }, "description": "\u7522\u54c1\uff1a", - "title": "Plugwise \u985e\u578b" + "title": "Plugwise \u985e\u5225" }, "user_gateway": { "data": { diff --git a/homeassistant/components/plum_lightpad/translations/fr.json b/homeassistant/components/plum_lightpad/translations/fr.json index 20c633e8d0f..53819587be4 100644 --- a/homeassistant/components/plum_lightpad/translations/fr.json +++ b/homeassistant/components/plum_lightpad/translations/fr.json @@ -10,7 +10,7 @@ "user": { "data": { "password": "Mot de passe", - "username": "Email" + "username": "Courriel" } } } diff --git a/homeassistant/components/point/translations/fr.json b/homeassistant/components/point/translations/fr.json index 0d05e0a5363..440bd076bef 100644 --- a/homeassistant/components/point/translations/fr.json +++ b/homeassistant/components/point/translations/fr.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_setup": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible.", - "authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration de l'URL d'authentification d\u00e9pass\u00e9.", + "authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration de l'URL d'authentification expir\u00e9.", "external_setup": "Point correctement configur\u00e9 \u00e0 partir d\u2019un autre flux.", "no_flows": "Le composant n'est pas configur\u00e9. Veuillez suivre la documentation.", "unknown_authorize_url_generation": "Erreur inconnue lors de la g\u00e9n\u00e9ration d'une URL d'autorisation." @@ -23,7 +23,7 @@ "data": { "flow_impl": "Fournisseur" }, - "description": "Voulez-vous commencer la configuration ?", + "description": "Voulez-vous commencer la configuration\u00a0?", "title": "S\u00e9lectionner une m\u00e9thode d'authentification" } } diff --git a/homeassistant/components/poolsense/strings.json b/homeassistant/components/poolsense/strings.json index 71b30e0bf77..2ddf3ee77e8 100644 --- a/homeassistant/components/poolsense/strings.json +++ b/homeassistant/components/poolsense/strings.json @@ -2,8 +2,6 @@ "config": { "step": { "user": { - "title": "PoolSense", - "description": "[%key:common::config_flow::description::confirm_setup%]", "data": { "email": "[%key:common::config_flow::data::email%]", "password": "[%key:common::config_flow::data::password%]" diff --git a/homeassistant/components/poolsense/translations/fr.json b/homeassistant/components/poolsense/translations/fr.json index bfe2ecf1bd2..4a32776bccb 100644 --- a/homeassistant/components/poolsense/translations/fr.json +++ b/homeassistant/components/poolsense/translations/fr.json @@ -4,15 +4,15 @@ "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" }, "error": { - "invalid_auth": "Authentification invalide" + "invalid_auth": "Authentification non valide" }, "step": { "user": { "data": { - "email": "Email", + "email": "Courriel", "password": "Mot de passe" }, - "description": "Voulez-vous commencer la configuration ?", + "description": "Voulez-vous commencer la configuration\u00a0?", "title": "PoolSense" } } diff --git a/homeassistant/components/powerwall/__init__.py b/homeassistant/components/powerwall/__init__.py index 10504e2aa06..d2850330b7a 100644 --- a/homeassistant/components/powerwall/__init__.py +++ b/homeassistant/components/powerwall/__init__.py @@ -229,6 +229,7 @@ def _fetch_powerwall_data(power_wall: Powerwall) -> PowerwallData: meters=power_wall.get_meters(), grid_services_active=power_wall.is_grid_services_active(), grid_status=power_wall.get_grid_status(), + backup_reserve=power_wall.get_backup_reserve_percentage(), ) diff --git a/homeassistant/components/powerwall/config_flow.py b/homeassistant/components/powerwall/config_flow.py index 08e9f90df1b..836aa46e2a4 100644 --- a/homeassistant/components/powerwall/config_flow.py +++ b/homeassistant/components/powerwall/config_flow.py @@ -145,7 +145,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): } return self.async_show_form( step_id="confirm_discovery", - data_schema=vol.Schema({}), description_placeholders={ "name": self.title, "ip_address": self.ip_address, diff --git a/homeassistant/components/powerwall/entity.py b/homeassistant/components/powerwall/entity.py index 20871944663..5d55b8b8bf1 100644 --- a/homeassistant/components/powerwall/entity.py +++ b/homeassistant/components/powerwall/entity.py @@ -1,7 +1,10 @@ """The Tesla Powerwall integration base entity.""" from homeassistant.helpers.entity import DeviceInfo -from homeassistant.helpers.update_coordinator import CoordinatorEntity +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) from .const import ( DOMAIN, @@ -13,7 +16,7 @@ from .const import ( from .models import PowerwallData, PowerwallRuntimeData -class PowerWallEntity(CoordinatorEntity[PowerwallData]): +class PowerWallEntity(CoordinatorEntity[DataUpdateCoordinator[PowerwallData]]): """Base class for powerwall entities.""" def __init__(self, powerwall_data: PowerwallRuntimeData) -> None: diff --git a/homeassistant/components/powerwall/models.py b/homeassistant/components/powerwall/models.py index 472d9e59304..cb9b84be16a 100644 --- a/homeassistant/components/powerwall/models.py +++ b/homeassistant/components/powerwall/models.py @@ -38,6 +38,7 @@ class PowerwallData: meters: MetersAggregates grid_services_active: bool grid_status: GridStatus + backup_reserve: float class PowerwallRuntimeData(TypedDict): diff --git a/homeassistant/components/powerwall/sensor.py b/homeassistant/components/powerwall/sensor.py index 93b3c64d18c..9e66c61a2bb 100644 --- a/homeassistant/components/powerwall/sensor.py +++ b/homeassistant/components/powerwall/sensor.py @@ -1,28 +1,30 @@ """Support for powerwall sensors.""" from __future__ import annotations -from typing import Any +from collections.abc import Callable +from dataclasses import dataclass from tesla_powerwall import Meter, MeterType from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, + SensorEntityDescription, SensorStateClass, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ENERGY_KILO_WATT_HOUR, PERCENTAGE, POWER_KILO_WATT +from homeassistant.const import ( + ELECTRIC_CURRENT_AMPERE, + ELECTRIC_POTENTIAL_VOLT, + ENERGY_KILO_WATT_HOUR, + FREQUENCY_HERTZ, + PERCENTAGE, + POWER_KILO_WATT, +) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import ( - ATTR_FREQUENCY, - ATTR_INSTANT_AVERAGE_VOLTAGE, - ATTR_INSTANT_TOTAL_CURRENT, - ATTR_IS_ACTIVE, - DOMAIN, - POWERWALL_COORDINATOR, -) +from .const import DOMAIN, POWERWALL_COORDINATOR from .entity import PowerWallEntity from .models import PowerwallData, PowerwallRuntimeData @@ -30,6 +32,79 @@ _METER_DIRECTION_EXPORT = "export" _METER_DIRECTION_IMPORT = "import" +@dataclass +class PowerwallRequiredKeysMixin: + """Mixin for required keys.""" + + value_fn: Callable[[Meter], float] + + +@dataclass +class PowerwallSensorEntityDescription( + SensorEntityDescription, PowerwallRequiredKeysMixin +): + """Describes Powerwall entity.""" + + +def _get_meter_power(meter: Meter) -> float: + """Get the current value in kW.""" + return meter.get_power(precision=3) + + +def _get_meter_frequency(meter: Meter) -> float: + """Get the current value in Hz.""" + return round(meter.frequency, 1) + + +def _get_meter_total_current(meter: Meter) -> float: + """Get the current value in A.""" + return meter.get_instant_total_current() + + +def _get_meter_average_voltage(meter: Meter) -> float: + """Get the current value in V.""" + return round(meter.average_voltage, 1) + + +POWERWALL_INSTANT_SENSORS = ( + PowerwallSensorEntityDescription( + key="instant_power", + name="Now", + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.POWER, + native_unit_of_measurement=POWER_KILO_WATT, + value_fn=_get_meter_power, + ), + PowerwallSensorEntityDescription( + key="instant_frequency", + name="Frequency Now", + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.FREQUENCY, + native_unit_of_measurement=FREQUENCY_HERTZ, + entity_registry_enabled_default=False, + value_fn=_get_meter_frequency, + ), + PowerwallSensorEntityDescription( + key="instant_current", + name="Average Current Now", + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.CURRENT, + native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + entity_registry_enabled_default=False, + value_fn=_get_meter_total_current, + ), + PowerwallSensorEntityDescription( + key="instant_voltage", + name="Average Voltage Now", + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.VOLTAGE, + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + entity_registry_enabled_default=False, + value_fn=_get_meter_average_voltage, + ), +) + + async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, @@ -40,20 +115,17 @@ async def async_setup_entry( coordinator = powerwall_data[POWERWALL_COORDINATOR] assert coordinator is not None data: PowerwallData = coordinator.data - entities: list[ - PowerWallEnergySensor - | PowerWallImportSensor - | PowerWallExportSensor - | PowerWallChargeSensor - ] = [PowerWallChargeSensor(powerwall_data)] + entities: list[PowerWallEntity] = [ + PowerWallChargeSensor(powerwall_data), + PowerWallBackupReserveSensor(powerwall_data), + ] for meter in data.meters.meters: + entities.append(PowerWallExportSensor(powerwall_data, meter)) + entities.append(PowerWallImportSensor(powerwall_data, meter)) entities.extend( - [ - PowerWallEnergySensor(powerwall_data, meter), - PowerWallExportSensor(powerwall_data, meter), - PowerWallImportSensor(powerwall_data, meter), - ] + PowerWallEnergySensor(powerwall_data, meter, description) + for description in POWERWALL_INSTANT_SENSORS ) async_add_entities(entities) @@ -81,34 +153,46 @@ class PowerWallChargeSensor(PowerWallEntity, SensorEntity): class PowerWallEnergySensor(PowerWallEntity, SensorEntity): """Representation of an Powerwall Energy sensor.""" - _attr_state_class = SensorStateClass.MEASUREMENT - _attr_native_unit_of_measurement = POWER_KILO_WATT - _attr_device_class = SensorDeviceClass.POWER + entity_description: PowerwallSensorEntityDescription - def __init__(self, powerwall_data: PowerwallRuntimeData, meter: MeterType) -> None: + def __init__( + self, + powerwall_data: PowerwallRuntimeData, + meter: MeterType, + description: PowerwallSensorEntityDescription, + ) -> None: """Initialize the sensor.""" + self.entity_description = description super().__init__(powerwall_data) self._meter = meter - self._attr_name = f"Powerwall {self._meter.value.title()} Now" + self._attr_name = f"Powerwall {self._meter.value.title()} {description.name}" self._attr_unique_id = ( - f"{self.base_unique_id}_{self._meter.value}_instant_power" + f"{self.base_unique_id}_{self._meter.value}_{description.key}" ) @property def native_value(self) -> float: - """Get the current value in kW.""" - return self.data.meters.get_meter(self._meter).get_power(precision=3) + """Get the current value.""" + return self.entity_description.value_fn(self.data.meters.get_meter(self._meter)) + + +class PowerWallBackupReserveSensor(PowerWallEntity, SensorEntity): + """Representation of the Powerwall backup reserve setting.""" + + _attr_name = "Powerwall Backup Reserve" + _attr_state_class = SensorStateClass.MEASUREMENT + _attr_native_unit_of_measurement = PERCENTAGE + _attr_device_class = SensorDeviceClass.BATTERY @property - def extra_state_attributes(self) -> dict[str, Any]: - """Return the device specific state attributes.""" - meter = self.data.meters.get_meter(self._meter) - return { - ATTR_FREQUENCY: round(meter.frequency, 1), - ATTR_INSTANT_AVERAGE_VOLTAGE: round(meter.average_voltage, 1), - ATTR_INSTANT_TOTAL_CURRENT: meter.get_instant_total_current(), - ATTR_IS_ACTIVE: meter.is_active(), - } + def unique_id(self) -> str: + """Device Uniqueid.""" + return f"{self.base_unique_id}_backup_reserve" + + @property + def native_value(self) -> int: + """Get the current value in percentage.""" + return round(self.data.backup_reserve) class PowerWallEnergyDirectionSensor(PowerWallEntity, SensorEntity): diff --git a/homeassistant/components/powerwall/strings.json b/homeassistant/components/powerwall/strings.json index 8995a0f956e..f4ece53178f 100644 --- a/homeassistant/components/powerwall/strings.json +++ b/homeassistant/components/powerwall/strings.json @@ -16,7 +16,7 @@ "data": { "password": "[%key:common::config_flow::data::password%]" } - }, + }, "confirm_discovery": { "title": "[%key:component::powerwall::config::step::user::title%]", "description": "Do you want to setup {name} ({ip_address})?" diff --git a/homeassistant/components/powerwall/translations/el.json b/homeassistant/components/powerwall/translations/el.json index d3649e44a8d..630dea5bd23 100644 --- a/homeassistant/components/powerwall/translations/el.json +++ b/homeassistant/components/powerwall/translations/el.json @@ -11,7 +11,7 @@ "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1", "wrong_version": "\u03a4\u03bf powerwall \u03c3\u03b1\u03c2 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af \u03ad\u03ba\u03b4\u03bf\u03c3\u03b7 \u03bb\u03bf\u03b3\u03b9\u03c3\u03bc\u03b9\u03ba\u03bf\u03cd \u03c0\u03bf\u03c5 \u03b4\u03b5\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9. \u03a3\u03ba\u03b5\u03c6\u03c4\u03b5\u03af\u03c4\u03b5 \u03bd\u03b1 \u03b1\u03bd\u03b1\u03b2\u03b1\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03ae \u03bd\u03b1 \u03b1\u03bd\u03b1\u03c6\u03ad\u03c1\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c0\u03c1\u03cc\u03b2\u03bb\u03b7\u03bc\u03b1 \u03ce\u03c3\u03c4\u03b5 \u03bd\u03b1 \u03b5\u03c0\u03b9\u03bb\u03c5\u03b8\u03b5\u03af." }, - "flow_title": "{ip_address}", + "flow_title": "{name} ({ip_address})", "step": { "confirm_discovery": { "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {name} ( {ip_address});", diff --git a/homeassistant/components/powerwall/translations/fr.json b/homeassistant/components/powerwall/translations/fr.json index 1f5d5ac22cd..f7510cf46e4 100644 --- a/homeassistant/components/powerwall/translations/fr.json +++ b/homeassistant/components/powerwall/translations/fr.json @@ -7,14 +7,14 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue", "wrong_version": "Votre Powerwall utilise une version logicielle qui n'est pas prise en charge. Veuillez envisager de mettre \u00e0 niveau ou de signaler ce probl\u00e8me afin qu'il puisse \u00eatre r\u00e9solu." }, "flow_title": "{nom} ({ip_address})", "step": { "confirm_discovery": { - "description": "Voulez-vous configurer {name} ({ip_address})?", + "description": "Voulez-vous configurer {name} ({ip_address})\u00a0?", "title": "Connectez-vous au Powerwall" }, "reauth_confim": { diff --git a/homeassistant/components/powerwall/translations/he.json b/homeassistant/components/powerwall/translations/he.json index c4a69c402c4..53849afa25b 100644 --- a/homeassistant/components/powerwall/translations/he.json +++ b/homeassistant/components/powerwall/translations/he.json @@ -10,8 +10,11 @@ "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" }, - "flow_title": "{ip_address}", + "flow_title": "{name} ({ip_address})", "step": { + "confirm_discovery": { + "title": "\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05dc-powerwall" + }, "reauth_confim": { "data": { "password": "\u05e1\u05d9\u05e1\u05de\u05d4" @@ -23,7 +26,8 @@ "ip_address": "\u05db\u05ea\u05d5\u05d1\u05ea IP", "password": "\u05e1\u05d9\u05e1\u05de\u05d4" }, - "description": "\u05d4\u05e1\u05d9\u05e1\u05de\u05d4 \u05d4\u05d9\u05d0 \u05d1\u05d3\u05e8\u05da \u05db\u05dc\u05dc 5 \u05d4\u05ea\u05d5\u05d5\u05d9\u05dd \u05d4\u05d0\u05d7\u05e8\u05d5\u05e0\u05d9\u05dd \u05e9\u05dc \u05d4\u05de\u05e1\u05e4\u05e8 \u05d4\u05e1\u05d9\u05d3\u05d5\u05e8\u05d9 \u05e2\u05d1\u05d5\u05e8 Backup Gateway \u05d5\u05e0\u05d9\u05ea\u05df \u05dc\u05de\u05e6\u05d5\u05d0 \u05d0\u05d5\u05ea\u05d4 \u05d1\u05d9\u05d9\u05e9\u05d5\u05dd \u05d8\u05e1\u05dc\u05d4 \u05d0\u05d5 \u05d1-5 \u05d4\u05ea\u05d5\u05d5\u05d9\u05dd \u05d4\u05d0\u05d7\u05e8\u05d5\u05e0\u05d9\u05dd \u05e9\u05dc \u05d4\u05e1\u05d9\u05e1\u05de\u05d4 \u05e9\u05e0\u05de\u05e6\u05d0\u05d4 \u05d1\u05ea\u05d5\u05da \u05d4\u05d3\u05dc\u05ea \u05e2\u05d1\u05d5\u05e8 Backup Gateway 2." + "description": "\u05d4\u05e1\u05d9\u05e1\u05de\u05d4 \u05d4\u05d9\u05d0 \u05d1\u05d3\u05e8\u05da \u05db\u05dc\u05dc 5 \u05d4\u05ea\u05d5\u05d5\u05d9\u05dd \u05d4\u05d0\u05d7\u05e8\u05d5\u05e0\u05d9\u05dd \u05e9\u05dc \u05d4\u05de\u05e1\u05e4\u05e8 \u05d4\u05e1\u05d9\u05d3\u05d5\u05e8\u05d9 \u05e2\u05d1\u05d5\u05e8 Backup Gateway \u05d5\u05e0\u05d9\u05ea\u05df \u05dc\u05de\u05e6\u05d5\u05d0 \u05d0\u05d5\u05ea\u05d4 \u05d1\u05d9\u05d9\u05e9\u05d5\u05dd \u05d8\u05e1\u05dc\u05d4 \u05d0\u05d5 \u05d1-5 \u05d4\u05ea\u05d5\u05d5\u05d9\u05dd \u05d4\u05d0\u05d7\u05e8\u05d5\u05e0\u05d9\u05dd \u05e9\u05dc \u05d4\u05e1\u05d9\u05e1\u05de\u05d4 \u05e9\u05e0\u05de\u05e6\u05d0\u05d4 \u05d1\u05ea\u05d5\u05da \u05d4\u05d3\u05dc\u05ea \u05e2\u05d1\u05d5\u05e8 Backup Gateway 2.", + "title": "\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05dc-powerwall" } } } diff --git a/homeassistant/components/profiler/__init__.py b/homeassistant/components/profiler/__init__.py index 99d074d95fe..0f997dd41bd 100644 --- a/homeassistant/components/profiler/__init__.py +++ b/homeassistant/components/profiler/__init__.py @@ -8,6 +8,7 @@ import sys import threading import time import traceback +from typing import Any from guppy import hpy import objgraph @@ -87,13 +88,24 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: persistent_notification.async_dismiss(hass, "profile_object_logging") domain_data.pop(LOG_INTERVAL_SUB)() + def _safe_repr(obj: Any) -> str: + """Get the repr of an object but keep going if there is an exception. + + We wrap repr to ensure if one object cannot be serialized, we can + still get the rest. + """ + try: + return repr(obj) + except Exception: # pylint: disable=broad-except + return f"Failed to serialize {type(obj)}" + def _dump_log_objects(call: ServiceCall) -> None: obj_type = call.data[CONF_TYPE] _LOGGER.critical( "%s objects in memory: %s", obj_type, - objgraph.by_type(obj_type), + [_safe_repr(obj) for obj in objgraph.by_type(obj_type)], ) persistent_notification.create( diff --git a/homeassistant/components/profiler/config_flow.py b/homeassistant/components/profiler/config_flow.py index b63246ce386..db3241bf1d3 100644 --- a/homeassistant/components/profiler/config_flow.py +++ b/homeassistant/components/profiler/config_flow.py @@ -1,6 +1,4 @@ """Config flow for Profiler integration.""" -import voluptuous as vol - from homeassistant import config_entries from .const import DEFAULT_NAME, DOMAIN @@ -19,4 +17,4 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if user_input is not None: return self.async_create_entry(title=DEFAULT_NAME, data={}) - return self.async_show_form(step_id="user", data_schema=vol.Schema({})) + return self.async_show_form(step_id="user") diff --git a/homeassistant/components/profiler/manifest.json b/homeassistant/components/profiler/manifest.json index 6e6dde8df1b..c68cba58559 100644 --- a/homeassistant/components/profiler/manifest.json +++ b/homeassistant/components/profiler/manifest.json @@ -2,7 +2,11 @@ "domain": "profiler", "name": "Profiler", "documentation": "https://www.home-assistant.io/integrations/profiler", - "requirements": ["pyprof2calltree==1.4.5", "guppy3==3.1.2", "objgraph==3.4.1"], + "requirements": [ + "pyprof2calltree==1.4.5", + "guppy3==3.1.2", + "objgraph==3.5.0" + ], "codeowners": ["@bdraco"], "quality_scale": "internal", "config_flow": true diff --git a/homeassistant/components/profiler/translations/fr.json b/homeassistant/components/profiler/translations/fr.json index 4d10b4f4ecc..1bd91ac8a21 100644 --- a/homeassistant/components/profiler/translations/fr.json +++ b/homeassistant/components/profiler/translations/fr.json @@ -5,7 +5,7 @@ }, "step": { "user": { - "description": "Voulez-vous commencer la configuration ?" + "description": "Voulez-vous commencer la configuration\u00a0?" } } } diff --git a/homeassistant/components/prosegur/manifest.json b/homeassistant/components/prosegur/manifest.json index ecb3a9e6c41..4dbdf9f48ba 100644 --- a/homeassistant/components/prosegur/manifest.json +++ b/homeassistant/components/prosegur/manifest.json @@ -3,12 +3,8 @@ "name": "Prosegur Alarm", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/prosegur", - "requirements": [ - "pyprosegur==0.0.5" - ], - "codeowners": [ - "@dgomes" - ], + "requirements": ["pyprosegur==0.0.5"], + "codeowners": ["@dgomes"], "iot_class": "cloud_polling", "loggers": ["pyprosegur"] } diff --git a/homeassistant/components/prosegur/strings.json b/homeassistant/components/prosegur/strings.json index 919628c7510..bf0beb4e766 100644 --- a/homeassistant/components/prosegur/strings.json +++ b/homeassistant/components/prosegur/strings.json @@ -26,4 +26,4 @@ "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } } -} \ No newline at end of file +} diff --git a/homeassistant/components/prosegur/translations/fr.json b/homeassistant/components/prosegur/translations/fr.json index 42061673128..5a0c38508d3 100644 --- a/homeassistant/components/prosegur/translations/fr.json +++ b/homeassistant/components/prosegur/translations/fr.json @@ -6,7 +6,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/ps4/strings.json b/homeassistant/components/ps4/strings.json index f2a59830b59..b44862c527b 100644 --- a/homeassistant/components/ps4/strings.json +++ b/homeassistant/components/ps4/strings.json @@ -2,25 +2,26 @@ "config": { "step": { "creds": { - "title": "PlayStation 4", "description": "Credentials needed. Press 'Submit' and then in the PS4 2nd Screen App, refresh devices and select the 'Home-Assistant' device to continue." }, "mode": { - "title": "PlayStation 4", - "description": "Select mode for configuration. The [%key:common::config_flow::data::ip%] field can be left blank if selecting Auto Discovery, as devices will be automatically discovered.", "data": { "mode": "Config Mode", "ip_address": "[%key:common::config_flow::data::ip%] (Leave empty if using Auto Discovery)." + }, + "data_description": { + "ip_address": "Leave blank if selecting auto-discovery." } }, "link": { - "title": "PlayStation 4", - "description": "Enter your PlayStation 4 information. For [%key:common::config_flow::data::pin%], navigate to 'Settings' on your PlayStation 4 console. Then navigate to 'Mobile App Connection Settings' and select 'Add Device'. Enter the [%key:common::config_flow::data::pin%] that is displayed. Refer to the [documentation](https://www.home-assistant.io/components/ps4/) for additional info.", "data": { "region": "Region", "name": "[%key:common::config_flow::data::name%]", "code": "[%key:common::config_flow::data::pin%]", "ip_address": "[%key:common::config_flow::data::ip%]" + }, + "data_description": { + "code": "Navigate to 'Settings' on your PlayStation 4 console. Then navigate to 'Mobile App Connection Settings' and select 'Add Device' to get the pin." } } }, diff --git a/homeassistant/components/ps4/translations/fr.json b/homeassistant/components/ps4/translations/fr.json index c4765c31c4a..8fc216fd20b 100644 --- a/homeassistant/components/ps4/translations/fr.json +++ b/homeassistant/components/ps4/translations/fr.json @@ -15,7 +15,7 @@ }, "step": { "creds": { - "description": "Informations d\u2019identification n\u00e9cessaires. Appuyez sur \u00ab\u00a0Envoyer\u00a0\u00bb puis dans la PS4 2\u00e8me \u00e9cran App, actualisez les p\u00e9riph\u00e9riques et s\u00e9lectionnez le dispositif \u00ab\u00a0Home Assistant\u00a0\u00bb pour continuer.", + "description": "Informations d'identification requises. Appuyez sur \u00ab\u00a0Envoyer\u00a0\u00bb, puis dans l'application PS4 Second Screen, actualisez les appareils et s\u00e9lectionnez l'appareil \u00ab\u00a0Home Assistant\u00a0\u00bb pour continuer.", "title": "PlayStation 4" }, "link": { @@ -25,7 +25,7 @@ "name": "Nom", "region": "R\u00e9gion" }, - "description": "Entrez vos informations PlayStation 4. Pour \"Code PIN\", acc\u00e9dez \u00e0 \"Param\u00e8tres\" sur votre console PlayStation 4. Ensuite, acc\u00e9dez \u00e0 \"Param\u00e8tres de connexion de l'application mobile\" et s\u00e9lectionnez \"Ajouter un p\u00e9riph\u00e9rique\". Entrez le code PIN qui est affich\u00e9. Consultez la documentation pour plus d'informations.", + "description": "Saisissez les informations de votre PlayStation\u00a04. Pour le Code PIN, acc\u00e9dez aux \u00ab\u00a0Param\u00e8tres\u00a0\u00bb sur votre console PlayStation\u00a04, puis acc\u00e9dez \u00e0 \u00ab\u00a0Param\u00e8tres de connexion de l'application mobile\u00a0\u00bb et s\u00e9lectionnez \u00ab\u00a0Ajouter un appareil\u00a0\u00bb. Entrez le Code PIN affich\u00e9. Consultez la [documentation](https://www.home-assistant.io/components/ps4/) pour plus d'informations.", "title": "PlayStation 4" }, "mode": { @@ -33,7 +33,7 @@ "ip_address": "Adresse IP (laissez vide si vous utilisez la d\u00e9couverte automatique).", "mode": "Mode de configuration" }, - "description": "S\u00e9lectionnez le mode de configuration. Le champ Adresse IP peut rester vide si vous s\u00e9lectionnez D\u00e9couverte automatique, car les p\u00e9riph\u00e9riques seront automatiquement d\u00e9couverts.", + "description": "S\u00e9lectionnez le mode de configuration. Le champ Adresse IP peut \u00eatre laiss\u00e9 vide si vous s\u00e9lectionnez D\u00e9couverte automatique, car les appareils seront automatiquement d\u00e9couverts.", "title": "PlayStation 4" } } diff --git a/homeassistant/components/ps4/translations/it.json b/homeassistant/components/ps4/translations/it.json index 0a7db1888f6..8df24f24afb 100644 --- a/homeassistant/components/ps4/translations/it.json +++ b/homeassistant/components/ps4/translations/it.json @@ -33,7 +33,7 @@ "ip_address": "Indirizzo IP (Lascia vuoto se stai usando il rilevamento automatico).", "mode": "Modalit\u00e0 di configurazione" }, - "description": "Seleziona la modalit\u00e0 per la configurazione. Il campo per l'indirizzo IP pu\u00f2 essere lasciato vuoto se si seleziona il rilevamento automatico, poich\u00e9 i dispositivi saranno automaticamente individuati.", + "description": "Seleziona la modalit\u00e0 per la configurazione. Il campo per l'indirizzo IP pu\u00f2 essere lasciato vuoto se si seleziona il rilevamento automatico, poich\u00e9 i dispositivi saranno rilevati automaticamente.", "title": "PlayStation 4" } } diff --git a/homeassistant/components/ps4/translations/zh-Hant.json b/homeassistant/components/ps4/translations/zh-Hant.json index 4475700481a..e9c8dac2a26 100644 --- a/homeassistant/components/ps4/translations/zh-Hant.json +++ b/homeassistant/components/ps4/translations/zh-Hant.json @@ -30,10 +30,10 @@ }, "mode": { "data": { - "ip_address": "IP \u4f4d\u5740\uff08\u5982\u679c\u4f7f\u7528\u81ea\u52d5\u63a2\u7d22\u65b9\u5f0f\uff0c\u8acb\u4fdd\u7559\u7a7a\u767d\uff09\u3002", + "ip_address": "IP \u4f4d\u5740\uff08\u5982\u679c\u4f7f\u7528\u81ea\u52d5\u641c\u7d22\u65b9\u5f0f\uff0c\u8acb\u4fdd\u7559\u7a7a\u767d\uff09\u3002", "mode": "\u8a2d\u5b9a\u6a21\u5f0f" }, - "description": "\u9078\u64c7\u6a21\u5f0f\u4ee5\u9032\u884c\u8a2d\u5b9a\u3002\u5047\u5982\u9078\u64c7\u81ea\u52d5\u63a2\u7d22\u6a21\u5f0f\u7684\u8a71\uff0c\u7531\u65bc\u6703\u81ea\u52d5\u9032\u884c\u88dd\u7f6e\u641c\u5c0b\uff0cIP \u4f4d\u5740\u53ef\u4fdd\u7559\u70ba\u7a7a\u767d\u3002", + "description": "\u9078\u64c7\u6a21\u5f0f\u4ee5\u9032\u884c\u8a2d\u5b9a\u3002\u5047\u5982\u9078\u64c7\u81ea\u52d5\u641c\u7d22\u6a21\u5f0f\u7684\u8a71\uff0c\u7531\u65bc\u6703\u81ea\u52d5\u9032\u884c\u88dd\u7f6e\u641c\u5c0b\uff0cIP \u4f4d\u5740\u53ef\u4fdd\u7559\u70ba\u7a7a\u767d\u3002", "title": "PlayStation 4" } } diff --git a/homeassistant/components/pure_energie/manifest.json b/homeassistant/components/pure_energie/manifest.json index 7997e9c4b5d..1bd63f431e7 100644 --- a/homeassistant/components/pure_energie/manifest.json +++ b/homeassistant/components/pure_energie/manifest.json @@ -13,4 +13,4 @@ "name": "smartbridge*" } ] -} \ No newline at end of file +} diff --git a/homeassistant/components/pure_energie/sensor.py b/homeassistant/components/pure_energie/sensor.py index fffbfd7c7bb..8b07a2818f8 100644 --- a/homeassistant/components/pure_energie/sensor.py +++ b/homeassistant/components/pure_energie/sensor.py @@ -78,10 +78,11 @@ async def async_setup_entry( ) -class PureEnergieSensorEntity(CoordinatorEntity[PureEnergieData], SensorEntity): +class PureEnergieSensorEntity( + CoordinatorEntity[PureEnergieDataUpdateCoordinator], SensorEntity +): """Defines an Pure Energie sensor.""" - coordinator: PureEnergieDataUpdateCoordinator entity_description: PureEnergieSensorEntityDescription def __init__( diff --git a/homeassistant/components/pure_energie/strings.json b/homeassistant/components/pure_energie/strings.json index 356d161f006..4f65d2d8be1 100644 --- a/homeassistant/components/pure_energie/strings.json +++ b/homeassistant/components/pure_energie/strings.json @@ -20,4 +20,4 @@ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" } } -} \ No newline at end of file +} diff --git a/homeassistant/components/pure_energie/translations/fr.json b/homeassistant/components/pure_energie/translations/fr.json new file mode 100644 index 00000000000..7ab74aae074 --- /dev/null +++ b/homeassistant/components/pure_energie/translations/fr.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "cannot_connect": "\u00c9chec de connexion" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion" + }, + "flow_title": "{model} ({host})", + "step": { + "user": { + "data": { + "host": "H\u00f4te" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pure_energie/translations/it.json b/homeassistant/components/pure_energie/translations/it.json index 457f7cfebc0..b7fb47b1ea4 100644 --- a/homeassistant/components/pure_energie/translations/it.json +++ b/homeassistant/components/pure_energie/translations/it.json @@ -2,10 +2,10 @@ "config": { "abort": { "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", - "cannot_connect": "Connessione fallita" + "cannot_connect": "Impossibile connettersi" }, "error": { - "cannot_connect": "Connessione fallita" + "cannot_connect": "Impossibile connettersi" }, "flow_title": "{model} ({host})", "step": { diff --git a/homeassistant/components/pure_energie/translations/pt-BR.json b/homeassistant/components/pure_energie/translations/pt-BR.json index 148cd129376..b43c1c285ba 100644 --- a/homeassistant/components/pure_energie/translations/pt-BR.json +++ b/homeassistant/components/pure_energie/translations/pt-BR.json @@ -1,21 +1,21 @@ { "config": { "abort": { - "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", - "cannot_connect": "Falhou em conectar" + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "cannot_connect": "Falha ao conectar" }, "error": { - "cannot_connect": "Falhou em conectar" + "cannot_connect": "Falha ao conectar" }, - "flow_title": "{model} ( {host} )", + "flow_title": "{model} ({host})", "step": { "user": { "data": { - "host": "Host" + "host": "Nome do host" } }, "zeroconf_confirm": { - "description": "Deseja adicionar medidor Pure Energie (` {model} `) ao Home Assistant?", + "description": "Deseja adicionar medidor Pure Energie (`{model}`) ao Home Assistant?", "title": "Descoberto o dispositivo medidor Pure Energie" } } diff --git a/homeassistant/components/pure_energie/translations/zh-Hant.json b/homeassistant/components/pure_energie/translations/zh-Hant.json index 7fa7144f914..56235b16952 100644 --- a/homeassistant/components/pure_energie/translations/zh-Hant.json +++ b/homeassistant/components/pure_energie/translations/zh-Hant.json @@ -16,7 +16,7 @@ }, "zeroconf_confirm": { "description": "\u662f\u5426\u8981\u65b0\u589e Pure Energie Meter (`{model}`) \u81f3 Home Assistant\uff1f", - "title": "\u81ea\u52d5\u63a2\u7d22\u5230 Pure Energie Meter \u88dd\u7f6e" + "title": "\u81ea\u52d5\u641c\u7d22\u5230 Pure Energie Meter \u88dd\u7f6e" } } } diff --git a/homeassistant/components/pvoutput/config_flow.py b/homeassistant/components/pvoutput/config_flow.py index 53eabe225f6..4349a79593e 100644 --- a/homeassistant/components/pvoutput/config_flow.py +++ b/homeassistant/components/pvoutput/config_flow.py @@ -7,7 +7,7 @@ from pvo import PVOutput, PVOutputAuthenticationError, PVOutputError import voluptuous as vol from homeassistant.config_entries import ConfigEntry, ConfigFlow -from homeassistant.const import CONF_API_KEY, CONF_NAME +from homeassistant.const import CONF_API_KEY from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -83,16 +83,6 @@ class PVOutputFlowHandler(ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_import(self, config: dict[str, Any]) -> FlowResult: - """Handle a flow initialized by importing a config.""" - self.imported_name = config[CONF_NAME] - return await self.async_step_user( - user_input={ - CONF_SYSTEM_ID: config[CONF_SYSTEM_ID], - CONF_API_KEY: config[CONF_API_KEY], - } - ) - async def async_step_reauth(self, data: dict[str, Any]) -> FlowResult: """Handle initiation of re-authentication with PVOutput.""" self.reauth_entry = self.hass.config_entries.async_get_entry( diff --git a/homeassistant/components/pvoutput/const.py b/homeassistant/components/pvoutput/const.py index dd2aca530ed..f01d10fd13d 100644 --- a/homeassistant/components/pvoutput/const.py +++ b/homeassistant/components/pvoutput/const.py @@ -14,12 +14,4 @@ LOGGER = logging.getLogger(__package__) SCAN_INTERVAL = timedelta(minutes=2) -ATTR_ENERGY_GENERATION = "energy_generation" -ATTR_POWER_GENERATION = "power_generation" -ATTR_ENERGY_CONSUMPTION = "energy_consumption" -ATTR_POWER_CONSUMPTION = "power_consumption" -ATTR_EFFICIENCY = "efficiency" - CONF_SYSTEM_ID = "system_id" - -DEFAULT_NAME = "PVOutput" diff --git a/homeassistant/components/pvoutput/sensor.py b/homeassistant/components/pvoutput/sensor.py index ef8537c548e..7bd4b8789eb 100644 --- a/homeassistant/components/pvoutput/sensor.py +++ b/homeassistant/components/pvoutput/sensor.py @@ -5,21 +5,15 @@ from collections.abc import Callable from dataclasses import dataclass from pvo import Status, System -import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, SensorEntityDescription, SensorStateClass, ) -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - ATTR_TEMPERATURE, - ATTR_VOLTAGE, - CONF_API_KEY, - CONF_NAME, ELECTRIC_POTENTIAL_VOLT, ENERGY_KILO_WATT_HOUR, ENERGY_WATT_HOUR, @@ -28,33 +22,13 @@ from homeassistant.const import ( TEMP_CELSIUS, ) from homeassistant.core import HomeAssistant -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, DiscoveryInfoType from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import ( - ATTR_EFFICIENCY, - ATTR_ENERGY_CONSUMPTION, - ATTR_ENERGY_GENERATION, - ATTR_POWER_CONSUMPTION, - ATTR_POWER_GENERATION, - CONF_SYSTEM_ID, - DEFAULT_NAME, - DOMAIN, - LOGGER, -) +from .const import CONF_SYSTEM_ID, DOMAIN from .coordinator import PVOutputDataUpdateCoordinator -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_API_KEY): cv.string, - vol.Required(CONF_SYSTEM_ID): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - } -) - @dataclass class PVOutputSensorEntityDescriptionMixin: @@ -129,32 +103,6 @@ SENSORS: tuple[PVOutputSensorEntityDescription, ...] = ( ) -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the PVOutput sensor.""" - LOGGER.warning( - "Configuration of the PVOutput platform in YAML is deprecated and will be " - "removed in Home Assistant 2022.4; Your existing configuration " - "has been imported into the UI automatically and can be safely removed " - "from your configuration.yaml file" - ) - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={ - CONF_SYSTEM_ID: config[CONF_SYSTEM_ID], - CONF_API_KEY: config[CONF_API_KEY], - CONF_NAME: config[CONF_NAME], - }, - ) - ) - - async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, @@ -175,10 +123,11 @@ async def async_setup_entry( ) -class PVOutputSensorEntity(CoordinatorEntity, SensorEntity): +class PVOutputSensorEntity( + CoordinatorEntity[PVOutputDataUpdateCoordinator], SensorEntity +): """Representation of a PVOutput sensor.""" - coordinator: PVOutputDataUpdateCoordinator entity_description: PVOutputSensorEntityDescription def __init__( @@ -205,21 +154,3 @@ class PVOutputSensorEntity(CoordinatorEntity, SensorEntity): def native_value(self) -> int | float | None: """Return the state of the device.""" return self.entity_description.value_fn(self.coordinator.data) - - @property - def extra_state_attributes(self) -> dict[str, int | float | None] | None: - """Return the state attributes of the monitored installation.""" - - # Only add attributes to the original sensor - if self.entity_description.key != "energy_generation": - return None - - return { - ATTR_ENERGY_GENERATION: self.coordinator.data.energy_generation, - ATTR_POWER_GENERATION: self.coordinator.data.power_generation, - ATTR_ENERGY_CONSUMPTION: self.coordinator.data.energy_consumption, - ATTR_POWER_CONSUMPTION: self.coordinator.data.power_consumption, - ATTR_EFFICIENCY: self.coordinator.data.normalized_output, - ATTR_TEMPERATURE: self.coordinator.data.temperature, - ATTR_VOLTAGE: self.coordinator.data.voltage, - } diff --git a/homeassistant/components/pvoutput/strings.json b/homeassistant/components/pvoutput/strings.json index 513866025a4..644a756924c 100644 --- a/homeassistant/components/pvoutput/strings.json +++ b/homeassistant/components/pvoutput/strings.json @@ -9,7 +9,7 @@ } }, "reauth_confirm": { - "description":"To re-authenticate with PVOutput you'll need to get the API key at {account_url}.", + "description": "To re-authenticate with PVOutput you'll need to get the API key at {account_url}.", "data": { "api_key": "[%key:common::config_flow::data::api_key%]" } diff --git a/homeassistant/components/pvoutput/translations/fr.json b/homeassistant/components/pvoutput/translations/fr.json index 3ca874733e4..5ebf1cf3eb8 100644 --- a/homeassistant/components/pvoutput/translations/fr.json +++ b/homeassistant/components/pvoutput/translations/fr.json @@ -1,22 +1,22 @@ { "config": { "abort": { - "reauth_successful": "R\u00e9-authentification r\u00e9ussie" + "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" }, "error": { - "cannot_connect": "Erreur de connexion", - "invalid_auth": "Erreur d'authentification" + "cannot_connect": "\u00c9chec de connexion", + "invalid_auth": "Authentification non valide" }, "step": { "reauth_confirm": { "data": { - "api_key": "Cl\u00e9 API" + "api_key": "Cl\u00e9 d'API" }, "description": "Pour vous r\u00e9-authentifier avec PVOutput, vous devrez obtenir la cl\u00e9 API sur {account_url} ." }, "user": { "data": { - "api_key": "Cl\u00e9 API", + "api_key": "Cl\u00e9 d'API", "system_id": "ID syst\u00e8me" }, "description": "Pour vous authentifier avec PVOutput, vous devrez obtenir la cl\u00e9 API sur {account_url} . \n\n Les ID syst\u00e8me des syst\u00e8mes enregistr\u00e9s sont r\u00e9pertori\u00e9s sur cette m\u00eame page." diff --git a/homeassistant/components/pvpc_hourly_pricing/sensor.py b/homeassistant/components/pvpc_hourly_pricing/sensor.py index 6d9ac9402e6..8cfae034bff 100644 --- a/homeassistant/components/pvpc_hourly_pricing/sensor.py +++ b/homeassistant/components/pvpc_hourly_pricing/sensor.py @@ -125,11 +125,9 @@ async def async_setup_entry( ) -class ElecPriceSensor(CoordinatorEntity, SensorEntity): +class ElecPriceSensor(CoordinatorEntity[ElecPricesDataUpdateCoordinator], SensorEntity): """Class to hold the prices of electricity as a sensor.""" - coordinator: ElecPricesDataUpdateCoordinator - def __init__( self, coordinator: ElecPricesDataUpdateCoordinator, diff --git a/homeassistant/components/pvpc_hourly_pricing/strings.json b/homeassistant/components/pvpc_hourly_pricing/strings.json index 89da917c8ea..a008ef9f4da 100644 --- a/homeassistant/components/pvpc_hourly_pricing/strings.json +++ b/homeassistant/components/pvpc_hourly_pricing/strings.json @@ -2,8 +2,6 @@ "config": { "step": { "user": { - "title": "Sensor setup", - "description": "This sensor uses official API to get [hourly pricing of electricity (PVPC)](https://www.esios.ree.es/es/pvpc) in Spain.\nFor more precise explanation visit the [integration docs](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/).", "data": { "name": "Sensor Name", "tariff": "Applicable tariff by geographic zone", @@ -19,8 +17,6 @@ "options": { "step": { "init": { - "title": "Sensor setup", - "description": "This sensor uses official API to get [hourly pricing of electricity (PVPC)](https://www.esios.ree.es/es/pvpc) in Spain.\nFor more precise explanation visit the [integration docs](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/).", "data": { "tariff": "Applicable tariff by geographic zone", "power": "Contracted power (kW)", diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/pl.json b/homeassistant/components/pvpc_hourly_pricing/translations/pl.json index d689fd9383a..052cf66c1a6 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/pl.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/pl.json @@ -11,7 +11,7 @@ "power_p3": "Moc zakontraktowana dla okresu zni\u017ckowego P3 (kW)", "tariff": "Obowi\u0105zuj\u0105ca taryfa wed\u0142ug strefy geograficznej" }, - "description": "Ten czujnik u\u017cywa oficjalnego interfejsu API w celu uzyskania [godzinowej ceny energii elektrycznej (PVPC)] (https://www.esios.ree.es/es/pvpc) w Hiszpanii. \n Aby uzyska\u0107 bardziej szczeg\u00f3\u0142owe wyja\u015bnienia, odwied\u017a [dokumentacj\u0119 dotycz\u0105c\u0105 integracji] (https://www.home-assistant.io/integrations/pvpc_hourly_pricing/). \n\n Wybierz stawk\u0119 umown\u0105 na podstawie liczby okres\u00f3w rozliczeniowych dziennie: \n - 1 okres: normalny \n - 2 okresy: dyskryminacja (nocna stawka) \n - 3 okresy: samoch\u00f3d elektryczny (stawka nocna za 3 okresy)", + "description": "Ten sensor u\u017cywa oficjalnego interfejsu API w celu uzyskania [godzinowej ceny energii elektrycznej (PVPC)] (https://www.esios.ree.es/es/pvpc) w Hiszpanii. \n Aby uzyska\u0107 bardziej szczeg\u00f3\u0142owe wyja\u015bnienia, odwied\u017a [dokumentacj\u0119 dotycz\u0105c\u0105 integracji] (https://www.home-assistant.io/integrations/pvpc_hourly_pricing/). \n\n Wybierz stawk\u0119 umown\u0105 na podstawie liczby okres\u00f3w rozliczeniowych dziennie: \n - 1 okres: normalny \n - 2 okresy: dyskryminacja (nocna stawka) \n - 3 okresy: samoch\u00f3d elektryczny (stawka nocna za 3 okresy)", "title": "Konfiguracja sensora" } } @@ -24,7 +24,7 @@ "power_p3": "Moc zakontraktowana dla okresu zni\u017ckowego P3 (kW)", "tariff": "Obowi\u0105zuj\u0105ca taryfa wed\u0142ug strefy geograficznej" }, - "description": "Ten czujnik u\u017cywa oficjalnego interfejsu API w celu uzyskania [godzinowej ceny energii elektrycznej (PVPC)] (https://www.esios.ree.es/es/pvpc) w Hiszpanii. \n Aby uzyska\u0107 bardziej szczeg\u00f3\u0142owe wyja\u015bnienia, odwied\u017a [dokumentacj\u0119 dotycz\u0105c\u0105 integracji] (https://www.home-assistant.io/integrations/pvpc_hourly_pricing/). \n\n Wybierz stawk\u0119 umown\u0105 na podstawie liczby okres\u00f3w rozliczeniowych dziennie: \n - 1 okres: normalny \n - 2 okresy: dyskryminacja (nocna stawka) \n - 3 okresy: samoch\u00f3d elektryczny (stawka nocna za 3 okresy)", + "description": "Ten sensor u\u017cywa oficjalnego interfejsu API w celu uzyskania [godzinowej ceny energii elektrycznej (PVPC)] (https://www.esios.ree.es/es/pvpc) w Hiszpanii. \n Aby uzyska\u0107 bardziej szczeg\u00f3\u0142owe wyja\u015bnienia, odwied\u017a [dokumentacj\u0119 dotycz\u0105c\u0105 integracji] (https://www.home-assistant.io/integrations/pvpc_hourly_pricing/). \n\n Wybierz stawk\u0119 umown\u0105 na podstawie liczby okres\u00f3w rozliczeniowych dziennie: \n - 1 okres: normalny \n - 2 okresy: dyskryminacja (nocna stawka) \n - 3 okresy: samoch\u00f3d elektryczny (stawka nocna za 3 okresy)", "title": "Konfiguracja sensora" } } diff --git a/homeassistant/components/qwikswitch/__init__.py b/homeassistant/components/qwikswitch/__init__.py index ecf7720b283..e529483d0cc 100644 --- a/homeassistant/components/qwikswitch/__init__.py +++ b/homeassistant/components/qwikswitch/__init__.py @@ -21,6 +21,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import load_platform +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import ConfigType @@ -96,9 +97,7 @@ class QSEntity(Entity): async def async_added_to_hass(self): """Listen for updates from QSUSb via dispatcher.""" self.async_on_remove( - self.hass.helpers.dispatcher.async_dispatcher_connect( - self.qsid, self.update_packet - ) + async_dispatcher_connect(self.hass, self.qsid, self.update_packet) ) diff --git a/homeassistant/components/rachio/strings.json b/homeassistant/components/rachio/strings.json index 105a2f7c78a..697b0bce2db 100644 --- a/homeassistant/components/rachio/strings.json +++ b/homeassistant/components/rachio/strings.json @@ -27,4 +27,4 @@ } } } -} \ No newline at end of file +} diff --git a/homeassistant/components/rachio/translations/fr.json b/homeassistant/components/rachio/translations/fr.json index 343256cea9a..88de6eb89f7 100644 --- a/homeassistant/components/rachio/translations/fr.json +++ b/homeassistant/components/rachio/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/radio_browser/strings.json b/homeassistant/components/radio_browser/strings.json index 7bf9bc9ca66..fd0470d26dc 100644 --- a/homeassistant/components/radio_browser/strings.json +++ b/homeassistant/components/radio_browser/strings.json @@ -9,4 +9,4 @@ "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" } } -} \ No newline at end of file +} diff --git a/homeassistant/components/ozw/translations/bg.json b/homeassistant/components/radio_browser/translations/bg.json similarity index 63% rename from homeassistant/components/ozw/translations/bg.json rename to homeassistant/components/radio_browser/translations/bg.json index 6f032fa973a..1c6120581b0 100644 --- a/homeassistant/components/ozw/translations/bg.json +++ b/homeassistant/components/radio_browser/translations/bg.json @@ -1,7 +1,6 @@ { "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", "single_instance_allowed": "\u0412\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e. \u0412\u044a\u0437\u043c\u043e\u0436\u043d\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f." } } diff --git a/homeassistant/components/radio_browser/translations/es.json b/homeassistant/components/radio_browser/translations/es.json new file mode 100644 index 00000000000..6bb377d2d28 --- /dev/null +++ b/homeassistant/components/radio_browser/translations/es.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Ya est\u00e1 configurado. Solamente una configuraci\u00f3n es posible." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/radio_browser/translations/fr.json b/homeassistant/components/radio_browser/translations/fr.json new file mode 100644 index 00000000000..807ba246694 --- /dev/null +++ b/homeassistant/components/radio_browser/translations/fr.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/radio_browser/translations/he.json b/homeassistant/components/radio_browser/translations/he.json new file mode 100644 index 00000000000..d0c3523da94 --- /dev/null +++ b/homeassistant/components/radio_browser/translations/he.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/radio_browser/translations/id.json b/homeassistant/components/radio_browser/translations/id.json new file mode 100644 index 00000000000..a06ce8b840d --- /dev/null +++ b/homeassistant/components/radio_browser/translations/id.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan." + }, + "step": { + "user": { + "description": "Ingin menambahkan Radio Browser ke Home Assistant?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/radio_browser/translations/pt-BR.json b/homeassistant/components/radio_browser/translations/pt-BR.json index b25a8cbef92..2b5b6d1ad13 100644 --- a/homeassistant/components/radio_browser/translations/pt-BR.json +++ b/homeassistant/components/radio_browser/translations/pt-BR.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "single_instance_allowed": "J\u00e1 est\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + "single_instance_allowed": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel." }, "step": { "user": { - "description": "Deseja adicionar o Radio Browser ao Home Assistant?" + "description": "Deseja adicionar o R\u00e1dio Browser ao Home Assistant?" } } } diff --git a/homeassistant/components/radio_browser/translations/tr.json b/homeassistant/components/radio_browser/translations/tr.json new file mode 100644 index 00000000000..41b37f2b0c2 --- /dev/null +++ b/homeassistant/components/radio_browser/translations/tr.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + }, + "step": { + "user": { + "description": "Home Assistant'a Radyo Taray\u0131c\u0131 eklemek istiyor musunuz?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rainforest_eagle/sensor.py b/homeassistant/components/rainforest_eagle/sensor.py index 8f59888603c..a2c336bc5d0 100644 --- a/homeassistant/components/rainforest_eagle/sensor.py +++ b/homeassistant/components/rainforest_eagle/sensor.py @@ -67,11 +67,9 @@ async def async_setup_entry( async_add_entities(entities) -class EagleSensor(CoordinatorEntity, SensorEntity): +class EagleSensor(CoordinatorEntity[EagleDataCoordinator], SensorEntity): """Implementation of the Rainforest Eagle sensor.""" - coordinator: EagleDataCoordinator - def __init__(self, coordinator, entity_description): """Initialize the sensor.""" super().__init__(coordinator) diff --git a/homeassistant/components/rainforest_eagle/translations/fr.json b/homeassistant/components/rainforest_eagle/translations/fr.json index 9631ff6cc93..cb86945aaca 100644 --- a/homeassistant/components/rainforest_eagle/translations/fr.json +++ b/homeassistant/components/rainforest_eagle/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/rainmachine/services.yaml b/homeassistant/components/rainmachine/services.yaml index b4fb3c8bf1b..7dcbe2cd8d7 100644 --- a/homeassistant/components/rainmachine/services.yaml +++ b/homeassistant/components/rainmachine/services.yaml @@ -155,7 +155,7 @@ push_weather_data: min: -40 max: 40 step: 0.1 - unit_of_measurement: '°C' + unit_of_measurement: "°C" maxtemp: name: Max Temp description: Maximum Temperature (°C). @@ -164,7 +164,7 @@ push_weather_data: min: -40 max: 40 step: 0.1 - unit_of_measurement: '°C' + unit_of_measurement: "°C" temperature: name: Temperature description: Current Temperature (°C). @@ -173,7 +173,7 @@ push_weather_data: min: -40 max: 40 step: 0.1 - unit_of_measurement: '°C' + unit_of_measurement: "°C" wind: name: Wind Speed description: Wind Speed (m/s) @@ -181,7 +181,7 @@ push_weather_data: number: min: 0 max: 65 - unit_of_measurement: 'm/s' + unit_of_measurement: "m/s" solarrad: name: Solar Radiation description: Solar Radiation (MJ/m²/h) @@ -190,7 +190,7 @@ push_weather_data: min: 0 max: 5 step: 0.1 - unit_of_measurement: 'MJ/m²/h' + unit_of_measurement: "MJ/m²/h" et: name: Evapotranspiration description: Evapotranspiration (mm) @@ -198,7 +198,7 @@ push_weather_data: number: min: 0 max: 1000 - unit_of_measurement: 'mm' + unit_of_measurement: "mm" qpf: name: Quantitative Precipitation Forecast description: >- @@ -209,7 +209,7 @@ push_weather_data: number: min: 0 max: 1000 - unit_of_measurement: 'mm' + unit_of_measurement: "mm" rain: name: Measured Rainfall description: >- @@ -220,7 +220,7 @@ push_weather_data: number: min: 0 max: 1000 - unit_of_measurement: 'mm' + unit_of_measurement: "mm" minrh: name: Min Relative Humidity description: Min Relative Humidity (%RH) @@ -228,7 +228,7 @@ push_weather_data: number: min: 0 max: 100 - unit_of_measurement: '%' + unit_of_measurement: "%" maxrh: name: Max Relative Humidity description: Max Relative Humidity (%RH) @@ -236,7 +236,7 @@ push_weather_data: number: min: 0 max: 100 - unit_of_measurement: '%' + unit_of_measurement: "%" condition: name: Weather Condition Code description: Current weather condition code (WNUM). @@ -258,7 +258,7 @@ push_weather_data: min: -40 max: 40 step: 0.1 - unit_of_measurement: '°C' + unit_of_measurement: "°C" unrestrict_watering: name: Unrestrict All Watering description: Unrestrict all watering activities diff --git a/homeassistant/components/rainmachine/translations/fr.json b/homeassistant/components/rainmachine/translations/fr.json index db8bac46b59..d9a6c011755 100644 --- a/homeassistant/components/rainmachine/translations/fr.json +++ b/homeassistant/components/rainmachine/translations/fr.json @@ -4,7 +4,7 @@ "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" }, "error": { - "invalid_auth": "Authentification invalide" + "invalid_auth": "Authentification non valide" }, "flow_title": "{ip}", "step": { diff --git a/homeassistant/components/raspihats/__init__.py b/homeassistant/components/raspihats/__init__.py deleted file mode 100644 index 8f4a8b0aca4..00000000000 --- a/homeassistant/components/raspihats/__init__.py +++ /dev/null @@ -1,253 +0,0 @@ -"""Support for controlling raspihats boards.""" -import logging -import threading -import time - -from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP -from homeassistant.core import HomeAssistant -from homeassistant.helpers.typing import ConfigType - -_LOGGER = logging.getLogger(__name__) - -DOMAIN = "raspihats" - -CONF_I2C_HATS = "i2c_hats" -CONF_BOARD = "board" -CONF_CHANNELS = "channels" -CONF_INDEX = "index" -CONF_INVERT_LOGIC = "invert_logic" -CONF_INITIAL_STATE = "initial_state" - -I2C_HAT_NAMES = [ - "Di16", - "Rly10", - "Di6Rly6", - "DI16ac", - "DQ10rly", - "DQ16oc", - "DI6acDQ6rly", -] - -I2C_HATS_MANAGER = "I2CH_MNG" - - -def setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up the raspihats component.""" - _LOGGER.warning( - "The Raspihats pHAT integration is deprecated and will be removed " - "in Home Assistant Core 2022.4; this integration is removed under " - "Architectural Decision Record 0019, more information can be found here: " - "https://github.com/home-assistant/architecture/blob/master/adr/0019-GPIO.md" - ) - - hass.data[DOMAIN] = {I2C_HATS_MANAGER: I2CHatsManager()} - - def start_i2c_hats_keep_alive(event): - """Start I2C-HATs keep alive.""" - hass.data[DOMAIN][I2C_HATS_MANAGER].start_keep_alive() - - def stop_i2c_hats_keep_alive(event): - """Stop I2C-HATs keep alive.""" - hass.data[DOMAIN][I2C_HATS_MANAGER].stop_keep_alive() - - hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_i2c_hats_keep_alive) - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_i2c_hats_keep_alive) - return True - - -def log_message(source, *parts): - """Build log message.""" - message = source.__class__.__name__ - for part in parts: - message += f": {part!s}" - return message - - -class I2CHatsException(Exception): - """I2C-HATs exception.""" - - -class I2CHatsDIScanner: - """Scan Digital Inputs and fire callbacks.""" - - _DIGITAL_INPUTS = "di" - _OLD_VALUE = "old_value" - _CALLBACKS = "callbacks" - - def setup(self, i2c_hat): - """Set up the I2C-HAT instance for digital inputs scanner.""" - if hasattr(i2c_hat, self._DIGITAL_INPUTS): - digital_inputs = getattr(i2c_hat, self._DIGITAL_INPUTS) - old_value = None - # Add old value attribute - setattr(digital_inputs, self._OLD_VALUE, old_value) - # Add callbacks dict attribute {channel: callback} - setattr(digital_inputs, self._CALLBACKS, {}) - - def register_callback(self, i2c_hat, channel, callback): - """Register edge callback.""" - if hasattr(i2c_hat, self._DIGITAL_INPUTS): - digital_inputs = getattr(i2c_hat, self._DIGITAL_INPUTS) - callbacks = getattr(digital_inputs, self._CALLBACKS) - callbacks[channel] = callback - setattr(digital_inputs, self._CALLBACKS, callbacks) - - def scan(self, i2c_hat): - """Scan I2C-HATs digital inputs and fire callbacks.""" - if hasattr(i2c_hat, self._DIGITAL_INPUTS): - digital_inputs = getattr(i2c_hat, self._DIGITAL_INPUTS) - callbacks = getattr(digital_inputs, self._CALLBACKS) - old_value = getattr(digital_inputs, self._OLD_VALUE) - value = digital_inputs.value # i2c data transfer - if old_value is not None and value != old_value: - for channel in range(0, len(digital_inputs.channels)): - state = (value >> channel) & 0x01 - old_state = (old_value >> channel) & 0x01 - if state != old_state: - callback = callbacks.get(channel) - if callback is not None: - callback(state) - setattr(digital_inputs, self._OLD_VALUE, value) - - -class I2CHatsManager(threading.Thread): - """Manages all I2C-HATs instances.""" - - _EXCEPTION = "exception" - _CALLBACKS = "callbacks" - - def __init__(self): - """Init I2C-HATs Manager.""" - threading.Thread.__init__(self) - self._lock = threading.Lock() - self._i2c_hats = {} - self._run = False - self._di_scanner = I2CHatsDIScanner() - - def register_board(self, board, address): - """Register I2C-HAT.""" - with self._lock: - if (i2c_hat := self._i2c_hats.get(address)) is None: - # This is a Pi module and can't be installed in CI without - # breaking the build. - # pylint: disable=import-outside-toplevel,import-error - import raspihats.i2c_hats as module - - constructor = getattr(module, board) - i2c_hat = constructor(address) - setattr(i2c_hat, self._CALLBACKS, {}) - - # Setting exception attribute will trigger online callbacks - # when keep alive thread starts. - setattr(i2c_hat, self._EXCEPTION, None) - - self._di_scanner.setup(i2c_hat) - self._i2c_hats[address] = i2c_hat - status_word = i2c_hat.status # read status_word to reset bits - _LOGGER.info(log_message(self, i2c_hat, "registered", status_word)) - - def run(self): - """Keep alive for I2C-HATs.""" - # This is a Pi module and can't be installed in CI without - # breaking the build. - # pylint: disable=import-outside-toplevel,import-error - from raspihats.i2c_hats import ResponseException - - _LOGGER.info(log_message(self, "starting")) - - while self._run: - with self._lock: - for i2c_hat in list(self._i2c_hats.values()): - try: - self._di_scanner.scan(i2c_hat) - self._read_status(i2c_hat) - - if hasattr(i2c_hat, self._EXCEPTION): - if getattr(i2c_hat, self._EXCEPTION) is not None: - _LOGGER.warning( - log_message(self, i2c_hat, "online again") - ) - delattr(i2c_hat, self._EXCEPTION) - # trigger online callbacks - callbacks = getattr(i2c_hat, self._CALLBACKS) - for callback in list(callbacks.values()): - callback() - except ResponseException as ex: - if not hasattr(i2c_hat, self._EXCEPTION): - _LOGGER.error(log_message(self, i2c_hat, ex)) - setattr(i2c_hat, self._EXCEPTION, ex) - time.sleep(0.05) - _LOGGER.info(log_message(self, "exiting")) - - def _read_status(self, i2c_hat): - """Read I2C-HATs status.""" - status_word = i2c_hat.status - if status_word.value != 0x00: - _LOGGER.error(log_message(self, i2c_hat, status_word)) - - def start_keep_alive(self): - """Start keep alive mechanism.""" - self._run = True - threading.Thread.start(self) - - def stop_keep_alive(self): - """Stop keep alive mechanism.""" - self._run = False - self.join() - - def register_di_callback(self, address, channel, callback): - """Register I2C-HAT digital input edge callback.""" - with self._lock: - i2c_hat = self._i2c_hats[address] - self._di_scanner.register_callback(i2c_hat, channel, callback) - - def register_online_callback(self, address, channel, callback): - """Register I2C-HAT online callback.""" - with self._lock: - i2c_hat = self._i2c_hats[address] - callbacks = getattr(i2c_hat, self._CALLBACKS) - callbacks[channel] = callback - setattr(i2c_hat, self._CALLBACKS, callbacks) - - def read_di(self, address, channel): - """Read a value from a I2C-HAT digital input.""" - # This is a Pi module and can't be installed in CI without - # breaking the build. - # pylint: disable=import-outside-toplevel,import-error - from raspihats.i2c_hats import ResponseException - - with self._lock: - i2c_hat = self._i2c_hats[address] - try: - value = i2c_hat.di.value - return (value >> channel) & 0x01 - except ResponseException as ex: - raise I2CHatsException(str(ex)) from ex - - def write_dq(self, address, channel, value): - """Write a value to a I2C-HAT digital output.""" - # This is a Pi module and can't be installed in CI without - # breaking the build. - # pylint: disable=import-outside-toplevel,import-error - from raspihats.i2c_hats import ResponseException - - with self._lock: - i2c_hat = self._i2c_hats[address] - try: - i2c_hat.dq.channels[channel] = value - except ResponseException as ex: - raise I2CHatsException(str(ex)) from ex - - def read_dq(self, address, channel): - """Read a value from a I2C-HAT digital output.""" - # This is a Pi module and can't be installed in CI without - # breaking the build. - # pylint: disable=import-outside-toplevel,import-error - from raspihats.i2c_hats import ResponseException - - with self._lock: - i2c_hat = self._i2c_hats[address] - try: - return i2c_hat.dq.channels[channel] - except ResponseException as ex: - raise I2CHatsException(str(ex)) from ex diff --git a/homeassistant/components/raspihats/binary_sensor.py b/homeassistant/components/raspihats/binary_sensor.py deleted file mode 100644 index f8fbc0d010f..00000000000 --- a/homeassistant/components/raspihats/binary_sensor.py +++ /dev/null @@ -1,146 +0,0 @@ -"""Support for raspihats board binary sensors.""" -from __future__ import annotations - -import logging - -import voluptuous as vol - -from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity -from homeassistant.const import ( - CONF_ADDRESS, - CONF_DEVICE_CLASS, - CONF_NAME, - DEVICE_DEFAULT_NAME, -) -from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType - -from . import ( - CONF_BOARD, - CONF_CHANNELS, - CONF_I2C_HATS, - CONF_INDEX, - CONF_INVERT_LOGIC, - DOMAIN, - I2C_HAT_NAMES, - I2C_HATS_MANAGER, - I2CHatsException, - I2CHatsManager, -) - -_LOGGER = logging.getLogger(__name__) - -DEFAULT_INVERT_LOGIC = False -DEFAULT_DEVICE_CLASS = None - -_CHANNELS_SCHEMA = vol.Schema( - [ - { - vol.Required(CONF_INDEX): cv.positive_int, - vol.Required(CONF_NAME): cv.string, - vol.Optional(CONF_INVERT_LOGIC, default=DEFAULT_INVERT_LOGIC): cv.boolean, - vol.Optional(CONF_DEVICE_CLASS, default=DEFAULT_DEVICE_CLASS): cv.string, - } - ] -) - -_I2C_HATS_SCHEMA = vol.Schema( - [ - { - vol.Required(CONF_BOARD): vol.In(I2C_HAT_NAMES), - vol.Required(CONF_ADDRESS): vol.Coerce(int), - vol.Required(CONF_CHANNELS): _CHANNELS_SCHEMA, - } - ] -) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - {vol.Optional(CONF_I2C_HATS): _I2C_HATS_SCHEMA} -) - - -def setup_platform( - hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the raspihats binary_sensor devices.""" - I2CHatBinarySensor.I2C_HATS_MANAGER = hass.data[DOMAIN][I2C_HATS_MANAGER] - binary_sensors = [] - i2c_hat_configs = config.get(CONF_I2C_HATS, []) - for i2c_hat_config in i2c_hat_configs: - address = i2c_hat_config[CONF_ADDRESS] - board = i2c_hat_config[CONF_BOARD] - try: - assert I2CHatBinarySensor.I2C_HATS_MANAGER - I2CHatBinarySensor.I2C_HATS_MANAGER.register_board(board, address) - for channel_config in i2c_hat_config[CONF_CHANNELS]: - binary_sensors.append( - I2CHatBinarySensor( - address, - channel_config[CONF_INDEX], - channel_config[CONF_NAME], - channel_config[CONF_INVERT_LOGIC], - channel_config[CONF_DEVICE_CLASS], - ) - ) - except I2CHatsException as ex: - _LOGGER.error( - "Failed to register %s I2CHat@%s %s", board, hex(address), str(ex) - ) - add_entities(binary_sensors) - - -class I2CHatBinarySensor(BinarySensorEntity): - """Representation of a binary sensor that uses a I2C-HAT digital input.""" - - I2C_HATS_MANAGER: I2CHatsManager | None = None - - def __init__(self, address, channel, name, invert_logic, device_class): - """Initialize the raspihats sensor.""" - self._address = address - self._channel = channel - self._name = name or DEVICE_DEFAULT_NAME - self._invert_logic = invert_logic - self._device_class = device_class - self._state = self.I2C_HATS_MANAGER.read_di(self._address, self._channel) - - def online_callback(): - """Call fired when board is online.""" - self.schedule_update_ha_state() - - self.I2C_HATS_MANAGER.register_online_callback( - self._address, self._channel, online_callback - ) - - def edge_callback(state): - """Read digital input state.""" - self._state = state - self.schedule_update_ha_state() - - self.I2C_HATS_MANAGER.register_di_callback( - self._address, self._channel, edge_callback - ) - - @property - def device_class(self): - """Return the class of this sensor.""" - return self._device_class - - @property - def name(self): - """Return the name of this sensor.""" - return self._name - - @property - def should_poll(self): - """No polling needed for this sensor.""" - return False - - @property - def is_on(self): - """Return the state of this sensor.""" - return self._state != self._invert_logic diff --git a/homeassistant/components/raspihats/manifest.json b/homeassistant/components/raspihats/manifest.json deleted file mode 100644 index 984f440e064..00000000000 --- a/homeassistant/components/raspihats/manifest.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "domain": "raspihats", - "name": "Raspihats", - "documentation": "https://www.home-assistant.io/integrations/raspihats", - "requirements": ["raspihats==2.2.3", "smbus-cffi==0.5.1"], - "codeowners": [], - "iot_class": "local_push" -} diff --git a/homeassistant/components/raspihats/switch.py b/homeassistant/components/raspihats/switch.py deleted file mode 100644 index 8ca88528543..00000000000 --- a/homeassistant/components/raspihats/switch.py +++ /dev/null @@ -1,161 +0,0 @@ -"""Support for raspihats board switches.""" -from __future__ import annotations - -import logging - -import voluptuous as vol - -from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity -from homeassistant.const import CONF_ADDRESS, CONF_NAME, DEVICE_DEFAULT_NAME -from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType - -from . import ( - CONF_BOARD, - CONF_CHANNELS, - CONF_I2C_HATS, - CONF_INDEX, - CONF_INITIAL_STATE, - CONF_INVERT_LOGIC, - DOMAIN, - I2C_HAT_NAMES, - I2C_HATS_MANAGER, - I2CHatsException, - I2CHatsManager, -) - -_LOGGER = logging.getLogger(__name__) - -_CHANNELS_SCHEMA = vol.Schema( - [ - { - vol.Required(CONF_INDEX): cv.positive_int, - vol.Required(CONF_NAME): cv.string, - vol.Optional(CONF_INVERT_LOGIC, default=False): cv.boolean, - vol.Optional(CONF_INITIAL_STATE): cv.boolean, - } - ] -) - -_I2C_HATS_SCHEMA = vol.Schema( - [ - { - vol.Required(CONF_BOARD): vol.In(I2C_HAT_NAMES), - vol.Required(CONF_ADDRESS): vol.Coerce(int), - vol.Required(CONF_CHANNELS): _CHANNELS_SCHEMA, - } - ] -) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - {vol.Optional(CONF_I2C_HATS): _I2C_HATS_SCHEMA} -) - - -def setup_platform( - hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the raspihats switch devices.""" - I2CHatSwitch.I2C_HATS_MANAGER = hass.data[DOMAIN][I2C_HATS_MANAGER] - switches = [] - i2c_hat_configs = config.get(CONF_I2C_HATS, []) - for i2c_hat_config in i2c_hat_configs: - board = i2c_hat_config[CONF_BOARD] - address = i2c_hat_config[CONF_ADDRESS] - try: - assert I2CHatSwitch.I2C_HATS_MANAGER - I2CHatSwitch.I2C_HATS_MANAGER.register_board(board, address) - for channel_config in i2c_hat_config[CONF_CHANNELS]: - switches.append( - I2CHatSwitch( - board, - address, - channel_config[CONF_INDEX], - channel_config[CONF_NAME], - channel_config[CONF_INVERT_LOGIC], - channel_config.get(CONF_INITIAL_STATE), - ) - ) - except I2CHatsException as ex: - _LOGGER.error( - "Failed to register %s I2CHat@%s %s", board, hex(address), str(ex) - ) - add_entities(switches) - - -class I2CHatSwitch(SwitchEntity): - """Representation a switch that uses a I2C-HAT digital output.""" - - I2C_HATS_MANAGER: I2CHatsManager | None = None - - def __init__(self, board, address, channel, name, invert_logic, initial_state): - """Initialize switch.""" - self._board = board - self._address = address - self._channel = channel - self._name = name or DEVICE_DEFAULT_NAME - self._invert_logic = invert_logic - if initial_state is not None: - if self._invert_logic: - state = not initial_state - else: - state = initial_state - self.I2C_HATS_MANAGER.write_dq(self._address, self._channel, state) - - def online_callback(): - """Call fired when board is online.""" - self.schedule_update_ha_state() - - self.I2C_HATS_MANAGER.register_online_callback( - self._address, self._channel, online_callback - ) - - def _log_message(self, message): - """Create log message.""" - string = f"{self._name} " - string += f"{self._board}I2CHat@{hex(self._address)} " - string += f"channel:{str(self._channel)}{message}" - return string - - @property - def name(self): - """Return the name of the switch.""" - return self._name - - @property - def should_poll(self): - """Return the polling state.""" - return False - - @property - def is_on(self): - """Return true if device is on.""" - try: - state = self.I2C_HATS_MANAGER.read_dq(self._address, self._channel) - return state != self._invert_logic - except I2CHatsException as ex: - _LOGGER.error(self._log_message(f"Is ON check failed, {ex!s}")) - return False - - def turn_on(self, **kwargs): - """Turn the device on.""" - try: - state = self._invert_logic is False - self.I2C_HATS_MANAGER.write_dq(self._address, self._channel, state) - self.schedule_update_ha_state() - except I2CHatsException as ex: - _LOGGER.error(self._log_message(f"Turn ON failed, {ex!s}")) - - def turn_off(self, **kwargs): - """Turn the device off.""" - try: - state = self._invert_logic is not False - self.I2C_HATS_MANAGER.write_dq(self._address, self._channel, state) - self.schedule_update_ha_state() - except I2CHatsException as ex: - _LOGGER.error(self._log_message(f"Turn OFF failed, {ex!s}")) diff --git a/homeassistant/components/rdw/manifest.json b/homeassistant/components/rdw/manifest.json index c2e71185c64..774d0234d06 100644 --- a/homeassistant/components/rdw/manifest.json +++ b/homeassistant/components/rdw/manifest.json @@ -3,7 +3,7 @@ "name": "RDW", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/rdw", - "requirements": ["vehicle==0.3.1"], + "requirements": ["vehicle==0.4.0"], "codeowners": ["@frenck"], "quality_scale": "platinum", "iot_class": "cloud_polling" diff --git a/homeassistant/components/recollect_waste/translations/zh-Hant.json b/homeassistant/components/recollect_waste/translations/zh-Hant.json index 2444a202720..62c064526ae 100644 --- a/homeassistant/components/recollect_waste/translations/zh-Hant.json +++ b/homeassistant/components/recollect_waste/translations/zh-Hant.json @@ -19,7 +19,7 @@ "step": { "init": { "data": { - "friendly_name": "\u91dd\u5c0d\u9078\u53d6\u985e\u578b\u4f7f\u7528\u53cb\u5584\u540d\u7a31\uff08\u5047\u5982\u9069\u7528\uff09" + "friendly_name": "\u91dd\u5c0d\u9078\u53d6\u985e\u5225\u4f7f\u7528\u53cb\u5584\u540d\u7a31\uff08\u5047\u5982\u9069\u7528\uff09" }, "title": "\u8a2d\u5b9a Recollect Waste" } diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index 410ad165ce1..0381e5a4671 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -4,7 +4,6 @@ from __future__ import annotations import abc import asyncio from collections.abc import Callable, Iterable -import concurrent.futures from dataclasses import dataclass from datetime import datetime, timedelta import logging @@ -12,9 +11,11 @@ import queue import sqlite3 import threading import time -from typing import Any +from typing import Any, TypeVar, cast +from lru import LRU # pylint: disable=no-name-in-module from sqlalchemy import create_engine, event as sqlalchemy_event, exc, func, select +from sqlalchemy.engine import Engine from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.orm import scoped_session, sessionmaker from sqlalchemy.orm.session import Session @@ -32,7 +33,14 @@ from homeassistant.const import ( EVENT_TIME_CHANGED, MATCH_ALL, ) -from homeassistant.core import CoreState, HomeAssistant, ServiceCall, callback +from homeassistant.core import ( + CALLBACK_TYPE, + CoreState, + Event, + HomeAssistant, + ServiceCall, + callback, +) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entityfilter import ( INCLUDE_EXCLUDE_BASE_FILTER_SCHEMA, @@ -57,19 +65,22 @@ from . import history, migration, purge, statistics, websocket_api from .const import ( CONF_DB_INTEGRITY_CHECK, DATA_INSTANCE, + DB_WORKER_PREFIX, DOMAIN, MAX_QUEUE_BACKLOG, SQLITE_URL_PREFIX, ) +from .executor import DBInterruptibleThreadPoolExecutor from .models import ( Base, Events, RecorderRuns, + StateAttributes, States, StatisticsRuns, process_timestamp, ) -from .pool import RecorderPool +from .pool import POOL_SIZE, RecorderPool from .util import ( dburl_to_path, end_incomplete_runs, @@ -83,6 +94,10 @@ from .util import ( _LOGGER = logging.getLogger(__name__) +T = TypeVar("T") + +EXCLUDE_ATTRIBUTES = f"{DOMAIN}_exclude_attributes_by_domain" + SERVICE_PURGE = "purge" SERVICE_PURGE_ENTITIES = "purge_entities" SERVICE_ENABLE = "enable" @@ -126,6 +141,17 @@ KEEPALIVE_TIME = 30 # States and Events objects EXPIRE_AFTER_COMMITS = 120 +# The number of attribute ids to cache in memory +# +# Based on: +# - The number of overlapping attributes +# - How frequently states with overlapping attributes will change +# - How much memory our low end hardware has +STATE_ATTRIBUTES_ID_CACHE_SIZE = 2048 + +SHUTDOWN_TASK = object() + + DB_LOCK_TIMEOUT = 30 DB_LOCK_QUEUE_CHECK_TIMEOUT = 1 @@ -182,6 +208,16 @@ CONFIG_SCHEMA = vol.Schema( ) +# Pool size must accommodate Recorder thread + All db executors +MAX_DB_EXECUTOR_WORKERS = POOL_SIZE - 1 + + +def get_instance(hass: HomeAssistant) -> Recorder: + """Get the recorder instance.""" + instance: Recorder = hass.data[DATA_INSTANCE] + return instance + + @bind_hass def is_entity_recorded(hass: HomeAssistant, entity_id: str) -> bool: """Check if an entity is being recorded. @@ -190,34 +226,40 @@ def is_entity_recorded(hass: HomeAssistant, entity_id: str) -> bool: """ if DATA_INSTANCE not in hass.data: return False - return hass.data[DATA_INSTANCE].entity_filter(entity_id) + instance: Recorder = hass.data[DATA_INSTANCE] + return instance.entity_filter(entity_id) -def run_information(hass, point_in_time: datetime | None = None): +def run_information( + hass: HomeAssistant, point_in_time: datetime | None = None +) -> RecorderRuns | None: """Return information about current run. There is also the run that covers point_in_time. """ - run_info = run_information_from_instance(hass, point_in_time) - if run_info: + if run_info := run_information_from_instance(hass, point_in_time): return run_info with session_scope(hass=hass) as session: return run_information_with_session(session, point_in_time) -def run_information_from_instance(hass, point_in_time: datetime | None = None): +def run_information_from_instance( + hass: HomeAssistant, point_in_time: datetime | None = None +) -> RecorderRuns | None: """Return information about current run from the existing instance. Does not query the database for older runs. """ - ins = hass.data[DATA_INSTANCE] - + ins = get_instance(hass) if point_in_time is None or point_in_time > ins.recording_start: return ins.run_info + return None -def run_information_with_session(session, point_in_time: datetime | None = None): +def run_information_with_session( + session: Session, point_in_time: datetime | None = None +) -> RecorderRuns | None: """Return information about current run from the database.""" recorder_runs = RecorderRuns @@ -227,15 +269,17 @@ def run_information_with_session(session, point_in_time: datetime | None = None) (recorder_runs.start < point_in_time) & (recorder_runs.end > point_in_time) ) - res = query.first() - if res: + if (res := query.first()) is not None: session.expunge(res) + return cast(RecorderRuns, res) return res async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the recorder.""" hass.data[DOMAIN] = {} + exclude_attributes_by_domain: dict[str, set[str]] = {} + hass.data[EXCLUDE_ATTRIBUTES] = exclude_attributes_by_domain conf = config[DOMAIN] entity_filter = convert_include_exclude_filter(conf) auto_purge = conf[CONF_AUTO_PURGE] @@ -263,8 +307,10 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: db_retry_wait=db_retry_wait, entity_filter=entity_filter, exclude_t=exclude_t, + exclude_attributes_by_domain=exclude_attributes_by_domain, ) instance.async_initialize() + instance.async_register() instance.start() _async_register_services(hass, instance) history.async_setup(hass) @@ -275,13 +321,18 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return await instance.async_db_ready -async def _process_recorder_platform(hass, domain, platform): +async def _process_recorder_platform( + hass: HomeAssistant, domain: str, platform: Any +) -> None: """Process a recorder platform.""" - hass.data[DOMAIN][domain] = platform + platforms: dict[str, Any] = hass.data[DOMAIN] + platforms[domain] = platform + if hasattr(platform, "exclude_attributes"): + hass.data[EXCLUDE_ATTRIBUTES][domain] = platform.exclude_attributes(hass) @callback -def _async_register_services(hass, instance): +def _async_register_services(hass: HomeAssistant, instance: Recorder) -> None: """Register recorder services.""" async def async_handle_purge_service(service: ServiceCall) -> None: @@ -331,6 +382,8 @@ def _async_register_services(hass, instance): class RecorderTask(abc.ABC): """ABC for recorder tasks.""" + commit_before = True + @abc.abstractmethod def run(self, instance: Recorder) -> None: """Handle the task.""" @@ -435,10 +488,37 @@ class ExternalStatisticsTask(RecorderTask): instance.queue.put(ExternalStatisticsTask(self.metadata, self.statistics)) +@dataclass +class AdjustStatisticsTask(RecorderTask): + """An object to insert into the recorder queue to run an adjust statistics task.""" + + statistic_id: str + start_time: datetime + sum_adjustment: float + + def run(self, instance: Recorder) -> None: + """Run statistics task.""" + if statistics.adjust_statistics( + instance, + self.statistic_id, + self.start_time, + self.sum_adjustment, + ): + return + # Schedule a new adjust statistics task if this one didn't finish + instance.queue.put( + AdjustStatisticsTask( + self.statistic_id, self.start_time, self.sum_adjustment + ) + ) + + @dataclass class WaitTask(RecorderTask): """An object to insert into the recorder queue to tell it set the _queue_watch event.""" + commit_before = False + def run(self, instance: Recorder) -> None: """Handle the task.""" instance._queue_watch.set() # pylint: disable=[protected-access] @@ -461,6 +541,8 @@ class DatabaseLockTask(RecorderTask): class StopTask(RecorderTask): """An object to insert into the recorder queue to stop the event handler.""" + commit_before = False + def run(self, instance: Recorder) -> None: """Handle the task.""" instance.stop_requested = True @@ -468,9 +550,10 @@ class StopTask(RecorderTask): @dataclass class EventTask(RecorderTask): - """An object to insert into the recorder queue to stop the event handler.""" + """An event to be processed.""" - event: bool + event: Event + commit_before = False def run(self, instance: Recorder) -> None: """Handle the task.""" @@ -494,6 +577,7 @@ class Recorder(threading.Thread): db_retry_wait: int, entity_filter: Callable[[str], bool], exclude_t: list[str], + exclude_attributes_by_domain: dict[str, set[str]], ) -> None: """Initialize the recorder.""" threading.Thread.__init__(self, name="Recorder") @@ -501,17 +585,18 @@ class Recorder(threading.Thread): self.hass = hass self.auto_purge = auto_purge self.keep_days = keep_days + self._hass_started: asyncio.Future[object] = asyncio.Future() self.commit_interval = commit_interval self.queue: queue.SimpleQueue[RecorderTask] = queue.SimpleQueue() self.recording_start = dt_util.utcnow() self.db_url = uri self.db_max_retries = db_max_retries self.db_retry_wait = db_retry_wait - self.async_db_ready: asyncio.Future = asyncio.Future() + self.async_db_ready: asyncio.Future[bool] = asyncio.Future() self.async_recorder_ready = asyncio.Event() self._queue_watch = threading.Event() - self.engine: Any = None - self.run_info: Any = None + self.engine: Engine | None = None + self.run_info: RecorderRuns | None = None self.entity_filter = entity_filter self.exclude_t = exclude_t @@ -520,25 +605,43 @@ class Recorder(threading.Thread): self._commits_without_expire = 0 self._keepalive_count = 0 self._old_states: dict[str, States] = {} + self._state_attributes_ids: LRU = LRU(STATE_ATTRIBUTES_ID_CACHE_SIZE) + self._pending_state_attributes: dict[str, StateAttributes] = {} self._pending_expunge: list[States] = [] - self.event_session = None - self.get_session = None - self._completed_first_database_setup = None - self._event_listener = None + self.event_session: Session | None = None + self.get_session: Callable[[], Session] | None = None + self._completed_first_database_setup: bool | None = None + self._event_listener: CALLBACK_TYPE | None = None self.async_migration_event = asyncio.Event() self.migration_in_progress = False - self._queue_watcher = None + self._queue_watcher: CALLBACK_TYPE | None = None self._db_supports_row_number = True self._database_lock_task: DatabaseLockTask | None = None + self._db_executor: DBInterruptibleThreadPoolExecutor | None = None + self._exclude_attributes_by_domain = exclude_attributes_by_domain self.enabled = True - def set_enable(self, enable): + def set_enable(self, enable: bool) -> None: """Enable or disable recording events and states.""" self.enabled = enable @callback - def async_initialize(self): + def async_start_executor(self) -> None: + """Start the executor.""" + self._db_executor = DBInterruptibleThreadPoolExecutor( + thread_name_prefix=DB_WORKER_PREFIX, + max_workers=MAX_DB_EXECUTOR_WORKERS, + shutdown_hook=self._shutdown_pool, + ) + + def _shutdown_pool(self) -> None: + """Close the dbpool connections in the current thread.""" + if self.engine and hasattr(self.engine.pool, "shutdown"): + self.engine.pool.shutdown() + + @callback + def async_initialize(self) -> None: """Initialize the recorder.""" self._event_listener = self.hass.bus.async_listen( MATCH_ALL, self.event_listener, event_filter=self._async_event_filter @@ -548,7 +651,20 @@ class Recorder(threading.Thread): ) @callback - def _async_check_queue(self, *_): + def async_add_executor_job( + self, target: Callable[..., T], *args: Any + ) -> asyncio.Future[T]: + """Add an executor job from within the event loop.""" + return self.hass.loop.run_in_executor(self._db_executor, target, *args) + + def _stop_executor(self) -> None: + """Stop the executor.""" + assert self._db_executor is not None + self._db_executor.shutdown() + self._db_executor = None + + @callback + def _async_check_queue(self, *_: Any) -> None: """Periodic check of the queue size to ensure we do not exaust memory. The queue grows during migraton or if something really goes wrong. @@ -564,7 +680,7 @@ class Recorder(threading.Thread): self._async_stop_queue_watcher_and_event_listener() @callback - def _async_stop_queue_watcher_and_event_listener(self): + def _async_stop_queue_watcher_and_event_listener(self) -> None: """Stop watching the queue and listening for events.""" if self._queue_watcher: self._queue_watcher() @@ -574,7 +690,7 @@ class Recorder(threading.Thread): self._event_listener = None @callback - def _async_event_filter(self, event) -> bool: + def _async_event_filter(self, event: Event) -> bool: """Filter events.""" if event.event_type in self.exclude_t: return False @@ -594,31 +710,33 @@ class Recorder(threading.Thread): # Unknown what it is. return True - def do_adhoc_purge(self, **kwargs): + def do_adhoc_purge(self, **kwargs: Any) -> None: """Trigger an adhoc purge retaining keep_days worth of data.""" keep_days = kwargs.get(ATTR_KEEP_DAYS, self.keep_days) - repack = kwargs.get(ATTR_REPACK) - apply_filter = kwargs.get(ATTR_APPLY_FILTER) + repack = cast(bool, kwargs[ATTR_REPACK]) + apply_filter = cast(bool, kwargs[ATTR_APPLY_FILTER]) purge_before = dt_util.utcnow() - timedelta(days=keep_days) self.queue.put(PurgeTask(purge_before, repack, apply_filter)) - def do_adhoc_purge_entities(self, entity_ids, domains, entity_globs): + def do_adhoc_purge_entities( + self, entity_ids: set[str], domains: list[str], entity_globs: list[str] + ) -> None: """Trigger an adhoc purge of requested entities.""" - entity_filter = generate_filter(domains, entity_ids, [], [], entity_globs) + entity_filter = generate_filter(domains, list(entity_ids), [], [], entity_globs) self.queue.put(PurgeEntitiesTask(entity_filter)) - def do_adhoc_statistics(self, **kwargs): + def do_adhoc_statistics(self, **kwargs: Any) -> None: """Trigger an adhoc statistics run.""" if not (start := kwargs.get("start")): start = statistics.get_start_time() self.queue.put(StatisticsTask(start)) @callback - def async_register(self, shutdown_task, hass_started): + def async_register(self) -> None: """Post connection initialize.""" - def _empty_queue(event): + def _empty_queue(event: Event) -> None: """Empty the queue if its still present at final write.""" # If the queue is full of events to be processed because @@ -637,29 +755,31 @@ class Recorder(threading.Thread): self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_FINAL_WRITE, _empty_queue) - def shutdown(event): + async def _async_shutdown(event: Event) -> None: """Shut down the Recorder.""" - if not hass_started.done(): - hass_started.set_result(shutdown_task) + if not self._hass_started.done(): + self._hass_started.set_result(SHUTDOWN_TASK) self.queue.put(StopTask()) - self.hass.add_job(self._async_stop_queue_watcher_and_event_listener) - self.join() + self._async_stop_queue_watcher_and_event_listener() + await self.hass.async_add_executor_job(self.join) - self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, shutdown) + self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_shutdown) if self.hass.state == CoreState.running: - hass_started.set_result(None) + self._hass_started.set_result(None) return @callback - def async_hass_started(event): + def _async_hass_started(event: Event) -> None: """Notify that hass has started.""" - hass_started.set_result(None) + self._hass_started.set_result(None) - self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, async_hass_started) + self.hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STARTED, _async_hass_started + ) @callback - def async_connection_failed(self): + def async_connection_failed(self) -> None: """Connect failed tasks.""" self.async_db_ready.set_result(False) persistent_notification.async_create( @@ -670,18 +790,19 @@ class Recorder(threading.Thread): self._async_stop_queue_watcher_and_event_listener() @callback - def async_connection_success(self): + def async_connection_success(self) -> None: """Connect success tasks.""" self.async_db_ready.set_result(True) + self.async_start_executor() @callback - def _async_recorder_ready(self): + def _async_recorder_ready(self) -> None: """Finish start and mark recorder ready.""" self._async_setup_periodic_tasks() self.async_recorder_ready.set() @callback - def async_nightly_tasks(self, now): + def async_nightly_tasks(self, now: datetime) -> None: """Trigger the purge.""" if self.auto_purge: # Purge will schedule the perodic cleanups @@ -693,28 +814,37 @@ class Recorder(threading.Thread): self.queue.put(PerodicCleanupTask()) @callback - def async_periodic_statistics(self, now): + def async_periodic_statistics(self, now: datetime) -> None: """Trigger the hourly statistics run.""" start = statistics.get_start_time() self.queue.put(StatisticsTask(start)) @callback - def async_clear_statistics(self, statistic_ids): + def async_adjust_statistics( + self, statistic_id: str, start_time: datetime, sum_adjustment: float + ) -> None: + """Adjust statistics.""" + self.queue.put(AdjustStatisticsTask(statistic_id, start_time, sum_adjustment)) + + @callback + def async_clear_statistics(self, statistic_ids: list[str]) -> None: """Clear statistics for a list of statistic_ids.""" self.queue.put(ClearStatisticsTask(statistic_ids)) @callback - def async_update_statistics_metadata(self, statistic_id, unit_of_measurement): + def async_update_statistics_metadata( + self, statistic_id: str, unit_of_measurement: str | None + ) -> None: """Update statistics metadata for a statistic_id.""" self.queue.put(UpdateStatisticsMetadataTask(statistic_id, unit_of_measurement)) @callback - def async_external_statistics(self, metadata, stats): + def async_external_statistics(self, metadata: dict, stats: Iterable[dict]) -> None: """Schedule external statistics.""" self.queue.put(ExternalStatisticsTask(metadata, stats)) @callback - def _async_setup_periodic_tasks(self): + def _async_setup_periodic_tasks(self) -> None: """Prepare periodic tasks.""" if self.hass.is_stopping or not self.get_session: # Home Assistant is shutting down @@ -730,13 +860,18 @@ class Recorder(threading.Thread): self.hass, self.async_periodic_statistics, minute=range(0, 60, 5), second=10 ) - def run(self): + async def _async_wait_for_started(self) -> object | None: + """Wait for the hass started future.""" + return await self._hass_started + + def _wait_startup_or_shutdown(self) -> object | None: + """Wait for startup or shutdown before starting.""" + return asyncio.run_coroutine_threadsafe( + self._async_wait_for_started(), self.hass.loop + ).result() + + def run(self) -> None: """Start processing events to save.""" - shutdown_task = object() - hass_started = concurrent.futures.Future() - - self.hass.add_job(self.async_register, shutdown_task, hass_started) - current_version = self._setup_recorder() if current_version is None: @@ -750,8 +885,9 @@ class Recorder(threading.Thread): self.migration_in_progress = True self.hass.add_job(self.async_connection_success) + # If shutdown happened before Home Assistant finished starting - if hass_started.result() is shutdown_task: + if self._wait_startup_or_shutdown() is SHUTDOWN_TASK: self.migration_in_progress = False # Make sure we cleanly close the run if # we restart before startup finishes @@ -782,7 +918,7 @@ class Recorder(threading.Thread): self.hass.add_job(self._async_recorder_ready) self._run_event_loop() - def _run_event_loop(self): + def _run_event_loop(self) -> None: """Run the event loop for the recorder.""" # Use a session for the event read loop # with a commit every time the event time @@ -797,9 +933,13 @@ class Recorder(threading.Thread): self._shutdown() - def _process_one_task_or_recover(self, task: RecorderTask): + def _process_one_task_or_recover(self, task: RecorderTask) -> None: """Process an event, reconnect, or recover a malformed database.""" try: + # If its not an event, commit everything + # that is pending before running the task + if task.commit_before: + self._commit_event_session_or_retry() return task.run(self) except exc.DatabaseError as err: if self._handle_database_error(err): @@ -834,11 +974,11 @@ class Recorder(threading.Thread): return None @callback - def _async_migration_started(self): + def _async_migration_started(self) -> None: """Set the migration started event.""" self.async_migration_event.set() - def _migrate_schema_and_setup_run(self, current_version) -> bool: + def _migrate_schema_and_setup_run(self, current_version: int) -> bool: """Migrate schema to the latest version.""" persistent_notification.create( self.hass, @@ -865,9 +1005,9 @@ class Recorder(threading.Thread): self.migration_in_progress = False persistent_notification.dismiss(self.hass, "recorder_database_migration") - def _lock_database(self, task: DatabaseLockTask): + def _lock_database(self, task: DatabaseLockTask) -> None: @callback - def _async_set_database_locked(task: DatabaseLockTask): + def _async_set_database_locked(task: DatabaseLockTask) -> None: task.database_locked.set() with write_lock_db_sqlite(self): @@ -888,7 +1028,7 @@ class Recorder(threading.Thread): self.queue.qsize(), ) - def _process_one_event(self, event): + def _process_one_event(self, event: Event) -> None: if event.event_type == EVENT_TIME_CHANGED: self._keepalive_count += 1 if self._keepalive_count >= KEEPALIVE_TIME: @@ -903,48 +1043,80 @@ class Recorder(threading.Thread): if not self.enabled: return + assert self.event_session is not None try: if event.event_type == EVENT_STATE_CHANGED: - dbevent = Events.from_event(event, event_data="{}") + dbevent = Events.from_event(event, event_data=None) else: dbevent = Events.from_event(event) - dbevent.created = event.time_fired - self.event_session.add(dbevent) except (TypeError, ValueError): _LOGGER.warning("Event is not JSON serializable: %s", event) return + self.event_session.add(dbevent) if event.event_type == EVENT_STATE_CHANGED: try: dbstate = States.from_event(event) - has_new_state = event.data.get("new_state") - if dbstate.entity_id in self._old_states: - old_state = self._old_states.pop(dbstate.entity_id) - if old_state.state_id: - dbstate.old_state_id = old_state.state_id - else: - dbstate.old_state = old_state - if not has_new_state: - dbstate.state = None - dbstate.event = dbevent - dbstate.created = event.time_fired - self.event_session.add(dbstate) - if has_new_state: - self._old_states[dbstate.entity_id] = dbstate - self._pending_expunge.append(dbstate) - except (TypeError, ValueError): - _LOGGER.warning( - "State is not JSON serializable: %s", - event.data.get("new_state"), + shared_attrs = StateAttributes.shared_attrs_from_event( + event, self._exclude_attributes_by_domain ) + except (TypeError, ValueError) as ex: + _LOGGER.warning( + "State is not JSON serializable: %s: %s", + event.data.get("new_state"), + ex, + ) + return + + dbstate.attributes = None + # Matching attributes found in the pending commit + if pending_attributes := self._pending_state_attributes.get(shared_attrs): + dbstate.state_attributes = pending_attributes + # Matching attributes id found in the cache + elif attributes_id := self._state_attributes_ids.get(shared_attrs): + dbstate.attributes_id = attributes_id + else: + attr_hash = StateAttributes.hash_shared_attrs(shared_attrs) + # Matching attributes found in the database + if ( + attributes := self.event_session.query( + StateAttributes.attributes_id + ) + .filter(StateAttributes.hash == attr_hash) + .filter(StateAttributes.shared_attrs == shared_attrs) + .first() + ): + dbstate.attributes_id = attributes[0] + self._state_attributes_ids[shared_attrs] = attributes[0] + # No matching attributes found, save them in the DB + else: + dbstate_attributes = StateAttributes( + shared_attrs=shared_attrs, hash=attr_hash + ) + dbstate.state_attributes = dbstate_attributes + self._pending_state_attributes[shared_attrs] = dbstate_attributes + self.event_session.add(dbstate_attributes) + + if old_state := self._old_states.pop(dbstate.entity_id, None): + if old_state.state_id: + dbstate.old_state_id = old_state.state_id + else: + dbstate.old_state = old_state + if event.data.get("new_state"): + self._old_states[dbstate.entity_id] = dbstate + self._pending_expunge.append(dbstate) + else: + dbstate.state = None + self.event_session.add(dbstate) + dbstate.event = dbevent # If they do not have a commit interval # than we commit right away if not self.commit_interval: self._commit_event_session_or_retry() - def _handle_database_error(self, err): + def _handle_database_error(self, err: Exception) -> bool: """Handle a database error that may result in moving away the corrupt db.""" if isinstance(err.__cause__, sqlite3.DatabaseError): _LOGGER.exception( @@ -954,9 +1126,11 @@ class Recorder(threading.Thread): return True return False - def _commit_event_session_or_retry(self): + def _commit_event_session_or_retry(self) -> None: """Commit the event session if there is work to do.""" - if not self.event_session.new and not self.event_session.dirty: + if not self.event_session or ( + not self.event_session.new and not self.event_session.dirty + ): return tries = 1 while tries <= self.db_max_retries: @@ -976,7 +1150,8 @@ class Recorder(threading.Thread): tries += 1 time.sleep(self.db_retry_wait) - def _commit_event_session(self): + def _commit_event_session(self) -> None: + assert self.event_session is not None self._commits_without_expire += 1 if self._pending_expunge: @@ -989,6 +1164,16 @@ class Recorder(threading.Thread): self._pending_expunge = [] self.event_session.commit() + # We just committed the state attributes to the database + # and we now know the attributes_ids. We can save + # many selects for matching attributes by loading them + # into the LRU cache now. + for state_attr in self._pending_state_attributes.values(): + self._state_attributes_ids[ + state_attr.shared_attrs + ] = state_attr.attributes_id + self._pending_state_attributes = {} + # Expire is an expensive operation (frequently more expensive # than the flush and commit itself) so we only # do it after EXPIRE_AFTER_COMMITS commits @@ -996,7 +1181,7 @@ class Recorder(threading.Thread): self._commits_without_expire = 0 self.event_session.expire_all() - def _handle_sqlite_corruption(self): + def _handle_sqlite_corruption(self) -> None: """Handle the sqlite3 database being corrupt.""" self._close_event_session() self._close_connection() @@ -1004,9 +1189,11 @@ class Recorder(threading.Thread): self._setup_recorder() self._setup_run() - def _close_event_session(self): + def _close_event_session(self) -> None: """Close the event session.""" self._old_states = {} + self._state_attributes_ids = {} + self._pending_state_attributes = {} if not self.event_session: return @@ -1019,27 +1206,29 @@ class Recorder(threading.Thread): "Error while rolling back and closing the event session: %s", err ) - def _reopen_event_session(self): + def _reopen_event_session(self) -> None: """Rollback the event session and reopen it after a failure.""" self._close_event_session() self._open_event_session() - def _open_event_session(self): + def _open_event_session(self) -> None: """Open the event session.""" + assert self.get_session is not None self.event_session = self.get_session() self.event_session.expire_on_commit = False - def _send_keep_alive(self): + def _send_keep_alive(self) -> None: """Send a keep alive to keep the db connection open.""" + assert self.event_session is not None _LOGGER.debug("Sending keepalive") self.event_session.connection().scalar(select([1])) @callback - def event_listener(self, event): + def event_listener(self, event: Event) -> None: """Listen for new events and put them in the process queue.""" self.queue.put(EventTask(event)) - def block_till_done(self): + def block_till_done(self) -> None: """Block till all events processed. This is only called in tests. @@ -1103,13 +1292,16 @@ class Recorder(threading.Thread): return success - def _setup_connection(self): + def _setup_connection(self) -> None: """Ensure database is ready to fly.""" - kwargs = {} + kwargs: dict[str, Any] = {} self._completed_first_database_setup = False - def setup_recorder_connection(dbapi_connection, connection_record): + def setup_recorder_connection( + dbapi_connection: Any, connection_record: Any + ) -> None: """Dbapi specific connection settings.""" + assert self.engine is not None setup_connection_for_dialect( self, self.engine.dialect.name, @@ -1139,20 +1331,22 @@ class Recorder(threading.Thread): _LOGGER.debug("Connected to recorder database") @property - def _using_file_sqlite(self): + def _using_file_sqlite(self) -> bool: """Short version to check if we are using sqlite3 as a file.""" return self.db_url != SQLITE_URL_PREFIX and self.db_url.startswith( SQLITE_URL_PREFIX ) - def _close_connection(self): + def _close_connection(self) -> None: """Close the connection.""" + assert self.engine is not None self.engine.dispose() self.engine = None self.get_session = None - def _setup_run(self): + def _setup_run(self) -> None: """Log the start of the current run and schedule any needed jobs.""" + assert self.get_session is not None with session_scope(session=self.get_session()) as session: start = self.recording_start end_incomplete_runs(session, start) @@ -1183,10 +1377,11 @@ class Recorder(threading.Thread): self.queue.put(StatisticsTask(start)) start = end - def _end_session(self): + def _end_session(self) -> None: """End the recorder session.""" if self.event_session is None: return + assert self.run_info is not None try: self.run_info.end = dt_util.utcnow() self.event_session.add(self.run_info) @@ -1197,13 +1392,14 @@ class Recorder(threading.Thread): self.run_info = None - def _shutdown(self): + def _shutdown(self) -> None: """Save end time for current run.""" self.hass.add_job(self._async_stop_queue_watcher_and_event_listener) + self._stop_executor() self._end_session() self._close_connection() @property - def recording(self): + def recording(self) -> bool: """Return if the recorder is recording.""" return self._event_listener is not None diff --git a/homeassistant/components/recorder/backup.py b/homeassistant/components/recorder/backup.py new file mode 100644 index 00000000000..cec9f85748b --- /dev/null +++ b/homeassistant/components/recorder/backup.py @@ -0,0 +1,28 @@ +"""Backup platform for the Recorder integration.""" +from logging import getLogger + +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError + +from . import Recorder +from .const import DATA_INSTANCE +from .util import async_migration_in_progress + +_LOGGER = getLogger(__name__) + + +async def async_pre_backup(hass: HomeAssistant) -> None: + """Perform operations before a backup starts.""" + _LOGGER.info("Backup start notification, locking database for writes") + instance: Recorder = hass.data[DATA_INSTANCE] + if async_migration_in_progress(hass): + raise HomeAssistantError("Database migration in progress") + await instance.lock_database() + + +async def async_post_backup(hass: HomeAssistant) -> None: + """Perform operations after a backup finishes.""" + instance: Recorder = hass.data[DATA_INSTANCE] + _LOGGER.info("Backup end notification, releasing write lock") + if not instance.unlock_database(): + raise HomeAssistantError("Could not release database write lock") diff --git a/homeassistant/components/recorder/const.py b/homeassistant/components/recorder/const.py index a04218264ee..0fce9657624 100644 --- a/homeassistant/components/recorder/const.py +++ b/homeassistant/components/recorder/const.py @@ -1,5 +1,12 @@ """Recorder constants.""" +from functools import partial +import json +from typing import Final + +from homeassistant.const import ATTR_ATTRIBUTION, ATTR_SUPPORTED_FEATURES +from homeassistant.helpers.json import JSONEncoder + DATA_INSTANCE = "recorder_instance" SQLITE_URL_PREFIX = "sqlite://" DOMAIN = "recorder" @@ -15,3 +22,9 @@ MAX_QUEUE_BACKLOG = 30000 # We can increase this back to 1000 once most # have upgraded their sqlite version MAX_ROWS_TO_PURGE = 998 + +DB_WORKER_PREFIX = "DbWorker" + +JSON_DUMP: Final = partial(json.dumps, cls=JSONEncoder, separators=(",", ":")) + +ALL_DOMAIN_EXCLUDE_ATTRS = {ATTR_ATTRIBUTION, ATTR_SUPPORTED_FEATURES} diff --git a/homeassistant/components/recorder/executor.py b/homeassistant/components/recorder/executor.py new file mode 100644 index 00000000000..0d913310e74 --- /dev/null +++ b/homeassistant/components/recorder/executor.py @@ -0,0 +1,61 @@ +"""Database executor helpers.""" +from __future__ import annotations + +from collections.abc import Callable +from concurrent.futures.thread import _threads_queues, _worker +import threading +from typing import Any +import weakref + +from homeassistant.util.executor import InterruptibleThreadPoolExecutor + + +def _worker_with_shutdown_hook( + shutdown_hook: Callable[[], None], *args: Any, **kwargs: Any +) -> None: + """Create a worker that calls a function after its finished.""" + _worker(*args, **kwargs) + shutdown_hook() + + +class DBInterruptibleThreadPoolExecutor(InterruptibleThreadPoolExecutor): + """A database instance that will not deadlock on shutdown.""" + + def __init__(self, *args: Any, **kwargs: Any) -> None: + """Init the executor with a shutdown hook support.""" + self._shutdown_hook: Callable[[], None] = kwargs.pop("shutdown_hook") + super().__init__(*args, **kwargs) + + def _adjust_thread_count(self) -> None: + """Overridden to add support for shutdown hook. + + Based on the CPython 3.10 implementation. + """ + # if idle threads are available, don't spin new threads + if self._idle_semaphore.acquire( # pylint: disable=consider-using-with + timeout=0 + ): + return + + # When the executor gets lost, the weakref callback will wake up + # the worker threads. + def weakref_cb(_: Any, q=self._work_queue) -> None: # type: ignore[no-untyped-def] # pylint: disable=invalid-name + q.put(None) + + num_threads = len(self._threads) + if num_threads < self._max_workers: + thread_name = "%s_%d" % (self._thread_name_prefix or self, num_threads) + executor_thread = threading.Thread( + name=thread_name, + target=_worker_with_shutdown_hook, + args=( + self._shutdown_hook, + weakref.ref(self, weakref_cb), + self._work_queue, + self._initializer, + self._initargs, + ), + ) + executor_thread.start() + self._threads.add(executor_thread) # type: ignore[attr-defined] + _threads_queues[executor_thread] = self._work_queue # type: ignore[index] diff --git a/homeassistant/components/recorder/history.py b/homeassistant/components/recorder/history.py index 8be60a60ac6..82a74c36a83 100644 --- a/homeassistant/components/recorder/history.py +++ b/homeassistant/components/recorder/history.py @@ -2,18 +2,29 @@ from __future__ import annotations from collections import defaultdict +from collections.abc import Iterable, MutableMapping +from datetime import datetime from itertools import groupby import logging import time +from typing import Any -from sqlalchemy import and_, bindparam, func +from sqlalchemy import Column, Text, and_, bindparam, func, or_ from sqlalchemy.ext import baked +from sqlalchemy.orm.session import Session +from sqlalchemy.sql.expression import literal from homeassistant.components import recorder -from homeassistant.core import split_entity_id +from homeassistant.core import HomeAssistant, State, split_entity_id import homeassistant.util.dt as dt_util -from .models import LazyState, States, process_timestamp_to_utc_isoformat +from .models import ( + LazyState, + RecorderRuns, + StateAttributes, + States, + process_timestamp_to_utc_isoformat, +) from .util import execute, session_scope # mypy: allow-untyped-defs, no-check-untyped-defs @@ -23,14 +34,16 @@ _LOGGER = logging.getLogger(__name__) STATE_KEY = "state" LAST_CHANGED_KEY = "last_changed" -SIGNIFICANT_DOMAINS = ( +SIGNIFICANT_DOMAINS = { "climate", "device_tracker", "humidifier", "thermostat", "water_heater", -) -IGNORE_DOMAINS = ("zone", "scene") +} +SIGNIFICANT_DOMAINS_ENTITY_ID_LIKE = [f"{domain}.%" for domain in SIGNIFICANT_DOMAINS] +IGNORE_DOMAINS = {"zone", "scene"} +IGNORE_DOMAINS_ENTITY_ID_LIKE = [f"{domain}.%" for domain in IGNORE_DOMAINS] NEED_ATTRIBUTE_DOMAINS = { "climate", "humidifier", @@ -39,40 +52,124 @@ NEED_ATTRIBUTE_DOMAINS = { "water_heater", } -QUERY_STATES = [ - States.domain, +BASE_STATES = [ States.entity_id, States.state, - States.attributes, States.last_changed, States.last_updated, ] +QUERY_STATE_NO_ATTR = [ + *BASE_STATES, + literal(value=None, type_=Text).label("attributes"), + literal(value=None, type_=Text).label("shared_attrs"), +] +# Remove QUERY_STATES_PRE_SCHEMA_25 +# and the migration_in_progress check +# once schema 26 is created +QUERY_STATES_PRE_SCHEMA_25 = [ + *BASE_STATES, + States.attributes, + literal(value=None, type_=Text).label("shared_attrs"), +] +QUERY_STATES = [ + *BASE_STATES, + # Remove States.attributes once all attributes are in StateAttributes.shared_attrs + States.attributes, + StateAttributes.shared_attrs, +] HISTORY_BAKERY = "recorder_history_bakery" -def async_setup(hass): +def query_and_join_attributes( + hass: HomeAssistant, no_attributes: bool +) -> tuple[list[Column], bool]: + """Return the query keys and if StateAttributes should be joined.""" + # If no_attributes was requested we do the query + # without the attributes fields and do not join the + # state_attributes table + if no_attributes: + return QUERY_STATE_NO_ATTR, False + # If we in the process of migrating schema we do + # not want to join the state_attributes table as we + # do not know if it will be there yet + if recorder.get_instance(hass).migration_in_progress: + return QUERY_STATES_PRE_SCHEMA_25, False + # Finally if no migration is in progress and no_attributes + # was not requested, we query both attributes columns and + # join state_attributes + return QUERY_STATES, True + + +def bake_query_and_join_attributes( + hass: HomeAssistant, no_attributes: bool +) -> tuple[Any, bool]: + """Return the initial backed query and if StateAttributes should be joined. + + Because these are baked queries the values inside the lambdas need + to be explicitly written out to avoid caching the wrong values. + """ + bakery: baked.bakery = hass.data[HISTORY_BAKERY] + # If no_attributes was requested we do the query + # without the attributes fields and do not join the + # state_attributes table + if no_attributes: + return bakery(lambda session: session.query(*QUERY_STATE_NO_ATTR)), False + # If we in the process of migrating schema we do + # not want to join the state_attributes table as we + # do not know if it will be there yet + if recorder.get_instance(hass).migration_in_progress: + return bakery(lambda session: session.query(*QUERY_STATES_PRE_SCHEMA_25)), False + # Finally if no migration is in progress and no_attributes + # was not requested, we query both attributes columns and + # join state_attributes + return bakery(lambda session: session.query(*QUERY_STATES)), True + + +def async_setup(hass: HomeAssistant) -> None: """Set up the history hooks.""" hass.data[HISTORY_BAKERY] = baked.bakery() -def get_significant_states(hass, *args, **kwargs): +def get_significant_states( + hass: HomeAssistant, + start_time: datetime, + end_time: datetime | None = None, + entity_ids: list[str] | None = None, + filters: Any | None = None, + include_start_time_state: bool = True, + significant_changes_only: bool = True, + minimal_response: bool = False, + no_attributes: bool = False, +) -> MutableMapping[str, Iterable[LazyState | State | dict[str, Any]]]: """Wrap get_significant_states_with_session with an sql session.""" with session_scope(hass=hass) as session: - return get_significant_states_with_session(hass, session, *args, **kwargs) + return get_significant_states_with_session( + hass, + session, + start_time, + end_time, + entity_ids, + filters, + include_start_time_state, + significant_changes_only, + minimal_response, + no_attributes, + ) def get_significant_states_with_session( - hass, - session, - start_time, - end_time=None, - entity_ids=None, - filters=None, - include_start_time_state=True, - significant_changes_only=True, - minimal_response=False, -): + hass: HomeAssistant, + session: Session, + start_time: datetime, + end_time: datetime | None = None, + entity_ids: list[str] | None = None, + filters: Any = None, + include_start_time_state: bool = True, + significant_changes_only: bool = True, + minimal_response: bool = False, + no_attributes: bool = False, +) -> MutableMapping[str, Iterable[LazyState | State | dict[str, Any]]]: """ Return states changes during UTC period start_time - end_time. @@ -86,34 +183,51 @@ def get_significant_states_with_session( thermostat so that we get current temperature in our graphs). """ timer_start = time.perf_counter() + baked_query, join_attributes = bake_query_and_join_attributes(hass, no_attributes) - baked_query = hass.data[HISTORY_BAKERY]( - lambda session: session.query(*QUERY_STATES) - ) - - if significant_changes_only: - baked_query += lambda q: q.filter( - ( - States.domain.in_(SIGNIFICANT_DOMAINS) - | (States.last_changed == States.last_updated) + if entity_ids is not None and len(entity_ids) == 1: + if ( + significant_changes_only + and split_entity_id(entity_ids[0])[0] not in SIGNIFICANT_DOMAINS + ): + baked_query += lambda q: q.filter( + States.last_changed == States.last_updated + ) + elif significant_changes_only: + baked_query += lambda q: q.filter( + or_( + *[ + States.entity_id.like(entity_domain) + for entity_domain in SIGNIFICANT_DOMAINS_ENTITY_ID_LIKE + ], + (States.last_changed == States.last_updated), ) - & (States.last_updated > bindparam("start_time")) ) - else: - baked_query += lambda q: q.filter(States.last_updated > bindparam("start_time")) if entity_ids is not None: baked_query += lambda q: q.filter( States.entity_id.in_(bindparam("entity_ids", expanding=True)) ) else: - baked_query += lambda q: q.filter(~States.domain.in_(IGNORE_DOMAINS)) + baked_query += lambda q: q.filter( + and_( + *[ + ~States.entity_id.like(entity_domain) + for entity_domain in IGNORE_DOMAINS_ENTITY_ID_LIKE + ] + ) + ) if filters: filters.bake(baked_query) + baked_query += lambda q: q.filter(States.last_updated > bindparam("start_time")) if end_time is not None: baked_query += lambda q: q.filter(States.last_updated < bindparam("end_time")) + if join_attributes: + baked_query += lambda q: q.outerjoin( + StateAttributes, States.attributes_id == StateAttributes.attributes_id + ) baked_query += lambda q: q.order_by(States.entity_id, States.last_updated) states = execute( @@ -135,14 +249,24 @@ def get_significant_states_with_session( filters, include_start_time_state, minimal_response, + no_attributes, ) -def state_changes_during_period(hass, start_time, end_time=None, entity_id=None): +def state_changes_during_period( + hass: HomeAssistant, + start_time: datetime, + end_time: datetime | None = None, + entity_id: str | None = None, + no_attributes: bool = False, + descending: bool = False, + limit: int | None = None, + include_start_time_state: bool = True, +) -> MutableMapping[str, Iterable[LazyState | State | dict[str, Any]]]: """Return states changes during UTC period start_time - end_time.""" with session_scope(hass=hass) as session: - baked_query = hass.data[HISTORY_BAKERY]( - lambda session: session.query(*QUERY_STATES) + baked_query, join_attributes = bake_query_and_join_attributes( + hass, no_attributes ) baked_query += lambda q: q.filter( @@ -159,7 +283,16 @@ def state_changes_during_period(hass, start_time, end_time=None, entity_id=None) baked_query += lambda q: q.filter_by(entity_id=bindparam("entity_id")) entity_id = entity_id.lower() - baked_query += lambda q: q.order_by(States.entity_id, States.last_updated) + if join_attributes: + baked_query += lambda q: q.outerjoin( + StateAttributes, States.attributes_id == StateAttributes.attributes_id + ) + + last_updated = States.last_updated.desc() if descending else States.last_updated + baked_query += lambda q: q.order_by(States.entity_id, last_updated) + + if limit: + baked_query += lambda q: q.limit(limit) states = execute( baked_query(session).params( @@ -169,23 +302,35 @@ def state_changes_during_period(hass, start_time, end_time=None, entity_id=None) entity_ids = [entity_id] if entity_id is not None else None - return _sorted_states_to_dict(hass, session, states, start_time, entity_ids) + return _sorted_states_to_dict( + hass, + session, + states, + start_time, + entity_ids, + include_start_time_state=include_start_time_state, + ) -def get_last_state_changes(hass, number_of_states, entity_id): +def get_last_state_changes( + hass: HomeAssistant, number_of_states: int, entity_id: str +) -> MutableMapping[str, Iterable[LazyState | State | dict[str, Any]]]: """Return the last number_of_states.""" start_time = dt_util.utcnow() with session_scope(hass=hass) as session: - baked_query = hass.data[HISTORY_BAKERY]( - lambda session: session.query(*QUERY_STATES) - ) + baked_query, join_attributes = bake_query_and_join_attributes(hass, False) + baked_query += lambda q: q.filter(States.last_changed == States.last_updated) if entity_id is not None: baked_query += lambda q: q.filter_by(entity_id=bindparam("entity_id")) entity_id = entity_id.lower() + if join_attributes: + baked_query += lambda q: q.outerjoin( + StateAttributes, States.attributes_id == StateAttributes.attributes_id + ) baked_query += lambda q: q.order_by( States.entity_id, States.last_updated.desc() ) @@ -210,40 +355,56 @@ def get_last_state_changes(hass, number_of_states, entity_id): ) -def get_states(hass, utc_point_in_time, entity_ids=None, run=None, filters=None): +def get_states( + hass: HomeAssistant, + utc_point_in_time: datetime, + entity_ids: list[str] | None = None, + run: RecorderRuns | None = None, + filters: Any = None, + no_attributes: bool = False, +) -> list[LazyState]: """Return the states at a specific point in time.""" - if run is None: - run = recorder.run_information_from_instance(hass, utc_point_in_time) - + if ( + run is None + and (run := (recorder.run_information_from_instance(hass, utc_point_in_time))) + is None + ): # History did not run before utc_point_in_time - if run is None: - return [] + return [] with session_scope(hass=hass) as session: return _get_states_with_session( - hass, session, utc_point_in_time, entity_ids, run, filters + hass, session, utc_point_in_time, entity_ids, run, filters, no_attributes ) def _get_states_with_session( - hass, session, utc_point_in_time, entity_ids=None, run=None, filters=None -): + hass: HomeAssistant, + session: Session, + utc_point_in_time: datetime, + entity_ids: list[str] | None = None, + run: RecorderRuns | None = None, + filters: Any | None = None, + no_attributes: bool = False, +) -> list[LazyState]: """Return the states at a specific point in time.""" if entity_ids and len(entity_ids) == 1: return _get_single_entity_states_with_session( - hass, session, utc_point_in_time, entity_ids[0] + hass, session, utc_point_in_time, entity_ids[0], no_attributes ) - if run is None: - run = recorder.run_information_with_session(session, utc_point_in_time) - + if ( + run is None + and (run := (recorder.run_information_from_instance(hass, utc_point_in_time))) + is None + ): # History did not run before utc_point_in_time - if run is None: - return [] + return [] # We have more than one entity to look at so we need to do a query on states # since the last recorder run started. - query = session.query(*QUERY_STATES) + query_keys, join_attributes = query_and_join_attributes(hass, no_attributes) + query = session.query(*query_keys) if entity_ids: # We got an include-list of entities, accelerate the query by filtering already @@ -264,6 +425,10 @@ def _get_states_with_session( most_recent_state_ids, States.state_id == most_recent_state_ids.c.max_state_id, ) + if join_attributes: + query = query.outerjoin( + StateAttributes, (States.attributes_id == StateAttributes.attributes_id) + ) else: # We did not get an include-list of entities, query all states in the inner # query, then filter out unwanted domains as well as applying the custom filter. @@ -298,23 +463,37 @@ def _get_states_with_session( most_recent_state_ids, States.state_id == most_recent_state_ids.c.max_state_id, ) - query = query.filter(~States.domain.in_(IGNORE_DOMAINS)) + for entity_domain in IGNORE_DOMAINS_ENTITY_ID_LIKE: + query = query.filter(~States.entity_id.like(entity_domain)) if filters: query = filters.apply(query) + if join_attributes: + query = query.outerjoin( + StateAttributes, (States.attributes_id == StateAttributes.attributes_id) + ) - return [LazyState(row) for row in execute(query)] + attr_cache: dict[str, dict[str, Any]] = {} + return [LazyState(row, attr_cache) for row in execute(query)] -def _get_single_entity_states_with_session(hass, session, utc_point_in_time, entity_id): +def _get_single_entity_states_with_session( + hass: HomeAssistant, + session: Session, + utc_point_in_time: datetime, + entity_id: str, + no_attributes: bool = False, +) -> list[LazyState]: # Use an entirely different (and extremely fast) query if we only # have a single entity id - baked_query = hass.data[HISTORY_BAKERY]( - lambda session: session.query(*QUERY_STATES) - ) + baked_query, join_attributes = bake_query_and_join_attributes(hass, no_attributes) baked_query += lambda q: q.filter( States.last_updated < bindparam("utc_point_in_time"), States.entity_id == bindparam("entity_id"), ) + if join_attributes: + baked_query += lambda q: q.outerjoin( + StateAttributes, States.attributes_id == StateAttributes.attributes_id + ) baked_query += lambda q: q.order_by(States.last_updated.desc()) baked_query += lambda q: q.limit(1) @@ -326,15 +505,16 @@ def _get_single_entity_states_with_session(hass, session, utc_point_in_time, ent def _sorted_states_to_dict( - hass, - session, - states, - start_time, - entity_ids, - filters=None, - include_start_time_state=True, - minimal_response=False, -): + hass: HomeAssistant, + session: Session, + states: Iterable[States], + start_time: datetime, + entity_ids: list[str] | None, + filters: Any = None, + include_start_time_state: bool = True, + minimal_response: bool = False, + no_attributes: bool = False, +) -> MutableMapping[str, Iterable[LazyState | State | dict[str, Any]]]: """Convert SQL results into JSON friendly data structure. This takes our state list and turns it into a JSON friendly data @@ -346,7 +526,7 @@ def _sorted_states_to_dict( each list of states, otherwise our graphs won't start on the Y axis correctly. """ - result = defaultdict(list) + result: dict[str, list[LazyState | dict[str, Any]]] = defaultdict(list) # Set all entity IDs to empty lists in result set to maintain the order if entity_ids is not None: for ent_id in entity_ids: @@ -357,7 +537,13 @@ def _sorted_states_to_dict( if include_start_time_state: run = recorder.run_information_from_instance(hass, start_time) for state in _get_states_with_session( - hass, session, start_time, entity_ids, run=run, filters=filters + hass, + session, + start_time, + entity_ids, + run=run, + filters=filters, + no_attributes=no_attributes, ): state.last_changed = start_time state.last_updated = start_time @@ -372,20 +558,23 @@ def _sorted_states_to_dict( _process_timestamp_to_utc_isoformat = process_timestamp_to_utc_isoformat # Append all changes to it - for ent_id, group in groupby(states, lambda state: state.entity_id): + for ent_id, group in groupby(states, lambda state: state.entity_id): # type: ignore[no-any-return] domain = split_entity_id(ent_id)[0] ent_results = result[ent_id] + attr_cache: dict[str, dict[str, Any]] = {} + if not minimal_response or domain in NEED_ATTRIBUTE_DOMAINS: - ent_results.extend(LazyState(db_state) for db_state in group) + ent_results.extend(LazyState(db_state, attr_cache) for db_state in group) # With minimal response we only provide a native # State for the first and last response. All the states # in-between only provide the "state" and the # "last_changed". if not ent_results: - ent_results.append(LazyState(next(group))) + ent_results.append(LazyState(next(group), attr_cache)) prev_state = ent_results[-1] + assert isinstance(prev_state, LazyState) initial_state_count = len(ent_results) for db_state in group: @@ -408,13 +597,19 @@ def _sorted_states_to_dict( # There was at least one state change # replace the last minimal state with # a full state - ent_results[-1] = LazyState(prev_state) + ent_results[-1] = LazyState(prev_state, attr_cache) # Filter out the empty lists if some states had 0 results. return {key: val for key, val in result.items() if val} -def get_state(hass, utc_point_in_time, entity_id, run=None): +def get_state( + hass: HomeAssistant, + utc_point_in_time: datetime, + entity_id: str, + run: RecorderRuns | None = None, + no_attributes: bool = False, +) -> LazyState | None: """Return a state at a specific point in time.""" - states = get_states(hass, utc_point_in_time, (entity_id,), run) + states = get_states(hass, utc_point_in_time, [entity_id], run, None, no_attributes) return states[0] if states else None diff --git a/homeassistant/components/recorder/manifest.json b/homeassistant/components/recorder/manifest.json index a6a746f019e..62f740da174 100644 --- a/homeassistant/components/recorder/manifest.json +++ b/homeassistant/components/recorder/manifest.json @@ -2,7 +2,7 @@ "domain": "recorder", "name": "Recorder", "documentation": "https://www.home-assistant.io/integrations/recorder", - "requirements": ["sqlalchemy==1.4.27"], + "requirements": ["sqlalchemy==1.4.32", "fnvhash==0.1.0", "lru-dict==1.1.7"], "codeowners": ["@home-assistant/core"], "quality_scale": "internal", "iot_class": "local_push" diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index 48dca4d42ed..26234be0502 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -2,6 +2,7 @@ import contextlib from datetime import timedelta import logging +from typing import Any import sqlalchemy from sqlalchemy import ForeignKeyConstraint, MetaData, Table, func, text @@ -43,8 +44,9 @@ def raise_if_exception_missing_str(ex, match_substrs): raise ex -def get_schema_version(instance): +def get_schema_version(instance: Any) -> int: """Get the schema version.""" + assert instance.get_session is not None with session_scope(session=instance.get_session()) as session: res = ( session.query(SchemaChanges) @@ -62,13 +64,14 @@ def get_schema_version(instance): return current_version -def schema_is_current(current_version): +def schema_is_current(current_version: int) -> bool: """Check if the schema is current.""" return current_version == SCHEMA_VERSION -def migrate_schema(instance, current_version): +def migrate_schema(instance: Any, current_version: int) -> None: """Check if the schema needs to be upgraded.""" + assert instance.get_session is not None _LOGGER.warning("Database is about to upgrade. Schema version: %s", current_version) for version in range(current_version, SCHEMA_VERSION): new_version = version + 1 @@ -376,6 +379,7 @@ def _drop_foreign_key_constraints(instance, engine, table, columns): def _apply_update(instance, new_version, old_version): # noqa: C901 """Perform operations to bring schema up to date.""" engine = instance.engine + dialect = engine.dialect.name if new_version == 1: _create_index(instance, "events", "ix_events_time_fired") elif new_version == 2: @@ -638,6 +642,10 @@ def _apply_update(instance, new_version, old_version): # noqa: C901 "statistics_short_term", "ix_statistics_short_term_statistic_id_start", ) + elif new_version == 25: + big_int = "INTEGER(20)" if dialect == "mysql" else "INTEGER" + _add_columns(instance, "states", [f"attributes_id {big_int}"]) + _create_index(instance, "states", "ix_states_attributes_id") else: raise ValueError(f"No schema migration defined for version {new_version}") diff --git a/homeassistant/components/recorder/models.py b/homeassistant/components/recorder/models.py index 579f47ed4a7..384bf4b22e2 100644 --- a/homeassistant/components/recorder/models.py +++ b/homeassistant/components/recorder/models.py @@ -4,9 +4,11 @@ from __future__ import annotations from datetime import datetime, timedelta import json import logging -from typing import TypedDict, overload +from typing import Any, TypedDict, cast, overload +from fnvhash import fnv1a_32 from sqlalchemy import ( + BigInteger, Boolean, Column, DateTime, @@ -20,6 +22,7 @@ from sqlalchemy import ( distinct, ) from sqlalchemy.dialects import mysql, oracle, postgresql +from sqlalchemy.engine.row import Row from sqlalchemy.ext.declarative import declared_attr from sqlalchemy.orm import declarative_base, relationship from sqlalchemy.orm.session import Session @@ -28,19 +31,20 @@ from homeassistant.const import ( MAX_LENGTH_EVENT_CONTEXT_ID, MAX_LENGTH_EVENT_EVENT_TYPE, MAX_LENGTH_EVENT_ORIGIN, - MAX_LENGTH_STATE_DOMAIN, MAX_LENGTH_STATE_ENTITY_ID, MAX_LENGTH_STATE_STATE, ) from homeassistant.core import Context, Event, EventOrigin, State, split_entity_id -from homeassistant.helpers.json import JSONEncoder +from homeassistant.helpers.typing import UNDEFINED, UndefinedType import homeassistant.util.dt as dt_util +from .const import ALL_DOMAIN_EXCLUDE_ATTRS, JSON_DUMP + # SQLAlchemy Schema # pylint: disable=invalid-name Base = declarative_base() -SCHEMA_VERSION = 24 +SCHEMA_VERSION = 25 _LOGGER = logging.getLogger(__name__) @@ -48,6 +52,7 @@ DB_TIMEZONE = "+00:00" TABLE_EVENTS = "events" TABLE_STATES = "states" +TABLE_STATE_ATTRIBUTES = "state_attributes" TABLE_RECORDER_RUNS = "recorder_runs" TABLE_SCHEMA_CHANGES = "schema_changes" TABLE_STATISTICS = "statistics" @@ -66,6 +71,9 @@ ALL_TABLES = [ TABLE_STATISTICS_SHORT_TERM, ] +EMPTY_JSON_OBJECT = "{}" + + DATETIME_TYPE = DateTime(timezone=True).with_variant( mysql.DATETIME(timezone=True, fsp=6), "mysql" ) @@ -92,7 +100,6 @@ class Events(Base): # type: ignore[misc,valid-type] event_data = Column(Text().with_variant(mysql.LONGTEXT, "mysql")) origin = Column(String(MAX_LENGTH_EVENT_ORIGIN)) time_fired = Column(DATETIME_TYPE, index=True) - created = Column(DATETIME_TYPE, default=dt_util.utcnow) context_id = Column(String(MAX_LENGTH_EVENT_CONTEXT_ID), index=True) context_user_id = Column(String(MAX_LENGTH_EVENT_CONTEXT_ID), index=True) context_parent_id = Column(String(MAX_LENGTH_EVENT_CONTEXT_ID), index=True) @@ -107,12 +114,13 @@ class Events(Base): # type: ignore[misc,valid-type] ) @staticmethod - def from_event(event, event_data=None): + def from_event( + event: Event, event_data: UndefinedType | None = UNDEFINED + ) -> Events: """Create an event database object from a native event.""" return Events( event_type=event.event_type, - event_data=event_data - or json.dumps(event.data, cls=JSONEncoder, separators=(",", ":")), + event_data=JSON_DUMP(event.data) if event_data is UNDEFINED else event_data, origin=str(event.origin.value), time_fired=event.time_fired, context_id=event.context.id, @@ -120,7 +128,7 @@ class Events(Base): # type: ignore[misc,valid-type] context_parent_id=event.context.parent_id, ) - def to_native(self, validate_entity_id=True): + def to_native(self, validate_entity_id: bool = True) -> Event | None: """Convert to a native HA Event.""" context = Context( id=self.context_id, @@ -152,7 +160,6 @@ class States(Base): # type: ignore[misc,valid-type] ) __tablename__ = TABLE_STATES state_id = Column(Integer, Identity(), primary_key=True) - domain = Column(String(MAX_LENGTH_STATE_DOMAIN)) entity_id = Column(String(MAX_LENGTH_STATE_ENTITY_ID)) state = Column(String(MAX_LENGTH_STATE_STATE)) attributes = Column(Text().with_variant(mysql.LONGTEXT, "mysql")) @@ -161,60 +168,58 @@ class States(Base): # type: ignore[misc,valid-type] ) last_changed = Column(DATETIME_TYPE, default=dt_util.utcnow) last_updated = Column(DATETIME_TYPE, default=dt_util.utcnow, index=True) - created = Column(DATETIME_TYPE, default=dt_util.utcnow) old_state_id = Column(Integer, ForeignKey("states.state_id"), index=True) + attributes_id = Column( + Integer, ForeignKey("state_attributes.attributes_id"), index=True + ) event = relationship("Events", uselist=False) old_state = relationship("States", remote_side=[state_id]) + state_attributes = relationship("StateAttributes") def __repr__(self) -> str: """Return string representation of instance for debugging.""" return ( f"" ) @staticmethod - def from_event(event): + def from_event(event: Event) -> States: """Create object from a state_changed event.""" entity_id = event.data["entity_id"] - state = event.data.get("new_state") + state: State | None = event.data.get("new_state") + dbstate = States(entity_id=entity_id, attributes=None) - dbstate = States(entity_id=entity_id) - - # State got deleted + # None state means the state was removed from the state machine if state is None: dbstate.state = "" - dbstate.domain = split_entity_id(entity_id)[0] - dbstate.attributes = "{}" dbstate.last_changed = event.time_fired dbstate.last_updated = event.time_fired else: - dbstate.domain = state.domain dbstate.state = state.state - dbstate.attributes = json.dumps( - dict(state.attributes), cls=JSONEncoder, separators=(",", ":") - ) dbstate.last_changed = state.last_changed dbstate.last_updated = state.last_updated return dbstate - def to_native(self, validate_entity_id=True): + def to_native(self, validate_entity_id: bool = True) -> State | None: """Convert to an HA state object.""" try: return State( self.entity_id, self.state, - json.loads(self.attributes), + # Join the state_attributes table on attributes_id to get the attributes + # for newer states + json.loads(self.attributes) if self.attributes else {}, process_timestamp(self.last_changed), process_timestamp(self.last_updated), # Join the events table on event_id to get the context instead # as it will always be there for state_changed events - context=Context(id=None), + context=Context(id=None), # type: ignore[arg-type] validate_entity_id=validate_entity_id, ) except ValueError: @@ -223,6 +228,69 @@ class States(Base): # type: ignore[misc,valid-type] return None +class StateAttributes(Base): # type: ignore[misc,valid-type] + """State attribute change history.""" + + __table_args__ = ( + {"mysql_default_charset": "utf8mb4", "mysql_collate": "utf8mb4_unicode_ci"}, + ) + __tablename__ = TABLE_STATE_ATTRIBUTES + attributes_id = Column(Integer, Identity(), primary_key=True) + hash = Column(BigInteger, index=True) + # Note that this is not named attributes to avoid confusion with the states table + shared_attrs = Column(Text().with_variant(mysql.LONGTEXT, "mysql")) + + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + return ( + f"" + ) + + @staticmethod + def from_event(event: Event) -> StateAttributes: + """Create object from a state_changed event.""" + state: State | None = event.data.get("new_state") + # None state means the state was removed from the state machine + dbstate = StateAttributes( + shared_attrs="{}" if state is None else JSON_DUMP(state.attributes) + ) + dbstate.hash = StateAttributes.hash_shared_attrs(dbstate.shared_attrs) + return dbstate + + @staticmethod + def shared_attrs_from_event( + event: Event, exclude_attrs_by_domain: dict[str, set[str]] + ) -> str: + """Create shared_attrs from a state_changed event.""" + state: State | None = event.data.get("new_state") + # None state means the state was removed from the state machine + if state is None: + return "{}" + domain = split_entity_id(state.entity_id)[0] + exclude_attrs = ( + exclude_attrs_by_domain.get(domain, set()) | ALL_DOMAIN_EXCLUDE_ATTRS + ) + return JSON_DUMP( + {k: v for k, v in state.attributes.items() if k not in exclude_attrs} + ) + + @staticmethod + def hash_shared_attrs(shared_attrs: str) -> int: + """Return the hash of json encoded shared attributes.""" + return cast(int, fnv1a_32(shared_attrs.encode("utf-8"))) + + def to_native(self) -> dict[str, Any]: + """Convert to an HA state object.""" + try: + return cast(dict[str, Any], json.loads(self.shared_attrs)) + except ValueError: + # When json.loads fails + _LOGGER.exception("Error converting row to state attributes: %s", self) + return {} + + class StatisticResult(TypedDict): """Statistic result data class. @@ -256,8 +324,8 @@ class StatisticsBase: id = Column(Integer, Identity(), primary_key=True) created = Column(DATETIME_TYPE, default=dt_util.utcnow) - @declared_attr - def metadata_id(self): + @declared_attr # type: ignore[misc] + def metadata_id(self) -> Column: """Define the metadata_id column for sub classes.""" return Column( Integer, @@ -274,7 +342,7 @@ class StatisticsBase: sum = Column(DOUBLE_TYPE) @classmethod - def from_stats(cls, metadata_id: int, stats: StatisticData): + def from_stats(cls, metadata_id: int, stats: StatisticData) -> StatisticsBase: """Create object from a statistics.""" return cls( # type: ignore[call-arg,misc] metadata_id=metadata_id, @@ -367,7 +435,7 @@ class RecorderRuns(Base): # type: ignore[misc,valid-type] f")>" ) - def entity_ids(self, point_in_time=None): + def entity_ids(self, point_in_time: datetime | None = None) -> list[str]: """Return the entity ids that existed in this run. Specify point_in_time if you want to know which existed at that point @@ -388,7 +456,7 @@ class RecorderRuns(Base): # type: ignore[misc,valid-type] return [row[0] for row in query] - def to_native(self, validate_entity_id=True): + def to_native(self, validate_entity_id: bool = True) -> RecorderRuns: """Return self, native format is this model.""" return self @@ -473,96 +541,115 @@ class LazyState(State): __slots__ = [ "_row", - "entity_id", - "state", "_attributes", "_last_changed", "_last_updated", "_context", + "_attr_cache", ] - def __init__(self, row): # pylint: disable=super-init-not-called + def __init__( # pylint: disable=super-init-not-called + self, row: Row, attr_cache: dict[str, dict[str, Any]] | None = None + ) -> None: """Init the lazy state.""" self._row = row - self.entity_id = self._row.entity_id + self.entity_id: str = self._row.entity_id self.state = self._row.state or "" - self._attributes = None - self._last_changed = None - self._last_updated = None - self._context = None + self._attributes: dict[str, Any] | None = None + self._last_changed: datetime | None = None + self._last_updated: datetime | None = None + self._context: Context | None = None + self._attr_cache = attr_cache @property # type: ignore[override] - def attributes(self): + def attributes(self) -> dict[str, Any]: # type: ignore[override] """State attributes.""" - if not self._attributes: + if self._attributes is None: + source = self._row.shared_attrs or self._row.attributes + if self._attr_cache is not None and ( + attributes := self._attr_cache.get(source) + ): + self._attributes = attributes + return attributes + if source == EMPTY_JSON_OBJECT or source is None: + self._attributes = {} + return self._attributes try: - self._attributes = json.loads(self._row.attributes) + self._attributes = json.loads(source) except ValueError: # When json.loads fails - _LOGGER.exception("Error converting row to state: %s", self._row) + _LOGGER.exception( + "Error converting row to state attributes: %s", self._row + ) self._attributes = {} + if self._attr_cache is not None: + self._attr_cache[source] = self._attributes return self._attributes @attributes.setter - def attributes(self, value): + def attributes(self, value: dict[str, Any]) -> None: """Set attributes.""" self._attributes = value @property # type: ignore[override] - def context(self): + def context(self) -> Context: # type: ignore[override] """State context.""" - if not self._context: - self._context = Context(id=None) + if self._context is None: + self._context = Context(id=None) # type: ignore[arg-type] return self._context @context.setter - def context(self, value): + def context(self, value: Context) -> None: """Set context.""" self._context = value @property # type: ignore[override] - def last_changed(self): + def last_changed(self) -> datetime: # type: ignore[override] """Last changed datetime.""" - if not self._last_changed: + if self._last_changed is None: self._last_changed = process_timestamp(self._row.last_changed) return self._last_changed @last_changed.setter - def last_changed(self, value): + def last_changed(self, value: datetime) -> None: """Set last changed datetime.""" self._last_changed = value @property # type: ignore[override] - def last_updated(self): + def last_updated(self) -> datetime: # type: ignore[override] """Last updated datetime.""" - if not self._last_updated: + if self._last_updated is None: self._last_updated = process_timestamp(self._row.last_updated) return self._last_updated @last_updated.setter - def last_updated(self, value): + def last_updated(self, value: datetime) -> None: """Set last updated datetime.""" self._last_updated = value - def as_dict(self): + def as_dict(self) -> dict[str, Any]: # type: ignore[override] """Return a dict representation of the LazyState. Async friendly. To be used for JSON serialization. """ - if self._last_changed: - last_changed_isoformat = self._last_changed.isoformat() - else: + if self._last_changed is None and self._last_updated is None: last_changed_isoformat = process_timestamp_to_utc_isoformat( self._row.last_changed ) - if self._last_updated: - last_updated_isoformat = self._last_updated.isoformat() + if self._row.last_changed == self._row.last_updated: + last_updated_isoformat = last_changed_isoformat + else: + last_updated_isoformat = process_timestamp_to_utc_isoformat( + self._row.last_updated + ) else: - last_updated_isoformat = process_timestamp_to_utc_isoformat( - self._row.last_updated - ) + 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() return { "entity_id": self.entity_id, "state": self.state, @@ -571,7 +658,7 @@ class LazyState(State): "last_updated": last_updated_isoformat, } - def __eq__(self, other): + def __eq__(self, other: Any) -> bool: """Return the comparison.""" return ( other.__class__ in [self.__class__, State] diff --git a/homeassistant/components/recorder/pool.py b/homeassistant/components/recorder/pool.py index b30237f98da..633c084ade4 100644 --- a/homeassistant/components/recorder/pool.py +++ b/homeassistant/components/recorder/pool.py @@ -1,34 +1,65 @@ """A pool for sqlite connections.""" import threading +from typing import Any -from sqlalchemy.pool import NullPool, StaticPool +from sqlalchemy.pool import NullPool, SingletonThreadPool + +from homeassistant.helpers.frame import report + +from .const import DB_WORKER_PREFIX + +POOL_SIZE = 5 -class RecorderPool(StaticPool, NullPool): - """A hybrid of NullPool and StaticPool. +class RecorderPool(SingletonThreadPool, NullPool): # type: ignore[misc] + """A hybrid of NullPool and SingletonThreadPool. - When called from the creating thread acts like StaticPool + When called from the creating thread or db executor acts like SingletonThreadPool When called from any other thread, acts like NullPool """ - def __init__(self, *args, **kw): # pylint: disable=super-init-not-called + def __init__( # pylint: disable=super-init-not-called + self, *args: Any, **kw: Any + ) -> None: """Create the pool.""" - self._tid = threading.current_thread().ident - StaticPool.__init__(self, *args, **kw) + kw["pool_size"] = POOL_SIZE + SingletonThreadPool.__init__(self, *args, **kw) - def _do_return_conn(self, conn): - if threading.current_thread().ident == self._tid: + @property + def recorder_or_dbworker(self) -> bool: + """Check if the thread is a recorder or dbworker thread.""" + thread_name = threading.current_thread().name + return bool( + thread_name == "Recorder" or thread_name.startswith(DB_WORKER_PREFIX) + ) + + # Any can be switched out for ConnectionPoolEntry in the next version of sqlalchemy + def _do_return_conn(self, conn: Any) -> Any: + if self.recorder_or_dbworker: return super()._do_return_conn(conn) conn.close() - def dispose(self): - """Dispose of the connection.""" - if threading.current_thread().ident == self._tid: - return super().dispose() + def shutdown(self) -> None: + """Close the connection.""" + if self.recorder_or_dbworker and self._conn and (conn := self._conn.current()): + conn.close() - def _do_get(self): - if threading.current_thread().ident == self._tid: + def dispose(self) -> None: + """Dispose of the connection.""" + if self.recorder_or_dbworker: + super().dispose() + + # Any can be switched out for ConnectionPoolEntry in the next version of sqlalchemy + def _do_get(self) -> Any: + if self.recorder_or_dbworker: return super()._do_get() + report( + "accesses the database without the database executor; " + "Use homeassistant.components.recorder.get_instance(hass).async_add_executor_job() " + "for faster database operations", + exclude_integrations={"recorder"}, + error_if_core=False, + ) return super( # pylint: disable=bad-super-call NullPool, self )._create_connection() diff --git a/homeassistant/components/recorder/purge.py b/homeassistant/components/recorder/purge.py index dd80fb15479..9bbe13ca5a7 100644 --- a/homeassistant/components/recorder/purge.py +++ b/homeassistant/components/recorder/purge.py @@ -10,8 +10,17 @@ from sqlalchemy import func from sqlalchemy.orm.session import Session from sqlalchemy.sql.expression import distinct +from homeassistant.const import EVENT_STATE_CHANGED + from .const import MAX_ROWS_TO_PURGE -from .models import Events, RecorderRuns, States, StatisticsRuns, StatisticsShortTerm +from .models import ( + Events, + RecorderRuns, + StateAttributes, + States, + StatisticsRuns, + StatisticsShortTerm, +) from .repack import repack_database from .util import retryable_database_job, session_scope @@ -37,7 +46,9 @@ def purge_old_data( with session_scope(session=instance.get_session()) as session: # type: ignore[misc] # Purge a max of MAX_ROWS_TO_PURGE, based on the oldest states or events record event_ids = _select_event_ids_to_purge(session, purge_before) - state_ids = _select_state_ids_to_purge(session, purge_before, event_ids) + state_ids, attributes_ids = _select_state_and_attributes_ids_to_purge( + session, purge_before, event_ids + ) statistics_runs = _select_statistics_runs_to_purge(session, purge_before) short_term_statistics = _select_short_term_statistics_to_purge( session, purge_before @@ -46,6 +57,11 @@ def purge_old_data( if state_ids: _purge_state_ids(instance, session, state_ids) + if unused_attribute_ids_set := _select_unused_attributes_ids( + session, attributes_ids + ): + _purge_attributes_ids(instance, session, unused_attribute_ids_set) + if event_ids: _purge_event_ids(session, event_ids) @@ -82,20 +98,45 @@ def _select_event_ids_to_purge(session: Session, purge_before: datetime) -> list return [event.event_id for event in events] -def _select_state_ids_to_purge( +def _select_state_and_attributes_ids_to_purge( session: Session, purge_before: datetime, event_ids: list[int] -) -> set[int]: +) -> tuple[set[int], set[int]]: """Return a list of state ids to purge.""" if not event_ids: - return set() + return set(), set() states = ( - session.query(States.state_id) + session.query(States.state_id, States.attributes_id) .filter(States.last_updated < purge_before) .filter(States.event_id.in_(event_ids)) .all() ) _LOGGER.debug("Selected %s state ids to remove", len(states)) - return {state.state_id for state in states} + state_ids = set() + attributes_ids = set() + for state in states: + state_ids.add(state.state_id) + if state.attributes_id: + attributes_ids.add(state.attributes_id) + return state_ids, attributes_ids + + +def _select_unused_attributes_ids( + session: Session, attributes_ids: set[int] +) -> set[int]: + """Return a set of attributes ids that are not used by any states in the database.""" + if not attributes_ids: + return set() + to_remove = attributes_ids - { + state[0] + for state in session.query(distinct(States.attributes_id)) + .filter(States.attributes_id.in_(attributes_ids)) + .all() + } + _LOGGER.debug( + "Selected %s shared attributes to remove", + len(to_remove), + ) + return to_remove def _select_statistics_runs_to_purge( @@ -175,6 +216,44 @@ def _evict_purged_states_from_old_states_cache( old_states.pop(old_state_reversed[purged_state_id], None) +def _evict_purged_attributes_from_attributes_cache( + instance: Recorder, purged_attributes_ids: set[int] +) -> None: + """Evict purged attribute ids from the attribute ids cache.""" + # Make a map from attributes_id to the attributes json + state_attributes_ids = ( + instance._state_attributes_ids # pylint: disable=protected-access + ) + state_attributes_ids_reversed = { + attributes_id: attributes + for attributes, attributes_id in state_attributes_ids.items() + } + + # Evict any purged attributes from the state_attributes_ids cache + for purged_attribute_id in purged_attributes_ids.intersection( + state_attributes_ids_reversed + ): + state_attributes_ids.pop( + state_attributes_ids_reversed[purged_attribute_id], None + ) + + +def _purge_attributes_ids( + instance: Recorder, session: Session, attributes_ids: set[int] +) -> None: + """Delete old attributes ids.""" + + deleted_rows = ( + session.query(StateAttributes) + .filter(StateAttributes.attributes_id.in_(attributes_ids)) + .delete(synchronize_session=False) + ) + _LOGGER.debug("Deleted %s attribute states", deleted_rows) + + # Evict any entries in the state_attributes_ids cache referring to a purged state + _evict_purged_attributes_from_attributes_cache(instance, attributes_ids) + + def _purge_statistics_runs(session: Session, statistics_runs: list[int]) -> None: """Delete by run_id.""" deleted_rows = ( @@ -212,6 +291,7 @@ def _purge_old_recorder_runs( ) -> None: """Purge all old recorder runs.""" # Recorder runs is small, no need to batch run it + assert instance.run_info is not None deleted_rows = ( session.query(RecorderRuns) .filter(RecorderRuns.start < purge_before) @@ -253,10 +333,11 @@ def _purge_filtered_states( ) -> None: """Remove filtered states and linked events.""" state_ids: list[int] + attributes_ids: list[int] event_ids: list[int | None] - state_ids, event_ids = zip( + state_ids, attributes_ids, event_ids = zip( *( - session.query(States.state_id, States.event_id) + session.query(States.state_id, States.attributes_id, States.event_id) .filter(States.entity_id.in_(excluded_entity_ids)) .limit(MAX_ROWS_TO_PURGE) .all() @@ -268,6 +349,10 @@ def _purge_filtered_states( ) _purge_state_ids(instance, session, set(state_ids)) _purge_event_ids(session, event_ids) # type: ignore[arg-type] # type of event_ids already narrowed to 'list[int]' + unused_attribute_ids_set = _select_unused_attributes_ids( + session, {id_ for id_ in attributes_ids if id_ is not None} + ) + _purge_attributes_ids(instance, session, unused_attribute_ids_set) def _purge_filtered_events( @@ -280,7 +365,9 @@ def _purge_filtered_events( .limit(MAX_ROWS_TO_PURGE) .all() ) - event_ids: list[int] = [event.event_id for event in events] + event_ids: list[int] = [ + event.event_id for event in events if event.event_id is not None + ] _LOGGER.debug( "Selected %s event_ids to remove that should be filtered", len(event_ids) ) @@ -290,6 +377,9 @@ def _purge_filtered_events( state_ids: set[int] = {state.state_id for state in states} _purge_state_ids(instance, session, state_ids) _purge_event_ids(session, event_ids) + if EVENT_STATE_CHANGED in excluded_event_types: + session.query(StateAttributes).delete(synchronize_session=False) + instance._state_attributes_ids = {} # pylint: disable=protected-access @retryable_database_job("purge") diff --git a/homeassistant/components/recorder/repack.py b/homeassistant/components/recorder/repack.py index 68d7d5954c9..95df0681ddb 100644 --- a/homeassistant/components/recorder/repack.py +++ b/homeassistant/components/recorder/repack.py @@ -12,15 +12,17 @@ _LOGGER = logging.getLogger(__name__) def repack_database(instance: Recorder) -> None: """Repack based on engine type.""" + assert instance.engine is not None + dialect_name = instance.engine.dialect.name # Execute sqlite command to free up space on disk - if instance.engine.dialect.name == "sqlite": + if dialect_name == "sqlite": _LOGGER.debug("Vacuuming SQL DB to free space") instance.engine.execute("VACUUM") return # Execute postgresql vacuum command to free up space on disk - if instance.engine.dialect.name == "postgresql": + if dialect_name == "postgresql": _LOGGER.debug("Vacuuming SQL DB to free space") with instance.engine.connect().execution_options( isolation_level="AUTOCOMMIT" @@ -29,7 +31,7 @@ def repack_database(instance: Recorder) -> None: return # Optimize mysql / mariadb tables to free up space on disk - if instance.engine.dialect.name == "mysql": + if dialect_name == "mysql": _LOGGER.debug("Optimizing SQL DB to free space") instance.engine.execute("OPTIMIZE TABLE states, events, recorder_runs") return diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index 4154ae83055..f01190097df 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -19,6 +19,7 @@ from sqlalchemy.exc import SQLAlchemyError, StatementError from sqlalchemy.ext import baked from sqlalchemy.orm.scoping import scoped_session from sqlalchemy.sql.expression import literal_column, true +import voluptuous as vol from homeassistant.const import ( PRESSURE_PA, @@ -163,6 +164,14 @@ def valid_statistic_id(statistic_id: str) -> bool: return VALID_STATISTIC_ID.match(statistic_id) is not None +def validate_statistic_id(value: str) -> str: + """Validate statistic ID.""" + if valid_statistic_id(value): + return value + + raise vol.Invalid(f"Statistics ID {value} is an invalid statistic ID") + + @dataclasses.dataclass class ValidationIssue: """Error or warning message.""" @@ -181,7 +190,7 @@ def async_setup(hass: HomeAssistant) -> None: hass.data[STATISTICS_META_BAKERY] = baked.bakery() hass.data[STATISTICS_SHORT_TERM_BAKERY] = baked.bakery() - def entity_id_changed(event: Event) -> None: + def _entity_id_changed(event: Event) -> None: """Handle entity_id changed.""" old_entity_id = event.data["old_entity_id"] entity_id = event.data["entity_id"] @@ -191,6 +200,9 @@ def async_setup(hass: HomeAssistant) -> None: & (StatisticsMeta.source == DOMAIN) ).update({StatisticsMeta.statistic_id: entity_id}) + async def _async_entity_id_changed(event: Event) -> None: + await hass.data[DATA_INSTANCE].async_add_executor_job(_entity_id_changed, event) + @callback def entity_registry_changed_filter(event: Event) -> bool: """Handle entity_id changed filter.""" @@ -202,7 +214,7 @@ def async_setup(hass: HomeAssistant) -> None: if hass.is_running: hass.bus.async_listen( entity_registry.EVENT_ENTITY_REGISTRY_UPDATED, - entity_id_changed, + _async_entity_id_changed, event_filter=entity_registry_changed_filter, ) @@ -567,6 +579,30 @@ def compile_statistics(instance: Recorder, start: datetime) -> bool: return True +def _adjust_sum_statistics( + session: scoped_session, + table: type[Statistics | StatisticsShortTerm], + metadata_id: int, + start_time: datetime, + adj: float, +) -> None: + """Adjust statistics in the database.""" + try: + session.query(table).filter_by(metadata_id=metadata_id).filter( + table.start >= start_time + ).update( + { + table.sum: table.sum + adj, + }, + synchronize_session=False, + ) + except SQLAlchemyError: + _LOGGER.exception( + "Unexpected exception when updating statistics %s", + id, + ) + + def _insert_statistics( session: scoped_session, table: type[Statistics | StatisticsShortTerm], @@ -606,7 +642,7 @@ def _update_statistics( except SQLAlchemyError: _LOGGER.exception( "Unexpected exception when updating statistics %s:%s ", - id, + stat_id, statistic, ) @@ -718,21 +754,22 @@ def update_statistics_metadata( def list_statistic_ids( hass: HomeAssistant, + statistic_ids: list[str] | tuple[str] | None = None, statistic_type: Literal["mean"] | Literal["sum"] | None = None, -) -> list[dict | None]: - """Return all statistic_ids and unit of measurement. +) -> list[dict]: + """Return all statistic_ids (or filtered one) and unit of measurement. Queries the database for existing statistic_ids, as well as integrations with a recorder platform for statistic_ids which will be added in the next statistics period. """ units = hass.config.units - statistic_ids = {} + result = {} # Query the database with session_scope(hass=hass) as session: metadata = get_metadata_with_session( - hass, session, statistic_type=statistic_type + hass, session, statistic_type=statistic_type, statistic_ids=statistic_ids ) for _, meta in metadata.values(): @@ -741,8 +778,10 @@ def list_statistic_ids( unit = _configured_unit(unit, units) meta["unit_of_measurement"] = unit - statistic_ids = { + result = { meta["statistic_id"]: { + "has_mean": meta["has_mean"], + "has_sum": meta["has_sum"], "name": meta["name"], "source": meta["source"], "unit_of_measurement": meta["unit_of_measurement"], @@ -754,7 +793,9 @@ def list_statistic_ids( for platform in hass.data[DOMAIN].values(): if not hasattr(platform, "list_statistic_ids"): continue - platform_statistic_ids = platform.list_statistic_ids(hass, statistic_type) + platform_statistic_ids = platform.list_statistic_ids( + hass, statistic_ids=statistic_ids, statistic_type=statistic_type + ) for statistic_id, info in platform_statistic_ids.items(): if (unit := info["unit_of_measurement"]) is not None: @@ -763,17 +804,19 @@ def list_statistic_ids( platform_statistic_ids[statistic_id]["unit_of_measurement"] = unit for key, value in platform_statistic_ids.items(): - statistic_ids.setdefault(key, value) + result.setdefault(key, value) # Return a list of statistic_id + metadata return [ { "statistic_id": _id, + "has_mean": info["has_mean"], + "has_sum": info["has_sum"], "name": info.get("name"), "source": info["source"], "unit_of_measurement": info["unit_of_measurement"], } - for _id, info in statistic_ids.items() + for _id, info in result.items() ] @@ -1211,19 +1254,19 @@ def _filter_unique_constraint_integrity_error( if not isinstance(err, StatementError): return False + assert instance.engine is not None + dialect_name = instance.engine.dialect.name + ignore = False - if ( - instance.engine.dialect.name == "sqlite" - and "UNIQUE constraint failed" in str(err) - ): + if dialect_name == "sqlite" and "UNIQUE constraint failed" in str(err): ignore = True if ( - instance.engine.dialect.name == "postgresql" + dialect_name == "postgresql" and hasattr(err.orig, "pgcode") and err.orig.pgcode == "23505" ): ignore = True - if instance.engine.dialect.name == "mysql" and hasattr(err.orig, "args"): + if dialect_name == "mysql" and hasattr(err.orig, "args"): with contextlib.suppress(TypeError): if err.orig.args[0] == 1062: ignore = True @@ -1246,7 +1289,7 @@ def add_external_statistics( metadata: StatisticMetaData, statistics: Iterable[StatisticData], ) -> bool: - """Process an add_statistics job.""" + """Process an add_external_statistics job.""" with session_scope( session=instance.get_session(), # type: ignore[misc] @@ -1262,3 +1305,35 @@ def add_external_statistics( _insert_statistics(session, Statistics, metadata_id, stat) return True + + +@retryable_database_job("adjust_statistics") +def adjust_statistics( + instance: Recorder, + statistic_id: str, + start_time: datetime, + sum_adjustment: float, +) -> bool: + """Process an add_statistics job.""" + + with session_scope(session=instance.get_session()) as session: # type: ignore[misc] + metadata = get_metadata_with_session( + instance.hass, session, statistic_ids=(statistic_id,) + ) + if statistic_id not in metadata: + return True + + tables: tuple[type[Statistics | StatisticsShortTerm], ...] = ( + Statistics, + StatisticsShortTerm, + ) + for table in tables: + _adjust_sum_statistics( + session, + table, + metadata[statistic_id][0], + start_time, + sum_adjustment, + ) + + return True diff --git a/homeassistant/components/recorder/util.py b/homeassistant/components/recorder/util.py index 9d5ee6f29d3..487b8dd22f7 100644 --- a/homeassistant/components/recorder/util.py +++ b/homeassistant/components/recorder/util.py @@ -3,12 +3,12 @@ from __future__ import annotations from collections.abc import Callable, Generator from contextlib import contextmanager -from datetime import timedelta +from datetime import datetime, timedelta import functools import logging import os import time -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any from awesomeversion import ( AwesomeVersion, @@ -16,7 +16,9 @@ from awesomeversion import ( AwesomeVersionStrategy, ) from sqlalchemy import text +from sqlalchemy.engine.cursor import CursorFetchStrategy from sqlalchemy.exc import OperationalError, SQLAlchemyError +from sqlalchemy.orm.query import Query from sqlalchemy.orm.session import Session from homeassistant.core import HomeAssistant @@ -94,7 +96,7 @@ def session_scope( session.close() -def commit(session, work): +def commit(session: Session, work: Any) -> bool: """Commit & retry work: Either a model or in a function.""" for _ in range(0, RETRIES): try: @@ -111,12 +113,13 @@ def commit(session, work): return False -def execute(qry, to_native=False, validate_entity_ids=True) -> list | None: +def execute( + qry: Query, to_native: bool = False, validate_entity_ids: bool = True +) -> list: """Query the database and convert the objects to HA native form. This method also retries a few times in the case of stale connections. """ - for tryno in range(0, RETRIES): try: timer_start = time.perf_counter() @@ -130,7 +133,7 @@ def execute(qry, to_native=False, validate_entity_ids=True) -> list | None: if row is not None ] else: - result = list(qry) + result = qry.all() if _LOGGER.isEnabledFor(logging.DEBUG): elapsed = time.perf_counter() - timer_start @@ -155,7 +158,7 @@ def execute(qry, to_native=False, validate_entity_ids=True) -> list | None: raise time.sleep(QUERY_RETRY_WAIT) - return None + assert False # unreachable def validate_or_move_away_sqlite_database(dburl: str) -> bool: @@ -173,12 +176,12 @@ def validate_or_move_away_sqlite_database(dburl: str) -> bool: return True -def dburl_to_path(dburl): +def dburl_to_path(dburl: str) -> str: """Convert the db url into a filesystem path.""" return dburl[len(SQLITE_URL_PREFIX) :] -def last_run_was_recently_clean(cursor): +def last_run_was_recently_clean(cursor: CursorFetchStrategy) -> bool: """Verify the last recorder run was recently clean.""" cursor.execute("SELECT end FROM recorder_runs ORDER BY start DESC LIMIT 1;") @@ -188,6 +191,7 @@ def last_run_was_recently_clean(cursor): return False last_run_end_time = process_timestamp(dt_util.parse_datetime(end_time[0])) + assert last_run_end_time is not None now = dt_util.utcnow() _LOGGER.debug("The last run ended at: %s (now: %s)", last_run_end_time, now) @@ -198,7 +202,7 @@ def last_run_was_recently_clean(cursor): return True -def basic_sanity_check(cursor): +def basic_sanity_check(cursor: CursorFetchStrategy) -> bool: """Check tables to make sure select does not fail.""" for table in ALL_TABLES: @@ -233,7 +237,7 @@ def validate_sqlite_database(dbpath: str) -> bool: return True -def run_checks_on_open_db(dbpath, cursor): +def run_checks_on_open_db(dbpath: str, cursor: CursorFetchStrategy) -> None: """Run checks that will generate a sqlite3 exception if there is corruption.""" sanity_check_passed = basic_sanity_check(cursor) last_run_was_clean = last_run_was_recently_clean(cursor) @@ -276,14 +280,14 @@ def move_away_broken_database(dbfile: str) -> None: os.rename(path, f"{path}{corrupt_postfix}") -def execute_on_connection(dbapi_connection, statement): +def execute_on_connection(dbapi_connection: Any, statement: str) -> None: """Execute a single statement with a dbapi connection.""" cursor = dbapi_connection.cursor() cursor.execute(statement) cursor.close() -def query_on_connection(dbapi_connection, statement): +def query_on_connection(dbapi_connection: Any, statement: str) -> Any: """Execute a single statement with a dbapi connection and return the result.""" cursor = dbapi_connection.cursor() cursor.execute(statement) @@ -292,30 +296,34 @@ def query_on_connection(dbapi_connection, statement): return result -def _warn_unsupported_dialect(dialect): +def _warn_unsupported_dialect(dialect_name: str) -> None: """Warn about unsupported database version.""" _LOGGER.warning( "Database %s is not supported; Home Assistant supports %s. " "Starting with Home Assistant 2022.2 this will prevent the recorder from " "starting. Please migrate your database to a supported software before then", - dialect, + dialect_name, "MariaDB ≥ 10.3, MySQL ≥ 8.0, PostgreSQL ≥ 12, SQLite ≥ 3.31.0", ) -def _warn_unsupported_version(server_version, dialect, minimum_version): +def _warn_unsupported_version( + server_version: str, dialect_name: str, minimum_version: str +) -> None: """Warn about unsupported database version.""" _LOGGER.warning( "Version %s of %s is not supported; minimum supported version is %s. " "Starting with Home Assistant 2022.2 this will prevent the recorder from " "starting. Please upgrade your database software before then", server_version, - dialect, + dialect_name, minimum_version, ) -def _extract_version_from_server_response(server_response): +def _extract_version_from_server_response( + server_response: str, +) -> AwesomeVersion | None: """Attempt to extract version from server response.""" try: return AwesomeVersion( @@ -328,8 +336,11 @@ def _extract_version_from_server_response(server_response): def setup_connection_for_dialect( - instance, dialect_name, dbapi_connection, first_connection -): + instance: Recorder, + dialect_name: str, + dbapi_connection: Any, + first_connection: bool, +) -> None: """Execute statements needed for dialect connection.""" # Returns False if the the connection needs to be setup # on the next connection, returns True if the connection @@ -404,7 +415,7 @@ def setup_connection_for_dialect( _warn_unsupported_dialect(dialect_name) -def end_incomplete_runs(session, start_time): +def end_incomplete_runs(session: Session, start_time: datetime) -> None: """End any incomplete recorder runs.""" for run in session.query(RecorderRuns).filter_by(end=None): run.closed_incorrect = True @@ -421,12 +432,13 @@ def retryable_database_job(description: str) -> Callable: The job should return True if it finished, and False if it needs to be rescheduled. """ - def decorator(job: Callable) -> Callable: + def decorator(job: Callable[[Any], bool]) -> Callable: @functools.wraps(job) - def wrapper(instance: Recorder, *args, **kwargs): + def wrapper(instance: Recorder, *args: Any, **kwargs: Any) -> bool: try: return job(instance, *args, **kwargs) except OperationalError as err: + assert instance.engine is not None if ( instance.engine.dialect.name == "mysql" and err.orig.args[0] in RETRYABLE_MYSQL_ERRORS @@ -448,12 +460,12 @@ def retryable_database_job(description: str) -> Callable: return decorator -def perodic_db_cleanups(instance: Recorder): +def perodic_db_cleanups(instance: Recorder) -> None: """Run any database cleanups that need to happen perodiclly. These cleanups will happen nightly or after any purge. """ - + assert instance.engine is not None if instance.engine.dialect.name == "sqlite": # Execute sqlite to create a wal checkpoint and free up disk space _LOGGER.debug("WAL checkpoint") @@ -462,9 +474,9 @@ def perodic_db_cleanups(instance: Recorder): @contextmanager -def write_lock_db_sqlite(instance: Recorder): +def write_lock_db_sqlite(instance: Recorder) -> Generator[None, None, None]: """Lock database for writes.""" - + assert instance.engine is not None with instance.engine.connect() as connection: # Execute sqlite to create a wal checkpoint # This is optional but makes sure the backup is going to be minimal @@ -487,4 +499,5 @@ def async_migration_in_progress(hass: HomeAssistant) -> bool: """ if DATA_INSTANCE not in hass.data: return False - return hass.data[DATA_INSTANCE].migration_in_progress + instance: Recorder = hass.data[DATA_INSTANCE] + return instance.migration_in_progress diff --git a/homeassistant/components/recorder/websocket_api.py b/homeassistant/components/recorder/websocket_api.py index aec7905615f..585641665af 100644 --- a/homeassistant/components/recorder/websocket_api.py +++ b/homeassistant/components/recorder/websocket_api.py @@ -8,9 +8,10 @@ import voluptuous as vol from homeassistant.components import websocket_api from homeassistant.core import HomeAssistant, callback +from homeassistant.util import dt as dt_util from .const import DATA_INSTANCE, MAX_QUEUE_BACKLOG -from .statistics import validate_statistics +from .statistics import list_statistic_ids, validate_statistics from .util import async_migration_in_progress if TYPE_CHECKING: @@ -24,10 +25,12 @@ def async_setup(hass: HomeAssistant) -> None: """Set up the recorder websocket API.""" websocket_api.async_register_command(hass, ws_validate_statistics) websocket_api.async_register_command(hass, ws_clear_statistics) + websocket_api.async_register_command(hass, ws_get_statistics_metadata) websocket_api.async_register_command(hass, ws_update_statistics_metadata) websocket_api.async_register_command(hass, ws_info) websocket_api.async_register_command(hass, ws_backup_start) websocket_api.async_register_command(hass, ws_backup_end) + websocket_api.async_register_command(hass, ws_adjust_sum_statistics) @websocket_api.websocket_command( @@ -40,7 +43,8 @@ async def ws_validate_statistics( hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict ) -> None: """Fetch a list of available statistic_id.""" - statistic_ids = await hass.async_add_executor_job( + instance: Recorder = hass.data[DATA_INSTANCE] + statistic_ids = await instance.async_add_executor_job( validate_statistics, hass, ) @@ -67,6 +71,24 @@ def ws_clear_statistics( connection.send_result(msg["id"]) +@websocket_api.websocket_command( + { + vol.Required("type"): "recorder/get_statistics_metadata", + vol.Optional("statistic_ids"): [str], + } +) +@websocket_api.async_response +async def ws_get_statistics_metadata( + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict +) -> None: + """Get metadata for a list of statistic_ids.""" + instance: Recorder = hass.data[DATA_INSTANCE] + statistic_ids = await instance.async_add_executor_job( + list_statistic_ids, hass, msg.get("statistic_ids") + ) + connection.send_result(msg["id"], statistic_ids) + + @websocket_api.require_admin @websocket_api.websocket_command( { @@ -86,6 +108,34 @@ def ws_update_statistics_metadata( connection.send_result(msg["id"]) +@websocket_api.require_admin +@websocket_api.websocket_command( + { + vol.Required("type"): "recorder/adjust_sum_statistics", + vol.Required("statistic_id"): str, + vol.Required("start_time"): str, + vol.Required("adjustment"): vol.Any(float, int), + } +) +@callback +def ws_adjust_sum_statistics( + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict +) -> None: + """Adjust sum statistics.""" + start_time_str = msg["start_time"] + + if start_time := dt_util.parse_datetime(start_time_str): + start_time = dt_util.as_utc(start_time) + else: + connection.send_error(msg["id"], "invalid_start_time", "Invalid start time") + return + + hass.data[DATA_INSTANCE].async_adjust_statistics( + msg["statistic_id"], start_time, msg["adjustment"] + ) + connection.send_result(msg["id"]) + + @websocket_api.websocket_command( { vol.Required("type"): "recorder/info", diff --git a/homeassistant/components/remember_the_milk/manifest.json b/homeassistant/components/remember_the_milk/manifest.json index 40bfbe1683c..2dd01f7fe6d 100644 --- a/homeassistant/components/remember_the_milk/manifest.json +++ b/homeassistant/components/remember_the_milk/manifest.json @@ -2,7 +2,7 @@ "domain": "remember_the_milk", "name": "Remember The Milk", "documentation": "https://www.home-assistant.io/integrations/remember_the_milk", - "requirements": ["RtmAPI==0.7.2", "httplib2==0.19.0"], + "requirements": ["RtmAPI==0.7.2", "httplib2==0.20.4"], "dependencies": ["configurator"], "codeowners": [], "iot_class": "cloud_push", diff --git a/homeassistant/components/remote/manifest.json b/homeassistant/components/remote/manifest.json index e2caf2d5606..d08cb624c16 100644 --- a/homeassistant/components/remote/manifest.json +++ b/homeassistant/components/remote/manifest.json @@ -2,6 +2,6 @@ "domain": "remote", "name": "Remote", "documentation": "https://www.home-assistant.io/integrations/remote", - "codeowners": [], + "codeowners": ["@home-assistant/core"], "quality_scale": "internal" } diff --git a/homeassistant/components/remote/translations/el.json b/homeassistant/components/remote/translations/el.json index 8f0221bdd74..431d2e8af79 100644 --- a/homeassistant/components/remote/translations/el.json +++ b/homeassistant/components/remote/translations/el.json @@ -18,9 +18,9 @@ }, "state": { "_": { - "off": "\u039a\u03bb\u03b5\u03b9\u03c3\u03c4\u03cc", + "off": "\u0395\u03ba\u03c4\u03cc\u03c2 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1\u03c2", "on": "\u0391\u03bd\u03bf\u03b9\u03c7\u03c4\u03cc" } }, - "title": "\u03a4\u03b7\u03bb\u03b5\u03c7\u03b5\u03b9\u03c1\u03b9\u03c3\u03bc\u03cc\u03c2" + "title": "\u0391\u03c0\u03bf\u03bc\u03b1\u03ba\u03c1\u03c5\u03c3\u03bc\u03ad\u03bd\u03bf" } \ No newline at end of file diff --git a/homeassistant/components/remote/translations/fr.json b/homeassistant/components/remote/translations/fr.json index c2052edaab8..1560c52cba0 100644 --- a/homeassistant/components/remote/translations/fr.json +++ b/homeassistant/components/remote/translations/fr.json @@ -18,8 +18,8 @@ }, "state": { "_": { - "off": "Inactif", - "on": "Actif" + "off": "D\u00e9sactiv\u00e9", + "on": "Activ\u00e9" } }, "title": "T\u00e9l\u00e9commande" diff --git a/homeassistant/components/remote/translations/zh-Hant.json b/homeassistant/components/remote/translations/zh-Hant.json index 80f59008dc3..e8a07049e29 100644 --- a/homeassistant/components/remote/translations/zh-Hant.json +++ b/homeassistant/components/remote/translations/zh-Hant.json @@ -22,5 +22,5 @@ "on": "\u958b\u5553" } }, - "title": "\u9059\u63a7" + "title": "\u9059\u63a7\u5668" } \ No newline at end of file diff --git a/homeassistant/components/renault/manifest.json b/homeassistant/components/renault/manifest.json index d41686f0c1f..807a694e39f 100644 --- a/homeassistant/components/renault/manifest.json +++ b/homeassistant/components/renault/manifest.json @@ -3,13 +3,9 @@ "name": "Renault", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/renault", - "requirements": [ - "renault-api==0.1.10" - ], - "codeowners": [ - "@epenet" - ], + "requirements": ["renault-api==0.1.10"], + "codeowners": ["@epenet"], "iot_class": "cloud_polling", "loggers": ["renault_api"], - "supported_brands":{"dacia":"Dacia"} + "supported_brands": { "dacia": "Dacia" } } diff --git a/homeassistant/components/renault/renault_coordinator.py b/homeassistant/components/renault/renault_coordinator.py index 4487d9db9ab..7db5ed0c4e1 100644 --- a/homeassistant/components/renault/renault_coordinator.py +++ b/homeassistant/components/renault/renault_coordinator.py @@ -4,7 +4,7 @@ from __future__ import annotations from collections.abc import Awaitable, Callable from datetime import timedelta import logging -from typing import TypeVar +from typing import Optional, TypeVar from renault_api.kamereon.exceptions import ( AccessDeniedException, @@ -16,7 +16,7 @@ from renault_api.kamereon.models import KamereonVehicleDataAttributes from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -T = TypeVar("T", bound=KamereonVehicleDataAttributes) +T = TypeVar("T", bound=Optional[KamereonVehicleDataAttributes]) class RenaultDataUpdateCoordinator(DataUpdateCoordinator[T]): diff --git a/homeassistant/components/renault/renault_entities.py b/homeassistant/components/renault/renault_entities.py index 14ebcf2c2e4..82f071decae 100644 --- a/homeassistant/components/renault/renault_entities.py +++ b/homeassistant/components/renault/renault_entities.py @@ -2,14 +2,14 @@ from __future__ import annotations from dataclasses import dataclass -from typing import Optional, cast +from typing import cast from homeassistant.const import ATTR_NAME from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .renault_coordinator import T +from .renault_coordinator import RenaultDataUpdateCoordinator, T from .renault_vehicle import RenaultVehicleProxy @@ -50,7 +50,9 @@ class RenaultEntity(Entity): return f"{self.vehicle.device_info[ATTR_NAME]} {self.entity_description.name}" -class RenaultDataEntity(CoordinatorEntity[Optional[T]], RenaultEntity): +class RenaultDataEntity( + CoordinatorEntity[RenaultDataUpdateCoordinator[T]], RenaultEntity +): """Implementation of a Renault entity with a data coordinator.""" def __init__( @@ -65,5 +67,5 @@ class RenaultDataEntity(CoordinatorEntity[Optional[T]], RenaultEntity): def _get_data_attr(self, key: str) -> StateType: """Return the attribute value from the coordinator data.""" if self.coordinator.data is None: - return None + return None # type: ignore[unreachable] return cast(StateType, getattr(self.coordinator.data, key)) diff --git a/homeassistant/components/renault/sensor.py b/homeassistant/components/renault/sensor.py index c6621b16bbc..3b486af175b 100644 --- a/homeassistant/components/renault/sensor.py +++ b/homeassistant/components/renault/sensor.py @@ -4,7 +4,7 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass from datetime import datetime -from typing import TYPE_CHECKING, cast +from typing import TYPE_CHECKING, Any, Generic, cast from renault_api.kamereon.enums import ChargeState, PlugState from renault_api.kamereon.models import ( @@ -45,18 +45,18 @@ from .renault_vehicle import RenaultVehicleProxy @dataclass -class RenaultSensorRequiredKeysMixin: +class RenaultSensorRequiredKeysMixin(Generic[T]): """Mixin for required keys.""" data_key: str - entity_class: type[RenaultSensor] + entity_class: type[RenaultSensor[T]] @dataclass class RenaultSensorEntityDescription( SensorEntityDescription, RenaultDataEntityDescription, - RenaultSensorRequiredKeysMixin, + RenaultSensorRequiredKeysMixin[T], ): """Class describing Renault sensor entities.""" @@ -73,7 +73,7 @@ async def async_setup_entry( ) -> None: """Set up the Renault entities from config entry.""" proxy: RenaultHub = hass.data[DOMAIN][config_entry.entry_id] - entities: list[RenaultSensor] = [ + entities: list[RenaultSensor[Any]] = [ description.entity_class(vehicle, description) for vehicle in proxy.vehicles.values() for description in SENSOR_TYPES @@ -87,7 +87,7 @@ async def async_setup_entry( class RenaultSensor(RenaultDataEntity[T], SensorEntity): """Mixin for sensor specific attributes.""" - entity_description: RenaultSensorEntityDescription + entity_description: RenaultSensorEntityDescription[T] @property def data(self) -> StateType: @@ -157,7 +157,7 @@ def _get_utc_value(entity: RenaultSensor[T]) -> datetime: return as_utc(original_dt) -SENSOR_TYPES: tuple[RenaultSensorEntityDescription, ...] = ( +SENSOR_TYPES: tuple[RenaultSensorEntityDescription[Any], ...] = ( RenaultSensorEntityDescription( key="battery_level", coordinator="battery", diff --git a/homeassistant/components/renault/translations/fr.json b/homeassistant/components/renault/translations/fr.json index 406339b0445..32dcac2929f 100644 --- a/homeassistant/components/renault/translations/fr.json +++ b/homeassistant/components/renault/translations/fr.json @@ -6,7 +6,7 @@ "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" }, "error": { - "invalid_credentials": "Authentification invalide" + "invalid_credentials": "Authentification non valide" }, "step": { "kamereon": { @@ -26,7 +26,7 @@ "data": { "locale": "Lieu", "password": "Mot de passe", - "username": "Email" + "username": "Courriel" }, "title": "D\u00e9finir les informations d'identification de Renault" } diff --git a/homeassistant/components/rest_command/__init__.py b/homeassistant/components/rest_command/__init__.py index f9174743f3c..70f887cf896 100644 --- a/homeassistant/components/rest_command/__init__.py +++ b/homeassistant/components/rest_command/__init__.py @@ -144,8 +144,12 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: except asyncio.TimeoutError: _LOGGER.warning("Timeout call %s", request_url) - except aiohttp.ClientError: - _LOGGER.error("Client error %s", request_url) + except aiohttp.ClientError as err: + _LOGGER.error( + "Client error. Url: %s. Error: %s", + request_url, + err, + ) # register services hass.services.async_register(DOMAIN, name, async_service_handler) diff --git a/homeassistant/components/rfxtrx/__init__.py b/homeassistant/components/rfxtrx/__init__.py index 8dda4d32644..c10075bbb79 100644 --- a/homeassistant/components/rfxtrx/__init__.py +++ b/homeassistant/components/rfxtrx/__init__.py @@ -12,7 +12,6 @@ import RFXtrx as rfxtrxmod import async_timeout import voluptuous as vol -from homeassistant import config_entries from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_DEVICE_ID, @@ -31,6 +30,7 @@ from homeassistant.helpers.device_registry import ( DeviceEntry, DeviceRegistry, ) +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import DeviceInfo, Entity from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity @@ -40,15 +40,15 @@ from .const import ( COMMAND_GROUP_LIST, CONF_AUTOMATIC_ADD, CONF_DATA_BITS, + CONF_PROTOCOLS, DATA_RFXOBJECT, DEVICE_PACKET_TYPE_LIGHTING4, + DOMAIN, EVENT_RFXTRX_EVENT, SERVICE_SEND, ) -DOMAIN = "rfxtrx" - -DEFAULT_SIGNAL_REPETITIONS = 1 +DEFAULT_OFF_DELAY = 2.0 SIGNAL_EVENT = f"{DOMAIN}_event" @@ -81,12 +81,11 @@ PLATFORMS = [ Platform.LIGHT, Platform.BINARY_SENSOR, Platform.COVER, + Platform.SIREN, ] -async def async_setup_entry( - hass: HomeAssistant, entry: config_entries.ConfigEntry -) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up the RFXtrx component.""" hass.data.setdefault(DOMAIN, {}) @@ -121,15 +120,28 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: def _create_rfx(config): """Construct a rfx object based on config.""" + + modes = config.get(CONF_PROTOCOLS) + + if modes: + _LOGGER.debug("Using modes: %s", ",".join(modes)) + else: + _LOGGER.debug("No modes defined, using device configuration") + if config[CONF_PORT] is not None: # If port is set then we create a TCP connection rfx = rfxtrxmod.Connect( (config[CONF_HOST], config[CONF_PORT]), None, transport_protocol=rfxtrxmod.PyNetworkTransport, + modes=modes, ) else: - rfx = rfxtrxmod.Connect(config[CONF_DEVICE], None) + rfx = rfxtrxmod.Connect( + config[CONF_DEVICE], + None, + modes=modes, + ) return rfx @@ -147,7 +159,7 @@ def _get_device_lookup(devices): return lookup -async def async_setup_internal(hass, entry: config_entries.ConfigEntry): +async def async_setup_internal(hass, entry: ConfigEntry): """Set up the RFXtrx component.""" config = entry.data @@ -276,7 +288,7 @@ async def async_setup_internal(hass, entry: config_entries.ConfigEntry): async def async_setup_platform_entry( hass: HomeAssistant, - config_entry: config_entries.ConfigEntry, + config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, supported: Callable[[rfxtrxmod.RFXtrxEvent], bool], constructor: Callable[ @@ -323,7 +335,7 @@ async def async_setup_platform_entry( async_add_entities(constructor(event, event, device_id, {})) config_entry.async_on_unload( - hass.helpers.dispatcher.async_dispatcher_connect(SIGNAL_EVENT, _update) + async_dispatcher_connect(hass, SIGNAL_EVENT, _update) ) @@ -473,9 +485,7 @@ class RfxtrxEntity(RestoreEntity): self._apply_event(self._event) self.async_on_remove( - self.hass.helpers.dispatcher.async_dispatcher_connect( - SIGNAL_EVENT, self._handle_event - ) + async_dispatcher_connect(self.hass, SIGNAL_EVENT, self._handle_event) ) @property @@ -549,15 +559,12 @@ class RfxtrxCommandEntity(RfxtrxEntity): self, device: rfxtrxmod.RFXtrxDevice, device_id: DeviceTuple, - signal_repetitions: int = 1, event: rfxtrxmod.RFXtrxEvent | None = None, ) -> None: """Initialzie a switch or light device.""" super().__init__(device, device_id, event=event) - self.signal_repetitions = signal_repetitions self._state: bool | None = None async def _async_send(self, fun, *args): rfx_object = self.hass.data[DOMAIN][DATA_RFXOBJECT] - for _ in range(self.signal_repetitions): - await self.hass.async_add_executor_job(fun, rfx_object.transport, *args) + await self.hass.async_add_executor_job(fun, rfx_object.transport, *args) diff --git a/homeassistant/components/rfxtrx/config_flow.py b/homeassistant/components/rfxtrx/config_flow.py index 549a5c3ccbf..8fce56564a4 100644 --- a/homeassistant/components/rfxtrx/config_flow.py +++ b/homeassistant/components/rfxtrx/config_flow.py @@ -2,6 +2,7 @@ from __future__ import annotations import copy +import itertools import os from typing import TypedDict, cast @@ -23,6 +24,7 @@ from homeassistant.const import ( CONF_TYPE, ) from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import ( DeviceEntry, DeviceRegistry, @@ -46,21 +48,20 @@ from .const import ( CONF_AUTOMATIC_ADD, CONF_DATA_BITS, CONF_OFF_DELAY, + CONF_PROTOCOLS, CONF_REPLACE_DEVICE, - CONF_SIGNAL_REPETITIONS, CONF_VENETIAN_BLIND_MODE, CONST_VENETIAN_BLIND_MODE_DEFAULT, CONST_VENETIAN_BLIND_MODE_EU, CONST_VENETIAN_BLIND_MODE_US, DEVICE_PACKET_TYPE_LIGHTING4, ) -from .cover import supported as cover_supported -from .light import supported as light_supported -from .switch import supported as switch_supported CONF_EVENT_CODE = "event_code" CONF_MANUAL_PATH = "Enter Manually" +RECV_MODES = sorted(itertools.chain(*rfxtrxmod.lowlevel.Status.RECMODES)) + class DeviceData(TypedDict): """Dict data representing a device entry.""" @@ -102,6 +103,7 @@ class OptionsFlow(config_entries.OptionsFlow): if user_input is not None: self._global_options = { CONF_AUTOMATIC_ADD: user_input[CONF_AUTOMATIC_ADD], + CONF_PROTOCOLS: user_input[CONF_PROTOCOLS] or None, } if CONF_DEVICE in user_input: entry_id = user_input[CONF_DEVICE] @@ -151,6 +153,10 @@ class OptionsFlow(config_entries.OptionsFlow): CONF_AUTOMATIC_ADD, default=self._config_entry.data[CONF_AUTOMATIC_ADD], ): bool, + vol.Optional( + CONF_PROTOCOLS, + default=self._config_entry.data.get(CONF_PROTOCOLS) or [], + ): cv.multi_select(RECV_MODES), vol.Optional(CONF_EVENT_CODE): str, vol.Optional(CONF_DEVICE): vol.In(configure_devices), } @@ -200,7 +206,6 @@ class OptionsFlow(config_entries.OptionsFlow): devices = {} device = { CONF_DEVICE_ID: device_id, - CONF_SIGNAL_REPETITIONS: user_input.get(CONF_SIGNAL_REPETITIONS, 1), } devices[self._selected_device_event_code] = device @@ -242,21 +247,6 @@ class OptionsFlow(config_entries.OptionsFlow): } data_schema.update(off_delay_schema) - if ( - binary_supported(self._selected_device_object) - or cover_supported(self._selected_device_object) - or light_supported(self._selected_device_object) - or switch_supported(self._selected_device_object) - ): - data_schema.update( - { - vol.Optional( - CONF_SIGNAL_REPETITIONS, - default=device_data.get(CONF_SIGNAL_REPETITIONS, 1), - ): int, - } - ) - if ( self._selected_device_object.device.packettype == DEVICE_PACKET_TYPE_LIGHTING4 diff --git a/homeassistant/components/rfxtrx/const.py b/homeassistant/components/rfxtrx/const.py index 50cd355c457..7a6e333d3db 100644 --- a/homeassistant/components/rfxtrx/const.py +++ b/homeassistant/components/rfxtrx/const.py @@ -2,9 +2,9 @@ CONF_DATA_BITS = "data_bits" CONF_AUTOMATIC_ADD = "automatic_add" -CONF_SIGNAL_REPETITIONS = "signal_repetitions" CONF_OFF_DELAY = "off_delay" CONF_VENETIAN_BLIND_MODE = "venetian_blind_mode" +CONF_PROTOCOLS = "protocols" CONF_REPLACE_DEVICE = "replace_device" @@ -44,3 +44,5 @@ DEVICE_PACKET_TYPE_LIGHTING4 = 0x13 EVENT_RFXTRX_EVENT = "rfxtrx_event" DATA_RFXOBJECT = "rfxobject" + +DOMAIN = "rfxtrx" diff --git a/homeassistant/components/rfxtrx/cover.py b/homeassistant/components/rfxtrx/cover.py index 80b58b6f0c4..d350998e2a9 100644 --- a/homeassistant/components/rfxtrx/cover.py +++ b/homeassistant/components/rfxtrx/cover.py @@ -19,16 +19,10 @@ from homeassistant.const import STATE_OPEN from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import ( - DEFAULT_SIGNAL_REPETITIONS, - DeviceTuple, - RfxtrxCommandEntity, - async_setup_platform_entry, -) +from . import DeviceTuple, RfxtrxCommandEntity, async_setup_platform_entry from .const import ( COMMAND_OFF_LIST, COMMAND_ON_LIST, - CONF_SIGNAL_REPETITIONS, CONF_VENETIAN_BLIND_MODE, CONST_VENETIAN_BLIND_MODE_EU, CONST_VENETIAN_BLIND_MODE_US, @@ -59,7 +53,6 @@ async def async_setup_entry( RfxtrxCover( event.device, device_id, - entity_info.get(CONF_SIGNAL_REPETITIONS, DEFAULT_SIGNAL_REPETITIONS), venetian_blind_mode=entity_info.get(CONF_VENETIAN_BLIND_MODE), event=event if auto else None, ) @@ -79,12 +72,11 @@ class RfxtrxCover(RfxtrxCommandEntity, CoverEntity): self, device: rfxtrxmod.RFXtrxDevice, device_id: DeviceTuple, - signal_repetitions: int, event: rfxtrxmod.RFXtrxEvent = None, venetian_blind_mode: bool | None = None, ) -> None: """Initialize the RFXtrx cover device.""" - super().__init__(device, device_id, signal_repetitions, event) + super().__init__(device, device_id, event) self._venetian_blind_mode = venetian_blind_mode async def async_added_to_hass(self): diff --git a/homeassistant/components/rfxtrx/diagnostics.py b/homeassistant/components/rfxtrx/diagnostics.py new file mode 100644 index 00000000000..bc2fae2452d --- /dev/null +++ b/homeassistant/components/rfxtrx/diagnostics.py @@ -0,0 +1,19 @@ +"""Diagnostics support for RFXCOM RFXtrx.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.components.diagnostics import async_redact_data +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +TO_REDACT = {"host"} + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, entry: ConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + return { + "entry": async_redact_data(entry.as_dict(), TO_REDACT), + } diff --git a/homeassistant/components/rfxtrx/light.py b/homeassistant/components/rfxtrx/light.py index 6baada1fb75..7212b65cd7e 100644 --- a/homeassistant/components/rfxtrx/light.py +++ b/homeassistant/components/rfxtrx/light.py @@ -15,13 +15,8 @@ from homeassistant.const import STATE_ON from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import ( - DEFAULT_SIGNAL_REPETITIONS, - DeviceTuple, - RfxtrxCommandEntity, - async_setup_platform_entry, -) -from .const import COMMAND_OFF_LIST, COMMAND_ON_LIST, CONF_SIGNAL_REPETITIONS +from . import DeviceTuple, RfxtrxCommandEntity, async_setup_platform_entry +from .const import COMMAND_OFF_LIST, COMMAND_ON_LIST _LOGGER = logging.getLogger(__name__) @@ -53,7 +48,6 @@ async def async_setup_entry( RfxtrxLight( event.device, device_id, - entity_info.get(CONF_SIGNAL_REPETITIONS, DEFAULT_SIGNAL_REPETITIONS), event=event if auto else None, ) ] diff --git a/homeassistant/components/rfxtrx/siren.py b/homeassistant/components/rfxtrx/siren.py new file mode 100644 index 00000000000..9a4a475998d --- /dev/null +++ b/homeassistant/components/rfxtrx/siren.py @@ -0,0 +1,234 @@ +"""Support for RFXtrx sirens.""" +from __future__ import annotations + +from typing import Any + +import RFXtrx as rfxtrxmod + +from homeassistant.components.siren import ( + SUPPORT_TONES, + SUPPORT_TURN_OFF, + SUPPORT_TURN_ON, + SirenEntity, +) +from homeassistant.components.siren.const import ATTR_TONE +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.event import async_call_later + +from . import ( + DEFAULT_OFF_DELAY, + DeviceTuple, + RfxtrxCommandEntity, + async_setup_platform_entry, +) +from .const import CONF_OFF_DELAY + +SUPPORT_RFXTRX = SUPPORT_TURN_ON | SUPPORT_TONES + +SECURITY_PANIC_ON = "Panic" +SECURITY_PANIC_OFF = "End Panic" +SECURITY_PANIC_ALL = {SECURITY_PANIC_ON, SECURITY_PANIC_OFF} + + +def supported(event: rfxtrxmod.RFXtrxEvent): + """Return whether an event supports sirens.""" + device = event.device + + if isinstance(device, rfxtrxmod.ChimeDevice): + return True + + if isinstance(device, rfxtrxmod.SecurityDevice) and isinstance( + event, rfxtrxmod.SensorEvent + ): + if event.values["Sensor Status"] in SECURITY_PANIC_ALL: + return True + + return False + + +def get_first_key(data: dict[int, str], entry: str) -> int: + """Find a key based on the items value.""" + return next((key for key, value in data.items() if value == entry)) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up config entry.""" + + def _constructor( + event: rfxtrxmod.RFXtrxEvent, + auto: rfxtrxmod.RFXtrxEvent | None, + device_id: DeviceTuple, + entity_info: dict, + ): + """Construct a entity from an event.""" + device = event.device + + if isinstance(device, rfxtrxmod.ChimeDevice): + return [ + RfxtrxChime( + event.device, + device_id, + entity_info.get(CONF_OFF_DELAY, DEFAULT_OFF_DELAY), + auto, + ) + ] + + if isinstance(device, rfxtrxmod.SecurityDevice) and isinstance( + event, rfxtrxmod.SensorEvent + ): + if event.values["Sensor Status"] in SECURITY_PANIC_ALL: + return [ + RfxtrxSecurityPanic( + event.device, + device_id, + entity_info.get(CONF_OFF_DELAY, DEFAULT_OFF_DELAY), + auto, + ) + ] + + await async_setup_platform_entry( + hass, config_entry, async_add_entities, supported, _constructor + ) + + +class RfxtrxOffDelayMixin(Entity): + """Mixin to support timeouts on data. + + Many 433 devices only send data when active. They will + repeatedly (every x seconds) send a command to indicate + being active and stop sending this command when inactive. + This mixin allow us to keep track of the timeout once + they go inactive. + """ + + _timeout: CALLBACK_TYPE | None = None + _off_delay: float | None = None + + def _setup_timeout(self): + @callback + def _done(_): + self._timeout = None + self.async_write_ha_state() + + if self._off_delay: + self._timeout = async_call_later(self.hass, self._off_delay, _done) + + def _cancel_timeout(self): + if self._timeout: + self._timeout() + self._timeout = None + + +class RfxtrxChime(RfxtrxCommandEntity, SirenEntity, RfxtrxOffDelayMixin): + """Representation of a RFXtrx chime.""" + + _device: rfxtrxmod.ChimeDevice + + def __init__(self, device, device_id, off_delay=None, event=None): + """Initialize the entity.""" + super().__init__(device, device_id, event) + self._attr_available_tones = list(self._device.COMMANDS.values()) + self._attr_supported_features = SUPPORT_TURN_ON | SUPPORT_TONES + self._default_tone = next(iter(self._device.COMMANDS)) + self._off_delay = off_delay + + @property + def is_on(self): + """Return true if device is on.""" + return self._timeout is not None + + async def async_turn_on(self, **kwargs): + """Turn the device on.""" + self._cancel_timeout() + + if tone := kwargs.get(ATTR_TONE): + command = get_first_key(self._device.COMMANDS, tone) + else: + command = self._default_tone + + await self._async_send(self._device.send_command, command) + + self._setup_timeout() + + self.async_write_ha_state() + + def _apply_event(self, event: rfxtrxmod.ControlEvent): + """Apply a received event.""" + super()._apply_event(event) + + sound = event.values.get("Sound") + if sound is not None: + self._cancel_timeout() + self._setup_timeout() + + @callback + def _handle_event(self, event, device_id): + """Check if event applies to me and update.""" + if self._event_applies(event, device_id): + self._apply_event(event) + + self.async_write_ha_state() + + +class RfxtrxSecurityPanic(RfxtrxCommandEntity, SirenEntity, RfxtrxOffDelayMixin): + """Representation of a security device.""" + + _device: rfxtrxmod.SecurityDevice + + def __init__(self, device, device_id, off_delay=None, event=None): + """Initialize the entity.""" + super().__init__(device, device_id, event) + self._attr_supported_features = SUPPORT_TURN_ON | SUPPORT_TURN_OFF + self._on_value = get_first_key(self._device.STATUS, SECURITY_PANIC_ON) + self._off_value = get_first_key(self._device.STATUS, SECURITY_PANIC_OFF) + self._off_delay = off_delay + + @property + def is_on(self): + """Return true if device is on.""" + return self._timeout is not None + + async def async_turn_on(self, **kwargs: Any): + """Turn the device on.""" + self._cancel_timeout() + + await self._async_send(self._device.send_status, self._on_value) + + self._setup_timeout() + + self.async_write_ha_state() + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the device off.""" + self._cancel_timeout() + + await self._async_send(self._device.send_status, self._off_value) + + self.async_write_ha_state() + + def _apply_event(self, event: rfxtrxmod.SensorEvent): + """Apply a received event.""" + super()._apply_event(event) + + status = event.values.get("Sensor Status") + + if status == SECURITY_PANIC_ON: + self._cancel_timeout() + self._setup_timeout() + elif status == SECURITY_PANIC_OFF: + self._cancel_timeout() + + @callback + def _handle_event(self, event, device_id): + """Check if event applies to me and update.""" + if self._event_applies(event, device_id): + self._apply_event(event) + + self.async_write_ha_state() diff --git a/homeassistant/components/rfxtrx/strings.json b/homeassistant/components/rfxtrx/strings.json index 542ff9a45cd..4469fd59801 100644 --- a/homeassistant/components/rfxtrx/strings.json +++ b/homeassistant/components/rfxtrx/strings.json @@ -41,6 +41,7 @@ "data": { "debug": "Enable debugging", "automatic_add": "Enable automatic add", + "protocols": "Protocols", "event_code": "Enter event code to add", "device": "Select device to configure" }, @@ -53,7 +54,6 @@ "data_bit": "Number of data bits", "command_on": "Data bits value for command on", "command_off": "Data bits value for command off", - "signal_repetitions": "Number of signal repetitions", "venetian_blind_mode": "Venetian blind mode", "replace_device": "Select device to replace" }, diff --git a/homeassistant/components/rfxtrx/switch.py b/homeassistant/components/rfxtrx/switch.py index 8bc1fa42874..fc826b4ebe6 100644 --- a/homeassistant/components/rfxtrx/switch.py +++ b/homeassistant/components/rfxtrx/switch.py @@ -12,7 +12,6 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import ( - DEFAULT_SIGNAL_REPETITIONS, DOMAIN, DeviceTuple, RfxtrxCommandEntity, @@ -23,7 +22,6 @@ from .const import ( COMMAND_OFF_LIST, COMMAND_ON_LIST, CONF_DATA_BITS, - CONF_SIGNAL_REPETITIONS, DEVICE_PACKET_TYPE_LIGHTING4, ) @@ -59,7 +57,6 @@ async def async_setup_entry( RfxtrxSwitch( event.device, device_id, - entity_info.get(CONF_SIGNAL_REPETITIONS, DEFAULT_SIGNAL_REPETITIONS), entity_info.get(CONF_DATA_BITS), entity_info.get(CONF_COMMAND_ON), entity_info.get(CONF_COMMAND_OFF), @@ -79,14 +76,13 @@ class RfxtrxSwitch(RfxtrxCommandEntity, SwitchEntity): self, device: rfxtrxmod.RFXtrxDevice, device_id: DeviceTuple, - signal_repetitions: int = 1, data_bits: int | None = None, cmd_on: int | None = None, cmd_off: int | None = None, event: rfxtrxmod.RFXtrxEvent | None = None, ) -> None: """Initialize the RFXtrx switch.""" - super().__init__(device, device_id, signal_repetitions, event=event) + super().__init__(device, device_id, event=event) self._data_bits = data_bits self._cmd_on = cmd_on self._cmd_off = cmd_off diff --git a/homeassistant/components/rfxtrx/translations/bg.json b/homeassistant/components/rfxtrx/translations/bg.json index 725534d7ac5..9a7983491d6 100644 --- a/homeassistant/components/rfxtrx/translations/bg.json +++ b/homeassistant/components/rfxtrx/translations/bg.json @@ -26,6 +26,13 @@ "error": { "already_configured_device": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "prompt_options": { + "data": { + "protocols": "\u041f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u0438" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/rfxtrx/translations/ca.json b/homeassistant/components/rfxtrx/translations/ca.json index dd1bb23c56a..a8a4f958cb6 100644 --- a/homeassistant/components/rfxtrx/translations/ca.json +++ b/homeassistant/components/rfxtrx/translations/ca.json @@ -61,6 +61,7 @@ "debug": "Activa la depuraci\u00f3", "device": "Selecciona el dispositiu a configurar", "event_code": "Introdueix el codi de l'esdeveniment a afegir", + "protocols": "Protocols", "remove_device": "Selecciona el dispositiu a eliminar" }, "title": "Opcions de Rfxtrx" diff --git a/homeassistant/components/rfxtrx/translations/de.json b/homeassistant/components/rfxtrx/translations/de.json index ee65e371330..a80bde85b0b 100644 --- a/homeassistant/components/rfxtrx/translations/de.json +++ b/homeassistant/components/rfxtrx/translations/de.json @@ -61,6 +61,7 @@ "debug": "Debugging aktivieren", "device": "Zu konfigurierendes Ger\u00e4t ausw\u00e4hlen", "event_code": "Ereigniscode zum Hinzuf\u00fcgen eingeben", + "protocols": "Protokolle", "remove_device": "Zu l\u00f6schendes Ger\u00e4t ausw\u00e4hlen" }, "title": "Rfxtrx Optionen" diff --git a/homeassistant/components/rfxtrx/translations/el.json b/homeassistant/components/rfxtrx/translations/el.json index b04b6172969..8d55c7252a0 100644 --- a/homeassistant/components/rfxtrx/translations/el.json +++ b/homeassistant/components/rfxtrx/translations/el.json @@ -61,6 +61,7 @@ "debug": "\u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03b5\u03bd\u03c4\u03bf\u03c0\u03b9\u03c3\u03bc\u03bf\u03cd \u03c3\u03c6\u03b1\u03bb\u03bc\u03ac\u03c4\u03c9\u03bd", "device": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 \u03b3\u03b9\u03b1 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c0\u03b1\u03c1\u03b1\u03bc\u03ad\u03c4\u03c1\u03c9\u03bd", "event_code": "\u0395\u03b9\u03c3\u03b1\u03b3\u03c9\u03b3\u03ae \u03ba\u03c9\u03b4\u03b9\u03ba\u03bf\u03cd \u03c3\u03c5\u03bc\u03b2\u03ac\u03bd\u03c4\u03bf\u03c2 \u03b3\u03b9\u03b1 \u03c0\u03c1\u03bf\u03c3\u03b8\u03ae\u03ba\u03b7", + "protocols": "\u03a0\u03c1\u03c9\u03c4\u03cc\u03ba\u03bf\u03bb\u03bb\u03b1", "remove_device": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 \u03b3\u03b9\u03b1 \u03b4\u03b9\u03b1\u03b3\u03c1\u03b1\u03c6\u03ae" }, "title": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 Rfxtrx" diff --git a/homeassistant/components/rfxtrx/translations/en.json b/homeassistant/components/rfxtrx/translations/en.json index 2728c189010..af042719f77 100644 --- a/homeassistant/components/rfxtrx/translations/en.json +++ b/homeassistant/components/rfxtrx/translations/en.json @@ -61,6 +61,7 @@ "debug": "Enable debugging", "device": "Select device to configure", "event_code": "Enter event code to add", + "protocols": "Protocols", "remove_device": "Select device to delete" }, "title": "Rfxtrx Options" diff --git a/homeassistant/components/rfxtrx/translations/et.json b/homeassistant/components/rfxtrx/translations/et.json index 1b414db656c..2c4a36b1664 100644 --- a/homeassistant/components/rfxtrx/translations/et.json +++ b/homeassistant/components/rfxtrx/translations/et.json @@ -61,6 +61,7 @@ "debug": "Luba silumine", "device": "Vali seadistatav seade", "event_code": "Sisesta lisatava s\u00fcndmuse kood", + "protocols": "Protokollid", "remove_device": "Vali eemaldatav seade" }, "title": "Rfxtrx valikud" diff --git a/homeassistant/components/rfxtrx/translations/fr.json b/homeassistant/components/rfxtrx/translations/fr.json index d101cfbc57a..5eaff1ce406 100644 --- a/homeassistant/components/rfxtrx/translations/fr.json +++ b/homeassistant/components/rfxtrx/translations/fr.json @@ -46,7 +46,7 @@ }, "trigger_type": { "command": "Commande re\u00e7ue\u00a0: {subtype}", - "status": "Statut re\u00e7u\u00a0: {subtype}" + "status": "\u00c9tat re\u00e7u\u00a0: {subtype}" } }, "one": "Vide", @@ -66,6 +66,7 @@ "debug": "Activer le d\u00e9bogage", "device": "S\u00e9lectionnez l'appareil \u00e0 configurer", "event_code": "Entrez le code d'\u00e9v\u00e9nement \u00e0 ajouter", + "protocols": "Protocoles", "remove_device": "S\u00e9lectionnez l'appareil \u00e0 supprimer" }, "title": "Options Rfxtrx" diff --git a/homeassistant/components/rfxtrx/translations/hu.json b/homeassistant/components/rfxtrx/translations/hu.json index 86242a4e973..a6f1c925fbf 100644 --- a/homeassistant/components/rfxtrx/translations/hu.json +++ b/homeassistant/components/rfxtrx/translations/hu.json @@ -66,6 +66,7 @@ "debug": "Enged\u00e9lyezze a hibakeres\u00e9st", "device": "V\u00e1lassza ki a konfigur\u00e1lni k\u00edv\u00e1nt eszk\u00f6zt", "event_code": "\u00cdrja be a hozz\u00e1adni k\u00edv\u00e1nt esem\u00e9ny k\u00f3dj\u00e1t", + "protocols": "Protokollok", "remove_device": "V\u00e1lassza ki a t\u00f6r\u00f6lni k\u00edv\u00e1nt eszk\u00f6zt" }, "title": "Rfxtrx opci\u00f3k" diff --git a/homeassistant/components/rfxtrx/translations/id.json b/homeassistant/components/rfxtrx/translations/id.json index e04b0685050..cea00e08f9c 100644 --- a/homeassistant/components/rfxtrx/translations/id.json +++ b/homeassistant/components/rfxtrx/translations/id.json @@ -61,6 +61,7 @@ "debug": "Aktifkan debugging", "device": "Pilih perangkat untuk dikonfigurasi", "event_code": "Masukkan kode event untuk ditambahkan", + "protocols": "Protokol", "remove_device": "Pilih perangkat yang akan dihapus" }, "title": "Opsi Rfxtrx" diff --git a/homeassistant/components/rfxtrx/translations/it.json b/homeassistant/components/rfxtrx/translations/it.json index b811985349a..5e5f07a013f 100644 --- a/homeassistant/components/rfxtrx/translations/it.json +++ b/homeassistant/components/rfxtrx/translations/it.json @@ -66,6 +66,7 @@ "debug": "Attiva il debug", "device": "Seleziona il dispositivo da configurare", "event_code": "Inserire il codice dell'evento da aggiungere", + "protocols": "Protocolli", "remove_device": "Seleziona il dispositivo da eliminare" }, "title": "Opzioni Rfxtrx" diff --git a/homeassistant/components/rfxtrx/translations/ja.json b/homeassistant/components/rfxtrx/translations/ja.json index bfbf14b31fb..cb79cc60a8e 100644 --- a/homeassistant/components/rfxtrx/translations/ja.json +++ b/homeassistant/components/rfxtrx/translations/ja.json @@ -61,6 +61,7 @@ "debug": "\u30c7\u30d0\u30c3\u30b0\u306e\u6709\u52b9\u5316", "device": "\u8a2d\u5b9a\u3059\u308b\u30c7\u30d0\u30a4\u30b9\u3092\u9078\u629e", "event_code": "\u30a4\u30d9\u30f3\u30c8\u30b3\u30fc\u30c9\u3092\u5165\u529b\u3057\u3066\u8ffd\u52a0", + "protocols": "\u30d7\u30ed\u30c8\u30b3\u30eb", "remove_device": "\u524a\u9664\u3059\u308b\u30c7\u30d0\u30a4\u30b9\u306e\u9078\u629e" }, "title": "Rfxtrx\u306e\u30aa\u30d7\u30b7\u30e7\u30f3" diff --git a/homeassistant/components/rfxtrx/translations/nl.json b/homeassistant/components/rfxtrx/translations/nl.json index 92154861f15..e9b4c1a6c04 100644 --- a/homeassistant/components/rfxtrx/translations/nl.json +++ b/homeassistant/components/rfxtrx/translations/nl.json @@ -61,6 +61,7 @@ "debug": "Foutopsporing inschakelen", "device": "Selecteer het apparaat om te configureren", "event_code": "Voer de gebeurteniscode in om toe te voegen", + "protocols": "Protocollen", "remove_device": "Apparaat selecteren dat u wilt verwijderen" }, "title": "Rfxtrx-opties" diff --git a/homeassistant/components/rfxtrx/translations/no.json b/homeassistant/components/rfxtrx/translations/no.json index 2f867554442..62a658d4067 100644 --- a/homeassistant/components/rfxtrx/translations/no.json +++ b/homeassistant/components/rfxtrx/translations/no.json @@ -61,6 +61,7 @@ "debug": "Aktiver feils\u00f8king", "device": "Velg enhet du vil konfigurere", "event_code": "Angi hendelseskode for \u00e5 legge til", + "protocols": "Protokoller", "remove_device": "Velg enhet du vil slette" }, "title": "[%key:component::rfxtrx::title%] alternativer" diff --git a/homeassistant/components/rfxtrx/translations/pl.json b/homeassistant/components/rfxtrx/translations/pl.json index be3e39fca70..b9f83ded473 100644 --- a/homeassistant/components/rfxtrx/translations/pl.json +++ b/homeassistant/components/rfxtrx/translations/pl.json @@ -72,6 +72,7 @@ "debug": "W\u0142\u0105cz debugowanie", "device": "Wybierz urz\u0105dzenie do skonfigurowania", "event_code": "Podaj kod zdarzenia do dodania", + "protocols": "Protoko\u0142y", "remove_device": "Wybierz urz\u0105dzenie do usuni\u0119cia" }, "title": "Opcje Rfxtrx" diff --git a/homeassistant/components/rfxtrx/translations/pt-BR.json b/homeassistant/components/rfxtrx/translations/pt-BR.json index 6f867a22a55..83252586d6a 100644 --- a/homeassistant/components/rfxtrx/translations/pt-BR.json +++ b/homeassistant/components/rfxtrx/translations/pt-BR.json @@ -61,6 +61,7 @@ "debug": "Habilitar a depura\u00e7\u00e3o", "device": "Selecione o dispositivo para configurar", "event_code": "Insira o c\u00f3digo do evento para adicionar", + "protocols": "Protocolos", "remove_device": "Selecione o dispositivo para excluir" }, "title": "Op\u00e7\u00f5es de Rfxtrx" diff --git a/homeassistant/components/rfxtrx/translations/ru.json b/homeassistant/components/rfxtrx/translations/ru.json index 4a56f37687a..cb0fa979197 100644 --- a/homeassistant/components/rfxtrx/translations/ru.json +++ b/homeassistant/components/rfxtrx/translations/ru.json @@ -61,6 +61,7 @@ "debug": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0440\u0435\u0436\u0438\u043c \u043e\u0442\u043b\u0430\u0434\u043a\u0438", "device": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438", "event_code": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043a\u043e\u0434 \u0441\u043e\u0431\u044b\u0442\u0438\u044f", + "protocols": "\u041f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u044b", "remove_device": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0434\u043b\u044f \u0443\u0434\u0430\u043b\u0435\u043d\u0438\u044f" }, "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438" diff --git a/homeassistant/components/rfxtrx/translations/tr.json b/homeassistant/components/rfxtrx/translations/tr.json index 2d720196874..fd29e034e31 100644 --- a/homeassistant/components/rfxtrx/translations/tr.json +++ b/homeassistant/components/rfxtrx/translations/tr.json @@ -66,6 +66,7 @@ "debug": "Hata ay\u0131klamay\u0131 etkinle\u015ftir", "device": "Yap\u0131land\u0131rmak i\u00e7in cihaz\u0131 se\u00e7in", "event_code": "Eklemek i\u00e7in etkinlik kodunu girin", + "protocols": "Protokoller", "remove_device": "Silinecek cihaz\u0131 se\u00e7in" }, "title": "Rfxtrx Se\u00e7enekleri" diff --git a/homeassistant/components/rfxtrx/translations/zh-Hant.json b/homeassistant/components/rfxtrx/translations/zh-Hant.json index d66b0b1cf7c..3f84f0a5f98 100644 --- a/homeassistant/components/rfxtrx/translations/zh-Hant.json +++ b/homeassistant/components/rfxtrx/translations/zh-Hant.json @@ -29,9 +29,9 @@ }, "user": { "data": { - "type": "\u9023\u7dda\u985e\u578b" + "type": "\u9023\u7dda\u985e\u5225" }, - "title": "\u9078\u64c7\u9023\u7dda\u985e\u578b" + "title": "\u9078\u64c7\u9023\u7dda\u985e\u5225" } } }, @@ -59,8 +59,9 @@ "data": { "automatic_add": "\u958b\u555f\u81ea\u52d5\u65b0\u589e", "debug": "\u958b\u555f\u9664\u932f", - "device": "\u9078\u64c7\u88dd\u7f6e\u4ee5\u8a2d\u5b9a", + "device": "\u9078\u64c7\u6240\u8981\u8a2d\u5b9a\u7684\u88dd\u7f6e", "event_code": "\u8f38\u5165\u4e8b\u4ef6\u4ee3\u78bc\u4ee5\u65b0\u589e", + "protocols": "\u901a\u8a0a\u5354\u5b9a", "remove_device": "\u9078\u64c7\u88dd\u7f6e\u4ee5\u522a\u9664" }, "title": "Rfxtrx \u9078\u9805" diff --git a/homeassistant/components/ridwell/manifest.json b/homeassistant/components/ridwell/manifest.json index e02a0ba6526..0dbb33b2435 100644 --- a/homeassistant/components/ridwell/manifest.json +++ b/homeassistant/components/ridwell/manifest.json @@ -3,12 +3,8 @@ "name": "Ridwell", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ridwell", - "requirements": [ - "aioridwell==2021.12.2" - ], - "codeowners": [ - "@bachya" - ], + "requirements": ["aioridwell==2022.03.0"], + "codeowners": ["@bachya"], "iot_class": "cloud_polling", "loggers": ["aioridwell"] } diff --git a/homeassistant/components/ridwell/translations/fr.json b/homeassistant/components/ridwell/translations/fr.json index 09b73beb37c..82bdb749d13 100644 --- a/homeassistant/components/ridwell/translations/fr.json +++ b/homeassistant/components/ridwell/translations/fr.json @@ -5,7 +5,7 @@ "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" }, "error": { - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/ring/translations/fr.json b/homeassistant/components/ring/translations/fr.json index c86cd78564c..01bbd6587c3 100644 --- a/homeassistant/components/ring/translations/fr.json +++ b/homeassistant/components/ring/translations/fr.json @@ -4,7 +4,7 @@ "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" }, "error": { - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/risco/translations/el.json b/homeassistant/components/risco/translations/el.json index 18799057793..b1c6ddb668e 100644 --- a/homeassistant/components/risco/translations/el.json +++ b/homeassistant/components/risco/translations/el.json @@ -32,8 +32,8 @@ }, "init": { "data": { - "code_arm_required": "\u039d\u03b1 \u03b1\u03c0\u03b1\u03b9\u03c4\u03b5\u03af\u03c4\u03b1\u03b9 \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 PIN \u03b3\u03b9\u03b1 \u03bf\u03c0\u03bb\u03b9\u03c3\u03bc\u03cc", - "code_disarm_required": "\u039d\u03b1 \u03b1\u03c0\u03b1\u03b9\u03c4\u03b5\u03af\u03c4\u03b1\u03b9 \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 PIN \u03b3\u03b9\u03b1 \u03b1\u03c6\u03bf\u03c0\u03bb\u03b9\u03c3\u03bc\u03cc", + "code_arm_required": "\u039d\u03b1 \u03b1\u03c0\u03b1\u03b9\u03c4\u03b5\u03af\u03c4\u03b1\u03b9 \u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 PIN \u03b3\u03b9\u03b1 \u03cc\u03c0\u03bb\u03b9\u03c3\u03b7", + "code_disarm_required": "\u0391\u03c0\u03b1\u03b9\u03c4\u03b5\u03af\u03c4\u03b1\u03b9 \u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 PIN \u03b3\u03b9\u03b1 \u03b1\u03c6\u03cc\u03c0\u03bb\u03b9\u03c3\u03b7", "scan_interval": "\u03a0\u03cc\u03c3\u03bf \u03c3\u03c5\u03c7\u03bd\u03ac \u03bd\u03b1 \u03ba\u03ac\u03bd\u03b5\u03c4\u03b5 \u03bb\u03ae\u03c8\u03b5\u03b9\u03c2 \u03b1\u03c0\u03cc \u03c4\u03bf Risco (\u03c3\u03b5 \u03b4\u03b5\u03c5\u03c4\u03b5\u03c1\u03cc\u03bb\u03b5\u03c0\u03c4\u03b1)" }, "title": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ce\u03bd" diff --git a/homeassistant/components/risco/translations/fr.json b/homeassistant/components/risco/translations/fr.json index 0b33b841e1d..c83e0f8263f 100644 --- a/homeassistant/components/risco/translations/fr.json +++ b/homeassistant/components/risco/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/risco/translations/nl.json b/homeassistant/components/risco/translations/nl.json index 6eeb5fff2e9..fdf1ee5ad23 100644 --- a/homeassistant/components/risco/translations/nl.json +++ b/homeassistant/components/risco/translations/nl.json @@ -28,13 +28,13 @@ "armed_night": "Ingeschakeld nacht" }, "description": "Selecteer in welke staat u uw Risco-alarm wilt instellen wanneer u het Home Assistant-alarm inschakelt", - "title": "Wijs Home Assistant-staten toe aan Risco-staten" + "title": "Wijs Home Assistant statussen toe aan Risco status" }, "init": { "data": { "code_arm_required": "PIN-code vereist om in te schakelen", "code_disarm_required": "PIN-code vereist om uit te schakelen", - "scan_interval": "Polling-interval (in seconden)" + "scan_interval": "Hoe vaak moet Risco worden ververst (in seconden)" }, "title": "Configureer opties" }, @@ -47,8 +47,8 @@ "arm": "Ingeschakeld (AFWEZIG)", "partial_arm": "Gedeeltelijk ingeschakeld (AANWEZIG)" }, - "description": "Selecteer welke staat uw Home Assistant alarm zal melden voor elke staat gemeld door Risco", - "title": "Wijs Risco-staten toe aan Home Assistant-staten" + "description": "Selecteer welke status uw Home Assistant alarm zal melden voor elke status gemeld door Risco", + "title": "Wijs Risco status toe aan Home Assistant statussen" } } } diff --git a/homeassistant/components/rituals_perfume_genie/entity.py b/homeassistant/components/rituals_perfume_genie/entity.py index 3ad71cdad67..e3bf1ef4e63 100644 --- a/homeassistant/components/rituals_perfume_genie/entity.py +++ b/homeassistant/components/rituals_perfume_genie/entity.py @@ -14,11 +14,9 @@ MODEL = "The Perfume Genie" MODEL2 = "The Perfume Genie 2.0" -class DiffuserEntity(CoordinatorEntity): +class DiffuserEntity(CoordinatorEntity[RitualsDataUpdateCoordinator]): """Representation of a diffuser entity.""" - coordinator: RitualsDataUpdateCoordinator - def __init__( self, diffuser: Diffuser, diff --git a/homeassistant/components/rituals_perfume_genie/translations/fr.json b/homeassistant/components/rituals_perfume_genie/translations/fr.json index 2a1fb9c8bb8..ba1d542bfc9 100644 --- a/homeassistant/components/rituals_perfume_genie/translations/fr.json +++ b/homeassistant/components/rituals_perfume_genie/translations/fr.json @@ -5,13 +5,13 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "step": { "user": { "data": { - "email": "Email", + "email": "Courriel", "password": "Mot de passe" }, "title": "Connectez-vous \u00e0 votre compte Rituals" diff --git a/homeassistant/components/roku/entity.py b/homeassistant/components/roku/entity.py index ef969b846aa..39373c96c6a 100644 --- a/homeassistant/components/roku/entity.py +++ b/homeassistant/components/roku/entity.py @@ -9,11 +9,9 @@ from . import RokuDataUpdateCoordinator from .const import DOMAIN -class RokuEntity(CoordinatorEntity): +class RokuEntity(CoordinatorEntity[RokuDataUpdateCoordinator]): """Defines a base Roku entity.""" - coordinator: RokuDataUpdateCoordinator - def __init__( self, *, diff --git a/homeassistant/components/roku/manifest.json b/homeassistant/components/roku/manifest.json index 433ce6b29d1..3f63a7039c1 100644 --- a/homeassistant/components/roku/manifest.json +++ b/homeassistant/components/roku/manifest.json @@ -2,7 +2,7 @@ "domain": "roku", "name": "Roku", "documentation": "https://www.home-assistant.io/integrations/roku", - "requirements": ["rokuecp==0.15.0"], + "requirements": ["rokuecp==0.16.0"], "homekit": { "models": ["3810X", "4660X", "7820X", "C105X", "C135X"] }, diff --git a/homeassistant/components/roku/media_player.py b/homeassistant/components/roku/media_player.py index 8dd76f0b9cb..f7e88ad1ed1 100644 --- a/homeassistant/components/roku/media_player.py +++ b/homeassistant/components/roku/media_player.py @@ -402,9 +402,6 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity): stream_name = original_media_id stream_format = guess_stream_format(media_id, mime_type) - # If media ID is a relative URL, we serve it from HA. - media_id = async_process_play_media_url(self.hass, media_id) - if media_type == FORMAT_CONTENT_TYPE[HLS_PROVIDER]: media_type = MEDIA_TYPE_VIDEO mime_type = FORMAT_CONTENT_TYPE[HLS_PROVIDER] @@ -412,6 +409,9 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity): stream_format = "hls" if media_type in (MEDIA_TYPE_MUSIC, MEDIA_TYPE_URL, MEDIA_TYPE_VIDEO): + # If media ID is a relative URL, we serve it from HA. + media_id = async_process_play_media_url(self.hass, media_id) + parsed = yarl.URL(media_id) if mime_type is None: diff --git a/homeassistant/components/roku/strings.json b/homeassistant/components/roku/strings.json index 68cbe528e87..04c504def03 100644 --- a/homeassistant/components/roku/strings.json +++ b/homeassistant/components/roku/strings.json @@ -9,7 +9,6 @@ } }, "discovery_confirm": { - "title": "Roku", "description": "Do you want to set up {name}?" } }, diff --git a/homeassistant/components/roomba/__init__.py b/homeassistant/components/roomba/__init__.py index 0a58effa481..31b5187a195 100644 --- a/homeassistant/components/roomba/__init__.py +++ b/homeassistant/components/roomba/__init__.py @@ -1,5 +1,6 @@ """The roomba component.""" import asyncio +from functools import partial import logging import async_timeout @@ -42,12 +43,15 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b }, ) - roomba = RoombaFactory.create_roomba( - address=config_entry.data[CONF_HOST], - blid=config_entry.data[CONF_BLID], - password=config_entry.data[CONF_PASSWORD], - continuous=config_entry.options[CONF_CONTINUOUS], - delay=config_entry.options[CONF_DELAY], + roomba = await hass.async_add_executor_job( + partial( + RoombaFactory.create_roomba, + address=config_entry.data[CONF_HOST], + blid=config_entry.data[CONF_BLID], + password=config_entry.data[CONF_PASSWORD], + continuous=config_entry.options[CONF_CONTINUOUS], + delay=config_entry.options[CONF_DELAY], + ) ) try: diff --git a/homeassistant/components/roomba/config_flow.py b/homeassistant/components/roomba/config_flow.py index 2dbecdf32f2..7aee875308b 100644 --- a/homeassistant/components/roomba/config_flow.py +++ b/homeassistant/components/roomba/config_flow.py @@ -1,6 +1,7 @@ """Config flow to configure roomba component.""" import asyncio +from functools import partial from roombapy import RoombaFactory from roombapy.discovery import RoombaDiscovery @@ -41,12 +42,15 @@ async def validate_input(hass: core.HomeAssistant, data): Data has the keys from DATA_SCHEMA with values provided by the user. """ - roomba = RoombaFactory.create_roomba( - address=data[CONF_HOST], - blid=data[CONF_BLID], - password=data[CONF_PASSWORD], - continuous=False, - delay=data[CONF_DELAY], + roomba = await hass.async_add_executor_job( + partial( + RoombaFactory.create_roomba, + address=data[CONF_HOST], + blid=data[CONF_BLID], + password=data[CONF_PASSWORD], + continuous=False, + delay=data[CONF_DELAY], + ) ) info = await async_connect_or_timeout(hass, roomba) diff --git a/homeassistant/components/roomba/manifest.json b/homeassistant/components/roomba/manifest.json index 70053e8ee40..af60705f73c 100644 --- a/homeassistant/components/roomba/manifest.json +++ b/homeassistant/components/roomba/manifest.json @@ -17,7 +17,7 @@ { "hostname": "roomba-*", "macaddress": "DCF505*" - } + } ], "iot_class": "local_push", "loggers": ["paho_mqtt", "roombapy"] diff --git a/homeassistant/components/roon/services.yaml b/homeassistant/components/roon/services.yaml index d3a33ec2fe7..9d9d02f0efc 100644 --- a/homeassistant/components/roon/services.yaml +++ b/homeassistant/components/roon/services.yaml @@ -14,4 +14,3 @@ transfer: entity: integration: roon domain: media_player - diff --git a/homeassistant/components/roon/translations/el.json b/homeassistant/components/roon/translations/el.json index 16fca72db26..e88cbed685c 100644 --- a/homeassistant/components/roon/translations/el.json +++ b/homeassistant/components/roon/translations/el.json @@ -16,7 +16,7 @@ "data": { "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2" }, - "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf \u03cc\u03bd\u03bf\u03bc\u03b1 \u03ba\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03bf\u03cd \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae \u03ae \u03c4\u03b7\u03bd IP \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae Roon." + "description": "\u0394\u03b5\u03bd \u03ae\u03c4\u03b1\u03bd \u03b4\u03c5\u03bd\u03b1\u03c4\u03cc\u03c2 \u03bf \u03b5\u03bd\u03c4\u03bf\u03c0\u03b9\u03c3\u03bc\u03cc\u03c2 \u03c4\u03bf\u03c5 \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae Roon, \u03b5\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf \u03cc\u03bd\u03bf\u03bc\u03b1 \u03ba\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03bf\u03cd \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae \u03ae \u03c4\u03b7\u03bd IP \u03c3\u03b1\u03c2." } } } diff --git a/homeassistant/components/roon/translations/fr.json b/homeassistant/components/roon/translations/fr.json index 94e31ba445f..b0fb3e6784b 100644 --- a/homeassistant/components/roon/translations/fr.json +++ b/homeassistant/components/roon/translations/fr.json @@ -4,7 +4,7 @@ "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" }, "error": { - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/rova/manifest.json b/homeassistant/components/rova/manifest.json index 01f2e2703e8..d1500d33610 100644 --- a/homeassistant/components/rova/manifest.json +++ b/homeassistant/components/rova/manifest.json @@ -2,7 +2,7 @@ "domain": "rova", "name": "ROVA", "documentation": "https://www.home-assistant.io/integrations/rova", - "requirements": ["rova==0.2.1"], + "requirements": ["rova==0.3.0"], "codeowners": [], "iot_class": "cloud_polling", "loggers": ["rova"] diff --git a/homeassistant/components/rpi_gpio_pwm/__init__.py b/homeassistant/components/rpi_gpio_pwm/__init__.py deleted file mode 100644 index 46aa24c12e6..00000000000 --- a/homeassistant/components/rpi_gpio_pwm/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The rpi_gpio_pwm component.""" diff --git a/homeassistant/components/rpi_gpio_pwm/light.py b/homeassistant/components/rpi_gpio_pwm/light.py deleted file mode 100644 index 02444f902b4..00000000000 --- a/homeassistant/components/rpi_gpio_pwm/light.py +++ /dev/null @@ -1,259 +0,0 @@ -"""Support for LED lights that can be controlled using PWM.""" -from __future__ import annotations - -import logging - -from pwmled import Color -from pwmled.driver.gpio import GpioDriver -from pwmled.driver.pca9685 import Pca9685Driver -from pwmled.led import SimpleLed -from pwmled.led.rgb import RgbLed -from pwmled.led.rgbw import RgbwLed -import voluptuous as vol - -from homeassistant.components.light import ( - ATTR_BRIGHTNESS, - ATTR_HS_COLOR, - ATTR_TRANSITION, - PLATFORM_SCHEMA, - SUPPORT_BRIGHTNESS, - SUPPORT_COLOR, - SUPPORT_TRANSITION, - LightEntity, -) -from homeassistant.const import CONF_ADDRESS, CONF_HOST, CONF_NAME, CONF_TYPE, STATE_ON -from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.restore_state import RestoreEntity -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -import homeassistant.util.color as color_util - -_LOGGER = logging.getLogger(__name__) - -CONF_LEDS = "leds" -CONF_DRIVER = "driver" -CONF_PINS = "pins" -CONF_FREQUENCY = "frequency" - -CONF_DRIVER_GPIO = "gpio" -CONF_DRIVER_PCA9685 = "pca9685" -CONF_DRIVER_TYPES = [CONF_DRIVER_GPIO, CONF_DRIVER_PCA9685] - -CONF_LED_TYPE_SIMPLE = "simple" -CONF_LED_TYPE_RGB = "rgb" -CONF_LED_TYPE_RGBW = "rgbw" -CONF_LED_TYPES = [CONF_LED_TYPE_SIMPLE, CONF_LED_TYPE_RGB, CONF_LED_TYPE_RGBW] - -DEFAULT_BRIGHTNESS = 255 -DEFAULT_COLOR = [0, 0] - -SUPPORT_SIMPLE_LED = SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION -SUPPORT_RGB_LED = SUPPORT_BRIGHTNESS | SUPPORT_COLOR | SUPPORT_TRANSITION - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_LEDS): vol.All( - cv.ensure_list, - [ - { - vol.Required(CONF_NAME): cv.string, - vol.Required(CONF_DRIVER): vol.In(CONF_DRIVER_TYPES), - vol.Required(CONF_PINS): vol.All(cv.ensure_list, [cv.positive_int]), - vol.Required(CONF_TYPE): vol.In(CONF_LED_TYPES), - vol.Optional(CONF_FREQUENCY): cv.positive_int, - vol.Optional(CONF_ADDRESS): cv.byte, - vol.Optional(CONF_HOST): cv.string, - } - ], - ) - } -) - - -def setup_platform( - hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the PWM LED lights.""" - _LOGGER.warning( - "The pigpio Daemon PWM LED integration is deprecated and will be removed " - "in Home Assistant Core 2022.4; this integration is removed under " - "Architectural Decision Record 0019, more information can be found here: " - "https://github.com/home-assistant/architecture/blob/master/adr/0019-GPIO.md" - ) - - leds = [] - for led_conf in config[CONF_LEDS]: - driver_type = led_conf[CONF_DRIVER] - pins = led_conf[CONF_PINS] - opt_args = {} - if CONF_FREQUENCY in led_conf: - opt_args["freq"] = led_conf[CONF_FREQUENCY] - if driver_type == CONF_DRIVER_GPIO: - if CONF_HOST in led_conf: - opt_args["host"] = led_conf[CONF_HOST] - driver = GpioDriver(pins, **opt_args) - elif driver_type == CONF_DRIVER_PCA9685: - if CONF_ADDRESS in led_conf: - opt_args["address"] = led_conf[CONF_ADDRESS] - driver = Pca9685Driver(pins, **opt_args) - else: - _LOGGER.error("Invalid driver type") - return - - name = led_conf[CONF_NAME] - led_type = led_conf[CONF_TYPE] - if led_type == CONF_LED_TYPE_SIMPLE: - led = PwmSimpleLed(SimpleLed(driver), name) - elif led_type == CONF_LED_TYPE_RGB: - led = PwmRgbLed(RgbLed(driver), name) - elif led_type == CONF_LED_TYPE_RGBW: - led = PwmRgbLed(RgbwLed(driver), name) - else: - _LOGGER.error("Invalid led type") - return - leds.append(led) - - add_entities(leds) - - -class PwmSimpleLed(LightEntity, RestoreEntity): - """Representation of a simple one-color PWM LED.""" - - def __init__(self, led, name): - """Initialize one-color PWM LED.""" - self._led = led - self._name = name - self._is_on = False - self._brightness = DEFAULT_BRIGHTNESS - - async def async_added_to_hass(self): - """Handle entity about to be added to hass event.""" - await super().async_added_to_hass() - if last_state := await self.async_get_last_state(): - self._is_on = last_state.state == STATE_ON - self._brightness = last_state.attributes.get( - "brightness", DEFAULT_BRIGHTNESS - ) - - @property - def should_poll(self): - """No polling needed.""" - return False - - @property - def name(self): - """Return the name of the group.""" - return self._name - - @property - def is_on(self): - """Return true if device is on.""" - return self._is_on - - @property - def brightness(self): - """Return the brightness property.""" - return self._brightness - - @property - def supported_features(self): - """Flag supported features.""" - return SUPPORT_SIMPLE_LED - - def turn_on(self, **kwargs): - """Turn on a led.""" - if ATTR_BRIGHTNESS in kwargs: - self._brightness = kwargs[ATTR_BRIGHTNESS] - - if ATTR_TRANSITION in kwargs: - transition_time = kwargs[ATTR_TRANSITION] - self._led.transition( - transition_time, - is_on=True, - brightness=_from_hass_brightness(self._brightness), - ) - else: - self._led.set( - is_on=True, brightness=_from_hass_brightness(self._brightness) - ) - - self._is_on = True - self.schedule_update_ha_state() - - def turn_off(self, **kwargs): - """Turn off a LED.""" - if self.is_on: - if ATTR_TRANSITION in kwargs: - transition_time = kwargs[ATTR_TRANSITION] - self._led.transition(transition_time, is_on=False) - else: - self._led.off() - - self._is_on = False - self.schedule_update_ha_state() - - -class PwmRgbLed(PwmSimpleLed): - """Representation of a RGB(W) PWM LED.""" - - def __init__(self, led, name): - """Initialize a RGB(W) PWM LED.""" - super().__init__(led, name) - self._color = DEFAULT_COLOR - - async def async_added_to_hass(self): - """Handle entity about to be added to hass event.""" - await super().async_added_to_hass() - if last_state := await self.async_get_last_state(): - self._color = last_state.attributes.get("hs_color", DEFAULT_COLOR) - - @property - def hs_color(self): - """Return the color property.""" - return self._color - - @property - def supported_features(self): - """Flag supported features.""" - return SUPPORT_RGB_LED - - def turn_on(self, **kwargs): - """Turn on a LED.""" - if ATTR_HS_COLOR in kwargs: - self._color = kwargs[ATTR_HS_COLOR] - if ATTR_BRIGHTNESS in kwargs: - self._brightness = kwargs[ATTR_BRIGHTNESS] - - if ATTR_TRANSITION in kwargs: - transition_time = kwargs[ATTR_TRANSITION] - self._led.transition( - transition_time, - is_on=True, - brightness=_from_hass_brightness(self._brightness), - color=_from_hass_color(self._color), - ) - else: - self._led.set( - is_on=True, - brightness=_from_hass_brightness(self._brightness), - color=_from_hass_color(self._color), - ) - - self._is_on = True - self.schedule_update_ha_state() - - -def _from_hass_brightness(brightness): - """Convert Home Assistant brightness units to percentage.""" - return brightness / 255 - - -def _from_hass_color(color): - """Convert Home Assistant RGB list to Color tuple.""" - - rgb = color_util.color_hs_to_RGB(*color) - return Color(*tuple(rgb)) diff --git a/homeassistant/components/rpi_gpio_pwm/manifest.json b/homeassistant/components/rpi_gpio_pwm/manifest.json deleted file mode 100644 index 403f607e379..00000000000 --- a/homeassistant/components/rpi_gpio_pwm/manifest.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "domain": "rpi_gpio_pwm", - "name": "pigpio Daemon PWM LED", - "documentation": "https://www.home-assistant.io/integrations/rpi_gpio_pwm", - "requirements": ["pwmled==1.6.10"], - "codeowners": ["@soldag"], - "iot_class": "local_push", - "loggers": ["adafruit_blinka", "adafruit_circuitpython_pca9685", "pwmled"] -} diff --git a/homeassistant/components/rpi_pfio/__init__.py b/homeassistant/components/rpi_pfio/__init__.py deleted file mode 100644 index c4687c59114..00000000000 --- a/homeassistant/components/rpi_pfio/__init__.py +++ /dev/null @@ -1,62 +0,0 @@ -"""Support for controlling the PiFace Digital I/O module on a RPi.""" -import logging - -import pifacedigitalio as PFIO - -from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP -from homeassistant.core import HomeAssistant -from homeassistant.helpers.typing import ConfigType - -DOMAIN = "rpi_pfio" - -DATA_PFIO_LISTENER = "pfio_listener" - -_LOGGER = logging.getLogger(__name__) - - -def setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up the Raspberry PI PFIO component.""" - _LOGGER.warning( - "The PiFace Digital I/O (PFIO) integration is deprecated and will be removed " - "in Home Assistant Core 2022.4; this integration is removed under " - "Architectural Decision Record 0019, more information can be found here: " - "https://github.com/home-assistant/architecture/blob/master/adr/0019-GPIO.md" - ) - - pifacedigital = PFIO.PiFaceDigital() - hass.data[DATA_PFIO_LISTENER] = PFIO.InputEventListener(chip=pifacedigital) - - def cleanup_pfio(event): - """Stuff to do before stopping.""" - PFIO.deinit() - - def prepare_pfio(event): - """Stuff to do when Home Assistant starts.""" - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, cleanup_pfio) - - hass.bus.listen_once(EVENT_HOMEASSISTANT_START, prepare_pfio) - PFIO.init() - - return True - - -def write_output(port, value): - """Write a value to a PFIO.""" - PFIO.digital_write(port, value) - - -def read_input(port): - """Read a value from a PFIO.""" - return PFIO.digital_read(port) - - -def edge_detect(hass, port, event_callback, settle): - """Add detection for RISING and FALLING events.""" - hass.data[DATA_PFIO_LISTENER].register( - port, PFIO.IODIR_BOTH, event_callback, settle_time=settle - ) - - -def activate_listener(hass): - """Activate the registered listener events.""" - hass.data[DATA_PFIO_LISTENER].activate() diff --git a/homeassistant/components/rpi_pfio/binary_sensor.py b/homeassistant/components/rpi_pfio/binary_sensor.py deleted file mode 100644 index ddce88949fd..00000000000 --- a/homeassistant/components/rpi_pfio/binary_sensor.py +++ /dev/null @@ -1,90 +0,0 @@ -"""Support for binary sensor using the PiFace Digital I/O module on a RPi.""" -from __future__ import annotations - -import voluptuous as vol - -from homeassistant.components import rpi_pfio -from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity -from homeassistant.const import CONF_NAME, DEVICE_DEFAULT_NAME -from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType - -CONF_INVERT_LOGIC = "invert_logic" -CONF_PORTS = "ports" -CONF_SETTLE_TIME = "settle_time" - -DEFAULT_INVERT_LOGIC = False -DEFAULT_SETTLE_TIME = 20 - -PORT_SCHEMA = vol.Schema( - { - vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_SETTLE_TIME, default=DEFAULT_SETTLE_TIME): cv.positive_int, - vol.Optional(CONF_INVERT_LOGIC, default=DEFAULT_INVERT_LOGIC): cv.boolean, - } -) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - {vol.Optional(CONF_PORTS, default={}): vol.Schema({cv.positive_int: PORT_SCHEMA})} -) - - -def setup_platform( - hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the PiFace Digital Input devices.""" - binary_sensors = [] - ports = config[CONF_PORTS] - for port, port_entity in ports.items(): - name = port_entity.get(CONF_NAME) - settle_time = port_entity[CONF_SETTLE_TIME] / 1000 - invert_logic = port_entity[CONF_INVERT_LOGIC] - - binary_sensors.append( - RPiPFIOBinarySensor(hass, port, name, settle_time, invert_logic) - ) - add_entities(binary_sensors, True) - - rpi_pfio.activate_listener(hass) - - -class RPiPFIOBinarySensor(BinarySensorEntity): - """Represent a binary sensor that a PiFace Digital Input.""" - - def __init__(self, hass, port, name, settle_time, invert_logic): - """Initialize the RPi binary sensor.""" - self._port = port - self._name = name or DEVICE_DEFAULT_NAME - self._invert_logic = invert_logic - self._state = None - - def read_pfio(port): - """Read state from PFIO.""" - self._state = rpi_pfio.read_input(self._port) - self.schedule_update_ha_state() - - rpi_pfio.edge_detect(hass, self._port, read_pfio, settle_time) - - @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 - - def update(self): - """Update the PFIO state.""" - self._state = rpi_pfio.read_input(self._port) diff --git a/homeassistant/components/rpi_pfio/manifest.json b/homeassistant/components/rpi_pfio/manifest.json deleted file mode 100644 index 7f72a7ba77d..00000000000 --- a/homeassistant/components/rpi_pfio/manifest.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "domain": "rpi_pfio", - "name": "PiFace Digital I/O (PFIO)", - "documentation": "https://www.home-assistant.io/integrations/rpi_pfio", - "requirements": ["pifacecommon==4.2.2", "pifacedigitalio==3.0.5"], - "codeowners": [], - "iot_class": "local_push", - "loggers": ["pifacedigitalio"] -} diff --git a/homeassistant/components/rpi_pfio/switch.py b/homeassistant/components/rpi_pfio/switch.py deleted file mode 100644 index ca3d176eecb..00000000000 --- a/homeassistant/components/rpi_pfio/switch.py +++ /dev/null @@ -1,85 +0,0 @@ -"""Support for switches using the PiFace Digital I/O module on a RPi.""" -from __future__ import annotations - -import voluptuous as vol - -from homeassistant.components import rpi_pfio -from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity -from homeassistant.const import ATTR_NAME, DEVICE_DEFAULT_NAME -from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType - -ATTR_INVERT_LOGIC = "invert_logic" - -CONF_PORTS = "ports" - -DEFAULT_INVERT_LOGIC = False - -PORT_SCHEMA = vol.Schema( - { - vol.Optional(ATTR_NAME): cv.string, - vol.Optional(ATTR_INVERT_LOGIC, default=DEFAULT_INVERT_LOGIC): cv.boolean, - } -) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - {vol.Optional(CONF_PORTS, default={}): vol.Schema({cv.positive_int: PORT_SCHEMA})} -) - - -def setup_platform( - hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the PiFace Digital Output devices.""" - switches = [] - ports = config[CONF_PORTS] - for port, port_entity in ports.items(): - name = port_entity.get(ATTR_NAME) - invert_logic = port_entity[ATTR_INVERT_LOGIC] - - switches.append(RPiPFIOSwitch(port, name, invert_logic)) - add_entities(switches) - - -class RPiPFIOSwitch(SwitchEntity): - """Representation of a PiFace Digital Output.""" - - def __init__(self, port, name, invert_logic): - """Initialize the pin.""" - self._port = port - self._name = name or DEVICE_DEFAULT_NAME - self._invert_logic = invert_logic - self._state = False - rpi_pfio.write_output(self._port, 1 if self._invert_logic else 0) - - @property - def name(self): - """Return the name of the switch.""" - return self._name - - @property - def should_poll(self): - """Return the polling state.""" - 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.""" - rpi_pfio.write_output(self._port, 0 if self._invert_logic else 1) - self._state = True - self.schedule_update_ha_state() - - def turn_off(self, **kwargs): - """Turn the device off.""" - rpi_pfio.write_output(self._port, 1 if self._invert_logic else 0) - self._state = False - self.schedule_update_ha_state() diff --git a/homeassistant/components/rpi_power/strings.json b/homeassistant/components/rpi_power/strings.json index a9cd6c2d907..9a46ca1e10e 100644 --- a/homeassistant/components/rpi_power/strings.json +++ b/homeassistant/components/rpi_power/strings.json @@ -11,4 +11,4 @@ "no_devices_found": "Can't find the system class needed for this component, make sure that your kernel is recent and the hardware is supported" } } -} \ No newline at end of file +} diff --git a/homeassistant/components/rpi_power/translations/fr.json b/homeassistant/components/rpi_power/translations/fr.json index 7e4fd715ee0..d6226259b9d 100644 --- a/homeassistant/components/rpi_power/translations/fr.json +++ b/homeassistant/components/rpi_power/translations/fr.json @@ -6,7 +6,7 @@ }, "step": { "confirm": { - "description": "Voulez-vous commencer la configuration ?" + "description": "Voulez-vous commencer la configuration\u00a0?" } } }, diff --git a/homeassistant/components/rpi_power/translations/he.json b/homeassistant/components/rpi_power/translations/he.json index a4e4e475087..efaad0a039b 100644 --- a/homeassistant/components/rpi_power/translations/he.json +++ b/homeassistant/components/rpi_power/translations/he.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "no_devices_found": "\u05d0\u05d9\u05df \u05d0\u05e4\u05e9\u05e8\u05d5\u05ea \u05dc\u05de\u05e6\u05d5\u05d0 \u05d0\u05ea \u05de\u05d7\u05dc\u05e7\u05ea \u05d4\u05de\u05e2\u05e8\u05db\u05ea \u05d4\u05d3\u05e8\u05d5\u05e9\u05d4 \u05dc\u05e8\u05db\u05d9\u05d1 \u05d6\u05d4, \u05d9\u05e9 \u05dc\u05d5\u05d5\u05d3\u05d0 \u05e9\u05d4\u05dc\u05d9\u05d1\u05d4 \u05e9\u05dc\u05da \u05e2\u05d3\u05db\u05e0\u05d9\u05ea \u05d5\u05d4\u05d7\u05d5\u05de\u05e8\u05d4 \u05e0\u05ea\u05de\u05db\u05ea", "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." }, "step": { diff --git a/homeassistant/components/rpi_rf/__init__.py b/homeassistant/components/rpi_rf/__init__.py deleted file mode 100644 index 6e4d58099d9..00000000000 --- a/homeassistant/components/rpi_rf/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The rpi_rf component.""" diff --git a/homeassistant/components/rpi_rf/manifest.json b/homeassistant/components/rpi_rf/manifest.json deleted file mode 100644 index 022c84eb13f..00000000000 --- a/homeassistant/components/rpi_rf/manifest.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "domain": "rpi_rf", - "name": "Raspberry Pi RF", - "documentation": "https://www.home-assistant.io/integrations/rpi_rf", - "requirements": ["rpi-rf==0.9.7", "RPi.GPIO==0.7.1a4"], - "codeowners": [], - "iot_class": "assumed_state" -} diff --git a/homeassistant/components/rpi_rf/switch.py b/homeassistant/components/rpi_rf/switch.py deleted file mode 100644 index e82b2cb12b1..00000000000 --- a/homeassistant/components/rpi_rf/switch.py +++ /dev/null @@ -1,153 +0,0 @@ -"""Support for a switch using a 433MHz module via GPIO on a Raspberry Pi.""" -from __future__ import annotations - -import importlib -import logging -from threading import RLock - -import voluptuous as vol - -from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity -from homeassistant.const import ( - CONF_NAME, - CONF_PROTOCOL, - CONF_SWITCHES, - EVENT_HOMEASSISTANT_STOP, -) -from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType - -_LOGGER = logging.getLogger(__name__) - -CONF_CODE_OFF = "code_off" -CONF_CODE_ON = "code_on" -CONF_GPIO = "gpio" -CONF_PULSELENGTH = "pulselength" -CONF_SIGNAL_REPETITIONS = "signal_repetitions" - -DEFAULT_PROTOCOL = 1 -DEFAULT_SIGNAL_REPETITIONS = 10 - -SWITCH_SCHEMA = vol.Schema( - { - vol.Required(CONF_CODE_OFF): vol.All(cv.ensure_list_csv, [cv.positive_int]), - vol.Required(CONF_CODE_ON): vol.All(cv.ensure_list_csv, [cv.positive_int]), - vol.Optional(CONF_PULSELENGTH): cv.positive_int, - vol.Optional( - CONF_SIGNAL_REPETITIONS, default=DEFAULT_SIGNAL_REPETITIONS - ): cv.positive_int, - vol.Optional(CONF_PROTOCOL, default=DEFAULT_PROTOCOL): cv.positive_int, - } -) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_GPIO): cv.positive_int, - vol.Required(CONF_SWITCHES): vol.Schema({cv.string: SWITCH_SCHEMA}), - } -) - - -def setup_platform( - hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Find and return switches controlled by a generic RF device via GPIO.""" - _LOGGER.warning( - "The Raspberry Pi RF integration is deprecated and will be removed " - "in Home Assistant Core 2022.4; this integration is removed under " - "Architectural Decision Record 0019, more information can be found here: " - "https://github.com/home-assistant/architecture/blob/master/adr/0019-GPIO.md" - ) - rpi_rf = importlib.import_module("rpi_rf") - - gpio = config[CONF_GPIO] - rfdevice = rpi_rf.RFDevice(gpio) - rfdevice_lock = RLock() - switches = config[CONF_SWITCHES] - - devices = [] - for dev_name, properties in switches.items(): - devices.append( - RPiRFSwitch( - properties.get(CONF_NAME, dev_name), - rfdevice, - rfdevice_lock, - properties.get(CONF_PROTOCOL), - properties.get(CONF_PULSELENGTH), - properties.get(CONF_SIGNAL_REPETITIONS), - properties.get(CONF_CODE_ON), - properties.get(CONF_CODE_OFF), - ) - ) - if devices: - rfdevice.enable_tx() - - add_entities(devices) - - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, lambda event: rfdevice.cleanup()) - - -class RPiRFSwitch(SwitchEntity): - """Representation of a GPIO RF switch.""" - - def __init__( - self, - name, - rfdevice, - lock, - protocol, - pulselength, - signal_repetitions, - code_on, - code_off, - ): - """Initialize the switch.""" - self._name = name - self._state = False - self._rfdevice = rfdevice - self._lock = lock - self._protocol = protocol - self._pulselength = pulselength - self._code_on = code_on - self._code_off = code_off - self._rfdevice.tx_repeat = signal_repetitions - - @property - def should_poll(self): - """No polling needed.""" - return False - - @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 - - def _send_code(self, code_list, protocol, pulselength): - """Send the code(s) with a specified pulselength.""" - with self._lock: - _LOGGER.info("Sending code(s): %s", code_list) - for code in code_list: - self._rfdevice.tx_code(code, protocol, pulselength) - return True - - def turn_on(self, **kwargs): - """Turn the switch on.""" - if self._send_code(self._code_on, self._protocol, self._pulselength): - self._state = True - self.schedule_update_ha_state() - - def turn_off(self, **kwargs): - """Turn the switch off.""" - if self._send_code(self._code_off, self._protocol, self._pulselength): - self._state = False - self.schedule_update_ha_state() diff --git a/homeassistant/components/rss_feed_template/manifest.json b/homeassistant/components/rss_feed_template/manifest.json index 46b449b03dd..ad0ab14cadd 100644 --- a/homeassistant/components/rss_feed_template/manifest.json +++ b/homeassistant/components/rss_feed_template/manifest.json @@ -3,7 +3,7 @@ "name": "RSS Feed Template", "documentation": "https://www.home-assistant.io/integrations/rss_feed_template", "dependencies": ["http"], - "codeowners": [], + "codeowners": ["@home-assistant/core"], "quality_scale": "internal", "iot_class": "local_push" } diff --git a/homeassistant/components/rtsp_to_webrtc/config_flow.py b/homeassistant/components/rtsp_to_webrtc/config_flow.py index 32735f3e824..815c5e5db7b 100644 --- a/homeassistant/components/rtsp_to_webrtc/config_flow.py +++ b/homeassistant/components/rtsp_to_webrtc/config_flow.py @@ -97,7 +97,6 @@ class RTSPToWebRTCConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_show_form( step_id="hassio_confirm", description_placeholders={"addon": self._hassio_discovery["addon"]}, - data_schema=vol.Schema({}), errors=errors, ) diff --git a/homeassistant/components/rtsp_to_webrtc/manifest.json b/homeassistant/components/rtsp_to_webrtc/manifest.json index d3a56ebdee6..bf11e7c00bc 100644 --- a/homeassistant/components/rtsp_to_webrtc/manifest.json +++ b/homeassistant/components/rtsp_to_webrtc/manifest.json @@ -5,9 +5,7 @@ "documentation": "https://www.home-assistant.io/integrations/rtsp_to_webrtc", "requirements": ["rtsp-to-webrtc==0.5.0"], "dependencies": ["camera"], - "codeowners": [ - "@allenporter" - ], + "codeowners": ["@allenporter"], "iot_class": "local_push", "loggers": ["rtsp_to_webrtc"] } diff --git a/homeassistant/components/rtsp_to_webrtc/translations/fr.json b/homeassistant/components/rtsp_to_webrtc/translations/fr.json index 1235f36d26a..e51207a6254 100644 --- a/homeassistant/components/rtsp_to_webrtc/translations/fr.json +++ b/homeassistant/components/rtsp_to_webrtc/translations/fr.json @@ -3,7 +3,7 @@ "abort": { "server_failure": "Le serveur RTSPtoWebRTC a renvoy\u00e9 une erreur. Consultez les journaux pour plus d'informations.", "server_unreachable": "Impossible de communiquer avec le serveur RTSPtoWebRTC. Consultez les journaux pour plus d'informations.", - "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible" + "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." }, "error": { "invalid_url": "Doit \u00eatre une URL de serveur RTSPtoWebRTC valide, par exemple https://example.com", diff --git a/homeassistant/components/ruckus_unleashed/translations/fr.json b/homeassistant/components/ruckus_unleashed/translations/fr.json index 45620fe7795..bb317c2149f 100644 --- a/homeassistant/components/ruckus_unleashed/translations/fr.json +++ b/homeassistant/components/ruckus_unleashed/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/samsungtv/__init__.py b/homeassistant/components/samsungtv/__init__.py index 508ed2f876f..dae4033ad4c 100644 --- a/homeassistant/components/samsungtv/__init__.py +++ b/homeassistant/components/samsungtv/__init__.py @@ -1,19 +1,23 @@ """The Samsung TV integration.""" from __future__ import annotations +from collections.abc import Mapping from functools import partial import socket from typing import Any +from urllib.parse import urlparse import getmac import voluptuous as vol from homeassistant import config_entries +from homeassistant.components import ssdp from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_HOST, CONF_MAC, CONF_METHOD, + CONF_MODEL, CONF_NAME, CONF_PORT, CONF_TOKEN, @@ -21,25 +25,26 @@ from homeassistant.const import ( Platform, ) from homeassistant.core import Event, HomeAssistant, callback -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.typing import ConfigType -from .bridge import ( - SamsungTVBridge, - SamsungTVLegacyBridge, - SamsungTVWSBridge, - async_get_device_info, - mac_from_device_info, -) +from .bridge import SamsungTVBridge, async_get_device_info, mac_from_device_info from .const import ( CONF_ON_ACTION, + CONF_SESSION_ID, + CONF_SSDP_MAIN_TV_AGENT_LOCATION, + CONF_SSDP_RENDERING_CONTROL_LOCATION, DEFAULT_NAME, DOMAIN, + ENTRY_RELOAD_COOLDOWN, LEGACY_PORT, LOGGER, + METHOD_ENCRYPTED_WEBSOCKET, METHOD_LEGACY, - METHOD_WEBSOCKET, + UPNP_SVC_MAIN_TV_AGENT, + UPNP_SVC_RENDERING_CONTROL, ) @@ -101,90 +106,187 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: @callback def _async_get_device_bridge( hass: HomeAssistant, data: dict[str, Any] -) -> SamsungTVLegacyBridge | SamsungTVWSBridge: +) -> SamsungTVBridge: """Get device bridge.""" return SamsungTVBridge.get_bridge( hass, data[CONF_METHOD], data[CONF_HOST], data[CONF_PORT], - data.get(CONF_TOKEN), + data, ) +class DebouncedEntryReloader: + """Reload only after the timer expires.""" + + def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: + """Init the debounced entry reloader.""" + self.hass = hass + self.entry = entry + self.token = self.entry.data.get(CONF_TOKEN) + self._debounced_reload = Debouncer( + hass, + LOGGER, + cooldown=ENTRY_RELOAD_COOLDOWN, + immediate=False, + function=self._async_reload_entry, + ) + + async def async_call(self, hass: HomeAssistant, entry: ConfigEntry) -> None: + """Start the countdown for a reload.""" + if (new_token := entry.data.get(CONF_TOKEN)) != self.token: + LOGGER.debug("Skipping reload as its a token update") + self.token = new_token + return # Token updates should not trigger a reload + LOGGER.debug("Calling debouncer to get a reload after cooldown") + await self._debounced_reload.async_call() + + @callback + def async_cancel(self) -> None: + """Cancel any pending reload.""" + self._debounced_reload.async_cancel() + + async def _async_reload_entry(self) -> None: + """Reload entry.""" + LOGGER.debug("Reloading entry %s", self.entry.title) + await self.hass.config_entries.async_reload(self.entry.entry_id) + + +async def _async_update_ssdp_locations(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Update ssdp locations from discovery cache.""" + updates = {} + for ssdp_st, key in ( + (UPNP_SVC_RENDERING_CONTROL, CONF_SSDP_RENDERING_CONTROL_LOCATION), + (UPNP_SVC_MAIN_TV_AGENT, CONF_SSDP_MAIN_TV_AGENT_LOCATION), + ): + for discovery_info in await ssdp.async_get_discovery_info_by_st(hass, ssdp_st): + location = discovery_info.ssdp_location + host = urlparse(location).hostname + if host == entry.data[CONF_HOST]: + updates[key] = location + break + + if updates: + hass.config_entries.async_update_entry(entry, data={**entry.data, **updates}) + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up the Samsung TV platform.""" # Initialize bridge + if entry.data.get(CONF_METHOD) == METHOD_ENCRYPTED_WEBSOCKET: + if not entry.data.get(CONF_TOKEN) or not entry.data.get(CONF_SESSION_ID): + raise ConfigEntryAuthFailed( + "Token and session id are required in encrypted mode" + ) bridge = await _async_create_bridge_with_updated_data(hass, entry) - # Ensure new token gets saved against the config_entry + # Ensure updates get saved against the config_entry @callback - def _update_token() -> None: + def _update_config_entry(updates: Mapping[str, Any]) -> None: """Update config entry with the new token.""" - hass.config_entries.async_update_entry( - entry, data={**entry.data, CONF_TOKEN: bridge.token} - ) + hass.config_entries.async_update_entry(entry, data={**entry.data, **updates}) - def new_token_callback() -> None: - """Update config entry with the new token.""" - hass.add_job(_update_token) - - bridge.register_new_token_callback(new_token_callback) + bridge.register_update_config_entry_callback(_update_config_entry) async def stop_bridge(event: Event) -> None: """Stop SamsungTV bridge connection.""" - await bridge.async_stop() + LOGGER.debug("Stopping SamsungTVBridge %s", bridge.host) + await bridge.async_close_remote() entry.async_on_unload( hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_bridge) ) + await _async_update_ssdp_locations(hass, entry) + + # We must not await after we setup the reload or there + # will be a race where the config flow will see the entry + # as not loaded and may reload it + debounced_reloader = DebouncedEntryReloader(hass, entry) + entry.async_on_unload(debounced_reloader.async_cancel) + entry.async_on_unload(entry.add_update_listener(debounced_reloader.async_call)) + hass.data[DOMAIN][entry.entry_id] = bridge hass.config_entries.async_setup_platforms(entry, PLATFORMS) + return True +def _model_requires_encryption(model: str | None) -> bool: + """H and J models need pairing with PIN.""" + return model is not None and len(model) > 4 and model[4] in ("H", "J") + + async def _async_create_bridge_with_updated_data( hass: HomeAssistant, entry: ConfigEntry -) -> SamsungTVLegacyBridge | SamsungTVWSBridge: +) -> SamsungTVBridge: """Create a bridge object and update any missing data in the config entry.""" - updated_data = {} - host = entry.data[CONF_HOST] - port = entry.data.get(CONF_PORT) - method = entry.data.get(CONF_METHOD) - info = None + updated_data: dict[str, str | int] = {} + host: str = entry.data[CONF_HOST] + port: int | None = entry.data.get(CONF_PORT) + method: str | None = entry.data.get(CONF_METHOD) + load_info_attempted = False + info: dict[str, Any] | None = None if not port or not method: + LOGGER.debug("Attempting to get port or method for %s", host) if method == METHOD_LEGACY: port = LEGACY_PORT else: # When we imported from yaml we didn't setup the method # because we didn't know it - port, method, info = await async_get_device_info(hass, None, host) - if not port: + _result, port, method, info = await async_get_device_info(hass, host) + load_info_attempted = True + if not port or not method: raise ConfigEntryNotReady( "Failed to determine connection method, make sure the device is on." ) + LOGGER.info("Updated port to %s and method to %s for %s", port, method, host) updated_data[CONF_PORT] = port updated_data[CONF_METHOD] = method bridge = _async_get_device_bridge(hass, {**entry.data, **updated_data}) - mac = entry.data.get(CONF_MAC) - if not mac and bridge.method == METHOD_WEBSOCKET: - if info: - mac = mac_from_device_info(info) - else: - mac = await bridge.async_mac_from_device() + mac: str | None = entry.data.get(CONF_MAC) + model: str | None = entry.data.get(CONF_MODEL) + if (not mac or not model) and not load_info_attempted: + info = await bridge.async_device_info() if not mac: - mac = await hass.async_add_executor_job( - partial(getmac.get_mac_address, ip=host) + LOGGER.debug("Attempting to get mac for %s", host) + if info: + mac = mac_from_device_info(info) + + if not mac: + mac = await hass.async_add_executor_job( + partial(getmac.get_mac_address, ip=host) + ) + + if mac: + LOGGER.info("Updated mac to %s for %s", mac, host) + updated_data[CONF_MAC] = mac + else: + LOGGER.info("Failed to get mac for %s", host) + + if not model: + LOGGER.debug("Attempting to get model for %s", host) + if info: + model = info.get("device", {}).get("modelName") + if model: + LOGGER.info("Updated model to %s for %s", model, host) + updated_data[CONF_MODEL] = model + + if _model_requires_encryption(model) and method != METHOD_ENCRYPTED_WEBSOCKET: + LOGGER.info( + "Detected model %s for %s. Some televisions from H and J series use " + "an encrypted protocol but you are using %s which may not be supported", + model, + host, + method, ) - if mac: - updated_data[CONF_MAC] = mac if updated_data: data = {**entry.data, **updated_data} @@ -197,7 +299,9 @@ 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: - await hass.data[DOMAIN][entry.entry_id].async_stop() + bridge: SamsungTVBridge = hass.data[DOMAIN][entry.entry_id] + LOGGER.debug("Stopping SamsungTVBridge %s", bridge.host) + await bridge.async_close_remote() return unload_ok diff --git a/homeassistant/components/samsungtv/bridge.py b/homeassistant/components/samsungtv/bridge.py index 616820aec26..52ab86337dd 100644 --- a/homeassistant/components/samsungtv/bridge.py +++ b/homeassistant/components/samsungtv/bridge.py @@ -2,38 +2,64 @@ from __future__ import annotations from abc import ABC, abstractmethod +import asyncio +from asyncio.exceptions import TimeoutError as AsyncioTimeoutError +from collections.abc import Callable, Iterable, Mapping import contextlib -from typing import Any +from typing import Any, Generic, TypeVar, cast -from requests.exceptions import Timeout as RequestsTimeout from samsungctl import Remote from samsungctl.exceptions import AccessDenied, ConnectionClosed, UnhandledResponse -from samsungtvws import SamsungTVWS -from samsungtvws.exceptions import ConnectionFailure, HttpApiError -from websocket import WebSocketException, WebSocketTimeoutException +from samsungtvws.async_remote import SamsungTVWSAsyncRemote +from samsungtvws.async_rest import SamsungTVAsyncRest +from samsungtvws.command import SamsungTVCommand +from samsungtvws.encrypted.command import SamsungTVEncryptedCommand +from samsungtvws.encrypted.remote import ( + SamsungTVEncryptedWSAsyncRemote, + SendRemoteKey as SendEncryptedRemoteKey, +) +from samsungtvws.event import ( + ED_INSTALLED_APP_EVENT, + MS_ERROR_EVENT, + parse_installed_app, +) +from samsungtvws.exceptions import ( + ConnectionFailure, + HttpApiError, + ResponseError, + UnauthorizedError, +) +from samsungtvws.remote import ChannelEmitCommand, SendRemoteKey +from websockets.exceptions import ConnectionClosedError, WebSocketException from homeassistant.const import ( CONF_HOST, CONF_ID, CONF_METHOD, + CONF_MODEL, CONF_NAME, CONF_PORT, CONF_TIMEOUT, CONF_TOKEN, ) from homeassistant.core import CALLBACK_TYPE, HomeAssistant +from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.device_registry import format_mac from .const import ( CONF_DESCRIPTION, + CONF_SESSION_ID, + ENCRYPTED_WEBSOCKET_PORT, LEGACY_PORT, LOGGER, + METHOD_ENCRYPTED_WEBSOCKET, METHOD_LEGACY, METHOD_WEBSOCKET, RESULT_AUTH_MISSING, RESULT_CANNOT_CONNECT, RESULT_NOT_SUPPORTED, RESULT_SUCCESS, + SUCCESSFUL_RESULTS, TIMEOUT_REQUEST, TIMEOUT_WEBSOCKET, VALUE_CONF_ID, @@ -41,35 +67,59 @@ from .const import ( WEBSOCKET_PORTS, ) +KEY_PRESS_TIMEOUT = 1.2 + +ENCRYPTED_MODEL_USES_POWER_OFF = {"H6400"} +ENCRYPTED_MODEL_USES_POWER = {"JU6400", "JU641D"} + +REST_EXCEPTIONS = (HttpApiError, AsyncioTimeoutError, ResponseError) + +_TRemote = TypeVar("_TRemote", SamsungTVWSAsyncRemote, SamsungTVEncryptedWSAsyncRemote) +_TCommand = TypeVar("_TCommand", SamsungTVCommand, SamsungTVEncryptedCommand) + def mac_from_device_info(info: dict[str, Any]) -> str | None: """Extract the mac address from the device info.""" - dev_info = info.get("device", {}) - if dev_info.get("networkType") == "wireless" and dev_info.get("wifiMac"): - return format_mac(dev_info["wifiMac"]) + if wifi_mac := info.get("device", {}).get("wifiMac"): + return format_mac(wifi_mac) return None async def async_get_device_info( hass: HomeAssistant, - bridge: SamsungTVWSBridge | SamsungTVLegacyBridge | None, host: str, -) -> tuple[int | None, str | None, dict[str, Any] | None]: +) -> tuple[str, int | None, str | None, dict[str, Any] | None]: """Fetch the port, method, and device info.""" - if bridge and bridge.port: - return bridge.port, bridge.method, await bridge.async_device_info() - + # Try the websocket ssl and non-ssl ports for port in WEBSOCKET_PORTS: bridge = SamsungTVBridge.get_bridge(hass, METHOD_WEBSOCKET, host, port) if info := await bridge.async_device_info(): - return port, METHOD_WEBSOCKET, info + LOGGER.debug( + "Fetching rest info via %s was successful: %s, checking for encrypted", + port, + info, + ) + encrypted_bridge = SamsungTVEncryptedBridge( + hass, METHOD_ENCRYPTED_WEBSOCKET, host, ENCRYPTED_WEBSOCKET_PORT + ) + result = await encrypted_bridge.async_try_connect() + if result != RESULT_CANNOT_CONNECT: + return ( + result, + ENCRYPTED_WEBSOCKET_PORT, + METHOD_ENCRYPTED_WEBSOCKET, + info, + ) + return RESULT_SUCCESS, port, METHOD_WEBSOCKET, info + # Try legacy port bridge = SamsungTVBridge.get_bridge(hass, METHOD_LEGACY, host, LEGACY_PORT) result = await bridge.async_try_connect() - if result in (RESULT_SUCCESS, RESULT_AUTH_MISSING): - return LEGACY_PORT, METHOD_LEGACY, None + if result in SUCCESSFUL_RESULTS: + return result, LEGACY_PORT, METHOD_LEGACY, await bridge.async_device_info() - return None, None, None + # Failed to get info + return result, None, None, None class SamsungTVBridge(ABC): @@ -81,12 +131,14 @@ class SamsungTVBridge(ABC): method: str, host: str, port: int | None = None, - token: str | None = None, - ) -> SamsungTVLegacyBridge | SamsungTVWSBridge: + entry_data: Mapping[str, Any] | None = None, + ) -> SamsungTVBridge: """Get Bridge instance.""" if method == METHOD_LEGACY or port == LEGACY_PORT: return SamsungTVLegacyBridge(hass, method, host, port) - return SamsungTVWSBridge(hass, method, host, port, token) + if method == METHOD_ENCRYPTED_WEBSOCKET or port == ENCRYPTED_WEBSOCKET_PORT: + return SamsungTVEncryptedBridge(hass, method, host, port, entry_data) + return SamsungTVWSBridge(hass, method, host, port, entry_data) def __init__( self, hass: HomeAssistant, method: str, host: str, port: int | None = None @@ -97,104 +149,81 @@ class SamsungTVBridge(ABC): self.method = method self.host = host self.token: str | None = None - self._remote: Remote | None = None + self.session_id: str | None = None self._reauth_callback: CALLBACK_TYPE | None = None - self._new_token_callback: CALLBACK_TYPE | None = None + self._update_config_entry: Callable[[Mapping[str, Any]], None] | None = None + self._app_list_callback: Callable[[dict[str, str]], None] | None = None def register_reauth_callback(self, func: CALLBACK_TYPE) -> None: """Register a callback function.""" self._reauth_callback = func - def register_new_token_callback(self, func: CALLBACK_TYPE) -> None: + def register_update_config_entry_callback( + self, func: Callable[[Mapping[str, Any]], None] + ) -> None: """Register a callback function.""" - self._new_token_callback = func + self._update_config_entry = func + + def register_app_list_callback( + self, func: Callable[[dict[str, str]], None] + ) -> None: + """Register app_list callback function.""" + self._app_list_callback = func @abstractmethod - async def async_try_connect(self) -> str | None: + async def async_try_connect(self) -> str: """Try to connect to the TV.""" @abstractmethod async def async_device_info(self) -> dict[str, Any] | None: """Try to gather infos of this TV.""" - @abstractmethod - async def async_mac_from_device(self) -> str | None: - """Try to fetch the mac address of the TV.""" + async def async_request_app_list(self) -> None: + """Request app list.""" + # Overridden in SamsungTVWSBridge + LOGGER.debug( + "App list request is not supported on %s TV: %s", + self.method, + self.host, + ) + self._notify_app_list_callback({}) @abstractmethod - async def async_get_app_list(self) -> dict[str, str] | None: - """Get installed app list.""" - async def async_is_on(self) -> bool: """Tells if the TV is on.""" - if self._remote is not None: - await self.async_close_remote() - - try: - remote = await self.hass.async_add_executor_job(self._get_remote) - return remote is not None - except ( - UnhandledResponse, - AccessDenied, - ConnectionFailure, - ): - # We got a response so it's working. - return True - except OSError: - # Different reasons, e.g. hostname not resolveable - return False - - async def async_send_key(self, key: str, key_type: str | None = None) -> None: - """Send a key to the tv and handles exceptions.""" - try: - # recreate connection if connection was dead - retry_count = 1 - for _ in range(retry_count + 1): - try: - await self._async_send_key(key, key_type) - break - except ( - ConnectionClosed, - BrokenPipeError, - WebSocketException, - ): - # BrokenPipe can occur when the commands is sent to fast - # WebSocketException can occur when timed out - self._remote = None - except (UnhandledResponse, AccessDenied): - # We got a response so it's on. - LOGGER.debug("Failed sending command %s", key, exc_info=True) - except OSError: - # Different reasons, e.g. hostname not resolveable - pass @abstractmethod - async def _async_send_key(self, key: str, key_type: str | None = None) -> None: - """Send the key.""" + async def async_send_keys(self, keys: list[str]) -> None: + """Send a list of keys to the tv.""" + + async def async_power_off(self) -> None: + """Send power off command to remote and close.""" + await self._async_send_power_off() + # Force closing of remote session to provide instant UI feedback + await self.async_close_remote() @abstractmethod - def _get_remote(self, avoid_open: bool = False) -> Remote | SamsungTVWS: - """Get Remote object.""" + async def _async_send_power_off(self) -> None: + """Send power off command.""" + @abstractmethod async def async_close_remote(self) -> None: """Close remote object.""" - try: - if self._remote is not None: - # Close the current remote connection - await self.hass.async_add_executor_job(self._remote.close) - self._remote = None - except OSError: - LOGGER.debug("Could not establish connection") def _notify_reauth_callback(self) -> None: """Notify access denied callback.""" if self._reauth_callback is not None: self._reauth_callback() - def _notify_new_token_callback(self) -> None: - """Notify new token callback.""" - if self._new_token_callback is not None: - self._new_token_callback() + def _notify_update_config_entry(self, updates: Mapping[str, Any]) -> None: + """Notify update config callback.""" + if self._update_config_entry is not None: + self._update_config_entry(updates) + + def _notify_app_list_callback(self, app_list: dict[str, str]) -> None: + """Notify update config callback.""" + if self._app_list_callback is not None: + self._app_list_callback(app_list) class SamsungTVLegacyBridge(SamsungTVBridge): @@ -214,14 +243,22 @@ class SamsungTVLegacyBridge(SamsungTVBridge): CONF_PORT: None, CONF_TIMEOUT: 1, } + self._remote: Remote | None = None - async def async_mac_from_device(self) -> None: - """Try to fetch the mac address of the TV.""" - return None + async def async_is_on(self) -> bool: + """Tells if the TV is on.""" + return await self.hass.async_add_executor_job(self._is_on) - async def async_get_app_list(self) -> dict[str, str]: - """Get installed app list.""" - return {} + def _is_on(self) -> bool: + """Tells if the TV is on.""" + if self._remote is not None: + self._close_remote() + + try: + return self._get_remote() is not None + except (UnhandledResponse, AccessDenied): + # We got a response so it's working. + return True async def async_try_connect(self) -> str: """Try to connect to the Legacy TV.""" @@ -258,7 +295,7 @@ class SamsungTVLegacyBridge(SamsungTVBridge): """Try to gather infos of this device.""" return None - def _get_remote(self, avoid_open: bool = False) -> Remote: + def _get_remote(self) -> Remote: """Create or return a remote control instance.""" if self._remote is None: # We need to create a new instance to reconnect. @@ -276,23 +313,57 @@ class SamsungTVLegacyBridge(SamsungTVBridge): pass return self._remote - async def _async_send_key(self, key: str, key_type: str | None = None) -> None: - """Send the key using legacy protocol.""" - return await self.hass.async_add_executor_job(self._send_key, key) + async def async_send_keys(self, keys: list[str]) -> None: + """Send a list of keys using legacy protocol.""" + first_key = True + for key in keys: + if first_key: + first_key = False + else: + await asyncio.sleep(KEY_PRESS_TIMEOUT) + await self.hass.async_add_executor_job(self._send_key, key) def _send_key(self, key: str) -> None: - """Send the key using legacy protocol.""" - if remote := self._get_remote(): - remote.control(key) + """Send a key using legacy protocol.""" + try: + # recreate connection if connection was dead + retry_count = 1 + for _ in range(retry_count + 1): + try: + if remote := self._get_remote(): + remote.control(key) + break + except (ConnectionClosed, BrokenPipeError): + # BrokenPipe can occur when the commands is sent to fast + self._remote = None + except (UnhandledResponse, AccessDenied): + # We got a response so it's on. + LOGGER.debug("Failed sending command %s", key, exc_info=True) + except OSError: + # Different reasons, e.g. hostname not resolveable + pass - async def async_stop(self) -> None: - """Stop Bridge.""" - LOGGER.debug("Stopping SamsungTVLegacyBridge") - await self.async_close_remote() + async def _async_send_power_off(self) -> None: + """Send power off command to remote.""" + await self.async_send_keys(["KEY_POWEROFF"]) + + async def async_close_remote(self) -> None: + """Close remote object.""" + await self.hass.async_add_executor_job(self._close_remote) + + def _close_remote(self) -> None: + """Close remote object.""" + try: + if self._remote is not None: + # Close the current remote connection + self._remote.close() + self._remote = None + except OSError: + LOGGER.debug("Could not establish connection") -class SamsungTVWSBridge(SamsungTVBridge): - """The Bridge for WebSocket TVs.""" +class SamsungTVWSBaseBridge(SamsungTVBridge, Generic[_TRemote, _TCommand]): + """The Bridge for WebSocket TVs (v1/v2).""" def __init__( self, @@ -300,40 +371,106 @@ class SamsungTVWSBridge(SamsungTVBridge): method: str, host: str, port: int | None = None, - token: str | None = None, ) -> None: """Initialize Bridge.""" super().__init__(hass, method, host, port) - self.token = token - self._app_list: dict[str, str] | None = None + self._remote: _TRemote | None = None + self._remote_lock = asyncio.Lock() - async def async_mac_from_device(self) -> str | None: - """Try to fetch the mac address of the TV.""" - info = await self.async_device_info() - return mac_from_device_info(info) if info else None + async def async_is_on(self) -> bool: + """Tells if the TV is on.""" + LOGGER.debug("Checking if TV %s is on using websocket", self.host) + if remote := await self._async_get_remote(): + return remote.is_alive() # type: ignore[no-any-return] + return False - async def async_get_app_list(self) -> dict[str, str] | None: - """Get installed app list.""" - return await self.hass.async_add_executor_job(self._get_app_list) + async def _async_send_commands(self, commands: list[_TCommand]) -> None: + """Send the commands using websocket protocol.""" + try: + # recreate connection if connection was dead + retry_count = 1 + for _ in range(retry_count + 1): + try: + if remote := await self._async_get_remote(): + await remote.send_commands(commands) + break + except ( + BrokenPipeError, + WebSocketException, + ): + # BrokenPipe can occur when the commands is sent to fast + # WebSocketException can occur when timed out + self._remote = None + except OSError: + # Different reasons, e.g. hostname not resolveable + pass - def _get_app_list(self) -> dict[str, str] | None: - """Get installed app list.""" - if self._app_list is None and (remote := self._get_remote()): - with contextlib.suppress(TypeError, WebSocketTimeoutException): - raw_app_list: list[dict[str, str]] = remote.app_list() - LOGGER.debug("Received app list: %s", raw_app_list) - self._app_list = { - app["name"]: app["appId"] - for app in sorted(raw_app_list, key=lambda app: app["name"]) - } + async def _async_get_remote(self) -> _TRemote | None: + """Create or return a remote control instance.""" + if (remote := self._remote) and remote.is_alive(): + # If we have one then try to use it + return remote # type: ignore[no-any-return] - return self._app_list + async with self._remote_lock: + # If we don't have one make sure we do it under the lock + # so we don't make two do due a race to get the remote + return await self._async_get_remote_under_lock() + + @abstractmethod + async def _async_get_remote_under_lock(self) -> _TRemote | None: + """Create or return a remote control instance.""" + + async def async_close_remote(self) -> None: + """Close remote object.""" + try: + if self._remote is not None: + # Close the current remote connection + await self._remote.close() + self._remote = None + except OSError as err: + LOGGER.debug("Error closing connection to %s: %s", self.host, err) + + +class SamsungTVWSBridge( + SamsungTVWSBaseBridge[SamsungTVWSAsyncRemote, SamsungTVCommand] +): + """The Bridge for WebSocket TVs (v2).""" + + def __init__( + self, + hass: HomeAssistant, + method: str, + host: str, + port: int | None = None, + entry_data: Mapping[str, Any] | None = None, + ) -> None: + """Initialize Bridge.""" + super().__init__(hass, method, host, port) + if entry_data: + self.token = entry_data.get(CONF_TOKEN) + self._rest_api: SamsungTVAsyncRest | None = None + self._device_info: dict[str, Any] | None = None + + def _get_device_spec(self, key: str) -> Any | None: + """Check if a flag exists in latest device info.""" + if not ((info := self._device_info) and (device := info.get("device"))): + return None + return device.get(key) + + async def async_is_on(self) -> bool: + """Tells if the TV is on.""" + # On some TVs, opening a websocket turns on the TV + # so first check "PowerState" if device_info has it + # then fallback to default, trying to open a websocket + if self._get_device_spec("PowerState") is not None: + LOGGER.debug("Checking if TV %s is on using device info", self.host) + # Ensure we get an updated value + info = await self.async_device_info(force=True) + return info is not None and info["device"]["PowerState"] == "on" + + return await super().async_is_on() async def async_try_connect(self) -> str: - """Try to connect to the Websocket TV.""" - return await self.hass.async_add_executor_job(self._try_connect) - - def _try_connect(self) -> str: """Try to connect to the Websocket TV.""" for self.port in WEBSOCKET_PORTS: config = { @@ -348,26 +485,36 @@ class SamsungTVWSBridge(SamsungTVBridge): result = None try: LOGGER.debug("Try config: %s", config) - with SamsungTVWS( + async with SamsungTVWSAsyncRemote( host=self.host, port=self.port, token=self.token, - timeout=config[CONF_TIMEOUT], - name=config[CONF_NAME], + timeout=TIMEOUT_REQUEST, + name=VALUE_CONF_NAME, ) as remote: - remote.open("samsung.remote.control") + await remote.open() self.token = remote.token - if self.token is None: - config[CONF_TOKEN] = "*****" LOGGER.debug("Working config: %s", config) return RESULT_SUCCESS + except ConnectionClosedError as err: + LOGGER.info( + "Working but unsupported config: %s, error: '%s'; this may " + "be an indication that access to the TV has been denied. Please " + "check the Device Connection Manager on your TV", + config, + err, + ) + result = RESULT_NOT_SUPPORTED except WebSocketException as err: LOGGER.debug( "Working but unsupported config: %s, error: %s", config, err ) result = RESULT_NOT_SUPPORTED - except (OSError, ConnectionFailure) as err: - LOGGER.debug("Failing config: %s, error: %s", config, err) + except UnauthorizedError as err: + LOGGER.debug("Failing config: %s, %s error: %s", config, type(err), err) + return RESULT_AUTH_MISSING + except (ConnectionFailure, OSError, AsyncioTimeoutError) as err: + LOGGER.debug("Failing config: %s, %s error: %s", config, type(err), err) # pylint: disable=useless-else-on-loop else: if result: @@ -375,70 +522,264 @@ class SamsungTVWSBridge(SamsungTVBridge): return RESULT_CANNOT_CONNECT - async def async_device_info(self) -> dict[str, Any] | None: + async def async_device_info(self, force: bool = False) -> dict[str, Any] | None: """Try to gather infos of this TV.""" - if remote := self._get_remote(avoid_open=True): - with contextlib.suppress(HttpApiError, RequestsTimeout): - device_info: dict[str, Any] = await self.hass.async_add_executor_job( - remote.rest_device_info - ) - return device_info + if self._rest_api is None: + assert self.port + rest_api = SamsungTVAsyncRest( + host=self.host, + session=async_get_clientsession(self.hass), + port=self.port, + timeout=TIMEOUT_WEBSOCKET, + ) - return None + with contextlib.suppress(*REST_EXCEPTIONS): + device_info: dict[str, Any] = await rest_api.rest_device_info() + LOGGER.debug("Device info on %s is: %s", self.host, device_info) + self._device_info = device_info + return device_info - async def _async_send_key(self, key: str, key_type: str | None = None) -> None: - """Send the key using websocket protocol.""" - return await self.hass.async_add_executor_job(self._send_key, key, key_type) + return None if force else self._device_info - def _send_key(self, key: str, key_type: str | None = None) -> None: - """Send the key using websocket protocol.""" - if key == "KEY_POWEROFF": - key = "KEY_POWER" - if remote := self._get_remote(): - if key_type == "run_app": - remote.run_app(key) - else: - remote.send_key(key) + async def async_launch_app(self, app_id: str) -> None: + """Send the launch_app command using websocket protocol.""" + await self._async_send_commands([ChannelEmitCommand.launch_app(app_id)]) - def _get_remote(self, avoid_open: bool = False) -> SamsungTVWS: + async def async_request_app_list(self) -> None: + """Get installed app list.""" + await self._async_send_commands([ChannelEmitCommand.get_installed_app()]) + + async def async_send_keys(self, keys: list[str]) -> None: + """Send a list of keys using websocket protocol.""" + await self._async_send_commands([SendRemoteKey.click(key) for key in keys]) + + async def _async_get_remote_under_lock(self) -> SamsungTVWSAsyncRemote | None: """Create or return a remote control instance.""" - if self._remote is None: + if self._remote is None or not self._remote.is_alive(): # We need to create a new instance to reconnect. + LOGGER.debug("Create SamsungTVWSBridge for %s", self.host) + assert self.port + self._remote = SamsungTVWSAsyncRemote( + host=self.host, + port=self.port, + token=self.token, + timeout=TIMEOUT_WEBSOCKET, + name=VALUE_CONF_NAME, + ) try: - LOGGER.debug( - "Create SamsungTVWSBridge for %s (%s)", CONF_NAME, self.host + await self._remote.start_listening(self._remote_event) + except UnauthorizedError as err: + LOGGER.info( + "Failed to get remote for %s, re-authentication required: %s", + self.host, + repr(err), ) - self._remote = SamsungTVWS( - host=self.host, - port=self.port, - token=self.token, - timeout=TIMEOUT_WEBSOCKET, - name=VALUE_CONF_NAME, - ) - if not avoid_open: - self._remote.open("samsung.remote.control") - # This is only happening when the auth was switched to DENY - # A removed auth will lead to socket timeout because waiting for auth popup is just an open socket - except ConnectionFailure as err: - LOGGER.debug("ConnectionFailure %s", err.__repr__()) self._notify_reauth_callback() - except (WebSocketException, OSError) as err: - LOGGER.debug("WebSocketException, OSError %s", err.__repr__()) + self._remote = None + except ConnectionClosedError as err: + LOGGER.info( + "Failed to get remote for %s: %s", + self.host, + repr(err), + ) + self._remote = None + except ConnectionFailure as err: + LOGGER.warning( + "Unexpected ConnectionFailure trying to get remote for %s, " + "please report this issue: %s", + self.host, + repr(err), + ) + self._remote = None + except (WebSocketException, AsyncioTimeoutError, OSError) as err: + LOGGER.debug("Failed to get remote for %s: %s", self.host, repr(err)) self._remote = None else: - LOGGER.debug( - "Created SamsungTVWSBridge for %s (%s)", CONF_NAME, self.host - ) + LOGGER.debug("Created SamsungTVWSBridge for %s", self.host) + if self._device_info is None: + # Initialise device info on first connect + await self.async_device_info() if self.token != self._remote.token: - LOGGER.debug( + LOGGER.info( "SamsungTVWSBridge has provided a new token %s", self._remote.token, ) self.token = self._remote.token - self._notify_new_token_callback() + self._notify_update_config_entry({CONF_TOKEN: self.token}) return self._remote - async def async_stop(self) -> None: - """Stop Bridge.""" - LOGGER.debug("Stopping SamsungTVWSBridge") - await self.async_close_remote() + def _remote_event(self, event: str, response: Any) -> None: + """Received event from remote websocket.""" + if event == ED_INSTALLED_APP_EVENT: + self._notify_app_list_callback( + { + app["name"]: app["appId"] + for app in sorted( + parse_installed_app(response), + key=lambda app: cast(str, app["name"]), + ) + } + ) + return + if event == MS_ERROR_EVENT: + # { 'event': 'ms.error', + # 'data': {'message': 'unrecognized method value : ms.remote.control'}} + if (data := response.get("data")) and ( + message := data.get("message") + ) == "unrecognized method value : ms.remote.control": + LOGGER.error( + "Your TV seems to be unsupported by SamsungTVWSBridge" + " and needs a PIN: '%s'. Updating config entry", + message, + ) + self._notify_update_config_entry( + { + CONF_METHOD: METHOD_ENCRYPTED_WEBSOCKET, + CONF_PORT: ENCRYPTED_WEBSOCKET_PORT, + } + ) + + async def _async_send_power_off(self) -> None: + """Send power off command to remote.""" + if self._get_device_spec("FrameTVSupport") == "true": + await self._async_send_commands(SendRemoteKey.hold("KEY_POWER", 3)) + else: + await self._async_send_commands([SendRemoteKey.click("KEY_POWER")]) + + +class SamsungTVEncryptedBridge( + SamsungTVWSBaseBridge[SamsungTVEncryptedWSAsyncRemote, SamsungTVEncryptedCommand] +): + """The Bridge for Encrypted WebSocket TVs (v1 - J/H models).""" + + def __init__( + self, + hass: HomeAssistant, + method: str, + host: str, + port: int | None = None, + entry_data: Mapping[str, Any] | None = None, + ) -> None: + """Initialize Bridge.""" + super().__init__(hass, method, host, port) + self._power_off_warning_logged: bool = False + self._model: str | None = None + self._short_model: str | None = None + if entry_data: + self.token = entry_data.get(CONF_TOKEN) + self.session_id = entry_data.get(CONF_SESSION_ID) + self._model = entry_data.get(CONF_MODEL) + if self._model and len(self._model) > 4: + self._short_model = self._model[4:] + + self._rest_api_port: int | None = None + self._device_info: dict[str, Any] | None = None + + async def async_try_connect(self) -> str: + """Try to connect to the Websocket TV.""" + self.port = ENCRYPTED_WEBSOCKET_PORT + config = { + CONF_NAME: VALUE_CONF_NAME, + CONF_HOST: self.host, + CONF_METHOD: self.method, + CONF_PORT: self.port, + CONF_TIMEOUT: TIMEOUT_WEBSOCKET, + } + + try: + LOGGER.debug("Try config: %s", config) + async with SamsungTVEncryptedWSAsyncRemote( + host=self.host, + port=self.port, + web_session=async_get_clientsession(self.hass), + token=self.token or "", + session_id=self.session_id or "", + timeout=TIMEOUT_REQUEST, + ) as remote: + await remote.start_listening() + except WebSocketException as err: + LOGGER.debug("Working but unsupported config: %s, error: %s", config, err) + return RESULT_NOT_SUPPORTED + except (OSError, AsyncioTimeoutError, ConnectionFailure) as err: + LOGGER.debug("Failing config: %s, error: %s", config, err) + else: + LOGGER.debug("Working config: %s", config) + return RESULT_SUCCESS + + return RESULT_CANNOT_CONNECT + + async def async_device_info(self) -> dict[str, Any] | None: + """Try to gather infos of this TV.""" + # Default to try all ports + rest_api_ports: Iterable[int] = WEBSOCKET_PORTS + if self._rest_api_port: + # We have already made a successful call to the REST api + rest_api_ports = (self._rest_api_port,) + + for rest_api_port in rest_api_ports: + assert self.port + rest_api = SamsungTVAsyncRest( + host=self.host, + session=async_get_clientsession(self.hass), + port=rest_api_port, + timeout=TIMEOUT_WEBSOCKET, + ) + + with contextlib.suppress(*REST_EXCEPTIONS): + device_info: dict[str, Any] = await rest_api.rest_device_info() + LOGGER.debug("Device info on %s is: %s", self.host, device_info) + self._device_info = device_info + self._rest_api_port = rest_api_port + return device_info + + return self._device_info + + async def async_send_keys(self, keys: list[str]) -> None: + """Send a list of keys using websocket protocol.""" + await self._async_send_commands( + [SendEncryptedRemoteKey.click(key) for key in keys] + ) + + async def _async_get_remote_under_lock( + self, + ) -> SamsungTVEncryptedWSAsyncRemote | None: + """Create or return a remote control instance.""" + if self._remote is None or not self._remote.is_alive(): + # We need to create a new instance to reconnect. + LOGGER.debug("Create SamsungTVEncryptedBridge for %s", self.host) + assert self.port + self._remote = SamsungTVEncryptedWSAsyncRemote( + host=self.host, + port=self.port, + web_session=async_get_clientsession(self.hass), + token=self.token or "", + session_id=self.session_id or "", + timeout=TIMEOUT_WEBSOCKET, + ) + try: + await self._remote.start_listening() + except (WebSocketException, AsyncioTimeoutError, OSError) as err: + LOGGER.debug("Failed to get remote for %s: %s", self.host, repr(err)) + self._remote = None + else: + LOGGER.debug("Created SamsungTVEncryptedBridge for %s", self.host) + return self._remote + + async def _async_send_power_off(self) -> None: + """Send power off command to remote.""" + power_off_commands: list[SamsungTVEncryptedCommand] = [] + if self._short_model in ENCRYPTED_MODEL_USES_POWER_OFF: + power_off_commands.append(SendEncryptedRemoteKey.click("KEY_POWEROFF")) + elif self._short_model in ENCRYPTED_MODEL_USES_POWER: + power_off_commands.append(SendEncryptedRemoteKey.click("KEY_POWER")) + else: + if self._model and not self._power_off_warning_logged: + LOGGER.warning( + "Unknown power_off command for %s (%s): sending KEY_POWEROFF and KEY_POWER", + self._model, + self.host, + ) + self._power_off_warning_logged = True + power_off_commands.append(SendEncryptedRemoteKey.click("KEY_POWEROFF")) + power_off_commands.append(SendEncryptedRemoteKey.click("KEY_POWER")) + await self._async_send_commands(power_off_commands) diff --git a/homeassistant/components/samsungtv/config_flow.py b/homeassistant/components/samsungtv/config_flow.py index 8b7e0f83a49..f37020cb029 100644 --- a/homeassistant/components/samsungtv/config_flow.py +++ b/homeassistant/components/samsungtv/config_flow.py @@ -8,6 +8,7 @@ from typing import Any from urllib.parse import urlparse import getmac +from samsungtvws.encrypted.authenticator import SamsungTVEncryptedWSAsyncAuthenticator import voluptuous as vol from homeassistant import config_entries, data_entry_flow @@ -16,48 +17,70 @@ from homeassistant.const import ( CONF_HOST, CONF_MAC, CONF_METHOD, + CONF_MODEL, CONF_NAME, CONF_PORT, CONF_TOKEN, ) from homeassistant.core import callback +from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.device_registry import format_mac -from .bridge import ( - SamsungTVBridge, - SamsungTVLegacyBridge, - SamsungTVWSBridge, - async_get_device_info, - mac_from_device_info, -) +from .bridge import SamsungTVBridge, async_get_device_info, mac_from_device_info from .const import ( CONF_MANUFACTURER, - CONF_MODEL, + CONF_SESSION_ID, + CONF_SSDP_MAIN_TV_AGENT_LOCATION, + CONF_SSDP_RENDERING_CONTROL_LOCATION, DEFAULT_MANUFACTURER, DOMAIN, + ENCRYPTED_WEBSOCKET_PORT, LEGACY_PORT, LOGGER, + METHOD_ENCRYPTED_WEBSOCKET, METHOD_LEGACY, METHOD_WEBSOCKET, RESULT_AUTH_MISSING, RESULT_CANNOT_CONNECT, + RESULT_INVALID_PIN, RESULT_NOT_SUPPORTED, RESULT_SUCCESS, RESULT_UNKNOWN_HOST, + SUCCESSFUL_RESULTS, + UPNP_SVC_MAIN_TV_AGENT, + UPNP_SVC_RENDERING_CONTROL, WEBSOCKET_PORTS, ) DATA_SCHEMA = vol.Schema({vol.Required(CONF_HOST): str, vol.Required(CONF_NAME): str}) -SUPPORTED_METHODS = [METHOD_LEGACY, METHOD_WEBSOCKET] def _strip_uuid(udn: str) -> str: return udn[5:] if udn.startswith("uuid:") else udn -def _entry_is_complete(entry: config_entries.ConfigEntry) -> bool: - """Return True if the config entry information is complete.""" - return bool(entry.unique_id and entry.data.get(CONF_MAC)) +def _entry_is_complete( + entry: config_entries.ConfigEntry, + ssdp_rendering_control_location: str | None, + ssdp_main_tv_agent_location: str | None, +) -> bool: + """Return True if the config entry information is complete. + + If we do not have an ssdp location we consider it complete + as some TVs will not support SSDP/UPNP + """ + return bool( + entry.unique_id + and entry.data.get(CONF_MAC) + and ( + not ssdp_rendering_control_location + or entry.data.get(CONF_SSDP_RENDERING_CONTROL_LOCATION) + ) + and ( + not ssdp_main_tv_agent_location + or entry.data.get(CONF_SSDP_MAIN_TV_AGENT_LOCATION) + ) + ) class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @@ -71,19 +94,24 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._host: str = "" self._mac: str | None = None self._udn: str | None = None + self._upnp_udn: str | None = None + self._ssdp_rendering_control_location: str | None = None + self._ssdp_main_tv_agent_location: str | None = None self._manufacturer: str | None = None self._model: str | None = None + self._connect_result: str | None = None + self._method: str | None = None self._name: str | None = None self._title: str = "" self._id: int | None = None - self._bridge: SamsungTVLegacyBridge | SamsungTVWSBridge | None = None + self._bridge: SamsungTVBridge | None = None self._device_info: dict[str, Any] | None = None + self._authenticator: SamsungTVEncryptedWSAsyncAuthenticator | None = None - def _get_entry_from_bridge(self) -> data_entry_flow.FlowResult: - """Get device entry.""" - assert self._bridge - - data = { + def _base_config_entry(self) -> dict[str, Any]: + """Generate the base config entry without the method.""" + assert self._bridge is not None + return { CONF_HOST: self._host, CONF_MAC: self._mac, CONF_MANUFACTURER: self._manufacturer or DEFAULT_MANUFACTURER, @@ -91,7 +119,14 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): CONF_MODEL: self._model, CONF_NAME: self._name, CONF_PORT: self._bridge.port, + CONF_SSDP_RENDERING_CONTROL_LOCATION: self._ssdp_rendering_control_location, + CONF_SSDP_MAIN_TV_AGENT_LOCATION: self._ssdp_main_tv_agent_location, } + + def _get_entry_from_bridge(self) -> data_entry_flow.FlowResult: + """Get device entry.""" + assert self._bridge + data = self._base_config_entry() if self._bridge.token: data[CONF_TOKEN] = self._bridge.token return self.async_create_entry( @@ -111,48 +146,74 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) -> None: """Set the unique id from the udn.""" assert self._host is not None - await self.async_set_unique_id(self._udn, raise_on_progress=raise_on_progress) - if (entry := self._async_update_existing_host_entry()) and _entry_is_complete( - entry + # Set the unique id without raising on progress in case + # there are two SSDP flows with for each ST + await self.async_set_unique_id(self._udn, raise_on_progress=False) + if ( + entry := self._async_update_existing_matching_entry() + ) and _entry_is_complete( + entry, + self._ssdp_rendering_control_location, + self._ssdp_main_tv_agent_location, ): raise data_entry_flow.AbortFlow("already_configured") + # Now that we have updated the config entry, we can raise + # if another one is progressing + if raise_on_progress: + await self.async_set_unique_id(self._udn, raise_on_progress=True) def _async_update_and_abort_for_matching_unique_id(self) -> None: """Abort and update host and mac if we have it.""" updates = {CONF_HOST: self._host} if self._mac: updates[CONF_MAC] = self._mac - self._abort_if_unique_id_configured(updates=updates) + if self._ssdp_rendering_control_location: + updates[ + CONF_SSDP_RENDERING_CONTROL_LOCATION + ] = self._ssdp_rendering_control_location + if self._ssdp_main_tv_agent_location: + updates[ + CONF_SSDP_MAIN_TV_AGENT_LOCATION + ] = self._ssdp_main_tv_agent_location + self._abort_if_unique_id_configured(updates=updates, reload_on_update=False) - async def _try_connect(self) -> None: - """Try to connect and check auth.""" - for method in SUPPORTED_METHODS: - self._bridge = SamsungTVBridge.get_bridge(self.hass, method, self._host) - result = await self._bridge.async_try_connect() - if result == RESULT_SUCCESS: - return - if result != RESULT_CANNOT_CONNECT: - raise data_entry_flow.AbortFlow(result) - LOGGER.debug("No working config found") - raise data_entry_flow.AbortFlow(RESULT_CANNOT_CONNECT) + async def _async_create_bridge(self) -> None: + """Create the bridge.""" + result, method, _info = await self._async_get_device_info_and_method() + if result not in SUCCESSFUL_RESULTS: + LOGGER.debug("No working config found for %s", self._host) + raise data_entry_flow.AbortFlow(result) + assert method is not None + self._bridge = SamsungTVBridge.get_bridge(self.hass, method, self._host) + return + + async def _async_get_device_info_and_method( + self, + ) -> tuple[str, str | None, dict[str, Any] | None]: + """Get device info and method only once.""" + if self._connect_result is None: + result, _, method, info = await async_get_device_info(self.hass, self._host) + self._connect_result = result + self._method = method + self._device_info = info + if not method: + LOGGER.debug("Host:%s did not return device info", self._host) + return result, None, None + return self._connect_result, self._method, self._device_info async def _async_get_and_check_device_info(self) -> bool: """Try to get the device info.""" - _port, _method, info = await async_get_device_info( - self.hass, self._bridge, self._host - ) + result, _method, info = await self._async_get_device_info_and_method() + if result not in SUCCESSFUL_RESULTS: + raise data_entry_flow.AbortFlow(result) if not info: - if not _method: - LOGGER.debug( - "Samsung host %s is not supported by either %s or %s methods", - self._host, - METHOD_LEGACY, - METHOD_WEBSOCKET, - ) - raise data_entry_flow.AbortFlow(RESULT_NOT_SUPPORTED) return False dev_info = info.get("device", {}) + assert dev_info is not None if (device_type := dev_info.get("type")) != "Samsung SmartTV": + LOGGER.debug( + "Host:%s has type: %s which is not supported", self._host, device_type + ) raise data_entry_flow.AbortFlow(RESULT_NOT_SUPPORTED) self._model = dev_info.get("modelName") name = dev_info.get("name") @@ -165,7 +226,6 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): partial(getmac.get_mac_address, ip=self._host) ): self._mac = mac - self._device_info = info return True async def async_step_import( @@ -179,6 +239,8 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): port = user_input.get(CONF_PORT) if port in WEBSOCKET_PORTS: user_input[CONF_METHOD] = METHOD_WEBSOCKET + elif port == ENCRYPTED_WEBSOCKET_PORT: + user_input[CONF_METHOD] = METHOD_ENCRYPTED_WEBSOCKET elif port == LEGACY_PORT: user_input[CONF_METHOD] = METHOD_LEGACY user_input[CONF_MANUFACTURER] = DEFAULT_MANUFACTURER @@ -203,42 +265,154 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a flow initialized by the user.""" if user_input is not None: await self._async_set_name_host_from_input(user_input) - await self._try_connect() + await self._async_create_bridge() assert self._bridge self._async_abort_entries_match({CONF_HOST: self._host}) if self._bridge.method != METHOD_LEGACY: # Legacy bridge does not provide device info await self._async_set_device_unique_id(raise_on_progress=False) - return self._get_entry_from_bridge() + if self._bridge.method == METHOD_ENCRYPTED_WEBSOCKET: + return await self.async_step_encrypted_pairing() + return await self.async_step_pairing({}) return self.async_show_form(step_id="user", data_schema=DATA_SCHEMA) + async def async_step_pairing( + self, user_input: dict[str, Any] | None = None + ) -> data_entry_flow.FlowResult: + """Handle a pairing by accepting the message on the TV.""" + assert self._bridge is not None + errors: dict[str, str] = {} + if user_input is not None: + result = await self._bridge.async_try_connect() + if result == RESULT_SUCCESS: + return self._get_entry_from_bridge() + if result != RESULT_AUTH_MISSING: + raise data_entry_flow.AbortFlow(result) + errors = {"base": RESULT_AUTH_MISSING} + + self.context["title_placeholders"] = {"device": self._title} + return self.async_show_form( + step_id="pairing", + errors=errors, + description_placeholders={"device": self._title}, + data_schema=vol.Schema({}), + ) + + async def async_step_encrypted_pairing( + self, user_input: dict[str, Any] | None = None + ) -> data_entry_flow.FlowResult: + """Handle a encrypted pairing.""" + assert self._host is not None + await self._async_start_encrypted_pairing(self._host) + assert self._authenticator is not None + errors: dict[str, str] = {} + + if user_input is not None: + if ( + (pin := user_input.get("pin")) + and (token := await self._authenticator.try_pin(pin)) + and (session_id := await self._authenticator.get_session_id_and_close()) + ): + return self.async_create_entry( + data={ + **self._base_config_entry(), + CONF_TOKEN: token, + CONF_SESSION_ID: session_id, + }, + title=self._title, + ) + errors = {"base": RESULT_INVALID_PIN} + + self.context["title_placeholders"] = {"device": self._title} + return self.async_show_form( + step_id="encrypted_pairing", + errors=errors, + description_placeholders={"device": self._title}, + data_schema=vol.Schema({vol.Required("pin"): str}), + ) + @callback - def _async_update_existing_host_entry(self) -> config_entries.ConfigEntry | None: + def _async_get_existing_matching_entry( + self, + ) -> tuple[config_entries.ConfigEntry | None, bool]: + """Get first existing matching entry (prefer unique id).""" + matching_host_entry: config_entries.ConfigEntry | None = None + for entry in self._async_current_entries(include_ignore=False): + if (self._mac and self._mac == entry.data.get(CONF_MAC)) or ( + self._upnp_udn and self._upnp_udn == entry.unique_id + ): + LOGGER.debug("Found entry matching unique_id for %s", self._host) + return entry, True + + if entry.data[CONF_HOST] == self._host: + LOGGER.debug("Found entry matching host for %s", self._host) + matching_host_entry = entry + + return matching_host_entry, False + + @callback + def _async_update_existing_matching_entry( + self, + ) -> config_entries.ConfigEntry | None: """Check existing entries and update them. Returns the existing entry if it was updated. """ - for entry in self._async_current_entries(include_ignore=False): - if entry.data[CONF_HOST] != self._host: - continue - entry_kw_args: dict = {} - if self.unique_id and entry.unique_id is None: - entry_kw_args["unique_id"] = self.unique_id - if self._mac and not entry.data.get(CONF_MAC): - entry_kw_args["data"] = {**entry.data, CONF_MAC: self._mac} - if entry_kw_args: - self.hass.config_entries.async_update_entry(entry, **entry_kw_args) - self.hass.async_create_task( - self.hass.config_entries.async_reload(entry.entry_id) - ) - return entry - return None + entry, is_unique_match = self._async_get_existing_matching_entry() + if not entry: + return None + entry_kw_args: dict = {} + if ( + self.unique_id + and entry.unique_id is None + or (is_unique_match and self.unique_id != entry.unique_id) + ): + entry_kw_args["unique_id"] = self.unique_id + data: dict[str, Any] = dict(entry.data) + update_ssdp_rendering_control_location = ( + self._ssdp_rendering_control_location + and data.get(CONF_SSDP_RENDERING_CONTROL_LOCATION) + != self._ssdp_rendering_control_location + ) + update_ssdp_main_tv_agent_location = ( + self._ssdp_main_tv_agent_location + and data.get(CONF_SSDP_MAIN_TV_AGENT_LOCATION) + != self._ssdp_main_tv_agent_location + ) + update_mac = self._mac and not data.get(CONF_MAC) + if ( + update_ssdp_rendering_control_location + or update_ssdp_main_tv_agent_location + or update_mac + ): + if update_ssdp_rendering_control_location: + data[ + CONF_SSDP_RENDERING_CONTROL_LOCATION + ] = self._ssdp_rendering_control_location + if update_ssdp_main_tv_agent_location: + data[ + CONF_SSDP_MAIN_TV_AGENT_LOCATION + ] = self._ssdp_main_tv_agent_location + if update_mac: + data[CONF_MAC] = self._mac + entry_kw_args["data"] = data + if not entry_kw_args: + return None + LOGGER.debug("Updating existing config entry with %s", entry_kw_args) + self.hass.config_entries.async_update_entry(entry, **entry_kw_args) + if entry.state != config_entries.ConfigEntryState.LOADED: + # If its loaded it already has a reload listener in place + # and we do not want to trigger multiple reloads + self.hass.async_create_task( + self.hass.config_entries.async_reload(entry.entry_id) + ) + return entry async def _async_start_discovery_with_mac_address(self) -> None: """Start discovery.""" assert self._host is not None - if (entry := self._async_update_existing_host_entry()) and entry.unique_id: + if (entry := self._async_update_existing_matching_entry()) and entry.unique_id: # If we have the unique id and the mac we abort # as we do not need anything else raise data_entry_flow.AbortFlow("already_configured") @@ -264,18 +438,44 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a flow initialized by ssdp discovery.""" LOGGER.debug("Samsung device found via SSDP: %s", discovery_info) model_name: str = discovery_info.upnp.get(ssdp.ATTR_UPNP_MODEL_NAME) or "" - self._udn = _strip_uuid(discovery_info.upnp[ssdp.ATTR_UPNP_UDN]) + if discovery_info.ssdp_st == UPNP_SVC_RENDERING_CONTROL: + self._ssdp_rendering_control_location = discovery_info.ssdp_location + LOGGER.debug( + "Set SSDP RenderingControl location to: %s", + self._ssdp_rendering_control_location, + ) + elif discovery_info.ssdp_st == UPNP_SVC_MAIN_TV_AGENT: + self._ssdp_main_tv_agent_location = discovery_info.ssdp_location + LOGGER.debug( + "Set SSDP MainTvAgent location to: %s", + self._ssdp_main_tv_agent_location, + ) + self._udn = self._upnp_udn = _strip_uuid( + discovery_info.upnp[ssdp.ATTR_UPNP_UDN] + ) if hostname := urlparse(discovery_info.ssdp_location or "").hostname: self._host = hostname - await self._async_set_unique_id_from_udn() self._manufacturer = discovery_info.upnp[ssdp.ATTR_UPNP_MANUFACTURER] self._abort_if_manufacturer_is_not_samsung() - if not await self._async_get_and_check_device_info(): - # If we cannot get device info for an SSDP discovery - # its likely a legacy tv. - self._name = self._title = self._model = model_name + + # Set defaults, in case they cannot be extracted from device_info + self._name = self._title = self._model = model_name + # Update from device_info (if accessible) + await self._async_get_and_check_device_info() + + # The UDN provided by the ssdp discovery doesn't always match the UDN + # from the device_info, used by the the other methods so we need to + # ensure the device_info is loaded before setting the unique_id + await self._async_set_unique_id_from_udn() self._async_update_and_abort_for_matching_unique_id() self._async_abort_if_host_already_in_progress() + if self._method == METHOD_LEGACY and discovery_info.ssdp_st in ( + UPNP_SVC_RENDERING_CONTROL, + UPNP_SVC_MAIN_TV_AGENT, + ): + # The UDN we use for the unique id cannot be determined + # from device_info for legacy devices + return self.async_abort(reason="not_supported") self.context["title_placeholders"] = {"device": self._title} return await self.async_step_confirm() @@ -308,12 +508,12 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) -> data_entry_flow.FlowResult: """Handle user-confirmation of discovered node.""" if user_input is not None: - - await self._try_connect() + await self._async_create_bridge() assert self._bridge - return self._get_entry_from_bridge() + if self._bridge.method == METHOD_ENCRYPTED_WEBSOCKET: + return await self.async_step_encrypted_pairing() + return await self.async_step_pairing({}) - self._set_confirm_only() return self.async_show_form( step_id="confirm", description_placeholders={"device": self._title} ) @@ -339,10 +539,13 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Confirm reauth.""" errors = {} assert self._reauth_entry + method = self._reauth_entry.data[CONF_METHOD] if user_input is not None: + if method == METHOD_ENCRYPTED_WEBSOCKET: + return await self.async_step_reauth_confirm_encrypted() bridge = SamsungTVBridge.get_bridge( self.hass, - self._reauth_entry.data[CONF_METHOD], + method, self._reauth_entry.data[CONF_HOST], ) result = await bridge.async_try_connect() @@ -366,3 +569,47 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, description_placeholders={"device": self._title}, ) + + async def _async_start_encrypted_pairing(self, host: str) -> None: + if self._authenticator is None: + self._authenticator = SamsungTVEncryptedWSAsyncAuthenticator( + host, + web_session=async_get_clientsession(self.hass), + ) + await self._authenticator.start_pairing() + + async def async_step_reauth_confirm_encrypted( + self, user_input: dict[str, Any] | None = None + ) -> data_entry_flow.FlowResult: + """Confirm reauth (encrypted method).""" + errors = {} + assert self._reauth_entry + await self._async_start_encrypted_pairing(self._reauth_entry.data[CONF_HOST]) + assert self._authenticator is not None + + if user_input is not None: + if ( + (pin := user_input.get("pin")) + and (token := await self._authenticator.try_pin(pin)) + and (session_id := await self._authenticator.get_session_id_and_close()) + ): + self.hass.config_entries.async_update_entry( + self._reauth_entry, + data={ + **self._reauth_entry.data, + CONF_TOKEN: token, + CONF_SESSION_ID: session_id, + }, + ) + await self.hass.config_entries.async_reload(self._reauth_entry.entry_id) + return self.async_abort(reason="reauth_successful") + + errors = {"base": RESULT_INVALID_PIN} + + self.context["title_placeholders"] = {"device": self._title} + return self.async_show_form( + step_id="reauth_confirm_encrypted", + errors=errors, + description_placeholders={"device": self._title}, + data_schema=vol.Schema({vol.Required("pin"): str}), + ) diff --git a/homeassistant/components/samsungtv/const.py b/homeassistant/components/samsungtv/const.py index f2571372b1f..2585d742be0 100644 --- a/homeassistant/components/samsungtv/const.py +++ b/homeassistant/components/samsungtv/const.py @@ -14,20 +14,35 @@ VALUE_CONF_ID = "ha.component.samsung" CONF_DESCRIPTION = "description" CONF_MANUFACTURER = "manufacturer" -CONF_MODEL = "model" +CONF_SSDP_RENDERING_CONTROL_LOCATION = "ssdp_rendering_control_location" +CONF_SSDP_MAIN_TV_AGENT_LOCATION = "ssdp_main_tv_agent_location" CONF_ON_ACTION = "turn_on_action" +CONF_SESSION_ID = "session_id" RESULT_AUTH_MISSING = "auth_missing" +RESULT_INVALID_PIN = "invalid_pin" RESULT_SUCCESS = "success" RESULT_CANNOT_CONNECT = "cannot_connect" RESULT_NOT_SUPPORTED = "not_supported" RESULT_UNKNOWN_HOST = "unknown" METHOD_LEGACY = "legacy" +METHOD_ENCRYPTED_WEBSOCKET = "encrypted" METHOD_WEBSOCKET = "websocket" TIMEOUT_REQUEST = 31 TIMEOUT_WEBSOCKET = 5 LEGACY_PORT = 55000 -WEBSOCKET_PORTS = (8002, 8001) +ENCRYPTED_WEBSOCKET_PORT = 8000 +WEBSOCKET_NO_SSL_PORT = 8001 +WEBSOCKET_SSL_PORT = 8002 +WEBSOCKET_PORTS = (WEBSOCKET_SSL_PORT, WEBSOCKET_NO_SSL_PORT) + +SUCCESSFUL_RESULTS = {RESULT_AUTH_MISSING, RESULT_SUCCESS} + +UPNP_SVC_RENDERING_CONTROL = "urn:schemas-upnp-org:service:RenderingControl:1" +UPNP_SVC_MAIN_TV_AGENT = "urn:samsung.com:service:MainTVAgent2:1" + +# Time to wait before reloading entry upon device config change +ENTRY_RELOAD_COOLDOWN = 5 diff --git a/homeassistant/components/samsungtv/diagnostics.py b/homeassistant/components/samsungtv/diagnostics.py index 007ab283cfd..319e08827cf 100644 --- a/homeassistant/components/samsungtv/diagnostics.py +++ b/homeassistant/components/samsungtv/diagnostics.py @@ -8,19 +8,17 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_TOKEN from homeassistant.core import HomeAssistant -from .bridge import SamsungTVLegacyBridge, SamsungTVWSBridge -from .const import DOMAIN +from .bridge import SamsungTVBridge +from .const import CONF_SESSION_ID, DOMAIN -TO_REDACT = {CONF_TOKEN} +TO_REDACT = {CONF_TOKEN, CONF_SESSION_ID} async def async_get_config_entry_diagnostics( hass: HomeAssistant, entry: ConfigEntry ) -> dict[str, Any]: """Return diagnostics for a config entry.""" - bridge: SamsungTVLegacyBridge | SamsungTVWSBridge = hass.data[DOMAIN][ - entry.entry_id - ] + bridge: SamsungTVBridge = hass.data[DOMAIN][entry.entry_id] return { "entry": async_redact_data(entry.as_dict(), TO_REDACT), "device_info": await bridge.async_device_info(), diff --git a/homeassistant/components/samsungtv/manifest.json b/homeassistant/components/samsungtv/manifest.json index 21e23c74eb1..b15505d58c8 100644 --- a/homeassistant/components/samsungtv/manifest.json +++ b/homeassistant/components/samsungtv/manifest.json @@ -5,34 +5,47 @@ "requirements": [ "getmac==0.8.2", "samsungctl[websocket]==0.7.1", - "samsungtvws==1.7.0", - "wakeonlan==2.0.1" + "samsungtvws[async,encrypted]==2.5.0", + "wakeonlan==2.0.1", + "async-upnp-client==0.27.0" ], "ssdp": [ { "st": "urn:samsung.com:device:RemoteControlReceiver:1" + }, + { + "st": "urn:samsung.com:service:MainTVAgent2:1" + }, + { + "manufacturer": "Samsung", + "st": "urn:schemas-upnp-org:service:RenderingControl:1" + }, + { + "manufacturer": "Samsung Electronics", + "st": "urn:schemas-upnp-org:service:RenderingControl:1" } ], "zeroconf": [ - {"type":"_airplay._tcp.local.","properties":{"manufacturer":"samsung*"}} + { + "type": "_airplay._tcp.local.", + "properties": { "manufacturer": "samsung*" } + } ], + "dependencies": ["ssdp"], "dhcp": [ - {"registered_devices": true}, + { "registered_devices": true }, { "hostname": "tizen*" }, - {"macaddress": "8CC8CD*"}, - {"macaddress": "606BBD*"}, - {"macaddress": "F47B5E*"}, - {"macaddress": "4844F7*"}, - {"macaddress": "8CEA48*"} - ], - "codeowners": [ - "@escoand", - "@chemelli74", - "@epenet" + { "macaddress": "4844F7*" }, + { "macaddress": "606BBD*" }, + { "macaddress": "641CB0*" }, + { "macaddress": "8CC8CD*" }, + { "macaddress": "8CEA48*" }, + { "macaddress": "F47B5E*" } ], + "codeowners": ["@chemelli74", "@epenet"], "config_flow": true, - "iot_class": "local_polling", + "iot_class": "local_push", "loggers": ["samsungctl", "samsungtvws"] } diff --git a/homeassistant/components/samsungtv/media_player.py b/homeassistant/components/samsungtv/media_player.py index cb857a96afb..47dae90d3e6 100644 --- a/homeassistant/components/samsungtv/media_player.py +++ b/homeassistant/components/samsungtv/media_player.py @@ -2,9 +2,22 @@ from __future__ import annotations import asyncio +from collections.abc import Coroutine, Sequence +import contextlib from datetime import datetime, timedelta from typing import Any +from async_upnp_client.aiohttp import AiohttpNotifyServer, AiohttpSessionRequester +from async_upnp_client.client import UpnpDevice, UpnpService, UpnpStateVariable +from async_upnp_client.client_factory import UpnpFactory +from async_upnp_client.exceptions import ( + UpnpActionResponseError, + UpnpConnectionError, + UpnpError, + UpnpResponseError, +) +from async_upnp_client.profiles.dlna import DmrDevice +from async_upnp_client.utils import async_get_local_ip import voluptuous as vol from wakeonlan import send_magic_packet @@ -24,12 +37,21 @@ from homeassistant.components.media_player.const import ( SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, + SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP, ) from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry -from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, STATE_OFF, STATE_ON -from homeassistant.core import HomeAssistant +from homeassistant.const import ( + CONF_HOST, + CONF_MAC, + CONF_MODEL, + CONF_NAME, + STATE_OFF, + STATE_ON, +) +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_component +from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.entity import DeviceInfo @@ -37,17 +59,16 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.script import Script from homeassistant.util import dt as dt_util -from .bridge import SamsungTVLegacyBridge, SamsungTVWSBridge +from .bridge import SamsungTVBridge, SamsungTVWSBridge from .const import ( CONF_MANUFACTURER, - CONF_MODEL, CONF_ON_ACTION, + CONF_SSDP_RENDERING_CONTROL_LOCATION, DEFAULT_NAME, DOMAIN, LOGGER, ) -KEY_PRESS_TIMEOUT = 1.2 SOURCES = {"TV": "KEY_TV", "HDMI": "KEY_HDMI"} SUPPORT_SAMSUNGTV = ( @@ -69,6 +90,9 @@ SCAN_INTERVAL_PLUS_OFF_TIME = entity_component.DEFAULT_SCAN_INTERVAL + timedelta seconds=5 ) +# Max delay waiting for app_list to return, as some TVs simply ignore the request +APP_LIST_DELAY = 3 + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback @@ -94,7 +118,7 @@ class SamsungTVDevice(MediaPlayerEntity): def __init__( self, - bridge: SamsungTVLegacyBridge | SamsungTVWSBridge, + bridge: SamsungTVBridge, config_entry: ConfigEntry, on_script: Script | None, ) -> None: @@ -102,6 +126,9 @@ class SamsungTVDevice(MediaPlayerEntity): self._config_entry = config_entry self._host: str | None = config_entry.data[CONF_HOST] self._mac: str | None = config_entry.data.get(CONF_MAC) + self._ssdp_rendering_control_location: str | None = config_entry.data.get( + CONF_SSDP_RENDERING_CONTROL_LOCATION + ) self._on_script = on_script # Assume that the TV is in Play mode self._playing: bool = True @@ -113,11 +140,14 @@ class SamsungTVDevice(MediaPlayerEntity): self._attr_device_class = MediaPlayerDeviceClass.TV self._attr_source_list = list(SOURCES) self._app_list: dict[str, str] | None = None + self._app_list_event: asyncio.Event = asyncio.Event() + self._attr_supported_features = SUPPORT_SAMSUNGTV if self._on_script or self._mac: - self._attr_supported_features = SUPPORT_SAMSUNGTV | SUPPORT_TURN_ON - else: - self._attr_supported_features = SUPPORT_SAMSUNGTV + # Add turn-on if on_script or mac is available + self._attr_supported_features |= SUPPORT_TURN_ON + if self._ssdp_rendering_control_location: + self._attr_supported_features |= SUPPORT_VOLUME_SET self._attr_device_info = DeviceInfo( name=self.name, @@ -137,6 +167,21 @@ class SamsungTVDevice(MediaPlayerEntity): self._bridge = bridge self._auth_failed = False self._bridge.register_reauth_callback(self.access_denied) + self._bridge.register_app_list_callback(self._app_list_callback) + + self._dmr_device: DmrDevice | None = None + self._upnp_server: AiohttpNotifyServer | None = None + + def _update_sources(self) -> None: + self._attr_source_list = list(SOURCES) + if app_list := self._app_list: + self._attr_source_list.extend(app_list) + + def _app_list_callback(self, app_list: dict[str, str]) -> None: + """App list callback.""" + self._app_list = app_list + self._update_sources() + self._app_list_event.set() def access_denied(self) -> None: """Access denied callback.""" @@ -153,6 +198,10 @@ class SamsungTVDevice(MediaPlayerEntity): ) ) + async def async_will_remove_from_hass(self) -> None: + """Handle removal.""" + await self._async_shutdown_dmr() + async def async_update(self) -> None: """Update state of device.""" if self._auth_failed or self.hass.is_stopping: @@ -164,21 +213,137 @@ class SamsungTVDevice(MediaPlayerEntity): STATE_ON if await self._bridge.async_is_on() else STATE_OFF ) - if self._attr_state == STATE_ON and self._app_list is None: - self._app_list = {} # Ensure that we don't update it twice in parallel - await self._async_update_app_list() - - async def _async_update_app_list(self) -> None: - self._app_list = await self._bridge.async_get_app_list() - if self._app_list is not None: - self._attr_source_list.extend(self._app_list) - - async def _async_send_key(self, key: str, key_type: str | None = None) -> None: - """Send a key to the tv and handles exceptions.""" - if self._power_off_in_progress() and key != "KEY_POWEROFF": - LOGGER.info("TV is powering off, not sending command: %s", key) + if self._attr_state != STATE_ON: return - await self._bridge.async_send_key(key, key_type) + + startup_tasks: list[Coroutine[Any, Any, None]] = [] + + if not self._app_list_event.is_set(): + startup_tasks.append(self._async_startup_app_list()) + + if not self._dmr_device and self._ssdp_rendering_control_location: + startup_tasks.append(self._async_startup_dmr()) + + if startup_tasks: + await asyncio.gather(*startup_tasks) + + self._update_from_upnp() + + @callback + def _update_from_upnp(self) -> bool: + # Upnp events can affect other attributes that we currently do not track + # We want to avoid checking every attribute in 'async_write_ha_state' as we + # currently only care about two attributes + if (dmr_device := self._dmr_device) is None: + return False + + has_updates = False + + if ( + volume_level := dmr_device.volume_level + ) is not None and self._attr_volume_level != volume_level: + self._attr_volume_level = volume_level + has_updates = True + + if ( + is_muted := dmr_device.is_volume_muted + ) is not None and self._attr_is_volume_muted != is_muted: + self._attr_is_volume_muted = is_muted + has_updates = True + + return has_updates + + async def _async_startup_app_list(self) -> None: + await self._bridge.async_request_app_list() + if self._app_list_event.is_set(): + # The try+wait_for is a bit expensive so we should try not to + # enter it unless we have to (Python 3.11 will have zero cost try) + return + try: + await asyncio.wait_for(self._app_list_event.wait(), APP_LIST_DELAY) + except asyncio.TimeoutError as err: + # No need to try again + self._app_list_event.set() + LOGGER.debug( + "Failed to load app list from %s: %s", self._host, err.__repr__() + ) + + async def _async_startup_dmr(self) -> None: + assert self._ssdp_rendering_control_location is not None + if self._dmr_device is None: + session = async_get_clientsession(self.hass) + upnp_requester = AiohttpSessionRequester(session) + upnp_factory = UpnpFactory(upnp_requester) + upnp_device: UpnpDevice | None = None + with contextlib.suppress(UpnpConnectionError): + upnp_device = await upnp_factory.async_create_device( + self._ssdp_rendering_control_location + ) + if not upnp_device: + return + _, event_ip = await async_get_local_ip( + self._ssdp_rendering_control_location, self.hass.loop + ) + source = (event_ip or "0.0.0.0", 0) + self._upnp_server = AiohttpNotifyServer( + requester=upnp_requester, + source=source, + callback_url=None, + loop=self.hass.loop, + ) + await self._upnp_server.async_start_server() + self._dmr_device = DmrDevice(upnp_device, self._upnp_server.event_handler) + + try: + self._dmr_device.on_event = self._on_upnp_event + await self._dmr_device.async_subscribe_services(auto_resubscribe=True) + except UpnpResponseError as err: + # Device rejected subscription request. This is OK, variables + # will be polled instead. + LOGGER.debug("Device rejected subscription: %r", err) + except UpnpError as err: + # Don't leave the device half-constructed + self._dmr_device.on_event = None + self._dmr_device = None + await self._upnp_server.async_stop_server() + self._upnp_server = None + LOGGER.debug("Error while subscribing during device connect: %r", err) + raise + + async def _async_shutdown_dmr(self) -> None: + """Handle removal.""" + if (dmr_device := self._dmr_device) is not None: + self._dmr_device = None + dmr_device.on_event = None + await dmr_device.async_unsubscribe_services() + + if (upnp_server := self._upnp_server) is not None: + self._upnp_server = None + await upnp_server.async_stop_server() + + def _on_upnp_event( + self, service: UpnpService, state_variables: Sequence[UpnpStateVariable] + ) -> None: + """State variable(s) changed, let home-assistant know.""" + # Ensure the entity has been added to hass to avoid race condition + if self._update_from_upnp() and self.entity_id: + self.async_write_ha_state() + + async def _async_launch_app(self, app_id: str) -> None: + """Send launch_app to the tv.""" + if self._power_off_in_progress(): + LOGGER.info("TV is powering off, not sending launch_app command") + return + assert isinstance(self._bridge, SamsungTVWSBridge) + await self._bridge.async_launch_app(app_id) + + async def _async_send_keys(self, keys: list[str]) -> None: + """Send a key to the tv and handles exceptions.""" + assert keys + if self._power_off_in_progress() and keys[0] != "KEY_POWEROFF": + LOGGER.info("TV is powering off, not sending keys: %s", keys) + return + await self._bridge.async_send_keys(keys) def _power_off_in_progress(self) -> bool: return ( @@ -201,22 +366,31 @@ class SamsungTVDevice(MediaPlayerEntity): async def async_turn_off(self) -> None: """Turn off media player.""" self._end_of_power_off = dt_util.utcnow() + SCAN_INTERVAL_PLUS_OFF_TIME + await self._bridge.async_power_off() - await self._async_send_key("KEY_POWEROFF") - # Force closing of remote session to provide instant UI feedback - await self._bridge.async_close_remote() + async def async_set_volume_level(self, volume: float) -> None: + """Set volume level on the media player.""" + if (dmr_device := self._dmr_device) is None: + LOGGER.info("Upnp services are not available on %s", self._host) + return + try: + await dmr_device.async_set_volume_level(volume) + except UpnpActionResponseError as err: + LOGGER.warning( + "Unable to set volume level on %s: %s", self._host, err.__repr__() + ) async def async_volume_up(self) -> None: """Volume up the media player.""" - await self._async_send_key("KEY_VOLUP") + await self._async_send_keys(["KEY_VOLUP"]) async def async_volume_down(self) -> None: """Volume down media player.""" - await self._async_send_key("KEY_VOLDOWN") + await self._async_send_keys(["KEY_VOLDOWN"]) async def async_mute_volume(self, mute: bool) -> None: """Send mute command.""" - await self._async_send_key("KEY_MUTE") + await self._async_send_keys(["KEY_MUTE"]) async def async_media_play_pause(self) -> None: """Simulate play pause media player.""" @@ -228,27 +402,27 @@ class SamsungTVDevice(MediaPlayerEntity): async def async_media_play(self) -> None: """Send play command.""" self._playing = True - await self._async_send_key("KEY_PLAY") + await self._async_send_keys(["KEY_PLAY"]) async def async_media_pause(self) -> None: """Send media pause command to media player.""" self._playing = False - await self._async_send_key("KEY_PAUSE") + await self._async_send_keys(["KEY_PAUSE"]) async def async_media_next_track(self) -> None: """Send next track command.""" - await self._async_send_key("KEY_CHUP") + await self._async_send_keys(["KEY_CHUP"]) async def async_media_previous_track(self) -> None: """Send the previous track command.""" - await self._async_send_key("KEY_CHDOWN") + await self._async_send_keys(["KEY_CHDOWN"]) async def async_play_media( self, media_type: str, media_id: str, **kwargs: Any ) -> None: """Support changing a channel.""" if media_type == MEDIA_TYPE_APP: - await self._async_send_key(media_id, "run_app") + await self._async_launch_app(media_id) return if media_type != MEDIA_TYPE_CHANNEL: @@ -262,10 +436,9 @@ class SamsungTVDevice(MediaPlayerEntity): LOGGER.error("Media ID must be positive integer") return - for digit in media_id: - await self._async_send_key(f"KEY_{digit}") - await asyncio.sleep(KEY_PRESS_TIMEOUT) - await self._async_send_key("KEY_ENTER") + await self._async_send_keys( + keys=[f"KEY_{digit}" for digit in media_id] + ["KEY_ENTER"] + ) def _wake_on_lan(self) -> None: """Wake the device via wake on lan.""" @@ -284,11 +457,11 @@ class SamsungTVDevice(MediaPlayerEntity): async def async_select_source(self, source: str) -> None: """Select input source.""" if self._app_list and source in self._app_list: - await self._async_send_key(self._app_list[source], "run_app") + await self._async_launch_app(self._app_list[source]) return if source in SOURCES: - await self._async_send_key(SOURCES[source]) + await self._async_send_keys([SOURCES[source]]) return LOGGER.error("Unsupported source") diff --git a/homeassistant/components/samsungtv/strings.json b/homeassistant/components/samsungtv/strings.json index f413a7f1219..d0f4526335a 100644 --- a/homeassistant/components/samsungtv/strings.json +++ b/homeassistant/components/samsungtv/strings.json @@ -12,12 +12,22 @@ "confirm": { "description": "Do you want to set up {device}? If you never connected Home Assistant before you should see a popup on your TV asking for authorization." }, + "pairing": { + "description": "[%key:component::samsungtv::config::step::confirm::description%]" + }, "reauth_confirm": { - "description": "After submitting, accept the the popup on {device} requesting authorization within 30 seconds." + "description": "After submitting, accept the the popup on {device} requesting authorization within 30 seconds or input PIN." + }, + "encrypted_pairing": { + "description": "Please enter the PIN displayed on {device}." + }, + "reauth_confirm_encrypted": { + "description": "[%key:component::samsungtv::config::step::encrypted_pairing::description%]" } }, "error": { - "auth_missing": "[%key:component::samsungtv::config::abort::auth_missing%]" + "auth_missing": "[%key:component::samsungtv::config::abort::auth_missing%]", + "invalid_pin": "PIN is invalid, please try again." }, "abort": { "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", @@ -30,4 +40,4 @@ "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } } -} \ No newline at end of file +} diff --git a/homeassistant/components/samsungtv/translations/en.json b/homeassistant/components/samsungtv/translations/en.json index 4648f930e9b..c4e0e181090 100644 --- a/homeassistant/components/samsungtv/translations/en.json +++ b/homeassistant/components/samsungtv/translations/en.json @@ -6,22 +6,30 @@ "auth_missing": "Home Assistant is not authorized to connect to this Samsung TV. Check your TV's External Device Manager settings to authorize Home Assistant.", "cannot_connect": "Failed to connect", "id_missing": "This Samsung device doesn't have a SerialNumber.", - "missing_config_entry": "This Samsung device doesn't have a configuration entry.", "not_supported": "This Samsung device is currently not supported.", "reauth_successful": "Re-authentication was successful", "unknown": "Unexpected error" }, "error": { - "auth_missing": "Home Assistant is not authorized to connect to this Samsung TV. Check your TV's External Device Manager settings to authorize Home Assistant." + "auth_missing": "Home Assistant is not authorized to connect to this Samsung TV. Check your TV's External Device Manager settings to authorize Home Assistant.", + "invalid_pin": "PIN is invalid, please try again." }, "flow_title": "{device}", "step": { "confirm": { - "description": "Do you want to set up {device}? If you never connected Home Assistant before you should see a popup on your TV asking for authorization.", - "title": "Samsung TV" + "description": "Do you want to set up {device}? If you never connected Home Assistant before you should see a popup on your TV asking for authorization." + }, + "encrypted_pairing": { + "description": "Please enter the PIN displayed on {device}." + }, + "pairing": { + "description": "Do you want to set up {device}? If you never connected Home Assistant before you should see a popup on your TV asking for authorization." }, "reauth_confirm": { - "description": "After submitting, accept the the popup on {device} requesting authorization within 30 seconds." + "description": "After submitting, accept the the popup on {device} requesting authorization within 30 seconds or input PIN." + }, + "reauth_confirm_encrypted": { + "description": "Please enter the PIN displayed on {device}." }, "user": { "data": { diff --git a/homeassistant/components/scene/services.yaml b/homeassistant/components/scene/services.yaml index 3b780a939cc..cbe5e70f688 100644 --- a/homeassistant/components/scene/services.yaml +++ b/homeassistant/components/scene/services.yaml @@ -9,8 +9,7 @@ turn_on: fields: transition: name: Transition - description: - Transition duration it takes to bring devices to the state + description: Transition duration it takes to bring devices to the state defined in the scene. selector: number: @@ -39,8 +38,7 @@ apply: object: transition: name: Transition - description: - Transition duration it takes to bring devices to the state + description: Transition duration it takes to bring devices to the state defined in the scene. selector: number: diff --git a/homeassistant/components/screenlogic/__init__.py b/homeassistant/components/screenlogic/__init__.py index 8580ce8f8fc..fe7d8a79afd 100644 --- a/homeassistant/components/screenlogic/__init__.py +++ b/homeassistant/components/screenlogic/__init__.py @@ -167,11 +167,9 @@ class ScreenlogicDataUpdateCoordinator(DataUpdateCoordinator): raise UpdateFailed(ex) from ex -class ScreenlogicEntity(CoordinatorEntity): +class ScreenlogicEntity(CoordinatorEntity[ScreenlogicDataUpdateCoordinator]): """Base class for all ScreenLogic entities.""" - coordinator: ScreenlogicDataUpdateCoordinator - def __init__(self, coordinator, data_key, enabled=True): """Initialize of the entity.""" super().__init__(coordinator) diff --git a/homeassistant/components/screenlogic/diagnostics.py b/homeassistant/components/screenlogic/diagnostics.py new file mode 100644 index 00000000000..33041597b75 --- /dev/null +++ b/homeassistant/components/screenlogic/diagnostics.py @@ -0,0 +1,21 @@ +"""Diagnostics for Screenlogic.""" + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from . import ScreenlogicDataUpdateCoordinator +from .const import DOMAIN + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, config_entry: ConfigEntry +) -> dict: + """Return diagnostics for a config entry.""" + coordinator: ScreenlogicDataUpdateCoordinator = hass.data[DOMAIN][ + config_entry.entry_id + ] + + return { + "config_entry": config_entry.as_dict(), + "data": coordinator.data, + } diff --git a/homeassistant/components/screenlogic/manifest.json b/homeassistant/components/screenlogic/manifest.json index 32ec1872877..4998b7b507f 100644 --- a/homeassistant/components/screenlogic/manifest.json +++ b/homeassistant/components/screenlogic/manifest.json @@ -6,7 +6,7 @@ "requirements": ["screenlogicpy==0.5.4"], "codeowners": ["@dieselrabbit", "@bdraco"], "dhcp": [ - {"registered_devices": true}, + { "registered_devices": true }, { "hostname": "pentair*", "macaddress": "00C033*" diff --git a/homeassistant/components/screenlogic/strings.json b/homeassistant/components/screenlogic/strings.json index 8a9ec196c91..b0958d31727 100644 --- a/homeassistant/components/screenlogic/strings.json +++ b/homeassistant/components/screenlogic/strings.json @@ -9,23 +9,23 @@ "title": "ScreenLogic", "description": "Enter your ScreenLogic Gateway information.", "data": { - "ip_address": "[%key:common::config_flow::data::ip%]", - "port": "[%key:common::config_flow::data::port%]" - } + "ip_address": "[%key:common::config_flow::data::ip%]", + "port": "[%key:common::config_flow::data::port%]" + } }, "gateway_select": { "title": "ScreenLogic", "description": "The following ScreenLogic gateways were discovered. Please select one to configure, or choose to manually configure a ScreenLogic gateway.", "data": { - "selected_gateway": "Gateway" - } + "selected_gateway": "Gateway" + } } }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } }, - "options":{ + "options": { "step": { "init": { "title": "ScreenLogic", diff --git a/homeassistant/components/screenlogic/translations/el.json b/homeassistant/components/screenlogic/translations/el.json index 26906ac3b29..0bb5a9a6322 100644 --- a/homeassistant/components/screenlogic/translations/el.json +++ b/homeassistant/components/screenlogic/translations/el.json @@ -6,7 +6,7 @@ "error": { "cannot_connect": "\u0391\u03b4\u03c5\u03bd\u03b1\u03bc\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" }, - "flow_title": "ScreenLogic {name}", + "flow_title": "{name}", "step": { "gateway_entry": { "data": { diff --git a/homeassistant/components/script/translations/el.json b/homeassistant/components/script/translations/el.json index 9fbc773c9f5..bfa0dbc9644 100644 --- a/homeassistant/components/script/translations/el.json +++ b/homeassistant/components/script/translations/el.json @@ -5,5 +5,5 @@ "on": "\u0395\u03bd\u03b5\u03c1\u03b3\u03cc" } }, - "title": "\u0394\u03ad\u03c3\u03bc\u03b7 \u03b5\u03bd\u03b5\u03c1\u03b3\u03b5\u03b9\u03ce\u03bd" + "title": "\u03a3\u03b5\u03bd\u03ac\u03c1\u03b9\u03bf" } \ No newline at end of file diff --git a/homeassistant/components/script/translations/fr.json b/homeassistant/components/script/translations/fr.json index 910192a5f37..fe1481f5a7c 100644 --- a/homeassistant/components/script/translations/fr.json +++ b/homeassistant/components/script/translations/fr.json @@ -1,8 +1,8 @@ { "state": { "_": { - "off": "Inactif", - "on": "Actif" + "off": "D\u00e9sactiv\u00e9", + "on": "Activ\u00e9" } }, "title": "Script" diff --git a/homeassistant/components/season/__init__.py b/homeassistant/components/season/__init__.py index 095a4704b12..6d4a2974522 100644 --- a/homeassistant/components/season/__init__.py +++ b/homeassistant/components/season/__init__.py @@ -1 +1,16 @@ -"""The season component.""" +"""The Season integration.""" +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from .const import PLATFORMS + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up from a config entry.""" + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/season/config_flow.py b/homeassistant/components/season/config_flow.py new file mode 100644 index 00000000000..854c0158439 --- /dev/null +++ b/homeassistant/components/season/config_flow.py @@ -0,0 +1,48 @@ +"""Config flow to configure the Season integration.""" +from __future__ import annotations + +from typing import Any + +import voluptuous as vol + +from homeassistant.config_entries import ConfigFlow +from homeassistant.const import CONF_NAME, CONF_TYPE +from homeassistant.data_entry_flow import FlowResult + +from .const import DEFAULT_NAME, DOMAIN, TYPE_ASTRONOMICAL, TYPE_METEOROLOGICAL + + +class SeasonConfigFlow(ConfigFlow, domain=DOMAIN): + """Config flow for Season.""" + + VERSION = 1 + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle a flow initialized by the user.""" + if user_input is not None: + await self.async_set_unique_id(user_input[CONF_TYPE]) + self._abort_if_unique_id_configured() + return self.async_create_entry( + title=user_input.get(CONF_NAME, DEFAULT_NAME), + data={CONF_TYPE: user_input[CONF_TYPE]}, + ) + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required(CONF_TYPE, default=TYPE_ASTRONOMICAL): vol.In( + { + TYPE_ASTRONOMICAL: "Astronomical", + TYPE_METEOROLOGICAL: "Meteorological", + } + ) + }, + ), + ) + + async def async_step_import(self, user_input: dict[str, Any]) -> FlowResult: + """Handle import from configuration.yaml.""" + return await self.async_step_user(user_input) diff --git a/homeassistant/components/season/const.py b/homeassistant/components/season/const.py new file mode 100644 index 00000000000..c27d4f5c40e --- /dev/null +++ b/homeassistant/components/season/const.py @@ -0,0 +1,14 @@ +"""Constants for the Season integration.""" +from typing import Final + +from homeassistant.const import Platform + +DOMAIN: Final = "season" +PLATFORMS: Final = [Platform.SENSOR] + +DEFAULT_NAME: Final = "Season" + +TYPE_ASTRONOMICAL: Final = "astronomical" +TYPE_METEOROLOGICAL: Final = "meteorological" + +VALID_TYPES: Final = [TYPE_ASTRONOMICAL, TYPE_METEOROLOGICAL] diff --git a/homeassistant/components/season/manifest.json b/homeassistant/components/season/manifest.json index cfe04f9b1f7..7b6feeca8a4 100644 --- a/homeassistant/components/season/manifest.json +++ b/homeassistant/components/season/manifest.json @@ -2,9 +2,10 @@ "domain": "season", "name": "Season", "documentation": "https://www.home-assistant.io/integrations/season", - "requirements": ["ephem==3.7.7.0"], - "codeowners": [], + "requirements": ["ephem==4.1.2"], + "codeowners": ["@frenck"], "quality_scale": "internal", "iot_class": "local_polling", - "loggers": ["ephem"] + "loggers": ["ephem"], + "config_flow": true } diff --git a/homeassistant/components/season/sensor.py b/homeassistant/components/season/sensor.py index 23b50c0939f..216475f0cdf 100644 --- a/homeassistant/components/season/sensor.py +++ b/homeassistant/components/season/sensor.py @@ -1,8 +1,7 @@ -"""Support for tracking which astronomical or meteorological season it is.""" +"""Support for Season sensors.""" from __future__ import annotations from datetime import date, datetime -import logging import ephem import voluptuous as vol @@ -11,6 +10,7 @@ from homeassistant.components.sensor import ( PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, SensorEntity, ) +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_NAME, CONF_TYPE from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -18,9 +18,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util.dt import utcnow -_LOGGER = logging.getLogger(__name__) - -DEFAULT_NAME = "Season" +from .const import DEFAULT_NAME, DOMAIN, TYPE_ASTRONOMICAL, VALID_TYPES EQUATOR = "equator" @@ -32,11 +30,6 @@ STATE_SPRING = "spring" STATE_SUMMER = "summer" STATE_WINTER = "winter" -TYPE_ASTRONOMICAL = "astronomical" -TYPE_METEOROLOGICAL = "meteorological" - -VALID_TYPES = [TYPE_ASTRONOMICAL, TYPE_METEOROLOGICAL] - HEMISPHERE_SEASON_SWAP = { STATE_WINTER: STATE_SUMMER, STATE_SPRING: STATE_AUTUMN, @@ -60,25 +53,35 @@ PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( ) -def setup_platform( +async def async_setup_platform( hass: HomeAssistant, config: ConfigType, - add_entities: AddEntitiesCallback, + async_add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: - """Display the current season.""" - _type: str = config[CONF_TYPE] - name: str = config[CONF_NAME] + """Set up the season sensor platform.""" + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=config, + ) + ) + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the platform from config entry.""" + hemisphere = EQUATOR if hass.config.latitude < 0: hemisphere = SOUTHERN elif hass.config.latitude > 0: hemisphere = NORTHERN - else: - hemisphere = EQUATOR - _LOGGER.debug(_type) - add_entities([Season(hemisphere, _type, name)], True) + async_add_entities([SeasonSensorEntity(entry, hemisphere)], True) def get_season( @@ -100,14 +103,13 @@ def get_season( autumn_start = spring_start.replace(month=9) winter_start = spring_start.replace(month=12) + season = STATE_WINTER if spring_start <= current_date < summer_start: season = STATE_SPRING elif summer_start <= current_date < autumn_start: season = STATE_SUMMER elif autumn_start <= current_date < winter_start: season = STATE_AUTUMN - elif winter_start <= current_date or spring_start > current_date: - season = STATE_WINTER # If user is located in the southern hemisphere swap the season if hemisphere == NORTHERN: @@ -115,16 +117,17 @@ def get_season( return HEMISPHERE_SEASON_SWAP.get(season) -class Season(SensorEntity): +class SeasonSensorEntity(SensorEntity): """Representation of the current season.""" _attr_device_class = "season__season" - def __init__(self, hemisphere: str, season_tracking_type: str, name: str) -> None: + def __init__(self, entry: ConfigEntry, hemisphere: str) -> None: """Initialize the season.""" - self._attr_name = name + self._attr_name = entry.title + self._attr_unique_id = entry.entry_id self.hemisphere = hemisphere - self.type = season_tracking_type + self.type = entry.data[CONF_TYPE] def update(self) -> None: """Update season.""" diff --git a/homeassistant/components/season/strings.json b/homeassistant/components/season/strings.json new file mode 100644 index 00000000000..c75c0f1c507 --- /dev/null +++ b/homeassistant/components/season/strings.json @@ -0,0 +1,14 @@ +{ + "config": { + "step": { + "user": { + "data": { + "type": "Type of season definition" + } + } + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" + } + } +} diff --git a/homeassistant/components/season/translations/bg.json b/homeassistant/components/season/translations/bg.json new file mode 100644 index 00000000000..80a7cc489a9 --- /dev/null +++ b/homeassistant/components/season/translations/bg.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u043b\u0443\u0433\u0430\u0442\u0430 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0430" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/season/translations/ca.json b/homeassistant/components/season/translations/ca.json new file mode 100644 index 00000000000..b12e0e3019c --- /dev/null +++ b/homeassistant/components/season/translations/ca.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "El servei ja est\u00e0 configurat" + }, + "step": { + "user": { + "data": { + "type": "Definici\u00f3 del tipus d'estaci\u00f3" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/season/translations/de.json b/homeassistant/components/season/translations/de.json new file mode 100644 index 00000000000..fe2819b3c40 --- /dev/null +++ b/homeassistant/components/season/translations/de.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "Der Dienst ist bereits konfiguriert" + }, + "step": { + "user": { + "data": { + "type": "Definition der Saison" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/season/translations/el.json b/homeassistant/components/season/translations/el.json new file mode 100644 index 00000000000..52bf5ca6126 --- /dev/null +++ b/homeassistant/components/season/translations/el.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "\u0397 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af" + }, + "step": { + "user": { + "data": { + "type": "\u03a4\u03cd\u03c0\u03bf\u03c2 \u03bf\u03c1\u03b9\u03c3\u03bc\u03bf\u03cd \u03b5\u03c0\u03bf\u03c7\u03ae\u03c2" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/season/translations/en.json b/homeassistant/components/season/translations/en.json new file mode 100644 index 00000000000..1638f3c0a20 --- /dev/null +++ b/homeassistant/components/season/translations/en.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "Service is already configured" + }, + "step": { + "user": { + "data": { + "type": "Type of season definition" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/season/translations/et.json b/homeassistant/components/season/translations/et.json new file mode 100644 index 00000000000..6c11c8136e1 --- /dev/null +++ b/homeassistant/components/season/translations/et.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "Teenus on juba h\u00e4\u00e4lestatud" + }, + "step": { + "user": { + "data": { + "type": "Hooaja t\u00fc\u00fcbi m\u00e4\u00e4ratlus" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/season/translations/fr.json b/homeassistant/components/season/translations/fr.json new file mode 100644 index 00000000000..a3bf66a400e --- /dev/null +++ b/homeassistant/components/season/translations/fr.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "Le service est d\u00e9j\u00e0 configur\u00e9" + }, + "step": { + "user": { + "data": { + "type": "D\u00e9finition du type de saison" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/season/translations/he.json b/homeassistant/components/season/translations/he.json new file mode 100644 index 00000000000..48a6eeeea33 --- /dev/null +++ b/homeassistant/components/season/translations/he.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "\u05e9\u05d9\u05e8\u05d5\u05ea \u05d6\u05d4 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/season/translations/hu.json b/homeassistant/components/season/translations/hu.json new file mode 100644 index 00000000000..11bbd17ad6c --- /dev/null +++ b/homeassistant/components/season/translations/hu.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van" + }, + "step": { + "user": { + "data": { + "type": "Az \u00e9vszak meghat\u00e1roz\u00e1s\u00e1nak t\u00edpusa" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/season/translations/id.json b/homeassistant/components/season/translations/id.json new file mode 100644 index 00000000000..ef7de1c9d3a --- /dev/null +++ b/homeassistant/components/season/translations/id.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "Layanan sudah dikonfigurasi" + }, + "step": { + "user": { + "data": { + "type": "Jenis definisi musim" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/season/translations/it.json b/homeassistant/components/season/translations/it.json new file mode 100644 index 00000000000..f77a7410705 --- /dev/null +++ b/homeassistant/components/season/translations/it.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "Il servizio \u00e8 gi\u00e0 configurato" + }, + "step": { + "user": { + "data": { + "type": "Tipo di definizione della stagione" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/season/translations/ja.json b/homeassistant/components/season/translations/ja.json new file mode 100644 index 00000000000..006f6c8146d --- /dev/null +++ b/homeassistant/components/season/translations/ja.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "\u30b5\u30fc\u30d3\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "step": { + "user": { + "data": { + "type": "\u30b7\u30fc\u30ba\u30f3\u5b9a\u7fa9\u306e\u7a2e\u985e" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/season/translations/nl.json b/homeassistant/components/season/translations/nl.json new file mode 100644 index 00000000000..63067c8a814 --- /dev/null +++ b/homeassistant/components/season/translations/nl.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "Dienst is al geconfigureerd" + }, + "step": { + "user": { + "data": { + "type": "Type sessie definitie" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/season/translations/no.json b/homeassistant/components/season/translations/no.json new file mode 100644 index 00000000000..2c177da8227 --- /dev/null +++ b/homeassistant/components/season/translations/no.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "Tjenesten er allerede konfigurert" + }, + "step": { + "user": { + "data": { + "type": "Type sesongdefinisjon" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/season/translations/pl.json b/homeassistant/components/season/translations/pl.json new file mode 100644 index 00000000000..bef7f92841d --- /dev/null +++ b/homeassistant/components/season/translations/pl.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "Us\u0142uga jest ju\u017c skonfigurowana" + }, + "step": { + "user": { + "data": { + "type": "Typ definicji sezonu" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/season/translations/pt-BR.json b/homeassistant/components/season/translations/pt-BR.json new file mode 100644 index 00000000000..aa4f7601808 --- /dev/null +++ b/homeassistant/components/season/translations/pt-BR.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" + }, + "step": { + "user": { + "data": { + "type": "Defini\u00e7\u00e3o do tipo de temporada" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/season/translations/pt.json b/homeassistant/components/season/translations/pt.json new file mode 100644 index 00000000000..b7bb07e9522 --- /dev/null +++ b/homeassistant/components/season/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/season/translations/ru.json b/homeassistant/components/season/translations/ru.json new file mode 100644 index 00000000000..4a914a520e5 --- /dev/null +++ b/homeassistant/components/season/translations/ru.json @@ -0,0 +1,14 @@ +{ + "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." + }, + "step": { + "user": { + "data": { + "type": "\u0422\u0438\u043f \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u0438\u044f \u0441\u0435\u0437\u043e\u043d\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/season/translations/tr.json b/homeassistant/components/season/translations/tr.json new file mode 100644 index 00000000000..a625042d5dd --- /dev/null +++ b/homeassistant/components/season/translations/tr.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "step": { + "user": { + "data": { + "type": "Sezon tan\u0131m\u0131 t\u00fcr\u00fc" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/season/translations/zh-Hant.json b/homeassistant/components/season/translations/zh-Hant.json new file mode 100644 index 00000000000..738ccab2c26 --- /dev/null +++ b/homeassistant/components/season/translations/zh-Hant.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "step": { + "user": { + "data": { + "type": "\u5b63\u7bc0\u985e\u5225\u5b9a\u7fa9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/select/recorder.py b/homeassistant/components/select/recorder.py new file mode 100644 index 00000000000..6660c8383d0 --- /dev/null +++ b/homeassistant/components/select/recorder.py @@ -0,0 +1,12 @@ +"""Integration platform for recorder.""" +from __future__ import annotations + +from homeassistant.core import HomeAssistant, callback + +from . import ATTR_OPTIONS + + +@callback +def exclude_attributes(hass: HomeAssistant) -> set[str]: + """Exclude static attributes from being recorded in the database.""" + return {ATTR_OPTIONS} diff --git a/homeassistant/components/sense/translations/bg.json b/homeassistant/components/sense/translations/bg.json new file mode 100644 index 00000000000..d42d6dba5c1 --- /dev/null +++ b/homeassistant/components/sense/translations/bg.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" + }, + "step": { + "reauth_validate": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430" + }, + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u043d\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430" + }, + "validation": { + "data": { + "code": "\u041a\u043e\u0434 \u0437\u0430 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sense/translations/es.json b/homeassistant/components/sense/translations/es.json index cdc833eeebf..ca77c97900b 100644 --- a/homeassistant/components/sense/translations/es.json +++ b/homeassistant/components/sense/translations/es.json @@ -9,6 +9,11 @@ "unknown": "Error inesperado" }, "step": { + "reauth_validate": { + "data": { + "password": "Contrase\u00f1a" + } + }, "user": { "data": { "email": "Correo electr\u00f3nico", @@ -16,6 +21,11 @@ "timeout": "Timeout" }, "title": "Conectar a tu Sense Energy Monitor" + }, + "validation": { + "data": { + "code": "C\u00f3digo de verificaci\u00f3n" + } } } } diff --git a/homeassistant/components/sense/translations/fr.json b/homeassistant/components/sense/translations/fr.json index bdd588eae74..000240517b9 100644 --- a/homeassistant/components/sense/translations/fr.json +++ b/homeassistant/components/sense/translations/fr.json @@ -1,21 +1,33 @@ { "config": { "abort": { - "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "step": { + "reauth_validate": { + "data": { + "password": "Mot de passe" + }, + "title": "R\u00e9-authentifier l'int\u00e9gration" + }, "user": { "data": { - "email": "Email", + "email": "Courriel", "password": "Mot de passe", "timeout": "D\u00e9lai expir\u00e9" }, "title": "Connectez-vous \u00e0 votre moniteur d'\u00e9nergie Sense" + }, + "validation": { + "data": { + "code": "Code de v\u00e9rification" + } } } } diff --git a/homeassistant/components/sense/translations/he.json b/homeassistant/components/sense/translations/he.json index c4e87259193..8c4cc7fb508 100644 --- a/homeassistant/components/sense/translations/he.json +++ b/homeassistant/components/sense/translations/he.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7" }, "error": { "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", @@ -9,12 +10,24 @@ "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" }, "step": { + "reauth_validate": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4" + }, + "description": "\u05e9\u05d9\u05dc\u05d5\u05d1 Sense \u05e6\u05e8\u05d9\u05da \u05dc\u05d0\u05de\u05ea \u05de\u05d7\u05d3\u05e9 \u05d0\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05e9\u05dc\u05da {email}.", + "title": "\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05e9\u05dc \u05e9\u05d9\u05dc\u05d5\u05d1" + }, "user": { "data": { "email": "\u05d3\u05d5\u05d0\"\u05dc", "password": "\u05e1\u05d9\u05e1\u05de\u05d4", "timeout": "\u05e4\u05e1\u05e7 \u05d6\u05de\u05df" } + }, + "validation": { + "data": { + "code": "\u05e7\u05d5\u05d3 \u05d0\u05d9\u05de\u05d5\u05ea" + } } } } diff --git a/homeassistant/components/sense/translations/id.json b/homeassistant/components/sense/translations/id.json index 6767f1a54ca..8f9fc17e2c0 100644 --- a/homeassistant/components/sense/translations/id.json +++ b/homeassistant/components/sense/translations/id.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Perangkat sudah dikonfigurasi" + "already_configured": "Perangkat sudah dikonfigurasi", + "reauth_successful": "Autentikasi ulang berhasil" }, "error": { "cannot_connect": "Gagal terhubung", @@ -9,6 +10,13 @@ "unknown": "Kesalahan yang tidak diharapkan" }, "step": { + "reauth_validate": { + "data": { + "password": "Kata Sandi" + }, + "description": "Integrasi Sense perlu mengautentikasi ulang akun Anda {email}.", + "title": "Autentikasi Ulang Integrasi" + }, "user": { "data": { "email": "Email", @@ -16,6 +24,12 @@ "timeout": "Tenggang waktu" }, "title": "Hubungkan ke Sense Energy Monitor Anda" + }, + "validation": { + "data": { + "code": "Kode verifikasi" + }, + "title": "Autentikasi Multifaktor Sense" } } } diff --git a/homeassistant/components/sense/translations/it.json b/homeassistant/components/sense/translations/it.json index 66c09d294c1..f769de0c3d0 100644 --- a/homeassistant/components/sense/translations/it.json +++ b/homeassistant/components/sense/translations/it.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" }, "error": { "cannot_connect": "Impossibile connettersi", @@ -9,6 +10,13 @@ "unknown": "Errore imprevisto" }, "step": { + "reauth_validate": { + "data": { + "password": "Password" + }, + "description": "L'integrazione Sense deve autenticare nuovamente il tuo account {email}.", + "title": "Autentica nuovamente l'integrazione" + }, "user": { "data": { "email": "Email", @@ -16,6 +24,12 @@ "timeout": "Tempo scaduto" }, "title": "Connettiti al tuo Sense Energy Monitor" + }, + "validation": { + "data": { + "code": "Codice di verifica" + }, + "title": "Autenticazione a pi\u00f9 fattori Sense" } } } diff --git a/homeassistant/components/sense/translations/nl.json b/homeassistant/components/sense/translations/nl.json index 59e0e3ade8a..11b1c07350c 100644 --- a/homeassistant/components/sense/translations/nl.json +++ b/homeassistant/components/sense/translations/nl.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Apparaat is al geconfigureerd" + "already_configured": "Apparaat is al geconfigureerd", + "reauth_successful": "Herauthenticatie was succesvol" }, "error": { "cannot_connect": "Kan geen verbinding maken", @@ -9,6 +10,13 @@ "unknown": "Onverwachte fout" }, "step": { + "reauth_validate": { + "data": { + "password": "Wachtwoord" + }, + "description": "De Sense-integratie moet uw account {email} opnieuw verifi\u00ebren.", + "title": "Verifieer de integratie opnieuw" + }, "user": { "data": { "email": "E-mail", @@ -16,6 +24,12 @@ "timeout": "Timeout" }, "title": "Maak verbinding met uw Sense Energy Monitor" + }, + "validation": { + "data": { + "code": "Verificatiecode" + }, + "title": "Sense Multi-factor authenticatie" } } } diff --git a/homeassistant/components/sense/translations/pt-BR.json b/homeassistant/components/sense/translations/pt-BR.json index 5944daf63ca..13ea0e901c7 100644 --- a/homeassistant/components/sense/translations/pt-BR.json +++ b/homeassistant/components/sense/translations/pt-BR.json @@ -14,7 +14,7 @@ "data": { "password": "Senha" }, - "description": "A integra\u00e7\u00e3o do Sense precisa autenticar novamente sua conta {email} .", + "description": "A integra\u00e7\u00e3o do Sense precisa autenticar novamente sua conta {email}.", "title": "Reautenticar Integra\u00e7\u00e3o" }, "user": { diff --git a/homeassistant/components/sense/translations/tr.json b/homeassistant/components/sense/translations/tr.json index 3261bbec3b9..ea0f653c20a 100644 --- a/homeassistant/components/sense/translations/tr.json +++ b/homeassistant/components/sense/translations/tr.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + "already_configured": "Cihaz 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", @@ -9,6 +10,13 @@ "unknown": "Beklenmeyen hata" }, "step": { + "reauth_validate": { + "data": { + "password": "Parola" + }, + "description": "Sense entegrasyonunun hesab\u0131n\u0131z\u0131 {email} yeniden do\u011frulamas\u0131 gerekiyor.", + "title": "Entegrasyonu Yeniden Do\u011frula" + }, "user": { "data": { "email": "E-posta", @@ -16,6 +24,12 @@ "timeout": "Zaman a\u015f\u0131m\u0131" }, "title": "Sense Enerji Monit\u00f6r\u00fcn\u00fcze ba\u011flan\u0131n" + }, + "validation": { + "data": { + "code": "Do\u011frulama kodu" + }, + "title": "Sense \u00c7ok fakt\u00f6rl\u00fc kimlik do\u011frulama" } } } diff --git a/homeassistant/components/sensehat/__init__.py b/homeassistant/components/sensehat/__init__.py deleted file mode 100644 index baef85d7f53..00000000000 --- a/homeassistant/components/sensehat/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The sensehat component.""" diff --git a/homeassistant/components/sensehat/light.py b/homeassistant/components/sensehat/light.py deleted file mode 100644 index c1184576362..00000000000 --- a/homeassistant/components/sensehat/light.py +++ /dev/null @@ -1,125 +0,0 @@ -"""Support for Sense Hat LEDs.""" -from __future__ import annotations - -import logging - -from sense_hat import SenseHat -import voluptuous as vol - -from homeassistant.components.light import ( - ATTR_BRIGHTNESS, - ATTR_HS_COLOR, - PLATFORM_SCHEMA, - SUPPORT_BRIGHTNESS, - SUPPORT_COLOR, - LightEntity, -) -from homeassistant.const import CONF_NAME -from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -import homeassistant.util.color as color_util - -SUPPORT_SENSEHAT = SUPPORT_BRIGHTNESS | SUPPORT_COLOR - -DEFAULT_NAME = "sensehat" - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - {vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string} -) - -_LOGGER = logging.getLogger(__name__) - - -def setup_platform( - hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the Sense Hat Light platform.""" - _LOGGER.warning( - "The Sense HAT integration is deprecated and will be removed " - "in Home Assistant Core 2022.4; this integration is removed under " - "Architectural Decision Record 0019, more information can be found here: " - "https://github.com/home-assistant/architecture/blob/master/adr/0019-GPIO.md" - ) - - sensehat = SenseHat() - - name = config.get(CONF_NAME) - - add_entities([SenseHatLight(sensehat, name)]) - - -class SenseHatLight(LightEntity): - """Representation of an Sense Hat Light.""" - - def __init__(self, sensehat, name): - """Initialize an Sense Hat Light. - - Full brightness and white color. - """ - self._sensehat = sensehat - self._name = name - self._is_on = False - self._brightness = 255 - self._hs_color = [0, 0] - - @property - def name(self): - """Return the display name of this light.""" - return self._name - - @property - def brightness(self): - """Read back the brightness of the light.""" - return self._brightness - - @property - def hs_color(self): - """Read back the color of the light.""" - return self._hs_color - - @property - def supported_features(self): - """Flag supported features.""" - return SUPPORT_SENSEHAT - - @property - def is_on(self): - """Return true if light is on.""" - return self._is_on - - @property - def should_poll(self): - """Return if we should poll this device.""" - return False - - @property - def assumed_state(self) -> bool: - """Return True if unable to access real state of the entity.""" - return True - - def turn_on(self, **kwargs): - """Instruct the light to turn on and set correct brightness & color.""" - if ATTR_BRIGHTNESS in kwargs: - self._brightness = kwargs[ATTR_BRIGHTNESS] - - if ATTR_HS_COLOR in kwargs: - self._hs_color = kwargs[ATTR_HS_COLOR] - - rgb = color_util.color_hsv_to_RGB( - self._hs_color[0], self._hs_color[1], self._brightness / 255 * 100 - ) - self._sensehat.clear(*rgb) - - self._is_on = True - self.schedule_update_ha_state() - - def turn_off(self, **kwargs): - """Instruct the light to turn off.""" - self._sensehat.clear() - self._is_on = False - self.schedule_update_ha_state() diff --git a/homeassistant/components/sensehat/manifest.json b/homeassistant/components/sensehat/manifest.json deleted file mode 100644 index 78f6e0609bc..00000000000 --- a/homeassistant/components/sensehat/manifest.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "domain": "sensehat", - "name": "Sense HAT", - "documentation": "https://www.home-assistant.io/integrations/sensehat", - "requirements": ["sense-hat==2.2.0"], - "codeowners": [], - "iot_class": "assumed_state", - "loggers": ["sense_hat"] -} diff --git a/homeassistant/components/sensehat/sensor.py b/homeassistant/components/sensehat/sensor.py deleted file mode 100644 index bf9d77104da..00000000000 --- a/homeassistant/components/sensehat/sensor.py +++ /dev/null @@ -1,163 +0,0 @@ -"""Support for Sense HAT sensors.""" -from __future__ import annotations - -from datetime import timedelta -import logging -from pathlib import Path - -from sense_hat import SenseHat -import voluptuous as vol - -from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, - SensorDeviceClass, - SensorEntity, - SensorEntityDescription, -) -from homeassistant.const import ( - CONF_DISPLAY_OPTIONS, - CONF_NAME, - PERCENTAGE, - TEMP_CELSIUS, -) -from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from homeassistant.util import Throttle - -_LOGGER = logging.getLogger(__name__) - -DEFAULT_NAME = "sensehat" -CONF_IS_HAT_ATTACHED = "is_hat_attached" - -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) - -SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( - SensorEntityDescription( - key="temperature", - name="temperature", - native_unit_of_measurement=TEMP_CELSIUS, - device_class=SensorDeviceClass.TEMPERATURE, - ), - SensorEntityDescription( - key="humidity", - name="humidity", - native_unit_of_measurement=PERCENTAGE, - ), - SensorEntityDescription( - key="pressure", - name="pressure", - native_unit_of_measurement="mb", - ), -) - -SENSOR_KEYS: list[str] = [desc.key for desc in SENSOR_TYPES] - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_DISPLAY_OPTIONS, default=SENSOR_KEYS): [vol.In(SENSOR_KEYS)], - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_IS_HAT_ATTACHED, default=True): cv.boolean, - } -) - - -def get_cpu_temp(): - """Get CPU temperature.""" - t_cpu = ( - Path("/sys/class/thermal/thermal_zone0/temp") - .read_text(encoding="utf-8") - .strip() - ) - return float(t_cpu) * 0.001 - - -def get_average(temp_base): - """Use moving average to get better readings.""" - if not hasattr(get_average, "temp"): - get_average.temp = [temp_base, temp_base, temp_base] - get_average.temp[2] = get_average.temp[1] - get_average.temp[1] = get_average.temp[0] - get_average.temp[0] = temp_base - temp_avg = (get_average.temp[0] + get_average.temp[1] + get_average.temp[2]) / 3 - return temp_avg - - -def setup_platform( - hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the Sense HAT sensor platform.""" - _LOGGER.warning( - "The Sense HAT integration is deprecated and will be removed " - "in Home Assistant Core 2022.4; this integration is removed under " - "Architectural Decision Record 0019, more information can be found here: " - "https://github.com/home-assistant/architecture/blob/master/adr/0019-GPIO.md" - ) - data = SenseHatData(config.get(CONF_IS_HAT_ATTACHED)) - display_options = config[CONF_DISPLAY_OPTIONS] - entities = [ - SenseHatSensor(data, description) - for description in SENSOR_TYPES - if description.key in display_options - ] - - add_entities(entities, True) - - -class SenseHatSensor(SensorEntity): - """Representation of a Sense HAT sensor.""" - - def __init__(self, data, description: SensorEntityDescription): - """Initialize the sensor.""" - self.entity_description = description - self.data = data - - def update(self): - """Get the latest data and updates the states.""" - self.data.update() - if not self.data.humidity: - _LOGGER.error("Don't receive data") - return - - sensor_type = self.entity_description.key - if sensor_type == "temperature": - self._attr_native_value = self.data.temperature - elif sensor_type == "humidity": - self._attr_native_value = self.data.humidity - elif sensor_type == "pressure": - self._attr_native_value = self.data.pressure - - -class SenseHatData: - """Get the latest data and update.""" - - def __init__(self, is_hat_attached): - """Initialize the data object.""" - self.temperature = None - self.humidity = None - self.pressure = None - self.is_hat_attached = is_hat_attached - - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self): - """Get the latest data from Sense HAT.""" - - sense = SenseHat() - temp_from_h = sense.get_temperature_from_humidity() - temp_from_p = sense.get_temperature_from_pressure() - t_total = (temp_from_h + temp_from_p) / 2 - - if self.is_hat_attached: - t_cpu = get_cpu_temp() - t_correct = t_total - ((t_cpu - t_total) / 1.5) - t_correct = get_average(t_correct) - else: - t_correct = get_average(t_total) - - self.temperature = t_correct - self.humidity = sense.get_humidity() - self.pressure = sense.get_pressure() diff --git a/homeassistant/components/senseme/fan.py b/homeassistant/components/senseme/fan.py index b274941b378..8dd31343058 100644 --- a/homeassistant/components/senseme/fan.py +++ b/homeassistant/components/senseme/fan.py @@ -84,7 +84,6 @@ class HASensemeFan(SensemeEntity, FanEntity): async def async_turn_on( self, - speed: str | None = None, percentage: int | None = None, preset_mode: str | None = None, **kwargs: Any, diff --git a/homeassistant/components/senseme/manifest.json b/homeassistant/components/senseme/manifest.json index 97a73434b26..793dcf4b947 100644 --- a/homeassistant/components/senseme/manifest.json +++ b/homeassistant/components/senseme/manifest.json @@ -3,16 +3,9 @@ "name": "SenseME", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/senseme", - "requirements": [ - "aiosenseme==0.6.1" - ], - "codeowners": [ - "@mikelawrence", "@bdraco" - ], - "dhcp": [ - {"registered_devices": true}, - {"macaddress":"20F85E*"} - ], + "requirements": ["aiosenseme==0.6.1"], + "codeowners": ["@mikelawrence", "@bdraco"], + "dhcp": [{ "registered_devices": true }, { "macaddress": "20F85E*" }], "iot_class": "local_push", "loggers": ["aiosenseme"] } diff --git a/homeassistant/components/senseme/strings.json b/homeassistant/components/senseme/strings.json index 9241c9058ad..f5129ee001e 100644 --- a/homeassistant/components/senseme/strings.json +++ b/homeassistant/components/senseme/strings.json @@ -1,30 +1,30 @@ { "config": { "flow_title": "{name} - {model} ({host})", - "step": { - "user": { - "description": "Select a device, or choose 'IP Address' to manually enter an IP Address.", - "data": { - "device": "Device" - } - }, - "discovery_confirm": { - "description": "Do you want to setup {name} - {model} ({host})?" - }, - "manual": { - "description": "Enter an IP Address.", - "data": { - "host": "[%key:common::config_flow::data::host%]" - } - } + "step": { + "user": { + "description": "Select a device, or choose 'IP Address' to manually enter an IP Address.", + "data": { + "device": "Device" + } }, - "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + "discovery_confirm": { + "description": "Do you want to setup {name} - {model} ({host})?" }, - "error": { - "invalid_host": "[%key:common::config_flow::error::invalid_host%]", - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + "manual": { + "description": "Enter an IP Address.", + "data": { + "host": "[%key:common::config_flow::data::host%]" + } } + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + }, + "error": { + "invalid_host": "[%key:common::config_flow::error::invalid_host%]", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + } } -} \ No newline at end of file +} diff --git a/homeassistant/components/senseme/translations/fr.json b/homeassistant/components/senseme/translations/fr.json index efa82925191..bebe137a135 100644 --- a/homeassistant/components/senseme/translations/fr.json +++ b/homeassistant/components/senseme/translations/fr.json @@ -5,13 +5,13 @@ "cannot_connect": "\u00c9chec de connexion" }, "error": { - "cannot_connect": "Impossible de se connecter", - "invalid_host": "Adresse IP ou nom d'h\u00f4te invalide" + "cannot_connect": "\u00c9chec de connexion", + "invalid_host": "Nom d'h\u00f4te ou adresse IP non valide" }, "flow_title": "{name} - {model} ({host})", "step": { "discovery_confirm": { - "description": "Voulez-vous configurer {name} - {model} ( {host} )\u00a0?" + "description": "Voulez-vous configurer {name} - {model} ({host})\u00a0?" }, "manual": { "data": { @@ -23,7 +23,7 @@ "data": { "device": "Appareil" }, - "description": "S\u00e9lectionnez un appareil ou choisissez \u00ab Adresse IP \u00bb pour entrer manuellement une adresse IP." + "description": "S\u00e9lectionnez un appareil ou choisissez \u00ab\u00a0Adresse IP\u00a0\u00bb pour saisir manuellement une adresse IP." } } } diff --git a/homeassistant/components/sensibo/__init__.py b/homeassistant/components/sensibo/__init__.py index b62482b60b5..ab8e4e85d39 100644 --- a/homeassistant/components/sensibo/__init__.py +++ b/homeassistant/components/sensibo/__init__.py @@ -1,11 +1,15 @@ """The sensibo component.""" from __future__ import annotations +from pysensibo.exceptions import AuthenticationError + from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_API_KEY from homeassistant.core import HomeAssistant -from .const import DOMAIN, PLATFORMS +from .const import DOMAIN, LOGGER, PLATFORMS from .coordinator import SensiboDataUpdateCoordinator +from .util import NoDevicesError, NoUsernameError, async_validate_api async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @@ -28,3 +32,25 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: del hass.data[DOMAIN] return True return False + + +async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Migrate old entry.""" + # Change entry unique id from api_key to username + if entry.version == 1: + api_key = entry.data[CONF_API_KEY] + + try: + new_unique_id = await async_validate_api(hass, api_key) + except (AuthenticationError, ConnectionError, NoDevicesError, NoUsernameError): + return False + + entry.version = 2 + + LOGGER.debug("Migrate Sensibo config entry unique id to %s", new_unique_id) + hass.config_entries.async_update_entry( + entry, + unique_id=new_unique_id, + ) + + return True diff --git a/homeassistant/components/sensibo/binary_sensor.py b/homeassistant/components/sensibo/binary_sensor.py new file mode 100644 index 00000000000..02f86dbe009 --- /dev/null +++ b/homeassistant/components/sensibo/binary_sensor.py @@ -0,0 +1,177 @@ +"""Binary Sensor platform for Sensibo integration.""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass + +from pysensibo.model import MotionSensor, SensiboDevice + +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + BinarySensorEntity, + BinarySensorEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .coordinator import SensiboDataUpdateCoordinator +from .entity import SensiboDeviceBaseEntity, SensiboMotionBaseEntity + + +@dataclass +class MotionBaseEntityDescriptionMixin: + """Mixin for required Sensibo base description keys.""" + + value_fn: Callable[[MotionSensor], bool | None] + + +@dataclass +class DeviceBaseEntityDescriptionMixin: + """Mixin for required Sensibo base description keys.""" + + value_fn: Callable[[SensiboDevice], bool | None] + + +@dataclass +class SensiboMotionBinarySensorEntityDescription( + BinarySensorEntityDescription, MotionBaseEntityDescriptionMixin +): + """Describes Sensibo Motion sensor entity.""" + + +@dataclass +class SensiboDeviceBinarySensorEntityDescription( + BinarySensorEntityDescription, DeviceBaseEntityDescriptionMixin +): + """Describes Sensibo Motion sensor entity.""" + + +MOTION_SENSOR_TYPES: tuple[SensiboMotionBinarySensorEntityDescription, ...] = ( + SensiboMotionBinarySensorEntityDescription( + key="alive", + device_class=BinarySensorDeviceClass.CONNECTIVITY, + entity_category=EntityCategory.DIAGNOSTIC, + name="Alive", + icon="mdi:wifi", + value_fn=lambda data: data.alive, + ), + SensiboMotionBinarySensorEntityDescription( + key="is_main_sensor", + entity_category=EntityCategory.DIAGNOSTIC, + name="Main Sensor", + icon="mdi:connection", + value_fn=lambda data: data.is_main_sensor, + ), + SensiboMotionBinarySensorEntityDescription( + key="motion", + device_class=BinarySensorDeviceClass.MOTION, + name="Motion", + icon="mdi:motion-sensor", + value_fn=lambda data: data.motion, + ), +) + +DEVICE_SENSOR_TYPES: tuple[SensiboDeviceBinarySensorEntityDescription, ...] = ( + SensiboDeviceBinarySensorEntityDescription( + key="room_occupied", + device_class=BinarySensorDeviceClass.MOTION, + name="Room Occupied", + icon="mdi:motion-sensor", + value_fn=lambda data: data.room_occupied, + ), + SensiboDeviceBinarySensorEntityDescription( + key="update_available", + device_class=BinarySensorDeviceClass.UPDATE, + entity_category=EntityCategory.DIAGNOSTIC, + name="Update Available", + icon="mdi:rocket-launch", + value_fn=lambda data: data.update_available, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up Sensibo binary sensor platform.""" + + coordinator: SensiboDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + + entities: list[SensiboMotionSensor | SensiboDeviceSensor] = [] + entities.extend( + SensiboMotionSensor(coordinator, device_id, sensor_id, sensor_data, description) + for device_id, device_data in coordinator.data.parsed.items() + for sensor_id, sensor_data in device_data.motion_sensors.items() + for description in MOTION_SENSOR_TYPES + if device_data.motion_sensors + ) + entities.extend( + SensiboDeviceSensor(coordinator, device_id, description) + for description in DEVICE_SENSOR_TYPES + for device_id, device_data in coordinator.data.parsed.items() + if getattr(device_data, description.key) is not None + ) + + async_add_entities(entities) + + +class SensiboMotionSensor(SensiboMotionBaseEntity, BinarySensorEntity): + """Representation of a Sensibo Motion Binary Sensor.""" + + entity_description: SensiboMotionBinarySensorEntityDescription + + def __init__( + self, + coordinator: SensiboDataUpdateCoordinator, + device_id: str, + sensor_id: str, + sensor_data: MotionSensor, + entity_description: SensiboMotionBinarySensorEntityDescription, + ) -> None: + """Initiate Sensibo Motion Binary Sensor.""" + super().__init__( + coordinator, + device_id, + sensor_id, + sensor_data, + entity_description.name, + ) + self.entity_description = entity_description + self._attr_unique_id = f"{sensor_id}-{entity_description.key}" + self._attr_name = ( + f"{self.device_data.name} Motion Sensor {entity_description.name}" + ) + + @property + def is_on(self) -> bool | None: + """Return true if the binary sensor is on.""" + return self.entity_description.value_fn(self.sensor_data) + + +class SensiboDeviceSensor(SensiboDeviceBaseEntity, BinarySensorEntity): + """Representation of a Sensibo Device Binary Sensor.""" + + entity_description: SensiboDeviceBinarySensorEntityDescription + + def __init__( + self, + coordinator: SensiboDataUpdateCoordinator, + device_id: str, + entity_description: SensiboDeviceBinarySensorEntityDescription, + ) -> None: + """Initiate Sensibo Device Binary Sensor.""" + super().__init__( + coordinator, + device_id, + ) + self.entity_description = entity_description + self._attr_unique_id = f"{device_id}-{entity_description.key}" + self._attr_name = f"{self.device_data.name} {entity_description.name}" + + @property + def is_on(self) -> bool | None: + """Return true if the binary sensor is on.""" + return self.entity_description.value_fn(self.device_data) diff --git a/homeassistant/components/sensibo/climate.py b/homeassistant/components/sensibo/climate.py index f829fd9ed39..8f7b671a948 100644 --- a/homeassistant/components/sensibo/climate.py +++ b/homeassistant/components/sensibo/climate.py @@ -1,17 +1,9 @@ """Support for Sensibo wifi-enabled home thermostats.""" from __future__ import annotations -import asyncio - -from aiohttp.client_exceptions import ClientConnectionError -import async_timeout -from pysensibo.exceptions import AuthenticationError, SensiboError import voluptuous as vol -from homeassistant.components.climate import ( - PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, - ClimateEntity, -) +from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( HVAC_MODE_COOL, HVAC_MODE_DRY, @@ -23,37 +15,26 @@ from homeassistant.components.climate.const import ( SUPPORT_SWING_MODE, SUPPORT_TARGET_TEMPERATURE, ) -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_STATE, ATTR_TEMPERATURE, - CONF_API_KEY, - CONF_ID, + PRECISION_TENTHS, TEMP_CELSIUS, TEMP_FAHRENHEIT, ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import config_validation as cv, entity_platform -from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers import entity_platform from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util.temperature import convert as convert_temperature -from .const import ALL, DOMAIN, LOGGER, TIMEOUT +from .const import DOMAIN from .coordinator import SensiboDataUpdateCoordinator +from .entity import SensiboDeviceBaseEntity SERVICE_ASSUME_STATE = "assume_state" -PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_API_KEY): cv.string, - vol.Optional(CONF_ID, default=ALL): vol.All(cv.ensure_list, [cv.string]), - } -) - FIELD_TO_FLAG = { "fanLevel": SUPPORT_FAN_MODE, "swing": SUPPORT_SWING_MODE, @@ -80,25 +61,6 @@ AC_STATE_TO_DATA = { } -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up Sensibo devices.""" - LOGGER.warning( - "Loading Sensibo via platform setup is deprecated; Please remove it from your configuration" - ) - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data=config, - ) - ) - - async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: @@ -108,9 +70,7 @@ async def async_setup_entry( entities = [ SensiboClimate(coordinator, device_id) - for device_id, device_data in coordinator.data.items() - # Remove none climate devices - if device_data["hvac_modes"] and device_data["temp"] + for device_id, device_data in coordinator.data.parsed.items() ] async_add_entities(entities) @@ -125,72 +85,54 @@ async def async_setup_entry( ) -class SensiboClimate(CoordinatorEntity, ClimateEntity): +class SensiboClimate(SensiboDeviceBaseEntity, ClimateEntity): """Representation of a Sensibo device.""" - coordinator: SensiboDataUpdateCoordinator - def __init__( self, coordinator: SensiboDataUpdateCoordinator, device_id: str ) -> None: """Initiate SensiboClimate.""" - super().__init__(coordinator) - self._client = coordinator.client + super().__init__(coordinator, device_id) self._attr_unique_id = device_id - self._attr_name = coordinator.data[device_id]["name"] + self._attr_name = self.device_data.name self._attr_temperature_unit = ( - TEMP_CELSIUS - if coordinator.data[device_id]["temp_unit"] == "C" - else TEMP_FAHRENHEIT + TEMP_CELSIUS if self.device_data.temp_unit == "C" else TEMP_FAHRENHEIT ) self._attr_supported_features = self.get_features() - self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, coordinator.data[device_id]["id"])}, - name=coordinator.data[device_id]["name"], - connections={(CONNECTION_NETWORK_MAC, coordinator.data[device_id]["mac"])}, - manufacturer="Sensibo", - configuration_url="https://home.sensibo.com/", - model=coordinator.data[device_id]["model"], - sw_version=coordinator.data[device_id]["fw_ver"], - hw_version=coordinator.data[device_id]["fw_type"], - suggested_area=coordinator.data[device_id]["name"], - ) + self._attr_precision = PRECISION_TENTHS def get_features(self) -> int: """Get supported features.""" features = 0 - for key in self.coordinator.data[self.unique_id]["full_features"]: + for key in self.device_data.full_features: if key in FIELD_TO_FLAG: features |= FIELD_TO_FLAG[key] return features @property - def current_humidity(self) -> int: + def current_humidity(self) -> int | None: """Return the current humidity.""" - return self.coordinator.data[self.unique_id]["humidity"] + return self.device_data.humidity @property def hvac_mode(self) -> str: """Return hvac operation.""" return ( - SENSIBO_TO_HA[self.coordinator.data[self.unique_id]["hvac_mode"]] - if self.coordinator.data[self.unique_id]["on"] + SENSIBO_TO_HA[self.device_data.hvac_mode] + if self.device_data.device_on else HVAC_MODE_OFF ) @property def hvac_modes(self) -> list[str]: """Return the list of available hvac operation modes.""" - return [ - SENSIBO_TO_HA[mode] - for mode in self.coordinator.data[self.unique_id]["hvac_modes"] - ] + return [SENSIBO_TO_HA[mode] for mode in self.device_data.hvac_modes] @property - def current_temperature(self) -> float: + def current_temperature(self) -> float | None: """Return the current temperature.""" return convert_temperature( - self.coordinator.data[self.unique_id]["temp"], + self.device_data.temp, TEMP_CELSIUS, self.temperature_unit, ) @@ -198,54 +140,51 @@ class SensiboClimate(CoordinatorEntity, ClimateEntity): @property def target_temperature(self) -> float | None: """Return the temperature we try to reach.""" - return self.coordinator.data[self.unique_id]["target_temp"] + return self.device_data.target_temp @property def target_temperature_step(self) -> float | None: """Return the supported step of target temperature.""" - return self.coordinator.data[self.unique_id]["temp_step"] + return self.device_data.temp_step @property def fan_mode(self) -> str | None: """Return the fan setting.""" - return self.coordinator.data[self.unique_id]["fan_mode"] + return self.device_data.fan_mode @property def fan_modes(self) -> list[str] | None: """Return the list of available fan modes.""" - return self.coordinator.data[self.unique_id]["fan_modes"] + return self.device_data.fan_modes @property def swing_mode(self) -> str | None: """Return the swing setting.""" - return self.coordinator.data[self.unique_id]["swing_mode"] + return self.device_data.swing_mode @property def swing_modes(self) -> list[str] | None: """Return the list of available swing modes.""" - return self.coordinator.data[self.unique_id]["swing_modes"] + return self.device_data.swing_modes @property def min_temp(self) -> float: """Return the minimum temperature.""" - return self.coordinator.data[self.unique_id]["temp_list"][0] + return self.device_data.temp_list[0] @property def max_temp(self) -> float: """Return the maximum temperature.""" - return self.coordinator.data[self.unique_id]["temp_list"][-1] + return self.device_data.temp_list[-1] @property def available(self) -> bool: """Return True if entity is available.""" - return self.coordinator.data[self.unique_id]["available"] and super().available + return self.device_data.available and super().available async def async_set_temperature(self, **kwargs) -> None: """Set new target temperature.""" - if ( - "targetTemperature" - not in self.coordinator.data[self.unique_id]["active_features"] - ): + if "targetTemperature" not in self.device_data.active_features: raise HomeAssistantError( "Current mode doesn't support setting Target Temperature" ) @@ -256,13 +195,13 @@ class SensiboClimate(CoordinatorEntity, ClimateEntity): if temperature == self.target_temperature: return - if temperature not in self.coordinator.data[self.unique_id]["temp_list"]: + if temperature not in self.device_data.temp_list: # Requested temperature is not supported. - if temperature > self.coordinator.data[self.unique_id]["temp_list"][-1]: - temperature = self.coordinator.data[self.unique_id]["temp_list"][-1] + if temperature > self.device_data.temp_list[-1]: + temperature = self.device_data.temp_list[-1] - elif temperature < self.coordinator.data[self.unique_id]["temp_list"][0]: - temperature = self.coordinator.data[self.unique_id]["temp_list"][0] + elif temperature < self.device_data.temp_list[0]: + temperature = self.device_data.temp_list[0] else: return @@ -271,7 +210,7 @@ class SensiboClimate(CoordinatorEntity, ClimateEntity): async def async_set_fan_mode(self, fan_mode: str) -> None: """Set new target fan mode.""" - if "fanLevel" not in self.coordinator.data[self.unique_id]["active_features"]: + if "fanLevel" not in self.device_data.active_features: raise HomeAssistantError("Current mode doesn't support setting Fanlevel") await self._async_set_ac_state_property("fanLevel", fan_mode) @@ -283,14 +222,15 @@ class SensiboClimate(CoordinatorEntity, ClimateEntity): return # Turn on if not currently on. - if not self.coordinator.data[self.unique_id]["on"]: + if not self.device_data.device_on: await self._async_set_ac_state_property("on", True) await self._async_set_ac_state_property("mode", HA_TO_SENSIBO[hvac_mode]) + await self.coordinator.async_request_refresh() async def async_set_swing_mode(self, swing_mode: str) -> None: """Set new target swing operation.""" - if "swing" not in self.coordinator.data[self.unique_id]["active_features"]: + if "swing" not in self.device_data.active_features: raise HomeAssistantError("Current mode doesn't support setting Swing") await self._async_set_ac_state_property("swing", swing_mode) @@ -307,28 +247,16 @@ class SensiboClimate(CoordinatorEntity, ClimateEntity): self, name: str, value: str | int | bool, assumed_state: bool = False ) -> None: """Set AC state.""" - result = {} - try: - async with async_timeout.timeout(TIMEOUT): - result = await self._client.async_set_ac_state_property( - self.unique_id, - name, - value, - self.coordinator.data[self.unique_id]["ac_states"], - assumed_state, - ) - except ( - ClientConnectionError, - asyncio.TimeoutError, - AuthenticationError, - SensiboError, - ) as err: - raise HomeAssistantError( - f"Failed to set AC state for device {self.name} to Sensibo servers: {err}" - ) from err - LOGGER.debug("Result: %s", result) + params = { + "name": name, + "value": value, + "ac_states": self.device_data.ac_states, + "assumed_state": assumed_state, + } + result = await self.async_send_command("set_ac_state", params) + if result["result"]["status"] == "Success": - self.coordinator.data[self.unique_id][AC_STATE_TO_DATA[name]] = value + setattr(self.device_data, AC_STATE_TO_DATA[name], value) self.async_write_ha_state() return diff --git a/homeassistant/components/sensibo/config_flow.py b/homeassistant/components/sensibo/config_flow.py index f970581e2a8..c4b637e4439 100644 --- a/homeassistant/components/sensibo/config_flow.py +++ b/homeassistant/components/sensibo/config_flow.py @@ -1,25 +1,18 @@ """Adds config flow for Sensibo integration.""" from __future__ import annotations -import asyncio -import logging +from typing import Any -import aiohttp -import async_timeout -from pysensibo import SensiboClient -from pysensibo.exceptions import AuthenticationError, SensiboError +from pysensibo.exceptions import AuthenticationError import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_API_KEY -from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResult -from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv -from .const import DEFAULT_NAME, DOMAIN, TIMEOUT - -_LOGGER = logging.getLogger(__name__) +from .const import DEFAULT_NAME, DOMAIN +from .util import NoDevicesError, NoUsernameError, async_validate_api DATA_SCHEMA = vol.Schema( { @@ -28,40 +21,59 @@ DATA_SCHEMA = vol.Schema( ) -async def async_validate_api(hass: HomeAssistant, api_key: str) -> bool: - """Get data from API.""" - client = SensiboClient( - api_key, - session=async_get_clientsession(hass), - timeout=TIMEOUT, - ) - - try: - async with async_timeout.timeout(TIMEOUT): - if await client.async_get_devices(): - return True - except ( - aiohttp.ClientConnectionError, - asyncio.TimeoutError, - AuthenticationError, - SensiboError, - ) as err: - _LOGGER.error("Failed to get devices from Sensibo servers %s", err) - return False - - class SensiboConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for Sensibo integration.""" - VERSION = 1 + VERSION = 2 - async def async_step_import(self, config: dict) -> FlowResult: - """Import a configuration from config.yaml.""" + entry: config_entries.ConfigEntry | None - self.context.update( - {"title_placeholders": {"Sensibo": f"YAML import {DOMAIN}"}} + async def async_step_reauth( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle re-authentication with Sensibo.""" + + self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Confirm re-authentication with Sensibo.""" + errors: dict[str, str] = {} + + if user_input: + api_key = user_input[CONF_API_KEY] + try: + username = await async_validate_api(self.hass, api_key) + except AuthenticationError: + errors["base"] = "invalid_auth" + except ConnectionError: + errors["base"] = "cannot_connect" + except NoDevicesError: + errors["base"] = "no_devices" + except NoUsernameError: + errors["base"] = "no_username" + else: + assert self.entry is not None + + if username == self.entry.unique_id: + self.hass.config_entries.async_update_entry( + self.entry, + data={ + **self.entry.data, + CONF_API_KEY: api_key, + }, + ) + await self.hass.config_entries.async_reload(self.entry.entry_id) + return self.async_abort(reason="reauth_successful") + errors["base"] = "incorrect_api_key" + + return self.async_show_form( + step_id="reauth_confirm", + data_schema=DATA_SCHEMA, + errors=errors, ) - return await self.async_step_user(user_input=config) async def async_step_user(self, user_input=None) -> FlowResult: """Handle the initial step.""" @@ -71,17 +83,24 @@ class SensiboConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if user_input: api_key = user_input[CONF_API_KEY] + try: + username = await async_validate_api(self.hass, api_key) + except AuthenticationError: + errors["base"] = "invalid_auth" + except ConnectionError: + errors["base"] = "cannot_connect" + except NoDevicesError: + errors["base"] = "no_devices" + except NoUsernameError: + errors["base"] = "no_username" + else: + await self.async_set_unique_id(username) + self._abort_if_unique_id_configured() - await self.async_set_unique_id(api_key) - self._abort_if_unique_id_configured() - - validate = await async_validate_api(self.hass, api_key) - if validate: return self.async_create_entry( title=DEFAULT_NAME, data={CONF_API_KEY: api_key}, ) - errors["base"] = "cannot_connect" return self.async_show_form( step_id="user", diff --git a/homeassistant/components/sensibo/const.py b/homeassistant/components/sensibo/const.py index 683a403cb08..ac3df435e29 100644 --- a/homeassistant/components/sensibo/const.py +++ b/homeassistant/components/sensibo/const.py @@ -12,8 +12,13 @@ LOGGER = logging.getLogger(__package__) DEFAULT_SCAN_INTERVAL = 60 DOMAIN = "sensibo" -PLATFORMS = [Platform.CLIMATE, Platform.NUMBER] -ALL = ["all"] +PLATFORMS = [ + Platform.BINARY_SENSOR, + Platform.CLIMATE, + Platform.NUMBER, + Platform.SELECT, + Platform.SENSOR, +] DEFAULT_NAME = "Sensibo" TIMEOUT = 8 diff --git a/homeassistant/components/sensibo/coordinator.py b/homeassistant/components/sensibo/coordinator.py index a76654e3c68..a0321bf611e 100644 --- a/homeassistant/components/sensibo/coordinator.py +++ b/homeassistant/components/sensibo/coordinator.py @@ -2,25 +2,26 @@ from __future__ import annotations from datetime import timedelta -from typing import Any from pysensibo import SensiboClient from pysensibo.exceptions import AuthenticationError, SensiboError +from pysensibo.model import SensiboData from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import DEFAULT_SCAN_INTERVAL, DOMAIN, LOGGER, TIMEOUT -MAX_POSSIBLE_STEP = 1000 - class SensiboDataUpdateCoordinator(DataUpdateCoordinator): """A Sensibo Data Update Coordinator.""" + data: SensiboData + def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: """Initialize the Sensibo coordinator.""" self.client = SensiboClient( @@ -35,99 +36,16 @@ class SensiboDataUpdateCoordinator(DataUpdateCoordinator): update_interval=timedelta(seconds=DEFAULT_SCAN_INTERVAL), ) - async def _async_update_data(self) -> dict[str, dict[str, Any]]: + async def _async_update_data(self) -> SensiboData: """Fetch data from Sensibo.""" - devices = [] try: - data = await self.client.async_get_devices() - for dev in data["result"]: - devices.append(dev) - except (AuthenticationError, SensiboError) as error: + data = await self.client.async_get_devices_data() + except AuthenticationError as error: + raise ConfigEntryAuthFailed from error + except SensiboError as error: raise UpdateFailed from error - device_data: dict[str, dict[str, Any]] = {} - for dev in devices: - unique_id = dev["id"] - mac = dev["macAddress"] - name = dev["room"]["name"] - temperature = dev["measurements"].get("temperature", 0.0) - humidity = dev["measurements"].get("humidity", 0) - ac_states = dev["acState"] - target_temperature = ac_states.get("targetTemperature") - hvac_mode = ac_states.get("mode") - running = ac_states.get("on") - fan_mode = ac_states.get("fanLevel") - swing_mode = ac_states.get("swing") - available = dev["connectionStatus"].get("isAlive", True) - capabilities = dev["remoteCapabilities"] - hvac_modes = list(capabilities["modes"]) - if hvac_modes: - hvac_modes.append("off") - current_capabilities = capabilities["modes"][ac_states.get("mode")] - fan_modes = current_capabilities.get("fanLevels") - swing_modes = current_capabilities.get("swing") - temperature_unit_key = dev.get("temperatureUnit") or ac_states.get( - "temperatureUnit" - ) - temperatures_list = ( - current_capabilities["temperatures"] - .get(temperature_unit_key, {}) - .get("values", [0, 1]) - ) - if temperatures_list: - diff = MAX_POSSIBLE_STEP - for i in range(len(temperatures_list) - 1): - if temperatures_list[i + 1] - temperatures_list[i] < diff: - diff = temperatures_list[i + 1] - temperatures_list[i] - temperature_step = diff - - active_features = list(ac_states) - full_features = set() - for mode in capabilities["modes"]: - if "temperatures" in capabilities["modes"][mode]: - full_features.add("targetTemperature") - if "swing" in capabilities["modes"][mode]: - full_features.add("swing") - if "fanLevels" in capabilities["modes"][mode]: - full_features.add("fanLevel") - - state = hvac_mode if hvac_mode else "off" - - fw_ver = dev["firmwareVersion"] - fw_type = dev["firmwareType"] - model = dev["productModel"] - - calibration_temp = dev["sensorsCalibration"].get("temperature", 0.0) - calibration_hum = dev["sensorsCalibration"].get("humidity", 0.0) - - device_data[unique_id] = { - "id": unique_id, - "mac": mac, - "name": name, - "ac_states": ac_states, - "temp": temperature, - "humidity": humidity, - "target_temp": target_temperature, - "hvac_mode": hvac_mode, - "on": running, - "fan_mode": fan_mode, - "swing_mode": swing_mode, - "available": available, - "hvac_modes": hvac_modes, - "fan_modes": fan_modes, - "swing_modes": swing_modes, - "temp_unit": temperature_unit_key, - "temp_list": temperatures_list, - "temp_step": temperature_step, - "active_features": active_features, - "full_features": full_features, - "state": state, - "fw_ver": fw_ver, - "fw_type": fw_type, - "model": model, - "calibration_temp": calibration_temp, - "calibration_hum": calibration_hum, - "full_capabilities": capabilities, - } - return device_data + if not data.raw: + raise UpdateFailed("No devices found") + return data diff --git a/homeassistant/components/sensibo/diagnostics.py b/homeassistant/components/sensibo/diagnostics.py index d3e2382c7a8..e4a4672bf64 100644 --- a/homeassistant/components/sensibo/diagnostics.py +++ b/homeassistant/components/sensibo/diagnostics.py @@ -3,16 +3,34 @@ from __future__ import annotations from typing import Any +from homeassistant.components.diagnostics.util import async_redact_data from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from .const import DOMAIN from .coordinator import SensiboDataUpdateCoordinator +TO_REDACT = { + "location", + "ssid", + "id", + "macAddress", + "parentDeviceUid", + "qrId", + "serial", + "uid", + "email", + "firstName", + "lastName", + "username", + "podUid", + "deviceUid", +} + async def async_get_config_entry_diagnostics( hass: HomeAssistant, entry: ConfigEntry ) -> dict[str, Any]: """Return diagnostics for a config entry.""" coordinator: SensiboDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - return coordinator.data + return async_redact_data(coordinator.data.raw, TO_REDACT) diff --git a/homeassistant/components/sensibo/entity.py b/homeassistant/components/sensibo/entity.py new file mode 100644 index 00000000000..ce85ecf2a38 --- /dev/null +++ b/homeassistant/components/sensibo/entity.py @@ -0,0 +1,124 @@ +"""Base entity for Sensibo integration.""" +from __future__ import annotations + +from typing import Any + +import async_timeout +from pysensibo.model import MotionSensor, SensiboDevice + +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN, LOGGER, SENSIBO_ERRORS, TIMEOUT +from .coordinator import SensiboDataUpdateCoordinator + + +class SensiboBaseEntity(CoordinatorEntity[SensiboDataUpdateCoordinator]): + """Representation of a Sensibo entity.""" + + def __init__( + self, + coordinator: SensiboDataUpdateCoordinator, + device_id: str, + ) -> None: + """Initiate Sensibo Number.""" + super().__init__(coordinator) + self._device_id = device_id + self._client = coordinator.client + + @property + def device_data(self) -> SensiboDevice: + """Return data for device.""" + return self.coordinator.data.parsed[self._device_id] + + +class SensiboDeviceBaseEntity(SensiboBaseEntity): + """Representation of a Sensibo device.""" + + def __init__( + self, + coordinator: SensiboDataUpdateCoordinator, + device_id: str, + ) -> None: + """Initiate Sensibo Number.""" + super().__init__(coordinator, device_id) + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, self.device_data.id)}, + name=self.device_data.name, + connections={(CONNECTION_NETWORK_MAC, self.device_data.mac)}, + manufacturer="Sensibo", + configuration_url="https://home.sensibo.com/", + model=self.device_data.model, + sw_version=self.device_data.fw_ver, + hw_version=self.device_data.fw_type, + suggested_area=self.device_data.name, + ) + + async def async_send_command( + self, command: str, params: dict[str, Any] + ) -> dict[str, Any]: + """Send command to Sensibo api.""" + try: + async with async_timeout.timeout(TIMEOUT): + result = await self.async_send_api_call(command, params) + except SENSIBO_ERRORS as err: + raise HomeAssistantError( + f"Failed to send command {command} for device {self.name} to Sensibo servers: {err}" + ) from err + + LOGGER.debug("Result: %s", result) + return result + + async def async_send_api_call( + self, command: str, params: dict[str, Any] + ) -> dict[str, Any]: + """Send api call.""" + result: dict[str, Any] = {"status": None} + if command == "set_calibration": + result = await self._client.async_set_calibration( + self._device_id, + params["data"], + ) + if command == "set_ac_state": + result = await self._client.async_set_ac_state_property( + self._device_id, + params["name"], + params["value"], + params["ac_states"], + params["assumed_state"], + ) + return result + + +class SensiboMotionBaseEntity(SensiboBaseEntity): + """Representation of a Sensibo motion entity.""" + + def __init__( + self, + coordinator: SensiboDataUpdateCoordinator, + device_id: str, + sensor_id: str, + sensor_data: MotionSensor, + name: str | None, + ) -> None: + """Initiate Sensibo Number.""" + super().__init__(coordinator, device_id) + self._sensor_id = sensor_id + + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, sensor_id)}, + name=f"{self.device_data.name} Motion Sensor {name}", + via_device=(DOMAIN, device_id), + manufacturer="Sensibo", + configuration_url="https://home.sensibo.com/", + model=sensor_data.model, + sw_version=sensor_data.fw_ver, + hw_version=sensor_data.fw_type, + ) + + @property + def sensor_data(self) -> MotionSensor: + """Return data for device.""" + return self.device_data.motion_sensors[self._sensor_id] diff --git a/homeassistant/components/sensibo/manifest.json b/homeassistant/components/sensibo/manifest.json index 35273bb3d6f..233bf009c75 100644 --- a/homeassistant/components/sensibo/manifest.json +++ b/homeassistant/components/sensibo/manifest.json @@ -2,15 +2,13 @@ "domain": "sensibo", "name": "Sensibo", "documentation": "https://www.home-assistant.io/integrations/sensibo", - "requirements": ["pysensibo==1.0.7"], + "requirements": ["pysensibo==1.0.9"], "config_flow": true, "codeowners": ["@andrey-git", "@gjohansson-ST"], "iot_class": "cloud_polling", "homekit": { "models": ["Sensibo"] }, - "dhcp": [ - {"hostname":"sensibo*"} - ], + "dhcp": [{ "hostname": "sensibo*" }], "loggers": ["pysensibo"] } diff --git a/homeassistant/components/sensibo/number.py b/homeassistant/components/sensibo/number.py index 9e531249bf7..69d9237da7a 100644 --- a/homeassistant/components/sensibo/number.py +++ b/homeassistant/components/sensibo/number.py @@ -3,19 +3,16 @@ from __future__ import annotations from dataclasses import dataclass -import async_timeout - from homeassistant.components.number import NumberEntity, NumberEntityDescription from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC -from homeassistant.helpers.entity import DeviceInfo, EntityCategory +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import DOMAIN, LOGGER, SENSIBO_ERRORS, TIMEOUT +from .const import DOMAIN from .coordinator import SensiboDataUpdateCoordinator +from .entity import SensiboDeviceBaseEntity @dataclass @@ -67,16 +64,14 @@ async def async_setup_entry( async_add_entities( SensiboNumber(coordinator, device_id, description) - for device_id, device_data in coordinator.data.items() + for device_id, device_data in coordinator.data.parsed.items() for description in NUMBER_TYPES - if device_data["hvac_modes"] and device_data["temp"] ) -class SensiboNumber(CoordinatorEntity, NumberEntity): +class SensiboNumber(SensiboDeviceBaseEntity, NumberEntity): """Representation of a Sensibo numbers.""" - coordinator: SensiboDataUpdateCoordinator entity_description: SensiboNumberEntityDescription def __init__( @@ -86,47 +81,22 @@ class SensiboNumber(CoordinatorEntity, NumberEntity): entity_description: SensiboNumberEntityDescription, ) -> None: """Initiate Sensibo Number.""" - super().__init__(coordinator) + super().__init__(coordinator, device_id) self.entity_description = entity_description - self._device_id = device_id - self._client = coordinator.client self._attr_unique_id = f"{device_id}-{entity_description.key}" - self._attr_name = ( - f"{coordinator.data[device_id]['name']} {entity_description.name}" - ) - self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, coordinator.data[device_id]["id"])}, - name=coordinator.data[device_id]["name"], - connections={(CONNECTION_NETWORK_MAC, coordinator.data[device_id]["mac"])}, - manufacturer="Sensibo", - configuration_url="https://home.sensibo.com/", - model=coordinator.data[device_id]["model"], - sw_version=coordinator.data[device_id]["fw_ver"], - hw_version=coordinator.data[device_id]["fw_type"], - suggested_area=coordinator.data[device_id]["name"], - ) + self._attr_name = f"{self.device_data.name} {entity_description.name}" @property - def value(self) -> float: + def value(self) -> float | None: """Return the value from coordinator data.""" - return self.coordinator.data[self._device_id][self.entity_description.key] + return getattr(self.device_data, self.entity_description.key) async def async_set_value(self, value: float) -> None: """Set value for calibration.""" data = {self.entity_description.remote_key: value} - try: - async with async_timeout.timeout(TIMEOUT): - result = await self._client.async_set_calibration( - self._device_id, - data, - ) - except SENSIBO_ERRORS as err: - raise HomeAssistantError( - f"Failed to set calibration for device {self.name} to Sensibo servers: {err}" - ) from err - LOGGER.debug("Result: %s", result) + result = await self.async_send_command("set_calibration", {"data": data}) if result["status"] == "success": - self.coordinator.data[self._device_id][self.entity_description.key] = value + setattr(self.device_data, self.entity_description.key, value) self.async_write_ha_state() return raise HomeAssistantError(f"Could not set calibration for device {self.name}") diff --git a/homeassistant/components/sensibo/select.py b/homeassistant/components/sensibo/select.py new file mode 100644 index 00000000000..c442fbc1374 --- /dev/null +++ b/homeassistant/components/sensibo/select.py @@ -0,0 +1,115 @@ +"""Number platform for Sensibo integration.""" +from __future__ import annotations + +from dataclasses import dataclass + +from homeassistant.components.select import SelectEntity, SelectEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .coordinator import SensiboDataUpdateCoordinator +from .entity import SensiboDeviceBaseEntity + + +@dataclass +class SensiboSelectDescriptionMixin: + """Mixin values for Sensibo entities.""" + + remote_key: str + remote_options: str + + +@dataclass +class SensiboSelectEntityDescription( + SelectEntityDescription, SensiboSelectDescriptionMixin +): + """Class describing Sensibo Number entities.""" + + +SELECT_TYPES = ( + SensiboSelectEntityDescription( + key="horizontalSwing", + remote_key="horizontal_swing_mode", + remote_options="horizontal_swing_modes", + name="Horizontal Swing", + icon="mdi:air-conditioner", + ), + SensiboSelectEntityDescription( + key="light", + remote_key="light_mode", + remote_options="light_modes", + name="Light", + icon="mdi:flashlight", + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up Sensibo number platform.""" + + coordinator: SensiboDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + + async_add_entities( + SensiboSelect(coordinator, device_id, description) + for device_id, device_data in coordinator.data.parsed.items() + for description in SELECT_TYPES + if description.key in device_data.full_features + ) + + +class SensiboSelect(SensiboDeviceBaseEntity, SelectEntity): + """Representation of a Sensibo Select.""" + + entity_description: SensiboSelectEntityDescription + + def __init__( + self, + coordinator: SensiboDataUpdateCoordinator, + device_id: str, + entity_description: SensiboSelectEntityDescription, + ) -> None: + """Initiate Sensibo Select.""" + super().__init__(coordinator, device_id) + self.entity_description = entity_description + self._attr_unique_id = f"{device_id}-{entity_description.key}" + self._attr_name = f"{self.device_data.name} {entity_description.name}" + + @property + def current_option(self) -> str | None: + """Return the current selected option.""" + return getattr(self.device_data, self.entity_description.remote_key) + + @property + def options(self) -> list[str]: + """Return possible options.""" + return getattr(self.device_data, self.entity_description.remote_options) or [] + + async def async_select_option(self, option: str) -> None: + """Set state to the selected option.""" + if self.entity_description.key not in self.device_data.active_features: + raise HomeAssistantError( + f"Current mode {self.device_data.hvac_mode} doesn't support setting {self.entity_description.name}" + ) + + params = { + "name": self.entity_description.key, + "value": option, + "ac_states": self.device_data.ac_states, + "assumed_state": False, + } + result = await self.async_send_command("set_ac_state", params) + + if result["result"]["status"] == "Success": + setattr(self.device_data, self.entity_description.remote_key, option) + self.async_write_ha_state() + return + + failure = result["result"]["failureReason"] + raise HomeAssistantError( + f"Could not set state for device {self.name} due to reason {failure}" + ) diff --git a/homeassistant/components/sensibo/sensor.py b/homeassistant/components/sensibo/sensor.py new file mode 100644 index 00000000000..307cfac6003 --- /dev/null +++ b/homeassistant/components/sensibo/sensor.py @@ -0,0 +1,202 @@ +"""Sensor platform for Sensibo integration.""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass + +from pysensibo.model import MotionSensor, SensiboDevice + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + ELECTRIC_POTENTIAL_VOLT, + PERCENTAGE, + SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + TEMP_CELSIUS, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import StateType + +from .const import DOMAIN +from .coordinator import SensiboDataUpdateCoordinator +from .entity import SensiboDeviceBaseEntity, SensiboMotionBaseEntity + + +@dataclass +class MotionBaseEntityDescriptionMixin: + """Mixin for required Sensibo base description keys.""" + + value_fn: Callable[[MotionSensor], StateType] + + +@dataclass +class DeviceBaseEntityDescriptionMixin: + """Mixin for required Sensibo base description keys.""" + + value_fn: Callable[[SensiboDevice], StateType] + + +@dataclass +class SensiboMotionSensorEntityDescription( + SensorEntityDescription, MotionBaseEntityDescriptionMixin +): + """Describes Sensibo Motion sensor entity.""" + + +@dataclass +class SensiboDeviceSensorEntityDescription( + SensorEntityDescription, DeviceBaseEntityDescriptionMixin +): + """Describes Sensibo Motion sensor entity.""" + + +MOTION_SENSOR_TYPES: tuple[SensiboMotionSensorEntityDescription, ...] = ( + SensiboMotionSensorEntityDescription( + key="rssi", + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + state_class=SensorStateClass.MEASUREMENT, + name="rssi", + icon="mdi:wifi", + value_fn=lambda data: data.rssi, + entity_registry_enabled_default=False, + ), + SensiboMotionSensorEntityDescription( + key="battery_voltage", + device_class=SensorDeviceClass.VOLTAGE, + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + state_class=SensorStateClass.MEASUREMENT, + name="Battery Voltage", + icon="mdi:battery", + value_fn=lambda data: data.battery_voltage, + ), + SensiboMotionSensorEntityDescription( + key="humidity", + device_class=SensorDeviceClass.HUMIDITY, + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + name="Humidity", + icon="mdi:water", + value_fn=lambda data: data.humidity, + ), + SensiboMotionSensorEntityDescription( + key="temperature", + device_class=SensorDeviceClass.TEMPERATURE, + native_unit_of_measurement=TEMP_CELSIUS, + state_class=SensorStateClass.MEASUREMENT, + name="Temperature", + icon="mdi:thermometer", + value_fn=lambda data: data.temperature, + ), +) +DEVICE_SENSOR_TYPES: tuple[SensiboDeviceSensorEntityDescription, ...] = ( + SensiboDeviceSensorEntityDescription( + key="pm25", + device_class=SensorDeviceClass.PM25, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + state_class=SensorStateClass.MEASUREMENT, + name="PM2.5", + icon="mdi:air-filter", + value_fn=lambda data: data.pm25, + ), + SensiboDeviceSensorEntityDescription( + key="pure_sensitivity", + name="Pure Sensitivity", + icon="mdi:air-filter", + value_fn=lambda data: data.pure_sensitivity, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up Sensibo sensor platform.""" + + coordinator: SensiboDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + + entities: list[SensiboMotionSensor | SensiboDeviceSensor] = [] + + entities.extend( + SensiboMotionSensor(coordinator, device_id, sensor_id, sensor_data, description) + for device_id, device_data in coordinator.data.parsed.items() + for sensor_id, sensor_data in device_data.motion_sensors.items() + for description in MOTION_SENSOR_TYPES + if device_data.motion_sensors + ) + entities.extend( + SensiboDeviceSensor(coordinator, device_id, description) + for device_id, device_data in coordinator.data.parsed.items() + for description in DEVICE_SENSOR_TYPES + if getattr(device_data, description.key) is not None + ) + async_add_entities(entities) + + +class SensiboMotionSensor(SensiboMotionBaseEntity, SensorEntity): + """Representation of a Sensibo Motion Sensor.""" + + entity_description: SensiboMotionSensorEntityDescription + + def __init__( + self, + coordinator: SensiboDataUpdateCoordinator, + device_id: str, + sensor_id: str, + sensor_data: MotionSensor, + entity_description: SensiboMotionSensorEntityDescription, + ) -> None: + """Initiate Sensibo Motion Sensor.""" + super().__init__( + coordinator, + device_id, + sensor_id, + sensor_data, + entity_description.name, + ) + self.entity_description = entity_description + self._attr_unique_id = f"{sensor_id}-{entity_description.key}" + self._attr_name = ( + f"{self.device_data.name} Motion Sensor {entity_description.name}" + ) + + @property + def native_value(self) -> StateType: + """Return value of sensor.""" + return self.entity_description.value_fn(self.sensor_data) + + +class SensiboDeviceSensor(SensiboDeviceBaseEntity, SensorEntity): + """Representation of a Sensibo Device Sensor.""" + + entity_description: SensiboDeviceSensorEntityDescription + + def __init__( + self, + coordinator: SensiboDataUpdateCoordinator, + device_id: str, + entity_description: SensiboDeviceSensorEntityDescription, + ) -> None: + """Initiate Sensibo Device Sensor.""" + super().__init__( + coordinator, + device_id, + ) + self.entity_description = entity_description + self._attr_unique_id = f"{device_id}-{entity_description.key}" + self._attr_name = f"{self.device_data.name} {entity_description.name}" + + @property + def native_value(self) -> StateType: + """Return value of sensor.""" + return self.entity_description.value_fn(self.device_data) diff --git a/homeassistant/components/sensibo/strings.json b/homeassistant/components/sensibo/strings.json index 22751964999..dc27644b3e1 100644 --- a/homeassistant/components/sensibo/strings.json +++ b/homeassistant/components/sensibo/strings.json @@ -1,18 +1,27 @@ { "config": { "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_account%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" }, - "error":{ - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "no_devices": "No devices discovered", + "no_username": "Could not get username", + "incorrect_api_key": "Invalid API key for selected account" }, "step": { "user": { "data": { - "api_key": "[%key:common::config_flow::data::api_key%]", - "name": "[%key:common::config_flow::data::name%]" + "api_key": "[%key:common::config_flow::data::api_key%]" + } + }, + "reauth_confirm": { + "data": { + "api_key": "[%key:common::config_flow::data::api_key%]" } } } } -} \ No newline at end of file +} diff --git a/homeassistant/components/sensibo/translations/bg.json b/homeassistant/components/sensibo/translations/bg.json index ef66d0b9368..e600bafdb4a 100644 --- a/homeassistant/components/sensibo/translations/bg.json +++ b/homeassistant/components/sensibo/translations/bg.json @@ -1,12 +1,19 @@ { "config": { "abort": { - "already_configured": "\u0410\u043a\u0430\u0443\u043d\u0442\u044a\u0442 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d" + "already_configured": "\u0410\u043a\u0430\u0443\u043d\u0442\u044a\u0442 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" }, "error": { - "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435" }, "step": { + "reauth_confirm": { + "data": { + "api_key": "API \u043a\u043b\u044e\u0447" + } + }, "user": { "data": { "api_key": "API \u043a\u043b\u044e\u0447", diff --git a/homeassistant/components/sensibo/translations/ca.json b/homeassistant/components/sensibo/translations/ca.json index adff28113ca..f062af9e519 100644 --- a/homeassistant/components/sensibo/translations/ca.json +++ b/homeassistant/components/sensibo/translations/ca.json @@ -1,12 +1,22 @@ { "config": { "abort": { - "already_configured": "El compte ja est\u00e0 configurat" + "already_configured": "El compte ja est\u00e0 configurat", + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" }, "error": { - "cannot_connect": "Ha fallat la connexi\u00f3" + "cannot_connect": "Ha fallat la connexi\u00f3", + "incorrect_api_key": "Clau API inv\u00e0lida per al compte seleccionat", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "no_devices": "No s'ha descobert cap dispositiu", + "no_username": "No s'ha pogut obtenir el nom d'usuari" }, "step": { + "reauth_confirm": { + "data": { + "api_key": "Clau API" + } + }, "user": { "data": { "api_key": "Clau API", diff --git a/homeassistant/components/sensibo/translations/de.json b/homeassistant/components/sensibo/translations/de.json index 5bb5dd3cf84..9e3b02c726c 100644 --- a/homeassistant/components/sensibo/translations/de.json +++ b/homeassistant/components/sensibo/translations/de.json @@ -1,12 +1,22 @@ { "config": { "abort": { - "already_configured": "Konto wurde bereits konfiguriert" + "already_configured": "Konto wurde bereits konfiguriert", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich" }, "error": { - "cannot_connect": "Verbindung fehlgeschlagen" + "cannot_connect": "Verbindung fehlgeschlagen", + "incorrect_api_key": "Ung\u00fcltiger API-Schl\u00fcssel f\u00fcr ausgew\u00e4hltes Konto", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "no_devices": "Keine Ger\u00e4te gefunden", + "no_username": "Benutzername konnte nicht ermittelt werden" }, "step": { + "reauth_confirm": { + "data": { + "api_key": "API-Schl\u00fcssel" + } + }, "user": { "data": { "api_key": "API-Schl\u00fcssel", diff --git a/homeassistant/components/sensibo/translations/el.json b/homeassistant/components/sensibo/translations/el.json index 455c89deba4..baa2545a7e0 100644 --- a/homeassistant/components/sensibo/translations/el.json +++ b/homeassistant/components/sensibo/translations/el.json @@ -1,12 +1,22 @@ { "config": { "abort": { - "already_configured": "\u039f \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc\u03c2 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af" + "already_configured": "\u039f \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc\u03c2 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", + "reauth_successful": "\u039f \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03ae\u03c4\u03b1\u03bd \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2" }, "error": { - "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "incorrect_api_key": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af API \u03b3\u03b9\u03b1 \u03c4\u03bf\u03bd \u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03bf \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc", + "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2", + "no_devices": "\u0394\u03b5\u03bd \u03b1\u03bd\u03b1\u03ba\u03b1\u03bb\u03cd\u03c6\u03b8\u03b7\u03ba\u03b1\u03bd \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2", + "no_username": "\u0394\u03b5\u03bd \u03ae\u03c4\u03b1\u03bd \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae \u03b7 \u03bb\u03ae\u03c8\u03b7 \u03bf\u03bd\u03cc\u03bc\u03b1\u03c4\u03bf\u03c2 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" }, "step": { + "reauth_confirm": { + "data": { + "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API" + } + }, "user": { "data": { "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API", diff --git a/homeassistant/components/sensibo/translations/en.json b/homeassistant/components/sensibo/translations/en.json index d3ee9fb1336..1b5d7cd9214 100644 --- a/homeassistant/components/sensibo/translations/en.json +++ b/homeassistant/components/sensibo/translations/en.json @@ -1,12 +1,22 @@ { "config": { "abort": { - "already_configured": "Account is already configured" + "already_configured": "Account is already configured", + "reauth_successful": "Re-authentication was successful" }, "error": { - "cannot_connect": "Failed to connect" + "cannot_connect": "Failed to connect", + "incorrect_api_key": "Invalid API key for selected account", + "invalid_auth": "Invalid authentication", + "no_devices": "No devices discovered", + "no_username": "Could not get username" }, "step": { + "reauth_confirm": { + "data": { + "api_key": "API Key" + } + }, "user": { "data": { "api_key": "API Key", diff --git a/homeassistant/components/sensibo/translations/et.json b/homeassistant/components/sensibo/translations/et.json index ccee8f1c41d..de5a158c1ad 100644 --- a/homeassistant/components/sensibo/translations/et.json +++ b/homeassistant/components/sensibo/translations/et.json @@ -1,12 +1,22 @@ { "config": { "abort": { - "already_configured": "Konto on juba h\u00e4\u00e4lestatud" + "already_configured": "Konto on juba h\u00e4\u00e4lestatud", + "reauth_successful": "Taastuvastamine \u00f5nnestus" }, "error": { - "cannot_connect": "\u00dchendamine nurjus" + "cannot_connect": "\u00dchendamine nurjus", + "incorrect_api_key": "Valitud konto API-v\u00f5ti on kehtetu", + "invalid_auth": "Tuvastamine nurjus", + "no_devices": "\u00dchtegi seadet ei leitud", + "no_username": "Kasutajanime ei \u00f5nnestunud hankida" }, "step": { + "reauth_confirm": { + "data": { + "api_key": "API v\u00f5ti" + } + }, "user": { "data": { "api_key": "API v\u00f5ti", diff --git a/homeassistant/components/sensibo/translations/fr.json b/homeassistant/components/sensibo/translations/fr.json index 43b91dff520..09e2e5e5045 100644 --- a/homeassistant/components/sensibo/translations/fr.json +++ b/homeassistant/components/sensibo/translations/fr.json @@ -1,12 +1,22 @@ { "config": { "abort": { - "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9" + "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9", + "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" }, "error": { - "cannot_connect": "\u00c9chec de connexion" + "cannot_connect": "\u00c9chec de connexion", + "incorrect_api_key": "Cl\u00e9 d'API non valide pour le compte s\u00e9lectionn\u00e9", + "invalid_auth": "Authentification non valide", + "no_devices": "Aucun appareil d\u00e9couvert", + "no_username": "Impossible d'obtenir le nom d'utilisateur" }, "step": { + "reauth_confirm": { + "data": { + "api_key": "Cl\u00e9 d'API" + } + }, "user": { "data": { "api_key": "Cl\u00e9 d'API", diff --git a/homeassistant/components/sensibo/translations/he.json b/homeassistant/components/sensibo/translations/he.json index 403b3c7e2ea..3486bd3c646 100644 --- a/homeassistant/components/sensibo/translations/he.json +++ b/homeassistant/components/sensibo/translations/he.json @@ -1,12 +1,19 @@ { "config": { "abort": { - "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7" }, "error": { - "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" }, "step": { + "reauth_confirm": { + "data": { + "api_key": "\u05de\u05e4\u05ea\u05d7 API" + } + }, "user": { "data": { "api_key": "\u05de\u05e4\u05ea\u05d7 API", diff --git a/homeassistant/components/sensibo/translations/hu.json b/homeassistant/components/sensibo/translations/hu.json index 09aca99c669..802b718cc82 100644 --- a/homeassistant/components/sensibo/translations/hu.json +++ b/homeassistant/components/sensibo/translations/hu.json @@ -1,12 +1,22 @@ { "config": { "abort": { - "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van" + "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt." }, "error": { - "cannot_connect": "Sikertelen csatlakoz\u00e1s" + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "incorrect_api_key": "\u00c9rv\u00e9nytelen API-kulcs a megadott fi\u00f3khoz", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "no_devices": "Nincs \u00e9szlelt eszk\u00f6z", + "no_username": "Nem siker\u00fclt beolvasni a felhaszn\u00e1l\u00f3nevet" }, "step": { + "reauth_confirm": { + "data": { + "api_key": "API kulcs" + } + }, "user": { "data": { "api_key": "API kulcs", diff --git a/homeassistant/components/sensibo/translations/id.json b/homeassistant/components/sensibo/translations/id.json index d4ea1c29254..479edabe69b 100644 --- a/homeassistant/components/sensibo/translations/id.json +++ b/homeassistant/components/sensibo/translations/id.json @@ -1,12 +1,22 @@ { "config": { "abort": { - "already_configured": "Akun sudah dikonfigurasi" + "already_configured": "Akun sudah dikonfigurasi", + "reauth_successful": "Autentikasi ulang berhasil" }, "error": { - "cannot_connect": "Gagal terhubung" + "cannot_connect": "Gagal terhubung", + "incorrect_api_key": "Kunci API tidak valid untuk akun yang dipilih", + "invalid_auth": "Autentikasi tidak valid", + "no_devices": "Tidak ada perangkat yang ditemukan", + "no_username": "Tidak bisa mendapatkan nama pengguna" }, "step": { + "reauth_confirm": { + "data": { + "api_key": "Kunci API" + } + }, "user": { "data": { "api_key": "Kunci API", diff --git a/homeassistant/components/sensibo/translations/it.json b/homeassistant/components/sensibo/translations/it.json index 48c5b477073..c10adf2cf38 100644 --- a/homeassistant/components/sensibo/translations/it.json +++ b/homeassistant/components/sensibo/translations/it.json @@ -1,12 +1,22 @@ { "config": { "abort": { - "already_configured": "L'account \u00e8 gi\u00e0 configurato" + "already_configured": "L'account \u00e8 gi\u00e0 configurato", + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" }, "error": { - "cannot_connect": "Impossibile connettersi" + "cannot_connect": "Impossibile connettersi", + "incorrect_api_key": "Chiave API non valida per l'account selezionato", + "invalid_auth": "Autenticazione non valida", + "no_devices": "Nessun dispositivo rilevato", + "no_username": "Impossibile ottenere il nome utente" }, "step": { + "reauth_confirm": { + "data": { + "api_key": "Chiave API" + } + }, "user": { "data": { "api_key": "Chiave API", diff --git a/homeassistant/components/sensibo/translations/ja.json b/homeassistant/components/sensibo/translations/ja.json index 92e6e077f51..859722fbe73 100644 --- a/homeassistant/components/sensibo/translations/ja.json +++ b/homeassistant/components/sensibo/translations/ja.json @@ -1,12 +1,22 @@ { "config": { "abort": { - "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", + "incorrect_api_key": "\u9078\u629e\u3057\u305f\u30a2\u30ab\u30a6\u30f3\u30c8\u306eAPI\u30ad\u30fc\u304c\u7121\u52b9\u3067\u3059", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "no_devices": "\u30c7\u30d0\u30a4\u30b9\u306f\u691c\u51fa\u3055\u308c\u307e\u305b\u3093\u3067\u3057\u305f", + "no_username": "\u30e6\u30fc\u30b6\u30fc\u540d\u3092\u53d6\u5f97\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f" }, "step": { + "reauth_confirm": { + "data": { + "api_key": "API\u30ad\u30fc" + } + }, "user": { "data": { "api_key": "API\u30ad\u30fc", diff --git a/homeassistant/components/sensibo/translations/nl.json b/homeassistant/components/sensibo/translations/nl.json index c1aa1c36340..cc6529c3540 100644 --- a/homeassistant/components/sensibo/translations/nl.json +++ b/homeassistant/components/sensibo/translations/nl.json @@ -1,12 +1,22 @@ { "config": { "abort": { - "already_configured": "Account is al geconfigureerd" + "already_configured": "Account is al geconfigureerd", + "reauth_successful": "Herauthenticatie was succesvol" }, "error": { - "cannot_connect": "Kan geen verbinding maken" + "cannot_connect": "Kan geen verbinding maken", + "incorrect_api_key": "Ongeldige API-sleutel voor geselecteerd account", + "invalid_auth": "Ongeldige authenticatie", + "no_devices": "Geen apparaten gevonden", + "no_username": "Kan gebruikersnaam niet ophalen" }, "step": { + "reauth_confirm": { + "data": { + "api_key": "API-sleutel" + } + }, "user": { "data": { "api_key": "API-sleutel", diff --git a/homeassistant/components/sensibo/translations/no.json b/homeassistant/components/sensibo/translations/no.json index 6c2c4613d0a..9d3098ccd9e 100644 --- a/homeassistant/components/sensibo/translations/no.json +++ b/homeassistant/components/sensibo/translations/no.json @@ -1,12 +1,22 @@ { "config": { "abort": { - "already_configured": "Kontoen er allerede konfigurert" + "already_configured": "Kontoen er allerede konfigurert", + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" }, "error": { - "cannot_connect": "Tilkobling mislyktes" + "cannot_connect": "Tilkobling mislyktes", + "incorrect_api_key": "Ugyldig API-n\u00f8kkel for valgt konto", + "invalid_auth": "Ugyldig godkjenning", + "no_devices": "Ingen enheter oppdaget", + "no_username": "Kunne ikke hente brukernavn" }, "step": { + "reauth_confirm": { + "data": { + "api_key": "API-n\u00f8kkel" + } + }, "user": { "data": { "api_key": "API-n\u00f8kkel", diff --git a/homeassistant/components/sensibo/translations/pl.json b/homeassistant/components/sensibo/translations/pl.json index a65564329d3..022aaf52039 100644 --- a/homeassistant/components/sensibo/translations/pl.json +++ b/homeassistant/components/sensibo/translations/pl.json @@ -1,12 +1,22 @@ { "config": { "abort": { - "already_configured": "Konto jest ju\u017c skonfigurowane" + "already_configured": "Konto jest ju\u017c skonfigurowane", + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" }, "error": { - "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "incorrect_api_key": "Nieprawid\u0142owy klucz API dla wybranego konta", + "invalid_auth": "Niepoprawne uwierzytelnienie", + "no_devices": "Nie wykryto urz\u0105dze\u0144", + "no_username": "Nie uda\u0142o si\u0119 uzyska\u0107 nazwy u\u017cytkownika" }, "step": { + "reauth_confirm": { + "data": { + "api_key": "Klucz API" + } + }, "user": { "data": { "api_key": "Klucz API", diff --git a/homeassistant/components/sensibo/translations/pt-BR.json b/homeassistant/components/sensibo/translations/pt-BR.json index fac1d04755f..ff0ac4883ba 100644 --- a/homeassistant/components/sensibo/translations/pt-BR.json +++ b/homeassistant/components/sensibo/translations/pt-BR.json @@ -1,12 +1,22 @@ { "config": { "abort": { - "already_configured": "A conta j\u00e1 foi configurada" + "already_configured": "A conta j\u00e1 foi configurada", + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" }, "error": { - "cannot_connect": "Falha ao conectar" + "cannot_connect": "Falha ao conectar", + "incorrect_api_key": "Chave de API inv\u00e1lida para a conta selecionada", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "no_devices": "Nenhum dispositivo descoberto", + "no_username": "N\u00e3o foi poss\u00edvel obter o nome de usu\u00e1rio" }, "step": { + "reauth_confirm": { + "data": { + "api_key": "Chave da API" + } + }, "user": { "data": { "api_key": "Chave da API", diff --git a/homeassistant/components/sensibo/translations/ru.json b/homeassistant/components/sensibo/translations/ru.json index 0d6a22bde74..96574822758 100644 --- a/homeassistant/components/sensibo/translations/ru.json +++ b/homeassistant/components/sensibo/translations/ru.json @@ -1,12 +1,22 @@ { "config": { "abort": { - "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant." + "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant.", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." }, "error": { - "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\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.", + "incorrect_api_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API \u0434\u043b\u044f \u0432\u044b\u0431\u0440\u0430\u043d\u043d\u043e\u0439 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", + "no_devices": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u044b.", + "no_username": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0438\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f." }, "step": { + "reauth_confirm": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API" + } + }, "user": { "data": { "api_key": "\u041a\u043b\u044e\u0447 API", diff --git a/homeassistant/components/sensibo/translations/tr.json b/homeassistant/components/sensibo/translations/tr.json index eb5e2eb4129..b08f683b200 100644 --- a/homeassistant/components/sensibo/translations/tr.json +++ b/homeassistant/components/sensibo/translations/tr.json @@ -1,12 +1,22 @@ { "config": { "abort": { - "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" }, "error": { - "cannot_connect": "Ba\u011flanma hatas\u0131" + "cannot_connect": "Ba\u011flanma hatas\u0131", + "incorrect_api_key": "Se\u00e7ilen hesap i\u00e7in ge\u00e7ersiz API anahtar\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "no_devices": "Hi\u00e7bir cihaz ke\u015ffedilmedi", + "no_username": "Kullan\u0131c\u0131 ad\u0131 al\u0131namad\u0131" }, "step": { + "reauth_confirm": { + "data": { + "api_key": "API Anahtar\u0131" + } + }, "user": { "data": { "api_key": "API Anahtar\u0131", diff --git a/homeassistant/components/sensibo/translations/zh-Hant.json b/homeassistant/components/sensibo/translations/zh-Hant.json index 4abe3544048..39cc7dc9662 100644 --- a/homeassistant/components/sensibo/translations/zh-Hant.json +++ b/homeassistant/components/sensibo/translations/zh-Hant.json @@ -1,12 +1,22 @@ { "config": { "abort": { - "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" }, "error": { - "cannot_connect": "\u9023\u7dda\u5931\u6557" + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "incorrect_api_key": "\u6240\u9078\u64c7\u5e33\u865f\u4e4b API \u91d1\u9470\u7121\u6548\u3002", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "no_devices": "\u672a\u767c\u73fe\u4efb\u4f55\u88dd\u7f6e", + "no_username": "\u7121\u6cd5\u53d6\u5f97\u4f7f\u7528\u8005\u540d\u7a31" }, "step": { + "reauth_confirm": { + "data": { + "api_key": "API \u91d1\u9470" + } + }, "user": { "data": { "api_key": "API \u91d1\u9470", diff --git a/homeassistant/components/sensibo/util.py b/homeassistant/components/sensibo/util.py new file mode 100644 index 00000000000..fda9d4a210e --- /dev/null +++ b/homeassistant/components/sensibo/util.py @@ -0,0 +1,49 @@ +"""Utils for Sensibo integration.""" +from __future__ import annotations + +import async_timeout +from pysensibo import SensiboClient +from pysensibo.exceptions import AuthenticationError + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import LOGGER, SENSIBO_ERRORS, TIMEOUT + + +async def async_validate_api(hass: HomeAssistant, api_key: str) -> str: + """Get data from API.""" + client = SensiboClient( + api_key, + session=async_get_clientsession(hass), + timeout=TIMEOUT, + ) + + try: + async with async_timeout.timeout(TIMEOUT): + device_query = await client.async_get_devices() + user_query = await client.async_get_me() + except AuthenticationError as err: + LOGGER.error("Could not authenticate on Sensibo servers %s", err) + raise AuthenticationError from err + except SENSIBO_ERRORS as err: + LOGGER.error("Failed to get information from Sensibo servers %s", err) + raise ConnectionError from err + + devices = device_query["result"] + user = user_query["result"].get("username") + if not devices: + LOGGER.error("Could not retrieve any devices from Sensibo servers") + raise NoDevicesError + if not user: + LOGGER.error("Could not retrieve username from Sensibo servers") + raise NoUsernameError + return user + + +class NoDevicesError(Exception): + """No devices from Sensibo api.""" + + +class NoUsernameError(Exception): + """No username from Sensibo api.""" diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index 3414f13268f..b9cb3d94796 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -1,12 +1,13 @@ """Component to interface with various sensors that can be monitored.""" from __future__ import annotations -from collections.abc import Mapping +from collections.abc import Callable, Mapping from contextlib import suppress from dataclasses import dataclass from datetime import date, datetime, timedelta, timezone import inspect import logging +from math import floor, log10 from typing import Any, Final, cast, final import voluptuous as vol @@ -14,6 +15,7 @@ import voluptuous as vol from homeassistant.backports.enum import StrEnum from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( # noqa: F401 + CONF_UNIT_OF_MEASUREMENT, DEVICE_CLASS_AQI, DEVICE_CLASS_BATTERY, DEVICE_CLASS_CO, @@ -44,8 +46,9 @@ from homeassistant.const import ( # noqa: F401 DEVICE_CLASS_VOLTAGE, TEMP_CELSIUS, TEMP_FAHRENHEIT, + TEMP_KELVIN, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, @@ -54,7 +57,11 @@ from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import ExtraStoredData, RestoreEntity from homeassistant.helpers.typing import ConfigType, StateType -from homeassistant.util import dt as dt_util +from homeassistant.util import ( + dt as dt_util, + pressure as pressure_util, + temperature as temperature_util, +) from .const import CONF_STATE_CLASS # noqa: F401 @@ -194,6 +201,25 @@ STATE_CLASS_TOTAL: Final = "total" STATE_CLASS_TOTAL_INCREASING: Final = "total_increasing" STATE_CLASSES: Final[list[str]] = [cls.value for cls in SensorStateClass] +UNIT_CONVERSIONS: dict[str, Callable[[float, str, str], float]] = { + SensorDeviceClass.PRESSURE: pressure_util.convert, + SensorDeviceClass.TEMPERATURE: temperature_util.convert, +} + +UNIT_RATIOS: dict[str, dict[str, float]] = { + SensorDeviceClass.PRESSURE: pressure_util.UNIT_CONVERSION, + SensorDeviceClass.TEMPERATURE: { + TEMP_CELSIUS: 1.0, + TEMP_FAHRENHEIT: 1.8, + TEMP_KELVIN: 1.0, + }, +} + +VALID_UNITS: dict[str, tuple[str, ...]] = { + SensorDeviceClass.PRESSURE: pressure_util.VALID_UNITS, + SensorDeviceClass.TEMPERATURE: temperature_util.VALID_UNITS, +} + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Track states and offer events for sensors.""" @@ -264,10 +290,18 @@ class SensorEntity(Entity): ) _last_reset_reported = False _temperature_conversion_reported = False + _sensor_option_unit_of_measurement: str | None = None # Temporary private attribute to track if deprecation has been logged. __datetime_as_string_deprecation_logged = False + async def async_internal_added_to_hass(self) -> None: + """Call when the sensor entity is added to hass.""" + await super().async_internal_added_to_hass() + if not self.registry_entry: + return + self.async_registry_entry_updated() + @property def device_class(self) -> SensorDeviceClass | str | None: """Return the class of this entity.""" @@ -350,6 +384,9 @@ class SensorEntity(Entity): @property def unit_of_measurement(self) -> str | None: """Return the unit of measurement of the entity, after unit conversion.""" + if self._sensor_option_unit_of_measurement: + return self._sensor_option_unit_of_measurement + # Support for _attr_unit_of_measurement will be removed in Home Assistant 2021.11 if ( hasattr(self, "_attr_unit_of_measurement") @@ -368,7 +405,8 @@ class SensorEntity(Entity): @property def state(self) -> Any: """Return the state of the sensor and perform unit conversions, if needed.""" - unit_of_measurement = self.native_unit_of_measurement + native_unit_of_measurement = self.native_unit_of_measurement + unit_of_measurement = self.unit_of_measurement value = self.native_value device_class = self.device_class @@ -407,16 +445,48 @@ class SensorEntity(Entity): f"but does not provide a date state but {type(value)}" ) from err - units = self.hass.config.units if ( value is not None - and unit_of_measurement in (TEMP_CELSIUS, TEMP_FAHRENHEIT) - and unit_of_measurement != units.temperature_unit + and native_unit_of_measurement != unit_of_measurement + and self.device_class in UNIT_CONVERSIONS ): - if ( - self.device_class != DEVICE_CLASS_TEMPERATURE - and not self._temperature_conversion_reported - ): + assert unit_of_measurement + assert native_unit_of_measurement + + value_s = str(value) + prec = len(value_s) - value_s.index(".") - 1 if "." in value_s else 0 + + # Scale the precision when converting to a larger unit + # For example 1.1 kWh should be rendered as 0.0011 kWh, not 0.0 kWh + ratio_log = max( + 0, + log10( + UNIT_RATIOS[self.device_class][native_unit_of_measurement] + / UNIT_RATIOS[self.device_class][unit_of_measurement] + ), + ) + prec = prec + floor(ratio_log) + + # Suppress ValueError (Could not convert sensor_value to float) + with suppress(ValueError): + value_f = float(value) # type: ignore[arg-type] + value_f_new = UNIT_CONVERSIONS[self.device_class]( + value_f, + native_unit_of_measurement, + unit_of_measurement, + ) + + # Round to the wanted precision + value = round(value_f_new) if prec == 0 else round(value_f_new, prec) + + elif ( + value is not None + and self.device_class != DEVICE_CLASS_TEMPERATURE + and native_unit_of_measurement != self.hass.config.units.temperature_unit + and native_unit_of_measurement in (TEMP_CELSIUS, TEMP_FAHRENHEIT) + ): + units = self.hass.config.units + if not self._temperature_conversion_reported: self._temperature_conversion_reported = True report_issue = self._suggest_report_issue() _LOGGER.warning( @@ -429,7 +499,7 @@ class SensorEntity(Entity): self.entity_id, type(self), self.device_class, - unit_of_measurement, + native_unit_of_measurement, units.temperature_unit, report_issue, ) @@ -437,7 +507,7 @@ class SensorEntity(Entity): prec = len(value_s) - value_s.index(".") - 1 if "." in value_s else 0 # Suppress ValueError (Could not convert sensor_value to float) with suppress(ValueError): - temp = units.temperature(float(value), unit_of_measurement) # type: ignore[arg-type] + temp = units.temperature(float(value), native_unit_of_measurement) # type: ignore[arg-type] value = round(temp) if prec == 0 else round(temp, prec) return value @@ -453,6 +523,22 @@ class SensorEntity(Entity): return super().__repr__() + @callback + def async_registry_entry_updated(self) -> None: + """Run when the entity registry entry has been updated.""" + assert self.registry_entry + if ( + (sensor_options := self.registry_entry.options.get(DOMAIN)) + and (custom_unit := sensor_options.get(CONF_UNIT_OF_MEASUREMENT)) + and (device_class := self.device_class) in UNIT_CONVERSIONS + and self.native_unit_of_measurement in VALID_UNITS[device_class] + and custom_unit in VALID_UNITS[device_class] + ): + self._sensor_option_unit_of_measurement = custom_unit + return + + self._sensor_option_unit_of_measurement = None + @dataclass class SensorExtraStoredData(ExtraStoredData): diff --git a/homeassistant/components/sensor/manifest.json b/homeassistant/components/sensor/manifest.json index 163bc895975..4726ac790a7 100644 --- a/homeassistant/components/sensor/manifest.json +++ b/homeassistant/components/sensor/manifest.json @@ -2,7 +2,7 @@ "domain": "sensor", "name": "Sensor", "documentation": "https://www.home-assistant.io/integrations/sensor", - "codeowners": [], + "codeowners": ["@home-assistant/core"], "quality_scale": "internal", "after_dependencies": ["recorder"] } diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index 635c5af6242..ae148d45e72 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -2,12 +2,12 @@ from __future__ import annotations from collections import defaultdict -from collections.abc import Callable, Iterable +from collections.abc import Callable, Iterable, MutableMapping import datetime import itertools import logging import math -from typing import Any +from typing import Any, cast from sqlalchemy.orm.session import Session @@ -19,6 +19,7 @@ from homeassistant.components.recorder import ( ) from homeassistant.components.recorder.const import DOMAIN as RECORDER_DOMAIN from homeassistant.components.recorder.models import ( + LazyState, StatisticData, StatisticMetaData, StatisticResult, @@ -416,9 +417,9 @@ def _compile_statistics( # noqa: C901 entities_full_history = [ i.entity_id for i in sensor_states if "sum" in wanted_statistics[i.entity_id] ] - history_list = {} + history_list: MutableMapping[str, Iterable[LazyState | State | dict[str, Any]]] = {} if entities_full_history: - history_list = history.get_significant_states_with_session( # type: ignore[no-untyped-call] + history_list = history.get_significant_states_with_session( hass, session, start - datetime.timedelta.resolution, @@ -432,7 +433,7 @@ def _compile_statistics( # noqa: C901 if "sum" not in wanted_statistics[i.entity_id] ] if entities_significant_history: - _history_list = history.get_significant_states_with_session( # type: ignore[no-untyped-call] + _history_list = history.get_significant_states_with_session( hass, session, start - datetime.timedelta.resolution, @@ -455,7 +456,14 @@ def _compile_statistics( # noqa: C901 device_class = _state.attributes.get(ATTR_DEVICE_CLASS) entity_history = history_list[entity_id] unit, fstates = _normalize_states( - hass, session, old_metadatas, entity_history, device_class, entity_id + hass, + session, + old_metadatas, + # entity_history does not contain minimal responses + # so we must cast here + cast(list[State], entity_history), + device_class, + entity_id, ) if not fstates: @@ -596,11 +604,15 @@ def _compile_statistics( # noqa: C901 return result -def list_statistic_ids(hass: HomeAssistant, statistic_type: str | None = None) -> dict: - """Return statistic_ids and meta data.""" +def list_statistic_ids( + hass: HomeAssistant, + statistic_ids: list[str] | tuple[str] | None = None, + statistic_type: str | None = None, +) -> dict: + """Return all or filtered statistic_ids and meta data.""" entities = _get_sensor_states(hass) - statistic_ids = {} + result = {} for state in entities: state_class = state.attributes[ATTR_STATE_CLASS] @@ -611,6 +623,9 @@ def list_statistic_ids(hass: HomeAssistant, statistic_type: str | None = None) - if statistic_type is not None and statistic_type not in provided_statistics: continue + if statistic_ids is not None and state.entity_id not in statistic_ids: + continue + if ( "sum" in provided_statistics and ATTR_LAST_RESET not in state.attributes @@ -619,7 +634,9 @@ def list_statistic_ids(hass: HomeAssistant, statistic_type: str | None = None) - continue if device_class not in UNIT_CONVERSIONS: - statistic_ids[state.entity_id] = { + result[state.entity_id] = { + "has_mean": "mean" in provided_statistics, + "has_sum": "sum" in provided_statistics, "source": RECORDER_DOMAIN, "unit_of_measurement": native_unit, } @@ -629,12 +646,14 @@ def list_statistic_ids(hass: HomeAssistant, statistic_type: str | None = None) - continue statistics_unit = DEVICE_CLASS_UNITS[device_class] - statistic_ids[state.entity_id] = { + result[state.entity_id] = { + "has_mean": "mean" in provided_statistics, + "has_sum": "sum" in provided_statistics, "source": RECORDER_DOMAIN, "unit_of_measurement": statistics_unit, } - return statistic_ids + return result def validate_statistics( diff --git a/homeassistant/components/sensor/translations/el.json b/homeassistant/components/sensor/translations/el.json index 25b4e7bd72b..dc15aec106a 100644 --- a/homeassistant/components/sensor/translations/el.json +++ b/homeassistant/components/sensor/translations/el.json @@ -5,6 +5,7 @@ "is_battery_level": "\u03a4\u03c1\u03ad\u03c7\u03bf\u03bd \u03b5\u03c0\u03af\u03c0\u03b5\u03b4\u03bf \u03bc\u03c0\u03b1\u03c4\u03b1\u03c1\u03af\u03b1\u03c2 {entity_name}", "is_carbon_dioxide": "\u03a4\u03c1\u03ad\u03c7\u03bf\u03bd \u03b5\u03c0\u03af\u03c0\u03b5\u03b4\u03bf \u03c3\u03c5\u03b3\u03ba\u03ad\u03bd\u03c4\u03c1\u03c9\u03c3\u03b7\u03c2 \u03b4\u03b9\u03bf\u03be\u03b5\u03b9\u03b4\u03af\u03bf\u03c5 \u03c4\u03bf\u03c5 \u03ac\u03bd\u03b8\u03c1\u03b1\u03ba\u03b1 \u03c4\u03bf\u03c5 {entity_name}", "is_carbon_monoxide": "\u03a4\u03c1\u03ad\u03c7\u03bf\u03bd \u03b5\u03c0\u03af\u03c0\u03b5\u03b4\u03bf \u03c3\u03c5\u03b3\u03ba\u03ad\u03bd\u03c4\u03c1\u03c9\u03c3\u03b7\u03c2 \u03bc\u03bf\u03bd\u03bf\u03be\u03b5\u03b9\u03b4\u03af\u03bf\u03c5 \u03c4\u03bf\u03c5 \u03ac\u03bd\u03b8\u03c1\u03b1\u03ba\u03b1 \u03c4\u03bf\u03c5 {entity_name}", + "is_current": "\u03a4\u03c1\u03ad\u03c7\u03bf\u03bd \u03c1\u03b5\u03cd\u03bc\u03b1 \u03b3\u03b9\u03b1 {entity_name}", "is_energy": "\u03a4\u03c1\u03ad\u03c7\u03bf\u03c5\u03c3\u03b1 \u03b5\u03bd\u03ad\u03c1\u03b3\u03b5\u03b9\u03b1 {entity_name}", "is_frequency": "\u03a4\u03c1\u03ad\u03c7\u03bf\u03c5\u03c3\u03b1 \u03c3\u03c5\u03c7\u03bd\u03cc\u03c4\u03b7\u03c4\u03b1 {entity_name}", "is_gas": "\u03a4\u03c1\u03ad\u03c7\u03bf\u03bd \u03b1\u03ad\u03c1\u03b9\u03bf {entity_name}", @@ -60,8 +61,8 @@ }, "state": { "_": { - "off": "\u039a\u03bb\u03b5\u03b9\u03c3\u03c4\u03cc", - "on": "\u0391\u03bd\u03bf\u03b9\u03c7\u03c4\u03cc" + "off": "\u0391\u03bd\u03b5\u03bd\u03b5\u03c1\u03b3\u03cc\u03c2", + "on": "\u0395\u03bd\u03b5\u03c1\u03b3\u03cc\u03c2" } }, "title": "\u0391\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1\u03c2" diff --git a/homeassistant/components/sensor/translations/fr.json b/homeassistant/components/sensor/translations/fr.json index 3ef16bd17ec..fd9cb2aeecb 100644 --- a/homeassistant/components/sensor/translations/fr.json +++ b/homeassistant/components/sensor/translations/fr.json @@ -2,15 +2,15 @@ "device_automation": { "condition_type": { "is_apparent_power": "Puissance apparente actuelle de {entity_name}", - "is_battery_level": "Niveau de la batterie de {entity_name}", + "is_battery_level": "Niveau de la batterie actuel de {entity_name}", "is_carbon_dioxide": "Niveau actuel de concentration de dioxyde de carbone {entity_name}", "is_carbon_monoxide": "Niveau actuel de concentration de monoxyde de carbone {entity_name}", "is_current": "Courant actuel pour {entity_name}", "is_energy": "\u00c9nergie actuelle pour {entity_name}", "is_frequency": "Fr\u00e9quence actuelle de {entity_name}", "is_gas": "Gaz actuel de {entity_name}", - "is_humidity": "Humidit\u00e9 de {entity_name}", - "is_illuminance": "\u00c9clairement de {entity_name}", + "is_humidity": "Humidit\u00e9 actuelle de {entity_name}", + "is_illuminance": "\u00c9clairement lumineux actuel de {entity_name}", "is_nitrogen_dioxide": "Niveau actuel de concentration en dioxyde d'azote de {entity_name}", "is_nitrogen_monoxide": "Niveau actuel de concentration en monoxyde d'azote de {entity_name}", "is_nitrous_oxide": "Niveau actuel de concentration d'oxyde nitreux de {entity_name}", @@ -18,25 +18,25 @@ "is_pm1": "Niveau de concentration actuel de {entity_name}", "is_pm10": "Niveau de concentration actuel de {entity_name}", "is_pm25": "Niveau de concentration actuel de {entity_name}", - "is_power": "Puissance de {entity_name}", + "is_power": "Puissance actuelle de {entity_name}", "is_power_factor": "Facteur de puissance actuel pour {entity_name}", - "is_pressure": "Pression de {entity_name}", + "is_pressure": "Pression actuelle de {entity_name}", "is_reactive_power": "Puissance r\u00e9active actuelle de {entity_name}", - "is_signal_strength": "Force du signal de {entity_name}", + "is_signal_strength": "Force du signal actuelle de {entity_name}", "is_sulphur_dioxide": "Niveau de concentration actuel de {entity_name}", - "is_temperature": "Temp\u00e9rature de {entity_name}", - "is_value": "La valeur actuelle de {entity_name}", + "is_temperature": "Temp\u00e9rature actuelle de {entity_name}", + "is_value": "Valeur actuelle de {entity_name}", "is_volatile_organic_compounds": "Niveau actuel de concentration en compos\u00e9s organiques volatils de {entity_name}", "is_voltage": "Tension actuelle pour {entity_name}" }, "trigger_type": { - "apparent_power": "{entity_name} changement de puissance apparente", + "apparent_power": "Variation de la puissance apparente de {entity_name}", "battery_level": "{entity_name} modification du niveau de batterie", "carbon_dioxide": "{entity_name} changements de concentration de dioxyde de carbone", "carbon_monoxide": "{entity_name} changements de concentration de monoxyde de carbone", "current": "{entity_name} changement de courant", "energy": "{entity_name} changement d'\u00e9nergie", - "frequency": "{entity_name} changements de fr\u00e9quence", + "frequency": "Variation de la fr\u00e9quence de {entity_name}", "gas": "{entity_name} changements de gaz", "humidity": "{entity_name} modification de l'humidit\u00e9", "illuminance": "{entity_name} modification de l'\u00e9clairement", @@ -50,7 +50,7 @@ "power": "{entity_name} modification de la puissance", "power_factor": "{entity_name} changement de facteur de puissance", "pressure": "{entity_name} modification de la pression", - "reactive_power": "{entity_name} changements de puissance r\u00e9active", + "reactive_power": "Variation de la puissance r\u00e9active de {entity_name}", "signal_strength": "{entity_name} modification de la force du signal", "sulphur_dioxide": "{entity_name} changements de concentration de dioxyde de soufre", "temperature": "{entity_name} modification de temp\u00e9rature", @@ -61,8 +61,8 @@ }, "state": { "_": { - "off": "Inactif", - "on": "Actif" + "off": "D\u00e9sactiv\u00e9", + "on": "Activ\u00e9" } }, "title": "Capteur" diff --git a/homeassistant/components/sensor/translations/it.json b/homeassistant/components/sensor/translations/it.json index 1e13fb79fd2..321e3e3108b 100644 --- a/homeassistant/components/sensor/translations/it.json +++ b/homeassistant/components/sensor/translations/it.json @@ -30,7 +30,7 @@ "is_voltage": "Tensione attuale di {entity_name}" }, "trigger_type": { - "apparent_power": "variazioni di potenza apparente di {entity_name}", + "apparent_power": "Variazioni di potenza apparente di {entity_name}", "battery_level": "variazioni del livello di batteria di {entity_name} ", "carbon_dioxide": "Variazioni della concentrazione di anidride carbonica di {entity_name}", "carbon_monoxide": "Variazioni nella concentrazione di monossido di carbonio di {entity_name}", @@ -50,7 +50,7 @@ "power": "Variazioni di alimentazione di {entity_name}", "power_factor": "variazioni del fattore di potenza di {entity_name}", "pressure": "Variazioni della pressione di {entity_name}", - "reactive_power": "variazioni di potenza reattiva di {entity_name}", + "reactive_power": "Variazioni di potenza reattiva di {entity_name}", "signal_strength": "Variazioni della potenza del segnale di {entity_name}", "sulphur_dioxide": "Variazioni della concentrazione di anidride solforosa di {entity_name}", "temperature": "Variazioni di temperatura di {entity_name}", diff --git a/homeassistant/components/sensor/translations/nl.json b/homeassistant/components/sensor/translations/nl.json index fea321fe221..69e18061858 100644 --- a/homeassistant/components/sensor/translations/nl.json +++ b/homeassistant/components/sensor/translations/nl.json @@ -1,6 +1,7 @@ { "device_automation": { "condition_type": { + "is_apparent_power": "Huidig {entity_name} schijnbaar vermogen", "is_battery_level": "Huidige batterijniveau {entity_name}", "is_carbon_dioxide": "Huidig niveau {entity_name} kooldioxideconcentratie", "is_carbon_monoxide": "Huidig niveau {entity_name} koolmonoxideconcentratie", @@ -20,6 +21,7 @@ "is_power": "Huidige {entity_name}\nvermogen", "is_power_factor": "Huidige {entity_name} vermogensfactor", "is_pressure": "Huidige {entity_name} druk", + "is_reactive_power": "Huidig {entity_name} blindvermogen", "is_signal_strength": "Huidige {entity_name} signaalsterkte", "is_sulphur_dioxide": "Huidige {entity_name} zwaveldioxideconcentratie", "is_temperature": "Huidige {entity_name} temperatuur", @@ -28,6 +30,7 @@ "is_voltage": "Huidige {entity_name} spanning" }, "trigger_type": { + "apparent_power": "{entity_name} schijnbare vermogensveranderingen", "battery_level": "{entity_name} batterijniveau gewijzigd", "carbon_dioxide": "{entity_name} kooldioxideconcentratie gewijzigd", "carbon_monoxide": "{entity_name} koolmonoxideconcentratie gewijzigd", @@ -47,6 +50,7 @@ "power": "{entity_name} vermogen gewijzigd", "power_factor": "{entity_name} power factor verandert", "pressure": "{entity_name} druk gewijzigd", + "reactive_power": "{entity_name} blindvermogen veranderingen", "signal_strength": "{entity_name} signaalsterkte gewijzigd", "sulphur_dioxide": "{entity_name} zwaveldioxideconcentratieveranderingen", "temperature": "{entity_name} temperatuur gewijzigd", diff --git a/homeassistant/components/sentry/manifest.json b/homeassistant/components/sentry/manifest.json index 52f7cba7a19..a8af46270ae 100644 --- a/homeassistant/components/sentry/manifest.json +++ b/homeassistant/components/sentry/manifest.json @@ -3,7 +3,7 @@ "name": "Sentry", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sentry", - "requirements": ["sentry-sdk==1.5.5"], + "requirements": ["sentry-sdk==1.5.8"], "codeowners": ["@dcramer", "@frenck"], "iot_class": "cloud_polling" } diff --git a/homeassistant/components/sentry/strings.json b/homeassistant/components/sentry/strings.json index 71196d52f8d..efcdb631f3c 100644 --- a/homeassistant/components/sentry/strings.json +++ b/homeassistant/components/sentry/strings.json @@ -2,10 +2,8 @@ "config": { "step": { "user": { - "title": "Sentry", - "description": "Enter your Sentry DSN", "data": { - "dsn": "DSN" + "dsn": "Sentry DSN" } } }, diff --git a/homeassistant/components/sentry/translations/fr.json b/homeassistant/components/sentry/translations/fr.json index 913da4b373d..acad5566ec3 100644 --- a/homeassistant/components/sentry/translations/fr.json +++ b/homeassistant/components/sentry/translations/fr.json @@ -4,7 +4,7 @@ "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." }, "error": { - "bad_dsn": "DSN invalide", + "bad_dsn": "DSN non valide", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/sentry/translations/nl.json b/homeassistant/components/sentry/translations/nl.json index 53f54ac1968..50af985b47c 100644 --- a/homeassistant/components/sentry/translations/nl.json +++ b/homeassistant/components/sentry/translations/nl.json @@ -26,9 +26,9 @@ "event_handled": "Stuur afgehandelde gebeurtenissen", "event_third_party_packages": "Gebeurtenissen verzenden vanuit pakketten van derden", "logging_event_level": "Het logniveau waarvoor Sentry een gebeurtenis registreert", - "logging_level": "Het logniveau Sentry zal logs opnemen als broodkruimels voor", + "logging_level": "Het logniveau hoe Sentry logs zal opnemen als \"breadcrums\" is", "tracing": "Schakel prestatietracering in", - "tracing_sample_rate": "Tracering van de steekproefsnelheid; tussen 0,0 en 1,0 (1,0 = 100%)" + "tracing_sample_rate": "Steekproefsnelheid voor prestatietracering; tussen 0,0 en 1,0 (1,0 = 100%)" } } } diff --git a/homeassistant/components/sharkiq/__init__.py b/homeassistant/components/sharkiq/__init__.py index 172ad6722ea..aad05652b0b 100644 --- a/homeassistant/components/sharkiq/__init__.py +++ b/homeassistant/components/sharkiq/__init__.py @@ -3,7 +3,7 @@ import asyncio from contextlib import suppress import async_timeout -from sharkiqpy import ( +from sharkiq import ( AylaApi, SharkIqAuthError, SharkIqAuthExpiringError, diff --git a/homeassistant/components/sharkiq/config_flow.py b/homeassistant/components/sharkiq/config_flow.py index a2d143e3e28..4875e2b25e1 100644 --- a/homeassistant/components/sharkiq/config_flow.py +++ b/homeassistant/components/sharkiq/config_flow.py @@ -5,7 +5,7 @@ import asyncio import aiohttp import async_timeout -from sharkiqpy import SharkIqAuthError, get_ayla_api +from sharkiq import SharkIqAuthError, get_ayla_api import voluptuous as vol from homeassistant import config_entries, core, exceptions diff --git a/homeassistant/components/sharkiq/manifest.json b/homeassistant/components/sharkiq/manifest.json index 0875609db1e..d1896af3f6c 100644 --- a/homeassistant/components/sharkiq/manifest.json +++ b/homeassistant/components/sharkiq/manifest.json @@ -3,8 +3,8 @@ "name": "Shark IQ", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sharkiq", - "requirements": ["sharkiqpy==0.1.8"], - "codeowners": ["@ajmarks"], + "requirements": ["sharkiq==0.0.1"], + "codeowners": ["@JeffResc", "@funkybunch", "@AritroSaha10"], "iot_class": "cloud_polling", - "loggers": ["sharkiqpy"] + "loggers": ["sharkiq"] } diff --git a/homeassistant/components/sharkiq/translations/fr.json b/homeassistant/components/sharkiq/translations/fr.json index 552715f0d1c..dfa145af8ab 100644 --- a/homeassistant/components/sharkiq/translations/fr.json +++ b/homeassistant/components/sharkiq/translations/fr.json @@ -8,7 +8,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/sharkiq/update_coordinator.py b/homeassistant/components/sharkiq/update_coordinator.py index 08e383a7437..41853ad4e41 100644 --- a/homeassistant/components/sharkiq/update_coordinator.py +++ b/homeassistant/components/sharkiq/update_coordinator.py @@ -4,7 +4,7 @@ from __future__ import annotations import asyncio from async_timeout import timeout -from sharkiqpy import ( +from sharkiq import ( AylaApi, SharkIqAuthError, SharkIqAuthExpiringError, diff --git a/homeassistant/components/sharkiq/vacuum.py b/homeassistant/components/sharkiq/vacuum.py index d9d22eb7a4d..cb7ec6da2d9 100644 --- a/homeassistant/components/sharkiq/vacuum.py +++ b/homeassistant/components/sharkiq/vacuum.py @@ -3,7 +3,7 @@ from __future__ import annotations from collections.abc import Iterable -from sharkiqpy import OperatingModes, PowerModes, Properties, SharkIqVacuum +from sharkiq import OperatingModes, PowerModes, Properties, SharkIqVacuum from homeassistant.components.vacuum import ( STATE_CLEANING, @@ -84,11 +84,9 @@ async def async_setup_entry( async_add_entities([SharkVacuumEntity(d, coordinator) for d in devices]) -class SharkVacuumEntity(CoordinatorEntity, StateVacuumEntity): +class SharkVacuumEntity(CoordinatorEntity[SharkIqUpdateCoordinator], StateVacuumEntity): """Shark IQ vacuum entity.""" - coordinator: SharkIqUpdateCoordinator - def __init__( self, sharkiq: SharkIqVacuum, coordinator: SharkIqUpdateCoordinator ) -> None: diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index b29079affcf..d29584c4e83 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -3,7 +3,6 @@ from __future__ import annotations import asyncio from datetime import timedelta -import logging from typing import Any, Final, cast import aioshelly @@ -47,6 +46,7 @@ from .const import ( ENTRY_RELOAD_COOLDOWN, EVENT_SHELLY_CLICK, INPUTS_EVENTS_DICT, + LOGGER, MODELS_SUPPORTING_LIGHT_EFFECTS, POLLING_TIMEOUT_SEC, REST, @@ -86,11 +86,12 @@ BLOCK_SLEEPING_PLATFORMS: Final = [ RPC_PLATFORMS: Final = [ Platform.BINARY_SENSOR, Platform.BUTTON, + Platform.COVER, Platform.LIGHT, Platform.SENSOR, Platform.SWITCH, ] -_LOGGER: Final = logging.getLogger(__name__) + COAP_SCHEMA: Final = vol.Schema( { @@ -118,7 +119,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # error. The config entry data for this custom component doesn't contain host # value, so if host isn't present, config entry will not be configured. if not entry.data.get(CONF_HOST): - _LOGGER.warning( + LOGGER.warning( "The config entry %s probably comes from a custom integration, please remove it if you want to use core Shelly integration", entry.title, ) @@ -172,7 +173,7 @@ async def async_setup_block_entry(hass: HomeAssistant, entry: ConfigEntry) -> bo @callback def _async_device_online(_: Any) -> None: - _LOGGER.debug("Device %s is online, resuming setup", entry.title) + LOGGER.debug("Device %s is online, resuming setup", entry.title) hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id][DEVICE] = None if sleep_period is None: @@ -185,7 +186,7 @@ async def async_setup_block_entry(hass: HomeAssistant, entry: ConfigEntry) -> bo if sleep_period == 0: # Not a sleeping device, finish setup - _LOGGER.debug("Setting up online block device %s", entry.title) + LOGGER.debug("Setting up online block device %s", entry.title) try: async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC): await device.initialize() @@ -200,13 +201,13 @@ async def async_setup_block_entry(hass: HomeAssistant, entry: ConfigEntry) -> bo elif sleep_period is None or device_entry is None: # Need to get sleep info or first time sleeping device setup, wait for device hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id][DEVICE] = device - _LOGGER.debug( + LOGGER.debug( "Setup for device %s will resume when device is online", entry.title ) device.subscribe_updates(_async_device_online) else: # Restore sensors for sleeping device - _LOGGER.debug("Setting up offline block device %s", entry.title) + LOGGER.debug("Setting up offline block device %s", entry.title) await async_block_device_setup(hass, entry, device) return True @@ -240,7 +241,7 @@ async def async_setup_rpc_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool entry.data.get(CONF_PASSWORD), ) - _LOGGER.debug("Setting up online RPC device %s", entry.title) + LOGGER.debug("Setting up online RPC device %s", entry.title) try: async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC): device = await RpcDevice.create( @@ -286,7 +287,7 @@ class BlockDeviceWrapper(update_coordinator.DataUpdateCoordinator): ) super().__init__( hass, - _LOGGER, + LOGGER, name=device_name, update_interval=timedelta(seconds=update_interval), ) @@ -296,7 +297,7 @@ class BlockDeviceWrapper(update_coordinator.DataUpdateCoordinator): self._debounced_reload = Debouncer( hass, - _LOGGER, + LOGGER, cooldown=ENTRY_RELOAD_COOLDOWN, immediate=False, function=self._async_reload_entry, @@ -317,7 +318,7 @@ class BlockDeviceWrapper(update_coordinator.DataUpdateCoordinator): async def _async_reload_entry(self) -> None: """Reload entry.""" - _LOGGER.debug("Reloading entry %s", self.name) + LOGGER.debug("Reloading entry %s", self.name) await self.hass.config_entries.async_reload(self.entry.entry_id) @callback @@ -388,14 +389,14 @@ class BlockDeviceWrapper(update_coordinator.DataUpdateCoordinator): }, ) else: - _LOGGER.warning( + LOGGER.warning( "Shelly input event %s for device %s is not supported, please open issue", event_type, self.name, ) if self._last_cfg_changed is not None and cfg_changed > self._last_cfg_changed: - _LOGGER.info( + LOGGER.info( "Config for %s changed, reloading entry in %s seconds", self.name, ENTRY_RELOAD_COOLDOWN, @@ -411,7 +412,7 @@ class BlockDeviceWrapper(update_coordinator.DataUpdateCoordinator): f"Sleeping device did not update within {sleep_period} seconds interval" ) - _LOGGER.debug("Polling Shelly Block Device - %s", self.name) + LOGGER.debug("Polling Shelly Block Device - %s", self.name) try: async with async_timeout.timeout(POLLING_TIMEOUT_SEC): await self.device.update() @@ -453,26 +454,26 @@ class BlockDeviceWrapper(update_coordinator.DataUpdateCoordinator): async def async_trigger_ota_update(self, beta: bool = False) -> None: """Trigger or schedule an ota update.""" update_data = self.device.status["update"] - _LOGGER.debug("OTA update service - update_data: %s", update_data) + LOGGER.debug("OTA update service - update_data: %s", update_data) if not update_data["has_update"] and not beta: - _LOGGER.warning("No OTA update available for device %s", self.name) + LOGGER.warning("No OTA update available for device %s", self.name) return if beta and not update_data.get("beta_version"): - _LOGGER.warning( + LOGGER.warning( "No OTA update on beta channel available for device %s", self.name ) return if update_data["status"] == "updating": - _LOGGER.warning("OTA update already in progress for %s", self.name) + LOGGER.warning("OTA update already in progress for %s", self.name) return new_version = update_data["new_version"] if beta: new_version = update_data["beta_version"] - _LOGGER.info( + LOGGER.info( "Start OTA update of device %s from '%s' to '%s'", self.name, self.device.firmware_version, @@ -482,8 +483,8 @@ class BlockDeviceWrapper(update_coordinator.DataUpdateCoordinator): async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC): result = await self.device.trigger_ota_update(beta=beta) except (asyncio.TimeoutError, OSError) as err: - _LOGGER.exception("Error while perform ota update: %s", err) - _LOGGER.debug("Result of OTA update call: %s", result) + LOGGER.exception("Error while perform ota update: %s", err) + LOGGER.debug("Result of OTA update call: %s", result) def shutdown(self) -> None: """Shutdown the wrapper.""" @@ -492,7 +493,7 @@ class BlockDeviceWrapper(update_coordinator.DataUpdateCoordinator): @callback def _handle_ha_stop(self, _event: Event) -> None: """Handle Home Assistant stopping.""" - _LOGGER.debug("Stopping BlockDeviceWrapper for %s", self.name) + LOGGER.debug("Stopping BlockDeviceWrapper for %s", self.name) self.shutdown() @@ -515,7 +516,7 @@ class ShellyDeviceRestWrapper(update_coordinator.DataUpdateCoordinator): super().__init__( hass, - _LOGGER, + LOGGER, name=get_block_device_name(device), update_interval=timedelta(seconds=update_interval), ) @@ -526,7 +527,7 @@ class ShellyDeviceRestWrapper(update_coordinator.DataUpdateCoordinator): """Fetch data.""" try: async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC): - _LOGGER.debug("REST update for %s", self.name) + LOGGER.debug("REST update for %s", self.name) await self.device.update_status() if self.device.status["uptime"] > 2 * REST_SENSORS_UPDATE_INTERVAL: @@ -627,7 +628,7 @@ class RpcDeviceWrapper(update_coordinator.DataUpdateCoordinator): device_name = get_rpc_device_name(device) if device.initialized else entry.title super().__init__( hass, - _LOGGER, + LOGGER, name=device_name, update_interval=timedelta(seconds=RPC_RECONNECT_INTERVAL), ) @@ -636,7 +637,7 @@ class RpcDeviceWrapper(update_coordinator.DataUpdateCoordinator): self._debounced_reload = Debouncer( hass, - _LOGGER, + LOGGER, cooldown=ENTRY_RELOAD_COOLDOWN, immediate=False, function=self._async_reload_entry, @@ -654,7 +655,7 @@ class RpcDeviceWrapper(update_coordinator.DataUpdateCoordinator): async def _async_reload_entry(self) -> None: """Reload entry.""" - _LOGGER.debug("Reloading entry %s", self.name) + LOGGER.debug("Reloading entry %s", self.name) await self.hass.config_entries.async_reload(self.entry.entry_id) @callback @@ -675,7 +676,7 @@ class RpcDeviceWrapper(update_coordinator.DataUpdateCoordinator): continue if event_type == "config_changed": - _LOGGER.info( + LOGGER.info( "Config for %s changed, reloading entry in %s seconds", self.name, ENTRY_RELOAD_COOLDOWN, @@ -699,7 +700,7 @@ class RpcDeviceWrapper(update_coordinator.DataUpdateCoordinator): return try: - _LOGGER.debug("Reconnecting to Shelly RPC Device - %s", self.name) + LOGGER.debug("Reconnecting to Shelly RPC Device - %s", self.name) async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC): await self.device.initialize() device_update_info(self.hass, self.device, self.entry) @@ -741,14 +742,14 @@ class RpcDeviceWrapper(update_coordinator.DataUpdateCoordinator): """Trigger an ota update.""" update_data = self.device.status["sys"]["available_updates"] - _LOGGER.debug("OTA update service - update_data: %s", update_data) + LOGGER.debug("OTA update service - update_data: %s", update_data) if not bool(update_data) or (not update_data.get("stable") and not beta): - _LOGGER.warning("No OTA update available for device %s", self.name) + LOGGER.warning("No OTA update available for device %s", self.name) return if beta and not update_data.get(ATTR_BETA): - _LOGGER.warning( + LOGGER.warning( "No OTA update on beta channel available for device %s", self.name ) return @@ -758,7 +759,7 @@ class RpcDeviceWrapper(update_coordinator.DataUpdateCoordinator): new_version = update_data.get(ATTR_BETA, {"version": ""})["version"] assert self.device.shelly - _LOGGER.info( + LOGGER.info( "Start OTA update of device %s from '%s' to '%s'", self.name, self.device.firmware_version, @@ -769,9 +770,9 @@ class RpcDeviceWrapper(update_coordinator.DataUpdateCoordinator): async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC): result = await self.device.trigger_ota_update(beta=beta) except (asyncio.TimeoutError, OSError) as err: - _LOGGER.exception("Error while perform ota update: %s", err) + LOGGER.exception("Error while perform ota update: %s", err) - _LOGGER.debug("Result of OTA update call: %s", result) + LOGGER.debug("Result of OTA update call: %s", result) async def shutdown(self) -> None: """Shutdown the wrapper.""" @@ -779,7 +780,7 @@ class RpcDeviceWrapper(update_coordinator.DataUpdateCoordinator): async def _handle_ha_stop(self, _event: Event) -> None: """Handle Home Assistant stopping.""" - _LOGGER.debug("Stopping RpcDeviceWrapper for %s", self.name) + LOGGER.debug("Stopping RpcDeviceWrapper for %s", self.name) await self.shutdown() @@ -795,7 +796,7 @@ class RpcPollingWrapper(update_coordinator.DataUpdateCoordinator): device_name = get_rpc_device_name(device) if device.initialized else entry.title super().__init__( hass, - _LOGGER, + LOGGER, name=device_name, update_interval=timedelta(seconds=RPC_SENSORS_POLLING_INTERVAL), ) @@ -808,7 +809,7 @@ class RpcPollingWrapper(update_coordinator.DataUpdateCoordinator): raise update_coordinator.UpdateFailed("Device disconnected") try: - _LOGGER.debug("Polling Shelly RPC Device - %s", self.name) + LOGGER.debug("Polling Shelly RPC Device - %s", self.name) async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC): await self.device.update_status() except OSError as err: diff --git a/homeassistant/components/shelly/climate.py b/homeassistant/components/shelly/climate.py index 1e7ba2dd183..4e6767e1d61 100644 --- a/homeassistant/components/shelly/climate.py +++ b/homeassistant/components/shelly/climate.py @@ -3,8 +3,7 @@ from __future__ import annotations import asyncio from collections.abc import Mapping -import logging -from typing import Any, Final, cast +from typing import Any, cast from aioshelly.block_device import Block import async_timeout @@ -34,12 +33,11 @@ from .const import ( BLOCK, DATA_CONFIG_ENTRY, DOMAIN, + LOGGER, SHTRV_01_TEMPERATURE_SETTINGS, ) from .utils import get_device_entry_gen -_LOGGER: Final = logging.getLogger(__name__) - async def async_setup_entry( hass: HomeAssistant, @@ -81,7 +79,7 @@ async def async_setup_climate_entities( sensor_block = block if sensor_block and device_block: - _LOGGER.debug("Setup online climate device %s", wrapper.name) + LOGGER.debug("Setup online climate device %s", wrapper.name) async_add_entities([BlockSleepingClimate(wrapper, sensor_block, device_block)]) @@ -103,8 +101,8 @@ async def async_restore_climate_entities( if entry.domain != CLIMATE_DOMAIN: continue - _LOGGER.debug("Setup sleeping climate device %s", wrapper.name) - _LOGGER.debug("Found entry %s [%s]", entry.original_name, entry.domain) + LOGGER.debug("Setup sleeping climate device %s", wrapper.name) + LOGGER.debug("Found entry %s [%s]", entry.original_name, entry.domain) async_add_entities([BlockSleepingClimate(wrapper, None, None, entry)]) break @@ -242,14 +240,14 @@ class BlockSleepingClimate( async def set_state_full_path(self, **kwargs: Any) -> Any: """Set block state (HTTP request).""" - _LOGGER.debug("Setting state for entity %s, state: %s", self.name, kwargs) + LOGGER.debug("Setting state for entity %s, state: %s", self.name, kwargs) try: async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC): return await self.wrapper.device.http_request( "get", f"thermostat/{self._channel}", kwargs ) except (asyncio.TimeoutError, OSError) as err: - _LOGGER.error( + LOGGER.error( "Setting state for entity %s failed, state: %s, error: %s", self.name, kwargs, @@ -287,7 +285,7 @@ class BlockSleepingClimate( async def async_added_to_hass(self) -> None: """Handle entity which will be added.""" - _LOGGER.info("Restoring entity %s", self.name) + LOGGER.info("Restoring entity %s", self.name) last_state = await self.async_get_last_state() @@ -316,7 +314,7 @@ class BlockSleepingClimate( self.block = block if self.device_block and self.block: - _LOGGER.debug("Entity %s attached to blocks", self.name) + LOGGER.debug("Entity %s attached to blocks", self.name) assert self.block.channel diff --git a/homeassistant/components/shelly/config_flow.py b/homeassistant/components/shelly/config_flow.py index 521fca79dc9..d0e962b5e5f 100644 --- a/homeassistant/components/shelly/config_flow.py +++ b/homeassistant/components/shelly/config_flow.py @@ -3,7 +3,6 @@ from __future__ import annotations import asyncio from http import HTTPStatus -import logging from typing import Any, Final import aiohttp @@ -20,7 +19,7 @@ from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import aiohttp_client -from .const import AIOSHELLY_DEVICE_TIMEOUT_SEC, CONF_SLEEP_PERIOD, DOMAIN +from .const import AIOSHELLY_DEVICE_TIMEOUT_SEC, CONF_SLEEP_PERIOD, DOMAIN, LOGGER from .utils import ( get_block_device_name, get_block_device_sleep_period, @@ -31,8 +30,6 @@ from .utils import ( get_rpc_device_name, ) -_LOGGER: Final = logging.getLogger(__name__) - HOST_SCHEMA: Final = vol.Schema({vol.Required(CONF_HOST): str}) HTTP_CONNECT_ERRORS: Final = (asyncio.TimeoutError, aiohttp.ClientError) @@ -107,7 +104,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): except aioshelly.exceptions.FirmwareUnsupported: return self.async_abort(reason="unsupported_firmware") except Exception: # pylint: disable=broad-except - _LOGGER.exception("Unexpected exception") + LOGGER.exception("Unexpected exception") errors["base"] = "unknown" else: await self.async_set_unique_id(self.info["mac"]) @@ -123,7 +120,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): except HTTP_CONNECT_ERRORS: errors["base"] = "cannot_connect" except Exception: # pylint: disable=broad-except - _LOGGER.exception("Unexpected exception") + LOGGER.exception("Unexpected exception") errors["base"] = "unknown" else: return self.async_create_entry( @@ -158,7 +155,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): except HTTP_CONNECT_ERRORS: errors["base"] = "cannot_connect" except Exception: # pylint: disable=broad-except - _LOGGER.exception("Unexpected exception") + LOGGER.exception("Unexpected exception") errors["base"] = "unknown" else: return self.async_create_entry( diff --git a/homeassistant/components/shelly/const.py b/homeassistant/components/shelly/const.py index cd5735b2888..3dacf2bfd6a 100644 --- a/homeassistant/components/shelly/const.py +++ b/homeassistant/components/shelly/const.py @@ -1,13 +1,17 @@ """Constants for the Shelly integration.""" from __future__ import annotations +from logging import Logger, getLogger import re from typing import Final +DOMAIN: Final = "shelly" + +LOGGER: Logger = getLogger(__package__) + BLOCK: Final = "block" DATA_CONFIG_ENTRY: Final = "config_entry" DEVICE: Final = "device" -DOMAIN: Final = "shelly" REST: Final = "rest" RPC: Final = "rpc" RPC_POLL: Final = "rpc_poll" diff --git a/homeassistant/components/shelly/cover.py b/homeassistant/components/shelly/cover.py index f9bd1b7ce85..4885a2a0d2e 100644 --- a/homeassistant/components/shelly/cover.py +++ b/homeassistant/components/shelly/cover.py @@ -18,15 +18,28 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import BlockDeviceWrapper -from .const import BLOCK, DATA_CONFIG_ENTRY, DOMAIN -from .entity import ShellyBlockEntity +from . import BlockDeviceWrapper, RpcDeviceWrapper +from .const import BLOCK, DATA_CONFIG_ENTRY, DOMAIN, RPC +from .entity import ShellyBlockEntity, ShellyRpcEntity +from .utils import get_device_entry_gen, get_rpc_key_ids async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, +) -> None: + """Set up switches for device.""" + if get_device_entry_gen(config_entry) == 2: + return await async_setup_rpc_entry(hass, config_entry, async_add_entities) + + return await async_setup_block_entry(hass, config_entry, async_add_entities) + + +async def async_setup_block_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up cover for device.""" wrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id][BLOCK] @@ -35,16 +48,32 @@ async def async_setup_entry( if not blocks: return - async_add_entities(ShellyCover(wrapper, block) for block in blocks) + async_add_entities(BlockShellyCover(wrapper, block) for block in blocks) -class ShellyCover(ShellyBlockEntity, CoverEntity): - """Switch that controls a cover block on Shelly devices.""" +async def async_setup_rpc_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up entities for RPC device.""" + wrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id][RPC] + + cover_key_ids = get_rpc_key_ids(wrapper.device.status, "cover") + + if not cover_key_ids: + return + + async_add_entities(RpcShellyCover(wrapper, id_) for id_ in cover_key_ids) + + +class BlockShellyCover(ShellyBlockEntity, CoverEntity): + """Entity that controls a cover on block based Shelly devices.""" _attr_device_class = CoverDeviceClass.SHUTTER def __init__(self, wrapper: BlockDeviceWrapper, block: Block) -> None: - """Initialize light.""" + """Initialize block cover.""" super().__init__(wrapper, block) self.control_result: dict[str, Any] | None = None self._attr_supported_features: int = SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_STOP @@ -110,3 +139,61 @@ class ShellyCover(ShellyBlockEntity, CoverEntity): """When device updates, clear control result that overrides state.""" self.control_result = None super()._update_callback() + + +class RpcShellyCover(ShellyRpcEntity, CoverEntity): + """Entity that controls a cover on RPC based Shelly devices.""" + + _attr_device_class = CoverDeviceClass.SHUTTER + + def __init__(self, wrapper: RpcDeviceWrapper, id_: int) -> None: + """Initialize rpc cover.""" + super().__init__(wrapper, f"cover:{id_}") + self._id = id_ + self._attr_supported_features: int = SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_STOP + if self.status["pos_control"]: + self._attr_supported_features |= SUPPORT_SET_POSITION + + @property + def is_closed(self) -> bool | None: + """If cover is closed.""" + if not self.status["pos_control"]: + return None + + return cast(bool, self.status["state"] == "closed") + + @property + def current_cover_position(self) -> int | None: + """Position of the cover.""" + if not self.status["pos_control"]: + return None + + return cast(int, self.status["current_pos"]) + + @property + def is_closing(self) -> bool: + """Return if the cover is closing.""" + return cast(bool, self.status["state"] == "closing") + + @property + def is_opening(self) -> bool: + """Return if the cover is opening.""" + return cast(bool, self.status["state"] == "opening") + + async def async_close_cover(self, **kwargs: Any) -> None: + """Close cover.""" + await self.call_rpc("Cover.Close", {"id": self._id}) + + async def async_open_cover(self, **kwargs: Any) -> None: + """Open cover.""" + await self.call_rpc("Cover.Open", {"id": self._id}) + + async def async_set_cover_position(self, **kwargs: Any) -> None: + """Move the cover to a specific position.""" + await self.call_rpc( + "Cover.GoToPosition", {"id": self._id, "pos": kwargs[ATTR_POSITION]} + ) + + async def async_stop_cover(self, **_kwargs: Any) -> None: + """Stop the cover.""" + await self.call_rpc("Cover.Stop", {"id": self._id}) diff --git a/homeassistant/components/shelly/entity.py b/homeassistant/components/shelly/entity.py index 51e0711b035..f544770722f 100644 --- a/homeassistant/components/shelly/entity.py +++ b/homeassistant/components/shelly/entity.py @@ -4,8 +4,7 @@ from __future__ import annotations import asyncio from collections.abc import Callable, Mapping from dataclasses import dataclass -import logging -from typing import Any, Final, cast +from typing import Any, cast from aioshelly.block_device import Block import async_timeout @@ -34,6 +33,7 @@ from .const import ( BLOCK, DATA_CONFIG_ENTRY, DOMAIN, + LOGGER, REST, RPC, RPC_POLL, @@ -45,8 +45,6 @@ from .utils import ( get_rpc_key_instances, ) -_LOGGER: Final = logging.getLogger(__name__) - async def async_setup_entry_attribute_entities( hass: HomeAssistant, @@ -313,12 +311,12 @@ class ShellyBlockEntity(entity.Entity): async def set_state(self, **kwargs: Any) -> Any: """Set block state (HTTP request).""" - _LOGGER.debug("Setting state for entity %s, state: %s", self.name, kwargs) + LOGGER.debug("Setting state for entity %s, state: %s", self.name, kwargs) try: async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC): return await self.block.set_state(**kwargs) except (asyncio.TimeoutError, OSError) as err: - _LOGGER.error( + LOGGER.error( "Setting state for entity %s failed, state: %s, error: %s", self.name, kwargs, @@ -351,6 +349,11 @@ class ShellyRpcEntity(entity.Entity): """Available.""" return self.wrapper.device.connected + @property + def status(self) -> dict: + """Device status by entity key.""" + return cast(dict, self.wrapper.device.status[self.key]) + async def async_added_to_hass(self) -> None: """When entity is added to HASS.""" self.async_on_remove(self.wrapper.async_add_listener(self._update_callback)) @@ -366,7 +369,7 @@ class ShellyRpcEntity(entity.Entity): async def call_rpc(self, method: str, params: Any) -> Any: """Call RPC method.""" - _LOGGER.debug( + LOGGER.debug( "Call RPC for entity %s, method: %s, params: %s", self.name, method, @@ -376,7 +379,7 @@ class ShellyRpcEntity(entity.Entity): async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC): return await self.wrapper.device.call_rpc(method, params) except asyncio.TimeoutError as err: - _LOGGER.error( + LOGGER.error( "Call RPC for entity %s failed, method: %s, params: %s, error: %s", self.name, method, @@ -620,6 +623,6 @@ class ShellySleepingBlockAttributeEntity(ShellyBlockAttributeEntity, RestoreEnti self.block = block self.entity_description = description - _LOGGER.debug("Entity %s attached to block", self.name) + LOGGER.debug("Entity %s attached to block", self.name) super()._update_callback() return diff --git a/homeassistant/components/shelly/light.py b/homeassistant/components/shelly/light.py index 13a7720e7ed..e86c53c1914 100644 --- a/homeassistant/components/shelly/light.py +++ b/homeassistant/components/shelly/light.py @@ -1,8 +1,7 @@ """Light for Shelly.""" from __future__ import annotations -import logging -from typing import Any, Final, cast +from typing import Any, cast from aioshelly.block_device import Block @@ -42,6 +41,7 @@ from .const import ( KELVIN_MIN_VALUE_COLOR, KELVIN_MIN_VALUE_WHITE, LIGHT_TRANSITION_MIN_FIRMWARE_DATE, + LOGGER, MAX_TRANSITION_TIME, MODELS_SUPPORTING_LIGHT_TRANSITION, RGBW_MODELS, @@ -58,8 +58,6 @@ from .utils import ( is_rpc_channel_type_light, ) -_LOGGER: Final = logging.getLogger(__name__) - MIRED_MAX_VALUE_WHITE = color_temperature_kelvin_to_mired(KELVIN_MIN_VALUE_WHITE) MIRED_MIN_VALUE = color_temperature_kelvin_to_mired(KELVIN_MAX_VALUE) MIRED_MAX_VALUE_COLOR = color_temperature_kelvin_to_mired(KELVIN_MIN_VALUE_COLOR) @@ -348,7 +346,7 @@ class BlockShellyLight(ShellyBlockEntity, LightEntity): k for k, v in effect_dict.items() if v == kwargs[ATTR_EFFECT] ][0] else: - _LOGGER.error( + LOGGER.error( "Effect '%s' not supported by device %s", kwargs[ATTR_EFFECT], self.wrapper.model, diff --git a/homeassistant/components/shelly/number.py b/homeassistant/components/shelly/number.py index 27773c629c0..bfac4cd4033 100644 --- a/homeassistant/components/shelly/number.py +++ b/homeassistant/components/shelly/number.py @@ -3,7 +3,6 @@ from __future__ import annotations import asyncio from dataclasses import dataclass -import logging from typing import Any, Final, cast import async_timeout @@ -20,7 +19,7 @@ from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_registry import RegistryEntry -from .const import AIOSHELLY_DEVICE_TIMEOUT_SEC, CONF_SLEEP_PERIOD +from .const import AIOSHELLY_DEVICE_TIMEOUT_SEC, CONF_SLEEP_PERIOD, LOGGER from .entity import ( BlockEntityDescription, ShellySleepingBlockAttributeEntity, @@ -28,8 +27,6 @@ from .entity import ( ) from .utils import get_device_entry_gen -_LOGGER: Final = logging.getLogger(__name__) - @dataclass class BlockNumberDescription(BlockEntityDescription, NumberEntityDescription): @@ -119,12 +116,12 @@ class BlockSleepingNumber(ShellySleepingBlockAttributeEntity, NumberEntity): async def _set_state_full_path(self, path: str, params: Any) -> Any: """Set block state (HTTP request).""" - _LOGGER.debug("Setting state for entity %s, state: %s", self.name, params) + LOGGER.debug("Setting state for entity %s, state: %s", self.name, params) try: async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC): return await self.wrapper.device.http_request("get", path, params) except (asyncio.TimeoutError, OSError) as err: - _LOGGER.error( + LOGGER.error( "Setting state for entity %s failed, state: %s, error: %s", self.name, params, diff --git a/homeassistant/components/shelly/strings.json b/homeassistant/components/shelly/strings.json index 19a464fe7ce..209ae6682b8 100644 --- a/homeassistant/components/shelly/strings.json +++ b/homeassistant/components/shelly/strings.json @@ -28,26 +28,26 @@ "unsupported_firmware": "The device is using an unsupported firmware version." } }, - "device_automation":{ - "trigger_subtype": { - "button": "Button", - "button1": "First button", - "button2": "Second button", - "button3": "Third button", - "button4": "Fourth button" - }, - "trigger_type": { - "single": "{subtype} single clicked", - "double": "{subtype} double clicked", - "triple": "{subtype} triple clicked", - "long": "{subtype} long clicked", - "single_long": "{subtype} single clicked and then long clicked", - "long_single": "{subtype} long clicked and then single clicked", - "btn_down": "{subtype} button down", - "btn_up": "{subtype} button up", - "single_push": "{subtype} single push", - "double_push": "{subtype} double push", - "long_push": "{subtype} long push" - } + "device_automation": { + "trigger_subtype": { + "button": "Button", + "button1": "First button", + "button2": "Second button", + "button3": "Third button", + "button4": "Fourth button" + }, + "trigger_type": { + "single": "{subtype} single clicked", + "double": "{subtype} double clicked", + "triple": "{subtype} triple clicked", + "long": "{subtype} long clicked", + "single_long": "{subtype} single clicked and then long clicked", + "long_single": "{subtype} long clicked and then single clicked", + "btn_down": "{subtype} button down", + "btn_up": "{subtype} button up", + "single_push": "{subtype} single push", + "double_push": "{subtype} double push", + "long_push": "{subtype} long push" + } } } diff --git a/homeassistant/components/shelly/translations/el.json b/homeassistant/components/shelly/translations/el.json index c4a52891406..e83971e34fe 100644 --- a/homeassistant/components/shelly/translations/el.json +++ b/homeassistant/components/shelly/translations/el.json @@ -9,10 +9,10 @@ "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2", "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" }, - "flow_title": "Shelly: {\u03cc\u03bd\u03bf\u03bc\u03b1}", + "flow_title": "{name}", "step": { "confirm_discovery": { - "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {model} \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 {host};\n\n\u03a0\u03c1\u03b9\u03bd \u03b1\u03c0\u03cc \u03c4\u03b7 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7, \u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03bc\u03b5 \u03bc\u03c0\u03b1\u03c4\u03b1\u03c1\u03af\u03b1 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03be\u03c5\u03c0\u03bd\u03ae\u03c3\u03b5\u03b9 \u03c0\u03b1\u03c4\u03ce\u03bd\u03c4\u03b1\u03c2 \u03c4\u03bf \u03ba\u03bf\u03c5\u03bc\u03c0\u03af \u03c3\u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae." + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {model} \u03c3\u03c4\u03bf {host}; \n\n \u039f\u03b9 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03c0\u03bf\u03c5 \u03c4\u03c1\u03bf\u03c6\u03bf\u03b4\u03bf\u03c4\u03bf\u03cd\u03bd\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03bc\u03c0\u03b1\u03c4\u03b1\u03c1\u03af\u03b5\u03c2 \u03ba\u03b1\u03b9 \u03c0\u03c1\u03bf\u03c3\u03c4\u03b1\u03c4\u03b5\u03cd\u03bf\u03bd\u03c4\u03b1\u03b9 \u03bc\u03b5 \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03be\u03c5\u03c0\u03bd\u03ae\u03c3\u03bf\u03c5\u03bd \u03c0\u03c1\u03b9\u03bd \u03c3\u03c5\u03bd\u03b5\u03c7\u03af\u03c3\u03b5\u03c4\u03b5 \u03bc\u03b5 \u03c4\u03b7 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7.\n \u039f\u03b9 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03c0\u03bf\u03c5 \u03c4\u03c1\u03bf\u03c6\u03bf\u03b4\u03bf\u03c4\u03bf\u03cd\u03bd\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03bc\u03c0\u03b1\u03c4\u03b1\u03c1\u03af\u03b5\u03c2 \u03ba\u03b1\u03b9 \u03b4\u03b5\u03bd \u03c0\u03c1\u03bf\u03c3\u03c4\u03b1\u03c4\u03b5\u03cd\u03bf\u03bd\u03c4\u03b1\u03b9 \u03bc\u03b5 \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03b8\u03b1 \u03c0\u03c1\u03bf\u03c3\u03c4\u03b5\u03b8\u03bf\u03cd\u03bd \u03cc\u03c4\u03b1\u03bd \u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03be\u03c5\u03c0\u03bd\u03ae\u03c3\u03b5\u03b9, \u03bc\u03c0\u03bf\u03c1\u03b5\u03af\u03c4\u03b5 \u03c0\u03bb\u03ad\u03bf\u03bd \u03bd\u03b1 \u03be\u03c5\u03c0\u03bd\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03bc\u03b5 \u03bc\u03b7 \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03bf \u03c4\u03c1\u03cc\u03c0\u03bf \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ce\u03bd\u03c4\u03b1\u03c2 \u03ad\u03bd\u03b1 \u03ba\u03bf\u03c5\u03bc\u03c0\u03af \u03c3\u03b5 \u03b1\u03c5\u03c4\u03ae\u03bd \u03ae \u03bd\u03b1 \u03c0\u03b5\u03c1\u03b9\u03bc\u03ad\u03bd\u03b5\u03c4\u03b5 \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd \u03b5\u03c0\u03cc\u03bc\u03b5\u03bd\u03b7 \u03b5\u03bd\u03b7\u03bc\u03ad\u03c1\u03c9\u03c3\u03b7 \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03c9\u03bd \u03b1\u03c0\u03cc \u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae." }, "credentials": { "data": { @@ -24,7 +24,7 @@ "data": { "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2" }, - "description": "\u03a0\u03c1\u03b9\u03bd \u03b1\u03c0\u03cc \u03c4\u03b7 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7, \u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03bc\u03b5 \u03bc\u03c0\u03b1\u03c4\u03b1\u03c1\u03af\u03b1 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03be\u03c5\u03c0\u03bd\u03ae\u03c3\u03b5\u03b9 \u03c0\u03b1\u03c4\u03ce\u03bd\u03c4\u03b1\u03c2 \u03c4\u03bf \u03ba\u03bf\u03c5\u03bc\u03c0\u03af \u03c3\u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae." + "description": "\u03a0\u03c1\u03b9\u03bd \u03b1\u03c0\u03cc \u03c4\u03b7 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7, \u03bf\u03b9 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03c0\u03bf\u03c5 \u03c4\u03c1\u03bf\u03c6\u03bf\u03b4\u03bf\u03c4\u03bf\u03cd\u03bd\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03bc\u03c0\u03b1\u03c4\u03b1\u03c1\u03af\u03b1 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03be\u03c5\u03c0\u03bd\u03ae\u03c3\u03bf\u03c5\u03bd, \u03c4\u03ce\u03c1\u03b1 \u03bc\u03c0\u03bf\u03c1\u03b5\u03af\u03c4\u03b5 \u03bd\u03b1 \u03be\u03c5\u03c0\u03bd\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ce\u03bd\u03c4\u03b1\u03c2 \u03ad\u03bd\u03b1 \u03ba\u03bf\u03c5\u03bc\u03c0\u03af \u03c0\u03ac\u03bd\u03c9 \u03c4\u03b7\u03c2." } } }, diff --git a/homeassistant/components/shelly/translations/fr.json b/homeassistant/components/shelly/translations/fr.json index 83b66645157..cd19af62bad 100644 --- a/homeassistant/components/shelly/translations/fr.json +++ b/homeassistant/components/shelly/translations/fr.json @@ -6,7 +6,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "flow_title": "Shelly: {name}", @@ -40,13 +40,13 @@ "btn_down": "{sous-type} bouton en bas", "btn_up": "{sous-type} bouton haut", "double": "{subtype} double-cliqu\u00e9", - "double_push": "{subtype} double pression", + "double_push": "Double pression sur {subtype}", "long": "{subtype} long cliqu\u00e9", - "long_push": "{subtype} appui long", + "long_push": "Pression longue sur {subtype}", "long_single": "{subtype} clic long et simple clic", "single": "{subtype} simple clic", "single_long": "{subtype} simple clic, puis un clic long", - "single_push": "{subtype} simple pression", + "single_push": "Pression simple sur {subtype}", "triple": "{subtype} cliqu\u00e9 trois fois" } } diff --git a/homeassistant/components/shelly/utils.py b/homeassistant/components/shelly/utils.py index 7a41c914e8a..df5a75a7ed9 100644 --- a/homeassistant/components/shelly/utils.py +++ b/homeassistant/components/shelly/utils.py @@ -2,8 +2,7 @@ from __future__ import annotations from datetime import datetime, timedelta -import logging -from typing import Any, Final, cast +from typing import Any, cast from aioshelly.block_device import BLOCK_VALUE_UNIT, COAP, Block, BlockDevice from aioshelly.const import MODEL_NAMES @@ -21,6 +20,7 @@ from .const import ( CONF_COAP_PORT, DEFAULT_COAP_PORT, DOMAIN, + LOGGER, MAX_RPC_KEY_INSTANCES, RPC_INPUTS_EVENTS_TYPES, SHBTN_INPUTS_EVENTS_TYPES, @@ -29,8 +29,6 @@ from .const import ( UPTIME_DEVIATION, ) -_LOGGER: Final = logging.getLogger(__name__) - async def async_remove_shelly_entity( hass: HomeAssistant, domain: str, unique_id: str @@ -39,7 +37,7 @@ async def async_remove_shelly_entity( entity_reg = await hass.helpers.entity_registry.async_get_registry() entity_id = entity_reg.async_get_entity_id(domain, DOMAIN, unique_id) if entity_id: - _LOGGER.debug("Removing entity: %s", entity_id) + LOGGER.debug("Removing entity: %s", entity_id) entity_reg.async_remove(entity_id) @@ -220,7 +218,7 @@ async def get_coap_context(hass: HomeAssistant) -> COAP: port = hass.data[DOMAIN].get(CONF_COAP_PORT, DEFAULT_COAP_PORT) else: port = DEFAULT_COAP_PORT - _LOGGER.info("Starting CoAP context with UDP port %s", port) + LOGGER.info("Starting CoAP context with UDP port %s", port) await context.initialize(port) @callback @@ -267,7 +265,9 @@ def get_rpc_channel_name(device: RpcDevice, key: str) -> str: if device.config.get("switch:0"): key = key.replace("input", "switch") device_name = get_rpc_device_name(device) - entity_name: str | None = device.config[key].get("name", device_name) + entity_name: str | None = None + if key in device.config: + entity_name = device.config[key].get("name", device_name) if entity_name is None: return f"{device_name} {key.replace(':', '_')}" @@ -362,7 +362,7 @@ def device_update_info( ) -> None: """Update device registry info.""" - _LOGGER.debug("Updating device registry info for %s", entry.title) + LOGGER.debug("Updating device registry info for %s", entry.title) assert entry.unique_id diff --git a/homeassistant/components/shodan/manifest.json b/homeassistant/components/shodan/manifest.json index 49e6a14b715..7d609a5e9c3 100644 --- a/homeassistant/components/shodan/manifest.json +++ b/homeassistant/components/shodan/manifest.json @@ -2,7 +2,7 @@ "domain": "shodan", "name": "Shodan", "documentation": "https://www.home-assistant.io/integrations/shodan", - "requirements": ["shodan==1.26.1"], + "requirements": ["shodan==1.27.0"], "codeowners": ["@fabaff"], "iot_class": "cloud_polling", "loggers": ["shodan"] diff --git a/homeassistant/components/sht31/__init__.py b/homeassistant/components/sht31/__init__.py deleted file mode 100644 index 16bfe384d94..00000000000 --- a/homeassistant/components/sht31/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The sht31 component.""" diff --git a/homeassistant/components/sht31/manifest.json b/homeassistant/components/sht31/manifest.json deleted file mode 100644 index c91d6a62768..00000000000 --- a/homeassistant/components/sht31/manifest.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "domain": "sht31", - "name": "Sensirion SHT31", - "documentation": "https://www.home-assistant.io/integrations/sht31", - "requirements": ["Adafruit-GPIO==1.0.3", "Adafruit-SHT31==1.0.2"], - "codeowners": [], - "iot_class": "local_polling" -} diff --git a/homeassistant/components/sht31/sensor.py b/homeassistant/components/sht31/sensor.py deleted file mode 100644 index 4c9dc63606d..00000000000 --- a/homeassistant/components/sht31/sensor.py +++ /dev/null @@ -1,159 +0,0 @@ -"""Support for Sensirion SHT31 temperature and humidity sensor.""" -from __future__ import annotations - -from collections.abc import Callable -from dataclasses import dataclass -from datetime import timedelta -import logging -import math - -from Adafruit_SHT31 import SHT31 -import voluptuous as vol - -from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, - SensorDeviceClass, - SensorEntity, - SensorEntityDescription, -) -from homeassistant.const import ( - CONF_MONITORED_CONDITIONS, - CONF_NAME, - PERCENTAGE, - TEMP_CELSIUS, -) -from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from homeassistant.util import Throttle - -_LOGGER = logging.getLogger(__name__) - -CONF_I2C_ADDRESS = "i2c_address" - -DEFAULT_NAME = "SHT31" -DEFAULT_I2C_ADDRESS = 0x44 - - -@dataclass -class SHT31RequiredKeysMixin: - """Mixin for required keys.""" - - value_fn: Callable[[SHTClient], float | None] - - -@dataclass -class SHT31SensorEntityDescription(SensorEntityDescription, SHT31RequiredKeysMixin): - """Describes SHT31 sensor entity.""" - - -SENSOR_TYPES = ( - SHT31SensorEntityDescription( - key="temperature", - name="Temperature", - device_class=SensorDeviceClass.TEMPERATURE, - native_unit_of_measurement=TEMP_CELSIUS, - value_fn=lambda sensor: sensor.temperature, - ), - SHT31SensorEntityDescription( - key="humidity", - name="Humidity", - device_class=SensorDeviceClass.HUMIDITY, - native_unit_of_measurement=PERCENTAGE, - value_fn=lambda sensor: ( - round(val) # pylint: disable=undefined-variable - if (val := sensor.humidity) - else None - ), - ), -) -SENSOR_KEYS: list[str] = [desc.key for desc in SENSOR_TYPES] - -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=10) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_I2C_ADDRESS, default=DEFAULT_I2C_ADDRESS): vol.All( - vol.Coerce(int), vol.Range(min=0x44, max=0x45) - ), - vol.Optional(CONF_MONITORED_CONDITIONS, default=SENSOR_KEYS): vol.All( - cv.ensure_list, [vol.In(SENSOR_KEYS)] - ), - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - } -) - - -def setup_platform( - hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the sensor platform.""" - _LOGGER.warning( - "The Sensirion SHT31 integration is deprecated and will be removed " - "in Home Assistant Core 2022.4; this integration is removed under " - "Architectural Decision Record 0019, more information can be found here: " - "https://github.com/home-assistant/architecture/blob/master/adr/0019-GPIO.md" - ) - - name = config[CONF_NAME] - monitored_conditions = config[CONF_MONITORED_CONDITIONS] - i2c_address = config[CONF_I2C_ADDRESS] - sensor = SHT31(address=i2c_address) - - try: - if sensor.read_status() is None: - raise ValueError("CRC error while reading SHT31 status") - except (OSError, ValueError): - _LOGGER.error("SHT31 sensor not detected at address %s", hex(i2c_address)) - return - sensor_client = SHTClient(sensor) - - entities = [ - SHTSensor(sensor_client, name, description) - for description in SENSOR_TYPES - if description.key in monitored_conditions - ] - - add_entities(entities) - - -class SHTClient: - """Get the latest data from the SHT sensor.""" - - def __init__(self, adafruit_sht): - """Initialize the sensor.""" - self.adafruit_sht = adafruit_sht - self.temperature: float | None = None - self.humidity: float | None = None - - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self): - """Get the latest data from the SHT sensor.""" - temperature, humidity = self.adafruit_sht.read_temperature_humidity() - if math.isnan(temperature) or math.isnan(humidity): - _LOGGER.warning("Bad sample from sensor SHT31") - return - self.temperature = temperature - self.humidity = humidity - - -class SHTSensor(SensorEntity): - """An abstract SHTSensor, can be either temperature or humidity.""" - - entity_description: SHT31SensorEntityDescription - - def __init__(self, sensor, name, description: SHT31SensorEntityDescription): - """Initialize the sensor.""" - self.entity_description = description - self._sensor = sensor - - self._attr_name = f"{name} {description.name}" - - def update(self): - """Fetch temperature and humidity from the sensor.""" - self._sensor.update() - self._attr_native_value = self.entity_description.value_fn(self._sensor) diff --git a/homeassistant/components/signal_messenger/manifest.json b/homeassistant/components/signal_messenger/manifest.json index e95760fc1e0..b38931fd067 100644 --- a/homeassistant/components/signal_messenger/manifest.json +++ b/homeassistant/components/signal_messenger/manifest.json @@ -2,12 +2,8 @@ "domain": "signal_messenger", "name": "Signal Messenger", "documentation": "https://www.home-assistant.io/integrations/signal_messenger", - "codeowners": [ - "@bbernhard" - ], - "requirements": [ - "pysignalclirestapi==0.3.18" - ], + "codeowners": ["@bbernhard"], + "requirements": ["pysignalclirestapi==0.3.18"], "iot_class": "cloud_push", "loggers": ["pysignalclirestapi"] -} \ No newline at end of file +} diff --git a/homeassistant/components/simplisafe/diagnostics.py b/homeassistant/components/simplisafe/diagnostics.py index c7c03467c94..dac89715c10 100644 --- a/homeassistant/components/simplisafe/diagnostics.py +++ b/homeassistant/components/simplisafe/diagnostics.py @@ -5,7 +5,7 @@ from typing import Any from homeassistant.components.diagnostics import async_redact_data from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_ADDRESS +from homeassistant.const import CONF_ADDRESS, CONF_LOCATION from homeassistant.core import HomeAssistant from . import SimpliSafe @@ -13,7 +13,6 @@ from .const import DOMAIN CONF_CREDIT_CARD = "creditCard" CONF_EXPIRES = "expires" -CONF_LOCATION = "location" CONF_LOCATION_NAME = "locationName" CONF_PAYMENT_PROFILE_ID = "paymentProfileId" CONF_SERIAL = "serial" diff --git a/homeassistant/components/simplisafe/manifest.json b/homeassistant/components/simplisafe/manifest.json index deb9577d576..3791a9cace9 100644 --- a/homeassistant/components/simplisafe/manifest.json +++ b/homeassistant/components/simplisafe/manifest.json @@ -3,14 +3,14 @@ "name": "SimpliSafe", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/simplisafe", - "requirements": ["simplisafe-python==2022.02.1"], + "requirements": ["simplisafe-python==2022.03.0"], "codeowners": ["@bachya"], "iot_class": "cloud_polling", "dhcp": [ - { - "hostname": "simplisafe*", - "macaddress": "30AEA4*" - } + { + "hostname": "simplisafe*", + "macaddress": "30AEA4*" + } ], "loggers": ["simplipy"] } diff --git a/homeassistant/components/simplisafe/services.yaml b/homeassistant/components/simplisafe/services.yaml index 3d0965b4b0b..9875d5baa37 100644 --- a/homeassistant/components/simplisafe/services.yaml +++ b/homeassistant/components/simplisafe/services.yaml @@ -82,20 +82,20 @@ set_system_properties: selector: select: options: - - 'low' - - 'medium' - - 'high' - - 'off' + - "low" + - "medium" + - "high" + - "off" chime_volume: name: Chime volume description: The volume level of the door chime selector: select: options: - - 'low' - - 'medium' - - 'high' - - 'off' + - "low" + - "medium" + - "high" + - "off" entry_delay_away: name: Entry delay away description: How long to delay when entering while "away" @@ -139,7 +139,7 @@ set_system_properties: selector: select: options: - - 'low' - - 'medium' - - 'high' - - 'off' + - "low" + - "medium" + - "high" + - "off" diff --git a/homeassistant/components/simplisafe/translations/fr.json b/homeassistant/components/simplisafe/translations/fr.json index d0ff1347bbd..d50ac11f851 100644 --- a/homeassistant/components/simplisafe/translations/fr.json +++ b/homeassistant/components/simplisafe/translations/fr.json @@ -7,7 +7,7 @@ }, "error": { "identifier_exists": "Compte d\u00e9j\u00e0 enregistr\u00e9", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "still_awaiting_mfa": "En attente de clic sur le message \u00e9lectronique d'authentification multi facteur", "unknown": "Erreur inattendue" }, @@ -35,7 +35,7 @@ "auth_code": "Code d'autorisation", "code": "Code (utilis\u00e9 dans l'interface Home Assistant)", "password": "Mot de passe", - "username": "Email" + "username": "Courriel" }, "description": "SimpliSafe s'authentifie avec Home Assistant via l'application Web SimpliSafe. En raison de limitations techniques, il y a une \u00e9tape manuelle \u00e0 la fin de ce processus ; veuillez vous assurer de lire la [documentation]( {docs_url} ) avant de commencer. \n\n 1. Cliquez sur [ici]( {url} ) pour ouvrir l'application Web SimpliSafe et saisissez vos informations d'identification. \n\n 2. Une fois le processus de connexion termin\u00e9, revenez ici et saisissez le code d'autorisation ci-dessous.", "title": "Veuillez saisir vos informations" diff --git a/homeassistant/components/siren/manifest.json b/homeassistant/components/siren/manifest.json index 454835c33b0..a3f3989e3f1 100644 --- a/homeassistant/components/siren/manifest.json +++ b/homeassistant/components/siren/manifest.json @@ -4,4 +4,4 @@ "documentation": "https://www.home-assistant.io/integrations/siren", "codeowners": ["@home-assistant/core", "@raman325"], "quality_scale": "internal" -} \ No newline at end of file +} diff --git a/homeassistant/components/siren/recorder.py b/homeassistant/components/siren/recorder.py new file mode 100644 index 00000000000..3daf4fc52b2 --- /dev/null +++ b/homeassistant/components/siren/recorder.py @@ -0,0 +1,12 @@ +"""Integration platform for recorder.""" +from __future__ import annotations + +from homeassistant.core import HomeAssistant, callback + +from . import ATTR_AVAILABLE_TONES + + +@callback +def exclude_attributes(hass: HomeAssistant) -> set[str]: + """Exclude static attributes from being recorded in the database.""" + return {ATTR_AVAILABLE_TONES} diff --git a/homeassistant/components/sisyphus/manifest.json b/homeassistant/components/sisyphus/manifest.json index 62cfca125f6..36941e64d24 100644 --- a/homeassistant/components/sisyphus/manifest.json +++ b/homeassistant/components/sisyphus/manifest.json @@ -2,12 +2,8 @@ "domain": "sisyphus", "name": "Sisyphus", "documentation": "https://www.home-assistant.io/integrations/sisyphus", - "requirements": [ - "sisyphus-control==3.1.2" - ], - "codeowners": [ - "@jkeljo" - ], + "requirements": ["sisyphus-control==3.1.2"], + "codeowners": ["@jkeljo"], "iot_class": "local_push", "loggers": ["sisyphus_control"] -} \ No newline at end of file +} diff --git a/homeassistant/components/sleepiq/__init__.py b/homeassistant/components/sleepiq/__init__.py index bac88880cdb..b98be3dcfe1 100644 --- a/homeassistant/components/sleepiq/__init__.py +++ b/homeassistant/components/sleepiq/__init__.py @@ -1,5 +1,8 @@ """Support for SleepIQ from SleepNumber.""" +from __future__ import annotations + import logging +from typing import Any from asyncsleepiq import ( AsyncSleepIQ, @@ -10,14 +13,15 @@ from asyncsleepiq import ( import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform -from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, PRESSURE, Platform +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady +from homeassistant.helpers import entity_registry as er from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType -from .const import DOMAIN +from .const import DOMAIN, IS_IN_BED, SLEEP_NUMBER from .coordinator import ( SleepIQData, SleepIQDataUpdateCoordinator, @@ -26,7 +30,15 @@ from .coordinator import ( _LOGGER = logging.getLogger(__name__) -PLATFORMS = [Platform.BINARY_SENSOR, Platform.BUTTON, Platform.SENSOR, Platform.SWITCH] +PLATFORMS = [ + Platform.BINARY_SENSOR, + Platform.BUTTON, + Platform.LIGHT, + Platform.NUMBER, + Platform.SELECT, + Platform.SENSOR, + Platform.SWITCH, +] CONFIG_SCHEMA = vol.Schema( { @@ -63,9 +75,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: await gateway.login(email, password) - except SleepIQLoginException: + except SleepIQLoginException as err: _LOGGER.error("Could not authenticate with SleepIQ server") - return False + raise ConfigEntryAuthFailed(err) from err except SleepIQTimeoutException as err: raise ConfigEntryNotReady( str(err) or "Timed out during authentication" @@ -80,6 +92,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except SleepIQAPIException as err: raise ConfigEntryNotReady(str(err) or "Error reading from SleepIQ API") from err + await _async_migrate_unique_ids(hass, entry, gateway) + coordinator = SleepIQDataUpdateCoordinator(hass, gateway, email) pause_coordinator = SleepIQPauseUpdateCoordinator(hass, gateway, email) @@ -103,3 +117,48 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): hass.data[DOMAIN].pop(entry.entry_id) return unload_ok + + +async def _async_migrate_unique_ids( + hass: HomeAssistant, entry: ConfigEntry, gateway: AsyncSleepIQ +) -> None: + """Migrate old unique ids.""" + names_to_ids = { + sleeper.name: sleeper.sleeper_id + for bed in gateway.beds.values() + for sleeper in bed.sleepers + } + + bed_ids = {bed.id for bed in gateway.beds.values()} + + @callback + def _async_migrator(entity_entry: er.RegistryEntry) -> dict[str, Any] | None: + # Old format for sleeper entities was {bed_id}_{sleeper.name}_{sensor_type}..... + # New format is {sleeper.sleeper_id}_{sensor_type}.... + sensor_types = [IS_IN_BED, PRESSURE, SLEEP_NUMBER] + + old_unique_id = entity_entry.unique_id + parts = old_unique_id.split("_") + + # If it doesn't begin with a bed id or end with one of the sensor types, + # it doesn't need to be migrated + if parts[0] not in bed_ids or not old_unique_id.endswith(tuple(sensor_types)): + return None + + sensor_type = next(filter(old_unique_id.endswith, sensor_types)) + sleeper_name = "_".join(parts[1:]).removesuffix(f"_{sensor_type}") + sleeper_id = names_to_ids.get(sleeper_name) + + if not sleeper_id: + return None + + new_unique_id = f"{sleeper_id}_{sensor_type}" + + _LOGGER.debug( + "Migrating unique_id from [%s] to [%s]", + old_unique_id, + new_unique_id, + ) + return {"new_unique_id": new_unique_id} + + await er.async_migrate_entries(hass, entry.entry_id, _async_migrator) diff --git a/homeassistant/components/sleepiq/binary_sensor.py b/homeassistant/components/sleepiq/binary_sensor.py index 53611edc66b..b176320e671 100644 --- a/homeassistant/components/sleepiq/binary_sensor.py +++ b/homeassistant/components/sleepiq/binary_sensor.py @@ -12,7 +12,7 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import DOMAIN, ICON_EMPTY, ICON_OCCUPIED, IS_IN_BED from .coordinator import SleepIQData -from .entity import SleepIQSensor +from .entity import SleepIQSleeperEntity async def async_setup_entry( @@ -29,7 +29,7 @@ async def async_setup_entry( ) -class IsInBedBinarySensor(SleepIQSensor, BinarySensorEntity): +class IsInBedBinarySensor(SleepIQSleeperEntity, BinarySensorEntity): """Implementation of a SleepIQ presence sensor.""" _attr_device_class = BinarySensorDeviceClass.OCCUPANCY diff --git a/homeassistant/components/sleepiq/config_flow.py b/homeassistant/components/sleepiq/config_flow.py index dffb30f39d7..49f14eff0b9 100644 --- a/homeassistant/components/sleepiq/config_flow.py +++ b/homeassistant/components/sleepiq/config_flow.py @@ -1,12 +1,13 @@ """Config flow to configure SleepIQ component.""" from __future__ import annotations +import logging from typing import Any from asyncsleepiq import AsyncSleepIQ, SleepIQLoginException, SleepIQTimeoutException import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigEntry, ConfigFlow from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResult @@ -14,12 +15,18 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DOMAIN +_LOGGER = logging.getLogger(__name__) -class SleepIQFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + +class SleepIQFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a SleepIQ config flow.""" VERSION = 1 + def __init__(self) -> None: + """Initialize the config flow.""" + self._reauth_entry: ConfigEntry | None = None + async def async_step_import(self, import_config: dict[str, Any]) -> FlowResult: """Import a SleepIQ account as a config entry. @@ -28,6 +35,10 @@ class SleepIQFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): await self.async_set_unique_id(import_config[CONF_USERNAME].lower()) self._abort_if_unique_id_configured() + if error := await try_connection(self.hass, import_config): + _LOGGER.error("Could not authenticate with SleepIQ server: %s", error) + return self.async_abort(reason=error) + return self.async_create_entry( title=import_config[CONF_USERNAME], data=import_config ) @@ -43,26 +54,23 @@ class SleepIQFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): await self.async_set_unique_id(user_input[CONF_USERNAME].lower()) self._abort_if_unique_id_configured() - try: - await try_connection(self.hass, user_input) - except SleepIQLoginException: - errors["base"] = "invalid_auth" - except SleepIQTimeoutException: - errors["base"] = "cannot_connect" + if error := await try_connection(self.hass, user_input): + errors["base"] = error else: return self.async_create_entry( title=user_input[CONF_USERNAME], data=user_input ) + else: + user_input = {} + return self.async_show_form( step_id="user", data_schema=vol.Schema( { vol.Required( CONF_USERNAME, - default=user_input.get(CONF_USERNAME) - if user_input is not None - else "", + default=user_input.get(CONF_USERNAME), ): str, vol.Required(CONF_PASSWORD): str, } @@ -71,11 +79,54 @@ class SleepIQFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): last_step=True, ) + async def async_step_reauth(self, user_input: dict[str, Any]) -> FlowResult: + """Perform reauth upon an API authentication error.""" + self._reauth_entry = self.hass.config_entries.async_get_entry( + self.context["entry_id"] + ) + return await self.async_step_reauth_confirm(user_input) -async def try_connection(hass: HomeAssistant, user_input: dict[str, Any]) -> None: + async def async_step_reauth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Confirm reauth.""" + errors: dict[str, str] = {} + assert self._reauth_entry is not None + if user_input is not None: + data = { + CONF_USERNAME: self._reauth_entry.data[CONF_USERNAME], + CONF_PASSWORD: user_input[CONF_PASSWORD], + } + + if not (error := await try_connection(self.hass, data)): + self.hass.config_entries.async_update_entry( + self._reauth_entry, data=data + ) + await self.hass.config_entries.async_reload(self._reauth_entry.entry_id) + return self.async_abort(reason="reauth_successful") + errors["base"] = error + + return self.async_show_form( + step_id="reauth_confirm", + data_schema=vol.Schema({vol.Required(CONF_PASSWORD): str}), + errors=errors, + description_placeholders={ + CONF_USERNAME: self._reauth_entry.data[CONF_USERNAME], + }, + ) + + +async def try_connection(hass: HomeAssistant, user_input: dict[str, Any]) -> str | None: """Test if the given credentials can successfully login to SleepIQ.""" client_session = async_get_clientsession(hass) gateway = AsyncSleepIQ(client_session=client_session) - await gateway.login(user_input[CONF_USERNAME], user_input[CONF_PASSWORD]) + try: + await gateway.login(user_input[CONF_USERNAME], user_input[CONF_PASSWORD]) + except SleepIQLoginException: + return "invalid_auth" + except SleepIQTimeoutException: + return "cannot_connect" + + return None diff --git a/homeassistant/components/sleepiq/const.py b/homeassistant/components/sleepiq/const.py index 63e86270925..4eb6148f9b8 100644 --- a/homeassistant/components/sleepiq/const.py +++ b/homeassistant/components/sleepiq/const.py @@ -2,14 +2,22 @@ DATA_SLEEPIQ = "data_sleepiq" DOMAIN = "sleepiq" -SLEEPYQ_INVALID_CREDENTIALS_MESSAGE = "username or password" +ACTUATOR = "actuator" BED = "bed" +FIRMNESS = "firmness" ICON_EMPTY = "mdi:bed-empty" ICON_OCCUPIED = "mdi:bed" IS_IN_BED = "is_in_bed" +PRESSURE = "pressure" SLEEP_NUMBER = "sleep_number" -SENSOR_TYPES = {SLEEP_NUMBER: "SleepNumber", IS_IN_BED: "Is In Bed"} +ENTITY_TYPES = { + ACTUATOR: "Position", + FIRMNESS: "Firmness", + PRESSURE: "Pressure", + IS_IN_BED: "Is In Bed", + SLEEP_NUMBER: "SleepNumber", +} LEFT = "left" RIGHT = "right" diff --git a/homeassistant/components/sleepiq/coordinator.py b/homeassistant/components/sleepiq/coordinator.py index a2394de20b1..8d51da5f47a 100644 --- a/homeassistant/components/sleepiq/coordinator.py +++ b/homeassistant/components/sleepiq/coordinator.py @@ -34,7 +34,11 @@ class SleepIQDataUpdateCoordinator(DataUpdateCoordinator[None]): self.client = client async def _async_update_data(self) -> None: - await self.client.fetch_bed_statuses() + tasks = [self.client.fetch_bed_statuses()] + [ + bed.foundation.update_foundation_status() + for bed in self.client.beds.values() + ] + await asyncio.gather(*tasks) class SleepIQPauseUpdateCoordinator(DataUpdateCoordinator[None]): diff --git a/homeassistant/components/sleepiq/entity.py b/homeassistant/components/sleepiq/entity.py index 6d0c8784eec..c73988ce638 100644 --- a/homeassistant/components/sleepiq/entity.py +++ b/homeassistant/components/sleepiq/entity.py @@ -11,7 +11,7 @@ from homeassistant.helpers.update_coordinator import ( DataUpdateCoordinator, ) -from .const import ICON_OCCUPIED, SENSOR_TYPES +from .const import ENTITY_TYPES, ICON_OCCUPIED def device_from_bed(bed: SleepIQBed) -> DeviceInfo: @@ -33,7 +33,7 @@ class SleepIQEntity(Entity): self._attr_device_info = device_from_bed(bed) -class SleepIQSensor(CoordinatorEntity): +class SleepIQBedEntity(CoordinatorEntity): """Implementation of a SleepIQ sensor.""" _attr_icon = ICON_OCCUPIED @@ -42,17 +42,11 @@ class SleepIQSensor(CoordinatorEntity): self, coordinator: DataUpdateCoordinator, bed: SleepIQBed, - sleeper: SleepIQSleeper, - name: str, ) -> None: """Initialize the SleepIQ sensor entity.""" super().__init__(coordinator) - self.sleeper = sleeper self.bed = bed self._attr_device_info = device_from_bed(bed) - - self._attr_name = f"SleepNumber {bed.name} {sleeper.name} {SENSOR_TYPES[name]}" - self._attr_unique_id = f"{bed.id}_{sleeper.name}_{name}" self._async_update_attrs() @callback @@ -67,7 +61,7 @@ class SleepIQSensor(CoordinatorEntity): """Update sensor attributes.""" -class SleepIQBedCoordinator(CoordinatorEntity): +class SleepIQSleeperEntity(SleepIQBedEntity): """Implementation of a SleepIQ sensor.""" _attr_icon = ICON_OCCUPIED @@ -76,8 +70,12 @@ class SleepIQBedCoordinator(CoordinatorEntity): self, coordinator: DataUpdateCoordinator, bed: SleepIQBed, + sleeper: SleepIQSleeper, + name: str, ) -> None: """Initialize the SleepIQ sensor entity.""" - super().__init__(coordinator) - self.bed = bed - self._attr_device_info = device_from_bed(bed) + self.sleeper = sleeper + super().__init__(coordinator, bed) + + self._attr_name = f"SleepNumber {bed.name} {sleeper.name} {ENTITY_TYPES[name]}" + self._attr_unique_id = f"{sleeper.sleeper_id}_{name}" diff --git a/homeassistant/components/sleepiq/light.py b/homeassistant/components/sleepiq/light.py new file mode 100644 index 00000000000..1017051f94c --- /dev/null +++ b/homeassistant/components/sleepiq/light.py @@ -0,0 +1,59 @@ +"""Support for SleepIQ outlet lights.""" +import logging +from typing import Any + +from asyncsleepiq import SleepIQBed, SleepIQLight + +from homeassistant.components.light import LightEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from .const import DOMAIN +from .coordinator import SleepIQData +from .entity import SleepIQBedEntity + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the SleepIQ bed lights.""" + data: SleepIQData = hass.data[DOMAIN][entry.entry_id] + async_add_entities( + SleepIQLightEntity(data.data_coordinator, bed, light) + for bed in data.client.beds.values() + for light in bed.foundation.lights + ) + + +class SleepIQLightEntity(SleepIQBedEntity, LightEntity): + """Representation of a light.""" + + def __init__( + self, coordinator: DataUpdateCoordinator, bed: SleepIQBed, light: SleepIQLight + ) -> None: + """Initialize the light.""" + self.light = light + super().__init__(coordinator, bed) + self._attr_name = f"SleepNumber {bed.name} Light {light.outlet_id}" + self._attr_unique_id = f"{bed.id}-light-{light.outlet_id}" + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn on light.""" + await self.light.turn_on() + self._handle_coordinator_update() + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn off light.""" + await self.light.turn_off() + self._handle_coordinator_update() + + @callback + def _async_update_attrs(self) -> None: + """Update light attributes.""" + self._attr_is_on = self.light.is_on diff --git a/homeassistant/components/sleepiq/manifest.json b/homeassistant/components/sleepiq/manifest.json index 93cd1be3204..cadfe126ecf 100644 --- a/homeassistant/components/sleepiq/manifest.json +++ b/homeassistant/components/sleepiq/manifest.json @@ -3,7 +3,7 @@ "name": "SleepIQ", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sleepiq", - "requirements": ["asyncsleepiq==1.1.0"], + "requirements": ["asyncsleepiq==1.2.3"], "codeowners": ["@mfugate1", "@kbickar"], "dhcp": [ { diff --git a/homeassistant/components/sleepiq/number.py b/homeassistant/components/sleepiq/number.py new file mode 100644 index 00000000000..fb17336ccb3 --- /dev/null +++ b/homeassistant/components/sleepiq/number.py @@ -0,0 +1,160 @@ +"""Support for SleepIQ SleepNumber firmness number entities.""" +from __future__ import annotations + +from collections.abc import Callable, Coroutine +from dataclasses import dataclass +from typing import Any, cast + +from asyncsleepiq import SleepIQActuator, SleepIQBed, SleepIQSleeper + +from homeassistant.components.number import NumberEntity, NumberEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from .const import ACTUATOR, DOMAIN, ENTITY_TYPES, FIRMNESS, ICON_OCCUPIED +from .coordinator import SleepIQData +from .entity import SleepIQBedEntity + + +@dataclass +class SleepIQNumberEntityDescriptionMixin: + """Mixin to describe a SleepIQ number entity.""" + + value_fn: Callable[[Any], float] + set_value_fn: Callable[[Any, int], Coroutine[None, None, None]] + get_name_fn: Callable[[SleepIQBed, Any], str] + get_unique_id_fn: Callable[[SleepIQBed, Any], str] + + +@dataclass +class SleepIQNumberEntityDescription( + NumberEntityDescription, SleepIQNumberEntityDescriptionMixin +): + """Class to describe a SleepIQ number entity.""" + + +async def _async_set_firmness(sleeper: SleepIQSleeper, firmness: int) -> None: + await sleeper.set_sleepnumber(firmness) + + +async def _async_set_actuator_position( + actuator: SleepIQActuator, position: int +) -> None: + await actuator.set_position(position) + + +def _get_actuator_name(bed: SleepIQBed, actuator: SleepIQActuator) -> str: + if actuator.side: + return f"SleepNumber {bed.name} {actuator.side_full} {actuator.actuator_full} {ENTITY_TYPES[ACTUATOR]}" + + return f"SleepNumber {bed.name} {actuator.actuator_full} {ENTITY_TYPES[ACTUATOR]}" + + +def _get_actuator_unique_id(bed: SleepIQBed, actuator: SleepIQActuator) -> str: + if actuator.side: + return f"{bed.id}_{actuator.side}_{actuator.actuator}" + + return f"{bed.id}_{actuator.actuator}" + + +def _get_sleeper_name(bed: SleepIQBed, sleeper: SleepIQSleeper) -> str: + return f"SleepNumber {bed.name} {sleeper.name} {ENTITY_TYPES[FIRMNESS]}" + + +def _get_sleeper_unique_id(bed: SleepIQBed, sleeper: SleepIQSleeper) -> str: + return f"{sleeper.sleeper_id}_{FIRMNESS}" + + +NUMBER_DESCRIPTIONS: dict[str, SleepIQNumberEntityDescription] = { + FIRMNESS: SleepIQNumberEntityDescription( + key=FIRMNESS, + min_value=5, + max_value=100, + step=5, + name=ENTITY_TYPES[FIRMNESS], + icon=ICON_OCCUPIED, + value_fn=lambda sleeper: cast(float, sleeper.sleep_number), + set_value_fn=_async_set_firmness, + get_name_fn=_get_sleeper_name, + get_unique_id_fn=_get_sleeper_unique_id, + ), + ACTUATOR: SleepIQNumberEntityDescription( + key=ACTUATOR, + min_value=0, + max_value=100, + step=1, + name=ENTITY_TYPES[ACTUATOR], + icon=ICON_OCCUPIED, + value_fn=lambda actuator: cast(float, actuator.position), + set_value_fn=_async_set_actuator_position, + get_name_fn=_get_actuator_name, + get_unique_id_fn=_get_actuator_unique_id, + ), +} + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the SleepIQ bed sensors.""" + data: SleepIQData = hass.data[DOMAIN][entry.entry_id] + + entities = [] + for bed in data.client.beds.values(): + for sleeper in bed.sleepers: + entities.append( + SleepIQNumberEntity( + data.data_coordinator, + bed, + sleeper, + NUMBER_DESCRIPTIONS[FIRMNESS], + ) + ) + for actuator in bed.foundation.actuators: + entities.append( + SleepIQNumberEntity( + data.data_coordinator, + bed, + actuator, + NUMBER_DESCRIPTIONS[ACTUATOR], + ) + ) + + async_add_entities(entities) + + +class SleepIQNumberEntity(SleepIQBedEntity, NumberEntity): + """Representation of a SleepIQ number entity.""" + + _attr_icon = "mdi:bed" + + def __init__( + self, + coordinator: DataUpdateCoordinator, + bed: SleepIQBed, + device: Any, + description: SleepIQNumberEntityDescription, + ) -> None: + """Initialize the number.""" + self.description = description + self.device = device + + self._attr_name = description.get_name_fn(bed, device) + self._attr_unique_id = description.get_unique_id_fn(bed, device) + + super().__init__(coordinator, bed) + + @callback + def _async_update_attrs(self) -> None: + """Update number attributes.""" + self._attr_value = float(self.description.value_fn(self.device)) + + async def async_set_value(self, value: float) -> None: + """Set the number value.""" + await self.description.set_value_fn(self.device, int(value)) + self._attr_value = value + self.async_write_ha_state() diff --git a/homeassistant/components/sleepiq/select.py b/homeassistant/components/sleepiq/select.py new file mode 100644 index 00000000000..0cbf1671e2b --- /dev/null +++ b/homeassistant/components/sleepiq/select.py @@ -0,0 +1,60 @@ +"""Support for SleepIQ foundation preset selection.""" +from __future__ import annotations + +from asyncsleepiq import BED_PRESETS, Side, SleepIQBed, SleepIQPreset + +from homeassistant.components.select import SelectEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from .const import DOMAIN +from .coordinator import SleepIQData +from .entity import SleepIQBedEntity + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the SleepIQ foundation preset select entities.""" + data: SleepIQData = hass.data[DOMAIN][entry.entry_id] + async_add_entities( + SleepIQSelectEntity(data.data_coordinator, bed, preset) + for bed in data.client.beds.values() + for preset in bed.foundation.presets + ) + + +class SleepIQSelectEntity(SleepIQBedEntity, SelectEntity): + """Representation of a SleepIQ select entity.""" + + _attr_options = list(BED_PRESETS) + + def __init__( + self, coordinator: DataUpdateCoordinator, bed: SleepIQBed, preset: SleepIQPreset + ) -> None: + """Initialize the select entity.""" + self.preset = preset + + self._attr_name = f"SleepNumber {bed.name} Foundation Preset" + self._attr_unique_id = f"{bed.id}_preset" + if preset.side != Side.NONE: + self._attr_name += f" {preset.side_full}" + self._attr_unique_id += f"_{preset.side}" + + super().__init__(coordinator, bed) + self._async_update_attrs() + + @callback + def _async_update_attrs(self) -> None: + """Update entity attributes.""" + self._attr_current_option = self.preset.preset + + async def async_select_option(self, option: str) -> None: + """Change the current preset.""" + await self.preset.set_preset(option) + self._attr_current_option = option + self.async_write_ha_state() diff --git a/homeassistant/components/sleepiq/sensor.py b/homeassistant/components/sleepiq/sensor.py index 7d50876b1b2..b101aee8a6e 100644 --- a/homeassistant/components/sleepiq/sensor.py +++ b/homeassistant/components/sleepiq/sensor.py @@ -9,9 +9,11 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from .const import DOMAIN, SLEEP_NUMBER +from .const import DOMAIN, PRESSURE, SLEEP_NUMBER from .coordinator import SleepIQData -from .entity import SleepIQSensor +from .entity import SleepIQSleeperEntity + +SENSORS = [PRESSURE, SLEEP_NUMBER] async def async_setup_entry( @@ -22,13 +24,14 @@ async def async_setup_entry( """Set up the SleepIQ bed sensors.""" data: SleepIQData = hass.data[DOMAIN][entry.entry_id] async_add_entities( - SleepNumberSensorEntity(data.data_coordinator, bed, sleeper) + SleepIQSensorEntity(data.data_coordinator, bed, sleeper, sensor_type) for bed in data.client.beds.values() for sleeper in bed.sleepers + for sensor_type in SENSORS ) -class SleepNumberSensorEntity(SleepIQSensor, SensorEntity): +class SleepIQSensorEntity(SleepIQSleeperEntity, SensorEntity): """Representation of an SleepIQ Entity with CoordinatorEntity.""" _attr_icon = "mdi:bed" @@ -38,11 +41,13 @@ class SleepNumberSensorEntity(SleepIQSensor, SensorEntity): coordinator: DataUpdateCoordinator, bed: SleepIQBed, sleeper: SleepIQSleeper, + sensor_type: str, ) -> None: """Initialize the sensor.""" - super().__init__(coordinator, bed, sleeper, SLEEP_NUMBER) + self.sensor_type = sensor_type + super().__init__(coordinator, bed, sleeper, sensor_type) @callback def _async_update_attrs(self) -> None: """Update sensor attributes.""" - self._attr_native_value = self.sleeper.sleep_number + self._attr_native_value = getattr(self.sleeper, self.sensor_type) diff --git a/homeassistant/components/sleepiq/strings.json b/homeassistant/components/sleepiq/strings.json index 21ceead3d0a..7a9a4c58464 100644 --- a/homeassistant/components/sleepiq/strings.json +++ b/homeassistant/components/sleepiq/strings.json @@ -1,19 +1,27 @@ { - "config": { - "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_account%]" - }, - "error": { - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", - "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" - }, - "step": { - "user": { - "data": { - "password": "[%key:common::config_flow::data::password%]", - "username": "[%key:common::config_flow::data::username%]" - } - } + "config": { + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" + }, + "step": { + "user": { + "data": { + "password": "[%key:common::config_flow::data::password%]", + "username": "[%key:common::config_flow::data::username%]" } + }, + "reauth_confirm": { + "title": "[%key:common::config_flow::title::reauth%]", + "description": "The SleepIQ integration needs to re-authenticate your account {username}.", + "data": { + "password": "[%key:common::config_flow::data::password%]" + } + } } + } } diff --git a/homeassistant/components/sleepiq/switch.py b/homeassistant/components/sleepiq/switch.py index c8977f0ce73..ebc0f720b43 100644 --- a/homeassistant/components/sleepiq/switch.py +++ b/homeassistant/components/sleepiq/switch.py @@ -7,12 +7,12 @@ from asyncsleepiq import SleepIQBed from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN from .coordinator import SleepIQData, SleepIQPauseUpdateCoordinator -from .entity import SleepIQBedCoordinator +from .entity import SleepIQBedEntity async def async_setup_entry( @@ -28,7 +28,7 @@ async def async_setup_entry( ) -class SleepNumberPrivateSwitch(SleepIQBedCoordinator, SwitchEntity): +class SleepNumberPrivateSwitch(SleepIQBedEntity, SwitchEntity): """Representation of SleepIQ privacy mode.""" def __init__( @@ -39,15 +39,17 @@ class SleepNumberPrivateSwitch(SleepIQBedCoordinator, SwitchEntity): self._attr_name = f"SleepNumber {bed.name} Pause Mode" self._attr_unique_id = f"{bed.id}-pause-mode" - @property - def is_on(self) -> bool: - """Return whether the switch is on or off.""" - return bool(self.bed.paused) - async def async_turn_on(self, **kwargs: Any) -> None: """Turn on switch.""" await self.bed.set_pause_mode(True) + self._handle_coordinator_update() async def async_turn_off(self, **kwargs: Any) -> None: """Turn off switch.""" await self.bed.set_pause_mode(False) + self._handle_coordinator_update() + + @callback + def _async_update_attrs(self) -> None: + """Update switch attributes.""" + self._attr_is_on = self.bed.paused diff --git a/homeassistant/components/sleepiq/translations/bg.json b/homeassistant/components/sleepiq/translations/bg.json index b8fb3b61a77..e1494bd66a1 100644 --- a/homeassistant/components/sleepiq/translations/bg.json +++ b/homeassistant/components/sleepiq/translations/bg.json @@ -1,13 +1,20 @@ { "config": { "abort": { - "already_configured": "\u0410\u043a\u0430\u0443\u043d\u0442\u044a\u0442 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d" + "already_configured": "\u0410\u043a\u0430\u0443\u043d\u0442\u044a\u0442 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" }, "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435" }, "step": { + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430" + }, + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u043d\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430" + }, "user": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u0430", diff --git a/homeassistant/components/sleepiq/translations/ca.json b/homeassistant/components/sleepiq/translations/ca.json index 9c37f37a0ef..4b42b0411e9 100644 --- a/homeassistant/components/sleepiq/translations/ca.json +++ b/homeassistant/components/sleepiq/translations/ca.json @@ -1,13 +1,21 @@ { "config": { "abort": { - "already_configured": "El compte ja est\u00e0 configurat" + "already_configured": "El compte ja est\u00e0 configurat", + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3", "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida" }, "step": { + "reauth_confirm": { + "data": { + "password": "Contrasenya" + }, + "description": "La integraci\u00f3 SleepIQ ha de tornar a autenticar-se amb el teu compte {username}.", + "title": "Reautenticaci\u00f3 de la integraci\u00f3" + }, "user": { "data": { "password": "Contrasenya", diff --git a/homeassistant/components/sleepiq/translations/de.json b/homeassistant/components/sleepiq/translations/de.json index 12a870b4cc9..280b0bf2ba7 100644 --- a/homeassistant/components/sleepiq/translations/de.json +++ b/homeassistant/components/sleepiq/translations/de.json @@ -1,13 +1,21 @@ { "config": { "abort": { - "already_configured": "Konto wurde bereits konfiguriert" + "already_configured": "Konto wurde bereits konfiguriert", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich" }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", "invalid_auth": "Ung\u00fcltige Authentifizierung" }, "step": { + "reauth_confirm": { + "data": { + "password": "Passwort" + }, + "description": "Die SleepIQ-Integration muss dein Konto {username} erneut authentifizieren.", + "title": "Integration erneut authentifizieren" + }, "user": { "data": { "password": "Passwort", diff --git a/homeassistant/components/sleepiq/translations/el.json b/homeassistant/components/sleepiq/translations/el.json index 45b5ef57ba5..fff470c0ec1 100644 --- a/homeassistant/components/sleepiq/translations/el.json +++ b/homeassistant/components/sleepiq/translations/el.json @@ -1,13 +1,21 @@ { "config": { "abort": { - "already_configured": "\u039f \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc\u03c2 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af" + "already_configured": "\u039f \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc\u03c2 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", + "reauth_successful": "\u039f \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03ae\u03c4\u03b1\u03bd \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2" }, "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\u03bf\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2" }, "step": { + "reauth_confirm": { + "data": { + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2" + }, + "description": "\u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 SleepIQ \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03bb\u03ad\u03b3\u03be\u03b5\u03b9 \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03c4\u03b7\u03bd \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1 \u03c4\u03bf\u03c5 \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03bf\u03cd \u03c3\u03b1\u03c2 {username}.", + "title": "\u0395\u03c0\u03b1\u03bd\u03b1\u03bb\u03b7\u03c0\u03c4\u03b9\u03ba\u03cc\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2" + }, "user": { "data": { "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", diff --git a/homeassistant/components/sleepiq/translations/en.json b/homeassistant/components/sleepiq/translations/en.json index 31de29c8690..fcefeace3c8 100644 --- a/homeassistant/components/sleepiq/translations/en.json +++ b/homeassistant/components/sleepiq/translations/en.json @@ -1,13 +1,21 @@ { "config": { "abort": { - "already_configured": "Account is already configured" + "already_configured": "Account is already configured", + "reauth_successful": "Re-authentication was successful" }, "error": { "cannot_connect": "Failed to connect", "invalid_auth": "Invalid authentication" }, "step": { + "reauth_confirm": { + "data": { + "password": "Password" + }, + "description": "The SleepIQ integration needs to re-authenticate your account {username}.", + "title": "Reauthenticate Integration" + }, "user": { "data": { "password": "Password", diff --git a/homeassistant/components/sleepiq/translations/es.json b/homeassistant/components/sleepiq/translations/es.json index 6b8cd4ac642..ae6059aa227 100644 --- a/homeassistant/components/sleepiq/translations/es.json +++ b/homeassistant/components/sleepiq/translations/es.json @@ -1,10 +1,18 @@ { "config": { + "abort": { + "already_configured": "La cuenta ya est\u00e1 configurada" + }, "error": { "cannot_connect": "Error al conectar", "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida" }, "step": { + "reauth_confirm": { + "data": { + "password": "Contrase\u00f1a" + } + }, "user": { "data": { "password": "Contrase\u00f1a", diff --git a/homeassistant/components/sleepiq/translations/et.json b/homeassistant/components/sleepiq/translations/et.json index db09683450a..89cc0a85714 100644 --- a/homeassistant/components/sleepiq/translations/et.json +++ b/homeassistant/components/sleepiq/translations/et.json @@ -1,13 +1,21 @@ { "config": { "abort": { - "already_configured": "Konto on juba seadistatud" + "already_configured": "Konto on juba seadistatud", + "reauth_successful": "Taastuvastamine \u00f5nnestus" }, "error": { "cannot_connect": "\u00dchendamine nurjus", "invalid_auth": "Tuvastamine nurjus" }, "step": { + "reauth_confirm": { + "data": { + "password": "Salas\u00f5na" + }, + "description": "SleepIQ sidumine peab konto {username} uuesti autentima.", + "title": "Taastuvasta sidumine" + }, "user": { "data": { "password": "Salas\u00f5na", diff --git a/homeassistant/components/sleepiq/translations/fr.json b/homeassistant/components/sleepiq/translations/fr.json new file mode 100644 index 00000000000..9c43b00feba --- /dev/null +++ b/homeassistant/components/sleepiq/translations/fr.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9", + "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "invalid_auth": "Authentification non valide" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Mot de passe" + }, + "title": "R\u00e9-authentifier l'int\u00e9gration" + }, + "user": { + "data": { + "password": "Mot de passe", + "username": "Nom d'utilisateur" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sleepiq/translations/he.json b/homeassistant/components/sleepiq/translations/he.json index 49f37a267d0..33d3b9421f1 100644 --- a/homeassistant/components/sleepiq/translations/he.json +++ b/homeassistant/components/sleepiq/translations/he.json @@ -1,13 +1,20 @@ { "config": { "abort": { - "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7" }, "error": { "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" }, "step": { + "reauth_confirm": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4" + }, + "title": "\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05e9\u05dc \u05e9\u05d9\u05dc\u05d5\u05d1" + }, "user": { "data": { "password": "\u05e1\u05d9\u05e1\u05de\u05d4", diff --git a/homeassistant/components/sleepiq/translations/hu.json b/homeassistant/components/sleepiq/translations/hu.json index c4adcb1bd9e..45caff592a8 100644 --- a/homeassistant/components/sleepiq/translations/hu.json +++ b/homeassistant/components/sleepiq/translations/hu.json @@ -1,13 +1,21 @@ { "config": { "abort": { - "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van" + "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt." }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s" }, "step": { + "reauth_confirm": { + "data": { + "password": "Jelsz\u00f3" + }, + "description": "A SleepIQ-integr\u00e1ci\u00f3nak \u00fajra kell hiteles\u00edtenie fi\u00f3kj\u00e1t: {username}.", + "title": "Integr\u00e1ci\u00f3 \u00fajrahiteles\u00edt\u00e9se" + }, "user": { "data": { "password": "Jelsz\u00f3", diff --git a/homeassistant/components/sleepiq/translations/id.json b/homeassistant/components/sleepiq/translations/id.json index a974f44967e..175f7a37794 100644 --- a/homeassistant/components/sleepiq/translations/id.json +++ b/homeassistant/components/sleepiq/translations/id.json @@ -1,13 +1,21 @@ { "config": { "abort": { - "already_configured": "Akun sudah dikonfigurasi" + "already_configured": "Akun sudah dikonfigurasi", + "reauth_successful": "Autentikasi ulang berhasil" }, "error": { "cannot_connect": "Gagal terhubung", "invalid_auth": "Autentikasi tidak valid" }, "step": { + "reauth_confirm": { + "data": { + "password": "Kata Sandi" + }, + "description": "Integrasi SlepIQ perlu mengautentikasi ulang akun Anda {username}.", + "title": "Autentikasi Ulang Integrasi" + }, "user": { "data": { "password": "Kata Sandi", diff --git a/homeassistant/components/sleepiq/translations/it.json b/homeassistant/components/sleepiq/translations/it.json index 7ae1601843e..e0e0d75cb2a 100644 --- a/homeassistant/components/sleepiq/translations/it.json +++ b/homeassistant/components/sleepiq/translations/it.json @@ -1,13 +1,21 @@ { "config": { "abort": { - "already_configured": "L'account \u00e8 gi\u00e0 configurato" + "already_configured": "L'account \u00e8 gi\u00e0 configurato", + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" }, "error": { "cannot_connect": "Impossibile connettersi", "invalid_auth": "Autenticazione non valida" }, "step": { + "reauth_confirm": { + "data": { + "password": "Password" + }, + "description": "L'integrazione SleepIQ deve autenticare nuovamente il tuo account {username}.", + "title": "Autentica nuovamente l'integrazione" + }, "user": { "data": { "password": "Password", diff --git a/homeassistant/components/sleepiq/translations/ja.json b/homeassistant/components/sleepiq/translations/ja.json index 35b6807586d..b1e0ec1d86e 100644 --- a/homeassistant/components/sleepiq/translations/ja.json +++ b/homeassistant/components/sleepiq/translations/ja.json @@ -1,13 +1,21 @@ { "config": { "abort": { - "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c" }, "step": { + "reauth_confirm": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + }, + "description": "SleepIQ\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u306f\u3001\u30a2\u30ab\u30a6\u30f3\u30c8 {username} \u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002", + "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" + }, "user": { "data": { "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", diff --git a/homeassistant/components/sleepiq/translations/nl.json b/homeassistant/components/sleepiq/translations/nl.json index 3271c6bce45..09029be118e 100644 --- a/homeassistant/components/sleepiq/translations/nl.json +++ b/homeassistant/components/sleepiq/translations/nl.json @@ -1,13 +1,21 @@ { "config": { "abort": { - "already_configured": "Account is al geconfigureerd" + "already_configured": "Account is al geconfigureerd", + "reauth_successful": "Herauthenticatie was succesvol" }, "error": { "cannot_connect": "Kan geen verbinding maken", "invalid_auth": "Ongeldige authenticatie" }, "step": { + "reauth_confirm": { + "data": { + "password": "Wachtwoord" + }, + "description": "De SleepIQ-integratie moet uw account {username} opnieuw verifi\u00ebren.", + "title": "Verifieer de integratie opnieuw" + }, "user": { "data": { "password": "Wachtwoord", diff --git a/homeassistant/components/sleepiq/translations/no.json b/homeassistant/components/sleepiq/translations/no.json index 51f351fb833..ffe74f7048a 100644 --- a/homeassistant/components/sleepiq/translations/no.json +++ b/homeassistant/components/sleepiq/translations/no.json @@ -1,13 +1,21 @@ { "config": { "abort": { - "already_configured": "Kontoen er allerede konfigurert" + "already_configured": "Kontoen er allerede konfigurert", + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" }, "error": { "cannot_connect": "Tilkobling mislyktes", "invalid_auth": "Ugyldig godkjenning" }, "step": { + "reauth_confirm": { + "data": { + "password": "Passord" + }, + "description": "SleepIQ-integrasjonen m\u00e5 autentisere kontoen din {username} p\u00e5 nytt.", + "title": "Godkjenne integrering p\u00e5 nytt" + }, "user": { "data": { "password": "Passord", diff --git a/homeassistant/components/sleepiq/translations/pl.json b/homeassistant/components/sleepiq/translations/pl.json index 49be6d1efde..ae26dfb0167 100644 --- a/homeassistant/components/sleepiq/translations/pl.json +++ b/homeassistant/components/sleepiq/translations/pl.json @@ -1,13 +1,21 @@ { "config": { "abort": { - "already_configured": "Konto jest ju\u017c skonfigurowane" + "already_configured": "Konto jest ju\u017c skonfigurowane", + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "invalid_auth": "Niepoprawne uwierzytelnienie" }, "step": { + "reauth_confirm": { + "data": { + "password": "Has\u0142o" + }, + "description": "Integracja SleepIQ wymaga ponownego uwierzytelnienia Twojego konta {username}.", + "title": "Ponownie uwierzytelnij integracj\u0119" + }, "user": { "data": { "password": "Has\u0142o", diff --git a/homeassistant/components/sleepiq/translations/pt-BR.json b/homeassistant/components/sleepiq/translations/pt-BR.json index 86cf9781d3a..2556c273b95 100644 --- a/homeassistant/components/sleepiq/translations/pt-BR.json +++ b/homeassistant/components/sleepiq/translations/pt-BR.json @@ -1,13 +1,21 @@ { "config": { "abort": { - "already_configured": "A conta j\u00e1 foi configurada" + "already_configured": "A conta j\u00e1 foi configurada", + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" }, "error": { "cannot_connect": "Falha ao conectar", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" }, "step": { + "reauth_confirm": { + "data": { + "password": "Senha" + }, + "description": "A integra\u00e7\u00e3o do SleepIQ precisa autenticar novamente sua conta {username}.", + "title": "Reautenticar Integra\u00e7\u00e3o" + }, "user": { "data": { "password": "Senha", diff --git a/homeassistant/components/sleepiq/translations/ru.json b/homeassistant/components/sleepiq/translations/ru.json index f74355cdc7d..1cf8dfe0e60 100644 --- a/homeassistant/components/sleepiq/translations/ru.json +++ b/homeassistant/components/sleepiq/translations/ru.json @@ -1,13 +1,21 @@ { "config": { "abort": { - "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant." + "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant.", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438." }, "step": { + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + }, + "description": "\u0422\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 SleepIQ {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" + }, "user": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u044c", diff --git a/homeassistant/components/sleepiq/translations/tr.json b/homeassistant/components/sleepiq/translations/tr.json index 153aa4126b0..abc741a6e17 100644 --- a/homeassistant/components/sleepiq/translations/tr.json +++ b/homeassistant/components/sleepiq/translations/tr.json @@ -1,13 +1,21 @@ { "config": { "abort": { - "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" }, "error": { "cannot_connect": "Ba\u011flanma hatas\u0131", "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" }, "step": { + "reauth_confirm": { + "data": { + "password": "Parola" + }, + "description": "SleepIQ entegrasyonunun {username} hesab\u0131n\u0131z\u0131 yeniden do\u011frulamas\u0131 gerekiyor.", + "title": "Entegrasyonu Yeniden Do\u011frula" + }, "user": { "data": { "password": "Parola", diff --git a/homeassistant/components/sleepiq/translations/zh-Hant.json b/homeassistant/components/sleepiq/translations/zh-Hant.json index d93bfe0fa68..193f9a5d96f 100644 --- a/homeassistant/components/sleepiq/translations/zh-Hant.json +++ b/homeassistant/components/sleepiq/translations/zh-Hant.json @@ -1,13 +1,21 @@ { "config": { "abort": { - "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548" }, "step": { + "reauth_confirm": { + "data": { + "password": "\u5bc6\u78bc" + }, + "description": "SleepIQ \u6574\u5408\u9700\u8981\u91cd\u65b0\u8a8d\u8b49\u60a8\u7684\u5e33\u865f {username}\u3002", + "title": "\u91cd\u65b0\u8a8d\u8b49\u6574\u5408" + }, "user": { "data": { "password": "\u5bc6\u78bc", diff --git a/homeassistant/components/sma/config_flow.py b/homeassistant/components/sma/config_flow.py index c2fd48a00ca..070610d6ae2 100644 --- a/homeassistant/components/sma/config_flow.py +++ b/homeassistant/components/sma/config_flow.py @@ -78,7 +78,7 @@ class SmaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if not errors: await self.async_set_unique_id(device_info["serial"]) - self._abort_if_unique_id_configured() + self._abort_if_unique_id_configured(updates=self._data) return self.async_create_entry( title=self._data[CONF_HOST], data=self._data ) diff --git a/homeassistant/components/sma/manifest.json b/homeassistant/components/sma/manifest.json index 308c11f91a1..6438c3a5777 100644 --- a/homeassistant/components/sma/manifest.json +++ b/homeassistant/components/sma/manifest.json @@ -3,7 +3,7 @@ "name": "SMA Solar", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sma", - "requirements": ["pysma==0.6.10"], + "requirements": ["pysma==0.6.11"], "codeowners": ["@kellerza", "@rklomp"], "iot_class": "local_polling", "loggers": ["pysma"] diff --git a/homeassistant/components/sma/translations/fr.json b/homeassistant/components/sma/translations/fr.json index 46b3be072f7..c52ca19fafc 100644 --- a/homeassistant/components/sma/translations/fr.json +++ b/homeassistant/components/sma/translations/fr.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "\u00c9chec de connexion", "cannot_retrieve_device_info": "Connexion r\u00e9ussie, mais impossible de r\u00e9cup\u00e9rer les informations sur l'appareil", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/smappee/manifest.json b/homeassistant/components/smappee/manifest.json index f27ec29996e..b9f94b94dde 100644 --- a/homeassistant/components/smappee/manifest.json +++ b/homeassistant/components/smappee/manifest.json @@ -4,12 +4,8 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/smappee", "dependencies": ["http"], - "requirements": [ - "pysmappee==0.2.29" - ], - "codeowners": [ - "@bsmappee" - ], + "requirements": ["pysmappee==0.2.29"], + "codeowners": ["@bsmappee"], "zeroconf": [ { "type": "_ssh._tcp.local.", diff --git a/homeassistant/components/smappee/translations/fr.json b/homeassistant/components/smappee/translations/fr.json index ba57f8c36c4..d43b5382de8 100644 --- a/homeassistant/components/smappee/translations/fr.json +++ b/homeassistant/components/smappee/translations/fr.json @@ -3,11 +3,11 @@ "abort": { "already_configured_device": "L'appareil est d\u00e9j\u00e0 configur\u00e9", "already_configured_local_device": "Le ou les p\u00e9riph\u00e9riques locaux sont d\u00e9j\u00e0 configur\u00e9s. Veuillez les supprimer avant de configurer un appareil cloud.", - "authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration de l'URL d'authentification d\u00e9pass\u00e9.", + "authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration de l'URL d'authentification expir\u00e9.", "cannot_connect": "\u00c9chec de connexion", "invalid_mdns": "Appareil non pris en charge pour l'int\u00e9gration Smappee.", "missing_configuration": "Le composant n'est pas configur\u00e9. Veuillez suivre la documentation.", - "no_url_available": "Aucune URL disponible. Pour plus d'informations sur cette erreur, [consultez la section d'aide] ( {docs_url} )" + "no_url_available": "Aucune URL disponible. Pour plus d'informations sur cette erreur, [consultez la section d'aide]({docs_url})" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/smappee/translations/zh-Hant.json b/homeassistant/components/smappee/translations/zh-Hant.json index 530dddbb7a4..98c8deb1ac3 100644 --- a/homeassistant/components/smappee/translations/zh-Hant.json +++ b/homeassistant/components/smappee/translations/zh-Hant.json @@ -28,7 +28,7 @@ }, "zeroconf_confirm": { "description": "\u662f\u5426\u8981\u5c07\u5e8f\u865f\u70ba `{serial_number}` \u4e4b Smappee \u88dd\u7f6e\u65b0\u589e\u81f3 Home Assistant\uff1f", - "title": "\u81ea\u52d5\u63a2\u7d22\u5230 Smappee \u88dd\u7f6e" + "title": "\u6240\u767c\u73fe\u7684 Smappee \u88dd\u7f6e" } } } diff --git a/homeassistant/components/smart_meter_texas/translations/fr.json b/homeassistant/components/smart_meter_texas/translations/fr.json index aa84ec33d8c..744b9c6a862 100644 --- a/homeassistant/components/smart_meter_texas/translations/fr.json +++ b/homeassistant/components/smart_meter_texas/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/smarthab/__init__.py b/homeassistant/components/smarthab/__init__.py deleted file mode 100644 index a4dc1a43f31..00000000000 --- a/homeassistant/components/smarthab/__init__.py +++ /dev/null @@ -1,85 +0,0 @@ -"""Support for SmartHab device integration.""" -import logging - -import pysmarthab -import voluptuous as vol - -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, Platform -from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.typing import ConfigType - -DOMAIN = "smarthab" -DATA_HUB = "hub" -PLATFORMS = [Platform.LIGHT, Platform.COVER] - -_LOGGER = logging.getLogger(__name__) - -CONFIG_SCHEMA = vol.Schema( - vol.All( - cv.deprecated(DOMAIN), - { - DOMAIN: vol.Schema( - { - vol.Required(CONF_EMAIL): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - } - ) - }, - ), - extra=vol.ALLOW_EXTRA, -) - - -async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up the SmartHab platform.""" - - hass.data.setdefault(DOMAIN, {}) - - if DOMAIN not in config: - return True - - if not hass.config_entries.async_entries(DOMAIN): - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data=config[DOMAIN], - ) - ) - - return True - - -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Set up config entry for SmartHab integration.""" - - # Assign configuration variables - username = entry.data[CONF_EMAIL] - password = entry.data[CONF_PASSWORD] - - # Setup connection with SmartHab API - hub = pysmarthab.SmartHab() - - try: - await hub.async_login(username, password) - except pysmarthab.RequestFailedException as err: - _LOGGER.exception("Error while trying to reach SmartHab API") - raise ConfigEntryNotReady from err - - # Pass hub object to child platforms - hass.data[DOMAIN][entry.entry_id] = {DATA_HUB: hub} - - hass.config_entries.async_setup_platforms(entry, PLATFORMS) - - return True - - -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Unload config entry from SmartHab integration.""" - unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - if unload_ok: - hass.data[DOMAIN].pop(entry.entry_id) - return unload_ok diff --git a/homeassistant/components/smarthab/config_flow.py b/homeassistant/components/smarthab/config_flow.py deleted file mode 100644 index 826454ab4d8..00000000000 --- a/homeassistant/components/smarthab/config_flow.py +++ /dev/null @@ -1,78 +0,0 @@ -"""SmartHab configuration flow.""" -import logging - -import pysmarthab -import voluptuous as vol - -from homeassistant import config_entries -from homeassistant.const import CONF_EMAIL, CONF_PASSWORD - -from . import DOMAIN - -_LOGGER = logging.getLogger(__name__) - - -class SmartHabConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): - """SmartHab config flow.""" - - VERSION = 1 - - 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_EMAIL, default=user_input.get(CONF_EMAIL, "") - ): str, - vol.Required(CONF_PASSWORD): str, - } - ), - errors=errors or {}, - ) - - async def async_step_user(self, user_input=None): - """Handle a flow initiated by the user.""" - errors = {} - - if user_input is None: - return self._show_setup_form(user_input, None) - - username = user_input[CONF_EMAIL] - password = user_input[CONF_PASSWORD] - - # Check if already configured - if self.unique_id is None: - await self.async_set_unique_id(username) - self._abort_if_unique_id_configured() - - # Setup connection with SmartHab API - hub = pysmarthab.SmartHab() - - try: - await hub.async_login(username, password) - - # Verify that passed in configuration works - if hub.is_logged_in(): - return self.async_create_entry( - title=username, data={CONF_EMAIL: username, CONF_PASSWORD: password} - ) - - errors["base"] = "invalid_auth" - except pysmarthab.RequestFailedException: - _LOGGER.exception("Error while trying to reach SmartHab API") - errors["base"] = "service" - except Exception: # pylint: disable=broad-except - _LOGGER.exception("Unexpected error during login") - errors["base"] = "unknown" - - return self._show_setup_form(user_input, errors) - - async def async_step_import(self, import_info): - """Handle import from legacy config.""" - return await self.async_step_user(import_info) diff --git a/homeassistant/components/smarthab/cover.py b/homeassistant/components/smarthab/cover.py deleted file mode 100644 index 3c581774b03..00000000000 --- a/homeassistant/components/smarthab/cover.py +++ /dev/null @@ -1,100 +0,0 @@ -"""Support for SmartHab device integration.""" -from datetime import timedelta -import logging - -import pysmarthab -from requests.exceptions import Timeout - -from homeassistant.components.cover import ( - ATTR_POSITION, - SUPPORT_CLOSE, - SUPPORT_OPEN, - SUPPORT_SET_POSITION, - CoverDeviceClass, - CoverEntity, -) -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_platform import AddEntitiesCallback - -from . import DATA_HUB, DOMAIN - -_LOGGER = logging.getLogger(__name__) - -SCAN_INTERVAL = timedelta(seconds=60) - - -async def async_setup_entry( - hass: HomeAssistant, - config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up SmartHab covers from a config entry.""" - hub = hass.data[DOMAIN][config_entry.entry_id][DATA_HUB] - - entities = ( - SmartHabCover(cover) - for cover in await hub.async_get_device_list() - if isinstance(cover, pysmarthab.Shutter) - ) - - async_add_entities(entities, True) - - -class SmartHabCover(CoverEntity): - """Representation a cover.""" - - _attr_device_class = CoverDeviceClass.WINDOW - - def __init__(self, cover): - """Initialize a SmartHabCover.""" - self._cover = cover - - @property - def unique_id(self) -> str: - """Return a unique ID.""" - return self._cover.device_id - - @property - def name(self) -> str: - """Return the display name of this cover.""" - return self._cover.label - - @property - def current_cover_position(self) -> int: - """Return current position of cover. - - None is unknown, 0 is closed, 100 is fully open. - """ - return self._cover.state - - @property - def supported_features(self) -> int: - """Flag supported features.""" - return SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_SET_POSITION - - @property - def is_closed(self) -> bool: - """Return if the cover is closed or not.""" - return self._cover.state == 0 - - async def async_open_cover(self, **kwargs): - """Open the cover.""" - await self._cover.async_open() - - async def async_close_cover(self, **kwargs): - """Close cover.""" - await self._cover.async_close() - - async def async_set_cover_position(self, **kwargs): - """Move the cover to a specific position.""" - await self._cover.async_set_state(kwargs[ATTR_POSITION]) - - async def async_update(self): - """Fetch new state data for this cover.""" - try: - await self._cover.async_update() - except Timeout: - _LOGGER.error( - "Reached timeout while updating cover %s from API", self.entity_id - ) diff --git a/homeassistant/components/smarthab/light.py b/homeassistant/components/smarthab/light.py deleted file mode 100644 index 9fcc952b24a..00000000000 --- a/homeassistant/components/smarthab/light.py +++ /dev/null @@ -1,74 +0,0 @@ -"""Support for SmartHab device integration.""" -from datetime import timedelta -import logging - -import pysmarthab -from requests.exceptions import Timeout - -from homeassistant.components.light import LightEntity -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_platform import AddEntitiesCallback - -from . import DATA_HUB, DOMAIN - -_LOGGER = logging.getLogger(__name__) - -SCAN_INTERVAL = timedelta(seconds=60) - - -async def async_setup_entry( - hass: HomeAssistant, - config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up SmartHab lights from a config entry.""" - hub = hass.data[DOMAIN][config_entry.entry_id][DATA_HUB] - - entities = ( - SmartHabLight(light) - for light in await hub.async_get_device_list() - if isinstance(light, pysmarthab.Light) - ) - - async_add_entities(entities, True) - - -class SmartHabLight(LightEntity): - """Representation of a SmartHab Light.""" - - def __init__(self, light): - """Initialize a SmartHabLight.""" - self._light = light - - @property - def unique_id(self) -> str: - """Return a unique ID.""" - return self._light.device_id - - @property - def name(self) -> str: - """Return the display name of this light.""" - return self._light.label - - @property - def is_on(self) -> bool: - """Return true if light is on.""" - return self._light.state - - async def async_turn_on(self, **kwargs): - """Instruct the light to turn on.""" - await self._light.async_turn_on() - - async def async_turn_off(self, **kwargs): - """Instruct the light to turn off.""" - await self._light.async_turn_off() - - async def async_update(self): - """Fetch new state data for this light.""" - try: - await self._light.async_update() - except Timeout: - _LOGGER.error( - "Reached timeout while updating light %s from API", self.entity_id - ) diff --git a/homeassistant/components/smarthab/manifest.json b/homeassistant/components/smarthab/manifest.json deleted file mode 100644 index 7974215de64..00000000000 --- a/homeassistant/components/smarthab/manifest.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "domain": "smarthab", - "name": "SmartHab", - "documentation": "https://www.home-assistant.io/integrations/smarthab", - "config_flow": true, - "requirements": ["smarthab==0.21"], - "codeowners": ["@outadoc"], - "iot_class": "cloud_polling", - "loggers": ["pysmarthab"] -} diff --git a/homeassistant/components/smarthab/strings.json b/homeassistant/components/smarthab/strings.json deleted file mode 100644 index e1cb6eb4411..00000000000 --- a/homeassistant/components/smarthab/strings.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "error": { - "service": "Error while trying to reach SmartHab. Service might be down. Check your connection.", - "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", - "unknown": "[%key:common::config_flow::error::unknown%]" - }, - "step": { - "user": { - "data": { - "password": "[%key:common::config_flow::data::password%]", - "email": "[%key:common::config_flow::data::email%]" - }, - "description": "For technical reasons, be sure to use a secondary account specific to your Home Assistant setup. You can create one from the SmartHab application.", - "title": "Setup SmartHab" - } - } - } -} diff --git a/homeassistant/components/smarthab/translations/bg.json b/homeassistant/components/smarthab/translations/bg.json deleted file mode 100644 index 75022ed3005..00000000000 --- a/homeassistant/components/smarthab/translations/bg.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "config": { - "step": { - "user": { - "data": { - "email": "Email", - "password": "\u041f\u0430\u0440\u043e\u043b\u0430" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/smarthab/translations/ca.json b/homeassistant/components/smarthab/translations/ca.json deleted file mode 100644 index cac81a0acea..00000000000 --- a/homeassistant/components/smarthab/translations/ca.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "error": { - "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", - "service": "Error en l'intent de connexi\u00f3 a SmartHab. Pot ser que el servei no estigui disponible. Comprova la connexi\u00f3.", - "unknown": "Error inesperat" - }, - "step": { - "user": { - "data": { - "email": "Correu electr\u00f2nic", - "password": "Contrasenya" - }, - "description": "Per motius t\u00e8cnics, assegura't utilitzar un compte secundari espec\u00edfic per a la configuraci\u00f3 de Home Assistant. Pots crear-ne un des de l'aplicaci\u00f3 SmartHab.", - "title": "Configuraci\u00f3 de SmartHab" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/smarthab/translations/cs.json b/homeassistant/components/smarthab/translations/cs.json deleted file mode 100644 index 1e862ff0069..00000000000 --- a/homeassistant/components/smarthab/translations/cs.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "config": { - "error": { - "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", - "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" - }, - "step": { - "user": { - "data": { - "email": "E-mail", - "password": "Heslo" - }, - "title": "Nastaven\u00ed SmartHab" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/smarthab/translations/de.json b/homeassistant/components/smarthab/translations/de.json deleted file mode 100644 index ca2bf3373f2..00000000000 --- a/homeassistant/components/smarthab/translations/de.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "error": { - "invalid_auth": "Ung\u00fcltige Authentifizierung", - "service": "Fehler beim Versuch, SmartHab zu erreichen. Der Dienst ist m\u00f6glicherweise nicht erreichbar. Pr\u00fcfe deine Verbindung.", - "unknown": "Unerwarteter Fehler" - }, - "step": { - "user": { - "data": { - "email": "E-Mail", - "password": "Passwort" - }, - "description": "Stelle aus technischen Gr\u00fcnden sicher, dass du ein sekund\u00e4res Konto speziell f\u00fcr deine Home Assistant-Einrichtung verwendest. Du kannst ein solches Konto \u00fcber die SmartHab-Anwendung erstellen.", - "title": "SmartHab einrichten" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/smarthab/translations/el.json b/homeassistant/components/smarthab/translations/el.json deleted file mode 100644 index 43de6c1ca89..00000000000 --- a/homeassistant/components/smarthab/translations/el.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "error": { - "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2", - "service": "\u03a3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03c0\u03c1\u03bf\u03c3\u03c0\u03ac\u03b8\u03b5\u03b9\u03b1 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03c3\u03c4\u03bf SmartHab. \u0397 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03ba\u03c4\u03cc\u03c2 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1\u03c2. \u0395\u03bb\u03ad\u03b3\u03be\u03c4\u03b5 \u03c4\u03b7 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03ae \u03c3\u03b1\u03c2.", - "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" - }, - "step": { - "user": { - "data": { - "email": "Email", - "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2" - }, - "description": "\u0393\u03b9\u03b1 \u03c4\u03b5\u03c7\u03bd\u03b9\u03ba\u03bf\u03cd\u03c2 \u03bb\u03cc\u03b3\u03bf\u03c5\u03c2, \u03b2\u03b5\u03b2\u03b1\u03b9\u03c9\u03b8\u03b5\u03af\u03c4\u03b5 \u03cc\u03c4\u03b9 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b5 \u03ad\u03bd\u03b1\u03bd \u03b4\u03b5\u03c5\u03c4\u03b5\u03c1\u03b5\u03cd\u03bf\u03bd\u03c4\u03b1 \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc \u03b5\u03b9\u03b4\u03b9\u03ba\u03ac \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c4\u03bf\u03c5 Home Assistant. \u039c\u03c0\u03bf\u03c1\u03b5\u03af\u03c4\u03b5 \u03bd\u03b1 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03ae\u03c3\u03b5\u03c4\u03b5 \u03ad\u03bd\u03b1\u03bd \u03b1\u03c0\u03cc \u03c4\u03b7\u03bd \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae SmartHab.", - "title": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 SmartHab" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/smarthab/translations/en.json b/homeassistant/components/smarthab/translations/en.json deleted file mode 100644 index 854f7a7ddb5..00000000000 --- a/homeassistant/components/smarthab/translations/en.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "error": { - "invalid_auth": "Invalid authentication", - "service": "Error while trying to reach SmartHab. Service might be down. Check your connection.", - "unknown": "Unexpected error" - }, - "step": { - "user": { - "data": { - "email": "Email", - "password": "Password" - }, - "description": "For technical reasons, be sure to use a secondary account specific to your Home Assistant setup. You can create one from the SmartHab application.", - "title": "Setup SmartHab" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/smarthab/translations/es.json b/homeassistant/components/smarthab/translations/es.json deleted file mode 100644 index b111ddbccd1..00000000000 --- a/homeassistant/components/smarthab/translations/es.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "error": { - "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", - "service": "Error al intentar contactar con SmartHab. El servicio podr\u00eda estar ca\u00eddo. Verifica tu conexi\u00f3n.", - "unknown": "Error inesperado" - }, - "step": { - "user": { - "data": { - "email": "Correo electr\u00f3nico", - "password": "Contrase\u00f1a" - }, - "description": "Por razones t\u00e9cnicas, aseg\u00farate de usar una cuenta secundaria espec\u00edfica para su configuraci\u00f3n de Home Assistant. Puedes crear una desde la aplicaci\u00f3n SmartHab.", - "title": "Configurar SmartHab" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/smarthab/translations/et.json b/homeassistant/components/smarthab/translations/et.json deleted file mode 100644 index f7c91f8dce6..00000000000 --- a/homeassistant/components/smarthab/translations/et.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "error": { - "invalid_auth": "Tuvastamise viga", - "service": "Viga SmartHabiga \u00fchendumisel. Teenus v\u00f5ib olla h\u00e4iritud. Kontrolli oma \u00fchendust.", - "unknown": "Tundmatu viga" - }, - "step": { - "user": { - "data": { - "email": "E-post", - "password": "Salas\u00f5na" - }, - "description": "Tehnilistel p\u00f5hjustel kasuta kindlasti oma Home Assistanti seadistustele vastavat sekundaarset kontot. Selle saad luua rakendusest SmartHab.", - "title": "Seadista SmartHab" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/smarthab/translations/fr.json b/homeassistant/components/smarthab/translations/fr.json deleted file mode 100644 index efbbfe25818..00000000000 --- a/homeassistant/components/smarthab/translations/fr.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "error": { - "invalid_auth": "Authentification invalide", - "service": "Erreur de connexion \u00e0 SmartHab. V\u00e9rifiez votre connexion. Le service peut \u00eatre indisponible.", - "unknown": "Erreur inattendue" - }, - "step": { - "user": { - "data": { - "email": "Email", - "password": "Mot de passe" - }, - "description": "Pour des raisons techniques, utilisez un compte sp\u00e9cifique \u00e0 Home Assistant. Vous pouvez cr\u00e9er un compte secondaire depuis l'application SmartHab.", - "title": "Configurer SmartHab" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/smarthab/translations/he.json b/homeassistant/components/smarthab/translations/he.json deleted file mode 100644 index c00515506ac..00000000000 --- a/homeassistant/components/smarthab/translations/he.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "config": { - "error": { - "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", - "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" - }, - "step": { - "user": { - "data": { - "email": "\u05d3\u05d5\u05d0\"\u05dc", - "password": "\u05e1\u05d9\u05e1\u05de\u05d4" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/smarthab/translations/hu.json b/homeassistant/components/smarthab/translations/hu.json deleted file mode 100644 index 2e3cf430a9f..00000000000 --- a/homeassistant/components/smarthab/translations/hu.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "error": { - "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", - "service": "Hiba t\u00f6rt\u00e9nt a SmartHab el\u00e9r\u00e9se k\u00f6zben. A szolg\u00e1ltat\u00e1s le\u00e1llhat. Ellen\u0151rizze a kapcsolatot.", - "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" - }, - "step": { - "user": { - "data": { - "email": "E-mail", - "password": "Jelsz\u00f3" - }, - "description": "Technikai okokb\u00f3l ne felejtsen el m\u00e1sodlagos fi\u00f3kot haszn\u00e1lni a Home Assistant be\u00e1ll\u00edt\u00e1s\u00e1hoz. A SmartHab alkalmaz\u00e1sb\u00f3l l\u00e9trehozhat egyet.", - "title": "A SmartHab be\u00e1ll\u00edt\u00e1sa" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/smarthab/translations/id.json b/homeassistant/components/smarthab/translations/id.json deleted file mode 100644 index 7a776eac304..00000000000 --- a/homeassistant/components/smarthab/translations/id.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "error": { - "invalid_auth": "Autentikasi tidak valid", - "service": "Terjadi kesalahan saat mencoba menjangkau SmartHab. Layanan mungkin sedang mengalami gangguan. Periksa koneksi Anda.", - "unknown": "Kesalahan yang tidak diharapkan" - }, - "step": { - "user": { - "data": { - "email": "Email", - "password": "Kata Sandi" - }, - "description": "Untuk alasan teknis, pastikan untuk menggunakan akun sekunder khusus untuk penyiapan Home Assistant Anda. Anda dapat membuatnya dari aplikasi SmartHab.", - "title": "Siapkan SmartHab" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/smarthab/translations/it.json b/homeassistant/components/smarthab/translations/it.json deleted file mode 100644 index b74da607f77..00000000000 --- a/homeassistant/components/smarthab/translations/it.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "error": { - "invalid_auth": "Autenticazione non valida", - "service": "Errore durante il tentativo di raggiungere SmartHab. Il servizio potrebbe non essere attivo. Controlla la connessione.", - "unknown": "Errore imprevisto" - }, - "step": { - "user": { - "data": { - "email": "Email", - "password": "Password" - }, - "description": "Per motivi tecnici, assicurati di utilizzare un account secondario specifico per la tua configurazione di Home Assistant. \u00c8 possibile crearne uno dall'applicazione SmartHab.", - "title": "Configurazione SmartHab" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/smarthab/translations/ja.json b/homeassistant/components/smarthab/translations/ja.json deleted file mode 100644 index a463f259c2a..00000000000 --- a/homeassistant/components/smarthab/translations/ja.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "error": { - "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", - "service": "SmartHab\u306b\u30a2\u30af\u30bb\u30b9\u3057\u3088\u3046\u3068\u3057\u3066\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002\u30b5\u30fc\u30d3\u30b9\u304c\u30c0\u30a6\u30f3\u3057\u3066\u3044\u308b\u53ef\u80fd\u6027\u304c\u3042\u308a\u307e\u3059\u3002\u63a5\u7d9a\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002", - "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" - }, - "step": { - "user": { - "data": { - "email": "E\u30e1\u30fc\u30eb", - "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" - }, - "description": "\u6280\u8853\u7684\u306a\u7406\u7531\u304b\u3089\u3001Home Assistant\u306e\u8a2d\u5b9a\u306b\u56fa\u6709\u306e\u30bb\u30ab\u30f3\u30c0\u30ea\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u4f7f\u7528\u3057\u3066\u304f\u3060\u3055\u3044\u3002SmartHab\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u304b\u3089\u4f5c\u6210\u3067\u304d\u307e\u3059\u3002", - "title": "SmartHab\u306e\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/smarthab/translations/ko.json b/homeassistant/components/smarthab/translations/ko.json deleted file mode 100644 index 1641555b412..00000000000 --- a/homeassistant/components/smarthab/translations/ko.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "error": { - "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", - "service": "SmartHab \uc5d0 \uc811\uc18d\ud558\ub294 \uc911 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4. \uc11c\ube44\uc2a4\uac00 \ub2e4\uc6b4\ub418\uc5c8\uc744 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \uc5f0\uacb0\uc744 \ud655\uc778\ud574\uc8fc\uc138\uc694.", - "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" - }, - "step": { - "user": { - "data": { - "email": "\uc774\uba54\uc77c", - "password": "\ube44\ubc00\ubc88\ud638" - }, - "description": "\uae30\uc220\uc801\uc778 \uc774\uc720\ub85c Home Assistant \uc124\uc815\uacfc \uad00\ub828\ub41c \ubcf4\uc870 \uacc4\uc815\uc744 \uc0ac\uc6a9\ud574\uc57c \ud569\ub2c8\ub2e4. SmartHab \uc560\ud50c\ub9ac\ucf00\uc774\uc158\uc5d0\uc11c \uc0dd\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.", - "title": "SmartHab \uc124\uce58\ud558\uae30" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/smarthab/translations/lb.json b/homeassistant/components/smarthab/translations/lb.json deleted file mode 100644 index e651190a1e2..00000000000 --- a/homeassistant/components/smarthab/translations/lb.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "error": { - "invalid_auth": "Ong\u00eblteg Authentifikatioun", - "service": "Feeler beim verbanne mat SmartHab. De Service ass viellaicht net ereechbar. Iwwerpr\u00e9if deng Verbindung.", - "unknown": "Onerwaarte Feeler" - }, - "step": { - "user": { - "data": { - "email": "E-Mail", - "password": "Passwuert" - }, - "description": "W\u00e9inst technesche Gr\u00ebnn soll een zweeten Kont benotz gin fir d\u00e4in Home Assistant. Du kanns een zous\u00e4tzleche Kont an der SmartHab Applikatioun erstellen.", - "title": "SmartHab ariichten" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/smarthab/translations/nl.json b/homeassistant/components/smarthab/translations/nl.json deleted file mode 100644 index 31a02ae2b97..00000000000 --- a/homeassistant/components/smarthab/translations/nl.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "error": { - "invalid_auth": "Ongeldige authenticatie", - "service": "Fout bij het bereiken van SmartHab. De service is mogelijk uitgevallen. Controleer uw verbinding.", - "unknown": "Onverwachte fout" - }, - "step": { - "user": { - "data": { - "email": "E-mail", - "password": "Wachtwoord" - }, - "description": "Om technische redenen moet u een tweede account gebruiken dat specifiek is voor uw Home Assistant-installatie. U kunt er een aanmaken vanuit de SmartHab-toepassing.", - "title": "Stel SmartHab in" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/smarthab/translations/no.json b/homeassistant/components/smarthab/translations/no.json deleted file mode 100644 index ed2beaf7836..00000000000 --- a/homeassistant/components/smarthab/translations/no.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "error": { - "invalid_auth": "Ugyldig godkjenning", - "service": "Feil under fors\u00f8k p\u00e5 \u00e5 n\u00e5 SmartHab. Tjenesten kan v\u00e6re nede. Sjekk tilkoblingen din.", - "unknown": "Uventet feil" - }, - "step": { - "user": { - "data": { - "email": "E-post", - "password": "Passord" - }, - "description": "Av tekniske \u00e5rsaker m\u00e5 du s\u00f8rge for \u00e5 bruke en sekund\u00e6r konto som er spesifikk for oppsettet i Home Assistant. Du kan opprette en fra SmartHab-programmet.", - "title": "Oppsett av SmartHab" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/smarthab/translations/pl.json b/homeassistant/components/smarthab/translations/pl.json deleted file mode 100644 index 14ca88f1c00..00000000000 --- a/homeassistant/components/smarthab/translations/pl.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "error": { - "invalid_auth": "Niepoprawne uwierzytelnienie", - "service": "B\u0142\u0105d podczas pr\u00f3by osi\u0105gni\u0119cia SmartHab. Us\u0142uga mo\u017ce by\u0107 wy\u0142\u0105czna. Sprawd\u017a po\u0142\u0105czenie.", - "unknown": "Nieoczekiwany b\u0142\u0105d" - }, - "step": { - "user": { - "data": { - "email": "Adres e-mail", - "password": "Has\u0142o" - }, - "description": "Ze wzgl\u0119d\u00f3w technicznych, nale\u017cy u\u017cy\u0107 dodatkowego konta, specjalnie na u\u017cytek dla Home Assistanta. Mo\u017cesz je utworzy\u0107 z poziomu aplikacji SmartHab.", - "title": "Konfiguracja SmartHab" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/smarthab/translations/pt-BR.json b/homeassistant/components/smarthab/translations/pt-BR.json deleted file mode 100644 index ef205c53827..00000000000 --- a/homeassistant/components/smarthab/translations/pt-BR.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "error": { - "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", - "service": "Erro ao tentar acessar o SmartHab. O servi\u00e7o pode estar inoperante. Verifique sua conex\u00e3o.", - "unknown": "Erro inesperado" - }, - "step": { - "user": { - "data": { - "email": "Email", - "password": "Senha" - }, - "description": "Por motivos t\u00e9cnicos, certifique-se de usar uma conta secund\u00e1ria espec\u00edfica para a configura\u00e7\u00e3o do Home Assistant. Voc\u00ea pode criar um a partir do aplicativo SmartHab.", - "title": "Configurar SmartHab" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/smarthab/translations/pt.json b/homeassistant/components/smarthab/translations/pt.json deleted file mode 100644 index 7430480cc09..00000000000 --- a/homeassistant/components/smarthab/translations/pt.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "config": { - "error": { - "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", - "unknown": "Erro inesperado" - }, - "step": { - "user": { - "data": { - "email": "Email", - "password": "Palavra-passe" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/smarthab/translations/ru.json b/homeassistant/components/smarthab/translations/ru.json deleted file mode 100644 index 45e3698034f..00000000000 --- a/homeassistant/components/smarthab/translations/ru.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "error": { - "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", - "service": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \u043f\u043e\u043f\u044b\u0442\u043a\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438 \u043a SmartHab. \u0421\u0435\u0440\u0432\u0438\u0441 \u043c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d. \u041f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u0435.", - "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." - }, - "step": { - "user": { - "data": { - "email": "\u0410\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b", - "password": "\u041f\u0430\u0440\u043e\u043b\u044c" - }, - "description": "\u041f\u043e \u0442\u0435\u0445\u043d\u0438\u0447\u0435\u0441\u043a\u0438\u043c \u043f\u0440\u0438\u0447\u0438\u043d\u0430\u043c \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0434\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u0443\u044e \u0443\u0447\u0435\u0442\u043d\u0443\u044e \u0437\u0430\u043f\u0438\u0441\u044c \u0434\u043b\u044f Home Assistant. \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0441\u043e\u0437\u0434\u0430\u0442\u044c \u0435\u0451 \u0432 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0438 SmartHab.", - "title": "SmartHab" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/smarthab/translations/tr.json b/homeassistant/components/smarthab/translations/tr.json deleted file mode 100644 index 699967ce8ee..00000000000 --- a/homeassistant/components/smarthab/translations/tr.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "error": { - "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", - "service": "SmartHab'a ula\u015fmaya \u00e7al\u0131\u015f\u0131rken hata olu\u015ftu. Servis kapal\u0131 olabilir. Ba\u011flant\u0131n\u0131z\u0131 kontrol edin.", - "unknown": "Beklenmeyen hata" - }, - "step": { - "user": { - "data": { - "email": "E-posta", - "password": "Parola" - }, - "description": "Teknik nedenlerle, Ev Asistan\u0131 kurulumunuza \u00f6zel ikincil bir hesap kulland\u0131\u011f\u0131n\u0131zdan emin olun. SmartHab uygulamas\u0131ndan bir tane olu\u015fturabilirsiniz.", - "title": "SmartHab'\u0131 kurun" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/smarthab/translations/uk.json b/homeassistant/components/smarthab/translations/uk.json deleted file mode 100644 index 036ec0a78d4..00000000000 --- a/homeassistant/components/smarthab/translations/uk.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "error": { - "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", - "service": "\u041f\u043e\u043c\u0438\u043b\u043a\u0430 \u043f\u0440\u0438 \u0441\u043f\u0440\u043e\u0431\u0456 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e SmartHab. \u0421\u0435\u0440\u0432\u0456\u0441 \u043c\u043e\u0436\u0435 \u0431\u0443\u0442\u0438 \u0432\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0439. \u041f\u0435\u0440\u0435\u0432\u0456\u0440\u0442\u0435 \u0437'\u0454\u0434\u043d\u0430\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": { - "email": "\u0410\u0434\u0440\u0435\u0441\u0430 \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0457 \u043f\u043e\u0448\u0442\u0438", - "password": "\u041f\u0430\u0440\u043e\u043b\u044c" - }, - "description": "\u0417 \u0442\u0435\u0445\u043d\u0456\u0447\u043d\u0438\u0445 \u043f\u0440\u0438\u0447\u0438\u043d \u043d\u0435\u043e\u0431\u0445\u0456\u0434\u043d\u043e \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0432\u0430\u0442\u0438 \u0434\u043e\u0434\u0430\u0442\u043a\u043e\u0432\u0438\u0439 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 \u0434\u043b\u044f Home Assistant. \u0412\u0438 \u043c\u043e\u0436\u0435\u0442\u0435 \u0441\u0442\u0432\u043e\u0440\u0438\u0442\u0438 \u0457\u0457 \u0432 \u0434\u043e\u0434\u0430\u0442\u043a\u0443 SmartHab.", - "title": "SmartHab" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/smarthab/translations/zh-Hans.json b/homeassistant/components/smarthab/translations/zh-Hans.json deleted file mode 100644 index f339adebd86..00000000000 --- a/homeassistant/components/smarthab/translations/zh-Hans.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "config": { - "error": { - "invalid_auth": "\u9a8c\u8bc1\u7801\u65e0\u6548" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/smarthab/translations/zh-Hant.json b/homeassistant/components/smarthab/translations/zh-Hant.json deleted file mode 100644 index 9f1d903a9b1..00000000000 --- a/homeassistant/components/smarthab/translations/zh-Hant.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "error": { - "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", - "service": "\u5617\u8a66\u8a2a\u554f Smarthab \u6642\u767c\u751f\u932f\u8aa4\uff0c\u670d\u52d9\u53ef\u4ee5\u5df2\u7d93\u5931\u6548\uff0c\u8acb\u6aa2\u67e5\u9023\u7dda\u3002", - "unknown": "\u672a\u9810\u671f\u932f\u8aa4" - }, - "step": { - "user": { - "data": { - "email": "\u96fb\u5b50\u90f5\u4ef6", - "password": "\u5bc6\u78bc" - }, - "description": "\u7531\u65bc\u6280\u8853\u539f\u56e0\u3001\u8acb\u78ba\u5b9a\u6307\u5b9a Home Assistant \u8a2d\u5b9a\u5099\u7528\u5e33\u6236\u3002\u53ef\u4ee5\u900f\u904e Smarthab \u61c9\u7528\u7a0b\u5f0f\u5275\u5efa\u3002", - "title": "\u8a2d\u5b9a SmartHab" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/smartthings/fan.py b/homeassistant/components/smartthings/fan.py index 81171ecf554..019cd5dc7b6 100644 --- a/homeassistant/components/smartthings/fan.py +++ b/homeassistant/components/smartthings/fan.py @@ -65,7 +65,6 @@ class SmartThingsFan(SmartThingsEntity, FanEntity): async def async_turn_on( self, - speed: str | None = None, percentage: int | None = None, preset_mode: str | None = None, **kwargs, diff --git a/homeassistant/components/smartthings/manifest.json b/homeassistant/components/smartthings/manifest.json index b4a043e2f13..8925e95eb80 100644 --- a/homeassistant/components/smartthings/manifest.json +++ b/homeassistant/components/smartthings/manifest.json @@ -28,7 +28,7 @@ { "hostname": "hub*", "macaddress": "286D97*" - } + } ], "loggers": ["httpsig", "pysmartapp", "pysmartthings"] } diff --git a/homeassistant/components/smartthings/translations/fr.json b/homeassistant/components/smartthings/translations/fr.json index 6051cbbabce..4978c5d160c 100644 --- a/homeassistant/components/smartthings/translations/fr.json +++ b/homeassistant/components/smartthings/translations/fr.json @@ -8,7 +8,7 @@ "app_setup_error": "Impossible de configurer la SmartApp. Veuillez r\u00e9essayer.", "token_forbidden": "Le jeton n'a pas les port\u00e9es OAuth requises.", "token_invalid_format": "Le jeton doit \u00eatre au format UID / GUID", - "token_unauthorized": "Le jeton est invalide ou n'est plus autoris\u00e9.", + "token_unauthorized": "Le jeton n'est pas valide ou n'est plus autoris\u00e9.", "webhook_error": "SmartThings n'a pas pu valider le point de terminaison configur\u00e9 en \u00ab\u00a0base_url\u00a0\u00bb. Veuillez consulter les exigences du composant." }, "step": { diff --git a/homeassistant/components/smartthings/translations/he.json b/homeassistant/components/smartthings/translations/he.json index 8b7c47304ff..73f9c8491e1 100644 --- a/homeassistant/components/smartthings/translations/he.json +++ b/homeassistant/components/smartthings/translations/he.json @@ -12,17 +12,22 @@ "webhook_error": "SmartThings \u05dc\u05d0 \u05d4\u05e6\u05dc\u05d9\u05d7 \u05dc\u05d0\u05de\u05ea \u05d0\u05ea \u05db\u05ea\u05d5\u05d1\u05ea \u05d4\u05d0\u05ea\u05e8 \u05e9\u05dc webhook. \u05e0\u05d0 \u05dc\u05d5\u05d5\u05d3\u05d0 \u05e9\u05db\u05ea\u05d5\u05d1\u05ea \u05d4-webhook \u05e0\u05d2\u05d9\u05e9\u05d4 \u05de\u05d4\u05d0\u05d9\u05e0\u05d8\u05e8\u05e0\u05d8 \u05d5\u05e0\u05e1\u05d4 \u05e9\u05d5\u05d1." }, "step": { + "authorize": { + "title": "\u05d4\u05e8\u05e9\u05d0\u05ea Home Assistant" + }, "pat": { "data": { "access_token": "\u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d2\u05d9\u05e9\u05d4" }, - "description": "\u05e0\u05d0 \u05dc\u05d4\u05d6\u05d9\u05df SmartThings [\u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d2\u05d9\u05e9\u05d4 \u05d0\u05d9\u05e9\u05d9\u05ea]({token_url}) \u05e9\u05e0\u05d5\u05e6\u05e8 \u05dc\u05e4\u05d9 [\u05d4\u05d5\u05e8\u05d0\u05d5\u05ea]({component_url}). \u05e4\u05e2\u05d5\u05dc\u05d4 \u05d6\u05d5 \u05ea\u05e9\u05de\u05e9 \u05dc\u05d9\u05e6\u05d9\u05e8\u05ea \u05d4\u05e9\u05d9\u05dc\u05d5\u05d1 \u05e9\u05dc Home Assistant \u05d1\u05d7\u05e9\u05d1\u05d5\u05df SmartThings \u05e9\u05dc\u05da." + "description": "\u05e0\u05d0 \u05dc\u05d4\u05d6\u05d9\u05df SmartThings [\u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d2\u05d9\u05e9\u05d4 \u05d0\u05d9\u05e9\u05d9\u05ea]({token_url}) \u05e9\u05e0\u05d5\u05e6\u05e8 \u05dc\u05e4\u05d9 [\u05d4\u05d5\u05e8\u05d0\u05d5\u05ea]({component_url}). \u05e4\u05e2\u05d5\u05dc\u05d4 \u05d6\u05d5 \u05ea\u05e9\u05de\u05e9 \u05dc\u05d9\u05e6\u05d9\u05e8\u05ea \u05d4\u05e9\u05d9\u05dc\u05d5\u05d1 \u05e9\u05dc Home Assistant \u05d1\u05d7\u05e9\u05d1\u05d5\u05df SmartThings \u05e9\u05dc\u05da.", + "title": "\u05d4\u05d6\u05e0\u05ea \u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d2\u05d9\u05e9\u05d4 \u05d0\u05d9\u05e9\u05d9\u05ea" }, "select_location": { "data": { "location_id": "\u05de\u05d9\u05e7\u05d5\u05dd" }, - "description": "\u05e0\u05d0 \u05dc\u05d1\u05d7\u05d5\u05e8 \u05d0\u05ea \u05de\u05d9\u05e7\u05d5\u05dd \u05d4-SmartThings \u05e9\u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d5\u05e1\u05d9\u05e3 \u05dc-Home Assistant. \u05dc\u05d0\u05d7\u05e8 \u05de\u05db\u05df \u05d9\u05e4\u05ea\u05d7 \u05d7\u05dc\u05d5\u05df \u05d7\u05d3\u05e9 \u05d5\u05e0\u05d1\u05e7\u05e9 \u05de\u05de\u05da \u05dc\u05d4\u05ea\u05d7\u05d1\u05e8 \u05d5\u05dc\u05d0\u05e9\u05e8 \u05d4\u05ea\u05e7\u05e0\u05d4 \u05e9\u05dc \u05e9\u05d9\u05dc\u05d5\u05d1 Home Assistant \u05d1\u05de\u05d9\u05e7\u05d5\u05dd \u05e9\u05e0\u05d1\u05d7\u05e8." + "description": "\u05e0\u05d0 \u05dc\u05d1\u05d7\u05d5\u05e8 \u05d0\u05ea \u05de\u05d9\u05e7\u05d5\u05dd \u05d4-SmartThings \u05e9\u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d5\u05e1\u05d9\u05e3 \u05dc-Home Assistant. \u05dc\u05d0\u05d7\u05e8 \u05de\u05db\u05df \u05d9\u05e4\u05ea\u05d7 \u05d7\u05dc\u05d5\u05df \u05d7\u05d3\u05e9 \u05d5\u05e0\u05d1\u05e7\u05e9 \u05de\u05de\u05da \u05dc\u05d4\u05ea\u05d7\u05d1\u05e8 \u05d5\u05dc\u05d0\u05e9\u05e8 \u05d4\u05ea\u05e7\u05e0\u05d4 \u05e9\u05dc \u05e9\u05d9\u05dc\u05d5\u05d1 Home Assistant \u05d1\u05de\u05d9\u05e7\u05d5\u05dd \u05e9\u05e0\u05d1\u05d7\u05e8.", + "title": "\u05d1\u05d7\u05d9\u05e8\u05ea \u05de\u05d9\u05e7\u05d5\u05dd" }, "user": { "description": "SmartThings \u05d9\u05d5\u05d2\u05d3\u05e8 \u05dc\u05e9\u05dc\u05d5\u05d7 \u05e2\u05d3\u05db\u05d5\u05e0\u05d9 \u05d3\u05d7\u05d9\u05e4\u05d4 \u05dc-Home Assistant \u05d1\u05db\u05ea\u05d5\u05d1\u05ea:\n> {webhook_url}\n\n\u05d0\u05dd \u05d4\u05d3\u05d1\u05e8 \u05d0\u05d9\u05e0\u05d5 \u05e0\u05db\u05d5\u05df, \u05e0\u05d0 \u05dc\u05e2\u05d3\u05db\u05df \u05d0\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4, \u05d5\u05dc\u05d4\u05e4\u05e2\u05d9\u05dc \u05de\u05d7\u05d3\u05e9 \u05d0\u05ea Home Assistant \u05d5\u05dc\u05e0\u05e1\u05d5\u05ea \u05e9\u05d5\u05d1.", diff --git a/homeassistant/components/smarttub/manifest.json b/homeassistant/components/smarttub/manifest.json index 9bec5d4a72e..90f85b6c839 100644 --- a/homeassistant/components/smarttub/manifest.json +++ b/homeassistant/components/smarttub/manifest.json @@ -5,7 +5,7 @@ "documentation": "https://www.home-assistant.io/integrations/smarttub", "dependencies": [], "codeowners": ["@mdz"], - "requirements": ["python-smarttub==0.0.29"], + "requirements": ["python-smarttub==0.0.30"], "quality_scale": "platinum", "iot_class": "cloud_polling", "loggers": ["smarttub"] diff --git a/homeassistant/components/smarttub/translations/fr.json b/homeassistant/components/smarttub/translations/fr.json index bb481048fff..9796fb079aa 100644 --- a/homeassistant/components/smarttub/translations/fr.json +++ b/homeassistant/components/smarttub/translations/fr.json @@ -5,7 +5,7 @@ "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" }, "error": { - "invalid_auth": "Authentification invalide" + "invalid_auth": "Authentification non valide" }, "step": { "reauth_confirm": { @@ -14,7 +14,7 @@ }, "user": { "data": { - "email": "Email", + "email": "Courriel", "password": "Mot de passe" }, "description": "Entrez votre adresse e-mail et votre mot de passe SmartTub pour vous connecter", diff --git a/homeassistant/components/smarty/fan.py b/homeassistant/components/smarty/fan.py index 0fadaf667a1..98df91acbc3 100644 --- a/homeassistant/components/smarty/fan.py +++ b/homeassistant/components/smarty/fan.py @@ -99,9 +99,9 @@ class SmartyFan(FanEntity): self._smarty_fan_speed = fan_speed self.schedule_update_ha_state() - def turn_on(self, speed=None, percentage=None, preset_mode=None, **kwargs): + def turn_on(self, percentage=None, preset_mode=None, **kwargs): """Turn on the fan.""" - _LOGGER.debug("Turning on fan. Speed is %s", speed) + _LOGGER.debug("Turning on fan. percentage is %s", percentage) self.set_percentage(percentage or DEFAULT_ON_PERCENTAGE) def turn_off(self, **kwargs): diff --git a/homeassistant/components/snips/services.yaml b/homeassistant/components/snips/services.yaml index 407eab996c7..df3a46281c8 100644 --- a/homeassistant/components/snips/services.yaml +++ b/homeassistant/components/snips/services.yaml @@ -28,7 +28,7 @@ say: name: Custom data description: custom data that will be included with all messages in this session example: user=UserName - default: '' + default: "" selector: text: site_id: @@ -59,7 +59,7 @@ say_action: name: Custom data description: custom data that will be included with all messages in this session example: user=UserName - default: '' + default: "" selector: text: intent_filter: diff --git a/homeassistant/components/snmp/const.py b/homeassistant/components/snmp/const.py index b3a93cfe98b..e51bbc33b90 100644 --- a/homeassistant/components/snmp/const.py +++ b/homeassistant/components/snmp/const.py @@ -16,6 +16,7 @@ DEFAULT_HOST = "localhost" DEFAULT_NAME = "SNMP" DEFAULT_PORT = "161" DEFAULT_PRIV_PROTOCOL = "none" +DEFAULT_TIMEOUT = 8 DEFAULT_VERSION = "1" DEFAULT_VARTYPE = "none" diff --git a/homeassistant/components/snmp/sensor.py b/homeassistant/components/snmp/sensor.py index 3530ca180a4..ba111ffc9bc 100644 --- a/homeassistant/components/snmp/sensor.py +++ b/homeassistant/components/snmp/sensor.py @@ -48,6 +48,7 @@ from .const import ( DEFAULT_NAME, DEFAULT_PORT, DEFAULT_PRIV_PROTOCOL, + DEFAULT_TIMEOUT, DEFAULT_VERSION, MAP_AUTH_PROTOCOLS, MAP_PRIV_PROTOCOLS, @@ -125,14 +126,14 @@ async def async_setup_platform( authProtocol=getattr(hlapi, MAP_AUTH_PROTOCOLS[authproto]), privProtocol=getattr(hlapi, MAP_PRIV_PROTOCOLS[privproto]), ), - UdpTransportTarget((host, port)), + UdpTransportTarget((host, port), timeout=DEFAULT_TIMEOUT), ContextData(), ] else: request_args = [ SnmpEngine(), CommunityData(community, mpModel=SNMP_VERSIONS[version]), - UdpTransportTarget((host, port)), + UdpTransportTarget((host, port), timeout=DEFAULT_TIMEOUT), ContextData(), ] diff --git a/homeassistant/components/sochain/manifest.json b/homeassistant/components/sochain/manifest.json index 6ff42bd4800..5a568340197 100644 --- a/homeassistant/components/sochain/manifest.json +++ b/homeassistant/components/sochain/manifest.json @@ -1,4 +1,5 @@ { + "disabled": "Integration library depends on async_timeout==3.x.x", "domain": "sochain", "name": "SoChain", "documentation": "https://www.home-assistant.io/integrations/sochain", diff --git a/homeassistant/components/sochain/sensor.py b/homeassistant/components/sochain/sensor.py index 52733a25bf9..157d94b8706 100644 --- a/homeassistant/components/sochain/sensor.py +++ b/homeassistant/components/sochain/sensor.py @@ -1,4 +1,5 @@ """Support for watching multiple cryptocurrencies.""" +# pylint: disable=import-error from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/solaredge/coordinator.py b/homeassistant/components/solaredge/coordinator.py index b2fe27db808..4e93571f8a4 100644 --- a/homeassistant/components/solaredge/coordinator.py +++ b/homeassistant/components/solaredge/coordinator.py @@ -77,8 +77,9 @@ class SolarEdgeOverviewDataService(SolarEdgeDataService): self.data = {} + energy_keys = ["lifeTimeData", "lastYearData", "lastMonthData", "lastDayData"] for key, value in overview.items(): - if key in ["lifeTimeData", "lastYearData", "lastMonthData", "lastDayData"]: + if key in energy_keys: data = value["energy"] elif key in ["currentPower"]: data = value["power"] @@ -86,6 +87,16 @@ class SolarEdgeOverviewDataService(SolarEdgeDataService): data = value self.data[key] = data + # Sanity check the energy values. SolarEdge API sometimes report "lifetimedata" of zero, + # while values for last Year, Month and Day energy are still OK. + # See https://github.com/home-assistant/core/issues/59285 . + if set(energy_keys).issubset(self.data.keys()): + for index, key in enumerate(energy_keys, start=1): + # All coming values in list should be larger than the current value. + if any(self.data[k] > self.data[key] for k in energy_keys[index:]): + self.data = {} + raise UpdateFailed("Invalid energy values, skipping update") + LOGGER.debug("Updated SolarEdge overview: %s", self.data) diff --git a/homeassistant/components/solaredge/translations/fr.json b/homeassistant/components/solaredge/translations/fr.json index 36b283a9145..fb4e12811fa 100644 --- a/homeassistant/components/solaredge/translations/fr.json +++ b/homeassistant/components/solaredge/translations/fr.json @@ -6,7 +6,7 @@ "error": { "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", "could_not_connect": "Impossible de se connecter \u00e0 l'API solaredge", - "invalid_api_key": "Cl\u00e9 API invalide", + "invalid_api_key": "Cl\u00e9 d'API non valide", "site_not_active": "The site n'est pas actif" }, "step": { diff --git a/homeassistant/components/solaredge_local/sensor.py b/homeassistant/components/solaredge_local/sensor.py index 368121e79e9..d07a95683eb 100644 --- a/homeassistant/components/solaredge_local/sensor.py +++ b/homeassistant/components/solaredge_local/sensor.py @@ -209,7 +209,6 @@ def setup_platform( _LOGGER.debug("Credentials correct and site is active") except AttributeError: _LOGGER.error("Missing details data in solaredge status") - _LOGGER.debug("Status is: %s", status) return except (ConnectTimeout, HTTPError): _LOGGER.error("Could not retrieve details from SolarEdge API") diff --git a/homeassistant/components/solarlog/sensor.py b/homeassistant/components/solarlog/sensor.py index 6269210756c..4180d48cdef 100644 --- a/homeassistant/components/solarlog/sensor.py +++ b/homeassistant/components/solarlog/sensor.py @@ -2,9 +2,9 @@ from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers import update_coordinator from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import SolarlogData from .const import DOMAIN, SENSOR_TYPES, SolarLogSensorEntityDescription @@ -20,7 +20,7 @@ async def async_setup_entry( ) -class SolarlogSensor(update_coordinator.CoordinatorEntity, SensorEntity): +class SolarlogSensor(CoordinatorEntity[SolarlogData], SensorEntity): """Representation of a Sensor.""" entity_description: SolarLogSensorEntityDescription diff --git a/homeassistant/components/solax/__init__.py b/homeassistant/components/solax/__init__.py index 2f9d4509dd2..3915f414d0e 100644 --- a/homeassistant/components/solax/__init__.py +++ b/homeassistant/components/solax/__init__.py @@ -21,7 +21,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry.data[CONF_PASSWORD], ) await api.get_data() - except Exception as err: # pylint: disable=broad-except + except Exception as err: raise ConfigEntryNotReady from err hass.data.setdefault(DOMAIN, {})[entry.entry_id] = api diff --git a/homeassistant/components/solax/config_flow.py b/homeassistant/components/solax/config_flow.py index 5c4ef05da4b..56c6989cc7f 100644 --- a/homeassistant/components/solax/config_flow.py +++ b/homeassistant/components/solax/config_flow.py @@ -63,14 +63,3 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_show_form( step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors ) - - async def async_step_import(self, config: dict[str, Any]) -> FlowResult: - """Handle import of solax config from YAML.""" - - import_data = { - CONF_IP_ADDRESS: config[CONF_IP_ADDRESS], - CONF_PORT: config[CONF_PORT], - CONF_PASSWORD: DEFAULT_PASSWORD, - } - - return await self.async_step_user(user_input=import_data) diff --git a/homeassistant/components/solax/sensor.py b/homeassistant/components/solax/sensor.py index 6f1b5ef6cf3..7f9d81ac9b0 100644 --- a/homeassistant/components/solax/sensor.py +++ b/homeassistant/components/solax/sensor.py @@ -3,38 +3,25 @@ from __future__ import annotations import asyncio from datetime import timedelta -import logging from solax.inverter import InverterError -import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, SensorStateClass, ) -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import CONF_IP_ADDRESS, CONF_PORT, TEMP_CELSIUS +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import TEMP_CELSIUS from homeassistant.core import HomeAssistant from homeassistant.exceptions import PlatformNotReady -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_time_interval -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .const import DOMAIN, MANUFACTURER -_LOGGER = logging.getLogger(__name__) - DEFAULT_PORT = 80 -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_IP_ADDRESS): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - }, -) SCAN_INTERVAL = timedelta(seconds=30) @@ -81,30 +68,6 @@ async def async_setup_entry( async_add_entities(devices) -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Platform setup.""" - - _LOGGER.warning( - "Configuration of the SolaX Power platform in YAML is deprecated and " - "will be removed in Home Assistant 2022.4; Your existing configuration " - "has been imported into the UI automatically and can be safely removed " - "from your configuration.yaml file" - ) - - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data=config, - ) - ) - - class RealTimeDataEndpoint: """Representation of a Sensor.""" diff --git a/homeassistant/components/solax/strings.json b/homeassistant/components/solax/strings.json index cad2575cbce..e73c9f3bc88 100644 --- a/homeassistant/components/solax/strings.json +++ b/homeassistant/components/solax/strings.json @@ -14,4 +14,4 @@ "unknown": "[%key:common::config_flow::error::unknown%]" } } -} \ No newline at end of file +} diff --git a/homeassistant/components/solax/translations/fr.json b/homeassistant/components/solax/translations/fr.json index a0d7a14dcdb..703bd8de288 100644 --- a/homeassistant/components/solax/translations/fr.json +++ b/homeassistant/components/solax/translations/fr.json @@ -1,7 +1,7 @@ { "config": { "error": { - "cannot_connect": "Impossible de se connecter", + "cannot_connect": "\u00c9chec de connexion", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/soma/manifest.json b/homeassistant/components/soma/manifest.json index 88d77b775c5..39029199c29 100644 --- a/homeassistant/components/soma/manifest.json +++ b/homeassistant/components/soma/manifest.json @@ -3,10 +3,7 @@ "name": "Soma Connect", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/soma", - "codeowners": [ - "@ratsept", - "@sebfortier2288" - ], + "codeowners": ["@ratsept", "@sebfortier2288"], "requirements": ["pysoma==0.0.10"], "iot_class": "local_polling", "loggers": ["api"] diff --git a/homeassistant/components/soma/strings.json b/homeassistant/components/soma/strings.json index 7181698db40..931a33fff56 100644 --- a/homeassistant/components/soma/strings.json +++ b/homeassistant/components/soma/strings.json @@ -21,4 +21,4 @@ } } } -} \ No newline at end of file +} diff --git a/homeassistant/components/soma/translations/fr.json b/homeassistant/components/soma/translations/fr.json index c9c9bb07643..4feed187711 100644 --- a/homeassistant/components/soma/translations/fr.json +++ b/homeassistant/components/soma/translations/fr.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_setup": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible.", - "authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration de l'URL d'authentification d\u00e9pass\u00e9.", + "authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration de l'URL d'authentification expir\u00e9.", "connection_error": "\u00c9chec de connexion", "missing_configuration": "Le composant Soma n'est pas configur\u00e9. Veuillez suivre la documentation.", "result_error": "SOMA Connect a r\u00e9pondu avec l'\u00e9tat d'erreur." diff --git a/homeassistant/components/somfy/translations/fr.json b/homeassistant/components/somfy/translations/fr.json index 08b978e3f12..0c7a25831bc 100644 --- a/homeassistant/components/somfy/translations/fr.json +++ b/homeassistant/components/somfy/translations/fr.json @@ -1,9 +1,9 @@ { "config": { "abort": { - "authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration de l'URL d'authentification d\u00e9pass\u00e9.", + "authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration de l'URL d'authentification expir\u00e9.", "missing_configuration": "Le composant n'est pas configur\u00e9. Veuillez suivre la documentation.", - "no_url_available": "Aucune URL disponible. Pour plus d'informations sur cette erreur, [consultez la section d'aide] ( {docs_url} )", + "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." }, "create_entry": { diff --git a/homeassistant/components/somfy_mylink/strings.json b/homeassistant/components/somfy_mylink/strings.json index 81fb9b78c74..2609e8d893e 100644 --- a/homeassistant/components/somfy_mylink/strings.json +++ b/homeassistant/components/somfy_mylink/strings.json @@ -39,5 +39,5 @@ } } } - } + } } diff --git a/homeassistant/components/somfy_mylink/translations/fr.json b/homeassistant/components/somfy_mylink/translations/fr.json index 46781252523..cc257e16451 100644 --- a/homeassistant/components/somfy_mylink/translations/fr.json +++ b/homeassistant/components/somfy_mylink/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "flow_title": "{mac} ({ip})", diff --git a/homeassistant/components/sonarr/config_flow.py b/homeassistant/components/sonarr/config_flow.py index c9ef2d3ecc8..0bdcca6c033 100644 --- a/homeassistant/components/sonarr/config_flow.py +++ b/homeassistant/components/sonarr/config_flow.py @@ -76,7 +76,6 @@ class SonarrConfigFlow(ConfigFlow, domain=DOMAIN): return self.async_show_form( step_id="reauth_confirm", description_placeholders={"url": self.entry.data[CONF_URL]}, - data_schema=vol.Schema({}), errors={}, ) diff --git a/homeassistant/components/sonarr/translations/bg.json b/homeassistant/components/sonarr/translations/bg.json index 29dc5bfd9a9..c6ab1b205e9 100644 --- a/homeassistant/components/sonarr/translations/bg.json +++ b/homeassistant/components/sonarr/translations/bg.json @@ -18,7 +18,8 @@ "data": { "api_key": "API \u043a\u043b\u044e\u0447", "host": "\u0425\u043e\u0441\u0442", - "port": "\u041f\u043e\u0440\u0442" + "port": "\u041f\u043e\u0440\u0442", + "url": "URL" } } } diff --git a/homeassistant/components/sonarr/translations/el.json b/homeassistant/components/sonarr/translations/el.json index a348d76f798..22895b3a38a 100644 --- a/homeassistant/components/sonarr/translations/el.json +++ b/homeassistant/components/sonarr/translations/el.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u0397 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af", - "reauth_successful": "\u0395\u03c0\u03b1\u03bb\u03b7\u03b8\u03b5\u03cd\u03c4\u03b7\u03ba\u03b5 \u03be\u03b1\u03bd\u03ac \u03bc\u03b5 \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03af\u03b1", + "reauth_successful": "\u039f \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03ae\u03c4\u03b1\u03bd \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2", "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" }, "error": { @@ -12,8 +12,8 @@ "flow_title": "{name}", "step": { "reauth_confirm": { - "description": "\u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 Sonarr \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03c0\u03b1\u03bd\u03b1\u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03b7\u03b8\u03b5\u03af \u03c7\u03b5\u03b9\u03c1\u03bf\u03ba\u03af\u03bd\u03b7\u03c4\u03b1 \u03bc\u03b5 \u03c4\u03bf Sonarr API \u03c0\u03bf\u03c5 \u03c6\u03b9\u03bb\u03bf\u03be\u03b5\u03bd\u03b5\u03af\u03c4\u03b1\u03b9 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7: {host}", - "title": "\u0395\u03c0\u03b1\u03bb\u03b7\u03b8\u03b5\u03cd\u03c3\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac \u03c4\u03bf\u03bd \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03bc\u03b5 \u03c4\u03bf Sonarr" + "description": "\u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 Sonarr \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03b7\u03b8\u03b5\u03af \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03bc\u03b5 \u03bc\u03b7 \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03bf \u03c4\u03c1\u03cc\u03c0\u03bf \u03bc\u03b5 \u03c4\u03bf Sonarr API \u03c0\u03bf\u03c5 \u03c6\u03b9\u03bb\u03bf\u03be\u03b5\u03bd\u03b5\u03af\u03c4\u03b1\u03b9 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7: {url}", + "title": "\u0395\u03c0\u03b1\u03bd\u03b1\u03bb\u03b7\u03c0\u03c4\u03b9\u03ba\u03cc\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2" }, "user": { "data": { diff --git a/homeassistant/components/sonarr/translations/es.json b/homeassistant/components/sonarr/translations/es.json index cee9f6661c5..b2f073297d9 100644 --- a/homeassistant/components/sonarr/translations/es.json +++ b/homeassistant/components/sonarr/translations/es.json @@ -22,6 +22,7 @@ "host": "Host", "port": "Puerto", "ssl": "Utiliza un certificado SSL", + "url": "URL", "verify_ssl": "Verificar certificado SSL" } } diff --git a/homeassistant/components/sonarr/translations/et.json b/homeassistant/components/sonarr/translations/et.json index ba8f96413c3..4629e59e68d 100644 --- a/homeassistant/components/sonarr/translations/et.json +++ b/homeassistant/components/sonarr/translations/et.json @@ -12,7 +12,7 @@ "flow_title": "{name}", "step": { "reauth_confirm": { - "description": "Sonarr-i sidumine tuleb k\u00e4sitsi taastuvastada Sonarr API abil: {host}", + "description": "Sonarr-i sidumine tuleb k\u00e4sitsi taastuvastada Sonarr API abil: {url}", "title": "Autendi Sonarriga uuesti" }, "user": { @@ -22,6 +22,7 @@ "host": "", "port": "", "ssl": "Kasutab SSL serti", + "url": "URL", "verify_ssl": "Kontrolli SSL sertifikaati" } } diff --git a/homeassistant/components/sonarr/translations/fr.json b/homeassistant/components/sonarr/translations/fr.json index c4a7a8764ac..0793adc1b9f 100644 --- a/homeassistant/components/sonarr/translations/fr.json +++ b/homeassistant/components/sonarr/translations/fr.json @@ -7,12 +7,12 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide" + "invalid_auth": "Authentification non valide" }, "flow_title": "{name}", "step": { "reauth_confirm": { - "description": "L'int\u00e9gration Sonarr doit \u00eatre r\u00e9-authentifi\u00e9e manuellement avec l'API Sonarr h\u00e9berg\u00e9e sur: {host}", + "description": "L'int\u00e9gration Sonarr doit \u00eatre r\u00e9-authentifi\u00e9e manuellement avec l'API Sonarr h\u00e9berg\u00e9e sur\u00a0: {url}", "title": "R\u00e9-authentifier l'int\u00e9gration" }, "user": { @@ -22,6 +22,7 @@ "host": "H\u00f4te", "port": "Port", "ssl": "Utilise un certificat SSL", + "url": "URL", "verify_ssl": "V\u00e9rifier le certificat SSL" } } diff --git a/homeassistant/components/sonarr/translations/he.json b/homeassistant/components/sonarr/translations/he.json index d033613184e..53b98ae4139 100644 --- a/homeassistant/components/sonarr/translations/he.json +++ b/homeassistant/components/sonarr/translations/he.json @@ -20,6 +20,7 @@ "host": "\u05de\u05d0\u05e8\u05d7", "port": "\u05e4\u05ea\u05d7\u05d4", "ssl": "\u05e9\u05d9\u05de\u05d5\u05e9 \u05d1\u05d0\u05d9\u05e9\u05d5\u05e8 SSL", + "url": "\u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8", "verify_ssl": "\u05d0\u05d9\u05de\u05d5\u05ea \u05d0\u05d9\u05e9\u05d5\u05e8 SSL" } } diff --git a/homeassistant/components/sonarr/translations/hu.json b/homeassistant/components/sonarr/translations/hu.json index f5286094b73..c3cc2ac0f6c 100644 --- a/homeassistant/components/sonarr/translations/hu.json +++ b/homeassistant/components/sonarr/translations/hu.json @@ -22,6 +22,7 @@ "host": "C\u00edm", "port": "Port", "ssl": "SSL tan\u00fas\u00edtv\u00e1ny haszn\u00e1lata", + "url": "URL", "verify_ssl": "SSL-tan\u00fas\u00edtv\u00e1ny ellen\u0151rz\u00e9se" } } diff --git a/homeassistant/components/sonarr/translations/id.json b/homeassistant/components/sonarr/translations/id.json index 9d906a07f91..ec76bf44491 100644 --- a/homeassistant/components/sonarr/translations/id.json +++ b/homeassistant/components/sonarr/translations/id.json @@ -12,7 +12,7 @@ "flow_title": "{name}", "step": { "reauth_confirm": { - "description": "Integrasi Sonarr perlu diautentikasi ulang secara manual dengan API Sonarr yang dihosting di: {host}", + "description": "Integrasi Sonarr perlu diautentikasi ulang secara manual dengan API Sonarr yang dihosting di: {url}", "title": "Autentikasi Ulang Integrasi" }, "user": { @@ -22,6 +22,7 @@ "host": "Host", "port": "Port", "ssl": "Menggunakan sertifikat SSL", + "url": "URL", "verify_ssl": "Verifikasi sertifikat SSL" } } diff --git a/homeassistant/components/sonarr/translations/it.json b/homeassistant/components/sonarr/translations/it.json index 9dc6582c150..ba78810d928 100644 --- a/homeassistant/components/sonarr/translations/it.json +++ b/homeassistant/components/sonarr/translations/it.json @@ -12,7 +12,7 @@ "flow_title": "{name}", "step": { "reauth_confirm": { - "description": "L'integrazione di Sonarr deve essere nuovamente autenticata manualmente con l'API Sonarr ospitata su: {host}", + "description": "L'integrazione di Sonarr deve essere nuovamente autenticata manualmente con l'API Sonarr ospitata su: {url}", "title": "Autentica nuovamente l'integrazione" }, "user": { @@ -22,6 +22,7 @@ "host": "Host", "port": "Porta", "ssl": "Utilizza un certificato SSL", + "url": "URL", "verify_ssl": "Verifica il certificato SSL" } } diff --git a/homeassistant/components/sonarr/translations/ja.json b/homeassistant/components/sonarr/translations/ja.json index 64785eed5ec..53a659d9bde 100644 --- a/homeassistant/components/sonarr/translations/ja.json +++ b/homeassistant/components/sonarr/translations/ja.json @@ -22,6 +22,7 @@ "host": "\u30db\u30b9\u30c8", "port": "\u30dd\u30fc\u30c8", "ssl": "SSL\u8a3c\u660e\u66f8\u3092\u4f7f\u7528\u3059\u308b", + "url": "URL", "verify_ssl": "SSL\u8a3c\u660e\u66f8\u3092\u78ba\u8a8d\u3059\u308b" } } diff --git a/homeassistant/components/sonarr/translations/nl.json b/homeassistant/components/sonarr/translations/nl.json index d2ded412f1b..58bf04e283d 100644 --- a/homeassistant/components/sonarr/translations/nl.json +++ b/homeassistant/components/sonarr/translations/nl.json @@ -12,7 +12,7 @@ "flow_title": "{name}", "step": { "reauth_confirm": { - "description": "De Sonarr-integratie moet handmatig opnieuw worden geverifieerd met de Sonarr-API die wordt gehost op: {host}", + "description": "De Sonarr-integratie moet handmatig opnieuw worden geverifieerd met de Sonarr-API die wordt gehost op: {url}", "title": "Verifieer de integratie opnieuw" }, "user": { @@ -22,6 +22,7 @@ "host": "Host", "port": "Poort", "ssl": "Maakt gebruik van een SSL-certificaat", + "url": "URL", "verify_ssl": "SSL-certificaat verifi\u00ebren" } } diff --git a/homeassistant/components/sonarr/translations/no.json b/homeassistant/components/sonarr/translations/no.json index 312d4d5e91a..5ee028b8f02 100644 --- a/homeassistant/components/sonarr/translations/no.json +++ b/homeassistant/components/sonarr/translations/no.json @@ -12,7 +12,7 @@ "flow_title": "{name}", "step": { "reauth_confirm": { - "description": "Sonarr-integrasjonen m\u00e5 autentiseres p\u00e5 nytt med Sonarr API vert p\u00e5: {host}", + "description": "Sonarr-integrasjonen m\u00e5 re-autentiseres manuelt med Sonarr API som er vert for: {url}", "title": "Godkjenne integrering p\u00e5 nytt" }, "user": { @@ -22,6 +22,7 @@ "host": "Vert", "port": "Port", "ssl": "Bruker et SSL-sertifikat", + "url": "URL", "verify_ssl": "Verifisere SSL-sertifikat" } } diff --git a/homeassistant/components/sonarr/translations/pl.json b/homeassistant/components/sonarr/translations/pl.json index 7985bdf8f7f..e5be700a752 100644 --- a/homeassistant/components/sonarr/translations/pl.json +++ b/homeassistant/components/sonarr/translations/pl.json @@ -12,7 +12,7 @@ "flow_title": "{name}", "step": { "reauth_confirm": { - "description": "Integracja Sonarr musi by\u0107 r\u0119cznie ponownie uwierzytelniona za pomoc\u0105 API Sonarr pod adresem: {host}", + "description": "Integracja Sonarr musi by\u0107 r\u0119cznie ponownie uwierzytelniona za pomoc\u0105 API Sonarr pod adresem: {url}", "title": "Ponownie uwierzytelnij integracj\u0119" }, "user": { @@ -22,6 +22,7 @@ "host": "Nazwa hosta lub adres IP", "port": "Port", "ssl": "Certyfikat SSL", + "url": "URL", "verify_ssl": "Weryfikacja certyfikatu SSL" } } diff --git a/homeassistant/components/sonarr/translations/ru.json b/homeassistant/components/sonarr/translations/ru.json index 3f3b9593a09..531058f3fff 100644 --- a/homeassistant/components/sonarr/translations/ru.json +++ b/homeassistant/components/sonarr/translations/ru.json @@ -12,7 +12,7 @@ "flow_title": "{name}", "step": { "reauth_confirm": { - "description": "\u0422\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e API Sonarr \u043f\u043e \u0430\u0434\u0440\u0435\u0441\u0443: {host}", + "description": "\u0422\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e API Sonarr \u043f\u043e \u0430\u0434\u0440\u0435\u0441\u0443: {url}", "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": { @@ -22,6 +22,7 @@ "host": "\u0425\u043e\u0441\u0442", "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", + "url": "URL-\u0430\u0434\u0440\u0435\u0441", "verify_ssl": "\u041f\u0440\u043e\u0432\u0435\u0440\u044f\u0442\u044c \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 SSL" } } diff --git a/homeassistant/components/sonarr/translations/tr.json b/homeassistant/components/sonarr/translations/tr.json index d1e961cb2b9..c064e4947c3 100644 --- a/homeassistant/components/sonarr/translations/tr.json +++ b/homeassistant/components/sonarr/translations/tr.json @@ -12,7 +12,7 @@ "flow_title": "{name}", "step": { "reauth_confirm": { - "description": "Sonarr entegrasyonunun, \u015fu adreste bar\u0131nd\u0131r\u0131lan Sonarr API ile manuel olarak yeniden do\u011frulanmas\u0131 gerekir: {host}", + "description": "Sonarr entegrasyonunun \u015fu adreste bar\u0131nd\u0131r\u0131lan Sonarr API ile manuel olarak yeniden do\u011frulanmas\u0131 gerekiyor: {url}", "title": "Entegrasyonu Yeniden Do\u011frula" }, "user": { @@ -22,6 +22,7 @@ "host": "Ana Bilgisayar", "port": "Port", "ssl": "SSL sertifikas\u0131 kullan\u0131r", + "url": "URL", "verify_ssl": "SSL sertifikas\u0131n\u0131 do\u011frulay\u0131n" } } diff --git a/homeassistant/components/sonarr/translations/zh-Hant.json b/homeassistant/components/sonarr/translations/zh-Hant.json index 0a107efae6e..4688ec2e438 100644 --- a/homeassistant/components/sonarr/translations/zh-Hant.json +++ b/homeassistant/components/sonarr/translations/zh-Hant.json @@ -12,7 +12,7 @@ "flow_title": "{name}", "step": { "reauth_confirm": { - "description": "Sonarr \u6574\u5408\u9700\u8981\u624b\u52d5\u91cd\u65b0\u8a8d\u8b49 Sonarr API\uff1a{host}", + "description": "Sonarr \u6574\u5408\u9700\u8981\u624b\u52d5\u91cd\u65b0\u8a8d\u8b49 Sonarr API\uff1a{url}", "title": "\u91cd\u65b0\u8a8d\u8b49\u6574\u5408" }, "user": { @@ -22,6 +22,7 @@ "host": "\u4e3b\u6a5f\u7aef", "port": "\u901a\u8a0a\u57e0", "ssl": "\u4f7f\u7528 SSL \u8a8d\u8b49", + "url": "\u7db2\u5740", "verify_ssl": "\u78ba\u8a8d SSL \u8a8d\u8b49" } } diff --git a/homeassistant/components/sonos/__init__.py b/homeassistant/components/sonos/__init__.py index 27d51d8f3e6..bb4c61fdadf 100644 --- a/homeassistant/components/sonos/__init__.py +++ b/homeassistant/components/sonos/__init__.py @@ -188,6 +188,7 @@ class SonosDiscoveryManager: _ = soco.volume return soco except NotSupportedException as exc: + # pylint: disable-next=used-before-assignment _LOGGER.debug("Device %s is not supported, ignoring: %s", uid, exc) self.data.discovery_ignored.add(ip_address) except (OSError, SoCoException) as ex: diff --git a/homeassistant/components/sonos/const.py b/homeassistant/components/sonos/const.py index 6c4bdd07b31..7b85828377a 100644 --- a/homeassistant/components/sonos/const.py +++ b/homeassistant/components/sonos/const.py @@ -171,6 +171,20 @@ SOURCE_LINEIN = "Line-in" SOURCE_SPOTIFY_CONNECT = "Spotify Connect" SOURCE_TV = "TV" +MODELS_LINEIN_ONLY = ( + "CONNECT", + "CONNECT:AMP", + "PORT", + "PLAY:5", +) +MODELS_TV_ONLY = ( + "ARC", + "BEAM", + "PLAYBAR", + "PLAYBASE", +) +MODELS_LINEIN_AND_TV = ("AMP",) + AVAILABILITY_CHECK_INTERVAL = datetime.timedelta(minutes=1) AVAILABILITY_TIMEOUT = AVAILABILITY_CHECK_INTERVAL.total_seconds() * 4.5 BATTERY_SCAN_INTERVAL = datetime.timedelta(minutes=15) diff --git a/homeassistant/components/sonos/helpers.py b/homeassistant/components/sonos/helpers.py index a11847d2b0c..3edf23f0c3c 100644 --- a/homeassistant/components/sonos/helpers.py +++ b/homeassistant/components/sonos/helpers.py @@ -3,7 +3,7 @@ from __future__ import annotations from collections.abc import Callable import logging -from typing import TYPE_CHECKING, Any, TypeVar +from typing import TYPE_CHECKING, Any, TypeVar, overload from soco import SoCo from soco.exceptions import SoCoException, SoCoUPnPException @@ -17,6 +17,7 @@ from .exception import SonosUpdateError if TYPE_CHECKING: from .entity import SonosEntity from .household_coordinator import SonosHouseholdCoordinator + from .media import SonosMedia from .speaker import SonosSpeaker UID_PREFIX = "RINCON_" @@ -24,11 +25,31 @@ UID_POSTFIX = "01400" _LOGGER = logging.getLogger(__name__) -_T = TypeVar("_T", bound="SonosSpeaker | SonosEntity | SonosHouseholdCoordinator") +_T = TypeVar( + "_T", bound="SonosSpeaker | SonosMedia | SonosEntity | SonosHouseholdCoordinator" +) _R = TypeVar("_R") _P = ParamSpec("_P") +@overload +def soco_error( + errorcodes: None = ..., +) -> Callable[ # type: ignore[misc] + [Callable[Concatenate[_T, _P], _R]], Callable[Concatenate[_T, _P], _R] +]: + ... + + +@overload +def soco_error( + errorcodes: list[str], +) -> Callable[ # type: ignore[misc] + [Callable[Concatenate[_T, _P], _R]], Callable[Concatenate[_T, _P], _R | None] +]: + ... + + def soco_error( errorcodes: list[str] | None = None, ) -> Callable[ # type: ignore[misc] @@ -43,7 +64,7 @@ def soco_error( def wrapper(self: _T, *args: _P.args, **kwargs: _P.kwargs) -> _R | None: """Wrap for all soco UPnP exception.""" - args_soco = next((arg for arg in args if isinstance(arg, SoCo)), None) + args_soco = next((arg for arg in args if isinstance(arg, SoCo)), None) # type: ignore[attr-defined] try: result = funct(self, *args, **kwargs) except (OSError, SoCoException, SoCoUPnPException) as err: @@ -61,7 +82,7 @@ def soco_error( message = f"Error calling {function} on {target}: {err}" raise SonosUpdateError(message) from err - dispatch_soco = args_soco or self.soco + dispatch_soco = args_soco or self.soco # type: ignore[union-attr] dispatcher_send( self.hass, f"{SONOS_SPEAKER_ACTIVITY}-{dispatch_soco.uid}", diff --git a/homeassistant/components/sonos/manifest.json b/homeassistant/components/sonos/manifest.json index 6f482e92dc9..9144ca559f2 100644 --- a/homeassistant/components/sonos/manifest.json +++ b/homeassistant/components/sonos/manifest.json @@ -3,7 +3,7 @@ "name": "Sonos", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sonos", - "requirements": ["soco==0.26.4"], + "requirements": ["soco==0.27.1"], "dependencies": ["ssdp"], "after_dependencies": ["plex", "spotify", "zeroconf", "media_source"], "zeroconf": ["_sonos._tcp.local."], diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 65e8c4111ae..16d805a578a 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -3,7 +3,6 @@ from __future__ import annotations from asyncio import run_coroutine_threadsafe import datetime -import json import logging from typing import Any @@ -50,7 +49,7 @@ from homeassistant.components.media_player.const import ( SUPPORT_VOLUME_SET, ) from homeassistant.components.plex.const import PLEX_URI_SCHEME -from homeassistant.components.plex.services import lookup_plex_media +from homeassistant.components.plex.services import process_plex_payload from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TIME, STATE_IDLE, STATE_PAUSED, STATE_PLAYING from homeassistant.core import HomeAssistant, ServiceCall, callback @@ -64,6 +63,9 @@ from .const import ( DATA_SONOS, DOMAIN as SONOS_DOMAIN, MEDIA_TYPES_TO_SONOS, + MODELS_LINEIN_AND_TV, + MODELS_LINEIN_ONLY, + MODELS_TV_ONLY, PLAYABLE_MEDIA_TYPES, SONOS_CREATE_MEDIA_PLAYER, SONOS_MEDIA_UPDATED, @@ -474,20 +476,17 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): soco.add_to_queue(favorite.reference) soco.play_from_queue(0) - @property # type: ignore[misc] + @property def source_list(self) -> list[str]: """List of available input sources.""" - sources = [fav.title for fav in self.speaker.favorites] - - model = self.coordinator.model_name.upper() - if "PLAY:5" in model or "CONNECT" in model: - sources += [SOURCE_LINEIN] - elif "PLAYBAR" in model: - sources += [SOURCE_LINEIN, SOURCE_TV] - elif "BEAM" in model or "PLAYBASE" in model: - sources += [SOURCE_TV] - - return sources + model = self.coordinator.model_name.split()[-1].upper() + if model in MODELS_LINEIN_ONLY: + return [SOURCE_LINEIN] + if model in MODELS_TV_ONLY: + return [SOURCE_TV] + if model in MODELS_LINEIN_AND_TV: + return [SOURCE_LINEIN, SOURCE_TV] + return [] @soco_error(UPNP_ERRORS_TO_IGNORE) def media_play(self) -> None: @@ -567,18 +566,17 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): soco = self.coordinator.soco if media_id and media_id.startswith(PLEX_URI_SCHEME): plex_plugin = self.speaker.plex_plugin - media_id = media_id[len(PLEX_URI_SCHEME) :] - payload = json.loads(media_id) - if isinstance(payload, dict): - shuffle = payload.pop("shuffle", False) - else: - shuffle = False - media = lookup_plex_media(self.hass, media_type, json.dumps(payload)) - if not kwargs.get(ATTR_MEDIA_ENQUEUE): - soco.clear_queue() - if shuffle: + result = process_plex_payload( + self.hass, media_type, media_id, supports_playqueues=False + ) + if result.shuffle: self.set_shuffle(True) - plex_plugin.play_now(media) + if kwargs.get(ATTR_MEDIA_ENQUEUE): + plex_plugin.add_to_queue(result.media) + else: + soco.clear_queue() + plex_plugin.add_to_queue(result.media) + soco.play_from_queue(0) return share_link = self.coordinator.share_link diff --git a/homeassistant/components/sonos/number.py b/homeassistant/components/sonos/number.py index c9b8ec47583..b0a10690ce6 100644 --- a/homeassistant/components/sonos/number.py +++ b/homeassistant/components/sonos/number.py @@ -19,6 +19,7 @@ LEVEL_TYPES = { "audio_delay": (0, 5), "bass": (-10, 10), "treble": (-10, 10), + "sub_gain": (-15, 15), } _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sonos/services.yaml b/homeassistant/components/sonos/services.yaml index 4f04b2407ff..a172b45ecd9 100644 --- a/homeassistant/components/sonos/services.yaml +++ b/homeassistant/components/sonos/services.yaml @@ -4,8 +4,7 @@ join: fields: master: name: Master - description: - Entity ID of the player that should become the coordinator of the group. + description: Entity ID of the player that should become the coordinator of the group. required: true selector: entity: diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index 08efa3571c6..ba4fec0cf57 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -152,6 +152,7 @@ class SonosSpeaker: self.dialog_level: bool | None = None self.night_mode: bool | None = None self.sub_enabled: bool | None = None + self.sub_gain: int | None = None self.surround_enabled: bool | None = None # Misc features @@ -492,7 +493,7 @@ class SonosSpeaker: if bool_var in variables: setattr(self, bool_var, variables[bool_var] == "1") - for int_var in ("audio_delay", "bass", "treble"): + for int_var in ("audio_delay", "bass", "treble", "sub_gain"): if int_var in variables: setattr(self, int_var, variables[int_var]) diff --git a/homeassistant/components/spaceapi/__init__.py b/homeassistant/components/spaceapi/__init__.py index e2ca5b972f8..c18f150a925 100644 --- a/homeassistant/components/spaceapi/__init__.py +++ b/homeassistant/components/spaceapi/__init__.py @@ -14,6 +14,7 @@ from homeassistant.const import ( CONF_ADDRESS, CONF_EMAIL, CONF_ENTITY_ID, + CONF_LOCATION, CONF_SENSORS, CONF_STATE, CONF_URL, @@ -55,7 +56,6 @@ CONF_ICON_OPEN = "icon_open" CONF_ICONS = "icons" CONF_IRC = "irc" CONF_ISSUE_REPORT_CHANNELS = "issue_report_channels" -CONF_LOCATION = "location" CONF_SPACEFED = "spacefed" CONF_SPACENET = "spacenet" CONF_SPACESAML = "spacesaml" diff --git a/homeassistant/components/speedtestdotnet/sensor.py b/homeassistant/components/speedtestdotnet/sensor.py index aa4cd72f746..4f16c012fa6 100644 --- a/homeassistant/components/speedtestdotnet/sensor.py +++ b/homeassistant/components/speedtestdotnet/sensor.py @@ -43,10 +43,11 @@ async def async_setup_entry( ) -class SpeedtestSensor(CoordinatorEntity, RestoreEntity, SensorEntity): +class SpeedtestSensor( + CoordinatorEntity[SpeedTestDataCoordinator], RestoreEntity, SensorEntity +): """Implementation of a speedtest.net sensor.""" - coordinator: SpeedTestDataCoordinator entity_description: SpeedtestSensorEntityDescription _attr_icon = ICON diff --git a/homeassistant/components/speedtestdotnet/translations/fr.json b/homeassistant/components/speedtestdotnet/translations/fr.json index 41a1ae7dc2d..4ad47bb6a3e 100644 --- a/homeassistant/components/speedtestdotnet/translations/fr.json +++ b/homeassistant/components/speedtestdotnet/translations/fr.json @@ -6,7 +6,7 @@ }, "step": { "user": { - "description": "Voulez-vous commencer la configuration ?" + "description": "Voulez-vous commencer la configuration\u00a0?" } } }, diff --git a/homeassistant/components/spider/manifest.json b/homeassistant/components/spider/manifest.json index 56cd6876e9f..f4c6225a649 100644 --- a/homeassistant/components/spider/manifest.json +++ b/homeassistant/components/spider/manifest.json @@ -7,4 +7,4 @@ "config_flow": true, "iot_class": "cloud_polling", "loggers": ["spiderpy"] -} \ No newline at end of file +} diff --git a/homeassistant/components/spider/translations/fr.json b/homeassistant/components/spider/translations/fr.json index 8658343db6a..a5e5d021cd2 100644 --- a/homeassistant/components/spider/translations/fr.json +++ b/homeassistant/components/spider/translations/fr.json @@ -4,7 +4,7 @@ "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." }, "error": { - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/spotify/config_flow.py b/homeassistant/components/spotify/config_flow.py index bd01fa64acb..e1780ff9d40 100644 --- a/homeassistant/components/spotify/config_flow.py +++ b/homeassistant/components/spotify/config_flow.py @@ -5,7 +5,6 @@ import logging from typing import Any from spotipy import Spotify -import voluptuous as vol from homeassistant.components import persistent_notification from homeassistant.config_entries import ConfigEntry @@ -83,7 +82,6 @@ class SpotifyFlowHandler( return self.async_show_form( step_id="reauth_confirm", description_placeholders={"account": self.reauth_entry.data["id"]}, - data_schema=vol.Schema({}), errors={}, ) diff --git a/homeassistant/components/spotify/media_player.py b/homeassistant/components/spotify/media_player.py index 2b62fdd78c4..3ed7be99041 100644 --- a/homeassistant/components/spotify/media_player.py +++ b/homeassistant/components/spotify/media_player.py @@ -47,7 +47,7 @@ from .util import fetch_image_url _LOGGER = logging.getLogger(__name__) -SCAN_INTERVAL = timedelta(seconds=30) +SCAN_INTERVAL = timedelta(minutes=1) SUPPORT_SPOTIFY = ( SUPPORT_BROWSE_MEDIA @@ -117,6 +117,7 @@ class SpotifyMediaPlayer(MediaPlayerEntity): _attr_icon = "mdi:spotify" _attr_media_content_type = MEDIA_TYPE_MUSIC _attr_media_image_remotely_accessible = False + _attr_entity_registry_enabled_default = False def __init__( self, diff --git a/homeassistant/components/spotify/translations/el.json b/homeassistant/components/spotify/translations/el.json index 099a7ebcd3b..1b9aadb7caf 100644 --- a/homeassistant/components/spotify/translations/el.json +++ b/homeassistant/components/spotify/translations/el.json @@ -15,7 +15,7 @@ }, "reauth_confirm": { "description": "\u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 Spotify \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03c0\u03b1\u03bb\u03b7\u03b8\u03b5\u03cd\u03c3\u03b5\u03b9 \u03be\u03b1\u03bd\u03ac \u03c4\u03bf\u03bd \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03bc\u03b5 \u03c4\u03bf Spotify \u03b3\u03b9\u03b1 \u03c4\u03bf\u03bd \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc: {account}", - "title": "\u0395\u03c0\u03b1\u03bb\u03b7\u03b8\u03b5\u03cd\u03c3\u03c4\u03b5 \u03c4\u03bf\u03bd \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03bc\u03b5 \u03c4\u03bf Spotify" + "title": "\u0395\u03c0\u03b1\u03bd\u03b1\u03bb\u03b7\u03c0\u03c4\u03b9\u03ba\u03cc\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2" } } }, diff --git a/homeassistant/components/spotify/translations/fr.json b/homeassistant/components/spotify/translations/fr.json index 4422ddef176..6cfcb3486fe 100644 --- a/homeassistant/components/spotify/translations/fr.json +++ b/homeassistant/components/spotify/translations/fr.json @@ -3,7 +3,7 @@ "abort": { "authorize_url_timeout": "D\u00e9lai d'expiration g\u00e9n\u00e9rant une URL d'autorisation.", "missing_configuration": "L'int\u00e9gration Spotify n'est pas configur\u00e9e. Veuillez suivre la documentation.", - "no_url_available": "Aucune URL disponible. Pour plus d'informations sur cette erreur, [consultez la section d'aide] ( {docs_url} )", + "no_url_available": "Aucune URL disponible. Pour plus d'informations sur cette erreur, [consultez la section d'aide]({docs_url})", "reauth_account_mismatch": "Le compte Spotify authentifi\u00e9 ne correspond pas au compte requis pour la r\u00e9-authentification." }, "create_entry": { diff --git a/homeassistant/components/sql/manifest.json b/homeassistant/components/sql/manifest.json index dfc58474366..d8ced1625ad 100644 --- a/homeassistant/components/sql/manifest.json +++ b/homeassistant/components/sql/manifest.json @@ -2,7 +2,7 @@ "domain": "sql", "name": "SQL", "documentation": "https://www.home-assistant.io/integrations/sql", - "requirements": ["sqlalchemy==1.4.27"], + "requirements": ["sqlalchemy==1.4.32"], "codeowners": ["@dgomes"], "iot_class": "local_polling" } diff --git a/homeassistant/components/squeezebox/browse_media.py b/homeassistant/components/squeezebox/browse_media.py index 81d0231ae70..76627119b85 100644 --- a/homeassistant/components/squeezebox/browse_media.py +++ b/homeassistant/components/squeezebox/browse_media.py @@ -1,4 +1,7 @@ """Support for media browsing.""" +import contextlib + +from homeassistant.components import media_source from homeassistant.components.media_player import BrowseError, BrowseMedia from homeassistant.components.media_player.const import ( MEDIA_CLASS_ALBUM, @@ -134,7 +137,7 @@ async def build_item_response(entity, player, payload): ) -async def library_payload(player): +async def library_payload(hass, player): """Create response payload to describe contents of library.""" library_info = { "title": "Music Library", @@ -161,13 +164,29 @@ async def library_payload(player): media_content_type=item, can_play=True, can_expand=True, + thumbnail="https://brands.home-assistant.io/_/squeezebox/logo.png", ) ) + with contextlib.suppress(media_source.BrowseError): + item = await media_source.async_browse_media( + hass, None, content_filter=media_source_content_filter + ) + # If domain is None, it's overview of available sources + if item.domain is None: + library_info["children"].extend(item.children) + else: + library_info["children"].append(item) + response = BrowseMedia(**library_info) return response +def media_source_content_filter(item: BrowseMedia) -> bool: + """Content filter for media sources.""" + return item.media_content_type.startswith("audio/") + + async def generate_playlist(player, payload): """Generate playlist from browsing payload.""" media_type = payload["search_type"] diff --git a/homeassistant/components/squeezebox/media_player.py b/homeassistant/components/squeezebox/media_player.py index 5956b9fdf06..d8a1c29b723 100644 --- a/homeassistant/components/squeezebox/media_player.py +++ b/homeassistant/components/squeezebox/media_player.py @@ -8,7 +8,11 @@ import logging from pysqueezebox import Server, async_discover import voluptuous as vol +from homeassistant.components import media_source from homeassistant.components.media_player import MediaPlayerEntity +from homeassistant.components.media_player.browse_media import ( + async_process_play_media_url, +) from homeassistant.components.media_player.const import ( ATTR_MEDIA_ENQUEUE, MEDIA_TYPE_MUSIC, @@ -53,7 +57,12 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_call_later from homeassistant.util.dt import utcnow -from .browse_media import build_item_response, generate_playlist, library_payload +from .browse_media import ( + build_item_response, + generate_playlist, + library_payload, + media_source_content_filter, +) from .const import DISCOVERY_TASK, DOMAIN, KNOWN_PLAYERS, PLAYER_DISCOVERY_UNSUB SERVICE_CALL_METHOD = "call_method" @@ -460,7 +469,14 @@ class SqueezeBoxEntity(MediaPlayerEntity): if kwargs.get(ATTR_MEDIA_ENQUEUE): cmd = "add" - if media_type == MEDIA_TYPE_MUSIC: + if media_source.is_media_source_id(media_id): + media_type = MEDIA_TYPE_MUSIC + play_item = await media_source.async_resolve_media(self.hass, media_id) + media_id = play_item.url + + if media_type in MEDIA_TYPE_MUSIC: + media_id = async_process_play_media_url(self.hass, media_id) + await self._player.async_load_url(media_id, cmd) return @@ -554,7 +570,12 @@ class SqueezeBoxEntity(MediaPlayerEntity): ) if media_content_type in [None, "library"]: - return await library_payload(self._player) + return await library_payload(self.hass, self._player) + + if media_source.is_media_source_id(media_content_id): + return await media_source.async_browse_media( + self.hass, media_content_id, content_filter=media_source_content_filter + ) payload = { "search_type": media_content_type, diff --git a/homeassistant/components/squeezebox/services.yaml b/homeassistant/components/squeezebox/services.yaml index 293b89fe35a..4c2d34ba88b 100644 --- a/homeassistant/components/squeezebox/services.yaml +++ b/homeassistant/components/squeezebox/services.yaml @@ -34,7 +34,7 @@ call_query: name: Command description: Command to pass to Logitech Media Server (p0 in the CLI documentation). required: true - example: 'albums' + example: "albums" selector: text: parameters: diff --git a/homeassistant/components/squeezebox/translations/fr.json b/homeassistant/components/squeezebox/translations/fr.json index 1dca7a14308..7dda170361d 100644 --- a/homeassistant/components/squeezebox/translations/fr.json +++ b/homeassistant/components/squeezebox/translations/fr.json @@ -6,7 +6,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "no_server_found": "Impossible de d\u00e9couvrir automatiquement le serveur.", "unknown": "Erreur inattendue" }, diff --git a/homeassistant/components/srp_energy/translations/fr.json b/homeassistant/components/srp_energy/translations/fr.json index 4ce2a6dbbea..b8544d4d469 100644 --- a/homeassistant/components/srp_energy/translations/fr.json +++ b/homeassistant/components/srp_energy/translations/fr.json @@ -6,7 +6,7 @@ "error": { "cannot_connect": "\u00c9chec de connexion", "invalid_account": "L'ID de compte doit \u00eatre un num\u00e9ro \u00e0 9 chiffres", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/ssdp/__init__.py b/homeassistant/components/ssdp/__init__.py index b1b06832918..cdc5fe3242e 100644 --- a/homeassistant/components/ssdp/__init__.py +++ b/homeassistant/components/ssdp/__init__.py @@ -11,10 +11,15 @@ import logging from typing import Any from async_upnp_client.aiohttp import AiohttpSessionRequester -from async_upnp_client.const import DeviceOrServiceType, SsdpHeaders, SsdpSource +from async_upnp_client.const import ( + AddressTupleVXType, + DeviceOrServiceType, + SsdpHeaders, + SsdpSource, +) from async_upnp_client.description_cache import DescriptionCache -from async_upnp_client.ssdp import SSDP_PORT -from async_upnp_client.ssdp_listener import SsdpDevice, SsdpListener +from async_upnp_client.ssdp import SSDP_PORT, determine_source_target, is_ipv4_address +from async_upnp_client.ssdp_listener import SsdpDevice, SsdpDeviceTracker, SsdpListener from async_upnp_client.utils import CaseInsensitiveDict from homeassistant import config_entries @@ -372,17 +377,10 @@ class Scanner: async def _async_build_source_set(self) -> set[IPv4Address | IPv6Address]: """Build the list of ssdp sources.""" - adapters = await network.async_get_adapters(self.hass) - sources: set[IPv4Address | IPv6Address] = set() - if network.async_only_default_interface_enabled(adapters): - sources.add(IPv4Address("0.0.0.0")) - return sources - return { source_ip for source_ip in await network.async_get_enabled_source_ips(self.hass) - if not source_ip.is_loopback - and not (isinstance(source_ip, IPv6Address) and source_ip.is_global) + if not source_ip.is_loopback and not source_ip.is_global } async def async_scan(self, *_: Any) -> None: @@ -401,11 +399,8 @@ class Scanner: # address. This matches pysonos' behavior # https://github.com/amelchio/pysonos/blob/d4329b4abb657d106394ae69357805269708c996/pysonos/discovery.py#L120 for listener in self._ssdp_listeners: - try: - IPv4Address(listener.source_ip) - except ValueError: - continue - await listener.async_search((str(IPV4_BROADCAST), SSDP_PORT)) + if is_ipv4_address(listener.source): + await listener.async_search((str(IPV4_BROADCAST), SSDP_PORT)) async def async_start(self) -> None: """Start the scanners.""" @@ -425,11 +420,26 @@ class Scanner: async def _async_start_ssdp_listeners(self) -> None: """Start the SSDP Listeners.""" + # Devices are shared between all sources. + device_tracker = SsdpDeviceTracker() for source_ip in await self._async_build_source_set(): + source_ip_str = str(source_ip) + if source_ip.version == 6: + source_tuple: AddressTupleVXType = ( + source_ip_str, + 0, + 0, + int(getattr(source_ip, "scope_id")), + ) + else: + source_tuple = (source_ip_str, 0) + source, target = determine_source_target(source_tuple) self._ssdp_listeners.append( SsdpListener( async_callback=self._ssdp_listener_callback, - source_ip=source_ip, + source=source, + target=target, + device_tracker=device_tracker, ) ) results = await asyncio.gather( @@ -439,9 +449,9 @@ class Scanner: failed_listeners = [] for idx, result in enumerate(results): if isinstance(result, Exception): - _LOGGER.warning( + _LOGGER.debug( "Failed to setup listener for %s: %s", - self._ssdp_listeners[idx].source_ip, + self._ssdp_listeners[idx].source, result, ) failed_listeners.append(self._ssdp_listeners[idx]) diff --git a/homeassistant/components/ssdp/manifest.json b/homeassistant/components/ssdp/manifest.json index 93512d08238..e55f16d9247 100644 --- a/homeassistant/components/ssdp/manifest.json +++ b/homeassistant/components/ssdp/manifest.json @@ -2,7 +2,7 @@ "domain": "ssdp", "name": "Simple Service Discovery Protocol (SSDP)", "documentation": "https://www.home-assistant.io/integrations/ssdp", - "requirements": ["async-upnp-client==0.23.5"], + "requirements": ["async-upnp-client==0.27.0"], "dependencies": ["network"], "after_dependencies": ["zeroconf"], "codeowners": [], diff --git a/homeassistant/components/starline/strings.json b/homeassistant/components/starline/strings.json index 33e28f9a29d..10e99f93814 100644 --- a/homeassistant/components/starline/strings.json +++ b/homeassistant/components/starline/strings.json @@ -38,4 +38,4 @@ "error_auth_mfa": "Incorrect code" } } -} \ No newline at end of file +} diff --git a/homeassistant/components/statistics/sensor.py b/homeassistant/components/statistics/sensor.py index fb5daa97475..ed2352657f4 100644 --- a/homeassistant/components/statistics/sensor.py +++ b/homeassistant/components/statistics/sensor.py @@ -12,8 +12,7 @@ from typing import Any, Literal, cast import voluptuous as vol from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN -from homeassistant.components.recorder.models import States -from homeassistant.components.recorder.util import execute, session_scope +from homeassistant.components.recorder import get_instance, history from homeassistant.components.sensor import ( PLATFORM_SCHEMA, SensorDeviceClass, @@ -303,7 +302,7 @@ class StatisticsSensor(SensorEntity): if "recorder" in self.hass.config.components: self.hass.async_create_task(self._initialize_from_database()) - async_at_start(self.hass, async_stats_sensor_startup) + self.async_on_remove(async_at_start(self.hass, async_stats_sensor_startup)) def _add_state_to_queue(self, new_state: State) -> None: """Add the state to the queue.""" @@ -470,6 +469,33 @@ class StatisticsSensor(SensorEntity): self.hass, _scheduled_update, next_to_purge_timestamp ) + def _fetch_states_from_database(self) -> list[State]: + """Fetch the states from the database.""" + _LOGGER.debug("%s: initializing values from the database", self.entity_id) + lower_entity_id = self._source_entity_id.lower() + if self._samples_max_age is not None: + start_date = ( + dt_util.utcnow() - self._samples_max_age - timedelta(microseconds=1) + ) + _LOGGER.debug( + "%s: retrieve records not older then %s", + self.entity_id, + start_date, + ) + else: + start_date = datetime.fromtimestamp(0, tz=dt_util.UTC) + _LOGGER.debug("%s: retrieving all records", self.entity_id) + entity_states = history.state_changes_during_period( + self.hass, + start_date, + entity_id=lower_entity_id, + descending=True, + limit=self._samples_max_buffer_size, + include_start_time_state=False, + ) + # Need to cast since minimal responses is not passed in + return cast(list[State], entity_states.get(lower_entity_id, [])) + async def _initialize_from_database(self) -> None: """Initialize the list of states from the database. @@ -480,31 +506,9 @@ class StatisticsSensor(SensorEntity): If MaxAge is provided then query will restrict to entries younger then current datetime - MaxAge. """ - - _LOGGER.debug("%s: initializing values from the database", self.entity_id) - - with session_scope(hass=self.hass) as session: - query = session.query(States).filter( - States.entity_id == self._source_entity_id.lower() - ) - - if self._samples_max_age is not None: - records_older_then = dt_util.utcnow() - self._samples_max_age - _LOGGER.debug( - "%s: retrieve records not older then %s", - self.entity_id, - records_older_then, - ) - query = query.filter(States.last_updated >= records_older_then) - else: - _LOGGER.debug("%s: retrieving all records", self.entity_id) - - query = query.order_by(States.last_updated.desc()).limit( - self._samples_max_buffer_size - ) - states = execute(query, to_native=True, validate_entity_ids=False) - - if states: + if states := await get_instance(self.hass).async_add_executor_job( + self._fetch_states_from_database + ): for state in reversed(states): self._add_state_to_queue(state) diff --git a/homeassistant/components/steamist/config_flow.py b/homeassistant/components/steamist/config_flow.py index c0ec18157b6..f182189a9c7 100644 --- a/homeassistant/components/steamist/config_flow.py +++ b/homeassistant/components/steamist/config_flow.py @@ -10,14 +10,14 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.components import dhcp -from homeassistant.const import CONF_DEVICE, CONF_HOST, CONF_NAME +from homeassistant.const import CONF_DEVICE, CONF_HOST, CONF_MODEL, CONF_NAME from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import device_registry as dr from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.typing import DiscoveryInfoType -from .const import CONF_MODEL, CONNECTION_EXCEPTIONS, DISCOVER_SCAN_TIMEOUT, DOMAIN +from .const import CONNECTION_EXCEPTIONS, DISCOVER_SCAN_TIMEOUT, DOMAIN from .discovery import ( async_discover_device, async_discover_devices, diff --git a/homeassistant/components/steamist/const.py b/homeassistant/components/steamist/const.py index 2375bdaf92f..cacd79b77ac 100644 --- a/homeassistant/components/steamist/const.py +++ b/homeassistant/components/steamist/const.py @@ -8,8 +8,6 @@ DOMAIN = "steamist" CONNECTION_EXCEPTIONS = (asyncio.TimeoutError, aiohttp.ClientError) -CONF_MODEL = "model" - STARTUP_SCAN_TIMEOUT = 5 DISCOVER_SCAN_TIMEOUT = 10 diff --git a/homeassistant/components/steamist/discovery.py b/homeassistant/components/steamist/discovery.py index 773e56d6612..7600503658f 100644 --- a/homeassistant/components/steamist/discovery.py +++ b/homeassistant/components/steamist/discovery.py @@ -9,12 +9,12 @@ from discovery30303 import AIODiscovery30303, Device30303 from homeassistant import config_entries from homeassistant.components import network -from homeassistant.const import CONF_NAME +from homeassistant.const import CONF_MODEL, CONF_NAME from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import device_registry as dr from homeassistant.util.network import is_ip_address -from .const import CONF_MODEL, DISCOVER_SCAN_TIMEOUT, DISCOVERY, DOMAIN +from .const import DISCOVER_SCAN_TIMEOUT, DISCOVERY, DOMAIN _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/steamist/entity.py b/homeassistant/components/steamist/entity.py index 0a2bc239633..4692b48d314 100644 --- a/homeassistant/components/steamist/entity.py +++ b/homeassistant/components/steamist/entity.py @@ -4,20 +4,17 @@ from __future__ import annotations from aiosteamist import SteamistStatus from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_HOST, CONF_NAME +from homeassistant.const import CONF_HOST, CONF_MODEL, CONF_NAME from homeassistant.helpers import device_registry as dr from homeassistant.helpers.entity import DeviceInfo, Entity, EntityDescription from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import CONF_MODEL from .coordinator import SteamistDataUpdateCoordinator -class SteamistEntity(CoordinatorEntity, Entity): +class SteamistEntity(CoordinatorEntity[SteamistDataUpdateCoordinator], Entity): """Representation of an Steamist entity.""" - coordinator: SteamistDataUpdateCoordinator - def __init__( self, coordinator: SteamistDataUpdateCoordinator, diff --git a/homeassistant/components/steamist/manifest.json b/homeassistant/components/steamist/manifest.json index e3095ada47a..057645a1d4c 100644 --- a/homeassistant/components/steamist/manifest.json +++ b/homeassistant/components/steamist/manifest.json @@ -8,11 +8,11 @@ "codeowners": ["@bdraco"], "iot_class": "local_polling", "dhcp": [ - {"registered_devices": true}, + { "registered_devices": true }, { "macaddress": "001E0C*", "hostname": "my[45]50*" } ], "loggers": ["aiosteamist", "discovery30303"] -} \ No newline at end of file +} diff --git a/homeassistant/components/steamist/strings.json b/homeassistant/components/steamist/strings.json index 787866f03b3..7ad5913e718 100644 --- a/homeassistant/components/steamist/strings.json +++ b/homeassistant/components/steamist/strings.json @@ -15,7 +15,7 @@ }, "discovery_confirm": { "description": "Do you want to setup {name} ({ipaddress})?" - } + } }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", diff --git a/homeassistant/components/steamist/translations/fr.json b/homeassistant/components/steamist/translations/fr.json index f3c122a2f2c..0427cb2e87e 100644 --- a/homeassistant/components/steamist/translations/fr.json +++ b/homeassistant/components/steamist/translations/fr.json @@ -3,18 +3,18 @@ "abort": { "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours", - "cannot_connect": "Impossible de se connecter", - "no_devices_found": "Pas d'appareils trouv\u00e9 sur le r\u00e9seau", + "cannot_connect": "\u00c9chec de connexion", + "no_devices_found": "Aucun appareil trouv\u00e9 sur le r\u00e9seau", "not_steamist_device": "Pas un appareil \u00e0 vapeur" }, "error": { - "cannot_connect": "Impossible de se connecter", + "cannot_connect": "\u00c9chec de connexion", "unknown": "Erreur inattendue" }, - "flow_title": "{name} ( {ipaddress} )", + "flow_title": "{name} ({ipaddress})", "step": { "discovery_confirm": { - "description": "Voulez-vous configurer {name} ( {ipaddress} )\u00a0?" + "description": "Voulez-vous configurer {name} ({ipaddress})\u00a0?" }, "pick_device": { "data": { @@ -25,7 +25,7 @@ "data": { "host": "H\u00f4te" }, - "description": "Si vous laissez l'h\u00f4te vide, la d\u00e9couverte sera utilis\u00e9e pour trouver des p\u00e9riph\u00e9riques." + "description": "Si vous laissez l'h\u00f4te vide, la d\u00e9couverte sera utilis\u00e9e pour trouver des appareils." } } } diff --git a/homeassistant/components/steamist/translations/it.json b/homeassistant/components/steamist/translations/it.json index 284eb8e8401..8bcec86344c 100644 --- a/homeassistant/components/steamist/translations/it.json +++ b/homeassistant/components/steamist/translations/it.json @@ -3,15 +3,15 @@ "abort": { "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso", - "cannot_connect": "Connessione non riuscita", + "cannot_connect": "Impossibile connettersi", "no_devices_found": "Nessun dispositivo trovato sulla rete", "not_steamist_device": "Non \u00e8 un dispositivo a vapore" }, "error": { "cannot_connect": "Impossibile connettersi", - "unknown": "Errore inatteso" + "unknown": "Errore imprevisto" }, - "flow_title": "{name} ( {ipaddress} )", + "flow_title": "{name} ({ipaddress})", "step": { "discovery_confirm": { "description": "Vuoi configurare {name} ({ipaddress})?" diff --git a/homeassistant/components/steamist/translations/zh-Hant.json b/homeassistant/components/steamist/translations/zh-Hant.json index c3d9ad377b7..3820aa5dcf2 100644 --- a/homeassistant/components/steamist/translations/zh-Hant.json +++ b/homeassistant/components/steamist/translations/zh-Hant.json @@ -25,7 +25,7 @@ "data": { "host": "\u4e3b\u6a5f\u7aef" }, - "description": "\u5047\u5982\u4e3b\u6a5f\u7aef\u4f4d\u5740\u6b04\u4f4d\u70ba\u7a7a\u767d\uff0c\u5c07\u6703\u63a2\u7d22\u6240\u6709\u53ef\u7528\u88dd\u7f6e\u3002" + "description": "\u5047\u5982\u4e3b\u6a5f\u7aef\u4f4d\u5740\u6b04\u4f4d\u70ba\u7a7a\u767d\uff0c\u5c07\u6703\u641c\u7d22\u6240\u6709\u53ef\u7528\u88dd\u7f6e\u3002" } } } diff --git a/homeassistant/components/stream/__init__.py b/homeassistant/components/stream/__init__.py index 157f20b5b37..abaf367486d 100644 --- a/homeassistant/components/stream/__init__.py +++ b/homeassistant/components/stream/__init__.py @@ -23,7 +23,7 @@ import secrets import threading import time from types import MappingProxyType -from typing import cast +from typing import Any, cast import voluptuous as vol @@ -51,6 +51,7 @@ from .const import ( TARGET_SEGMENT_DURATION_NON_LL_HLS, ) from .core import PROVIDERS, IdleTimer, KeyFrameConverter, StreamOutput, StreamSettings +from .diagnostics import Diagnostics from .hls import HlsStreamOutput, async_setup_hls _LOGGER = logging.getLogger(__name__) @@ -225,6 +226,7 @@ class Stream: if stream_label else _LOGGER ) + self._diagnostics = Diagnostics() def endpoint_url(self, fmt: str) -> str: """Start the stream and returns a url for the output format.""" @@ -259,6 +261,7 @@ class Stream: self.hass, IdleTimer(self.hass, timeout, idle_callback) ) self._outputs[fmt] = provider + return provider def remove_provider(self, provider: StreamOutput) -> None: @@ -310,6 +313,7 @@ class Stream: def update_source(self, new_source: str) -> None: """Restart the stream with a new stream source.""" + self._diagnostics.increment("update_source") self._logger.debug( "Updating stream source %s", redact_credentials(str(new_source)) ) @@ -323,11 +327,13 @@ class Stream: # pylint: disable=import-outside-toplevel from .worker import StreamState, StreamWorkerError, stream_worker - stream_state = StreamState(self.hass, self.outputs) + stream_state = StreamState(self.hass, self.outputs, self._diagnostics) wait_timeout = 0 while not self._thread_quit.wait(timeout=wait_timeout): start_time = time.time() self.hass.add_job(self._async_update_state, True) + self._diagnostics.set_value("keepalive", self.keepalive) + self._diagnostics.increment("start_worker") try: stream_worker( self.source, @@ -337,6 +343,7 @@ class Stream: self._thread_quit, ) except StreamWorkerError as err: + self._diagnostics.increment("worker_error") self._logger.error("Error from stream worker: %s", str(err)) stream_state.discontinuity() @@ -358,22 +365,24 @@ class Stream: if time.time() - start_time > STREAM_RESTART_RESET_TIME: wait_timeout = 0 wait_timeout += STREAM_RESTART_INCREMENT + self._diagnostics.set_value("retry_timeout", wait_timeout) self._logger.debug( "Restarting stream worker in %d seconds: %s", wait_timeout, redact_credentials(str(self.source)), ) - self._worker_finished() - - def _worker_finished(self) -> None: - """Schedule cleanup of all outputs.""" @callback - def remove_outputs() -> None: + def worker_finished() -> None: + # The worker is no checking availability of the stream and can no longer track + # availability so mark it as available, otherwise the frontend may not be able to + # interact with the stream. + if not self.available: + self._async_update_state(True) for provider in self.outputs().values(): self.remove_provider(provider) - self.hass.loop.call_soon_threadsafe(remove_outputs) + self.hass.loop.call_soon_threadsafe(worker_finished) def stop(self) -> None: """Remove outputs and access token.""" @@ -447,6 +456,10 @@ class Stream: width=width, height=height ) + def get_diagnostics(self) -> dict[str, Any]: + """Return diagnostics information for the stream.""" + return self._diagnostics.as_dict() + def _should_retry() -> bool: """Return true if worker failures should be retried, for disabling during tests.""" diff --git a/homeassistant/components/stream/diagnostics.py b/homeassistant/components/stream/diagnostics.py new file mode 100644 index 00000000000..47370eeb5f9 --- /dev/null +++ b/homeassistant/components/stream/diagnostics.py @@ -0,0 +1,33 @@ +"""Diagnostics for debugging. + +The stream component does not have config entries itself, and all diagnostics +information is managed by dependent components (e.g. camera) +""" + +from __future__ import annotations + +from collections import Counter +from typing import Any + + +class Diagnostics: + """Holds diagnostics counters and key/values.""" + + def __init__(self) -> None: + """Initialize Diagnostics.""" + self._counter: Counter = Counter() + self._values: dict[str, Any] = {} + + def increment(self, key: str) -> None: + """Increment a counter for the spcified key/event.""" + self._counter.update(Counter({key: 1})) + + def set_value(self, key: str, value: Any) -> None: + """Update a key/value pair.""" + self._values[key] = value + + def as_dict(self) -> dict[str, Any]: + """Return diagnostics as a debug dictionary.""" + result = {k: self._counter[k] for k in self._counter} + result.update(self._values) + return result diff --git a/homeassistant/components/stream/manifest.json b/homeassistant/components/stream/manifest.json index 1fe64defe36..f2c56d0af80 100644 --- a/homeassistant/components/stream/manifest.json +++ b/homeassistant/components/stream/manifest.json @@ -2,7 +2,7 @@ "domain": "stream", "name": "Stream", "documentation": "https://www.home-assistant.io/integrations/stream", - "requirements": ["PyTurboJPEG==1.6.5", "av==8.1.0"], + "requirements": ["PyTurboJPEG==1.6.6", "av==9.0.0"], "dependencies": ["http"], "codeowners": ["@hunterjm", "@uvjustin", "@allenporter"], "quality_scale": "internal", diff --git a/homeassistant/components/stream/worker.py b/homeassistant/components/stream/worker.py index 42a34cf4e8c..bde5ab0fb05 100644 --- a/homeassistant/components/stream/worker.py +++ b/homeassistant/components/stream/worker.py @@ -27,6 +27,7 @@ from .const import ( SOURCE_TIMEOUT, ) from .core import KeyFrameConverter, Part, Segment, StreamOutput, StreamSettings +from .diagnostics import Diagnostics from .hls import HlsStreamOutput _LOGGER = logging.getLogger(__name__) @@ -52,6 +53,7 @@ class StreamState: self, hass: HomeAssistant, outputs_callback: Callable[[], Mapping[str, StreamOutput]], + diagnostics: Diagnostics, ) -> None: """Initialize StreamState.""" self._stream_id: int = 0 @@ -62,6 +64,7 @@ class StreamState: # sequence gets incremented before the first segment so the first segment # has a sequence number of 0. self._sequence = -1 + self._diagnostics = diagnostics @property def sequence(self) -> int: @@ -93,6 +96,11 @@ class StreamState: """Return the active stream outputs.""" return list(self._outputs_callback().values()) + @property + def diagnostics(self) -> Diagnostics: + """Return diagnostics object.""" + return self._diagnostics + class StreamMuxer: """StreamMuxer re-packages video/audio packets for output.""" @@ -468,6 +476,10 @@ def stream_worker( # Some audio streams do not have a profile and throw errors when remuxing if audio_stream and audio_stream.profile is None: audio_stream = None + stream_state.diagnostics.set_value("container_format", container.format.name) + stream_state.diagnostics.set_value("video_codec", video_stream.name) + if audio_stream: + stream_state.diagnostics.set_value("audio_codec", audio_stream.name) dts_validator = TimestampValidator() container_packets = PeekIterator( diff --git a/homeassistant/components/streamlabswater/services.yaml b/homeassistant/components/streamlabswater/services.yaml index 3a483fec264..b54c2cf15eb 100644 --- a/homeassistant/components/streamlabswater/services.yaml +++ b/homeassistant/components/streamlabswater/services.yaml @@ -9,5 +9,5 @@ set_away_mode: selector: select: options: - - 'away' - - 'home' + - "away" + - "home" diff --git a/homeassistant/components/subaru/__init__.py b/homeassistant/components/subaru/__init__.py index a252e61b690..6e05586706f 100644 --- a/homeassistant/components/subaru/__init__.py +++ b/homeassistant/components/subaru/__init__.py @@ -10,6 +10,7 @@ from homeassistant.const import CONF_DEVICE_ID, CONF_PASSWORD, CONF_PIN, CONF_US from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import aiohttp_client +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import ( @@ -21,6 +22,7 @@ from .const import ( ENTRY_COORDINATOR, ENTRY_VEHICLES, FETCH_INTERVAL, + MANUFACTURER, PLATFORMS, UPDATE_INTERVAL, VEHICLE_API_GEN, @@ -154,3 +156,12 @@ def get_vehicle_info(controller, vin): VEHICLE_LAST_UPDATE: 0, } return info + + +def get_device_info(vehicle_info): + """Return DeviceInfo object based on vehicle info.""" + return DeviceInfo( + identifiers={(DOMAIN, vehicle_info[VEHICLE_VIN])}, + manufacturer=MANUFACTURER, + name=vehicle_info[VEHICLE_NAME], + ) diff --git a/homeassistant/components/subaru/config_flow.py b/homeassistant/components/subaru/config_flow.py index f21abbdb56f..788b6f04fd5 100644 --- a/homeassistant/components/subaru/config_flow.py +++ b/homeassistant/components/subaru/config_flow.py @@ -19,6 +19,8 @@ from homeassistant.helpers import aiohttp_client, config_validation as cv from .const import CONF_COUNTRY, CONF_UPDATE_ENABLED, DOMAIN _LOGGER = logging.getLogger(__name__) +CONF_CONTACT_METHOD = "contact_method" +CONF_VALIDATION_CODE = "validation_code" PIN_SCHEMA = vol.Schema({vol.Required(CONF_PIN): str}) @@ -47,6 +49,9 @@ class SubaruConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): _LOGGER.error("Unable to communicate with Subaru API: %s", ex.message) return self.async_abort(reason="cannot_connect") else: + if not self.controller.device_registered: + _LOGGER.debug("2FA validation is required") + return await self.async_step_two_factor() if self.controller.is_pin_required(): return await self.async_step_pin() return self.async_create_entry( @@ -103,13 +108,60 @@ class SubaruConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): device_name=device_name, country=data[CONF_COUNTRY], ) - _LOGGER.debug( - "Setting up first time connection to Subaru API. This may take up to 20 seconds" - ) + _LOGGER.debug("Setting up first time connection to Subaru API") if await self.controller.connect(): - _LOGGER.debug("Successfully authenticated and authorized with Subaru API") + _LOGGER.debug("Successfully authenticated with Subaru API") self.config_data.update(data) + async def async_step_two_factor(self, user_input=None): + """Select contact method and request 2FA code from Subaru.""" + error = None + if user_input: + # self.controller.contact_methods is a dict: + # {"phone":"555-555-5555", "userName":"my@email.com"} + selected_method = next( + k + for k, v in self.controller.contact_methods.items() + if v == user_input[CONF_CONTACT_METHOD] + ) + if await self.controller.request_auth_code(selected_method): + return await self.async_step_two_factor_validate() + return self.async_abort(reason="two_factor_request_failed") + + data_schema = vol.Schema( + { + vol.Required(CONF_CONTACT_METHOD): vol.In( + list(self.controller.contact_methods.values()) + ) + } + ) + return self.async_show_form( + step_id="two_factor", data_schema=data_schema, errors=error + ) + + async def async_step_two_factor_validate(self, user_input=None): + """Validate received 2FA code with Subaru.""" + error = None + if user_input: + try: + vol.Match(r"^[0-9]{6}$")(user_input[CONF_VALIDATION_CODE]) + if await self.controller.submit_auth_code( + user_input[CONF_VALIDATION_CODE] + ): + if self.controller.is_pin_required(): + return await self.async_step_pin() + return self.async_create_entry( + title=self.config_data[CONF_USERNAME], data=self.config_data + ) + error = {"base": "incorrect_validation_code"} + except vol.Invalid: + error = {"base": "bad_validation_code_format"} + + data_schema = vol.Schema({vol.Required(CONF_VALIDATION_CODE): str}) + return self.async_show_form( + step_id="two_factor_validate", data_schema=data_schema, errors=error + ) + async def async_step_pin(self, user_input=None): """Handle second part of config flow, if required.""" error = None diff --git a/homeassistant/components/subaru/const.py b/homeassistant/components/subaru/const.py index 596923cbc06..3ad7dd58af5 100644 --- a/homeassistant/components/subaru/const.py +++ b/homeassistant/components/subaru/const.py @@ -1,4 +1,6 @@ """Constants for the Subaru integration.""" +from subarulink.const import ALL_DOORS, DRIVERS_DOOR, TAILGATE_DOOR + from homeassistant.const import Platform DOMAIN = "subaru" @@ -32,9 +34,25 @@ API_GEN_2 = "g2" MANUFACTURER = "Subaru Corp." PLATFORMS = [ + Platform.LOCK, Platform.SENSOR, ] +SERVICE_LOCK = "lock" +SERVICE_UNLOCK = "unlock" +SERVICE_UNLOCK_SPECIFIC_DOOR = "unlock_specific_door" + +ATTR_DOOR = "door" + +UNLOCK_DOOR_ALL = "all" +UNLOCK_DOOR_DRIVERS = "driver" +UNLOCK_DOOR_TAILGATE = "tailgate" +UNLOCK_VALID_DOORS = { + UNLOCK_DOOR_ALL: ALL_DOORS, + UNLOCK_DOOR_DRIVERS: DRIVERS_DOOR, + UNLOCK_DOOR_TAILGATE: TAILGATE_DOOR, +} + ICONS = { "Avg Fuel Consumption": "mdi:leaf", "EV Range": "mdi:ev-station", diff --git a/homeassistant/components/subaru/lock.py b/homeassistant/components/subaru/lock.py new file mode 100644 index 00000000000..fb460c6279a --- /dev/null +++ b/homeassistant/components/subaru/lock.py @@ -0,0 +1,91 @@ +"""Support for Subaru door locks.""" +import logging + +import voluptuous as vol + +from homeassistant.components.lock import LockEntity +from homeassistant.const import SERVICE_LOCK, SERVICE_UNLOCK +from homeassistant.helpers import entity_platform + +from . import DOMAIN, get_device_info +from .const import ( + ATTR_DOOR, + ENTRY_CONTROLLER, + ENTRY_VEHICLES, + SERVICE_UNLOCK_SPECIFIC_DOOR, + UNLOCK_DOOR_ALL, + UNLOCK_VALID_DOORS, + VEHICLE_HAS_REMOTE_SERVICE, + VEHICLE_NAME, + VEHICLE_VIN, +) +from .remote_service import async_call_remote_service + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Subaru locks by config_entry.""" + entry = hass.data[DOMAIN][config_entry.entry_id] + controller = entry[ENTRY_CONTROLLER] + vehicle_info = entry[ENTRY_VEHICLES] + async_add_entities( + SubaruLock(vehicle, controller) + for vehicle in vehicle_info.values() + if vehicle[VEHICLE_HAS_REMOTE_SERVICE] + ) + + platform = entity_platform.async_get_current_platform() + + platform.async_register_entity_service( + SERVICE_UNLOCK_SPECIFIC_DOOR, + {vol.Required(ATTR_DOOR): vol.In(UNLOCK_VALID_DOORS)}, + "async_unlock_specific_door", + ) + + +class SubaruLock(LockEntity): + """ + Representation of a Subaru door lock. + + Note that the Subaru API currently does not support returning the status of the locks. Lock status is always unknown. + """ + + def __init__(self, vehicle_info, controller): + """Initialize the locks for the vehicle.""" + self.controller = controller + self.vehicle_info = vehicle_info + vin = vehicle_info[VEHICLE_VIN] + self.car_name = vehicle_info[VEHICLE_NAME] + self._attr_name = f"{self.car_name} Door Locks" + self._attr_unique_id = f"{vin}_door_locks" + self._attr_device_info = get_device_info(vehicle_info) + + async def async_lock(self, **kwargs): + """Send the lock command.""" + _LOGGER.debug("Locking doors for: %s", self.car_name) + await async_call_remote_service( + self.controller, + SERVICE_LOCK, + self.vehicle_info, + ) + + async def async_unlock(self, **kwargs): + """Send the unlock command.""" + _LOGGER.debug("Unlocking doors for: %s", self.car_name) + await async_call_remote_service( + self.controller, + SERVICE_UNLOCK, + self.vehicle_info, + UNLOCK_VALID_DOORS[UNLOCK_DOOR_ALL], + ) + + async def async_unlock_specific_door(self, door): + """Send the unlock command for a specified door.""" + _LOGGER.debug("Unlocking %s door for: %s", door, self.car_name) + await async_call_remote_service( + self.controller, + SERVICE_UNLOCK, + self.vehicle_info, + UNLOCK_VALID_DOORS[door], + ) diff --git a/homeassistant/components/subaru/manifest.json b/homeassistant/components/subaru/manifest.json index 6e1151cdccb..0123f26f916 100644 --- a/homeassistant/components/subaru/manifest.json +++ b/homeassistant/components/subaru/manifest.json @@ -3,7 +3,7 @@ "name": "Subaru", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/subaru", - "requirements": ["subarulink==0.4.2"], + "requirements": ["subarulink==0.5.0"], "codeowners": ["@G-Two"], "iot_class": "cloud_polling", "loggers": ["stdiomask", "subarulink"] diff --git a/homeassistant/components/subaru/remote_service.py b/homeassistant/components/subaru/remote_service.py new file mode 100644 index 00000000000..04c87b6b8d2 --- /dev/null +++ b/homeassistant/components/subaru/remote_service.py @@ -0,0 +1,33 @@ +"""Remote vehicle services for Subaru integration.""" +import logging + +from subarulink.exceptions import SubaruException + +from homeassistant.exceptions import HomeAssistantError + +from .const import SERVICE_UNLOCK, VEHICLE_NAME, VEHICLE_VIN + +_LOGGER = logging.getLogger(__name__) + + +async def async_call_remote_service(controller, cmd, vehicle_info, arg=None): + """Execute subarulink remote command.""" + car_name = vehicle_info[VEHICLE_NAME] + vin = vehicle_info[VEHICLE_VIN] + + _LOGGER.debug("Sending %s command command to %s", cmd, car_name) + success = False + err_msg = "" + try: + if cmd == SERVICE_UNLOCK: + success = await getattr(controller, cmd)(vin, arg) + else: + success = await getattr(controller, cmd)(vin) + except SubaruException as err: + err_msg = err.message + + if success: + _LOGGER.debug("%s command successfully completed for %s", cmd, car_name) + return + + raise HomeAssistantError(f"Service {cmd} failed for {car_name}: {err_msg}") diff --git a/homeassistant/components/subaru/services.yaml b/homeassistant/components/subaru/services.yaml new file mode 100644 index 00000000000..58be48f9d18 --- /dev/null +++ b/homeassistant/components/subaru/services.yaml @@ -0,0 +1,19 @@ +unlock_specific_door: + name: Unlock Specific Door + description: Unlocks specific door(s) + target: + entity: + domain: lock + integration: subaru + fields: + door: + name: Door + description: "One of the following: 'all', 'driver', 'tailgate'" + example: driver + required: true + selector: + select: + options: + - "all" + - "driver" + - "tailgate" diff --git a/homeassistant/components/subaru/strings.json b/homeassistant/components/subaru/strings.json index ea9df082f3a..abde396ba75 100644 --- a/homeassistant/components/subaru/strings.json +++ b/homeassistant/components/subaru/strings.json @@ -10,6 +10,20 @@ "country": "Select country" } }, + "two_factor": { + "title": "Subaru Starlink Configuration", + "description": "Two factor authentication required", + "data": { + "contact_method": "Please select a contact method:" + } + }, + "two_factor_validate": { + "title": "Subaru Starlink Configuration", + "description": "Please enter validation code received", + "data": { + "validation_code": "Validation code" + } + }, "pin": { "title": "Subaru Starlink Configuration", "description": "Please enter your MySubaru PIN\nNOTE: All vehicles in account must have the same PIN", @@ -22,7 +36,10 @@ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", "incorrect_pin": "Incorrect PIN", - "bad_pin_format": "PIN should be 4 digits" + "bad_pin_format": "PIN should be 4 digits", + "two_factor_request_failed": "Request for 2FA code failed, please try again", + "bad_validation_code_format": "Validation code should be 6 digits", + "incorrect_validation_code": "Incorrect validation code" }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", diff --git a/homeassistant/components/subaru/translations/fr.json b/homeassistant/components/subaru/translations/fr.json index 473492ddd07..fafcf81157b 100644 --- a/homeassistant/components/subaru/translations/fr.json +++ b/homeassistant/components/subaru/translations/fr.json @@ -8,7 +8,7 @@ "bad_pin_format": "Le code PIN doit \u00eatre compos\u00e9 de 4 chiffres", "cannot_connect": "\u00c9chec de connexion", "incorrect_pin": "PIN incorrect", - "invalid_auth": "Authentification invalide" + "invalid_auth": "Authentification non valide" }, "step": { "pin": { diff --git a/homeassistant/components/subaru/translations/pl.json b/homeassistant/components/subaru/translations/pl.json index 8bc9976fa2e..b0d491d475e 100644 --- a/homeassistant/components/subaru/translations/pl.json +++ b/homeassistant/components/subaru/translations/pl.json @@ -35,7 +35,7 @@ "data": { "update_enabled": "W\u0142\u0105cz odpytywanie pojazdu" }, - "description": "Po w\u0142\u0105czeniu, odpytywanie pojazdu b\u0119dzie co 2 godziny wysy\u0142a\u0107 zdalne polecenie do pojazdu w celu uzyskania nowych danych z czujnika. Bez odpytywania pojazdu, nowe dane z czujnika s\u0105 odbierane tylko wtedy, gdy pojazd automatycznie przesy\u0142a dane (zwykle po wy\u0142\u0105czeniu silnika).", + "description": "Po w\u0142\u0105czeniu, odpytywanie pojazdu b\u0119dzie co 2 godziny wysy\u0142a\u0107 zdalne polecenie do pojazdu w celu uzyskania nowych danych z sensora. Bez odpytywania pojazdu, nowe dane z sensora s\u0105 odbierane tylko wtedy, gdy pojazd automatycznie przesy\u0142a dane (zwykle po wy\u0142\u0105czeniu silnika).", "title": "Opcje Subaru Starlink" } } diff --git a/homeassistant/components/sun/__init__.py b/homeassistant/components/sun/__init__.py index 4789490ef0d..c619607d5b2 100644 --- a/homeassistant/components/sun/__init__.py +++ b/homeassistant/components/sun/__init__.py @@ -2,13 +2,15 @@ from datetime import timedelta import logging +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( CONF_ELEVATION, + EVENT_COMPONENT_LOADED, EVENT_CORE_CONFIG_UPDATE, SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import Event, HomeAssistant, callback from homeassistant.helpers import event from homeassistant.helpers.entity import Entity from homeassistant.helpers.sun import ( @@ -16,14 +18,15 @@ from homeassistant.helpers.sun import ( get_location_astral_event_next, ) from homeassistant.helpers.typing import ConfigType +from homeassistant.setup import ATTR_COMPONENT from homeassistant.util import dt as dt_util +from .const import DOMAIN + # mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs _LOGGER = logging.getLogger(__name__) -DOMAIN = "sun" - ENTITY_ID = "sun.sun" STATE_ABOVE_HORIZON = "above_horizon" @@ -80,7 +83,27 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: "Elevation is now configured in Home Assistant core. " "See https://www.home-assistant.io/docs/configuration/basic/" ) - Sun(hass) + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=config, + ) + ) + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up from a config entry.""" + hass.data[DOMAIN] = Sun(hass) + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + sun = hass.data.pop(DOMAIN) + sun.remove_listeners() + hass.states.async_remove(sun.entity_id) return True @@ -100,18 +123,52 @@ class Sun(Entity): self.solar_elevation = self.solar_azimuth = None self.rising = self.phase = None self._next_change = None + self._config_listener = None + self._update_events_listener = None + self._update_sun_position_listener = None + self._loaded_listener = None + self._config_listener = self.hass.bus.async_listen( + EVENT_CORE_CONFIG_UPDATE, self.update_location + ) + self._loaded_listener = self.hass.bus.async_listen( + EVENT_COMPONENT_LOADED, self.loading_complete + ) - @callback - def update_location(_event): - location, elevation = get_astral_location(self.hass) - if location == self.location: - return - self.location = location - self.elevation = elevation - self.update_events() + @callback + def loading_complete(self, event_: Event) -> None: + """Update location when loading is complete.""" + if event_.data[ATTR_COMPONENT] == DOMAIN: + self.update_location() + self._remove_loaded_listener() - update_location(None) - self.hass.bus.async_listen(EVENT_CORE_CONFIG_UPDATE, update_location) + @callback + def update_location(self, *_): + """Update location.""" + location, elevation = get_astral_location(self.hass) + if location == self.location: + return + self.location = location + self.elevation = elevation + if self._update_events_listener: + self._update_events_listener() + self.update_events() + + @callback + def _remove_loaded_listener(self): + """Remove the loaded listener.""" + if self._loaded_listener: + self._loaded_listener() + + @callback + def remove_listeners(self): + """Remove listeners.""" + self._remove_loaded_listener() + if self._config_listener: + self._config_listener() + if self._update_events_listener: + self._update_events_listener() + if self._update_sun_position_listener: + self._update_sun_position_listener() @property def name(self): @@ -211,10 +268,12 @@ class Sun(Entity): _LOGGER.debug( "sun phase_update@%s: phase=%s", utc_point_in_time.isoformat(), self.phase ) + if self._update_sun_position_listener: + self._update_sun_position_listener() self.update_sun_position() # Set timer for the next solar event - event.async_track_point_in_utc_time( + self._update_events_listener = event.async_track_point_in_utc_time( self.hass, self.update_events, self._next_change ) _LOGGER.debug("next time: %s", self._next_change.isoformat()) @@ -244,7 +303,8 @@ class Sun(Entity): # if the next update is within 1.25 of the next # position update just drop it if utc_point_in_time + delta * 1.25 > self._next_change: + self._update_sun_position_listener = None return - event.async_track_point_in_utc_time( + self._update_sun_position_listener = event.async_track_point_in_utc_time( self.hass, self.update_sun_position, utc_point_in_time + delta ) diff --git a/homeassistant/components/sun/config_flow.py b/homeassistant/components/sun/config_flow.py new file mode 100644 index 00000000000..ae2e5a42efc --- /dev/null +++ b/homeassistant/components/sun/config_flow.py @@ -0,0 +1,31 @@ +"""Config flow to configure the Sun integration.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.config_entries import ConfigFlow +from homeassistant.data_entry_flow import FlowResult + +from .const import DEFAULT_NAME, DOMAIN + + +class SunConfigFlow(ConfigFlow, domain=DOMAIN): + """Config flow for Sun.""" + + VERSION = 1 + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle a flow initialized by the user.""" + if self._async_current_entries(): + return self.async_abort(reason="single_instance_allowed") + + if user_input is not None: + return self.async_create_entry(title=DEFAULT_NAME, data={}) + + return self.async_show_form(step_id="user") + + async def async_step_import(self, user_input: dict[str, Any]) -> FlowResult: + """Handle import from configuration.yaml.""" + return await self.async_step_user(user_input) diff --git a/homeassistant/components/sun/const.py b/homeassistant/components/sun/const.py new file mode 100644 index 00000000000..f567c77e62a --- /dev/null +++ b/homeassistant/components/sun/const.py @@ -0,0 +1,6 @@ +"""Constants for the Sun integration.""" +from typing import Final + +DOMAIN: Final = "sun" + +DEFAULT_NAME: Final = "Sun" diff --git a/homeassistant/components/sun/manifest.json b/homeassistant/components/sun/manifest.json index 93fb76629cc..dd13bf556fb 100644 --- a/homeassistant/components/sun/manifest.json +++ b/homeassistant/components/sun/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/sun", "codeowners": ["@Swamp-Ig"], "quality_scale": "internal", - "iot_class": "calculated" + "iot_class": "calculated", + "config_flow": true } diff --git a/homeassistant/components/sun/recorder.py b/homeassistant/components/sun/recorder.py new file mode 100644 index 00000000000..710d7ff4559 --- /dev/null +++ b/homeassistant/components/sun/recorder.py @@ -0,0 +1,32 @@ +"""Integration platform for recorder.""" +from __future__ import annotations + +from homeassistant.core import HomeAssistant, callback + +from . import ( + STATE_ATTR_AZIMUTH, + STATE_ATTR_ELEVATION, + STATE_ATTR_NEXT_DAWN, + STATE_ATTR_NEXT_DUSK, + STATE_ATTR_NEXT_MIDNIGHT, + STATE_ATTR_NEXT_NOON, + STATE_ATTR_NEXT_RISING, + STATE_ATTR_NEXT_SETTING, + STATE_ATTR_RISING, +) + + +@callback +def exclude_attributes(hass: HomeAssistant) -> set[str]: + """Exclude sun attributes from being recorded in the database.""" + return { + STATE_ATTR_AZIMUTH, + STATE_ATTR_ELEVATION, + STATE_ATTR_RISING, + STATE_ATTR_NEXT_DAWN, + STATE_ATTR_NEXT_DUSK, + STATE_ATTR_NEXT_MIDNIGHT, + STATE_ATTR_NEXT_NOON, + STATE_ATTR_NEXT_RISING, + STATE_ATTR_NEXT_SETTING, + } diff --git a/homeassistant/components/sun/strings.json b/homeassistant/components/sun/strings.json index 980879b95cb..cdcaa416eda 100644 --- a/homeassistant/components/sun/strings.json +++ b/homeassistant/components/sun/strings.json @@ -1,5 +1,15 @@ { "title": "Sun", + "config": { + "step": { + "user": { + "description": "[%key:common::config_flow::description::confirm_setup%]" + } + }, + "abort": { + "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" + } + }, "state": { "_": { "above_horizon": "Above horizon", diff --git a/homeassistant/components/surepetcare/entity.py b/homeassistant/components/surepetcare/entity.py index e1faaf07e26..301479c4b95 100644 --- a/homeassistant/components/surepetcare/entity.py +++ b/homeassistant/components/surepetcare/entity.py @@ -13,7 +13,7 @@ from . import SurePetcareDataCoordinator from .const import DOMAIN -class SurePetcareEntity(CoordinatorEntity): +class SurePetcareEntity(CoordinatorEntity[SurePetcareDataCoordinator]): """An implementation for Sure Petcare Entities.""" def __init__( diff --git a/homeassistant/components/surepetcare/lock.py b/homeassistant/components/surepetcare/lock.py index 8ebdd9958f0..3161fa6e0bc 100644 --- a/homeassistant/components/surepetcare/lock.py +++ b/homeassistant/components/surepetcare/lock.py @@ -46,8 +46,6 @@ async def async_setup_entry( class SurePetcareLock(SurePetcareEntity, LockEntity): """A lock implementation for Sure Petcare Entities.""" - coordinator: SurePetcareDataCoordinator - def __init__( self, surepetcare_id: int, diff --git a/homeassistant/components/surepetcare/manifest.json b/homeassistant/components/surepetcare/manifest.json index 8675099c530..6368bab898e 100644 --- a/homeassistant/components/surepetcare/manifest.json +++ b/homeassistant/components/surepetcare/manifest.json @@ -2,14 +2,9 @@ "domain": "surepetcare", "name": "Sure Petcare", "documentation": "https://www.home-assistant.io/integrations/surepetcare", - "codeowners": [ - "@benleb", - "@danielhiversen" - ], - "requirements": [ - "surepy==0.7.2" - ], + "codeowners": ["@benleb", "@danielhiversen"], + "requirements": ["surepy==0.7.2"], "iot_class": "cloud_polling", "config_flow": true, "loggers": ["rich", "surepy"] -} \ No newline at end of file +} diff --git a/homeassistant/components/surepetcare/services.yaml b/homeassistant/components/surepetcare/services.yaml index 57b1ef22008..3c3919f5d01 100644 --- a/homeassistant/components/surepetcare/services.yaml +++ b/homeassistant/components/surepetcare/services.yaml @@ -16,10 +16,10 @@ set_lock_state: selector: select: options: - - 'locked_all' - - 'locked_in' - - 'locked_out' - - 'unlocked' + - "locked_all" + - "locked_in" + - "locked_out" + - "unlocked" set_pet_location: name: Set pet location @@ -38,5 +38,5 @@ set_pet_location: selector: select: options: - - 'Inside' - - 'Outside' + - "Inside" + - "Outside" diff --git a/homeassistant/components/surepetcare/strings.json b/homeassistant/components/surepetcare/strings.json index f3d4d11008f..f7a539fe0e6 100644 --- a/homeassistant/components/surepetcare/strings.json +++ b/homeassistant/components/surepetcare/strings.json @@ -3,8 +3,8 @@ "step": { "user": { "data": { - "username": "[%key:common::config_flow::data::username%]", - "password": "[%key:common::config_flow::data::password%]" + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" } } }, diff --git a/homeassistant/components/surepetcare/translations/fr.json b/homeassistant/components/surepetcare/translations/fr.json index b6704aabae1..dc122998917 100644 --- a/homeassistant/components/surepetcare/translations/fr.json +++ b/homeassistant/components/surepetcare/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/switch/__init__.py b/homeassistant/components/switch/__init__.py index 157a5cd40c7..7ef4a754b55 100644 --- a/homeassistant/components/switch/__init__.py +++ b/homeassistant/components/switch/__init__.py @@ -4,7 +4,6 @@ from __future__ import annotations from dataclasses import dataclass from datetime import timedelta import logging -from typing import Any, final import voluptuous as vol @@ -26,21 +25,14 @@ from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass -DOMAIN = "switch" +from .const import DOMAIN + SCAN_INTERVAL = timedelta(seconds=30) ENTITY_ID_FORMAT = DOMAIN + ".{}" -ATTR_TODAY_ENERGY_KWH = "today_energy_kwh" -ATTR_CURRENT_POWER_W = "current_power_w" - MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) -PROP_TO_ATTR = { - "current_power_w": ATTR_CURRENT_POWER_W, - "today_energy_kwh": ATTR_TODAY_ENERGY_KWH, -} - _LOGGER = logging.getLogger(__name__) @@ -106,14 +98,7 @@ class SwitchEntity(ToggleEntity): """Base class for switch entities.""" entity_description: SwitchEntityDescription - _attr_current_power_w: float | None = None _attr_device_class: SwitchDeviceClass | str | None - _attr_today_energy_kwh: float | None = None - - @property - def current_power_w(self) -> float | None: - """Return the current power usage in W.""" - return self._attr_current_power_w @property def device_class(self) -> SwitchDeviceClass | str | None: @@ -123,20 +108,3 @@ class SwitchEntity(ToggleEntity): if hasattr(self, "entity_description"): return self.entity_description.device_class return None - - @property - def today_energy_kwh(self) -> float | None: - """Return the today total energy usage in kWh.""" - return self._attr_today_energy_kwh - - @final - @property - def state_attributes(self) -> dict[str, Any] | None: - """Return the optional state attributes.""" - data = {} - - for prop, attr in PROP_TO_ATTR.items(): - if (value := getattr(self, prop)) is not None: - data[attr] = value - - return data diff --git a/homeassistant/components/switch/const.py b/homeassistant/components/switch/const.py new file mode 100644 index 00000000000..aaff452c5ce --- /dev/null +++ b/homeassistant/components/switch/const.py @@ -0,0 +1,3 @@ +"""Constants for the Switch integration.""" + +DOMAIN = "switch" diff --git a/homeassistant/components/switch/light.py b/homeassistant/components/switch/light.py index 32c0aff74fa..ac548652953 100644 --- a/homeassistant/components/switch/light.py +++ b/homeassistant/components/switch/light.py @@ -5,7 +5,6 @@ from typing import Any import voluptuous as vol -from homeassistant.components import switch from homeassistant.components.light import ( COLOR_MODE_ONOFF, PLATFORM_SCHEMA, @@ -27,12 +26,14 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from .const import DOMAIN as SWITCH_DOMAIN + DEFAULT_NAME = "Light Switch" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Required(CONF_ENTITY_ID): cv.entity_domain(switch.DOMAIN), + vol.Required(CONF_ENTITY_ID): cv.entity_domain(SWITCH_DOMAIN), } ) @@ -75,7 +76,7 @@ class LightSwitch(LightEntity): async def async_turn_on(self, **kwargs: Any) -> None: """Forward the turn_on command to the switch in this light switch.""" await self.hass.services.async_call( - switch.DOMAIN, + SWITCH_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: self._switch_entity_id}, blocking=True, @@ -85,7 +86,7 @@ class LightSwitch(LightEntity): async def async_turn_off(self, **kwargs: Any) -> None: """Forward the turn_off command to the switch in this light switch.""" await self.hass.services.async_call( - switch.DOMAIN, + SWITCH_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: self._switch_entity_id}, blocking=True, @@ -103,7 +104,6 @@ class LightSwitch(LightEntity): ) is None or state.state == STATE_UNAVAILABLE: self._attr_available = False return - self._attr_available = True self._attr_is_on = state.state == STATE_ON self.async_write_ha_state() @@ -113,6 +113,5 @@ class LightSwitch(LightEntity): self.hass, [self._switch_entity_id], async_state_changed_listener ) ) - # Call once on adding async_state_changed_listener() diff --git a/homeassistant/components/switch/manifest.json b/homeassistant/components/switch/manifest.json index 6f0113d1b9c..929c617e46a 100644 --- a/homeassistant/components/switch/manifest.json +++ b/homeassistant/components/switch/manifest.json @@ -2,6 +2,7 @@ "domain": "switch", "name": "Switch", "documentation": "https://www.home-assistant.io/integrations/switch", - "codeowners": [], + "after_dependencies": ["switch_as_x"], + "codeowners": ["@home-assistant/core"], "quality_scale": "internal" } diff --git a/homeassistant/components/switch/translations/ca.json b/homeassistant/components/switch/translations/ca.json index c2406b7873e..49667b9f60a 100644 --- a/homeassistant/components/switch/translations/ca.json +++ b/homeassistant/components/switch/translations/ca.json @@ -1,4 +1,14 @@ { + "config": { + "step": { + "init": { + "data": { + "entity_id": "Entitat d'interruptor" + }, + "description": "Selecciona l'interruptor del llum." + } + } + }, "device_automation": { "action_type": { "toggle": "Commuta {entity_name}", diff --git a/homeassistant/components/switch/translations/de.json b/homeassistant/components/switch/translations/de.json index 2b02f1304d2..12784fab206 100644 --- a/homeassistant/components/switch/translations/de.json +++ b/homeassistant/components/switch/translations/de.json @@ -1,4 +1,14 @@ { + "config": { + "step": { + "init": { + "data": { + "entity_id": "Switch-Entit\u00e4t" + }, + "description": "W\u00e4hle den Schalter f\u00fcr den Lichtschalter aus." + } + } + }, "device_automation": { "action_type": { "toggle": "{entity_name} umschalten", diff --git a/homeassistant/components/switch/translations/el.json b/homeassistant/components/switch/translations/el.json index e2aa65788f1..2067276af42 100644 --- a/homeassistant/components/switch/translations/el.json +++ b/homeassistant/components/switch/translations/el.json @@ -1,4 +1,14 @@ { + "config": { + "step": { + "init": { + "data": { + "entity_id": "\u039f\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1 \u03b4\u03b9\u03b1\u03ba\u03cc\u03c0\u03c4\u03b7" + }, + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03bf \u03b4\u03b9\u03b1\u03ba\u03cc\u03c0\u03c4\u03b7 \u03b3\u03b9\u03b1 \u03c4\u03bf \u03b4\u03b9\u03b1\u03ba\u03cc\u03c0\u03c4\u03b7 \u03c6\u03ce\u03c4\u03c9\u03bd." + } + } + }, "device_automation": { "action_type": { "toggle": "\u0395\u03bd\u03b1\u03bb\u03bb\u03b1\u03b3\u03ae {entity_name}", @@ -18,8 +28,8 @@ }, "state": { "_": { - "off": "\u039a\u03bb\u03b5\u03b9\u03c3\u03c4\u03cc", - "on": "\u0391\u03bd\u03bf\u03b9\u03c7\u03c4\u03cc" + "off": "\u039a\u03bb\u03b5\u03b9\u03c3\u03c4\u03cc\u03c2", + "on": "\u0391\u03bd\u03bf\u03b9\u03c7\u03c4\u03cc\u03c2" } }, "title": "\u0394\u03b9\u03b1\u03ba\u03cc\u03c0\u03c4\u03b7\u03c2" diff --git a/homeassistant/components/switch/translations/en.json b/homeassistant/components/switch/translations/en.json index 35f658e7e1f..ae04ed42520 100644 --- a/homeassistant/components/switch/translations/en.json +++ b/homeassistant/components/switch/translations/en.json @@ -1,4 +1,14 @@ { + "config": { + "step": { + "init": { + "data": { + "entity_id": "Switch entity" + }, + "description": "Select the switch for the light switch." + } + } + }, "device_automation": { "action_type": { "toggle": "Toggle {entity_name}", diff --git a/homeassistant/components/switch/translations/et.json b/homeassistant/components/switch/translations/et.json index 394d908c9a1..e9c9928c89d 100644 --- a/homeassistant/components/switch/translations/et.json +++ b/homeassistant/components/switch/translations/et.json @@ -1,4 +1,14 @@ { + "config": { + "step": { + "init": { + "data": { + "entity_id": "L\u00fcliti olem" + }, + "description": "Vali valgusti l\u00fcliti" + } + } + }, "device_automation": { "action_type": { "toggle": "Muuda {entity_name} olekut", diff --git a/homeassistant/components/switch/translations/fr.json b/homeassistant/components/switch/translations/fr.json index e2abc370909..ec357e10c72 100644 --- a/homeassistant/components/switch/translations/fr.json +++ b/homeassistant/components/switch/translations/fr.json @@ -1,26 +1,36 @@ { + "config": { + "step": { + "init": { + "data": { + "entity_id": "Entit\u00e9 du commutateur" + }, + "description": "S\u00e9lectionnez le commutateur correspondant \u00e0 l'interrupteur d'\u00e9clairage." + } + } + }, "device_automation": { "action_type": { "toggle": "Basculer {entity_name}", - "turn_off": "\u00c9teindre {entity_name}", - "turn_on": "Allumer {entity_name}" + "turn_off": "D\u00e9sactiver {entity_name}", + "turn_on": "Activer {entity_name}" }, "condition_type": { - "is_off": "{entity_name} est \u00e9teint", - "is_on": "{entity_name} est allum\u00e9" + "is_off": "{entity_name} est d\u00e9sactiv\u00e9", + "is_on": "{entity_name} est activ\u00e9" }, "trigger_type": { - "changed_states": "{entity_name} activ\u00e9 ou d\u00e9sactiv\u00e9", - "toggled": "{entity_name} activ\u00e9 ou d\u00e9sactiv\u00e9", - "turned_off": "{entity_name} \u00e9teint", - "turned_on": "{entity_name} allum\u00e9" + "changed_states": "{entity_name} a \u00e9t\u00e9 activ\u00e9 ou d\u00e9sactiv\u00e9", + "toggled": "{entity_name} a \u00e9t\u00e9 activ\u00e9 ou d\u00e9sactiv\u00e9", + "turned_off": "{entity_name} a \u00e9t\u00e9 d\u00e9sactiv\u00e9", + "turned_on": "{entity_name} a \u00e9t\u00e9 activ\u00e9" } }, "state": { "_": { - "off": "Inactif", - "on": "Actif" + "off": "D\u00e9sactiv\u00e9", + "on": "Activ\u00e9" } }, - "title": "Interrupteur" + "title": "Commutateur" } \ No newline at end of file diff --git a/homeassistant/components/switch/translations/he.json b/homeassistant/components/switch/translations/he.json index ea387cf7c67..c3d76bb6f9f 100644 --- a/homeassistant/components/switch/translations/he.json +++ b/homeassistant/components/switch/translations/he.json @@ -1,4 +1,14 @@ { + "config": { + "step": { + "init": { + "data": { + "entity_id": "\u05d9\u05e9\u05d5\u05ea \u05de\u05ea\u05d2" + }, + "description": "\u05d1\u05d7\u05d9\u05e8\u05ea \u05de\u05ea\u05d2 \u05e2\u05d1\u05d5\u05e8 \u05de\u05ea\u05d2 \u05d4\u05d0\u05d5\u05e8." + } + } + }, "device_automation": { "action_type": { "toggle": "\u05d4\u05d7\u05dc\u05e4\u05ea \u05de\u05e6\u05d1 {entity_name}", diff --git a/homeassistant/components/switch/translations/hu.json b/homeassistant/components/switch/translations/hu.json index 4b1dc0712bd..423a48f345d 100644 --- a/homeassistant/components/switch/translations/hu.json +++ b/homeassistant/components/switch/translations/hu.json @@ -1,4 +1,14 @@ { + "config": { + "step": { + "init": { + "data": { + "entity_id": "Kapcsol\u00f3 entit\u00e1s" + }, + "description": "V\u00e1lassza ki a kapcsol\u00f3t a vil\u00e1g\u00edt\u00e1shoz." + } + } + }, "device_automation": { "action_type": { "toggle": "{entity_name} kapcsol\u00e1sa", diff --git a/homeassistant/components/switch/translations/id.json b/homeassistant/components/switch/translations/id.json index ca341378714..c49cab13a9c 100644 --- a/homeassistant/components/switch/translations/id.json +++ b/homeassistant/components/switch/translations/id.json @@ -1,4 +1,14 @@ { + "config": { + "step": { + "init": { + "data": { + "entity_id": "Entitas saklar" + }, + "description": "Pilih saklar mana untuk saklar lampu." + } + } + }, "device_automation": { "action_type": { "toggle": "Nyala/matikan {entity_name}", diff --git a/homeassistant/components/switch/translations/it.json b/homeassistant/components/switch/translations/it.json index 59a533c2a09..b71a46e48be 100644 --- a/homeassistant/components/switch/translations/it.json +++ b/homeassistant/components/switch/translations/it.json @@ -1,4 +1,14 @@ { + "config": { + "step": { + "init": { + "data": { + "entity_id": "Entit\u00e0 dell'interruttore" + }, + "description": "Seleziona l'interruttore per l'interruttore della luce." + } + } + }, "device_automation": { "action_type": { "toggle": "Attiva/Disattiva {entity_name}", diff --git a/homeassistant/components/switch/translations/ja.json b/homeassistant/components/switch/translations/ja.json index f806c67bb95..d4e335257ad 100644 --- a/homeassistant/components/switch/translations/ja.json +++ b/homeassistant/components/switch/translations/ja.json @@ -1,4 +1,14 @@ { + "config": { + "step": { + "init": { + "data": { + "entity_id": "\u30b9\u30a4\u30c3\u30c1\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3" + }, + "description": "\u7167\u660e\u30b9\u30a4\u30c3\u30c1\u306e\u30b9\u30a4\u30c3\u30c1\u3092\u9078\u629e\u3057\u307e\u3059\u3002" + } + } + }, "device_automation": { "action_type": { "toggle": "\u30c8\u30b0\u30eb {entity_name}", diff --git a/homeassistant/components/switch/translations/nl.json b/homeassistant/components/switch/translations/nl.json index 71d3b0f6b8e..87636c8a1f1 100644 --- a/homeassistant/components/switch/translations/nl.json +++ b/homeassistant/components/switch/translations/nl.json @@ -1,4 +1,14 @@ { + "config": { + "step": { + "init": { + "data": { + "entity_id": "Entiteit wijzigen" + }, + "description": "Kies de schakelaar voor de lichtschakelaar." + } + } + }, "device_automation": { "action_type": { "toggle": "Omschakelen {entity_name}", diff --git a/homeassistant/components/switch/translations/no.json b/homeassistant/components/switch/translations/no.json index a39787efe79..802df4e6a42 100644 --- a/homeassistant/components/switch/translations/no.json +++ b/homeassistant/components/switch/translations/no.json @@ -1,4 +1,14 @@ { + "config": { + "step": { + "init": { + "data": { + "entity_id": "Bytt enhet" + }, + "description": "Velg bryteren for lysbryteren." + } + } + }, "device_automation": { "action_type": { "toggle": "Veksle {entity_name}", diff --git a/homeassistant/components/switch/translations/pl.json b/homeassistant/components/switch/translations/pl.json index 9132a6c6b9e..51ecfc44c5d 100644 --- a/homeassistant/components/switch/translations/pl.json +++ b/homeassistant/components/switch/translations/pl.json @@ -1,4 +1,14 @@ { + "config": { + "step": { + "init": { + "data": { + "entity_id": "Encja prze\u0142\u0105cznika" + }, + "description": "Wybierz prze\u0142\u0105cznik do w\u0142\u0105cznika \u015bwiat\u0142a." + } + } + }, "device_automation": { "action_type": { "toggle": "prze\u0142\u0105cz {entity_name}", diff --git a/homeassistant/components/switch/translations/pt-BR.json b/homeassistant/components/switch/translations/pt-BR.json index cb73ce3c5cf..6f7f076332a 100644 --- a/homeassistant/components/switch/translations/pt-BR.json +++ b/homeassistant/components/switch/translations/pt-BR.json @@ -1,4 +1,14 @@ { + "config": { + "step": { + "init": { + "data": { + "entity_id": "Entidade de switch" + }, + "description": "Selecione o switch para o interruptor de luz." + } + } + }, "device_automation": { "action_type": { "toggle": "Alternar {entity_name}", diff --git a/homeassistant/components/switch/translations/ru.json b/homeassistant/components/switch/translations/ru.json index 801146b11d3..2602a1ac20b 100644 --- a/homeassistant/components/switch/translations/ru.json +++ b/homeassistant/components/switch/translations/ru.json @@ -1,4 +1,14 @@ { + "config": { + "step": { + "init": { + "data": { + "entity_id": "\u0412\u044b\u043a\u043b\u044e\u0447\u0430\u0442\u0435\u043b\u044c" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0432\u044b\u043a\u043b\u044e\u0447\u0430\u0442\u0435\u043b\u044c \u0434\u043b\u044f \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f \u0435\u0433\u043e \u043a\u0430\u043a \u0432\u044b\u043a\u043b\u044e\u0447\u0430\u0442\u0435\u043b\u044c \u043e\u0441\u0432\u0435\u0449\u0435\u043d\u0438\u044f." + } + } + }, "device_automation": { "action_type": { "toggle": "{entity_name}: \u043f\u0435\u0440\u0435\u043a\u043b\u044e\u0447\u0438\u0442\u044c", diff --git a/homeassistant/components/switch/translations/tr.json b/homeassistant/components/switch/translations/tr.json index 421dca0df67..2a98d2e972f 100644 --- a/homeassistant/components/switch/translations/tr.json +++ b/homeassistant/components/switch/translations/tr.json @@ -1,4 +1,14 @@ { + "config": { + "step": { + "init": { + "data": { + "entity_id": "Varl\u0131\u011f\u0131 de\u011fi\u015ftir" + }, + "description": "I\u015f\u0131k anahtar\u0131 i\u00e7in anahtar\u0131 se\u00e7in." + } + } + }, "device_automation": { "action_type": { "toggle": "{entity_name} de\u011fi\u015ftir", diff --git a/homeassistant/components/switch/translations/zh-Hant.json b/homeassistant/components/switch/translations/zh-Hant.json index 95f3ac4d641..631db326ae0 100644 --- a/homeassistant/components/switch/translations/zh-Hant.json +++ b/homeassistant/components/switch/translations/zh-Hant.json @@ -1,4 +1,14 @@ { + "config": { + "step": { + "init": { + "data": { + "entity_id": "\u958b\u95dc\u5be6\u9ad4" + }, + "description": "\u9078\u64c7\u6307\u5b9a\u70ba\u71c8\u5149\u4e4b\u958b\u95dc\u3002" + } + } + }, "device_automation": { "action_type": { "toggle": "\u5207\u63db{entity_name}", diff --git a/homeassistant/components/switch_as_x/__init__.py b/homeassistant/components/switch_as_x/__init__.py new file mode 100644 index 00000000000..1adeace7a96 --- /dev/null +++ b/homeassistant/components/switch_as_x/__init__.py @@ -0,0 +1,120 @@ +"""Component to wrap switch entities in entities of other domains.""" +from __future__ import annotations + +import logging + +import voluptuous as vol + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_ENTITY_ID +from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers.event import async_track_entity_registry_updated_event + +from .const import CONF_TARGET_DOMAIN +from .light import LightSwitch + +__all__ = ["LightSwitch"] + +_LOGGER = logging.getLogger(__name__) + + +@callback +def async_add_to_device( + hass: HomeAssistant, entry: ConfigEntry, entity_id: str +) -> str | None: + """Add our config entry to the tracked entity's device.""" + registry = er.async_get(hass) + device_registry = dr.async_get(hass) + device_id = None + + if ( + not (wrapped_switch := registry.async_get(entity_id)) + or not (device_id := wrapped_switch.device_id) + or not (device_registry.async_get(device_id)) + ): + return device_id + + device_registry.async_update_device(device_id, add_config_entry_id=entry.entry_id) + + return device_id + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up a config entry.""" + registry = er.async_get(hass) + device_registry = dr.async_get(hass) + try: + entity_id = er.async_validate_entity_id(registry, entry.options[CONF_ENTITY_ID]) + except vol.Invalid: + # The entity is identified by an unknown entity registry ID + _LOGGER.error( + "Failed to setup switch_as_x for unknown entity %s", + entry.options[CONF_ENTITY_ID], + ) + return False + + async def async_registry_updated(event: Event) -> None: + """Handle entity registry update.""" + data = event.data + if data["action"] == "remove": + await hass.config_entries.async_remove(entry.entry_id) + + if data["action"] != "update": + return + + if "entity_id" in data["changes"]: + # Entity_id changed, reload the config entry + await hass.config_entries.async_reload(entry.entry_id) + + if device_id and "device_id" in data["changes"]: + # If the tracked switch is no longer in the device, remove our config entry + # from the device + if ( + not (entity_entry := registry.async_get(data[CONF_ENTITY_ID])) + or not device_registry.async_get(device_id) + or entity_entry.device_id == device_id + ): + # No need to do any cleanup + return + + device_registry.async_update_device( + device_id, remove_config_entry_id=entry.entry_id + ) + + entry.async_on_unload( + async_track_entity_registry_updated_event( + hass, entity_id, async_registry_updated + ) + ) + + device_id = async_add_to_device(hass, entry, entity_id) + + hass.config_entries.async_setup_platforms( + entry, (entry.options[CONF_TARGET_DOMAIN],) + ) + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + return await hass.config_entries.async_unload_platforms( + entry, (entry.options[CONF_TARGET_DOMAIN],) + ) + + +async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Unload a config entry.""" + # Unhide the wrapped entry if registered + registry = er.async_get(hass) + try: + entity_id = er.async_validate_entity_id(registry, entry.options[CONF_ENTITY_ID]) + except vol.Invalid: + # The source entity has been removed from the entity registry + return + + if not (entity_entry := registry.async_get(entity_id)): + return + + if entity_entry.hidden_by == er.RegistryEntryHider.INTEGRATION: + registry.async_update_entity(entity_id, hidden_by=None) diff --git a/homeassistant/components/switch_as_x/config_flow.py b/homeassistant/components/switch_as_x/config_flow.py new file mode 100644 index 00000000000..a70e0a371e8 --- /dev/null +++ b/homeassistant/components/switch_as_x/config_flow.py @@ -0,0 +1,61 @@ +"""Config flow for Switch as X integration.""" +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any + +import voluptuous as vol + +from homeassistant.const import CONF_ENTITY_ID, Platform +from homeassistant.helpers import entity_registry as er, selector +from homeassistant.helpers.schema_config_entry_flow import ( + SchemaConfigFlowHandler, + SchemaFlowFormStep, + SchemaFlowMenuStep, + wrapped_entity_config_entry_title, +) + +from .const import CONF_TARGET_DOMAIN, DOMAIN + +CONFIG_FLOW: dict[str, SchemaFlowFormStep | SchemaFlowMenuStep] = { + "user": SchemaFlowFormStep( + vol.Schema( + { + vol.Required(CONF_ENTITY_ID): selector.selector( + {"entity": {"domain": Platform.SWITCH}} + ), + vol.Required(CONF_TARGET_DOMAIN): selector.selector( + { + "select": { + "options": [ + {"value": Platform.COVER, "label": "Cover"}, + {"value": Platform.FAN, "label": "Fan"}, + {"value": Platform.LIGHT, "label": "Light"}, + {"value": Platform.LOCK, "label": "Lock"}, + {"value": Platform.SIREN, "label": "Siren"}, + ] + } + } + ), + } + ) + ) +} + + +class SwitchAsXConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN): + """Handle a config flow for Switch as X.""" + + config_flow = CONFIG_FLOW + + def async_config_entry_title(self, options: Mapping[str, Any]) -> str: + """Return config entry title and hide the wrapped entity if registered.""" + # Hide the wrapped entry if registered + registry = er.async_get(self.hass) + entity_entry = registry.async_get(options[CONF_ENTITY_ID]) + if entity_entry is not None and not entity_entry.hidden: + registry.async_update_entity( + options[CONF_ENTITY_ID], hidden_by=er.RegistryEntryHider.INTEGRATION + ) + + return wrapped_entity_config_entry_title(self.hass, options[CONF_ENTITY_ID]) diff --git a/homeassistant/components/switch_as_x/const.py b/homeassistant/components/switch_as_x/const.py new file mode 100644 index 00000000000..4963d6fa60b --- /dev/null +++ b/homeassistant/components/switch_as_x/const.py @@ -0,0 +1,7 @@ +"""Constants for the Switch as X integration.""" + +from typing import Final + +DOMAIN: Final = "switch_as_x" + +CONF_TARGET_DOMAIN: Final = "target_domain" diff --git a/homeassistant/components/switch_as_x/cover.py b/homeassistant/components/switch_as_x/cover.py new file mode 100644 index 00000000000..3825953ed63 --- /dev/null +++ b/homeassistant/components/switch_as_x/cover.py @@ -0,0 +1,83 @@ +"""Cover support for switch entities.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.components.cover import SUPPORT_CLOSE, SUPPORT_OPEN, CoverEntity +from homeassistant.components.switch.const import DOMAIN as SWITCH_DOMAIN +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + ATTR_ENTITY_ID, + CONF_ENTITY_ID, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_ON, +) +from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .entity import BaseEntity + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Initialize Cover Switch config entry.""" + registry = er.async_get(hass) + entity_id = er.async_validate_entity_id( + registry, config_entry.options[CONF_ENTITY_ID] + ) + wrapped_switch = registry.async_get(entity_id) + device_id = wrapped_switch.device_id if wrapped_switch else None + + async_add_entities( + [ + CoverSwitch( + config_entry.title, + entity_id, + config_entry.entry_id, + device_id, + ) + ] + ) + + +class CoverSwitch(BaseEntity, CoverEntity): + """Represents a Switch as a Cover.""" + + _attr_supported_features = SUPPORT_OPEN | SUPPORT_CLOSE + + async def async_open_cover(self, **kwargs: Any) -> None: + """Open the cover.""" + await self.hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: self._switch_entity_id}, + blocking=True, + context=self._context, + ) + + async def async_close_cover(self, **kwargs: Any) -> None: + """Close cover.""" + await self.hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: self._switch_entity_id}, + blocking=True, + context=self._context, + ) + + @callback + def async_state_changed_listener(self, event: Event | None = None) -> None: + """Handle child updates.""" + super().async_state_changed_listener(event) + if ( + not self.available + or (state := self.hass.states.get(self._switch_entity_id)) is None + ): + return + + self._attr_is_closed = state.state != STATE_ON diff --git a/homeassistant/components/switch_as_x/entity.py b/homeassistant/components/switch_as_x/entity.py new file mode 100644 index 00000000000..040a5d35232 --- /dev/null +++ b/homeassistant/components/switch_as_x/entity.py @@ -0,0 +1,106 @@ +"""Base entity for the Switch as X integration.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.components.switch.const import DOMAIN as SWITCH_DOMAIN +from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_ON, + STATE_UNAVAILABLE, +) +from homeassistant.core import Event, callback +from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.entity import Entity, ToggleEntity +from homeassistant.helpers.event import async_track_state_change_event + + +class BaseEntity(Entity): + """Represents a Switch as a X.""" + + _attr_should_poll = False + + def __init__( + self, + name: str, + switch_entity_id: str, + unique_id: str | None, + device_id: str | None = None, + ) -> None: + """Initialize Light Switch.""" + self._device_id = device_id + self._attr_name = name + self._attr_unique_id = unique_id + self._switch_entity_id = switch_entity_id + + @callback + def async_state_changed_listener(self, event: Event | None = None) -> None: + """Handle child updates.""" + if ( + state := self.hass.states.get(self._switch_entity_id) + ) is None or state.state == STATE_UNAVAILABLE: + self._attr_available = False + return + + self._attr_available = True + + async def async_added_to_hass(self) -> None: + """Register callbacks.""" + + @callback + def _async_state_changed_listener(event: Event | None = None) -> None: + """Handle child updates.""" + self.async_state_changed_listener(event) + self.async_write_ha_state() + + self.async_on_remove( + async_track_state_change_event( + self.hass, [self._switch_entity_id], _async_state_changed_listener + ) + ) + + # Call once on adding + _async_state_changed_listener() + + # Add this entity to the wrapped switch's device + registry = er.async_get(self.hass) + if registry.async_get(self.entity_id) is not None: + registry.async_update_entity(self.entity_id, device_id=self._device_id) + + +class BaseToggleEntity(BaseEntity, ToggleEntity): + """Represents a Switch as a ToggleEntity.""" + + async def async_turn_on(self, **kwargs: Any) -> None: + """Forward the turn_on command to the switch in this light switch.""" + await self.hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: self._switch_entity_id}, + blocking=True, + context=self._context, + ) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Forward the turn_off command to the switch in this light switch.""" + await self.hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: self._switch_entity_id}, + blocking=True, + context=self._context, + ) + + @callback + def async_state_changed_listener(self, event: Event | None = None) -> None: + """Handle child updates.""" + super().async_state_changed_listener(event) + if ( + not self.available + or (state := self.hass.states.get(self._switch_entity_id)) is None + ): + return + + self._attr_is_on = state.state == STATE_ON diff --git a/homeassistant/components/switch_as_x/fan.py b/homeassistant/components/switch_as_x/fan.py new file mode 100644 index 00000000000..5ebc8902d06 --- /dev/null +++ b/homeassistant/components/switch_as_x/fan.py @@ -0,0 +1,63 @@ +"""Fan support for switch entities.""" +from __future__ import annotations + +from homeassistant.components.fan import FanEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_ENTITY_ID +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .entity import BaseToggleEntity + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Initialize Fan Switch config entry.""" + registry = er.async_get(hass) + entity_id = er.async_validate_entity_id( + registry, config_entry.options[CONF_ENTITY_ID] + ) + wrapped_switch = registry.async_get(entity_id) + device_id = wrapped_switch.device_id if wrapped_switch else None + + async_add_entities( + [ + FanSwitch( + config_entry.title, + entity_id, + config_entry.entry_id, + device_id, + ) + ] + ) + + +class FanSwitch(BaseToggleEntity, FanEntity): + """Represents a Switch as a Fan.""" + + @property + def is_on(self) -> bool | None: + """Return true if the entity is on. + + Fan logic uses speed percentage or preset mode to determine + if it's on or off, however, when using a wrapped switch, we + just use the wrapped switch's state. + """ + return self._attr_is_on + + async def async_turn_on( + self, + percentage: int | None = None, + preset_mode: str | None = None, + **kwargs, + ) -> None: + """Turn on the fan. + + Arguments of the turn_on methods fan entity differ, + thus we need to override them here. + """ + await super().async_turn_on() diff --git a/homeassistant/components/switch_as_x/light.py b/homeassistant/components/switch_as_x/light.py new file mode 100644 index 00000000000..93dba7c8551 --- /dev/null +++ b/homeassistant/components/switch_as_x/light.py @@ -0,0 +1,43 @@ +"""Light support for switch entities.""" +from __future__ import annotations + +from homeassistant.components.light import COLOR_MODE_ONOFF, LightEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_ENTITY_ID +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .entity import BaseToggleEntity + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Initialize Light Switch config entry.""" + registry = er.async_get(hass) + entity_id = er.async_validate_entity_id( + registry, config_entry.options[CONF_ENTITY_ID] + ) + wrapped_switch = registry.async_get(entity_id) + device_id = wrapped_switch.device_id if wrapped_switch else None + + async_add_entities( + [ + LightSwitch( + config_entry.title, + entity_id, + config_entry.entry_id, + device_id, + ) + ] + ) + + +class LightSwitch(BaseToggleEntity, LightEntity): + """Represents a Switch as a Light.""" + + _attr_color_mode = COLOR_MODE_ONOFF + _attr_supported_color_modes = {COLOR_MODE_ONOFF} diff --git a/homeassistant/components/switch_as_x/lock.py b/homeassistant/components/switch_as_x/lock.py new file mode 100644 index 00000000000..0eaabb03770 --- /dev/null +++ b/homeassistant/components/switch_as_x/lock.py @@ -0,0 +1,83 @@ +"""Lock support for switch entities.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.components.lock import LockEntity +from homeassistant.components.switch.const import DOMAIN as SWITCH_DOMAIN +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + ATTR_ENTITY_ID, + CONF_ENTITY_ID, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_ON, +) +from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .entity import BaseEntity + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Initialize Lock Switch config entry.""" + registry = er.async_get(hass) + entity_id = er.async_validate_entity_id( + registry, config_entry.options[CONF_ENTITY_ID] + ) + wrapped_switch = registry.async_get(entity_id) + device_id = wrapped_switch.device_id if wrapped_switch else None + + async_add_entities( + [ + LockSwitch( + config_entry.title, + entity_id, + config_entry.entry_id, + device_id, + ) + ] + ) + + +class LockSwitch(BaseEntity, LockEntity): + """Represents a Switch as a Lock.""" + + async def async_lock(self, **kwargs: Any) -> None: + """Lock the lock.""" + await self.hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: self._switch_entity_id}, + blocking=True, + context=self._context, + ) + + async def async_unlock(self, **kwargs: Any) -> None: + """Unlock the lock.""" + await self.hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: self._switch_entity_id}, + blocking=True, + context=self._context, + ) + + @callback + def async_state_changed_listener(self, event: Event | None = None) -> None: + """Handle child updates.""" + super().async_state_changed_listener(event) + if ( + not self.available + or (state := self.hass.states.get(self._switch_entity_id)) is None + ): + return + + # Logic is the same as the lock device class for binary sensors + # on means open (unlocked), off means closed (locked) + self._attr_is_locked = state.state != STATE_ON diff --git a/homeassistant/components/switch_as_x/manifest.json b/homeassistant/components/switch_as_x/manifest.json new file mode 100644 index 00000000000..ec748157743 --- /dev/null +++ b/homeassistant/components/switch_as_x/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "switch_as_x", + "integration_type": "helper", + "name": "Switch as X", + "documentation": "https://www.home-assistant.io/integrations/switch_as_x", + "codeowners": ["@home-assistant/core"], + "quality_scale": "internal", + "iot_class": "calculated", + "config_flow": true +} diff --git a/homeassistant/components/switch_as_x/siren.py b/homeassistant/components/switch_as_x/siren.py new file mode 100644 index 00000000000..752f7fd76ad --- /dev/null +++ b/homeassistant/components/switch_as_x/siren.py @@ -0,0 +1,43 @@ +"""Siren support for switch entities.""" +from __future__ import annotations + +from homeassistant.components.siren import SirenEntity +from homeassistant.components.siren.const import SUPPORT_TURN_OFF, SUPPORT_TURN_ON +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_ENTITY_ID +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .entity import BaseToggleEntity + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Initialize Siren Switch config entry.""" + registry = er.async_get(hass) + entity_id = er.async_validate_entity_id( + registry, config_entry.options[CONF_ENTITY_ID] + ) + wrapped_switch = registry.async_get(entity_id) + device_id = wrapped_switch.device_id if wrapped_switch else None + + async_add_entities( + [ + SirenSwitch( + config_entry.title, + entity_id, + config_entry.entry_id, + device_id, + ) + ] + ) + + +class SirenSwitch(BaseToggleEntity, SirenEntity): + """Represents a Switch as a Siren.""" + + _attr_supported_features = SUPPORT_TURN_ON | SUPPORT_TURN_OFF diff --git a/homeassistant/components/switch_as_x/strings.json b/homeassistant/components/switch_as_x/strings.json new file mode 100644 index 00000000000..10adfd7686e --- /dev/null +++ b/homeassistant/components/switch_as_x/strings.json @@ -0,0 +1,14 @@ +{ + "title": "Change device type of a switch", + "config": { + "step": { + "user": { + "description": "Pick a switch that you want to show up in Home Assistant as a light, cover or anything else. The original switch will be hidden.", + "data": { + "entity_id": "Switch", + "target_domain": "New Type" + } + } + } + } +} diff --git a/homeassistant/components/switch_as_x/translations/ca.json b/homeassistant/components/switch_as_x/translations/ca.json new file mode 100644 index 00000000000..07d6288d33e --- /dev/null +++ b/homeassistant/components/switch_as_x/translations/ca.json @@ -0,0 +1,14 @@ +{ + "config": { + "step": { + "init": { + "data": { + "entity_id": "Entitat d'interruptor", + "target_domain": "Tipus" + }, + "title": "Converteix un interruptor en \u2026" + } + } + }, + "title": "Interruptor com a X" +} \ No newline at end of file diff --git a/homeassistant/components/switch_as_x/translations/de.json b/homeassistant/components/switch_as_x/translations/de.json new file mode 100644 index 00000000000..63a99ad40e2 --- /dev/null +++ b/homeassistant/components/switch_as_x/translations/de.json @@ -0,0 +1,14 @@ +{ + "config": { + "step": { + "init": { + "data": { + "entity_id": "Switch-Entit\u00e4t", + "target_domain": "Typ" + }, + "title": "Mache einen Schalter zu..." + } + } + }, + "title": "Schalter als X" +} \ No newline at end of file diff --git a/homeassistant/components/switch_as_x/translations/el.json b/homeassistant/components/switch_as_x/translations/el.json new file mode 100644 index 00000000000..1fb546c13b7 --- /dev/null +++ b/homeassistant/components/switch_as_x/translations/el.json @@ -0,0 +1,14 @@ +{ + "config": { + "step": { + "init": { + "data": { + "entity_id": "\u039f\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1 \u03b4\u03b9\u03b1\u03ba\u03cc\u03c0\u03c4\u03b7", + "target_domain": "\u03a4\u03cd\u03c0\u03bf\u03c2" + }, + "title": "\u039a\u03ac\u03bd\u03c4\u03b5 \u03ad\u03bd\u03b1\u03bd \u03b4\u03b9\u03b1\u03ba\u03cc\u03c0\u03c4\u03b7 \u03ad\u03bd\u03b1 ..." + } + } + }, + "title": "Switch as X" +} \ No newline at end of file diff --git a/homeassistant/components/switch_as_x/translations/en.json b/homeassistant/components/switch_as_x/translations/en.json new file mode 100644 index 00000000000..7709a27cf35 --- /dev/null +++ b/homeassistant/components/switch_as_x/translations/en.json @@ -0,0 +1,14 @@ +{ + "config": { + "step": { + "user": { + "data": { + "entity_id": "Switch", + "target_domain": "New Type" + }, + "description": "Pick a switch that you want to show up in Home Assistant as a light, cover or anything else. The original switch will be hidden." + } + } + }, + "title": "Change device type of a switch" +} \ No newline at end of file diff --git a/homeassistant/components/switch_as_x/translations/et.json b/homeassistant/components/switch_as_x/translations/et.json new file mode 100644 index 00000000000..9e18ddced09 --- /dev/null +++ b/homeassistant/components/switch_as_x/translations/et.json @@ -0,0 +1,14 @@ +{ + "config": { + "step": { + "init": { + "data": { + "entity_id": "L\u00fcliti olem", + "target_domain": "T\u00fc\u00fcp" + }, + "title": "Tee l\u00fcliti ..." + } + } + }, + "title": "L\u00fclita kui X" +} \ No newline at end of file diff --git a/homeassistant/components/switch_as_x/translations/fr.json b/homeassistant/components/switch_as_x/translations/fr.json new file mode 100644 index 00000000000..4b5bd6beebe --- /dev/null +++ b/homeassistant/components/switch_as_x/translations/fr.json @@ -0,0 +1,14 @@ +{ + "config": { + "step": { + "init": { + "data": { + "entity_id": "Entit\u00e9 du commutateur", + "target_domain": "Type" + }, + "title": "Transformer un commutateur en \u2026" + } + } + }, + "title": "Commutateur en tant que X" +} \ No newline at end of file diff --git a/homeassistant/components/switch_as_x/translations/he.json b/homeassistant/components/switch_as_x/translations/he.json new file mode 100644 index 00000000000..8ca833876fe --- /dev/null +++ b/homeassistant/components/switch_as_x/translations/he.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "init": { + "data": { + "entity_id": "\u05d9\u05e9\u05d5\u05ea \u05de\u05ea\u05d2" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switch_as_x/translations/hu.json b/homeassistant/components/switch_as_x/translations/hu.json new file mode 100644 index 00000000000..b3ea0af39fd --- /dev/null +++ b/homeassistant/components/switch_as_x/translations/hu.json @@ -0,0 +1,14 @@ +{ + "config": { + "step": { + "init": { + "data": { + "entity_id": "Kapcsol\u00f3 entit\u00e1s", + "target_domain": "T\u00edpus" + }, + "title": "Kapcsol\u00f3 mint..." + } + } + }, + "title": "Kapcsol\u00f3 mint X" +} \ No newline at end of file diff --git a/homeassistant/components/switch_as_x/translations/id.json b/homeassistant/components/switch_as_x/translations/id.json new file mode 100644 index 00000000000..1a5cce08f01 --- /dev/null +++ b/homeassistant/components/switch_as_x/translations/id.json @@ -0,0 +1,14 @@ +{ + "config": { + "step": { + "init": { + "data": { + "entity_id": "Entitas saklar", + "target_domain": "Jenis" + }, + "title": "Jadikan saklar sebagai\u2026" + } + } + }, + "title": "Saklar sebagai X" +} \ No newline at end of file diff --git a/homeassistant/components/switch_as_x/translations/it.json b/homeassistant/components/switch_as_x/translations/it.json new file mode 100644 index 00000000000..1ef154b0578 --- /dev/null +++ b/homeassistant/components/switch_as_x/translations/it.json @@ -0,0 +1,14 @@ +{ + "config": { + "step": { + "init": { + "data": { + "entity_id": "Cambia entit\u00e0", + "target_domain": "Tipo" + }, + "title": "Rendi un interruttore un..." + } + } + }, + "title": "Interruttore come X" +} \ No newline at end of file diff --git a/homeassistant/components/switch_as_x/translations/ja.json b/homeassistant/components/switch_as_x/translations/ja.json new file mode 100644 index 00000000000..44ceaecdd75 --- /dev/null +++ b/homeassistant/components/switch_as_x/translations/ja.json @@ -0,0 +1,14 @@ +{ + "config": { + "step": { + "init": { + "data": { + "entity_id": "\u30b9\u30a4\u30c3\u30c1\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3", + "target_domain": "\u30bf\u30a4\u30d7" + }, + "title": "\u30b9\u30a4\u30c3\u30c1\u3092..." + } + } + }, + "title": "X\u3068\u3057\u3066\u5207\u308a\u66ff\u3048\u308b" +} \ No newline at end of file diff --git a/homeassistant/components/switch_as_x/translations/nl.json b/homeassistant/components/switch_as_x/translations/nl.json new file mode 100644 index 00000000000..b1712904a76 --- /dev/null +++ b/homeassistant/components/switch_as_x/translations/nl.json @@ -0,0 +1,14 @@ +{ + "config": { + "step": { + "init": { + "data": { + "entity_id": "Entiteit wijzigen", + "target_domain": "Type" + }, + "title": "Schakel een..." + } + } + }, + "title": "Schakelen als X" +} \ No newline at end of file diff --git a/homeassistant/components/switch_as_x/translations/no.json b/homeassistant/components/switch_as_x/translations/no.json new file mode 100644 index 00000000000..09d8d4e813e --- /dev/null +++ b/homeassistant/components/switch_as_x/translations/no.json @@ -0,0 +1,14 @@ +{ + "config": { + "step": { + "init": { + "data": { + "entity_id": "Bytt enhet", + "target_domain": "Type" + }, + "title": "Gj\u00f8r en bryter til en ..." + } + } + }, + "title": "Bryter som X" +} \ No newline at end of file diff --git a/homeassistant/components/switch_as_x/translations/pl.json b/homeassistant/components/switch_as_x/translations/pl.json new file mode 100644 index 00000000000..c3f9af2601d --- /dev/null +++ b/homeassistant/components/switch_as_x/translations/pl.json @@ -0,0 +1,14 @@ +{ + "config": { + "step": { + "init": { + "data": { + "entity_id": "Encja prze\u0142\u0105cznika", + "target_domain": "Rodzaj" + }, + "title": "Zmie\u0144 prze\u0142\u0105cznik na ..." + } + } + }, + "title": "Prze\u0142\u0105cznik jako \"X\"" +} \ No newline at end of file diff --git a/homeassistant/components/switch_as_x/translations/pt-BR.json b/homeassistant/components/switch_as_x/translations/pt-BR.json new file mode 100644 index 00000000000..e8ccabe7841 --- /dev/null +++ b/homeassistant/components/switch_as_x/translations/pt-BR.json @@ -0,0 +1,14 @@ +{ + "config": { + "step": { + "init": { + "data": { + "entity_id": "Entidade de interruptor", + "target_domain": "Tipo" + }, + "title": "Fa\u00e7a um interruptor um ..." + } + } + }, + "title": "Switch as X" +} \ No newline at end of file diff --git a/homeassistant/components/switch_as_x/translations/pt.json b/homeassistant/components/switch_as_x/translations/pt.json new file mode 100644 index 00000000000..558030dc42d --- /dev/null +++ b/homeassistant/components/switch_as_x/translations/pt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "init": { + "data": { + "target_domain": "Tipo" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switch_as_x/translations/ru.json b/homeassistant/components/switch_as_x/translations/ru.json new file mode 100644 index 00000000000..b4136768f03 --- /dev/null +++ b/homeassistant/components/switch_as_x/translations/ru.json @@ -0,0 +1,14 @@ +{ + "config": { + "step": { + "init": { + "data": { + "entity_id": "\u0412\u044b\u043a\u043b\u044e\u0447\u0430\u0442\u0435\u043b\u044c", + "target_domain": "\u0422\u0438\u043f" + }, + "title": "\u041e\u0442\u043e\u0431\u0440\u0430\u0436\u0430\u0442\u044c \u0432\u044b\u043a\u043b\u044e\u0447\u0430\u0442\u0435\u043b\u044c \u043a\u0430\u043a \u2026" + } + } + }, + "title": "\u0412\u044b\u043a\u043b\u044e\u0447\u0430\u0442\u0435\u043b\u044c \u043a\u0430\u043a \u2026" +} \ No newline at end of file diff --git a/homeassistant/components/switch_as_x/translations/sv.json b/homeassistant/components/switch_as_x/translations/sv.json new file mode 100644 index 00000000000..835de2f5a7f --- /dev/null +++ b/homeassistant/components/switch_as_x/translations/sv.json @@ -0,0 +1,14 @@ +{ + "config": { + "step": { + "init": { + "data": { + "entity_id": "Kontakt-entitet", + "target_domain": "Typ" + }, + "title": "G\u00f6r en kontakt till ..." + } + } + }, + "title": "Kontakt som X" +} \ No newline at end of file diff --git a/homeassistant/components/switch_as_x/translations/zh-Hant.json b/homeassistant/components/switch_as_x/translations/zh-Hant.json new file mode 100644 index 00000000000..231f5b58eff --- /dev/null +++ b/homeassistant/components/switch_as_x/translations/zh-Hant.json @@ -0,0 +1,14 @@ +{ + "config": { + "step": { + "init": { + "data": { + "entity_id": "\u958b\u95dc\u5be6\u9ad4", + "target_domain": "\u985e\u5225" + }, + "title": "\u5c07\u958b\u95dc\u8a2d\u5b9a\u70ba ..." + } + } + }, + "title": "\u958b\u95dc\u8a2d\u70ba X" +} \ No newline at end of file diff --git a/homeassistant/components/switchbot/binary_sensor.py b/homeassistant/components/switchbot/binary_sensor.py index c8b3aec4573..c3f88e924ea 100644 --- a/homeassistant/components/switchbot/binary_sensor.py +++ b/homeassistant/components/switchbot/binary_sensor.py @@ -54,8 +54,6 @@ async def async_setup_entry( class SwitchBotBinarySensor(SwitchbotEntity, BinarySensorEntity): """Representation of a Switchbot binary sensor.""" - coordinator: SwitchbotDataUpdateCoordinator - def __init__( self, coordinator: SwitchbotDataUpdateCoordinator, diff --git a/homeassistant/components/switchbot/config_flow.py b/homeassistant/components/switchbot/config_flow.py index 2d4e61bada5..70e032414a7 100644 --- a/homeassistant/components/switchbot/config_flow.py +++ b/homeassistant/components/switchbot/config_flow.py @@ -131,19 +131,6 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN): step_id="user", data_schema=data_schema, errors=errors ) - async def async_step_import(self, import_config: dict[str, Any]) -> FlowResult: - """Handle config import from yaml.""" - _LOGGER.debug("import config: %s", import_config) - - import_config[CONF_MAC] = import_config[CONF_MAC].replace("-", ":").lower() - - await self.async_set_unique_id(import_config[CONF_MAC].replace(":", "")) - self._abort_if_unique_id_configured() - - return self.async_create_entry( - title=import_config[CONF_NAME], data=import_config - ) - class SwitchbotOptionsFlowHandler(OptionsFlow): """Handle Switchbot options.""" diff --git a/homeassistant/components/switchbot/cover.py b/homeassistant/components/switchbot/cover.py index 9814534ce6d..97ca1aa15bb 100644 --- a/homeassistant/components/switchbot/cover.py +++ b/homeassistant/components/switchbot/cover.py @@ -59,7 +59,6 @@ async def async_setup_entry( class SwitchBotCurtainEntity(SwitchbotEntity, CoverEntity, RestoreEntity): """Representation of a Switchbot.""" - coordinator: SwitchbotDataUpdateCoordinator _attr_device_class = CoverDeviceClass.CURTAIN _attr_supported_features = ( SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_STOP | SUPPORT_SET_POSITION diff --git a/homeassistant/components/switchbot/entity.py b/homeassistant/components/switchbot/entity.py index 688cfea6a86..c27c40613c7 100644 --- a/homeassistant/components/switchbot/entity.py +++ b/homeassistant/components/switchbot/entity.py @@ -12,7 +12,7 @@ from .const import MANUFACTURER from .coordinator import SwitchbotDataUpdateCoordinator -class SwitchbotEntity(CoordinatorEntity, Entity): +class SwitchbotEntity(CoordinatorEntity[SwitchbotDataUpdateCoordinator], Entity): """Generic entity encapsulating common features of Switchbot device.""" def __init__( diff --git a/homeassistant/components/switchbot/sensor.py b/homeassistant/components/switchbot/sensor.py index c3afff27654..1ee0276b7ee 100644 --- a/homeassistant/components/switchbot/sensor.py +++ b/homeassistant/components/switchbot/sensor.py @@ -74,8 +74,6 @@ async def async_setup_entry( class SwitchBotSensor(SwitchbotEntity, SensorEntity): """Representation of a Switchbot sensor.""" - coordinator: SwitchbotDataUpdateCoordinator - def __init__( self, coordinator: SwitchbotDataUpdateCoordinator, diff --git a/homeassistant/components/switchbot/switch.py b/homeassistant/components/switchbot/switch.py index 845d27488ad..b5507594521 100644 --- a/homeassistant/components/switchbot/switch.py +++ b/homeassistant/components/switchbot/switch.py @@ -5,27 +5,15 @@ import logging from typing import Any from switchbot import Switchbot # pylint: disable=import-error -import voluptuous as vol -from homeassistant.components.switch import ( - PLATFORM_SCHEMA, - SwitchDeviceClass, - SwitchEntity, -) -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import ( - CONF_MAC, - CONF_NAME, - CONF_PASSWORD, - CONF_SENSOR_TYPE, - STATE_ON, -) +from homeassistant.components.switch import SwitchDeviceClass, SwitchEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_MAC, CONF_NAME, CONF_PASSWORD, STATE_ON from homeassistant.core import HomeAssistant -from homeassistant.helpers import config_validation as cv, entity_platform +from homeassistant.helpers import entity_platform from homeassistant.helpers.restore_state import RestoreEntity -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from .const import ATTR_BOT, CONF_RETRY_COUNT, DATA_COORDINATOR, DEFAULT_NAME, DOMAIN +from .const import CONF_RETRY_COUNT, DATA_COORDINATOR, DOMAIN from .coordinator import SwitchbotDataUpdateCoordinator from .entity import SwitchbotEntity @@ -33,46 +21,6 @@ from .entity import SwitchbotEntity _LOGGER = logging.getLogger(__name__) PARALLEL_UPDATES = 1 -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_MAC): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_PASSWORD): cv.string, - } -) - - -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: entity_platform.AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Import yaml config and initiates config flow for Switchbot devices.""" - _LOGGER.warning( - "Configuration of the Switchbot switch platform in YAML is deprecated and " - "will be removed in Home Assistant 2022.4; Your existing configuration " - "has been imported into the UI automatically and can be safely removed " - "from your configuration.yaml file" - ) - - # Check if entry config exists and skips import if it does. - if hass.config_entries.async_entries(DOMAIN): - return - - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={ - CONF_NAME: config[CONF_NAME], - CONF_PASSWORD: config.get(CONF_PASSWORD, None), - CONF_MAC: config[CONF_MAC].replace("-", ":").lower(), - CONF_SENSOR_TYPE: ATTR_BOT, - }, - ) - ) - async def async_setup_entry( hass: HomeAssistant, @@ -104,7 +52,6 @@ async def async_setup_entry( class SwitchBotBotEntity(SwitchbotEntity, SwitchEntity, RestoreEntity): """Representation of a Switchbot.""" - coordinator: SwitchbotDataUpdateCoordinator _attr_device_class = SwitchDeviceClass.SWITCH def __init__( diff --git a/homeassistant/components/switchbot/translations/fr.json b/homeassistant/components/switchbot/translations/fr.json index c554326c5da..41b52f253ac 100644 --- a/homeassistant/components/switchbot/translations/fr.json +++ b/homeassistant/components/switchbot/translations/fr.json @@ -3,7 +3,7 @@ "abort": { "already_configured_device": "L'appareil est d\u00e9j\u00e0 configur\u00e9", "cannot_connect": "\u00c9chec de connexion", - "no_unconfigured_devices": "Aucun p\u00e9riph\u00e9rique non configur\u00e9 trouv\u00e9.", + "no_unconfigured_devices": "Aucun appareil non configur\u00e9 n'a \u00e9t\u00e9 trouv\u00e9.", "switchbot_unsupported_type": "Type Switchbot non pris en charge.", "unknown": "Erreur inattendue" }, @@ -28,8 +28,8 @@ "data": { "retry_count": "Nombre de nouvelles tentatives", "retry_timeout": "D\u00e9lai d'attente entre les tentatives", - "scan_timeout": "Combien de temps pour rechercher les donn\u00e9es publicitaires", - "update_time": "Temps entre les mises \u00e0 jour (secondes)" + "scan_timeout": "Dur\u00e9e de la recherche de donn\u00e9es publicitaires", + "update_time": "Dur\u00e9e (en secondes) entre deux mises \u00e0 jour" } } } diff --git a/homeassistant/components/switchbot/translations/zh-Hant.json b/homeassistant/components/switchbot/translations/zh-Hant.json index 44fe1fe5c54..8e7b4495328 100644 --- a/homeassistant/components/switchbot/translations/zh-Hant.json +++ b/homeassistant/components/switchbot/translations/zh-Hant.json @@ -4,7 +4,7 @@ "already_configured_device": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "cannot_connect": "\u9023\u7dda\u5931\u6557", "no_unconfigured_devices": "\u627e\u4e0d\u5230\u4efb\u4f55\u672a\u8a2d\u5b9a\u88dd\u7f6e\u3002", - "switchbot_unsupported_type": "\u4e0d\u652f\u6301\u7684 Switchbot \u985e\u578b\u3002", + "switchbot_unsupported_type": "\u4e0d\u652f\u6301\u7684 Switchbot \u985e\u5225\u3002", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "error": { diff --git a/homeassistant/components/switcher_kis/manifest.json b/homeassistant/components/switcher_kis/manifest.json index 9ebf83b4acd..f5b7bdae148 100644 --- a/homeassistant/components/switcher_kis/manifest.json +++ b/homeassistant/components/switcher_kis/manifest.json @@ -2,7 +2,7 @@ "domain": "switcher_kis", "name": "Switcher", "documentation": "https://www.home-assistant.io/integrations/switcher_kis/", - "codeowners": ["@tomerfi","@thecode"], + "codeowners": ["@tomerfi", "@thecode"], "requirements": ["aioswitcher==2.0.6"], "quality_scale": "platinum", "iot_class": "local_push", diff --git a/homeassistant/components/switcher_kis/sensor.py b/homeassistant/components/switcher_kis/sensor.py index 6b6bf1bec4a..c8dced8663c 100644 --- a/homeassistant/components/switcher_kis/sensor.py +++ b/homeassistant/components/switcher_kis/sensor.py @@ -92,7 +92,9 @@ async def async_setup_entry( ) -class SwitcherSensorEntity(CoordinatorEntity, SensorEntity): +class SwitcherSensorEntity( + CoordinatorEntity[SwitcherDataUpdateCoordinator], SensorEntity +): """Representation of a Switcher sensor entity.""" def __init__( diff --git a/homeassistant/components/switcher_kis/services.yaml b/homeassistant/components/switcher_kis/services.yaml index 752b7f3de4c..a7c3df5903e 100644 --- a/homeassistant/components/switcher_kis/services.yaml +++ b/homeassistant/components/switcher_kis/services.yaml @@ -17,7 +17,7 @@ set_auto_off: turn_on_with_timer: name: Turn on with timer - description: 'Turn on the Switcher device with timer.' + description: "Turn on the Switcher device with timer." target: entity: integration: switcher_kis @@ -26,7 +26,7 @@ turn_on_with_timer: fields: timer_minutes: name: Timer - description: 'Time to turn on.' + description: "Time to turn on." required: true selector: number: diff --git a/homeassistant/components/switcher_kis/strings.json b/homeassistant/components/switcher_kis/strings.json index 9f3518bcf8d..ad8f0f41ae7 100644 --- a/homeassistant/components/switcher_kis/strings.json +++ b/homeassistant/components/switcher_kis/strings.json @@ -10,4 +10,4 @@ "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]" } } -} \ No newline at end of file +} diff --git a/homeassistant/components/switcher_kis/switch.py b/homeassistant/components/switcher_kis/switch.py index 6df841b1e4e..0065038954f 100644 --- a/homeassistant/components/switcher_kis/switch.py +++ b/homeassistant/components/switcher_kis/switch.py @@ -78,7 +78,9 @@ async def async_setup_entry( ) -class SwitcherBaseSwitchEntity(CoordinatorEntity, SwitchEntity): +class SwitcherBaseSwitchEntity( + CoordinatorEntity[SwitcherDataUpdateCoordinator], SwitchEntity +): """Representation of a Switcher switch entity.""" def __init__(self, coordinator: SwitcherDataUpdateCoordinator) -> None: diff --git a/homeassistant/components/switcher_kis/translations/fr.json b/homeassistant/components/switcher_kis/translations/fr.json index e9ae4e0b644..3d447c0cbb0 100644 --- a/homeassistant/components/switcher_kis/translations/fr.json +++ b/homeassistant/components/switcher_kis/translations/fr.json @@ -6,7 +6,7 @@ }, "step": { "confirm": { - "description": "Voulez-vous commencer la configuration ?" + "description": "Voulez-vous commencer la configuration\u00a0?" } } } diff --git a/homeassistant/components/syncthing/manifest.json b/homeassistant/components/syncthing/manifest.json index 9d2897abf66..f1c08fa250c 100644 --- a/homeassistant/components/syncthing/manifest.json +++ b/homeassistant/components/syncthing/manifest.json @@ -4,9 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/syncthing", "requirements": ["aiosyncthing==0.5.1"], - "codeowners": [ - "@zhulik" - ], + "codeowners": ["@zhulik"], "quality_scale": "silver", "iot_class": "local_polling", "loggers": ["aiosyncthing"] diff --git a/homeassistant/components/syncthing/translations/fr.json b/homeassistant/components/syncthing/translations/fr.json index 99c31269565..f0d93986445 100644 --- a/homeassistant/components/syncthing/translations/fr.json +++ b/homeassistant/components/syncthing/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide" + "invalid_auth": "Authentification non valide" }, "step": { "user": { diff --git a/homeassistant/components/syncthru/translations/fr.json b/homeassistant/components/syncthru/translations/fr.json index 1936e2f4a16..0aaae24fe5c 100644 --- a/homeassistant/components/syncthru/translations/fr.json +++ b/homeassistant/components/syncthru/translations/fr.json @@ -4,7 +4,7 @@ "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" }, "error": { - "invalid_url": "URL invalide", + "invalid_url": "URL non valide", "syncthru_not_supported": "L'appareil ne prend pas en charge SyncThru", "unknown_state": "\u00c9tat de l'imprimante inconnu, v\u00e9rifiez l'URL et la connectivit\u00e9 r\u00e9seau" }, diff --git a/homeassistant/components/synology_dsm/__init__.py b/homeassistant/components/synology_dsm/__init__.py index e07df808198..8eed02118c0 100644 --- a/homeassistant/components/synology_dsm/__init__.py +++ b/homeassistant/components/synology_dsm/__init__.py @@ -98,14 +98,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: SynologyDSMLoginPermissionDeniedException, ) as err: if err.args[0] and isinstance(err.args[0], dict): - # pylint: disable=no-member details = err.args[0].get(EXCEPTION_DETAILS, EXCEPTION_UNKNOWN) else: details = EXCEPTION_UNKNOWN raise ConfigEntryAuthFailed(f"reason: {details}") from err except (SynologyDSMLoginFailedException, SynologyDSMRequestException) as err: if err.args[0] and isinstance(err.args[0], dict): - # pylint: disable=no-member details = err.args[0].get(EXCEPTION_DETAILS, EXCEPTION_UNKNOWN) else: details = EXCEPTION_UNKNOWN @@ -227,7 +225,9 @@ async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> Non await hass.config_entries.async_reload(entry.entry_id) -class SynologyDSMBaseEntity(CoordinatorEntity): +class SynologyDSMBaseEntity( + CoordinatorEntity[DataUpdateCoordinator[dict[str, dict[str, Any]]]] +): """Representation of a Synology NAS entry.""" entity_description: SynologyDSMEntityDescription diff --git a/homeassistant/components/synology_dsm/const.py b/homeassistant/components/synology_dsm/const.py index 37100f1a759..78f1a1b916d 100644 --- a/homeassistant/components/synology_dsm/const.py +++ b/homeassistant/components/synology_dsm/const.py @@ -38,6 +38,7 @@ PLATFORMS = [ Platform.CAMERA, Platform.SENSOR, Platform.SWITCH, + Platform.UPDATE, ] COORDINATOR_CAMERAS = "coordinator_cameras" COORDINATOR_CENTRAL = "coordinator_central" @@ -112,9 +113,11 @@ class SynologyDSMSwitchEntityDescription( # Binary sensors UPGRADE_BINARY_SENSORS: tuple[SynologyDSMBinarySensorEntityDescription, ...] = ( SynologyDSMBinarySensorEntityDescription( + # Deprecated, scheduled to be removed in 2022.6 (#68664) api_key=SynoCoreUpgrade.API_KEY, key="update_available", name="Update Available", + entity_registry_enabled_default=False, device_class=BinarySensorDeviceClass.UPDATE, entity_category=EntityCategory.DIAGNOSTIC, ), diff --git a/homeassistant/components/synology_dsm/manifest.json b/homeassistant/components/synology_dsm/manifest.json index 1c9df126a89..7dcf7cf702a 100644 --- a/homeassistant/components/synology_dsm/manifest.json +++ b/homeassistant/components/synology_dsm/manifest.json @@ -2,7 +2,7 @@ "domain": "synology_dsm", "name": "Synology DSM", "documentation": "https://www.home-assistant.io/integrations/synology_dsm", - "requirements": ["py-synologydsm-api==1.0.7"], + "requirements": ["py-synologydsm-api==1.0.8"], "codeowners": ["@hacf-fr", "@Quentame", "@mib1185"], "config_flow": true, "ssdp": [ diff --git a/homeassistant/components/synology_dsm/strings.json b/homeassistant/components/synology_dsm/strings.json index 501cbfb5fff..6b13226aaee 100644 --- a/homeassistant/components/synology_dsm/strings.json +++ b/homeassistant/components/synology_dsm/strings.json @@ -3,7 +3,6 @@ "flow_title": "{name} ({host})", "step": { "user": { - "title": "Synology DSM", "data": { "host": "[%key:common::config_flow::data::host%]", "port": "[%key:common::config_flow::data::port%]", @@ -20,7 +19,6 @@ } }, "link": { - "title": "Synology DSM", "description": "Do you want to setup {name} ({host})?", "data": { "ssl": "[%key:common::config_flow::data::ssl%]", diff --git a/homeassistant/components/synology_dsm/translations/fr.json b/homeassistant/components/synology_dsm/translations/fr.json index 34766766828..c6488ea7356 100644 --- a/homeassistant/components/synology_dsm/translations/fr.json +++ b/homeassistant/components/synology_dsm/translations/fr.json @@ -7,7 +7,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "missing_data": "Donn\u00e9es manquantes: veuillez r\u00e9essayer plus tard ou utilisez une autre configuration", "otp_failed": "\u00c9chec de l'authentification en deux \u00e9tapes, r\u00e9essayez avec un nouveau code d'acc\u00e8s", "unknown": "Erreur inattendue" diff --git a/homeassistant/components/synology_dsm/update.py b/homeassistant/components/synology_dsm/update.py new file mode 100644 index 00000000000..836468d6f50 --- /dev/null +++ b/homeassistant/components/synology_dsm/update.py @@ -0,0 +1,82 @@ +"""Support for Synology DSM update platform.""" +from __future__ import annotations + +from dataclasses import dataclass +from typing import Final + +from synology_dsm.api.core.upgrade import SynoCoreUpgrade +from yarl import URL + +from homeassistant.components.update import UpdateEntity, UpdateEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import SynoApi, SynologyDSMBaseEntity +from .const import COORDINATOR_CENTRAL, DOMAIN, SYNO_API, SynologyDSMEntityDescription + + +@dataclass +class SynologyDSMUpdateEntityEntityDescription( + UpdateEntityDescription, SynologyDSMEntityDescription +): + """Describes Synology DSM update entity.""" + + +UPDATE_ENTITIES: Final = [ + SynologyDSMUpdateEntityEntityDescription( + api_key=SynoCoreUpgrade.API_KEY, + key="update", + name="DSM Update", + entity_category=EntityCategory.DIAGNOSTIC, + ) +] + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up Synology DSM update entities.""" + data = hass.data[DOMAIN][entry.unique_id] + api: SynoApi = data[SYNO_API] + coordinator = data[COORDINATOR_CENTRAL] + + async_add_entities( + SynoDSMUpdateEntity(api, coordinator, description) + for description in UPDATE_ENTITIES + ) + + +class SynoDSMUpdateEntity(SynologyDSMBaseEntity, UpdateEntity): + """Mixin for update entity specific attributes.""" + + entity_description: SynologyDSMUpdateEntityEntityDescription + _attr_title = "Synology DSM" + + @property + def installed_version(self) -> str | None: + """Version installed and in use.""" + return self._api.information.version_string # type: ignore[no-any-return] + + @property + def latest_version(self) -> str | None: + """Latest version available for install.""" + if not self._api.upgrade.update_available: + return self.installed_version + return self._api.upgrade.available_version # type: ignore[no-any-return] + + @property + def release_url(self) -> str | None: + """URL to the full release notes of the latest version available.""" + if (details := self._api.upgrade.available_version_details) is None: + return None + + url = URL("http://update.synology.com/autoupdate/whatsnew.php") + query = {"model": self._api.information.model} + if details.get("nano") > 0: + query["update_version"] = f"{details['buildnumber']}-{details['nano']}" + else: + query["update_version"] = details["buildnumber"] + + return url.update_query(query).human_repr() diff --git a/homeassistant/components/system_bridge/__init__.py b/homeassistant/components/system_bridge/__init__.py index 37beb997755..c6edf5b61ea 100644 --- a/homeassistant/components/system_bridge/__init__.py +++ b/homeassistant/components/system_bridge/__init__.py @@ -289,7 +289,7 @@ async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: await hass.config_entries.async_reload(entry.entry_id) -class SystemBridgeEntity(CoordinatorEntity): +class SystemBridgeEntity(CoordinatorEntity[SystemBridgeDataUpdateCoordinator]): """Defines a base System Bridge entity.""" def __init__( diff --git a/homeassistant/components/system_bridge/binary_sensor.py b/homeassistant/components/system_bridge/binary_sensor.py index fca64dc6102..e592c8e82e4 100644 --- a/homeassistant/components/system_bridge/binary_sensor.py +++ b/homeassistant/components/system_bridge/binary_sensor.py @@ -67,7 +67,6 @@ async def async_setup_entry( class SystemBridgeBinarySensor(SystemBridgeDeviceEntity, BinarySensorEntity): """Define a System Bridge binary sensor.""" - coordinator: SystemBridgeDataUpdateCoordinator entity_description: SystemBridgeBinarySensorEntityDescription def __init__( diff --git a/homeassistant/components/system_bridge/sensor.py b/homeassistant/components/system_bridge/sensor.py index c4969e2c14c..e66749820a7 100644 --- a/homeassistant/components/system_bridge/sensor.py +++ b/homeassistant/components/system_bridge/sensor.py @@ -484,7 +484,6 @@ async def async_setup_entry( class SystemBridgeSensor(SystemBridgeDeviceEntity, SensorEntity): """Define a System Bridge sensor.""" - coordinator: SystemBridgeDataUpdateCoordinator entity_description: SystemBridgeSensorEntityDescription def __init__( diff --git a/homeassistant/components/system_bridge/translations/fr.json b/homeassistant/components/system_bridge/translations/fr.json index 1c2a1b23c9c..cbcad2d0330 100644 --- a/homeassistant/components/system_bridge/translations/fr.json +++ b/homeassistant/components/system_bridge/translations/fr.json @@ -7,7 +7,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "flow_title": "{name}", diff --git a/homeassistant/components/system_health/translations/el.json b/homeassistant/components/system_health/translations/el.json index e0c4c7043f5..825f8420d82 100644 --- a/homeassistant/components/system_health/translations/el.json +++ b/homeassistant/components/system_health/translations/el.json @@ -1,3 +1,3 @@ { - "title": "\u03a5\u03b3\u03b5\u03af\u03b1 \u03a3\u03c5\u03c3\u03c4\u03ae\u03bc\u03b1\u03c4\u03bf\u03c2" + "title": "\u03a5\u03b3\u03b5\u03af\u03b1 \u03c3\u03c5\u03c3\u03c4\u03ae\u03bc\u03b1\u03c4\u03bf\u03c2" } \ No newline at end of file diff --git a/homeassistant/components/system_log/services.yaml b/homeassistant/components/system_log/services.yaml index b6444bcecc5..0f9ae61ba4c 100644 --- a/homeassistant/components/system_log/services.yaml +++ b/homeassistant/components/system_log/services.yaml @@ -20,15 +20,19 @@ write: selector: select: options: - - "debug" - - "info" - - "warning" - - "error" - - "critical" + - label: "Debug" + value: "debug" + - label: "Info" + value: "info" + - label: "Warning" + value: "warning" + - label: "Error" + value: "error" + - label: "Critical" + value: "critical" logger: name: Logger - description: - Logger name under which to log the message. Defaults to + description: Logger name under which to log the message. Defaults to 'system_log.external'. example: mycomponent.myplatform selector: diff --git a/homeassistant/components/tado/__init__.py b/homeassistant/components/tado/__init__.py index 42fc806ab54..9a5e1eb9c1e 100644 --- a/homeassistant/components/tado/__init__.py +++ b/homeassistant/components/tado/__init__.py @@ -3,7 +3,6 @@ from datetime import timedelta import logging from PyTado.interface import Tado -from PyTado.zone import TadoZone from requests import RequestException import requests.exceptions @@ -19,6 +18,7 @@ from homeassistant.util import Throttle from .const import ( CONF_FALLBACK, + CONST_OVERLAY_TADO_MODE, DATA, DOMAIN, INSIDE_TEMPERATURE_MEASUREMENT, @@ -51,7 +51,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: username = entry.data[CONF_USERNAME] password = entry.data[CONF_PASSWORD] - fallback = entry.options.get(CONF_FALLBACK, True) + fallback = entry.options.get(CONF_FALLBACK, CONST_OVERLAY_TADO_MODE) tadoconnector = TadoConnector(hass, username, password, fallback) @@ -99,7 +99,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: def _async_import_options_from_data_if_missing(hass: HomeAssistant, entry: ConfigEntry): options = dict(entry.options) if CONF_FALLBACK not in options: - options[CONF_FALLBACK] = entry.data.get(CONF_FALLBACK, True) + options[CONF_FALLBACK] = entry.data.get(CONF_FALLBACK, CONST_OVERLAY_TADO_MODE) hass.config_entries.async_update_entry(entry, options=options) @@ -213,23 +213,8 @@ class TadoConnector: _LOGGER.error("Unable to connect to Tado while updating zones") return - for zone in self.zones: - zone_id = zone["id"] - _LOGGER.debug("Updating zone %s", zone_id) - zone_state = TadoZone(zone_states[str(zone_id)], zone_id) - - self.data["zone"][zone_id] = zone_state - - _LOGGER.debug( - "Dispatching update to %s zone %s: %s", - self.home_id, - zone_id, - zone_state, - ) - dispatcher_send( - self.hass, - SIGNAL_TADO_UPDATE_RECEIVED.format(self.home_id, "zone", zone["id"]), - ) + for zone in zone_states: + self.update_zone(int(zone)) def update_zone(self, zone_id): """Update the internal data from Tado.""" diff --git a/homeassistant/components/tado/climate.py b/homeassistant/components/tado/climate.py index 6489c78cbe3..3e6dd2cb130 100644 --- a/homeassistant/components/tado/climate.py +++ b/homeassistant/components/tado/climate.py @@ -24,6 +24,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( + CONST_EXCLUSIVE_OVERLAY_GROUP, CONST_FAN_AUTO, CONST_FAN_OFF, CONST_MODE_AUTO, @@ -32,10 +33,14 @@ from .const import ( CONST_MODE_OFF, CONST_MODE_SMART_SCHEDULE, CONST_OVERLAY_MANUAL, + CONST_OVERLAY_TADO_DEFAULT, CONST_OVERLAY_TADO_MODE, + CONST_OVERLAY_TADO_OPTIONS, CONST_OVERLAY_TIMER, DATA, DOMAIN, + HA_TERMINATION_DURATION, + HA_TERMINATION_TYPE, HA_TO_TADO_FAN_MODE_MAP, HA_TO_TADO_HVAC_MODE_MAP, ORDERED_KNOWN_TADO_MODES, @@ -58,12 +63,16 @@ _LOGGER = logging.getLogger(__name__) SERVICE_CLIMATE_TIMER = "set_climate_timer" ATTR_TIME_PERIOD = "time_period" +ATTR_REQUESTED_OVERLAY = "requested_overlay" CLIMATE_TIMER_SCHEMA = { - vol.Required(ATTR_TIME_PERIOD, default="01:00:00"): vol.All( + vol.Required(ATTR_TEMPERATURE): vol.Coerce(float), + vol.Exclusive(ATTR_TIME_PERIOD, CONST_EXCLUSIVE_OVERLAY_GROUP): vol.All( cv.time_period, cv.positive_timedelta, lambda td: td.total_seconds() ), - vol.Required(ATTR_TEMPERATURE): vol.Coerce(float), + vol.Exclusive(ATTR_REQUESTED_OVERLAY, CONST_EXCLUSIVE_OVERLAY_GROUP): vol.In( + CONST_OVERLAY_TADO_OPTIONS + ), } SERVICE_TEMP_OFFSET = "set_climate_temperature_offset" @@ -379,11 +388,14 @@ class TadoClimate(TadoZoneEntity, ClimateEntity): # the device is switching states return self._tado_zone_data.target_temp or self._tado_zone_data.current_temp - def set_timer(self, time_period, temperature=None): + def set_timer(self, temperature=None, time_period=None, requested_overlay=None): """Set the timer on the entity, and temperature if supported.""" self._control_hvac( - hvac_mode=CONST_MODE_HEAT, target_temp=temperature, duration=time_period + hvac_mode=CONST_MODE_HEAT, + target_temp=temperature, + duration=time_period, + overlay_mode=requested_overlay, ) def set_temp_offset(self, offset): @@ -464,7 +476,14 @@ class TadoClimate(TadoZoneEntity, ClimateEntity): @property def extra_state_attributes(self): """Return temperature offset.""" - return self._tado_zone_temp_offset + state_attr = self._tado_zone_temp_offset + state_attr[ + HA_TERMINATION_TYPE + ] = self._tado_zone_data.default_overlay_termination_type + state_attr[ + HA_TERMINATION_DURATION + ] = self._tado_zone_data.default_overlay_termination_duration + return state_attr def set_swing_mode(self, swing_mode): """Set swing modes for the device.""" @@ -474,6 +493,7 @@ class TadoClimate(TadoZoneEntity, ClimateEntity): def _async_update_zone_data(self): """Load tado data into zone.""" self._tado_zone_data = self._tado.data["zone"][self.zone_id] + # Assign offset values to mapped attributes for offset_key, attr in TADO_TO_HA_OFFSET_MAP.items(): if ( @@ -518,6 +538,7 @@ class TadoClimate(TadoZoneEntity, ClimateEntity): fan_mode=None, swing_mode=None, duration=None, + overlay_mode=None, ): """Send new target temperature to Tado.""" @@ -559,22 +580,41 @@ class TadoClimate(TadoZoneEntity, ClimateEntity): self._tado.reset_zone_overlay(self.zone_id) return + # If user gave duration then overlay mode needs to be timer + if duration: + overlay_mode = CONST_OVERLAY_TIMER + # If no duration or timer set to fallback setting + if overlay_mode is None: + overlay_mode = ( + self._tado.fallback + if self._tado.fallback is not None + else CONST_OVERLAY_TADO_MODE + ) + # If default is Tado default then look it up + if overlay_mode == CONST_OVERLAY_TADO_DEFAULT: + overlay_mode = ( + self._tado_zone_data.default_overlay_termination_type + if self._tado_zone_data.default_overlay_termination_type is not None + else CONST_OVERLAY_TADO_MODE + ) + # If we ended up with a timer but no duration, set a default duration + if overlay_mode == CONST_OVERLAY_TIMER and duration is None: + duration = ( + self._tado_zone_data.default_overlay_termination_duration + if self._tado_zone_data.default_overlay_termination_duration is not None + else "3600" + ) + _LOGGER.debug( - "Switching to %s for zone %s (%d) with temperature %s °C and duration %s", + "Switching to %s for zone %s (%d) with temperature %s °C and duration %s using overlay %s", self._current_tado_hvac_mode, self.zone_name, self.zone_id, self._target_temp, duration, + overlay_mode, ) - overlay_mode = CONST_OVERLAY_MANUAL - if duration: - overlay_mode = CONST_OVERLAY_TIMER - elif self._tado.fallback: - # Fallback to Smart Schedule at next Schedule switch if we have fallback enabled - overlay_mode = CONST_OVERLAY_TADO_MODE - temperature_to_send = self._target_temp if self._current_tado_hvac_mode in TADO_MODES_WITH_NO_TEMP_SETTING: # A temperature cannot be passed with these modes diff --git a/homeassistant/components/tado/config_flow.py b/homeassistant/components/tado/config_flow.py index e2a67c21b5d..95b415c5acc 100644 --- a/homeassistant/components/tado/config_flow.py +++ b/homeassistant/components/tado/config_flow.py @@ -11,12 +11,15 @@ from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult -from .const import CONF_FALLBACK, DOMAIN, UNIQUE_ID +from .const import CONF_FALLBACK, CONST_OVERLAY_TADO_OPTIONS, DOMAIN, UNIQUE_ID _LOGGER = logging.getLogger(__name__) DATA_SCHEMA = vol.Schema( - {vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str} + { + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + } ) @@ -122,9 +125,9 @@ class OptionsFlowHandler(config_entries.OptionsFlow): data_schema = vol.Schema( { - vol.Required( + vol.Optional( CONF_FALLBACK, default=self.config_entry.options.get(CONF_FALLBACK) - ): bool, + ): vol.In(CONST_OVERLAY_TADO_OPTIONS), } ) return self.async_show_form(step_id="init", data_schema=data_schema) diff --git a/homeassistant/components/tado/const.py b/homeassistant/components/tado/const.py index 2c86fa2d642..291d02cb403 100644 --- a/homeassistant/components/tado/const.py +++ b/homeassistant/components/tado/const.py @@ -95,6 +95,17 @@ CONST_OVERLAY_TADO_MODE = ( ) CONST_OVERLAY_MANUAL = "MANUAL" # the user has change the temperature or mode manually CONST_OVERLAY_TIMER = "TIMER" # the temperature will be reset after a timespan +CONST_OVERLAY_TADO_DEFAULT = ( + "TADO_DEFAULT" # use the setting from tado zone itself (set in Tado app or webapp) +) +CONST_OVERLAY_TADO_OPTIONS = [ + CONST_OVERLAY_TADO_MODE, + CONST_OVERLAY_MANUAL, + CONST_OVERLAY_TADO_DEFAULT, +] +CONST_EXCLUSIVE_OVERLAY_GROUP = ( + "overlay_group" # Overlay group for set_climate_timer service +) # Heat always comes first since we get the @@ -180,3 +191,7 @@ TADO_TO_HA_OFFSET_MAP = { TADO_OFFSET_CELSIUS: HA_OFFSET_CELSIUS, TADO_OFFSET_FAHRENHEIT: HA_OFFSET_FAHRENHEIT, } + +# Constants for Overlay Default settings +HA_TERMINATION_TYPE = "default_overlay_type" +HA_TERMINATION_DURATION = "default_overlay_seconds" diff --git a/homeassistant/components/tado/manifest.json b/homeassistant/components/tado/manifest.json index 529b4bcfb97..078c821eeb4 100644 --- a/homeassistant/components/tado/manifest.json +++ b/homeassistant/components/tado/manifest.json @@ -3,7 +3,7 @@ "name": "Tado", "documentation": "https://www.home-assistant.io/integrations/tado", "requirements": ["python-tado==0.12.0"], - "codeowners": ["@michaelarnauts"], + "codeowners": ["@michaelarnauts", "@north3221"], "config_flow": true, "homekit": { "models": ["tado", "AC02"] diff --git a/homeassistant/components/tado/services.yaml b/homeassistant/components/tado/services.yaml index d3aaa71cbbc..8f585879395 100644 --- a/homeassistant/components/tado/services.yaml +++ b/homeassistant/components/tado/services.yaml @@ -6,14 +6,6 @@ set_climate_timer: integration: tado domain: climate fields: - time_period: - name: Time period - description: Set the time period for the boost. - required: true - example: "01:30:00" - default: "01:00:00" - selector: - text: temperature: name: Temperature description: Temperature to set climate entity to @@ -23,7 +15,27 @@ set_climate_timer: min: 0 max: 100 step: 0.5 - unit_of_measurement: '°' + unit_of_measurement: "°" + time_period: + name: Time period + description: Choose this or Overlay. Set the time period for the change if you want to be specific. Alternatively use Overlay + required: false + example: "01:30:00" + default: "01:00:00" + selector: + text: + requested_overlay: + name: Overlay + description: Choose this or Time Period. Allows you to choose an overlay. MANUAL:=Overlay until user removes; NEXT_TIME_BLOCK:=Overlay until next timeblock; TADO_DEFAULT:=Overlay based on tado app setting + required: false + example: "MANUAL" + default: "TADO_DEFAULT" + selector: + select: + options: + - "NEXT_TIME_BLOCK" + - "MANUAL" + - "TADO_DEFAULT" set_water_heater_timer: name: Set water heater timer @@ -49,7 +61,7 @@ set_water_heater_timer: min: 0 max: 100 step: 0.5 - unit_of_measurement: '°' + unit_of_measurement: "°" set_climate_temperature_offset: name: Set climate temperature offset @@ -68,4 +80,4 @@ set_climate_temperature_offset: min: -10 max: 10 step: 0.01 - unit_of_measurement: '°' + unit_of_measurement: "°" diff --git a/homeassistant/components/tado/strings.json b/homeassistant/components/tado/strings.json index e0eb90f7ddc..e1bf1a1406d 100644 --- a/homeassistant/components/tado/strings.json +++ b/homeassistant/components/tado/strings.json @@ -22,9 +22,9 @@ "options": { "step": { "init": { - "description": "Fallback mode will switch to Smart Schedule at next schedule switch after manually adjusting a zone.", + "description": "Fallback mode lets you choose when to fallback to Smart Schedule from your manual zone overlay. (NEXT_TIME_BLOCK:= Change at next Smart Schedule change; MANUAL:= Dont change until you cancel; TADO_DEFAULT:= Change based on your setting in Tado App).", "data": { - "fallback": "Enable fallback mode." + "fallback": "Choose fallback mode." }, "title": "Adjust Tado options." } diff --git a/homeassistant/components/tado/translations/fr.json b/homeassistant/components/tado/translations/fr.json index d862a27f392..2e10dc38a2e 100644 --- a/homeassistant/components/tado/translations/fr.json +++ b/homeassistant/components/tado/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "no_homes": "Il n\u2019y a pas de maisons li\u00e9es \u00e0 ce compte tado.", "unknown": "Erreur inattendue" }, diff --git a/homeassistant/components/tailscale/config_flow.py b/homeassistant/components/tailscale/config_flow.py index cda4020a290..f1180db5254 100644 --- a/homeassistant/components/tailscale/config_flow.py +++ b/homeassistant/components/tailscale/config_flow.py @@ -65,6 +65,9 @@ class TailscaleFlowHandler(ConfigFlow, domain=DOMAIN): return self.async_show_form( step_id="user", + description_placeholders={ + "authkeys_url": "https://login.tailscale.com/admin/settings/authkeys" + }, data_schema=vol.Schema( { vol.Required( diff --git a/homeassistant/components/tailscale/strings.json b/homeassistant/components/tailscale/strings.json index 247d6032c03..0ac0db0ef08 100644 --- a/homeassistant/components/tailscale/strings.json +++ b/homeassistant/components/tailscale/strings.json @@ -2,19 +2,18 @@ "config": { "step": { "user": { - "description": "To authenticate with Tailscale you'll need to create an API key at https://login.tailscale.com/admin/settings/authkeys.\n\nA Tailnet is the name of your Tailscale network. You can find it in the top left corner in the Tailscale Admin Panel (beside the Tailscale logo).", + "description": "This integration monitors your Tailscale network, it **DOES NOT** make your Home Assistant accessible via Tailscale VPN. \n\nTo authenticate with Tailscale you'll need to create an API key at {authkeys_url}.\n\nA Tailnet is the name of your Tailscale network. You can find it in the top left corner in the Tailscale Admin Panel (beside the Tailscale logo).", "data": { "tailnet": "Tailnet", "api_key": "[%key:common::config_flow::data::api_key%]" } }, "reauth_confirm": { - "description":"Tailscale API tokens are valid for 90-days. You can create a fresh Tailscale API key at https://login.tailscale.com/admin/settings/authkeys.", + "description": "Tailscale API tokens are valid for 90-days. You can create a fresh Tailscale API key at https://login.tailscale.com/admin/settings/authkeys.", "data": { "api_key": "[%key:common::config_flow::data::api_key%]" } } - }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", diff --git a/homeassistant/components/tailscale/translations/en.json b/homeassistant/components/tailscale/translations/en.json index f1e79785cbf..dd607f6360d 100644 --- a/homeassistant/components/tailscale/translations/en.json +++ b/homeassistant/components/tailscale/translations/en.json @@ -19,7 +19,7 @@ "api_key": "API Key", "tailnet": "Tailnet" }, - "description": "To authenticate with Tailscale you'll need to create an API key at https://login.tailscale.com/admin/settings/authkeys.\n\nA Tailnet is the name of your Tailscale network. You can find it in the top left corner in the Tailscale Admin Panel (beside the Tailscale logo)." + "description": "This integration monitors your Tailscale network, it **DOES NOT** make your Home Assistant accessible via Tailscale VPN. \n\nTo authenticate with Tailscale you'll need to create an API key at {authkeys_url}.\n\nA Tailnet is the name of your Tailscale network. You can find it in the top left corner in the Tailscale Admin Panel (beside the Tailscale logo)." } } } diff --git a/homeassistant/components/tailscale/translations/fr.json b/homeassistant/components/tailscale/translations/fr.json index eb307c84de9..1163ec2d151 100644 --- a/homeassistant/components/tailscale/translations/fr.json +++ b/homeassistant/components/tailscale/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide" + "invalid_auth": "Authentification non valide" }, "step": { "reauth_confirm": { diff --git a/homeassistant/components/tankerkoenig/__init__.py b/homeassistant/components/tankerkoenig/__init__.py index 5cbe6e0b9a8..08520c8f5cc 100644 --- a/homeassistant/components/tankerkoenig/__init__.py +++ b/homeassistant/components/tankerkoenig/__init__.py @@ -1,67 +1,82 @@ """Ask tankerkoenig.de for petrol price information.""" +from __future__ import annotations + from datetime import timedelta import logging from math import ceil import pytankerkoenig +from requests.exceptions import RequestException import voluptuous as vol -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( CONF_API_KEY, CONF_LATITUDE, + CONF_LOCATION, CONF_LONGITUDE, + CONF_NAME, CONF_RADIUS, CONF_SCAN_INTERVAL, CONF_SHOW_ON_MAP, + Platform, ) from homeassistant.core import HomeAssistant -from homeassistant.exceptions import HomeAssistantError +from homeassistant.exceptions import ConfigEntryNotReady import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import CONF_FUEL_TYPES, CONF_STATIONS, DOMAIN, FUEL_TYPES +from .const import ( + CONF_FUEL_TYPES, + CONF_STATIONS, + DEFAULT_RADIUS, + DEFAULT_SCAN_INTERVAL, + DOMAIN, + FUEL_TYPES, +) _LOGGER = logging.getLogger(__name__) -DEFAULT_RADIUS = 2 -DEFAULT_SCAN_INTERVAL = timedelta(minutes=30) - CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Required(CONF_API_KEY): cv.string, - vol.Optional( - CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL - ): cv.time_period, - vol.Optional(CONF_FUEL_TYPES, default=FUEL_TYPES): vol.All( - cv.ensure_list, [vol.In(FUEL_TYPES)] - ), - vol.Inclusive( - CONF_LATITUDE, - "coordinates", - "Latitude and longitude must exist together", - ): cv.latitude, - vol.Inclusive( - CONF_LONGITUDE, - "coordinates", - "Latitude and longitude must exist together", - ): cv.longitude, - vol.Optional(CONF_RADIUS, default=DEFAULT_RADIUS): vol.All( - cv.positive_int, vol.Range(min=1) - ), - vol.Optional(CONF_STATIONS, default=[]): vol.All( - cv.ensure_list, [cv.string] - ), - vol.Optional(CONF_SHOW_ON_MAP, default=True): cv.boolean, - } - ) - }, + vol.All( + cv.deprecated(DOMAIN), + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_API_KEY): cv.string, + vol.Optional( + CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL + ): cv.time_period, + vol.Optional(CONF_FUEL_TYPES, default=FUEL_TYPES): vol.All( + cv.ensure_list, [vol.In(FUEL_TYPES)] + ), + vol.Inclusive( + CONF_LATITUDE, + "coordinates", + "Latitude and longitude must exist together", + ): cv.latitude, + vol.Inclusive( + CONF_LONGITUDE, + "coordinates", + "Latitude and longitude must exist together", + ): cv.longitude, + vol.Optional(CONF_RADIUS, default=DEFAULT_RADIUS): vol.All( + cv.positive_int, vol.Range(min=1) + ), + vol.Optional(CONF_STATIONS, default=[]): vol.All( + cv.ensure_list, [cv.string] + ), + vol.Optional(CONF_SHOW_ON_MAP, default=True): cv.boolean, + } + ) + }, + ), extra=vol.ALLOW_EXTRA, ) +PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR] + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set the tankerkoenig component up.""" @@ -69,106 +84,119 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return True conf = config[DOMAIN] - - _LOGGER.debug("Setting up integration") - - tankerkoenig = TankerkoenigData(hass, conf) - - latitude = conf.get(CONF_LATITUDE, hass.config.latitude) - longitude = conf.get(CONF_LONGITUDE, hass.config.longitude) - radius = conf[CONF_RADIUS] - additional_stations = conf[CONF_STATIONS] - - setup_ok = await hass.async_add_executor_job( - tankerkoenig.setup, latitude, longitude, radius, additional_stations - ) - if not setup_ok: - _LOGGER.error("Could not setup integration") - return False - - hass.data[DOMAIN] = tankerkoenig - hass.async_create_task( - async_load_platform( - hass, - SENSOR_DOMAIN, + hass.config_entries.flow.async_init( DOMAIN, - discovered=tankerkoenig.stations, - hass_config=conf, + context={"source": SOURCE_IMPORT}, + data={ + CONF_NAME: "Home", + CONF_API_KEY: conf[CONF_API_KEY], + CONF_FUEL_TYPES: conf[CONF_FUEL_TYPES], + CONF_LOCATION: { + "latitude": conf.get(CONF_LATITUDE, hass.config.latitude), + "longitude": conf.get(CONF_LONGITUDE, hass.config.longitude), + }, + CONF_RADIUS: conf[CONF_RADIUS], + CONF_STATIONS: conf[CONF_STATIONS], + CONF_SHOW_ON_MAP: conf[CONF_SHOW_ON_MAP], + }, ) ) return True -class TankerkoenigData: +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set a tankerkoenig configuration entry up.""" + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][ + entry.unique_id + ] = coordinator = TankerkoenigDataUpdateCoordinator( + hass, + entry, + _LOGGER, + name=entry.unique_id or DOMAIN, + update_interval=DEFAULT_SCAN_INTERVAL, + ) + + try: + setup_ok = await hass.async_add_executor_job(coordinator.setup) + except RequestException as err: + raise ConfigEntryNotReady from err + if not setup_ok: + _LOGGER.error("Could not setup integration") + return False + + await coordinator.async_config_entry_first_refresh() + + entry.async_on_unload(entry.add_update_listener(_async_update_listener)) + + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload Tankerkoenig config entry.""" + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if unload_ok: + hass.data[DOMAIN].pop(entry.unique_id) + + return unload_ok + + +async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Handle options update.""" + await hass.config_entries.async_reload(entry.entry_id) + + +class TankerkoenigDataUpdateCoordinator(DataUpdateCoordinator): """Get the latest data from the API.""" - def __init__(self, hass, conf): + def __init__( + self, + hass: HomeAssistant, + entry: ConfigEntry, + logger: logging.Logger, + name: str, + update_interval: int, + ) -> None: """Initialize the data object.""" - self._api_key = conf[CONF_API_KEY] - self.stations = {} - self.fuel_types = conf[CONF_FUEL_TYPES] - self.update_interval = conf[CONF_SCAN_INTERVAL] - self.show_on_map = conf[CONF_SHOW_ON_MAP] + + super().__init__( + hass=hass, + logger=logger, + name=name, + update_interval=timedelta(minutes=update_interval), + ) + + self._api_key: str = entry.data[CONF_API_KEY] + self._selected_stations: list[str] = entry.data[CONF_STATIONS] self._hass = hass + self.stations: dict[str, dict] = {} + self.fuel_types: list[str] = entry.data[CONF_FUEL_TYPES] + self.show_on_map: bool = entry.options[CONF_SHOW_ON_MAP] - def setup(self, latitude, longitude, radius, additional_stations): - """Set up the tankerkoenig API. - - Read the initial data from the server, to initialize the list of fuel stations to monitor. - """ - _LOGGER.debug("Fetching data for (%s, %s) rad: %s", latitude, longitude, radius) - try: - data = pytankerkoenig.getNearbyStations( - self._api_key, latitude, longitude, radius, "all", "dist" - ) - except pytankerkoenig.customException as err: - data = {"ok": False, "message": err, "exception": True} - _LOGGER.debug("Received data: %s", data) - if not data["ok"]: - _LOGGER.error( - "Setup for sensors was unsuccessful. Error occurred while fetching data from tankerkoenig.de: %s", - data["message"], - ) - return False - - # Add stations found via location + radius - if not (nearby_stations := data["stations"]): - if not additional_stations: - _LOGGER.error( - "Could not find any station in range." - "Try with a bigger radius or manually specify stations in additional_stations" - ) - return False - _LOGGER.warning( - "Could not find any station in range. Will only use manually specified stations" - ) - else: - for station in nearby_stations: - self.add_station(station) - - # Add manually specified additional stations - for station_id in additional_stations: + def setup(self) -> bool: + """Set up the tankerkoenig API.""" + for station_id in self._selected_stations: try: - additional_station_data = pytankerkoenig.getStationData( - self._api_key, station_id - ) + station_data = pytankerkoenig.getStationData(self._api_key, station_id) except pytankerkoenig.customException as err: - additional_station_data = { + station_data = { "ok": False, "message": err, "exception": True, } - if not additional_station_data["ok"]: + if not station_data["ok"]: _LOGGER.error( "Error when adding station %s:\n %s", station_id, - additional_station_data["message"], + station_data["message"], ) return False - self.add_station(additional_station_data["station"]) + self.add_station(station_data["station"]) if len(self.stations) > 10: _LOGGER.warning( "Found more than 10 stations to check. " @@ -177,7 +205,7 @@ class TankerkoenigData: ) return True - async def fetch_data(self): + async def _async_update_data(self) -> dict: """Get the latest data from tankerkoenig.de.""" _LOGGER.debug("Fetching new data from tankerkoenig.de") station_ids = list(self.stations) @@ -198,10 +226,10 @@ class TankerkoenigData: _LOGGER.error( "Error fetching data from tankerkoenig.de: %s", data["message"] ) - raise TankerkoenigError(data["message"]) + raise UpdateFailed(data["message"]) if "prices" not in data: _LOGGER.error("Did not receive price information from tankerkoenig.de") - raise TankerkoenigError("No prices in data") + raise UpdateFailed("No prices in data") prices.update(data["prices"]) return prices @@ -216,7 +244,3 @@ class TankerkoenigData: self.stations[station_id] = station _LOGGER.debug("add_station called for station: %s", station) - - -class TankerkoenigError(HomeAssistantError): - """An error occurred while contacting tankerkoenig.de.""" diff --git a/homeassistant/components/tankerkoenig/binary_sensor.py b/homeassistant/components/tankerkoenig/binary_sensor.py new file mode 100644 index 00000000000..4b58ea26703 --- /dev/null +++ b/homeassistant/components/tankerkoenig/binary_sensor.py @@ -0,0 +1,78 @@ +"""Tankerkoenig binary sensor integration.""" +from __future__ import annotations + +import logging + +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + BinarySensorEntity, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_ID, ATTR_LATITUDE, ATTR_LONGITUDE +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 TankerkoenigDataUpdateCoordinator +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up the tankerkoenig binary sensors.""" + + coordinator: TankerkoenigDataUpdateCoordinator = hass.data[DOMAIN][entry.unique_id] + + stations = coordinator.stations.values() + entities = [] + for station in stations: + sensor = StationOpenBinarySensorEntity( + station, + coordinator, + coordinator.show_on_map, + ) + entities.append(sensor) + _LOGGER.debug("Added sensors %s", entities) + + async_add_entities(entities) + + +class StationOpenBinarySensorEntity(CoordinatorEntity, BinarySensorEntity): + """Shows if a station is open or closed.""" + + _attr_device_class = BinarySensorDeviceClass.DOOR + + def __init__( + self, + station: dict, + coordinator: TankerkoenigDataUpdateCoordinator, + show_on_map: bool, + ) -> None: + """Initialize the sensor.""" + super().__init__(coordinator) + self._station_id = station["id"] + self._attr_name = ( + f"{station['brand']} {station['street']} {station['houseNumber']} status" + ) + self._attr_unique_id = f"{station['id']}_status" + self._attr_device_info = DeviceInfo( + identifiers={(ATTR_ID, station["id"])}, + name=f"{station['brand']} {station['street']} {station['houseNumber']}", + model=station["brand"], + configuration_url="https://www.tankerkoenig.de", + ) + if show_on_map: + self._attr_extra_state_attributes = { + ATTR_LATITUDE: station["lat"], + ATTR_LONGITUDE: station["lng"], + } + + @property + def is_on(self) -> bool | None: + """Return true if the station is open.""" + data = self.coordinator.data[self._station_id] + return data is not None and "status" in data diff --git a/homeassistant/components/tankerkoenig/config_flow.py b/homeassistant/components/tankerkoenig/config_flow.py new file mode 100644 index 00000000000..3f3449c26e4 --- /dev/null +++ b/homeassistant/components/tankerkoenig/config_flow.py @@ -0,0 +1,224 @@ +"""Config flow for Tankerkoenig.""" +from __future__ import annotations + +from typing import Any + +from pytankerkoenig import customException, getNearbyStations +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import ( + CONF_API_KEY, + CONF_LATITUDE, + CONF_LOCATION, + CONF_LONGITUDE, + CONF_NAME, + CONF_RADIUS, + CONF_SHOW_ON_MAP, + CONF_UNIT_OF_MEASUREMENT, + LENGTH_KILOMETERS, +) +from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.selector import selector + +from .const import CONF_FUEL_TYPES, CONF_STATIONS, DEFAULT_RADIUS, DOMAIN, FUEL_TYPES + + +class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow.""" + + VERSION = 1 + + def __init__(self) -> None: + """Init the FlowHandler.""" + super().__init__() + self._data: dict[str, Any] = {} + self._stations: dict[str, str] = {} + + @staticmethod + @callback + def async_get_options_flow( + config_entry: config_entries.ConfigEntry, + ) -> OptionsFlowHandler: + """Get the options flow for this handler.""" + return OptionsFlowHandler(config_entry) + + async def async_step_import(self, config: dict[str, Any]) -> FlowResult: + """Import YAML configuration.""" + await self.async_set_unique_id( + f"{config[CONF_LOCATION][CONF_LATITUDE]}_{config[CONF_LOCATION][CONF_LONGITUDE]}" + ) + self._abort_if_unique_id_configured() + + selected_station_ids: list[str] = [] + # add all nearby stations + nearby_stations = await self._get_nearby_stations(config) + for station in nearby_stations.get("stations", []): + selected_station_ids.append(station["id"]) + + # add all manual added stations + for station_id in config[CONF_STATIONS]: + selected_station_ids.append(station_id) + + return self._create_entry( + data={ + CONF_NAME: "Home", + CONF_API_KEY: config[CONF_API_KEY], + CONF_FUEL_TYPES: config[CONF_FUEL_TYPES], + CONF_LOCATION: config[CONF_LOCATION], + CONF_RADIUS: config[CONF_RADIUS], + CONF_STATIONS: selected_station_ids, + }, + options={ + CONF_SHOW_ON_MAP: config[CONF_SHOW_ON_MAP], + }, + ) + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle a flow initialized by the user.""" + if not user_input: + return self._show_form_user() + + await self.async_set_unique_id( + f"{user_input[CONF_LOCATION][CONF_LATITUDE]}_{user_input[CONF_LOCATION][CONF_LONGITUDE]}" + ) + self._abort_if_unique_id_configured() + + data = await self._get_nearby_stations(user_input) + if not data.get("ok"): + return self._show_form_user( + user_input, errors={CONF_API_KEY: "invalid_auth"} + ) + if stations := data.get("stations"): + for station in stations: + self._stations[ + station["id"] + ] = f"{station['brand']} {station['street']} {station['houseNumber']} - ({station['dist']}km)" + + else: + return self._show_form_user(user_input, errors={CONF_RADIUS: "no_stations"}) + + self._data = user_input + + return await self.async_step_select_station() + + async def async_step_select_station( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the step select_station of a flow initialized by the user.""" + if not user_input: + return self.async_show_form( + step_id="select_station", + description_placeholders={"stations_count": len(self._stations)}, + data_schema=vol.Schema( + {vol.Required(CONF_STATIONS): cv.multi_select(self._stations)} + ), + ) + + return self._create_entry( + data={**self._data, **user_input}, + options={CONF_SHOW_ON_MAP: True}, + ) + + def _show_form_user( + self, + user_input: dict[str, Any] | None = None, + errors: dict[str, Any] | None = None, + ) -> FlowResult: + if user_input is None: + user_input = {} + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required( + CONF_NAME, default=user_input.get(CONF_NAME, "") + ): cv.string, + vol.Required( + CONF_API_KEY, default=user_input.get(CONF_API_KEY, "") + ): cv.string, + vol.Required( + CONF_FUEL_TYPES, + default=user_input.get(CONF_FUEL_TYPES, list(FUEL_TYPES)), + ): cv.multi_select(FUEL_TYPES), + vol.Required( + CONF_LOCATION, + default=user_input.get( + CONF_LOCATION, + { + "latitude": self.hass.config.latitude, + "longitude": self.hass.config.longitude, + }, + ), + ): selector({"location": {}}), + vol.Required( + CONF_RADIUS, default=user_input.get(CONF_RADIUS, DEFAULT_RADIUS) + ): selector( + { + "number": { + "min": 0.1, + "max": 25, + "step": 0.1, + CONF_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS, + } + } + ), + } + ), + errors=errors, + ) + + def _create_entry( + self, data: dict[str, Any], options: dict[str, Any] + ) -> FlowResult: + return self.async_create_entry( + title=data[CONF_NAME], + data=data, + options=options, + ) + + async def _get_nearby_stations(self, data: dict[str, Any]) -> dict[str, Any]: + """Fetch nearby stations.""" + try: + return await self.hass.async_add_executor_job( + getNearbyStations, + data[CONF_API_KEY], + data[CONF_LOCATION][CONF_LATITUDE], + data[CONF_LOCATION][CONF_LONGITUDE], + data[CONF_RADIUS], + "all", + "dist", + ) + except customException as err: + return {"ok": False, "message": err, "exception": True} + + +class OptionsFlowHandler(config_entries.OptionsFlow): + """Handle an options flow.""" + + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + """Initialize options flow.""" + self.config_entry = config_entry + + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle options flow.""" + 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[CONF_SHOW_ON_MAP], + ): bool, + } + ), + ) diff --git a/homeassistant/components/tankerkoenig/const.py b/homeassistant/components/tankerkoenig/const.py index 04e6e08ba37..c2a1dba9b6a 100644 --- a/homeassistant/components/tankerkoenig/const.py +++ b/homeassistant/components/tankerkoenig/const.py @@ -6,4 +6,16 @@ NAME = "tankerkoenig" CONF_FUEL_TYPES = "fuel_types" CONF_STATIONS = "stations" -FUEL_TYPES = ["e5", "e10", "diesel"] +DEFAULT_RADIUS = 2 +DEFAULT_SCAN_INTERVAL = 30 + +FUEL_TYPES = {"e5": "Super", "e10": "Super E10", "diesel": "Diesel"} + +ATTR_BRAND = "brand" +ATTR_CITY = "city" +ATTR_FUEL_TYPE = "fuel_type" +ATTR_HOUSE_NUMBER = "house_number" +ATTR_POSTCODE = "postcode" +ATTR_STATION_NAME = "station_name" +ATTR_STREET = "street" +ATTRIBUTION = "Data provided by https://www.tankerkoenig.de" diff --git a/homeassistant/components/tankerkoenig/manifest.json b/homeassistant/components/tankerkoenig/manifest.json index d3ad7fbe2e1..054e72aa0e3 100644 --- a/homeassistant/components/tankerkoenig/manifest.json +++ b/homeassistant/components/tankerkoenig/manifest.json @@ -1,9 +1,10 @@ { "domain": "tankerkoenig", "name": "Tankerkoenig", + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tankerkoenig", "requirements": ["pytankerkoenig==0.0.6"], - "codeowners": ["@guillempages"], + "codeowners": ["@guillempages", "@mib1185"], "iot_class": "cloud_polling", "loggers": ["pytankerkoenig"] } diff --git a/homeassistant/components/tankerkoenig/sensor.py b/homeassistant/components/tankerkoenig/sensor.py index 0c1220ac07c..bbaeda44fd7 100644 --- a/homeassistant/components/tankerkoenig/sensor.py +++ b/homeassistant/components/tankerkoenig/sensor.py @@ -3,74 +3,48 @@ from __future__ import annotations import logging -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT, SensorEntity +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ATTRIBUTION, + ATTR_ID, ATTR_LATITUDE, ATTR_LONGITUDE, CURRENCY_EURO, ) from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from homeassistant.helpers.update_coordinator import ( - CoordinatorEntity, - DataUpdateCoordinator, - UpdateFailed, -) +from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import DOMAIN, NAME +from . import TankerkoenigDataUpdateCoordinator +from .const import ( + ATTR_BRAND, + ATTR_CITY, + ATTR_FUEL_TYPE, + ATTR_HOUSE_NUMBER, + ATTR_POSTCODE, + ATTR_STATION_NAME, + ATTR_STREET, + ATTRIBUTION, + DOMAIN, + FUEL_TYPES, +) _LOGGER = logging.getLogger(__name__) -ATTR_BRAND = "brand" -ATTR_CITY = "city" -ATTR_FUEL_TYPE = "fuel_type" -ATTR_HOUSE_NUMBER = "house_number" -ATTR_IS_OPEN = "is_open" -ATTR_POSTCODE = "postcode" -ATTR_STATION_NAME = "station_name" -ATTR_STREET = "street" -ATTRIBUTION = "Data provided by https://creativecommons.tankerkoenig.de" -ICON = "mdi:gas-station" - - -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the tankerkoenig sensors.""" - if discovery_info is None: - return + coordinator: TankerkoenigDataUpdateCoordinator = hass.data[DOMAIN][entry.unique_id] - tankerkoenig = hass.data[DOMAIN] - - async def async_update_data(): - """Fetch data from API endpoint.""" - try: - return await tankerkoenig.fetch_data() - except LookupError as err: - raise UpdateFailed("Failed to fetch data") from err - - coordinator = DataUpdateCoordinator( - hass, - _LOGGER, - name=NAME, - update_method=async_update_data, - update_interval=tankerkoenig.update_interval, - ) - - # Fetch initial data so we have data when entities subscribe - await coordinator.async_refresh() - - stations = discovery_info.values() + stations = coordinator.stations.values() entities = [] for station in stations: - for fuel in tankerkoenig.fuel_types: + for fuel in coordinator.fuel_types: if fuel not in station: _LOGGER.warning( "Station %s does not offer %s fuel", station["id"], fuel @@ -80,8 +54,7 @@ async def async_setup_platform( fuel, station, coordinator, - f"{NAME}_{station['name']}_{fuel}", - tankerkoenig.show_on_map, + coordinator.show_on_map, ) entities.append(sensor) _LOGGER.debug("Added sensors %s", entities) @@ -92,68 +65,42 @@ async def async_setup_platform( class FuelPriceSensor(CoordinatorEntity, SensorEntity): """Contains prices for fuel in a given station.""" - def __init__(self, fuel_type, station, coordinator, name, show_on_map): + _attr_state_class = STATE_CLASS_MEASUREMENT + _attr_icon = "mdi:gas-station" + + def __init__(self, fuel_type, station, coordinator, show_on_map): """Initialize the sensor.""" super().__init__(coordinator) - self._station = station self._station_id = station["id"] self._fuel_type = fuel_type - self._name = name - self._latitude = station["lat"] - self._longitude = station["lng"] - self._city = station["place"] - self._house_number = station["houseNumber"] - self._postcode = station["postCode"] - self._street = station["street"] - self._price = station[fuel_type] - self._show_on_map = show_on_map + self._attr_name = f"{station['brand']} {station['street']} {station['houseNumber']} {FUEL_TYPES[fuel_type]}" + self._attr_native_unit_of_measurement = CURRENCY_EURO + self._attr_unique_id = f"{station['id']}_{fuel_type}" + self._attr_device_info = DeviceInfo( + identifiers={(ATTR_ID, station["id"])}, + name=f"{station['brand']} {station['street']} {station['houseNumber']}", + model=station["brand"], + configuration_url="https://www.tankerkoenig.de", + ) - @property - def name(self): - """Return the name of the sensor.""" - return self._name + attrs = { + ATTR_ATTRIBUTION: ATTRIBUTION, + ATTR_BRAND: station["brand"], + ATTR_FUEL_TYPE: fuel_type, + ATTR_STATION_NAME: station["name"], + ATTR_STREET: station["street"], + ATTR_HOUSE_NUMBER: station["houseNumber"], + ATTR_POSTCODE: station["postCode"], + ATTR_CITY: station["place"], + } - @property - def icon(self): - """Icon to use in the frontend.""" - return ICON - - @property - def native_unit_of_measurement(self): - """Return unit of measurement.""" - return CURRENCY_EURO + if show_on_map: + attrs[ATTR_LATITUDE] = station["lat"] + attrs[ATTR_LONGITUDE] = station["lng"] + self._attr_extra_state_attributes = attrs @property def native_value(self): """Return the state of the device.""" # key Fuel_type is not available when the fuel station is closed, use "get" instead of "[]" to avoid exceptions return self.coordinator.data[self._station_id].get(self._fuel_type) - - @property - def unique_id(self) -> str: - """Return a unique identifier for this entity.""" - return f"{self._station_id}_{self._fuel_type}" - - @property - def extra_state_attributes(self): - """Return the attributes of the device.""" - data = self.coordinator.data[self._station_id] - - attrs = { - ATTR_ATTRIBUTION: ATTRIBUTION, - ATTR_BRAND: self._station["brand"], - ATTR_FUEL_TYPE: self._fuel_type, - ATTR_STATION_NAME: self._station["name"], - ATTR_STREET: self._street, - ATTR_HOUSE_NUMBER: self._house_number, - ATTR_POSTCODE: self._postcode, - ATTR_CITY: self._city, - } - - if self._show_on_map: - attrs[ATTR_LATITUDE] = self._latitude - attrs[ATTR_LONGITUDE] = self._longitude - - if data is not None and "status" in data: - attrs[ATTR_IS_OPEN] = data["status"] == "open" - return attrs diff --git a/homeassistant/components/tankerkoenig/strings.json b/homeassistant/components/tankerkoenig/strings.json new file mode 100644 index 00000000000..7c1ba54fcc0 --- /dev/null +++ b/homeassistant/components/tankerkoenig/strings.json @@ -0,0 +1,41 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "Region name", + "api_key": "[%key:common::config_flow::data::api_key%]", + "fuel_types": "Fuel types", + "location": "[%key:common::config_flow::data::location%]", + "stations": "Additional fuel stations", + "radius": "Search radius" + } + }, + "select_station": { + "title": "Select stations to add", + "description": "found {stations_count} stations in radius", + "data": { + "stations": "Stations" + } + } + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_location%]" + }, + "error": { + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "no_stations": "Could not find any station in range." + } + }, + "options": { + "step": { + "init": { + "title": "Tankerkoenig options", + "data": { + "scan_interval": "Update Interval", + "show_on_map": "Show stations on map" + } + } + } + } +} diff --git a/homeassistant/components/tankerkoenig/translations/en.json b/homeassistant/components/tankerkoenig/translations/en.json new file mode 100644 index 00000000000..399788de8f4 --- /dev/null +++ b/homeassistant/components/tankerkoenig/translations/en.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "already_configured": "Location is already configured" + }, + "error": { + "invalid_auth": "Invalid authentication", + "no_stations": "Could not find any station in range." + }, + "step": { + "select_station": { + "data": { + "stations": "Stations" + }, + "description": "found {stations_count} stations in radius", + "title": "Select stations to add" + }, + "user": { + "data": { + "api_key": "API Key", + "fuel_types": "Fuel types", + "location": "Location", + "name": "Region name", + "radius": "Search radius", + "stations": "Additional fuel stations" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Update Interval", + "show_on_map": "Show stations on map" + }, + "title": "Tankerkoenig options" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tapsaff/binary_sensor.py b/homeassistant/components/tapsaff/binary_sensor.py index 3d82da340e9..9608971e78a 100644 --- a/homeassistant/components/tapsaff/binary_sensor.py +++ b/homeassistant/components/tapsaff/binary_sensor.py @@ -8,7 +8,7 @@ from tapsaff import TapsAff import voluptuous as vol from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity -from homeassistant.const import CONF_NAME +from homeassistant.const import CONF_LOCATION, CONF_NAME from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -16,8 +16,6 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType _LOGGER = logging.getLogger(__name__) -CONF_LOCATION = "location" - DEFAULT_NAME = "Taps Aff" SCAN_INTERVAL = timedelta(minutes=30) diff --git a/homeassistant/components/tasmota/__init__.py b/homeassistant/components/tasmota/__init__.py index 44dd2489177..7e37c6ea7f2 100644 --- a/homeassistant/components/tasmota/__init__.py +++ b/homeassistant/components/tasmota/__init__.py @@ -12,24 +12,20 @@ from hatasmota.const import ( CONF_NAME, CONF_SW_VERSION, ) -from hatasmota.discovery import clear_discovery_topic from hatasmota.models import TasmotaDeviceConfig from hatasmota.mqtt import TasmotaMQTTClient -import voluptuous as vol -from homeassistant.components import mqtt, websocket_api +from homeassistant.components import mqtt from homeassistant.components.mqtt.subscription import ( async_prepare_subscribe_topics, async_subscribe_topics, async_unsubscribe_topics, ) -from homeassistant.components.websocket_api.connection import ActiveConnection from homeassistant.config_entries import ConfigEntry -from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import device_registry as dr from homeassistant.helpers.device_registry import ( CONNECTION_NETWORK_MAC, - EVENT_DEVICE_REGISTRY_UPDATED, DeviceRegistry, async_entries_for_config_entry, ) @@ -39,7 +35,6 @@ from .const import ( CONF_DISCOVERY_PREFIX, DATA_REMOVE_DISCOVER_COMPONENT, DATA_UNSUB, - DOMAIN, PLATFORMS, ) @@ -48,7 +43,6 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Tasmota from a config entry.""" - websocket_api.async_register_command(hass, websocket_remove_device) hass.data[DATA_UNSUB] = [] async def _publish( @@ -81,42 +75,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass, mac, config, entry, tasmota_mqtt, device_registry ) - async def async_device_updated(event: Event) -> None: - """Handle the removal of a device.""" - device_registry = dr.async_get(hass) - device_id = event.data["device_id"] - if event.data["action"] not in ("remove", "update"): - return - - connections: set[tuple[str, str]] - if event.data["action"] == "update": - if "config_entries" not in event.data["changes"]: - return - - device = device_registry.async_get(device_id) - if not device: - # The device is already removed, do cleanup when we get "remove" event - return - if entry.entry_id in device.config_entries: - # Not removed from device - return - connections = device.connections - else: - deleted_device = device_registry.deleted_devices[event.data["device_id"]] - connections = deleted_device.connections - if entry.entry_id not in deleted_device.config_entries: - return - - macs = [c[1] for c in connections if c[0] == CONNECTION_NETWORK_MAC] - for mac in macs: - await clear_discovery_topic( - mac, entry.data[CONF_DISCOVERY_PREFIX], tasmota_mqtt - ) - - hass.data[DATA_UNSUB].append( - hass.bus.async_listen(EVENT_DEVICE_REGISTRY_UPDATED, async_device_updated) - ) - async def start_platforms() -> None: await device_automation.async_setup_entry(hass, entry) await asyncio.gather( @@ -169,7 +127,7 @@ async def _remove_device( tasmota_mqtt: TasmotaMQTTClient, device_registry: DeviceRegistry, ) -> None: - """Remove device from device registry.""" + """Remove a discovered Tasmota device.""" device = device_registry.async_get_device(set(), {(CONNECTION_NETWORK_MAC, mac)}) if device is None or config_entry.entry_id not in device.config_entries: @@ -179,9 +137,6 @@ async def _remove_device( device_registry.async_update_device( device.id, remove_config_entry_id=config_entry.entry_id ) - await clear_discovery_topic( - mac, config_entry.data[CONF_DISCOVERY_PREFIX], tasmota_mqtt - ) def _update_device( @@ -218,40 +173,17 @@ async def async_setup_device( _update_device(hass, config_entry, config, device_registry) -@websocket_api.websocket_command( - {vol.Required("type"): "tasmota/device/remove", vol.Required("device_id"): str} -) -@callback -def websocket_remove_device( - hass: HomeAssistant, connection: ActiveConnection, msg: dict -) -> None: - """Delete device.""" - device_id = msg["device_id"] - dev_registry = dr.async_get(hass) - - if not (device := dev_registry.async_get(device_id)): - connection.send_error( - msg["id"], websocket_api.const.ERR_NOT_FOUND, "Device not found" - ) - return - - for config_entry_id in device.config_entries: - config_entry = hass.config_entries.async_get_entry(config_entry_id) - assert config_entry - # Only delete the device if it belongs to a Tasmota device entry - if config_entry.domain == DOMAIN: - dev_registry.async_remove_device(device_id) - connection.send_message(websocket_api.result_message(msg["id"])) - return - - connection.send_error( - msg["id"], websocket_api.const.ERR_NOT_FOUND, "Non Tasmota device" - ) - - async def async_remove_config_entry_device( hass: HomeAssistant, config_entry: ConfigEntry, device_entry: dr.DeviceEntry ) -> bool: """Remove Tasmota config entry from a device.""" - # Just return True, cleanup is done on when handling device registry events + + connections = device_entry.connections + macs = [c[1] for c in connections if c[0] == CONNECTION_NETWORK_MAC] + tasmota_discovery = hass.data[discovery.TASMOTA_DISCOVERY_INSTANCE] + for mac in macs: + await tasmota_discovery.clear_discovery_topic( + mac, config_entry.data[CONF_DISCOVERY_PREFIX] + ) + return True diff --git a/homeassistant/components/tasmota/fan.py b/homeassistant/components/tasmota/fan.py index 6aabfb05091..a0002ff85f8 100644 --- a/homeassistant/components/tasmota/fan.py +++ b/homeassistant/components/tasmota/fan.py @@ -113,7 +113,6 @@ class TasmotaFan( async def async_turn_on( self, - speed: str | None = None, percentage: int | None = None, preset_mode: str | None = None, **kwargs: Any, diff --git a/homeassistant/components/tasmota/manifest.json b/homeassistant/components/tasmota/manifest.json index a1f52517690..2f3a1b66fea 100644 --- a/homeassistant/components/tasmota/manifest.json +++ b/homeassistant/components/tasmota/manifest.json @@ -3,7 +3,7 @@ "name": "Tasmota", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tasmota", - "requirements": ["hatasmota==0.3.1"], + "requirements": ["hatasmota==0.4.0"], "dependencies": ["mqtt"], "mqtt": ["tasmota/discovery/#"], "codeowners": ["@emontnemery"], diff --git a/homeassistant/components/tasmota/strings.json b/homeassistant/components/tasmota/strings.json index 3d32b54b95d..2a23912b80c 100644 --- a/homeassistant/components/tasmota/strings.json +++ b/homeassistant/components/tasmota/strings.json @@ -5,8 +5,6 @@ "description": "Do you want to set up Tasmota?" }, "config": { - "title": "Tasmota", - "description": "Please enter the Tasmota configuration.", "data": { "discovery_prefix": "Discovery topic prefix" } diff --git a/homeassistant/components/tasmota/translations/it.json b/homeassistant/components/tasmota/translations/it.json index 9d1f143ebad..6cb52a616b0 100644 --- a/homeassistant/components/tasmota/translations/it.json +++ b/homeassistant/components/tasmota/translations/it.json @@ -4,7 +4,7 @@ "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione." }, "error": { - "invalid_discovery_topic": "Prefisso dell'argomento di individuazione non valido." + "invalid_discovery_topic": "Prefisso dell'argomento di rilevamento non valido." }, "step": { "config": { diff --git a/homeassistant/components/tasmota/translations/zh-Hant.json b/homeassistant/components/tasmota/translations/zh-Hant.json index 3a11beed839..e823937f972 100644 --- a/homeassistant/components/tasmota/translations/zh-Hant.json +++ b/homeassistant/components/tasmota/translations/zh-Hant.json @@ -4,7 +4,7 @@ "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "error": { - "invalid_discovery_topic": "\u63a2\u7d22\u4e3b\u984c prefix \u7121\u6548\u3002" + "invalid_discovery_topic": "\u641c\u7d22\u4e3b\u984c prefix \u7121\u6548\u3002" }, "step": { "config": { diff --git a/homeassistant/components/tautulli/sensor.py b/homeassistant/components/tautulli/sensor.py index 814f6c9da50..f28e334e1c0 100644 --- a/homeassistant/components/tautulli/sensor.py +++ b/homeassistant/components/tautulli/sensor.py @@ -94,11 +94,9 @@ async def async_setup_platform( ) -class TautulliSensor(CoordinatorEntity, SensorEntity): +class TautulliSensor(CoordinatorEntity[TautulliDataUpdateCoordinator], SensorEntity): """Representation of a Tautulli sensor.""" - coordinator: TautulliDataUpdateCoordinator - def __init__( self, coordinator: TautulliDataUpdateCoordinator, diff --git a/homeassistant/components/telegram_bot/__init__.py b/homeassistant/components/telegram_bot/__init__.py index cebdd4f4573..29dbabcbbfe 100644 --- a/homeassistant/components/telegram_bot/__init__.py +++ b/homeassistant/components/telegram_bot/__init__.py @@ -1,20 +1,27 @@ """Support to send and receive Telegram messages.""" +from __future__ import annotations + from functools import partial import importlib import io from ipaddress import ip_network import logging +from typing import Any import requests from requests.auth import HTTPBasicAuth, HTTPDigestAuth from telegram import ( Bot, + CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup, + Message, ReplyKeyboardMarkup, ReplyKeyboardRemove, + Update, ) from telegram.error import TelegramError +from telegram.ext import CallbackContext, Filters from telegram.parsemode import ParseMode from telegram.utils.request import Request import voluptuous as vol @@ -311,14 +318,15 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return False for p_config in config[DOMAIN]: - + # Each platform config gets its own bot + bot = initialize_bot(p_config) p_type = p_config.get(CONF_PLATFORM) platform = importlib.import_module(f".{p_config[CONF_PLATFORM]}", __name__) _LOGGER.info("Setting up %s.%s", DOMAIN, p_type) try: - receiver_service = await platform.async_setup_platform(hass, p_config) + receiver_service = await platform.async_setup_platform(hass, bot, p_config) if receiver_service is False: _LOGGER.error("Failed to initialize Telegram bot %s", p_type) return False @@ -327,7 +335,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: _LOGGER.exception("Error setting up platform %s", p_type) return False - bot = initialize_bot(p_config) notify_service = TelegramNotificationService( hass, bot, p_config.get(CONF_ALLOWED_CHAT_IDS), p_config.get(ATTR_PARSER) ) @@ -416,7 +423,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: def initialize_bot(p_config): """Initialize telegram bot with proxy support.""" - api_key = p_config.get(CONF_API_KEY) proxy_url = p_config.get(CONF_PROXY_URL) proxy_params = p_config.get(CONF_PROXY_PARAMS) @@ -435,7 +441,6 @@ class TelegramNotificationService: def __init__(self, hass, bot, allowed_chat_ids, parser): """Initialize the service.""" - self.allowed_chat_ids = allowed_chat_ids self._default_user = self.allowed_chat_ids[0] self._last_message_id = {user: None for user in self.allowed_chat_ids} @@ -495,7 +500,6 @@ class TelegramNotificationService: - a string like: `/cmd1, /cmd2, /cmd3` - or a string like: `text_b1:/cmd1, text_b2:/cmd2` """ - buttons = [] if isinstance(row_keyboard, str): for key in row_keyboard.split(","): @@ -566,7 +570,6 @@ class TelegramNotificationService: def _send_msg(self, func_send, msg_error, message_tag, *args_msg, **kwargs_msg): """Send one message.""" - try: out = func_send(*args_msg, **kwargs_msg) if not isinstance(out, bool) and hasattr(out, ATTR_MESSAGEID): @@ -857,131 +860,99 @@ class TelegramNotificationService: class BaseTelegramBotEntity: """The base class for the telegram bot.""" - def __init__(self, hass, allowed_chat_ids): + def __init__(self, hass, config): """Initialize the bot base class.""" - self.allowed_chat_ids = allowed_chat_ids + self.allowed_chat_ids = config[CONF_ALLOWED_CHAT_IDS] self.hass = hass - def _get_message_data(self, msg_data): - """Return boolean msg_data_is_ok and dict msg_data.""" - if not msg_data: - return False, None - bad_fields = ( - "text" not in msg_data and "data" not in msg_data and "chat" not in msg_data - ) - if bad_fields or "from" not in msg_data: - # Message is not correct. - _LOGGER.error("Incoming message does not have required data (%s)", msg_data) - return False, None + def handle_update(self, update: Update, context: CallbackContext) -> bool: + """Handle updates from bot dispatcher set up by the respective platform.""" + _LOGGER.debug("Handling update %s", update) + if not self.authorize_update(update): + return False - if ( - msg_data["from"].get("id") not in self.allowed_chat_ids - and msg_data["message"]["chat"].get("id") not in self.allowed_chat_ids - ): - # Neither from id nor chat id was in allowed_chat_ids, - # origin is not allowed. - _LOGGER.error("Incoming message is not allowed (%s)", msg_data) - return True, None - - data = { - ATTR_USER_ID: msg_data["from"]["id"], - ATTR_FROM_FIRST: msg_data["from"]["first_name"], - } - if "message_id" in msg_data: - data[ATTR_MSGID] = msg_data["message_id"] - if "last_name" in msg_data["from"]: - data[ATTR_FROM_LAST] = msg_data["from"]["last_name"] - if "chat" in msg_data: - data[ATTR_CHAT_ID] = msg_data["chat"]["id"] - elif ATTR_MESSAGE in msg_data and "chat" in msg_data[ATTR_MESSAGE]: - data[ATTR_CHAT_ID] = msg_data[ATTR_MESSAGE]["chat"]["id"] - - return True, data - - def _get_channel_post_data(self, msg_data): - """Return boolean msg_data_is_ok and dict msg_data.""" - if not msg_data: - return False, None - - if "sender_chat" in msg_data and "chat" in msg_data and "text" in msg_data: - if ( - msg_data["sender_chat"].get("id") not in self.allowed_chat_ids - and msg_data["chat"].get("id") not in self.allowed_chat_ids - ): - # Neither sender_chat id nor chat id was in allowed_chat_ids, - # origin is not allowed. - _LOGGER.error("Incoming message is not allowed (%s)", msg_data) - return True, None - - data = { - ATTR_MSGID: msg_data["message_id"], - ATTR_CHAT_ID: msg_data["chat"]["id"], - ATTR_TEXT: msg_data["text"], - } - return True, data - - _LOGGER.error("Incoming message does not have required data (%s)", msg_data) - return False, None - - def process_message(self, data): - """Check for basic message rules and fire an event if message is ok.""" - if ATTR_MSG in data or ATTR_EDITED_MSG in data: - event = EVENT_TELEGRAM_COMMAND - if ATTR_MSG in data: - data = data.get(ATTR_MSG) - else: - data = data.get(ATTR_EDITED_MSG) - message_ok, event_data = self._get_message_data(data) - if event_data is None: - return message_ok - - if ATTR_MSGID in data: - event_data[ATTR_MSGID] = data[ATTR_MSGID] - - if "text" in data: - if data["text"][0] == "/": - pieces = data["text"].split(" ") - event_data[ATTR_COMMAND] = pieces[0] - event_data[ATTR_ARGS] = pieces[1:] - else: - event_data[ATTR_TEXT] = data["text"] - event = EVENT_TELEGRAM_TEXT - else: - _LOGGER.warning("Message without text data received: %s", data) - event_data[ATTR_TEXT] = str(data) - event = EVENT_TELEGRAM_TEXT - - self.hass.bus.async_fire(event, event_data) - return True - if ATTR_CALLBACK_QUERY in data: - event = EVENT_TELEGRAM_CALLBACK - data = data.get(ATTR_CALLBACK_QUERY) - message_ok, event_data = self._get_message_data(data) - if event_data is None: - return message_ok - - query_data = event_data[ATTR_DATA] = data[ATTR_DATA] - - if query_data[0] == "/": - pieces = query_data.split(" ") - event_data[ATTR_COMMAND] = pieces[0] - event_data[ATTR_ARGS] = pieces[1:] - - event_data[ATTR_MSG] = data[ATTR_MSG] - event_data[ATTR_CHAT_INSTANCE] = data[ATTR_CHAT_INSTANCE] - event_data[ATTR_MSGID] = data[ATTR_MSGID] - - self.hass.bus.async_fire(event, event_data) - return True - if ATTR_CHANNEL_POST in data: - event = EVENT_TELEGRAM_TEXT - data = data.get(ATTR_CHANNEL_POST) - message_ok, event_data = self._get_channel_post_data(data) - if event_data is None: - return message_ok - - self.hass.bus.async_fire(event, event_data) + # establish event type: text, command or callback_query + if update.callback_query: + # NOTE: Check for callback query first since effective message will be populated with the message + # in .callback_query (python-telegram-bot docs are wrong) + event_type, event_data = self._get_callback_query_event_data( + update.callback_query + ) + elif update.effective_message: + event_type, event_data = self._get_message_event_data( + update.effective_message + ) + else: + _LOGGER.warning("Unhandled update: %s", update) return True - _LOGGER.warning("Message with unknown data received: %s", data) + _LOGGER.debug("Firing event %s: %s", event_type, event_data) + self.hass.bus.fire(event_type, event_data) return True + + @staticmethod + def _get_command_event_data(command_text: str) -> dict[str, str | list]: + if not command_text.startswith("/"): + return {} + command_parts = command_text.split() + command = command_parts[0] + args = command_parts[1:] + return {ATTR_COMMAND: command, ATTR_ARGS: args} + + def _get_message_event_data(self, message: Message) -> tuple[str, dict[str, Any]]: + event_data: dict[str, Any] = { + ATTR_MSGID: message.message_id, + ATTR_CHAT_ID: message.chat.id, + } + if Filters.command.filter(message): + # This is a command message - set event type to command and split data into command and args + event_type = EVENT_TELEGRAM_COMMAND + event_data.update(self._get_command_event_data(message.text)) + else: + event_type = EVENT_TELEGRAM_TEXT + event_data[ATTR_TEXT] = message.text + + if message.from_user: + event_data.update( + { + ATTR_USER_ID: message.from_user.id, + ATTR_FROM_FIRST: message.from_user.first_name, + ATTR_FROM_LAST: message.from_user.last_name, + } + ) + + return event_type, event_data + + def _get_callback_query_event_data( + self, callback_query: CallbackQuery + ) -> tuple[str, dict[str, Any]]: + event_type = EVENT_TELEGRAM_CALLBACK + event_data: dict[str, Any] = { + ATTR_MSGID: callback_query.id, + ATTR_CHAT_INSTANCE: callback_query.chat_instance, + ATTR_DATA: callback_query.data, + ATTR_MSG: None, + ATTR_CHAT_ID: None, + } + if callback_query.message: + event_data[ATTR_MSG] = callback_query.message.to_dict() + event_data[ATTR_CHAT_ID] = callback_query.message.chat.id + + # Split data into command and args if possible + event_data.update(self._get_command_event_data(callback_query.data)) + + return event_type, event_data + + def authorize_update(self, update: Update) -> bool: + """Make sure either user or chat is in allowed_chat_ids.""" + from_user = update.effective_user.id if update.effective_user else None + from_chat = update.effective_chat.id if update.effective_chat else None + if from_user in self.allowed_chat_ids or from_chat in self.allowed_chat_ids: + return True + _LOGGER.error( + "Unauthorized update - neither user id %s nor chat id %s is in allowed chats: %s", + from_user, + from_chat, + self.allowed_chat_ids, + ) + return False diff --git a/homeassistant/components/telegram_bot/polling.py b/homeassistant/components/telegram_bot/polling.py index b617826411d..4c0de6ade1e 100644 --- a/homeassistant/components/telegram_bot/polling.py +++ b/homeassistant/components/telegram_bot/polling.py @@ -3,31 +3,21 @@ import logging from telegram import Update from telegram.error import NetworkError, RetryAfter, TelegramError, TimedOut -from telegram.ext import CallbackContext, Dispatcher, Handler, Updater -from telegram.utils.types import HandlerArg +from telegram.ext import CallbackContext, TypeHandler, Updater from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP -from . import CONF_ALLOWED_CHAT_IDS, BaseTelegramBotEntity, initialize_bot +from . import BaseTelegramBotEntity _LOGGER = logging.getLogger(__name__) -async def async_setup_platform(hass, config): +async def async_setup_platform(hass, bot, config): """Set up the Telegram polling platform.""" - bot = initialize_bot(config) - pol = TelegramPoll(bot, hass, config[CONF_ALLOWED_CHAT_IDS]) + pollbot = PollBot(hass, bot, config) - def _start_bot(_event): - """Start the bot.""" - pol.start_polling() - - def _stop_bot(_event): - """Stop the bot.""" - pol.stop_polling() - - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, _start_bot) - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _stop_bot) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, pollbot.start_polling) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, pollbot.stop_polling) return True @@ -43,57 +33,28 @@ def process_error(update: Update, context: CallbackContext): _LOGGER.error('Update "%s" caused error: "%s"', update, context.error) -def message_handler(handler): - """Create messages handler.""" +class PollBot(BaseTelegramBotEntity): + """ + Controls the Updater object that holds the bot and a dispatcher. - class MessageHandler(Handler): - """Telegram bot message handler.""" - - def __init__(self): - """Initialize the messages handler instance.""" - super().__init__(handler) - - def check_update(self, update): - """Check is update valid.""" - return isinstance(update, Update) - - def handle_update( - self, - update: HandlerArg, - dispatcher: Dispatcher, - check_result: object, - context: CallbackContext = None, - ): - """Handle update.""" - optional_args = self.collect_optional_args(dispatcher, update) - context.args = optional_args - return self.callback(update, context) - - return MessageHandler() - - -class TelegramPoll(BaseTelegramBotEntity): - """Asyncio telegram incoming message handler.""" - - def __init__(self, bot, hass, allowed_chat_ids): - """Initialize the polling instance.""" - - BaseTelegramBotEntity.__init__(self, hass, allowed_chat_ids) + The dispatcher is set up by the super class to pass telegram updates to `self.handle_update` + """ + def __init__(self, hass, bot, config): + """Create Updater and Dispatcher before calling super().""" + self.bot = bot self.updater = Updater(bot=bot, workers=4) self.dispatcher = self.updater.dispatcher - - self.dispatcher.add_handler(message_handler(self.process_update)) + self.dispatcher.add_handler(TypeHandler(Update, self.handle_update)) self.dispatcher.add_error_handler(process_error) + super().__init__(hass, config) - def start_polling(self): + def start_polling(self, event=None): """Start the polling task.""" + _LOGGER.debug("Starting polling") self.updater.start_polling() - def stop_polling(self): + def stop_polling(self, event=None): """Stop the polling task.""" + _LOGGER.debug("Stopping polling") self.updater.stop() - - def process_update(self, update: HandlerArg, context: CallbackContext): - """Process incoming message.""" - self.process_message(update.to_dict()) diff --git a/homeassistant/components/telegram_bot/webhooks.py b/homeassistant/components/telegram_bot/webhooks.py index c1e86129ebb..8b94cb66496 100644 --- a/homeassistant/components/telegram_bot/webhooks.py +++ b/homeassistant/components/telegram_bot/webhooks.py @@ -4,90 +4,115 @@ from http import HTTPStatus from ipaddress import ip_address import logging +from telegram import Update from telegram.error import TimedOut +from telegram.ext import Dispatcher, TypeHandler from homeassistant.components.http import HomeAssistantView from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.helpers.network import get_url -from . import ( - CONF_ALLOWED_CHAT_IDS, - CONF_TRUSTED_NETWORKS, - CONF_URL, - BaseTelegramBotEntity, - initialize_bot, -) +from . import CONF_TRUSTED_NETWORKS, CONF_URL, BaseTelegramBotEntity _LOGGER = logging.getLogger(__name__) -TELEGRAM_HANDLER_URL = "/api/telegram_webhooks" -REMOVE_HANDLER_URL = "" +TELEGRAM_WEBHOOK_URL = "/api/telegram_webhooks" +REMOVE_WEBHOOK_URL = "" -async def async_setup_platform(hass, config): +async def async_setup_platform(hass, bot, config): """Set up the Telegram webhooks platform.""" + pushbot = PushBot(hass, bot, config) - bot = initialize_bot(config) - - current_status = await hass.async_add_executor_job(bot.getWebhookInfo) - if not (base_url := config.get(CONF_URL)): - base_url = get_url(hass, require_ssl=True, allow_internal=False) - - # Some logging of Bot current status: - last_error_date = getattr(current_status, "last_error_date", None) - if (last_error_date is not None) and (isinstance(last_error_date, int)): - last_error_date = dt.datetime.fromtimestamp(last_error_date) - _LOGGER.info( - "Telegram webhook last_error_date: %s. Status: %s", - last_error_date, - current_status, - ) - else: - _LOGGER.debug("telegram webhook Status: %s", current_status) - - handler_url = f"{base_url}{TELEGRAM_HANDLER_URL}" - if not handler_url.startswith("https"): - _LOGGER.error("Invalid telegram webhook %s must be https", handler_url) + if not pushbot.webhook_url.startswith("https"): + _LOGGER.error("Invalid telegram webhook %s must be https", pushbot.webhook_url) return False - def _try_to_set_webhook(): - retry_num = 0 - while retry_num < 3: - try: - return bot.setWebhook(handler_url, timeout=5) - except TimedOut: - retry_num += 1 - _LOGGER.warning("Timeout trying to set webhook (retry #%d)", retry_num) + webhook_registered = await pushbot.register_webhook() + if not webhook_registered: + return False - if current_status and current_status["url"] != handler_url: - result = await hass.async_add_executor_job(_try_to_set_webhook) - if result: - _LOGGER.info("Set new telegram webhook %s", handler_url) - else: - _LOGGER.error("Set telegram webhook failed %s", handler_url) - return False - - hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STOP, lambda event: bot.setWebhook(REMOVE_HANDLER_URL) - ) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, pushbot.deregister_webhook) hass.http.register_view( - BotPushReceiver( - hass, config[CONF_ALLOWED_CHAT_IDS], config[CONF_TRUSTED_NETWORKS] - ) + PushBotView(hass, bot, pushbot.dispatcher, config[CONF_TRUSTED_NETWORKS]) ) return True -class BotPushReceiver(HomeAssistantView, BaseTelegramBotEntity): - """Handle pushes from Telegram.""" +class PushBot(BaseTelegramBotEntity): + """Handles all the push/webhook logic and passes telegram updates to `self.handle_update`.""" + + def __init__(self, hass, bot, config): + """Create Dispatcher before calling super().""" + self.bot = bot + self.trusted_networks = config[CONF_TRUSTED_NETWORKS] + # Dumb dispatcher that just gets our updates to our handler callback (self.handle_update) + self.dispatcher = Dispatcher(bot, None) + self.dispatcher.add_handler(TypeHandler(Update, self.handle_update)) + super().__init__(hass, config) + + self.base_url = config.get(CONF_URL) or get_url( + hass, require_ssl=True, allow_internal=False + ) + self.webhook_url = f"{self.base_url}{TELEGRAM_WEBHOOK_URL}" + + def _try_to_set_webhook(self): + _LOGGER.debug("Registering webhook URL: %s", self.webhook_url) + retry_num = 0 + while retry_num < 3: + try: + return self.bot.set_webhook(self.webhook_url, timeout=5) + except TimedOut: + retry_num += 1 + _LOGGER.warning("Timeout trying to set webhook (retry #%d)", retry_num) + + return False + + async def register_webhook(self): + """Query telegram and register the URL for our webhook.""" + current_status = await self.hass.async_add_executor_job( + self.bot.get_webhook_info + ) + # Some logging of Bot current status: + last_error_date = getattr(current_status, "last_error_date", None) + if (last_error_date is not None) and (isinstance(last_error_date, int)): + last_error_date = dt.datetime.fromtimestamp(last_error_date) + _LOGGER.debug( + "Telegram webhook last_error_date: %s. Status: %s", + last_error_date, + current_status, + ) + else: + _LOGGER.debug("telegram webhook status: %s", current_status) + + if current_status and current_status["url"] != self.webhook_url: + result = await self.hass.async_add_executor_job(self._try_to_set_webhook) + if result: + _LOGGER.info("Set new telegram webhook %s", self.webhook_url) + else: + _LOGGER.error("Set telegram webhook failed %s", self.webhook_url) + return False + + return True + + def deregister_webhook(self, event=None): + """Query telegram and deregister the URL for our webhook.""" + _LOGGER.debug("Deregistering webhook URL") + return self.bot.delete_webhook() + + +class PushBotView(HomeAssistantView): + """View for handling webhook calls from Telegram.""" requires_auth = False - url = TELEGRAM_HANDLER_URL + url = TELEGRAM_WEBHOOK_URL name = "telegram_webhooks" - def __init__(self, hass, allowed_chat_ids, trusted_networks): - """Initialize the class.""" - BaseTelegramBotEntity.__init__(self, hass, allowed_chat_ids) + def __init__(self, hass, bot, dispatcher, trusted_networks): + """Initialize by storing stuff needed for setting up our webhook endpoint.""" + self.hass = hass + self.bot = bot + self.dispatcher = dispatcher self.trusted_networks = trusted_networks async def post(self, request): @@ -98,10 +123,12 @@ class BotPushReceiver(HomeAssistantView, BaseTelegramBotEntity): return self.json_message("Access denied", HTTPStatus.UNAUTHORIZED) try: - data = await request.json() + update_data = await request.json() except ValueError: return self.json_message("Invalid JSON", HTTPStatus.BAD_REQUEST) - if not self.process_message(data): - return self.json_message("Invalid message", HTTPStatus.BAD_REQUEST) + update = Update.de_json(update_data, self.bot) + _LOGGER.debug("Received Update on %s: %s", self.url, update) + await self.hass.async_add_executor_job(self.dispatcher.process_update, update) + return None diff --git a/homeassistant/components/tellduslive/translations/fr.json b/homeassistant/components/tellduslive/translations/fr.json index 9dd1a8cd3f8..adcef0c1fd4 100644 --- a/homeassistant/components/tellduslive/translations/fr.json +++ b/homeassistant/components/tellduslive/translations/fr.json @@ -2,12 +2,12 @@ "config": { "abort": { "already_configured": "Le service est d\u00e9j\u00e0 configur\u00e9", - "authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration de l'URL d'authentification d\u00e9pass\u00e9.", + "authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration de l'URL d'authentification expir\u00e9.", "unknown": "Erreur inattendue", "unknown_authorize_url_generation": "Erreur inconnue lors de la g\u00e9n\u00e9ration d'une URL d'autorisation." }, "error": { - "invalid_auth": "Authentification invalide" + "invalid_auth": "Authentification non valide" }, "step": { "auth": { diff --git a/homeassistant/components/tellstick/__init__.py b/homeassistant/components/tellstick/__init__.py index b95928c01b4..8611c99b654 100644 --- a/homeassistant/components/tellstick/__init__.py +++ b/homeassistant/components/tellstick/__init__.py @@ -17,6 +17,7 @@ from homeassistant.const import CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import ConfigType @@ -188,8 +189,8 @@ class TellstickDevice(Entity): async def async_added_to_hass(self): """Register callbacks.""" self.async_on_remove( - self.hass.helpers.dispatcher.async_dispatcher_connect( - SIGNAL_TELLCORE_CALLBACK, self.update_from_callback + async_dispatcher_connect( + self.hass, SIGNAL_TELLCORE_CALLBACK, self.update_from_callback ) ) diff --git a/homeassistant/components/tellstick/sensor.py b/homeassistant/components/tellstick/sensor.py index f3b322c5d95..e820c46da45 100644 --- a/homeassistant/components/tellstick/sensor.py +++ b/homeassistant/components/tellstick/sensor.py @@ -15,6 +15,7 @@ from homeassistant.components.sensor import ( ) from homeassistant.const import ( CONF_ID, + CONF_MODEL, CONF_NAME, CONF_PROTOCOL, PERCENTAGE, @@ -34,7 +35,6 @@ DatatypeDescription = namedtuple( CONF_DATATYPE_MASK = "datatype_mask" CONF_ONLY_NAMED = "only_named" CONF_TEMPERATURE_SCALE = "temperature_scale" -CONF_MODEL = "model" DEFAULT_DATATYPE_MASK = 127 DEFAULT_TEMPERATURE_SCALE = TEMP_CELSIUS diff --git a/homeassistant/components/template/alarm_control_panel.py b/homeassistant/components/template/alarm_control_panel.py index ca037f23bc4..b1f1af4a6e0 100644 --- a/homeassistant/components/template/alarm_control_panel.py +++ b/homeassistant/components/template/alarm_control_panel.py @@ -220,7 +220,7 @@ class AlarmControlPanelTemplate(TemplateEntity, AlarmControlPanelEntity): ) await super().async_added_to_hass() - async def _async_alarm_arm(self, state, script=None, code=None): + async def _async_alarm_arm(self, state, script, code): """Arm the panel to specified state with supplied script.""" optimistic_set = False @@ -228,10 +228,7 @@ class AlarmControlPanelTemplate(TemplateEntity, AlarmControlPanelEntity): self._state = state optimistic_set = True - if script is not None: - await script.async_run({ATTR_CODE: code}, context=self._context) - else: - _LOGGER.error("No script action defined for %s", state) + await script.async_run({ATTR_CODE: code}, context=self._context) if optimistic_set: self.async_write_ha_state() diff --git a/homeassistant/components/template/binary_sensor.py b/homeassistant/components/template/binary_sensor.py index 4c080c736d0..2a537e2aa6b 100644 --- a/homeassistant/components/template/binary_sensor.py +++ b/homeassistant/components/template/binary_sensor.py @@ -1,7 +1,8 @@ """Support for exposing a templated binary sensor.""" from __future__ import annotations -from datetime import timedelta +from dataclasses import dataclass +from datetime import datetime, timedelta from functools import partial import logging from typing import Any @@ -30,6 +31,9 @@ from homeassistant.const import ( CONF_UNIQUE_ID, CONF_UNIT_OF_MEASUREMENT, CONF_VALUE_TEMPLATE, + STATE_ON, + STATE_UNAVAILABLE, + STATE_UNKNOWN, ) from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.exceptions import TemplateError @@ -37,8 +41,10 @@ from homeassistant.helpers import template import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.event import async_call_later +from homeassistant.helpers.event import async_call_later, async_track_point_in_utc_time +from homeassistant.helpers.restore_state import ExtraStoredData, RestoreEntity from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.util import dt as dt_util from . import TriggerUpdateCoordinator from .const import ( @@ -186,7 +192,7 @@ async def async_setup_platform( ) -class BinarySensorTemplate(TemplateEntity, BinarySensorEntity): +class BinarySensorTemplate(TemplateEntity, BinarySensorEntity, RestoreEntity): """A virtual binary sensor that triggers from another sensor.""" def __init__( @@ -212,7 +218,14 @@ class BinarySensorTemplate(TemplateEntity, BinarySensorEntity): self._delay_off_raw = config.get(CONF_DELAY_OFF) async def async_added_to_hass(self): - """Register callbacks.""" + """Restore state and register callbacks.""" + if ( + (self._delay_on_raw is not None or self._delay_off_raw is not None) + and (last_state := await self.async_get_last_state()) is not None + and last_state.state not in (STATE_UNKNOWN, STATE_UNAVAILABLE) + ): + self._state = last_state.state == STATE_ON + self.add_template_attribute("_state", self._template, None, self._update_state) if self._delay_on_raw is not None: @@ -280,7 +293,7 @@ class BinarySensorTemplate(TemplateEntity, BinarySensorEntity): return self._device_class -class TriggerBinarySensorEntity(TriggerEntity, BinarySensorEntity): +class TriggerBinarySensorEntity(TriggerEntity, BinarySensorEntity, RestoreEntity): """Sensor entity based on trigger data.""" domain = BINARY_SENSOR_DOMAIN @@ -301,9 +314,37 @@ class TriggerBinarySensorEntity(TriggerEntity, BinarySensorEntity): self._parse_result.add(key) self._delay_cancel: CALLBACK_TYPE | None = None - self._auto_off_cancel = None + self._auto_off_cancel: CALLBACK_TYPE | None = None + self._auto_off_time: datetime | None = None self._state: bool | None = None + async def async_added_to_hass(self) -> None: + """Restore last state.""" + await super().async_added_to_hass() + if ( + (last_state := await self.async_get_last_state()) is not None + and (extra_data := await self.async_get_last_binary_sensor_data()) + is not None + and last_state.state not in (STATE_UNKNOWN, STATE_UNAVAILABLE) + # The trigger might have fired already while we waited for stored data, + # then we should not restore state + and self._state is None + ): + self._state = last_state.state == STATE_ON + self.restore_attributes(last_state) + + if CONF_AUTO_OFF not in self._config: + return + + if ( + auto_off_time := extra_data.auto_off_time + ) is not None and auto_off_time <= dt_util.utcnow(): + # It's already past the saved auto off time + self._state = False + + if self._state and auto_off_time is not None: + self._set_auto_off(auto_off_time) + @property def is_on(self) -> bool | None: """Return state of the sensor.""" @@ -321,6 +362,7 @@ class TriggerBinarySensorEntity(TriggerEntity, BinarySensorEntity): if self._auto_off_cancel: self._auto_off_cancel() self._auto_off_cancel = None + self._auto_off_time = None if not self.available: self.async_write_ha_state() @@ -361,28 +403,85 @@ class TriggerBinarySensorEntity(TriggerEntity, BinarySensorEntity): if not state: return - auto_off_time = self._rendered.get(CONF_AUTO_OFF) or self._config.get( + auto_off_delay = self._rendered.get(CONF_AUTO_OFF) or self._config.get( CONF_AUTO_OFF ) - if auto_off_time is None: + if auto_off_delay is None: return - if not isinstance(auto_off_time, timedelta): + if not isinstance(auto_off_delay, timedelta): try: - auto_off_time = cv.positive_time_period(auto_off_time) + auto_off_delay = cv.positive_time_period(auto_off_delay) except vol.Invalid as err: logging.getLogger(__name__).warning( "Error rendering %s template: %s", CONF_AUTO_OFF, err ) return + auto_off_time = dt_util.utcnow() + auto_off_delay + self._set_auto_off(auto_off_time) + + def _set_auto_off(self, auto_off_time: datetime) -> None: @callback def _auto_off(_): - """Set state of template binary sensor.""" + """Reset state of template binary sensor.""" self._state = False self.async_write_ha_state() - self._auto_off_cancel = async_call_later( - self.hass, auto_off_time.total_seconds(), _auto_off + self._auto_off_time = auto_off_time + self._auto_off_cancel = async_track_point_in_utc_time( + self.hass, _auto_off, self._auto_off_time ) + + @property + def extra_restore_state_data(self) -> AutoOffExtraStoredData: + """Return specific state data to be restored.""" + return AutoOffExtraStoredData(self._auto_off_time) + + async def async_get_last_binary_sensor_data( + self, + ) -> AutoOffExtraStoredData | None: + """Restore auto_off_time.""" + if (restored_last_extra_data := await self.async_get_last_extra_data()) is None: + return None + return AutoOffExtraStoredData.from_dict(restored_last_extra_data.as_dict()) + + +@dataclass +class AutoOffExtraStoredData(ExtraStoredData): + """Object to hold extra stored data.""" + + auto_off_time: datetime | None + + def as_dict(self) -> dict[str, Any]: + """Return a dict representation of additional data.""" + auto_off_time: datetime | None | dict[str, str] = self.auto_off_time + if isinstance(auto_off_time, datetime): + auto_off_time = { + "__type": str(type(auto_off_time)), + "isoformat": auto_off_time.isoformat(), + } + return { + "auto_off_time": auto_off_time, + } + + @classmethod + def from_dict(cls, restored: dict[str, Any]) -> AutoOffExtraStoredData | None: + """Initialize a stored binary sensor state from a dict.""" + try: + auto_off_time = restored["auto_off_time"] + except KeyError: + return None + try: + type_ = auto_off_time["__type"] + if type_ == "": + auto_off_time = dt_util.parse_datetime(auto_off_time["isoformat"]) + except TypeError: + # native_value is not a dict + pass + except KeyError: + # native_value is a dict, but does not have all values + return None + + return cls(auto_off_time) diff --git a/homeassistant/components/template/fan.py b/homeassistant/components/template/fan.py index 1d25c24017f..b172e94016f 100644 --- a/homeassistant/components/template/fan.py +++ b/homeassistant/components/template/fan.py @@ -11,7 +11,6 @@ from homeassistant.components.fan import ( ATTR_OSCILLATING, ATTR_PERCENTAGE, ATTR_PRESET_MODE, - ATTR_SPEED, DIRECTION_FORWARD, DIRECTION_REVERSE, ENTITY_ID_FORMAT, @@ -250,7 +249,6 @@ class TemplateFan(TemplateEntity, FanEntity): async def async_turn_on( self, - speed: str = None, percentage: int = None, preset_mode: str = None, **kwargs, @@ -258,7 +256,6 @@ class TemplateFan(TemplateEntity, FanEntity): """Turn on the fan.""" await self._on_script.async_run( { - ATTR_SPEED: speed, ATTR_PERCENTAGE: percentage, ATTR_PRESET_MODE: preset_mode, }, @@ -270,8 +267,6 @@ class TemplateFan(TemplateEntity, FanEntity): await self.async_set_preset_mode(preset_mode) elif percentage is not None: await self.async_set_percentage(percentage) - elif speed is not None: - await self.async_set_speed(speed) async def async_turn_off(self, **kwargs: Any) -> None: """Turn off the fan.""" diff --git a/homeassistant/components/template/sensor.py b/homeassistant/components/template/sensor.py index c1fae1f4f3b..a61d15cb7a4 100644 --- a/homeassistant/components/template/sensor.py +++ b/homeassistant/components/template/sensor.py @@ -253,6 +253,11 @@ class TriggerSensorEntity(TriggerEntity, SensorEntity): """Sensor state class.""" return self._config.get(CONF_STATE_CLASS) + @property + def native_unit_of_measurement(self) -> str | None: + """Return the unit of measurement of the sensor, if any.""" + return self._config.get(CONF_UNIT_OF_MEASUREMENT) + @callback def _process_data(self) -> None: """Process new data.""" diff --git a/homeassistant/components/template/services.yaml b/homeassistant/components/template/services.yaml index b6fa371e818..6186bc6dccb 100644 --- a/homeassistant/components/template/services.yaml +++ b/homeassistant/components/template/services.yaml @@ -1,4 +1,3 @@ reload: name: Reload description: Reload all template entities. - diff --git a/homeassistant/components/template/trigger_entity.py b/homeassistant/components/template/trigger_entity.py index 2768609e65d..6780d12c507 100644 --- a/homeassistant/components/template/trigger_entity.py +++ b/homeassistant/components/template/trigger_entity.py @@ -5,21 +5,30 @@ import logging from typing import Any from homeassistant.const import ( + ATTR_ENTITY_PICTURE, + ATTR_FRIENDLY_NAME, + ATTR_ICON, CONF_DEVICE_CLASS, CONF_ICON, CONF_NAME, CONF_UNIQUE_ID, - CONF_UNIT_OF_MEASUREMENT, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant, State, callback from homeassistant.exceptions import TemplateError -from homeassistant.helpers import template, update_coordinator +from homeassistant.helpers import template +from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import TriggerUpdateCoordinator from .const import CONF_ATTRIBUTES, CONF_AVAILABILITY, CONF_PICTURE +CONF_TO_ATTRIBUTE = { + CONF_ICON: ATTR_ICON, + CONF_NAME: ATTR_FRIENDLY_NAME, + CONF_PICTURE: ATTR_ENTITY_PICTURE, +} -class TriggerEntity(update_coordinator.CoordinatorEntity): + +class TriggerEntity(CoordinatorEntity[TriggerUpdateCoordinator]): """Template entity based on trigger data.""" domain: str @@ -50,10 +59,10 @@ class TriggerEntity(update_coordinator.CoordinatorEntity): self._to_render_complex: list[str] = [] for itm in ( - CONF_NAME, - CONF_ICON, - CONF_PICTURE, CONF_AVAILABILITY, + CONF_ICON, + CONF_NAME, + CONF_PICTURE, ): if itm not in config: continue @@ -88,11 +97,6 @@ class TriggerEntity(update_coordinator.CoordinatorEntity): """Return device class of the entity.""" return self._config.get(CONF_DEVICE_CLASS) - @property - def unit_of_measurement(self) -> str | None: - """Return unit of measurement.""" - return self._config.get(CONF_UNIT_OF_MEASUREMENT) - @property def icon(self) -> str | None: """Return icon.""" @@ -125,6 +129,21 @@ class TriggerEntity(update_coordinator.CoordinatorEntity): if self.coordinator.data is not None: self._process_data() + def restore_attributes(self, last_state: State) -> None: + """Restore attributes.""" + for conf_key, attr in CONF_TO_ATTRIBUTE.items(): + if conf_key not in self._config or attr not in last_state.attributes: + continue + self._rendered[conf_key] = last_state.attributes[attr] + + if CONF_ATTRIBUTES in self._config: + extra_state_attributes = {} + for attr in self._config[CONF_ATTRIBUTES]: + if attr not in last_state.attributes: + continue + extra_state_attributes[attr] = last_state.attributes[attr] + self._rendered[CONF_ATTRIBUTES] = extra_state_attributes + @callback def _process_data(self) -> None: """Process new data.""" diff --git a/homeassistant/components/tensorflow/image_processing.py b/homeassistant/components/tensorflow/image_processing.py index d90558e8b2e..60e9dc54b80 100644 --- a/homeassistant/components/tensorflow/image_processing.py +++ b/homeassistant/components/tensorflow/image_processing.py @@ -19,6 +19,7 @@ from homeassistant.components.image_processing import ( ) from homeassistant.const import ( CONF_ENTITY_ID, + CONF_MODEL, CONF_NAME, CONF_SOURCE, EVENT_HOMEASSISTANT_START, @@ -49,7 +50,6 @@ CONF_GRAPH = "graph" CONF_LABELS = "labels" CONF_LABEL_OFFSET = "label_offset" CONF_LEFT = "left" -CONF_MODEL = "model" CONF_MODEL_DIR = "model_dir" CONF_RIGHT = "right" CONF_TOP = "top" diff --git a/homeassistant/components/tesla_wall_connector/manifest.json b/homeassistant/components/tesla_wall_connector/manifest.json index 28c2d222f3a..a4bc1969954 100644 --- a/homeassistant/components/tesla_wall_connector/manifest.json +++ b/homeassistant/components/tesla_wall_connector/manifest.json @@ -18,9 +18,7 @@ "macaddress": "4CFCAA*" } ], - "codeowners": [ - "@einarhauks" - ], + "codeowners": ["@einarhauks"], "iot_class": "local_polling", "loggers": ["tesla_wall_connector"] -} \ No newline at end of file +} diff --git a/homeassistant/components/tesla_wall_connector/strings.json b/homeassistant/components/tesla_wall_connector/strings.json index 6fd43f52f30..907209cdcca 100644 --- a/homeassistant/components/tesla_wall_connector/strings.json +++ b/homeassistant/components/tesla_wall_connector/strings.json @@ -17,4 +17,4 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } } -} \ No newline at end of file +} diff --git a/homeassistant/components/threshold/__init__.py b/homeassistant/components/threshold/__init__.py index 98ebdcd8418..75f4c5d1abc 100644 --- a/homeassistant/components/threshold/__init__.py +++ b/homeassistant/components/threshold/__init__.py @@ -1 +1,26 @@ """The threshold component.""" + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Min/Max from a config entry.""" + hass.config_entries.async_setup_platforms(entry, (Platform.BINARY_SENSOR,)) + + entry.async_on_unload(entry.add_update_listener(config_entry_update_listener)) + + return True + + +async def config_entry_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Update listener, called when the config entry options are changed.""" + await hass.config_entries.async_reload(entry.entry_id) + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + return await hass.config_entries.async_unload_platforms( + entry, (Platform.BINARY_SENSOR,) + ) diff --git a/homeassistant/components/threshold/binary_sensor.py b/homeassistant/components/threshold/binary_sensor.py index ccb998c7586..802aa22d759 100644 --- a/homeassistant/components/threshold/binary_sensor.py +++ b/homeassistant/components/threshold/binary_sensor.py @@ -10,6 +10,7 @@ from homeassistant.components.binary_sensor import ( PLATFORM_SCHEMA, BinarySensorEntity, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ENTITY_ID, CONF_DEVICE_CLASS, @@ -19,11 +20,13 @@ from homeassistant.const import ( STATE_UNKNOWN, ) from homeassistant.core import HomeAssistant, callback -import homeassistant.helpers.config_validation as cv +from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from .const import CONF_HYSTERESIS, CONF_LOWER, CONF_UPPER + _LOGGER = logging.getLogger(__name__) ATTR_HYSTERESIS = "hysteresis" @@ -33,10 +36,6 @@ ATTR_SENSOR_VALUE = "sensor_value" ATTR_TYPE = "type" ATTR_UPPER = "upper" -CONF_HYSTERESIS = "hysteresis" -CONF_LOWER = "lower" -CONF_UPPER = "upper" - DEFAULT_NAME = "Threshold" DEFAULT_HYSTERESIS = 0.0 @@ -61,6 +60,32 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Initialize threshold config entry.""" + registry = er.async_get(hass) + device_class = None + entity_id = er.async_validate_entity_id( + registry, config_entry.options[CONF_ENTITY_ID] + ) + hysteresis = config_entry.options[CONF_HYSTERESIS] + lower = config_entry.options[CONF_LOWER] + name = config_entry.title + unique_id = config_entry.entry_id + upper = config_entry.options[CONF_UPPER] + + async_add_entities( + [ + ThresholdSensor( + hass, entity_id, name, lower, upper, hysteresis, device_class, unique_id + ) + ] + ) + + async def async_setup_platform( hass: HomeAssistant, config: ConfigType, @@ -78,7 +103,7 @@ async def async_setup_platform( async_add_entities( [ ThresholdSensor( - hass, entity_id, name, lower, upper, hysteresis, device_class + hass, entity_id, name, lower, upper, hysteresis, device_class, None ) ], ) @@ -87,9 +112,11 @@ async def async_setup_platform( class ThresholdSensor(BinarySensorEntity): """Representation of a Threshold sensor.""" - def __init__(self, hass, entity_id, name, lower, upper, hysteresis, device_class): + def __init__( + self, hass, entity_id, name, lower, upper, hysteresis, device_class, unique_id + ): """Initialize the Threshold sensor.""" - self._hass = hass + self._attr_unique_id = unique_id self._entity_id = entity_id self._name = name self._threshold_lower = lower @@ -101,10 +128,9 @@ class ThresholdSensor(BinarySensorEntity): self._state = None self.sensor_value = None - @callback - def async_threshold_sensor_state_listener(event): + def _update_sensor_state(): """Handle sensor state changes.""" - if (new_state := event.data.get("new_state")) is None: + if (new_state := hass.states.get(self._entity_id)) is None: return try: @@ -118,11 +144,19 @@ class ThresholdSensor(BinarySensorEntity): _LOGGER.warning("State is not numerical") self._update_state() + + @callback + def async_threshold_sensor_state_listener(event): + """Handle sensor state changes.""" + _update_sensor_state() self.async_write_ha_state() - async_track_state_change_event( - hass, [entity_id], async_threshold_sensor_state_listener + self.async_on_remove( + async_track_state_change_event( + hass, [entity_id], async_threshold_sensor_state_listener + ) ) + _update_sensor_state() @property def name(self): diff --git a/homeassistant/components/threshold/config_flow.py b/homeassistant/components/threshold/config_flow.py new file mode 100644 index 00000000000..c77d4b57115 --- /dev/null +++ b/homeassistant/components/threshold/config_flow.py @@ -0,0 +1,64 @@ +"""Config flow for Threshold integration.""" +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any + +import voluptuous as vol + +from homeassistant.const import CONF_ENTITY_ID, CONF_NAME +from homeassistant.helpers import selector +from homeassistant.helpers.schema_config_entry_flow import ( + SchemaConfigFlowHandler, + SchemaFlowError, + SchemaFlowFormStep, + SchemaFlowMenuStep, +) + +from .const import CONF_HYSTERESIS, CONF_LOWER, CONF_UPPER, DEFAULT_HYSTERESIS, DOMAIN + + +def _validate_mode(data: Any) -> Any: + """Validate the threshold mode, and set limits to None if not set.""" + if CONF_LOWER not in data and CONF_UPPER not in data: + raise SchemaFlowError("need_lower_upper") + return {CONF_LOWER: None, CONF_UPPER: None, **data} + + +OPTIONS_SCHEMA = vol.Schema( + { + vol.Required(CONF_HYSTERESIS, default=DEFAULT_HYSTERESIS): selector.selector( + {"number": {"mode": "box"}} + ), + vol.Optional(CONF_LOWER): selector.selector({"number": {"mode": "box"}}), + vol.Optional(CONF_UPPER): selector.selector({"number": {"mode": "box"}}), + } +) + +CONFIG_SCHEMA = vol.Schema( + { + vol.Required(CONF_NAME): selector.selector({"text": {}}), + vol.Required(CONF_ENTITY_ID): selector.selector( + {"entity": {"domain": "sensor"}} + ), + } +).extend(OPTIONS_SCHEMA.schema) + +CONFIG_FLOW: dict[str, SchemaFlowFormStep | SchemaFlowMenuStep] = { + "user": SchemaFlowFormStep(CONFIG_SCHEMA, validate_user_input=_validate_mode) +} + +OPTIONS_FLOW: dict[str, SchemaFlowFormStep | SchemaFlowMenuStep] = { + "init": SchemaFlowFormStep(OPTIONS_SCHEMA, validate_user_input=_validate_mode) +} + + +class ConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN): + """Handle a config or options flow for Threshold.""" + + config_flow = CONFIG_FLOW + options_flow = OPTIONS_FLOW + + def async_config_entry_title(self, options: Mapping[str, Any]) -> str: + """Return config entry title.""" + return options[CONF_NAME] diff --git a/homeassistant/components/threshold/const.py b/homeassistant/components/threshold/const.py new file mode 100644 index 00000000000..2cb9dc88f0f --- /dev/null +++ b/homeassistant/components/threshold/const.py @@ -0,0 +1,9 @@ +"""Constants for the Threshold integration.""" + +DOMAIN = "threshold" + +CONF_HYSTERESIS = "hysteresis" +CONF_LOWER = "lower" +CONF_UPPER = "upper" + +DEFAULT_HYSTERESIS = 0.0 diff --git a/homeassistant/components/threshold/manifest.json b/homeassistant/components/threshold/manifest.json index c4eabcfe6a5..48a44c04a1d 100644 --- a/homeassistant/components/threshold/manifest.json +++ b/homeassistant/components/threshold/manifest.json @@ -1,8 +1,10 @@ { "domain": "threshold", + "integration_type": "helper", "name": "Threshold", "documentation": "https://www.home-assistant.io/integrations/threshold", "codeowners": ["@fabaff"], "quality_scale": "internal", - "iot_class": "local_polling" + "iot_class": "local_polling", + "config_flow": true } diff --git a/homeassistant/components/threshold/strings.json b/homeassistant/components/threshold/strings.json new file mode 100644 index 00000000000..8bfd9fb96b1 --- /dev/null +++ b/homeassistant/components/threshold/strings.json @@ -0,0 +1,38 @@ +{ + "title": "Threshold Sensor", + "config": { + "step": { + "user": { + "title": "Add Threshold Sensor", + "description": "Create a binary sensor that turns on and off depending on the value of a sensor\n\nOnly lower limit configured - Turn on when the input sensor's value is less than the lower limit.\nOnly upper limit configured - Turn on when the input sensor's value is greater than the upper limit.\nBoth lower and upper limit configured - Turn on when the input sensor's value is in the range [lower limit .. upper limit].", + "data": { + "entity_id": "Input sensor", + "hysteresis": "Hysteresis", + "lower": "Lower limit", + "name": "Name", + "upper": "Upper limit" + } + } + }, + "error": { + "need_lower_upper": "Lower and upper limits can't both be empty" + } + }, + "options": { + "step": { + "init": { + "description": "Only lower limit configured - Turn on when the input sensor's value is less than the lower limit.\nOnly upper limit configured - Turn on when the input sensor's value is greater than the upper limit.\nBoth lower and upper limit configured - Turn on when the input sensor's value is in the range [lower limit .. upper limit].", + "data": { + "entity_id": "[%key:component::threshold::config::step::user::data::entity_id%]", + "hysteresis": "[%key:component::threshold::config::step::user::data::hysteresis%]", + "lower": "[%key:component::threshold::config::step::user::data::lower%]", + "name": "[%key:component::threshold::config::step::user::data::name%]", + "upper": "[%key:component::threshold::config::step::user::data::upper%]" + } + } + }, + "error": { + "need_lower_upper": "[%key:component::threshold::config::error::need_lower_upper%]" + } + } +} diff --git a/homeassistant/components/threshold/translations/en.json b/homeassistant/components/threshold/translations/en.json new file mode 100644 index 00000000000..461ca244353 --- /dev/null +++ b/homeassistant/components/threshold/translations/en.json @@ -0,0 +1,38 @@ +{ + "config": { + "error": { + "need_lower_upper": "Lower and upper limits can't both be empty" + }, + "step": { + "user": { + "data": { + "entity_id": "Input sensor", + "hysteresis": "Hysteresis", + "lower": "Lower limit", + "name": "Name", + "upper": "Upper limit" + }, + "description": "Create a binary sensor that turns on and off depending on the value of a sensor\n\nOnly lower limit configured - Turn on when the input sensor's value is less than the lower limit.\nOnly upper limit configured - Turn on when the input sensor's value is greater than the upper limit.\nBoth lower and upper limit configured - Turn on when the input sensor's value is in the range [lower limit .. upper limit].", + "title": "Add Threshold Sensor" + } + } + }, + "options": { + "error": { + "need_lower_upper": "Lower and upper limits can't both be empty" + }, + "step": { + "init": { + "data": { + "entity_id": "Input sensor", + "hysteresis": "Hysteresis", + "lower": "Lower limit", + "name": "Name", + "upper": "Upper limit" + }, + "description": "Only lower limit configured - Turn on when the input sensor's value is less than the lower limit.\nOnly upper limit configured - Turn on when the input sensor's value is greater than the upper limit.\nBoth lower and upper limit configured - Turn on when the input sensor's value is in the range [lower limit .. upper limit]." + } + } + }, + "title": "Threshold Sensor" +} \ No newline at end of file diff --git a/homeassistant/components/tibber/manifest.json b/homeassistant/components/tibber/manifest.json index 9b84b9a54c2..598810b1a0a 100644 --- a/homeassistant/components/tibber/manifest.json +++ b/homeassistant/components/tibber/manifest.json @@ -3,7 +3,7 @@ "domain": "tibber", "name": "Tibber", "documentation": "https://www.home-assistant.io/integrations/tibber", - "requirements": ["pyTibber==0.22.1"], + "requirements": ["pyTibber==0.22.2"], "codeowners": ["@danielhiversen"], "quality_scale": "silver", "config_flow": true, diff --git a/homeassistant/components/tibber/sensor.py b/homeassistant/components/tibber/sensor.py index 12bcec295d0..bc3f6015286 100644 --- a/homeassistant/components/tibber/sensor.py +++ b/homeassistant/components/tibber/sensor.py @@ -8,6 +8,7 @@ from random import randrange import aiohttp +from homeassistant.components.recorder import get_instance from homeassistant.components.recorder.models import StatisticData, StatisticMetaData from homeassistant.components.recorder.statistics import ( async_add_external_statistics, @@ -32,11 +33,14 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import PlatformNotReady -from homeassistant.helpers import update_coordinator from homeassistant.helpers.device_registry import async_get as async_get_dev_reg from homeassistant.helpers.entity import DeviceInfo, EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_registry import async_get as async_get_entity_reg +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) from homeassistant.util import Throttle, dt as dt_util from .const import DOMAIN as TIBBER_DOMAIN, MANUFACTURER @@ -239,7 +243,7 @@ async def async_setup_entry( entity_registry = async_get_entity_reg(hass) device_registry = async_get_dev_reg(hass) - coordinator: update_coordinator.DataUpdateCoordinator | None = None + coordinator: TibberDataCoordinator | None = None entities: list[TibberSensor] = [] for home in tibber_connection.get_homes(only_active=False): try: @@ -392,13 +396,13 @@ class TibberSensorElPrice(TibberSensor): ]["estimatedAnnualConsumption"] -class TibberDataSensor(TibberSensor, update_coordinator.CoordinatorEntity): +class TibberDataSensor(TibberSensor, CoordinatorEntity["TibberDataCoordinator"]): """Representation of a Tibber sensor.""" def __init__( self, tibber_home, - coordinator: update_coordinator.DataUpdateCoordinator, + coordinator: TibberDataCoordinator, entity_description: SensorEntityDescription, ): """Initialize the sensor.""" @@ -420,7 +424,7 @@ class TibberDataSensor(TibberSensor, update_coordinator.CoordinatorEntity): return getattr(self._tibber_home, self.entity_description.key) -class TibberSensorRT(TibberSensor, update_coordinator.CoordinatorEntity): +class TibberSensorRT(TibberSensor, CoordinatorEntity["TibberRtDataCoordinator"]): """Representation of a Tibber sensor for real time consumption.""" def __init__( @@ -450,7 +454,7 @@ class TibberSensorRT(TibberSensor, update_coordinator.CoordinatorEntity): @callback def _handle_coordinator_update(self) -> None: - if not (live_measurement := self.coordinator.get_live_measurement()): # type: ignore[attr-defined] + if not (live_measurement := self.coordinator.get_live_measurement()): return state = live_measurement.get(self.entity_description.key) if state is None: @@ -479,7 +483,7 @@ class TibberSensorRT(TibberSensor, update_coordinator.CoordinatorEntity): self.async_write_ha_state() -class TibberRtDataCoordinator(update_coordinator.DataUpdateCoordinator): +class TibberRtDataCoordinator(DataUpdateCoordinator): """Handle Tibber realtime data.""" def __init__(self, async_add_entities, tibber_home, hass): @@ -538,7 +542,7 @@ class TibberRtDataCoordinator(update_coordinator.DataUpdateCoordinator): return self.data.get("data", {}).get("liveMeasurement") -class TibberDataCoordinator(update_coordinator.DataUpdateCoordinator): +class TibberDataCoordinator(DataUpdateCoordinator): """Handle Tibber data and insert statistics.""" def __init__(self, hass, tibber_connection): @@ -571,7 +575,7 @@ class TibberDataCoordinator(update_coordinator.DataUpdateCoordinator): f"{home.home_id.replace('-', '')}" ) - last_stats = await self.hass.async_add_executor_job( + last_stats = await get_instance(self.hass).async_add_executor_job( get_last_statistics, self.hass, 1, statistic_id, True ) @@ -591,7 +595,7 @@ class TibberDataCoordinator(update_coordinator.DataUpdateCoordinator): start = dt_util.parse_datetime( hourly_consumption_data[0]["from"] ) - timedelta(hours=1) - stat = await self.hass.async_add_executor_job( + stat = await get_instance(self.hass).async_add_executor_job( statistics_during_period, self.hass, start, diff --git a/homeassistant/components/tibber/strings.json b/homeassistant/components/tibber/strings.json index 34e218741ca..2876bf5bd02 100644 --- a/homeassistant/components/tibber/strings.json +++ b/homeassistant/components/tibber/strings.json @@ -13,8 +13,7 @@ "data": { "access_token": "[%key:common::config_flow::data::access_token%]" }, - "description": "Enter your access token from https://developer.tibber.com/settings/accesstoken", - "title": "Tibber" + "description": "Enter your access token from https://developer.tibber.com/settings/accesstoken" } } } diff --git a/homeassistant/components/tile/translations/fr.json b/homeassistant/components/tile/translations/fr.json index c71349619b9..c178454e48a 100644 --- a/homeassistant/components/tile/translations/fr.json +++ b/homeassistant/components/tile/translations/fr.json @@ -5,7 +5,7 @@ "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" }, "error": { - "invalid_auth": "Authentification invalide" + "invalid_auth": "Authentification non valide" }, "step": { "reauth_confirm": { @@ -17,7 +17,7 @@ "user": { "data": { "password": "Mot de passe", - "username": "Email" + "username": "Courriel" }, "title": "Configurer Tile" } diff --git a/homeassistant/components/timer/__init__.py b/homeassistant/components/timer/__init__.py index e4aa6be1ff1..e5564736c74 100644 --- a/homeassistant/components/timer/__init__.py +++ b/homeassistant/components/timer/__init__.py @@ -9,6 +9,7 @@ import voluptuous as vol from homeassistant.const import ( ATTR_EDITABLE, + ATTR_ENTITY_ID, CONF_ICON, CONF_ID, CONF_NAME, @@ -31,10 +32,16 @@ DOMAIN = "timer" ENTITY_ID_FORMAT = DOMAIN + ".{}" DEFAULT_DURATION = 0 +DEFAULT_RESTORE = False + ATTR_DURATION = "duration" ATTR_REMAINING = "remaining" ATTR_FINISHES_AT = "finishes_at" +ATTR_RESTORE = "restore" +ATTR_FINISHED_AT = "finished_at" + CONF_DURATION = "duration" +CONF_RESTORE = "restore" STATUS_IDLE = "idle" STATUS_ACTIVE = "active" @@ -55,15 +62,16 @@ STORAGE_KEY = DOMAIN STORAGE_VERSION = 1 CREATE_FIELDS = { - vol.Required(CONF_NAME): vol.All(str, vol.Length(min=1)), - vol.Optional(CONF_NAME): cv.string, + vol.Required(CONF_NAME): cv.string, vol.Optional(CONF_ICON): cv.icon, vol.Optional(CONF_DURATION, default=DEFAULT_DURATION): cv.time_period, + vol.Optional(CONF_RESTORE, default=DEFAULT_RESTORE): cv.boolean, } UPDATE_FIELDS = { vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_ICON): cv.icon, vol.Optional(CONF_DURATION): cv.time_period, + vol.Optional(CONF_RESTORE): cv.boolean, } @@ -91,6 +99,7 @@ CONFIG_SCHEMA = vol.Schema( vol.Optional(CONF_DURATION, default=DEFAULT_DURATION): vol.All( cv.time_period, _format_timedelta ), + vol.Optional(CONF_RESTORE, default=DEFAULT_RESTORE): cv.boolean, }, ) ) @@ -198,6 +207,10 @@ class Timer(RestoreEntity): self._remaining: timedelta | None = None self._end: datetime | None = None self._listener: Callable[[], None] | None = None + self._restore: bool = self._config.get(CONF_RESTORE, DEFAULT_RESTORE) + + self._attr_should_poll = False + self._attr_force_update = True @classmethod def from_yaml(cls, config: dict) -> Timer: @@ -207,16 +220,6 @@ class Timer(RestoreEntity): timer.editable = False return timer - @property - def should_poll(self): - """If entity should be polled.""" - return False - - @property - def force_update(self) -> bool: - """Return True to fix restart issues.""" - return True - @property def name(self): """Return name of the timer.""" @@ -243,6 +246,8 @@ class Timer(RestoreEntity): attrs[ATTR_FINISHES_AT] = self._end.isoformat() if self._remaining is not None: attrs[ATTR_REMAINING] = _format_timedelta(self._remaining) + if self._restore: + attrs[ATTR_RESTORE] = self._restore return attrs @@ -253,22 +258,45 @@ class Timer(RestoreEntity): async def async_added_to_hass(self): """Call when entity is about to be added to Home Assistant.""" - # If not None, we got an initial value. - if self._state is not None: + # If we don't need to restore a previous state or no previous state exists, + # start at idle + if not self._restore or (state := await self.async_get_last_state()) is None: + self._state = STATUS_IDLE return - state = await self.async_get_last_state() - self._state = state and state.state == state + # Begin restoring state + self._state = state.state + self._duration = cv.time_period(state.attributes[ATTR_DURATION]) + + # Nothing more to do if the timer is idle + if self._state == STATUS_IDLE: + return + + # If the timer was paused, we restore the remaining time + if self._state == STATUS_PAUSED: + self._remaining = cv.time_period(state.attributes[ATTR_REMAINING]) + return + # If we get here, the timer must have been active so we need to decide what + # to do based on end time and the current time + end = cv.datetime(state.attributes[ATTR_FINISHES_AT]) + # If there is time remaining in the timer, restore the remaining time then + # start the timer + if (remaining := end - dt_util.utcnow().replace(microsecond=0)) > timedelta(0): + self._remaining = remaining + self._state = STATUS_PAUSED + self.async_start() + # If the timer ended before now, finish the timer. The event will indicate + # when the timer was expected to fire. + else: + self._end = end + self.async_finish() @callback - def async_start(self, duration: timedelta): + def async_start(self, duration: timedelta | None = None): """Start a timer.""" if self._listener: self._listener() self._listener = None - newduration = None - if duration: - newduration = duration event = EVENT_TIMER_STARTED if self._state in (STATUS_ACTIVE, STATUS_PAUSED): @@ -277,19 +305,15 @@ class Timer(RestoreEntity): self._state = STATUS_ACTIVE start = dt_util.utcnow().replace(microsecond=0) - if self._remaining and newduration is None: - self._end = start + self._remaining - - elif newduration: - self._duration = newduration - self._remaining = newduration - self._end = start + self._duration - - else: + # Set remaining to new value if needed + if duration: + self._remaining = self._duration = duration + elif not self._remaining: self._remaining = self._duration - self._end = start + self._duration - self.hass.bus.async_fire(event, {"entity_id": self.entity_id}) + self._end = start + self._remaining + + self.hass.bus.async_fire(event, {ATTR_ENTITY_ID: self.entity_id}) self._listener = async_track_point_in_utc_time( self.hass, self._async_finished, self._end @@ -307,7 +331,7 @@ class Timer(RestoreEntity): self._remaining = self._end - dt_util.utcnow().replace(microsecond=0) self._state = STATUS_PAUSED self._end = None - self.hass.bus.async_fire(EVENT_TIMER_PAUSED, {"entity_id": self.entity_id}) + self.hass.bus.async_fire(EVENT_TIMER_PAUSED, {ATTR_ENTITY_ID: self.entity_id}) self.async_write_ha_state() @callback @@ -319,7 +343,9 @@ class Timer(RestoreEntity): self._state = STATUS_IDLE self._end = None self._remaining = None - self.hass.bus.async_fire(EVENT_TIMER_CANCELLED, {"entity_id": self.entity_id}) + self.hass.bus.async_fire( + EVENT_TIMER_CANCELLED, {ATTR_ENTITY_ID: self.entity_id} + ) self.async_write_ha_state() @callback @@ -331,10 +357,14 @@ class Timer(RestoreEntity): if self._listener: self._listener() self._listener = None + end = self._end self._state = STATUS_IDLE self._end = None self._remaining = None - self.hass.bus.async_fire(EVENT_TIMER_FINISHED, {"entity_id": self.entity_id}) + self.hass.bus.async_fire( + EVENT_TIMER_FINISHED, + {ATTR_ENTITY_ID: self.entity_id, ATTR_FINISHED_AT: end.isoformat()}, + ) self.async_write_ha_state() @callback @@ -345,13 +375,18 @@ class Timer(RestoreEntity): self._listener = None self._state = STATUS_IDLE + end = self._end self._end = None self._remaining = None - self.hass.bus.async_fire(EVENT_TIMER_FINISHED, {"entity_id": self.entity_id}) + self.hass.bus.async_fire( + EVENT_TIMER_FINISHED, + {ATTR_ENTITY_ID: self.entity_id, ATTR_FINISHED_AT: end.isoformat()}, + ) self.async_write_ha_state() async def async_update_config(self, config: dict) -> None: """Handle when the config is updated.""" self._config = config self._duration = cv.time_period_str(config[CONF_DURATION]) + self._restore = config.get(CONF_RESTORE, DEFAULT_RESTORE) self.async_write_ha_state() diff --git a/homeassistant/components/timer/translations/el.json b/homeassistant/components/timer/translations/el.json index ec4c4ab42e8..82d0c5c6b6e 100644 --- a/homeassistant/components/timer/translations/el.json +++ b/homeassistant/components/timer/translations/el.json @@ -1,9 +1,9 @@ { "state": { "_": { - "active": "\u03b5\u03bd\u03b5\u03c1\u03b3\u03cc", + "active": "\u0395\u03bd\u03b5\u03c1\u03b3\u03cc\u03c2", "idle": "\u03a3\u03b5 \u03b1\u03b4\u03c1\u03ac\u03bd\u03b5\u03b9\u03b1", - "paused": "\u03c3\u03b5 \u03c0\u03b1\u03cd\u03c3\u03b7" + "paused": "\u03a3\u03b5 \u03c0\u03b1\u03cd\u03c3\u03b7" } } } \ No newline at end of file diff --git a/homeassistant/components/timer/translations/fr.json b/homeassistant/components/timer/translations/fr.json index 6f9194f9bbf..fc43202fbfc 100644 --- a/homeassistant/components/timer/translations/fr.json +++ b/homeassistant/components/timer/translations/fr.json @@ -2,7 +2,7 @@ "state": { "_": { "active": "Actif", - "idle": "En veille", + "idle": "Inactif", "paused": "En pause" } } diff --git a/homeassistant/components/tod/__init__.py b/homeassistant/components/tod/__init__.py index fa15326becb..09038836e2e 100644 --- a/homeassistant/components/tod/__init__.py +++ b/homeassistant/components/tod/__init__.py @@ -1 +1,27 @@ -"""The tod component.""" +"""The Times of the Day integration.""" +from __future__ import annotations + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Times of the Day from a config entry.""" + hass.config_entries.async_setup_platforms(entry, (Platform.BINARY_SENSOR,)) + + entry.async_on_unload(entry.add_update_listener(config_entry_update_listener)) + + return True + + +async def config_entry_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Update listener, called when the config entry options are changed.""" + await hass.config_entries.async_reload(entry.entry_id) + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + return await hass.config_entries.async_unload_platforms( + entry, (Platform.BINARY_SENSOR,) + ) diff --git a/homeassistant/components/tod/binary_sensor.py b/homeassistant/components/tod/binary_sensor.py index 6f14d5735fb..1100892b876 100644 --- a/homeassistant/components/tod/binary_sensor.py +++ b/homeassistant/components/tod/binary_sensor.py @@ -8,6 +8,7 @@ import logging import voluptuous as vol from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_AFTER, CONF_BEFORE, @@ -22,15 +23,19 @@ from homeassistant.helpers.sun import get_astral_event_date, get_astral_event_ne from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util import dt as dt_util +from .const import ( + CONF_AFTER_OFFSET, + CONF_AFTER_TIME, + CONF_BEFORE_OFFSET, + CONF_BEFORE_TIME, +) + _LOGGER = logging.getLogger(__name__) ATTR_AFTER = "after" ATTR_BEFORE = "before" ATTR_NEXT_UPDATE = "next_update" -CONF_AFTER_OFFSET = "after_offset" -CONF_BEFORE_OFFSET = "before_offset" - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_AFTER): vol.Any(cv.time, vol.All(vol.Lower, cv.sun_event)), @@ -42,6 +47,28 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Initialize Times of the Day config entry.""" + if hass.config.time_zone is None: + _LOGGER.error("Timezone is not set in Home Assistant configuration") + return + + after = cv.time(config_entry.options[CONF_AFTER_TIME]) + after_offset = timedelta(0) + before = cv.time(config_entry.options[CONF_BEFORE_TIME]) + before_offset = timedelta(0) + name = config_entry.title + unique_id = config_entry.entry_id + + async_add_entities( + [TodSensor(name, after, after_offset, before, before_offset, unique_id)] + ) + + async def async_setup_platform( hass: HomeAssistant, config: ConfigType, @@ -58,12 +85,12 @@ async def async_setup_platform( before = config[CONF_BEFORE] before_offset = config[CONF_BEFORE_OFFSET] name = config[CONF_NAME] - sensor = TodSensor(name, after, after_offset, before, before_offset) + sensor = TodSensor(name, after, after_offset, before, before_offset, None) async_add_entities([sensor]) -def is_sun_event(sun_event): +def _is_sun_event(sun_event): """Return true if event is sun event not time.""" return sun_event in (SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET) @@ -71,8 +98,9 @@ def is_sun_event(sun_event): class TodSensor(BinarySensorEntity): """Time of the Day Sensor.""" - def __init__(self, name, after, after_offset, before, before_offset): + def __init__(self, name, after, after_offset, before, before_offset, unique_id): """Init the ToD Sensor...""" + self._attr_unique_id = unique_id self._name = name self._time_before = self._time_after = self._next_update = None self._after_offset = after_offset @@ -119,11 +147,11 @@ class TodSensor(BinarySensorEntity): # calculate utc datetime corresponding to local time return dt_util.as_utc(datetime.combine(current_local_date, naive_time)) - def _calculate_boudary_time(self): + def _calculate_boundary_time(self): """Calculate internal absolute time boundaries.""" nowutc = dt_util.utcnow() # If after value is a sun event instead of absolute time - if is_sun_event(self._after): + if _is_sun_event(self._after): # Calculate the today's event utc time or # if not available take next after_event_date = get_astral_event_date( @@ -139,7 +167,7 @@ class TodSensor(BinarySensorEntity): self._time_after = after_event_date # If before value is a sun event instead of absolute time - if is_sun_event(self._before): + if _is_sun_event(self._before): # Calculate the today's event utc time or if not available take # next before_event_date = get_astral_event_date( @@ -168,7 +196,7 @@ class TodSensor(BinarySensorEntity): # _time_after is set to 23:00 today # nowutc is set to 10:00 today if ( - not is_sun_event(self._after) + not _is_sun_event(self._after) and self._time_after > nowutc and self._time_before > nowutc + timedelta(days=1) ): @@ -182,7 +210,7 @@ class TodSensor(BinarySensorEntity): def _turn_to_next_day(self): """Turn to to the next day.""" - if is_sun_event(self._after): + if _is_sun_event(self._after): self._time_after = get_astral_event_next( self.hass, self._after, self._time_after - self._after_offset ) @@ -191,7 +219,7 @@ class TodSensor(BinarySensorEntity): # Offset is already there self._time_after += timedelta(days=1) - if is_sun_event(self._before): + if _is_sun_event(self._before): self._time_before = get_astral_event_next( self.hass, self._before, self._time_before - self._before_offset ) @@ -202,7 +230,7 @@ class TodSensor(BinarySensorEntity): async def async_added_to_hass(self): """Call when entity about to be added to Home Assistant.""" - self._calculate_boudary_time() + self._calculate_boundary_time() self._calculate_next_update() @callback diff --git a/homeassistant/components/tod/config_flow.py b/homeassistant/components/tod/config_flow.py new file mode 100644 index 00000000000..bd1712d1db5 --- /dev/null +++ b/homeassistant/components/tod/config_flow.py @@ -0,0 +1,49 @@ +"""Config flow for Times of the Day integration.""" +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, cast + +import voluptuous as vol + +from homeassistant.const import CONF_NAME +from homeassistant.helpers import selector +from homeassistant.helpers.schema_config_entry_flow import ( + SchemaConfigFlowHandler, + SchemaFlowFormStep, + SchemaFlowMenuStep, +) + +from .const import CONF_AFTER_TIME, CONF_BEFORE_TIME, DOMAIN + +OPTIONS_SCHEMA = vol.Schema( + { + vol.Required(CONF_AFTER_TIME): selector.selector({"time": {}}), + vol.Required(CONF_BEFORE_TIME): selector.selector({"time": {}}), + } +) + +CONFIG_SCHEMA = vol.Schema( + { + vol.Required(CONF_NAME): selector.selector({"text": {}}), + } +).extend(OPTIONS_SCHEMA.schema) + +CONFIG_FLOW: dict[str, SchemaFlowFormStep | SchemaFlowMenuStep] = { + "user": SchemaFlowFormStep(CONFIG_SCHEMA) +} + +OPTIONS_FLOW: dict[str, SchemaFlowFormStep | SchemaFlowMenuStep] = { + "init": SchemaFlowFormStep(OPTIONS_SCHEMA) +} + + +class ConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN): + """Handle a config or options flow for Times of the Day.""" + + config_flow = CONFIG_FLOW + options_flow = OPTIONS_FLOW + + def async_config_entry_title(self, options: Mapping[str, Any]) -> str: + """Return config entry title.""" + return cast(str, options["name"]) diff --git a/homeassistant/components/tod/const.py b/homeassistant/components/tod/const.py new file mode 100644 index 00000000000..3b6f8c23e17 --- /dev/null +++ b/homeassistant/components/tod/const.py @@ -0,0 +1,8 @@ +"""Constants for the Times of the Day integration.""" + +DOMAIN = "tod" + +CONF_AFTER_TIME = "after_time" +CONF_AFTER_OFFSET = "after_offset" +CONF_BEFORE_TIME = "before_time" +CONF_BEFORE_OFFSET = "before_offset" diff --git a/homeassistant/components/tod/manifest.json b/homeassistant/components/tod/manifest.json index b74465e05c3..8d3c0d4eab4 100644 --- a/homeassistant/components/tod/manifest.json +++ b/homeassistant/components/tod/manifest.json @@ -1,8 +1,10 @@ { "domain": "tod", + "integration_type": "helper", "name": "Times of the Day", "documentation": "https://www.home-assistant.io/integrations/tod", "codeowners": [], "quality_scale": "internal", - "iot_class": "local_push" + "iot_class": "local_push", + "config_flow": true } diff --git a/homeassistant/components/tod/strings.json b/homeassistant/components/tod/strings.json new file mode 100644 index 00000000000..41e40525081 --- /dev/null +++ b/homeassistant/components/tod/strings.json @@ -0,0 +1,26 @@ +{ + "title": "Times of the Day Sensor", + "config": { + "step": { + "user": { + "title": "Add Times of the Day Sensor", + "description": "Create a binary sensor that turns on or off depending on the time.", + "data": { + "after_time": "On time", + "before_time": "Off time", + "name": "Name" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "after_time": "[%key:component::tod::config::step::user::data::after_time%]", + "before_time": "[%key:component::tod::config::step::user::data::before_time%]" + } + } + } + } +} diff --git a/homeassistant/components/tod/translations/en.json b/homeassistant/components/tod/translations/en.json new file mode 100644 index 00000000000..2ecb2c695c8 --- /dev/null +++ b/homeassistant/components/tod/translations/en.json @@ -0,0 +1,26 @@ +{ + "config": { + "step": { + "user": { + "data": { + "after_time": "On time", + "before_time": "Off time", + "name": "Name" + }, + "description": "Create a binary sensor that turns on or off depending on the time.", + "title": "Add Times of the Day Sensor" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "after_time": "On time", + "before_time": "Off time" + } + } + } + }, + "title": "Times of the Day Sensor" +} \ No newline at end of file diff --git a/homeassistant/components/todoist/calendar.py b/homeassistant/components/todoist/calendar.py index 560ad3efe1f..4582b1de0f0 100644 --- a/homeassistant/components/todoist/calendar.py +++ b/homeassistant/components/todoist/calendar.py @@ -1,7 +1,7 @@ """Support for Todoist task management (https://todoist.com).""" from __future__ import annotations -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone import logging from todoist.api import TodoistAPI @@ -55,6 +55,7 @@ from .const import ( SUMMARY, TASKS, ) +from .types import DueDate _LOGGER = logging.getLogger(__name__) @@ -219,7 +220,7 @@ def setup_platform( due_date = datetime(due.year, due.month, due.day) # Format it in the manner Todoist expects due_date = dt.as_utc(due_date) - date_format = "%Y-%m-%dT%H:%M%S" + date_format = "%Y-%m-%dT%H:%M:%S" _due["date"] = datetime.strftime(due_date, date_format) if _due: @@ -258,15 +259,15 @@ def setup_platform( ) -def _parse_due_date(data: dict, gmt_string) -> datetime | None: - """Parse the due date dict into a datetime object.""" - # Add time information to date only strings. - if len(data["date"]) == 10: - return datetime.fromisoformat(data["date"]).replace(tzinfo=dt.UTC) +def _parse_due_date(data: DueDate, timezone_offset: int) -> datetime | None: + """Parse the due date dict into a datetime object in UTC. + + This function will always return a timezone aware datetime if it can be parsed. + """ if not (nowtime := dt.parse_datetime(data["date"])): return None if nowtime.tzinfo is None: - data["date"] += gmt_string + nowtime = nowtime.replace(tzinfo=timezone(timedelta(hours=timezone_offset))) return dt.as_utc(nowtime) @@ -441,7 +442,7 @@ class TodoistProjectData: task[START] = dt.utcnow() if data[DUE] is not None: task[END] = _parse_due_date( - data[DUE], self._api.state["user"]["tz_info"]["gmt_string"] + data[DUE], self._api.state["user"]["tz_info"]["hours"] ) if self._due_date_days is not None and ( @@ -564,8 +565,9 @@ class TodoistProjectData: for task in project_task_data: if task["due"] is None: continue + # @NOTE: _parse_due_date always returns the date in UTC time. due_date = _parse_due_date( - task["due"], self._api.state["user"]["tz_info"]["gmt_string"] + task["due"], self._api.state["user"]["tz_info"]["hours"] ) if not due_date: continue diff --git a/homeassistant/components/todoist/types.py b/homeassistant/components/todoist/types.py new file mode 100644 index 00000000000..b9409e44daa --- /dev/null +++ b/homeassistant/components/todoist/types.py @@ -0,0 +1,14 @@ +"""Types for the Todoist component.""" +from __future__ import annotations + +from typing import TypedDict + + +class DueDate(TypedDict): + """Dict representing a due date in a todoist api response.""" + + date: str + is_recurring: bool + lang: str + string: str + timezone: str | None diff --git a/homeassistant/components/tof/__init__.py b/homeassistant/components/tof/__init__.py deleted file mode 100644 index 0e72aca724b..00000000000 --- a/homeassistant/components/tof/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Platform for Time of Flight sensor VL53L1X from STMicroelectronics.""" diff --git a/homeassistant/components/tof/manifest.json b/homeassistant/components/tof/manifest.json deleted file mode 100644 index e530c67b930..00000000000 --- a/homeassistant/components/tof/manifest.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "domain": "tof", - "name": "Time of Flight", - "documentation": "https://www.home-assistant.io/integrations/tof", - "requirements": ["VL53L1X2==0.1.5"], - "dependencies": ["rpi_gpio"], - "codeowners": [], - "iot_class": "local_polling", - "loggers": ["VL53L1X2"] -} diff --git a/homeassistant/components/tof/sensor.py b/homeassistant/components/tof/sensor.py deleted file mode 100644 index 2934aa744aa..00000000000 --- a/homeassistant/components/tof/sensor.py +++ /dev/null @@ -1,120 +0,0 @@ -"""Platform for Time of Flight sensor VL53L1X from STMicroelectronics.""" -from __future__ import annotations - -import asyncio -from functools import partial -import logging - -from VL53L1X2 import VL53L1X # pylint: disable=import-error -import voluptuous as vol - -from homeassistant.components import rpi_gpio -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity -from homeassistant.const import CONF_NAME, LENGTH_MILLIMETERS -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 - -CONF_I2C_ADDRESS = "i2c_address" -CONF_I2C_BUS = "i2c_bus" -CONF_XSHUT = "xshut" - -DEFAULT_NAME = "VL53L1X" -DEFAULT_I2C_ADDRESS = 0x29 -DEFAULT_I2C_BUS = 1 -DEFAULT_XSHUT = 16 -DEFAULT_RANGE = 2 - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_I2C_ADDRESS, default=DEFAULT_I2C_ADDRESS): vol.Coerce(int), - vol.Optional(CONF_I2C_BUS, default=DEFAULT_I2C_BUS): vol.Coerce(int), - vol.Optional(CONF_XSHUT, default=DEFAULT_XSHUT): cv.positive_int, - } -) - -_LOGGER = logging.getLogger(__name__) - - -def init_tof_0(xshut, sensor): - """XSHUT port LOW resets the device.""" - sensor.open() - rpi_gpio.setup_output(xshut) - rpi_gpio.write_output(xshut, 0) - - -def init_tof_1(xshut): - """XSHUT port HIGH enables the device.""" - rpi_gpio.setup_output(xshut) - rpi_gpio.write_output(xshut, 1) - - -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Reset and initialize the VL53L1X ToF Sensor from STMicroelectronics.""" - _LOGGER.warning( - "The Time of Flight integration is deprecated and will be removed " - "in Home Assistant Core 2022.4; this integration is removed under " - "Architectural Decision Record 0019, more information can be found here: " - "https://github.com/home-assistant/architecture/blob/master/adr/0019-GPIO.md" - ) - - name = config.get(CONF_NAME) - bus_number = config.get(CONF_I2C_BUS) - i2c_address = config.get(CONF_I2C_ADDRESS) - unit = LENGTH_MILLIMETERS - xshut = config.get(CONF_XSHUT) - - sensor = await hass.async_add_executor_job(partial(VL53L1X, bus_number)) - await hass.async_add_executor_job(init_tof_0, xshut, sensor) - await asyncio.sleep(0.01) - await hass.async_add_executor_job(init_tof_1, xshut) - await asyncio.sleep(0.01) - - dev = [VL53L1XSensor(sensor, name, unit, i2c_address)] - - async_add_entities(dev, True) - - -class VL53L1XSensor(SensorEntity): - """Implementation of VL53L1X sensor.""" - - def __init__(self, vl53l1x_sensor, name, unit, i2c_address): - """Initialize the sensor.""" - self._name = name - self._unit_of_measurement = unit - self.vl53l1x_sensor = vl53l1x_sensor - self.i2c_address = i2c_address - self._state = None - self.init = True - - @property - def name(self) -> str: - """Return the name of the sensor.""" - return self._name - - @property - def native_value(self) -> int: - """Return the state of the sensor.""" - return self._state - - @property - def native_unit_of_measurement(self) -> str: - """Return the unit of measurement.""" - return self._unit_of_measurement - - def update(self): - """Get the latest measurement and update state.""" - if self.init: - self.vl53l1x_sensor.add_sensor(self.i2c_address, self.i2c_address) - self.init = False - self.vl53l1x_sensor.start_ranging(self.i2c_address, DEFAULT_RANGE) - self.vl53l1x_sensor.update(self.i2c_address) - self.vl53l1x_sensor.stop_ranging(self.i2c_address) - self._state = self.vl53l1x_sensor.distance diff --git a/homeassistant/components/tolo/__init__.py b/homeassistant/components/tolo/__init__.py index 6f17379dfbf..3b78adacbac 100644 --- a/homeassistant/components/tolo/__init__.py +++ b/homeassistant/components/tolo/__init__.py @@ -92,11 +92,9 @@ class ToloSaunaUpdateCoordinator(DataUpdateCoordinator[ToloSaunaData]): return ToloSaunaData(status, settings) -class ToloSaunaCoordinatorEntity(CoordinatorEntity): +class ToloSaunaCoordinatorEntity(CoordinatorEntity[ToloSaunaUpdateCoordinator]): """CoordinatorEntity for TOLO Sauna.""" - coordinator: ToloSaunaUpdateCoordinator - def __init__( self, coordinator: ToloSaunaUpdateCoordinator, entry: ConfigEntry ) -> None: diff --git a/homeassistant/components/tolo/fan.py b/homeassistant/components/tolo/fan.py index 499a348bd0b..e767be9a3ce 100644 --- a/homeassistant/components/tolo/fan.py +++ b/homeassistant/components/tolo/fan.py @@ -43,7 +43,6 @@ class ToloFan(ToloSaunaCoordinatorEntity, FanEntity): def turn_on( self, - speed: str | None = None, percentage: int | None = None, preset_mode: str | None = None, **kwargs: Any, diff --git a/homeassistant/components/tolo/manifest.json b/homeassistant/components/tolo/manifest.json index aa60958591c..ba208a050a5 100644 --- a/homeassistant/components/tolo/manifest.json +++ b/homeassistant/components/tolo/manifest.json @@ -3,13 +3,9 @@ "name": "TOLO Sauna", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tolo", - "requirements": [ - "tololib==0.1.0b3" - ], - "codeowners": [ - "@MatthiasLohr" - ], + "requirements": ["tololib==0.1.0b3"], + "codeowners": ["@MatthiasLohr"], "iot_class": "local_polling", - "dhcp": [{"hostname": "usr-tcp232-ed2"}], + "dhcp": [{ "hostname": "usr-tcp232-ed2" }], "loggers": ["tololib"] -} \ No newline at end of file +} diff --git a/homeassistant/components/tolo/strings.json b/homeassistant/components/tolo/strings.json index 0a81dad73f6..7a8ed22da16 100644 --- a/homeassistant/components/tolo/strings.json +++ b/homeassistant/components/tolo/strings.json @@ -19,4 +19,4 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } } -} \ No newline at end of file +} diff --git a/homeassistant/components/tolo/strings.select.json b/homeassistant/components/tolo/strings.select.json index c65caaf5d2d..72a7a2c00e2 100644 --- a/homeassistant/components/tolo/strings.select.json +++ b/homeassistant/components/tolo/strings.select.json @@ -5,4 +5,4 @@ "manual": "manual" } } -} \ No newline at end of file +} diff --git a/homeassistant/components/tolo/translations/fr.json b/homeassistant/components/tolo/translations/fr.json index 95951419dc4..40b61d012a2 100644 --- a/homeassistant/components/tolo/translations/fr.json +++ b/homeassistant/components/tolo/translations/fr.json @@ -10,7 +10,7 @@ "flow_title": "{name}", "step": { "confirm": { - "description": "Voulez-vous commencer la configuration ?" + "description": "Voulez-vous commencer la configuration\u00a0?" }, "user": { "data": { diff --git a/homeassistant/components/tomorrowio/__init__.py b/homeassistant/components/tomorrowio/__init__.py new file mode 100644 index 00000000000..f55ea4c16da --- /dev/null +++ b/homeassistant/components/tomorrowio/__init__.py @@ -0,0 +1,357 @@ +"""The Tomorrow.io integration.""" +from __future__ import annotations + +from datetime import timedelta +import logging +from math import ceil +from typing import Any + +from pytomorrowio import TomorrowioV4 +from pytomorrowio.const import CURRENT, FORECASTS +from pytomorrowio.exceptions import ( + CantConnectException, + InvalidAPIKeyException, + RateLimitedException, + UnknownException, +) + +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.components.weather import DOMAIN as WEATHER_DOMAIN +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.const import ( + CONF_API_KEY, + CONF_LATITUDE, + CONF_LOCATION, + CONF_LONGITUDE, + CONF_NAME, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, + UpdateFailed, +) + +from .const import ( + ATTRIBUTION, + CONF_TIMESTEP, + DOMAIN, + INTEGRATION_NAME, + MAX_REQUESTS_PER_DAY, + TMRW_ATTR_CARBON_MONOXIDE, + TMRW_ATTR_CHINA_AQI, + TMRW_ATTR_CHINA_HEALTH_CONCERN, + TMRW_ATTR_CHINA_PRIMARY_POLLUTANT, + TMRW_ATTR_CLOUD_BASE, + TMRW_ATTR_CLOUD_CEILING, + TMRW_ATTR_CLOUD_COVER, + TMRW_ATTR_CONDITION, + TMRW_ATTR_DEW_POINT, + TMRW_ATTR_EPA_AQI, + TMRW_ATTR_EPA_HEALTH_CONCERN, + TMRW_ATTR_EPA_PRIMARY_POLLUTANT, + TMRW_ATTR_FEELS_LIKE, + TMRW_ATTR_FIRE_INDEX, + TMRW_ATTR_HUMIDITY, + TMRW_ATTR_NITROGEN_DIOXIDE, + TMRW_ATTR_OZONE, + TMRW_ATTR_PARTICULATE_MATTER_10, + TMRW_ATTR_PARTICULATE_MATTER_25, + TMRW_ATTR_POLLEN_GRASS, + TMRW_ATTR_POLLEN_TREE, + TMRW_ATTR_POLLEN_WEED, + TMRW_ATTR_PRECIPITATION, + TMRW_ATTR_PRECIPITATION_PROBABILITY, + TMRW_ATTR_PRECIPITATION_TYPE, + TMRW_ATTR_PRESSURE, + TMRW_ATTR_PRESSURE_SURFACE_LEVEL, + TMRW_ATTR_SOLAR_GHI, + TMRW_ATTR_SULPHUR_DIOXIDE, + TMRW_ATTR_TEMPERATURE, + TMRW_ATTR_TEMPERATURE_HIGH, + TMRW_ATTR_TEMPERATURE_LOW, + TMRW_ATTR_VISIBILITY, + TMRW_ATTR_WIND_DIRECTION, + TMRW_ATTR_WIND_GUST, + TMRW_ATTR_WIND_SPEED, +) + +_LOGGER = logging.getLogger(__name__) + +PLATFORMS = [SENSOR_DOMAIN, WEATHER_DOMAIN] + + +def _set_update_interval(hass: HomeAssistant, current_entry: ConfigEntry) -> timedelta: + """Recalculate update_interval based on existing Tomorrow.io instances and update them.""" + api_calls = 2 + # We check how many Tomorrow.io configured instances are using the same API key and + # calculate interval to not exceed allowed numbers of requests. Divide 90% of + # MAX_REQUESTS_PER_DAY by the number of API calls because we want a buffer in the + # number of API calls left at the end of the day. + other_instance_entry_ids = [ + entry.entry_id + for entry in hass.config_entries.async_entries(DOMAIN) + if entry.entry_id != current_entry.entry_id + and entry.data[CONF_API_KEY] == current_entry.data[CONF_API_KEY] + ] + + interval = timedelta( + minutes=( + ceil( + (24 * 60 * (len(other_instance_entry_ids) + 1) * api_calls) + / (MAX_REQUESTS_PER_DAY * 0.9) + ) + ) + ) + + for entry_id in other_instance_entry_ids: + if entry_id in hass.data[DOMAIN]: + hass.data[DOMAIN][entry_id].update_interval = interval + + return interval + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Tomorrow.io API from a config entry.""" + hass.data.setdefault(DOMAIN, {}) + + # Let's precreate the device so that if this is a first time setup for a config + # entry imported from a ClimaCell entry, we can apply customizations from the old + # device. + dev_reg = dr.async_get(hass) + device = dev_reg.async_get_or_create( + config_entry_id=entry.entry_id, + identifiers={(DOMAIN, entry.data[CONF_API_KEY])}, + name=INTEGRATION_NAME, + manufacturer=INTEGRATION_NAME, + sw_version="v4", + entry_type=dr.DeviceEntryType.SERVICE, + ) + + # If this is an import and we still have the old config entry ID in the entry data, + # it means we are setting this entry up for the first time after a migration from + # ClimaCell to Tomorrow.io. In order to preserve any customizations on the ClimaCell + # entities, we need to remove each old entity, creating a new entity in its place + # but attached to this entry. + if entry.source == SOURCE_IMPORT and "old_config_entry_id" in entry.data: + # Remove the old config entry ID from the entry data so we don't try this again + # on the next setup + data = entry.data.copy() + old_config_entry_id = data.pop("old_config_entry_id") + hass.config_entries.async_update_entry(entry, data=data) + _LOGGER.debug( + ( + "Setting up imported climacell entry %s for the first time as " + "tomorrowio entry %s" + ), + old_config_entry_id, + entry.entry_id, + ) + + ent_reg = er.async_get(hass) + for entity_entry in er.async_entries_for_config_entry( + ent_reg, old_config_entry_id + ): + _LOGGER.debug("Removing %s", entity_entry.entity_id) + ent_reg.async_remove(entity_entry.entity_id) + # In case the API key has changed due to a V3 -> V4 change, we need to + # generate the new entity's unique ID + new_unique_id = ( + f"{entry.data[CONF_API_KEY]}_" + f"{'_'.join(entity_entry.unique_id.split('_')[1:])}" + ) + _LOGGER.debug( + "Re-creating %s for the new config entry", entity_entry.entity_id + ) + # We will precreate the entity so that any customizations can be preserved + new_entity_entry = ent_reg.async_get_or_create( + entity_entry.domain, + DOMAIN, + new_unique_id, + suggested_object_id=entity_entry.entity_id.split(".")[1], + disabled_by=entity_entry.disabled_by, + config_entry=entry, + original_name=entity_entry.original_name, + original_icon=entity_entry.original_icon, + ) + _LOGGER.debug("Re-created %s", new_entity_entry.entity_id) + # If there are customizations on the old entity, apply them to the new one + if entity_entry.name or entity_entry.icon: + ent_reg.async_update_entity( + new_entity_entry.entity_id, + name=entity_entry.name, + icon=entity_entry.icon, + ) + + # We only have one device in the registry but we will do a loop just in case + for old_device in dr.async_entries_for_config_entry( + dev_reg, old_config_entry_id + ): + if old_device.name_by_user: + dev_reg.async_update_device( + device.id, name_by_user=old_device.name_by_user + ) + + # Remove the old config entry and now the entry is fully migrated + hass.async_create_task(hass.config_entries.async_remove(old_config_entry_id)) + + api = TomorrowioV4( + entry.data[CONF_API_KEY], + entry.data[CONF_LOCATION][CONF_LATITUDE], + entry.data[CONF_LOCATION][CONF_LONGITUDE], + session=async_get_clientsession(hass), + ) + + coordinator = TomorrowioDataUpdateCoordinator( + hass, + entry, + api, + _set_update_interval(hass, entry), + ) + + await coordinator.async_config_entry_first_refresh() + + hass.data[DOMAIN][entry.entry_id] = coordinator + + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + """Unload a config entry.""" + unload_ok = await hass.config_entries.async_unload_platforms( + config_entry, PLATFORMS + ) + + hass.data[DOMAIN].pop(config_entry.entry_id) + if not hass.data[DOMAIN]: + hass.data.pop(DOMAIN) + + return unload_ok + + +class TomorrowioDataUpdateCoordinator(DataUpdateCoordinator): + """Define an object to hold Tomorrow.io data.""" + + def __init__( + self, + hass: HomeAssistant, + config_entry: ConfigEntry, + api: TomorrowioV4, + update_interval: timedelta, + ) -> None: + """Initialize.""" + + self._config_entry = config_entry + self._api = api + self.name = config_entry.data[CONF_NAME] + self.data = {CURRENT: {}, FORECASTS: {}} + + super().__init__( + hass, + _LOGGER, + name=config_entry.data[CONF_NAME], + update_interval=update_interval, + ) + + async def _async_update_data(self) -> dict[str, Any]: + """Update data via library.""" + try: + return await self._api.realtime_and_all_forecasts( + [ + TMRW_ATTR_TEMPERATURE, + TMRW_ATTR_HUMIDITY, + TMRW_ATTR_PRESSURE, + TMRW_ATTR_WIND_SPEED, + TMRW_ATTR_WIND_DIRECTION, + TMRW_ATTR_CONDITION, + TMRW_ATTR_VISIBILITY, + TMRW_ATTR_OZONE, + TMRW_ATTR_WIND_GUST, + TMRW_ATTR_CLOUD_COVER, + TMRW_ATTR_PRECIPITATION_TYPE, + *( + TMRW_ATTR_CARBON_MONOXIDE, + TMRW_ATTR_CHINA_AQI, + TMRW_ATTR_CHINA_HEALTH_CONCERN, + TMRW_ATTR_CHINA_PRIMARY_POLLUTANT, + TMRW_ATTR_CLOUD_BASE, + TMRW_ATTR_CLOUD_CEILING, + TMRW_ATTR_CLOUD_COVER, + TMRW_ATTR_DEW_POINT, + TMRW_ATTR_EPA_AQI, + TMRW_ATTR_EPA_HEALTH_CONCERN, + TMRW_ATTR_EPA_PRIMARY_POLLUTANT, + TMRW_ATTR_FEELS_LIKE, + TMRW_ATTR_FIRE_INDEX, + TMRW_ATTR_NITROGEN_DIOXIDE, + TMRW_ATTR_OZONE, + TMRW_ATTR_PARTICULATE_MATTER_10, + TMRW_ATTR_PARTICULATE_MATTER_25, + TMRW_ATTR_POLLEN_GRASS, + TMRW_ATTR_POLLEN_TREE, + TMRW_ATTR_POLLEN_WEED, + TMRW_ATTR_PRECIPITATION_TYPE, + TMRW_ATTR_PRESSURE_SURFACE_LEVEL, + TMRW_ATTR_SOLAR_GHI, + TMRW_ATTR_SULPHUR_DIOXIDE, + TMRW_ATTR_WIND_GUST, + ), + ], + [ + TMRW_ATTR_TEMPERATURE_LOW, + TMRW_ATTR_TEMPERATURE_HIGH, + TMRW_ATTR_WIND_SPEED, + TMRW_ATTR_WIND_DIRECTION, + TMRW_ATTR_CONDITION, + TMRW_ATTR_PRECIPITATION, + TMRW_ATTR_PRECIPITATION_PROBABILITY, + ], + nowcast_timestep=self._config_entry.options[CONF_TIMESTEP], + ) + except ( + CantConnectException, + InvalidAPIKeyException, + RateLimitedException, + UnknownException, + ) as error: + raise UpdateFailed from error + + +class TomorrowioEntity(CoordinatorEntity[TomorrowioDataUpdateCoordinator]): + """Base Tomorrow.io Entity.""" + + def __init__( + self, + config_entry: ConfigEntry, + coordinator: TomorrowioDataUpdateCoordinator, + api_version: int, + ) -> None: + """Initialize Tomorrow.io Entity.""" + super().__init__(coordinator) + self.api_version = api_version + self._config_entry = config_entry + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, self._config_entry.data[CONF_API_KEY])}, + name="Tomorrow.io", + manufacturer="Tomorrow.io", + sw_version=f"v{self.api_version}", + entry_type=dr.DeviceEntryType.SERVICE, + ) + + def _get_current_property(self, property_name: str) -> int | str | float | None: + """ + Get property from current conditions. + + Used for V4 API. + """ + return self.coordinator.data.get(CURRENT, {}).get(property_name) + + @property + def attribution(self): + """Return the attribution.""" + return ATTRIBUTION diff --git a/homeassistant/components/tomorrowio/config_flow.py b/homeassistant/components/tomorrowio/config_flow.py new file mode 100644 index 00000000000..bde49ed3fe5 --- /dev/null +++ b/homeassistant/components/tomorrowio/config_flow.py @@ -0,0 +1,226 @@ +"""Config flow for Tomorrow.io integration.""" +from __future__ import annotations + +from collections.abc import Mapping +import logging +from typing import Any + +from pytomorrowio.exceptions import ( + CantConnectException, + InvalidAPIKeyException, + RateLimitedException, +) +from pytomorrowio.pytomorrowio import TomorrowioV4 +import voluptuous as vol + +from homeassistant import config_entries, core +from homeassistant.components.zone import async_active_zone +from homeassistant.const import ( + CONF_API_KEY, + CONF_API_VERSION, + CONF_FRIENDLY_NAME, + CONF_LATITUDE, + CONF_LOCATION, + CONF_LONGITUDE, + CONF_NAME, +) +from homeassistant.core import HomeAssistant, callback +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.selector import selector + +from .const import ( + AUTO_MIGRATION_MESSAGE, + CC_DOMAIN, + CONF_TIMESTEP, + DEFAULT_NAME, + DEFAULT_TIMESTEP, + DOMAIN, + INTEGRATION_NAME, + MANUAL_MIGRATION_MESSAGE, + TMRW_ATTR_TEMPERATURE, +) + +_LOGGER = logging.getLogger(__name__) + + +def _get_config_schema( + hass: core.HomeAssistant, source: str | None, input_dict: dict[str, Any] = None +) -> vol.Schema: + """ + Return schema defaults for init step based on user input/config dict. + + Retain info already provided for future form views by setting them as + defaults in schema. + """ + if input_dict is None: + input_dict = {} + + api_key_schema = { + vol.Required(CONF_API_KEY, default=input_dict.get(CONF_API_KEY)): str, + } + + # For imports we just need to ask for the API key + if source == config_entries.SOURCE_IMPORT: + return vol.Schema(api_key_schema, extra=vol.REMOVE_EXTRA) + + default_location = ( + input_dict[CONF_LOCATION] + if CONF_LOCATION in input_dict + else { + CONF_LATITUDE: hass.config.latitude, + CONF_LONGITUDE: hass.config.longitude, + } + ) + return vol.Schema( + { + **api_key_schema, + vol.Required( + CONF_LOCATION, + default=default_location, + ): selector({"location": {"radius": False}}), + }, + ) + + +def _get_unique_id(hass: HomeAssistant, input_dict: dict[str, Any]): + """Return unique ID from config data.""" + return ( + f"{input_dict[CONF_API_KEY]}" + f"_{input_dict[CONF_LOCATION][CONF_LATITUDE]}" + f"_{input_dict[CONF_LOCATION][CONF_LONGITUDE]}" + ) + + +class TomorrowioOptionsConfigFlow(config_entries.OptionsFlow): + """Handle Tomorrow.io options.""" + + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + """Initialize Tomorrow.io options flow.""" + self._config_entry = config_entry + + async def async_step_init(self, user_input: dict[str, Any] = None) -> FlowResult: + """Manage the Tomorrow.io options.""" + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + + options_schema = { + vol.Required( + CONF_TIMESTEP, + default=self._config_entry.options[CONF_TIMESTEP], + ): vol.In([1, 5, 15, 30]), + } + + return self.async_show_form( + step_id="init", data_schema=vol.Schema(options_schema) + ) + + +class TomorrowioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Tomorrow.io Weather API.""" + + VERSION = 1 + + def __init__(self) -> None: + """Initialize config flow.""" + self._showed_import_message = 0 + self._import_config: dict[str, Any] | None = None + + @staticmethod + @callback + def async_get_options_flow( + config_entry: config_entries.ConfigEntry, + ) -> TomorrowioOptionsConfigFlow: + """Get the options flow for this handler.""" + return TomorrowioOptionsConfigFlow(config_entry) + + async def async_step_user(self, user_input: dict[str, Any] = None) -> FlowResult: + """Handle the initial step.""" + errors = {} + if user_input is not None: + # Grab the API key and add it to the rest of the config before continuing + if self._import_config: + self._import_config[CONF_API_KEY] = user_input[CONF_API_KEY] + self._import_config[CONF_LOCATION] = { + CONF_LATITUDE: self._import_config.pop( + CONF_LATITUDE, self.hass.config.latitude + ), + CONF_LONGITUDE: self._import_config.pop( + CONF_LONGITUDE, self.hass.config.longitude + ), + } + user_input = self._import_config.copy() + await self.async_set_unique_id( + unique_id=_get_unique_id(self.hass, user_input) + ) + self._abort_if_unique_id_configured() + + location = user_input[CONF_LOCATION] + latitude = location[CONF_LATITUDE] + longitude = location[CONF_LONGITUDE] + if CONF_NAME not in user_input: + user_input[CONF_NAME] = DEFAULT_NAME + # Append zone name if it exists and we are using the default name + if zone_state := async_active_zone(self.hass, latitude, longitude): + zone_name = zone_state.attributes[CONF_FRIENDLY_NAME] + user_input[CONF_NAME] += f" - {zone_name}" + try: + await TomorrowioV4( + user_input[CONF_API_KEY], + str(latitude), + str(longitude), + session=async_get_clientsession(self.hass), + ).realtime([TMRW_ATTR_TEMPERATURE]) + except CantConnectException: + errors["base"] = "cannot_connect" + except InvalidAPIKeyException: + errors[CONF_API_KEY] = "invalid_api_key" + except RateLimitedException: + errors[CONF_API_KEY] = "rate_limited" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + + if not errors: + options: Mapping[str, Any] = {CONF_TIMESTEP: DEFAULT_TIMESTEP} + # Store the old config entry ID and retrieve options to recreate the entry + if self.source == config_entries.SOURCE_IMPORT: + old_config_entry_id = self.context["old_config_entry_id"] + old_config_entry = self.hass.config_entries.async_get_entry( + old_config_entry_id + ) + assert old_config_entry + options = dict(old_config_entry.options) + user_input["old_config_entry_id"] = old_config_entry_id + return self.async_create_entry( + title=user_input[CONF_NAME], + data=user_input, + options=options, + ) + + return self.async_show_form( + step_id="user", + data_schema=_get_config_schema(self.hass, self.source, user_input), + errors=errors, + ) + + async def async_step_import(self, import_config: dict) -> FlowResult: + """Import from config.""" + # Store import config for later + self._import_config = dict(import_config) + if self._import_config.pop(CONF_API_VERSION, 3) == 3: + # Clear API key from import config + self._import_config[CONF_API_KEY] = "" + self.hass.components.persistent_notification.async_create( + MANUAL_MIGRATION_MESSAGE, + INTEGRATION_NAME, + f"{CC_DOMAIN}_to_{DOMAIN}_new_api_key_needed", + ) + return await self.async_step_user() + + self.hass.components.persistent_notification.async_create( + AUTO_MIGRATION_MESSAGE, + INTEGRATION_NAME, + f"{CC_DOMAIN}_to_{DOMAIN}", + ) + return await self.async_step_user(self._import_config) diff --git a/homeassistant/components/tomorrowio/const.py b/homeassistant/components/tomorrowio/const.py new file mode 100644 index 00000000000..5f49700e511 --- /dev/null +++ b/homeassistant/components/tomorrowio/const.py @@ -0,0 +1,143 @@ +"""Constants for the Tomorrow.io integration.""" +from __future__ import annotations + +from pytomorrowio.const import DAILY, HOURLY, NOWCAST, WeatherCode + +from homeassistant.components.weather import ( + ATTR_CONDITION_CLEAR_NIGHT, + ATTR_CONDITION_CLOUDY, + ATTR_CONDITION_FOG, + ATTR_CONDITION_HAIL, + ATTR_CONDITION_LIGHTNING, + ATTR_CONDITION_PARTLYCLOUDY, + ATTR_CONDITION_POURING, + ATTR_CONDITION_RAINY, + ATTR_CONDITION_SNOWY, + ATTR_CONDITION_SNOWY_RAINY, + ATTR_CONDITION_SUNNY, + ATTR_CONDITION_WINDY, +) + +CONF_TIMESTEP = "timestep" +FORECAST_TYPES = [DAILY, HOURLY, NOWCAST] + +DEFAULT_TIMESTEP = 15 +DEFAULT_FORECAST_TYPE = DAILY +CC_DOMAIN = "climacell" +DOMAIN = "tomorrowio" +INTEGRATION_NAME = "Tomorrow.io" +DEFAULT_NAME = INTEGRATION_NAME +ATTRIBUTION = "Powered by Tomorrow.io" + +MAX_REQUESTS_PER_DAY = 500 + +CLEAR_CONDITIONS = {"night": ATTR_CONDITION_CLEAR_NIGHT, "day": ATTR_CONDITION_SUNNY} + +MAX_FORECASTS = { + DAILY: 14, + HOURLY: 24, + NOWCAST: 30, +} + +# Additional attributes +ATTR_WIND_GUST = "wind_gust" +ATTR_CLOUD_COVER = "cloud_cover" +ATTR_PRECIPITATION_TYPE = "precipitation_type" + +# V4 constants +CONDITIONS = { + WeatherCode.WIND: ATTR_CONDITION_WINDY, + WeatherCode.LIGHT_WIND: ATTR_CONDITION_WINDY, + WeatherCode.STRONG_WIND: ATTR_CONDITION_WINDY, + WeatherCode.FREEZING_RAIN: ATTR_CONDITION_SNOWY_RAINY, + WeatherCode.HEAVY_FREEZING_RAIN: ATTR_CONDITION_SNOWY_RAINY, + WeatherCode.LIGHT_FREEZING_RAIN: ATTR_CONDITION_SNOWY_RAINY, + WeatherCode.FREEZING_DRIZZLE: ATTR_CONDITION_SNOWY_RAINY, + WeatherCode.ICE_PELLETS: ATTR_CONDITION_HAIL, + WeatherCode.HEAVY_ICE_PELLETS: ATTR_CONDITION_HAIL, + WeatherCode.LIGHT_ICE_PELLETS: ATTR_CONDITION_HAIL, + WeatherCode.SNOW: ATTR_CONDITION_SNOWY, + WeatherCode.HEAVY_SNOW: ATTR_CONDITION_SNOWY, + WeatherCode.LIGHT_SNOW: ATTR_CONDITION_SNOWY, + WeatherCode.FLURRIES: ATTR_CONDITION_SNOWY, + WeatherCode.THUNDERSTORM: ATTR_CONDITION_LIGHTNING, + WeatherCode.RAIN: ATTR_CONDITION_POURING, + WeatherCode.HEAVY_RAIN: ATTR_CONDITION_RAINY, + WeatherCode.LIGHT_RAIN: ATTR_CONDITION_RAINY, + WeatherCode.DRIZZLE: ATTR_CONDITION_RAINY, + WeatherCode.FOG: ATTR_CONDITION_FOG, + WeatherCode.LIGHT_FOG: ATTR_CONDITION_FOG, + WeatherCode.CLOUDY: ATTR_CONDITION_CLOUDY, + WeatherCode.MOSTLY_CLOUDY: ATTR_CONDITION_CLOUDY, + WeatherCode.PARTLY_CLOUDY: ATTR_CONDITION_PARTLYCLOUDY, +} + +# Weather constants +TMRW_ATTR_TIMESTAMP = "startTime" +TMRW_ATTR_TEMPERATURE = "temperature" +TMRW_ATTR_TEMPERATURE_HIGH = "temperatureMax" +TMRW_ATTR_TEMPERATURE_LOW = "temperatureMin" +TMRW_ATTR_PRESSURE = "pressureSeaLevel" +TMRW_ATTR_HUMIDITY = "humidity" +TMRW_ATTR_WIND_SPEED = "windSpeed" +TMRW_ATTR_WIND_DIRECTION = "windDirection" +TMRW_ATTR_OZONE = "pollutantO3" +TMRW_ATTR_CONDITION = "weatherCode" +TMRW_ATTR_VISIBILITY = "visibility" +TMRW_ATTR_PRECIPITATION = "precipitationIntensityAvg" +TMRW_ATTR_PRECIPITATION_PROBABILITY = "precipitationProbability" +TMRW_ATTR_WIND_GUST = "windGust" +TMRW_ATTR_CLOUD_COVER = "cloudCover" +TMRW_ATTR_PRECIPITATION_TYPE = "precipitationType" + +# Sensor attributes +TMRW_ATTR_PARTICULATE_MATTER_25 = "particulateMatter25" +TMRW_ATTR_PARTICULATE_MATTER_10 = "particulateMatter10" +TMRW_ATTR_NITROGEN_DIOXIDE = "pollutantNO2" +TMRW_ATTR_CARBON_MONOXIDE = "pollutantCO" +TMRW_ATTR_SULPHUR_DIOXIDE = "pollutantSO2" +TMRW_ATTR_EPA_AQI = "epaIndex" +TMRW_ATTR_EPA_PRIMARY_POLLUTANT = "epaPrimaryPollutant" +TMRW_ATTR_EPA_HEALTH_CONCERN = "epaHealthConcern" +TMRW_ATTR_CHINA_AQI = "mepIndex" +TMRW_ATTR_CHINA_PRIMARY_POLLUTANT = "mepPrimaryPollutant" +TMRW_ATTR_CHINA_HEALTH_CONCERN = "mepHealthConcern" +TMRW_ATTR_POLLEN_TREE = "treeIndex" +TMRW_ATTR_POLLEN_WEED = "weedIndex" +TMRW_ATTR_POLLEN_GRASS = "grassIndex" +TMRW_ATTR_FIRE_INDEX = "fireIndex" +TMRW_ATTR_FEELS_LIKE = "temperatureApparent" +TMRW_ATTR_DEW_POINT = "dewPoint" +TMRW_ATTR_PRESSURE_SURFACE_LEVEL = "pressureSurfaceLevel" +TMRW_ATTR_SOLAR_GHI = "solarGHI" +TMRW_ATTR_CLOUD_BASE = "cloudBase" +TMRW_ATTR_CLOUD_CEILING = "cloudCeiling" + +MANUAL_MIGRATION_MESSAGE = ( + "As part of [ClimaCell's rebranding to Tomorrow.io](https://www.tomorrow.io/blog/my-last-day-as-ceo-of-climacell/) " + "we will migrate your existing ClimaCell config entry (or config " + "entries) to the new Tomorrow.io integration, but because **the " + " V3 API is now deprecated**, you will need to get a new V4 API " + "key from [Tomorrow.io](https://app.tomorrow.io/development/keys)." + " Once that is done, visit the " + "[Integrations Configuration](/config/integrations) page and " + "click Configure on the Tomorrow.io card(s) to submit the new " + "key. Once your key has been validated, your config entry will " + "automatically be migrated. The new integration is a drop in " + "replacement and your existing entities will be migrated over, " + "just note that the location of the integration card on the " + "[Integrations Configuration](/config/integrations) page has changed " + "since the integration name has changed." +) + +AUTO_MIGRATION_MESSAGE = ( + "As part of [ClimaCell's rebranding to Tomorrow.io](https://www.tomorrow.io/blog/my-last-day-as-ceo-of-climacell/) " + "we have automatically migrated your existing ClimaCell config entry " + "(or as many of your ClimaCell config entries as we could) to the new " + "Tomorrow.io integration. There is nothing you need to do since the " + "new integration is a drop in replacement and your existing entities " + "have been migrated over, just note that the location of the " + "integration card on the " + "[Integrations Configuration](/config/integrations) page has changed " + "since the integration name has changed." +) diff --git a/homeassistant/components/tomorrowio/manifest.json b/homeassistant/components/tomorrowio/manifest.json new file mode 100644 index 00000000000..6fc8a2f12ce --- /dev/null +++ b/homeassistant/components/tomorrowio/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "tomorrowio", + "name": "Tomorrow.io", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/tomorrowio", + "requirements": ["pytomorrowio==0.1.0"], + "codeowners": ["@raman325"], + "iot_class": "cloud_polling" +} diff --git a/homeassistant/components/tomorrowio/sensor.py b/homeassistant/components/tomorrowio/sensor.py new file mode 100644 index 00000000000..8361869c18a --- /dev/null +++ b/homeassistant/components/tomorrowio/sensor.py @@ -0,0 +1,384 @@ +"""Sensor component that handles additional Tomorrowio data for your location.""" +from __future__ import annotations + +from abc import abstractmethod +from collections.abc import Callable +from dataclasses import dataclass +from typing import Any + +from pytomorrowio.const import ( + HealthConcernType, + PollenIndex, + PrecipitationType, + PrimaryPollutantType, +) + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + ATTR_ATTRIBUTION, + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + CONCENTRATION_PARTS_PER_MILLION, + CONF_NAME, + IRRADIATION_BTUS_PER_HOUR_SQUARE_FOOT, + IRRADIATION_WATTS_PER_SQUARE_METER, + LENGTH_KILOMETERS, + LENGTH_METERS, + LENGTH_MILES, + PERCENTAGE, + PRESSURE_HPA, + PRESSURE_INHG, + SPEED_METERS_PER_SECOND, + SPEED_MILES_PER_HOUR, + TEMP_FAHRENHEIT, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.util import slugify +from homeassistant.util.distance import convert as distance_convert +from homeassistant.util.pressure import convert as pressure_convert + +from . import TomorrowioDataUpdateCoordinator, TomorrowioEntity +from .const import ( + DOMAIN, + TMRW_ATTR_CARBON_MONOXIDE, + TMRW_ATTR_CHINA_AQI, + TMRW_ATTR_CHINA_HEALTH_CONCERN, + TMRW_ATTR_CHINA_PRIMARY_POLLUTANT, + TMRW_ATTR_CLOUD_BASE, + TMRW_ATTR_CLOUD_CEILING, + TMRW_ATTR_CLOUD_COVER, + TMRW_ATTR_DEW_POINT, + TMRW_ATTR_EPA_AQI, + TMRW_ATTR_EPA_HEALTH_CONCERN, + TMRW_ATTR_EPA_PRIMARY_POLLUTANT, + TMRW_ATTR_FEELS_LIKE, + TMRW_ATTR_FIRE_INDEX, + TMRW_ATTR_NITROGEN_DIOXIDE, + TMRW_ATTR_OZONE, + TMRW_ATTR_PARTICULATE_MATTER_10, + TMRW_ATTR_PARTICULATE_MATTER_25, + TMRW_ATTR_POLLEN_GRASS, + TMRW_ATTR_POLLEN_TREE, + TMRW_ATTR_POLLEN_WEED, + TMRW_ATTR_PRECIPITATION_TYPE, + TMRW_ATTR_PRESSURE_SURFACE_LEVEL, + TMRW_ATTR_SOLAR_GHI, + TMRW_ATTR_SULPHUR_DIOXIDE, + TMRW_ATTR_WIND_GUST, +) + + +@dataclass +class TomorrowioSensorEntityDescription(SensorEntityDescription): + """Describes a Tomorrow.io sensor entity.""" + + unit_imperial: str | None = None + unit_metric: str | None = None + multiplication_factor: Callable[[float], float] | float | None = None + metric_conversion: Callable[[float], float] | float | None = None + value_map: Any | None = None + + def __post_init__(self) -> None: + """Handle post init.""" + if (self.unit_imperial is None and self.unit_metric is not None) or ( + self.unit_imperial is not None and self.unit_metric is None + ): + raise ValueError( + "Entity descriptions must include both imperial and metric units or " + "they must both be None" + ) + + +# From https://cfpub.epa.gov/ncer_abstracts/index.cfm/fuseaction/display.files/fileID/14285 +# x ug/m^3 = y ppb * molecular weight / 24.45 +def convert_ppb_to_ugm3(molecular_weight: int | float) -> Callable[[float], float]: + """Return function to convert ppb to ug/m^3.""" + return lambda x: (x * molecular_weight) / 24.45 + + +SENSOR_TYPES = ( + TomorrowioSensorEntityDescription( + key=TMRW_ATTR_FEELS_LIKE, + name="Feels Like", + native_unit_of_measurement=TEMP_FAHRENHEIT, + device_class=SensorDeviceClass.TEMPERATURE, + ), + TomorrowioSensorEntityDescription( + key=TMRW_ATTR_DEW_POINT, + name="Dew Point", + native_unit_of_measurement=TEMP_FAHRENHEIT, + device_class=SensorDeviceClass.TEMPERATURE, + ), + # Data comes in as inHg + TomorrowioSensorEntityDescription( + key=TMRW_ATTR_PRESSURE_SURFACE_LEVEL, + name="Pressure (Surface Level)", + native_unit_of_measurement=PRESSURE_HPA, + multiplication_factor=lambda val: pressure_convert( + val, PRESSURE_INHG, PRESSURE_HPA + ), + device_class=SensorDeviceClass.PRESSURE, + ), + # Data comes in as BTUs/(hr * ft^2) + # https://www.theunitconverter.com/watt-square-meter-to-btu-hour-square-foot-conversion/ + TomorrowioSensorEntityDescription( + key=TMRW_ATTR_SOLAR_GHI, + name="Global Horizontal Irradiance", + unit_imperial=IRRADIATION_BTUS_PER_HOUR_SQUARE_FOOT, + unit_metric=IRRADIATION_WATTS_PER_SQUARE_METER, + metric_conversion=3.15459, + ), + # Data comes in as miles + TomorrowioSensorEntityDescription( + key=TMRW_ATTR_CLOUD_BASE, + name="Cloud Base", + unit_imperial=LENGTH_MILES, + unit_metric=LENGTH_KILOMETERS, + metric_conversion=lambda val: distance_convert( + val, LENGTH_MILES, LENGTH_KILOMETERS + ), + ), + # Data comes in as miles + TomorrowioSensorEntityDescription( + key=TMRW_ATTR_CLOUD_CEILING, + name="Cloud Ceiling", + unit_imperial=LENGTH_MILES, + unit_metric=LENGTH_KILOMETERS, + metric_conversion=lambda val: distance_convert( + val, LENGTH_MILES, LENGTH_KILOMETERS + ), + ), + TomorrowioSensorEntityDescription( + key=TMRW_ATTR_CLOUD_COVER, + name="Cloud Cover", + native_unit_of_measurement=PERCENTAGE, + ), + # Data comes in as MPH + TomorrowioSensorEntityDescription( + key=TMRW_ATTR_WIND_GUST, + name="Wind Gust", + unit_imperial=SPEED_MILES_PER_HOUR, + unit_metric=SPEED_METERS_PER_SECOND, + metric_conversion=lambda val: distance_convert(val, LENGTH_MILES, LENGTH_METERS) + / 3600, + ), + TomorrowioSensorEntityDescription( + key=TMRW_ATTR_PRECIPITATION_TYPE, + name="Precipitation Type", + value_map=PrecipitationType, + device_class="tomorrowio__precipitation_type", + icon="mdi:weather-snowy-rainy", + ), + # Data comes in as ppb + # Molecular weight of Ozone is 48 + TomorrowioSensorEntityDescription( + key=TMRW_ATTR_OZONE, + name="Ozone", + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + multiplication_factor=convert_ppb_to_ugm3(48), + device_class=SensorDeviceClass.OZONE, + ), + # Data comes in as ug/ft^3 + TomorrowioSensorEntityDescription( + key=TMRW_ATTR_PARTICULATE_MATTER_25, + name="Particulate Matter < 2.5 μm", + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + multiplication_factor=3.2808399**3, + device_class=SensorDeviceClass.PM25, + ), + # Data comes in as ug/ft^3 + TomorrowioSensorEntityDescription( + key=TMRW_ATTR_PARTICULATE_MATTER_10, + name="Particulate Matter < 10 μm", + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + multiplication_factor=3.2808399**3, + device_class=SensorDeviceClass.PM10, + ), + # Data comes in as ppb + # Molecular weight of Nitrogen Dioxide is 46.01 + TomorrowioSensorEntityDescription( + key=TMRW_ATTR_NITROGEN_DIOXIDE, + name="Nitrogen Dioxide", + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + multiplication_factor=convert_ppb_to_ugm3(46.01), + device_class=SensorDeviceClass.NITROGEN_DIOXIDE, + ), + # Data comes in as ppb + TomorrowioSensorEntityDescription( + key=TMRW_ATTR_CARBON_MONOXIDE, + name="Carbon Monoxide", + native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, + multiplication_factor=1 / 1000, + device_class=SensorDeviceClass.CO, + ), + # Molecular weight of Sulphur Dioxide is 64.07 + TomorrowioSensorEntityDescription( + key=TMRW_ATTR_SULPHUR_DIOXIDE, + name="Sulphur Dioxide", + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + multiplication_factor=convert_ppb_to_ugm3(64.07), + device_class=SensorDeviceClass.SULPHUR_DIOXIDE, + ), + TomorrowioSensorEntityDescription( + key=TMRW_ATTR_EPA_AQI, + name="US EPA Air Quality Index", + device_class=SensorDeviceClass.AQI, + ), + TomorrowioSensorEntityDescription( + key=TMRW_ATTR_EPA_PRIMARY_POLLUTANT, + name="US EPA Primary Pollutant", + value_map=PrimaryPollutantType, + ), + TomorrowioSensorEntityDescription( + key=TMRW_ATTR_EPA_HEALTH_CONCERN, + name="US EPA Health Concern", + value_map=HealthConcernType, + device_class="tomorrowio__health_concern", + icon="mdi:hospital", + ), + TomorrowioSensorEntityDescription( + key=TMRW_ATTR_CHINA_AQI, + name="China MEP Air Quality Index", + device_class=SensorDeviceClass.AQI, + ), + TomorrowioSensorEntityDescription( + key=TMRW_ATTR_CHINA_PRIMARY_POLLUTANT, + name="China MEP Primary Pollutant", + value_map=PrimaryPollutantType, + ), + TomorrowioSensorEntityDescription( + key=TMRW_ATTR_CHINA_HEALTH_CONCERN, + name="China MEP Health Concern", + value_map=HealthConcernType, + device_class="tomorrowio__health_concern", + icon="mdi:hospital", + ), + TomorrowioSensorEntityDescription( + key=TMRW_ATTR_POLLEN_TREE, + name="Tree Pollen Index", + value_map=PollenIndex, + device_class="tomorrowio__pollen_index", + icon="mdi:flower-pollen", + ), + TomorrowioSensorEntityDescription( + key=TMRW_ATTR_POLLEN_WEED, + name="Weed Pollen Index", + value_map=PollenIndex, + device_class="tomorrowio__pollen_index", + icon="mdi:flower-pollen", + ), + TomorrowioSensorEntityDescription( + key=TMRW_ATTR_POLLEN_GRASS, + name="Grass Pollen Index", + value_map=PollenIndex, + device_class="tomorrowio__pollen_index", + icon="mdi:flower-pollen", + ), + TomorrowioSensorEntityDescription( + TMRW_ATTR_FIRE_INDEX, + name="Fire Index", + icon="mdi:fire", + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up a config entry.""" + coordinator = hass.data[DOMAIN][config_entry.entry_id] + entities = [ + TomorrowioSensorEntity(hass, config_entry, coordinator, 4, description) + for description in SENSOR_TYPES + ] + async_add_entities(entities) + + +def handle_conversion( + value: float | int, conversion: Callable[[float], float] | float +) -> float: + """Handle conversion of a value based on conversion type.""" + if callable(conversion): + return round(conversion(float(value)), 2) + + return round(float(value) * conversion, 2) + + +class BaseTomorrowioSensorEntity(TomorrowioEntity, SensorEntity): + """Base Tomorrow.io sensor entity.""" + + entity_description: TomorrowioSensorEntityDescription + _attr_entity_registry_enabled_default = False + + def __init__( + self, + hass: HomeAssistant, + config_entry: ConfigEntry, + coordinator: TomorrowioDataUpdateCoordinator, + api_version: int, + description: TomorrowioSensorEntityDescription, + ) -> None: + """Initialize Tomorrow.io Sensor Entity.""" + super().__init__(config_entry, coordinator, api_version) + self.entity_description = description + self._attr_name = f"{self._config_entry.data[CONF_NAME]} - {description.name}" + self._attr_unique_id = ( + f"{self._config_entry.unique_id}_{slugify(description.name)}" + ) + self._attr_extra_state_attributes = {ATTR_ATTRIBUTION: self.attribution} + if self.entity_description.native_unit_of_measurement is None: + self._attr_native_unit_of_measurement = ( + description.unit_metric + if hass.config.units.is_metric + else description.unit_imperial + ) + + @property + @abstractmethod + def _state(self) -> int | float | None: + """Return the raw state.""" + + @property + def native_value(self) -> str | int | float | None: + """Return the state.""" + state = self._state + desc = self.entity_description + + if state is None: + return state + + if desc.value_map is not None: + return desc.value_map(state).name.lower() + + if desc.multiplication_factor is not None: + state = handle_conversion(state, desc.multiplication_factor) + + # If an imperial unit isn't provided, we always want to convert to metric since + # that is what the UI expects + if ( + desc.metric_conversion + and desc.unit_imperial is not None + and desc.unit_imperial != desc.unit_metric + and self.hass.config.units.is_metric + ): + return handle_conversion(state, desc.metric_conversion) + + return state + + +class TomorrowioSensorEntity(BaseTomorrowioSensorEntity): + """Sensor entity that talks to Tomorrow.io v4 API to retrieve non-weather data.""" + + @property + def _state(self) -> int | float | None: + """Return the raw state.""" + val = self._get_current_property(self.entity_description.key) + assert not isinstance(val, str) + return val diff --git a/homeassistant/components/tomorrowio/strings.json b/homeassistant/components/tomorrowio/strings.json new file mode 100644 index 00000000000..30b729e6ef1 --- /dev/null +++ b/homeassistant/components/tomorrowio/strings.json @@ -0,0 +1,31 @@ +{ + "config": { + "step": { + "user": { + "description": "To get an API key, sign up at [Tomorrow.io](https://app.tomorrow.io/signup).", + "data": { + "name": "[%key:common::config_flow::data::name%]", + "api_key": "[%key:common::config_flow::data::api_key%]", + "location": "[%key:common::config_flow::data::location%]" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]", + "unknown": "[%key:common::config_flow::error::unknown%]", + "rate_limited": "Currently rate limited, please try again later." + } + }, + "options": { + "step": { + "init": { + "title": "Update Tomorrow.io Options", + "description": "If you choose to enable the `nowcast` forecast entity, you can configure the number of minutes between each forecast. The number of forecasts provided depends on the number of minutes chosen between forecasts.", + "data": { + "timestep": "Min. Between NowCast Forecasts" + } + } + } + } +} diff --git a/homeassistant/components/tomorrowio/strings.sensor.json b/homeassistant/components/tomorrowio/strings.sensor.json new file mode 100644 index 00000000000..a1ee6d1b744 --- /dev/null +++ b/homeassistant/components/tomorrowio/strings.sensor.json @@ -0,0 +1,27 @@ +{ + "state": { + "tomorrowio__pollen_index": { + "none": "None", + "very_low": "Very Low", + "low": "Low", + "medium": "Medium", + "high": "High", + "very_high": "Very High" + }, + "tomorrowio__health_concern": { + "good": "Good", + "moderate": "Moderate", + "unhealthy_for_sensitive_groups": "Unhealthy for Sensitive Groups", + "unhealthy": "Unhealthy", + "very_unhealthy": "Very Unhealthy", + "hazardous": "Hazardous" + }, + "tomorrowio__precipitation_type": { + "none": "None", + "rain": "Rain", + "snow": "Snow", + "freezing_rain": "Freezing Rain", + "ice_pellets": "Ice Pellets" + } + } +} diff --git a/homeassistant/components/tomorrowio/translations/en.json b/homeassistant/components/tomorrowio/translations/en.json new file mode 100644 index 00000000000..7c653b00574 --- /dev/null +++ b/homeassistant/components/tomorrowio/translations/en.json @@ -0,0 +1,32 @@ +{ + "config": { + "error": { + "cannot_connect": "Failed to connect", + "invalid_api_key": "Invalid API key", + "rate_limited": "Currently rate limited, please try again later.", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "api_key": "API Key", + "latitude": "Latitude", + "longitude": "Longitude", + "name": "Name" + }, + "description": "To get an API key, sign up at [Tomorrow.io](https://app.tomorrow.io/signup)." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "timestep": "Min. Between NowCast Forecasts" + }, + "description": "If you choose to enable the `nowcast` forecast entity, you can configure the number of minutes between each forecast. The number of forecasts provided depends on the number of minutes chosen between forecasts.", + "title": "Update Tomorrow.io Options" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tomorrowio/translations/sensor.en.json b/homeassistant/components/tomorrowio/translations/sensor.en.json new file mode 100644 index 00000000000..52b767dec3c --- /dev/null +++ b/homeassistant/components/tomorrowio/translations/sensor.en.json @@ -0,0 +1,27 @@ +{ + "state": { + "tomorrowio__health_concern": { + "good": "Good", + "hazardous": "Hazardous", + "moderate": "Moderate", + "unhealthy": "Unhealthy", + "unhealthy_for_sensitive_groups": "Unhealthy for Sensitive Groups", + "very_unhealthy": "Very Unhealthy" + }, + "tomorrowio__pollen_index": { + "high": "High", + "low": "Low", + "medium": "Medium", + "none": "None", + "very_high": "Very High", + "very_low": "Very Low" + }, + "tomorrowio__precipitation_type": { + "freezing_rain": "Freezing Rain", + "ice_pellets": "Ice Pellets", + "none": "None", + "rain": "Rain", + "snow": "Snow" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tomorrowio/weather.py b/homeassistant/components/tomorrowio/weather.py new file mode 100644 index 00000000000..3006be989ba --- /dev/null +++ b/homeassistant/components/tomorrowio/weather.py @@ -0,0 +1,253 @@ +"""Weather component that handles meteorological data for your location.""" +from __future__ import annotations + +from datetime import datetime +from typing import Any + +from pytomorrowio.const import DAILY, FORECASTS, HOURLY, NOWCAST, WeatherCode + +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.config_entries import ConfigEntry +from homeassistant.const import ( + CONF_NAME, + LENGTH_INCHES, + LENGTH_MILES, + PRESSURE_INHG, + SPEED_MILES_PER_HOUR, + TEMP_FAHRENHEIT, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.sun import is_up +from homeassistant.util import dt as dt_util + +from . import TomorrowioDataUpdateCoordinator, TomorrowioEntity +from .const import ( + CLEAR_CONDITIONS, + CONDITIONS, + CONF_TIMESTEP, + DEFAULT_FORECAST_TYPE, + DOMAIN, + MAX_FORECASTS, + TMRW_ATTR_CONDITION, + TMRW_ATTR_HUMIDITY, + TMRW_ATTR_OZONE, + TMRW_ATTR_PRECIPITATION, + TMRW_ATTR_PRECIPITATION_PROBABILITY, + TMRW_ATTR_PRESSURE, + TMRW_ATTR_TEMPERATURE, + TMRW_ATTR_TEMPERATURE_HIGH, + TMRW_ATTR_TEMPERATURE_LOW, + TMRW_ATTR_TIMESTAMP, + TMRW_ATTR_VISIBILITY, + TMRW_ATTR_WIND_DIRECTION, + TMRW_ATTR_WIND_SPEED, +) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up a config entry.""" + coordinator = hass.data[DOMAIN][config_entry.entry_id] + + entities = [ + TomorrowioWeatherEntity(config_entry, coordinator, 4, forecast_type) + for forecast_type in (DAILY, HOURLY, NOWCAST) + ] + async_add_entities(entities) + + +class TomorrowioWeatherEntity(TomorrowioEntity, WeatherEntity): + """Entity that talks to Tomorrow.io v4 API to retrieve weather data.""" + + _attr_temperature_unit = TEMP_FAHRENHEIT + _attr_pressure_unit = PRESSURE_INHG + _attr_wind_speed_unit = SPEED_MILES_PER_HOUR + _attr_visibility_unit = LENGTH_MILES + _attr_precipitation_unit = LENGTH_INCHES + + def __init__( + self, + config_entry: ConfigEntry, + coordinator: TomorrowioDataUpdateCoordinator, + api_version: int, + forecast_type: str, + ) -> None: + """Initialize Tomorrow.io Weather Entity.""" + super().__init__(config_entry, coordinator, api_version) + self.forecast_type = forecast_type + self._attr_entity_registry_enabled_default = ( + forecast_type == DEFAULT_FORECAST_TYPE + ) + self._attr_name = f"{config_entry.data[CONF_NAME]} - {forecast_type.title()}" + self._attr_unique_id = f"{config_entry.unique_id}_{forecast_type}" + + def _forecast_dict( + self, + forecast_dt: datetime, + use_datetime: bool, + condition: int, + precipitation: float | None, + precipitation_probability: float | None, + temp: float | None, + temp_low: float | None, + wind_direction: float | None, + wind_speed: float | None, + ) -> dict[str, Any]: + """Return formatted Forecast dict from Tomorrow.io forecast data.""" + if use_datetime: + translated_condition = self._translate_condition( + condition, is_up(self.hass, forecast_dt) + ) + else: + translated_condition = self._translate_condition(condition, True) + + data = { + ATTR_FORECAST_TIME: forecast_dt.isoformat(), + ATTR_FORECAST_CONDITION: translated_condition, + ATTR_FORECAST_PRECIPITATION: precipitation, + ATTR_FORECAST_PRECIPITATION_PROBABILITY: precipitation_probability, + ATTR_FORECAST_TEMP: temp, + ATTR_FORECAST_TEMP_LOW: temp_low, + ATTR_FORECAST_WIND_BEARING: wind_direction, + ATTR_FORECAST_WIND_SPEED: wind_speed, + } + + return {k: v for k, v in data.items() if v is not None} + + @staticmethod + def _translate_condition( + condition: int | None, sun_is_up: bool = True + ) -> str | None: + """Translate Tomorrow.io condition into an HA condition.""" + if condition is None: + return None + # We won't guard here, instead we will fail hard + condition = WeatherCode(condition) + if condition in (WeatherCode.CLEAR, WeatherCode.MOSTLY_CLEAR): + if sun_is_up: + return CLEAR_CONDITIONS["day"] + return CLEAR_CONDITIONS["night"] + return CONDITIONS[condition] + + @property + def temperature(self): + """Return the platform temperature.""" + return self._get_current_property(TMRW_ATTR_TEMPERATURE) + + @property + def pressure(self): + """Return the raw pressure.""" + return self._get_current_property(TMRW_ATTR_PRESSURE) + + @property + def humidity(self): + """Return the humidity.""" + return self._get_current_property(TMRW_ATTR_HUMIDITY) + + @property + def wind_speed(self): + """Return the raw wind speed.""" + return self._get_current_property(TMRW_ATTR_WIND_SPEED) + + @property + def wind_bearing(self): + """Return the wind bearing.""" + return self._get_current_property(TMRW_ATTR_WIND_DIRECTION) + + @property + def ozone(self): + """Return the O3 (ozone) level.""" + return self._get_current_property(TMRW_ATTR_OZONE) + + @property + def condition(self): + """Return the condition.""" + return self._translate_condition( + self._get_current_property(TMRW_ATTR_CONDITION), + is_up(self.hass), + ) + + @property + def visibility(self): + """Return the raw visibility.""" + return self._get_current_property(TMRW_ATTR_VISIBILITY) + + @property + def forecast(self): + """Return the forecast.""" + # Check if forecasts are available + raw_forecasts = self.coordinator.data.get(FORECASTS, {}).get(self.forecast_type) + if not raw_forecasts: + return None + + forecasts = [] + max_forecasts = MAX_FORECASTS[self.forecast_type] + forecast_count = 0 + + # Set default values (in cases where keys don't exist), None will be + # returned. Override properties per forecast type as needed + for forecast in raw_forecasts: + forecast_dt = dt_util.parse_datetime(forecast[TMRW_ATTR_TIMESTAMP]) + + # Throw out past data + if forecast_dt.date() < dt_util.utcnow().date(): + continue + + values = forecast["values"] + use_datetime = True + + condition = values.get(TMRW_ATTR_CONDITION) + precipitation = values.get(TMRW_ATTR_PRECIPITATION) + precipitation_probability = values.get(TMRW_ATTR_PRECIPITATION_PROBABILITY) + + temp = values.get(TMRW_ATTR_TEMPERATURE_HIGH) + temp_low = None + wind_direction = values.get(TMRW_ATTR_WIND_DIRECTION) + wind_speed = values.get(TMRW_ATTR_WIND_SPEED) + + if self.forecast_type == DAILY: + use_datetime = False + temp_low = values.get(TMRW_ATTR_TEMPERATURE_LOW) + if precipitation: + precipitation = precipitation * 24 + elif self.forecast_type == NOWCAST: + # Precipitation is forecasted in CONF_TIMESTEP increments but in a + # per hour rate, so value needs to be converted to an amount. + if precipitation: + precipitation = ( + precipitation / 60 * self._config_entry.options[CONF_TIMESTEP] + ) + + forecasts.append( + self._forecast_dict( + forecast_dt, + use_datetime, + condition, + precipitation, + precipitation_probability, + temp, + temp_low, + wind_direction, + wind_speed, + ) + ) + + forecast_count += 1 + if forecast_count == max_forecasts: + break + + return forecasts diff --git a/homeassistant/components/toon/models.py b/homeassistant/components/toon/models.py index 57db44beb6b..e39faa1efc6 100644 --- a/homeassistant/components/toon/models.py +++ b/homeassistant/components/toon/models.py @@ -10,11 +10,9 @@ from .const import DOMAIN from .coordinator import ToonDataUpdateCoordinator -class ToonEntity(CoordinatorEntity): +class ToonEntity(CoordinatorEntity[ToonDataUpdateCoordinator]): """Defines a base Toon entity.""" - coordinator: ToonDataUpdateCoordinator - class ToonDisplayDeviceEntity(ToonEntity): """Defines a Toon display device entity.""" diff --git a/homeassistant/components/toon/translations/fr.json b/homeassistant/components/toon/translations/fr.json index 2b70c85dc8f..3f6fab8d775 100644 --- a/homeassistant/components/toon/translations/fr.json +++ b/homeassistant/components/toon/translations/fr.json @@ -2,10 +2,10 @@ "config": { "abort": { "already_configured": "L'accord s\u00e9lectionn\u00e9 est d\u00e9j\u00e0 configur\u00e9.", - "authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration de l'URL d'authentification d\u00e9pass\u00e9.", + "authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration de l'URL d'authentification expir\u00e9.", "missing_configuration": "Le composant n'est pas configur\u00e9. Veuillez suivre la documentation.", "no_agreements": "Ce compte n'a pas d'affichages Toon.", - "no_url_available": "Aucune URL disponible. Pour plus d'informations sur cette erreur, [consultez la section d'aide] ( {docs_url} )", + "no_url_available": "Aucune URL disponible. Pour plus d'informations sur cette erreur, [consultez la section d'aide]({docs_url})", "unknown_authorize_url_generation": "Erreur inconnue lors de la g\u00e9n\u00e9ration d'une URL d'autorisation." }, "step": { diff --git a/homeassistant/components/totalconnect/config_flow.py b/homeassistant/components/totalconnect/config_flow.py index b529bdd80fd..013b08b50be 100644 --- a/homeassistant/components/totalconnect/config_flow.py +++ b/homeassistant/components/totalconnect/config_flow.py @@ -4,12 +4,10 @@ from total_connect_client.exceptions import AuthenticationError import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import CONF_LOCATION, CONF_PASSWORD, CONF_USERNAME from .const import CONF_USERCODES, DOMAIN -CONF_LOCATION = "location" - PASSWORD_DATA_SCHEMA = vol.Schema({vol.Required(CONF_PASSWORD): str}) diff --git a/homeassistant/components/totalconnect/const.py b/homeassistant/components/totalconnect/const.py index 22ecd14281f..ba217bd4ca7 100644 --- a/homeassistant/components/totalconnect/const.py +++ b/homeassistant/components/totalconnect/const.py @@ -2,7 +2,6 @@ DOMAIN = "totalconnect" CONF_USERCODES = "usercodes" -CONF_LOCATION = "location" # Most TotalConnect alarms will work passing '-1' as usercode DEFAULT_USERCODE = "-1" diff --git a/homeassistant/components/totalconnect/strings.json b/homeassistant/components/totalconnect/strings.json index 63505c2446c..64ca1beafd8 100644 --- a/homeassistant/components/totalconnect/strings.json +++ b/homeassistant/components/totalconnect/strings.json @@ -2,7 +2,6 @@ "config": { "step": { "user": { - "title": "Total Connect", "data": { "username": "[%key:common::config_flow::data::username%]", "password": "[%key:common::config_flow::data::password%]" diff --git a/homeassistant/components/totalconnect/translations/fr.json b/homeassistant/components/totalconnect/translations/fr.json index a538750f455..f6dcefac2bb 100644 --- a/homeassistant/components/totalconnect/translations/fr.json +++ b/homeassistant/components/totalconnect/translations/fr.json @@ -6,7 +6,7 @@ "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" }, "error": { - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "usercode": "Code d'utilisateur non valide pour cet utilisateur \u00e0 cet emplacement" }, "step": { diff --git a/homeassistant/components/tplink/__init__.py b/homeassistant/components/tplink/__init__.py index 33b03109cd8..10e621e6668 100644 --- a/homeassistant/components/tplink/__init__.py +++ b/homeassistant/components/tplink/__init__.py @@ -96,7 +96,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: device: SmartDevice = hass_data[entry.entry_id].device if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): hass_data.pop(entry.entry_id) - await device.protocol.close() # type: ignore[no-untyped-call] + await device.protocol.close() return unload_ok diff --git a/homeassistant/components/tplink/diagnostics.py b/homeassistant/components/tplink/diagnostics.py index 5771bee5bd3..5121def2e47 100644 --- a/homeassistant/components/tplink/diagnostics.py +++ b/homeassistant/components/tplink/diagnostics.py @@ -36,11 +36,6 @@ async def async_get_config_entry_diagnostics( ) -> dict[str, Any]: """Return diagnostics for a config entry.""" coordinator: TPLinkDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - device = coordinator.device - - data = {} - data[ - "device_last_response" - ] = device._last_update # pylint: disable=protected-access - - return async_redact_data(data, TO_REDACT) + return async_redact_data( + {"device_last_response": coordinator.device.internal_state}, TO_REDACT + ) diff --git a/homeassistant/components/tplink/entity.py b/homeassistant/components/tplink/entity.py index 4380b1397b6..173d1d7930f 100644 --- a/homeassistant/components/tplink/entity.py +++ b/homeassistant/components/tplink/entity.py @@ -30,11 +30,9 @@ def async_refresh_after( return _async_wrap -class CoordinatedTPLinkEntity(CoordinatorEntity): +class CoordinatedTPLinkEntity(CoordinatorEntity[TPLinkDataUpdateCoordinator]): """Common base class for all coordinated tplink entities.""" - coordinator: TPLinkDataUpdateCoordinator - def __init__( self, device: SmartDevice, coordinator: TPLinkDataUpdateCoordinator ) -> None: diff --git a/homeassistant/components/tplink/light.py b/homeassistant/components/tplink/light.py index 182bef586ee..9a67f8daf4b 100644 --- a/homeassistant/components/tplink/light.py +++ b/homeassistant/components/tplink/light.py @@ -1,25 +1,31 @@ """Support for TPLink lights.""" from __future__ import annotations +from collections.abc import Sequence import logging -from typing import Any, cast +from typing import Any, Final, cast -from kasa import SmartBulb +from kasa import SmartBulb, SmartLightStrip +import voluptuous as vol from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, + ATTR_EFFECT, ATTR_HS_COLOR, ATTR_TRANSITION, COLOR_MODE_BRIGHTNESS, COLOR_MODE_COLOR_TEMP, COLOR_MODE_HS, COLOR_MODE_ONOFF, + SUPPORT_EFFECT, SUPPORT_TRANSITION, LightEntity, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import entity_platform +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.color import ( color_temperature_kelvin_to_mired as kelvin_to_mired, @@ -33,6 +39,97 @@ from .entity import CoordinatedTPLinkEntity, async_refresh_after _LOGGER = logging.getLogger(__name__) +SERVICE_RANDOM_EFFECT = "random_effect" +SERVICE_SEQUENCE_EFFECT = "sequence_effect" + +HUE = vol.Range(min=0, max=360) +SAT = vol.Range(min=0, max=100) +VAL = vol.Range(min=0, max=100) +TRANSITION = vol.Range(min=0, max=6000) +HSV_SEQUENCE = vol.ExactSequence((HUE, SAT, VAL)) + +BASE_EFFECT_DICT: Final = { + vol.Optional("brightness", default=100): vol.All( + vol.Coerce(int), vol.Range(min=0, max=100) + ), + vol.Optional("duration", default=0): vol.All( + vol.Coerce(int), vol.Range(min=0, max=5000) + ), + vol.Optional("transition", default=0): vol.All(vol.Coerce(int), TRANSITION), + vol.Optional("segments", default=[0]): vol.All( + cv.ensure_list_csv, + vol.Length(min=1, max=80), + [vol.All(vol.Coerce(int), vol.Range(min=0, max=80))], + ), +} + +SEQUENCE_EFFECT_DICT: Final = { + **BASE_EFFECT_DICT, + vol.Required("sequence"): vol.All( + cv.ensure_list, + vol.Length(min=1, max=16), + [vol.All(vol.Coerce(tuple), HSV_SEQUENCE)], + ), + vol.Optional("spread", default=1): vol.All( + vol.Coerce(int), vol.Range(min=1, max=16) + ), + vol.Optional("direction", default=4): vol.All( + vol.Coerce(int), vol.Range(min=1, max=4) + ), +} + +RANDOM_EFFECT_DICT: Final = { + **BASE_EFFECT_DICT, + vol.Optional("fadeoff", default=0): vol.All( + vol.Coerce(int), vol.Range(min=0, max=3000) + ), + vol.Optional("hue_range"): vol.All( + cv.ensure_list_csv, [vol.Coerce(int)], vol.ExactSequence((HUE, HUE)) + ), + vol.Optional("saturation_range"): vol.All( + cv.ensure_list_csv, [vol.Coerce(int)], vol.ExactSequence((SAT, SAT)) + ), + vol.Optional("brightness_range"): vol.All( + cv.ensure_list_csv, [vol.Coerce(int)], vol.ExactSequence((VAL, VAL)) + ), + vol.Optional("transition_range"): vol.All( + cv.ensure_list_csv, + [vol.Coerce(int)], + vol.ExactSequence((TRANSITION, TRANSITION)), + ), + vol.Required("init_states"): vol.All( + cv.ensure_list_csv, [vol.Coerce(int)], HSV_SEQUENCE + ), + vol.Optional("random_seed", default=100): vol.All( + vol.Coerce(int), vol.Range(min=1, max=100) + ), + vol.Required("backgrounds"): vol.All( + cv.ensure_list, + vol.Length(min=1, max=16), + [vol.All(vol.Coerce(tuple), HSV_SEQUENCE)], + ), +} + + +@callback +def _async_build_base_effect( + brightness: int, + duration: int, + transition: int, + segments: list[int], +) -> dict[str, Any]: + return { + "custom": 1, + "id": "yMwcNpLxijmoKamskHCvvravpbnIqAIN", + "brightness": brightness, + "name": "Custom", + "segments": segments, + "expansion_strategy": 1, + "enable": 1, + "duration": duration, + "transition": transition, + } + async def async_setup_entry( hass: HomeAssistant, @@ -41,15 +138,34 @@ async def async_setup_entry( ) -> None: """Set up switches.""" coordinator: TPLinkDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] - device = cast(SmartBulb, coordinator.device) - if device.is_bulb or device.is_light_strip or device.is_dimmer: - async_add_entities([TPLinkSmartBulb(device, coordinator)]) + if coordinator.device.is_light_strip: + async_add_entities( + [ + TPLinkSmartLightStrip( + cast(SmartLightStrip, coordinator.device), coordinator + ) + ] + ) + platform = entity_platform.async_get_current_platform() + platform.async_register_entity_service( + SERVICE_RANDOM_EFFECT, + RANDOM_EFFECT_DICT, + "async_set_random_effect", + ) + platform.async_register_entity_service( + SERVICE_SEQUENCE_EFFECT, + SEQUENCE_EFFECT_DICT, + "async_set_sequence_effect", + ) + elif coordinator.device.is_bulb or coordinator.device.is_dimmer: + async_add_entities( + [TPLinkSmartBulb(cast(SmartBulb, coordinator.device), coordinator)] + ) class TPLinkSmartBulb(CoordinatedTPLinkEntity, LightEntity): """Representation of a TPLink Smart Bulb.""" - coordinator: TPLinkDataUpdateCoordinator device: SmartBulb def __init__( @@ -60,18 +176,19 @@ class TPLinkSmartBulb(CoordinatedTPLinkEntity, LightEntity): """Initialize the switch.""" super().__init__(device, coordinator) # For backwards compat with pyHS100 - if self.device.is_dimmer: + if device.is_dimmer: # Dimmers used to use the switch format since # pyHS100 treated them as SmartPlug but the old code # created them as lights # https://github.com/home-assistant/core/blob/2021.9.7/homeassistant/components/tplink/common.py#L86 self._attr_unique_id = legacy_device_id(device) else: - self._attr_unique_id = self.device.mac.replace(":", "").upper() + self._attr_unique_id = device.mac.replace(":", "").upper() - @async_refresh_after - async def async_turn_on(self, **kwargs: Any) -> None: - """Turn the light on.""" + @callback + def _async_extract_brightness_transition( + self, **kwargs: Any + ) -> tuple[int | None, int | None]: if (transition := kwargs.get(ATTR_TRANSITION)) is not None: transition = int(transition * 1_000) @@ -86,30 +203,48 @@ class TPLinkSmartBulb(CoordinatedTPLinkEntity, LightEntity): # except when transition is defined, so we leverage that here for now. transition = 1 - # Handle turning to temp mode - if ATTR_COLOR_TEMP in kwargs: - # Handle temp conversion mireds -> kelvin being slightly outside of valid range - kelvin = mired_to_kelvin(int(kwargs[ATTR_COLOR_TEMP])) - kelvin_range = self.device.valid_temperature_range - color_tmp = max(kelvin_range.min, min(kelvin_range.max, kelvin)) - _LOGGER.debug("Changing color temp to %s", color_tmp) - await self.device.set_color_temp( - color_tmp, brightness=brightness, transition=transition - ) - return + return brightness, transition - # Handling turning to hs color mode - if ATTR_HS_COLOR in kwargs: - # TP-Link requires integers. - hue, sat = tuple(int(val) for val in kwargs[ATTR_HS_COLOR]) - await self.device.set_hsv(hue, sat, brightness, transition=transition) - return + async def _async_set_color_temp( + self, color_temp_mireds: int, brightness: int | None, transition: int | None + ) -> None: + # Handle temp conversion mireds -> kelvin being slightly outside of valid range + kelvin = mired_to_kelvin(color_temp_mireds) + kelvin_range = self.device.valid_temperature_range + color_tmp = max(kelvin_range.min, min(kelvin_range.max, kelvin)) + _LOGGER.debug("Changing color temp to %s", color_tmp) + await self.device.set_color_temp( + color_tmp, brightness=brightness, transition=transition + ) + async def _async_set_hsv( + self, hs_color: tuple[int, int], brightness: int | None, transition: int | None + ) -> None: + # TP-Link requires integers. + hue, sat = tuple(int(val) for val in hs_color) + await self.device.set_hsv(hue, sat, brightness, transition=transition) + + async def _async_turn_on_with_brightness( + self, brightness: int | None, transition: int | None + ) -> None: # Fallback to adjusting brightness or turning the bulb on if brightness is not None: await self.device.set_brightness(brightness, transition=transition) + return + await self.device.turn_on(transition=transition) # type: ignore[arg-type] + + @async_refresh_after + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the light on.""" + brightness, transition = self._async_extract_brightness_transition(**kwargs) + if ATTR_COLOR_TEMP in kwargs: + await self._async_set_color_temp( + int(kwargs[ATTR_COLOR_TEMP]), brightness, transition + ) + if ATTR_HS_COLOR in kwargs: + await self._async_set_hsv(kwargs[ATTR_HS_COLOR], brightness, transition) else: - await self.device.turn_on(transition=transition) + await self._async_turn_on_with_brightness(brightness, transition) @async_refresh_after async def async_turn_off(self, **kwargs: Any) -> None: @@ -176,3 +311,108 @@ class TPLinkSmartBulb(CoordinatedTPLinkEntity, LightEntity): return COLOR_MODE_COLOR_TEMP return COLOR_MODE_BRIGHTNESS + + +class TPLinkSmartLightStrip(TPLinkSmartBulb): + """Representation of a TPLink Smart Light Strip.""" + + device: SmartLightStrip + + @property + def supported_features(self) -> int: + """Flag supported features.""" + return super().supported_features | SUPPORT_EFFECT + + @property + def effect_list(self) -> list[str] | None: + """Return the list of available effects.""" + if effect_list := self.device.effect_list: + return cast(list[str], effect_list) + return None + + @property + def effect(self) -> str | None: + """Return the current effect.""" + if (effect := self.device.effect) and effect["enable"]: + return cast(str, effect["name"]) + return None + + @async_refresh_after + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the light on.""" + brightness, transition = self._async_extract_brightness_transition(**kwargs) + if ATTR_COLOR_TEMP in kwargs: + if self.effect: + # If there is an effect in progress + # we have to set an HSV value to clear the effect + # before we can set a color temp + await self.device.set_hsv(0, 0, brightness) + await self._async_set_color_temp( + int(kwargs[ATTR_COLOR_TEMP]), brightness, transition + ) + elif ATTR_HS_COLOR in kwargs: + await self._async_set_hsv(kwargs[ATTR_HS_COLOR], brightness, transition) + elif ATTR_EFFECT in kwargs: + await self.device.set_effect(kwargs[ATTR_EFFECT]) + else: + await self._async_turn_on_with_brightness(brightness, transition) + + async def async_set_random_effect( + self, + brightness: int, + duration: int, + transition: int, + segments: list[int], + fadeoff: int, + init_states: tuple[int, int, int], + random_seed: int, + backgrounds: Sequence[tuple[int, int, int]], + hue_range: tuple[int, int] | None = None, + saturation_range: tuple[int, int] | None = None, + brightness_range: tuple[int, int] | None = None, + transition_range: tuple[int, int] | None = None, + ) -> None: + """Set a random effect.""" + effect: dict[str, Any] = { + **_async_build_base_effect(brightness, duration, transition, segments), + "type": "random", + "init_states": [init_states], + "random_seed": random_seed, + "backgrounds": backgrounds, + } + if fadeoff: + effect["fadeoff"] = fadeoff + if hue_range: + effect["hue_range"] = hue_range + if saturation_range: + effect["saturation_range"] = saturation_range + if brightness_range: + effect["brightness_range"] = brightness_range + effect["brightness"] = min( + brightness_range[1], max(brightness, brightness_range[0]) + ) + if transition_range: + effect["transition_range"] = transition_range + effect["transition"] = 0 + await self.device.set_custom_effect(effect) + + async def async_set_sequence_effect( + self, + brightness: int, + duration: int, + transition: int, + segments: list[int], + sequence: Sequence[tuple[int, int, int]], + spread: int, + direction: int, + ) -> None: + """Set a sequence effect.""" + effect: dict[str, Any] = { + **_async_build_base_effect(brightness, duration, transition, segments), + "type": "sequence", + "sequence": sequence, + "repeat_times": 0, + "spread": spread, + "direction": direction, + } + await self.device.set_custom_effect(effect) diff --git a/homeassistant/components/tplink/manifest.json b/homeassistant/components/tplink/manifest.json index 20f2e9dc171..3b23f466296 100644 --- a/homeassistant/components/tplink/manifest.json +++ b/homeassistant/components/tplink/manifest.json @@ -3,13 +3,13 @@ "name": "TP-Link Kasa Smart", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tplink", - "requirements": ["python-kasa==0.4.1"], + "requirements": ["python-kasa==0.4.3"], "codeowners": ["@rytilahti", "@thegardenmonkey"], "dependencies": ["network"], "quality_scale": "platinum", "iot_class": "local_polling", "dhcp": [ - {"registered_devices": true}, + { "registered_devices": true }, { "hostname": "k[lp]*", "macaddress": "60A4B7*" @@ -21,11 +21,11 @@ { "hostname": "k[lp]*", "macaddress": "1027F5*" - }, + }, { "hostname": "k[lp]*", "macaddress": "403F8C*" - }, + }, { "hostname": "k[lp]*", "macaddress": "C0C9E3*" @@ -117,7 +117,7 @@ { "hostname": "lb*", "macaddress": "B09575*" - } + } ], "loggers": ["kasa"] } diff --git a/homeassistant/components/tplink/sensor.py b/homeassistant/components/tplink/sensor.py index fe9cb699114..7ba28702114 100644 --- a/homeassistant/components/tplink/sensor.py +++ b/homeassistant/components/tplink/sensor.py @@ -140,7 +140,6 @@ async def async_setup_entry( class SmartPlugSensor(CoordinatedTPLinkEntity, SensorEntity): """Representation of a TPLink Smart Plug energy sensor.""" - coordinator: TPLinkDataUpdateCoordinator entity_description: TPLinkSensorEntityDescription def __init__( diff --git a/homeassistant/components/tplink/services.yaml b/homeassistant/components/tplink/services.yaml new file mode 100644 index 00000000000..26e002e0e30 --- /dev/null +++ b/homeassistant/components/tplink/services.yaml @@ -0,0 +1,183 @@ +sequence_effect: + description: Set a sequence effect + target: + entity: + integration: tplink + domain: light + fields: + sequence: + description: List of HSV sequences (Max 16) + example: | + - [340, 20, 50] + - [20, 50, 50] + - [0, 100, 50] + required: true + selector: + object: + segments: + description: List of Segments (0 for all) + example: 0, 2, 4, 6, 8 + default: 0 + required: false + selector: + object: + brightness: + description: Initial brightness + example: 80 + default: 100 + required: false + selector: + number: + min: 1 + step: 1 + max: 100 + unit_of_measurement: "%" + duration: + description: Duration + example: 0 + default: 0 + required: false + selector: + number: + min: 0 + step: 1 + max: 5000 + unit_of_measurement: "ms" + transition: + description: Transition + example: 2000 + default: 0 + required: false + selector: + number: + min: 0 + step: 1 + max: 6000 + unit_of_measurement: "ms" + spread: + description: Speed of spread + example: 1 + default: 0 + required: false + selector: + number: + min: 0 + step: 1 + max: 16 + direction: + description: Direction + example: 1 + default: 4 + required: false + selector: + number: + min: 1 + step: 1 + max: 4 +random_effect: + description: Set a random effect + target: + entity: + integration: tplink + domain: light + fields: + init_states: + description: Initial HSV sequence + example: [199, 99, 96] + required: true + selector: + object: + backgrounds: + description: List of HSV sequences (Max 16) + example: | + - [199, 89, 50] + - [160, 50, 50] + - [180, 100, 50] + required: true + selector: + object: + segments: + description: List of segments (0 for all) + example: 0, 2, 4, 6, 8 + default: 0 + required: false + selector: + object: + brightness: + description: Initial brightness + example: 90 + default: 100 + required: false + selector: + number: + min: 1 + step: 1 + max: 100 + unit_of_measurement: "%" + duration: + description: Duration + example: 0 + default: 0 + required: false + selector: + number: + min: 0 + step: 1 + max: 5000 + unit_of_measurement: "ms" + transition: + description: Transition + example: 2000 + default: 0 + required: false + selector: + number: + min: 0 + step: 1 + max: 6000 + unit_of_measurement: "ms" + fadeoff: + description: Fade off + example: 2000 + default: 0 + required: false + selector: + number: + min: 0 + step: 1 + max: 3000 + unit_of_measurement: "ms" + hue_range: + description: Range of hue + example: 340, 360 + required: false + selector: + object: + saturation_range: + description: Range of saturation + example: 40, 95 + required: false + selector: + object: + brightness_range: + description: Range of brightness + example: 90, 100 + required: false + selector: + object: + transition_range: + description: Range of transition + example: 2000, 6000 + required: false + selector: + object: + random_seed: + description: Random seed + example: 80 + default: 100 + required: false + selector: + number: + min: 1 + step: 1 + max: 100 diff --git a/homeassistant/components/tplink/switch.py b/homeassistant/components/tplink/switch.py index 451ec6d5f8b..2b53c67d296 100644 --- a/homeassistant/components/tplink/switch.py +++ b/homeassistant/components/tplink/switch.py @@ -47,7 +47,6 @@ async def async_setup_entry( class SmartPlugLedSwitch(CoordinatedTPLinkEntity, SwitchEntity): """Representation of switch for the LED of a TPLink Smart Plug.""" - coordinator: TPLinkDataUpdateCoordinator device: SmartPlug _attr_entity_category = EntityCategory.CONFIG @@ -85,8 +84,6 @@ class SmartPlugLedSwitch(CoordinatedTPLinkEntity, SwitchEntity): class SmartPlugSwitch(CoordinatedTPLinkEntity, SwitchEntity): """Representation of a TPLink Smart Plug switch.""" - coordinator: TPLinkDataUpdateCoordinator - def __init__( self, device: SmartDevice, diff --git a/homeassistant/components/tplink/translations/fr.json b/homeassistant/components/tplink/translations/fr.json index 0607c01c14d..d3a5fad5939 100644 --- a/homeassistant/components/tplink/translations/fr.json +++ b/homeassistant/components/tplink/translations/fr.json @@ -25,7 +25,7 @@ "data": { "host": "H\u00f4te" }, - "description": "Si vous laissez l'h\u00f4te vide, la d\u00e9couverte sera utilis\u00e9e pour trouver des p\u00e9riph\u00e9riques." + "description": "Si vous laissez l'h\u00f4te vide, la d\u00e9couverte sera utilis\u00e9e pour trouver des appareils." } } } diff --git a/homeassistant/components/tplink/translations/zh-Hant.json b/homeassistant/components/tplink/translations/zh-Hant.json index bfca7643b32..31bfeea42a2 100644 --- a/homeassistant/components/tplink/translations/zh-Hant.json +++ b/homeassistant/components/tplink/translations/zh-Hant.json @@ -25,7 +25,7 @@ "data": { "host": "\u4e3b\u6a5f\u7aef" }, - "description": "\u5047\u5982\u4e3b\u6a5f\u7aef\u4f4d\u5740\u6b04\u4f4d\u70ba\u7a7a\u767d\uff0c\u5c07\u6703\u63a2\u7d22\u6240\u6709\u53ef\u7528\u88dd\u7f6e\u3002" + "description": "\u5047\u5982\u4e3b\u6a5f\u7aef\u4f4d\u5740\u6b04\u4f4d\u70ba\u7a7a\u767d\uff0c\u5c07\u6703\u641c\u7d22\u6240\u6709\u53ef\u7528\u88dd\u7f6e\u3002" } } } diff --git a/homeassistant/components/traccar/translations/fr.json b/homeassistant/components/traccar/translations/fr.json index b3e9684424b..ab5d254dd98 100644 --- a/homeassistant/components/traccar/translations/fr.json +++ b/homeassistant/components/traccar/translations/fr.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "cloud_not_connected": "Pas connect\u00e9 \u00e0 Home Assistant Cloud", + "cloud_not_connected": "Non connect\u00e9 \u00e0 Home Assistant Cloud.", "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible.", "webhook_not_internet_accessible": "Votre installation de Home Assistant doit \u00eatre accessible depuis internet pour recevoir des messages webhook." }, diff --git a/homeassistant/components/trace/manifest.json b/homeassistant/components/trace/manifest.json index cdd857d00d3..572dff17b03 100644 --- a/homeassistant/components/trace/manifest.json +++ b/homeassistant/components/trace/manifest.json @@ -2,8 +2,6 @@ "domain": "trace", "name": "Trace", "documentation": "https://www.home-assistant.io/integrations/automation", - "codeowners": [ - "@home-assistant/core" - ], + "codeowners": ["@home-assistant/core"], "quality_scale": "internal" } diff --git a/homeassistant/components/tractive/manifest.json b/homeassistant/components/tractive/manifest.json index a73c8390ad8..be005e3104a 100644 --- a/homeassistant/components/tractive/manifest.json +++ b/homeassistant/components/tractive/manifest.json @@ -3,14 +3,8 @@ "name": "Tractive", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tractive", - "requirements": [ - "aiotractive==0.5.2" - ], - "codeowners": [ - "@Danielhiversen", - "@zhulik", - "@bieniu" - ], + "requirements": ["aiotractive==0.5.2"], + "codeowners": ["@Danielhiversen", "@zhulik", "@bieniu"], "iot_class": "cloud_push", "loggers": ["aiotractive"] } diff --git a/homeassistant/components/tractive/strings.json b/homeassistant/components/tractive/strings.json index 9711eb41489..7eba02e7f67 100644 --- a/homeassistant/components/tractive/strings.json +++ b/homeassistant/components/tractive/strings.json @@ -18,4 +18,4 @@ "reauth_failed_existing": "Could not update the config entry, please remove the integration and set it up again." } } -} \ No newline at end of file +} diff --git a/homeassistant/components/tractive/strings.sensor.json b/homeassistant/components/tractive/strings.sensor.json index b9c2cd603da..48a747961c0 100644 --- a/homeassistant/components/tractive/strings.sensor.json +++ b/homeassistant/components/tractive/strings.sensor.json @@ -1,10 +1,10 @@ { - "state": { - "tractive__tracker_state": { - "not_reporting": "Not reporting", - "operational": "Operational", - "system_shutdown_user": "System shutdown user", - "system_startup": "System startup" - } + "state": { + "tractive__tracker_state": { + "not_reporting": "Not reporting", + "operational": "Operational", + "system_shutdown_user": "System shutdown user", + "system_startup": "System startup" + } } } diff --git a/homeassistant/components/tractive/translations/fr.json b/homeassistant/components/tractive/translations/fr.json index 7e53cba0d74..47282699f6f 100644 --- a/homeassistant/components/tractive/translations/fr.json +++ b/homeassistant/components/tractive/translations/fr.json @@ -6,13 +6,13 @@ "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" }, "error": { - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "step": { "user": { "data": { - "email": "Email", + "email": "Courriel", "password": "Mot de passe" } } diff --git a/homeassistant/components/tradfri/__init__.py b/homeassistant/components/tradfri/__init__.py index 1971b14b2be..0054b1d7bff 100644 --- a/homeassistant/components/tradfri/__init__.py +++ b/homeassistant/components/tradfri/__init__.py @@ -141,7 +141,7 @@ async def async_setup_entry( # https://www.home-assistant.io/integrations/tradfri/ _LOGGER.warning( "Importing of Tradfri groups has been deprecated due to stability issues " - "and will be removed in Home Assistant core 2022.4" + "and will be removed in Home Assistant core 2022.5" ) # No need to load groups if the user hasn't requested it groups_commands: Command = await api( diff --git a/homeassistant/components/tradfri/base_class.py b/homeassistant/components/tradfri/base_class.py index a2bd28e3868..727e611dca4 100644 --- a/homeassistant/components/tradfri/base_class.py +++ b/homeassistant/components/tradfri/base_class.py @@ -37,11 +37,9 @@ def handle_error( return wrapper -class TradfriBaseEntity(CoordinatorEntity): +class TradfriBaseEntity(CoordinatorEntity[TradfriDeviceDataUpdateCoordinator]): """Base Tradfri device.""" - coordinator: TradfriDeviceDataUpdateCoordinator - def __init__( self, device_coordinator: TradfriDeviceDataUpdateCoordinator, diff --git a/homeassistant/components/tradfri/coordinator.py b/homeassistant/components/tradfri/coordinator.py index 039ff34c9f7..5a516e8f46e 100644 --- a/homeassistant/components/tradfri/coordinator.py +++ b/homeassistant/components/tradfri/coordinator.py @@ -76,7 +76,7 @@ class TradfriDeviceDataUpdateCoordinator(DataUpdateCoordinator[Device]): if self._exception: exc = self._exception self._exception = None # Clear stored exception - raise exc # pylint: disable-msg=raising-bad-type + raise exc except RequestError as err: raise UpdateFailed(f"Error communicating with API: {err}.") from err diff --git a/homeassistant/components/tradfri/fan.py b/homeassistant/components/tradfri/fan.py index 36e1c8b08ad..ed478025405 100644 --- a/homeassistant/components/tradfri/fan.py +++ b/homeassistant/components/tradfri/fan.py @@ -148,7 +148,6 @@ class TradfriAirPurifierFan(TradfriBaseEntity, FanEntity): async def async_turn_on( self, - speed: str | None = None, percentage: int | None = None, preset_mode: str | None = None, **kwargs: Any, diff --git a/homeassistant/components/tradfri/light.py b/homeassistant/components/tradfri/light.py index 9b6ad3e9f06..bae626017e7 100644 --- a/homeassistant/components/tradfri/light.py +++ b/homeassistant/components/tradfri/light.py @@ -78,7 +78,7 @@ async def async_setup_entry( async_add_entities(entities) -class TradfriGroup(CoordinatorEntity, LightEntity): +class TradfriGroup(CoordinatorEntity[TradfriGroupDataUpdateCoordinator], LightEntity): """The platform class for light groups required by hass.""" _attr_supported_features = SUPPORTED_GROUP_FEATURES diff --git a/homeassistant/components/trafikverket_train/__init__.py b/homeassistant/components/trafikverket_train/__init__.py index 3adcec068da..4411ccab948 100644 --- a/homeassistant/components/trafikverket_train/__init__.py +++ b/homeassistant/components/trafikverket_train/__init__.py @@ -1 +1,21 @@ """The trafikverket_train component.""" +from __future__ import annotations + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from .const import PLATFORMS + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Trafikverket Train from a config entry.""" + + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload Trafikverket Weatherstation config entry.""" + + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/trafikverket_train/config_flow.py b/homeassistant/components/trafikverket_train/config_flow.py new file mode 100644 index 00000000000..79c5978de2c --- /dev/null +++ b/homeassistant/components/trafikverket_train/config_flow.py @@ -0,0 +1,164 @@ +"""Adds config flow for Trafikverket Train integration.""" +from __future__ import annotations + +from typing import Any + +from pytrafikverket import TrafikverketTrain +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_API_KEY, CONF_NAME, CONF_WEEKDAY, WEEKDAYS +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv +import homeassistant.util.dt as dt_util + +from .const import CONF_FROM, CONF_TIME, CONF_TO, DOMAIN +from .util import create_unique_id + +ERROR_INVALID_AUTH = "Source: Security, message: Invalid authentication" +ERROR_INVALID_STATION = "Could not find a station with the specified name" +ERROR_MULTIPLE_STATION = "Found multiple stations with the specified name" + +DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_API_KEY): cv.string, + vol.Required(CONF_FROM): cv.string, + vol.Required(CONF_TO): cv.string, + vol.Optional(CONF_TIME): cv.string, + vol.Required(CONF_WEEKDAY, default=WEEKDAYS): cv.multi_select( + {day: day for day in WEEKDAYS} + ), + } +) +DATA_SCHEMA_REAUTH = vol.Schema( + { + vol.Required(CONF_API_KEY): cv.string, + } +) + + +class TVTrainConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Trafikverket Train integration.""" + + VERSION = 1 + + entry: config_entries.ConfigEntry | None + + async def validate_input( + self, api_key: str, train_from: str, train_to: str + ) -> None: + """Validate input from user input.""" + web_session = async_get_clientsession(self.hass) + train_api = TrafikverketTrain(web_session, api_key) + await train_api.async_get_train_station(train_from) + await train_api.async_get_train_station(train_to) + + async def async_step_reauth( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle re-authentication with Trafikverket.""" + + self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Confirm re-authentication with Trafikverket.""" + errors: dict[str, str] = {} + + if user_input: + api_key = user_input[CONF_API_KEY] + + assert self.entry is not None + try: + await self.validate_input( + api_key, self.entry.data[CONF_FROM], self.entry.data[CONF_TO] + ) + except ValueError as err: + if str(err) == ERROR_INVALID_AUTH: + errors["base"] = "invalid_auth" + elif str(err) == ERROR_INVALID_STATION: + errors["base"] = "invalid_station" + elif str(err) == ERROR_MULTIPLE_STATION: + errors["base"] = "more_stations" + else: + errors["base"] = "cannot_connect" + else: + self.hass.config_entries.async_update_entry( + self.entry, + data={ + **self.entry.data, + CONF_API_KEY: api_key, + }, + ) + await self.hass.config_entries.async_reload(self.entry.entry_id) + return self.async_abort(reason="reauth_successful") + + return self.async_show_form( + step_id="reauth_confirm", + data_schema=DATA_SCHEMA_REAUTH, + errors=errors, + ) + + async def async_step_import(self, config: dict[str, Any] | None) -> FlowResult: + """Import a configuration from config.yaml.""" + + return await self.async_step_user(user_input=config) + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the user step.""" + errors: dict[str, str] = {} + + if user_input is not None: + api_key: str = user_input[CONF_API_KEY] + train_from: str = user_input[CONF_FROM] + train_to: str = user_input[CONF_TO] + train_time: str | None = user_input.get(CONF_TIME) + train_days: list = user_input[CONF_WEEKDAY] + + name = f"{train_from} to {train_to}" + if train_time: + name = f"{train_from} to {train_to} at {train_time}" + + try: + await self.validate_input(api_key, train_from, train_to) + except ValueError as err: + if str(err) == ERROR_INVALID_AUTH: + errors["base"] = "invalid_auth" + elif str(err) == ERROR_INVALID_STATION: + errors["base"] = "invalid_station" + elif str(err) == ERROR_MULTIPLE_STATION: + errors["base"] = "more_stations" + else: + errors["base"] = "cannot_connect" + else: + if train_time: + if bool(dt_util.parse_time(train_time) is None): + errors["base"] = "invalid_time" + if not errors: + unique_id = create_unique_id( + train_from, train_to, train_time, train_days + ) + await self.async_set_unique_id(unique_id) + self._abort_if_unique_id_configured() + return self.async_create_entry( + title=name, + data={ + CONF_API_KEY: api_key, + CONF_NAME: name, + CONF_FROM: train_from, + CONF_TO: train_to, + CONF_TIME: train_time, + CONF_WEEKDAY: train_days, + }, + ) + + return self.async_show_form( + step_id="user", + data_schema=DATA_SCHEMA, + errors=errors, + ) diff --git a/homeassistant/components/trafikverket_train/const.py b/homeassistant/components/trafikverket_train/const.py new file mode 100644 index 00000000000..f0a6a1d6a18 --- /dev/null +++ b/homeassistant/components/trafikverket_train/const.py @@ -0,0 +1,11 @@ +"""Adds constants for Trafikverket Train integration.""" +from homeassistant.const import Platform + +DOMAIN = "trafikverket_train" +PLATFORMS = [Platform.SENSOR] +ATTRIBUTION = "Data provided by Trafikverket" + +CONF_TRAINS = "trains" +CONF_FROM = "from" +CONF_TO = "to" +CONF_TIME = "time" diff --git a/homeassistant/components/trafikverket_train/manifest.json b/homeassistant/components/trafikverket_train/manifest.json index da1d4de6c13..b3bf23ce5c4 100644 --- a/homeassistant/components/trafikverket_train/manifest.json +++ b/homeassistant/components/trafikverket_train/manifest.json @@ -4,6 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/trafikverket_train", "requirements": ["pytrafikverket==0.1.6.2"], "codeowners": ["@endor-force", "@gjohansson-ST"], + "config_flow": true, "iot_class": "cloud_polling", "loggers": ["pytrafikverket"] } diff --git a/homeassistant/components/trafikverket_train/sensor.py b/homeassistant/components/trafikverket_train/sensor.py index ef50bcc9229..82f248f9de0 100644 --- a/homeassistant/components/trafikverket_train/sensor.py +++ b/homeassistant/components/trafikverket_train/sensor.py @@ -14,21 +14,23 @@ from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, ) +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_API_KEY, CONF_NAME, CONF_WEEKDAY, WEEKDAYS from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.device_registry import DeviceEntryType +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from homeassistant.util.dt import as_utc, get_time_zone +from homeassistant.util.dt import as_utc, get_time_zone, parse_time + +from .const import CONF_FROM, CONF_TIME, CONF_TO, CONF_TRAINS, DOMAIN +from .util import create_unique_id _LOGGER = logging.getLogger(__name__) -CONF_TRAINS = "trains" -CONF_FROM = "from" -CONF_TO = "to" -CONF_TIME = "time" - ATTR_DEPARTURE_STATE = "departure_state" ATTR_CANCELED = "canceled" ATTR_DELAY_TIME = "number_of_minutes_delayed" @@ -66,43 +68,66 @@ async def async_setup_platform( async_add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: - """Set up the departure sensor.""" - httpsession = async_get_clientsession(hass) - train_api = TrafikverketTrain(httpsession, config[CONF_API_KEY]) - sensors = [] - station_cache = {} + """Import Trafikverket Train configuration from YAML.""" + _LOGGER.warning( + # Config flow added in Home Assistant Core 2022.3, remove import flow in 2022.7 + "Loading Trafikverket Train via platform setup is deprecated; Please remove it from your configuration" + ) + for train in config[CONF_TRAINS]: - try: - trainstops = [train[CONF_FROM], train[CONF_TO]] - for station in trainstops: - if station not in station_cache: - station_cache[station] = await train_api.async_get_train_station( - station - ) - except ValueError as station_error: - if "Invalid authentication" in station_error.args[0]: - _LOGGER.error("Unable to set up up component: %s", station_error) - return - _LOGGER.error( - "Problem when trying station %s to %s. Error: %s ", - train[CONF_FROM], - train[CONF_TO], - station_error, + new_config = { + CONF_API_KEY: config[CONF_API_KEY], + CONF_FROM: train[CONF_FROM], + CONF_TO: train[CONF_TO], + CONF_TIME: str(train.get(CONF_TIME)), + CONF_WEEKDAY: train.get(CONF_WEEKDAY, WEEKDAYS), + } + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=new_config, ) - continue - - sensor = TrainSensor( - train_api, - train[CONF_NAME], - station_cache[train[CONF_FROM]], - station_cache[train[CONF_TO]], - train[CONF_WEEKDAY], - train.get(CONF_TIME), ) - sensors.append(sensor) - async_add_entities(sensors, update_before_add=True) + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up the Trafikverket sensor entry.""" + + httpsession = async_get_clientsession(hass) + train_api = TrafikverketTrain(httpsession, entry.data[CONF_API_KEY]) + + try: + to_station = await train_api.async_get_train_station(entry.data[CONF_TO]) + from_station = await train_api.async_get_train_station(entry.data[CONF_FROM]) + except ValueError as error: + if "Invalid authentication" in error.args[0]: + raise ConfigEntryAuthFailed from error + raise ConfigEntryNotReady( + f"Problem when trying station {entry.data[CONF_FROM]} to {entry.data[CONF_TO]}. Error: {error} " + ) from error + + train_time = ( + parse_time(entry.data.get(CONF_TIME, "")) if entry.data.get(CONF_TIME) else None + ) + + async_add_entities( + [ + TrainSensor( + train_api, + entry.data[CONF_NAME], + from_station, + to_station, + entry.data[CONF_WEEKDAY], + train_time, + entry.entry_id, + ) + ], + True, + ) def next_weekday(fromdate: date, weekday: int) -> date: @@ -144,7 +169,8 @@ class TrainSensor(SensorEntity): from_station: str, to_station: str, weekday: list, - departuretime: time, + departuretime: time | None, + entry_id: str, ) -> None: """Initialize the sensor.""" self._train_api = train_api @@ -153,6 +179,17 @@ class TrainSensor(SensorEntity): self._to_station = to_station self._weekday = weekday self._time = departuretime + self._attr_device_info = DeviceInfo( + entry_type=DeviceEntryType.SERVICE, + identifiers={(DOMAIN, entry_id)}, + manufacturer="Trafikverket", + model="v1.2", + name=name, + configuration_url="https://api.trafikinfo.trafikverket.se/", + ) + self._attr_unique_id = create_unique_id( + from_station, to_station, departuretime, weekday + ) async def async_update(self) -> None: """Retrieve latest state.""" diff --git a/homeassistant/components/trafikverket_train/strings.json b/homeassistant/components/trafikverket_train/strings.json new file mode 100644 index 00000000000..6f6ed44f7a5 --- /dev/null +++ b/homeassistant/components/trafikverket_train/strings.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "invalid_station": "Could not find a station with the specified name", + "more_stations": "Found multiple stations with the specified name", + "invalid_time": "Invalid time provided", + "incorrect_api_key": "Invalid API key for selected account" + }, + "step": { + "user": { + "data": { + "api_key": "[%key:common::config_flow::data::api_key%]", + "to": "To station", + "from": "From station", + "time": "Time (optional)", + "weekday": "Days" + } + }, + "reauth_confirm": { + "data": { + "api_key": "[%key:common::config_flow::data::api_key%]" + } + } + } + } +} diff --git a/homeassistant/components/trafikverket_train/translations/en.json b/homeassistant/components/trafikverket_train/translations/en.json new file mode 100644 index 00000000000..dc91c2097f3 --- /dev/null +++ b/homeassistant/components/trafikverket_train/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", + "invalid_station": "Could not find a station with the specified name", + "more_stations": "Found multiple stations with the specified name", + "invalid_time": "Invalid time provided", + "incorrect_api_key": "Invalid API key for selected account" + }, + "step": { + "user": { + "data": { + "api_key": "API Key", + "to": "To station", + "from": "From station", + "time": "Time (optional)", + "weekday": "Days" + } + }, + "reauth_confirm": { + "data": { + "api_key": "API Key" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/trafikverket_train/util.py b/homeassistant/components/trafikverket_train/util.py new file mode 100644 index 00000000000..6ed672c9e7e --- /dev/null +++ b/homeassistant/components/trafikverket_train/util.py @@ -0,0 +1,15 @@ +"""Utils for trafikverket_train.""" +from __future__ import annotations + +from datetime import time + + +def create_unique_id( + from_station: str, to_station: str, depart_time: time | str | None, weekdays: list +) -> str: + """Create unique id.""" + timestr = str(depart_time) if depart_time else "" + return ( + f"{from_station.casefold().replace(' ', '')}-{to_station.casefold().replace(' ', '')}" + f"-{timestr.casefold().replace(' ', '')}-{str(weekdays)}" + ) diff --git a/homeassistant/components/trafikverket_weatherstation/__init__.py b/homeassistant/components/trafikverket_weatherstation/__init__.py index 854b5d54d70..eab54f8f45b 100644 --- a/homeassistant/components/trafikverket_weatherstation/__init__.py +++ b/homeassistant/components/trafikverket_weatherstation/__init__.py @@ -1,22 +1,21 @@ """The trafikverket_weatherstation component.""" from __future__ import annotations -import logging - from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from .const import PLATFORMS - -_LOGGER = logging.getLogger(__name__) +from .const import DOMAIN, PLATFORMS +from .coordinator import TVDataUpdateCoordinator async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Trafikverket Weatherstation from a config entry.""" - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + coordinator = TVDataUpdateCoordinator(hass, entry) + await coordinator.async_config_entry_first_refresh() + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - _LOGGER.debug("Loaded entry for %s", entry.title) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True @@ -24,10 +23,4 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload Trafikverket Weatherstation config entry.""" - unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - - if unload_ok: - _LOGGER.debug("Unloaded entry for %s", entry.title) - return unload_ok - - return False + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/trafikverket_weatherstation/config_flow.py b/homeassistant/components/trafikverket_weatherstation/config_flow.py index 5f364bb7472..345d625c7c1 100644 --- a/homeassistant/components/trafikverket_weatherstation/config_flow.py +++ b/homeassistant/components/trafikverket_weatherstation/config_flow.py @@ -1,8 +1,6 @@ """Adds config flow for Trafikverket Weather integration.""" from __future__ import annotations -from typing import Any - from pytrafikverket.trafikverket_weather import TrafikverketWeather import voluptuous as vol @@ -14,13 +12,6 @@ import homeassistant.helpers.config_validation as cv from .const import CONF_STATION, DOMAIN -DATA_SCHEMA = vol.Schema( - { - vol.Required(CONF_API_KEY): cv.string, - vol.Required(CONF_STATION): cv.string, - } -) - class TVWeatherConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for Trafikverket Weatherstation integration.""" @@ -39,18 +30,8 @@ class TVWeatherConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return str(err) return "connected" - async def async_step_import(self, config: dict[str, Any]) -> FlowResult: - """Import a configuration from config.yaml.""" - - self.context.update( - {"title_placeholders": {CONF_STATION: f"YAML import {DOMAIN}"}} - ) - - self._async_abort_entries_match({CONF_STATION: config[CONF_STATION]}) - return await self.async_step_user(user_input=config) - async def async_step_user( - self, user_input: dict[str, Any] | None = None + self, user_input: dict[str, str] | None = None ) -> FlowResult: """Handle the initial step.""" errors = {} @@ -80,6 +61,11 @@ class TVWeatherConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_show_form( step_id="user", - data_schema=DATA_SCHEMA, + data_schema=vol.Schema( + { + vol.Required(CONF_API_KEY): cv.string, + vol.Required(CONF_STATION): cv.string, + } + ), errors=errors, ) diff --git a/homeassistant/components/trafikverket_weatherstation/const.py b/homeassistant/components/trafikverket_weatherstation/const.py index 7bb53dc5356..0d4680e9b37 100644 --- a/homeassistant/components/trafikverket_weatherstation/const.py +++ b/homeassistant/components/trafikverket_weatherstation/const.py @@ -5,8 +5,6 @@ DOMAIN = "trafikverket_weatherstation" CONF_STATION = "station" PLATFORMS = [Platform.SENSOR] ATTRIBUTION = "Data provided by Trafikverket" -ATTR_MEASURE_TIME = "measure_time" -ATTR_ACTIVE = "active" NONE_IS_ZERO_SENSORS = { "air_temp", diff --git a/homeassistant/components/trafikverket_weatherstation/coordinator.py b/homeassistant/components/trafikverket_weatherstation/coordinator.py new file mode 100644 index 00000000000..990dcc0bc0b --- /dev/null +++ b/homeassistant/components/trafikverket_weatherstation/coordinator.py @@ -0,0 +1,43 @@ +"""DataUpdateCoordinator for the Trafikverket Weather integration.""" +from __future__ import annotations + +from datetime import timedelta +import logging + +from pytrafikverket.trafikverket_weather import TrafikverketWeather, WeatherStationInfo + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_API_KEY +from homeassistant.core import HomeAssistant +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import CONF_STATION, DOMAIN + +_LOGGER = logging.getLogger(__name__) +TIME_BETWEEN_UPDATES = timedelta(minutes=10) + + +class TVDataUpdateCoordinator(DataUpdateCoordinator[WeatherStationInfo]): + """A Sensibo Data Update Coordinator.""" + + def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: + """Initialize the Sensibo coordinator.""" + super().__init__( + hass, + _LOGGER, + name=DOMAIN, + update_interval=TIME_BETWEEN_UPDATES, + ) + self._weather_api = TrafikverketWeather( + async_get_clientsession(hass), entry.data[CONF_API_KEY] + ) + self._station = entry.data[CONF_STATION] + + async def _async_update_data(self) -> WeatherStationInfo: + """Fetch data from Trafikverket.""" + try: + weatherdata = await self._weather_api.async_get_weather(self._station) + except ValueError as error: + raise UpdateFailed from error + return weatherdata diff --git a/homeassistant/components/trafikverket_weatherstation/sensor.py b/homeassistant/components/trafikverket_weatherstation/sensor.py index 92211b4d681..e659e42f82b 100644 --- a/homeassistant/components/trafikverket_weatherstation/sensor.py +++ b/homeassistant/components/trafikverket_weatherstation/sensor.py @@ -1,27 +1,17 @@ """Weather information for air and road temperature (by Trafikverket).""" from __future__ import annotations -import asyncio from dataclasses import dataclass -from datetime import timedelta -import logging - -import aiohttp -from pytrafikverket.trafikverket_weather import TrafikverketWeather, WeatherStationInfo -import voluptuous as vol +from datetime import datetime from homeassistant.components.sensor import ( - PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, SensorEntityDescription, SensorStateClass, ) -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - CONF_API_KEY, - CONF_MONITORED_CONDITIONS, - CONF_NAME, DEGREE, LENGTH_MILLIMETERS, PERCENTAGE, @@ -29,28 +19,17 @@ from homeassistant.const import ( TEMP_CELSIUS, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers.aiohttp_client import async_get_clientsession -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from homeassistant.util import Throttle +from homeassistant.helpers.typing import StateType +from homeassistant.helpers.update_coordinator import CoordinatorEntity +from homeassistant.util.dt import as_utc, get_time_zone -from .const import ( - ATTR_ACTIVE, - ATTR_MEASURE_TIME, - ATTRIBUTION, - CONF_STATION, - DOMAIN, - NONE_IS_ZERO_SENSORS, -) +from .const import ATTRIBUTION, CONF_STATION, DOMAIN, NONE_IS_ZERO_SENSORS +from .coordinator import TVDataUpdateCoordinator -_LOGGER = logging.getLogger(__name__) - -MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=10) - -SCAN_INTERVAL = timedelta(seconds=300) +STOCKHOLM_TIMEZONE = get_time_zone("Europe/Stockholm") @dataclass @@ -149,59 +128,41 @@ SENSOR_TYPES: tuple[TrafikverketSensorEntityDescription, ...] = ( icon="mdi:weather-pouring", entity_registry_enabled_default=False, ), + TrafikverketSensorEntityDescription( + key="measure_time", + api_key="measure_time", + name="Measure Time", + icon="mdi:clock", + entity_registry_enabled_default=False, + device_class=SensorDeviceClass.TIMESTAMP, + ), ) -SENSOR_KEYS = [desc.key for desc in SENSOR_TYPES] - -PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_NAME): cv.string, - vol.Required(CONF_API_KEY): cv.string, - vol.Required(CONF_STATION): cv.string, - vol.Required(CONF_MONITORED_CONDITIONS, default=[]): [vol.In(SENSOR_KEYS)], - } -) - - -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Import Trafikverket Weather configuration from YAML.""" - _LOGGER.warning( - # Config flow added in Home Assistant Core 2021.12, remove import flow in 2022.4 - "Loading Trafikverket Weather via platform setup is deprecated; Please remove it from your configuration" - ) - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data=config, - ) - ) - async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the Trafikverket sensor entry.""" - web_session = async_get_clientsession(hass) - weather_api = TrafikverketWeather(web_session, entry.data[CONF_API_KEY]) + coordinator: TVDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - entities = [ + async_add_entities( TrafikverketWeatherStation( - weather_api, entry.entry_id, entry.data[CONF_STATION], description + coordinator, entry.entry_id, entry.data[CONF_STATION], description ) for description in SENSOR_TYPES - ] - - async_add_entities(entities, True) + ) -class TrafikverketWeatherStation(SensorEntity): +def _to_datetime(measuretime: str) -> datetime: + """Return isoformatted utc time.""" + time_obj = datetime.strptime(measuretime, "%Y-%m-%dT%H:%M:%S") + return as_utc(time_obj.replace(tzinfo=STOCKHOLM_TIMEZONE)) + + +class TrafikverketWeatherStation( + CoordinatorEntity[TVDataUpdateCoordinator], SensorEntity +): """Representation of a Trafikverket sensor.""" entity_description: TrafikverketSensorEntityDescription @@ -209,17 +170,16 @@ class TrafikverketWeatherStation(SensorEntity): def __init__( self, - weather_api: TrafikverketWeather, + coordinator: TVDataUpdateCoordinator, entry_id: str, sensor_station: str, description: TrafikverketSensorEntityDescription, ) -> None: """Initialize the sensor.""" + super().__init__(coordinator) self.entity_description = description self._attr_name = f"{sensor_station} {description.name}" self._attr_unique_id = f"{entry_id}_{description.key}" - self._station = sensor_station - self._weather_api = weather_api self._attr_device_info = DeviceInfo( entry_type=DeviceEntryType.SERVICE, identifiers={(DOMAIN, entry_id)}, @@ -228,26 +188,23 @@ class TrafikverketWeatherStation(SensorEntity): name=sensor_station, configuration_url="https://api.trafikinfo.trafikverket.se/", ) - self._weather: WeatherStationInfo | None = None - @Throttle(MIN_TIME_BETWEEN_UPDATES) - async def async_update(self) -> None: - """Get the latest data from Trafikverket and updates the states.""" - try: - self._weather = await self._weather_api.async_get_weather(self._station) - except (asyncio.TimeoutError, aiohttp.ClientError, ValueError) as error: - _LOGGER.error("Could not fetch weather data: %s", error) - return - self._attr_native_value = getattr( - self._weather, self.entity_description.api_key + @property + def native_value(self) -> StateType | datetime: + """Return state of sensor.""" + if self.entity_description.api_key == "measure_time": + return _to_datetime(self.coordinator.data.measure_time) + + state: StateType = getattr( + self.coordinator.data, self.entity_description.api_key ) - if ( - self._attr_native_value is None - and self.entity_description.key in NONE_IS_ZERO_SENSORS - ): - self._attr_native_value = 0 - self._attr_extra_state_attributes = { - ATTR_ACTIVE: self._weather.active, - ATTR_MEASURE_TIME: self._weather.measure_time, - } + # For zero value state the api reports back None for certain sensors. + if state is None and self.entity_description.key in NONE_IS_ZERO_SENSORS: + return 0 + return state + + @property + def available(self) -> bool: + """Return if entity is available.""" + return self.coordinator.data.active and super().available diff --git a/homeassistant/components/trafikverket_weatherstation/strings.json b/homeassistant/components/trafikverket_weatherstation/strings.json index d4a1eb69f1d..1ac4bbed01e 100644 --- a/homeassistant/components/trafikverket_weatherstation/strings.json +++ b/homeassistant/components/trafikverket_weatherstation/strings.json @@ -1,21 +1,21 @@ { - "config": { - "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_account%]" - }, - "error": { - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", - "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", - "invalid_station": "Could not find a weather station with the specified name", - "more_stations": "Found multiple weather stations with the specified name" - }, - "step": { - "user": { - "data": { - "api_key": "[%key:common::config_flow::data::api_key%]", - "station": "Station" - } - } + "config": { + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]" + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "invalid_station": "Could not find a weather station with the specified name", + "more_stations": "Found multiple weather stations with the specified name" + }, + "step": { + "user": { + "data": { + "api_key": "[%key:common::config_flow::data::api_key%]", + "station": "Station" } + } } - } \ No newline at end of file + } +} diff --git a/homeassistant/components/trafikverket_weatherstation/translations/fr.json b/homeassistant/components/trafikverket_weatherstation/translations/fr.json index d988b3aba97..55a88c6a746 100644 --- a/homeassistant/components/trafikverket_weatherstation/translations/fr.json +++ b/homeassistant/components/trafikverket_weatherstation/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "invalid_station": "Impossible de trouver une station m\u00e9t\u00e9o avec le nom sp\u00e9cifi\u00e9", "more_stations": "Trouv\u00e9 plusieurs stations m\u00e9t\u00e9o avec le nom sp\u00e9cifi\u00e9" }, diff --git a/homeassistant/components/transmission/translations/fr.json b/homeassistant/components/transmission/translations/fr.json index 64efb47c8c3..f027b6909e8 100644 --- a/homeassistant/components/transmission/translations/fr.json +++ b/homeassistant/components/transmission/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "name_exists": "Ce nom est d\u00e9j\u00e0 utilis\u00e9" }, "step": { diff --git a/homeassistant/components/tts/__init__.py b/homeassistant/components/tts/__init__.py index 5e6629ca2a2..c001fb6b89b 100644 --- a/homeassistant/components/tts/__init__.py +++ b/homeassistant/components/tts/__init__.py @@ -17,6 +17,7 @@ from aiohttp import web import mutagen from mutagen.id3 import ID3, TextFrame as ID3Text import voluptuous as vol +import yarl from homeassistant.components.http import HomeAssistantView from homeassistant.components.media_player.const import ( @@ -41,6 +42,7 @@ from homeassistant.helpers.network import get_url from homeassistant.helpers.service import async_set_service_schema from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.setup import async_prepare_setup_platform +from homeassistant.util.network import normalize_url from homeassistant.util.yaml import load_yaml from .const import DOMAIN @@ -92,6 +94,16 @@ def _deprecated_platform(value): return value +def valid_base_url(value: str) -> str: + """Validate base url, return value.""" + url = yarl.URL(cv.url(value)) + + if url.path != "/": + raise vol.Invalid("Path should be empty") + + return normalize_url(value) + + PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend( { vol.Required(CONF_PLATFORM): vol.All(cv.string, _deprecated_platform), @@ -100,7 +112,7 @@ PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend( vol.Optional(CONF_TIME_MEMORY, default=DEFAULT_TIME_MEMORY): vol.All( vol.Coerce(int), vol.Range(min=60, max=57600) ), - vol.Optional(CONF_BASE_URL): cv.string, + vol.Optional(CONF_BASE_URL): valid_base_url, vol.Optional(CONF_SERVICE_NAME): cv.string, } ) diff --git a/homeassistant/components/tuya/__init__.py b/homeassistant/components/tuya/__init__.py index ad6c8142828..97422179960 100644 --- a/homeassistant/components/tuya/__init__.py +++ b/homeassistant/components/tuya/__init__.py @@ -234,6 +234,10 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: class DeviceListener(TuyaDeviceListener): """Device Update Listener.""" + # pylint: disable=arguments-differ + # Library incorrectly defines methods as 'classmethod' + # https://github.com/tuya/tuya-iot-python-sdk/pull/48 + def __init__( self, hass: HomeAssistant, diff --git a/homeassistant/components/tuya/fan.py b/homeassistant/components/tuya/fan.py index e2e98e3fd5c..be05acef3de 100644 --- a/homeassistant/components/tuya/fan.py +++ b/homeassistant/components/tuya/fan.py @@ -161,7 +161,6 @@ class TuyaFanEntity(TuyaEntity, FanEntity): def turn_on( self, - speed: str = None, percentage: int = None, preset_mode: str = None, **kwargs: Any, diff --git a/homeassistant/components/tuya/light.py b/homeassistant/components/tuya/light.py index 4c35c850c33..c4f874d0687 100644 --- a/homeassistant/components/tuya/light.py +++ b/homeassistant/components/tuya/light.py @@ -111,7 +111,7 @@ LIGHTS: dict[str, tuple[TuyaLightEntityDescription, ...]] = { # Not documented # Based on multiple reports: manufacturer customized Dimmer 2 switches TuyaLightEntityDescription( - key=DPCode.SWITCH_LED_1, + key=DPCode.SWITCH_1, name="Light", brightness=DPCode.BRIGHT_VALUE_1, ), diff --git a/homeassistant/components/tuya/strings.select.json b/homeassistant/components/tuya/strings.select.json index a765912d036..2ae5ff14c5b 100644 --- a/homeassistant/components/tuya/strings.select.json +++ b/homeassistant/components/tuya/strings.select.json @@ -50,84 +50,84 @@ "switch": "Switch" }, "tuya__vacuum_cistern": { - "low": "Low", - "middle": "Middle", - "high": "High", - "closed": "Closed" + "low": "Low", + "middle": "Middle", + "high": "High", + "closed": "Closed" }, "tuya__vacuum_collection": { - "small": "Small", - "middle": "Middle", - "large": "Large" + "small": "Small", + "middle": "Middle", + "large": "Large" }, "tuya__vacuum_mode": { - "standby": "Standby", - "random": "Random", - "smart": "Smart", - "wall_follow": "Follow Wall", - "mop": "Mop", - "spiral": "Spiral", - "left_spiral": "Spiral Left", - "right_spiral": "Spiral Right", - "bow": "Bow", - "left_bow": "Bow Left", - "right_bow": "Bow Right", - "partial_bow": "Bow Partially", - "chargego": "Return to dock", - "single": "Single", - "zone": "Zone", - "pose": "Pose", - "point": "Point", - "part": "Part", - "pick_zone": "Pick Zone" + "standby": "Standby", + "random": "Random", + "smart": "Smart", + "wall_follow": "Follow Wall", + "mop": "Mop", + "spiral": "Spiral", + "left_spiral": "Spiral Left", + "right_spiral": "Spiral Right", + "bow": "Bow", + "left_bow": "Bow Left", + "right_bow": "Bow Right", + "partial_bow": "Bow Partially", + "chargego": "Return to dock", + "single": "Single", + "zone": "Zone", + "pose": "Pose", + "point": "Point", + "part": "Part", + "pick_zone": "Pick Zone" }, "tuya__fan_angle": { - "30": "30°", - "60": "60°", - "90": "90°" + "30": "30°", + "60": "60°", + "90": "90°" }, "tuya__curtain_mode": { - "morning": "Morning", - "night": "Night" + "morning": "Morning", + "night": "Night" }, "tuya__curtain_motor_mode": { - "forward": "Forward", - "back": "Back" + "forward": "Forward", + "back": "Back" }, "tuya__countdown": { - "cancel": "Cancel", - "1h": "1 hour", - "2h": "2 hours", - "3h": "3 hours", - "4h": "4 hours", - "5h": "5 hours", - "6h": "6 hours" + "cancel": "Cancel", + "1h": "1 hour", + "2h": "2 hours", + "3h": "3 hours", + "4h": "4 hours", + "5h": "5 hours", + "6h": "6 hours" }, "tuya__humidifier_spray_mode": { - "auto": "Auto", - "health": "Health", - "sleep": "Sleep", - "humidity": "Humidity", - "work": "Work" + "auto": "Auto", + "health": "Health", + "sleep": "Sleep", + "humidity": "Humidity", + "work": "Work" }, "tuya__humidifier_level": { - "level_1": "Level 1", - "level_2": "Level 2", - "level_3": "Level 3", - "level_4": "Level 4", - "level_5": "Level 5", - "level_6": "Level 6", - "level_7": "Level 7", - "level_8": "Level 8", - "level_9": "Level 9", - "level_10": "Level 10" + "level_1": "Level 1", + "level_2": "Level 2", + "level_3": "Level 3", + "level_4": "Level 4", + "level_5": "Level 5", + "level_6": "Level 6", + "level_7": "Level 7", + "level_8": "Level 8", + "level_9": "Level 9", + "level_10": "Level 10" }, "tuya__humidifier_moodlighting": { - "1": "Mood 1", - "2": "Mood 2", - "3": "Mood 3", - "4": "Mood 4", - "5": "Mood 5" + "1": "Mood 1", + "2": "Mood 2", + "3": "Mood 3", + "4": "Mood 4", + "5": "Mood 5" } } } diff --git a/homeassistant/components/tuya/translations/fr.json b/homeassistant/components/tuya/translations/fr.json index 0f8dd7b6742..c28e62e7ab4 100644 --- a/homeassistant/components/tuya/translations/fr.json +++ b/homeassistant/components/tuya/translations/fr.json @@ -2,11 +2,11 @@ "config": { "abort": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." }, "error": { - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "login_error": "Erreur de connexion ( {code} ): {msg}" }, "flow_title": "Configuration Tuya", @@ -45,7 +45,7 @@ "cannot_connect": "\u00c9chec de connexion" }, "error": { - "dev_multi_type": "Plusieurs p\u00e9riph\u00e9riques s\u00e9lectionn\u00e9s \u00e0 configurer doivent \u00eatre du m\u00eame type", + "dev_multi_type": "Si plusieurs appareils sont s\u00e9lectionn\u00e9s pour \u00eatre configur\u00e9s, ils doivent tous \u00eatre du m\u00eame type", "dev_not_config": "Type d'appareil non configurable", "dev_not_found": "Appareil non trouv\u00e9" }, diff --git a/homeassistant/components/tuya/translations/he.json b/homeassistant/components/tuya/translations/he.json index 87168fcce41..02e2dc94776 100644 --- a/homeassistant/components/tuya/translations/he.json +++ b/homeassistant/components/tuya/translations/he.json @@ -6,7 +6,8 @@ "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." }, "error": { - "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", + "login_error": "\u05e9\u05d2\u05d9\u05d0\u05ea \u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea ({code}): {msg}" }, "flow_title": "\u05ea\u05e6\u05d5\u05e8\u05ea Tuya", "step": { @@ -52,9 +53,15 @@ "brightness_range_mode": "\u05d8\u05d5\u05d5\u05d7 \u05d1\u05d4\u05d9\u05e8\u05d5\u05ea \u05d4\u05de\u05e9\u05de\u05e9 \u05d0\u05ea \u05d4\u05d4\u05ea\u05e7\u05df", "max_kelvin": "\u05d8\u05de\u05e4\u05e8\u05d8\u05d5\u05e8\u05ea \u05e6\u05d1\u05e2 \u05de\u05e8\u05d1\u05d9\u05ea \u05d4\u05e0\u05ea\u05de\u05db\u05ea \u05d1\u05e7\u05dc\u05d5\u05d5\u05d9\u05df", "min_kelvin": "\u05d8\u05de\u05e4\u05e8\u05d8\u05d5\u05e8\u05ea \u05e6\u05d1\u05e2 \u05de\u05d9\u05e0\u05d9\u05de\u05dc\u05d9\u05ea \u05d4\u05e0\u05ea\u05de\u05db\u05ea \u05d1\u05e7\u05dc\u05d5\u05d5\u05d9\u05df", + "set_temp_divided": "\u05d4\u05e9\u05ea\u05de\u05e9 \u05d1\u05e2\u05e8\u05da \u05d8\u05de\u05e4\u05e8\u05d8\u05d5\u05e8\u05d4 \u05de\u05d7\u05d5\u05dc\u05e7 \u05e2\u05d1\u05d5\u05e8 \u05d4\u05e4\u05e7\u05d5\u05d3\u05d4 '\u05d8\u05de\u05e4\u05e8\u05d8\u05d5\u05e8\u05d4 \u05de\u05d5\u05d2\u05d3\u05e8\u05ea'", "support_color": "\u05db\u05e4\u05d4 \u05ea\u05de\u05d9\u05db\u05d4 \u05d1\u05e6\u05d1\u05e2", + "temp_step_override": "\u05e9\u05dc\u05d1 \u05d8\u05de\u05e4\u05e8\u05d8\u05d5\u05e8\u05ea \u05d4\u05d9\u05e2\u05d3", "unit_of_measurement": "\u05d9\u05d7\u05d9\u05d3\u05ea \u05d8\u05de\u05e4\u05e8\u05d8\u05d5\u05e8\u05d4 \u05d4\u05de\u05e9\u05de\u05e9\u05ea \u05d0\u05ea \u05d4\u05d4\u05ea\u05e7\u05df" - } + }, + "title": "\u05e7\u05d1\u05d9\u05e2\u05ea \u05ea\u05e6\u05d5\u05e8\u05d4 \u05e9\u05dc \u05d4\u05ea\u05e7\u05df \u05d8\u05d5\u05d9\u05d4" + }, + "init": { + "title": "\u05e7\u05d1\u05d9\u05e2\u05ea \u05ea\u05e6\u05d5\u05e8\u05d4 \u05e9\u05dc \u05d0\u05e4\u05e9\u05e8\u05d5\u05d9\u05d5\u05ea \u05d8\u05d5\u05d9\u05d4" } } } diff --git a/homeassistant/components/tuya/translations/nl.json b/homeassistant/components/tuya/translations/nl.json index 88078f8e6a5..e12c3ba29f2 100644 --- a/homeassistant/components/tuya/translations/nl.json +++ b/homeassistant/components/tuya/translations/nl.json @@ -73,7 +73,7 @@ "discovery_interval": "Polling-interval van nieuwe apparaten in seconden", "list_devices": "Selecteer de te configureren apparaten of laat leeg om de configuratie op te slaan", "query_device": "Selecteer apparaat dat query-methode zal gebruiken voor snellere statusupdate", - "query_interval": "Peilinginterval van het apparaat in seconden" + "query_interval": "Ververstijd van het apparaat in seconden" }, "description": "Stel de waarden voor het pollinginterval niet te laag in, anders zullen de oproepen geen foutmelding in het logboek genereren", "title": "Configureer Tuya opties" diff --git a/homeassistant/components/tuya/translations/select.es.json b/homeassistant/components/tuya/translations/select.es.json index adc306feae4..7dc4cf1067b 100644 --- a/homeassistant/components/tuya/translations/select.es.json +++ b/homeassistant/components/tuya/translations/select.es.json @@ -19,6 +19,10 @@ "6h": "6 horas", "cancel": "Cancelar" }, + "tuya__curtain_mode": { + "morning": "Ma\u00f1ana", + "night": "Noche" + }, "tuya__decibel_sensitivity": { "0": "Sensibilidad baja", "1": "Sensibilidad alta" @@ -29,7 +33,15 @@ }, "tuya__humidifier_level": { "level_1": "Nivel 1", - "level_10": "Nivel 10" + "level_10": "Nivel 10", + "level_2": "Nivel 2", + "level_3": "Nivel 3", + "level_4": "Nivel 4", + "level_5": "Nivel 5", + "level_6": "Nivel 6", + "level_7": "Nivel 7", + "level_8": "Nivel 8", + "level_9": "Nivel 9" }, "tuya__humidifier_spray_mode": { "health": "Salud", diff --git a/homeassistant/components/tuya/translations/select.fr.json b/homeassistant/components/tuya/translations/select.fr.json index ab67be514bc..c525238e4de 100644 --- a/homeassistant/components/tuya/translations/select.fr.json +++ b/homeassistant/components/tuya/translations/select.fr.json @@ -7,16 +7,16 @@ }, "tuya__basic_nightvision": { "0": "automatique", - "1": "Inactif", - "2": "Actif" + "1": "D\u00e9sactiv\u00e9", + "2": "Activ\u00e9" }, "tuya__countdown": { - "1h": "1 heure", - "2h": "2 heures", - "3h": "3 heures", - "4h": "4 heures", - "5h": "5 heures", - "6h": "6 heures", + "1h": "1\u00a0heure", + "2h": "2\u00a0heures", + "3h": "3\u00a0heures", + "4h": "4\u00a0heures", + "5h": "5\u00a0heures", + "6h": "6\u00a0heures", "cancel": "Annuler" }, "tuya__curtain_mode": { @@ -24,8 +24,8 @@ "night": "Nuit" }, "tuya__curtain_motor_mode": { - "back": "Retour", - "forward": "Avance rapide" + "back": "En arri\u00e8re", + "forward": "En avant" }, "tuya__decibel_sensitivity": { "0": "Faible sensibilit\u00e9", @@ -76,7 +76,7 @@ "led": "LED" }, "tuya__light_mode": { - "none": "Inactif", + "none": "D\u00e9sactiv\u00e9", "pos": "Indiquer l'emplacement du commutateur", "relay": "Indiquer l\u2019\u00e9tat marche/arr\u00eat de l\u2019interrupteur" }, @@ -92,10 +92,10 @@ "tuya__relay_status": { "last": "Se souvenir du dernier \u00e9tat", "memory": "Se souvenir du dernier \u00e9tat", - "off": "Inactif", - "on": "Actif", - "power_off": "Inactif", - "power_on": "Actif" + "off": "D\u00e9sactiv\u00e9", + "on": "Activ\u00e9", + "power_off": "D\u00e9sactiv\u00e9", + "power_on": "Activ\u00e9" }, "tuya__vacuum_cistern": { "closed": "Ferm\u00e9", diff --git a/homeassistant/components/tuya/translations/select.he.json b/homeassistant/components/tuya/translations/select.he.json index eacaecacb07..f31e63515c8 100644 --- a/homeassistant/components/tuya/translations/select.he.json +++ b/homeassistant/components/tuya/translations/select.he.json @@ -1,10 +1,12 @@ { "state": { "tuya__basic_anti_flickr": { + "0": "\u05de\u05d5\u05e9\u05d1\u05ea", "1": "50 \u05d4\u05e8\u05e5", "2": "60 \u05d4\u05e8\u05e5" }, "tuya__basic_nightvision": { + "0": "\u05d0\u05d5\u05d8\u05d5\u05de\u05d8\u05d9", "1": "\u05db\u05d1\u05d5\u05d9", "2": "\u05de\u05d5\u05e4\u05e2\u05dc" }, @@ -25,6 +27,10 @@ "back": "\u05d7\u05d6\u05d5\u05e8", "forward": "\u05e7\u05d3\u05d9\u05de\u05d4" }, + "tuya__decibel_sensitivity": { + "0": "\u05e8\u05d2\u05d9\u05e9\u05d5\u05ea \u05e0\u05de\u05d5\u05db\u05d4", + "1": "\u05e8\u05d2\u05d9\u05e9\u05d5\u05ea \u05d2\u05d1\u05d5\u05d4\u05d4" + }, "tuya__fan_angle": { "30": "30\u00b0", "60": "60\u00b0", @@ -60,17 +66,68 @@ "sleep": "\u05e9\u05d9\u05e0\u05d4", "work": "\u05e2\u05d1\u05d5\u05d3\u05d4" }, + "tuya__ipc_work_mode": { + "0": "\u05de\u05e6\u05d1 \u05e6\u05e8\u05d9\u05db\u05ea \u05d7\u05e9\u05de\u05dc \u05e0\u05de\u05d5\u05db\u05d4", + "1": "\u05de\u05e6\u05d1 \u05e2\u05d1\u05d5\u05d3\u05d4 \u05de\u05ea\u05de\u05e9\u05da" + }, "tuya__led_type": { + "halogen": "\u05d4\u05dc\u05d5\u05d2\u05df", + "incandescent": "\u05dc\u05d9\u05d1\u05d5\u05df", "led": "\u05dc\u05d3" }, "tuya__light_mode": { - "none": "\u05db\u05d1\u05d5\u05d9" + "none": "\u05db\u05d1\u05d5\u05d9", + "pos": "\u05e6\u05d9\u05d9\u05df \u05de\u05d9\u05e7\u05d5\u05dd \u05de\u05ea\u05d2", + "relay": "\u05e6\u05d9\u05d5\u05df \u05de\u05e6\u05d1 \u05d4\u05e4\u05e2\u05dc\u05d4/\u05db\u05d9\u05d1\u05d5\u05d9 \u05e9\u05dc \u05de\u05ea\u05d2" + }, + "tuya__motion_sensitivity": { + "0": "\u05e8\u05d2\u05d9\u05e9\u05d5\u05ea \u05e0\u05de\u05d5\u05db\u05d4", + "1": "\u05e8\u05d2\u05d9\u05e9\u05d5\u05ea \u05d1\u05d9\u05e0\u05d5\u05e0\u05d9\u05ea", + "2": "\u05e8\u05d2\u05d9\u05e9\u05d5\u05ea \u05d2\u05d1\u05d5\u05d4\u05d4" + }, + "tuya__record_mode": { + "1": "\u05d4\u05e7\u05dc\u05d8 \u05d0\u05d9\u05e8\u05d5\u05e2\u05d9\u05dd \u05d1\u05dc\u05d1\u05d3", + "2": "\u05d4\u05e7\u05dc\u05d8\u05d4 \u05e8\u05e6\u05d9\u05e4\u05d4" }, "tuya__relay_status": { + "last": "\u05d6\u05db\u05d5\u05e8 \u05de\u05e6\u05d1 \u05d0\u05d7\u05e8\u05d5\u05df", + "memory": "\u05d6\u05db\u05d5\u05e8 \u05de\u05e6\u05d1 \u05d0\u05d7\u05e8\u05d5\u05df", "off": "\u05db\u05d1\u05d5\u05d9", "on": "\u05de\u05d5\u05e4\u05e2\u05dc", "power_off": "\u05db\u05d1\u05d5\u05d9", "power_on": "\u05de\u05d5\u05e4\u05e2\u05dc" + }, + "tuya__vacuum_cistern": { + "closed": "\u05e1\u05d2\u05d5\u05e8", + "high": "\u05d2\u05d1\u05d5\u05d4", + "low": "\u05e0\u05de\u05d5\u05da", + "middle": "\u05d0\u05de\u05e6\u05e2" + }, + "tuya__vacuum_collection": { + "large": "\u05d2\u05d3\u05d5\u05dc", + "middle": "\u05d0\u05de\u05e6\u05e2", + "small": "\u05e7\u05d8\u05df" + }, + "tuya__vacuum_mode": { + "bow": "\u05e7\u05e9\u05ea", + "chargego": "\u05d7\u05d5\u05d6\u05e8 \u05dc\u05ea\u05d7\u05e0\u05ea \u05e2\u05d2\u05d9\u05e0\u05d4", + "left_bow": "\u05e7\u05e9\u05ea \u05e9\u05de\u05d0\u05dc\u05d4", + "left_spiral": "\u05e1\u05e4\u05d9\u05e8\u05dc\u05d4 \u05e9\u05de\u05d0\u05dc\u05d4", + "mop": "\u05e1\u05d7\u05d1\u05d4", + "part": "\u05d7\u05dc\u05e7", + "partial_bow": "\u05e7\u05e9\u05ea \u05d7\u05dc\u05e7\u05d9\u05ea", + "pick_zone": "\u05d1\u05d7\u05e8 \u05d0\u05d6\u05d5\u05e8", + "point": "\u05e0\u05e7\u05d5\u05d3\u05d4", + "pose": "\u05e4\u05d5\u05d6\u05d4", + "random": "\u05d0\u05e7\u05e8\u05d0\u05d9", + "right_bow": "\u05e7\u05e9\u05ea \u05d9\u05de\u05d9\u05e0\u05d4", + "right_spiral": "\u05e1\u05e4\u05d9\u05e8\u05dc\u05d4 \u05d9\u05de\u05d9\u05e0\u05d4", + "single": "\u05d9\u05d7\u05d9\u05d3", + "smart": "\u05d7\u05db\u05dd", + "spiral": "\u05e1\u05e4\u05d9\u05e8\u05dc\u05d4", + "standby": "\u05d4\u05de\u05ea\u05e0\u05d4", + "wall_follow": "\u05e2\u05e7\u05d5\u05d1 \u05d0\u05d7\u05e8 \u05e7\u05d9\u05e8", + "zone": "\u05d0\u05d6\u05d5\u05e8" } } } \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/select.id.json b/homeassistant/components/tuya/translations/select.id.json index 9b404daf612..2427f21f58e 100644 --- a/homeassistant/components/tuya/translations/select.id.json +++ b/homeassistant/components/tuya/translations/select.id.json @@ -109,13 +109,18 @@ "small": "Kecil" }, "tuya__vacuum_mode": { + "bow": "Zig Zag", "chargego": "Kembali ke dock", + "left_bow": "Zig Zag Kiri", "left_spiral": "Spiral Kiri", "mop": "Pel", "part": "Bagian", + "partial_bow": "Zig Zag Sebagian", "pick_zone": "Pilih Zona", "point": "Titik", + "pose": "Area", "random": "Acak", + "right_bow": "Bungkuk Kanan", "right_spiral": "Spiral Kanan", "single": "Tunggal", "smart": "Cerdas", diff --git a/homeassistant/components/tuya/translations/sensor.he.json b/homeassistant/components/tuya/translations/sensor.he.json index 73e5af2fc79..95664a2bd66 100644 --- a/homeassistant/components/tuya/translations/sensor.he.json +++ b/homeassistant/components/tuya/translations/sensor.he.json @@ -2,7 +2,20 @@ "state": { "tuya__air_quality": { "good": "\u05d8\u05d5\u05d1", - "great": "\u05e0\u05d4\u05d3\u05e8" + "great": "\u05e0\u05d4\u05d3\u05e8", + "mild": "\u05de\u05ea\u05d5\u05df", + "severe": "\u05d7\u05de\u05d5\u05e8" + }, + "tuya__status": { + "boiling_temp": "\u05d8\u05de\u05e4\u05e8\u05d8\u05d5\u05e8\u05ea \u05e8\u05ea\u05d9\u05d7\u05d4", + "cooling": "\u05e7\u05d9\u05e8\u05d5\u05e8", + "heating": "\u05d7\u05d9\u05de\u05d5\u05dd", + "heating_temp": "\u05d8\u05de\u05e4\u05e8\u05d8\u05d5\u05e8\u05ea \u05d7\u05d9\u05de\u05d5\u05dd", + "reserve_1": "\u05e9\u05de\u05d5\u05e8 1", + "reserve_2": "\u05e9\u05de\u05d5\u05e8 2", + "reserve_3": "\u05e9\u05de\u05d5\u05e8 3", + "standby": "\u05d4\u05de\u05ea\u05e0\u05d4", + "warm": "\u05e9\u05d9\u05de\u05d5\u05e8 \u05d7\u05d5\u05dd" } } } \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/zh-Hant.json b/homeassistant/components/tuya/translations/zh-Hant.json index b905eb0c1e3..e9898f782af 100644 --- a/homeassistant/components/tuya/translations/zh-Hant.json +++ b/homeassistant/components/tuya/translations/zh-Hant.json @@ -32,7 +32,7 @@ "password": "\u5bc6\u78bc", "platform": "\u5e33\u6236\u8a3b\u518a\u6240\u5728\u4f4d\u7f6e", "region": "\u5340\u57df", - "tuya_project_type": "Tuya \u96f2\u5c08\u6848\u985e\u578b", + "tuya_project_type": "Tuya \u96f2\u5c08\u6848\u985e\u5225", "username": "\u5e33\u865f" }, "description": "\u8f38\u5165 Tuya \u6191\u8b49", @@ -45,8 +45,8 @@ "cannot_connect": "\u9023\u7dda\u5931\u6557" }, "error": { - "dev_multi_type": "\u591a\u91cd\u9078\u64c7\u8a2d\u88dd\u7f6e\u4ee5\u8a2d\u5b9a\u4f7f\u7528\u76f8\u540c\u985e\u578b", - "dev_not_config": "\u88dd\u7f6e\u985e\u578b\u7121\u6cd5\u8a2d\u5b9a", + "dev_multi_type": "\u591a\u91cd\u9078\u64c7\u8a2d\u88dd\u7f6e\u4ee5\u8a2d\u5b9a\u4f7f\u7528\u76f8\u540c\u985e\u5225", + "dev_not_config": "\u88dd\u7f6e\u985e\u5225\u7121\u6cd5\u8a2d\u5b9a", "dev_not_found": "\u627e\u4e0d\u5230\u88dd\u7f6e" }, "step": { @@ -70,8 +70,8 @@ }, "init": { "data": { - "discovery_interval": "\u63a2\u7d22\u88dd\u7f6e\u66f4\u65b0\u79d2\u9593\u8ddd", - "list_devices": "\u9078\u64c7\u88dd\u7f6e\u4ee5\u8a2d\u5b9a\u3001\u6216\u4fdd\u6301\u7a7a\u767d\u4ee5\u5132\u5b58\u8a2d\u5b9a", + "discovery_interval": "\u641c\u7d22\u88dd\u7f6e\u66f4\u65b0\u79d2\u9593\u8ddd", + "list_devices": "\u9078\u64c7\u6240\u8981\u8a2d\u5b9a\u7684\u88dd\u7f6e\u3001\u6216\u4fdd\u6301\u7a7a\u767d\u4ee5\u5132\u5b58\u8a2d\u5b9a", "query_device": "\u9078\u64c7\u88dd\u7f6e\u5c07\u4f7f\u7528\u67e5\u8a62\u65b9\u5f0f\u4ee5\u7372\u5f97\u66f4\u5feb\u7684\u72c0\u614b\u66f4\u65b0", "query_interval": "\u67e5\u8a62\u88dd\u7f6e\u66f4\u65b0\u79d2\u9593\u8ddd" }, diff --git a/homeassistant/components/twentemilieu/__init__.py b/homeassistant/components/twentemilieu/__init__.py index 576ab2068a7..bbe306392ba 100644 --- a/homeassistant/components/twentemilieu/__init__.py +++ b/homeassistant/components/twentemilieu/__init__.py @@ -20,7 +20,7 @@ SCAN_INTERVAL = timedelta(seconds=3600) SERVICE_UPDATE = "update" SERVICE_SCHEMA = vol.Schema({vol.Optional(CONF_ID): cv.string}) -PLATFORMS = [Platform.SENSOR] +PLATFORMS = [Platform.CALENDAR, Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @@ -34,7 +34,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) coordinator: DataUpdateCoordinator[ - dict[WasteType, date | None] + dict[WasteType, list[date]] ] = DataUpdateCoordinator( hass, LOGGER, diff --git a/homeassistant/components/twentemilieu/calendar.py b/homeassistant/components/twentemilieu/calendar.py new file mode 100644 index 00000000000..0d2768e5eb2 --- /dev/null +++ b/homeassistant/components/twentemilieu/calendar.py @@ -0,0 +1,101 @@ +"""Support for Twente Milieu Calendar.""" +from __future__ import annotations + +from datetime import datetime +from typing import Any + +from homeassistant.components.calendar import CalendarEventDevice +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_ID +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator +import homeassistant.util.dt as dt_util + +from .const import DOMAIN, WASTE_TYPE_TO_DESCRIPTION +from .entity import TwenteMilieuEntity + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Twente Milieu calendar based on a config entry.""" + coordinator = hass.data[DOMAIN][entry.data[CONF_ID]] + async_add_entities([TwenteMilieuCalendar(coordinator, entry)]) + + +class TwenteMilieuCalendar(TwenteMilieuEntity, CalendarEventDevice): + """Defines a Twente Milieu calendar.""" + + _attr_name = "Twente Milieu" + _attr_icon = "mdi:delete-empty" + + def __init__( + self, + coordinator: DataUpdateCoordinator, + entry: ConfigEntry, + ) -> None: + """Initialize the Twente Milieu entity.""" + super().__init__(coordinator, entry) + self._attr_unique_id = str(entry.data[CONF_ID]) + self._event: dict[str, Any] | None = None + + @property + def event(self) -> dict[str, Any] | None: + """Return the next upcoming event.""" + return self._event + + async def async_get_events( + self, hass: HomeAssistant, start_date: datetime, end_date: datetime + ) -> list[dict[str, Any]]: + """Return calendar events within a datetime range.""" + events: list[dict[str, Any]] = [] + for waste_type, waste_dates in self.coordinator.data.items(): + events.extend( + { + "all_day": True, + "start": {"date": waste_date.isoformat()}, + "end": {"date": waste_date.isoformat()}, + "summary": WASTE_TYPE_TO_DESCRIPTION[waste_type], + } + for waste_date in waste_dates + if start_date.date() <= waste_date <= end_date.date() + ) + + return events + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + next_waste_pickup_type = None + next_waste_pickup_date = None + for waste_type, waste_dates in self.coordinator.data.items(): + if ( + waste_dates + and ( + next_waste_pickup_date is None + or waste_dates[0] # type: ignore[unreachable] + < next_waste_pickup_date + ) + and waste_dates[0] >= dt_util.now().date() + ): + next_waste_pickup_date = waste_dates[0] + next_waste_pickup_type = waste_type + + self._event = None + if next_waste_pickup_date is not None and next_waste_pickup_type is not None: + self._event = { + "all_day": True, + "start": {"date": next_waste_pickup_date.isoformat()}, + "end": {"date": next_waste_pickup_date.isoformat()}, + "summary": WASTE_TYPE_TO_DESCRIPTION[next_waste_pickup_type], + } + + super()._handle_coordinator_update() + + async def async_added_to_hass(self) -> None: + """When entity is added to hass.""" + await super().async_added_to_hass() + self._handle_coordinator_update() diff --git a/homeassistant/components/twentemilieu/const.py b/homeassistant/components/twentemilieu/const.py index 95ab903cc17..c9f2f935772 100644 --- a/homeassistant/components/twentemilieu/const.py +++ b/homeassistant/components/twentemilieu/const.py @@ -3,6 +3,8 @@ from datetime import timedelta import logging from typing import Final +from twentemilieu import WasteType + DOMAIN: Final = "twentemilieu" LOGGER = logging.getLogger(__package__) @@ -11,3 +13,11 @@ SCAN_INTERVAL = timedelta(hours=1) CONF_POST_CODE = "post_code" CONF_HOUSE_NUMBER = "house_number" CONF_HOUSE_LETTER = "house_letter" + +WASTE_TYPE_TO_DESCRIPTION = { + WasteType.NON_RECYCLABLE: "Non-recyclable Waste Pickup", + WasteType.ORGANIC: "Organic Waste Pickup", + WasteType.PACKAGES: "Packages Waste Pickup", + WasteType.PAPER: "Paper Waste Pickup", + WasteType.TREE: "Christmas Tree Pickup", +} diff --git a/homeassistant/components/twentemilieu/diagnostics.py b/homeassistant/components/twentemilieu/diagnostics.py index 0d63fdbcf2c..a158ead0909 100644 --- a/homeassistant/components/twentemilieu/diagnostics.py +++ b/homeassistant/components/twentemilieu/diagnostics.py @@ -17,6 +17,6 @@ async def async_get_config_entry_diagnostics( """Return diagnostics for a config entry.""" coordinator: DataUpdateCoordinator = hass.data[DOMAIN][entry.data[CONF_ID]] return { - waste_type: waste_date.isoformat() if waste_date else None - for waste_type, waste_date in coordinator.data.items() + waste_type: [waste_date.isoformat() for waste_date in waste_dates] + for waste_type, waste_dates in coordinator.data.items() } diff --git a/homeassistant/components/twentemilieu/entity.py b/homeassistant/components/twentemilieu/entity.py new file mode 100644 index 00000000000..008c0fa441e --- /dev/null +++ b/homeassistant/components/twentemilieu/entity.py @@ -0,0 +1,38 @@ +"""Base entity for the Twente Milieu integration.""" +from __future__ import annotations + +from datetime import date + +from twentemilieu import WasteType + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_ID +from homeassistant.helpers.device_registry import DeviceEntryType +from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) + +from .const import DOMAIN + + +class TwenteMilieuEntity( + CoordinatorEntity[DataUpdateCoordinator[dict[WasteType, list[date]]]], Entity +): + """Defines a Twente Milieu entity.""" + + def __init__( + self, + coordinator: DataUpdateCoordinator[dict[WasteType, list[date]]], + entry: ConfigEntry, + ) -> None: + """Initialize the Twente Milieu entity.""" + super().__init__(coordinator=coordinator) + self._attr_device_info = DeviceInfo( + configuration_url="https://www.twentemilieu.nl", + entry_type=DeviceEntryType.SERVICE, + identifiers={(DOMAIN, str(entry.data[CONF_ID]))}, + manufacturer="Twente Milieu", + name="Twente Milieu", + ) diff --git a/homeassistant/components/twentemilieu/manifest.json b/homeassistant/components/twentemilieu/manifest.json index d0b94efe289..e8e280dcd3d 100644 --- a/homeassistant/components/twentemilieu/manifest.json +++ b/homeassistant/components/twentemilieu/manifest.json @@ -3,7 +3,7 @@ "name": "Twente Milieu", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/twentemilieu", - "requirements": ["twentemilieu==0.5.0"], + "requirements": ["twentemilieu==0.6.0"], "codeowners": ["@frenck"], "quality_scale": "platinum", "iot_class": "cloud_polling", diff --git a/homeassistant/components/twentemilieu/sensor.py b/homeassistant/components/twentemilieu/sensor.py index f25b84ace15..ab69aba9abf 100644 --- a/homeassistant/components/twentemilieu/sensor.py +++ b/homeassistant/components/twentemilieu/sensor.py @@ -14,15 +14,11 @@ from homeassistant.components.sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ID from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import ( - CoordinatorEntity, - DataUpdateCoordinator, -) +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from .const import DOMAIN +from .const import DOMAIN, WASTE_TYPE_TO_DESCRIPTION +from .entity import TwenteMilieuEntity @dataclass @@ -43,35 +39,35 @@ SENSORS: tuple[TwenteMilieuSensorDescription, ...] = ( TwenteMilieuSensorDescription( key="tree", waste_type=WasteType.TREE, - name="Christmas Tree Pickup", + name=WASTE_TYPE_TO_DESCRIPTION[WasteType.TREE], icon="mdi:pine-tree", device_class=SensorDeviceClass.DATE, ), TwenteMilieuSensorDescription( key="Non-recyclable", waste_type=WasteType.NON_RECYCLABLE, - name="Non-recyclable Waste Pickup", + name=WASTE_TYPE_TO_DESCRIPTION[WasteType.NON_RECYCLABLE], icon="mdi:delete-empty", device_class=SensorDeviceClass.DATE, ), TwenteMilieuSensorDescription( key="Organic", waste_type=WasteType.ORGANIC, - name="Organic Waste Pickup", + name=WASTE_TYPE_TO_DESCRIPTION[WasteType.ORGANIC], icon="mdi:delete-empty", device_class=SensorDeviceClass.DATE, ), TwenteMilieuSensorDescription( key="Paper", waste_type=WasteType.PAPER, - name="Paper Waste Pickup", + name=WASTE_TYPE_TO_DESCRIPTION[WasteType.PAPER], icon="mdi:delete-empty", device_class=SensorDeviceClass.DATE, ), TwenteMilieuSensorDescription( key="Plastic", waste_type=WasteType.PACKAGES, - name="Packages Waste Pickup", + name=WASTE_TYPE_TO_DESCRIPTION[WasteType.PACKAGES], icon="mdi:delete-empty", device_class=SensorDeviceClass.DATE, ), @@ -90,11 +86,10 @@ async def async_setup_entry( ) -class TwenteMilieuSensor(CoordinatorEntity, SensorEntity): +class TwenteMilieuSensor(TwenteMilieuEntity, SensorEntity): """Defines a Twente Milieu sensor.""" entity_description: TwenteMilieuSensorDescription - coordinator: DataUpdateCoordinator[dict[WasteType, date | None]] def __init__( self, @@ -103,18 +98,13 @@ class TwenteMilieuSensor(CoordinatorEntity, SensorEntity): entry: ConfigEntry, ) -> None: """Initialize the Twente Milieu entity.""" - super().__init__(coordinator=coordinator) + super().__init__(coordinator, entry) self.entity_description = description self._attr_unique_id = f"{DOMAIN}_{entry.data[CONF_ID]}_{description.key}" - self._attr_device_info = DeviceInfo( - configuration_url="https://www.twentemilieu.nl", - entry_type=DeviceEntryType.SERVICE, - identifiers={(DOMAIN, str(entry.data[CONF_ID]))}, - manufacturer="Twente Milieu", - name="Twente Milieu", - ) @property def native_value(self) -> date | None: """Return the state of the sensor.""" - return self.coordinator.data.get(self.entity_description.waste_type) + if not (dates := self.coordinator.data[self.entity_description.waste_type]): + return None + return dates[0] diff --git a/homeassistant/components/twentemilieu/strings.json b/homeassistant/components/twentemilieu/strings.json index 369484a6392..d9b59b2d02c 100644 --- a/homeassistant/components/twentemilieu/strings.json +++ b/homeassistant/components/twentemilieu/strings.json @@ -2,7 +2,6 @@ "config": { "step": { "user": { - "title": "Twente Milieu", "description": "Set up Twente Milieu providing waste collection information on your address.", "data": { "post_code": "Postal code", @@ -15,6 +14,8 @@ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "invalid_address": "Address not found in Twente Milieu service area." }, - "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_location%]" } + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_location%]" + } } } diff --git a/homeassistant/components/twilio/translations/fr.json b/homeassistant/components/twilio/translations/fr.json index f602994d8b2..a3ee815f4f9 100644 --- a/homeassistant/components/twilio/translations/fr.json +++ b/homeassistant/components/twilio/translations/fr.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "cloud_not_connected": "Pas connect\u00e9 \u00e0 Home Assistant Cloud", + "cloud_not_connected": "Non connect\u00e9 \u00e0 Home Assistant Cloud.", "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible.", "webhook_not_internet_accessible": "Votre installation de Home Assistant doit \u00eatre accessible depuis internet pour recevoir des messages webhook." }, @@ -10,7 +10,7 @@ }, "step": { "user": { - "description": "\u00cates-vous s\u00fbr de vouloir configurer Twilio?", + "description": "Voulez-vous commencer la configuration\u00a0?", "title": "Configurer le Webhook Twilio" } } diff --git a/homeassistant/components/twinkly/config_flow.py b/homeassistant/components/twinkly/config_flow.py index 8320e18c960..eab44dba591 100644 --- a/homeassistant/components/twinkly/config_flow.py +++ b/homeassistant/components/twinkly/config_flow.py @@ -11,10 +11,10 @@ from voluptuous import Required, Schema from homeassistant import config_entries, data_entry_flow from homeassistant.components import dhcp -from homeassistant.const import CONF_HOST +from homeassistant.const import CONF_HOST, CONF_MODEL from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .const import CONF_ID, CONF_MODEL, CONF_NAME, DEV_ID, DEV_MODEL, DEV_NAME, DOMAIN +from .const import CONF_ID, CONF_NAME, DEV_ID, DEV_MODEL, DEV_NAME, DOMAIN _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/twinkly/const.py b/homeassistant/components/twinkly/const.py index c266412432b..e48ff165c67 100644 --- a/homeassistant/components/twinkly/const.py +++ b/homeassistant/components/twinkly/const.py @@ -6,7 +6,6 @@ DOMAIN = "twinkly" CONF_ID = "id" CONF_HOST = "host" CONF_NAME = "name" -CONF_MODEL = "model" # Strongly named HA attributes keys ATTR_HOST = "host" diff --git a/homeassistant/components/twinkly/light.py b/homeassistant/components/twinkly/light.py index 6675461c06b..440dab9a6ff 100644 --- a/homeassistant/components/twinkly/light.py +++ b/homeassistant/components/twinkly/light.py @@ -18,6 +18,7 @@ from homeassistant.components.light import ( LightEntity, ) from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_MODEL from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -25,7 +26,6 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( CONF_HOST, CONF_ID, - CONF_MODEL, CONF_NAME, DATA_CLIENT, DATA_DEVICE_INFO, diff --git a/homeassistant/components/twinkly/strings.json b/homeassistant/components/twinkly/strings.json index 6dc7cc2eb97..630497ef28c 100644 --- a/homeassistant/components/twinkly/strings.json +++ b/homeassistant/components/twinkly/strings.json @@ -2,8 +2,6 @@ "config": { "step": { "user": { - "title": "Twinkly", - "description": "Set up your Twinkly led string", "data": { "host": "[%key:common::config_flow::data::host%]" } diff --git a/homeassistant/components/twinkly/translations/he.json b/homeassistant/components/twinkly/translations/he.json index db9e846ce56..6dd018f5c7c 100644 --- a/homeassistant/components/twinkly/translations/he.json +++ b/homeassistant/components/twinkly/translations/he.json @@ -9,7 +9,7 @@ "step": { "user": { "data": { - "host": "\u05de\u05d0\u05e8\u05d7 (\u05d0\u05d5 \u05db\u05ea\u05d5\u05d1\u05ea IP) \u05e9\u05dc \u05d4\u05d4\u05ea\u05e7\u05df \u05d4\u05de\u05e0\u05e6\u05e0\u05e5 \u05e9\u05dc\u05da" + "host": "\u05de\u05d0\u05e8\u05d7" } } } diff --git a/homeassistant/components/twitch/sensor.py b/homeassistant/components/twitch/sensor.py index 771f88f0ef1..95006a4cab7 100644 --- a/homeassistant/components/twitch/sensor.py +++ b/homeassistant/components/twitch/sensor.py @@ -62,13 +62,16 @@ def setup_platform( client_id = config[CONF_CLIENT_ID] client_secret = config[CONF_CLIENT_SECRET] oauth_token = config.get(CONF_TOKEN) - client = Twitch(app_id=client_id, app_secret=client_secret) - client.auto_refresh_auth = False try: - client.authenticate_app(scope=OAUTH_SCOPES) + client = Twitch( + app_id=client_id, + app_secret=client_secret, + target_app_auth_scope=OAUTH_SCOPES, + ) + client.auto_refresh_auth = False except TwitchAuthorizationException: - _LOGGER.error("INvalid client ID or client secret") + _LOGGER.error("Invalid client ID or client secret") return if oauth_token: @@ -86,7 +89,7 @@ def setup_platform( channels = client.get_users(logins=channels) add_entities( - [TwitchSensor(channel=channel, client=client) for channel in channels["data"]], + [TwitchSensor(channel, client) for channel in channels["data"]], True, ) @@ -94,80 +97,38 @@ def setup_platform( class TwitchSensor(SensorEntity): """Representation of an Twitch channel.""" - def __init__(self, channel, client: Twitch): + _attr_icon = ICON + + def __init__(self, channel: dict[str, str], client: Twitch) -> None: """Initialize the sensor.""" self._client = client - self._channel = channel self._enable_user_auth = client.has_required_auth(AuthType.USER, OAUTH_SCOPES) - self._state = None - self._preview = None - self._game = None - self._title = None - self._subscription = None - self._follow = None - self._statistics = None + self._attr_name = channel["display_name"] + self._attr_unique_id = channel["id"] - @property - def name(self): - """Return the name of the sensor.""" - return self._channel["display_name"] - - @property - def native_value(self): - """Return the state of the sensor.""" - return self._state - - @property - def entity_picture(self): - """Return preview of current game.""" - return self._preview - - @property - def extra_state_attributes(self): - """Return the state attributes.""" - attr = dict(self._statistics) - - if self._enable_user_auth: - attr.update(self._subscription) - attr.update(self._follow) - - if self._state == STATE_STREAMING: - attr.update({ATTR_GAME: self._game, ATTR_TITLE: self._title}) - return attr - - @property - def unique_id(self): - """Return unique ID for this sensor.""" - return self._channel["id"] - - @property - def icon(self): - """Icon to use in the frontend, if any.""" - return ICON - - def update(self): + def update(self) -> None: """Update device state.""" - followers = self._client.get_users_follows(to_id=self._channel["id"])["total"] - channel = self._client.get_users(user_ids=[self._channel["id"]])["data"][0] + followers = self._client.get_users_follows(to_id=self.unique_id)["total"] + channel = self._client.get_users(user_ids=[self.unique_id])["data"][0] - self._statistics = { + self._attr_extra_state_attributes = { ATTR_FOLLOWING: followers, ATTR_VIEWS: channel["view_count"], } if self._enable_user_auth: - user = self._client.get_users()["data"][0] + user = self._client.get_users()["data"][0]["id"] subs = self._client.check_user_subscription( - user_id=user["id"], broadcaster_id=self._channel["id"] + user_id=user, broadcaster_id=self.unique_id ) if "data" in subs: - self._subscription = { - ATTR_SUBSCRIPTION: True, - ATTR_SUBSCRIPTION_GIFTED: subs["data"][0]["is_gift"], - } + self._attr_extra_state_attributes[ATTR_SUBSCRIPTION] = True + self._attr_extra_state_attributes[ATTR_SUBSCRIPTION_GIFTED] = subs[ + "data" + ][0]["is_gift"] elif "status" in subs and subs["status"] == 404: - self._subscription = {ATTR_SUBSCRIPTION: False} + self._attr_extra_state_attributes[ATTR_SUBSCRIPTION] = False elif "error" in subs: raise Exception( f"Error response on check_user_subscription: {subs['error']}" @@ -176,23 +137,22 @@ class TwitchSensor(SensorEntity): raise Exception("Unknown error response on check_user_subscription") follows = self._client.get_users_follows( - from_id=user["id"], to_id=self._channel["id"] + from_id=user, to_id=self.unique_id )["data"] - if len(follows) > 0: - self._follow = { - ATTR_FOLLOW: True, - ATTR_FOLLOW_SINCE: follows[0]["followed_at"], - } - else: - self._follow = {ATTR_FOLLOW: False} + self._attr_extra_state_attributes[ATTR_FOLLOW] = len(follows) > 0 + if len(follows): + self._attr_extra_state_attributes[ATTR_FOLLOW_SINCE] = follows[0][ + "followed_at" + ] - streams = self._client.get_streams(user_id=[self._channel["id"]])["data"] - if len(streams) > 0: + if streams := self._client.get_streams(user_id=[self.unique_id])["data"]: stream = streams[0] - self._game = stream["game_name"] - self._title = stream["title"] - self._preview = stream["thumbnail_url"] - self._state = STATE_STREAMING + self._attr_native_value = STATE_STREAMING + self._attr_extra_state_attributes[ATTR_GAME] = stream["game_name"] + self._attr_extra_state_attributes[ATTR_TITLE] = stream["title"] + self._attr_entity_picture = stream["thumbnail_url"] else: - self._preview = channel["offline_image_url"] - self._state = STATE_OFFLINE + self._attr_native_value = STATE_OFFLINE + self._attr_extra_state_attributes[ATTR_GAME] = None + self._attr_extra_state_attributes[ATTR_TITLE] = None + self._attr_entity_picture = channel["offline_image_url"] diff --git a/homeassistant/components/unifi/manifest.json b/homeassistant/components/unifi/manifest.json index 79c8453431d..e779dca22f0 100644 --- a/homeassistant/components/unifi/manifest.json +++ b/homeassistant/components/unifi/manifest.json @@ -3,12 +3,8 @@ "name": "UniFi Network", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/unifi", - "requirements": [ - "aiounifi==31" - ], - "codeowners": [ - "@Kane610" - ], + "requirements": ["aiounifi==31"], + "codeowners": ["@Kane610"], "quality_scale": "platinum", "ssdp": [ { @@ -26,4 +22,4 @@ ], "iot_class": "local_push", "loggers": ["aiounifi"] -} \ No newline at end of file +} diff --git a/homeassistant/components/unifi/translations/fr.json b/homeassistant/components/unifi/translations/fr.json index 36c9ee5bcc6..d8f1bc0a84c 100644 --- a/homeassistant/components/unifi/translations/fr.json +++ b/homeassistant/components/unifi/translations/fr.json @@ -6,7 +6,7 @@ "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" }, "error": { - "faulty_credentials": "Authentification invalide", + "faulty_credentials": "Authentification non valide", "service_unavailable": "\u00c9chec de connexion", "unknown_client_mac": "Aucun client disponible sur cette adresse MAC" }, @@ -57,8 +57,8 @@ "simple_options": { "data": { "block_client": "Clients contr\u00f4l\u00e9s par acc\u00e8s r\u00e9seau", - "track_clients": "Suivi de clients r\u00e9seaux", - "track_devices": "Suivi d'\u00e9quipement r\u00e9seau (Equipements Ubiquiti)" + "track_clients": "Suivre les clients du r\u00e9seau", + "track_devices": "Suivre les p\u00e9riph\u00e9riques r\u00e9seau (p\u00e9riph\u00e9riques Ubiquiti)" }, "description": "Configurer l'int\u00e9gration UniFi" }, diff --git a/homeassistant/components/unifiprotect/binary_sensor.py b/homeassistant/components/unifiprotect/binary_sensor.py index a5b41a446fc..7613ffb8ebf 100644 --- a/homeassistant/components/unifiprotect/binary_sensor.py +++ b/homeassistant/components/unifiprotect/binary_sensor.py @@ -13,7 +13,7 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_LAST_TRIP_TIME, ATTR_MODEL +from homeassistant.const import ATTR_MODEL from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -27,7 +27,6 @@ from .entity import ( async_all_device_entities, ) from .models import ProtectRequiredKeysMixin -from .utils import get_nested_attr _LOGGER = logging.getLogger(__name__) _KEY_DOOR = "door" @@ -39,8 +38,6 @@ class ProtectBinaryEntityDescription( ): """Describes UniFi Protect Binary Sensor entity.""" - ufp_last_trip_value: str | None = None - MOUNT_DEVICE_CLASS_MAP = { MountType.GARAGE: BinarySensorDeviceClass.GARAGE_DOOR, @@ -57,7 +54,6 @@ CAMERA_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( icon="mdi:doorbell-video", ufp_required_field="feature_flags.has_chime", ufp_value="is_ringing", - ufp_last_trip_value="last_ring", ), ProtectBinaryEntityDescription( key="dark", @@ -79,7 +75,6 @@ LIGHT_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( name="Motion Detected", device_class=BinarySensorDeviceClass.MOTION, ufp_value="is_pir_motion_detected", - ufp_last_trip_value="last_motion", ), ) @@ -89,7 +84,6 @@ SENSE_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( name="Contact", device_class=BinarySensorDeviceClass.DOOR, ufp_value="is_opened", - ufp_last_trip_value="open_status_changed_at", ufp_enabled="is_contact_sensor_enabled", ), ProtectBinaryEntityDescription( @@ -104,7 +98,6 @@ SENSE_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( name="Motion Detected", device_class=BinarySensorDeviceClass.MOTION, ufp_value="is_motion_detected", - ufp_last_trip_value="motion_detected_at", ufp_enabled="is_motion_sensor_enabled", ), ProtectBinaryEntityDescription( @@ -112,7 +105,6 @@ SENSE_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( name="Tampering Detected", device_class=BinarySensorDeviceClass.TAMPER, ufp_value="is_tampering_detected", - ufp_last_trip_value="tampering_detected_at", ), ) @@ -122,7 +114,6 @@ MOTION_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( name="Motion", device_class=BinarySensorDeviceClass.MOTION, ufp_value="is_motion_detected", - ufp_last_trip_value="last_motion", ), ) @@ -215,16 +206,6 @@ class ProtectDeviceBinarySensor(ProtectDeviceEntity, BinarySensorEntity): super()._async_update_device_from_protect() self._attr_is_on = self.entity_description.get_ufp_value(self.device) - if self.entity_description.ufp_last_trip_value is not None: - last_trip = get_nested_attr( - self.device, self.entity_description.ufp_last_trip_value - ) - attrs = self.extra_state_attributes or {} - self._attr_extra_state_attributes = { - **attrs, - ATTR_LAST_TRIP_TIME: last_trip, - } - # UP Sense can be any of the 3 contact sensor device classes if self.entity_description.key == _KEY_DOOR and isinstance(self.device, Sensor): self.entity_description.device_class = MOUNT_DEVICE_CLASS_MAP.get( diff --git a/homeassistant/components/unifiprotect/entity.py b/homeassistant/components/unifiprotect/entity.py index 27b74d2b303..045a9c4fd6d 100644 --- a/homeassistant/components/unifiprotect/entity.py +++ b/homeassistant/components/unifiprotect/entity.py @@ -44,7 +44,6 @@ def _async_device_entities( for device in data.get_by_types({model_type}): assert isinstance(device, (Camera, Light, Sensor, Viewer, Doorlock)) for description in descs: - assert isinstance(description, EntityDescription) if description.ufp_required_field: required_field = get_nested_attr(device, description.ufp_required_field) if not required_field: diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index 5fcead53da2..a70d295fd74 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -3,18 +3,9 @@ "name": "UniFi Protect", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/unifiprotect", - "requirements": [ - "pyunifiprotect==3.2.0", - "unifi-discovery==1.1.2" - ], - "dependencies": [ - "http" - ], - "codeowners": [ - "@briis", - "@AngellusMortis", - "@bdraco" - ], + "requirements": ["pyunifiprotect==3.2.0", "unifi-discovery==1.1.2"], + "dependencies": ["http"], + "codeowners": ["@briis", "@AngellusMortis", "@bdraco"], "quality_scale": "platinum", "iot_class": "local_push", "dhcp": [ @@ -47,7 +38,7 @@ }, { "macaddress": "74ACB9*" - } + } ], "ssdp": [ { diff --git a/homeassistant/components/unifiprotect/media_player.py b/homeassistant/components/unifiprotect/media_player.py index a6ed8f65681..4de59c8252a 100644 --- a/homeassistant/components/unifiprotect/media_player.py +++ b/homeassistant/components/unifiprotect/media_player.py @@ -7,13 +7,19 @@ from typing import Any from pyunifiprotect.data import Camera from pyunifiprotect.exceptions import StreamError +from homeassistant.components import media_source from homeassistant.components.media_player import ( + BrowseMedia, MediaPlayerDeviceClass, MediaPlayerEntity, MediaPlayerEntityDescription, ) +from homeassistant.components.media_player.browse_media import ( + async_process_play_media_url, +) from homeassistant.components.media_player.const import ( MEDIA_TYPE_MUSIC, + SUPPORT_BROWSE_MEDIA, SUPPORT_PLAY_MEDIA, SUPPORT_STOP, SUPPORT_VOLUME_SET, @@ -74,7 +80,11 @@ class ProtectMediaPlayer(ProtectDeviceEntity, MediaPlayerEntity): self._attr_name = f"{self.device.name} Speaker" self._attr_supported_features = ( - SUPPORT_PLAY_MEDIA | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_STEP | SUPPORT_STOP + SUPPORT_PLAY_MEDIA + | SUPPORT_VOLUME_SET + | SUPPORT_VOLUME_STEP + | SUPPORT_STOP + | SUPPORT_BROWSE_MEDIA ) self._attr_media_content_type = MEDIA_TYPE_MUSIC @@ -112,16 +122,20 @@ class ProtectMediaPlayer(ProtectDeviceEntity, MediaPlayerEntity): self, media_type: str, media_id: str, **kwargs: Any ) -> None: """Play a piece of media.""" + if media_source.is_media_source_id(media_id): + media_type = MEDIA_TYPE_MUSIC + play_item = await media_source.async_resolve_media(self.hass, media_id) + media_id = async_process_play_media_url(self.hass, play_item.url) if media_type != MEDIA_TYPE_MUSIC: - raise ValueError("Only music media type is supported") + raise HomeAssistantError("Only music media type is supported") _LOGGER.debug("Playing Media %s for %s Speaker", media_id, self.device.name) await self.async_media_stop() try: await self.device.play_audio(media_id, blocking=False) except StreamError as err: - raise HomeAssistantError from err + raise HomeAssistantError(err) from err else: # update state after starting player self._async_updated_event() @@ -129,3 +143,13 @@ class ProtectMediaPlayer(ProtectDeviceEntity, MediaPlayerEntity): await self.device.wait_until_audio_completes() self._async_updated_event() + + async def async_browse_media( + self, media_content_type: str | None = None, media_content_id: str | None = None + ) -> BrowseMedia: + """Implement the websocket media browsing helper.""" + return await media_source.async_browse_media( + self.hass, + media_content_id, + content_filter=lambda item: item.media_content_type.startswith("audio/"), + ) diff --git a/homeassistant/components/unifiprotect/models.py b/homeassistant/components/unifiprotect/models.py index df11c46a5d9..ecc3e4210ff 100644 --- a/homeassistant/components/unifiprotect/models.py +++ b/homeassistant/components/unifiprotect/models.py @@ -18,7 +18,7 @@ T = TypeVar("T", bound=ProtectDeviceModel) @dataclass -class ProtectRequiredKeysMixin(Generic[T]): +class ProtectRequiredKeysMixin(EntityDescription, Generic[T]): """Mixin for required keys.""" ufp_required_field: str | None = None @@ -54,7 +54,6 @@ class ProtectSetableKeysMixin(ProtectRequiredKeysMixin, Generic[T]): async def ufp_set(self, obj: T, value: Any) -> None: """Set value for UniFi Protect device.""" - assert isinstance(self, EntityDescription) _LOGGER.debug("Setting %s to %s for %s", self.name, value, obj.name) if self.ufp_set_method is not None: await getattr(obj, self.ufp_set_method)(value) diff --git a/homeassistant/components/unifiprotect/sensor.py b/homeassistant/components/unifiprotect/sensor.py index 330c90880ab..00ac1304d88 100644 --- a/homeassistant/components/unifiprotect/sensor.py +++ b/homeassistant/components/unifiprotect/sensor.py @@ -154,6 +154,7 @@ CAMERA_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( name="Oldest Recording", device_class=SensorDeviceClass.TIMESTAMP, entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, ufp_value="stats.video.recording_start", ), ProtectSensorEntityDescription( @@ -186,6 +187,15 @@ CAMERA_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( ufp_required_field="voltage", precision=2, ), + ProtectSensorEntityDescription( + key="doorbell_last_trip_time", + name="Last Doorbell Ring", + device_class=SensorDeviceClass.TIMESTAMP, + icon="mdi:doorbell-video", + ufp_required_field="feature_flags.has_chime", + ufp_value="last_ring", + entity_registry_enabled_default=False, + ), ) CAMERA_DISABLED_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( @@ -252,6 +262,27 @@ SENSE_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( ufp_value_fn=_get_alarm_sound, ufp_enabled="is_alarm_sensor_enabled", ), + ProtectSensorEntityDescription( + key="door_last_trip_time", + name="Last Open", + device_class=SensorDeviceClass.TIMESTAMP, + ufp_value="open_status_changed_at", + entity_registry_enabled_default=False, + ), + ProtectSensorEntityDescription( + key="motion_last_trip_time", + name="Last Motion Detected", + device_class=SensorDeviceClass.TIMESTAMP, + ufp_value="motion_detected_at", + entity_registry_enabled_default=False, + ), + ProtectSensorEntityDescription( + key="tampering_last_trip_time", + name="Last Tampering Detected", + device_class=SensorDeviceClass.TIMESTAMP, + ufp_value="tampering_detected_at", + entity_registry_enabled_default=False, + ), ) DOORLOCK_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( @@ -399,6 +430,27 @@ MOTION_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( ) +LIGHT_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( + ProtectSensorEntityDescription( + key="motion_last_trip_time", + name="Last Motion Detected", + device_class=SensorDeviceClass.TIMESTAMP, + ufp_value="last_motion", + entity_registry_enabled_default=False, + ), +) + +MOTION_TRIP_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( + ProtectSensorEntityDescription( + key="motion_last_trip_time", + name="Last Motion Detected", + device_class=SensorDeviceClass.TIMESTAMP, + ufp_value="last_motion", + entity_registry_enabled_default=False, + ), +) + + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, @@ -412,6 +464,7 @@ async def async_setup_entry( all_descs=ALL_DEVICES_SENSORS, camera_descs=CAMERA_SENSORS + CAMERA_DISABLED_SENSORS, sense_descs=SENSE_SENSORS, + light_descs=LIGHT_SENSORS, lock_descs=DOORLOCK_SENSORS, ) entities += _async_motion_entities(data) @@ -426,6 +479,14 @@ def _async_motion_entities( ) -> list[ProtectDeviceEntity]: entities: list[ProtectDeviceEntity] = [] for device in data.api.bootstrap.cameras.values(): + for description in MOTION_TRIP_SENSORS: + entities.append(ProtectDeviceSensor(data, device, description)) + _LOGGER.debug( + "Adding trip sensor entity %s for %s", + description.name, + device.name, + ) + if not device.feature_flags.has_smart_detect: continue diff --git a/homeassistant/components/unifiprotect/strings.json b/homeassistant/components/unifiprotect/strings.json index 9ae85702769..d789459960b 100644 --- a/homeassistant/components/unifiprotect/strings.json +++ b/homeassistant/components/unifiprotect/strings.json @@ -27,9 +27,9 @@ "description": "Do you want to setup {name} ({ip_address})? [%key:component::unifiprotect::config::step::user::description%]", "data": { "username": "[%key:common::config_flow::data::username%]", - "password": "[%key:common::config_flow::data::password%]" + "password": "[%key:common::config_flow::data::password%]" } - } + } }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", diff --git a/homeassistant/components/unifiprotect/switch.py b/homeassistant/components/unifiprotect/switch.py index 086bd852049..9271e87db50 100644 --- a/homeassistant/components/unifiprotect/switch.py +++ b/homeassistant/components/unifiprotect/switch.py @@ -87,7 +87,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( key=_KEY_PRIVACY_MODE, name="Privacy Mode", icon="mdi:eye-settings", - entity_category=None, + entity_category=EntityCategory.CONFIG, ufp_required_field="feature_flags.has_privacy_mask", ufp_value="is_privacy_on", ), diff --git a/homeassistant/components/unifiprotect/translations/el.json b/homeassistant/components/unifiprotect/translations/el.json index 70c290590b1..c2f5d57d36d 100644 --- a/homeassistant/components/unifiprotect/translations/el.json +++ b/homeassistant/components/unifiprotect/translations/el.json @@ -18,7 +18,7 @@ "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7", "verify_ssl": "\u0395\u03c0\u03b1\u03bb\u03b7\u03b8\u03b5\u03cd\u03c3\u03c4\u03b5 \u03c4\u03bf \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03b7\u03c4\u03b9\u03ba\u03cc SSL" }, - "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {name} ( {ip_address});", + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {name} ({ip_address}); \u0398\u03b1 \u03c7\u03c1\u03b5\u03b9\u03b1\u03c3\u03c4\u03b5\u03af\u03c4\u03b5 \u03ad\u03bd\u03b1\u03bd \u03c4\u03bf\u03c0\u03b9\u03ba\u03cc \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7 \u03c0\u03bf\u03c5 \u03ad\u03c7\u03b5\u03b9 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03b7\u03b8\u03b5\u03af \u03c3\u03c4\u03b7\u03bd \u039a\u03bf\u03bd\u03c3\u03cc\u03bb\u03b1 UniFi OS \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af\u03c4\u03b5. \u039f\u03b9 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b5\u03c2 \u03c4\u03bf\u03c5 Ubiquiti Cloud \u03b4\u03b5\u03bd \u03b8\u03b1 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03ae\u03c3\u03bf\u03c5\u03bd. \u0393\u03b9\u03b1 \u03c0\u03b5\u03c1\u03b9\u03c3\u03c3\u03cc\u03c4\u03b5\u03c1\u03b5\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2: {local_user_documentation_url}", "title": "\u0391\u03bd\u03b1\u03ba\u03b1\u03bb\u03cd\u03c6\u03b8\u03b7\u03ba\u03b5 \u03c4\u03bf UniFi Protect" }, "reauth_confirm": { diff --git a/homeassistant/components/unifiprotect/translations/fr.json b/homeassistant/components/unifiprotect/translations/fr.json index efd0cb529b1..592a18427b7 100644 --- a/homeassistant/components/unifiprotect/translations/fr.json +++ b/homeassistant/components/unifiprotect/translations/fr.json @@ -6,11 +6,11 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "protect_version": "La version minimale requise est la v1.20.0. Veuillez mettre \u00e0 jour UniFi Protect, puis r\u00e9essayer.", "unknown": "Erreur inattendue" }, - "flow_title": "{name} ( {ip_address} )", + "flow_title": "{name} ({ip_address})", "step": { "discovery_confirm": { "data": { @@ -18,7 +18,7 @@ "username": "Nom d'utilisateur", "verify_ssl": "V\u00e9rifier le certificat SSL" }, - "description": "Voulez-vous configurer {name} ({ip_address})? Vous aurez besoin d'un utilisateur local cr\u00e9\u00e9 dans votre console UniFi OS pour vous connecter. Les utilisateurs Ubiquiti Cloud ne fonctionneront pas. Pour plus d'informations\u00a0: {local_user_documentation_url}", + "description": "Voulez-vous configurer {name} ({ip_address})\u00a0? Vous aurez besoin d'un utilisateur local cr\u00e9\u00e9 dans votre console UniFi OS pour vous connecter. Les utilisateurs Ubiquiti Cloud ne fonctionneront pas. Pour plus d'informations\u00a0: {local_user_documentation_url}", "title": "UniFi Protect d\u00e9couvert" }, "reauth_confirm": { diff --git a/homeassistant/components/unifiprotect/translations/it.json b/homeassistant/components/unifiprotect/translations/it.json index a7f82827ffb..747647b3d26 100644 --- a/homeassistant/components/unifiprotect/translations/it.json +++ b/homeassistant/components/unifiprotect/translations/it.json @@ -18,7 +18,7 @@ "username": "Nome utente", "verify_ssl": "Verifica il certificato SSL" }, - "description": "Vuoi configurare {name} ( {ip_address} )? Avrai bisogno di un utente locale creato nella tua console UniFi OS con cui accedere. Gli utenti Ubiquiti Cloud non funzioneranno. Per ulteriori informazioni: {local_user_documentation_url}", + "description": "Vuoi configurare {name} ({ip_address})? Avrai bisogno di un utente locale creato nella tua console UniFi OS con cui accedere. Gli utenti Ubiquiti Cloud non funzioneranno. Per ulteriori informazioni: {local_user_documentation_url}", "title": "Rilevato UniFi Protect" }, "reauth_confirm": { diff --git a/homeassistant/components/unifiprotect/translations/pl.json b/homeassistant/components/unifiprotect/translations/pl.json index 72460919989..ef879805546 100644 --- a/homeassistant/components/unifiprotect/translations/pl.json +++ b/homeassistant/components/unifiprotect/translations/pl.json @@ -51,7 +51,7 @@ "disable_rtsp": "Wy\u0142\u0105cz strumie\u0144 RTSP", "override_connection_host": "Zast\u0105p host po\u0142\u0105czenia" }, - "description": "Opcja metryk w czasie rzeczywistym powinna by\u0107 w\u0142\u0105czona tylko wtedy, gdy w\u0142\u0105czono czujniki diagnostyczne i chcesz je aktualizowa\u0107 w czasie rzeczywistym. Je\u015bli nie s\u0105 w\u0142\u0105czone, b\u0119d\u0105 aktualizowane tylko raz na 15 minut.", + "description": "Opcja metryk w czasie rzeczywistym powinna by\u0107 w\u0142\u0105czona tylko wtedy, gdy w\u0142\u0105czono sensory diagnostyczne i chcesz je aktualizowa\u0107 w czasie rzeczywistym. Je\u015bli nie s\u0105 w\u0142\u0105czone, b\u0119d\u0105 aktualizowane tylko raz na 15 minut.", "title": "Opcje UniFi Protect" } } diff --git a/homeassistant/components/unifiprotect/translations/zh-Hant.json b/homeassistant/components/unifiprotect/translations/zh-Hant.json index ab63b89075e..33a447e9f4c 100644 --- a/homeassistant/components/unifiprotect/translations/zh-Hant.json +++ b/homeassistant/components/unifiprotect/translations/zh-Hant.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", - "discovery_started": "\u958b\u59cb\u63a2\u7d22" + "discovery_started": "\u958b\u59cb\u641c\u7d22" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/upb/services.yaml b/homeassistant/components/upb/services.yaml index 6c5c1a8b606..af8eb81d9b0 100644 --- a/homeassistant/components/upb/services.yaml +++ b/homeassistant/components/upb/services.yaml @@ -20,7 +20,7 @@ light_fade_start: number: min: 0 max: 100 - unit_of_measurement: '%' + unit_of_measurement: "%" rate: name: Rate description: Rate for light to transition to new brightness @@ -89,7 +89,7 @@ link_goto: number: min: 0 max: 100 - unit_of_measurement: '%' + unit_of_measurement: "%" rate: name: Rate description: Amount of time for scene to transition to new brightness @@ -122,7 +122,7 @@ link_fade_start: number: min: 0 max: 100 - unit_of_measurement: '%' + unit_of_measurement: "%" rate: name: Rate description: Amount of time for scene to transition to new brightness diff --git a/homeassistant/components/upb/translations/fr.json b/homeassistant/components/upb/translations/fr.json index 6f96f42f3dd..72d149a0ee3 100644 --- a/homeassistant/components/upb/translations/fr.json +++ b/homeassistant/components/upb/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_upb_file": "Fichier d'exportation UPB UPStart manquant ou invalide, v\u00e9rifiez le nom et le chemin du fichier.", + "invalid_upb_file": "Fichier d'exportation UPB UPStart manquant ou non valide, v\u00e9rifiez le nom et le chemin d'acc\u00e8s du fichier.", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/upcloud/__init__.py b/homeassistant/components/upcloud/__init__.py index a824bc596d0..e42659948d8 100644 --- a/homeassistant/components/upcloud/__init__.py +++ b/homeassistant/components/upcloud/__init__.py @@ -174,7 +174,7 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> return unload_ok -class UpCloudServerEntity(CoordinatorEntity): +class UpCloudServerEntity(CoordinatorEntity[UpCloudDataUpdateCoordinator]): """Entity class for UpCloud servers.""" def __init__( diff --git a/homeassistant/components/upcloud/translations/fr.json b/homeassistant/components/upcloud/translations/fr.json index e4fe2301f6a..43a2c619388 100644 --- a/homeassistant/components/upcloud/translations/fr.json +++ b/homeassistant/components/upcloud/translations/fr.json @@ -2,7 +2,7 @@ "config": { "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide" + "invalid_auth": "Authentification non valide" }, "step": { "user": { diff --git a/homeassistant/components/update/__init__.py b/homeassistant/components/update/__init__.py new file mode 100644 index 00000000000..100b5dcc35e --- /dev/null +++ b/homeassistant/components/update/__init__.py @@ -0,0 +1,439 @@ +"""Component to allow for providing device or service updates.""" +from __future__ import annotations + +from dataclasses import dataclass +from datetime import timedelta +import logging +from typing import Any, Final, final + +from awesomeversion import AwesomeVersion, AwesomeVersionCompareException +import voluptuous as vol + +from homeassistant.backports.enum import StrEnum +from homeassistant.components import websocket_api +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.core import HomeAssistant, ServiceCall +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.config_validation import ( + PLATFORM_SCHEMA, + PLATFORM_SCHEMA_BASE, +) +from homeassistant.helpers.entity import EntityCategory, EntityDescription +from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.helpers.typing import ConfigType + +from .const import ( + ATTR_AUTO_UPDATE, + ATTR_BACKUP, + ATTR_IN_PROGRESS, + ATTR_INSTALLED_VERSION, + ATTR_LATEST_VERSION, + ATTR_RELEASE_SUMMARY, + ATTR_RELEASE_URL, + ATTR_SKIPPED_VERSION, + ATTR_TITLE, + ATTR_VERSION, + DOMAIN, + SERVICE_INSTALL, + SERVICE_SKIP, + UpdateEntityFeature, +) + +SCAN_INTERVAL = timedelta(minutes=15) + +ENTITY_ID_FORMAT: Final = DOMAIN + ".{}" + +_LOGGER = logging.getLogger(__name__) + + +class UpdateDeviceClass(StrEnum): + """Device class for update.""" + + FIRMWARE = "firmware" + + +DEVICE_CLASSES_SCHEMA = vol.All(vol.Lower, vol.Coerce(UpdateDeviceClass)) + + +__all__ = [ + "ATTR_BACKUP", + "ATTR_VERSION", + "DEVICE_CLASSES_SCHEMA", + "DOMAIN", + "PLATFORM_SCHEMA_BASE", + "PLATFORM_SCHEMA", + "SERVICE_INSTALL", + "SERVICE_SKIP", + "UpdateDeviceClass", + "UpdateEntity", + "UpdateEntityDescription", + "UpdateEntityFeature", +] + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up Select entities.""" + component = hass.data[DOMAIN] = EntityComponent( + _LOGGER, DOMAIN, hass, SCAN_INTERVAL + ) + await component.async_setup(config) + + component.async_register_entity_service( + SERVICE_INSTALL, + { + vol.Optional(ATTR_VERSION): cv.string, + vol.Optional(ATTR_BACKUP, default=False): cv.boolean, + }, + async_install, + [UpdateEntityFeature.INSTALL], + ) + + component.async_register_entity_service( + SERVICE_SKIP, + {}, + async_skip, + ) + websocket_api.async_register_command(hass, websocket_release_notes) + + 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) + + +async def async_install(entity: UpdateEntity, service_call: ServiceCall) -> None: + """Service call wrapper to validate the call.""" + # If version is not specified, but no update is available. + if (version := service_call.data.get(ATTR_VERSION)) is None and ( + entity.installed_version == entity.latest_version + or entity.latest_version is None + ): + raise HomeAssistantError(f"No update available for {entity.name}") + + # If version is specified, but not supported by the entity. + if ( + version is not None + and not entity.supported_features & UpdateEntityFeature.SPECIFIC_VERSION + ): + raise HomeAssistantError( + f"Installing a specific version is not supported for {entity.name}" + ) + + # If backup is requested, but not supported by the entity. + if ( + backup := service_call.data[ATTR_BACKUP] + ) and not entity.supported_features & UpdateEntityFeature.BACKUP: + raise HomeAssistantError(f"Backup is not supported for {entity.name}") + + # Update is already in progress. + if entity.in_progress is not False: + raise HomeAssistantError( + f"Update installation already in progress for {entity.name}" + ) + + await entity.async_install_with_progress(version, backup) + + +async def async_skip(entity: UpdateEntity, service_call: ServiceCall) -> None: + """Service call wrapper to validate the call.""" + if entity.auto_update: + raise HomeAssistantError(f"Skipping update is not supported for {entity.name}") + await entity.async_skip() + + +@dataclass +class UpdateEntityDescription(EntityDescription): + """A class that describes update entities.""" + + device_class: UpdateDeviceClass | str | None = None + entity_category: EntityCategory | None = EntityCategory.CONFIG + + +class UpdateEntity(RestoreEntity): + """Representation of an update entity.""" + + entity_description: UpdateEntityDescription + _attr_auto_update: bool = False + _attr_installed_version: str | None = None + _attr_device_class: UpdateDeviceClass | str | None + _attr_in_progress: bool | int = False + _attr_latest_version: str | None = None + _attr_release_summary: str | None = None + _attr_release_url: str | None = None + _attr_state: None = None + _attr_supported_features: int = 0 + _attr_title: str | None = None + __skipped_version: str | None = None + __in_progress: bool = False + + @property + def auto_update(self) -> bool: + """Indicate if the device or service has auto update enabled.""" + return self._attr_auto_update + + @property + def installed_version(self) -> str | None: + """Version installed and in use.""" + return self._attr_installed_version + + @property + def device_class(self) -> UpdateDeviceClass | str | None: + """Return the class of this entity.""" + if hasattr(self, "_attr_device_class"): + return self._attr_device_class + if hasattr(self, "entity_description"): + return self.entity_description.device_class + return None + + @property + def entity_category(self) -> EntityCategory | None: + """Return the category of the entity, if any.""" + if hasattr(self, "_attr_entity_category"): + return self._attr_entity_category + if hasattr(self, "entity_description"): + return self.entity_description.entity_category + if self.supported_features & UpdateEntityFeature.INSTALL: + return EntityCategory.CONFIG + return EntityCategory.DIAGNOSTIC + + @property + def entity_picture(self) -> str | None: + """Return the entity picture to use in the frontend. + + Update entities return the brand icon based on the integration + domain by default. + """ + if self.platform is None: + return None + + return ( + f"https://brands.home-assistant.io/_/{self.platform.platform_name}/icon.png" + ) + + @property + def in_progress(self) -> bool | int | None: + """Update installation progress. + + Needs UpdateEntityFeature.PROGRESS flag to be set for it to be used. + + Can either return a boolean (True if in progress, False if not) + or an integer to indicate the progress in from 0 to 100%. + """ + return self._attr_in_progress + + @property + def latest_version(self) -> str | None: + """Latest version available for install.""" + return self._attr_latest_version + + @property + def release_summary(self) -> str | None: + """Summary of the release notes or changelog. + + This is not suitable for long changelogs, but merely suitable + for a short excerpt update description of max 255 characters. + """ + return self._attr_release_summary + + @property + def release_url(self) -> str | None: + """URL to the full release notes of the latest version available.""" + return self._attr_release_url + + @property + def supported_features(self) -> int: + """Flag supported features.""" + return self._attr_supported_features + + @property + def title(self) -> str | None: + """Title of the software. + + This helps to differentiate between the device or entity name + versus the title of the software installed. + """ + return self._attr_title + + @final + async def async_skip(self) -> None: + """Skip the current offered version to update.""" + if (latest_version := self.latest_version) is None: + raise HomeAssistantError(f"Cannot skip an unknown version for {self.name}") + if self.installed_version == latest_version: + raise HomeAssistantError(f"No update available to skip for {self.name}") + self.__skipped_version = latest_version + self.async_write_ha_state() + + async def async_install( + self, version: str | None, backup: bool, **kwargs: Any + ) -> None: + """Install an update. + + Version can be specified to install a specific version. When `None`, the + latest version needs to be installed. + + The backup parameter indicates a backup should be taken before + installing the update. + """ + await self.hass.async_add_executor_job(self.install, version, backup) + + def install(self, version: str | None, backup: bool, **kwargs: Any) -> None: + """Install an update. + + Version can be specified to install a specific version. When `None`, the + latest version needs to be installed. + + The backup parameter indicates a backup should be taken before + installing the update. + """ + raise NotImplementedError() + + async def async_release_notes(self) -> str | None: + """Return full release notes. + + This is suitable for a long changelog that does not fit in the release_summary property. + The returned string can contain markdown. + """ + return await self.hass.async_add_executor_job(self.release_notes) + + def release_notes(self) -> str | None: + """Return full release notes. + + This is suitable for a long changelog that does not fit in the release_summary property. + The returned string can contain markdown. + """ + raise NotImplementedError() + + @property + @final + def state(self) -> str | None: + """Return the entity state.""" + if (installed_version := self.installed_version) is None or ( + latest_version := self.latest_version + ) is None: + return None + + if latest_version == self.__skipped_version: + return STATE_OFF + if latest_version == installed_version: + return STATE_OFF + + try: + newer = AwesomeVersion(latest_version) > installed_version + return STATE_ON if newer else STATE_OFF + except AwesomeVersionCompareException: + # Can't compare versions, already tried exact match + return STATE_ON + + @final + @property + def state_attributes(self) -> dict[str, Any] | None: + """Return state attributes.""" + if (release_summary := self.release_summary) is not None: + release_summary = release_summary[:255] + + # If entity supports progress, return the in_progress value. + # Otherwise, we use the internal progress value. + if self.supported_features & UpdateEntityFeature.PROGRESS: + in_progress = self.in_progress + else: + in_progress = self.__in_progress + + # Clear skipped version in case it matches the current installed + # version or the latest version diverged. + if ( + self.__skipped_version == self.installed_version + or self.__skipped_version != self.latest_version + ): + self.__skipped_version = None + + return { + ATTR_AUTO_UPDATE: self.auto_update, + ATTR_INSTALLED_VERSION: self.installed_version, + ATTR_IN_PROGRESS: in_progress, + ATTR_LATEST_VERSION: self.latest_version, + ATTR_RELEASE_SUMMARY: release_summary, + ATTR_RELEASE_URL: self.release_url, + ATTR_SKIPPED_VERSION: self.__skipped_version, + ATTR_TITLE: self.title, + } + + @final + async def async_install_with_progress( + self, version: str | None, backup: bool + ) -> None: + """Install update and handle progress if needed. + + Handles setting the in_progress state in case the entity doesn't + support it natively. + """ + if not self.supported_features & UpdateEntityFeature.PROGRESS: + self.__in_progress = True + self.async_write_ha_state() + + try: + await self.async_install(version, backup) + finally: + # No matter what happens, we always stop progress in the end + self._attr_in_progress = False + self.__in_progress = False + self.async_write_ha_state() + + async def async_internal_added_to_hass(self) -> None: + """Call when the update entity is added to hass. + + It is used to restore the skipped version, if any. + """ + await super().async_internal_added_to_hass() + state = await self.async_get_last_state() + if state is not None and state.attributes.get(ATTR_SKIPPED_VERSION) is not None: + self.__skipped_version = state.attributes[ATTR_SKIPPED_VERSION] + + +@websocket_api.require_admin +@websocket_api.websocket_command( + { + vol.Required("type"): "update/release_notes", + vol.Required("entity_id"): cv.entity_id, + } +) +@websocket_api.async_response +async def websocket_release_notes( + hass: HomeAssistant, + connection: websocket_api.connection.ActiveConnection, + msg: dict, +) -> None: + """Get the full release notes for a entity.""" + component = hass.data[DOMAIN] + entity: UpdateEntity | None = component.get_entity(msg["entity_id"]) + + if entity is None: + connection.send_error( + msg["id"], websocket_api.const.ERR_NOT_FOUND, "Entity not found" + ) + return + + if not entity.supported_features & UpdateEntityFeature.RELEASE_NOTES: + connection.send_error( + msg["id"], + websocket_api.const.ERR_NOT_SUPPORTED, + "Entity does not support release notes", + ) + return + + connection.send_result( + msg["id"], + await entity.async_release_notes(), + ) diff --git a/homeassistant/components/update/const.py b/homeassistant/components/update/const.py new file mode 100644 index 00000000000..aec3c183a63 --- /dev/null +++ b/homeassistant/components/update/const.py @@ -0,0 +1,32 @@ +"""Constants for the update component.""" +from __future__ import annotations + +from enum import IntEnum +from typing import Final + +DOMAIN: Final = "update" + + +class UpdateEntityFeature(IntEnum): + """Supported features of the update entity.""" + + INSTALL = 1 + SPECIFIC_VERSION = 2 + PROGRESS = 4 + BACKUP = 8 + RELEASE_NOTES = 16 + + +SERVICE_INSTALL: Final = "install" +SERVICE_SKIP: Final = "skip" + +ATTR_AUTO_UPDATE: Final = "auto_update" +ATTR_BACKUP: Final = "backup" +ATTR_INSTALLED_VERSION: Final = "installed_version" +ATTR_IN_PROGRESS: Final = "in_progress" +ATTR_LATEST_VERSION: Final = "latest_version" +ATTR_RELEASE_SUMMARY: Final = "release_summary" +ATTR_RELEASE_URL: Final = "release_url" +ATTR_SKIPPED_VERSION: Final = "skipped_version" +ATTR_TITLE: Final = "title" +ATTR_VERSION: Final = "version" diff --git a/homeassistant/components/update/manifest.json b/homeassistant/components/update/manifest.json new file mode 100644 index 00000000000..f5fe74c9d02 --- /dev/null +++ b/homeassistant/components/update/manifest.json @@ -0,0 +1,7 @@ +{ + "domain": "update", + "name": "Update", + "documentation": "https://www.home-assistant.io/integrations/update", + "codeowners": ["@home-assistant/core"], + "quality_scale": "internal" +} diff --git a/homeassistant/components/update/recorder.py b/homeassistant/components/update/recorder.py new file mode 100644 index 00000000000..1b22360761f --- /dev/null +++ b/homeassistant/components/update/recorder.py @@ -0,0 +1,13 @@ +"""Integration platform for recorder.""" +from __future__ import annotations + +from homeassistant.const import ATTR_ENTITY_PICTURE +from homeassistant.core import HomeAssistant, callback + +from .const import ATTR_IN_PROGRESS, ATTR_RELEASE_SUMMARY + + +@callback +def exclude_attributes(hass: HomeAssistant) -> set[str]: + """Exclude large and chatty update attributes from being recorded in the database.""" + return {ATTR_ENTITY_PICTURE, ATTR_IN_PROGRESS, ATTR_RELEASE_SUMMARY} diff --git a/homeassistant/components/update/services.yaml b/homeassistant/components/update/services.yaml new file mode 100644 index 00000000000..2a3370493cc --- /dev/null +++ b/homeassistant/components/update/services.yaml @@ -0,0 +1,27 @@ +install: + name: Install update + description: Install an update for this device or service + target: + entity: + domain: update + fields: + version: + name: Version + description: Version to install, if omitted, the latest version will be installed. + required: false + example: "1.0.0" + selector: + text: + backup: + name: Backup + description: Backup before installing the update, if supported by the integration. + required: false + selector: + boolean: + +skip: + name: Skip update + description: Mark currently available update as skipped. + target: + entity: + domain: update diff --git a/homeassistant/components/update/significant_change.py b/homeassistant/components/update/significant_change.py new file mode 100644 index 00000000000..8b37d227a1f --- /dev/null +++ b/homeassistant/components/update/significant_change.py @@ -0,0 +1,30 @@ +"""Helper to test significant update state changes.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.core import HomeAssistant, callback + +from .const import ATTR_INSTALLED_VERSION, ATTR_LATEST_VERSION + + +@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 + + if old_attrs.get(ATTR_INSTALLED_VERSION) != new_attrs.get(ATTR_INSTALLED_VERSION): + return True + + if old_attrs.get(ATTR_LATEST_VERSION) != new_attrs.get(ATTR_LATEST_VERSION): + return True + + return False diff --git a/homeassistant/components/update/strings.json b/homeassistant/components/update/strings.json new file mode 100644 index 00000000000..b079c9ec8b6 --- /dev/null +++ b/homeassistant/components/update/strings.json @@ -0,0 +1,3 @@ +{ + "title": "Update" +} diff --git a/homeassistant/components/update/translations/bg.json b/homeassistant/components/update/translations/bg.json new file mode 100644 index 00000000000..661fdd4f830 --- /dev/null +++ b/homeassistant/components/update/translations/bg.json @@ -0,0 +1,3 @@ +{ + "title": "\u0410\u043a\u0442\u0443\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f" +} \ No newline at end of file diff --git a/homeassistant/components/update/translations/ca.json b/homeassistant/components/update/translations/ca.json new file mode 100644 index 00000000000..396e79c14c0 --- /dev/null +++ b/homeassistant/components/update/translations/ca.json @@ -0,0 +1,3 @@ +{ + "title": "Actualitza" +} \ No newline at end of file diff --git a/homeassistant/components/update/translations/de.json b/homeassistant/components/update/translations/de.json new file mode 100644 index 00000000000..18562d81eaf --- /dev/null +++ b/homeassistant/components/update/translations/de.json @@ -0,0 +1,3 @@ +{ + "title": "Aktualisieren" +} \ No newline at end of file diff --git a/homeassistant/components/update/translations/el.json b/homeassistant/components/update/translations/el.json new file mode 100644 index 00000000000..d687d342ec3 --- /dev/null +++ b/homeassistant/components/update/translations/el.json @@ -0,0 +1,3 @@ +{ + "title": "\u0395\u03bd\u03b7\u03bc\u03ad\u03c1\u03c9\u03c3\u03b7" +} \ No newline at end of file diff --git a/homeassistant/components/update/translations/en.json b/homeassistant/components/update/translations/en.json new file mode 100644 index 00000000000..95b82de3b4d --- /dev/null +++ b/homeassistant/components/update/translations/en.json @@ -0,0 +1,3 @@ +{ + "title": "Update" +} \ No newline at end of file diff --git a/homeassistant/components/update/translations/et.json b/homeassistant/components/update/translations/et.json new file mode 100644 index 00000000000..7db9a98a507 --- /dev/null +++ b/homeassistant/components/update/translations/et.json @@ -0,0 +1,3 @@ +{ + "title": "Uuenda" +} \ No newline at end of file diff --git a/homeassistant/components/update/translations/fr.json b/homeassistant/components/update/translations/fr.json new file mode 100644 index 00000000000..1a49a0cab9f --- /dev/null +++ b/homeassistant/components/update/translations/fr.json @@ -0,0 +1,3 @@ +{ + "title": "Mettre \u00e0 jour" +} \ No newline at end of file diff --git a/homeassistant/components/update/translations/hu.json b/homeassistant/components/update/translations/hu.json new file mode 100644 index 00000000000..1e2ec425a88 --- /dev/null +++ b/homeassistant/components/update/translations/hu.json @@ -0,0 +1,3 @@ +{ + "title": "Friss\u00edt\u00e9s" +} \ No newline at end of file diff --git a/homeassistant/components/update/translations/id.json b/homeassistant/components/update/translations/id.json new file mode 100644 index 00000000000..70f495575fa --- /dev/null +++ b/homeassistant/components/update/translations/id.json @@ -0,0 +1,3 @@ +{ + "title": "Versi Baru" +} \ No newline at end of file diff --git a/homeassistant/components/update/translations/it.json b/homeassistant/components/update/translations/it.json new file mode 100644 index 00000000000..539f0bb4294 --- /dev/null +++ b/homeassistant/components/update/translations/it.json @@ -0,0 +1,3 @@ +{ + "title": "Aggiornamento" +} \ No newline at end of file diff --git a/homeassistant/components/update/translations/ja.json b/homeassistant/components/update/translations/ja.json new file mode 100644 index 00000000000..4cb5e4959a1 --- /dev/null +++ b/homeassistant/components/update/translations/ja.json @@ -0,0 +1,3 @@ +{ + "title": "\u30a2\u30c3\u30d7\u30c7\u30fc\u30c8" +} \ No newline at end of file diff --git a/homeassistant/components/update/translations/nl.json b/homeassistant/components/update/translations/nl.json new file mode 100644 index 00000000000..95b82de3b4d --- /dev/null +++ b/homeassistant/components/update/translations/nl.json @@ -0,0 +1,3 @@ +{ + "title": "Update" +} \ No newline at end of file diff --git a/homeassistant/components/update/translations/no.json b/homeassistant/components/update/translations/no.json new file mode 100644 index 00000000000..e98d60ab4fc --- /dev/null +++ b/homeassistant/components/update/translations/no.json @@ -0,0 +1,3 @@ +{ + "title": "Oppdater" +} \ No newline at end of file diff --git a/homeassistant/components/update/translations/pl.json b/homeassistant/components/update/translations/pl.json new file mode 100644 index 00000000000..eff0431a518 --- /dev/null +++ b/homeassistant/components/update/translations/pl.json @@ -0,0 +1,3 @@ +{ + "title": "Aktualizacja" +} \ No newline at end of file diff --git a/homeassistant/components/update/translations/pt-BR.json b/homeassistant/components/update/translations/pt-BR.json new file mode 100644 index 00000000000..4003445e2c3 --- /dev/null +++ b/homeassistant/components/update/translations/pt-BR.json @@ -0,0 +1,3 @@ +{ + "title": "Atualiza\u00e7\u00e3o" +} \ No newline at end of file diff --git a/homeassistant/components/update/translations/ru.json b/homeassistant/components/update/translations/ru.json new file mode 100644 index 00000000000..a2ee79efd15 --- /dev/null +++ b/homeassistant/components/update/translations/ru.json @@ -0,0 +1,3 @@ +{ + "title": "\u041e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435" +} \ No newline at end of file diff --git a/homeassistant/components/update/translations/tr.json b/homeassistant/components/update/translations/tr.json new file mode 100644 index 00000000000..30c3c90c437 --- /dev/null +++ b/homeassistant/components/update/translations/tr.json @@ -0,0 +1,3 @@ +{ + "title": "G\u00fcncelle" +} \ No newline at end of file diff --git a/homeassistant/components/update/translations/zh-Hant.json b/homeassistant/components/update/translations/zh-Hant.json new file mode 100644 index 00000000000..46cdfb48f90 --- /dev/null +++ b/homeassistant/components/update/translations/zh-Hant.json @@ -0,0 +1,3 @@ +{ + "title": "\u66f4\u65b0" +} \ No newline at end of file diff --git a/homeassistant/components/updater/translations/el.json b/homeassistant/components/updater/translations/el.json index b3ae655e025..f44dc928c16 100644 --- a/homeassistant/components/updater/translations/el.json +++ b/homeassistant/components/updater/translations/el.json @@ -1,3 +1,3 @@ { - "title": "\u0395\u03c0\u03b9\u03ba\u03b1\u03b9\u03c1\u03bf\u03c0\u03bf\u03b9\u03b7\u03c4\u03ae\u03c2" + "title": "\u0395\u03bd\u03b7\u03bc\u03b5\u03c1\u03c9\u03c4\u03ae\u03c2" } \ No newline at end of file diff --git a/homeassistant/components/upnp/__init__.py b/homeassistant/components/upnp/__init__.py index 5180932080c..e15c90c7079 100644 --- a/homeassistant/components/upnp/__init__.py +++ b/homeassistant/components/upnp/__init__.py @@ -121,6 +121,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: cancel_discovered_callback() # Create device. + assert discovery_info is not None + assert discovery_info.ssdp_location is not None location = discovery_info.ssdp_location try: device = await Device.async_create_device(hass, location) @@ -177,8 +179,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() - # Create sensors. - LOGGER.debug("Enabling sensors") + # Setup platforms, creating sensors/binary_sensors. hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True @@ -188,7 +189,7 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> """Unload a UPnP/IGD device from a config entry.""" LOGGER.debug("Unloading config entry: %s", config_entry.unique_id) - LOGGER.debug("Deleting sensors") + # Unload platforms. return await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS) @@ -234,10 +235,9 @@ class UpnpDataUpdateCoordinator(DataUpdateCoordinator): } -class UpnpEntity(CoordinatorEntity): +class UpnpEntity(CoordinatorEntity[UpnpDataUpdateCoordinator]): """Base class for UPnP/IGD entities.""" - coordinator: UpnpDataUpdateCoordinator entity_description: UpnpSensorEntityDescription | UpnpBinarySensorEntityDescription def __init__( diff --git a/homeassistant/components/upnp/binary_sensor.py b/homeassistant/components/upnp/binary_sensor.py index 1ff1164cc58..d49bbddf996 100644 --- a/homeassistant/components/upnp/binary_sensor.py +++ b/homeassistant/components/upnp/binary_sensor.py @@ -28,8 +28,6 @@ async def async_setup_entry( """Set up the UPnP/IGD sensors.""" coordinator = hass.data[DOMAIN][config_entry.entry_id] - LOGGER.debug("Adding binary sensor") - entities = [ UpnpStatusBinarySensor( coordinator=coordinator, @@ -38,13 +36,14 @@ async def async_setup_entry( for entity_description in BINARYSENSOR_ENTITY_DESCRIPTIONS if coordinator.data.get(entity_description.key) is not None ] - LOGGER.debug("Adding entities: %s", entities) + LOGGER.debug("Adding binary_sensor entities: %s", entities) async_add_entities(entities) class UpnpStatusBinarySensor(UpnpEntity, BinarySensorEntity): """Class for UPnP/IGD binary sensors.""" + entity_description: UpnpBinarySensorEntityDescription _attr_device_class = BinarySensorDeviceClass.CONNECTIVITY def __init__( diff --git a/homeassistant/components/upnp/config_flow.py b/homeassistant/components/upnp/config_flow.py index 74acb88983b..e339c69d5d8 100644 --- a/homeassistant/components/upnp/config_flow.py +++ b/homeassistant/components/upnp/config_flow.py @@ -4,7 +4,7 @@ from __future__ import annotations import asyncio from collections.abc import Mapping from datetime import timedelta -from typing import Any +from typing import Any, cast import voluptuous as vol @@ -31,16 +31,17 @@ from .const import ( def _friendly_name_from_discovery(discovery_info: ssdp.SsdpServiceInfo) -> str: """Extract user-friendly name from discovery.""" - return ( + return cast( + str, discovery_info.upnp.get(ssdp.ATTR_UPNP_FRIENDLY_NAME) or discovery_info.upnp.get(ssdp.ATTR_UPNP_MODEL_NAME) - or discovery_info.ssdp_headers.get("_host", "") + or discovery_info.ssdp_headers.get("_host", ""), ) def _is_complete_discovery(discovery_info: ssdp.SsdpServiceInfo) -> bool: """Test if discovery is complete and usable.""" - return ( + return bool( ssdp.ATTR_UPNP_UDN in discovery_info.upnp and discovery_info.ssdp_st and discovery_info.ssdp_location @@ -114,14 +115,13 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Initialize the UPnP/IGD config flow.""" self._discoveries: list[SsdpServiceInfo] | None = None - async def async_step_user( - self, user_input: Mapping | None = None - ) -> Mapping[str, Any]: + async def async_step_user(self, user_input: Mapping | None = None) -> FlowResult: """Handle a flow start.""" LOGGER.debug("async_step_user: user_input: %s", user_input) if user_input is not None: # Ensure wanted device was discovered. + assert self._discoveries matching_discoveries = [ discovery for discovery in self._discoveries @@ -222,7 +222,9 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): unique_id = discovery_info.ssdp_usn await self.async_set_unique_id(unique_id) hostname = discovery_info.ssdp_headers["_host"] - self._abort_if_unique_id_configured(updates={CONFIG_ENTRY_HOSTNAME: hostname}) + self._abort_if_unique_id_configured( + updates={CONFIG_ENTRY_HOSTNAME: hostname}, reload_on_update=False + ) # Handle devices changing their UDN, only allow a single host. existing_entries = self._async_current_entries() @@ -246,12 +248,13 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_ssdp_confirm( self, user_input: Mapping | None = None - ) -> Mapping[str, Any]: + ) -> FlowResult: """Confirm integration via SSDP.""" LOGGER.debug("async_step_ssdp_confirm: user_input: %s", user_input) if user_input is None: return self.async_show_form(step_id="ssdp_confirm") + assert self._discoveries discovery = self._discoveries[0] return await self._async_create_entry_from_discovery(discovery) @@ -266,7 +269,7 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def _async_create_entry_from_discovery( self, discovery: SsdpServiceInfo, - ) -> Mapping[str, Any]: + ) -> FlowResult: """Create an entry from discovery.""" LOGGER.debug( "_async_create_entry_from_discovery: discovery: %s", @@ -289,7 +292,7 @@ class UpnpOptionsFlowHandler(config_entries.OptionsFlow): """Initialize.""" self.config_entry = config_entry - async def async_step_init(self, user_input: Mapping = None) -> None: + async def async_step_init(self, user_input: Mapping = None) -> FlowResult: """Manage the options.""" if user_input is not None: coordinator = self.hass.data[DOMAIN][self.config_entry.entry_id] diff --git a/homeassistant/components/upnp/device.py b/homeassistant/components/upnp/device.py index 3231d34a342..a3b5e63bf41 100644 --- a/homeassistant/components/upnp/device.py +++ b/homeassistant/components/upnp/device.py @@ -6,10 +6,11 @@ from collections.abc import Mapping from typing import Any from urllib.parse import urlparse -from async_upnp_client import UpnpDevice, UpnpFactory from async_upnp_client.aiohttp import AiohttpSessionRequester +from async_upnp_client.client import UpnpDevice +from async_upnp_client.client_factory import UpnpFactory from async_upnp_client.exceptions import UpnpError -from async_upnp_client.profiles.igd import IgdDevice +from async_upnp_client.profiles.igd import IgdDevice, StatusInfo from homeassistant.components import ssdp from homeassistant.components.ssdp import SsdpChange, SsdpServiceInfo @@ -49,7 +50,7 @@ class Device: """Initialize UPnP/IGD device.""" self.hass = hass self._igd_device = igd_device - self.coordinator: DataUpdateCoordinator = None + self.coordinator: DataUpdateCoordinator | None = None @classmethod async def async_create_device( @@ -80,6 +81,10 @@ class Device: if service_info.ssdp_location is None: return + if change == SsdpChange.ALIVE: + # We care only about updates. + return + device = self._igd_device.device if service_info.ssdp_location == device.device_url: return @@ -125,7 +130,7 @@ class Device: return self.usn @property - def hostname(self) -> str: + def hostname(self) -> str | None: """Get the hostname.""" url = self._igd_device.device.device_url parsed = urlparse(url) @@ -173,7 +178,9 @@ class Device: self._igd_device.async_get_external_ip_address(), return_exceptions=True, ) - result = [] + status_info: StatusInfo | None = None + ip_address: str | None = None + for idx, value in enumerate(values): if isinstance(value, UpnpError): # Not all routers support some of these items although based @@ -184,16 +191,18 @@ class Device: self, str(value), ) - result.append(None) continue if isinstance(value, Exception): raise value - result.append(value) + if isinstance(value, StatusInfo): + status_info = value + elif isinstance(value, str): + ip_address = value return { - WAN_STATUS: result[0][0] if result[0] is not None else None, - ROUTER_UPTIME: result[0][2] if result[0] is not None else None, - ROUTER_IP: result[1], + WAN_STATUS: status_info[0] if status_info is not None else None, + ROUTER_UPTIME: status_info[2] if status_info is not None else None, + ROUTER_IP: ip_address, } diff --git a/homeassistant/components/upnp/manifest.json b/homeassistant/components/upnp/manifest.json index 5b630973f67..f3c11c61841 100644 --- a/homeassistant/components/upnp/manifest.json +++ b/homeassistant/components/upnp/manifest.json @@ -3,9 +3,9 @@ "name": "UPnP/IGD", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/upnp", - "requirements": ["async-upnp-client==0.23.5"], + "requirements": ["async-upnp-client==0.27.0"], "dependencies": ["network", "ssdp"], - "codeowners": ["@StevenLooman","@ehendrix23"], + "codeowners": ["@StevenLooman", "@ehendrix23"], "ssdp": [ { "st": "urn:schemas-upnp-org:device:InternetGatewayDevice:1" diff --git a/homeassistant/components/upnp/sensor.py b/homeassistant/components/upnp/sensor.py index 54176715b84..908a5b53940 100644 --- a/homeassistant/components/upnp/sensor.py +++ b/homeassistant/components/upnp/sensor.py @@ -136,13 +136,15 @@ async def async_setup_entry( ] ) - LOGGER.debug("Adding entities: %s", entities) + LOGGER.debug("Adding sensor entities: %s", entities) async_add_entities(entities) class UpnpSensor(UpnpEntity, SensorEntity): """Base class for UPnP/IGD sensors.""" + entity_description: UpnpSensorEntityDescription + class RawUpnpSensor(UpnpSensor): """Representation of a UPnP/IGD sensor.""" @@ -159,8 +161,6 @@ class RawUpnpSensor(UpnpSensor): class DerivedUpnpSensor(UpnpSensor): """Representation of a UNIT Sent/Received per second sensor.""" - entity_description: UpnpSensorEntityDescription - def __init__( self, coordinator: UpnpDataUpdateCoordinator, diff --git a/homeassistant/components/upnp/strings.json b/homeassistant/components/upnp/strings.json index b0bc476ae61..45d0c7de1c8 100644 --- a/homeassistant/components/upnp/strings.json +++ b/homeassistant/components/upnp/strings.json @@ -25,5 +25,5 @@ } } } -} + } } diff --git a/homeassistant/components/uptime/__init__.py b/homeassistant/components/uptime/__init__.py index 99abc91cdf1..1c36fea3b32 100644 --- a/homeassistant/components/uptime/__init__.py +++ b/homeassistant/components/uptime/__init__.py @@ -1 +1,16 @@ -"""The uptime component.""" +"""The Uptime integration.""" +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from .const import PLATFORMS + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up from a config entry.""" + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/uptime/config_flow.py b/homeassistant/components/uptime/config_flow.py new file mode 100644 index 00000000000..6ff36ee34b1 --- /dev/null +++ b/homeassistant/components/uptime/config_flow.py @@ -0,0 +1,37 @@ +"""Config flow to configure the Uptime integration.""" +from __future__ import annotations + +from typing import Any + +import voluptuous as vol + +from homeassistant.config_entries import ConfigFlow +from homeassistant.const import CONF_NAME +from homeassistant.data_entry_flow import FlowResult + +from .const import DEFAULT_NAME, DOMAIN + + +class UptimeConfigFlow(ConfigFlow, domain=DOMAIN): + """Config flow for Uptime.""" + + VERSION = 1 + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle a flow initialized by the user.""" + if self._async_current_entries(): + return self.async_abort(reason="single_instance_allowed") + + if user_input is not None: + return self.async_create_entry( + title=user_input.get(CONF_NAME, DEFAULT_NAME), + data={}, + ) + + return self.async_show_form(step_id="user", data_schema=vol.Schema({})) + + async def async_step_import(self, user_input: dict[str, Any]) -> FlowResult: + """Handle import from configuration.yaml.""" + return await self.async_step_user(user_input) diff --git a/homeassistant/components/uptime/const.py b/homeassistant/components/uptime/const.py new file mode 100644 index 00000000000..bbce8021474 --- /dev/null +++ b/homeassistant/components/uptime/const.py @@ -0,0 +1,9 @@ +"""Constants for the Uptime integration.""" +from typing import Final + +from homeassistant.const import Platform + +DOMAIN: Final = "uptime" +PLATFORMS: Final = [Platform.SENSOR] + +DEFAULT_NAME: Final = "Uptime" diff --git a/homeassistant/components/uptime/manifest.json b/homeassistant/components/uptime/manifest.json index cf2dd1a6ea1..3bcc47815f8 100644 --- a/homeassistant/components/uptime/manifest.json +++ b/homeassistant/components/uptime/manifest.json @@ -2,7 +2,8 @@ "domain": "uptime", "name": "Uptime", "documentation": "https://www.home-assistant.io/integrations/uptime", - "codeowners": [], + "codeowners": ["@frenck"], "quality_scale": "internal", - "iot_class": "local_push" + "iot_class": "local_push", + "config_flow": true } diff --git a/homeassistant/components/uptime/sensor.py b/homeassistant/components/uptime/sensor.py index a622835a0da..944f9b77de8 100644 --- a/homeassistant/components/uptime/sensor.py +++ b/homeassistant/components/uptime/sensor.py @@ -8,6 +8,7 @@ from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, ) +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_NAME, CONF_UNIT_OF_MEASUREMENT from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -15,17 +16,15 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType import homeassistant.util.dt as dt_util -DEFAULT_NAME = "Uptime" +from .const import DEFAULT_NAME, DOMAIN PLATFORM_SCHEMA = vol.All( - cv.deprecated(CONF_UNIT_OF_MEASUREMENT), + cv.removed(CONF_UNIT_OF_MEASUREMENT, raise_if_present=False), PLATFORM_SCHEMA.extend( { vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_UNIT_OF_MEASUREMENT, default="days"): vol.All( - cv.string, vol.In(["minutes", "hours", "days", "seconds"]) - ), - } + vol.Remove(CONF_UNIT_OF_MEASUREMENT): cv.string, + }, ), ) @@ -37,9 +36,22 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the uptime sensor platform.""" - name = config[CONF_NAME] + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=config, + ) + ) - async_add_entities([UptimeSensor(name)], True) + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the platform from config_entry.""" + async_add_entities([UptimeSensor(entry)]) class UptimeSensor(SensorEntity): @@ -48,7 +60,8 @@ class UptimeSensor(SensorEntity): _attr_device_class = SensorDeviceClass.TIMESTAMP _attr_should_poll = False - def __init__(self, name: str) -> None: + def __init__(self, entry: ConfigEntry) -> None: """Initialize the uptime sensor.""" - self._attr_name = name + self._attr_name = entry.title self._attr_native_value = dt_util.utcnow() + self._attr_unique_id = entry.entry_id diff --git a/homeassistant/components/uptime/strings.json b/homeassistant/components/uptime/strings.json new file mode 100644 index 00000000000..9ceb91de9ba --- /dev/null +++ b/homeassistant/components/uptime/strings.json @@ -0,0 +1,13 @@ +{ + "title": "Uptime", + "config": { + "step": { + "user": { + "description": "[%key:common::config_flow::description::confirm_setup%]" + } + }, + "abort": { + "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" + } + } +} diff --git a/homeassistant/components/uptime/translations/ca.json b/homeassistant/components/uptime/translations/ca.json new file mode 100644 index 00000000000..fe8852d4488 --- /dev/null +++ b/homeassistant/components/uptime/translations/ca.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3." + }, + "step": { + "user": { + "description": "Vols comen\u00e7ar la configuraci\u00f3?" + } + } + }, + "title": "Temps en funcionament" +} \ No newline at end of file diff --git a/homeassistant/components/uptime/translations/de.json b/homeassistant/components/uptime/translations/de.json new file mode 100644 index 00000000000..1aa173edbd8 --- /dev/null +++ b/homeassistant/components/uptime/translations/de.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." + }, + "step": { + "user": { + "description": "M\u00f6chtest Du mit der Einrichtung beginnen?" + } + } + }, + "title": "Betriebszeit" +} \ No newline at end of file diff --git a/homeassistant/components/uptime/translations/el.json b/homeassistant/components/uptime/translations/el.json new file mode 100644 index 00000000000..d70141f2173 --- /dev/null +++ b/homeassistant/components/uptime/translations/el.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u0388\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af. \u039c\u03cc\u03bd\u03bf \u03bc\u03af\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae." + }, + "step": { + "user": { + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03be\u03b5\u03ba\u03b9\u03bd\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7;" + } + } + }, + "title": "\u03a7\u03c1\u03cc\u03bd\u03bf\u03c2 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1\u03c2" +} \ No newline at end of file diff --git a/homeassistant/components/uptime/translations/en.json b/homeassistant/components/uptime/translations/en.json new file mode 100644 index 00000000000..5d38ae74e21 --- /dev/null +++ b/homeassistant/components/uptime/translations/en.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Already configured. Only a single configuration possible." + }, + "step": { + "user": { + "description": "Do you want to start set up?" + } + } + }, + "title": "Uptime" +} \ No newline at end of file diff --git a/homeassistant/components/uptime/translations/et.json b/homeassistant/components/uptime/translations/et.json new file mode 100644 index 00000000000..f1b40328dab --- /dev/null +++ b/homeassistant/components/uptime/translations/et.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Juba seadistatud. lubatud on ainult \u00fcks sidumine." + }, + "step": { + "user": { + "description": "Kas alustan seadistamist?" + } + } + }, + "title": "T\u00f6\u00f6aeg" +} \ No newline at end of file diff --git a/homeassistant/components/uptime/translations/fr.json b/homeassistant/components/uptime/translations/fr.json new file mode 100644 index 00000000000..a550e33f51f --- /dev/null +++ b/homeassistant/components/uptime/translations/fr.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." + }, + "step": { + "user": { + "description": "Voulez-vous commencer la configuration\u00a0?" + } + } + }, + "title": "Dur\u00e9e de fonctionnement" +} \ No newline at end of file diff --git a/homeassistant/components/uptime/translations/he.json b/homeassistant/components/uptime/translations/he.json new file mode 100644 index 00000000000..d22746eb70d --- /dev/null +++ b/homeassistant/components/uptime/translations/he.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." + }, + "step": { + "user": { + "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05ea\u05d7\u05d9\u05dc \u05d1\u05d4\u05d2\u05d3\u05e8\u05d4?" + } + } + }, + "title": "\u05d6\u05de\u05df \u05e4\u05e2\u05d9\u05dc\u05d5\u05ea" +} \ No newline at end of file diff --git a/homeassistant/components/uptime/translations/hu.json b/homeassistant/components/uptime/translations/hu.json new file mode 100644 index 00000000000..241c28b48ea --- /dev/null +++ b/homeassistant/components/uptime/translations/hu.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." + }, + "step": { + "user": { + "description": "El szeretn\u00e9 kezdeni a be\u00e1ll\u00edt\u00e1st?" + } + } + }, + "title": "Uptime" +} \ No newline at end of file diff --git a/homeassistant/components/uptime/translations/it.json b/homeassistant/components/uptime/translations/it.json new file mode 100644 index 00000000000..cba8d0a264f --- /dev/null +++ b/homeassistant/components/uptime/translations/it.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "user": { + "description": "Vuoi iniziare la configurazione?" + } + } + }, + "title": "Tempo di funzionamento" +} \ No newline at end of file diff --git a/homeassistant/components/uptime/translations/ja.json b/homeassistant/components/uptime/translations/ja.json new file mode 100644 index 00000000000..99dc644a0e5 --- /dev/null +++ b/homeassistant/components/uptime/translations/ja.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + }, + "step": { + "user": { + "description": "\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3092\u958b\u59cb\u3057\u307e\u3059\u304b\uff1f" + } + } + }, + "title": "\u30a2\u30c3\u30d7\u30bf\u30a4\u30e0" +} \ No newline at end of file diff --git a/homeassistant/components/uptime/translations/nl.json b/homeassistant/components/uptime/translations/nl.json new file mode 100644 index 00000000000..786fec37f6b --- /dev/null +++ b/homeassistant/components/uptime/translations/nl.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk." + }, + "step": { + "user": { + "description": "Wilt u beginnen met instellen?" + } + } + }, + "title": "Uptime" +} \ No newline at end of file diff --git a/homeassistant/components/uptime/translations/pt-BR.json b/homeassistant/components/uptime/translations/pt-BR.json new file mode 100644 index 00000000000..fdec46961b5 --- /dev/null +++ b/homeassistant/components/uptime/translations/pt-BR.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o poss\u00edvel." + }, + "step": { + "user": { + "description": "Deseja iniciar a configura\u00e7\u00e3o?" + } + } + }, + "title": "Tempo de atividade" +} \ No newline at end of file diff --git a/homeassistant/components/uptime/translations/pt.json b/homeassistant/components/uptime/translations/pt.json new file mode 100644 index 00000000000..a61cff40967 --- /dev/null +++ b/homeassistant/components/uptime/translations/pt.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "J\u00e1 foi configurado. S\u00f3 \u00e9 possivel existir uma \u00fanica configura\u00e7\u00e3o." + }, + "step": { + "user": { + "description": "Quer iniciar a configura\u00e7\u00e3o?" + } + } + }, + "title": "Tempo de atividade" +} \ No newline at end of file diff --git a/homeassistant/components/uptime/translations/ru.json b/homeassistant/components/uptime/translations/ru.json new file mode 100644 index 00000000000..7c6270221a7 --- /dev/null +++ b/homeassistant/components/uptime/translations/ru.json @@ -0,0 +1,13 @@ +{ + "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." + }, + "step": { + "user": { + "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0447\u0430\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443?" + } + } + }, + "title": "\u0412\u0440\u0435\u043c\u044f \u0440\u0430\u0431\u043e\u0442\u044b \u0441\u0435\u0440\u0432\u0435\u0440\u0430" +} \ No newline at end of file diff --git a/homeassistant/components/uptime/translations/zh-Hant.json b/homeassistant/components/uptime/translations/zh-Hant.json new file mode 100644 index 00000000000..ed0b902348c --- /dev/null +++ b/homeassistant/components/uptime/translations/zh-Hant.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" + }, + "step": { + "user": { + "description": "\u662f\u5426\u8981\u958b\u59cb\u8a2d\u5b9a\uff1f" + } + } + }, + "title": "\u904b\u4f5c\u6642\u9593" +} \ No newline at end of file diff --git a/homeassistant/components/uptimerobot/__init__.py b/homeassistant/components/uptimerobot/__init__.py index 06da8a1b4b1..6d9be1b2364 100644 --- a/homeassistant/components/uptimerobot/__init__.py +++ b/homeassistant/components/uptimerobot/__init__.py @@ -26,9 +26,12 @@ from .const import API_ATTR_OK, COORDINATOR_UPDATE_INTERVAL, DOMAIN, LOGGER, PLA async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up UptimeRobot from a config entry.""" hass.data.setdefault(DOMAIN, {}) - uptime_robot_api = UptimeRobot( - entry.data[CONF_API_KEY], async_get_clientsession(hass) - ) + key: str = entry.data[CONF_API_KEY] + if key.startswith("ur") or key.startswith("m"): + raise ConfigEntryAuthFailed( + "Wrong API key type detected, use the 'main' API key" + ) + uptime_robot_api = UptimeRobot(key, async_get_clientsession(hass)) dev_reg = await async_get_registry(hass) hass.data[DOMAIN][entry.entry_id] = coordinator = UptimeRobotDataUpdateCoordinator( @@ -58,6 +61,7 @@ class UptimeRobotDataUpdateCoordinator(DataUpdateCoordinator): """Data update coordinator for UptimeRobot.""" data: list[UptimeRobotMonitor] + config_entry: ConfigEntry def __init__( self, diff --git a/homeassistant/components/uptimerobot/binary_sensor.py b/homeassistant/components/uptimerobot/binary_sensor.py index 40f850c5376..248212a8345 100644 --- a/homeassistant/components/uptimerobot/binary_sensor.py +++ b/homeassistant/components/uptimerobot/binary_sensor.py @@ -23,18 +23,16 @@ async def async_setup_entry( """Set up the UptimeRobot binary_sensors.""" coordinator: UptimeRobotDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] async_add_entities( - [ - UptimeRobotBinarySensor( - coordinator, - BinarySensorEntityDescription( - key=str(monitor.id), - name=monitor.friendly_name, - device_class=BinarySensorDeviceClass.CONNECTIVITY, - ), - monitor=monitor, - ) - for monitor in coordinator.data - ], + UptimeRobotBinarySensor( + coordinator, + BinarySensorEntityDescription( + key=str(monitor.id), + name=monitor.friendly_name, + device_class=BinarySensorDeviceClass.CONNECTIVITY, + ), + monitor=monitor, + ) + for monitor in coordinator.data ) diff --git a/homeassistant/components/uptimerobot/config_flow.py b/homeassistant/components/uptimerobot/config_flow.py index 3f08e7e692e..5b6ac1d4880 100644 --- a/homeassistant/components/uptimerobot/config_flow.py +++ b/homeassistant/components/uptimerobot/config_flow.py @@ -34,9 +34,12 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Validate the user input allows us to connect.""" errors: dict[str, str] = {} response: UptimeRobotApiResponse | UptimeRobotApiError | None = None - uptime_robot_api = UptimeRobot( - data[CONF_API_KEY], async_get_clientsession(self.hass) - ) + key: str = data[CONF_API_KEY] + if key.startswith("ur") or key.startswith("m"): + LOGGER.error("Wrong API key type detected, use the 'main' API key") + errors["base"] = "not_main_key" + return errors, None + uptime_robot_api = UptimeRobot(key, async_get_clientsession(self.hass)) try: response = await uptime_robot_api.async_get_account_details() diff --git a/homeassistant/components/uptimerobot/const.py b/homeassistant/components/uptimerobot/const.py index e98ae7b514c..e89c1c38e0e 100644 --- a/homeassistant/components/uptimerobot/const.py +++ b/homeassistant/components/uptimerobot/const.py @@ -13,7 +13,7 @@ LOGGER: Logger = getLogger(__package__) COORDINATOR_UPDATE_INTERVAL: timedelta = timedelta(seconds=10) DOMAIN: Final = "uptimerobot" -PLATFORMS: Final = [Platform.BINARY_SENSOR, Platform.SENSOR] +PLATFORMS: Final = [Platform.BINARY_SENSOR, Platform.SENSOR, Platform.SWITCH] ATTRIBUTION: Final = "Data provided by UptimeRobot" diff --git a/homeassistant/components/uptimerobot/entity.py b/homeassistant/components/uptimerobot/entity.py index 318e5a5094e..7991525c2a0 100644 --- a/homeassistant/components/uptimerobot/entity.py +++ b/homeassistant/components/uptimerobot/entity.py @@ -5,22 +5,20 @@ from pyuptimerobot import UptimeRobotMonitor from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo, EntityDescription -from homeassistant.helpers.update_coordinator import ( - CoordinatorEntity, - DataUpdateCoordinator, -) +from homeassistant.helpers.update_coordinator import CoordinatorEntity +from . import UptimeRobotDataUpdateCoordinator from .const import ATTR_TARGET, ATTRIBUTION, DOMAIN -class UptimeRobotEntity(CoordinatorEntity): +class UptimeRobotEntity(CoordinatorEntity[UptimeRobotDataUpdateCoordinator]): """Base UptimeRobot entity.""" _attr_attribution = ATTRIBUTION def __init__( self, - coordinator: DataUpdateCoordinator, + coordinator: UptimeRobotDataUpdateCoordinator, description: EntityDescription, monitor: UptimeRobotMonitor, ) -> None: @@ -40,6 +38,7 @@ class UptimeRobotEntity(CoordinatorEntity): ATTR_TARGET: self.monitor.url, } self._attr_unique_id = str(self.monitor.id) + self.api = coordinator.api @property def _monitors(self) -> list[UptimeRobotMonitor]: diff --git a/homeassistant/components/uptimerobot/manifest.json b/homeassistant/components/uptimerobot/manifest.json index de6399c2e9f..52d10f2880a 100644 --- a/homeassistant/components/uptimerobot/manifest.json +++ b/homeassistant/components/uptimerobot/manifest.json @@ -2,14 +2,10 @@ "domain": "uptimerobot", "name": "UptimeRobot", "documentation": "https://www.home-assistant.io/integrations/uptimerobot", - "requirements": [ - "pyuptimerobot==22.2.0" - ], - "codeowners": [ - "@ludeeus", "@chemelli74" - ], + "requirements": ["pyuptimerobot==22.2.0"], + "codeowners": ["@ludeeus", "@chemelli74"], "quality_scale": "platinum", "iot_class": "cloud_polling", "config_flow": true, "loggers": ["pyuptimerobot"] -} \ No newline at end of file +} diff --git a/homeassistant/components/uptimerobot/sensor.py b/homeassistant/components/uptimerobot/sensor.py index 77f32e20b4b..0e450bf24b7 100644 --- a/homeassistant/components/uptimerobot/sensor.py +++ b/homeassistant/components/uptimerobot/sensor.py @@ -38,19 +38,17 @@ async def async_setup_entry( """Set up the UptimeRobot sensors.""" coordinator: UptimeRobotDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] async_add_entities( - [ - UptimeRobotSensor( - coordinator, - SensorEntityDescription( - key=str(monitor.id), - name=monitor.friendly_name, - entity_category=EntityCategory.DIAGNOSTIC, - device_class="uptimerobot__monitor_status", - ), - monitor=monitor, - ) - for monitor in coordinator.data - ], + UptimeRobotSensor( + coordinator, + SensorEntityDescription( + key=str(monitor.id), + name=monitor.friendly_name, + entity_category=EntityCategory.DIAGNOSTIC, + device_class="uptimerobot__monitor_status", + ), + monitor=monitor, + ) + for monitor in coordinator.data ) diff --git a/homeassistant/components/uptimerobot/strings.json b/homeassistant/components/uptimerobot/strings.json index 2946f2e2d5d..4cca3c159ae 100644 --- a/homeassistant/components/uptimerobot/strings.json +++ b/homeassistant/components/uptimerobot/strings.json @@ -1,31 +1,32 @@ { - "config": { - "step": { - "user": { - "description": "You need to supply a read-only API key from UptimeRobot", - "data": { - "api_key": "[%key:common::config_flow::data::api_key%]" - } - }, - "reauth_confirm": { - "title": "[%key:common::config_flow::title::reauth%]", - "description": "You need to supply a new read-only API key from UptimeRobot", - "data": { - "api_key": "[%key:common::config_flow::data::api_key%]" - } - } - }, - "error": { - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", - "invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]", - "unknown": "[%key:common::config_flow::error::unknown%]", - "reauth_failed_matching_account": "The API key you provided does not match the account ID for existing configuration." - }, - "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", - "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", - "reauth_failed_existing": "Could not update the config entry, please remove the integration and set it up again.", - "unknown": "[%key:common::config_flow::error::unknown%]" + "config": { + "step": { + "user": { + "description": "You need to supply the 'main' API key from UptimeRobot", + "data": { + "api_key": "[%key:common::config_flow::data::api_key%]" } + }, + "reauth_confirm": { + "title": "[%key:common::config_flow::title::reauth%]", + "description": "You need to supply a new 'main' API key from UptimeRobot", + "data": { + "api_key": "[%key:common::config_flow::data::api_key%]" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]", + "unknown": "[%key:common::config_flow::error::unknown%]", + "not_main_key": "Wrong API key type detected, use the 'main' API key", + "reauth_failed_matching_account": "The API key you provided does not match the account ID for existing configuration." + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", + "reauth_failed_existing": "Could not update the config entry, please remove the integration and set it up again.", + "unknown": "[%key:common::config_flow::error::unknown%]" } -} \ No newline at end of file + } +} diff --git a/homeassistant/components/uptimerobot/strings.sensor.json b/homeassistant/components/uptimerobot/strings.sensor.json index a8177bab9cf..700f7cd9f4c 100644 --- a/homeassistant/components/uptimerobot/strings.sensor.json +++ b/homeassistant/components/uptimerobot/strings.sensor.json @@ -1,11 +1,11 @@ { - "state": { - "uptimerobot__monitor_status": { - "pause": "Pause", - "not_checked_yet": "Not checked yet", - "up": "Up", - "seems_down": "Seems down", - "down": "Down" - } + "state": { + "uptimerobot__monitor_status": { + "pause": "Pause", + "not_checked_yet": "Not checked yet", + "up": "Up", + "seems_down": "Seems down", + "down": "Down" } } +} diff --git a/homeassistant/components/uptimerobot/switch.py b/homeassistant/components/uptimerobot/switch.py new file mode 100644 index 00000000000..619f72ae47f --- /dev/null +++ b/homeassistant/components/uptimerobot/switch.py @@ -0,0 +1,75 @@ +"""UptimeRobot switch platform.""" +from __future__ import annotations + +from typing import Any + +from pyuptimerobot import UptimeRobotAuthenticationException, UptimeRobotException + +from homeassistant.components.switch import ( + SwitchDeviceClass, + SwitchEntity, + SwitchEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import UptimeRobotDataUpdateCoordinator +from .const import API_ATTR_OK, DOMAIN, LOGGER +from .entity import UptimeRobotEntity + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up the UptimeRobot switches.""" + coordinator: UptimeRobotDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + async_add_entities( + UptimeRobotSwitch( + coordinator, + SwitchEntityDescription( + key=str(monitor.id), + name=f"{monitor.friendly_name} Active", + device_class=SwitchDeviceClass.SWITCH, + ), + monitor=monitor, + ) + for monitor in coordinator.data + ) + + +class UptimeRobotSwitch(UptimeRobotEntity, SwitchEntity): + """Representation of a UptimeRobot switch.""" + + _attr_icon = "mdi:cog" + + @property + def is_on(self) -> bool: + """Return True if the entity is on.""" + return bool(self.monitor.status != 0) + + async def _async_edit_monitor(self, **kwargs: Any) -> None: + """Edit monitor status.""" + try: + response = await self.api.async_edit_monitor(**kwargs) + except UptimeRobotAuthenticationException: + LOGGER.debug("API authentication error, calling reauth") + self.coordinator.config_entry.async_start_reauth(self.hass) + return + except UptimeRobotException as exception: + LOGGER.error("API exception: %s", exception) + return + + if response.status != API_ATTR_OK: + LOGGER.error("API exception: %s", response.error.message, exc_info=True) + return + + await self.coordinator.async_request_refresh() + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn on switch.""" + await self._async_edit_monitor(id=self.monitor.id, status=0) + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn off switch.""" + await self._async_edit_monitor(id=self.monitor.id, status=1) diff --git a/homeassistant/components/uptimerobot/translations/el.json b/homeassistant/components/uptimerobot/translations/el.json index 7ade0ad11a5..2f15a945ab0 100644 --- a/homeassistant/components/uptimerobot/translations/el.json +++ b/homeassistant/components/uptimerobot/translations/el.json @@ -17,14 +17,14 @@ "data": { "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API" }, - "description": "\u03a0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03c0\u03b1\u03c1\u03ad\u03c7\u03b5\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03ba\u03bb\u03b5\u03b9\u03b4\u03af API \u03bc\u03cc\u03bd\u03bf \u03b3\u03b9\u03b1 \u03b1\u03bd\u03ac\u03b3\u03bd\u03c9\u03c3\u03b7 \u03b1\u03c0\u03cc \u03c4\u03bf {intergration}.", + "description": "\u03a0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03c0\u03b1\u03c1\u03ad\u03c7\u03b5\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03bd\u03ad\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af API \u03bc\u03cc\u03bd\u03bf \u03b3\u03b9\u03b1 \u03b1\u03bd\u03ac\u03b3\u03bd\u03c9\u03c3\u03b7 \u03b1\u03c0\u03cc \u03c4\u03bf UptimeRobot", "title": "\u0395\u03c0\u03b1\u03bd\u03b1\u03bb\u03b7\u03c0\u03c4\u03b9\u03ba\u03cc\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2" }, "user": { "data": { "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API" }, - "description": "\u03a0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03c0\u03b1\u03c1\u03ad\u03c7\u03b5\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03ba\u03bb\u03b5\u03b9\u03b4\u03af API \u03bc\u03cc\u03bd\u03bf \u03b3\u03b9\u03b1 \u03b1\u03bd\u03ac\u03b3\u03bd\u03c9\u03c3\u03b7 \u03b1\u03c0\u03cc \u03c4\u03bf {intergration}." + "description": "\u03a0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03c0\u03b1\u03c1\u03ad\u03c7\u03b5\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03ba\u03bb\u03b5\u03b9\u03b4\u03af API \u03bc\u03cc\u03bd\u03bf \u03b3\u03b9\u03b1 \u03b1\u03bd\u03ac\u03b3\u03bd\u03c9\u03c3\u03b7 \u03b1\u03c0\u03cc \u03c4\u03bf UptimeRobot" } } } diff --git a/homeassistant/components/uptimerobot/translations/en.json b/homeassistant/components/uptimerobot/translations/en.json index a78af34102d..f4fe398a195 100644 --- a/homeassistant/components/uptimerobot/translations/en.json +++ b/homeassistant/components/uptimerobot/translations/en.json @@ -9,6 +9,7 @@ "error": { "cannot_connect": "Failed to connect", "invalid_api_key": "Invalid API key", + "not_main_key": "Wrong API key type detected, use the 'main' API key", "reauth_failed_matching_account": "The API key you provided does not match the account ID for existing configuration.", "unknown": "Unexpected error" }, @@ -17,14 +18,14 @@ "data": { "api_key": "API Key" }, - "description": "You need to supply a new read-only API key from UptimeRobot", + "description": "You need to supply a new 'main' API key from UptimeRobot", "title": "Reauthenticate Integration" }, "user": { "data": { "api_key": "API Key" }, - "description": "You need to supply a read-only API key from UptimeRobot" + "description": "You need to supply the 'main' API key from UptimeRobot" } } } diff --git a/homeassistant/components/uptimerobot/translations/fr.json b/homeassistant/components/uptimerobot/translations/fr.json index 6d20816632f..674180a1d90 100644 --- a/homeassistant/components/uptimerobot/translations/fr.json +++ b/homeassistant/components/uptimerobot/translations/fr.json @@ -8,7 +8,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_api_key": "Cl\u00e9 API invalide", + "invalid_api_key": "Cl\u00e9 d'API non valide", "reauth_failed_matching_account": "La cl\u00e9 API que vous avez fournie ne correspond pas \u00e0 l\u2019ID de compte pour la configuration existante.", "unknown": "Erreur inattendue" }, diff --git a/homeassistant/components/usb/__init__.py b/homeassistant/components/usb/__init__.py index 38377aadd9f..5ac390ad168 100644 --- a/homeassistant/components/usb/__init__.py +++ b/homeassistant/components/usb/__init__.py @@ -6,7 +6,7 @@ import fnmatch import logging import os import sys -from typing import Any +from typing import TYPE_CHECKING, Any from serial.tools.list_ports import comports from serial.tools.list_ports_common import ListPortInfo @@ -28,6 +28,9 @@ from .const import DOMAIN from .models import USBDevice from .utils import usb_device_from_port +if TYPE_CHECKING: + from pyudev import Device + _LOGGER = logging.getLogger(__name__) REQUEST_SCAN_COOLDOWN = 60 # 1 minute cooldown @@ -163,12 +166,14 @@ class USBDiscovery: monitor, callback=self._device_discovered, name="usb-observer" ) observer.start() - self.hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STOP, lambda event: observer.stop() - ) + + def _stop_observer(event: Event) -> None: + observer.stop() + + self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _stop_observer) self.observer_active = True - def _device_discovered(self, device): + def _device_discovered(self, device: Device) -> None: """Call when the observer discovers a new usb tty device.""" if device.action != "add": return diff --git a/homeassistant/components/usb/manifest.json b/homeassistant/components/usb/manifest.json index fd22882b8b3..87b81965c25 100644 --- a/homeassistant/components/usb/manifest.json +++ b/homeassistant/components/usb/manifest.json @@ -2,12 +2,9 @@ "domain": "usb", "name": "USB Discovery", "documentation": "https://www.home-assistant.io/integrations/usb", - "requirements": [ - "pyudev==0.22.0", - "pyserial==3.5" - ], + "requirements": ["pyudev==0.22.0", "pyserial==3.5"], "codeowners": ["@bdraco"], "dependencies": ["websocket_api"], "quality_scale": "internal", "iot_class": "local_push" -} \ No newline at end of file +} diff --git a/homeassistant/components/utility_meter/__init__.py b/homeassistant/components/utility_meter/__init__.py index 525b4f3b43c..ecc1110d7ff 100644 --- a/homeassistant/components/utility_meter/__init__.py +++ b/homeassistant/components/utility_meter/__init__.py @@ -5,18 +5,18 @@ import logging from croniter import croniter import voluptuous as vol +from homeassistant.components.select import DOMAIN as SELECT_DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN -from homeassistant.const import CONF_NAME -from homeassistant.core import HomeAssistant -from homeassistant.helpers import discovery +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_ENTITY_ID, CONF_NAME, Platform +from homeassistant.core import HomeAssistant, split_entity_id +from homeassistant.helpers import discovery, entity_registry as er import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType from .const import ( - ATTR_TARIFF, CONF_CRON_PATTERN, CONF_METER, CONF_METER_DELTA_VALUES, @@ -27,22 +27,17 @@ from .const import ( CONF_TARIFF, CONF_TARIFF_ENTITY, CONF_TARIFFS, + DATA_LEGACY_COMPONENT, DATA_TARIFF_SENSORS, DATA_UTILITY, DOMAIN, METER_TYPES, SERVICE_RESET, - SERVICE_SELECT_NEXT_TARIFF, - SERVICE_SELECT_TARIFF, SIGNAL_RESET_METER, ) _LOGGER = logging.getLogger(__name__) -TARIFF_ICON = "mdi:clock-outline" - -ATTR_TARIFFS = "tariffs" - DEFAULT_OFFSET = timedelta(hours=0) @@ -90,7 +85,7 @@ METER_CONFIG_SCHEMA = vol.Schema( vol.Optional(CONF_METER_DELTA_VALUES, default=False): cv.boolean, vol.Optional(CONF_METER_NET_CONSUMPTION, default=False): cv.boolean, vol.Optional(CONF_TARIFFS, default=[]): vol.All( - cv.ensure_list, [cv.string] + cv.ensure_list, vol.Unique(), [cv.string] ), vol.Optional(CONF_CRON_PATTERN): validate_cron_pattern, }, @@ -105,9 +100,37 @@ CONFIG_SCHEMA = vol.Schema( async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up an Utility Meter.""" - component = EntityComponent(_LOGGER, DOMAIN, hass) + hass.data[DATA_LEGACY_COMPONENT] = EntityComponent(_LOGGER, DOMAIN, hass) + hass.data[DATA_UTILITY] = {} - register_services = False + + async def async_reset_meters(service_call): + """Reset all sensors of a meter.""" + entity_id = service_call.data["entity_id"] + + domain = split_entity_id(entity_id)[0] + if domain == DOMAIN: + for entity in hass.data[DATA_LEGACY_COMPONENT].entities: + if entity_id == entity.entity_id: + _LOGGER.debug( + "forward reset meter from %s to %s", + entity_id, + entity.tracked_entity_id, + ) + entity_id = entity.tracked_entity_id + + _LOGGER.debug("reset meter %s", entity_id) + async_dispatcher_send(hass, SIGNAL_RESET_METER, entity_id) + + hass.services.async_register( + DOMAIN, + SERVICE_RESET, + async_reset_meters, + vol.Schema({ATTR_ENTITY_ID: cv.entity_id}), + ) + + if DOMAIN not in config: + return True for meter, conf in config[DOMAIN].items(): _LOGGER.debug("Setup %s.%s", DOMAIN, meter) @@ -129,11 +152,18 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: ) else: # create tariff selection - await component.async_add_entities( - [TariffSelect(meter, list(conf[CONF_TARIFFS]))] + hass.async_create_task( + discovery.async_load_platform( + hass, + SELECT_DOMAIN, + DOMAIN, + {CONF_METER: meter, CONF_TARIFFS: conf[CONF_TARIFFS]}, + config, + ) ) + hass.data[DATA_UTILITY][meter][CONF_TARIFF_ENTITY] = "{}.{}".format( - DOMAIN, meter + SELECT_DOMAIN, meter ) # add one meter for each tariff @@ -151,89 +181,61 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: hass, SENSOR_DOMAIN, DOMAIN, tariff_confs, config ) ) - register_services = True - - if register_services: - component.async_register_entity_service(SERVICE_RESET, {}, "async_reset_meters") - - component.async_register_entity_service( - SERVICE_SELECT_TARIFF, - {vol.Required(ATTR_TARIFF): cv.string}, - "async_select_tariff", - ) - - component.async_register_entity_service( - SERVICE_SELECT_NEXT_TARIFF, {}, "async_next_tariff" - ) return True -class TariffSelect(RestoreEntity): - """Representation of a Tariff selector.""" +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Utility Meter from a config entry.""" + entity_registry = er.async_get(hass) + hass.data[DATA_UTILITY][entry.entry_id] = {} + hass.data[DATA_UTILITY][entry.entry_id][DATA_TARIFF_SENSORS] = [] - def __init__(self, name, tariffs): - """Initialize a tariff selector.""" - self._name = name - self._current_tariff = None - self._tariffs = tariffs - self._icon = TARIFF_ICON - - async def async_added_to_hass(self): - """Run when entity about to be added.""" - await super().async_added_to_hass() - - state = await self.async_get_last_state() - if not state or state.state not in self._tariffs: - self._current_tariff = self._tariffs[0] - else: - self._current_tariff = state.state - - @property - def should_poll(self): - """If entity should be polled.""" + try: + er.async_validate_entity_id(entity_registry, entry.options[CONF_SOURCE_SENSOR]) + except vol.Invalid: + # The entity is identified by an unknown entity registry ID + _LOGGER.error( + "Failed to setup utility_meter for unknown entity %s", + entry.options[CONF_SOURCE_SENSOR], + ) return False - @property - def name(self): - """Return the name of the select input.""" - return self._name + if not entry.options.get(CONF_TARIFFS): + # Only a single meter sensor is required + hass.data[DATA_UTILITY][entry.entry_id][CONF_TARIFF_ENTITY] = None + hass.config_entries.async_setup_platforms(entry, (Platform.SENSOR,)) + else: + # Create tariff selection + one meter sensor for each tariff + entity_entry = entity_registry.async_get_or_create( + Platform.SELECT, DOMAIN, entry.entry_id, suggested_object_id=entry.title + ) + hass.data[DATA_UTILITY][entry.entry_id][ + CONF_TARIFF_ENTITY + ] = entity_entry.entity_id + hass.config_entries.async_setup_platforms( + entry, (Platform.SELECT, Platform.SENSOR) + ) - @property - def icon(self): - """Return the icon to be used for this entity.""" - return self._icon + entry.async_on_unload(entry.add_update_listener(config_entry_update_listener)) - @property - def state(self): - """Return the state of the component.""" - return self._current_tariff + return True - @property - def extra_state_attributes(self): - """Return the state attributes.""" - return {ATTR_TARIFFS: self._tariffs} - async def async_reset_meters(self): - """Reset all sensors of this meter.""" - _LOGGER.debug("reset meter %s", self.entity_id) - async_dispatcher_send(self.hass, SIGNAL_RESET_METER, self.entity_id) +async def config_entry_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Update listener, called when the config entry options are changed.""" + await hass.config_entries.async_reload(entry.entry_id) - async def async_select_tariff(self, tariff): - """Select new option.""" - if tariff not in self._tariffs: - _LOGGER.warning( - "Invalid tariff: %s (possible tariffs: %s)", - tariff, - ", ".join(self._tariffs), - ) - return - self._current_tariff = tariff - self.async_write_ha_state() - async def async_next_tariff(self): - """Offset current index.""" - current_index = self._tariffs.index(self._current_tariff) - new_index = (current_index + 1) % len(self._tariffs) - self._current_tariff = self._tariffs[new_index] - self.async_write_ha_state() +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms( + entry, + ( + Platform.SELECT, + Platform.SENSOR, + ), + ): + hass.data[DATA_UTILITY].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/utility_meter/config_flow.py b/homeassistant/components/utility_meter/config_flow.py new file mode 100644 index 00000000000..ed12b3038b6 --- /dev/null +++ b/homeassistant/components/utility_meter/config_flow.py @@ -0,0 +1,116 @@ +"""Config flow for Utility Meter integration.""" +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, cast + +import voluptuous as vol + +from homeassistant.const import CONF_NAME, CONF_UNIT_OF_MEASUREMENT +from homeassistant.helpers import selector +from homeassistant.helpers.schema_config_entry_flow import ( + SchemaConfigFlowHandler, + SchemaFlowError, + SchemaFlowFormStep, + SchemaFlowMenuStep, +) + +from .const import ( + BIMONTHLY, + CONF_METER_DELTA_VALUES, + CONF_METER_NET_CONSUMPTION, + CONF_METER_OFFSET, + CONF_METER_TYPE, + CONF_SOURCE_SENSOR, + CONF_TARIFFS, + DAILY, + DOMAIN, + HOURLY, + MONTHLY, + QUARTER_HOURLY, + QUARTERLY, + WEEKLY, + YEARLY, +) + +METER_TYPES = [ + {"value": "none", "label": "No cycle"}, + {"value": QUARTER_HOURLY, "label": "Every 15 minutes"}, + {"value": HOURLY, "label": "Hourly"}, + {"value": DAILY, "label": "Daily"}, + {"value": WEEKLY, "label": "Weekly"}, + {"value": MONTHLY, "label": "Monthly"}, + {"value": BIMONTHLY, "label": "Every two months"}, + {"value": QUARTERLY, "label": "Quarterly"}, + {"value": YEARLY, "label": "Yearly"}, +] + + +def _validate_config(data: Any) -> Any: + """Validate config.""" + try: + vol.Unique()(data[CONF_TARIFFS]) + except vol.Invalid as exc: + raise SchemaFlowError("tariffs_not_unique") from exc + + return data + + +OPTIONS_SCHEMA = vol.Schema( + { + vol.Required(CONF_SOURCE_SENSOR): selector.selector( + {"entity": {"domain": "sensor"}}, + ), + } +) + +CONFIG_SCHEMA = vol.Schema( + { + vol.Required(CONF_NAME): selector.selector({"text": {}}), + vol.Required(CONF_SOURCE_SENSOR): selector.selector( + {"entity": {"domain": "sensor"}}, + ), + vol.Required(CONF_METER_TYPE): selector.selector( + {"select": {"options": METER_TYPES}} + ), + vol.Required(CONF_METER_OFFSET, default=0): selector.selector( + { + "number": { + "min": 0, + "max": 28, + "mode": "box", + CONF_UNIT_OF_MEASUREMENT: "days", + } + } + ), + vol.Required(CONF_TARIFFS, default=[]): selector.selector( + {"select": {"options": [], "custom_value": True, "multiple": True}} + ), + vol.Required(CONF_METER_NET_CONSUMPTION, default=False): selector.selector( + {"boolean": {}} + ), + vol.Required(CONF_METER_DELTA_VALUES, default=False): selector.selector( + {"boolean": {}} + ), + } +) + +CONFIG_FLOW: dict[str, SchemaFlowFormStep | SchemaFlowMenuStep] = { + "user": SchemaFlowFormStep(CONFIG_SCHEMA, validate_user_input=_validate_config) +} + +OPTIONS_FLOW: dict[str, SchemaFlowFormStep | SchemaFlowMenuStep] = { + "init": SchemaFlowFormStep(OPTIONS_SCHEMA) +} + + +class ConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN): + """Handle a config or options flow for Utility Meter.""" + + config_flow = CONFIG_FLOW + options_flow = OPTIONS_FLOW + + def async_config_entry_title(self, options: Mapping[str, Any]) -> str: + """Return config entry title.""" + + return cast(str, options[CONF_NAME]) diff --git a/homeassistant/components/utility_meter/const.py b/homeassistant/components/utility_meter/const.py index 097496e231d..2bac649aace 100644 --- a/homeassistant/components/utility_meter/const.py +++ b/homeassistant/components/utility_meter/const.py @@ -1,6 +1,8 @@ """Constants for the utility meter component.""" DOMAIN = "utility_meter" +TARIFF_ICON = "mdi:clock-outline" + QUARTER_HOURLY = "quarter-hourly" HOURLY = "hourly" DAILY = "daily" @@ -23,6 +25,7 @@ METER_TYPES = [ DATA_UTILITY = "utility_meter_data" DATA_TARIFF_SENSORS = "utility_meter_sensors" +DATA_LEGACY_COMPONENT = "utility_meter_legacy_component" CONF_METER = "meter" CONF_SOURCE_SENSOR = "source" @@ -37,6 +40,7 @@ CONF_TARIFF_ENTITY = "tariff_entity" CONF_CRON_PATTERN = "cron" ATTR_TARIFF = "tariff" +ATTR_TARIFFS = "tariffs" ATTR_VALUE = "value" ATTR_CRON_PATTERN = "cron pattern" diff --git a/homeassistant/components/utility_meter/manifest.json b/homeassistant/components/utility_meter/manifest.json index fb880f567d1..a662753bd18 100644 --- a/homeassistant/components/utility_meter/manifest.json +++ b/homeassistant/components/utility_meter/manifest.json @@ -1,10 +1,12 @@ { "domain": "utility_meter", + "integration_type": "helper", "name": "Utility Meter", "documentation": "https://www.home-assistant.io/integrations/utility_meter", "requirements": ["croniter==1.0.6"], "codeowners": ["@dgomes"], "quality_scale": "internal", "iot_class": "local_push", - "loggers": ["croniter"] + "loggers": ["croniter"], + "config_flow": true } diff --git a/homeassistant/components/utility_meter/select.py b/homeassistant/components/utility_meter/select.py new file mode 100644 index 00000000000..1f39b7f7c16 --- /dev/null +++ b/homeassistant/components/utility_meter/select.py @@ -0,0 +1,195 @@ +"""Support for tariff selection.""" +from __future__ import annotations + +import logging + +import voluptuous as vol + +from homeassistant.components.select import SelectEntity +from homeassistant.components.select.const import ( + ATTR_OPTION, + ATTR_OPTIONS, + DOMAIN as SELECT_DOMAIN, + SERVICE_SELECT_OPTION, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, STATE_UNAVAILABLE +from homeassistant.core import Event, HomeAssistant, callback, split_entity_id +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.event import async_track_state_change_event +from homeassistant.helpers.restore_state import RestoreEntity + +from .const import ( + ATTR_TARIFF, + ATTR_TARIFFS, + CONF_METER, + CONF_TARIFFS, + DATA_LEGACY_COMPONENT, + SERVICE_SELECT_NEXT_TARIFF, + SERVICE_SELECT_TARIFF, + TARIFF_ICON, +) + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Initialize Utility Meter config entry.""" + name = config_entry.title + tariffs = config_entry.options[CONF_TARIFFS] + + legacy_add_entities = None + unique_id = config_entry.entry_id + tariff_select = TariffSelect(name, tariffs, legacy_add_entities, unique_id) + async_add_entities([tariff_select]) + + +async def async_setup_platform(hass, conf, async_add_entities, discovery_info=None): + """Set up the utility meter select.""" + legacy_component = hass.data[DATA_LEGACY_COMPONENT] + async_add_entities( + [ + TariffSelect( + discovery_info[CONF_METER], + discovery_info[CONF_TARIFFS], + legacy_component.async_add_entities, + None, + ) + ] + ) + + legacy_component.async_register_entity_service( + SERVICE_SELECT_TARIFF, + {vol.Required(ATTR_TARIFF): cv.string}, + "async_select_tariff", + ) + + legacy_component.async_register_entity_service( + SERVICE_SELECT_NEXT_TARIFF, {}, "async_next_tariff" + ) + + +class TariffSelect(SelectEntity, RestoreEntity): + """Representation of a Tariff selector.""" + + def __init__(self, name, tariffs, add_legacy_entities, unique_id): + """Initialize a tariff selector.""" + self._attr_name = name + self._attr_unique_id = unique_id + self._current_tariff = None + self._tariffs = tariffs + self._attr_icon = TARIFF_ICON + self._attr_should_poll = False + self._add_legacy_entities = add_legacy_entities + + @property + def options(self): + """Return the available tariffs.""" + return self._tariffs + + @property + def current_option(self): + """Return current tariff.""" + return self._current_tariff + + async def async_added_to_hass(self): + """Run when entity about to be added.""" + await super().async_added_to_hass() + + if self._add_legacy_entities: + await self._add_legacy_entities([LegacyTariffSelect(self.entity_id)]) + + state = await self.async_get_last_state() + if not state or state.state not in self._tariffs: + self._current_tariff = self._tariffs[0] + else: + self._current_tariff = state.state + + async def async_select_option(self, option: str) -> None: + """Select new tariff (option).""" + self._current_tariff = option + self.async_write_ha_state() + + +class LegacyTariffSelect(Entity): + """Backwards compatibility for deprecated utility_meter select entity.""" + + def __init__(self, tracked_entity_id): + """Initialize the entity.""" + self._attr_icon = TARIFF_ICON + # Set name to influence enity_id + self._attr_name = split_entity_id(tracked_entity_id)[1] + self.tracked_entity_id = tracked_entity_id + + @callback + def async_state_changed_listener(self, event: Event | None = None) -> None: + """Handle child updates.""" + if ( + state := self.hass.states.get(self.tracked_entity_id) + ) is None or state.state == STATE_UNAVAILABLE: + self._attr_available = False + return + + self._attr_available = True + + self._attr_name = state.attributes.get(ATTR_FRIENDLY_NAME) + self._attr_state = state.state + self._attr_extra_state_attributes = { + ATTR_TARIFFS: state.attributes.get(ATTR_OPTIONS) + } + + async def async_added_to_hass(self) -> None: + """Register callbacks.""" + + @callback + def _async_state_changed_listener(event: Event | None = None) -> None: + """Handle child updates.""" + self.async_state_changed_listener(event) + self.async_write_ha_state() + + self.async_on_remove( + async_track_state_change_event( + self.hass, [self.tracked_entity_id], _async_state_changed_listener + ) + ) + + # Call once on adding + _async_state_changed_listener() + + async def async_select_tariff(self, tariff): + """Select new option.""" + _LOGGER.warning( + "The 'utility_meter.select_tariff' service has been deprecated and will " + "be removed in HA Core 2022.7. Please use 'select.select_option' instead", + ) + await self.hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + {ATTR_ENTITY_ID: self.tracked_entity_id, ATTR_OPTION: tariff}, + blocking=True, + context=self._context, + ) + + async def async_next_tariff(self): + """Offset current index.""" + _LOGGER.warning( + "The 'utility_meter.next_tariff' service has been deprecated and will " + "be removed in HA Core 2022.7. Please use 'select.select_option' instead", + ) + if ( + not self.available + or (state := self.hass.states.get(self.tracked_entity_id)) is None + ): + return + tariffs = state.attributes.get(ATTR_OPTIONS) + current_tariff = state.state + current_index = tariffs.index(current_tariff) + new_index = (current_index + 1) % len(tariffs) + + await self.async_select_tariff(tariffs[new_index]) diff --git a/homeassistant/components/utility_meter/sensor.py b/homeassistant/components/utility_meter/sensor.py index ec137968bc5..7b1f28ff5b4 100644 --- a/homeassistant/components/utility_meter/sensor.py +++ b/homeassistant/components/utility_meter/sensor.py @@ -1,7 +1,7 @@ """Utility meter from sensors providing raw data.""" from __future__ import annotations -from datetime import datetime +from datetime import datetime, timedelta from decimal import Decimal, DecimalException, InvalidOperation import logging @@ -14,17 +14,17 @@ from homeassistant.components.sensor import ( SensorEntity, SensorStateClass, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, CONF_NAME, ENERGY_KILO_WATT_HOUR, ENERGY_WATT_HOUR, - EVENT_HOMEASSISTANT_START, STATE_UNAVAILABLE, STATE_UNKNOWN, ) from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers import entity_platform +from homeassistant.helpers import entity_platform, entity_registry as er from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import ( @@ -32,6 +32,7 @@ from homeassistant.helpers.event import ( async_track_state_change_event, ) from homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.helpers.start import async_at_start from homeassistant.helpers.template import is_number from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType import homeassistant.util.dt as dt_util @@ -49,6 +50,7 @@ from .const import ( CONF_SOURCE_SENSOR, CONF_TARIFF, CONF_TARIFF_ENTITY, + CONF_TARIFFS, DAILY, DATA_TARIFF_SENSORS, DATA_UTILITY, @@ -93,6 +95,79 @@ PAUSED = "paused" COLLECTING = "collecting" +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Initialize Utility Meter config entry.""" + entry_id = config_entry.entry_id + registry = er.async_get(hass) + # Validate + resolve entity registry id to entity_id + source_entity_id = er.async_validate_entity_id( + registry, config_entry.options[CONF_SOURCE_SENSOR] + ) + + cron_pattern = None + delta_values = config_entry.options[CONF_METER_DELTA_VALUES] + meter_offset = timedelta(days=config_entry.options[CONF_METER_OFFSET]) + meter_type = config_entry.options[CONF_METER_TYPE] + if meter_type == "none": + meter_type = None + name = config_entry.title + net_consumption = config_entry.options[CONF_METER_NET_CONSUMPTION] + tariff_entity = hass.data[DATA_UTILITY][entry_id][CONF_TARIFF_ENTITY] + + meters = [] + tariffs = config_entry.options[CONF_TARIFFS] + + if not tariffs: + # Add single sensor, not gated by a tariff selector + meter_sensor = UtilityMeterSensor( + cron_pattern=cron_pattern, + delta_values=delta_values, + meter_offset=meter_offset, + meter_type=meter_type, + name=name, + net_consumption=net_consumption, + parent_meter=entry_id, + source_entity=source_entity_id, + tariff_entity=tariff_entity, + tariff=None, + unique_id=entry_id, + ) + meters.append(meter_sensor) + hass.data[DATA_UTILITY][entry_id][DATA_TARIFF_SENSORS].append(meter_sensor) + else: + # Add sensors for each tariff + for tariff in tariffs: + meter_sensor = UtilityMeterSensor( + cron_pattern=cron_pattern, + delta_values=delta_values, + meter_offset=meter_offset, + meter_type=meter_type, + name=f"{name} {tariff}", + net_consumption=net_consumption, + parent_meter=entry_id, + source_entity=source_entity_id, + tariff_entity=tariff_entity, + tariff=tariff, + unique_id=f"{entry_id}_{tariff}", + ) + meters.append(meter_sensor) + hass.data[DATA_UTILITY][entry_id][DATA_TARIFF_SENSORS].append(meter_sensor) + + async_add_entities(meters) + + platform = entity_platform.async_get_current_platform() + + platform.async_register_entity_service( + SERVICE_CALIBRATE_METER, + {vol.Required(ATTR_VALUE): vol.Coerce(Decimal)}, + "async_calibrate", + ) + + async def async_setup_platform( hass: HomeAssistant, config: ConfigType, @@ -121,16 +196,17 @@ async def async_setup_platform( ) conf_cron_pattern = hass.data[DATA_UTILITY][meter].get(CONF_CRON_PATTERN) meter_sensor = UtilityMeterSensor( - meter, - conf_meter_source, - conf.get(CONF_NAME), - conf_meter_type, - conf_meter_offset, - conf_meter_delta_values, - conf_meter_net_consumption, - conf.get(CONF_TARIFF), - conf_meter_tariff_entity, - conf_cron_pattern, + cron_pattern=conf_cron_pattern, + delta_values=conf_meter_delta_values, + meter_offset=conf_meter_offset, + meter_type=conf_meter_type, + name=conf.get(CONF_NAME), + net_consumption=conf_meter_net_consumption, + parent_meter=meter, + source_entity=conf_meter_source, + tariff_entity=conf_meter_tariff_entity, + tariff=conf.get(CONF_TARIFF), + unique_id=None, ) meters.append(meter_sensor) @@ -152,18 +228,21 @@ class UtilityMeterSensor(RestoreEntity, SensorEntity): def __init__( self, + *, + cron_pattern, + delta_values, + meter_offset, + meter_type, + name, + net_consumption, parent_meter, source_entity, - name, - meter_type, - meter_offset, - delta_values, - net_consumption, - tariff=None, - tariff_entity=None, - cron_pattern=None, + tariff_entity, + tariff, + unique_id, ): """Initialize the Utility Meter sensor.""" + self._attr_unique_id = unique_id self._parent_meter = parent_meter self._sensor_source_id = source_entity self._state = None @@ -265,10 +344,12 @@ class UtilityMeterSensor(RestoreEntity, SensorEntity): async def _async_reset_meter(self, event): """Determine cycle - Helper function for larger than daily cycles.""" if self._cron_pattern is not None: - async_track_point_in_time( - self.hass, - self._async_reset_meter, - croniter(self._cron_pattern, dt_util.now()).get_next(datetime), + self.async_on_remove( + async_track_point_in_time( + self.hass, + self._async_reset_meter, + croniter(self._cron_pattern, dt_util.now()).get_next(datetime), + ) ) await self.async_reset_meter(self._tariff_entity) @@ -293,13 +374,19 @@ class UtilityMeterSensor(RestoreEntity, SensorEntity): await super().async_added_to_hass() if self._cron_pattern is not None: - async_track_point_in_time( - self.hass, - self._async_reset_meter, - croniter(self._cron_pattern, dt_util.now()).get_next(datetime), + self.async_on_remove( + async_track_point_in_time( + self.hass, + self._async_reset_meter, + croniter(self._cron_pattern, dt_util.now()).get_next(datetime), + ) ) - async_dispatcher_connect(self.hass, SIGNAL_RESET_METER, self.async_reset_meter) + self.async_on_remove( + async_dispatcher_connect( + self.hass, SIGNAL_RESET_METER, self.async_reset_meter + ) + ) if state := await self.async_get_last_state(): try: @@ -334,11 +421,17 @@ class UtilityMeterSensor(RestoreEntity, SensorEntity): _LOGGER.debug( "<%s> tracks utility meter %s", self.name, self._tariff_entity ) - async_track_state_change_event( - self.hass, [self._tariff_entity], self.async_tariff_change + self.async_on_remove( + async_track_state_change_event( + self.hass, [self._tariff_entity], self.async_tariff_change + ) ) tariff_entity_state = self.hass.states.get(self._tariff_entity) + if not tariff_entity_state: + # The utility meter is not yet added + return + self._change_status(tariff_entity_state.state) return @@ -352,9 +445,13 @@ class UtilityMeterSensor(RestoreEntity, SensorEntity): self.hass, [self._sensor_source_id], self.async_reading ) - self.hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_START, async_source_tracking - ) + self.async_on_remove(async_at_start(self.hass, async_source_tracking)) + + async def async_will_remove_from_hass(self) -> None: + """Run when entity will be removed from hass.""" + if self._collecting: + self._collecting() + self._collecting = None @property def name(self): diff --git a/homeassistant/components/utility_meter/services.yaml b/homeassistant/components/utility_meter/services.yaml index c3f95d22175..32a6069d3bb 100644 --- a/homeassistant/components/utility_meter/services.yaml +++ b/homeassistant/components/utility_meter/services.yaml @@ -2,10 +2,11 @@ reset: name: Reset - description: Resets the counter of a utility meter. + description: Resets all counters of an utility meter. target: entity: - domain: utility_meter + domain: select + integration: utility_meter next_tariff: name: Next Tariff @@ -35,6 +36,7 @@ calibrate: target: entity: domain: sensor + integration: utility_meter fields: value: name: Value diff --git a/homeassistant/components/utility_meter/strings.json b/homeassistant/components/utility_meter/strings.json new file mode 100644 index 00000000000..35a35b7f2db --- /dev/null +++ b/homeassistant/components/utility_meter/strings.json @@ -0,0 +1,35 @@ +{ + "title": "Utility Meter", + "config": { + "step": { + "user": { + "title": "Add Utility Meter", + "description": "Create a sensor which tracks consumption of various utilities (e.g., energy, gas, water, heating) over a configured period of time, typically monthly. The utility meter sensor optionally supports splitting the consumption by tariffs, in that case one sensor for each tariff is created as well as a select entity to choose the current tariff.", + "data": { + "cycle": "Meter reset cycle", + "delta_values": "Delta values", + "name": "Name", + "net_consumption": "Net consumption", + "offset": "Meter reset offset", + "source": "Input sensor", + "tariffs": "Supported tariffs" + }, + "data_description": { + "delta_values": "Enable if the source values are delta values since the last reading instead of absolute values.", + "net_consumption": "Enable if the source is a net meter, meaning it can both increase and decrease.", + "offset": "Offset the day of a monthly meter reset.", + "tariffs": "A list of supported tariffs, leave empty if only a single tariff is needed." + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "source": "[%key:component::utility_meter::config::step::user::data::source%]" + } + } + } + } +} diff --git a/homeassistant/components/utility_meter/translations/en.json b/homeassistant/components/utility_meter/translations/en.json new file mode 100644 index 00000000000..d5dc7f18ddd --- /dev/null +++ b/homeassistant/components/utility_meter/translations/en.json @@ -0,0 +1,35 @@ +{ + "config": { + "step": { + "user": { + "data": { + "cycle": "Meter reset cycle", + "delta_values": "Delta values", + "name": "Name", + "net_consumption": "Net consumption", + "offset": "Meter reset offset", + "source": "Input sensor", + "tariffs": "Supported tariffs" + }, + "data_description": { + "delta_values": "Enable if the source values are delta values since the last reading instead of absolute values.", + "net_consumption": "Enable if the source is a net meter, meaning it can both increase and decrease.", + "offset": "Offset the day of a monthly meter reset.", + "tariffs": "A list of supported tariffs, leave empty if only a single tariff is needed." + }, + "description": "Create a sensor which tracks consumption of various utilities (e.g., energy, gas, water, heating) over a configured period of time, typically monthly. The utility meter sensor optionally supports splitting the consumption by tariffs, in that case one sensor for each tariff is created as well as a select entity to choose the current tariff.", + "title": "Add Utility Meter" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "source": "Input sensor" + } + } + } + }, + "title": "Utility Meter" +} \ No newline at end of file diff --git a/homeassistant/components/vacuum/manifest.json b/homeassistant/components/vacuum/manifest.json index 2a874b36a1c..ee4fa6a471e 100644 --- a/homeassistant/components/vacuum/manifest.json +++ b/homeassistant/components/vacuum/manifest.json @@ -2,6 +2,6 @@ "domain": "vacuum", "name": "Vacuum", "documentation": "https://www.home-assistant.io/integrations/vacuum", - "codeowners": [], + "codeowners": ["@home-assistant/core"], "quality_scale": "internal" } diff --git a/homeassistant/components/vacuum/recorder.py b/homeassistant/components/vacuum/recorder.py new file mode 100644 index 00000000000..7dc7e9e0408 --- /dev/null +++ b/homeassistant/components/vacuum/recorder.py @@ -0,0 +1,12 @@ +"""Integration platform for recorder.""" +from __future__ import annotations + +from homeassistant.core import HomeAssistant, callback + +from . import ATTR_FAN_SPEED_LIST + + +@callback +def exclude_attributes(hass: HomeAssistant) -> set[str]: + """Exclude static attributes from being recorded in the database.""" + return {ATTR_FAN_SPEED_LIST} diff --git a/homeassistant/components/vacuum/translations/el.json b/homeassistant/components/vacuum/translations/el.json index bb068b11068..7c849d93998 100644 --- a/homeassistant/components/vacuum/translations/el.json +++ b/homeassistant/components/vacuum/translations/el.json @@ -15,12 +15,12 @@ }, "state": { "_": { - "cleaning": "\u039a\u03b1\u03b8\u03b1\u03c1\u03b9\u03c3\u03bc\u03cc\u03c2", + "cleaning": "\u039a\u03b1\u03b8\u03b1\u03c1\u03af\u03b6\u03b5\u03b9", "docked": "\u03a6\u03bf\u03c1\u03c4\u03af\u03b6\u03b5\u03b9", "error": "\u03a3\u03c6\u03ac\u03bb\u03bc\u03b1", "idle": "\u03a3\u03b5 \u03b1\u03b4\u03c1\u03ac\u03bd\u03b5\u03b9\u03b1", - "off": "\u039c\u03b7 \u0395\u03bd\u03b5\u03c1\u03b3\u03cc", - "on": "\u0395\u03bd\u03b5\u03c1\u03b3\u03cc", + "off": "\u0395\u03ba\u03c4\u03cc\u03c2 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1\u03c2", + "on": "\u03a3\u03b5 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1", "paused": "\u03a0\u03b1\u03cd\u03c3\u03b7", "returning": "\u03a0\u03c1\u03bf\u03c2 \u03c6\u03cc\u03c1\u03c4\u03b9\u03c3\u03b7" } diff --git a/homeassistant/components/vacuum/translations/fr.json b/homeassistant/components/vacuum/translations/fr.json index 7bd851a3a8f..d520f1b4291 100644 --- a/homeassistant/components/vacuum/translations/fr.json +++ b/homeassistant/components/vacuum/translations/fr.json @@ -18,9 +18,9 @@ "cleaning": "Nettoyage", "docked": "Sur la base", "error": "Erreur", - "idle": "En veille", - "off": "Inactif", - "on": "Actif", + "idle": "Inactif", + "off": "D\u00e9sactiv\u00e9", + "on": "Activ\u00e9", "paused": "En pause", "returning": "Retourne \u00e0 la base" } diff --git a/homeassistant/components/vacuum/translations/zh-Hant.json b/homeassistant/components/vacuum/translations/zh-Hant.json index 0f141b0f225..1382ef7f1a0 100644 --- a/homeassistant/components/vacuum/translations/zh-Hant.json +++ b/homeassistant/components/vacuum/translations/zh-Hant.json @@ -25,5 +25,5 @@ "returning": "\u8fd4\u56de\u5145\u96fb" } }, - "title": "\u5438\u5875\u5668" + "title": "\u6383\u5730\u6a5f\u5668\u4eba" } \ No newline at end of file diff --git a/homeassistant/components/vallox/__init__.py b/homeassistant/components/vallox/__init__.py index aeb9e59e286..23e5cb53f97 100644 --- a/homeassistant/components/vallox/__init__.py +++ b/homeassistant/components/vallox/__init__.py @@ -131,11 +131,9 @@ class ValloxState: return next_filter_change_date -class ValloxDataUpdateCoordinator(DataUpdateCoordinator): +class ValloxDataUpdateCoordinator(DataUpdateCoordinator[ValloxState]): """The DataUpdateCoordinator for Vallox.""" - data: ValloxState - async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the integration from configuration.yaml (DEPRECATED).""" diff --git a/homeassistant/components/vallox/binary_sensor.py b/homeassistant/components/vallox/binary_sensor.py index e7b25ca2a80..348bad97158 100644 --- a/homeassistant/components/vallox/binary_sensor.py +++ b/homeassistant/components/vallox/binary_sensor.py @@ -16,11 +16,12 @@ from . import ValloxDataUpdateCoordinator from .const import DOMAIN -class ValloxBinarySensor(CoordinatorEntity, BinarySensorEntity): +class ValloxBinarySensor( + CoordinatorEntity[ValloxDataUpdateCoordinator], BinarySensorEntity +): """Representation of a Vallox binary sensor.""" entity_description: ValloxBinarySensorEntityDescription - coordinator: ValloxDataUpdateCoordinator def __init__( self, diff --git a/homeassistant/components/vallox/config_flow.py b/homeassistant/components/vallox/config_flow.py index 1658a987263..d30c8641d2c 100644 --- a/homeassistant/components/vallox/config_flow.py +++ b/homeassistant/components/vallox/config_flow.py @@ -13,7 +13,6 @@ from homeassistant.const import CONF_HOST, CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResult from homeassistant.exceptions import HomeAssistantError -from homeassistant.loader import async_get_integration from homeassistant.util.network import is_ip_address from .const import DEFAULT_NAME, DOMAIN @@ -83,14 +82,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Handle the initial step.""" - integration = await async_get_integration(self.hass, DOMAIN) - if user_input is None: return self.async_show_form( step_id="user", - description_placeholders={ - "integration_docs_url": integration.documentation - }, data_schema=STEP_USER_DATA_SCHEMA, ) @@ -120,9 +114,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_show_form( step_id="user", - description_placeholders={ - "integration_docs_url": integration.documentation - }, data_schema=STEP_USER_DATA_SCHEMA, errors=errors, ) diff --git a/homeassistant/components/vallox/fan.py b/homeassistant/components/vallox/fan.py index 59068d4819e..52535be3a29 100644 --- a/homeassistant/components/vallox/fan.py +++ b/homeassistant/components/vallox/fan.py @@ -80,11 +80,9 @@ async def async_setup_entry( async_add_entities([device]) -class ValloxFan(CoordinatorEntity, FanEntity): +class ValloxFan(CoordinatorEntity[ValloxDataUpdateCoordinator], FanEntity): """Representation of the fan.""" - coordinator: ValloxDataUpdateCoordinator - def __init__( self, name: str, @@ -168,7 +166,6 @@ class ValloxFan(CoordinatorEntity, FanEntity): async def async_turn_on( self, - speed: str | None = None, percentage: int | None = None, preset_mode: str | None = None, **kwargs: Any, diff --git a/homeassistant/components/vallox/sensor.py b/homeassistant/components/vallox/sensor.py index eece054c82e..92f0bc32e76 100644 --- a/homeassistant/components/vallox/sensor.py +++ b/homeassistant/components/vallox/sensor.py @@ -32,11 +32,10 @@ from .const import ( ) -class ValloxSensor(CoordinatorEntity, SensorEntity): +class ValloxSensor(CoordinatorEntity[ValloxDataUpdateCoordinator], SensorEntity): """Representation of a Vallox sensor.""" entity_description: ValloxSensorEntityDescription - coordinator: ValloxDataUpdateCoordinator def __init__( self, diff --git a/homeassistant/components/vallox/services.yaml b/homeassistant/components/vallox/services.yaml index d6a0ec238c3..15ce6c88b55 100644 --- a/homeassistant/components/vallox/services.yaml +++ b/homeassistant/components/vallox/services.yaml @@ -10,7 +10,7 @@ set_profile_fan_speed_home: number: min: 0 max: 100 - unit_of_measurement: '%' + unit_of_measurement: "%" set_profile_fan_speed_away: name: Set profile fan speed away @@ -24,7 +24,7 @@ set_profile_fan_speed_away: number: min: 0 max: 100 - unit_of_measurement: '%' + unit_of_measurement: "%" set_profile_fan_speed_boost: name: Set profile fan speed boost @@ -38,4 +38,4 @@ set_profile_fan_speed_boost: number: min: 0 max: 100 - unit_of_measurement: '%' + unit_of_measurement: "%" diff --git a/homeassistant/components/vallox/strings.json b/homeassistant/components/vallox/strings.json index dd341228db8..cada5a7febd 100644 --- a/homeassistant/components/vallox/strings.json +++ b/homeassistant/components/vallox/strings.json @@ -2,8 +2,6 @@ "config": { "step": { "user": { - "title": "Vallox", - "description": "Set up the Vallox integration. If you have problems with configuration go to {integration_docs_url}.", "data": { "host": "[%key:common::config_flow::data::host%]" } diff --git a/homeassistant/components/vallox/translations/fr.json b/homeassistant/components/vallox/translations/fr.json index 9bd74d4273c..c9823756185 100644 --- a/homeassistant/components/vallox/translations/fr.json +++ b/homeassistant/components/vallox/translations/fr.json @@ -1,15 +1,15 @@ { "config": { "abort": { - "already_configured": "Service d\u00e9j\u00e0 configur\u00e9", - "cannot_connect": "Erreur de connexion", - "invalid_host": "Nom d'h\u00f4te or IP invalide", - "unknown": "Erreur inconnue" + "already_configured": "Le service est d\u00e9j\u00e0 configur\u00e9", + "cannot_connect": "\u00c9chec de connexion", + "invalid_host": "Nom d'h\u00f4te ou adresse IP non valide", + "unknown": "Erreur inattendue" }, "error": { - "cannot_connect": "Erreur de connexion", - "invalid_host": "Nom d'h\u00f4te or IP invalide", - "unknown": "Erreur inconnue" + "cannot_connect": "\u00c9chec de connexion", + "invalid_host": "Nom d'h\u00f4te ou adresse IP non valide", + "unknown": "Erreur inattendue" }, "step": { "user": { diff --git a/homeassistant/components/velbus/config_flow.py b/homeassistant/components/velbus/config_flow.py index 189dc029ded..993146d375c 100644 --- a/homeassistant/components/velbus/config_flow.py +++ b/homeassistant/components/velbus/config_flow.py @@ -115,5 +115,4 @@ class VelbusConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_show_form( step_id="discovery_confirm", description_placeholders={CONF_NAME: self._title}, - data_schema=vol.Schema({}), ) diff --git a/homeassistant/components/velbus/manifest.json b/homeassistant/components/velbus/manifest.json index c9a72aa2d8e..ae50609baf1 100644 --- a/homeassistant/components/velbus/manifest.json +++ b/homeassistant/components/velbus/manifest.json @@ -9,20 +9,20 @@ "iot_class": "local_push", "usb": [ { - "vid": "10CF", - "pid": "0B1B" + "vid": "10CF", + "pid": "0B1B" }, { - "vid": "10CF", - "pid": "0516" + "vid": "10CF", + "pid": "0516" }, { - "vid": "10CF", - "pid": "0517" + "vid": "10CF", + "pid": "0517" }, { - "vid": "10CF", - "pid": "0518" + "vid": "10CF", + "pid": "0518" } ], "loggers": ["velbusaio"] diff --git a/homeassistant/components/velbus/services.yaml b/homeassistant/components/velbus/services.yaml index 83af09409c1..6dbf5e8cb47 100644 --- a/homeassistant/components/velbus/services.yaml +++ b/homeassistant/components/velbus/services.yaml @@ -7,7 +7,7 @@ sync_clock: description: The velbus interface to send the command to, this will be the same value as used during configuration required: true example: "192.168.1.5:27015" - default: '' + default: "" selector: text: @@ -20,7 +20,7 @@ scan: description: The velbus interface to send the command to, this will be the same value as used during configuration required: true example: "192.168.1.5:27015" - default: '' + default: "" selector: text: @@ -35,7 +35,7 @@ set_memo_text: description: The velbus interface to send the command to, this will be the same value as used during configuration required: true example: "192.168.1.5:27015" - default: '' + default: "" selector: text: address: @@ -54,6 +54,6 @@ set_memo_text: The actual text to be displayed. Text is limited to 64 characters. example: "Do not forget trash" - default: '' + default: "" selector: text: diff --git a/homeassistant/components/velbus/strings.json b/homeassistant/components/velbus/strings.json index c2defd782f4..6eb44d8cb0c 100644 --- a/homeassistant/components/velbus/strings.json +++ b/homeassistant/components/velbus/strings.json @@ -14,7 +14,7 @@ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" }, "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } } } diff --git a/homeassistant/components/velbus/translations/zh-Hant.json b/homeassistant/components/velbus/translations/zh-Hant.json index ec0c1ca2c63..0f5e65c7cba 100644 --- a/homeassistant/components/velbus/translations/zh-Hant.json +++ b/homeassistant/components/velbus/translations/zh-Hant.json @@ -13,7 +13,7 @@ "name": "Velbus \u9023\u7dda\u540d\u7a31", "port": "\u9023\u7dda\u5b57\u4e32" }, - "title": "\u5b9a\u7fa9 Velbus \u9023\u7dda\u985e\u578b" + "title": "\u5b9a\u7fa9 Velbus \u9023\u7dda\u985e\u5225" } } } diff --git a/homeassistant/components/venstar/__init__.py b/homeassistant/components/venstar/__init__.py index 66a458f210a..759908c87e4 100644 --- a/homeassistant/components/venstar/__init__.py +++ b/homeassistant/components/venstar/__init__.py @@ -126,11 +126,9 @@ class VenstarDataUpdateCoordinator(update_coordinator.DataUpdateCoordinator): return None -class VenstarEntity(CoordinatorEntity): +class VenstarEntity(CoordinatorEntity[VenstarDataUpdateCoordinator]): """Representation of a Venstar entity.""" - coordinator: VenstarDataUpdateCoordinator - def __init__( self, venstar_data_coordinator: VenstarDataUpdateCoordinator, diff --git a/homeassistant/components/venstar/manifest.json b/homeassistant/components/venstar/manifest.json index d9f5b51e0ef..42a97020fa3 100644 --- a/homeassistant/components/venstar/manifest.json +++ b/homeassistant/components/venstar/manifest.json @@ -3,9 +3,7 @@ "name": "Venstar", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/venstar", - "requirements": [ - "venstarcolortouch==0.15" - ], + "requirements": ["venstarcolortouch==0.15"], "codeowners": ["@garbled1"], "iot_class": "local_polling", "loggers": ["venstarcolortouch"] diff --git a/homeassistant/components/vera/__init__.py b/homeassistant/components/vera/__init__.py index ef293c279be..d923861112b 100644 --- a/homeassistant/components/vera/__init__.py +++ b/homeassistant/components/vera/__init__.py @@ -28,7 +28,7 @@ from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import ConfigType -from homeassistant.util import convert, slugify +from homeassistant.util import slugify from homeassistant.util.dt import utc_from_timestamp from .common import ( @@ -39,14 +39,7 @@ from .common import ( set_controller_data, ) from .config_flow import fix_device_id_list, new_options -from .const import ( - ATTR_CURRENT_ENERGY_KWH, - ATTR_CURRENT_POWER_W, - CONF_CONTROLLER, - CONF_LEGACY_UNIQUE_ID, - DOMAIN, - VERA_ID_FORMAT, -) +from .const import CONF_CONTROLLER, CONF_LEGACY_UNIQUE_ID, DOMAIN, VERA_ID_FORMAT _LOGGER = logging.getLogger(__name__) @@ -214,14 +207,14 @@ def map_vera_device( ) -DeviceType = TypeVar("DeviceType", bound=veraApi.VeraDevice) +_DeviceTypeT = TypeVar("_DeviceTypeT", bound=veraApi.VeraDevice) -class VeraDevice(Generic[DeviceType], Entity): +class VeraDevice(Generic[_DeviceTypeT], Entity): """Representation of a Vera device entity.""" def __init__( - self, vera_device: DeviceType, controller_data: ControllerData + self, vera_device: _DeviceTypeT, controller_data: ControllerData ) -> None: """Initialize the device.""" self.vera_device = vera_device @@ -242,7 +235,7 @@ class VeraDevice(Generic[DeviceType], Entity): """Subscribe to updates.""" self.controller.register(self.vera_device, self._update_callback) - def _update_callback(self, _device: DeviceType) -> None: + def _update_callback(self, _device: _DeviceTypeT) -> None: """Update the state.""" self.schedule_update_ha_state(True) @@ -279,12 +272,6 @@ class VeraDevice(Generic[DeviceType], Entity): tripped = self.vera_device.is_tripped attr[ATTR_TRIPPED] = "True" if tripped else "False" - if power := self.vera_device.power: - attr[ATTR_CURRENT_POWER_W] = convert(power, float, 0.0) - - if energy := self.vera_device.energy: - attr[ATTR_CURRENT_ENERGY_KWH] = convert(energy, float, 0.0) - attr["Vera Device Id"] = self.vera_device.vera_device_id return attr diff --git a/homeassistant/components/vera/climate.py b/homeassistant/components/vera/climate.py index 477351480d2..399d4ace278 100644 --- a/homeassistant/components/vera/climate.py +++ b/homeassistant/components/vera/climate.py @@ -25,7 +25,6 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.util import convert from . import VeraDevice from .common import ControllerData, get_controller_data @@ -111,13 +110,6 @@ class VeraThermostat(VeraDevice[veraApi.VeraThermostat], ClimateEntity): self.schedule_update_ha_state() - @property - def current_power_w(self) -> float | None: - """Return the current power usage in W.""" - if power := self.vera_device.power: - return convert(power, float, 0.0) - return None - @property def temperature_unit(self) -> str: """Return the unit of measurement.""" diff --git a/homeassistant/components/vera/strings.json b/homeassistant/components/vera/strings.json index 66958f44a62..50d60f9a8ab 100644 --- a/homeassistant/components/vera/strings.json +++ b/homeassistant/components/vera/strings.json @@ -5,12 +5,13 @@ }, "step": { "user": { - "title": "Setup Vera controller", - "description": "Provide a Vera controller URL below. It should look like this: http://192.168.1.161:3480.", "data": { "vera_controller_url": "Controller URL", "lights": "Vera switch device ids to treat as lights in Home Assistant.", "exclude": "Vera device ids to exclude from Home Assistant." + }, + "data_description": { + "vera_controller_url": "It should look like this: http://192.168.1.161:3480" } } } diff --git a/homeassistant/components/vera/switch.py b/homeassistant/components/vera/switch.py index f1de72870f7..b146ed39ade 100644 --- a/homeassistant/components/vera/switch.py +++ b/homeassistant/components/vera/switch.py @@ -10,7 +10,6 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.util import convert from . import VeraDevice from .common import ControllerData, get_controller_data @@ -55,13 +54,6 @@ class VeraSwitch(VeraDevice[veraApi.VeraSwitch], SwitchEntity): self._state = False self.schedule_update_ha_state() - @property - def current_power_w(self) -> float | None: - """Return the current power usage in W.""" - if power := self.vera_device.power: - return convert(power, float, 0.0) - return None - @property def is_on(self) -> bool: """Return true if device is on.""" diff --git a/homeassistant/components/verisure/alarm_control_panel.py b/homeassistant/components/verisure/alarm_control_panel.py index e9cb2f49842..4cc5e8f6cb3 100644 --- a/homeassistant/components/verisure/alarm_control_panel.py +++ b/homeassistant/components/verisure/alarm_control_panel.py @@ -30,11 +30,11 @@ async def async_setup_entry( async_add_entities([VerisureAlarm(coordinator=hass.data[DOMAIN][entry.entry_id])]) -class VerisureAlarm(CoordinatorEntity, AlarmControlPanelEntity): +class VerisureAlarm( + CoordinatorEntity[VerisureDataUpdateCoordinator], AlarmControlPanelEntity +): """Representation of a Verisure alarm status.""" - coordinator: VerisureDataUpdateCoordinator - _attr_code_format = FORMAT_NUMBER _attr_name = "Verisure Alarm" _attr_supported_features = SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY diff --git a/homeassistant/components/verisure/binary_sensor.py b/homeassistant/components/verisure/binary_sensor.py index 05e0d77845a..217890b8a01 100644 --- a/homeassistant/components/verisure/binary_sensor.py +++ b/homeassistant/components/verisure/binary_sensor.py @@ -33,11 +33,11 @@ async def async_setup_entry( async_add_entities(sensors) -class VerisureDoorWindowSensor(CoordinatorEntity, BinarySensorEntity): +class VerisureDoorWindowSensor( + CoordinatorEntity[VerisureDataUpdateCoordinator], BinarySensorEntity +): """Representation of a Verisure door window sensor.""" - coordinator: VerisureDataUpdateCoordinator - _attr_device_class = BinarySensorDeviceClass.OPENING def __init__( @@ -79,11 +79,11 @@ class VerisureDoorWindowSensor(CoordinatorEntity, BinarySensorEntity): ) -class VerisureEthernetStatus(CoordinatorEntity, BinarySensorEntity): +class VerisureEthernetStatus( + CoordinatorEntity[VerisureDataUpdateCoordinator], BinarySensorEntity +): """Representation of a Verisure VBOX internet status.""" - coordinator: VerisureDataUpdateCoordinator - _attr_name = "Verisure Ethernet status" _attr_device_class = BinarySensorDeviceClass.CONNECTIVITY _attr_entity_category = EntityCategory.DIAGNOSTIC diff --git a/homeassistant/components/verisure/camera.py b/homeassistant/components/verisure/camera.py index 787e496202f..c753bf2c5dc 100644 --- a/homeassistant/components/verisure/camera.py +++ b/homeassistant/components/verisure/camera.py @@ -43,11 +43,9 @@ async def async_setup_entry( ) -class VerisureSmartcam(CoordinatorEntity, Camera): +class VerisureSmartcam(CoordinatorEntity[VerisureDataUpdateCoordinator], Camera): """Representation of a Verisure camera.""" - coordinator: VerisureDataUpdateCoordinator - def __init__( self, coordinator: VerisureDataUpdateCoordinator, diff --git a/homeassistant/components/verisure/lock.py b/homeassistant/components/verisure/lock.py index 86b232d54fd..0e28298b2e8 100644 --- a/homeassistant/components/verisure/lock.py +++ b/homeassistant/components/verisure/lock.py @@ -55,11 +55,9 @@ async def async_setup_entry( ) -class VerisureDoorlock(CoordinatorEntity, LockEntity): +class VerisureDoorlock(CoordinatorEntity[VerisureDataUpdateCoordinator], LockEntity): """Representation of a Verisure doorlock.""" - coordinator: VerisureDataUpdateCoordinator - def __init__( self, coordinator: VerisureDataUpdateCoordinator, serial_number: str ) -> None: diff --git a/homeassistant/components/verisure/sensor.py b/homeassistant/components/verisure/sensor.py index 9e19e5d865f..3b8f722c6f7 100644 --- a/homeassistant/components/verisure/sensor.py +++ b/homeassistant/components/verisure/sensor.py @@ -45,11 +45,11 @@ async def async_setup_entry( async_add_entities(sensors) -class VerisureThermometer(CoordinatorEntity, SensorEntity): +class VerisureThermometer( + CoordinatorEntity[VerisureDataUpdateCoordinator], SensorEntity +): """Representation of a Verisure thermometer.""" - coordinator: VerisureDataUpdateCoordinator - _attr_device_class = SensorDeviceClass.TEMPERATURE _attr_native_unit_of_measurement = TEMP_CELSIUS _attr_state_class = SensorStateClass.MEASUREMENT @@ -100,11 +100,11 @@ class VerisureThermometer(CoordinatorEntity, SensorEntity): ) -class VerisureHygrometer(CoordinatorEntity, SensorEntity): +class VerisureHygrometer( + CoordinatorEntity[VerisureDataUpdateCoordinator], SensorEntity +): """Representation of a Verisure hygrometer.""" - coordinator: VerisureDataUpdateCoordinator - _attr_device_class = SensorDeviceClass.HUMIDITY _attr_native_unit_of_measurement = PERCENTAGE _attr_state_class = SensorStateClass.MEASUREMENT @@ -155,11 +155,11 @@ class VerisureHygrometer(CoordinatorEntity, SensorEntity): ) -class VerisureMouseDetection(CoordinatorEntity, SensorEntity): +class VerisureMouseDetection( + CoordinatorEntity[VerisureDataUpdateCoordinator], SensorEntity +): """Representation of a Verisure mouse detector.""" - coordinator: VerisureDataUpdateCoordinator - _attr_native_unit_of_measurement = "Mice" def __init__( diff --git a/homeassistant/components/verisure/switch.py b/homeassistant/components/verisure/switch.py index 777195d1a51..5d1fd728f4a 100644 --- a/homeassistant/components/verisure/switch.py +++ b/homeassistant/components/verisure/switch.py @@ -27,11 +27,9 @@ async def async_setup_entry( ) -class VerisureSmartplug(CoordinatorEntity, SwitchEntity): +class VerisureSmartplug(CoordinatorEntity[VerisureDataUpdateCoordinator], SwitchEntity): """Representation of a Verisure smartplug.""" - coordinator: VerisureDataUpdateCoordinator - def __init__( self, coordinator: VerisureDataUpdateCoordinator, serial_number: str ) -> None: diff --git a/homeassistant/components/verisure/translations/fr.json b/homeassistant/components/verisure/translations/fr.json index 4909fec8894..f3c26abb74a 100644 --- a/homeassistant/components/verisure/translations/fr.json +++ b/homeassistant/components/verisure/translations/fr.json @@ -5,7 +5,7 @@ "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" }, "error": { - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "step": { @@ -18,14 +18,14 @@ "reauth_confirm": { "data": { "description": "R\u00e9-authentifiez-vous avec votre compte Verisure My Pages.", - "email": "Email", + "email": "Courriel", "password": "Mot de passe" } }, "user": { "data": { "description": "Connectez-vous avec votre compte Verisure My Pages.", - "email": "Email", + "email": "Courriel", "password": "Mot de passe" } } diff --git a/homeassistant/components/version/config_flow.py b/homeassistant/components/version/config_flow.py index 292b194eea1..2fd670a7342 100644 --- a/homeassistant/components/version/config_flow.py +++ b/homeassistant/components/version/config_flow.py @@ -6,7 +6,7 @@ from typing import Any import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import CONF_NAME, CONF_SOURCE +from homeassistant.const import CONF_SOURCE from homeassistant.data_entry_flow import FlowResult from .const import ( @@ -20,25 +20,16 @@ from .const import ( DEFAULT_CHANNEL, DEFAULT_CONFIGURATION, DEFAULT_IMAGE, - DEFAULT_NAME, DEFAULT_NAME_CURRENT, - DEFAULT_NAME_LATEST, - DEFAULT_SOURCE, DOMAIN, - POSTFIX_CONTAINER_NAME, - SOURCE_DOCKER, - SOURCE_HASSIO, STEP_USER, STEP_VERSION_SOURCE, VALID_BOARDS, VALID_CHANNELS, VALID_CONTAINER_IMAGES, VALID_IMAGES, - VERSION_SOURCE_DOCKER_HUB, VERSION_SOURCE_LOCAL, VERSION_SOURCE_MAP, - VERSION_SOURCE_MAP_INVERTED, - VERSION_SOURCE_VERSIONS, ) @@ -137,16 +128,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): title=self._config_entry_name, data=self._entry_data ) - async def async_step_import(self, import_config: dict[str, Any]) -> FlowResult: - """Import a config entry from configuration.yaml.""" - self._entry_data = _convert_imported_configuration(import_config) - - self._async_abort_entries_match({**DEFAULT_CONFIGURATION, **self._entry_data}) - - return self.async_create_entry( - title=self._config_entry_name, data=self._entry_data - ) - @property def _config_entry_name(self) -> str: """Return the name of the config entry.""" @@ -159,36 +140,3 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return f"{name} {channel.title()}" return name - - -def _convert_imported_configuration(config: dict[str, Any]) -> Any: - """Convert a key from the imported configuration.""" - data = DEFAULT_CONFIGURATION.copy() - if config.get(CONF_BETA): - data[CONF_CHANNEL] = "beta" - - if (source := config.get(CONF_SOURCE)) and source != DEFAULT_SOURCE: - if source == SOURCE_HASSIO: - data[CONF_SOURCE] = "supervisor" - data[CONF_VERSION_SOURCE] = VERSION_SOURCE_VERSIONS - elif source == SOURCE_DOCKER: - data[CONF_SOURCE] = "container" - data[CONF_VERSION_SOURCE] = VERSION_SOURCE_DOCKER_HUB - else: - data[CONF_SOURCE] = source - data[CONF_VERSION_SOURCE] = VERSION_SOURCE_MAP_INVERTED[source] - - if (image := config.get(CONF_IMAGE)) and image != DEFAULT_IMAGE: - if data[CONF_SOURCE] == "container": - data[CONF_IMAGE] = f"{config[CONF_IMAGE]}{POSTFIX_CONTAINER_NAME}" - else: - data[CONF_IMAGE] = config[CONF_IMAGE] - - if (name := config.get(CONF_NAME)) and name != DEFAULT_NAME: - data[CONF_NAME] = config[CONF_NAME] - else: - if data[CONF_SOURCE] == "local": - data[CONF_NAME] = DEFAULT_NAME_CURRENT - else: - data[CONF_NAME] = DEFAULT_NAME_LATEST - return data diff --git a/homeassistant/components/version/const.py b/homeassistant/components/version/const.py index 9f480c25cc5..419e49d7240 100644 --- a/homeassistant/components/version/const.py +++ b/homeassistant/components/version/const.py @@ -31,7 +31,6 @@ ATTR_VERSION_SOURCE: Final = CONF_VERSION_SOURCE ATTR_SOURCE: Final = CONF_SOURCE SOURCE_DOCKER: Final = "docker" # Kept to not break existing configurations -SOURCE_HASSIO: Final = "hassio" # Kept to not break existing configurations VERSION_SOURCE_DOCKER_HUB: Final = "Docker Hub" VERSION_SOURCE_HAIO: Final = "Home Assistant Website" @@ -44,7 +43,6 @@ DEFAULT_BOARD: Final = "OVA" DEFAULT_CHANNEL: Final = "stable" DEFAULT_IMAGE: Final = "default" DEFAULT_NAME_CURRENT: Final = "Current Version" -DEFAULT_NAME_LATEST: Final = "Latest Version" DEFAULT_NAME: Final = "" DEFAULT_SOURCE: Final = "local" DEFAULT_CONFIGURATION: Final[dict[str, Any]] = { @@ -89,11 +87,6 @@ VERSION_SOURCE_MAP: Final[dict[str, str]] = { VERSION_SOURCE_PYPI: "pypi", } -VERSION_SOURCE_MAP_INVERTED: Final[dict[str, str]] = { - value: key for key, value in VERSION_SOURCE_MAP.items() -} - - VALID_SOURCES: Final[list[str]] = HA_VERSION_SOURCES + [ "hassio", # Kept to not break existing configurations "docker", # Kept to not break existing configurations diff --git a/homeassistant/components/version/entity.py b/homeassistant/components/version/entity.py index 1dcdc23fa9f..d950c6394b8 100644 --- a/homeassistant/components/version/entity.py +++ b/homeassistant/components/version/entity.py @@ -8,7 +8,7 @@ from .const import DOMAIN, HOME_ASSISTANT from .coordinator import VersionDataUpdateCoordinator -class VersionEntity(CoordinatorEntity): +class VersionEntity(CoordinatorEntity[VersionDataUpdateCoordinator]): """Common entity class for Version integration.""" _attr_device_info = DeviceInfo( @@ -18,8 +18,6 @@ class VersionEntity(CoordinatorEntity): entry_type=DeviceEntryType.SERVICE, ) - coordinator: VersionDataUpdateCoordinator - def __init__( self, coordinator: VersionDataUpdateCoordinator, diff --git a/homeassistant/components/version/manifest.json b/homeassistant/components/version/manifest.json index fa1410521ae..966286d0b3b 100644 --- a/homeassistant/components/version/manifest.json +++ b/homeassistant/components/version/manifest.json @@ -2,15 +2,10 @@ "domain": "version", "name": "Version", "documentation": "https://www.home-assistant.io/integrations/version", - "requirements": [ - "pyhaversion==22.02.0" - ], - "codeowners": [ - "@fabaff", - "@ludeeus" - ], + "requirements": ["pyhaversion==22.04.0"], + "codeowners": ["@fabaff", "@ludeeus"], "quality_scale": "internal", "iot_class": "local_push", "config_flow": true, "loggers": ["pyhaversion"] -} \ No newline at end of file +} diff --git a/homeassistant/components/version/sensor.py b/homeassistant/components/version/sensor.py index f0583a19068..82e49155603 100644 --- a/homeassistant/components/version/sensor.py +++ b/homeassistant/components/version/sensor.py @@ -1,69 +1,19 @@ """Sensor that can display the current Home Assistant versions.""" from __future__ import annotations -from typing import Any, Final +from typing import Any -import voluptuous as vol -from voluptuous.schema_builder import Schema - -from homeassistant.components.sensor import ( - PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, - SensorEntity, - SensorEntityDescription, -) -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.components.sensor import SensorEntity, SensorEntityDescription +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType +from homeassistant.helpers.typing import StateType -from .const import ( - ATTR_SOURCE, - CONF_BETA, - CONF_IMAGE, - CONF_SOURCE, - DEFAULT_BETA, - DEFAULT_IMAGE, - DEFAULT_NAME, - DEFAULT_SOURCE, - DOMAIN, - LOGGER, - VALID_IMAGES, - VALID_SOURCES, -) +from .const import CONF_SOURCE, DEFAULT_NAME, DOMAIN from .coordinator import VersionDataUpdateCoordinator from .entity import VersionEntity -PLATFORM_SCHEMA: Final[Schema] = SENSOR_PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_BETA, default=DEFAULT_BETA): cv.boolean, - vol.Optional(CONF_IMAGE, default=DEFAULT_IMAGE): vol.In(VALID_IMAGES), - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_SOURCE, default=DEFAULT_SOURCE): vol.In(VALID_SOURCES), - } -) - - -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the legacy version sensor platform.""" - LOGGER.warning( - "Configuration of the Version platform in YAML is deprecated and will be " - "removed in Home Assistant 2022.4; Your existing configuration " - "has been imported into the UI automatically and can be safely removed " - "from your configuration.yaml file" - ) - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={ATTR_SOURCE: SOURCE_IMPORT}, data=config - ) - ) - async def async_setup_entry( hass: HomeAssistant, diff --git a/homeassistant/components/version/strings.json b/homeassistant/components/version/strings.json index b147422b32a..299ab753cb9 100644 --- a/homeassistant/components/version/strings.json +++ b/homeassistant/components/version/strings.json @@ -1,26 +1,26 @@ { "config": { - "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + }, + "step": { + "user": { + "title": "Select installation type", + "description": "Select the source you want to track versions from", + "data": { + "version_source": "Version source" + } }, - "step": { - "user": { - "title": "Select installation type", - "description": "Select the source you want to track versions from", - "data": { - "version_source": "Version source" - } - }, - "version_source": { - "title": "Configure", - "description": "Configure {version_source} version tracking", - "data": { - "beta": "Include beta versions", - "board": "Which board should be tracked", - "channel": "Which channel should be tracked", - "image": "Which image should be tracked" - } - } + "version_source": { + "title": "Configure", + "description": "Configure {version_source} version tracking", + "data": { + "beta": "Include beta versions", + "board": "Which board should be tracked", + "channel": "Which channel should be tracked", + "image": "Which image should be tracked" + } } + } } -} \ No newline at end of file +} diff --git a/homeassistant/components/version/translations/zh-Hant.json b/homeassistant/components/version/translations/zh-Hant.json index ee24e6a4298..4e5ead19e3e 100644 --- a/homeassistant/components/version/translations/zh-Hant.json +++ b/homeassistant/components/version/translations/zh-Hant.json @@ -9,12 +9,12 @@ "version_source": "\u7248\u672c\u4f86\u6e90" }, "description": "\u8ffd\u8e64\u7248\u672c\u4f86\u6e90", - "title": "\u9078\u64c7\u5b89\u88dd\u985e\u578b" + "title": "\u9078\u64c7\u5b89\u88dd\u985e\u5225" }, "version_source": { "data": { "beta": "\u5305\u542b\u6e2c\u8a66\u7248\u672c", - "board": "\u6240\u8981\u8ffd\u8e64\u7684\u786c\u9ad4\u985e\u578b", + "board": "\u6240\u8981\u8ffd\u8e64\u7684\u786c\u9ad4\u985e\u5225", "channel": "\u6240\u8981\u8ffd\u8e64\u7684\u7248\u6b21", "image": "\u6240\u8981\u8ffd\u8e64\u7684\u6620\u50cf\u6a94\u7248\u672c" }, diff --git a/homeassistant/components/vesync/fan.py b/homeassistant/components/vesync/fan.py index 2dee94f58e6..fef6d24a9bd 100644 --- a/homeassistant/components/vesync/fan.py +++ b/homeassistant/components/vesync/fan.py @@ -34,7 +34,12 @@ PRESET_MODES = { "Core300S": [FAN_MODE_AUTO, FAN_MODE_SLEEP], "Core400S": [FAN_MODE_AUTO, FAN_MODE_SLEEP], } -SPEED_RANGE = (1, 3) # off is not included +SPEED_RANGE = { # off is not included + "LV-PUR131S": (1, 3), + "Core200S": (1, 3), + "Core300S": (1, 3), + "Core400S": (1, 4), +} async def async_setup_entry( @@ -92,13 +97,15 @@ class VeSyncFanHA(VeSyncDevice, FanEntity): self.smartfan.mode == "manual" and (current_level := self.smartfan.fan_level) is not None ): - return ranged_value_to_percentage(SPEED_RANGE, current_level) + return ranged_value_to_percentage( + SPEED_RANGE[self.device.device_type], current_level + ) return None @property def speed_count(self) -> int: """Return the number of speeds the fan supports.""" - return int_states_in_range(SPEED_RANGE) + return int_states_in_range(SPEED_RANGE[self.device.device_type]) @property def preset_modes(self): @@ -156,7 +163,11 @@ class VeSyncFanHA(VeSyncDevice, FanEntity): self.smartfan.manual_mode() self.smartfan.change_fan_speed( - math.ceil(percentage_to_ranged_value(SPEED_RANGE, percentage)) + math.ceil( + percentage_to_ranged_value( + SPEED_RANGE[self.device.device_type], percentage + ) + ) ) self.schedule_update_ha_state() @@ -164,7 +175,8 @@ class VeSyncFanHA(VeSyncDevice, FanEntity): """Set the preset mode of device.""" if preset_mode not in self.preset_modes: raise ValueError( - "{preset_mode} is not one of the valid preset modes: {self.preset_modes}" + f"{preset_mode} is not one of the valid preset modes: " + f"{self.preset_modes}" ) if not self.smartfan.is_on: @@ -179,7 +191,6 @@ class VeSyncFanHA(VeSyncDevice, FanEntity): def turn_on( self, - speed: str = None, percentage: int = None, preset_mode: str = None, **kwargs, diff --git a/homeassistant/components/vesync/translations/fr.json b/homeassistant/components/vesync/translations/fr.json index 80bb38b615d..c71842b3558 100644 --- a/homeassistant/components/vesync/translations/fr.json +++ b/homeassistant/components/vesync/translations/fr.json @@ -4,13 +4,13 @@ "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." }, "error": { - "invalid_auth": "Authentification invalide" + "invalid_auth": "Authentification non valide" }, "step": { "user": { "data": { "password": "Mot de passe", - "username": "Email" + "username": "Courriel" }, "title": "Entrez vos identifiants" } diff --git a/homeassistant/components/vicare/climate.py b/homeassistant/components/vicare/climate.py index 8320db73ad4..02998343744 100644 --- a/homeassistant/components/vicare/climate.py +++ b/homeassistant/components/vicare/climate.py @@ -24,7 +24,12 @@ from homeassistant.components.climate.const import ( SUPPORT_TARGET_TEMPERATURE, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_TEMPERATURE, PRECISION_WHOLE, TEMP_CELSIUS +from homeassistant.const import ( + ATTR_TEMPERATURE, + PRECISION_TENTHS, + PRECISION_WHOLE, + TEMP_CELSIUS, +) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_platform import homeassistant.helpers.config_validation as cv @@ -310,6 +315,11 @@ class ViCareClimate(ClimateEntity): @property def precision(self): """Return the precision of the system.""" + return PRECISION_TENTHS + + @property + def target_temperature_step(self) -> float: + """Set target temperature step to wholes.""" return PRECISION_WHOLE def set_temperature(self, **kwargs): diff --git a/homeassistant/components/vicare/services.yaml b/homeassistant/components/vicare/services.yaml index 94146c4250e..1fc1e61b6ee 100644 --- a/homeassistant/components/vicare/services.yaml +++ b/homeassistant/components/vicare/services.yaml @@ -13,10 +13,10 @@ set_vicare_mode: selector: select: options: - - 'dhw' - - 'dhwAndHeating' - - 'dhwAndHeatingCooling' - - 'forcedNormal' - - 'forcedReduced' - - 'heating' - - 'standby' + - "dhw" + - "dhwAndHeating" + - "dhwAndHeatingCooling" + - "forcedNormal" + - "forcedReduced" + - "heating" + - "standby" diff --git a/homeassistant/components/vicare/strings.json b/homeassistant/components/vicare/strings.json index b096bc4e4b7..28560ca41f3 100644 --- a/homeassistant/components/vicare/strings.json +++ b/homeassistant/components/vicare/strings.json @@ -1,24 +1,24 @@ { - "config": { - "flow_title": "{name} ({host})", - "step": { - "user": { - "title": "{name}", - "description": "Set up ViCare integration. To generate API key go to https://developer.viessmann.com", - "data": { - "username": "[%key:common::config_flow::data::email%]", - "password": "[%key:common::config_flow::data::password%]", - "client_id": "[%key:common::config_flow::data::api_key%]", - "heating_type": "Heating type" - } - } - }, - "error": { - "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" - }, - "abort": { - "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", - "unknown": "[%key:common::config_flow::error::unknown%]" + "config": { + "flow_title": "{name} ({host})", + "step": { + "user": { + "title": "{name}", + "description": "Set up ViCare integration. To generate API key go to https://developer.viessmann.com", + "data": { + "username": "[%key:common::config_flow::data::email%]", + "password": "[%key:common::config_flow::data::password%]", + "client_id": "[%key:common::config_flow::data::api_key%]", + "heating_type": "Heating type" } + } + }, + "error": { + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" + }, + "abort": { + "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", + "unknown": "[%key:common::config_flow::error::unknown%]" } -} \ No newline at end of file + } +} diff --git a/homeassistant/components/vicare/translations/fr.json b/homeassistant/components/vicare/translations/fr.json index 6f7d6df3624..260dbb25006 100644 --- a/homeassistant/components/vicare/translations/fr.json +++ b/homeassistant/components/vicare/translations/fr.json @@ -2,21 +2,21 @@ "config": { "abort": { "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible.", - "unknown": "Erreur inatendue" + "unknown": "Erreur inattendue" }, "error": { - "invalid_auth": "Authentification invalide" + "invalid_auth": "Authentification non valide" }, - "flow_title": "{name} ( {host} )", + "flow_title": "{name} ({host})", "step": { "user": { "data": { - "client_id": "Cl\u00e9 API", + "client_id": "Cl\u00e9 d'API", "heating_type": "Type de chauffage", "name": "Nom", "password": "Mot de passe", "scan_interval": "Intervalle de balayage (secondes)", - "username": "Email" + "username": "Courriel" }, "description": "Configurer l'int\u00e9gration ViCare. Pour g\u00e9n\u00e9rer une cl\u00e9 API se rendre sur https://developer.viessmann.com", "title": "{name}" diff --git a/homeassistant/components/vicare/translations/zh-Hant.json b/homeassistant/components/vicare/translations/zh-Hant.json index c86ebf3cb34..c3b8fc02c4f 100644 --- a/homeassistant/components/vicare/translations/zh-Hant.json +++ b/homeassistant/components/vicare/translations/zh-Hant.json @@ -12,7 +12,7 @@ "user": { "data": { "client_id": "API \u91d1\u9470", - "heating_type": "\u6696\u6c23\u985e\u578b", + "heating_type": "\u6696\u6c23\u985e\u5225", "name": "\u540d\u7a31", "password": "\u5bc6\u78bc", "scan_interval": "\u6383\u63cf\u9593\u8ddd\uff08\u79d2\uff09", diff --git a/homeassistant/components/vicare/water_heater.py b/homeassistant/components/vicare/water_heater.py index 139c8f75e7f..e1e44188d28 100644 --- a/homeassistant/components/vicare/water_heater.py +++ b/homeassistant/components/vicare/water_heater.py @@ -14,7 +14,12 @@ from homeassistant.components.water_heater import ( WaterHeaterEntity, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_TEMPERATURE, PRECISION_WHOLE, TEMP_CELSIUS +from homeassistant.const import ( + ATTR_TEMPERATURE, + PRECISION_TENTHS, + PRECISION_WHOLE, + TEMP_CELSIUS, +) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -191,10 +196,15 @@ class ViCareWater(WaterHeaterEntity): """Return the maximum temperature.""" return VICARE_TEMP_WATER_MAX + @property + def target_temperature_step(self) -> float: + """Set target temperature step to wholes.""" + return PRECISION_WHOLE + @property def precision(self): """Return the precision of the system.""" - return PRECISION_WHOLE + return PRECISION_TENTHS @property def current_operation(self): diff --git a/homeassistant/components/vilfo/strings.json b/homeassistant/components/vilfo/strings.json index c9b8882c264..6577c99456c 100644 --- a/homeassistant/components/vilfo/strings.json +++ b/homeassistant/components/vilfo/strings.json @@ -2,8 +2,6 @@ "config": { "step": { "user": { - "title": "Connect to the Vilfo Router", - "description": "Set up the Vilfo Router integration. You need your Vilfo Router hostname/IP and an API access token. For additional information on this integration and how to get those details, visit: https://www.home-assistant.io/integrations/vilfo", "data": { "host": "[%key:common::config_flow::data::host%]", "access_token": "[%key:common::config_flow::data::access_token%]" diff --git a/homeassistant/components/vilfo/translations/fr.json b/homeassistant/components/vilfo/translations/fr.json index bfd455fe379..10289500383 100644 --- a/homeassistant/components/vilfo/translations/fr.json +++ b/homeassistant/components/vilfo/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/vizio/config_flow.py b/homeassistant/components/vizio/config_flow.py index 019d016eb2a..8acc602dd36 100644 --- a/homeassistant/components/vizio/config_flow.py +++ b/homeassistant/components/vizio/config_flow.py @@ -454,7 +454,6 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._must_show_form = False return self.async_show_form( step_id=step_id, - data_schema=vol.Schema({}), description_placeholders={"access_token": self._data[CONF_ACCESS_TOKEN]}, ) diff --git a/homeassistant/components/vlc_telnet/config_flow.py b/homeassistant/components/vlc_telnet/config_flow.py index d714057e6f7..29508ad1120 100644 --- a/homeassistant/components/vlc_telnet/config_flow.py +++ b/homeassistant/components/vlc_telnet/config_flow.py @@ -165,7 +165,6 @@ class VLCTelnetConfigFlow(ConfigFlow, domain=DOMAIN): if user_input is None: return self.async_show_form( step_id="hassio_confirm", - data_schema=vol.Schema({}), description_placeholders={"addon": self.hassio_discovery["addon"]}, ) diff --git a/homeassistant/components/vlc_telnet/media_player.py b/homeassistant/components/vlc_telnet/media_player.py index 140c2b2c253..99b37f97e0c 100644 --- a/homeassistant/components/vlc_telnet/media_player.py +++ b/homeassistant/components/vlc_telnet/media_player.py @@ -31,7 +31,7 @@ from homeassistant.components.media_player.const import ( SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, ) -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import SOURCE_HASSIO, ConfigEntry from homeassistant.const import CONF_NAME, STATE_IDLE, STATE_PAUSED, STATE_PLAYING from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError @@ -60,7 +60,6 @@ SUPPORT_VLC = ( ) _T = TypeVar("_T", bound="VlcDevice") -_R = TypeVar("_R") _P = ParamSpec("_P") @@ -125,6 +124,7 @@ class VlcDevice(MediaPlayerEntity): manufacturer="VideoLAN", name=name, ) + self._using_addon = config_entry.source == SOURCE_HASSIO @catch_vlc_errors async def async_update(self) -> None: @@ -316,7 +316,9 @@ class VlcDevice(MediaPlayerEntity): ) # If media ID is a relative URL, we serve it from HA. - media_id = async_process_play_media_url(self.hass, media_id) + media_id = async_process_play_media_url( + self.hass, media_id, for_supervisor_network=self._using_addon + ) await self._vlc.add(media_id) self._state = STATE_PLAYING diff --git a/homeassistant/components/vlc_telnet/translations/fr.json b/homeassistant/components/vlc_telnet/translations/fr.json index 7dd5fabfdca..d8f6f374f89 100644 --- a/homeassistant/components/vlc_telnet/translations/fr.json +++ b/homeassistant/components/vlc_telnet/translations/fr.json @@ -3,13 +3,13 @@ "abort": { "already_configured": "Le service est d\u00e9j\u00e0 configur\u00e9", "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi", "unknown": "Erreur inattendue" }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "flow_title": "{host}", diff --git a/homeassistant/components/volkszaehler/manifest.json b/homeassistant/components/volkszaehler/manifest.json index 286e18b0b17..5212593da45 100644 --- a/homeassistant/components/volkszaehler/manifest.json +++ b/homeassistant/components/volkszaehler/manifest.json @@ -2,7 +2,7 @@ "domain": "volkszaehler", "name": "Volkszaehler", "documentation": "https://www.home-assistant.io/integrations/volkszaehler", - "requirements": ["volkszaehler==0.2.1"], + "requirements": ["volkszaehler==0.3.2"], "codeowners": ["@fabaff"], "iot_class": "local_polling", "loggers": ["volkszaehler"] diff --git a/homeassistant/components/volkszaehler/sensor.py b/homeassistant/components/volkszaehler/sensor.py index 02a136cc430..8694d8186fe 100644 --- a/homeassistant/components/volkszaehler/sensor.py +++ b/homeassistant/components/volkszaehler/sensor.py @@ -96,9 +96,7 @@ async def async_setup_platform( conditions = config[CONF_MONITORED_CONDITIONS] session = async_get_clientsession(hass) - vz_api = VolkszaehlerData( - Volkszaehler(hass.loop, session, uuid, host=host, port=port) - ) + vz_api = VolkszaehlerData(Volkszaehler(session, uuid, host=host, port=port)) await vz_api.async_update() diff --git a/homeassistant/components/volumio/strings.json b/homeassistant/components/volumio/strings.json index ffa53b2c438..ba283a3af37 100644 --- a/homeassistant/components/volumio/strings.json +++ b/homeassistant/components/volumio/strings.json @@ -21,4 +21,4 @@ "cannot_connect": "Cannot connect to discovered Volumio" } } -} \ No newline at end of file +} diff --git a/homeassistant/components/volumio/translations/zh-Hant.json b/homeassistant/components/volumio/translations/zh-Hant.json index f792fd85465..79edffd79aa 100644 --- a/homeassistant/components/volumio/translations/zh-Hant.json +++ b/homeassistant/components/volumio/translations/zh-Hant.json @@ -11,7 +11,7 @@ "step": { "discovery_confirm": { "description": "\u662f\u5426\u8981\u65b0\u589e Volumio (`{name}`) \u81f3 Home Assistant\uff1f", - "title": "\u5df2\u641c\u7d22\u5230\u7684 Volumio" + "title": "\u5df2\u767c\u73fe\u7684 Volumio" }, "user": { "data": { diff --git a/homeassistant/components/volvooncall/manifest.json b/homeassistant/components/volvooncall/manifest.json index 48caa75a824..93b7642425c 100644 --- a/homeassistant/components/volvooncall/manifest.json +++ b/homeassistant/components/volvooncall/manifest.json @@ -2,7 +2,7 @@ "domain": "volvooncall", "name": "Volvo On Call", "documentation": "https://www.home-assistant.io/integrations/volvooncall", - "requirements": ["volvooncall==0.9.1"], + "requirements": ["volvooncall==0.10.0"], "codeowners": ["@molobrakos", "@decompil3d"], "iot_class": "cloud_polling", "loggers": ["geopy", "hbmqtt", "volvooncall"] diff --git a/homeassistant/components/vulcan/__init__.py b/homeassistant/components/vulcan/__init__.py new file mode 100644 index 00000000000..37210778b1c --- /dev/null +++ b/homeassistant/components/vulcan/__init__.py @@ -0,0 +1,75 @@ +"""The Vulcan component.""" +import logging + +from aiohttp import ClientConnectorError +from vulcan import Account, Keystore, Vulcan +from vulcan._utils import VulcanAPIException + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +PLATFORMS = ["calendar"] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Uonet+ Vulcan integration.""" + hass.data.setdefault(DOMAIN, {}) + try: + keystore = Keystore.load(entry.data["keystore"]) + account = Account.load(entry.data["account"]) + client = Vulcan(keystore, account) + await client.select_student() + students = await client.get_students() + for student in students: + if str(student.pupil.id) == str(entry.data["student_id"]): + client.student = student + break + except VulcanAPIException as err: + if str(err) == "The certificate is not authorized.": + _LOGGER.error( + "The certificate is not authorized, please authorize integration again" + ) + raise ConfigEntryAuthFailed from err + _LOGGER.error("Vulcan API error: %s", err) + return False + except ClientConnectorError as err: + if "connection_error" not in hass.data[DOMAIN]: + _LOGGER.error( + "Connection error - please check your internet connection: %s", err + ) + hass.data[DOMAIN]["connection_error"] = True + await client.close() + raise ConfigEntryNotReady from err + hass.data[DOMAIN]["students_number"] = len( + hass.config_entries.async_entries(DOMAIN) + ) + hass.data[DOMAIN][entry.entry_id] = client + + if not entry.update_listeners: + entry.add_update_listener(_async_update_options) + + for platform in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, platform) + ) + + return True + + +async def async_unload_entry(hass, entry): + """Unload a config entry.""" + await hass.data[DOMAIN][entry.entry_id].close() + for platform in PLATFORMS: + await hass.config_entries.async_forward_entry_unload(entry, platform) + + return True + + +async def _async_update_options(hass, entry): + """Update options.""" + await hass.config_entries.async_reload(entry.entry_id) diff --git a/homeassistant/components/vulcan/calendar.py b/homeassistant/components/vulcan/calendar.py new file mode 100644 index 00000000000..9c853b03197 --- /dev/null +++ b/homeassistant/components/vulcan/calendar.py @@ -0,0 +1,219 @@ +"""Support for Vulcan Calendar platform.""" +import copy +from datetime import date, datetime, timedelta +import logging + +from aiohttp import ClientConnectorError +from vulcan._utils import VulcanAPIException + +from homeassistant.components.calendar import ENTITY_ID_FORMAT, CalendarEventDevice +from homeassistant.const import CONF_SCAN_INTERVAL +from homeassistant.exceptions import ConfigEntryAuthFailed +from homeassistant.helpers.device_registry import DeviceEntryType +from homeassistant.helpers.entity import generate_entity_id +from homeassistant.helpers.template import DATE_STR_FORMAT +from homeassistant.util import Throttle, dt + +from . import DOMAIN +from .const import DEFAULT_SCAN_INTERVAL +from .fetch_data import get_lessons, get_student_info + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the calendar platform for event devices.""" + VulcanCalendarData.MIN_TIME_BETWEEN_UPDATES = timedelta( + minutes=config_entry.options.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) + ) + client = hass.data[DOMAIN][config_entry.entry_id] + data = { + "student_info": await get_student_info( + client, config_entry.data.get("student_id") + ), + "students_number": hass.data[DOMAIN]["students_number"], + } + async_add_entities( + [ + VulcanCalendarEventDevice( + client, + data, + generate_entity_id( + ENTITY_ID_FORMAT, + f"vulcan_calendar_{data['student_info']['full_name']}", + hass=hass, + ), + ) + ], + ) + + +class VulcanCalendarEventDevice(CalendarEventDevice): + """A calendar event device.""" + + def __init__(self, client, data, entity_id): + """Create the Calendar event device.""" + self.student_info = data["student_info"] + self.data = VulcanCalendarData( + client, + self.student_info, + self.hass, + ) + self._event = None + self.entity_id = entity_id + self._unique_id = f"vulcan_calendar_{self.student_info['id']}" + + if data["students_number"] == 1: + self._attr_name = "Vulcan calendar" + self.device_name = "Calendar" + else: + self._attr_name = f"Vulcan calendar - {self.student_info['full_name']}" + self.device_name = f"{self.student_info['full_name']}: Calendar" + self._attr_unique_id = f"vulcan_calendar_{self.student_info['id']}" + self._attr_device_info = { + "identifiers": {(DOMAIN, f"calendar_{self.student_info['id']}")}, + "entry_type": DeviceEntryType.SERVICE, + "name": self.device_name, + "model": f"{self.student_info['full_name']} - {self.student_info['class']} {self.student_info['school']}", + "manufacturer": "Uonet +", + "configuration_url": f"https://uonetplus.vulcan.net.pl/{self.student_info['symbol']}", + } + + @property + def event(self): + """Return the next upcoming event.""" + return self._event + + async def async_get_events(self, hass, start_date, end_date): + """Get all events in a specific time frame.""" + return await self.data.async_get_events(hass, start_date, end_date) + + async def async_update(self): + """Update event data.""" + await self.data.async_update() + event = copy.deepcopy(self.data.event) + if event is None: + self._event = event + return + event["start"] = { + "dateTime": datetime.combine(event["date"], event["time"].from_) + .astimezone(dt.DEFAULT_TIME_ZONE) + .isoformat() + } + event["end"] = { + "dateTime": datetime.combine(event["date"], event["time"].to) + .astimezone(dt.DEFAULT_TIME_ZONE) + .isoformat() + } + self._event = event + + +class VulcanCalendarData: + """Class to utilize calendar service object to get next event.""" + + MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=DEFAULT_SCAN_INTERVAL) + + def __init__(self, client, student_info, hass): + """Set up how we are going to search the Vulcan calendar.""" + self.client = client + self.event = None + self.hass = hass + self.student_info = student_info + self._available = True + + async def async_get_events(self, hass, start_date, end_date): + """Get all events in a specific time frame.""" + try: + events = await get_lessons( + self.client, + date_from=start_date, + date_to=end_date, + ) + except VulcanAPIException as err: + if str(err) == "The certificate is not authorized.": + _LOGGER.error( + "The certificate is not authorized, please authorize integration again" + ) + raise ConfigEntryAuthFailed from err + _LOGGER.error("An API error has occurred: %s", err) + events = [] + except ClientConnectorError as err: + if self._available: + _LOGGER.warning( + "Connection error - please check your internet connection: %s", err + ) + events = [] + + event_list = [] + for item in events: + event = { + "uid": item["id"], + "start": { + "dateTime": datetime.combine( + item["date"], item["time"].from_ + ).strftime(DATE_STR_FORMAT) + }, + "end": { + "dateTime": datetime.combine( + item["date"], item["time"].to + ).strftime(DATE_STR_FORMAT) + }, + "summary": item["lesson"], + "location": item["room"], + "description": item["teacher"], + } + + event_list.append(event) + + return event_list + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + async def async_update(self): + """Get the latest data.""" + + try: + events = await get_lessons(self.client) + + if not self._available: + _LOGGER.info("Restored connection with API") + self._available = True + + if events == []: + events = await get_lessons( + self.client, + date_to=date.today() + timedelta(days=7), + ) + if events == []: + self.event = None + return + except VulcanAPIException as err: + if str(err) == "The certificate is not authorized.": + _LOGGER.error( + "The certificate is not authorized, please authorize integration again" + ) + raise ConfigEntryAuthFailed from err + _LOGGER.error("An API error has occurred: %s", err) + return + except ClientConnectorError as err: + if self._available: + _LOGGER.warning( + "Connection error - please check your internet connection: %s", err + ) + self._available = False + return + + new_event = min( + events, + key=lambda d: ( + datetime.combine(d["date"], d["time"].to) < datetime.now(), + abs(datetime.combine(d["date"], d["time"].to) - datetime.now()), + ), + ) + self.event = { + "uid": new_event["id"], + "date": new_event["date"], + "time": new_event["time"], + "summary": new_event["lesson"], + "location": new_event["room"], + "description": new_event["teacher"], + } diff --git a/homeassistant/components/vulcan/config_flow.py b/homeassistant/components/vulcan/config_flow.py new file mode 100644 index 00000000000..ef700560d73 --- /dev/null +++ b/homeassistant/components/vulcan/config_flow.py @@ -0,0 +1,342 @@ +"""Adds config flow for Vulcan.""" +import logging + +from aiohttp import ClientConnectionError +import voluptuous as vol +from vulcan import Account, Keystore, Vulcan +from vulcan._utils import VulcanAPIException + +from homeassistant import config_entries +from homeassistant.const import CONF_PIN, CONF_REGION, CONF_SCAN_INTERVAL, CONF_TOKEN +from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv + +from . import DOMAIN +from .const import DEFAULT_SCAN_INTERVAL +from .register import register + +_LOGGER = logging.getLogger(__name__) + +LOGIN_SCHEMA = { + vol.Required(CONF_TOKEN): str, + vol.Required(CONF_REGION): str, + vol.Required(CONF_PIN): str, +} + + +class VulcanFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a Uonet+ Vulcan config flow.""" + + VERSION = 1 + + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Get the options flow for this handler.""" + return VulcanOptionsFlowHandler(config_entry) + + async def async_step_user(self, user_input=None): + """Handle config flow.""" + if self._async_current_entries(): + return await self.async_step_add_next_config_entry() + + return await self.async_step_auth() + + async def async_step_auth(self, user_input=None, errors=None): + """Authorize integration.""" + + if user_input is not None: + try: + credentials = await register( + self.hass, + user_input[CONF_TOKEN], + user_input[CONF_REGION], + user_input[CONF_PIN], + ) + except VulcanAPIException as err: + if str(err) == "Invalid token!" or str(err) == "Invalid token.": + errors = {"base": "invalid_token"} + elif str(err) == "Expired token.": + errors = {"base": "expired_token"} + elif str(err) == "Invalid PIN.": + errors = {"base": "invalid_pin"} + else: + errors = {"base": "unknown"} + _LOGGER.error(err) + except RuntimeError as err: + if str(err) == "Internal Server Error (ArgumentException)": + errors = {"base": "invalid_symbol"} + else: + errors = {"base": "unknown"} + _LOGGER.error(err) + except ClientConnectionError as err: + errors = {"base": "cannot_connect"} + _LOGGER.error("Connection error: %s", err) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors = {"base": "unknown"} + if not errors: + account = credentials["account"] + keystore = credentials["keystore"] + client = Vulcan(keystore, account) + students = await client.get_students() + await client.close() + + if len(students) > 1: + # pylint:disable=attribute-defined-outside-init + self.account = account + self.keystore = keystore + self.students = students + return await self.async_step_select_student() + student = students[0] + await self.async_set_unique_id(str(student.pupil.id)) + self._abort_if_unique_id_configured() + return self.async_create_entry( + title=f"{student.pupil.first_name} {student.pupil.last_name}", + data={ + "student_id": str(student.pupil.id), + "keystore": keystore.as_dict, + "account": account.as_dict, + }, + ) + + return self.async_show_form( + step_id="auth", + data_schema=vol.Schema(LOGIN_SCHEMA), + errors=errors, + ) + + async def async_step_select_student(self, user_input=None): + """Allow user to select student.""" + errors = {} + students_list = {} + if self.students is not None: + for student in self.students: + students_list[ + str(student.pupil.id) + ] = f"{student.pupil.first_name} {student.pupil.last_name}" + if user_input is not None: + student_id = user_input["student"] + await self.async_set_unique_id(str(student_id)) + self._abort_if_unique_id_configured() + return self.async_create_entry( + title=students_list[student_id], + data={ + "student_id": str(student_id), + "keystore": self.keystore.as_dict, + "account": self.account.as_dict, + }, + ) + + data_schema = { + vol.Required( + "student", + ): vol.In(students_list), + } + return self.async_show_form( + step_id="select_student", + data_schema=vol.Schema(data_schema), + errors=errors, + ) + + async def async_step_select_saved_credentials(self, user_input=None, errors=None): + """Allow user to select saved credentials.""" + credentials_list = {} + for entry in self.hass.config_entries.async_entries(DOMAIN): + credentials_list[entry.entry_id] = entry.data["account"]["UserName"] + + if user_input is not None: + entry = self.hass.config_entries.async_get_entry(user_input["credentials"]) + keystore = Keystore.load(entry.data["keystore"]) + account = Account.load(entry.data["account"]) + client = Vulcan(keystore, account) + try: + students = await client.get_students() + except VulcanAPIException as err: + if str(err) == "The certificate is not authorized.": + return await self.async_step_auth( + errors={"base": "expired_credentials"} + ) + _LOGGER.error(err) + return await self.async_step_auth(errors={"base": "unknown"}) + except ClientConnectionError as err: + _LOGGER.error("Connection error: %s", err) + return await self.async_step_select_saved_credentials( + errors={"base": "cannot_connect"} + ) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + return await self.async_step_auth(errors={"base": "unknown"}) + finally: + await client.close() + if len(students) == 1: + student = students[0] + await self.async_set_unique_id(str(student.pupil.id)) + self._abort_if_unique_id_configured() + return self.async_create_entry( + title=f"{student.pupil.first_name} {student.pupil.last_name}", + data={ + "student_id": str(student.pupil.id), + "keystore": keystore.as_dict, + "account": account.as_dict, + }, + ) + # pylint:disable=attribute-defined-outside-init + self.account = account + self.keystore = keystore + self.students = students + return await self.async_step_select_student() + + data_schema = { + vol.Required( + "credentials", + ): vol.In(credentials_list), + } + return self.async_show_form( + step_id="select_saved_credentials", + data_schema=vol.Schema(data_schema), + errors=errors, + ) + + async def async_step_add_next_config_entry(self, user_input=None): + """Flow initialized when user is adding next entry of that integration.""" + existing_entries = [] + for entry in self.hass.config_entries.async_entries(DOMAIN): + existing_entries.append(entry) + + errors = {} + if user_input is not None: + if user_input["use_saved_credentials"]: + if len(existing_entries) == 1: + keystore = Keystore.load(existing_entries[0].data["keystore"]) + account = Account.load(existing_entries[0].data["account"]) + client = Vulcan(keystore, account) + students = await client.get_students() + await client.close() + new_students = [] + existing_entry_ids = [] + for entry in self.hass.config_entries.async_entries(DOMAIN): + existing_entry_ids.append(entry.data["student_id"]) + for student in students: + if str(student.pupil.id) not in existing_entry_ids: + new_students.append(student) + if not new_students: + return self.async_abort(reason="all_student_already_configured") + if len(new_students) == 1: + await self.async_set_unique_id(str(new_students[0].pupil.id)) + self._abort_if_unique_id_configured() + return self.async_create_entry( + title=f"{new_students[0].pupil.first_name} {new_students[0].pupil.last_name}", + data={ + "student_id": str(new_students[0].pupil.id), + "keystore": keystore.as_dict, + "account": account.as_dict, + }, + ) + # pylint:disable=attribute-defined-outside-init + self.account = account + self.keystore = keystore + self.students = new_students + return await self.async_step_select_student() + return await self.async_step_select_saved_credentials() + return await self.async_step_auth() + + data_schema = { + vol.Required("use_saved_credentials", default=True): bool, + } + return self.async_show_form( + step_id="add_next_config_entry", + data_schema=vol.Schema(data_schema), + errors=errors, + ) + + async def async_step_reauth(self, user_input=None): + """Reauthorize integration.""" + errors = {} + if user_input is not None: + try: + credentials = await register( + self.hass, + user_input[CONF_TOKEN], + user_input[CONF_REGION], + user_input[CONF_PIN], + ) + except VulcanAPIException as err: + if str(err) == "Invalid token!" or str(err) == "Invalid token.": + errors["base"] = "invalid_token" + elif str(err) == "Expired token.": + errors["base"] = "expired_token" + elif str(err) == "Invalid PIN.": + errors["base"] = "invalid_pin" + else: + errors["base"] = "unknown" + _LOGGER.error(err) + except RuntimeError as err: + if str(err) == "Internal Server Error (ArgumentException)": + errors["base"] = "invalid_symbol" + else: + errors["base"] = "unknown" + _LOGGER.error(err) + except ClientConnectionError as err: + errors["base"] = "cannot_connect" + _LOGGER.error("Connection error: %s", err) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + if not errors: + account = credentials["account"] + keystore = credentials["keystore"] + client = Vulcan(keystore, account) + students = await client.get_students() + await client.close() + existing_entries = [] + for entry in self.hass.config_entries.async_entries(DOMAIN): + existing_entries.append(entry) + for student in students: + for entry in existing_entries: + if str(student.pupil.id) == str(entry.data["student_id"]): + self.hass.config_entries.async_update_entry( + entry, + title=f"{student.pupil.first_name} {student.pupil.last_name}", + data={ + "student_id": str(student.pupil.id), + "keystore": keystore.as_dict, + "account": account.as_dict, + }, + ) + await self.hass.config_entries.async_reload(entry.entry_id) + return self.async_abort(reason="reauth_successful") + + return self.async_show_form( + step_id="reauth", + data_schema=vol.Schema(LOGIN_SCHEMA), + errors=errors, + ) + + +class VulcanOptionsFlowHandler(config_entries.OptionsFlow): + """Config flow options for Uonet+ Vulcan.""" + + def __init__(self, config_entry): + """Initialize options flow.""" + self.config_entry = config_entry + + async def async_step_init(self, user_input=None): + """Manage the options.""" + errors = {} + + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + + options = { + vol.Optional( + CONF_SCAN_INTERVAL, + default=self.config_entry.options.get( + CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL + ), + ): cv.positive_int, + } + + return self.async_show_form( + step_id="init", data_schema=vol.Schema(options), errors=errors + ) diff --git a/homeassistant/components/vulcan/const.py b/homeassistant/components/vulcan/const.py new file mode 100644 index 00000000000..938cc4df8cd --- /dev/null +++ b/homeassistant/components/vulcan/const.py @@ -0,0 +1,4 @@ +"""Constants for the Vulcan integration.""" + +DOMAIN = "vulcan" +DEFAULT_SCAN_INTERVAL = 5 diff --git a/homeassistant/components/vulcan/fetch_data.py b/homeassistant/components/vulcan/fetch_data.py new file mode 100644 index 00000000000..04da8d125d7 --- /dev/null +++ b/homeassistant/components/vulcan/fetch_data.py @@ -0,0 +1,97 @@ +"""Support for fetching Vulcan data.""" + + +async def get_lessons(client, date_from=None, date_to=None): + """Support for fetching Vulcan lessons.""" + changes = {} + list_ans = [] + async for lesson in await client.data.get_changed_lessons( + date_from=date_from, date_to=date_to + ): + temp_dict = {} + _id = str(lesson.id) + temp_dict["id"] = lesson.id + temp_dict["number"] = lesson.time.position if lesson.time is not None else None + temp_dict["lesson"] = ( + lesson.subject.name if lesson.subject is not None else None + ) + temp_dict["room"] = lesson.room.code if lesson.room is not None else None + temp_dict["changes"] = lesson.changes + temp_dict["note"] = lesson.note + temp_dict["reason"] = lesson.reason + temp_dict["event"] = lesson.event + temp_dict["group"] = lesson.group + temp_dict["teacher"] = ( + lesson.teacher.display_name if lesson.teacher is not None else None + ) + temp_dict["from_to"] = ( + lesson.time.displayed_time if lesson.time is not None else None + ) + + changes[str(_id)] = temp_dict + + async for lesson in await client.data.get_lessons( + date_from=date_from, date_to=date_to + ): + temp_dict = {} + temp_dict["id"] = lesson.id + temp_dict["number"] = lesson.time.position + temp_dict["time"] = lesson.time + temp_dict["date"] = lesson.date.date + temp_dict["lesson"] = ( + lesson.subject.name if lesson.subject is not None else None + ) + if lesson.room is not None: + temp_dict["room"] = lesson.room.code + else: + temp_dict["room"] = "-" + temp_dict["visible"] = lesson.visible + temp_dict["changes"] = lesson.changes + temp_dict["group"] = lesson.group + temp_dict["reason"] = None + temp_dict["teacher"] = ( + lesson.teacher.display_name if lesson.teacher is not None else None + ) + temp_dict["from_to"] = ( + lesson.time.displayed_time if lesson.time is not None else None + ) + if temp_dict["changes"] is None: + temp_dict["changes"] = "" + elif temp_dict["changes"].type == 1: + temp_dict["lesson"] = f"Lekcja odwołana ({temp_dict['lesson']})" + temp_dict["changes_info"] = f"Lekcja odwołana ({temp_dict['lesson']})" + if str(temp_dict["changes"].id) in changes: + temp_dict["reason"] = changes[str(temp_dict["changes"].id)]["reason"] + elif temp_dict["changes"].type == 2: + temp_dict["lesson"] = f"{temp_dict['lesson']} (Zastępstwo)" + temp_dict["teacher"] = changes[str(temp_dict["changes"].id)]["teacher"] + if str(temp_dict["changes"].id) in changes: + temp_dict["teacher"] = changes[str(temp_dict["changes"].id)]["teacher"] + temp_dict["reason"] = changes[str(temp_dict["changes"].id)]["reason"] + elif temp_dict["changes"].type == 4: + temp_dict["lesson"] = f"Lekcja odwołana ({temp_dict['lesson']})" + if str(temp_dict["changes"].id) in changes: + temp_dict["reason"] = changes[str(temp_dict["changes"].id)]["reason"] + if temp_dict["visible"]: + list_ans.append(temp_dict) + + return list_ans + + +async def get_student_info(client, student_id): + """Support for fetching Student info by student id.""" + student_info = {} + for student in await client.get_students(): + if str(student.pupil.id) == str(student_id): + student_info["first_name"] = student.pupil.first_name + if student.pupil.second_name: + student_info["second_name"] = student.pupil.second_name + student_info["last_name"] = student.pupil.last_name + student_info[ + "full_name" + ] = f"{student.pupil.first_name} {student.pupil.last_name}" + student_info["id"] = student.pupil.id + student_info["class"] = student.class_ + student_info["school"] = student.school.name + student_info["symbol"] = student.symbol + return student_info diff --git a/homeassistant/components/vulcan/manifest.json b/homeassistant/components/vulcan/manifest.json new file mode 100644 index 00000000000..6449f07a3fb --- /dev/null +++ b/homeassistant/components/vulcan/manifest.json @@ -0,0 +1,11 @@ +{ + "domain": "vulcan", + "name": "Uonet+ Vulcan", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/vulcan", + "requirements": ["vulcan-api==2.0.3"], + "dependencies": [], + "codeowners": ["@Antoni-Czaplicki"], + "iot_class": "cloud_polling", + "quality_scale": "platinum" +} diff --git a/homeassistant/components/vulcan/register.py b/homeassistant/components/vulcan/register.py new file mode 100644 index 00000000000..802805d1db8 --- /dev/null +++ b/homeassistant/components/vulcan/register.py @@ -0,0 +1,13 @@ +"""Support for register Vulcan account.""" +from functools import partial + +from vulcan import Account, Keystore + + +async def register(hass, token, symbol, pin): + """Register integration and save credentials.""" + keystore = await hass.async_add_executor_job( + partial(Keystore.create, device_model="Home Assistant") + ) + account = await Account.register(keystore, token, symbol, pin) + return {"account": account, "keystore": keystore} diff --git a/homeassistant/components/vulcan/strings.json b/homeassistant/components/vulcan/strings.json new file mode 100644 index 00000000000..abb3dce7c7f --- /dev/null +++ b/homeassistant/components/vulcan/strings.json @@ -0,0 +1,69 @@ +{ + "config": { + "abort": { + "already_configured": "That student has already been added.", + "all_student_already_configured": "All students have already been added.", + "reauth_successful": "Reauth successful" + }, + "error": { + "unknown": "Unknown error occurred", + "invalid_token": "Invalid token", + "expired_token": "Expired token - please generate a new token", + "invalid_pin": "Invalid pin", + "invalid_symbol": "Invalid symbol", + "expired_credentials": "Expired credentials - please create new on Vulcan mobile app registration page", + "cannot_connect": "Connection error - please check your internet connection" + }, + "step": { + "auth": { + "description": "Login to your Vulcan Account using mobile app registration page.", + "data": { + "token": "Token", + "region": "Symbol", + "pin": "Pin" + } + }, + "reauth": { + "description": "Login to your Vulcan Account using mobile app registration page.", + "data": { + "token": "Token", + "region": "Symbol", + "pin": "Pin" + } + }, + "select_student": { + "description": "Select student, you can add more students by adding integration again.", + "data": { + "student_name": "Select student" + } + }, + "select_saved_credentials": { + "description": "Select saved credentials.", + "data": { + "credentials": "Login" + } + }, + "add_next_config_entry": { + "description": "Add another student.", + "data": { + "use_saved_credentials": "Use saved credentials" + } + } + } + }, + "options": { + "error": { + "error": "Error occurred" + }, + "step": { + "init": { + "data": { + "message_notify": "Show notifications when new message received", + "attendance_notify": "Show notifications about the latest attendance entries", + "grade_notify": "Show notifications about the latest grades", + "scan_interval": "Update interval (in minutes)" + } + } + } + } +} diff --git a/homeassistant/components/vulcan/translations/en.json b/homeassistant/components/vulcan/translations/en.json new file mode 100644 index 00000000000..abb3dce7c7f --- /dev/null +++ b/homeassistant/components/vulcan/translations/en.json @@ -0,0 +1,69 @@ +{ + "config": { + "abort": { + "already_configured": "That student has already been added.", + "all_student_already_configured": "All students have already been added.", + "reauth_successful": "Reauth successful" + }, + "error": { + "unknown": "Unknown error occurred", + "invalid_token": "Invalid token", + "expired_token": "Expired token - please generate a new token", + "invalid_pin": "Invalid pin", + "invalid_symbol": "Invalid symbol", + "expired_credentials": "Expired credentials - please create new on Vulcan mobile app registration page", + "cannot_connect": "Connection error - please check your internet connection" + }, + "step": { + "auth": { + "description": "Login to your Vulcan Account using mobile app registration page.", + "data": { + "token": "Token", + "region": "Symbol", + "pin": "Pin" + } + }, + "reauth": { + "description": "Login to your Vulcan Account using mobile app registration page.", + "data": { + "token": "Token", + "region": "Symbol", + "pin": "Pin" + } + }, + "select_student": { + "description": "Select student, you can add more students by adding integration again.", + "data": { + "student_name": "Select student" + } + }, + "select_saved_credentials": { + "description": "Select saved credentials.", + "data": { + "credentials": "Login" + } + }, + "add_next_config_entry": { + "description": "Add another student.", + "data": { + "use_saved_credentials": "Use saved credentials" + } + } + } + }, + "options": { + "error": { + "error": "Error occurred" + }, + "step": { + "init": { + "data": { + "message_notify": "Show notifications when new message received", + "attendance_notify": "Show notifications about the latest attendance entries", + "grade_notify": "Show notifications about the latest grades", + "scan_interval": "Update interval (in minutes)" + } + } + } + } +} diff --git a/homeassistant/components/wallbox/__init__.py b/homeassistant/components/wallbox/__init__.py index f1ce91a5bdf..f9510421ba0 100644 --- a/homeassistant/components/wallbox/__init__.py +++ b/homeassistant/components/wallbox/__init__.py @@ -22,20 +22,56 @@ from ...helpers.entity import DeviceInfo from .const import ( CONF_CURRENT_VERSION_KEY, CONF_DATA_KEY, + CONF_LOCKED_UNLOCKED_KEY, CONF_MAX_CHARGING_CURRENT_KEY, CONF_NAME_KEY, CONF_PART_NUMBER_KEY, CONF_SERIAL_NUMBER_KEY, CONF_SOFTWARE_KEY, CONF_STATION, + CONF_STATUS_DESCRIPTION_KEY, + CONF_STATUS_ID_KEY, DOMAIN, ) _LOGGER = logging.getLogger(__name__) -PLATFORMS = [Platform.SENSOR, Platform.NUMBER] +PLATFORMS = [Platform.SENSOR, Platform.NUMBER, Platform.LOCK] UPDATE_INTERVAL = 30 +# Translation of StatusId based on Wallbox portal code: +# https://my.wallbox.com/src/utilities/charger/chargerStatuses.js +CHARGER_STATUS: dict[int, str] = { + 0: "Disconnected", + 14: "Error", + 15: "Error", + 161: "Ready", + 162: "Ready", + 163: "Disconnected", + 164: "Waiting", + 165: "Locked", + 166: "Updating", + 177: "Scheduled", + 178: "Paused", + 179: "Scheduled", + 180: "Waiting for car demand", + 181: "Waiting for car demand", + 182: "Paused", + 183: "Waiting in queue by Power Sharing", + 184: "Waiting in queue by Power Sharing", + 185: "Waiting in queue by Power Boost", + 186: "Waiting in queue by Power Boost", + 187: "Waiting MID failed", + 188: "Waiting MID safety margin exceeded", + 189: "Waiting in queue by Eco-Smart", + 193: "Charging", + 194: "Charging", + 195: "Charging", + 196: "Discharging", + 209: "Locked", + 210: "Locked", +} + class WallboxCoordinator(DataUpdateCoordinator[dict[str, Any]]): """Wallbox Coordinator class.""" @@ -70,6 +106,10 @@ class WallboxCoordinator(DataUpdateCoordinator[dict[str, Any]]): raise InvalidAuth from wallbox_connection_error raise ConnectionError from wallbox_connection_error + async def async_validate_input(self) -> None: + """Get new sensor data for Wallbox component.""" + await self.hass.async_add_executor_job(self._validate) + def _get_data(self) -> dict[str, Any]: """Get new sensor data for Wallbox component.""" try: @@ -78,12 +118,22 @@ class WallboxCoordinator(DataUpdateCoordinator[dict[str, Any]]): data[CONF_MAX_CHARGING_CURRENT_KEY] = data[CONF_DATA_KEY][ CONF_MAX_CHARGING_CURRENT_KEY ] + data[CONF_LOCKED_UNLOCKED_KEY] = data[CONF_DATA_KEY][ + CONF_LOCKED_UNLOCKED_KEY + ] + data[CONF_STATUS_DESCRIPTION_KEY] = CHARGER_STATUS.get( + data[CONF_STATUS_ID_KEY], "Unknown" + ) return data except requests.exceptions.HTTPError as wallbox_connection_error: raise ConnectionError from wallbox_connection_error + async def _async_update_data(self) -> dict[str, Any]: + """Get new sensor data for Wallbox component.""" + return await self.hass.async_add_executor_job(self._get_data) + def _set_charging_current(self, charging_current: float) -> None: """Set maximum charging current for Wallbox.""" try: @@ -101,14 +151,23 @@ class WallboxCoordinator(DataUpdateCoordinator[dict[str, Any]]): ) await self.async_request_refresh() - async def _async_update_data(self) -> dict[str, Any]: - """Get new sensor data for Wallbox component.""" - data = await self.hass.async_add_executor_job(self._get_data) - return data + def _set_lock_unlock(self, lock: bool) -> None: + """Set wallbox to locked or unlocked.""" + try: + self._authenticate() + if lock: + self._wallbox.lockCharger(self._station) + else: + self._wallbox.unlockCharger(self._station) + 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 - async def async_validate_input(self) -> None: - """Get new sensor data for Wallbox component.""" - await self.hass.async_add_executor_job(self._validate) + async def async_set_lock_unlock(self, lock: bool) -> None: + """Set wallbox to locked or unlocked.""" + await self.hass.async_add_executor_job(self._set_lock_unlock, lock) + await self.async_request_refresh() async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @@ -148,11 +207,9 @@ class InvalidAuth(HomeAssistantError): """Error to indicate there is invalid auth.""" -class WallboxEntity(CoordinatorEntity): +class WallboxEntity(CoordinatorEntity[WallboxCoordinator]): """Defines a base Wallbox entity.""" - coordinator: WallboxCoordinator - @property def device_info(self) -> DeviceInfo: """Return device information about this Wallbox device.""" diff --git a/homeassistant/components/wallbox/const.py b/homeassistant/components/wallbox/const.py index 263df7b4924..e4b33f6bc6b 100644 --- a/homeassistant/components/wallbox/const.py +++ b/homeassistant/components/wallbox/const.py @@ -18,7 +18,9 @@ CONF_PART_NUMBER_KEY = "part_number" CONF_SOFTWARE_KEY = "software" CONF_MAX_AVAILABLE_POWER_KEY = "max_available_power" CONF_MAX_CHARGING_CURRENT_KEY = "max_charging_current" +CONF_LOCKED_UNLOCKED_KEY = "locked" CONF_NAME_KEY = "name" CONF_STATE_OF_CHARGE_KEY = "state_of_charge" +CONF_STATUS_ID_KEY = "status_id" CONF_STATUS_DESCRIPTION_KEY = "status_description" CONF_CONNECTIONS = "connections" diff --git a/homeassistant/components/wallbox/lock.py b/homeassistant/components/wallbox/lock.py new file mode 100644 index 00000000000..1d12f086abe --- /dev/null +++ b/homeassistant/components/wallbox/lock.py @@ -0,0 +1,76 @@ +"""Home Assistant component for accessing the Wallbox Portal API. The lock component creates a lock entity.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.components.lock import LockEntity, LockEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import InvalidAuth, WallboxCoordinator, WallboxEntity +from .const import ( + CONF_DATA_KEY, + CONF_LOCKED_UNLOCKED_KEY, + CONF_SERIAL_NUMBER_KEY, + DOMAIN, +) + +LOCK_TYPES: dict[str, LockEntityDescription] = { + CONF_LOCKED_UNLOCKED_KEY: LockEntityDescription( + key=CONF_LOCKED_UNLOCKED_KEY, + name="Locked/Unlocked", + ), +} + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Create wallbox lock entities in HASS.""" + coordinator: WallboxCoordinator = hass.data[DOMAIN][entry.entry_id] + # Check if the user is authorized to lock, if so, add lock component + try: + await coordinator.async_set_lock_unlock( + coordinator.data[CONF_LOCKED_UNLOCKED_KEY] + ) + except InvalidAuth: + return + + async_add_entities( + [ + WallboxLock(coordinator, entry, description) + for ent in coordinator.data + if (description := LOCK_TYPES.get(ent)) + ] + ) + + +class WallboxLock(WallboxEntity, LockEntity): + """Representation of a wallbox lock.""" + + def __init__( + self, + coordinator: WallboxCoordinator, + entry: ConfigEntry, + description: LockEntityDescription, + ) -> None: + """Initialize a Wallbox lock.""" + + super().__init__(coordinator) + self.entity_description = description + self._attr_name = f"{entry.title} {description.name}" + self._attr_unique_id = f"{description.key}-{coordinator.data[CONF_DATA_KEY][CONF_SERIAL_NUMBER_KEY]}" + + @property + def is_locked(self) -> bool: + """Return the status of the lock.""" + return self.coordinator.data[CONF_LOCKED_UNLOCKED_KEY] # type: ignore[no-any-return] + + async def async_lock(self, **kwargs: Any) -> None: + """Lock charger.""" + await self.coordinator.async_set_lock_unlock(True) + + async def async_unlock(self, **kwargs: Any) -> None: + """Unlock charger.""" + await self.coordinator.async_set_lock_unlock(False) diff --git a/homeassistant/components/wallbox/number.py b/homeassistant/components/wallbox/number.py index 2bea3b1ef70..5822e7d45d9 100644 --- a/homeassistant/components/wallbox/number.py +++ b/homeassistant/components/wallbox/number.py @@ -59,7 +59,6 @@ class WallboxNumber(WallboxEntity, NumberEntity): """Representation of the Wallbox portal.""" entity_description: WallboxNumberEntityDescription - coordinator: WallboxCoordinator def __init__( self, diff --git a/homeassistant/components/wallbox/sensor.py b/homeassistant/components/wallbox/sensor.py index d19ea7347ca..9a7fd1a21a3 100644 --- a/homeassistant/components/wallbox/sensor.py +++ b/homeassistant/components/wallbox/sensor.py @@ -152,7 +152,6 @@ class WallboxSensor(WallboxEntity, SensorEntity): """Representation of the Wallbox portal.""" entity_description: WallboxSensorEntityDescription - coordinator: WallboxCoordinator def __init__( self, @@ -170,14 +169,8 @@ class WallboxSensor(WallboxEntity, SensorEntity): def native_value(self) -> StateType: """Return the state of the sensor.""" if (sensor_round := self.entity_description.precision) is not None: - try: - return cast( - StateType, - round( - self.coordinator.data[self.entity_description.key], sensor_round - ), - ) - except TypeError: - _LOGGER.debug("Cannot format %s", self._attr_name) - return None + return cast( + StateType, + round(self.coordinator.data[self.entity_description.key], sensor_round), + ) return cast(StateType, self.coordinator.data[self.entity_description.key]) diff --git a/homeassistant/components/wallbox/translations/fr.json b/homeassistant/components/wallbox/translations/fr.json index dc7972fd01d..b9499f72b15 100644 --- a/homeassistant/components/wallbox/translations/fr.json +++ b/homeassistant/components/wallbox/translations/fr.json @@ -6,7 +6,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "reauth_invalid": "\u00c9chec de la r\u00e9authentification\u00a0; Le num\u00e9ro de s\u00e9rie ne correspond pas \u00e0 l'original", "unknown": "Erreur inattendue" }, diff --git a/homeassistant/components/water_heater/manifest.json b/homeassistant/components/water_heater/manifest.json index ab12a8ab820..ac00bc64210 100644 --- a/homeassistant/components/water_heater/manifest.json +++ b/homeassistant/components/water_heater/manifest.json @@ -2,6 +2,6 @@ "domain": "water_heater", "name": "Water Heater", "documentation": "https://www.home-assistant.io/integrations/water_heater", - "codeowners": [], + "codeowners": ["@home-assistant/core"], "quality_scale": "internal" } diff --git a/homeassistant/components/water_heater/recorder.py b/homeassistant/components/water_heater/recorder.py new file mode 100644 index 00000000000..d76b96936fa --- /dev/null +++ b/homeassistant/components/water_heater/recorder.py @@ -0,0 +1,12 @@ +"""Integration platform for recorder.""" +from __future__ import annotations + +from homeassistant.core import HomeAssistant, callback + +from . import ATTR_MAX_TEMP, ATTR_MIN_TEMP, ATTR_OPERATION_LIST + + +@callback +def exclude_attributes(hass: HomeAssistant) -> set[str]: + """Exclude static attributes from being recorded in the database.""" + return {ATTR_OPERATION_LIST, ATTR_MIN_TEMP, ATTR_MAX_TEMP} diff --git a/homeassistant/components/water_heater/strings.json b/homeassistant/components/water_heater/strings.json index 71fc1dc9328..3d9ab67eab4 100644 --- a/homeassistant/components/water_heater/strings.json +++ b/homeassistant/components/water_heater/strings.json @@ -16,4 +16,4 @@ "performance": "Performance" } } -} \ No newline at end of file +} diff --git a/homeassistant/components/water_heater/translations/fr.json b/homeassistant/components/water_heater/translations/fr.json index 84c9b730b22..689647e02ba 100644 --- a/homeassistant/components/water_heater/translations/fr.json +++ b/homeassistant/components/water_heater/translations/fr.json @@ -12,7 +12,7 @@ "gas": "Gaz", "heat_pump": "Pompe \u00e0 chaleur", "high_demand": "Demande \u00e9lev\u00e9e", - "off": "Inactif", + "off": "D\u00e9sactiv\u00e9", "performance": "Performance" } } diff --git a/homeassistant/components/waterfurnace/sensor.py b/homeassistant/components/waterfurnace/sensor.py index 15f3f64c9ba..8418992ac5d 100644 --- a/homeassistant/components/waterfurnace/sensor.py +++ b/homeassistant/components/waterfurnace/sensor.py @@ -8,6 +8,7 @@ from homeassistant.components.sensor import ( ) from homeassistant.const import PERCENTAGE, POWER_WATT, TEMP_FAHRENHEIT from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util import slugify @@ -138,8 +139,8 @@ class WaterFurnaceSensor(SensorEntity): 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 + async_dispatcher_connect( + self.hass, UPDATE_TOPIC, self.async_update_callback ) ) diff --git a/homeassistant/components/watttime/manifest.json b/homeassistant/components/watttime/manifest.json index 95c53624069..1f233b5e105 100644 --- a/homeassistant/components/watttime/manifest.json +++ b/homeassistant/components/watttime/manifest.json @@ -3,12 +3,8 @@ "name": "WattTime", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/watttime", - "requirements": [ - "aiowatttime==0.1.1" - ], - "codeowners": [ - "@bachya" - ], + "requirements": ["aiowatttime==0.1.1"], + "codeowners": ["@bachya"], "iot_class": "cloud_polling", "loggers": ["aiowatttime"] } diff --git a/homeassistant/components/watttime/translations/fr.json b/homeassistant/components/watttime/translations/fr.json index 91a6c126817..5a31bb3bc2a 100644 --- a/homeassistant/components/watttime/translations/fr.json +++ b/homeassistant/components/watttime/translations/fr.json @@ -5,7 +5,7 @@ "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" }, "error": { - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue", "unknown_coordinates": "Aucune donn\u00e9e pour la latitude/longitude" }, diff --git a/homeassistant/components/waze_travel_time/strings.json b/homeassistant/components/waze_travel_time/strings.json index 0417962ccbf..9ed2ba8dfee 100644 --- a/homeassistant/components/waze_travel_time/strings.json +++ b/homeassistant/components/waze_travel_time/strings.json @@ -36,4 +36,4 @@ } } } -} \ No newline at end of file +} diff --git a/homeassistant/components/waze_travel_time/translations/zh-Hant.json b/homeassistant/components/waze_travel_time/translations/zh-Hant.json index 5c30067de6c..d08d215b815 100644 --- a/homeassistant/components/waze_travel_time/translations/zh-Hant.json +++ b/homeassistant/components/waze_travel_time/translations/zh-Hant.json @@ -29,7 +29,7 @@ "incl_filter": "\u6240\u9078\u64c7\u8def\u7dda\u63cf\u8ff0\u5305\u542b Substring", "realtime": "\u5373\u6642\u65c5\u7a0b\u6642\u9593\uff1f", "units": "\u55ae\u4f4d", - "vehicle_type": "\u8eca\u8f1b\u985e\u578b" + "vehicle_type": "\u8eca\u8f1b\u985e\u5225" }, "description": "`substring` \u8f38\u5165\u53ef\u4f9b\u5f37\u5236\u6574\u5408\u3001\u65bc\u8a08\u7b97\u65c5\u7a0b\u6642\u9593\u6642\uff0c\u4f7f\u7528\u7279\u5b9a\u8def\u7dda\u6216\u907f\u958b\u4f7f\u7528\u7279\u5b9a\u8def\u7dda\u3002" } diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index 15250059fec..2e0f8912867 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -198,7 +198,7 @@ class WeatherEntity(Entity): return self._attr_precision return ( PRECISION_TENTHS - if self.temperature_unit == TEMP_CELSIUS + if self.hass.config.units.temperature_unit == TEMP_CELSIUS else PRECISION_WHOLE ) diff --git a/homeassistant/components/weather/recorder.py b/homeassistant/components/weather/recorder.py new file mode 100644 index 00000000000..1c887ea5202 --- /dev/null +++ b/homeassistant/components/weather/recorder.py @@ -0,0 +1,12 @@ +"""Integration platform for recorder.""" +from __future__ import annotations + +from homeassistant.core import HomeAssistant, callback + +from . import ATTR_FORECAST + + +@callback +def exclude_attributes(hass: HomeAssistant) -> set[str]: + """Exclude (often large) forecasts from being recorded in the database.""" + return {ATTR_FORECAST} diff --git a/homeassistant/components/weather/translations/el.json b/homeassistant/components/weather/translations/el.json index 9127056dc1d..0a3a1f013cf 100644 --- a/homeassistant/components/weather/translations/el.json +++ b/homeassistant/components/weather/translations/el.json @@ -3,19 +3,19 @@ "_": { "clear-night": "\u039e\u03b1\u03c3\u03c4\u03b5\u03c1\u03b9\u03ac, \u03bd\u03cd\u03c7\u03c4\u03b1", "cloudy": "\u039d\u03b5\u03c6\u03b5\u03bb\u03ce\u03b4\u03b7\u03c2", - "exceptional": "\u0395\u03be\u03b1\u03b9\u03c1\u03b5\u03c4\u03b9\u03ba\u03cc", + "exceptional": "\u0395\u03be\u03b1\u03b9\u03c1\u03b5\u03c4\u03b9\u03ba\u03cc\u03c2", "fog": "\u039f\u03bc\u03af\u03c7\u03bb\u03b7", "hail": "\u03a7\u03b1\u03bb\u03ac\u03b6\u03b9", - "lightning": "\u0391\u03c3\u03c4\u03c1\u03b1\u03c0\u03ae", - "lightning-rainy": "\u039a\u03b1\u03c4\u03b1\u03b9\u03b3\u03af\u03b4\u03b1, \u03b2\u03c1\u03bf\u03c7\u03b5\u03c1\u03cc", + "lightning": "\u0391\u03c3\u03c4\u03c1\u03b1\u03c0\u03ad\u03c2", + "lightning-rainy": "\u039a\u03b5\u03c1\u03b1\u03c5\u03bd\u03cc\u03c2, \u03b2\u03c1\u03bf\u03c7\u03ae", "partlycloudy": "\u039c\u03b5\u03c1\u03b9\u03ba\u03ce\u03c2 \u03bd\u03b5\u03c6\u03b5\u03bb\u03ce\u03b4\u03b7\u03c2", "pouring": "\u03a8\u03b9\u03c7\u03b1\u03bb\u03af\u03b6\u03b5\u03b9", - "rainy": "\u0392\u03c1\u03bf\u03c7\u03b5\u03c1\u03ae", - "snowy": "\u03a7\u03b9\u03bf\u03bd\u03ce\u03b4\u03b7\u03c2", - "snowy-rainy": "\u03a7\u03b9\u03bf\u03bd\u03b9\u03c3\u03bc\u03ad\u03bd\u03bf, \u03b2\u03c1\u03bf\u03c7\u03b5\u03c1\u03cc", - "sunny": "\u0397\u03bb\u03b9\u03cc\u03bb\u03bf\u03c5\u03c3\u03c4\u03bf", - "windy": "\u0398\u03c5\u03b5\u03bb\u03bb\u03ce\u03b4\u03b5\u03b9\u03c2", - "windy-variant": "\u0398\u03c5\u03b5\u03bb\u03bb\u03ce\u03b4\u03b5\u03b9\u03c2" + "rainy": "\u0392\u03c1\u03bf\u03c7\u03b5\u03c1\u03cc\u03c2", + "snowy": "\u03a7\u03b9\u03bf\u03bd\u03b9\u03c3\u03bc\u03ad\u03bd\u03bf\u03c2", + "snowy-rainy": "\u03a7\u03b9\u03bf\u03bd\u03b9\u03c3\u03bc\u03ad\u03bd\u03bf\u03c2, \u03b2\u03c1\u03bf\u03c7\u03b5\u03c1\u03cc\u03c2", + "sunny": "\u0397\u03bb\u03b9\u03cc\u03bb\u03bf\u03c5\u03c3\u03c4\u03bf\u03c2", + "windy": "\u0398\u03c5\u03b5\u03bb\u03bb\u03ce\u03b4\u03b7\u03c2", + "windy-variant": "\u0398\u03c5\u03b5\u03bb\u03bb\u03ce\u03b4\u03b7\u03c2" } } } \ No newline at end of file diff --git a/homeassistant/components/webhook/manifest.json b/homeassistant/components/webhook/manifest.json index 509563bb4b0..4ca247ed720 100644 --- a/homeassistant/components/webhook/manifest.json +++ b/homeassistant/components/webhook/manifest.json @@ -3,6 +3,6 @@ "name": "Webhook", "documentation": "https://www.home-assistant.io/integrations/webhook", "dependencies": ["http"], - "codeowners": [], + "codeowners": ["@home-assistant/core"], "quality_scale": "internal" } diff --git a/homeassistant/components/webostv/__init__.py b/homeassistant/components/webostv/__init__.py index 9f78dcd0964..e759077ad3e 100644 --- a/homeassistant/components/webostv/__init__.py +++ b/homeassistant/components/webostv/__init__.py @@ -1,33 +1,24 @@ """Support for LG webOS Smart TV.""" from __future__ import annotations -import asyncio from collections.abc import Callable from contextlib import suppress -import json import logging -import os -from pickle import loads from typing import Any from aiowebostv import WebOsClient, WebOsTvPairError -import sqlalchemy as db import voluptuous as vol from homeassistant.components import notify as hass_notify from homeassistant.components.automation import AutomationActionType -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_COMMAND, ATTR_ENTITY_ID, CONF_CLIENT_SECRET, - CONF_CUSTOMIZE, CONF_HOST, - CONF_ICON, CONF_NAME, - CONF_UNIQUE_ID, EVENT_HOMEASSISTANT_STOP, - Platform, ) from homeassistant.core import ( Context, @@ -37,7 +28,7 @@ from homeassistant.core import ( ServiceCall, callback, ) -from homeassistant.helpers import config_validation as cv, discovery, entity_registry +from homeassistant.helpers import config_validation as cv, discovery from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.typing import ConfigType @@ -46,46 +37,17 @@ from .const import ( ATTR_CONFIG_ENTRY_ID, ATTR_PAYLOAD, ATTR_SOUND_OUTPUT, - CONF_ON_ACTION, - CONF_SOURCES, DATA_CONFIG_ENTRY, DATA_HASS_CONFIG, - DEFAULT_NAME, DOMAIN, PLATFORMS, SERVICE_BUTTON, SERVICE_COMMAND, SERVICE_SELECT_SOUND_OUTPUT, - WEBOSTV_CONFIG_FILE, WEBOSTV_EXCEPTIONS, ) -CUSTOMIZE_SCHEMA = vol.Schema( - {vol.Optional(CONF_SOURCES, default=[]): vol.All(cv.ensure_list, [cv.string])} -) - -CONFIG_SCHEMA = vol.Schema( - vol.All( - cv.deprecated(DOMAIN), - { - DOMAIN: vol.All( - cv.ensure_list, - [ - vol.Schema( - { - vol.Optional(CONF_CUSTOMIZE, default={}): CUSTOMIZE_SCHEMA, - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_ON_ACTION): cv.SCRIPT_SCHEMA, - vol.Optional(CONF_ICON): cv.string, - } - ) - ], - ) - }, - ), - extra=vol.ALLOW_EXTRA, -) +CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) CALL_SCHEMA = vol.Schema({vol.Required(ATTR_ENTITY_ID): cv.comp_entity_ids}) @@ -109,113 +71,12 @@ SERVICE_TO_METHOD = { _LOGGER = logging.getLogger(__name__) -def read_client_keys(config_file: str) -> dict[str, str]: - """Read legacy client keys from file.""" - if not os.path.isfile(config_file): - return {} - - # Try to parse the file as being JSON - with open(config_file, encoding="utf8") as json_file: - try: - client_keys = json.load(json_file) - if isinstance(client_keys, dict): - return client_keys - return {} - except (json.JSONDecodeError, UnicodeDecodeError): - pass - - # If the file is not JSON, read it as Sqlite DB - engine = db.create_engine(f"sqlite:///{config_file}") - table = db.Table("unnamed", db.MetaData(), autoload=True, autoload_with=engine) - results = engine.connect().execute(db.select([table])).fetchall() - db_client_keys = {k: loads(v) for k, v in results} - return db_client_keys - - async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the LG WebOS TV platform.""" hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN].setdefault(DATA_CONFIG_ENTRY, {}) hass.data[DOMAIN][DATA_HASS_CONFIG] = config - if DOMAIN not in config: - return True - - config_file = hass.config.path(WEBOSTV_CONFIG_FILE) - if not ( - client_keys := await hass.async_add_executor_job(read_client_keys, config_file) - ): - _LOGGER.debug("No pairing keys, Not importing webOS Smart TV YAML config") - return True - - async def async_migrate_task( - entity_id: str, conf: dict[str, str], key: str - ) -> None: - _LOGGER.debug("Migrating webOS Smart TV entity %s unique_id", entity_id) - client = WebOsClient(conf[CONF_HOST], key) - tries = 0 - while not client.is_connected(): - try: - await client.connect() - except WEBOSTV_EXCEPTIONS: - if tries == 0: - _LOGGER.warning( - "Please make sure webOS TV %s is turned on to complete " - "the migration of configuration.yaml to the UI", - entity_id, - ) - wait_time = 2 ** min(tries, 4) * 5 - tries += 1 - await asyncio.sleep(wait_time) - except WebOsTvPairError: - return - - ent_reg = entity_registry.async_get(hass) - if not ( - new_entity_id := ent_reg.async_get_entity_id( - Platform.MEDIA_PLAYER, DOMAIN, key - ) - ): - _LOGGER.debug( - "Not updating webOSTV Smart TV entity %s unique_id, entity missing", - entity_id, - ) - return - - uuid = client.hello_info["deviceUUID"] - ent_reg.async_update_entity(new_entity_id, new_unique_id=uuid) - await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={ - **conf, - CONF_CLIENT_SECRET: key, - CONF_UNIQUE_ID: uuid, - }, - ) - - ent_reg = entity_registry.async_get(hass) - - tasks = [] - for conf in config[DOMAIN]: - host = conf[CONF_HOST] - if (key := client_keys.get(host)) is None: - _LOGGER.debug( - "Not importing webOS Smart TV host %s without pairing key", host - ) - continue - - if entity_id := ent_reg.async_get_entity_id(Platform.MEDIA_PLAYER, DOMAIN, key): - tasks.append(asyncio.create_task(async_migrate_task(entity_id, conf, key))) - - async def async_tasks_cancel(_event: Event) -> None: - """Cancel config flow import tasks.""" - for task in tasks: - if not task.done(): - task.cancel() - - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_tasks_cancel) - return True diff --git a/homeassistant/components/webostv/config_flow.py b/homeassistant/components/webostv/config_flow.py index 18338e86f1a..85da9250539 100644 --- a/homeassistant/components/webostv/config_flow.py +++ b/homeassistant/components/webostv/config_flow.py @@ -10,13 +10,7 @@ import voluptuous as vol from homeassistant import config_entries, data_entry_flow from homeassistant.components import ssdp -from homeassistant.const import ( - CONF_CLIENT_SECRET, - CONF_CUSTOMIZE, - CONF_HOST, - CONF_NAME, - CONF_UNIQUE_ID, -) +from homeassistant.const import CONF_CLIENT_SECRET, CONF_HOST, CONF_NAME from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import config_validation as cv @@ -55,28 +49,6 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Get the options flow for this handler.""" return OptionsFlowHandler(config_entry) - async def async_step_import(self, import_info: dict[str, Any]) -> FlowResult: - """Set the config entry up from yaml.""" - self._host = import_info[CONF_HOST] - self._name = import_info.get(CONF_NAME) or import_info[CONF_HOST] - await self.async_set_unique_id( - import_info[CONF_UNIQUE_ID], raise_on_progress=False - ) - data = { - CONF_HOST: self._host, - CONF_CLIENT_SECRET: import_info[CONF_CLIENT_SECRET], - } - self._abort_if_unique_id_configured() - - options: dict[str, list[str]] | None = None - if sources := import_info.get(CONF_CUSTOMIZE, {}).get(CONF_SOURCES): - if not isinstance(sources, list): - sources = [s.strip() for s in sources.split(",")] - options = {CONF_SOURCES: sources} - - _LOGGER.debug("WebOS Smart TV host %s imported from YAML config", self._host) - return self.async_create_entry(title=self._name, data=data, options=options) - async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: diff --git a/homeassistant/components/webostv/const.py b/homeassistant/components/webostv/const.py index 9be44d86469..f471ca7340d 100644 --- a/homeassistant/components/webostv/const.py +++ b/homeassistant/components/webostv/const.py @@ -33,5 +33,3 @@ WEBOSTV_EXCEPTIONS = ( asyncio.TimeoutError, asyncio.CancelledError, ) - -WEBOSTV_CONFIG_FILE = "webostv.conf" diff --git a/homeassistant/components/webostv/manifest.json b/homeassistant/components/webostv/manifest.json index a60a12aba30..81c4d04901f 100644 --- a/homeassistant/components/webostv/manifest.json +++ b/homeassistant/components/webostv/manifest.json @@ -3,10 +3,10 @@ "name": "LG webOS Smart TV", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/webostv", - "requirements": ["aiowebostv==0.1.3", "sqlalchemy==1.4.27"], + "requirements": ["aiowebostv==0.2.0"], "codeowners": ["@bendavid", "@thecode"], - "ssdp": [{"st": "urn:lge-com:service:webos-second-screen:1"}], + "ssdp": [{ "st": "urn:lge-com:service:webos-second-screen:1" }], "quality_scale": "platinum", "iot_class": "local_push", "loggers": ["aiowebostv"] -} \ No newline at end of file +} diff --git a/homeassistant/components/webostv/translations/fr.json b/homeassistant/components/webostv/translations/fr.json index bccb1c3aa3c..7621d297a1d 100644 --- a/homeassistant/components/webostv/translations/fr.json +++ b/homeassistant/components/webostv/translations/fr.json @@ -3,16 +3,16 @@ "abort": { "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours", - "error_pairing": "Connect\u00e9 au t\u00e9l\u00e9viseur LG webOS mais non jumel\u00e9" + "error_pairing": "Connect\u00e9 \u00e0 la TV LG webOS mais non appair\u00e9" }, "error": { - "cannot_connect": "Impossible de vous connecter, veuillez allumer votre t\u00e9l\u00e9viseur ou v\u00e9rifier l\u2019adresse IP" + "cannot_connect": "\u00c9chec de la connexion, veuillez allumer votre TV ou v\u00e9rifier l'adresse IP" }, - "flow_title": "LG webOS Smart TV", + "flow_title": "TV connect\u00e9e LG webOS", "step": { "pairing": { "description": "Cliquez sur soumettre et acceptez la demande de jumelage sur votre t\u00e9l\u00e9viseur.\n\n![Image](/static/images/config_webos.png)", - "title": "Appairage webOS TV" + "title": "Appairage de la TV webOS" }, "user": { "data": { @@ -20,7 +20,7 @@ "name": "Nom" }, "description": "Allumez la t\u00e9l\u00e9vision, remplissez les champs suivants, cliquez sur Envoyer", - "title": "Se connecter \u00e0 webOS TV" + "title": "Se connecter \u00e0 la TV webOS" } } }, @@ -40,7 +40,7 @@ "sources": "Liste des sources" }, "description": "S\u00e9lectionnez les sources activ\u00e9es", - "title": "Options pour webOS Smart TV" + "title": "Options pour TV connect\u00e9e webOS" } } } diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index abc37dd2a0a..02121845ad6 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -16,7 +16,7 @@ from homeassistant.const import ( MATCH_ALL, SIGNAL_BOOTSTRAP_INTEGRATONS, ) -from homeassistant.core import Context, Event, HomeAssistant, callback +from homeassistant.core import Context, Event, HomeAssistant, State, callback from homeassistant.exceptions import ( HomeAssistantError, ServiceNotFound, @@ -68,6 +68,7 @@ def async_register_commands( async_reg(hass, handle_test_condition) async_reg(hass, handle_unsubscribe_events) async_reg(hass, handle_validate_config) + async_reg(hass, handle_subscribe_entities) def pong_message(iden: int) -> dict[str, Any]: @@ -213,21 +214,27 @@ async def handle_call_service( connection.send_error(msg["id"], const.ERR_UNKNOWN_ERROR, str(err)) +@callback +def _async_get_allowed_states( + hass: HomeAssistant, connection: ActiveConnection +) -> list[State]: + if connection.user.permissions.access_all_entities("read"): + return hass.states.async_all() + entity_perm = connection.user.permissions.check_entity + return [ + state + for state in hass.states.async_all() + if entity_perm(state.entity_id, "read") + ] + + @callback @decorators.websocket_command({vol.Required("type"): "get_states"}) def handle_get_states( hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] ) -> None: """Handle get states command.""" - if connection.user.permissions.access_all_entities("read"): - states = hass.states.async_all() - else: - entity_perm = connection.user.permissions.check_entity - states = [ - state - for state in hass.states.async_all() - if entity_perm(state.entity_id, "read") - ] + states = _async_get_allowed_states(hass, connection) # JSON serialize here so we can recover if it blows up due to the # state machine containing unserializable data. This command is required @@ -260,6 +267,77 @@ def handle_get_states( connection.send_message(response2) +@callback +@decorators.websocket_command( + { + vol.Required("type"): "subscribe_entities", + vol.Optional("entity_ids"): cv.entity_ids, + } +) +def handle_subscribe_entities( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: + """Handle subscribe entities command.""" + entity_ids = set(msg.get("entity_ids", [])) + + @callback + def forward_entity_changes(event: Event) -> None: + """Forward entity state changed events to websocket.""" + if not connection.user.permissions.check_entity( + event.data["entity_id"], POLICY_READ + ): + return + if entity_ids and event.data["entity_id"] not in entity_ids: + return + + connection.send_message(messages.cached_state_diff_message(msg["id"], event)) + + # We must never await between sending the states and listening for + # state changed events or we will introduce a race condition + # where some states are missed + states = _async_get_allowed_states(hass, connection) + connection.subscriptions[msg["id"]] = hass.bus.async_listen( + "state_changed", forward_entity_changes + ) + connection.send_result(msg["id"]) + data: dict[str, dict[str, dict]] = { + messages.ENTITY_EVENT_ADD: { + state.entity_id: messages.compressed_state_dict_add(state) + for state in states + if not entity_ids or state.entity_id in entity_ids + } + } + + # JSON serialize here so we can recover if it blows up due to the + # state machine containing unserializable data. This command is required + # to succeed for the UI to show. + response = messages.event_message(msg["id"], data) + try: + connection.send_message(const.JSON_DUMP(response)) + return + except (ValueError, TypeError): + connection.logger.error( + "Unable to serialize to JSON. Bad data found at %s", + format_unserializable_data( + find_paths_unserializable_data(response, dump=const.JSON_DUMP) + ), + ) + del response + + add_entities = data[messages.ENTITY_EVENT_ADD] + cannot_serialize: list[str] = [] + for entity_id, state_dict in add_entities.items(): + try: + const.JSON_DUMP(state_dict) + except (ValueError, TypeError): + cannot_serialize.append(entity_id) + + for entity_id in cannot_serialize: + del add_entities[entity_id] + + connection.send_message(const.JSON_DUMP(messages.event_message(msg["id"], data))) + + @decorators.websocket_command({vol.Required("type"): "get_services"}) @decorators.async_response async def handle_get_services( @@ -600,7 +678,7 @@ async def handle_validate_config( continue try: - await validator(hass, schema(msg[key])) # type: ignore + await validator(hass, schema(msg[key])) # type: ignore[operator] except vol.Invalid as err: result[key] = {"valid": False, "error": str(err)} else: diff --git a/homeassistant/components/websocket_api/connection.py b/homeassistant/components/websocket_api/connection.py index 075aed86453..9b53f358b85 100644 --- a/homeassistant/components/websocket_api/connection.py +++ b/homeassistant/components/websocket_api/connection.py @@ -121,6 +121,9 @@ class ActiveConnection: """Handle an exception while processing a handler.""" log_handler = self.logger.error + code = const.ERR_UNKNOWN_ERROR + err_message = None + if isinstance(err, Unauthorized): code = const.ERR_UNAUTHORIZED err_message = "Unauthorized" @@ -131,13 +134,15 @@ class ActiveConnection: code = const.ERR_TIMEOUT err_message = "Timeout" elif isinstance(err, HomeAssistantError): - code = const.ERR_UNKNOWN_ERROR err_message = str(err) - else: - code = const.ERR_UNKNOWN_ERROR + + # This if-check matches all other errors but also matches errors which + # result in an empty message. In that case we will also log the stack + # trace so it can be fixed. + if not err_message: err_message = "Unknown error" log_handler = self.logger.exception - log_handler("Error handling message: %s", err_message) + log_handler("Error handling message: %s (%s)", err_message, code) self.send_message(messages.error_message(msg["id"], code, err_message)) diff --git a/homeassistant/components/websocket_api/messages.py b/homeassistant/components/websocket_api/messages.py index 8cdda3f8fa3..eac40c9510b 100644 --- a/homeassistant/components/websocket_api/messages.py +++ b/homeassistant/components/websocket_api/messages.py @@ -7,7 +7,7 @@ from typing import Any, Final import voluptuous as vol -from homeassistant.core import Event +from homeassistant.core import Event, State from homeassistant.helpers import config_validation as cv from homeassistant.util.json import ( find_paths_unserializable_data, @@ -31,6 +31,19 @@ BASE_COMMAND_MESSAGE_SCHEMA: Final = vol.Schema({vol.Required("id"): cv.positive IDEN_TEMPLATE: Final = "__IDEN__" IDEN_JSON_TEMPLATE: Final = '"__IDEN__"' +COMPRESSED_STATE_STATE = "s" +COMPRESSED_STATE_ATTRIBUTES = "a" +COMPRESSED_STATE_CONTEXT = "c" +COMPRESSED_STATE_LAST_CHANGED = "lc" +COMPRESSED_STATE_LAST_UPDATED = "lu" + +STATE_DIFF_ADDITIONS = "+" +STATE_DIFF_REMOVALS = "-" + +ENTITY_EVENT_ADD = "a" +ENTITY_EVENT_REMOVE = "r" +ENTITY_EVENT_CHANGE = "c" + def result_message(iden: int, result: Any = None) -> dict[str, Any]: """Return a success result message.""" @@ -74,6 +87,108 @@ def _cached_event_message(event: Event) -> str: return message_to_json(event_message(IDEN_TEMPLATE, event)) +def cached_state_diff_message(iden: int, event: Event) -> str: + """Return an event message. + + Serialize to json once per message. + + Since we can have many clients connected that are + all getting many of the same events (mostly state changed) + we can avoid serializing the same data for each connection. + """ + return _cached_state_diff_message(event).replace(IDEN_JSON_TEMPLATE, str(iden), 1) + + +@lru_cache(maxsize=128) +def _cached_state_diff_message(event: Event) -> str: + """Cache and serialize the event to json. + + The IDEN_TEMPLATE is used which will be replaced + with the actual iden in cached_event_message + """ + return message_to_json(event_message(IDEN_TEMPLATE, _state_diff_event(event))) + + +def _state_diff_event(event: Event) -> dict: + """Convert a state_changed event to the minimal version. + + State update example + + { + "a": {entity_id: compressed_state,…} + "c": {entity_id: diff,…} + "r": [entity_id,…] + } + """ + if (event_new_state := event.data["new_state"]) is None: + return {ENTITY_EVENT_REMOVE: [event.data["entity_id"]]} + assert isinstance(event_new_state, State) + if (event_old_state := event.data["old_state"]) is None: + return { + ENTITY_EVENT_ADD: { + event_new_state.entity_id: compressed_state_dict_add(event_new_state) + } + } + assert isinstance(event_old_state, State) + return _state_diff(event_old_state, event_new_state) + + +def _state_diff( + old_state: State, new_state: State +) -> dict[str, dict[str, dict[str, dict[str, str | list[str]]]]]: + """Create a diff dict that can be used to overlay changes.""" + diff: dict = {STATE_DIFF_ADDITIONS: {}} + additions = diff[STATE_DIFF_ADDITIONS] + if old_state.state != new_state.state: + additions[COMPRESSED_STATE_STATE] = new_state.state + if old_state.last_changed != new_state.last_changed: + additions[COMPRESSED_STATE_LAST_CHANGED] = new_state.last_changed.timestamp() + elif old_state.last_updated != new_state.last_updated: + additions[COMPRESSED_STATE_LAST_UPDATED] = new_state.last_updated.timestamp() + if old_state.context.parent_id != new_state.context.parent_id: + additions.setdefault(COMPRESSED_STATE_CONTEXT, {})[ + "parent_id" + ] = new_state.context.parent_id + if old_state.context.user_id != new_state.context.user_id: + additions.setdefault(COMPRESSED_STATE_CONTEXT, {})[ + "user_id" + ] = new_state.context.user_id + if old_state.context.id != new_state.context.id: + if COMPRESSED_STATE_CONTEXT in additions: + additions[COMPRESSED_STATE_CONTEXT]["id"] = new_state.context.id + else: + additions[COMPRESSED_STATE_CONTEXT] = new_state.context.id + old_attributes = old_state.attributes + for key, value in new_state.attributes.items(): + if old_attributes.get(key) != value: + additions.setdefault(COMPRESSED_STATE_ATTRIBUTES, {})[key] = value + if removed := set(old_attributes).difference(new_state.attributes): + diff[STATE_DIFF_REMOVALS] = {COMPRESSED_STATE_ATTRIBUTES: removed} + return {ENTITY_EVENT_CHANGE: {new_state.entity_id: diff}} + + +def compressed_state_dict_add(state: State) -> dict[str, Any]: + """Build a compressed dict of a state for adds. + + Omits the lu (last_updated) if it matches (lc) last_changed. + + Sends c (context) as a string if it only contains an id. + """ + if state.context.parent_id is None and state.context.user_id is None: + context: dict[str, Any] | str = state.context.id + else: + context = state.context.as_dict() + compressed_state: dict[str, Any] = { + COMPRESSED_STATE_STATE: state.state, + COMPRESSED_STATE_ATTRIBUTES: state.attributes, + COMPRESSED_STATE_CONTEXT: context, + COMPRESSED_STATE_LAST_CHANGED: state.last_changed.timestamp(), + } + if state.last_changed != state.last_updated: + compressed_state[COMPRESSED_STATE_LAST_UPDATED] = state.last_updated.timestamp() + return compressed_state + + def message_to_json(message: dict[str, Any]) -> str: """Serialize a websocket message to json.""" try: diff --git a/homeassistant/components/websocket_api/sensor.py b/homeassistant/components/websocket_api/sensor.py index 29cc2f4a44d..9377fcefd92 100644 --- a/homeassistant/components/websocket_api/sensor.py +++ b/homeassistant/components/websocket_api/sensor.py @@ -3,6 +3,7 @@ from __future__ import annotations from homeassistant.components.sensor import SensorEntity from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -35,13 +36,13 @@ class APICount(SensorEntity): async def async_added_to_hass(self) -> None: """Added to hass.""" self.async_on_remove( - self.hass.helpers.dispatcher.async_dispatcher_connect( - SIGNAL_WEBSOCKET_CONNECTED, self._update_count + async_dispatcher_connect( + self.hass, SIGNAL_WEBSOCKET_CONNECTED, self._update_count ) ) self.async_on_remove( - self.hass.helpers.dispatcher.async_dispatcher_connect( - SIGNAL_WEBSOCKET_DISCONNECTED, self._update_count + async_dispatcher_connect( + self.hass, SIGNAL_WEBSOCKET_DISCONNECTED, self._update_count ) ) diff --git a/homeassistant/components/wemo/entity.py b/homeassistant/components/wemo/entity.py index 9884b5b340c..6d94e203932 100644 --- a/homeassistant/components/wemo/entity.py +++ b/homeassistant/components/wemo/entity.py @@ -16,11 +16,9 @@ from .wemo_device import DeviceCoordinator _LOGGER = logging.getLogger(__name__) -class WemoEntity(CoordinatorEntity): +class WemoEntity(CoordinatorEntity[DeviceCoordinator]): """Common methods for Wemo entities.""" - coordinator: DeviceCoordinator # Override CoordinatorEntity.coordinator type. - # Most pyWeMo devices are associated with a single Home Assistant entity. When # that is not the case, name_suffix & unique_id_suffix can be used to provide # names and unique ids for additional Home Assistant entities. diff --git a/homeassistant/components/wemo/fan.py b/homeassistant/components/wemo/fan.py index be78dd1752d..253ff34213e 100644 --- a/homeassistant/components/wemo/fan.py +++ b/homeassistant/components/wemo/fan.py @@ -138,7 +138,6 @@ class WemoHumidifier(WemoBinaryStateEntity, FanEntity): def turn_on( self, - speed: str | None = None, percentage: int | None = None, preset_mode: str | None = None, **kwargs: Any, diff --git a/homeassistant/components/wemo/sensor.py b/homeassistant/components/wemo/sensor.py index eed5c510936..b79ce08a1e0 100644 --- a/homeassistant/components/wemo/sensor.py +++ b/homeassistant/components/wemo/sensor.py @@ -1,5 +1,10 @@ """Support for power sensors in WeMo Insight devices.""" +from __future__ import annotations + import asyncio +from collections.abc import Callable +from dataclasses import dataclass +from typing import cast from homeassistant.components.sensor import ( SensorDeviceClass, @@ -13,13 +18,42 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType -from homeassistant.util import convert from .const import DOMAIN as WEMO_DOMAIN from .entity import WemoEntity from .wemo_device import DeviceCoordinator +@dataclass +class AttributeSensorDescription(SensorEntityDescription): + """SensorEntityDescription for WeMo AttributeSensor entities.""" + + state_conversion: Callable[[StateType], StateType] | None = None + unique_id_suffix: str | None = None + + +ATTRIBUTE_SENSORS = ( + AttributeSensorDescription( + name="Current Power", + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=POWER_WATT, + key="current_power_watts", + unique_id_suffix="currentpower", + state_conversion=lambda state: round(cast(float, state), 2), + ), + AttributeSensorDescription( + name="Today Energy", + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + key="today_kwh", + unique_id_suffix="todaymw", + state_conversion=lambda state: round(cast(float, state), 2), + ), +) + + async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, @@ -30,7 +64,9 @@ async def async_setup_entry( async def _discovered_wemo(coordinator: DeviceCoordinator) -> None: """Handle a discovered Wemo device.""" async_add_entities( - [InsightCurrentPower(coordinator), InsightTodayEnergy(coordinator)] + AttributeSensor(coordinator, description) + for description in ATTRIBUTE_SENSORS + if hasattr(coordinator.wemo, description.key) ) async_dispatcher_connect(hass, f"{WEMO_DOMAIN}.sensor", _discovered_wemo) @@ -43,66 +79,35 @@ async def async_setup_entry( ) -class InsightSensor(WemoEntity, SensorEntity): - """Common base for WeMo Insight power sensors.""" +class AttributeSensor(WemoEntity, SensorEntity): + """Sensor that reads attributes of a wemo device.""" + + entity_description: AttributeSensorDescription + + def __init__( + self, coordinator: DeviceCoordinator, description: AttributeSensorDescription + ) -> None: + """Init AttributeSensor.""" + super().__init__(coordinator) + self.entity_description = description @property - def name_suffix(self) -> str: - """Return the name of the entity if any.""" - assert self.entity_description.name + def name_suffix(self) -> str | None: + """Return the name of the entity.""" return self.entity_description.name @property - def unique_id_suffix(self) -> str: - """Return the id of this entity.""" - return self.entity_description.key + def unique_id_suffix(self) -> str | None: + """Suffix to append to the WeMo device's unique ID.""" + return self.entity_description.unique_id_suffix - @property - def available(self) -> bool: - """Return true if sensor is available.""" - return ( - self.entity_description.key in self.wemo.insight_params - and super().available - ) - - -class InsightCurrentPower(InsightSensor): - """Current instantaineous power consumption.""" - - entity_description = SensorEntityDescription( - key="currentpower", - name="Current Power", - device_class=SensorDeviceClass.POWER, - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=POWER_WATT, - ) + def convert_state(self, value: StateType) -> StateType: + """Convert native state to a value appropriate for the sensor.""" + if (convert := self.entity_description.state_conversion) is None: + return None + return convert(value) @property def native_value(self) -> StateType: - """Return the current power consumption.""" - milliwatts = convert( - self.wemo.insight_params.get(self.entity_description.key), float, 0.0 - ) - assert isinstance(milliwatts, float) - return milliwatts / 1000.0 - - -class InsightTodayEnergy(InsightSensor): - """Energy used today.""" - - entity_description = SensorEntityDescription( - key="todaymw", - name="Today Energy", - device_class=SensorDeviceClass.ENERGY, - state_class=SensorStateClass.TOTAL_INCREASING, - native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - ) - - @property - def native_value(self) -> StateType: - """Return the current energy use today.""" - milliwatt_seconds = convert( - self.wemo.insight_params.get(self.entity_description.key), float, 0.0 - ) - assert isinstance(milliwatt_seconds, float) - return round(milliwatt_seconds / (1000.0 * 1000.0 * 60), 2) + """Return the value of the device attribute.""" + return self.convert_state(getattr(self.wemo, self.entity_description.key)) diff --git a/homeassistant/components/wemo/services.yaml b/homeassistant/components/wemo/services.yaml index e86366b6a5c..58305798cf9 100644 --- a/homeassistant/components/wemo/services.yaml +++ b/homeassistant/components/wemo/services.yaml @@ -15,7 +15,7 @@ set_humidity: min: 0 max: 100 step: 5 - unit_of_measurement: '%' + unit_of_measurement: "%" reset_filter_life: name: Reset filter life diff --git a/homeassistant/components/wemo/switch.py b/homeassistant/components/wemo/switch.py index 8f8e5dcb5e3..d3c6264ab89 100644 --- a/homeassistant/components/wemo/switch.py +++ b/homeassistant/components/wemo/switch.py @@ -111,24 +111,6 @@ class WemoSwitch(WemoBinaryStateEntity, SwitchEntity): uptime.day - 1, uptime.hour, uptime.minute, uptime.second ) - @property - def current_power_w(self) -> float | None: - """Return the current power usage in W.""" - if not isinstance(self.wemo, Insight): - return None - milliwatts = convert(self.wemo.insight_params.get("currentpower"), float, 0.0) - assert isinstance(milliwatts, float) - return milliwatts / 1000.0 - - @property - def today_energy_kwh(self) -> float | None: - """Return the today total energy usage in kWh.""" - if not isinstance(self.wemo, Insight): - return None - milliwatt_seconds = convert(self.wemo.insight_params.get("todaymw"), float, 0.0) - assert isinstance(milliwatt_seconds, float) - return round(milliwatt_seconds / (1000.0 * 1000.0 * 60), 2) - @property def detail_state(self) -> str: """Return the state of the device.""" diff --git a/homeassistant/components/whirlpool/manifest.json b/homeassistant/components/whirlpool/manifest.json index ce5c76c72f0..4c7471e4715 100644 --- a/homeassistant/components/whirlpool/manifest.json +++ b/homeassistant/components/whirlpool/manifest.json @@ -3,12 +3,8 @@ "name": "Whirlpool Sixth Sense", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/whirlpool", - "requirements": [ - "whirlpool-sixth-sense==0.15.1" - ], - "codeowners": [ - "@abmantis" - ], + "requirements": ["whirlpool-sixth-sense==0.15.1"], + "codeowners": ["@abmantis"], "iot_class": "cloud_push", "loggers": ["whirlpool"] } diff --git a/homeassistant/components/whirlpool/strings.json b/homeassistant/components/whirlpool/strings.json index 4925d73e4c4..78a46954183 100644 --- a/homeassistant/components/whirlpool/strings.json +++ b/homeassistant/components/whirlpool/strings.json @@ -14,4 +14,4 @@ "unknown": "[%key:common::config_flow::error::unknown%]" } } -} \ No newline at end of file +} diff --git a/homeassistant/components/whirlpool/translations/fr.json b/homeassistant/components/whirlpool/translations/fr.json index 63e63fd1953..59ffb2aa537 100644 --- a/homeassistant/components/whirlpool/translations/fr.json +++ b/homeassistant/components/whirlpool/translations/fr.json @@ -2,7 +2,7 @@ "config": { "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/whois/config_flow.py b/homeassistant/components/whois/config_flow.py index 93a15f7fa39..640daaa314c 100644 --- a/homeassistant/components/whois/config_flow.py +++ b/homeassistant/components/whois/config_flow.py @@ -13,7 +13,7 @@ from whois.exceptions import ( ) from homeassistant.config_entries import ConfigFlow -from homeassistant.const import CONF_DOMAIN, CONF_NAME +from homeassistant.const import CONF_DOMAIN from homeassistant.data_entry_flow import FlowResult from .const import DOMAIN @@ -69,12 +69,3 @@ class WhoisFlowHandler(ConfigFlow, domain=DOMAIN): ), errors=errors, ) - - async def async_step_import(self, config: dict[str, Any]) -> FlowResult: - """Handle a flow initialized by importing a config.""" - self.imported_name = config[CONF_NAME] - return await self.async_step_user( - user_input={ - CONF_DOMAIN: config[CONF_DOMAIN], - } - ) diff --git a/homeassistant/components/whois/const.py b/homeassistant/components/whois/const.py index 8530d2e558f..3fbbd6ff3ab 100644 --- a/homeassistant/components/whois/const.py +++ b/homeassistant/components/whois/const.py @@ -14,8 +14,6 @@ LOGGER = logging.getLogger(__package__) SCAN_INTERVAL = timedelta(hours=24) -DEFAULT_NAME = "Whois" - ATTR_EXPIRES = "expires" ATTR_NAME_SERVERS = "name_servers" ATTR_REGISTRAR = "registrar" diff --git a/homeassistant/components/whois/sensor.py b/homeassistant/components/whois/sensor.py index 3d0b25640b3..48efaf7630d 100644 --- a/homeassistant/components/whois/sensor.py +++ b/homeassistant/components/whois/sensor.py @@ -6,44 +6,25 @@ from dataclasses import dataclass from datetime import datetime, timezone from typing import cast -import voluptuous as vol from whois import Domain from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, SensorEntityDescription, ) -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import CONF_DOMAIN, CONF_NAME, TIME_DAYS +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_DOMAIN, TIME_DAYS from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo, EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, ) -from .const import ( - ATTR_EXPIRES, - ATTR_NAME_SERVERS, - ATTR_REGISTRAR, - ATTR_UPDATED, - DEFAULT_NAME, - DOMAIN, - LOGGER, -) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_DOMAIN): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - } -) +from .const import ATTR_EXPIRES, ATTR_NAME_SERVERS, ATTR_REGISTRAR, ATTR_UPDATED, DOMAIN @dataclass @@ -152,28 +133,6 @@ SENSORS: tuple[WhoisSensorEntityDescription, ...] = ( ) -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the WHOIS sensor.""" - LOGGER.warning( - "Configuration of the Whois platform in YAML is deprecated and will be " - "removed in Home Assistant 2022.4; Your existing configuration " - "has been imported into the UI automatically and can be safely removed " - "from your configuration.yaml file" - ) - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={CONF_DOMAIN: config[CONF_DOMAIN], CONF_NAME: config[CONF_NAME]}, - ) - ) - - async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, diff --git a/homeassistant/components/whois/translations/fr.json b/homeassistant/components/whois/translations/fr.json index e45ecd615da..457c295bbca 100644 --- a/homeassistant/components/whois/translations/fr.json +++ b/homeassistant/components/whois/translations/fr.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Service d\u00e9j\u00e0 configur\u00e9" + "already_configured": "Le service est d\u00e9j\u00e0 configur\u00e9" }, "error": { "unexpected_response": "R\u00e9ponse inattendue du serveur whois", diff --git a/homeassistant/components/wilight/fan.py b/homeassistant/components/wilight/fan.py index e3f3ab055f9..fbf5b0858a7 100644 --- a/homeassistant/components/wilight/fan.py +++ b/homeassistant/components/wilight/fan.py @@ -108,7 +108,6 @@ class WiLightFan(WiLightDevice, FanEntity): async def async_turn_on( self, - speed: str = None, percentage: int = None, preset_mode: str = None, **kwargs, diff --git a/homeassistant/components/wilight/strings.json b/homeassistant/components/wilight/strings.json index e267a8e5327..0449a900c29 100644 --- a/homeassistant/components/wilight/strings.json +++ b/homeassistant/components/wilight/strings.json @@ -3,8 +3,7 @@ "flow_title": "{name}", "step": { "confirm": { - "title": "WiLight", - "description": "Do you want to set up WiLight {name}?\n\n It supports: {components}" + "description": "The following components are supported: {components}" } }, "abort": { diff --git a/homeassistant/components/wilight/translations/el.json b/homeassistant/components/wilight/translations/el.json index e83a8386341..5bb3130753a 100644 --- a/homeassistant/components/wilight/translations/el.json +++ b/homeassistant/components/wilight/translations/el.json @@ -5,7 +5,7 @@ "not_supported_device": "\u0391\u03c5\u03c4\u03cc \u03c4\u03bf WiLight \u03b4\u03b5\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9 \u03c0\u03c1\u03bf\u03c2 \u03c4\u03bf \u03c0\u03b1\u03c1\u03cc\u03bd", "not_wilight_device": "\u0391\u03c5\u03c4\u03ae \u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 WiLight" }, - "flow_title": "WiLight: {\u03cc\u03bd\u03bf\u03bc\u03b1}", + "flow_title": "{name}", "step": { "confirm": { "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf WiLight {name} ;\n\n \u03a5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03b9: {components}", diff --git a/homeassistant/components/wilight/translations/fr.json b/homeassistant/components/wilight/translations/fr.json index 0fb00748e25..0a3851bb816 100644 --- a/homeassistant/components/wilight/translations/fr.json +++ b/homeassistant/components/wilight/translations/fr.json @@ -2,13 +2,13 @@ "config": { "abort": { "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", - "not_supported_device": "Ce WiLight n'est actuellement pas pris en charge", + "not_supported_device": "Cette WiLight n'est actuellement pas prise en charge", "not_wilight_device": "Cet appareil n'est pas WiLight" }, "flow_title": "{name}", "step": { "confirm": { - "description": "Voulez-vous configurer WiLight {name} ? \n\n Il prend en charge: {components}", + "description": "Voulez-vous configurer la WiLight {name}\u00a0?\n\n Elle prend en charge\u00a0: {components}", "title": "WiLight" } } diff --git a/homeassistant/components/withings/common.py b/homeassistant/components/withings/common.py index 56f7f7cdf91..8682df59e0d 100644 --- a/homeassistant/components/withings/common.py +++ b/homeassistant/components/withings/common.py @@ -494,7 +494,7 @@ class ConfigEntryWithingsApi(AbstractWithingsApi): """Perform an async request.""" asyncio.run_coroutine_threadsafe( self.session.async_ensure_token_valid(), self._hass.loop - ) + ).result() access_token = self._config_entry.data["token"]["access_token"] response = requests.request( @@ -648,7 +648,11 @@ class DataManager: try: return await func() except Exception as exception1: # pylint: disable=broad-except - await asyncio.sleep(0.1) + _LOGGER.debug( + "Failed attempt %s of %s (%s)", attempt, attempts, exception1 + ) + # Make each backoff pause a little bit longer + await asyncio.sleep(0.5 * attempt) exception = exception1 continue diff --git a/homeassistant/components/withings/manifest.json b/homeassistant/components/withings/manifest.json index f15045b98da..8337068ba68 100644 --- a/homeassistant/components/withings/manifest.json +++ b/homeassistant/components/withings/manifest.json @@ -3,18 +3,9 @@ "name": "Withings", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/withings", - "requirements": [ - "withings-api==2.4.0" - ], - "dependencies": [ - "http", - "webhook" - ], - "codeowners": [ - "@vangorra" - ], + "requirements": ["withings-api==2.4.0"], + "dependencies": ["http", "webhook"], + "codeowners": ["@vangorra"], "iot_class": "cloud_polling", - "loggers": [ - "withings_api" - ] -} \ No newline at end of file + "loggers": ["withings_api"] +} diff --git a/homeassistant/components/withings/strings.json b/homeassistant/components/withings/strings.json index 81b0bbb79b5..433888d9ebf 100644 --- a/homeassistant/components/withings/strings.json +++ b/homeassistant/components/withings/strings.json @@ -7,7 +7,9 @@ "description": "Provide a unique profile name for this data. Typically this is the name of the profile you selected in the previous step.", "data": { "profile": "Profile Name" } }, - "pick_implementation": { "title": "[%key:common::config_flow::title::oauth2_pick_implementation%]" }, + "pick_implementation": { + "title": "[%key:common::config_flow::title::oauth2_pick_implementation%]" + }, "reauth": { "title": "[%key:common::config_flow::title::reauth%]", "description": "The \"{profile}\" profile needs to be re-authenticated in order to continue receiving Withings data." diff --git a/homeassistant/components/withings/translations/fr.json b/homeassistant/components/withings/translations/fr.json index a506d491a74..f59b8e2e714 100644 --- a/homeassistant/components/withings/translations/fr.json +++ b/homeassistant/components/withings/translations/fr.json @@ -2,9 +2,9 @@ "config": { "abort": { "already_configured": "Configuration mise \u00e0 jour pour le profil.", - "authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration de l'URL d'authentification d\u00e9pass\u00e9.", + "authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration de l'URL d'authentification expir\u00e9.", "missing_configuration": "Le composant n'est pas configur\u00e9. Veuillez suivre la documentation.", - "no_url_available": "Aucune URL disponible. Pour plus d'informations sur cette erreur, [consultez la section d'aide] ( {docs_url} )" + "no_url_available": "Aucune URL disponible. Pour plus d'informations sur cette erreur, [consultez la section d'aide]({docs_url})" }, "create_entry": { "default": "Authentifi\u00e9 avec succ\u00e8s \u00e0 Withings pour le profil s\u00e9lectionn\u00e9." diff --git a/homeassistant/components/wiz/__init__.py b/homeassistant/components/wiz/__init__.py index d739c571c8b..104ecb6f0c5 100644 --- a/homeassistant/components/wiz/__init__.py +++ b/homeassistant/components/wiz/__init__.py @@ -30,7 +30,13 @@ from .models import WizData _LOGGER = logging.getLogger(__name__) -PLATFORMS = [Platform.BINARY_SENSOR, Platform.LIGHT, Platform.NUMBER, Platform.SWITCH] +PLATFORMS = [ + Platform.BINARY_SENSOR, + Platform.LIGHT, + Platform.NUMBER, + Platform.SENSOR, + Platform.SWITCH, +] REQUEST_REFRESH_DELAY = 0.35 @@ -48,6 +54,11 @@ async def async_setup(hass: HomeAssistant, hass_config: ConfigType) -> bool: return True +async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Handle options update.""" + await hass.config_entries.async_reload(entry.entry_id) + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up the wiz integration from a config entry.""" ip_address = entry.data[CONF_HOST] @@ -117,6 +128,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: coordinator=coordinator, bulb=bulb, scenes=scenes ) hass.config_entries.async_setup_platforms(entry, PLATFORMS) + + entry.async_on_unload(entry.add_update_listener(_async_update_listener)) return True diff --git a/homeassistant/components/wiz/config_flow.py b/homeassistant/components/wiz/config_flow.py index 7d86502784c..04a0884059f 100644 --- a/homeassistant/components/wiz/config_flow.py +++ b/homeassistant/components/wiz/config_flow.py @@ -103,7 +103,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_show_form( step_id="discovery_confirm", description_placeholders=placeholders, - data_schema=vol.Schema({}), ) async def async_step_pick_device( diff --git a/homeassistant/components/wiz/manifest.json b/homeassistant/components/wiz/manifest.json index 26d32589c94..a3537d3cbd9 100644 --- a/homeassistant/components/wiz/manifest.json +++ b/homeassistant/components/wiz/manifest.json @@ -3,12 +3,12 @@ "name": "WiZ", "config_flow": true, "dhcp": [ - {"registered_devices": true}, - {"macaddress":"A8BB50*"}, - {"macaddress":"D8A011*"}, - {"macaddress":"444F8E*"}, - {"macaddress":"6C2990*"}, - {"hostname":"wiz_*"} + { "registered_devices": true }, + { "macaddress": "A8BB50*" }, + { "macaddress": "D8A011*" }, + { "macaddress": "444F8E*" }, + { "macaddress": "6C2990*" }, + { "hostname": "wiz_*" } ], "dependencies": ["network"], "quality_scale": "platinum", diff --git a/homeassistant/components/wiz/sensor.py b/homeassistant/components/wiz/sensor.py new file mode 100644 index 00000000000..d16130883d5 --- /dev/null +++ b/homeassistant/components/wiz/sensor.py @@ -0,0 +1,65 @@ +"""Support for WiZ sensors.""" +from __future__ import annotations + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import SIGNAL_STRENGTH_DECIBELS_MILLIWATT +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .entity import WizEntity +from .models import WizData + +SENSORS: tuple[SensorEntityDescription, ...] = ( + SensorEntityDescription( + key="rssi", + name="Signal Strength", + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the wiz sensor.""" + wiz_data: WizData = hass.data[DOMAIN][entry.entry_id] + async_add_entities( + WizSensor(wiz_data, entry.title, description) for description in SENSORS + ) + + +class WizSensor(WizEntity, SensorEntity): + """Defines a WiZ sensor.""" + + entity_description: SensorEntityDescription + + def __init__( + self, wiz_data: WizData, name: str, description: SensorEntityDescription + ) -> None: + """Initialize an WiZ sensor.""" + super().__init__(wiz_data, name) + self.entity_description = description + self._attr_unique_id = f"{self._device.mac}_{description.key}" + self._attr_name = f"{name} {description.name}" + self._async_update_attrs() + + @callback + def _async_update_attrs(self) -> None: + """Handle updating _attr values.""" + self._attr_native_value = self._device.state.pilotResult.get( + self.entity_description.key + ) diff --git a/homeassistant/components/wiz/strings.json b/homeassistant/components/wiz/strings.json index d9b2a19d752..3640b17195c 100644 --- a/homeassistant/components/wiz/strings.json +++ b/homeassistant/components/wiz/strings.json @@ -30,4 +30,4 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } } -} \ No newline at end of file +} diff --git a/homeassistant/components/wiz/translations/el.json b/homeassistant/components/wiz/translations/el.json index 8b0f37864c5..aa137e84fe0 100644 --- a/homeassistant/components/wiz/translations/el.json +++ b/homeassistant/components/wiz/translations/el.json @@ -6,7 +6,7 @@ "no_devices_found": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03c3\u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03bf" }, "error": { - "bulb_time_out": "\u0394\u03b5\u03bd \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af \u03bc\u03b5 \u03c4\u03bf\u03bd \u03bb\u03b1\u03bc\u03c0\u03c4\u03ae\u03c1\u03b1. \u038a\u03c3\u03c9\u03c2 \u03bf \u03bb\u03b1\u03bc\u03c0\u03c4\u03ae\u03c1\u03b1\u03c2 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03ba\u03c4\u03cc\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03ae \u03ad\u03c7\u03b5\u03b9 \u03b5\u03b9\u03c3\u03b1\u03c7\u03b8\u03b5\u03af \u03bb\u03ac\u03b8\u03bf\u03c2 IP/host. \u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03bf\u03cd\u03bc\u03b5 \u03b1\u03bd\u03ac\u03c8\u03c4\u03b5 \u03c4\u03bf \u03c6\u03c9\u03c2 \u03ba\u03b1\u03b9 \u03b4\u03bf\u03ba\u03b9\u03bc\u03ac\u03c3\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac!", + "bulb_time_out": "\u0394\u03b5\u03bd \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af \u03bc\u03b5 \u03c4\u03bf\u03bd \u03bb\u03b1\u03bc\u03c0\u03c4\u03ae\u03c1\u03b1. \u038a\u03c3\u03c9\u03c2 \u03bf \u03bb\u03b1\u03bc\u03c0\u03c4\u03ae\u03c1\u03b1\u03c2 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03ba\u03c4\u03cc\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03ae \u03ad\u03c7\u03b5\u03b9 \u03b5\u03b9\u03c3\u03b1\u03c7\u03b8\u03b5\u03af \u03bb\u03ac\u03b8\u03bf\u03c2 IP. \u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03bf\u03cd\u03bc\u03b5 \u03b1\u03bd\u03ac\u03c8\u03c4\u03b5 \u03c4\u03bf \u03c6\u03c9\u03c2 \u03ba\u03b1\u03b9 \u03b4\u03bf\u03ba\u03b9\u03bc\u03ac\u03c3\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac!", "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", "no_ip": "\u0394\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP.", "no_wiz_light": "\u039f \u03bb\u03b1\u03bc\u03c0\u03c4\u03ae\u03c1\u03b1\u03c2 \u03b4\u03b5\u03bd \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af \u03bc\u03ad\u03c3\u03c9 \u03c4\u03b7\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2 \u03c4\u03b7\u03c2 \u03c0\u03bb\u03b1\u03c4\u03c6\u03cc\u03c1\u03bc\u03b1\u03c2 WiZ.", @@ -30,7 +30,7 @@ "host": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP", "name": "\u038c\u03bd\u03bf\u03bc\u03b1" }, - "description": "\u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03cc\u03bd\u03bf\u03bc\u03b1 \u03ba\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03bf\u03cd \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae \u03ae \u03bc\u03b9\u03b1 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP \u03ba\u03b1\u03b9 \u03ad\u03bd\u03b1 \u03cc\u03bd\u03bf\u03bc\u03b1 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03c0\u03c1\u03bf\u03c3\u03b8\u03ad\u03c3\u03b5\u03c4\u03b5 \u03ad\u03bd\u03b1\u03bd \u03bd\u03ad\u03bf \u03bb\u03b1\u03bc\u03c0\u03c4\u03ae\u03c1\u03b1:" + "description": "\u0391\u03bd \u03b1\u03c6\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP \u03ba\u03b5\u03bd\u03ae, \u03b8\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b7\u03b8\u03b5\u03af \u03b7 \u03b1\u03bd\u03b1\u03ba\u03ac\u03bb\u03c5\u03c8\u03b7 \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd \u03b5\u03cd\u03c1\u03b5\u03c3\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ce\u03bd." } } } diff --git a/homeassistant/components/wiz/translations/fr.json b/homeassistant/components/wiz/translations/fr.json index e6123a1d715..290bda2405c 100644 --- a/homeassistant/components/wiz/translations/fr.json +++ b/homeassistant/components/wiz/translations/fr.json @@ -2,10 +2,11 @@ "config": { "abort": { "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "cannot_connect": "\u00c9chec de connexion", "no_devices_found": "Aucun appareil trouv\u00e9 sur le r\u00e9seau" }, "error": { - "bulb_time_out": "Impossible de se connecter \u00e0 l'ampoule. Peut-\u00eatre que l'ampoule est hors ligne ou qu'une mauvaise adresse IP a \u00e9t\u00e9 saisie. Veuillez allumer la lumi\u00e8re et r\u00e9essayer\u00a0!", + "bulb_time_out": "Impossible de se connecter \u00e0 l'ampoule. L'ampoule est peut-\u00eatre hors ligne ou bien une mauvaise adresse IP a \u00e9t\u00e9 saisie. Veuillez allumer la lumi\u00e8re et r\u00e9essayer\u00a0!", "cannot_connect": "\u00c9chec de connexion", "no_ip": "Adresse IP non valide", "no_wiz_light": "L'ampoule ne peut pas \u00eatre connect\u00e9e via l'int\u00e9gration de la plate-forme WiZ.", @@ -14,10 +15,10 @@ "flow_title": "{name} ({host})", "step": { "confirm": { - "description": "Voulez-vous commencer la configuration ?" + "description": "Voulez-vous commencer la configuration\u00a0?" }, "discovery_confirm": { - "description": "Voulez-vous configurer {name} ({host}) ?" + "description": "Voulez-vous configurer {name} ({host})\u00a0?" }, "pick_device": { "data": { diff --git a/homeassistant/components/wiz/translations/nl.json b/homeassistant/components/wiz/translations/nl.json index 79fc5c3db1f..3b59dd6a672 100644 --- a/homeassistant/components/wiz/translations/nl.json +++ b/homeassistant/components/wiz/translations/nl.json @@ -6,7 +6,7 @@ "no_devices_found": "Geen apparaten gevonden op het netwerk" }, "error": { - "bulb_time_out": "Kan geen verbinding maken met de lamp. Misschien is de lamp offline of is er een verkeerde IP/host ingevoerd. Doe het licht aan en probeer het opnieuw!", + "bulb_time_out": "Kan geen verbinding maken met de lamp. Misschien is de lamp offline of is er een verkeerde IP-adres ingevoerd. Doe het licht aan en probeer het opnieuw!", "cannot_connect": "Kan geen verbinding maken", "no_ip": "Geen geldig IP-adres.", "no_wiz_light": "De lamp kan niet worden aangesloten via WiZ Platform integratie.", diff --git a/homeassistant/components/wiz/translations/zh-Hant.json b/homeassistant/components/wiz/translations/zh-Hant.json index b677427996f..9e716b4928e 100644 --- a/homeassistant/components/wiz/translations/zh-Hant.json +++ b/homeassistant/components/wiz/translations/zh-Hant.json @@ -30,7 +30,7 @@ "host": "IP \u4f4d\u5740", "name": "\u540d\u7a31" }, - "description": "\u5047\u5982 IP \u4f4d\u5740\u6b04\u4f4d\u70ba\u7a7a\u767d\uff0c\u5c07\u6703\u63a2\u7d22\u6240\u6709\u53ef\u7528\u88dd\u7f6e\u3002" + "description": "\u5047\u5982 IP \u4f4d\u5740\u6b04\u4f4d\u70ba\u7a7a\u767d\uff0c\u5c07\u6703\u641c\u7d22\u6240\u6709\u53ef\u7528\u88dd\u7f6e\u3002" } } } diff --git a/homeassistant/components/wled/__init__.py b/homeassistant/components/wled/__init__.py index 9c32f9becb8..bcfb98b9916 100644 --- a/homeassistant/components/wled/__init__.py +++ b/homeassistant/components/wled/__init__.py @@ -16,6 +16,7 @@ PLATFORMS = ( Platform.SELECT, Platform.SENSOR, Platform.SWITCH, + Platform.UPDATE, ) diff --git a/homeassistant/components/wled/binary_sensor.py b/homeassistant/components/wled/binary_sensor.py index 1a8033701da..d2262798d50 100644 --- a/homeassistant/components/wled/binary_sensor.py +++ b/homeassistant/components/wled/binary_sensor.py @@ -35,6 +35,9 @@ class WLEDUpdateBinarySensor(WLEDEntity, BinarySensorEntity): _attr_entity_category = EntityCategory.DIAGNOSTIC _attr_device_class = BinarySensorDeviceClass.UPDATE + # Disabled by default, as this entity is deprecated. + _attr_entity_registry_enabled_default = False + def __init__(self, coordinator: WLEDDataUpdateCoordinator) -> None: """Initialize the button entity.""" super().__init__(coordinator=coordinator) diff --git a/homeassistant/components/wled/button.py b/homeassistant/components/wled/button.py index 5b2e13911bc..2967067ef44 100644 --- a/homeassistant/components/wled/button.py +++ b/homeassistant/components/wled/button.py @@ -7,7 +7,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN +from .const import DOMAIN, LOGGER from .coordinator import WLEDDataUpdateCoordinator from .helpers import wled_exception_handler from .models import WLEDEntity @@ -52,6 +52,9 @@ class WLEDUpdateButton(WLEDEntity, ButtonEntity): _attr_device_class = ButtonDeviceClass.UPDATE _attr_entity_category = EntityCategory.CONFIG + # Disabled by default, as this entity is deprecated. + _attr_entity_registry_enabled_default = False + def __init__(self, coordinator: WLEDDataUpdateCoordinator) -> None: """Initialize the button entity.""" super().__init__(coordinator=coordinator) @@ -83,6 +86,11 @@ class WLEDUpdateButton(WLEDEntity, ButtonEntity): @wled_exception_handler async def async_press(self) -> None: """Send out a update command.""" + LOGGER.warning( + "The WLED update button '%s' is deprecated, please " + "use the new update entity as a replacement", + self.entity_id, + ) current = self.coordinator.data.info.version beta = self.coordinator.data.info.version_latest_beta stable = self.coordinator.data.info.version_latest_stable diff --git a/homeassistant/components/wled/manifest.json b/homeassistant/components/wled/manifest.json index eb99b8519a9..668950b9326 100644 --- a/homeassistant/components/wled/manifest.json +++ b/homeassistant/components/wled/manifest.json @@ -3,7 +3,7 @@ "name": "WLED", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/wled", - "requirements": ["wled==0.13.0"], + "requirements": ["wled==0.13.2"], "zeroconf": ["_wled._tcp.local."], "codeowners": ["@frenck"], "quality_scale": "platinum", diff --git a/homeassistant/components/wled/models.py b/homeassistant/components/wled/models.py index c6f10daeff4..b5fc0855e04 100644 --- a/homeassistant/components/wled/models.py +++ b/homeassistant/components/wled/models.py @@ -7,11 +7,9 @@ from .const import DOMAIN from .coordinator import WLEDDataUpdateCoordinator -class WLEDEntity(CoordinatorEntity): +class WLEDEntity(CoordinatorEntity[WLEDDataUpdateCoordinator]): """Defines a base WLED entity.""" - coordinator: WLEDDataUpdateCoordinator - @property def device_info(self) -> DeviceInfo: """Return device information about this WLED device.""" diff --git a/homeassistant/components/wled/sensor.py b/homeassistant/components/wled/sensor.py index b37238aef64..720158938c7 100644 --- a/homeassistant/components/wled/sensor.py +++ b/homeassistant/components/wled/sensor.py @@ -44,6 +44,8 @@ class WLEDSensorEntityDescription( ): """Describes WLED sensor entity.""" + exists_fn: Callable[[WLEDDevice], bool] = lambda _: True + SENSORS: tuple[WLEDSensorEntityDescription, ...] = ( WLEDSensorEntityDescription( @@ -54,6 +56,7 @@ SENSORS: tuple[WLEDSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda device: device.info.leds.power, + exists_fn=lambda device: bool(device.info.leds.max_power), ), WLEDSensorEntityDescription( key="info_leds_count", @@ -68,6 +71,7 @@ SENSORS: tuple[WLEDSensorEntityDescription, ...] = ( entity_category=EntityCategory.DIAGNOSTIC, device_class=SensorDeviceClass.CURRENT, value_fn=lambda device: device.info.leds.max_power, + exists_fn=lambda device: bool(device.info.leds.max_power), ), WLEDSensorEntityDescription( key="uptime", @@ -132,7 +136,9 @@ async def async_setup_entry( """Set up WLED sensor based on a config entry.""" coordinator: WLEDDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] async_add_entities( - WLEDSensorEntity(coordinator, description) for description in SENSORS + WLEDSensorEntity(coordinator, description) + for description in SENSORS + if description.exists_fn(coordinator.data) ) diff --git a/homeassistant/components/wled/translations/es.json b/homeassistant/components/wled/translations/es.json index c1c50986b61..d883ba6ff23 100644 --- a/homeassistant/components/wled/translations/es.json +++ b/homeassistant/components/wled/translations/es.json @@ -2,7 +2,8 @@ "config": { "abort": { "already_configured": "Este dispositivo WLED ya est\u00e1 configurado.", - "cannot_connect": "No se pudo conectar" + "cannot_connect": "No se pudo conectar", + "cct_unsupported": "Este dispositivo WLED utiliza canales CCT, que no son compatibles con esta integraci\u00f3n." }, "error": { "cannot_connect": "No se pudo conectar" diff --git a/homeassistant/components/wled/translations/select.fr.json b/homeassistant/components/wled/translations/select.fr.json index 47a1a07989b..972cc1f7fae 100644 --- a/homeassistant/components/wled/translations/select.fr.json +++ b/homeassistant/components/wled/translations/select.fr.json @@ -1,8 +1,8 @@ { "state": { "wled__live_override": { - "0": "Inactif", - "1": "Actif", + "0": "D\u00e9sactiv\u00e9", + "1": "Activ\u00e9", "2": "Jusqu'au red\u00e9marrage de l'appareil" } } diff --git a/homeassistant/components/wled/translations/zh-Hant.json b/homeassistant/components/wled/translations/zh-Hant.json index 69f6476a768..6125c3a2cb5 100644 --- a/homeassistant/components/wled/translations/zh-Hant.json +++ b/homeassistant/components/wled/translations/zh-Hant.json @@ -18,7 +18,7 @@ }, "zeroconf_confirm": { "description": "\u662f\u5426\u8981\u65b0\u589e WLED \u540d\u7a31\u300c{name}\u300d\u88dd\u7f6e\u81f3 Home Assistant\uff1f", - "title": "\u81ea\u52d5\u63a2\u7d22\u5230 WLED \u88dd\u7f6e" + "title": "\u6240\u767c\u73fe\u7684 WLED \u88dd\u7f6e" } } }, diff --git a/homeassistant/components/wled/update.py b/homeassistant/components/wled/update.py new file mode 100644 index 00000000000..f0fc532b3b3 --- /dev/null +++ b/homeassistant/components/wled/update.py @@ -0,0 +1,90 @@ +"""Support for WLED updates.""" +from __future__ import annotations + +from typing import Any, cast + +from homeassistant.components.update import ( + UpdateDeviceClass, + UpdateEntity, + UpdateEntityFeature, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .coordinator import WLEDDataUpdateCoordinator +from .helpers import wled_exception_handler +from .models import WLEDEntity + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up WLED update based on a config entry.""" + coordinator: WLEDDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + async_add_entities([WLEDUpdateEntity(coordinator)]) + + +class WLEDUpdateEntity(WLEDEntity, UpdateEntity): + """Defines a WLED update entity.""" + + _attr_device_class = UpdateDeviceClass.FIRMWARE + _attr_supported_features = ( + UpdateEntityFeature.INSTALL | UpdateEntityFeature.SPECIFIC_VERSION + ) + _attr_title = "WLED" + + def __init__(self, coordinator: WLEDDataUpdateCoordinator) -> None: + """Initialize the update entity.""" + super().__init__(coordinator=coordinator) + self._attr_name = f"{coordinator.data.info.name} Firmware" + self._attr_unique_id = coordinator.data.info.mac_address + + @property + def installed_version(self) -> str | None: + """Version currently installed and in use.""" + if (version := self.coordinator.data.info.version) is None: + return None + return str(version) + + @property + def latest_version(self) -> str | None: + """Latest version available for install.""" + # If we already run a pre-release, we consider being on the beta channel. + # Offer beta version upgrade, unless stable is newer + if ( + (beta := self.coordinator.data.info.version_latest_beta) is not None + and (current := self.coordinator.data.info.version) is not None + and (current.alpha or current.beta or current.release_candidate) + and ( + (stable := self.coordinator.data.info.version_latest_stable) is None + or (stable is not None and stable < beta) + ) + ): + return str(beta) + + if (stable := self.coordinator.data.info.version_latest_stable) is not None: + return str(stable) + + return None + + @property + def release_url(self) -> str | None: + """URL to the full release notes of the latest version available.""" + if (version := self.latest_version) is None: + return None + return f"https://github.com/Aircoookie/WLED/releases/tag/v{version}" + + @wled_exception_handler + async def async_install( + self, version: str | None, backup: bool, **kwargs: Any + ) -> None: + """Install an update.""" + if version is None: + # We cast here, as we know that the latest_version is a string. + version = cast(str, self.latest_version) + await self.coordinator.wled.upgrade(version=version) + await self.coordinator.async_refresh() diff --git a/homeassistant/components/wolflink/translations/fr.json b/homeassistant/components/wolflink/translations/fr.json index 6e3348c3647..7b5916f237d 100644 --- a/homeassistant/components/wolflink/translations/fr.json +++ b/homeassistant/components/wolflink/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/wolflink/translations/sensor.id.json b/homeassistant/components/wolflink/translations/sensor.id.json index 3ceb09bf90a..7130716982e 100644 --- a/homeassistant/components/wolflink/translations/sensor.id.json +++ b/homeassistant/components/wolflink/translations/sensor.id.json @@ -1,25 +1,32 @@ { "state": { "wolflink__state": { - "1_x_warmwasser": "1 x DHW", + "1_x_warmwasser": "1 x Air Panas", "abgasklappe": "Katup saluran buang gas", + "absenkbetrieb": "Mode rendah", + "absenkstop": "Mode rendah berhenti", "aktiviert": "Diaktifkan", "antilegionellenfunktion": "Fungsi Anti-legionella", + "at_abschaltung": "Mati suhu luar", + "at_frostschutz": "Perlindungan embun beku suhu luar", "aus": "Dinonaktifkan", "auto": "Otomatis", - "auto_off_cool": "AutoOffCool", - "auto_on_cool": "AutoOnCool", - "automatik_aus": "Otomatis MATI", - "automatik_ein": "Otomatis NYALA", + "auto_off_cool": "Otomatis mati dingin", + "auto_on_cool": "Otomatis nyala dingin", + "automatik_aus": "Otomatis mati", + "automatik_ein": "Otomatis nyala", "bereit_keine_ladung": "Siap, tidak memuat", "betrieb_ohne_brenner": "Bekerja tanpa pembakar", "cooling": "Mendinginkan", "deaktiviert": "Tidak aktif", - "dhw_prior": "DHWPrior", + "dhw_prior": "Air panas sebelumnya", "eco": "Eco", "ein": "Diaktifkan", + "estrichtrocknung": "Pengeringan lapisan lantai", "externe_deaktivierung": "Penonaktifan eksternal", "fernschalter_ein": "Kontrol jarak jauh diaktifkan", + "frost_heizkreis": "Perlindungan beku sirkuit pemanasan", + "frost_warmwasser": "Perlindungan beku air panas", "frostschutz": "Perlindungan beku", "gasdruck": "Tekanan gas", "glt_betrieb": "Mode BMS", @@ -37,26 +44,38 @@ "kombigerat": "Ketel kombinasi", "kombigerat_mit_solareinbindung": "Ketel kombinasi dengan integrasi tenaga surya", "mindest_kombizeit": "Waktu kombi minimum", + "nachlauf_heizkreispumpe": "Pompa berjalan sirkuit pemanasan", + "nachspulen": "Putar ulang", "nur_heizgerat": "Hanya boiler", "parallelbetrieb": "Mode paralel", "partymodus": "Mode pesta", + "perm_cooling": "Pendinginan permanen", "permanent": "Permanen", "permanentbetrieb": "Mode permanen", "reduzierter_betrieb": "Mode terbatas", + "rt_abschaltung": "Mati suhu kamar", + "rt_frostschutz": "Perlindungan embun beku suhu kamar", + "ruhekontakt": "Kontak saat istirahat", "schornsteinfeger": "Uji emisi", "smart_grid": "SmartGrid", "smart_home": "SmartHome", + "softstart": "Mulai rendah", "solarbetrieb": "Mode surya", "sparbetrieb": "Mode ekonomi", "sparen": "Ekonomi", + "spreizung_hoch": "Perbedaan suhu sensor terlalu tinggi", + "spreizung_kf": "Sebaran sensor suhu ketel", "stabilisierung": "Stabilisasi", "standby": "Siaga", "start": "Mulai", "storung": "Kesalahan", + "taktsperre": "Anti-siklus", "telefonfernschalter": "Saklar jarak jauh per telepon", "test": "Pengujian", + "tpw": "Pemantau titik embun", "urlaubsmodus": "Mode liburan", "ventilprufung": "Uji katup", + "vorspulen": "Putar awal", "warmwasser": "Air Panas Domestik", "warmwasser_schnellstart": "Mulai cepat Air Panas Domestik", "warmwasserbetrieb": "Mode Air Panas Domestik", diff --git a/homeassistant/components/wolflink/translations/sensor.it.json b/homeassistant/components/wolflink/translations/sensor.it.json index e5eb50e6586..7ca8a76ceac 100644 --- a/homeassistant/components/wolflink/translations/sensor.it.json +++ b/homeassistant/components/wolflink/translations/sensor.it.json @@ -49,7 +49,7 @@ "nur_heizgerat": "Solo caldaia", "parallelbetrieb": "Modalit\u00e0 parallela", "partymodus": "Modalit\u00e0 festa", - "perm_cooling": "Raffreddamento Permanente", + "perm_cooling": "Raffreddamento permanente", "permanent": "Permanente", "permanentbetrieb": "Modalit\u00e0 permanente", "reduzierter_betrieb": "Modalit\u00e0 limitata", diff --git a/homeassistant/components/workday/manifest.json b/homeassistant/components/workday/manifest.json index ca95065e1a9..6028e6e6fc2 100644 --- a/homeassistant/components/workday/manifest.json +++ b/homeassistant/components/workday/manifest.json @@ -6,5 +6,10 @@ "codeowners": ["@fabaff"], "quality_scale": "internal", "iot_class": "local_polling", - "loggers": ["convertdate", "hijri_converter", "holidays", "korean_lunar_calendar"] + "loggers": [ + "convertdate", + "hijri_converter", + "holidays", + "korean_lunar_calendar" + ] } diff --git a/homeassistant/components/worldclock/sensor.py b/homeassistant/components/worldclock/sensor.py index 069ca5c55e7..e626add2534 100644 --- a/homeassistant/components/worldclock/sensor.py +++ b/homeassistant/components/worldclock/sensor.py @@ -1,6 +1,8 @@ """Support for showing the time in a different time zone.""" from __future__ import annotations +from datetime import tzinfo + import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity @@ -14,7 +16,6 @@ import homeassistant.util.dt as dt_util CONF_TIME_FORMAT = "time_format" DEFAULT_NAME = "Worldclock Sensor" -ICON = "mdi:clock" DEFAULT_TIME_STR_FORMAT = "%H:%M" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( @@ -33,15 +34,13 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the World clock sensor.""" - name = config.get(CONF_NAME) time_zone = dt_util.get_time_zone(config[CONF_TIME_ZONE]) - async_add_entities( [ WorldClockSensor( time_zone, - name, - config.get(CONF_TIME_FORMAT), + config[CONF_NAME], + config[CONF_TIME_FORMAT], ) ], True, @@ -51,28 +50,16 @@ async def async_setup_platform( class WorldClockSensor(SensorEntity): """Representation of a World clock sensor.""" - def __init__(self, time_zone, name, time_format): + _attr_icon = "mdi:clock" + + def __init__(self, time_zone: tzinfo | None, name: str, time_format: str) -> None: """Initialize the sensor.""" - self._name = name + self._attr_name = name self._time_zone = time_zone - self._state = None self._time_format = time_format - @property - def name(self): - """Return the name of the device.""" - return self._name - - @property - def native_value(self): - """Return the state of the device.""" - return self._state - - @property - def icon(self): - """Icon to use in the frontend, if any.""" - return ICON - - async def async_update(self): + async def async_update(self) -> None: """Get the time and updates the states.""" - self._state = dt_util.now(time_zone=self._time_zone).strftime(self._time_format) + self._attr_native_value = dt_util.now(time_zone=self._time_zone).strftime( + self._time_format + ) diff --git a/homeassistant/components/xbox/base_sensor.py b/homeassistant/components/xbox/base_sensor.py index 100685b6c34..024feb294b5 100644 --- a/homeassistant/components/xbox/base_sensor.py +++ b/homeassistant/components/xbox/base_sensor.py @@ -11,7 +11,7 @@ from . import PresenceData, XboxUpdateCoordinator from .const import DOMAIN -class XboxBaseSensorEntity(CoordinatorEntity): +class XboxBaseSensorEntity(CoordinatorEntity[XboxUpdateCoordinator]): """Base Sensor for the Xbox Integration.""" def __init__( diff --git a/homeassistant/components/xbox/media_player.py b/homeassistant/components/xbox/media_player.py index 4cc7ebda545..edf29c164ee 100644 --- a/homeassistant/components/xbox/media_player.py +++ b/homeassistant/components/xbox/media_player.py @@ -78,7 +78,7 @@ async def async_setup_entry( ) -class XboxMediaPlayer(CoordinatorEntity, MediaPlayerEntity): +class XboxMediaPlayer(CoordinatorEntity[XboxUpdateCoordinator], MediaPlayerEntity): """Representation of an Xbox Media Player.""" def __init__( diff --git a/homeassistant/components/xbox/remote.py b/homeassistant/components/xbox/remote.py index 897836e5c42..75595483608 100644 --- a/homeassistant/components/xbox/remote.py +++ b/homeassistant/components/xbox/remote.py @@ -45,7 +45,7 @@ async def async_setup_entry( ) -class XboxRemote(CoordinatorEntity, RemoteEntity): +class XboxRemote(CoordinatorEntity[XboxUpdateCoordinator], RemoteEntity): """Representation of an Xbox remote.""" def __init__( diff --git a/homeassistant/components/xbox/translations/fr.json b/homeassistant/components/xbox/translations/fr.json index a0dc75cc214..5b37704cec7 100644 --- a/homeassistant/components/xbox/translations/fr.json +++ b/homeassistant/components/xbox/translations/fr.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration de l'URL d'authentification d\u00e9pass\u00e9.", + "authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration de l'URL d'authentification expir\u00e9.", "missing_configuration": "Le composant n'est pas configur\u00e9. Veuillez suivre la documentation.", "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." }, diff --git a/homeassistant/components/xiaomi/camera.py b/homeassistant/components/xiaomi/camera.py index 15d92fb714d..8b7abcd2fe6 100644 --- a/homeassistant/components/xiaomi/camera.py +++ b/homeassistant/components/xiaomi/camera.py @@ -12,6 +12,7 @@ from homeassistant.components.camera import PLATFORM_SCHEMA, Camera from homeassistant.components.ffmpeg import get_ffmpeg_manager from homeassistant.const import ( CONF_HOST, + CONF_MODEL, CONF_NAME, CONF_PASSWORD, CONF_PATH, @@ -34,7 +35,6 @@ DEFAULT_USERNAME = "root" DEFAULT_ARGUMENTS = "-pred 1" CONF_FFMPEG_ARGUMENTS = "ffmpeg_arguments" -CONF_MODEL = "model" MODEL_YI = "yi" MODEL_XIAOFANG = "xiaofang" diff --git a/homeassistant/components/xiaomi/device_tracker.py b/homeassistant/components/xiaomi/device_tracker.py index c21f21e1f9b..f941dba2d01 100644 --- a/homeassistant/components/xiaomi/device_tracker.py +++ b/homeassistant/components/xiaomi/device_tracker.py @@ -113,7 +113,7 @@ def _retrieve_list(host, token, **kwargs): url = "http://{}/cgi-bin/luci/;stok={}/api/misystem/devicelist" url = url.format(host, token) try: - res = requests.get(url, timeout=5, **kwargs) + res = requests.get(url, timeout=10, **kwargs) except requests.exceptions.Timeout: _LOGGER.exception("Connection to the router timed out at URL %s", url) return diff --git a/homeassistant/components/xiaomi_aqara/binary_sensor.py b/homeassistant/components/xiaomi_aqara/binary_sensor.py index 0a21bb37d44..ef773805849 100644 --- a/homeassistant/components/xiaomi_aqara/binary_sensor.py +++ b/homeassistant/components/xiaomi_aqara/binary_sensor.py @@ -464,6 +464,11 @@ class XiaomiVibration(XiaomiBinarySensor): attrs.update(super().extra_state_attributes) return attrs + async def async_added_to_hass(self) -> None: + """Handle entity which will be added.""" + await super().async_added_to_hass() + self._state = False + def parse_data(self, data, raw_data): """Parse data sent by gateway.""" value = data.get(self._data_key) @@ -499,6 +504,11 @@ class XiaomiButton(XiaomiBinarySensor): attrs.update(super().extra_state_attributes) return attrs + async def async_added_to_hass(self) -> None: + """Handle entity which will be added.""" + await super().async_added_to_hass() + self._state = False + def parse_data(self, data, raw_data): """Parse data sent by gateway.""" value = data.get(self._data_key) @@ -545,7 +555,6 @@ class XiaomiCube(XiaomiBinarySensor): """Initialize the Xiaomi Cube.""" self._hass = hass self._last_action = None - self._state = False if "proto" not in device or int(device["proto"][0:1]) == 1: data_key = "status" else: @@ -559,6 +568,11 @@ class XiaomiCube(XiaomiBinarySensor): attrs.update(super().extra_state_attributes) return attrs + async def async_added_to_hass(self) -> None: + """Handle entity which will be added.""" + await super().async_added_to_hass() + self._state = False + def parse_data(self, data, raw_data): """Parse data sent by gateway.""" if self._data_key in data: diff --git a/homeassistant/components/xiaomi_aqara/light.py b/homeassistant/components/xiaomi_aqara/light.py index 637055144db..d6de34cf04c 100644 --- a/homeassistant/components/xiaomi_aqara/light.py +++ b/homeassistant/components/xiaomi_aqara/light.py @@ -61,8 +61,7 @@ class XiaomiGatewayLight(XiaomiDevice, LightEntity): return False if value == 0: - if self._state: - self._state = False + self._state = False return True rgbhexstr = f"{value:x}" diff --git a/homeassistant/components/xiaomi_aqara/strings.json b/homeassistant/components/xiaomi_aqara/strings.json index b1675992174..66ad4d01354 100644 --- a/homeassistant/components/xiaomi_aqara/strings.json +++ b/homeassistant/components/xiaomi_aqara/strings.json @@ -3,8 +3,7 @@ "flow_title": "{name}", "step": { "user": { - "title": "Xiaomi Aqara Gateway", - "description": "Connect to your Xiaomi Aqara Gateway, if the IP and MAC addresses are left empty, auto-discovery is used", + "description": "If the IP and MAC addresses are left empty, auto-discovery is used", "data": { "interface": "The network interface to use", "host": "[%key:common::config_flow::data::ip%] (optional)", @@ -12,7 +11,7 @@ } }, "settings": { - "title": "Xiaomi Aqara Gateway, optional settings", + "title": "Optional settings", "description": "The key (password) can be retrieved using this tutorial: https://www.domoticz.com/wiki/Xiaomi_Gateway_(Aqara)#Adding_the_Xiaomi_Gateway_to_Domoticz. If the key is not provided only sensors will be accessible", "data": { "key": "The key of your gateway", @@ -20,8 +19,7 @@ } }, "select": { - "title": "Select the Xiaomi Aqara Gateway that you wish to connect", - "description": "Run the setup again if you want to connect additional gateways", + "description": "Select the Xiaomi Aqara Gateway that you wish to connect", "data": { "select_ip": "[%key:common::config_flow::data::ip%]" } diff --git a/homeassistant/components/xiaomi_aqara/translations/fr.json b/homeassistant/components/xiaomi_aqara/translations/fr.json index 31779ba80b5..04be4e35e2f 100644 --- a/homeassistant/components/xiaomi_aqara/translations/fr.json +++ b/homeassistant/components/xiaomi_aqara/translations/fr.json @@ -6,8 +6,8 @@ "not_xiaomi_aqara": "Ce n'est pas une passerelle Xiaomi Aqara, l'appareil d\u00e9couvert ne correspond pas aux passerelles connues" }, "error": { - "discovery_error": "Impossible de d\u00e9couvrir une passerelle Xiaomi Aqara, essayez d'utiliser l'IP du p\u00e9riph\u00e9rique ex\u00e9cutant HomeAssistant comme interface", - "invalid_host": "Adresse IP non valide, voir https://www.home-assistant.io/integrations/xiaomi_aqara/#connection-problem", + "discovery_error": "Aucune passerelle Xiaomi Aqara d\u00e9couverte, essayez d'utiliser en tant qu'interface l'adresse IP de l'appareil ex\u00e9cutant Home Assistant", + "invalid_host": "Adresse IP non valide, consultez https://www.home-assistant.io/integrations/xiaomi_aqara/#connection-problem", "invalid_interface": "Interface r\u00e9seau non valide", "invalid_key": "Cl\u00e9 de passerelle non valide", "invalid_mac": "Adresse MAC non valide" diff --git a/homeassistant/components/xiaomi_aqara/translations/it.json b/homeassistant/components/xiaomi_aqara/translations/it.json index 27330c11242..319a33f3964 100644 --- a/homeassistant/components/xiaomi_aqara/translations/it.json +++ b/homeassistant/components/xiaomi_aqara/translations/it.json @@ -7,7 +7,7 @@ }, "error": { "discovery_error": "Impossibile individuare un gateway Xiaomi Aqara, prova a utilizzare l'IP del dispositivo che esegue HomeAssistant come interfaccia", - "invalid_host": "Nome host o indirizzo IP non valido, vedere https://www.home-assistant.io/integrations/xiaomi_aqara/#connection-problem", + "invalid_host": "Nome host o indirizzo IP non valido, vedi https://www.home-assistant.io/integrations/xiaomi_aqara/#connection-problem", "invalid_interface": "Interfaccia di rete non valida", "invalid_key": "Chiave gateway non valida", "invalid_mac": "Indirizzo Mac non valido" diff --git a/homeassistant/components/xiaomi_aqara/translations/zh-Hant.json b/homeassistant/components/xiaomi_aqara/translations/zh-Hant.json index dcc7b76a0c4..058efd39631 100644 --- a/homeassistant/components/xiaomi_aqara/translations/zh-Hant.json +++ b/homeassistant/components/xiaomi_aqara/translations/zh-Hant.json @@ -3,10 +3,10 @@ "abort": { "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", - "not_xiaomi_aqara": "\u4e26\u975e\u5c0f\u7c73 Aqara \u7db2\u95dc\uff0c\u6240\u63a2\u7d22\u4e4b\u88dd\u7f6e\u8207\u5df2\u77e5\u7db2\u95dc\u4e0d\u7b26\u5408" + "not_xiaomi_aqara": "\u4e26\u975e\u5c0f\u7c73 Aqara \u7db2\u95dc\uff0c\u6240\u641c\u7d22\u4e4b\u88dd\u7f6e\u8207\u5df2\u77e5\u7db2\u95dc\u4e0d\u7b26\u5408" }, "error": { - "discovery_error": "\u63a2\u7d22\u5c0f\u7c73 Aqara \u7db2\u95dc\u5931\u6557\uff0c\u8acb\u5617\u8a66\u4f7f\u7528\u57f7\u884c Home Assistant \u88dd\u7f6e\u7684 IP \u4f5c\u70ba\u4ecb\u9762", + "discovery_error": "\u641c\u7d22\u5c0f\u7c73 Aqara \u7db2\u95dc\u5931\u6557\uff0c\u8acb\u5617\u8a66\u4f7f\u7528\u57f7\u884c Home Assistant \u88dd\u7f6e\u7684 IP \u4f5c\u70ba\u4ecb\u9762", "invalid_host": "\u7121\u6548\u4e3b\u6a5f\u540d\u7a31\u6216 IP \u4f4d\u5740\uff0c\u8acb\u53c3\u95b1 https://www.home-assistant.io/integrations/xiaomi_aqara/#connection-problem", "invalid_interface": "\u7db2\u8def\u4ecb\u9762\u7121\u6548", "invalid_key": "\u7db2\u95dc\u91d1\u9470\u7121\u6548", @@ -35,7 +35,7 @@ "interface": "\u4f7f\u7528\u7684\u7db2\u8def\u4ecb\u9762", "mac": "Mac \u4f4d\u5740\uff08\u9078\u9805\uff09" }, - "description": "\u9023\u7dda\u81f3\u5c0f\u7c73 Aqara \u7db2\u95dc\uff0c\u5047\u5982 IP \u6216 Mac \u4f4d\u5740\u70ba\u7a7a\u767d\u3001\u5c07\u9032\u884c\u81ea\u52d5\u63a2\u7d22", + "description": "\u9023\u7dda\u81f3\u5c0f\u7c73 Aqara \u7db2\u95dc\uff0c\u5047\u5982 IP \u6216 Mac \u4f4d\u5740\u70ba\u7a7a\u767d\u3001\u5c07\u9032\u884c\u81ea\u52d5\u641c\u7d22", "title": "\u5c0f\u7c73 Aqara \u7db2\u95dc" } } diff --git a/homeassistant/components/xiaomi_miio/__init__.py b/homeassistant/components/xiaomi_miio/__init__.py index 7fb489b3467..68d3fe04653 100644 --- a/homeassistant/components/xiaomi_miio/__init__.py +++ b/homeassistant/components/xiaomi_miio/__init__.py @@ -32,7 +32,7 @@ from miio import ( from miio.gateway.gateway import GatewayException from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_HOST, CONF_TOKEN, Platform +from homeassistant.const import CONF_HOST, CONF_MODEL, CONF_TOKEN, Platform from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import device_registry as dr, entity_registry as er @@ -43,7 +43,6 @@ from .const import ( CONF_DEVICE, CONF_FLOW_TYPE, CONF_GATEWAY, - CONF_MODEL, DOMAIN, KEY_COORDINATOR, KEY_DEVICE, @@ -87,6 +86,7 @@ GATEWAY_PLATFORMS = [ SWITCH_PLATFORMS = [Platform.SWITCH] FAN_PLATFORMS = [ Platform.BINARY_SENSOR, + Platform.BUTTON, Platform.FAN, Platform.NUMBER, Platform.SELECT, diff --git a/homeassistant/components/xiaomi_miio/air_quality.py b/homeassistant/components/xiaomi_miio/air_quality.py index fdc62076c25..10d6c6129d3 100644 --- a/homeassistant/components/xiaomi_miio/air_quality.py +++ b/homeassistant/components/xiaomi_miio/air_quality.py @@ -5,14 +5,13 @@ from miio import AirQualityMonitor, AirQualityMonitorCGDN1, DeviceException from homeassistant.components.air_quality import AirQualityEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_HOST, CONF_TOKEN +from homeassistant.const import CONF_HOST, CONF_MODEL, CONF_TOKEN from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( CONF_DEVICE, CONF_FLOW_TYPE, - CONF_MODEL, MODEL_AIRQUALITYMONITOR_B1, MODEL_AIRQUALITYMONITOR_CGDN1, MODEL_AIRQUALITYMONITOR_S1, diff --git a/homeassistant/components/xiaomi_miio/binary_sensor.py b/homeassistant/components/xiaomi_miio/binary_sensor.py index 83bea4be9b1..d2f00e9805c 100644 --- a/homeassistant/components/xiaomi_miio/binary_sensor.py +++ b/homeassistant/components/xiaomi_miio/binary_sensor.py @@ -11,6 +11,7 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_MODEL from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -19,10 +20,11 @@ from . import VacuumCoordinatorDataAttributes from .const import ( CONF_DEVICE, CONF_FLOW_TYPE, - CONF_MODEL, DOMAIN, KEY_COORDINATOR, KEY_DEVICE, + MODEL_AIRFRESH_A1, + MODEL_AIRFRESH_T2017, MODEL_FAN_ZA5, MODELS_HUMIDIFIER_MIIO, MODELS_HUMIDIFIER_MIOT, @@ -36,6 +38,7 @@ from .device import XiaomiCoordinatedMiioEntity _LOGGER = logging.getLogger(__name__) ATTR_NO_WATER = "no_water" +ATTR_PTC_STATUS = "ptc_status" ATTR_POWERSUPPLY_ATTACHED = "powersupply_attached" ATTR_WATER_TANK_DETACHED = "water_tank_detached" ATTR_MOP_ATTACHED = "is_water_box_carriage_attached" @@ -66,6 +69,12 @@ BINARY_SENSOR_TYPES = ( value=lambda value: not value, entity_category=EntityCategory.DIAGNOSTIC, ), + XiaomiMiioBinarySensorDescription( + key=ATTR_PTC_STATUS, + name="Auxiliary Heat Status", + device_class=BinarySensorDeviceClass.POWER, + entity_category=EntityCategory.DIAGNOSTIC, + ), XiaomiMiioBinarySensorDescription( key=ATTR_POWERSUPPLY_ATTACHED, name="Power Supply", @@ -74,6 +83,7 @@ BINARY_SENSOR_TYPES = ( ), ) +AIRFRESH_A1_BINARY_SENSORS = (ATTR_PTC_STATUS,) FAN_ZA5_BINARY_SENSORS = (ATTR_POWERSUPPLY_ATTACHED,) VACUUM_SENSORS = { @@ -171,7 +181,9 @@ async def async_setup_entry( if config_entry.data[CONF_FLOW_TYPE] == CONF_DEVICE: model = config_entry.data[CONF_MODEL] sensors = [] - if model in MODEL_FAN_ZA5: + if model in MODEL_AIRFRESH_A1 or model in MODEL_AIRFRESH_T2017: + sensors = AIRFRESH_A1_BINARY_SENSORS + elif model in MODEL_FAN_ZA5: sensors = FAN_ZA5_BINARY_SENSORS elif model in MODELS_HUMIDIFIER_MIIO: sensors = HUMIDIFIER_MIIO_BINARY_SENSORS diff --git a/homeassistant/components/xiaomi_miio/button.py b/homeassistant/components/xiaomi_miio/button.py new file mode 100644 index 00000000000..6a69289f7ef --- /dev/null +++ b/homeassistant/components/xiaomi_miio/button.py @@ -0,0 +1,120 @@ +"""Support for Xiaomi buttons.""" +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + +from homeassistant.components.button import ( + ButtonDeviceClass, + ButtonEntity, + ButtonEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_MODEL +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import ( + DOMAIN, + KEY_COORDINATOR, + KEY_DEVICE, + MODEL_AIRFRESH_A1, + MODEL_AIRFRESH_T2017, +) +from .device import XiaomiCoordinatedMiioEntity + +ATTR_RESET_DUST_FILTER = "reset_dust_filter" +ATTR_RESET_UPPER_FILTER = "reset_upper_filter" + + +@dataclass +class XiaomiMiioButtonDescription(ButtonEntityDescription): + """A class that describes button entities.""" + + method_press: str = "" + method_press_error_message: str = "" + + +BUTTON_TYPES = ( + XiaomiMiioButtonDescription( + key=ATTR_RESET_DUST_FILTER, + name="Reset Dust Filter", + icon="mdi:air-filter", + method_press="reset_dust_filter", + method_press_error_message="Resetting the dust filter lifetime failed", + entity_category=EntityCategory.CONFIG, + ), + XiaomiMiioButtonDescription( + key=ATTR_RESET_UPPER_FILTER, + name="Reset Upper Filter", + icon="mdi:air-filter", + method_press="reset_upper_filter", + method_press_error_message="Resetting the upper filter lifetime failed.", + entity_category=EntityCategory.CONFIG, + ), +) + +MODEL_TO_BUTTON_MAP: dict[str, tuple[str, ...]] = { + MODEL_AIRFRESH_A1: (ATTR_RESET_DUST_FILTER,), + MODEL_AIRFRESH_T2017: ( + ATTR_RESET_DUST_FILTER, + ATTR_RESET_UPPER_FILTER, + ), +} + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the button from a config entry.""" + model = config_entry.data[CONF_MODEL] + + if model not in MODEL_TO_BUTTON_MAP: + return + + entities = [] + buttons = MODEL_TO_BUTTON_MAP[model] + unique_id = config_entry.unique_id + device = hass.data[DOMAIN][config_entry.entry_id][KEY_DEVICE] + coordinator = hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR] + + for description in BUTTON_TYPES: + if description.key not in buttons: + continue + + entities.append( + XiaomiGenericCoordinatedButton( + f"{config_entry.title} {description.name}", + device, + config_entry, + f"{description.key}_{unique_id}", + coordinator, + description, + ) + ) + + async_add_entities(entities) + + +class XiaomiGenericCoordinatedButton(XiaomiCoordinatedMiioEntity, ButtonEntity): + """A button implementation for Xiaomi.""" + + entity_description: XiaomiMiioButtonDescription + + _attr_device_class = ButtonDeviceClass.RESTART + + def __init__(self, name, device, entry, unique_id, coordinator, description): + """Initialize the plug switch.""" + super().__init__(name, device, entry, unique_id, coordinator) + self.entity_description = description + + async def async_press(self, **kwargs: Any) -> None: + """Press the button.""" + method = getattr(self._device, self.entity_description.method_press) + await self._try_command( + self.entity_description.method_press_error_message, + method, + ) diff --git a/homeassistant/components/xiaomi_miio/config_flow.py b/homeassistant/components/xiaomi_miio/config_flow.py index 04e2b58ad0f..a78e01c7fae 100644 --- a/homeassistant/components/xiaomi_miio/config_flow.py +++ b/homeassistant/components/xiaomi_miio/config_flow.py @@ -9,7 +9,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.components import zeroconf from homeassistant.config_entries import SOURCE_REAUTH -from homeassistant.const import CONF_HOST, CONF_NAME, CONF_TOKEN +from homeassistant.const import CONF_HOST, CONF_MODEL, CONF_NAME, CONF_TOKEN from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.device_registry import format_mac @@ -24,7 +24,6 @@ from .const import ( CONF_GATEWAY, CONF_MAC, CONF_MANUAL, - CONF_MODEL, DEFAULT_CLOUD_COUNTRY, DOMAIN, MODELS_ALL, @@ -136,9 +135,7 @@ class XiaomiMiioFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Dialog that informs the user that reauth is required.""" if user_input is not None: return await self.async_step_cloud() - return self.async_show_form( - step_id="reauth_confirm", data_schema=vol.Schema({}) - ) + return self.async_show_form(step_id="reauth_confirm") async def async_step_import(self, conf: dict): """Import a configuration from config.yaml.""" diff --git a/homeassistant/components/xiaomi_miio/const.py b/homeassistant/components/xiaomi_miio/const.py index cc607b3f419..3577e7b9907 100644 --- a/homeassistant/components/xiaomi_miio/const.py +++ b/homeassistant/components/xiaomi_miio/const.py @@ -13,7 +13,6 @@ DOMAIN = "xiaomi_miio" CONF_FLOW_TYPE = "config_flow_device" CONF_GATEWAY = "gateway" CONF_DEVICE = "device" -CONF_MODEL = "model" CONF_MAC = "mac" CONF_CLOUD_USERNAME = "cloud_username" CONF_CLOUD_PASSWORD = "cloud_password" @@ -315,6 +314,8 @@ FEATURE_SET_DELAY_OFF_COUNTDOWN = 65536 FEATURE_SET_LED_BRIGHTNESS_LEVEL = 131072 FEATURE_SET_FAVORITE_RPM = 262144 FEATURE_SET_IONIZER = 524288 +FEATURE_SET_DISPLAY = 1048576 +FEATURE_SET_PTC = 2097152 FEATURE_FLAGS_AIRPURIFIER_MIIO = ( FEATURE_SET_BUZZER @@ -387,7 +388,9 @@ FEATURE_FLAGS_AIRHUMIDIFIER_CA4 = ( | FEATURE_SET_CLEAN ) -FEATURE_FLAGS_AIRFRESH_A1 = FEATURE_SET_BUZZER | FEATURE_SET_CHILD_LOCK +FEATURE_FLAGS_AIRFRESH_A1 = ( + FEATURE_SET_BUZZER | FEATURE_SET_CHILD_LOCK | FEATURE_SET_DISPLAY | FEATURE_SET_PTC +) FEATURE_FLAGS_AIRFRESH = ( FEATURE_SET_BUZZER @@ -398,7 +401,9 @@ FEATURE_FLAGS_AIRFRESH = ( | FEATURE_SET_EXTRA_FEATURES ) -FEATURE_FLAGS_AIRFRESH_T2017 = FEATURE_SET_BUZZER | FEATURE_SET_CHILD_LOCK +FEATURE_FLAGS_AIRFRESH_T2017 = ( + FEATURE_SET_BUZZER | FEATURE_SET_CHILD_LOCK | FEATURE_SET_DISPLAY | FEATURE_SET_PTC +) FEATURE_FLAGS_FAN_P5 = ( FEATURE_SET_BUZZER diff --git a/homeassistant/components/xiaomi_miio/device.py b/homeassistant/components/xiaomi_miio/device.py index 5a186c23570..dc1cf86f7df 100644 --- a/homeassistant/components/xiaomi_miio/device.py +++ b/homeassistant/components/xiaomi_miio/device.py @@ -3,19 +3,25 @@ import datetime from enum import Enum from functools import partial import logging +from typing import Any, TypeVar from construct.core import ChecksumError from miio import Device, DeviceException -from homeassistant.const import ATTR_CONNECTIONS +from homeassistant.const import ATTR_CONNECTIONS, CONF_MODEL from homeassistant.helpers import device_registry as dr from homeassistant.helpers.entity import DeviceInfo, Entity -from homeassistant.helpers.update_coordinator import CoordinatorEntity +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) -from .const import CONF_MAC, CONF_MODEL, DOMAIN, AuthException, SetupException +from .const import CONF_MAC, DOMAIN, AuthException, SetupException _LOGGER = logging.getLogger(__name__) +_T = TypeVar("_T", bound=DataUpdateCoordinator[Any]) + class ConnectXiaomiDevice: """Class to async connect to a Xiaomi Device.""" @@ -101,7 +107,7 @@ class XiaomiMiioEntity(Entity): return device_info -class XiaomiCoordinatedMiioEntity(CoordinatorEntity): +class XiaomiCoordinatedMiioEntity(CoordinatorEntity[_T]): """Representation of a base a coordinated Xiaomi Miio Entity.""" def __init__(self, name, device, entry, unique_id, coordinator): diff --git a/homeassistant/components/xiaomi_miio/fan.py b/homeassistant/components/xiaomi_miio/fan.py index e4b79ed77a8..503556171f1 100644 --- a/homeassistant/components/xiaomi_miio/fan.py +++ b/homeassistant/components/xiaomi_miio/fan.py @@ -25,7 +25,7 @@ from homeassistant.components.fan import ( FanEntity, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.const import ATTR_ENTITY_ID, CONF_MODEL from homeassistant.core import HomeAssistant, ServiceCall, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -84,8 +84,6 @@ _LOGGER = logging.getLogger(__name__) DATA_KEY = "fan.xiaomi_miio" -CONF_MODEL = "model" - ATTR_MODE_NATURE = "Nature" ATTR_MODE_NORMAL = "Normal" @@ -326,7 +324,6 @@ class XiaomiGenericDevice(XiaomiCoordinatedMiioEntity, FanEntity): async def async_turn_on( self, - speed: str = None, percentage: int = None, preset_mode: str = None, **kwargs, diff --git a/homeassistant/components/xiaomi_miio/humidifier.py b/homeassistant/components/xiaomi_miio/humidifier.py index 7c3110e190a..9a9fe43b739 100644 --- a/homeassistant/components/xiaomi_miio/humidifier.py +++ b/homeassistant/components/xiaomi_miio/humidifier.py @@ -9,7 +9,7 @@ from miio.airhumidifier_mjjsq import OperationMode as AirhumidifierMjjsqOperatio from homeassistant.components.humidifier import HumidifierDeviceClass, HumidifierEntity from homeassistant.components.humidifier.const import SUPPORT_MODES from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_MODE +from homeassistant.const import ATTR_MODE, CONF_MODEL from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.percentage import percentage_to_ranged_value @@ -17,7 +17,6 @@ from homeassistant.util.percentage import percentage_to_ranged_value from .const import ( CONF_DEVICE, CONF_FLOW_TYPE, - CONF_MODEL, DOMAIN, KEY_COORDINATOR, KEY_DEVICE, diff --git a/homeassistant/components/xiaomi_miio/light.py b/homeassistant/components/xiaomi_miio/light.py index cae7bacaba4..acb726e7c05 100644 --- a/homeassistant/components/xiaomi_miio/light.py +++ b/homeassistant/components/xiaomi_miio/light.py @@ -25,7 +25,7 @@ from homeassistant.components.light import ( LightEntity, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST, CONF_TOKEN +from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST, CONF_MODEL, CONF_TOKEN from homeassistant.core import HomeAssistant, ServiceCall import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import DeviceInfo @@ -36,7 +36,6 @@ from .const import ( CONF_DEVICE, CONF_FLOW_TYPE, CONF_GATEWAY, - CONF_MODEL, DOMAIN, KEY_COORDINATOR, MODELS_LIGHT_BULB, diff --git a/homeassistant/components/xiaomi_miio/number.py b/homeassistant/components/xiaomi_miio/number.py index 4ffc773d1c1..f47d80ead17 100644 --- a/homeassistant/components/xiaomi_miio/number.py +++ b/homeassistant/components/xiaomi_miio/number.py @@ -6,7 +6,7 @@ from dataclasses import dataclass from homeassistant.components.number import NumberEntity, NumberEntityDescription from homeassistant.components.number.const import DOMAIN as PLATFORM_DOMAIN from homeassistant.config_entries import ConfigEntry -from homeassistant.const import DEGREE, TIME_MINUTES +from homeassistant.const import CONF_MODEL, DEGREE, TIME_MINUTES from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity import EntityCategory @@ -15,7 +15,6 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( CONF_DEVICE, CONF_FLOW_TYPE, - CONF_MODEL, DOMAIN, FEATURE_FLAGS_AIRFRESH, FEATURE_FLAGS_AIRFRESH_A1, diff --git a/homeassistant/components/xiaomi_miio/select.py b/homeassistant/components/xiaomi_miio/select.py index 2b5f6f3d5fd..f2fc736ed82 100644 --- a/homeassistant/components/xiaomi_miio/select.py +++ b/homeassistant/components/xiaomi_miio/select.py @@ -12,6 +12,7 @@ from miio.fan_common import LedBrightness as FanLedBrightness from homeassistant.components.select import SelectEntity, SelectEntityDescription from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_MODEL from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -19,7 +20,6 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( CONF_DEVICE, CONF_FLOW_TYPE, - CONF_MODEL, DOMAIN, FEATURE_SET_LED_BRIGHTNESS, KEY_COORDINATOR, diff --git a/homeassistant/components/xiaomi_miio/sensor.py b/homeassistant/components/xiaomi_miio/sensor.py index cbab107994b..0beac2c0041 100644 --- a/homeassistant/components/xiaomi_miio/sensor.py +++ b/homeassistant/components/xiaomi_miio/sensor.py @@ -28,6 +28,7 @@ from homeassistant.const import ( CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONCENTRATION_PARTS_PER_MILLION, CONF_HOST, + CONF_MODEL, CONF_TOKEN, LIGHT_LUX, PERCENTAGE, @@ -49,7 +50,6 @@ from .const import ( CONF_DEVICE, CONF_FLOW_TYPE, CONF_GATEWAY, - CONF_MODEL, DOMAIN, KEY_COORDINATOR, KEY_DEVICE, diff --git a/homeassistant/components/xiaomi_miio/services.yaml b/homeassistant/components/xiaomi_miio/services.yaml index b8f81e1a34d..e1cf03ba4ee 100644 --- a/homeassistant/components/xiaomi_miio/services.yaml +++ b/homeassistant/components/xiaomi_miio/services.yaml @@ -159,7 +159,7 @@ remote_learn_command: remote_set_led_on: name: Remote set LED on - description: 'Turn on blue LED.' + description: "Turn on blue LED." target: entity: integration: xiaomi_miio @@ -167,7 +167,7 @@ remote_set_led_on: remote_set_led_off: name: Remote set LED off - description: 'Turn off blue LED.' + description: "Turn off blue LED." target: entity: integration: xiaomi_miio @@ -231,8 +231,8 @@ switch_set_power_mode: selector: select: options: - - 'green' - - 'normal' + - "green" + - "normal" vacuum_remote_control_start: name: Vacuum remote control start @@ -273,7 +273,7 @@ vacuum_remote_control_move: number: min: -179 max: 179 - unit_of_measurement: '°' + unit_of_measurement: "°" duration: name: Duration description: Duration of the movement. @@ -306,7 +306,7 @@ vacuum_remote_control_move_step: number: min: -179 max: 179 - unit_of_measurement: '°' + unit_of_measurement: "°" duration: name: Duration description: Duration of the movement. diff --git a/homeassistant/components/xiaomi_miio/strings.json b/homeassistant/components/xiaomi_miio/strings.json index 1331d22e933..e359f54cc5a 100644 --- a/homeassistant/components/xiaomi_miio/strings.json +++ b/homeassistant/components/xiaomi_miio/strings.json @@ -28,30 +28,25 @@ "cloud_country": "Cloud server country", "manual": "Configure manually (not recommended)" }, - "description": "Log in to the Xiaomi Miio cloud, see https://www.openhab.org/addons/bindings/miio/#country-servers for the cloud server to use.", - "title": "Connect to a Xiaomi Miio Device or Xiaomi Gateway" + "description": "Log in to the Xiaomi Miio cloud, see https://www.openhab.org/addons/bindings/miio/#country-servers for the cloud server to use." }, "select": { "data": { "select_device": "Miio device" }, - "description": "Select the Xiaomi Miio device to setup.", - "title": "Connect to a Xiaomi Miio Device or Xiaomi Gateway" + "description": "Select the Xiaomi Miio device to setup." }, "manual": { "data": { "host": "[%key:common::config_flow::data::ip%]", "token": "[%key:common::config_flow::data::api_token%]" }, - "description": "You will need the 32 character [%key:common::config_flow::data::api_token%], see https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token for instructions. Please note, that this [%key:common::config_flow::data::api_token%] is different from the key used by the Xiaomi Aqara integration.", - "title": "Connect to a Xiaomi Miio Device or Xiaomi Gateway" + "description": "You will need the 32 character [%key:common::config_flow::data::api_token%], see https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token for instructions. Please note, that this [%key:common::config_flow::data::api_token%] is different from the key used by the Xiaomi Aqara integration." }, "connect": { "data": { "model": "Device model" - }, - "description": "Manually select the device model from the supported models.", - "title": "Connect to a Xiaomi Miio Device or Xiaomi Gateway" + } } } }, @@ -61,8 +56,6 @@ }, "step": { "init": { - "title": "Xiaomi Miio", - "description": "Specify optional settings", "data": { "cloud_subdevices": "Use cloud to get connected subdevices" } diff --git a/homeassistant/components/xiaomi_miio/strings.select.json b/homeassistant/components/xiaomi_miio/strings.select.json index 80edde042ce..265aec66531 100644 --- a/homeassistant/components/xiaomi_miio/strings.select.json +++ b/homeassistant/components/xiaomi_miio/strings.select.json @@ -1,9 +1,9 @@ { - "state": { - "xiaomi_miio__led_brightness": { - "bright": "Bright", - "dim": "Dim", - "off": "Off" - } + "state": { + "xiaomi_miio__led_brightness": { + "bright": "Bright", + "dim": "Dim", + "off": "Off" } - } \ No newline at end of file + } +} diff --git a/homeassistant/components/xiaomi_miio/switch.py b/homeassistant/components/xiaomi_miio/switch.py index ce05a891c0a..968abceb57c 100644 --- a/homeassistant/components/xiaomi_miio/switch.py +++ b/homeassistant/components/xiaomi_miio/switch.py @@ -21,6 +21,7 @@ from homeassistant.const import ( ATTR_MODE, ATTR_TEMPERATURE, CONF_HOST, + CONF_MODEL, CONF_TOKEN, ) from homeassistant.core import HomeAssistant, ServiceCall, callback @@ -32,7 +33,6 @@ from .const import ( CONF_DEVICE, CONF_FLOW_TYPE, CONF_GATEWAY, - CONF_MODEL, DOMAIN, FEATURE_FLAGS_AIRFRESH, FEATURE_FLAGS_AIRFRESH_A1, @@ -59,10 +59,12 @@ from .const import ( FEATURE_SET_BUZZER, FEATURE_SET_CHILD_LOCK, FEATURE_SET_CLEAN, + FEATURE_SET_DISPLAY, FEATURE_SET_DRY, FEATURE_SET_IONIZER, FEATURE_SET_LEARN_MODE, FEATURE_SET_LED, + FEATURE_SET_PTC, KEY_COORDINATOR, KEY_DEVICE, MODEL_AIRFRESH_A1, @@ -121,6 +123,7 @@ ATTR_AUTO_DETECT = "auto_detect" ATTR_BUZZER = "buzzer" ATTR_CHILD_LOCK = "child_lock" ATTR_CLEAN = "clean_mode" +ATTR_DISPLAY = "display" ATTR_DRY = "dry" ATTR_LEARN_MODE = "learn_mode" ATTR_LED = "led" @@ -131,6 +134,7 @@ ATTR_POWER = "power" ATTR_POWER_MODE = "power_mode" ATTR_POWER_PRICE = "power_price" ATTR_PRICE = "price" +ATTR_PTC = "ptc" ATTR_WIFI_LED = "wifi_led" FEATURE_SET_POWER_MODE = 1 @@ -225,6 +229,15 @@ SWITCH_TYPES = ( method_off="async_set_child_lock_off", entity_category=EntityCategory.CONFIG, ), + XiaomiMiioSwitchDescription( + key=ATTR_DISPLAY, + feature=FEATURE_SET_DISPLAY, + name="Display", + icon="mdi:led-outline", + method_on="async_set_display_on", + method_off="async_set_display_off", + entity_category=EntityCategory.CONFIG, + ), XiaomiMiioSwitchDescription( key=ATTR_DRY, feature=FEATURE_SET_DRY, @@ -279,6 +292,15 @@ SWITCH_TYPES = ( method_off="async_set_ionizer_off", entity_category=EntityCategory.CONFIG, ), + XiaomiMiioSwitchDescription( + key=ATTR_PTC, + feature=FEATURE_SET_PTC, + name="Auxiliary Heat", + icon="mdi:radiator", + method_on="async_set_ptc_on", + method_off="async_set_ptc_off", + entity_category=EntityCategory.CONFIG, + ), ) @@ -533,6 +555,22 @@ class XiaomiGenericCoordinatedSwitch(XiaomiCoordinatedMiioEntity, SwitchEntity): False, ) + async def async_set_display_on(self) -> bool: + """Turn the display on.""" + return await self._try_command( + "Turning the display of the miio device on failed.", + self._device.set_display, + True, + ) + + async def async_set_display_off(self) -> bool: + """Turn the display off.""" + return await self._try_command( + "Turning the display of the miio device off failed.", + self._device.set_display, + False, + ) + async def async_set_dry_on(self) -> bool: """Turn the dry mode on.""" return await self._try_command( @@ -629,6 +667,22 @@ class XiaomiGenericCoordinatedSwitch(XiaomiCoordinatedMiioEntity, SwitchEntity): False, ) + async def async_set_ptc_on(self) -> bool: + """Turn ionizer on.""" + return await self._try_command( + "Turning ionizer of the miio device on failed.", + self._device.set_ptc, + True, + ) + + async def async_set_ptc_off(self) -> bool: + """Turn ionizer off.""" + return await self._try_command( + "Turning ionizer of the miio device off failed.", + self._device.set_ptc, + False, + ) + class XiaomiGatewaySwitch(XiaomiGatewayDevice, SwitchEntity): """Representation of a XiaomiGatewaySwitch.""" diff --git a/homeassistant/components/xiaomi_miio/translations/zh-Hans.json b/homeassistant/components/xiaomi_miio/translations/zh-Hans.json index 02519414492..f488618b945 100644 --- a/homeassistant/components/xiaomi_miio/translations/zh-Hans.json +++ b/homeassistant/components/xiaomi_miio/translations/zh-Hans.json @@ -13,7 +13,8 @@ "cloud_login_error": "\u65e0\u6cd5\u767b\u5f55\u5c0f\u7c73\u4e91\u670d\u52a1\uff0c\u8bf7\u68c0\u67e5\u51ed\u636e\u3002", "cloud_no_devices": "\u672a\u5728\u5c0f\u7c73\u5e10\u6237\u4e2d\u53d1\u73b0\u8bbe\u5907\u3002", "no_device_selected": "\u672a\u9009\u62e9\u8bbe\u5907\uff0c\u8bf7\u9009\u62e9\u4e00\u4e2a\u8bbe\u5907\u3002", - "unknown_device": "\u8be5\u8bbe\u5907\u578b\u53f7\u6682\u672a\u9002\u914d\uff0c\u56e0\u6b64\u65e0\u6cd5\u901a\u8fc7\u914d\u7f6e\u5411\u5bfc\u6dfb\u52a0\u8bbe\u5907\u3002" + "unknown_device": "\u8be5\u8bbe\u5907\u578b\u53f7\u6682\u672a\u9002\u914d\uff0c\u56e0\u6b64\u65e0\u6cd5\u901a\u8fc7\u914d\u7f6e\u5411\u5bfc\u6dfb\u52a0\u8bbe\u5907\u3002", + "wrong_token": "\u6821\u9a8c\u548c\u9519\u8bef\uff0ctoken \u9519\u8bef" }, "flow_title": "Xiaomi Miio: {name}", "step": { diff --git a/homeassistant/components/xiaomi_miio/vacuum.py b/homeassistant/components/xiaomi_miio/vacuum.py index a6fa6c399eb..555a352a6ce 100644 --- a/homeassistant/components/xiaomi_miio/vacuum.py +++ b/homeassistant/components/xiaomi_miio/vacuum.py @@ -203,13 +203,19 @@ async def async_setup_entry( async_add_entities(entities, update_before_add=True) -class MiroboVacuum(XiaomiCoordinatedMiioEntity, StateVacuumEntity): +class MiroboVacuum( + XiaomiCoordinatedMiioEntity[DataUpdateCoordinator[VacuumCoordinatorData]], + StateVacuumEntity, +): """Representation of a Xiaomi Vacuum cleaner robot.""" - coordinator: DataUpdateCoordinator[VacuumCoordinatorData] - def __init__( - self, name, device, entry, unique_id, coordinator: DataUpdateCoordinator + self, + name, + device, + entry, + unique_id, + coordinator: DataUpdateCoordinator[VacuumCoordinatorData], ): """Initialize the Xiaomi vacuum cleaner robot handler.""" super().__init__(name, device, entry, unique_id, coordinator) diff --git a/homeassistant/components/xmpp/manifest.json b/homeassistant/components/xmpp/manifest.json index 840f2cd677d..d5b08b6bc23 100644 --- a/homeassistant/components/xmpp/manifest.json +++ b/homeassistant/components/xmpp/manifest.json @@ -2,7 +2,7 @@ "domain": "xmpp", "name": "Jabber (XMPP)", "documentation": "https://www.home-assistant.io/integrations/xmpp", - "requirements": ["slixmpp==1.7.1"], + "requirements": ["slixmpp==1.8.0.1"], "codeowners": ["@fabaff", "@flowolf"], "iot_class": "cloud_push", "loggers": ["pyasn1", "slixmpp"] diff --git a/homeassistant/components/yale_smart_alarm/__init__.py b/homeassistant/components/yale_smart_alarm/__init__.py index 626c7d0b206..8712241a2aa 100644 --- a/homeassistant/components/yale_smart_alarm/__init__.py +++ b/homeassistant/components/yale_smart_alarm/__init__.py @@ -5,31 +5,25 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed -from .const import COORDINATOR, DOMAIN, LOGGER, PLATFORMS +from .const import COORDINATOR, DOMAIN, PLATFORMS from .coordinator import YaleDataUpdateCoordinator async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Yale from a config entry.""" - hass.data.setdefault(DOMAIN, {}) - title = entry.title - - coordinator = YaleDataUpdateCoordinator(hass, entry=entry) + coordinator = YaleDataUpdateCoordinator(hass, entry) if not await hass.async_add_executor_job(coordinator.get_updates): raise ConfigEntryAuthFailed await coordinator.async_config_entry_first_refresh() - - hass.data[DOMAIN][entry.entry_id] = { + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = { COORDINATOR: coordinator, } hass.config_entries.async_setup_platforms(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(update_listener)) - LOGGER.debug("Loaded entry for %s", title) - return True @@ -41,12 +35,7 @@ async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: 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) - - title = entry.title - if unload_ok: + if await hass.config_entries.async_unload_platforms(entry, PLATFORMS): hass.data[DOMAIN].pop(entry.entry_id) - LOGGER.debug("Unloaded entry for %s", title) - return unload_ok - + return True return False diff --git a/homeassistant/components/yale_smart_alarm/alarm_control_panel.py b/homeassistant/components/yale_smart_alarm/alarm_control_panel.py index 0348676904e..20d88ca4859 100644 --- a/homeassistant/components/yale_smart_alarm/alarm_control_panel.py +++ b/homeassistant/components/yale_smart_alarm/alarm_control_panel.py @@ -3,72 +3,27 @@ from __future__ import annotations from typing import TYPE_CHECKING -import voluptuous as vol from yalesmartalarmclient.const import ( YALE_STATE_ARM_FULL, YALE_STATE_ARM_PARTIAL, YALE_STATE_DISARM, ) -from yalesmartalarmclient.exceptions import AuthenticationError, UnknownError -from homeassistant.components.alarm_control_panel import ( - PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, - AlarmControlPanelEntity, -) +from homeassistant.components.alarm_control_panel import AlarmControlPanelEntity from homeassistant.components.alarm_control_panel.const import ( SUPPORT_ALARM_ARM_AWAY, SUPPORT_ALARM_ARM_HOME, ) -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import CONF_NAME, CONF_PASSWORD, CONF_USERNAME +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError -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, DiscoveryInfoType, StateType -from homeassistant.helpers.update_coordinator import CoordinatorEntity +from homeassistant.helpers.typing import StateType -from .const import ( - CONF_AREA_ID, - COORDINATOR, - DEFAULT_AREA_ID, - DEFAULT_NAME, - DOMAIN, - LOGGER, - MANUFACTURER, - MODEL, - STATE_MAP, -) +from .const import COORDINATOR, DOMAIN, STATE_MAP, YALE_ALL_ERRORS from .coordinator import YaleDataUpdateCoordinator - -PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_AREA_ID, default=DEFAULT_AREA_ID): cv.string, - } -) - - -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Import Yale configuration from YAML.""" - LOGGER.warning( - "Loading Yale Alarm 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, - ) - ) +from .entity import YaleAlarmEntity async def async_setup_entry( @@ -81,11 +36,9 @@ async def async_setup_entry( ) -class YaleAlarmDevice(CoordinatorEntity, AlarmControlPanelEntity): +class YaleAlarmDevice(YaleAlarmEntity, AlarmControlPanelEntity): """Represent a Yale Smart Alarm.""" - coordinator: YaleDataUpdateCoordinator - _attr_code_arm_required = False _attr_supported_features = SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY @@ -94,90 +47,49 @@ class YaleAlarmDevice(CoordinatorEntity, AlarmControlPanelEntity): super().__init__(coordinator) self._attr_name = coordinator.entry.data[CONF_NAME] self._attr_unique_id = coordinator.entry.entry_id - self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, coordinator.entry.data[CONF_USERNAME])}, - manufacturer=MANUFACTURER, - model=MODEL, - name=self._attr_name, - ) - async def async_alarm_disarm(self, code=None) -> None: + async def async_alarm_disarm(self, code: str | None = None) -> None: """Send disarm command.""" - if TYPE_CHECKING: - assert self.coordinator.yale, "Connection to API is missing" + return await self.async_set_alarm(YALE_STATE_DISARM, code) - try: - alarm_state = await self.hass.async_add_executor_job( - self.coordinator.yale.disarm - ) - except ( - AuthenticationError, - ConnectionError, - TimeoutError, - UnknownError, - ) as error: - raise HomeAssistantError( - f"Could not verify disarmed for {self._attr_name}: {error}" - ) from error - - LOGGER.debug("Alarm disarmed: %s", alarm_state) - if alarm_state: - self.coordinator.data["alarm"] = YALE_STATE_DISARM - self.async_write_ha_state() - return - raise HomeAssistantError("Could not disarm, check system ready for disarming.") - - async def async_alarm_arm_home(self, code=None) -> None: + async def async_alarm_arm_home(self, code: str | None = None) -> None: """Send arm home command.""" - if TYPE_CHECKING: - assert self.coordinator.yale, "Connection to API is missing" + return await self.async_set_alarm(YALE_STATE_ARM_PARTIAL, code) - try: - alarm_state = await self.hass.async_add_executor_job( - self.coordinator.yale.arm_partial - ) - except ( - AuthenticationError, - ConnectionError, - TimeoutError, - UnknownError, - ) as error: - raise HomeAssistantError( - f"Could not verify armed home for {self._attr_name}: {error}" - ) from error - - LOGGER.debug("Alarm armed home: %s", alarm_state) - if alarm_state: - self.coordinator.data["alarm"] = YALE_STATE_ARM_PARTIAL - self.async_write_ha_state() - return - raise HomeAssistantError("Could not arm home, check system ready for arming.") - - async def async_alarm_arm_away(self, code=None) -> None: + async def async_alarm_arm_away(self, code: str | None = None) -> None: """Send arm away command.""" + return await self.async_set_alarm(YALE_STATE_ARM_FULL, code) + + async def async_set_alarm(self, command: str, code: str | None = None) -> None: + """Set alarm.""" if TYPE_CHECKING: assert self.coordinator.yale, "Connection to API is missing" try: - alarm_state = await self.hass.async_add_executor_job( - self.coordinator.yale.arm_full - ) - except ( - AuthenticationError, - ConnectionError, - TimeoutError, - UnknownError, - ) as error: + if command == YALE_STATE_ARM_FULL: + alarm_state = await self.hass.async_add_executor_job( + self.coordinator.yale.arm_full + ) + if command == YALE_STATE_ARM_PARTIAL: + alarm_state = await self.hass.async_add_executor_job( + self.coordinator.yale.arm_partial + ) + if command == YALE_STATE_DISARM: + alarm_state = await self.hass.async_add_executor_job( + self.coordinator.yale.disarm + ) + except YALE_ALL_ERRORS as error: raise HomeAssistantError( - f"Could not verify armed away for {self._attr_name}: {error}" + f"Could not set alarm for {self._attr_name}: {error}" ) from error - LOGGER.debug("Alarm armed away: %s", alarm_state) if alarm_state: - self.coordinator.data["alarm"] = YALE_STATE_ARM_FULL + self.coordinator.data["alarm"] = command self.async_write_ha_state() return - raise HomeAssistantError("Could not arm away, check system ready for arming.") + raise HomeAssistantError( + "Could not change alarm check system ready for arming." + ) @property def available(self) -> bool: diff --git a/homeassistant/components/yale_smart_alarm/binary_sensor.py b/homeassistant/components/yale_smart_alarm/binary_sensor.py index b017c4e33e3..566dbed8c33 100644 --- a/homeassistant/components/yale_smart_alarm/binary_sensor.py +++ b/homeassistant/components/yale_smart_alarm/binary_sensor.py @@ -4,14 +4,44 @@ from __future__ import annotations from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, BinarySensorEntity, + BinarySensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import COORDINATOR, DOMAIN from .coordinator import YaleDataUpdateCoordinator -from .entity import YaleEntity +from .entity import YaleAlarmEntity, YaleEntity + +SENSOR_TYPES = ( + BinarySensorEntityDescription( + key="acfail", + device_class=BinarySensorDeviceClass.PROBLEM, + entity_category=EntityCategory.DIAGNOSTIC, + name="Power Loss", + ), + BinarySensorEntityDescription( + key="battery", + device_class=BinarySensorDeviceClass.PROBLEM, + entity_category=EntityCategory.DIAGNOSTIC, + name="Battery", + ), + BinarySensorEntityDescription( + key="tamper", + device_class=BinarySensorDeviceClass.PROBLEM, + entity_category=EntityCategory.DIAGNOSTIC, + name="Tamper", + ), + BinarySensorEntityDescription( + key="jam", + device_class=BinarySensorDeviceClass.PROBLEM, + entity_category=EntityCategory.DIAGNOSTIC, + name="Jam", + ), +) async def async_setup_entry( @@ -22,18 +52,48 @@ async def async_setup_entry( coordinator: YaleDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][ COORDINATOR ] + sensors: list[YaleDoorSensor | YaleProblemSensor] = [] + for data in coordinator.data["door_windows"]: + sensors.append(YaleDoorSensor(coordinator, data)) + for description in SENSOR_TYPES: + sensors.append(YaleProblemSensor(coordinator, description)) - async_add_entities( - YaleBinarySensor(coordinator, data) for data in coordinator.data["door_windows"] - ) + async_add_entities(sensors) -class YaleBinarySensor(YaleEntity, BinarySensorEntity): - """Representation of a Yale binary sensor.""" +class YaleDoorSensor(YaleEntity, BinarySensorEntity): + """Representation of a Yale door sensor.""" _attr_device_class = BinarySensorDeviceClass.DOOR @property def is_on(self) -> bool: """Return true if the binary sensor is on.""" - return self.coordinator.data["sensor_map"][self._attr_unique_id] == "open" + return bool(self.coordinator.data["sensor_map"][self._attr_unique_id] == "open") + + +class YaleProblemSensor(YaleAlarmEntity, BinarySensorEntity): + """Representation of a Yale problem sensor.""" + + entity_description: BinarySensorEntityDescription + + def __init__( + self, + coordinator: YaleDataUpdateCoordinator, + entity_description: BinarySensorEntityDescription, + ) -> None: + """Initiate Yale Problem Sensor.""" + super().__init__(coordinator) + self.entity_description = entity_description + self._attr_name = ( + f"{coordinator.entry.data[CONF_NAME]} {entity_description.name}" + ) + self._attr_unique_id = f"{coordinator.entry.entry_id}-{entity_description.key}" + + @property + def is_on(self) -> bool: + """Return true if the binary sensor is on.""" + return bool( + self.coordinator.data["status"][self.entity_description.key] + != "main.normal" + ) diff --git a/homeassistant/components/yale_smart_alarm/config_flow.py b/homeassistant/components/yale_smart_alarm/config_flow.py index 62daa639f50..ae5f492bc6a 100644 --- a/homeassistant/components/yale_smart_alarm/config_flow.py +++ b/homeassistant/components/yale_smart_alarm/config_flow.py @@ -5,7 +5,7 @@ from typing import Any import voluptuous as vol from yalesmartalarmclient.client import YaleSmartAlarmClient -from yalesmartalarmclient.exceptions import AuthenticationError, UnknownError +from yalesmartalarmclient.exceptions import AuthenticationError from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow from homeassistant.const import CONF_CODE, CONF_NAME, CONF_PASSWORD, CONF_USERNAME @@ -21,6 +21,7 @@ from .const import ( DEFAULT_NAME, DOMAIN, LOGGER, + YALE_BASE_ERRORS, ) DATA_SCHEMA = vol.Schema( @@ -52,11 +53,6 @@ class YaleConfigFlow(ConfigFlow, domain=DOMAIN): """Get the options flow for this handler.""" return YaleOptionsFlowHandler(config_entry) - async def async_step_import(self, config: dict[str, Any]) -> FlowResult: - """Import a configuration from config.yaml.""" - - return await self.async_step_user(user_input=config) - async def async_step_reauth( self, user_input: dict[str, Any] | None = None ) -> FlowResult: @@ -81,7 +77,7 @@ class YaleConfigFlow(ConfigFlow, domain=DOMAIN): except AuthenticationError as error: LOGGER.error("Authentication failed. Check credentials %s", error) errors = {"base": "invalid_auth"} - except (ConnectionError, TimeoutError, UnknownError) as error: + except YALE_BASE_ERRORS as error: LOGGER.error("Connection to API failed %s", error) errors = {"base": "cannot_connect"} @@ -124,7 +120,7 @@ class YaleConfigFlow(ConfigFlow, domain=DOMAIN): except AuthenticationError as error: LOGGER.error("Authentication failed. Check credentials %s", error) errors = {"base": "invalid_auth"} - except (ConnectionError, TimeoutError, UnknownError) as error: + except YALE_BASE_ERRORS as error: LOGGER.error("Connection to API failed %s", error) errors = {"base": "cannot_connect"} diff --git a/homeassistant/components/yale_smart_alarm/const.py b/homeassistant/components/yale_smart_alarm/const.py index 0628e6aceb4..e506a2c70d6 100644 --- a/homeassistant/components/yale_smart_alarm/const.py +++ b/homeassistant/components/yale_smart_alarm/const.py @@ -6,6 +6,7 @@ from yalesmartalarmclient.client import ( YALE_STATE_ARM_PARTIAL, YALE_STATE_DISARM, ) +from yalesmartalarmclient.exceptions import AuthenticationError, UnknownError from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, @@ -40,3 +41,10 @@ STATE_MAP = { YALE_STATE_ARM_PARTIAL: STATE_ALARM_ARMED_HOME, YALE_STATE_ARM_FULL: STATE_ALARM_ARMED_AWAY, } + +YALE_BASE_ERRORS = ( + ConnectionError, + TimeoutError, + UnknownError, +) +YALE_ALL_ERRORS = (*YALE_BASE_ERRORS, AuthenticationError) diff --git a/homeassistant/components/yale_smart_alarm/coordinator.py b/homeassistant/components/yale_smart_alarm/coordinator.py index 2d476f920f9..1a350d1db98 100644 --- a/homeassistant/components/yale_smart_alarm/coordinator.py +++ b/homeassistant/components/yale_smart_alarm/coordinator.py @@ -2,9 +2,10 @@ from __future__ import annotations from datetime import timedelta +from typing import Any from yalesmartalarmclient.client import YaleSmartAlarmClient -from yalesmartalarmclient.exceptions import AuthenticationError, UnknownError +from yalesmartalarmclient.exceptions import AuthenticationError from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME @@ -12,7 +13,7 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import DEFAULT_SCAN_INTERVAL, DOMAIN, LOGGER +from .const import DEFAULT_SCAN_INTERVAL, DOMAIN, LOGGER, YALE_BASE_ERRORS class YaleDataUpdateCoordinator(DataUpdateCoordinator): @@ -29,7 +30,7 @@ class YaleDataUpdateCoordinator(DataUpdateCoordinator): update_interval=timedelta(seconds=DEFAULT_SCAN_INTERVAL), ) - async def _async_update_data(self) -> dict: + async def _async_update_data(self) -> dict[str, Any]: """Fetch data from Yale.""" updates = await self.hass.async_add_executor_job(self.get_updates) @@ -118,9 +119,10 @@ class YaleDataUpdateCoordinator(DataUpdateCoordinator): "online": updates["online"], "sensor_map": _sensor_map, "lock_map": _lock_map, + "panel_info": updates["panel_info"], } - def get_updates(self) -> dict: + def get_updates(self) -> dict[str, Any]: """Fetch data from Yale.""" if self.yale is None: @@ -130,18 +132,20 @@ class YaleDataUpdateCoordinator(DataUpdateCoordinator): ) except AuthenticationError as error: raise ConfigEntryAuthFailed from error - except (ConnectionError, TimeoutError, UnknownError) as error: + except YALE_BASE_ERRORS as error: raise UpdateFailed from error try: arm_status = self.yale.get_armed_status() - cycle = self.yale.get_cycle() - status = self.yale.get_status() - online = self.yale.get_online() + data = self.yale.get_all() + cycle = data["CYCLE"] + status = data["STATUS"] + online = data["ONLINE"] + panel_info = data["PANEL INFO"] except AuthenticationError as error: raise ConfigEntryAuthFailed from error - except (ConnectionError, TimeoutError, UnknownError) as error: + except YALE_BASE_ERRORS as error: raise UpdateFailed from error return { @@ -149,4 +153,5 @@ class YaleDataUpdateCoordinator(DataUpdateCoordinator): "cycle": cycle, "status": status, "online": online, + "panel_info": panel_info, } diff --git a/homeassistant/components/yale_smart_alarm/diagnostics.py b/homeassistant/components/yale_smart_alarm/diagnostics.py index 896a3240a22..c650ff5f5ed 100644 --- a/homeassistant/components/yale_smart_alarm/diagnostics.py +++ b/homeassistant/components/yale_smart_alarm/diagnostics.py @@ -15,8 +15,10 @@ TO_REDACT = { "name", "mac", "device_id", - "sensor_map", - "lock_map", + "user_id", + "id", + "mail_address", + "report_account", } @@ -27,4 +29,7 @@ async def async_get_config_entry_diagnostics( coordinator: YaleDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][ COORDINATOR ] - return async_redact_data(coordinator.data, TO_REDACT) + + assert coordinator.yale + get_all_data = await hass.async_add_executor_job(coordinator.yale.get_all) + return async_redact_data(get_all_data, TO_REDACT) diff --git a/homeassistant/components/yale_smart_alarm/entity.py b/homeassistant/components/yale_smart_alarm/entity.py index 318989a018c..b9a832f88e8 100644 --- a/homeassistant/components/yale_smart_alarm/entity.py +++ b/homeassistant/components/yale_smart_alarm/entity.py @@ -1,6 +1,7 @@ """Base class for yale_smart_alarm entity.""" -from homeassistant.const import CONF_USERNAME +from homeassistant.const import CONF_NAME, CONF_USERNAME +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.entity import DeviceInfo, Entity from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -8,11 +9,9 @@ from .const import DOMAIN, MANUFACTURER, MODEL from .coordinator import YaleDataUpdateCoordinator -class YaleEntity(CoordinatorEntity, Entity): +class YaleEntity(CoordinatorEntity[YaleDataUpdateCoordinator], Entity): """Base implementation for Yale device.""" - coordinator: YaleDataUpdateCoordinator - def __init__(self, coordinator: YaleDataUpdateCoordinator, data: dict) -> None: """Initialize an Yale device.""" super().__init__(coordinator) @@ -25,3 +24,20 @@ class YaleEntity(CoordinatorEntity, Entity): identifiers={(DOMAIN, data["address"])}, via_device=(DOMAIN, self.coordinator.entry.data[CONF_USERNAME]), ) + + +class YaleAlarmEntity(CoordinatorEntity[YaleDataUpdateCoordinator], Entity): + """Base implementation for Yale Alarm device.""" + + def __init__(self, coordinator: YaleDataUpdateCoordinator) -> None: + """Initialize an Yale device.""" + super().__init__(coordinator) + panel_info = coordinator.data["panel_info"] + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, coordinator.entry.data[CONF_USERNAME])}, + manufacturer=MANUFACTURER, + model=MODEL, + name=coordinator.entry.data[CONF_NAME], + connections={(CONNECTION_NETWORK_MAC, panel_info["mac"])}, + sw_version=panel_info["version"], + ) diff --git a/homeassistant/components/yale_smart_alarm/lock.py b/homeassistant/components/yale_smart_alarm/lock.py index a7231d78dce..a97a98a2afb 100644 --- a/homeassistant/components/yale_smart_alarm/lock.py +++ b/homeassistant/components/yale_smart_alarm/lock.py @@ -1,9 +1,7 @@ """Lock for Yale Alarm.""" from __future__ import annotations -from typing import TYPE_CHECKING - -from yalesmartalarmclient.exceptions import AuthenticationError, UnknownError +from typing import TYPE_CHECKING, Any from homeassistant.components.lock import LockEntity from homeassistant.config_entries import ConfigEntry @@ -17,7 +15,7 @@ from .const import ( COORDINATOR, DEFAULT_LOCK_CODE_DIGITS, DOMAIN, - LOGGER, + YALE_ALL_ERRORS, ) from .coordinator import YaleDataUpdateCoordinator from .entity import YaleEntity @@ -49,46 +47,19 @@ class YaleDoorlock(YaleEntity, LockEntity): super().__init__(coordinator, data) self._attr_code_format = f"^\\d{code_format}$" - async def async_unlock(self, **kwargs) -> None: + async def async_unlock(self, **kwargs: Any) -> None: """Send unlock command.""" - if TYPE_CHECKING: - assert self.coordinator.yale, "Connection to API is missing" + code: str | None = kwargs.get( + ATTR_CODE, self.coordinator.entry.options.get(CONF_CODE) + ) + return await self.async_set_lock("unlocked", code) - code = kwargs.get(ATTR_CODE, self.coordinator.entry.options.get(CONF_CODE)) - - if not code: - raise HomeAssistantError( - f"No code provided, {self._attr_name} not unlocked" - ) - - try: - get_lock = await self.hass.async_add_executor_job( - self.coordinator.yale.lock_api.get, self._attr_name - ) - lock_state = await self.hass.async_add_executor_job( - self.coordinator.yale.lock_api.open_lock, - get_lock, - code, - ) - except ( - AuthenticationError, - ConnectionError, - TimeoutError, - UnknownError, - ) as error: - raise HomeAssistantError( - f"Could not verify unlocking for {self._attr_name}: {error}" - ) from error - - LOGGER.debug("Door unlock: %s", lock_state) - if lock_state: - self.coordinator.data["lock_map"][self._attr_unique_id] = "unlocked" - self.async_write_ha_state() - return - raise HomeAssistantError("Could not unlock, check system ready for unlocking") - - async def async_lock(self, **kwargs) -> None: + async def async_lock(self, **kwargs: Any) -> None: """Send lock command.""" + return await self.async_set_lock("locked", None) + + async def async_set_lock(self, command: str, code: str | None) -> None: + """Set lock.""" if TYPE_CHECKING: assert self.coordinator.yale, "Connection to API is missing" @@ -96,28 +67,27 @@ class YaleDoorlock(YaleEntity, LockEntity): get_lock = await self.hass.async_add_executor_job( self.coordinator.yale.lock_api.get, self._attr_name ) - lock_state = await self.hass.async_add_executor_job( - self.coordinator.yale.lock_api.close_lock, - get_lock, - ) - except ( - AuthenticationError, - ConnectionError, - TimeoutError, - UnknownError, - ) as error: + if command == "locked": + lock_state = await self.hass.async_add_executor_job( + self.coordinator.yale.lock_api.close_lock, + get_lock, + ) + if command == "unlocked": + lock_state = await self.hass.async_add_executor_job( + self.coordinator.yale.lock_api.open_lock, get_lock, code + ) + except YALE_ALL_ERRORS as error: raise HomeAssistantError( - f"Could not verify unlocking for {self._attr_name}: {error}" + f"Could not set lock for {self._attr_name}: {error}" ) from error - LOGGER.debug("Door unlock: %s", lock_state) if lock_state: - self.coordinator.data["lock_map"][self._attr_unique_id] = "unlocked" + self.coordinator.data["lock_map"][self._attr_unique_id] = command self.async_write_ha_state() return - raise HomeAssistantError("Could not unlock, check system ready for unlocking") + raise HomeAssistantError("Could set lock, check system ready for lock.") @property def is_locked(self) -> bool | None: """Return true if the lock is locked.""" - return self.coordinator.data["lock_map"][self._attr_unique_id] == "locked" + return bool(self.coordinator.data["lock_map"][self._attr_unique_id] == "locked") diff --git a/homeassistant/components/yale_smart_alarm/strings.json b/homeassistant/components/yale_smart_alarm/strings.json index 5258e681c05..6a6443c1b9b 100644 --- a/homeassistant/components/yale_smart_alarm/strings.json +++ b/homeassistant/components/yale_smart_alarm/strings.json @@ -40,4 +40,4 @@ "code_format_mismatch": "The code does not match the required number of digits" } } -} \ No newline at end of file +} diff --git a/homeassistant/components/yale_smart_alarm/translations/fr.json b/homeassistant/components/yale_smart_alarm/translations/fr.json index 76065006684..f003f11b161 100644 --- a/homeassistant/components/yale_smart_alarm/translations/fr.json +++ b/homeassistant/components/yale_smart_alarm/translations/fr.json @@ -5,8 +5,8 @@ "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" }, "error": { - "cannot_connect": "Impossible de se connecter", - "invalid_auth": "Authentification invalide" + "cannot_connect": "\u00c9chec de connexion", + "invalid_auth": "Authentification non valide" }, "step": { "reauth_confirm": { diff --git a/homeassistant/components/yamaha_musiccast/__init__.py b/homeassistant/components/yamaha_musiccast/__init__.py index d984aaceb96..e28712cdf21 100644 --- a/homeassistant/components/yamaha_musiccast/__init__.py +++ b/homeassistant/components/yamaha_musiccast/__init__.py @@ -120,11 +120,9 @@ class MusicCastDataUpdateCoordinator(DataUpdateCoordinator[MusicCastData]): return self.musiccast.data -class MusicCastEntity(CoordinatorEntity): +class MusicCastEntity(CoordinatorEntity[MusicCastDataUpdateCoordinator]): """Defines a base MusicCast entity.""" - coordinator: MusicCastDataUpdateCoordinator - def __init__( self, *, diff --git a/homeassistant/components/yamaha_musiccast/config_flow.py b/homeassistant/components/yamaha_musiccast/config_flow.py index 4049a5d6a37..94153a47fdc 100644 --- a/homeassistant/components/yamaha_musiccast/config_flow.py +++ b/homeassistant/components/yamaha_musiccast/config_flow.py @@ -131,18 +131,3 @@ class MusicCastFlowHandler(ConfigFlow, domain=DOMAIN): ) return self.async_show_form(step_id="confirm") - - async def async_step_import(self, import_data: dict) -> data_entry_flow.FlowResult: - """Import data from configuration.yaml into the config flow.""" - res = await self.async_step_user(import_data) - if res["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY: - _LOGGER.info( - "Successfully imported %s from configuration.yaml", - import_data.get(CONF_HOST), - ) - elif res["type"] == data_entry_flow.RESULT_TYPE_FORM: - _LOGGER.error( - "Could not import %s from configuration.yaml", - import_data.get(CONF_HOST), - ) - return res diff --git a/homeassistant/components/yamaha_musiccast/const.py b/homeassistant/components/yamaha_musiccast/const.py index 21b7de82184..7846cab1754 100644 --- a/homeassistant/components/yamaha_musiccast/const.py +++ b/homeassistant/components/yamaha_musiccast/const.py @@ -34,7 +34,6 @@ HA_REPEAT_MODE_TO_MC_MAPPING = { NULL_GROUP = "00000000000000000000000000000000" -INTERVAL_SECONDS = "interval_seconds" MC_REPEAT_MODE_TO_HA_MAPPING = { val: key for key, val in HA_REPEAT_MODE_TO_MC_MAPPING.items() diff --git a/homeassistant/components/yamaha_musiccast/manifest.json b/homeassistant/components/yamaha_musiccast/manifest.json index a50ef69d57e..86115e77988 100644 --- a/homeassistant/components/yamaha_musiccast/manifest.json +++ b/homeassistant/components/yamaha_musiccast/manifest.json @@ -3,21 +3,14 @@ "name": "MusicCast", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/yamaha_musiccast", - "requirements": [ - "aiomusiccast==0.14.3" - ], + "requirements": ["aiomusiccast==0.14.3"], "ssdp": [ { "manufacturer": "Yamaha Corporation" } ], - "dependencies": [ - "ssdp" - ], + "dependencies": ["ssdp"], "iot_class": "local_push", - "codeowners": [ - "@vigonotion", - "@micha91" - ], + "codeowners": ["@vigonotion", "@micha91"], "loggers": ["aiomusiccast"] -} \ No newline at end of file +} diff --git a/homeassistant/components/yamaha_musiccast/media_player.py b/homeassistant/components/yamaha_musiccast/media_player.py index bcd1a0a2c1f..3618bc78dbe 100644 --- a/homeassistant/components/yamaha_musiccast/media_player.py +++ b/homeassistant/components/yamaha_musiccast/media_player.py @@ -1,16 +1,16 @@ """Implementation of the musiccast media player.""" from __future__ import annotations +import contextlib import logging from aiomusiccast import MusicCastGroupException, MusicCastMediaContent from aiomusiccast.features import ZoneFeature -import voluptuous as vol -from homeassistant.components.media_player import ( - PLATFORM_SCHEMA, - BrowseMedia, - MediaPlayerEntity, +from homeassistant.components import media_source +from homeassistant.components.media_player import BrowseMedia, MediaPlayerEntity +from homeassistant.components.media_player.browse_media import ( + async_process_play_media_url, ) from homeassistant.components.media_player.const import ( MEDIA_CLASS_DIRECTORY, @@ -35,21 +35,12 @@ from homeassistant.components.media_player.const import ( SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP, ) -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import ( - CONF_HOST, - CONF_PORT, - STATE_IDLE, - STATE_OFF, - STATE_PAUSED, - STATE_PLAYING, -) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util import uuid from . import MusicCastDataUpdateCoordinator, MusicCastDeviceEntity @@ -59,7 +50,6 @@ from .const import ( DEFAULT_ZONE, DOMAIN, HA_REPEAT_MODE_TO_MC_MAPPING, - INTERVAL_SECONDS, MC_REPEAT_MODE_TO_HA_MAPPING, MEDIA_CLASS_MAPPING, NULL_GROUP, @@ -76,42 +66,6 @@ MUSIC_PLAYER_BASE_SUPPORT = ( | SUPPORT_PLAY_MEDIA ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_PORT, default=5000): cv.port, - vol.Optional(INTERVAL_SECONDS, default=0): cv.positive_int, - } -) - - -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_devices: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Import legacy configurations.""" - - if hass.config_entries.async_entries(DOMAIN) and config[CONF_HOST] not in [ - entry.data[CONF_HOST] for entry in hass.config_entries.async_entries(DOMAIN) - ]: - _LOGGER.error( - "Configuration in configuration.yaml is not supported anymore. " - "Please add this device using the config flow: %s", - config[CONF_HOST], - ) - else: - _LOGGER.warning( - "Configuration in configuration.yaml is deprecated. Use the config flow instead" - ) - - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=config - ) - ) - async def async_setup_entry( hass: HomeAssistant, @@ -333,6 +287,10 @@ class MusicCastMediaPlayer(MusicCastDeviceEntity, MediaPlayerEntity): async def async_play_media(self, media_type: str, media_id: str, **kwargs) -> None: """Play media.""" + if media_source.is_media_source_id(media_id): + play_item = await media_source.async_resolve_media(self.hass, media_id) + media_id = play_item.url + if self.state == STATE_OFF: await self.async_turn_on() @@ -353,7 +311,9 @@ class MusicCastMediaPlayer(MusicCastDeviceEntity, MediaPlayerEntity): ) return - if parts[0] == "http": + if parts[0] in ("http", "https") or media_id.startswith("/"): + media_id = async_process_play_media_url(self.hass, media_id) + await self.coordinator.musiccast.play_url_media( self._zone_id, media_id, "HomeAssistant" ) @@ -365,6 +325,15 @@ class MusicCastMediaPlayer(MusicCastDeviceEntity, MediaPlayerEntity): async def async_browse_media(self, media_content_type=None, media_content_id=None): """Implement the websocket media browsing helper.""" + if media_content_id and media_source.is_media_source_id(media_content_id): + return await media_source.async_browse_media( + self.hass, + media_content_id, + content_filter=lambda item: item.media_content_type.startswith( + "audio/" + ), + ) + if self.state == STATE_OFF: raise HomeAssistantError( "The device has to be turned on to be able to browse media." @@ -375,11 +344,13 @@ class MusicCastMediaPlayer(MusicCastDeviceEntity, MediaPlayerEntity): media_content_provider = await MusicCastMediaContent.browse_media( self.coordinator.musiccast, self._zone_id, media_content_path, 24 ) + add_media_source = False else: media_content_provider = MusicCastMediaContent.categories( self.coordinator.musiccast, self._zone_id ) + add_media_source = True def get_content_type(item): if item.can_play: @@ -399,6 +370,21 @@ class MusicCastMediaPlayer(MusicCastDeviceEntity, MediaPlayerEntity): for child in media_content_provider.children ] + if add_media_source: + with contextlib.suppress(media_source.BrowseError): + item = await media_source.async_browse_media( + self.hass, + None, + content_filter=lambda item: item.media_content_type.startswith( + "audio/" + ), + ) + # If domain is None, it's overview of available sources + if item.domain is None: + children.extend(item.children) + else: + children.append(item) + overview = BrowseMedia( title=media_content_provider.title, media_class=MEDIA_CLASS_MAPPING.get(media_content_provider.content_type), diff --git a/homeassistant/components/yamaha_musiccast/strings.json b/homeassistant/components/yamaha_musiccast/strings.json index e2261882222..145c8a2849e 100644 --- a/homeassistant/components/yamaha_musiccast/strings.json +++ b/homeassistant/components/yamaha_musiccast/strings.json @@ -20,4 +20,4 @@ "no_musiccast_device": "This device seems to be no MusicCast Device." } } -} \ No newline at end of file +} diff --git a/homeassistant/components/yamaha_musiccast/strings.select.json b/homeassistant/components/yamaha_musiccast/strings.select.json index 59c763017bf..a59eef6070d 100644 --- a/homeassistant/components/yamaha_musiccast/strings.select.json +++ b/homeassistant/components/yamaha_musiccast/strings.select.json @@ -1,52 +1,52 @@ { - "state": { - "yamaha_musiccast__dimmer": { - "auto": "Auto" - }, - "yamaha_musiccast__zone_sleep": { - "off": "Off", - "30 min": "30 Minutes", - "60 min": "60 Minutes", - "90 min": "90 Minutes", - "120 min": "120 Minutes" - }, - "yamaha_musiccast__zone_tone_control_mode": { - "manual": "Manual", - "auto": "Auto", - "bypass": "Bypass" - }, - "yamaha_musiccast__zone_surr_decoder_type": { - "toggle": "Toggle", - "auto": "Auto", - "dolby_pl": "Dolby ProLogic", - "dolby_pl2x_movie": "Dolby ProLogic 2x Movie", - "dolby_pl2x_music": "Dolby ProLogic 2x Music", - "dolby_pl2x_game": "Dolby ProLogic 2x Game", - "dolby_surround": "Dolby Surround", - "dts_neural_x": "DTS Neural:X", - "dts_neo6_cinema": "DTS Neo:6 Cinema", - "dts_neo6_music": "DTS Neo:6 Music" - }, - "yamaha_musiccast__zone_equalizer_mode": { - "manual": "Manual", - "auto": "Auto", - "bypass": "Bypass" - }, - "yamaha_musiccast__zone_link_audio_quality": { - "compressed": "Compressed", - "uncompressed": "Uncompressed" - }, - "yamaha_musiccast__zone_link_control": { - "standard": "Standard", - "speed": "Speed", - "stability": "Stability" - }, - "yamaha_musiccast__zone_link_audio_delay": { - "audio_sync_on": "Audio Synchronization On", - "audio_sync_off": "Audio Synchronization Off", - "balanced": "Balanced", - "lip_sync": "Lip Synchronization", - "audio_sync": "Audio Synchronization" - } + "state": { + "yamaha_musiccast__dimmer": { + "auto": "Auto" + }, + "yamaha_musiccast__zone_sleep": { + "off": "Off", + "30 min": "30 Minutes", + "60 min": "60 Minutes", + "90 min": "90 Minutes", + "120 min": "120 Minutes" + }, + "yamaha_musiccast__zone_tone_control_mode": { + "manual": "Manual", + "auto": "Auto", + "bypass": "Bypass" + }, + "yamaha_musiccast__zone_surr_decoder_type": { + "toggle": "Toggle", + "auto": "Auto", + "dolby_pl": "Dolby ProLogic", + "dolby_pl2x_movie": "Dolby ProLogic 2x Movie", + "dolby_pl2x_music": "Dolby ProLogic 2x Music", + "dolby_pl2x_game": "Dolby ProLogic 2x Game", + "dolby_surround": "Dolby Surround", + "dts_neural_x": "DTS Neural:X", + "dts_neo6_cinema": "DTS Neo:6 Cinema", + "dts_neo6_music": "DTS Neo:6 Music" + }, + "yamaha_musiccast__zone_equalizer_mode": { + "manual": "Manual", + "auto": "Auto", + "bypass": "Bypass" + }, + "yamaha_musiccast__zone_link_audio_quality": { + "compressed": "Compressed", + "uncompressed": "Uncompressed" + }, + "yamaha_musiccast__zone_link_control": { + "standard": "Standard", + "speed": "Speed", + "stability": "Stability" + }, + "yamaha_musiccast__zone_link_audio_delay": { + "audio_sync_on": "Audio Synchronization On", + "audio_sync_off": "Audio Synchronization Off", + "balanced": "Balanced", + "lip_sync": "Lip Synchronization", + "audio_sync": "Audio Synchronization" } -} \ No newline at end of file + } +} diff --git a/homeassistant/components/yamaha_musiccast/translations/fr.json b/homeassistant/components/yamaha_musiccast/translations/fr.json index 14cbec9e877..0a8671dc2aa 100644 --- a/homeassistant/components/yamaha_musiccast/translations/fr.json +++ b/homeassistant/components/yamaha_musiccast/translations/fr.json @@ -10,7 +10,7 @@ "flow_title": "MusicCast: {name}", "step": { "confirm": { - "description": "Voulez-vous commencer la configuration ?" + "description": "Voulez-vous commencer la configuration\u00a0?" }, "user": { "data": { diff --git a/homeassistant/components/yeelight/__init__.py b/homeassistant/components/yeelight/__init__.py index 4d226f233b2..0bca1267565 100644 --- a/homeassistant/components/yeelight/__init__.py +++ b/homeassistant/components/yeelight/__init__.py @@ -13,6 +13,7 @@ from homeassistant.const import ( CONF_DEVICES, CONF_HOST, CONF_ID, + CONF_MODEL, CONF_NAME, EVENT_HOMEASSISTANT_STOP, ) @@ -31,7 +32,6 @@ from .const import ( CONF_DETECTED_MODEL, CONF_FLOW_PARAMS, CONF_MODE_MUSIC, - CONF_MODEL, CONF_NIGHTLIGHT_SWITCH, CONF_NIGHTLIGHT_SWITCH_TYPE, CONF_SAVE_ON_CHANGE, diff --git a/homeassistant/components/yeelight/config_flow.py b/homeassistant/components/yeelight/config_flow.py index 8dd127502e2..8a3a5b41320 100644 --- a/homeassistant/components/yeelight/config_flow.py +++ b/homeassistant/components/yeelight/config_flow.py @@ -11,7 +11,7 @@ from yeelight.main import get_known_models from homeassistant import config_entries, exceptions from homeassistant.components import dhcp, ssdp, zeroconf from homeassistant.config_entries import ConfigEntryState -from homeassistant.const import CONF_DEVICE, CONF_HOST, CONF_ID, CONF_NAME +from homeassistant.const import CONF_DEVICE, CONF_HOST, CONF_ID, CONF_MODEL, CONF_NAME from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult import homeassistant.helpers.config_validation as cv @@ -19,7 +19,6 @@ import homeassistant.helpers.config_validation as cv from .const import ( CONF_DETECTED_MODEL, CONF_MODE_MUSIC, - CONF_MODEL, CONF_NIGHTLIGHT_SWITCH, CONF_NIGHTLIGHT_SWITCH_TYPE, CONF_SAVE_ON_CHANGE, diff --git a/homeassistant/components/yeelight/const.py b/homeassistant/components/yeelight/const.py index 28b5591dcbf..e9ba80bca95 100644 --- a/homeassistant/components/yeelight/const.py +++ b/homeassistant/components/yeelight/const.py @@ -33,7 +33,6 @@ DEFAULT_MODE_MUSIC = False DEFAULT_SAVE_ON_CHANGE = False DEFAULT_NIGHTLIGHT_SWITCH = False -CONF_MODEL = "model" CONF_DETECTED_MODEL = "detected_model" CONF_TRANSITION = "transition" diff --git a/homeassistant/components/yeelight/device.py b/homeassistant/components/yeelight/device.py index 02228e5d9fc..6737f96c169 100644 --- a/homeassistant/components/yeelight/device.py +++ b/homeassistant/components/yeelight/device.py @@ -219,6 +219,7 @@ class YeelightDevice: @callback def async_update_callback(self, data): """Update push from device.""" + _LOGGER.debug("Received callback: %s", data) was_available = self._available self._available = data.get(KEY_CONNECTED, True) if update_needs_bg_power_workaround(data) or ( diff --git a/homeassistant/components/yeelight/light.py b/homeassistant/components/yeelight/light.py index 0c906b3e268..1de97c45fd0 100644 --- a/homeassistant/components/yeelight/light.py +++ b/homeassistant/components/yeelight/light.py @@ -245,7 +245,7 @@ def _parse_custom_effects(effects_config): def _async_cmd(func): """Define a wrapper to catch exceptions from the bulb.""" - async def _async_wrap(self, *args, **kwargs): + async def _async_wrap(self: "YeelightGenericLight", *args, **kwargs): for attempts in range(2): try: _LOGGER.debug("Calling %s with %s %s", func, args, kwargs) @@ -708,7 +708,7 @@ class YeelightGenericLight(YeelightEntity, LightEntity): """Activate flash.""" if not flash: return - if int(self._bulb.last_properties["color_mode"]) != 1: + if int(self._get_property("color_mode")) != 1: _LOGGER.error("Flash supported currently only in RGB mode") return diff --git a/homeassistant/components/yeelight/manifest.json b/homeassistant/components/yeelight/manifest.json index 7aa46881968..1967793b855 100644 --- a/homeassistant/components/yeelight/manifest.json +++ b/homeassistant/components/yeelight/manifest.json @@ -2,7 +2,7 @@ "domain": "yeelight", "name": "Yeelight", "documentation": "https://www.home-assistant.io/integrations/yeelight", - "requirements": ["yeelight==0.7.9", "async-upnp-client==0.23.5"], + "requirements": ["yeelight==0.7.10", "async-upnp-client==0.27.0"], "codeowners": ["@zewelor", "@shenxn", "@starkillerOG", "@alexyao2015"], "config_flow": true, "dependencies": ["network"], diff --git a/homeassistant/components/yeelight/scanner.py b/homeassistant/components/yeelight/scanner.py index 1756fbe865c..6876b93a0eb 100644 --- a/homeassistant/components/yeelight/scanner.py +++ b/homeassistant/components/yeelight/scanner.py @@ -67,12 +67,13 @@ class YeelightScanner: return _async_connected + source = (str(source_ip), 0) self._listeners.append( SsdpSearchListener( async_callback=self._async_process_entry, service_type=SSDP_ST, target=SSDP_TARGET, - source_ip=source_ip, + source=source, async_connect_callback=_wrap_async_connected_idx(idx), ) ) @@ -87,7 +88,7 @@ class YeelightScanner: continue _LOGGER.warning( "Failed to setup listener for %s: %s", - self._listeners[idx].source_ip, + self._listeners[idx].source, result, ) failed_listeners.append(self._listeners[idx]) diff --git a/homeassistant/components/yeelight/services.yaml b/homeassistant/components/yeelight/services.yaml index b365f273e31..d7850b34607 100644 --- a/homeassistant/components/yeelight/services.yaml +++ b/homeassistant/components/yeelight/services.yaml @@ -13,13 +13,18 @@ set_mode: selector: select: options: - - 'color_flow' - - 'hsv' - - 'last' - - 'moonlight' - - 'normal' - - 'rgb' - + - label: "Color Flow" + value: "color_flow" + - label: "HSV" + value: "hsv" + - label: "Last" + value: "last" + - label: "Moonlight" + value: "moonlight" + - label: "Normal" + value: "normal" + - label: "RGB" + value: "rgb" set_color_scene: name: Set color scene description: Changes the light to the specified RGB color and brightness. If the light is off, it will be turned on. @@ -108,13 +113,16 @@ set_color_flow_scene: action: name: Action description: The action to take after the flow stops. - default: 'recover' + default: "recover" selector: select: options: - - 'off' - - 'recover' - - 'stay' + - label: "Off" + value: "off" + - label: "Recover" + value: "recover" + - label: "Stay" + value: "stay" transitions: name: Transitions description: Array of transitions, for desired effect. Examples https://yeelight.readthedocs.io/en/stable/flow.html @@ -164,13 +172,16 @@ start_flow: action: name: Action description: The action to take after the flow stops. - default: 'recover' + default: "recover" selector: select: options: - - 'off' - - 'recover' - - 'stay' + - label: "Off" + value: "off" + - label: "Recover" + value: "recover" + - label: "Stay" + value: "stay" transitions: name: Transitions description: Array of transitions, for desired effect. Examples https://yeelight.readthedocs.io/en/stable/flow.html diff --git a/homeassistant/components/yeelight/translations/el.json b/homeassistant/components/yeelight/translations/el.json index b125dae0bc6..8b6c0ee2b9c 100644 --- a/homeassistant/components/yeelight/translations/el.json +++ b/homeassistant/components/yeelight/translations/el.json @@ -29,7 +29,7 @@ "step": { "init": { "data": { - "model": "\u039c\u03bf\u03bd\u03c4\u03ad\u03bb\u03bf (\u03a0\u03c1\u03bf\u03b1\u03b9\u03c1\u03b5\u03c4\u03b9\u03ba\u03cc)", + "model": "\u039c\u03bf\u03bd\u03c4\u03ad\u03bb\u03bf", "nightlight_switch": "\u03a7\u03c1\u03ae\u03c3\u03b7 \u03b4\u03b9\u03b1\u03ba\u03cc\u03c0\u03c4\u03b7 \u03bd\u03c5\u03c7\u03c4\u03b5\u03c1\u03b9\u03bd\u03bf\u03cd \u03c6\u03c9\u03c4\u03b9\u03c3\u03bc\u03bf\u03cd", "save_on_change": "\u0391\u03c0\u03bf\u03b8\u03ae\u03ba\u03b5\u03c5\u03c3\u03b7 \u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7\u03c2 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03b1\u03bb\u03bb\u03b1\u03b3\u03ae", "transition": "\u03a7\u03c1\u03cc\u03bd\u03bf\u03c2 \u03bc\u03b5\u03c4\u03ac\u03b2\u03b1\u03c3\u03b7\u03c2 (ms)", diff --git a/homeassistant/components/yeelight/translations/fr.json b/homeassistant/components/yeelight/translations/fr.json index 9682f0d9b6f..b2153e6aec0 100644 --- a/homeassistant/components/yeelight/translations/fr.json +++ b/homeassistant/components/yeelight/translations/fr.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "\u00c9chec de connexion" }, - "flow_title": "{model} {host}", + "flow_title": "{model} {id} ({host})", "step": { "discovery_confirm": { "description": "Voulez-vous configurer {model} ({host})\u00a0?" @@ -21,7 +21,7 @@ "data": { "host": "H\u00f4te" }, - "description": "Si vous laissez l'adresse IP vide, la d\u00e9couverte sera utilis\u00e9e pour trouver des appareils." + "description": "Si vous laissez l'h\u00f4te vide, la d\u00e9couverte sera utilis\u00e9e pour trouver des appareils." } } }, @@ -29,10 +29,10 @@ "step": { "init": { "data": { - "model": "Mod\u00e8le (facultatif)", - "nightlight_switch": "Utiliser l'interrupteur de la veilleuse", - "save_on_change": "Sauvegarder le statut lors d'un changement", - "transition": "Temps de transition (ms)", + "model": "Mod\u00e8le", + "nightlight_switch": "Utiliser le commutateur de veilleuse", + "save_on_change": "Enregistrer l'\u00e9tat lors d'un changement", + "transition": "Dur\u00e9e de transition (en millisecondes)", "use_music_mode": "Activer le mode musique" }, "description": "Si vous ne pr\u00e9cisez pas le mod\u00e8le, il sera automatiquement d\u00e9tect\u00e9." diff --git a/homeassistant/components/yeelight/translations/zh-Hant.json b/homeassistant/components/yeelight/translations/zh-Hant.json index 785584a6f2a..7601a0b9552 100644 --- a/homeassistant/components/yeelight/translations/zh-Hant.json +++ b/homeassistant/components/yeelight/translations/zh-Hant.json @@ -21,7 +21,7 @@ "data": { "host": "\u4e3b\u6a5f\u7aef" }, - "description": "\u5047\u5982\u4e3b\u6a5f\u7aef\u4f4d\u5740\u6b04\u4f4d\u70ba\u7a7a\u767d\uff0c\u5c07\u6703\u63a2\u7d22\u6240\u6709\u53ef\u7528\u88dd\u7f6e\u3002" + "description": "\u5047\u5982\u4e3b\u6a5f\u7aef\u4f4d\u5740\u6b04\u4f4d\u70ba\u7a7a\u767d\uff0c\u5c07\u6703\u641c\u7d22\u6240\u6709\u53ef\u7528\u88dd\u7f6e\u3002" } } }, diff --git a/homeassistant/components/youless/strings.json b/homeassistant/components/youless/strings.json index 3728db7ffe6..563e6834ddd 100644 --- a/homeassistant/components/youless/strings.json +++ b/homeassistant/components/youless/strings.json @@ -12,4 +12,4 @@ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" } } -} \ No newline at end of file +} diff --git a/homeassistant/components/zengge/manifest.json b/homeassistant/components/zengge/manifest.json index 98f2ab1de21..79ae849dd36 100644 --- a/homeassistant/components/zengge/manifest.json +++ b/homeassistant/components/zengge/manifest.json @@ -2,8 +2,8 @@ "domain": "zengge", "name": "Zengge", "documentation": "https://www.home-assistant.io/integrations/zengge", - "requirements": ["zengge==0.2"], - "codeowners": [], + "requirements": ["bluepy==1.3.0", "zengge==0.2"], + "codeowners": ["@emontnemery"], "iot_class": "local_polling", "loggers": ["zengge"] } diff --git a/homeassistant/components/zerproc/translations/fr.json b/homeassistant/components/zerproc/translations/fr.json index e9ae4e0b644..3d447c0cbb0 100644 --- a/homeassistant/components/zerproc/translations/fr.json +++ b/homeassistant/components/zerproc/translations/fr.json @@ -6,7 +6,7 @@ }, "step": { "confirm": { - "description": "Voulez-vous commencer la configuration ?" + "description": "Voulez-vous commencer la configuration\u00a0?" } } } diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index 1d5656a1b8d..c512e104ae8 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -120,8 +120,9 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b async def async_zha_shutdown(event): """Handle shutdown tasks.""" - await zha_data[DATA_ZHA_GATEWAY].shutdown() - await zha_data[DATA_ZHA_GATEWAY].async_update_device_storage() + zha_gateway: ZHAGateway = zha_data[DATA_ZHA_GATEWAY] + await zha_gateway.shutdown() + await zha_gateway.async_update_device_storage() zha_data[DATA_ZHA_SHUTDOWN_TASK] = hass.bus.async_listen_once( ha_const.EVENT_HOMEASSISTANT_STOP, async_zha_shutdown @@ -132,8 +133,9 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Unload ZHA config entry.""" - await hass.data[DATA_ZHA][DATA_ZHA_GATEWAY].shutdown() - await hass.data[DATA_ZHA][DATA_ZHA_GATEWAY].async_update_device_storage() + zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] + await zha_gateway.shutdown() + await zha_gateway.async_update_device_storage() GROUP_PROBE.cleanup() api.async_unload_api(hass) @@ -153,7 +155,8 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> async def async_load_entities(hass: HomeAssistant) -> None: """Load entities after integration was setup.""" - await hass.data[DATA_ZHA][DATA_ZHA_GATEWAY].async_initialize_devices_and_entities() + zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] + await zha_gateway.async_initialize_devices_and_entities() to_setup = hass.data[DATA_ZHA][DATA_ZHA_PLATFORM_LOADED] results = await asyncio.gather(*to_setup, return_exceptions=True) for res in results: diff --git a/homeassistant/components/zha/api.py b/homeassistant/components/zha/api.py index 86dad9d6bd0..c42384682da 100644 --- a/homeassistant/components/zha/api.py +++ b/homeassistant/components/zha/api.py @@ -1,10 +1,9 @@ """Web socket API for Zigbee Home Automation devices.""" +from __future__ import annotations import asyncio -import collections -from collections.abc import Mapping import logging -from typing import Any +from typing import TYPE_CHECKING, Any, NamedTuple import voluptuous as vol from zigpy.config.validators import cv_boolean @@ -13,10 +12,11 @@ from zigpy.zcl.clusters.security import IasAce import zigpy.zdo.types as zdo_types from homeassistant.components import websocket_api -from homeassistant.const import ATTR_COMMAND, ATTR_NAME -from homeassistant.core import ServiceCall, callback +from homeassistant.const import ATTR_COMMAND, ATTR_ID, ATTR_NAME +from homeassistant.core import HomeAssistant, ServiceCall, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.service import async_register_admin_service from .core.const import ( ATTR_ARGS, @@ -29,6 +29,7 @@ from .core.const import ( ATTR_LEVEL, ATTR_MANUFACTURER, ATTR_MEMBERS, + ATTR_TYPE, ATTR_VALUE, ATTR_WARNING_DEVICE_DURATION, ATTR_WARNING_DEVICE_MODE, @@ -67,7 +68,12 @@ from .core.helpers import ( get_matched_clusters, qr_to_install_code, ) -from .core.typing import ZhaDeviceType, ZhaGatewayType +from .core.typing import ZhaDeviceType + +if TYPE_CHECKING: + from homeassistant.components.websocket_api.connection import ActiveConnection + + from .core.gateway import ZHAGateway _LOGGER = logging.getLogger(__name__) @@ -97,14 +103,18 @@ SERVICE_WARNING_DEVICE_WARN = "warning_device_warn" SERVICE_ZIGBEE_BIND = "service_zigbee_bind" IEEE_SERVICE = "ieee_based_service" +IEEE_SCHEMA = vol.All(cv.string, EUI64.convert) + SERVICE_PERMIT_PARAMS = { - vol.Optional(ATTR_IEEE, default=None): EUI64.convert, + vol.Optional(ATTR_IEEE): IEEE_SCHEMA, vol.Optional(ATTR_DURATION, default=60): vol.All( vol.Coerce(int), vol.Range(0, 254) ), - vol.Inclusive(ATTR_SOURCE_IEEE, "install_code"): EUI64.convert, - vol.Inclusive(ATTR_INSTALL_CODE, "install_code"): convert_install_code, - vol.Exclusive(ATTR_QR_CODE, "install_code"): vol.All(str, qr_to_install_code), + vol.Inclusive(ATTR_SOURCE_IEEE, "install_code"): IEEE_SCHEMA, + vol.Inclusive(ATTR_INSTALL_CODE, "install_code"): vol.All( + cv.string, convert_install_code + ), + vol.Exclusive(ATTR_QR_CODE, "install_code"): vol.All(cv.string, qr_to_install_code), } SERVICE_SCHEMAS = { @@ -117,12 +127,12 @@ SERVICE_SCHEMAS = { IEEE_SERVICE: vol.Schema( vol.All( cv.deprecated(ATTR_IEEE_ADDRESS, replacement_key=ATTR_IEEE), - {vol.Required(ATTR_IEEE): EUI64.convert}, + {vol.Required(ATTR_IEEE): IEEE_SCHEMA}, ) ), SERVICE_SET_ZIGBEE_CLUSTER_ATTRIBUTE: vol.Schema( { - vol.Required(ATTR_IEEE): EUI64.convert, + vol.Required(ATTR_IEEE): IEEE_SCHEMA, vol.Required(ATTR_ENDPOINT_ID): cv.positive_int, vol.Required(ATTR_CLUSTER_ID): cv.positive_int, vol.Optional(ATTR_CLUSTER_TYPE, default=CLUSTER_TYPE_IN): cv.string, @@ -133,7 +143,7 @@ SERVICE_SCHEMAS = { ), SERVICE_WARNING_DEVICE_SQUAWK: vol.Schema( { - vol.Required(ATTR_IEEE): EUI64.convert, + vol.Required(ATTR_IEEE): IEEE_SCHEMA, vol.Optional( ATTR_WARNING_DEVICE_MODE, default=WARNING_DEVICE_SQUAWK_MODE_ARMED ): cv.positive_int, @@ -147,7 +157,7 @@ SERVICE_SCHEMAS = { ), SERVICE_WARNING_DEVICE_WARN: vol.Schema( { - vol.Required(ATTR_IEEE): EUI64.convert, + vol.Required(ATTR_IEEE): IEEE_SCHEMA, vol.Optional( ATTR_WARNING_DEVICE_MODE, default=WARNING_DEVICE_MODE_EMERGENCY ): cv.positive_int, @@ -168,7 +178,7 @@ SERVICE_SCHEMAS = { ), SERVICE_ISSUE_ZIGBEE_CLUSTER_COMMAND: vol.Schema( { - vol.Required(ATTR_IEEE): EUI64.convert, + vol.Required(ATTR_IEEE): IEEE_SCHEMA, vol.Required(ATTR_ENDPOINT_ID): cv.positive_int, vol.Required(ATTR_CLUSTER_ID): cv.positive_int, vol.Optional(ATTR_CLUSTER_TYPE, default=CLUSTER_TYPE_IN): cv.string, @@ -190,19 +200,73 @@ SERVICE_SCHEMAS = { ), } -ClusterBinding = collections.namedtuple("ClusterBinding", "id endpoint_id type name") + +class ClusterBinding(NamedTuple): + """Describes a cluster binding.""" + + name: str + type: str + id: int + endpoint_id: int + + +def _cv_group_member(value: dict[str, Any]) -> GroupMember: + """Transform a group member.""" + return GroupMember( + ieee=value[ATTR_IEEE], + endpoint_id=value[ATTR_ENDPOINT_ID], + ) + + +def _cv_cluster_binding(value: dict[str, Any]) -> ClusterBinding: + """Transform a cluster binding.""" + return ClusterBinding( + name=value[ATTR_NAME], + type=value[ATTR_TYPE], + id=value[ATTR_ID], + endpoint_id=value[ATTR_ENDPOINT_ID], + ) + + +GROUP_MEMBER_SCHEMA = vol.All( + vol.Schema( + { + vol.Required(ATTR_IEEE): IEEE_SCHEMA, + vol.Required(ATTR_ENDPOINT_ID): int, + } + ), + _cv_group_member, +) + + +CLUSTER_BINDING_SCHEMA = vol.All( + vol.Schema( + { + vol.Required(ATTR_NAME): cv.string, + vol.Required(ATTR_TYPE): cv.string, + vol.Required(ATTR_ID): int, + vol.Required(ATTR_ENDPOINT_ID): int, + } + ), + _cv_cluster_binding, +) @websocket_api.require_admin -@websocket_api.async_response @websocket_api.websocket_command( - {vol.Required("type"): "zha/devices/permit", **SERVICE_PERMIT_PARAMS} + { + vol.Required("type"): "zha/devices/permit", + **SERVICE_PERMIT_PARAMS, + } ) -async def websocket_permit_devices(hass, connection, msg): +@websocket_api.async_response +async def websocket_permit_devices( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: """Permit ZHA zigbee devices.""" - zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] - duration = msg.get(ATTR_DURATION) - ieee = msg.get(ATTR_IEEE) + zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] + duration: int = msg[ATTR_DURATION] + ieee: EUI64 | None = msg.get(ATTR_IEEE) async def forward_messages(data): """Forward events to websocket.""" @@ -220,6 +284,8 @@ async def websocket_permit_devices(hass, connection, msg): connection.subscriptions[msg["id"]] = async_cleanup zha_gateway.async_enable_debug_mode() + src_ieee: EUI64 + code: bytes if ATTR_SOURCE_IEEE in msg: src_ieee = msg[ATTR_SOURCE_IEEE] code = msg[ATTR_INSTALL_CODE] @@ -239,23 +305,25 @@ async def websocket_permit_devices(hass, connection, msg): @websocket_api.require_admin -@websocket_api.async_response @websocket_api.websocket_command({vol.Required(TYPE): "zha/devices"}) -async def websocket_get_devices(hass, connection, msg): +@websocket_api.async_response +async def websocket_get_devices( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: """Get ZHA devices.""" - zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] - + zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] devices = [device.zha_device_info for device in zha_gateway.devices.values()] - connection.send_result(msg[ID], devices) @websocket_api.require_admin -@websocket_api.async_response @websocket_api.websocket_command({vol.Required(TYPE): "zha/devices/groupable"}) -async def websocket_get_groupable_devices(hass, connection, msg): +@websocket_api.async_response +async def websocket_get_groupable_devices( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: """Get ZHA devices that can be grouped.""" - zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] + zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] devices = [device for device in zha_gateway.devices.values() if device.is_groupable] groupable_devices = [] @@ -289,106 +357,107 @@ async def websocket_get_groupable_devices(hass, connection, msg): @websocket_api.require_admin -@websocket_api.async_response @websocket_api.websocket_command({vol.Required(TYPE): "zha/groups"}) -async def websocket_get_groups(hass, connection, msg): +@websocket_api.async_response +async def websocket_get_groups( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: """Get ZHA groups.""" - zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] + zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] groups = [group.group_info for group in zha_gateway.groups.values()] connection.send_result(msg[ID], groups) @websocket_api.require_admin -@websocket_api.async_response @websocket_api.websocket_command( - {vol.Required(TYPE): "zha/device", vol.Required(ATTR_IEEE): EUI64.convert} + { + vol.Required(TYPE): "zha/device", + vol.Required(ATTR_IEEE): IEEE_SCHEMA, + } ) -async def websocket_get_device(hass, connection, msg): +@websocket_api.async_response +async def websocket_get_device( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: """Get ZHA devices.""" - zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] - ieee = msg[ATTR_IEEE] - device = None - if ieee in zha_gateway.devices: - device = zha_gateway.devices[ieee].zha_device_info - if not device: + zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] + ieee: EUI64 = msg[ATTR_IEEE] + + if not (zha_device := zha_gateway.devices.get(ieee)): connection.send_message( websocket_api.error_message( msg[ID], websocket_api.const.ERR_NOT_FOUND, "ZHA Device not found" ) ) return - connection.send_result(msg[ID], device) + + device_info = zha_device.zha_device_info + connection.send_result(msg[ID], device_info) @websocket_api.require_admin -@websocket_api.async_response @websocket_api.websocket_command( - {vol.Required(TYPE): "zha/group", vol.Required(GROUP_ID): cv.positive_int} + { + vol.Required(TYPE): "zha/group", + vol.Required(GROUP_ID): cv.positive_int, + } ) -async def websocket_get_group(hass, connection, msg): +@websocket_api.async_response +async def websocket_get_group( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: """Get ZHA group.""" - zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] - group_id = msg[GROUP_ID] - group = None + zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] + group_id: int = msg[GROUP_ID] - if group_id in zha_gateway.groups: - group = zha_gateway.groups.get(group_id).group_info - if not group: + if not (zha_group := zha_gateway.groups.get(group_id)): connection.send_message( websocket_api.error_message( msg[ID], websocket_api.const.ERR_NOT_FOUND, "ZHA Group not found" ) ) return - connection.send_result(msg[ID], group) - -def cv_group_member(value: Any) -> GroupMember: - """Validate and transform a group member.""" - if not isinstance(value, Mapping): - raise vol.Invalid("Not a group member") - try: - group_member = GroupMember( - ieee=EUI64.convert(value["ieee"]), endpoint_id=value["endpoint_id"] - ) - except KeyError as err: - raise vol.Invalid("Not a group member") from err - - return group_member + group_info = zha_group.group_info + connection.send_result(msg[ID], group_info) @websocket_api.require_admin -@websocket_api.async_response @websocket_api.websocket_command( { vol.Required(TYPE): "zha/group/add", vol.Required(GROUP_NAME): cv.string, vol.Optional(GROUP_ID): cv.positive_int, - vol.Optional(ATTR_MEMBERS): vol.All(cv.ensure_list, [cv_group_member]), + vol.Optional(ATTR_MEMBERS): vol.All(cv.ensure_list, [GROUP_MEMBER_SCHEMA]), } ) -async def websocket_add_group(hass, connection, msg): +@websocket_api.async_response +async def websocket_add_group( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: """Add a new ZHA group.""" - zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] - group_name = msg[GROUP_NAME] - members = msg.get(ATTR_MEMBERS) - group_id = msg.get(GROUP_ID) + zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] + group_name: str = msg[GROUP_NAME] + group_id: int | None = msg.get(GROUP_ID) + members: list[GroupMember] | None = msg.get(ATTR_MEMBERS) group = await zha_gateway.async_create_zigpy_group(group_name, members, group_id) connection.send_result(msg[ID], group.group_info) @websocket_api.require_admin -@websocket_api.async_response @websocket_api.websocket_command( { vol.Required(TYPE): "zha/group/remove", vol.Required(GROUP_IDS): vol.All(cv.ensure_list, [cv.positive_int]), } ) -async def websocket_remove_groups(hass, connection, msg): +@websocket_api.async_response +async def websocket_remove_groups( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: """Remove the specified ZHA groups.""" - zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] - group_ids = msg[GROUP_IDS] + zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] + group_ids: list[int] = msg[GROUP_IDS] if len(group_ids) > 1: tasks = [] @@ -402,77 +471,79 @@ async def websocket_remove_groups(hass, connection, msg): @websocket_api.require_admin -@websocket_api.async_response @websocket_api.websocket_command( { vol.Required(TYPE): "zha/group/members/add", vol.Required(GROUP_ID): cv.positive_int, - vol.Required(ATTR_MEMBERS): vol.All(cv.ensure_list, [cv_group_member]), + vol.Required(ATTR_MEMBERS): vol.All(cv.ensure_list, [GROUP_MEMBER_SCHEMA]), } ) -async def websocket_add_group_members(hass, connection, msg): +@websocket_api.async_response +async def websocket_add_group_members( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: """Add members to a ZHA group.""" - zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] - group_id = msg[GROUP_ID] - members = msg[ATTR_MEMBERS] - zha_group = None + zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] + group_id: int = msg[GROUP_ID] + members: list[GroupMember] = msg[ATTR_MEMBERS] - if group_id in zha_gateway.groups: - zha_group = zha_gateway.groups.get(group_id) - await zha_group.async_add_members(members) - if not zha_group: + if not (zha_group := zha_gateway.groups.get(group_id)): connection.send_message( websocket_api.error_message( msg[ID], websocket_api.const.ERR_NOT_FOUND, "ZHA Group not found" ) ) return + + await zha_group.async_add_members(members) ret_group = zha_group.group_info connection.send_result(msg[ID], ret_group) @websocket_api.require_admin -@websocket_api.async_response @websocket_api.websocket_command( { vol.Required(TYPE): "zha/group/members/remove", vol.Required(GROUP_ID): cv.positive_int, - vol.Required(ATTR_MEMBERS): vol.All(cv.ensure_list, [cv_group_member]), + vol.Required(ATTR_MEMBERS): vol.All(cv.ensure_list, [GROUP_MEMBER_SCHEMA]), } ) -async def websocket_remove_group_members(hass, connection, msg): +@websocket_api.async_response +async def websocket_remove_group_members( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: """Remove members from a ZHA group.""" - zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] - group_id = msg[GROUP_ID] - members = msg[ATTR_MEMBERS] - zha_group = None + zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] + group_id: int = msg[GROUP_ID] + members: list[GroupMember] = msg[ATTR_MEMBERS] - if group_id in zha_gateway.groups: - zha_group = zha_gateway.groups.get(group_id) - await zha_group.async_remove_members(members) - if not zha_group: + if not (zha_group := zha_gateway.groups.get(group_id)): connection.send_message( websocket_api.error_message( msg[ID], websocket_api.const.ERR_NOT_FOUND, "ZHA Group not found" ) ) return + + await zha_group.async_remove_members(members) ret_group = zha_group.group_info connection.send_result(msg[ID], ret_group) @websocket_api.require_admin -@websocket_api.async_response @websocket_api.websocket_command( { vol.Required(TYPE): "zha/devices/reconfigure", - vol.Required(ATTR_IEEE): EUI64.convert, + vol.Required(ATTR_IEEE): IEEE_SCHEMA, } ) -async def websocket_reconfigure_node(hass, connection, msg): +@websocket_api.async_response +async def websocket_reconfigure_node( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: """Reconfigure a ZHA nodes entities by its ieee address.""" - zha_gateway: ZhaGatewayType = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] - ieee = msg[ATTR_IEEE] + zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] + ieee: EUI64 = msg[ATTR_IEEE] device: ZhaDeviceType = zha_gateway.get_device(ieee) async def forward_messages(data): @@ -495,27 +566,34 @@ async def websocket_reconfigure_node(hass, connection, msg): @websocket_api.require_admin -@websocket_api.async_response @websocket_api.websocket_command( { vol.Required(TYPE): "zha/topology/update", } ) -async def websocket_update_topology(hass, connection, msg): +@websocket_api.async_response +async def websocket_update_topology( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: """Update the ZHA network topology.""" - zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] + zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] hass.async_create_task(zha_gateway.application_controller.topology.scan()) @websocket_api.require_admin -@websocket_api.async_response @websocket_api.websocket_command( - {vol.Required(TYPE): "zha/devices/clusters", vol.Required(ATTR_IEEE): EUI64.convert} + { + vol.Required(TYPE): "zha/devices/clusters", + vol.Required(ATTR_IEEE): IEEE_SCHEMA, + } ) -async def websocket_device_clusters(hass, connection, msg): +@websocket_api.async_response +async def websocket_device_clusters( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: """Return a list of device clusters.""" - zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] - ieee = msg[ATTR_IEEE] + zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] + ieee: EUI64 = msg[ATTR_IEEE] zha_device = zha_gateway.get_device(ieee) response_clusters = [] if zha_device is not None: @@ -544,24 +622,26 @@ async def websocket_device_clusters(hass, connection, msg): @websocket_api.require_admin -@websocket_api.async_response @websocket_api.websocket_command( { vol.Required(TYPE): "zha/devices/clusters/attributes", - vol.Required(ATTR_IEEE): EUI64.convert, + vol.Required(ATTR_IEEE): IEEE_SCHEMA, vol.Required(ATTR_ENDPOINT_ID): int, vol.Required(ATTR_CLUSTER_ID): int, vol.Required(ATTR_CLUSTER_TYPE): str, } ) -async def websocket_device_cluster_attributes(hass, connection, msg): +@websocket_api.async_response +async def websocket_device_cluster_attributes( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: """Return a list of cluster attributes.""" - zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] - ieee = msg[ATTR_IEEE] - endpoint_id = msg[ATTR_ENDPOINT_ID] - cluster_id = msg[ATTR_CLUSTER_ID] - cluster_type = msg[ATTR_CLUSTER_TYPE] - cluster_attributes = [] + zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] + ieee: EUI64 = msg[ATTR_IEEE] + endpoint_id: int = msg[ATTR_ENDPOINT_ID] + cluster_id: int = msg[ATTR_CLUSTER_ID] + cluster_type: str = msg[ATTR_CLUSTER_TYPE] + cluster_attributes: list[dict[str, Any]] = [] zha_device = zha_gateway.get_device(ieee) attributes = None if zha_device is not None: @@ -569,10 +649,8 @@ async def websocket_device_cluster_attributes(hass, connection, msg): endpoint_id, cluster_id, cluster_type ) if attributes is not None: - for attr_id in attributes: - cluster_attributes.append( - {ID: attr_id, ATTR_NAME: attributes[attr_id][0]} - ) + for attr_id, attr in attributes.items(): + cluster_attributes.append({ID: attr_id, ATTR_NAME: attr.name}) _LOGGER.debug( "Requested attributes for: %s: %s, %s: '%s', %s: %s, %s: %s", ATTR_CLUSTER_ID, @@ -589,25 +667,27 @@ async def websocket_device_cluster_attributes(hass, connection, msg): @websocket_api.require_admin -@websocket_api.async_response @websocket_api.websocket_command( { vol.Required(TYPE): "zha/devices/clusters/commands", - vol.Required(ATTR_IEEE): EUI64.convert, + vol.Required(ATTR_IEEE): IEEE_SCHEMA, vol.Required(ATTR_ENDPOINT_ID): int, vol.Required(ATTR_CLUSTER_ID): int, vol.Required(ATTR_CLUSTER_TYPE): str, } ) -async def websocket_device_cluster_commands(hass, connection, msg): +@websocket_api.async_response +async def websocket_device_cluster_commands( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: """Return a list of cluster commands.""" - zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] - cluster_id = msg[ATTR_CLUSTER_ID] - cluster_type = msg[ATTR_CLUSTER_TYPE] - ieee = msg[ATTR_IEEE] - endpoint_id = msg[ATTR_ENDPOINT_ID] + zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] + ieee: EUI64 = msg[ATTR_IEEE] + endpoint_id: int = msg[ATTR_ENDPOINT_ID] + cluster_id: int = msg[ATTR_CLUSTER_ID] + cluster_type: str = msg[ATTR_CLUSTER_TYPE] zha_device = zha_gateway.get_device(ieee) - cluster_commands = [] + cluster_commands: list[dict[str, Any]] = [] commands = None if zha_device is not None: commands = zha_device.async_get_cluster_commands( @@ -615,20 +695,20 @@ async def websocket_device_cluster_commands(hass, connection, msg): ) if commands is not None: - for cmd_id in commands[CLUSTER_COMMANDS_CLIENT]: + for cmd_id, cmd in commands[CLUSTER_COMMANDS_CLIENT].items(): cluster_commands.append( { TYPE: CLIENT, ID: cmd_id, - ATTR_NAME: commands[CLUSTER_COMMANDS_CLIENT][cmd_id][0], + ATTR_NAME: cmd.name, } ) - for cmd_id in commands[CLUSTER_COMMANDS_SERVER]: + for cmd_id, cmd in commands[CLUSTER_COMMANDS_SERVER].items(): cluster_commands.append( { TYPE: CLUSTER_COMMAND_SERVER, ID: cmd_id, - ATTR_NAME: commands[CLUSTER_COMMANDS_SERVER][cmd_id][0], + ATTR_NAME: cmd.name, } ) _LOGGER.debug( @@ -647,32 +727,35 @@ async def websocket_device_cluster_commands(hass, connection, msg): @websocket_api.require_admin -@websocket_api.async_response @websocket_api.websocket_command( { vol.Required(TYPE): "zha/devices/clusters/attributes/value", - vol.Required(ATTR_IEEE): EUI64.convert, + vol.Required(ATTR_IEEE): IEEE_SCHEMA, vol.Required(ATTR_ENDPOINT_ID): int, vol.Required(ATTR_CLUSTER_ID): int, vol.Required(ATTR_CLUSTER_TYPE): str, vol.Required(ATTR_ATTRIBUTE): int, - vol.Optional(ATTR_MANUFACTURER): object, + vol.Optional(ATTR_MANUFACTURER): cv.positive_int, } ) -async def websocket_read_zigbee_cluster_attributes(hass, connection, msg): +@websocket_api.async_response +async def websocket_read_zigbee_cluster_attributes( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: """Read zigbee attribute for cluster on zha entity.""" - zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] - ieee = msg[ATTR_IEEE] - endpoint_id = msg[ATTR_ENDPOINT_ID] - cluster_id = msg[ATTR_CLUSTER_ID] - cluster_type = msg[ATTR_CLUSTER_TYPE] - attribute = msg[ATTR_ATTRIBUTE] - manufacturer = msg.get(ATTR_MANUFACTURER) or None + zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] + ieee: EUI64 = msg[ATTR_IEEE] + endpoint_id: int = msg[ATTR_ENDPOINT_ID] + cluster_id: int = msg[ATTR_CLUSTER_ID] + cluster_type: str = msg[ATTR_CLUSTER_TYPE] + attribute: int = msg[ATTR_ATTRIBUTE] + manufacturer: int | None = msg.get(ATTR_MANUFACTURER) zha_device = zha_gateway.get_device(ieee) - if cluster_id >= MFG_CLUSTER_ID_START and manufacturer is None: - manufacturer = zha_device.manufacturer_code - success = failure = None + success = {} + failure = {} if zha_device is not None: + if cluster_id >= MFG_CLUSTER_ID_START and manufacturer is None: + manufacturer = zha_device.manufacturer_code cluster = zha_device.async_get_cluster( endpoint_id, cluster_id, cluster_type=cluster_type ) @@ -700,14 +783,19 @@ async def websocket_read_zigbee_cluster_attributes(hass, connection, msg): @websocket_api.require_admin -@websocket_api.async_response @websocket_api.websocket_command( - {vol.Required(TYPE): "zha/devices/bindable", vol.Required(ATTR_IEEE): EUI64.convert} + { + vol.Required(TYPE): "zha/devices/bindable", + vol.Required(ATTR_IEEE): IEEE_SCHEMA, + } ) -async def websocket_get_bindable_devices(hass, connection, msg): +@websocket_api.async_response +async def websocket_get_bindable_devices( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: """Directly bind devices.""" - zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] - source_ieee = msg[ATTR_IEEE] + zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] + source_ieee: EUI64 = msg[ATTR_IEEE] source_device = zha_gateway.get_device(source_ieee) devices = [ @@ -728,19 +816,21 @@ async def websocket_get_bindable_devices(hass, connection, msg): @websocket_api.require_admin -@websocket_api.async_response @websocket_api.websocket_command( { vol.Required(TYPE): "zha/devices/bind", - vol.Required(ATTR_SOURCE_IEEE): EUI64.convert, - vol.Required(ATTR_TARGET_IEEE): EUI64.convert, + vol.Required(ATTR_SOURCE_IEEE): IEEE_SCHEMA, + vol.Required(ATTR_TARGET_IEEE): IEEE_SCHEMA, } ) -async def websocket_bind_devices(hass, connection, msg): +@websocket_api.async_response +async def websocket_bind_devices( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: """Directly bind devices.""" - zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] - source_ieee = msg[ATTR_SOURCE_IEEE] - target_ieee = msg[ATTR_TARGET_IEEE] + zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] + source_ieee: EUI64 = msg[ATTR_SOURCE_IEEE] + target_ieee: EUI64 = msg[ATTR_TARGET_IEEE] await async_binding_operation( zha_gateway, source_ieee, target_ieee, zdo_types.ZDOCmd.Bind_req ) @@ -754,19 +844,21 @@ async def websocket_bind_devices(hass, connection, msg): @websocket_api.require_admin -@websocket_api.async_response @websocket_api.websocket_command( { vol.Required(TYPE): "zha/devices/unbind", - vol.Required(ATTR_SOURCE_IEEE): EUI64.convert, - vol.Required(ATTR_TARGET_IEEE): EUI64.convert, + vol.Required(ATTR_SOURCE_IEEE): IEEE_SCHEMA, + vol.Required(ATTR_TARGET_IEEE): IEEE_SCHEMA, } ) -async def websocket_unbind_devices(hass, connection, msg): +@websocket_api.async_response +async def websocket_unbind_devices( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: """Remove a direct binding between devices.""" - zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] - source_ieee = msg[ATTR_SOURCE_IEEE] - target_ieee = msg[ATTR_TARGET_IEEE] + zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] + source_ieee: EUI64 = msg[ATTR_SOURCE_IEEE] + target_ieee: EUI64 = msg[ATTR_TARGET_IEEE] await async_binding_operation( zha_gateway, source_ieee, target_ieee, zdo_types.ZDOCmd.Unbind_req ) @@ -779,65 +871,56 @@ async def websocket_unbind_devices(hass, connection, msg): ) -def is_cluster_binding(value: Any) -> ClusterBinding: - """Validate and transform a cluster binding.""" - if not isinstance(value, Mapping): - raise vol.Invalid("Not a cluster binding") - try: - cluster_binding = ClusterBinding( - name=value["name"], - type=value["type"], - id=value["id"], - endpoint_id=value["endpoint_id"], - ) - except KeyError as err: - raise vol.Invalid("Not a cluster binding") from err - - return cluster_binding - - @websocket_api.require_admin -@websocket_api.async_response @websocket_api.websocket_command( { vol.Required(TYPE): "zha/groups/bind", - vol.Required(ATTR_SOURCE_IEEE): EUI64.convert, + vol.Required(ATTR_SOURCE_IEEE): IEEE_SCHEMA, vol.Required(GROUP_ID): cv.positive_int, - vol.Required(BINDINGS): vol.All(cv.ensure_list, [is_cluster_binding]), + vol.Required(BINDINGS): vol.All(cv.ensure_list, [CLUSTER_BINDING_SCHEMA]), } ) -async def websocket_bind_group(hass, connection, msg): +@websocket_api.async_response +async def websocket_bind_group( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: """Directly bind a device to a group.""" - zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] - source_ieee = msg[ATTR_SOURCE_IEEE] - group_id = msg[GROUP_ID] - bindings = msg[BINDINGS] + zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] + source_ieee: EUI64 = msg[ATTR_SOURCE_IEEE] + group_id: int = msg[GROUP_ID] + bindings: list[ClusterBinding] = msg[BINDINGS] source_device = zha_gateway.get_device(source_ieee) - await source_device.async_bind_to_group(group_id, bindings) @websocket_api.require_admin -@websocket_api.async_response @websocket_api.websocket_command( { vol.Required(TYPE): "zha/groups/unbind", - vol.Required(ATTR_SOURCE_IEEE): EUI64.convert, + vol.Required(ATTR_SOURCE_IEEE): IEEE_SCHEMA, vol.Required(GROUP_ID): cv.positive_int, - vol.Required(BINDINGS): vol.All(cv.ensure_list, [is_cluster_binding]), + vol.Required(BINDINGS): vol.All(cv.ensure_list, [CLUSTER_BINDING_SCHEMA]), } ) -async def websocket_unbind_group(hass, connection, msg): +@websocket_api.async_response +async def websocket_unbind_group( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: """Unbind a device from a group.""" - zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] - source_ieee = msg[ATTR_SOURCE_IEEE] - group_id = msg[GROUP_ID] - bindings = msg[BINDINGS] + zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] + source_ieee: EUI64 = msg[ATTR_SOURCE_IEEE] + group_id: int = msg[GROUP_ID] + bindings: list[ClusterBinding] = msg[BINDINGS] source_device = zha_gateway.get_device(source_ieee) await source_device.async_unbind_from_group(group_id, bindings) -async def async_binding_operation(zha_gateway, source_ieee, target_ieee, operation): +async def async_binding_operation( + zha_gateway: ZHAGateway, + source_ieee: EUI64, + target_ieee: EUI64, + operation: zdo_types.ZDOCmd, +) -> None: """Create or remove a direct zigbee binding between 2 devices.""" source_device = zha_gateway.get_device(source_ieee) @@ -879,9 +962,11 @@ async def async_binding_operation(zha_gateway, source_ieee, target_ieee, operati @websocket_api.require_admin -@websocket_api.async_response @websocket_api.websocket_command({vol.Required(TYPE): "zha/configuration"}) -async def websocket_get_configuration(hass, connection, msg): +@websocket_api.async_response +async def websocket_get_configuration( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: """Get ZHA configuration.""" zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] import voluptuous_serialize # pylint: disable=import-outside-toplevel @@ -913,16 +998,18 @@ async def websocket_get_configuration(hass, connection, msg): @websocket_api.require_admin -@websocket_api.async_response @websocket_api.websocket_command( { vol.Required(TYPE): "zha/configuration/update", vol.Required("data"): ZHA_CONFIG_SCHEMAS, } ) -async def websocket_update_zha_configuration(hass, connection, msg): +@websocket_api.async_response +async def websocket_update_zha_configuration( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: """Update the ZHA configuration.""" - zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] + zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] options = zha_gateway.config_entry.options data_to_save = {**options, **{CUSTOM_CONFIGURATION: msg["data"]}} @@ -940,15 +1027,17 @@ async def websocket_update_zha_configuration(hass, connection, msg): @callback -def async_load_api(hass): +def async_load_api(hass: HomeAssistant) -> None: """Set up the web socket API.""" - zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] + zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] application_controller = zha_gateway.application_controller async def permit(service: ServiceCall) -> None: """Allow devices to join this network.""" - duration = service.data[ATTR_DURATION] - ieee = service.data.get(ATTR_IEEE) + duration: int = service.data[ATTR_DURATION] + ieee: EUI64 | None = service.data.get(ATTR_IEEE) + src_ieee: EUI64 + code: bytes if ATTR_SOURCE_IEEE in service.data: src_ieee = service.data[ATTR_SOURCE_IEEE] code = service.data[ATTR_INSTALL_CODE] @@ -972,14 +1061,14 @@ def async_load_api(hass): _LOGGER.info("Permitting joins for %ss", duration) await application_controller.permit(time_s=duration, node=ieee) - hass.helpers.service.async_register_admin_service( - DOMAIN, SERVICE_PERMIT, permit, schema=SERVICE_SCHEMAS[SERVICE_PERMIT] + async_register_admin_service( + hass, DOMAIN, SERVICE_PERMIT, permit, schema=SERVICE_SCHEMAS[SERVICE_PERMIT] ) async def remove(service: ServiceCall) -> None: """Remove a node from the network.""" - ieee = service.data[ATTR_IEEE] - zha_gateway: ZhaGatewayType = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] + zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] + ieee: EUI64 = service.data[ATTR_IEEE] zha_device: ZhaDeviceType = zha_gateway.get_device(ieee) if zha_device is not None and ( zha_device.is_coordinator @@ -990,24 +1079,24 @@ def async_load_api(hass): _LOGGER.info("Removing node %s", ieee) await application_controller.remove(ieee) - hass.helpers.service.async_register_admin_service( - DOMAIN, SERVICE_REMOVE, remove, schema=SERVICE_SCHEMAS[IEEE_SERVICE] + async_register_admin_service( + hass, DOMAIN, SERVICE_REMOVE, remove, schema=SERVICE_SCHEMAS[IEEE_SERVICE] ) async def set_zigbee_cluster_attributes(service: ServiceCall) -> None: """Set zigbee attribute for cluster on zha entity.""" - ieee = service.data.get(ATTR_IEEE) - endpoint_id = service.data.get(ATTR_ENDPOINT_ID) - cluster_id = service.data.get(ATTR_CLUSTER_ID) - cluster_type = service.data.get(ATTR_CLUSTER_TYPE) - attribute = service.data.get(ATTR_ATTRIBUTE) - value = service.data.get(ATTR_VALUE) - manufacturer = service.data.get(ATTR_MANUFACTURER) or None + ieee: EUI64 = service.data[ATTR_IEEE] + endpoint_id: int = service.data[ATTR_ENDPOINT_ID] + cluster_id: int = service.data[ATTR_CLUSTER_ID] + cluster_type: str = service.data[ATTR_CLUSTER_TYPE] + attribute: int | str = service.data[ATTR_ATTRIBUTE] + value: int | bool | str = service.data[ATTR_VALUE] + manufacturer: int | None = service.data.get(ATTR_MANUFACTURER) zha_device = zha_gateway.get_device(ieee) - if cluster_id >= MFG_CLUSTER_ID_START and manufacturer is None: - manufacturer = zha_device.manufacturer_code response = None if zha_device is not None: + if cluster_id >= MFG_CLUSTER_ID_START and manufacturer is None: + manufacturer = zha_device.manufacturer_code response = await zha_device.write_zigbee_attribute( endpoint_id, cluster_id, @@ -1034,7 +1123,8 @@ def async_load_api(hass): response, ) - hass.helpers.service.async_register_admin_service( + async_register_admin_service( + hass, DOMAIN, SERVICE_SET_ZIGBEE_CLUSTER_ATTRIBUTE, set_zigbee_cluster_attributes, @@ -1043,19 +1133,19 @@ def async_load_api(hass): async def issue_zigbee_cluster_command(service: ServiceCall) -> None: """Issue command on zigbee cluster on zha entity.""" - ieee = service.data.get(ATTR_IEEE) - endpoint_id = service.data.get(ATTR_ENDPOINT_ID) - cluster_id = service.data.get(ATTR_CLUSTER_ID) - cluster_type = service.data.get(ATTR_CLUSTER_TYPE) - command = service.data.get(ATTR_COMMAND) - command_type = service.data.get(ATTR_COMMAND_TYPE) - args = service.data.get(ATTR_ARGS) - manufacturer = service.data.get(ATTR_MANUFACTURER) or None + ieee: EUI64 = service.data[ATTR_IEEE] + endpoint_id: int = service.data[ATTR_ENDPOINT_ID] + cluster_id: int = service.data[ATTR_CLUSTER_ID] + cluster_type: str = service.data[ATTR_CLUSTER_TYPE] + command: int = service.data[ATTR_COMMAND] + command_type: str = service.data[ATTR_COMMAND_TYPE] + args: list = service.data[ATTR_ARGS] + manufacturer: int | None = service.data.get(ATTR_MANUFACTURER) zha_device = zha_gateway.get_device(ieee) - if cluster_id >= MFG_CLUSTER_ID_START and manufacturer is None: - manufacturer = zha_device.manufacturer_code response = None if zha_device is not None: + if cluster_id >= MFG_CLUSTER_ID_START and manufacturer is None: + manufacturer = zha_device.manufacturer_code response = await zha_device.issue_cluster_command( endpoint_id, cluster_id, @@ -1085,7 +1175,8 @@ def async_load_api(hass): response, ) - hass.helpers.service.async_register_admin_service( + async_register_admin_service( + hass, DOMAIN, SERVICE_ISSUE_ZIGBEE_CLUSTER_COMMAND, issue_zigbee_cluster_command, @@ -1094,11 +1185,11 @@ def async_load_api(hass): async def issue_zigbee_group_command(service: ServiceCall) -> None: """Issue command on zigbee cluster on a zigbee group.""" - group_id = service.data.get(ATTR_GROUP) - cluster_id = service.data.get(ATTR_CLUSTER_ID) - command = service.data.get(ATTR_COMMAND) - args = service.data.get(ATTR_ARGS) - manufacturer = service.data.get(ATTR_MANUFACTURER) or None + group_id: int = service.data[ATTR_GROUP] + cluster_id: int = service.data[ATTR_CLUSTER_ID] + command: int = service.data[ATTR_COMMAND] + args: list = service.data[ATTR_ARGS] + manufacturer: int | None = service.data.get(ATTR_MANUFACTURER) group = zha_gateway.get_group(group_id) if cluster_id >= MFG_CLUSTER_ID_START and manufacturer is None: _LOGGER.error("Missing manufacturer attribute for cluster: %d", cluster_id) @@ -1122,7 +1213,8 @@ def async_load_api(hass): response, ) - hass.helpers.service.async_register_admin_service( + async_register_admin_service( + hass, DOMAIN, SERVICE_ISSUE_ZIGBEE_GROUP_COMMAND, issue_zigbee_group_command, @@ -1140,10 +1232,10 @@ def async_load_api(hass): async def warning_device_squawk(service: ServiceCall) -> None: """Issue the squawk command for an IAS warning device.""" - ieee = service.data[ATTR_IEEE] - mode = service.data.get(ATTR_WARNING_DEVICE_MODE) - strobe = service.data.get(ATTR_WARNING_DEVICE_STROBE) - level = service.data.get(ATTR_LEVEL) + ieee: EUI64 = service.data[ATTR_IEEE] + mode: int = service.data[ATTR_WARNING_DEVICE_MODE] + strobe: int = service.data[ATTR_WARNING_DEVICE_STROBE] + level: int = service.data[ATTR_LEVEL] if (zha_device := zha_gateway.get_device(ieee)) is not None: if channel := _get_ias_wd_channel(zha_device): @@ -1170,7 +1262,8 @@ def async_load_api(hass): level, ) - hass.helpers.service.async_register_admin_service( + async_register_admin_service( + hass, DOMAIN, SERVICE_WARNING_DEVICE_SQUAWK, warning_device_squawk, @@ -1179,13 +1272,13 @@ def async_load_api(hass): async def warning_device_warn(service: ServiceCall) -> None: """Issue the warning command for an IAS warning device.""" - ieee = service.data[ATTR_IEEE] - mode = service.data.get(ATTR_WARNING_DEVICE_MODE) - strobe = service.data.get(ATTR_WARNING_DEVICE_STROBE) - level = service.data.get(ATTR_LEVEL) - duration = service.data.get(ATTR_WARNING_DEVICE_DURATION) - duty_mode = service.data.get(ATTR_WARNING_DEVICE_STROBE_DUTY_CYCLE) - intensity = service.data.get(ATTR_WARNING_DEVICE_STROBE_INTENSITY) + ieee: EUI64 = service.data[ATTR_IEEE] + mode: int = service.data[ATTR_WARNING_DEVICE_MODE] + strobe: int = service.data[ATTR_WARNING_DEVICE_STROBE] + level: int = service.data[ATTR_LEVEL] + duration: int = service.data[ATTR_WARNING_DEVICE_DURATION] + duty_mode: int = service.data[ATTR_WARNING_DEVICE_STROBE_DUTY_CYCLE] + intensity: int = service.data[ATTR_WARNING_DEVICE_STROBE_INTENSITY] if (zha_device := zha_gateway.get_device(ieee)) is not None: if channel := _get_ias_wd_channel(zha_device): @@ -1214,7 +1307,8 @@ def async_load_api(hass): level, ) - hass.helpers.service.async_register_admin_service( + async_register_admin_service( + hass, DOMAIN, SERVICE_WARNING_DEVICE_WARN, warning_device_warn, @@ -1247,7 +1341,7 @@ def async_load_api(hass): @callback -def async_unload_api(hass): +def async_unload_api(hass: HomeAssistant) -> None: """Unload the ZHA API.""" hass.services.async_remove(DOMAIN, SERVICE_PERMIT) hass.services.async_remove(DOMAIN, SERVICE_REMOVE) diff --git a/homeassistant/components/zha/climate.py b/homeassistant/components/zha/climate.py index d7e36c52517..06b1e8a47d8 100644 --- a/homeassistant/components/zha/climate.py +++ b/homeassistant/components/zha/climate.py @@ -161,9 +161,9 @@ class Thermostat(ZhaEntity, ClimateEntity): @property def current_temperature(self): """Return the current temperature.""" - if self._thrm.local_temp is None: + if self._thrm.local_temperature is None: return None - return self._thrm.local_temp / ZCL_TEMP + return self._thrm.local_temperature / ZCL_TEMP @property def extra_state_attributes(self): @@ -272,7 +272,7 @@ class Thermostat(ZhaEntity, ClimateEntity): @property def hvac_modes(self) -> tuple[str, ...]: """Return the list of available HVAC operation modes.""" - return SEQ_OF_OPERATION.get(self._thrm.ctrl_seqe_of_oper, (HVAC_MODE_OFF,)) + return SEQ_OF_OPERATION.get(self._thrm.ctrl_sequence_of_oper, (HVAC_MODE_OFF,)) @property def precision(self): @@ -601,6 +601,8 @@ class CentralitePearl(ZenWithinThermostat): "_TZE200_2atgpdho", "_TZE200_pvvbommb", "_TZE200_4eeyebrt", + "_TZE200_cpmgn2cf", + "_TZE200_9sfg7gm0", "_TYST11_ckud7u2l", "_TYST11_ywdxldoj", "_TYST11_cwnjrr72", @@ -771,3 +773,71 @@ class StelproFanHeater(Thermostat): def hvac_modes(self) -> tuple[str, ...]: """Return only the heat mode, because the device can't be turned off.""" return (HVAC_MODE_HEAT,) + + +@STRICT_MATCH( + channel_names=CHANNEL_THERMOSTAT, + manufacturers={ + "_TZE200_e9ba97vf", # TV01-ZG + "_TZE200_husqqvux", # TSL-TRV-TV01ZG + "_TZE200_hue3yfsn", # TV02-ZG + "_TZE200_kly8gjlz", # TV05-ZG + }, +) +class ZONNSMARTThermostat(Thermostat): + """ + ZONNSMART Thermostat implementation. + + Notice that this device uses two holiday presets (2: HolidayMode, + 3: HolidayModeTemp), but only one of them can be set. + """ + + PRESET_HOLIDAY = "holiday" + PRESET_FROST = "frost protect" + + def __init__(self, unique_id, zha_device, channels, **kwargs): + """Initialize ZHA Thermostat instance.""" + super().__init__(unique_id, zha_device, channels, **kwargs) + self._presets = [ + PRESET_NONE, + self.PRESET_HOLIDAY, + PRESET_SCHEDULE, + self.PRESET_FROST, + ] + self._supported_flags |= SUPPORT_PRESET_MODE + + async def async_attribute_updated(self, record): + """Handle attribute update from device.""" + if record.attr_name == "operation_preset": + if record.value == 0: + self._preset = PRESET_SCHEDULE + if record.value == 1: + self._preset = PRESET_NONE + if record.value == 2: + self._preset = self.PRESET_HOLIDAY + if record.value == 3: + self._preset = self.PRESET_HOLIDAY + if record.value == 4: + self._preset = self.PRESET_FROST + await super().async_attribute_updated(record) + + async def async_preset_handler(self, preset: str, enable: bool = False) -> bool: + """Set the preset mode.""" + mfg_code = self._zha_device.manufacturer_code + if not enable: + return await self._thrm.write_attributes( + {"operation_preset": 1}, manufacturer=mfg_code + ) + if preset == PRESET_SCHEDULE: + return await self._thrm.write_attributes( + {"operation_preset": 0}, manufacturer=mfg_code + ) + if preset == self.PRESET_HOLIDAY: + return await self._thrm.write_attributes( + {"operation_preset": 3}, manufacturer=mfg_code + ) + if preset == self.PRESET_FROST: + return await self._thrm.write_attributes( + {"operation_preset": 4}, manufacturer=mfg_code + ) + return False diff --git a/homeassistant/components/zha/config_flow.py b/homeassistant/components/zha/config_flow.py index 4d6b22a82e5..f8f696c56e3 100644 --- a/homeassistant/components/zha/config_flow.py +++ b/homeassistant/components/zha/config_flow.py @@ -156,7 +156,6 @@ class ZhaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return self.async_show_form( step_id="confirm", description_placeholders={CONF_NAME: self._title}, - data_schema=vol.Schema({}), ) async def async_step_zeroconf( diff --git a/homeassistant/components/zha/core/channels/__init__.py b/homeassistant/components/zha/core/channels/__init__.py index d60c38c69a6..2011f92a63b 100644 --- a/homeassistant/components/zha/core/channels/__init__.py +++ b/homeassistant/components/zha/core/channels/__init__.py @@ -2,12 +2,14 @@ from __future__ import annotations import asyncio -from typing import Any +from collections.abc import Coroutine +from typing import TYPE_CHECKING, Any, TypeVar +import zigpy.endpoint import zigpy.zcl.clusters.closures from homeassistant.const import ATTR_DEVICE_ID -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_send from . import ( # noqa: F401 @@ -29,20 +31,25 @@ from .. import ( device as zha_core_device, discovery as zha_disc, registries as zha_regs, - typing as zha_typing, ) -ChannelsDict = dict[str, zha_typing.ChannelType] +if TYPE_CHECKING: + from ...entity import ZhaEntity + from ..device import ZHADevice + +_ChannelsT = TypeVar("_ChannelsT", bound="Channels") +_ChannelPoolT = TypeVar("_ChannelPoolT", bound="ChannelPool") +_ChannelsDictType = dict[str, base.ZigbeeChannel] class Channels: """All discovered channels of a device.""" - def __init__(self, zha_device: zha_typing.ZhaDeviceType) -> None: + def __init__(self, zha_device: ZHADevice) -> None: """Initialize instance.""" - self._pools: list[zha_typing.ChannelPoolType] = [] - self._power_config = None - self._identify = None + self._pools: list[ChannelPool] = [] + self._power_config: base.ZigbeeChannel | None = None + self._identify: base.ZigbeeChannel | None = None self._semaphore = asyncio.Semaphore(3) self._unique_id = str(zha_device.ieee) self._zdo_channel = base.ZDOChannel(zha_device.device.endpoints[0], zha_device) @@ -54,23 +61,23 @@ class Channels: return self._pools @property - def power_configuration_ch(self) -> zha_typing.ChannelType: + def power_configuration_ch(self) -> base.ZigbeeChannel | None: """Return power configuration channel.""" return self._power_config @power_configuration_ch.setter - def power_configuration_ch(self, channel: zha_typing.ChannelType) -> None: + def power_configuration_ch(self, channel: base.ZigbeeChannel) -> None: """Power configuration channel setter.""" if self._power_config is None: self._power_config = channel @property - def identify_ch(self) -> zha_typing.ChannelType: + def identify_ch(self) -> base.ZigbeeChannel | None: """Return power configuration channel.""" return self._identify @identify_ch.setter - def identify_ch(self, channel: zha_typing.ChannelType) -> None: + def identify_ch(self, channel: base.ZigbeeChannel) -> None: """Power configuration channel setter.""" if self._identify is None: self._identify = channel @@ -81,17 +88,17 @@ class Channels: return self._semaphore @property - def zdo_channel(self) -> zha_typing.ZDOChannelType: + def zdo_channel(self) -> base.ZDOChannel: """Return ZDO channel.""" return self._zdo_channel @property - def zha_device(self) -> zha_typing.ZhaDeviceType: + def zha_device(self) -> ZHADevice: """Return parent zha device.""" return self._zha_device @property - def unique_id(self): + def unique_id(self) -> str: """Return the unique id for this channel.""" return self._unique_id @@ -104,7 +111,7 @@ class Channels: } @classmethod - def new(cls, zha_device: zha_typing.ZhaDeviceType) -> Channels: + def new(cls: type[_ChannelsT], zha_device: ZHADevice) -> _ChannelsT: """Create new instance.""" channels = cls(zha_device) for ep_id in sorted(zha_device.device.endpoints): @@ -142,9 +149,9 @@ class Channels: def async_new_entity( self, component: str, - entity_class: zha_typing.CALLABLE_T, + entity_class: type[ZhaEntity], unique_id: str, - channels: list[zha_typing.ChannelType], + channels: list[base.ZigbeeChannel], ): """Signal new entity addition.""" if self.zha_device.status == zha_core_device.DeviceStatus.INITIALIZED: @@ -178,30 +185,30 @@ class ChannelPool: def __init__(self, channels: Channels, ep_id: int) -> None: """Initialize instance.""" - self._all_channels: ChannelsDict = {} - self._channels: Channels = channels - self._claimed_channels: ChannelsDict = {} - self._id: int = ep_id - self._client_channels: dict[str, zha_typing.ClientChannelType] = {} - self._unique_id: str = f"{channels.unique_id}-{ep_id}" + self._all_channels: _ChannelsDictType = {} + self._channels = channels + self._claimed_channels: _ChannelsDictType = {} + self._id = ep_id + self._client_channels: dict[str, base.ClientChannel] = {} + self._unique_id = f"{channels.unique_id}-{ep_id}" @property - def all_channels(self) -> ChannelsDict: + def all_channels(self) -> _ChannelsDictType: """All server channels of an endpoint.""" return self._all_channels @property - def claimed_channels(self) -> ChannelsDict: + def claimed_channels(self) -> _ChannelsDictType: """Channels in use.""" return self._claimed_channels @property - def client_channels(self) -> dict[str, zha_typing.ClientChannelType]: + def client_channels(self) -> dict[str, base.ClientChannel]: """Return a dict of client channels.""" return self._client_channels @property - def endpoint(self) -> zha_typing.ZigpyEndpointType: + def endpoint(self) -> zigpy.endpoint.Endpoint: """Return endpoint of zigpy device.""" return self._channels.zha_device.device.endpoints[self.id] @@ -216,7 +223,7 @@ class ChannelPool: return self._channels.zha_device.nwk @property - def is_mains_powered(self) -> bool: + def is_mains_powered(self) -> bool | None: """Device is_mains_powered.""" return self._channels.zha_device.is_mains_powered @@ -231,7 +238,7 @@ class ChannelPool: return self._channels.zha_device.manufacturer_code @property - def hass(self): + def hass(self) -> HomeAssistant: """Return hass.""" return self._channels.zha_device.hass @@ -246,7 +253,7 @@ class ChannelPool: return self._channels.zha_device.skip_configuration @property - def unique_id(self): + def unique_id(self) -> str: """Return the unique id for this channel.""" return self._unique_id @@ -272,7 +279,7 @@ class ChannelPool: ) @classmethod - def new(cls, channels: Channels, ep_id: int) -> ChannelPool: + def new(cls: type[_ChannelPoolT], channels: Channels, ep_id: int) -> _ChannelPoolT: """Create new channels for an endpoint.""" pool = cls(channels, ep_id) pool.add_all_channels() @@ -330,7 +337,7 @@ class ChannelPool: async def _execute_channel_tasks(self, func_name: str, *args: Any) -> None: """Add a throttled channel task and swallow exceptions.""" - async def _throttle(coro): + async def _throttle(coro: Coroutine[Any, Any, None]) -> None: async with self._channels.semaphore: return await coro @@ -339,7 +346,9 @@ class ChannelPool: results = await asyncio.gather(*tasks, return_exceptions=True) for channel, outcome in zip(channels, results): if isinstance(outcome, Exception): - channel.warning("'%s' stage failed: %s", func_name, str(outcome)) + channel.warning( + "'%s' stage failed: %s", func_name, str(outcome), exc_info=outcome + ) continue channel.debug("'%s' stage succeeded", func_name) @@ -347,9 +356,9 @@ class ChannelPool: def async_new_entity( self, component: str, - entity_class: zha_typing.CALLABLE_T, + entity_class: type[ZhaEntity], unique_id: str, - channels: list[zha_typing.ChannelType], + channels: list[base.ZigbeeChannel], ): """Signal new entity addition.""" self._channels.async_new_entity(component, entity_class, unique_id, channels) @@ -360,12 +369,12 @@ class ChannelPool: self._channels.async_send_signal(signal, *args) @callback - def claim_channels(self, channels: list[zha_typing.ChannelType]) -> None: + def claim_channels(self, channels: list[base.ZigbeeChannel]) -> None: """Claim a channel.""" self.claimed_channels.update({ch.id: ch for ch in channels}) @callback - def unclaimed_channels(self) -> list[zha_typing.ChannelType]: + def unclaimed_channels(self) -> list[base.ZigbeeChannel]: """Return a list of available (unclaimed) channels.""" claimed = set(self.claimed_channels) available = set(self.all_channels) diff --git a/homeassistant/components/zha/core/channels/base.py b/homeassistant/components/zha/core/channels/base.py index f79000d0646..0dd6169373b 100644 --- a/homeassistant/components/zha/core/channels/base.py +++ b/homeassistant/components/zha/core/channels/base.py @@ -8,7 +8,7 @@ import logging from typing import Any import zigpy.exceptions -from zigpy.zcl.foundation import Status +from zigpy.zcl.foundation import ConfigureReportingResponseRecord, Status from homeassistant.const import ATTR_COMMAND from homeassistant.core import callback @@ -111,7 +111,7 @@ class ZigbeeChannel(LogMixin): if not hasattr(self, "_value_attribute") and self.REPORT_CONFIG: attr = self.REPORT_CONFIG[0].get("attr") if isinstance(attr, str): - self.value_attribute = self.cluster.attridx.get(attr) + self.value_attribute = self.cluster.attributes_by_name.get(attr) else: self.value_attribute = attr self._status = ChannelStatus.CREATED @@ -260,7 +260,7 @@ class ZigbeeChannel(LogMixin): self, attrs: dict[int | str, tuple], res: list | tuple ) -> None: """Parse configure reporting result.""" - if not isinstance(res, list): + if isinstance(res, (Exception, ConfigureReportingResponseRecord)): # assume default response self.debug( "attr reporting for '%s' on '%s': %s", @@ -345,7 +345,7 @@ class ZigbeeChannel(LogMixin): self.async_send_signal( f"{self.unique_id}_{SIGNAL_ATTR_UPDATED}", attrid, - self.cluster.attributes.get(attrid, [attrid])[0], + self._get_attribute_name(attrid), value, ) @@ -368,6 +368,12 @@ class ZigbeeChannel(LogMixin): async def async_update(self): """Retrieve latest state from cluster.""" + def _get_attribute_name(self, attrid: int) -> str | int: + if attrid not in self.cluster.attributes: + return attrid + + return self.cluster.attributes[attrid].name + async def get_attribute_value(self, attribute, from_cache=True): """Get the value for an attribute.""" manufacturer = None @@ -421,11 +427,11 @@ class ZigbeeChannel(LogMixin): get_attributes = partialmethod(_get_attributes, False) - def log(self, level, msg, *args): + def log(self, level, msg, *args, **kwargs): """Log a message.""" msg = f"[%s:%s]: {msg}" args = (self._ch_pool.nwk, self._id) + args - _LOGGER.log(level, msg, *args) + _LOGGER.log(level, msg, *args, **kwargs) def __getattr__(self, name): """Get attribute or a decorated cluster command.""" @@ -479,11 +485,11 @@ class ZDOChannel(LogMixin): """Configure channel.""" self._status = ChannelStatus.CONFIGURED - def log(self, level, msg, *args): + def log(self, level, msg, *args, **kwargs): """Log a message.""" msg = f"[%s:ZDO](%s): {msg}" args = (self._zha_device.nwk, self._zha_device.model) + args - _LOGGER.log(level, msg, *args) + _LOGGER.log(level, msg, *args, **kwargs) class ClientChannel(ZigbeeChannel): @@ -492,13 +498,17 @@ class ClientChannel(ZigbeeChannel): @callback def attribute_updated(self, attrid, value): """Handle an attribute updated on this cluster.""" + + try: + attr_name = self._cluster.attributes[attrid].name + except KeyError: + attr_name = "Unknown" + self.zha_send_event( SIGNAL_ATTR_UPDATED, { ATTR_ATTRIBUTE_ID: attrid, - ATTR_ATTRIBUTE_NAME: self._cluster.attributes.get(attrid, ["Unknown"])[ - 0 - ], + ATTR_ATTRIBUTE_NAME: attr_name, ATTR_VALUE: value, }, ) @@ -510,4 +520,4 @@ class ClientChannel(ZigbeeChannel): self._cluster.server_commands is not None and self._cluster.server_commands.get(command_id) is not None ): - self.zha_send_event(self._cluster.server_commands.get(command_id)[0], args) + self.zha_send_event(self._cluster.server_commands[command_id].name, args) diff --git a/homeassistant/components/zha/core/channels/closures.py b/homeassistant/components/zha/core/channels/closures.py index c63d069767d..bf50c8fc4ba 100644 --- a/homeassistant/components/zha/core/channels/closures.py +++ b/homeassistant/components/zha/core/channels/closures.py @@ -33,7 +33,8 @@ class DoorLockChannel(ZigbeeChannel): ): return - command_name = self._cluster.client_commands.get(command_id, [command_id])[0] + command_name = self._cluster.client_commands[command_id].name + if command_name == "operation_event_notification": self.zha_send_event( command_name, @@ -47,7 +48,7 @@ class DoorLockChannel(ZigbeeChannel): @callback def attribute_updated(self, attrid, value): """Handle attribute update from lock cluster.""" - attr_name = self.cluster.attributes.get(attrid, [attrid])[0] + attr_name = self._get_attribute_name(attrid) self.debug( "Attribute report '%s'[%s] = %s", self.cluster.name, attr_name, value ) @@ -140,7 +141,7 @@ class WindowCovering(ZigbeeChannel): @callback def attribute_updated(self, attrid, value): """Handle attribute update from window_covering cluster.""" - attr_name = self.cluster.attributes.get(attrid, [attrid])[0] + attr_name = self._get_attribute_name(attrid) self.debug( "Attribute report '%s'[%s] = %s", self.cluster.name, attr_name, value ) diff --git a/homeassistant/components/zha/core/channels/general.py b/homeassistant/components/zha/core/channels/general.py index 89d750465b8..09a1fd80f17 100644 --- a/homeassistant/components/zha/core/channels/general.py +++ b/homeassistant/components/zha/core/channels/general.py @@ -103,7 +103,7 @@ class AnalogOutput(ZigbeeChannel): except zigpy.exceptions.ZigbeeException as ex: self.error("Could not set value: %s", ex) return False - if isinstance(res, list) and all( + if not isinstance(res, Exception) and all( record.status == Status.SUCCESS for record in res[0] ): return True @@ -380,7 +380,11 @@ class Ota(ZigbeeChannel): self, tsn: int, command_id: int, args: list[Any] | None ) -> None: """Handle OTA commands.""" - cmd_name = self.cluster.server_commands.get(command_id, [command_id])[0] + if command_id in self.cluster.server_commands: + cmd_name = self.cluster.server_commands[command_id].name + else: + cmd_name = command_id + signal_id = self._ch_pool.unique_id.split("-")[0] if cmd_name == "query_next_image": self.async_send_signal(SIGNAL_UPDATE_DEVICE.format(signal_id), args[3]) @@ -418,7 +422,11 @@ class PollControl(ZigbeeChannel): self, tsn: int, command_id: int, args: list[Any] | None ) -> None: """Handle commands received to this cluster.""" - cmd_name = self.cluster.client_commands.get(command_id, [command_id])[0] + if command_id in self.cluster.client_commands: + cmd_name = self.cluster.client_commands[command_id].name + else: + cmd_name = command_id + self.debug("Received %s tsn command '%s': %s", tsn, cmd_name, args) self.zha_send_event(cmd_name, args) if cmd_name == "checkin": diff --git a/homeassistant/components/zha/core/channels/hvac.py b/homeassistant/components/zha/core/channels/hvac.py index 726d9f15376..5b102d062cb 100644 --- a/homeassistant/components/zha/core/channels/hvac.py +++ b/homeassistant/components/zha/core/channels/hvac.py @@ -70,7 +70,7 @@ class FanChannel(ZigbeeChannel): @callback def attribute_updated(self, attrid: int, value: Any) -> None: """Handle attribute update from fan cluster.""" - attr_name = self.cluster.attributes.get(attrid, [attrid])[0] + attr_name = self._get_attribute_name(attrid) self.debug( "Attribute report '%s'[%s] = %s", self.cluster.name, attr_name, value ) @@ -90,7 +90,7 @@ class ThermostatChannel(ZigbeeChannel): """Thermostat channel.""" REPORT_CONFIG = ( - {"attr": "local_temp", "config": REPORT_CONFIG_CLIMATE}, + {"attr": "local_temperature", "config": REPORT_CONFIG_CLIMATE}, {"attr": "occupied_cooling_setpoint", "config": REPORT_CONFIG_CLIMATE}, {"attr": "occupied_heating_setpoint", "config": REPORT_CONFIG_CLIMATE}, {"attr": "unoccupied_cooling_setpoint", "config": REPORT_CONFIG_CLIMATE}, @@ -107,7 +107,7 @@ class ThermostatChannel(ZigbeeChannel): "abs_max_heat_setpoint_limit": True, "abs_min_cool_setpoint_limit": True, "abs_max_cool_setpoint_limit": True, - "ctrl_seqe_of_oper": False, + "ctrl_sequence_of_oper": False, "max_cool_setpoint_limit": True, "max_heat_setpoint_limit": True, "min_cool_setpoint_limit": True, @@ -135,9 +135,9 @@ class ThermostatChannel(ZigbeeChannel): return self.cluster.get("abs_min_heat_setpoint_limit", 700) @property - def ctrl_seqe_of_oper(self) -> int: + def ctrl_sequence_of_oper(self) -> int: """Control Sequence of operations attribute.""" - return self.cluster.get("ctrl_seqe_of_oper", 0xFF) + return self.cluster.get("ctrl_sequence_of_oper", 0xFF) @property def max_cool_setpoint_limit(self) -> int: @@ -172,9 +172,9 @@ class ThermostatChannel(ZigbeeChannel): return sp_limit @property - def local_temp(self) -> int | None: + def local_temperature(self) -> int | None: """Thermostat temperature.""" - return self.cluster.get("local_temp") + return self.cluster.get("local_temperature") @property def occupancy(self) -> int | None: @@ -229,7 +229,7 @@ class ThermostatChannel(ZigbeeChannel): @callback def attribute_updated(self, attrid, value): """Handle attribute update cluster.""" - attr_name = self.cluster.attributes.get(attrid, [attrid])[0] + attr_name = self._get_attribute_name(attrid) self.debug( "Attribute report '%s'[%s] = %s", self.cluster.name, attr_name, value ) @@ -300,7 +300,7 @@ class ThermostatChannel(ZigbeeChannel): @staticmethod def check_result(res: list) -> bool: """Normalize the result.""" - if not isinstance(res, list): + if isinstance(res, Exception): return False return all(record.status == Status.SUCCESS for record in res[0]) diff --git a/homeassistant/components/zha/core/channels/lightlink.py b/homeassistant/components/zha/core/channels/lightlink.py index 46c40fdaff0..a29d9020a75 100644 --- a/homeassistant/components/zha/core/channels/lightlink.py +++ b/homeassistant/components/zha/core/channels/lightlink.py @@ -3,6 +3,7 @@ import asyncio import zigpy.exceptions from zigpy.zcl.clusters import lightlink +from zigpy.zcl.foundation import GENERAL_COMMANDS, GeneralCommand from .. import registries from .base import ChannelStatus, ZigbeeChannel @@ -30,11 +31,16 @@ class LightLink(ZigbeeChannel): return try: - _, _, groups = await self.cluster.get_group_identifiers(0) + rsp = await self.cluster.get_group_identifiers(0) except (zigpy.exceptions.ZigbeeException, asyncio.TimeoutError) as exc: self.warning("Couldn't get list of groups: %s", str(exc)) return + if isinstance(rsp, GENERAL_COMMANDS[GeneralCommand.Default_Response].schema): + groups = [] + else: + groups = rsp.group_info_records + if groups: for group in groups: self.debug("Adding coordinator to 0x%04x group id", group.group_id) diff --git a/homeassistant/components/zha/core/channels/manufacturerspecific.py b/homeassistant/components/zha/core/channels/manufacturerspecific.py index 8b840e76317..f31c7f51371 100644 --- a/homeassistant/components/zha/core/channels/manufacturerspecific.py +++ b/homeassistant/components/zha/core/channels/manufacturerspecific.py @@ -12,7 +12,7 @@ from ..const import ( SIGNAL_ATTR_UPDATED, UNKNOWN, ) -from .base import ZigbeeChannel +from .base import ClientChannel, ZigbeeChannel @registries.ZIGBEE_CHANNEL_REGISTRY.register(registries.SMARTTHINGS_HUMIDITY_CLUSTER) @@ -84,3 +84,11 @@ class SmartThingsAcceleration(ZigbeeChannel): ATTR_VALUE: value, }, ) + + +@registries.CHANNEL_ONLY_CLUSTERS.register(0xFC31) +@registries.CLIENT_CHANNELS_REGISTRY.register(0xFC31) +class InovelliCluster(ClientChannel): + """Inovelli Button Press Event channel.""" + + REPORT_CONFIG = [] diff --git a/homeassistant/components/zha/core/channels/security.py b/homeassistant/components/zha/core/channels/security.py index 5ef0ee3d9fa..19be861178f 100644 --- a/homeassistant/components/zha/core/channels/security.py +++ b/homeassistant/components/zha/core/channels/security.py @@ -12,7 +12,7 @@ from zigpy.exceptions import ZigbeeException from zigpy.zcl.clusters import security from zigpy.zcl.clusters.security import IasAce as AceCluster -from homeassistant.core import CALLABLE_T, callback +from homeassistant.core import callback from .. import registries, typing as zha_typing from ..const import ( @@ -23,6 +23,7 @@ from ..const import ( WARNING_DEVICE_STROBE_HIGH, WARNING_DEVICE_STROBE_YES, ) +from ..typing import CALLABLE_T from .base import ChannelStatus, ZigbeeChannel IAS_ACE_ARM = 0x0000 # ("arm", (t.enum8, t.CharacterString, t.uint8_t), False), @@ -84,7 +85,7 @@ class IasAce(ZigbeeChannel): def cluster_command(self, tsn, command_id, args) -> None: """Handle commands received to this cluster.""" self.warning( - "received command %s", self._cluster.server_commands.get(command_id)[NAME] + "received command %s", self._cluster.server_commands[command_id].name ) self.command_map[command_id](*args) @@ -93,7 +94,7 @@ class IasAce(ZigbeeChannel): mode = AceCluster.ArmMode(arm_mode) self.zha_send_event( - self._cluster.server_commands.get(IAS_ACE_ARM)[NAME], + self._cluster.server_commands[IAS_ACE_ARM].name, { "arm_mode": mode.value, "arm_mode_description": mode.name, @@ -189,7 +190,7 @@ class IasAce(ZigbeeChannel): def _bypass(self, zone_list, code) -> None: """Handle the IAS ACE bypass command.""" self.zha_send_event( - self._cluster.server_commands.get(IAS_ACE_BYPASS)[NAME], + self._cluster.server_commands[IAS_ACE_BYPASS].name, {"zone_list": zone_list, "code": code}, ) diff --git a/homeassistant/components/zha/core/channels/smartenergy.py b/homeassistant/components/zha/core/channels/smartenergy.py index 5877dad14fa..b153372a322 100644 --- a/homeassistant/components/zha/core/channels/smartenergy.py +++ b/homeassistant/components/zha/core/channels/smartenergy.py @@ -65,7 +65,7 @@ class Metering(ZigbeeChannel): "divisor": True, "metering_device_type": True, "multiplier": True, - "summa_formatting": True, + "summation_formatting": True, "unit_of_measure": True, } @@ -159,7 +159,7 @@ class Metering(ZigbeeChannel): self._format_spec = self.get_formatting(fmting) fmting = self.cluster.get( - "summa_formatting", 0xF9 + "summation_formatting", 0xF9 ) # 1 digit to the right, 15 digits to the left self._summa_format = self.get_formatting(fmting) diff --git a/homeassistant/components/zha/core/const.py b/homeassistant/components/zha/core/const.py index 9216b6dd600..b3f06b9eba6 100644 --- a/homeassistant/components/zha/core/const.py +++ b/homeassistant/components/zha/core/const.py @@ -6,6 +6,7 @@ import logging import bellows.zigbee.application import voluptuous as vol +import zigpy.application from zigpy.config import CONF_DEVICE_PATH # noqa: F401 # pylint: disable=unused-import import zigpy.types as t import zigpy_deconz.zigbee.application @@ -16,8 +17,6 @@ import zigpy_znp.zigbee.application from homeassistant.const import Platform import homeassistant.helpers.config_validation as cv -from .typing import CALLABLE_T - ATTR_ARGS = "args" ATTR_ATTRIBUTE = "attribute" ATTR_ATTRIBUTE_ID = "attribute_id" @@ -224,6 +223,8 @@ ZHA_CONFIG_SCHEMAS = { ZHA_ALARM_OPTIONS: CONF_ZHA_ALARM_SCHEMA, } +_ControllerClsType = type[zigpy.application.ControllerApplication] + class RadioType(enum.Enum): """Possible options for radio type.""" @@ -262,13 +263,13 @@ class RadioType(enum.Enum): return radio.name raise ValueError - def __init__(self, description: str, controller_cls: CALLABLE_T) -> None: + def __init__(self, description: str, controller_cls: _ControllerClsType) -> None: """Init instance.""" self._desc = description self._ctrl_cls = controller_cls @property - def controller(self) -> CALLABLE_T: + def controller(self) -> _ControllerClsType: """Return controller class.""" return self._ctrl_cls diff --git a/homeassistant/components/zha/core/decorators.py b/homeassistant/components/zha/core/decorators.py index a27e4cc0bfc..c57cad7d65e 100644 --- a/homeassistant/components/zha/core/decorators.py +++ b/homeassistant/components/zha/core/decorators.py @@ -2,37 +2,32 @@ from __future__ import annotations from collections.abc import Callable -from typing import TypeVar +from typing import Any, TypeVar, Union -CALLABLE_T = TypeVar("CALLABLE_T", bound=Callable) # pylint: disable=invalid-name +_TypeT = TypeVar("_TypeT", bound=type[Any]) -class DictRegistry(dict): +class DictRegistry(dict[Union[int, str], _TypeT]): """Dict Registry of items.""" - def register( - self, name: int | str, item: str | CALLABLE_T = None - ) -> Callable[[CALLABLE_T], CALLABLE_T]: + def register(self, name: int | str) -> Callable[[_TypeT], _TypeT]: """Return decorator to register item with a specific name.""" - def decorator(channel: CALLABLE_T) -> CALLABLE_T: + def decorator(channel: _TypeT) -> _TypeT: """Register decorated channel or item.""" - if item is None: - self[name] = channel - else: - self[name] = item + self[name] = channel return channel return decorator -class SetRegistry(set): +class SetRegistry(set[Union[int, str]]): """Set Registry of items.""" - def register(self, name: int | str) -> Callable[[CALLABLE_T], CALLABLE_T]: + def register(self, name: int | str) -> Callable[[_TypeT], _TypeT]: """Return decorator to register item with a specific name.""" - def decorator(channel: CALLABLE_T) -> CALLABLE_T: + def decorator(channel: _TypeT) -> _TypeT: """Register decorated channel or item.""" self.add(name) return channel diff --git a/homeassistant/components/zha/core/device.py b/homeassistant/components/zha/core/device.py index e8b38fb9699..e80a0725cc1 100644 --- a/homeassistant/components/zha/core/device.py +++ b/homeassistant/components/zha/core/device.py @@ -2,17 +2,19 @@ from __future__ import annotations import asyncio +from collections.abc import Callable from datetime import timedelta from enum import Enum import logging import random import time -from typing import Any +from typing import TYPE_CHECKING, Any from zigpy import types import zigpy.exceptions from zigpy.profiles import PROFILES import zigpy.quirks +from zigpy.types.named import EUI64, NWK from zigpy.zcl.clusters.general import Groups import zigpy.zdo.types as zdo_types @@ -73,6 +75,9 @@ from .const import ( ) from .helpers import LogMixin, async_get_zha_config_value +if TYPE_CHECKING: + from ..api import ClusterBinding + _LOGGER = logging.getLogger(__name__) _UPDATE_ALIVE_INTERVAL = (60, 90) _CHECKIN_GRACE_PERIODS = 2 @@ -88,6 +93,8 @@ class DeviceStatus(Enum): class ZHADevice(LogMixin): """ZHA Zigbee device object.""" + _ha_device_id: str + def __init__( self, hass: HomeAssistant, @@ -101,7 +108,7 @@ class ZHADevice(LogMixin): self._available = False self._available_signal = f"{self.name}_{self.ieee}_{SIGNAL_AVAILABLE}" self._checkins_missed_count = 0 - self.unsubs = [] + self.unsubs: list[Callable[[], None]] = [] self.quirk_applied = isinstance(self._zigpy_device, zigpy.quirks.CustomDevice) self.quirk_class = ( f"{self._zigpy_device.__class__.__module__}." @@ -129,16 +136,15 @@ class ZHADevice(LogMixin): self.hass, self._check_available, timedelta(seconds=keep_alive_interval) ) ) - self._ha_device_id = None - self.status = DeviceStatus.CREATED + self.status: DeviceStatus = DeviceStatus.CREATED self._channels = channels.Channels(self) @property - def device_id(self): + def device_id(self) -> str: """Return the HA device registry device id.""" return self._ha_device_id - def set_device_id(self, device_id): + def set_device_id(self, device_id: str) -> None: """Set the HA device registry device id.""" self._ha_device_id = device_id @@ -159,24 +165,24 @@ class ZHADevice(LogMixin): self._channels = value @property - def name(self): + def name(self) -> str: """Return device name.""" return f"{self.manufacturer} {self.model}" @property - def ieee(self): + def ieee(self) -> EUI64: """Return ieee address for device.""" return self._zigpy_device.ieee @property - def manufacturer(self): + def manufacturer(self) -> str: """Return manufacturer for device.""" if self._zigpy_device.manufacturer is None: return UNKNOWN_MANUFACTURER return self._zigpy_device.manufacturer @property - def model(self): + def model(self) -> str: """Return model for device.""" if self._zigpy_device.model is None: return UNKNOWN_MODEL @@ -191,7 +197,7 @@ class ZHADevice(LogMixin): return self._zigpy_device.node_desc.manufacturer_code @property - def nwk(self): + def nwk(self) -> NWK: """Return nwk for device.""" return self._zigpy_device.nwk @@ -206,7 +212,7 @@ class ZHADevice(LogMixin): return self._zigpy_device.rssi @property - def last_seen(self): + def last_seen(self) -> float | None: """Return last_seen for device.""" return self._zigpy_device.last_seen @@ -227,7 +233,7 @@ class ZHADevice(LogMixin): return self._zigpy_device.node_desc.logical_type.name @property - def power_source(self): + def power_source(self) -> str: """Return the power source for the device.""" return ( POWER_MAINS_POWERED if self.is_mains_powered else POWER_BATTERY_OR_UNKNOWN @@ -258,14 +264,14 @@ class ZHADevice(LogMixin): return self._zigpy_device.node_desc.is_end_device @property - def is_groupable(self): + def is_groupable(self) -> bool: """Return true if this device has a group cluster.""" return self.is_coordinator or ( - self.available and self.async_get_groupable_endpoints() + self.available and bool(self.async_get_groupable_endpoints()) ) @property - def skip_configuration(self): + def skip_configuration(self) -> bool: """Return true if the device should not issue configuration related commands.""" return self._zigpy_device.skip_configuration @@ -275,7 +281,7 @@ class ZHADevice(LogMixin): return self._zha_gateway @property - def device_automation_triggers(self): + def device_automation_triggers(self) -> dict[tuple[str, str], dict[str, str]]: """Return the device automation triggers for this device.""" triggers = { ("device_offline", "device_offline"): { @@ -289,7 +295,7 @@ class ZHADevice(LogMixin): return triggers @property - def available_signal(self): + def available_signal(self) -> str: """Signal to use to subscribe to device availability changes.""" return self._available_signal @@ -332,7 +338,7 @@ class ZHADevice(LogMixin): return zha_dev @callback - def async_update_sw_build_id(self, sw_version: int): + def async_update_sw_build_id(self, sw_version: int) -> None: """Update device sw version.""" if self.device_id is None: return @@ -340,7 +346,7 @@ class ZHADevice(LogMixin): self.device_id, sw_version=f"0x{sw_version:08x}" ) - async def _check_available(self, *_): + async def _check_available(self, *_: Any) -> None: # don't flip the availability state of the coordinator if self.is_coordinator: return @@ -400,7 +406,7 @@ class ZHADevice(LogMixin): async_dispatcher_send(self.hass, f"{self._available_signal}_entity") @property - def device_info(self): + def device_info(self) -> dict[str, Any]: """Return a device description for device.""" ieee = str(self.ieee) time_struct = time.localtime(self.last_seen) @@ -423,7 +429,7 @@ class ZHADevice(LogMixin): ATTR_SIGNATURE: self.zigbee_signature, } - async def async_configure(self): + async def async_configure(self) -> None: """Configure the device.""" should_identify = async_get_zha_config_value( self._zha_gateway.config_entry, @@ -446,7 +452,7 @@ class ZHADevice(LogMixin): EFFECT_OKAY, EFFECT_DEFAULT_VARIANT ) - async def async_initialize(self, from_cache=False): + async def async_initialize(self, from_cache: bool = False) -> None: """Initialize channels.""" self.debug("started initialization") await self._channels.async_initialize(from_cache) @@ -461,15 +467,15 @@ class ZHADevice(LogMixin): unsubscribe() @callback - def async_update_last_seen(self, last_seen): + def async_update_last_seen(self, last_seen: float | None) -> None: """Set last seen on the zigpy device.""" if self._zigpy_device.last_seen is None and last_seen is not None: self._zigpy_device.last_seen = last_seen @property - def zha_device_info(self): + def zha_device_info(self) -> dict[str, Any]: """Get ZHA device information.""" - device_info = {} + device_info: dict[str, Any] = {} device_info.update(self.device_info) device_info["entities"] = [ { @@ -496,7 +502,7 @@ class ZHADevice(LogMixin): ] # Return endpoint device type Names - names = [] + names: list[dict[str, str]] = [] for endpoint in (ep for epid, ep in self.device.endpoints.items() if epid): profile = PROFILES.get(endpoint.profile_id) if profile and endpoint.device_type is not None: @@ -652,7 +658,7 @@ class ZHADevice(LogMixin): ) return response - async def async_add_to_group(self, group_id): + async def async_add_to_group(self, group_id: int) -> None: """Add this device to the provided zigbee group.""" try: await self._zigpy_device.add_to_group(group_id) @@ -664,7 +670,7 @@ class ZHADevice(LogMixin): str(ex), ) - async def async_remove_from_group(self, group_id): + async def async_remove_from_group(self, group_id: int) -> None: """Remove this device from the provided zigbee group.""" try: await self._zigpy_device.remove_from_group(group_id) @@ -676,10 +682,12 @@ class ZHADevice(LogMixin): str(ex), ) - async def async_add_endpoint_to_group(self, endpoint_id, group_id): + async def async_add_endpoint_to_group( + self, endpoint_id: int, group_id: int + ) -> None: """Add the device endpoint to the provided zigbee group.""" try: - await self._zigpy_device.endpoints[int(endpoint_id)].add_to_group(group_id) + await self._zigpy_device.endpoints[endpoint_id].add_to_group(group_id) except (zigpy.exceptions.ZigbeeException, asyncio.TimeoutError) as ex: self.debug( "Failed to add endpoint: %s for device: '%s' to group: 0x%04x ex: %s", @@ -689,12 +697,12 @@ class ZHADevice(LogMixin): str(ex), ) - async def async_remove_endpoint_from_group(self, endpoint_id, group_id): + async def async_remove_endpoint_from_group( + self, endpoint_id: int, group_id: int + ) -> None: """Remove the device endpoint from the provided zigbee group.""" try: - await self._zigpy_device.endpoints[int(endpoint_id)].remove_from_group( - group_id - ) + await self._zigpy_device.endpoints[endpoint_id].remove_from_group(group_id) except (zigpy.exceptions.ZigbeeException, asyncio.TimeoutError) as ex: self.debug( "Failed to remove endpoint: %s for device '%s' from group: 0x%04x ex: %s", @@ -704,21 +712,28 @@ class ZHADevice(LogMixin): str(ex), ) - async def async_bind_to_group(self, group_id, cluster_bindings): + async def async_bind_to_group( + self, group_id: int, cluster_bindings: list[ClusterBinding] + ) -> None: """Directly bind this device to a group for the given clusters.""" await self._async_group_binding_operation( group_id, zdo_types.ZDOCmd.Bind_req, cluster_bindings ) - async def async_unbind_from_group(self, group_id, cluster_bindings): + async def async_unbind_from_group( + self, group_id: int, cluster_bindings: list[ClusterBinding] + ) -> None: """Unbind this device from a group for the given clusters.""" await self._async_group_binding_operation( group_id, zdo_types.ZDOCmd.Unbind_req, cluster_bindings ) async def _async_group_binding_operation( - self, group_id, operation, cluster_bindings - ): + self, + group_id: int, + operation: zdo_types.ZDOCmd, + cluster_bindings: list[ClusterBinding], + ) -> None: """Create or remove a direct zigbee binding between a device and a group.""" zdo = self._zigpy_device.zdo @@ -768,8 +783,8 @@ class ZHADevice(LogMixin): fmt = f"{log_msg[1]} completed: %s" zdo.debug(fmt, *(log_msg[2] + (outcome,))) - def log(self, level, msg, *args): + def log(self, level: int, msg: str, *args: Any, **kwargs: dict) -> None: """Log a message.""" msg = f"[%s](%s): {msg}" args = (self.nwk, self.model) + args - _LOGGER.log(level, msg, *args) + _LOGGER.log(level, msg, *args, **kwargs) diff --git a/homeassistant/components/zha/core/discovery.py b/homeassistant/components/zha/core/discovery.py index 26323793e13..9f7523d41f0 100644 --- a/homeassistant/components/zha/core/discovery.py +++ b/homeassistant/components/zha/core/discovery.py @@ -4,6 +4,7 @@ from __future__ import annotations from collections import Counter from collections.abc import Callable import logging +from typing import TYPE_CHECKING from homeassistant import const as ha_const from homeassistant.core import HomeAssistant, callback @@ -11,9 +12,11 @@ from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_registry import async_entries_for_device +from homeassistant.helpers.typing import ConfigType -from . import const as zha_const, registries as zha_regs, typing as zha_typing +from . import const as zha_const, registries as zha_regs from .. import ( # noqa: F401 pylint: disable=unused-import, alarm_control_panel, binary_sensor, @@ -32,16 +35,23 @@ from .. import ( # noqa: F401 pylint: disable=unused-import, ) from .channels import base +if TYPE_CHECKING: + from ..entity import ZhaEntity + from .channels import ChannelPool + from .device import ZHADevice + from .gateway import ZHAGateway + from .group import ZHAGroup + _LOGGER = logging.getLogger(__name__) @callback async def async_add_entities( - _async_add_entities: Callable, + _async_add_entities: AddEntitiesCallback, entities: list[ tuple[ - zha_typing.ZhaEntityType, - tuple[str, zha_typing.ZhaDeviceType, list[zha_typing.ChannelType]], + type[ZhaEntity], + tuple[str, ZHADevice, list[base.ZigbeeChannel]], ] ], update_before_add: bool = True, @@ -50,20 +60,20 @@ async def async_add_entities( if not entities: return to_add = [ent_cls.create_entity(*args) for ent_cls, args in entities] - to_add = [entity for entity in to_add if entity is not None] - _async_add_entities(to_add, update_before_add=update_before_add) + entities_to_add = [entity for entity in to_add if entity is not None] + _async_add_entities(entities_to_add, update_before_add=update_before_add) entities.clear() class ProbeEndpoint: """All discovered channels and entities of an endpoint.""" - def __init__(self): + def __init__(self) -> None: """Initialize instance.""" - self._device_configs = {} + self._device_configs: ConfigType = {} @callback - def discover_entities(self, channel_pool: zha_typing.ChannelPoolType) -> None: + def discover_entities(self, channel_pool: ChannelPool) -> None: """Process an endpoint on a zigpy device.""" self.discover_by_device_type(channel_pool) self.discover_multi_entities(channel_pool) @@ -71,12 +81,14 @@ class ProbeEndpoint: zha_regs.ZHA_ENTITIES.clean_up() @callback - def discover_by_device_type(self, channel_pool: zha_typing.ChannelPoolType) -> None: + def discover_by_device_type(self, channel_pool: ChannelPool) -> None: """Process an endpoint on a zigpy device.""" unique_id = channel_pool.unique_id - component = self._device_configs.get(unique_id, {}).get(ha_const.CONF_TYPE) + component: str | None = self._device_configs.get(unique_id, {}).get( + ha_const.CONF_TYPE + ) if component is None: ep_profile_id = channel_pool.endpoint.profile_id ep_device_type = channel_pool.endpoint.device_type @@ -93,7 +105,7 @@ class ProbeEndpoint: channel_pool.async_new_entity(component, entity_class, unique_id, claimed) @callback - def discover_by_cluster_id(self, channel_pool: zha_typing.ChannelPoolType) -> None: + def discover_by_cluster_id(self, channel_pool: ChannelPool) -> None: """Process an endpoint on a zigpy device.""" items = zha_regs.SINGLE_INPUT_CLUSTER_DEVICE_CLASS.items() @@ -125,8 +137,8 @@ class ProbeEndpoint: @staticmethod def probe_single_cluster( component: str, - channel: zha_typing.ChannelType, - ep_channels: zha_typing.ChannelPoolType, + channel: base.ZigbeeChannel, + ep_channels: ChannelPool, ) -> None: """Probe specified cluster for specific component.""" if component is None or component not in zha_const.PLATFORMS: @@ -142,9 +154,7 @@ class ProbeEndpoint: ep_channels.claim_channels(claimed) ep_channels.async_new_entity(component, entity_class, unique_id, claimed) - def handle_on_off_output_cluster_exception( - self, ep_channels: zha_typing.ChannelPoolType - ) -> None: + def handle_on_off_output_cluster_exception(self, ep_channels: ChannelPool) -> None: """Process output clusters of the endpoint.""" profile_id = ep_channels.endpoint.profile_id @@ -167,7 +177,7 @@ class ProbeEndpoint: @staticmethod @callback - def discover_multi_entities(channel_pool: zha_typing.ChannelPoolType) -> None: + def discover_multi_entities(channel_pool: ChannelPool) -> None: """Process an endpoint on and discover multiple entities.""" ep_profile_id = channel_pool.endpoint.profile_id @@ -209,7 +219,9 @@ class ProbeEndpoint: def initialize(self, hass: HomeAssistant) -> None: """Update device overrides config.""" - zha_config = hass.data[zha_const.DATA_ZHA].get(zha_const.DATA_ZHA_CONFIG, {}) + zha_config: ConfigType = hass.data[zha_const.DATA_ZHA].get( + zha_const.DATA_ZHA_CONFIG, {} + ) if overrides := zha_config.get(zha_const.CONF_DEVICE_CONFIG): self._device_configs.update(overrides) @@ -217,10 +229,11 @@ class ProbeEndpoint: class GroupProbe: """Determine the appropriate component for a group.""" - def __init__(self): + _hass: HomeAssistant + + def __init__(self) -> None: """Initialize instance.""" - self._hass = None - self._unsubs = [] + self._unsubs: list[Callable[[], None]] = [] def initialize(self, hass: HomeAssistant) -> None: """Initialize the group probe.""" @@ -231,7 +244,7 @@ class GroupProbe: ) ) - def cleanup(self): + def cleanup(self) -> None: """Clean up on when zha shuts down.""" for unsub in self._unsubs[:]: unsub() @@ -240,13 +253,15 @@ class GroupProbe: @callback def _reprobe_group(self, group_id: int) -> None: """Reprobe a group for entities after its members change.""" - zha_gateway = self._hass.data[zha_const.DATA_ZHA][zha_const.DATA_ZHA_GATEWAY] + zha_gateway: ZHAGateway = self._hass.data[zha_const.DATA_ZHA][ + zha_const.DATA_ZHA_GATEWAY + ] if (zha_group := zha_gateway.groups.get(group_id)) is None: return self.discover_group_entities(zha_group) @callback - def discover_group_entities(self, group: zha_typing.ZhaGroupType) -> None: + def discover_group_entities(self, group: ZHAGroup) -> None: """Process a group and create any entities that are needed.""" # only create a group entity if there are 2 or more members in a group if len(group.members) < 2: @@ -262,7 +277,9 @@ class GroupProbe: if not entity_domains: return - zha_gateway = self._hass.data[zha_const.DATA_ZHA][zha_const.DATA_ZHA_GATEWAY] + zha_gateway: ZHAGateway = self._hass.data[zha_const.DATA_ZHA][ + zha_const.DATA_ZHA_GATEWAY + ] for domain in entity_domains: entity_class = zha_regs.ZHA_ENTITIES.get_group_entity(domain) if entity_class is None: @@ -281,12 +298,12 @@ class GroupProbe: async_dispatcher_send(self._hass, zha_const.SIGNAL_ADD_ENTITIES) @staticmethod - def determine_entity_domains( - hass: HomeAssistant, group: zha_typing.ZhaGroupType - ) -> list[str]: + def determine_entity_domains(hass: HomeAssistant, group: ZHAGroup) -> list[str]: """Determine the entity domains for this group.""" entity_domains: list[str] = [] - zha_gateway = hass.data[zha_const.DATA_ZHA][zha_const.DATA_ZHA_GATEWAY] + zha_gateway: ZHAGateway = hass.data[zha_const.DATA_ZHA][ + zha_const.DATA_ZHA_GATEWAY + ] all_domain_occurrences = [] for member in group.members: if member.device.is_coordinator: diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index 252893683ef..64f7b24ff99 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -3,6 +3,7 @@ from __future__ import annotations import asyncio import collections +from collections.abc import Callable from datetime import timedelta from enum import Enum import itertools @@ -10,26 +11,36 @@ import logging import os import time import traceback +from typing import TYPE_CHECKING, Any, NamedTuple, Union from serial import SerialException +from zigpy.application import ControllerApplication from zigpy.config import CONF_DEVICE -import zigpy.device as zigpy_dev +import zigpy.device +import zigpy.endpoint +import zigpy.group +from zigpy.types.named import EUI64 from homeassistant.components.system_log import LogEntry, _figure_out_source -from homeassistant.core import callback +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.device_registry import ( CONNECTION_ZIGBEE, + DeviceRegistry, async_get_registry as get_dev_reg, ) from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_registry import ( + EntityRegistry, async_entries_for_device, async_get_registry as get_ent_reg, ) from homeassistant.helpers.event import async_track_time_interval +from homeassistant.helpers.typing import ConfigType -from . import discovery, typing as zha_typing +from . import discovery from .const import ( ATTR_IEEE, ATTR_MANUFACTURER, @@ -81,14 +92,27 @@ from .device import DeviceStatus, ZHADevice from .group import GroupMember, ZHAGroup from .registries import GROUP_ENTITY_DOMAINS from .store import async_get_registry -from .typing import ZhaGroupType, ZigpyEndpointType, ZigpyGroupType + +if TYPE_CHECKING: + from logging import Filter, LogRecord + + from ..entity import ZhaEntity + from .channels.base import ZigbeeChannel + from .store import ZhaStorage + + _LogFilterType = Union[Filter, Callable[[LogRecord], int]] _LOGGER = logging.getLogger(__name__) -EntityReference = collections.namedtuple( - "EntityReference", - "reference_id zha_device cluster_channels device_info remove_future", -) + +class EntityReference(NamedTuple): + """Describes an entity reference.""" + + reference_id: str + zha_device: ZHADevice + cluster_channels: dict[str, ZigbeeChannel] + device_info: DeviceInfo + remove_future: asyncio.Future[Any] class DevicePairingStatus(Enum): @@ -103,29 +127,35 @@ class DevicePairingStatus(Enum): class ZHAGateway: """Gateway that handles events that happen on the ZHA Zigbee network.""" - def __init__(self, hass, config, config_entry): + # -- Set in async_initialize -- + zha_storage: ZhaStorage + ha_device_registry: DeviceRegistry + ha_entity_registry: EntityRegistry + application_controller: ControllerApplication + radio_description: str + + def __init__( + self, hass: HomeAssistant, config: ConfigType, config_entry: ConfigEntry + ) -> None: """Initialize the gateway.""" self._hass = hass self._config = config - self._devices = {} - self._groups = {} - self.coordinator_zha_device = None - self._device_registry = collections.defaultdict(list) - self.zha_storage = None - self.ha_device_registry = None - self.ha_entity_registry = None - self.application_controller = None - self.radio_description = None - self._log_levels = { + self._devices: dict[EUI64, ZHADevice] = {} + self._groups: dict[int, ZHAGroup] = {} + self.coordinator_zha_device: ZHADevice | None = None + self._device_registry: collections.defaultdict[ + EUI64, list[EntityReference] + ] = collections.defaultdict(list) + self._log_levels: dict[str, dict[str, int]] = { DEBUG_LEVEL_ORIGINAL: async_capture_log_levels(), DEBUG_LEVEL_CURRENT: async_capture_log_levels(), } self.debug_enabled = False self._log_relay_handler = LogRelayHandler(hass, self) self.config_entry = config_entry - self._unsubs = [] + self._unsubs: list[Callable[[], None]] = [] - async def async_initialize(self): + async def async_initialize(self) -> None: """Initialize controller and connect radio.""" discovery.PROBE.initialize(self._hass) discovery.GROUP_PROBE.initialize(self._hass) @@ -211,7 +241,7 @@ class ZHAGateway: """Initialize devices and load entities.""" semaphore = asyncio.Semaphore(2) - async def _throttle(zha_device: zha_typing.ZhaDeviceType, cached: bool): + async def _throttle(zha_device: ZHADevice, cached: bool) -> None: async with semaphore: await zha_device.async_initialize(from_cache=cached) @@ -233,7 +263,7 @@ class ZHAGateway: ) ) - def device_joined(self, device): + def device_joined(self, device: zigpy.device.Device) -> None: """Handle device joined. At this point, no information about the device is known other than its @@ -252,7 +282,7 @@ class ZHAGateway: }, ) - def raw_device_initialized(self, device): + def raw_device_initialized(self, device: zigpy.device.Device) -> None: """Handle a device initialization without quirks loaded.""" manuf = device.manufacturer async_dispatcher_send( @@ -271,16 +301,16 @@ class ZHAGateway: }, ) - def device_initialized(self, device): + def device_initialized(self, device: zigpy.device.Device) -> None: """Handle device joined and basic information discovered.""" self._hass.async_create_task(self.async_device_initialized(device)) - def device_left(self, device: zigpy_dev.Device): + def device_left(self, device: zigpy.device.Device) -> None: """Handle device leaving the network.""" self.async_update_device(device, False) def group_member_removed( - self, zigpy_group: ZigpyGroupType, endpoint: ZigpyEndpointType + self, zigpy_group: zigpy.group.Group, endpoint: zigpy.endpoint.Endpoint ) -> None: """Handle zigpy group member removed event.""" # need to handle endpoint correctly on groups @@ -292,7 +322,7 @@ class ZHAGateway: ) def group_member_added( - self, zigpy_group: ZigpyGroupType, endpoint: ZigpyEndpointType + self, zigpy_group: zigpy.group.Group, endpoint: zigpy.endpoint.Endpoint ) -> None: """Handle zigpy group member added event.""" # need to handle endpoint correctly on groups @@ -306,14 +336,14 @@ class ZHAGateway: # we need to do this because there wasn't already a group entity to remove and re-add discovery.GROUP_PROBE.discover_group_entities(zha_group) - def group_added(self, zigpy_group: ZigpyGroupType) -> None: + def group_added(self, zigpy_group: zigpy.group.Group) -> None: """Handle zigpy group added event.""" zha_group = self._async_get_or_create_group(zigpy_group) zha_group.info("group_added") # need to dispatch for entity creation here self._send_group_gateway_message(zigpy_group, ZHA_GW_MSG_GROUP_ADDED) - def group_removed(self, zigpy_group: ZigpyGroupType) -> None: + def group_removed(self, zigpy_group: zigpy.group.Group) -> None: """Handle zigpy group removed event.""" self._send_group_gateway_message(zigpy_group, ZHA_GW_MSG_GROUP_REMOVED) zha_group = self._groups.pop(zigpy_group.group_id, None) @@ -321,7 +351,7 @@ class ZHAGateway: self._cleanup_group_entity_registry_entries(zigpy_group) def _send_group_gateway_message( - self, zigpy_group: ZigpyGroupType, gateway_message_type: str + self, zigpy_group: zigpy.group.Group, gateway_message_type: str ) -> None: """Send the gateway event for a zigpy group event.""" zha_group = self._groups.get(zigpy_group.group_id) @@ -335,9 +365,11 @@ class ZHAGateway: }, ) - async def _async_remove_device(self, device, entity_refs): + async def _async_remove_device( + self, device: ZHADevice, entity_refs: list[EntityReference] | None + ) -> None: if entity_refs is not None: - remove_tasks = [] + remove_tasks: list[asyncio.Future[Any]] = [] for entity_ref in entity_refs: remove_tasks.append(entity_ref.remove_future) if remove_tasks: @@ -346,7 +378,7 @@ class ZHAGateway: if reg_device is not None: self.ha_device_registry.async_remove_device(reg_device.id) - def device_removed(self, device): + def device_removed(self, device: zigpy.device.Device) -> None: """Handle device being removed from the network.""" zha_device = self._devices.pop(device.ieee, None) entity_refs = self._device_registry.pop(device.ieee, None) @@ -365,40 +397,41 @@ class ZHAGateway: }, ) - def get_device(self, ieee): + def get_device(self, ieee: EUI64) -> ZHADevice | None: """Return ZHADevice for given ieee.""" return self._devices.get(ieee) - def get_group(self, group_id: str) -> ZhaGroupType | None: + def get_group(self, group_id: int) -> ZHAGroup | None: """Return Group for given group id.""" return self.groups.get(group_id) @callback - def async_get_group_by_name(self, group_name: str) -> ZhaGroupType | None: + def async_get_group_by_name(self, group_name: str) -> ZHAGroup | None: """Get ZHA group by name.""" for group in self.groups.values(): if group.name == group_name: return group return None - def get_entity_reference(self, entity_id): + def get_entity_reference(self, entity_id: str) -> EntityReference | None: """Return entity reference for given entity_id if found.""" for entity_reference in itertools.chain.from_iterable( self.device_registry.values() ): if entity_id == entity_reference.reference_id: return entity_reference + return None - def remove_entity_reference(self, entity): + def remove_entity_reference(self, entity: ZhaEntity) -> None: """Remove entity reference for given entity_id if found.""" if entity.zha_device.ieee in self.device_registry: entity_refs = self.device_registry.get(entity.zha_device.ieee) self.device_registry[entity.zha_device.ieee] = [ - e for e in entity_refs if e.reference_id != entity.entity_id + e for e in entity_refs if e.reference_id != entity.entity_id # type: ignore[union-attr] ] def _cleanup_group_entity_registry_entries( - self, zigpy_group: ZigpyGroupType + self, zigpy_group: zigpy.group.Group ) -> None: """Remove entity registry entries for group entities when the groups are removed from HA.""" # first we collect the potential unique ids for entities that could be created from this group @@ -429,28 +462,28 @@ class ZHAGateway: self.ha_entity_registry.async_remove(entry.entity_id) @property - def devices(self): + def devices(self) -> dict[EUI64, ZHADevice]: """Return devices.""" return self._devices @property - def groups(self): + def groups(self) -> dict[int, ZHAGroup]: """Return groups.""" return self._groups @property - def device_registry(self): + def device_registry(self) -> collections.defaultdict[EUI64, list[EntityReference]]: """Return entities by ieee.""" return self._device_registry def register_entity_reference( self, - ieee, - reference_id, - zha_device, - cluster_channels, - device_info, - remove_future, + ieee: EUI64, + reference_id: str, + zha_device: ZHADevice, + cluster_channels: dict[str, ZigbeeChannel], + device_info: DeviceInfo, + remove_future: asyncio.Future[Any], ): """Record the creation of a hass entity associated with ieee.""" self._device_registry[ieee].append( @@ -464,7 +497,7 @@ class ZHAGateway: ) @callback - def async_enable_debug_mode(self, filterer=None): + def async_enable_debug_mode(self, filterer: _LogFilterType | None = None) -> None: """Enable debug mode for ZHA.""" self._log_levels[DEBUG_LEVEL_ORIGINAL] = async_capture_log_levels() async_set_logger_levels(DEBUG_LEVELS) @@ -479,7 +512,7 @@ class ZHAGateway: self.debug_enabled = True @callback - def async_disable_debug_mode(self, filterer=None): + def async_disable_debug_mode(self, filterer: _LogFilterType | None = None) -> None: """Disable debug mode for ZHA.""" async_set_logger_levels(self._log_levels[DEBUG_LEVEL_ORIGINAL]) self._log_levels[DEBUG_LEVEL_CURRENT] = async_capture_log_levels() @@ -491,8 +524,8 @@ class ZHAGateway: @callback def _async_get_or_create_device( - self, zigpy_device: zha_typing.ZigpyDeviceType, restored: bool = False - ): + self, zigpy_device: zigpy.device.Device, restored: bool = False + ) -> ZHADevice: """Get or create a ZHA device.""" if (zha_device := self._devices.get(zigpy_device.ieee)) is None: zha_device = ZHADevice.new(self._hass, zigpy_device, self, restored) @@ -511,7 +544,7 @@ class ZHAGateway: return zha_device @callback - def _async_get_or_create_group(self, zigpy_group: ZigpyGroupType) -> ZhaGroupType: + def _async_get_or_create_group(self, zigpy_group: zigpy.group.Group) -> ZHAGroup: """Get or create a ZHA group.""" zha_group = self._groups.get(zigpy_group.group_id) if zha_group is None: @@ -521,7 +554,7 @@ class ZHAGateway: @callback def async_update_device( - self, sender: zigpy_dev.Device, available: bool = True + self, sender: zigpy.device.Device, available: bool = True ) -> None: """Update device that has just become available.""" if sender.ieee in self.devices: @@ -530,12 +563,12 @@ class ZHAGateway: if device.status is DeviceStatus.INITIALIZED: device.update_available(available) - async def async_update_device_storage(self, *_): + async def async_update_device_storage(self, *_: Any) -> None: """Update the devices in the store.""" for device in self.devices.values(): self.zha_storage.async_update_device(device) - async def async_device_initialized(self, device: zha_typing.ZigpyDeviceType): + async def async_device_initialized(self, device: zigpy.device.Device) -> None: """Handle device joined and basic information discovered (async).""" zha_device = self._async_get_or_create_device(device) # This is an active device so set a last seen if it is none @@ -576,7 +609,7 @@ class ZHAGateway: }, ) - async def _async_device_joined(self, zha_device: zha_typing.ZhaDeviceType) -> None: + async def _async_device_joined(self, zha_device: ZHADevice) -> None: zha_device.available = True device_info = zha_device.device_info await zha_device.async_configure() @@ -592,7 +625,7 @@ class ZHAGateway: await zha_device.async_initialize(from_cache=False) async_dispatcher_send(self._hass, SIGNAL_ADD_ENTITIES) - async def _async_device_rejoined(self, zha_device): + async def _async_device_rejoined(self, zha_device: ZHADevice) -> None: _LOGGER.debug( "skipping discovery for previously discovered device - %s:%s", zha_device.nwk, @@ -615,8 +648,11 @@ class ZHAGateway: zha_device.update_available(True) async def async_create_zigpy_group( - self, name: str, members: list[GroupMember], group_id: int = None - ) -> ZhaGroupType: + self, + name: str, + members: list[GroupMember] | None, + group_id: int | None = None, + ) -> ZHAGroup | None: """Create a new Zigpy Zigbee group.""" # we start with two to fill any gaps from a user removing existing groups @@ -659,7 +695,7 @@ class ZHAGateway: await asyncio.gather(*tasks) self.application_controller.groups.pop(group_id) - async def shutdown(self): + async def shutdown(self) -> None: """Stop ZHA Controller Application.""" _LOGGER.debug("Shutting down ZHA ControllerApplication") for unsubscribe in self._unsubs: @@ -668,7 +704,7 @@ class ZHAGateway: def handle_message( self, - sender: zigpy_dev.Device, + sender: zigpy.device.Device, profile: int, cluster: int, src_ep: int, @@ -681,7 +717,7 @@ class ZHAGateway: @callback -def async_capture_log_levels(): +def async_capture_log_levels() -> dict[str, int]: """Capture current logger levels for ZHA.""" return { DEBUG_COMP_BELLOWS: logging.getLogger(DEBUG_COMP_BELLOWS).getEffectiveLevel(), @@ -703,7 +739,7 @@ def async_capture_log_levels(): @callback -def async_set_logger_levels(levels): +def async_set_logger_levels(levels: dict[str, int]) -> None: """Set logger levels for ZHA.""" logging.getLogger(DEBUG_COMP_BELLOWS).setLevel(levels[DEBUG_COMP_BELLOWS]) logging.getLogger(DEBUG_COMP_ZHA).setLevel(levels[DEBUG_COMP_ZHA]) @@ -717,13 +753,13 @@ def async_set_logger_levels(levels): class LogRelayHandler(logging.Handler): """Log handler for error messages.""" - def __init__(self, hass, gateway): + def __init__(self, hass: HomeAssistant, gateway: ZHAGateway) -> None: """Initialize a new LogErrorHandler.""" super().__init__() self.hass = hass self.gateway = gateway - def emit(self, record): + def emit(self, record: LogRecord) -> None: """Relay log message via dispatcher.""" stack = [] if record.levelno >= logging.WARN and not record.exc_info: diff --git a/homeassistant/components/zha/core/group.py b/homeassistant/components/zha/core/group.py index c8970b2d393..af17f28e622 100644 --- a/homeassistant/components/zha/core/group.py +++ b/homeassistant/components/zha/core/group.py @@ -4,25 +4,32 @@ from __future__ import annotations import asyncio import collections import logging -from typing import Any +from typing import TYPE_CHECKING, Any, NamedTuple +import zigpy.endpoint import zigpy.exceptions +import zigpy.group +from zigpy.types.named import EUI64 from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_registry import async_entries_for_device from .helpers import LogMixin -from .typing import ( - ZhaDeviceType, - ZhaGatewayType, - ZhaGroupType, - ZigpyEndpointType, - ZigpyGroupType, -) + +if TYPE_CHECKING: + from .device import ZHADevice + from .gateway import ZHAGateway _LOGGER = logging.getLogger(__name__) -GroupMember = collections.namedtuple("GroupMember", "ieee endpoint_id") + +class GroupMember(NamedTuple): + """Describes a group member.""" + + ieee: EUI64 + endpoint_id: int + + GroupEntityReference = collections.namedtuple( "GroupEntityReference", "name original_name entity_id" ) @@ -32,15 +39,15 @@ class ZHAGroupMember(LogMixin): """Composite object that represents a device endpoint in a Zigbee group.""" def __init__( - self, zha_group: ZhaGroupType, zha_device: ZhaDeviceType, endpoint_id: int + self, zha_group: ZHAGroup, zha_device: ZHADevice, endpoint_id: int ) -> None: """Initialize the group member.""" - self._zha_group: ZhaGroupType = zha_group - self._zha_device: ZhaDeviceType = zha_device - self._endpoint_id: int = endpoint_id + self._zha_group = zha_group + self._zha_device = zha_device + self._endpoint_id = endpoint_id @property - def group(self) -> ZhaGroupType: + def group(self) -> ZHAGroup: """Return the group this member belongs to.""" return self._zha_group @@ -50,12 +57,12 @@ class ZHAGroupMember(LogMixin): return self._endpoint_id @property - def endpoint(self) -> ZigpyEndpointType: + def endpoint(self) -> zigpy.endpoint.Endpoint: """Return the endpoint for this group member.""" return self._zha_device.device.endpoints.get(self.endpoint_id) @property - def device(self) -> ZhaDeviceType: + def device(self) -> ZHADevice: """Return the zha device for this group member.""" return self._zha_device @@ -101,11 +108,11 @@ class ZHAGroupMember(LogMixin): str(ex), ) - def log(self, level: int, msg: str, *args) -> None: + def log(self, level: int, msg: str, *args: Any, **kwargs) -> None: """Log a message.""" msg = f"[%s](%s): {msg}" args = (f"0x{self._zha_group.group_id:04x}", self.endpoint_id) + args - _LOGGER.log(level, msg, *args) + _LOGGER.log(level, msg, *args, **kwargs) class ZHAGroup(LogMixin): @@ -114,13 +121,13 @@ class ZHAGroup(LogMixin): def __init__( self, hass: HomeAssistant, - zha_gateway: ZhaGatewayType, - zigpy_group: ZigpyGroupType, + zha_gateway: ZHAGateway, + zigpy_group: zigpy.group.Group, ) -> None: """Initialize the group.""" - self.hass: HomeAssistant = hass - self._zigpy_group: ZigpyGroupType = zigpy_group - self._zha_gateway: ZhaGatewayType = zha_gateway + self.hass = hass + self._zha_gateway = zha_gateway + self._zigpy_group = zigpy_group @property def name(self) -> str: @@ -133,7 +140,7 @@ class ZHAGroup(LogMixin): return self._zigpy_group.group_id @property - def endpoint(self) -> ZigpyEndpointType: + def endpoint(self) -> zigpy.endpoint.Endpoint: """Return the endpoint for this group.""" return self._zigpy_group.endpoint @@ -192,7 +199,7 @@ class ZHAGroup(LogMixin): all_entity_ids.append(entity_reference["entity_id"]) return all_entity_ids - def get_domain_entity_ids(self, domain) -> list[str]: + def get_domain_entity_ids(self, domain: str) -> list[str]: """Return entity ids from the entity domain for this group.""" domain_entity_ids: list[str] = [] for member in self.members: @@ -217,8 +224,8 @@ class ZHAGroup(LogMixin): group_info["members"] = [member.member_info for member in self.members] return group_info - def log(self, level: int, msg: str, *args): + def log(self, level: int, msg: str, *args: Any, **kwargs) -> None: """Log a message.""" msg = f"[%s](%s): {msg}" args = (self.name, self.group_id) + args - _LOGGER.log(level, msg, *args) + _LOGGER.log(level, msg, *args, **kwargs) diff --git a/homeassistant/components/zha/core/helpers.py b/homeassistant/components/zha/core/helpers.py index d150ab9df45..fcd29c1619f 100644 --- a/homeassistant/components/zha/core/helpers.py +++ b/homeassistant/components/zha/core/helpers.py @@ -15,7 +15,7 @@ import itertools import logging from random import uniform import re -from typing import Any +from typing import Any, TypeVar import voluptuous as vol import zigpy.exceptions @@ -23,6 +23,7 @@ import zigpy.types import zigpy.util import zigpy.zdo.types as zdo_types +from homeassistant.config_entries import ConfigEntry from homeassistant.core import State, callback from .const import ( @@ -35,6 +36,8 @@ from .const import ( from .registries import BINDABLE_CLUSTERS from .typing import ZhaDeviceType, ZigpyClusterType +_T = TypeVar("_T") + @dataclass class BindingPair: @@ -130,7 +133,9 @@ def async_is_bindable_target(source_zha_device, target_zha_device): @callback -def async_get_zha_config_value(config_entry, section, config_key, default): +def async_get_zha_config_value( + config_entry: ConfigEntry, section: str, config_key: str, default: _T +) -> _T: """Get the value for the specified configuration from the zha config entry.""" return ( config_entry.options.get(CUSTOM_CONFIGURATION, {}) @@ -205,23 +210,23 @@ def reduce_attribute( class LogMixin: """Log helper.""" - def log(self, level, msg, *args): + def log(self, level, msg, *args, **kwargs): """Log with level.""" raise NotImplementedError - def debug(self, msg, *args): + def debug(self, msg, *args, **kwargs): """Debug level log.""" return self.log(logging.DEBUG, msg, *args) - def info(self, msg, *args): + def info(self, msg, *args, **kwargs): """Info level log.""" return self.log(logging.INFO, msg, *args) - def warning(self, msg, *args): + def warning(self, msg, *args, **kwargs): """Warning method log.""" return self.log(logging.WARNING, msg, *args) - def error(self, msg, *args): + def error(self, msg, *args, **kwargs): """Error level log.""" return self.log(logging.ERROR, msg, *args) diff --git a/homeassistant/components/zha/core/registries.py b/homeassistant/components/zha/core/registries.py index 1480469ce2c..1d3482cd8f4 100644 --- a/homeassistant/components/zha/core/registries.py +++ b/homeassistant/components/zha/core/registries.py @@ -4,6 +4,7 @@ from __future__ import annotations import collections from collections.abc import Callable import dataclasses +from typing import TYPE_CHECKING import attr from zigpy import zcl @@ -15,8 +16,11 @@ from homeassistant.const import Platform # importing channels updates registries from . import channels as zha_channels # noqa: F401 pylint: disable=unused-import -from .decorators import CALLABLE_T, DictRegistry, SetRegistry -from .typing import ChannelType +from .decorators import DictRegistry, SetRegistry +from .typing import CALLABLE_T, ChannelType + +if TYPE_CHECKING: + from .channels.base import ClientChannel, ZigbeeChannel GROUP_ENTITY_DOMAINS = [Platform.LIGHT, Platform.SWITCH, Platform.FAN] @@ -98,8 +102,8 @@ DEVICE_CLASS = { } DEVICE_CLASS = collections.defaultdict(dict, DEVICE_CLASS) -CLIENT_CHANNELS_REGISTRY = DictRegistry() -ZIGBEE_CHANNEL_REGISTRY = DictRegistry() +CLIENT_CHANNELS_REGISTRY: DictRegistry[type[ClientChannel]] = DictRegistry() +ZIGBEE_CHANNEL_REGISTRY: DictRegistry[type[ZigbeeChannel]] = DictRegistry() def set_or_callable(value): diff --git a/homeassistant/components/zha/cover.py b/homeassistant/components/zha/cover.py index 9f62d4b9c02..0fdb4daeaa5 100644 --- a/homeassistant/components/zha/cover.py +++ b/homeassistant/components/zha/cover.py @@ -133,20 +133,20 @@ class ZhaCover(ZhaEntity, CoverEntity): async def async_open_cover(self, **kwargs): """Open the window cover.""" res = await self._cover_channel.up_open() - if isinstance(res, list) and res[1] is Status.SUCCESS: + if not isinstance(res, Exception) and res[1] is Status.SUCCESS: self.async_update_state(STATE_OPENING) async def async_close_cover(self, **kwargs): """Close the window cover.""" res = await self._cover_channel.down_close() - if isinstance(res, list) and res[1] is Status.SUCCESS: + if not isinstance(res, Exception) and res[1] is Status.SUCCESS: self.async_update_state(STATE_CLOSING) async def async_set_cover_position(self, **kwargs): """Move the roller shutter to a specific position.""" new_pos = kwargs[ATTR_POSITION] res = await self._cover_channel.go_to_lift_percentage(100 - new_pos) - if isinstance(res, list) and res[1] is Status.SUCCESS: + if not isinstance(res, Exception) and res[1] is Status.SUCCESS: self.async_update_state( STATE_CLOSING if new_pos < self._current_position else STATE_OPENING ) @@ -154,7 +154,7 @@ class ZhaCover(ZhaEntity, CoverEntity): async def async_stop_cover(self, **kwargs): """Stop the window cover.""" res = await self._cover_channel.stop() - if isinstance(res, list) and res[1] is Status.SUCCESS: + if not isinstance(res, Exception) and res[1] is Status.SUCCESS: self._state = STATE_OPEN if self._current_position > 0 else STATE_CLOSED self.async_write_ha_state() @@ -250,7 +250,7 @@ class Shade(ZhaEntity, CoverEntity): async def async_open_cover(self, **kwargs): """Open the window cover.""" res = await self._on_off_channel.on() - if not isinstance(res, list) or res[1] != Status.SUCCESS: + if isinstance(res, Exception) or res[1] != Status.SUCCESS: self.debug("couldn't open cover: %s", res) return @@ -260,7 +260,7 @@ class Shade(ZhaEntity, CoverEntity): async def async_close_cover(self, **kwargs): """Close the window cover.""" res = await self._on_off_channel.off() - if not isinstance(res, list) or res[1] != Status.SUCCESS: + if isinstance(res, Exception) or res[1] != Status.SUCCESS: self.debug("couldn't open cover: %s", res) return @@ -274,7 +274,7 @@ class Shade(ZhaEntity, CoverEntity): new_pos * 255 / 100, 1 ) - if not isinstance(res, list) or res[1] != Status.SUCCESS: + if isinstance(res, Exception) or res[1] != Status.SUCCESS: self.debug("couldn't set cover's position: %s", res) return @@ -284,7 +284,7 @@ class Shade(ZhaEntity, CoverEntity): async def async_stop_cover(self, **kwargs) -> None: """Stop the cover.""" res = await self._level_channel.stop() - if not isinstance(res, list) or res[1] != Status.SUCCESS: + if isinstance(res, Exception) or res[1] != Status.SUCCESS: self.debug("couldn't stop cover: %s", res) return diff --git a/homeassistant/components/zha/entity.py b/homeassistant/components/zha/entity.py index 0b7f95efb64..13e43aa9ff0 100644 --- a/homeassistant/components/zha/entity.py +++ b/homeassistant/components/zha/entity.py @@ -2,10 +2,9 @@ from __future__ import annotations import asyncio -from collections.abc import Awaitable import functools import logging -from typing import Any +from typing import TYPE_CHECKING, Any from homeassistant.const import ATTR_NAME from homeassistant.core import CALLBACK_TYPE, Event, callback @@ -32,6 +31,10 @@ from .core.const import ( from .core.helpers import LogMixin from .core.typing import CALLABLE_T, ChannelType, ZhaDeviceType +if TYPE_CHECKING: + from .core.channels.base import ZigbeeChannel + from .core.device import ZHADevice + _LOGGER = logging.getLogger(__name__) ENTITY_SUFFIX = "entity_suffix" @@ -43,7 +46,7 @@ class BaseZhaEntity(LogMixin, entity.Entity): unique_id_suffix: str | None = None - def __init__(self, unique_id: str, zha_device: ZhaDeviceType, **kwargs) -> None: + def __init__(self, unique_id: str, zha_device: ZHADevice, **kwargs: Any) -> None: """Init ZHA entity.""" self._name: str = "" self._force_update: bool = False @@ -53,9 +56,9 @@ class BaseZhaEntity(LogMixin, entity.Entity): self._unique_id += f"-{self.unique_id_suffix}" self._state: Any = None self._extra_state_attributes: dict[str, Any] = {} - self._zha_device: ZhaDeviceType = zha_device + self._zha_device = zha_device self._unsubs: list[CALLABLE_T] = [] - self.remove_future: Awaitable[None] = None + self.remove_future: asyncio.Future[Any] = asyncio.Future() @property def name(self) -> str: @@ -68,7 +71,7 @@ class BaseZhaEntity(LogMixin, entity.Entity): return self._unique_id @property - def zha_device(self) -> ZhaDeviceType: + def zha_device(self) -> ZHADevice: """Return the zha device this entity is attached to.""" return self._zha_device @@ -136,11 +139,11 @@ class BaseZhaEntity(LogMixin, entity.Entity): ) self._unsubs.append(unsub) - def log(self, level: int, msg: str, *args): + def log(self, level: int, msg: str, *args, **kwargs): """Log a message.""" msg = f"%s: {msg}" args = (self.entity_id,) + args - _LOGGER.log(level, msg, *args) + _LOGGER.log(level, msg, *args, **kwargs) class ZhaEntity(BaseZhaEntity, RestoreEntity): @@ -159,9 +162,9 @@ class ZhaEntity(BaseZhaEntity, RestoreEntity): def __init__( self, unique_id: str, - zha_device: ZhaDeviceType, - channels: list[ChannelType], - **kwargs, + zha_device: ZHADevice, + channels: list[ZigbeeChannel], + **kwargs: Any, ) -> None: """Init ZHA entity.""" super().__init__(unique_id, zha_device, **kwargs) @@ -170,7 +173,7 @@ class ZhaEntity(BaseZhaEntity, RestoreEntity): self._name: str = f"{zha_device.name} {ieeetail} {ch_names}" if self.unique_id_suffix: self._name += f" {self.unique_id_suffix}" - self.cluster_channels: dict[str, ChannelType] = {} + self.cluster_channels: dict[str, ZigbeeChannel] = {} for channel in channels: self.cluster_channels[channel.name] = channel diff --git a/homeassistant/components/zha/fan.py b/homeassistant/components/zha/fan.py index 10b64caf974..e6f28935d10 100644 --- a/homeassistant/components/zha/fan.py +++ b/homeassistant/components/zha/fan.py @@ -91,9 +91,7 @@ class BaseFan(FanEntity): """Return the number of speeds the fan supports.""" return int_states_in_range(SPEED_RANGE) - async def async_turn_on( - self, speed=None, percentage=None, preset_mode=None, **kwargs - ) -> None: + async def async_turn_on(self, percentage=None, preset_mode=None, **kwargs) -> None: """Turn the entity on.""" if percentage is None: percentage = DEFAULT_ON_PERCENTAGE diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index 6855db22572..b6d344a57e7 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -243,7 +243,7 @@ class BaseLight(LogMixin, light.LightEntity): level, duration ) t_log["move_to_level_with_on_off"] = result - if not isinstance(result, list) or result[1] is not Status.SUCCESS: + if isinstance(result, Exception) or result[1] is not Status.SUCCESS: self.debug("turned on: %s", t_log) return self._state = bool(level) @@ -255,7 +255,7 @@ class BaseLight(LogMixin, light.LightEntity): # we should call the on command on the on_off cluster if brightness is not 0. result = await self._on_off_channel.on() t_log["on_off"] = result - if not isinstance(result, list) or result[1] is not Status.SUCCESS: + if isinstance(result, Exception) or result[1] is not Status.SUCCESS: self.debug("turned on: %s", t_log) return self._state = True @@ -266,7 +266,7 @@ class BaseLight(LogMixin, light.LightEntity): temperature = kwargs[light.ATTR_COLOR_TEMP] result = await self._color_channel.move_to_color_temp(temperature, duration) t_log["move_to_color_temp"] = result - if not isinstance(result, list) or result[1] is not Status.SUCCESS: + if isinstance(result, Exception) or result[1] is not Status.SUCCESS: self.debug("turned on: %s", t_log) return self._color_temp = temperature @@ -282,7 +282,7 @@ class BaseLight(LogMixin, light.LightEntity): int(xy_color[0] * 65535), int(xy_color[1] * 65535), duration ) t_log["move_to_color"] = result - if not isinstance(result, list) or result[1] is not Status.SUCCESS: + if isinstance(result, Exception) or result[1] is not Status.SUCCESS: self.debug("turned on: %s", t_log) return self._hs_color = hs_color @@ -340,7 +340,7 @@ class BaseLight(LogMixin, light.LightEntity): else: result = await self._on_off_channel.off() self.debug("turned off: %s", result) - if not isinstance(result, list) or result[1] is not Status.SUCCESS: + if isinstance(result, Exception) or result[1] is not Status.SUCCESS: return self._state = False diff --git a/homeassistant/components/zha/lock.py b/homeassistant/components/zha/lock.py index 341cfcebf68..1ebb10cacb6 100644 --- a/homeassistant/components/zha/lock.py +++ b/homeassistant/components/zha/lock.py @@ -122,7 +122,7 @@ class ZhaDoorLock(ZhaEntity, LockEntity): async def async_lock(self, **kwargs): """Lock the lock.""" result = await self._doorlock_channel.lock_door() - if not isinstance(result, list) or result[0] is not Status.SUCCESS: + if isinstance(result, Exception) or result[0] is not Status.SUCCESS: self.error("Error with lock_door: %s", result) return self.async_write_ha_state() @@ -130,7 +130,7 @@ class ZhaDoorLock(ZhaEntity, LockEntity): async def async_unlock(self, **kwargs): """Unlock the lock.""" result = await self._doorlock_channel.unlock_door() - if not isinstance(result, list) or result[0] is not Status.SUCCESS: + if isinstance(result, Exception) or result[0] is not Status.SUCCESS: self.error("Error with unlock_door: %s", result) return self.async_write_ha_state() diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index e542c77516e..fcf6a126963 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -7,9 +7,9 @@ "bellows==0.29.0", "pyserial==3.5", "pyserial-asyncio==0.6", - "zha-quirks==0.0.67", - "zigpy-deconz==0.14.0", - "zigpy==0.43.0", + "zha-quirks==0.0.69", + "zigpy-deconz==0.15.0", + "zigpy==0.44.1", "zigpy-xbee==0.14.0", "zigpy-zigate==0.8.0", "zigpy-znp==0.7.0" diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index 1e7d4f28a38..302bbc2a054 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -231,7 +231,7 @@ class Battery(Sensor): return cls(unique_id, zha_device, channels, **kwargs) @staticmethod - def formatter(value: int) -> int: + def formatter(value: int) -> int: # pylint: disable=arguments-differ """Return the state of the entity.""" # per zcl specs battery percent is reported at 200% ¯\_(ツ)_/¯ if not isinstance(value, numbers.Number) or value == -1: @@ -391,8 +391,7 @@ class Illuminance(Sensor): _attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT _unit = LIGHT_LUX - @staticmethod - def formatter(value: int) -> float: + def formatter(self, value: int) -> float: """Convert illumination data.""" return round(pow(10, ((value - 1) / 10000)), 1) diff --git a/homeassistant/components/zha/switch.py b/homeassistant/components/zha/switch.py index 29fb08b9bc0..87d2407c2dc 100644 --- a/homeassistant/components/zha/switch.py +++ b/homeassistant/components/zha/switch.py @@ -65,7 +65,7 @@ class BaseSwitch(SwitchEntity): async def async_turn_on(self, **kwargs) -> None: """Turn the entity on.""" result = await self._on_off_channel.on() - if not isinstance(result, list) or result[1] is not Status.SUCCESS: + if isinstance(result, Exception) or result[1] is not Status.SUCCESS: return self._state = True self.async_write_ha_state() @@ -73,7 +73,7 @@ class BaseSwitch(SwitchEntity): async def async_turn_off(self, **kwargs) -> None: """Turn the entity off.""" result = await self._on_off_channel.off() - if not isinstance(result, list) or result[1] is not Status.SUCCESS: + if isinstance(result, Exception) or result[1] is not Status.SUCCESS: return self._state = False self.async_write_ha_state() diff --git a/homeassistant/components/zha/translations/fr.json b/homeassistant/components/zha/translations/fr.json index fdb3a8476fe..1044b726b27 100644 --- a/homeassistant/components/zha/translations/fr.json +++ b/homeassistant/components/zha/translations/fr.json @@ -3,7 +3,7 @@ "abort": { "not_zha_device": "Cet appareil n'est pas un appareil zha", "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible.", - "usb_probe_failed": "\u00c9chec de la v\u00e9rification du p\u00e9riph\u00e9rique USB" + "usb_probe_failed": "\u00c9chec de l'analyse du p\u00e9riph\u00e9rique USB" }, "error": { "cannot_connect": "\u00c9chec de connexion" @@ -48,7 +48,7 @@ "zha_options": { "consider_unavailable_battery": "Consid\u00e9rer les appareils aliment\u00e9s par batterie indisponibles apr\u00e8s (secondes)", "consider_unavailable_mains": "Consid\u00e9rer les appareils aliment\u00e9s par le secteur indisponibles apr\u00e8s (secondes)", - "default_light_transition": "Temps de transition de la lumi\u00e8re par d\u00e9faut (en secondes)", + "default_light_transition": "Dur\u00e9e par d\u00e9faut de transition de la lumi\u00e8re (en secondes)", "enable_identify_on_join": "Activer l'effet d'identification quand les appareils rejoignent le r\u00e9seau", "title": "Options g\u00e9n\u00e9rales" } @@ -56,7 +56,7 @@ "device_automation": { "action_type": { "squawk": "Hurlement", - "warn": "Pr\u00e9venir" + "warn": "Avertissement" }, "trigger_subtype": { "both_buttons": "Les deux boutons", diff --git a/homeassistant/components/zha/translations/pt-BR.json b/homeassistant/components/zha/translations/pt-BR.json index 2e5aec0d1cc..8c43bf7f3e6 100644 --- a/homeassistant/components/zha/translations/pt-BR.json +++ b/homeassistant/components/zha/translations/pt-BR.json @@ -15,10 +15,10 @@ }, "pick_radio": { "data": { - "radio_type": "Tipo de r\u00e1dio" + "radio_type": "Tipo de hub zigbee" }, - "description": "Escolha o tipo de seu r\u00e1dio Zigbee", - "title": "Tipo de r\u00e1dio" + "description": "Escolha o tipo de seu hub Zigbee", + "title": "Tipo de hub zigbee" }, "port_config": { "data": { @@ -33,7 +33,7 @@ "data": { "path": "Caminho do dispositivo serial" }, - "description": "Selecione a porta serial para o r\u00e1dio Zigbee", + "description": "Selecione a porta serial para o hub Zigbee", "title": "ZHA" } } diff --git a/homeassistant/components/zha/translations/zh-Hant.json b/homeassistant/components/zha/translations/zh-Hant.json index e0904cf0683..9bf7d3c9208 100644 --- a/homeassistant/components/zha/translations/zh-Hant.json +++ b/homeassistant/components/zha/translations/zh-Hant.json @@ -15,10 +15,10 @@ }, "pick_radio": { "data": { - "radio_type": "\u7121\u7dda\u96fb\u985e\u578b" + "radio_type": "\u7121\u7dda\u96fb\u985e\u5225" }, - "description": "\u9078\u64c7 Zigbee \u7121\u7dda\u96fb\u985e\u578b", - "title": "\u7121\u7dda\u96fb\u985e\u578b" + "description": "\u9078\u64c7 Zigbee \u7121\u7dda\u96fb\u985e\u5225", + "title": "\u7121\u7dda\u96fb\u985e\u5225" }, "port_config": { "data": { diff --git a/homeassistant/components/zodiac/strings.sensor.json b/homeassistant/components/zodiac/strings.sensor.json index e33465967e3..fec38fe2147 100644 --- a/homeassistant/components/zodiac/strings.sensor.json +++ b/homeassistant/components/zodiac/strings.sensor.json @@ -1,18 +1,18 @@ { - "state": { - "zodiac__sign": { - "aries": "Aries", - "taurus": "Taurus", - "gemini": "Gemini", - "cancer": "Cancer", - "leo": "Leo", - "virgo": "Virgo", - "libra": "Libra", - "scorpio": "Scorpio", - "sagittarius": "Sagittarius", - "capricorn": "Capricorn", - "aquarius": "Aquarius", - "pisces": "Pisces" - } + "state": { + "zodiac__sign": { + "aries": "Aries", + "taurus": "Taurus", + "gemini": "Gemini", + "cancer": "Cancer", + "leo": "Leo", + "virgo": "Virgo", + "libra": "Libra", + "scorpio": "Scorpio", + "sagittarius": "Sagittarius", + "capricorn": "Capricorn", + "aquarius": "Aquarius", + "pisces": "Pisces" } -} \ No newline at end of file + } +} diff --git a/homeassistant/components/zone/__init__.py b/homeassistant/components/zone/__init__.py index ef2d21281d1..ebde3328c02 100644 --- a/homeassistant/components/zone/__init__.py +++ b/homeassistant/components/zone/__init__.py @@ -1,6 +1,7 @@ """Support for the definition of zones.""" from __future__ import annotations +from collections.abc import Callable import logging from typing import Any, cast @@ -9,6 +10,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import ( ATTR_EDITABLE, + ATTR_ENTITY_ID, ATTR_LATITUDE, ATTR_LONGITUDE, CONF_ICON, @@ -19,7 +21,10 @@ from homeassistant.const import ( CONF_RADIUS, EVENT_CORE_CONFIG_UPDATE, SERVICE_RELOAD, + STATE_HOME, + STATE_NOT_HOME, STATE_UNAVAILABLE, + STATE_UNKNOWN, ) from homeassistant.core import Event, HomeAssistant, ServiceCall, State, callback from homeassistant.helpers import ( @@ -27,6 +32,7 @@ from homeassistant.helpers import ( config_validation as cv, entity, entity_component, + event, service, storage, ) @@ -284,7 +290,10 @@ class Zone(entity.Entity): """Initialize the zone.""" self._config = config self.editable = True + self._attrs: dict | None = None + self._remove_listener: Callable[[], None] | None = None self._generate_attrs() + self._persons_in_zone: set[str] = set() @classmethod def from_yaml(cls, config: dict) -> Zone: @@ -295,9 +304,9 @@ class Zone(entity.Entity): return zone @property - def state(self) -> str: + def state(self) -> int: """Return the state property really does nothing for a zone.""" - return "zoning" + return len(self._persons_in_zone) @property def name(self) -> str: @@ -327,6 +336,35 @@ class Zone(entity.Entity): self._generate_attrs() self.async_write_ha_state() + @callback + def _person_state_change_listener(self, evt: Event) -> None: + person_entity_id = evt.data[ATTR_ENTITY_ID] + cur_count = len(self._persons_in_zone) + if self._state_is_in_zone(evt.data.get("new_state")): + self._persons_in_zone.add(person_entity_id) + elif person_entity_id in self._persons_in_zone: + self._persons_in_zone.remove(person_entity_id) + + if len(self._persons_in_zone) != cur_count: + self.async_write_ha_state() + + async def async_added_to_hass(self) -> None: + """Run when entity about to be added to hass.""" + await super().async_added_to_hass() + person_domain = "person" # avoid circular import + persons = self.hass.states.async_entity_ids(person_domain) + for person in persons: + if self._state_is_in_zone(self.hass.states.get(person)): + self._persons_in_zone.add(person) + + self.async_on_remove( + event.async_track_state_change_filtered( + self.hass, + event.TrackStates(False, set(), {person_domain}), + self._person_state_change_listener, + ).async_remove + ) + @callback def _generate_attrs(self) -> None: """Generate new attrs based on config.""" @@ -337,3 +375,20 @@ class Zone(entity.Entity): ATTR_PASSIVE: self._config[CONF_PASSIVE], ATTR_EDITABLE: self.editable, } + + @callback + def _state_is_in_zone(self, state: State | None) -> bool: + """Return if given state is in zone.""" + return ( + state is not None + and state.state + not in ( + STATE_NOT_HOME, + STATE_UNKNOWN, + STATE_UNAVAILABLE, + ) + and ( + state.state.casefold() == self.name.casefold() + or (state.state == STATE_HOME and self.entity_id == ENTITY_ID_HOME) + ) + ) diff --git a/homeassistant/components/zone/trigger.py b/homeassistant/components/zone/trigger.py index 5a11bf2068d..8c5af3a0ac2 100644 --- a/homeassistant/components/zone/trigger.py +++ b/homeassistant/components/zone/trigger.py @@ -49,7 +49,7 @@ async def async_validate_trigger_config( """Validate trigger config.""" config = _TRIGGER_SCHEMA(config) registry = er.async_get(hass) - config[CONF_ENTITY_ID] = er.async_resolve_entity_ids( + config[CONF_ENTITY_ID] = er.async_validate_entity_ids( registry, config[CONF_ENTITY_ID] ) return config diff --git a/homeassistant/components/zoneminder/translations/el.json b/homeassistant/components/zoneminder/translations/el.json index c370d342e1f..c5614ab9ec8 100644 --- a/homeassistant/components/zoneminder/translations/el.json +++ b/homeassistant/components/zoneminder/translations/el.json @@ -23,7 +23,7 @@ "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", "path": "\u0394\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae ZMS", "path_zms": "\u0394\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae ZMS", - "ssl": "\u03a7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 SSL \u03b3\u03b9\u03b1 \u03c3\u03c5\u03bd\u03b4\u03ad\u03c3\u03b5\u03b9\u03c2 \u03c3\u03c4\u03bf ZoneMinder", + "ssl": "\u03a7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af \u03ad\u03bd\u03b1 \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03b7\u03c4\u03b9\u03ba\u03cc SSL", "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7", "verify_ssl": "\u0395\u03c0\u03b1\u03bb\u03ae\u03b8\u03b5\u03c5\u03c3\u03b7 \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03b7\u03c4\u03b9\u03ba\u03bf\u03cd SSL" }, diff --git a/homeassistant/components/zoneminder/translations/fr.json b/homeassistant/components/zoneminder/translations/fr.json index 7c730786384..79205bef2d6 100644 --- a/homeassistant/components/zoneminder/translations/fr.json +++ b/homeassistant/components/zoneminder/translations/fr.json @@ -4,7 +4,7 @@ "auth_fail": "L'identifiant ou le mot de passe est incorrect.", "cannot_connect": "\u00c9chec de connexion", "connection_error": "\u00c9chec de la connexion \u00e0 un serveur ZoneMinder.", - "invalid_auth": "Authentification invalide" + "invalid_auth": "Authentification non valide" }, "create_entry": { "default": "Serveur Zoneminder ajout\u00e9." @@ -13,7 +13,7 @@ "auth_fail": "L'identifiant ou le mot de passe est incorrect.", "cannot_connect": "\u00c9chec de connexion", "connection_error": "\u00c9chec de la connexion \u00e0 un serveur ZoneMinder.", - "invalid_auth": "Authentification invalide" + "invalid_auth": "Authentification non valide" }, "flow_title": "ZoneMinder", "step": { diff --git a/homeassistant/components/zwave/__init__.py b/homeassistant/components/zwave/__init__.py deleted file mode 100644 index 3424aa11a87..00000000000 --- a/homeassistant/components/zwave/__init__.py +++ /dev/null @@ -1,1355 +0,0 @@ -"""Support for Z-Wave.""" -# pylint: disable=import-error -# pylint: disable=import-outside-toplevel -from __future__ import annotations - -import asyncio -import copy -from importlib import import_module -import logging -from pprint import pprint - -import voluptuous as vol - -from homeassistant import config_entries -from homeassistant.const import ( - ATTR_ENTITY_ID, - ATTR_NAME, - ATTR_VIA_DEVICE, - EVENT_HOMEASSISTANT_START, - EVENT_HOMEASSISTANT_STOP, - Platform, -) -from homeassistant.core import CoreState, Event, HomeAssistant, ServiceCall, callback -from homeassistant.helpers import discovery -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.device_registry import ( - async_get_registry as async_get_device_registry, -) -from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, - async_dispatcher_send, -) -from homeassistant.helpers.entity import DeviceInfo, generate_entity_id -from homeassistant.helpers.entity_component import DEFAULT_SCAN_INTERVAL -from homeassistant.helpers.entity_platform import EntityPlatform -from homeassistant.helpers.entity_registry import ( - async_get_registry as async_get_entity_registry, -) -from homeassistant.helpers.entity_values import EntityValues -from homeassistant.helpers.event import async_track_time_change -from homeassistant.helpers.typing import ConfigType -from homeassistant.util import convert -import homeassistant.util.dt as dt_util - -from . import const, websocket_api as wsapi, workaround -from .const import ( - CONF_AUTOHEAL, - CONF_CONFIG_PATH, - CONF_DEBUG, - CONF_NETWORK_KEY, - CONF_POLLING_INTERVAL, - CONF_USB_STICK_PATH, - DATA_DEVICES, - DATA_ENTITY_VALUES, - DATA_NETWORK, - DATA_ZWAVE_CONFIG, - DEFAULT_CONF_AUTOHEAL, - DEFAULT_CONF_USB_STICK_PATH, - DEFAULT_DEBUG, - DEFAULT_POLLING_INTERVAL, - DOMAIN, -) -from .discovery_schemas import DISCOVERY_SCHEMAS -from .migration import ( # noqa: F401 - async_add_migration_entity_value, - async_get_migration_data, - async_is_ozw_migrated, - async_is_zwave_js_migrated, -) -from .node_entity import ZWaveBaseEntity, ZWaveNodeEntity -from .util import ( - check_has_unique_id, - check_node_schema, - check_value_schema, - compute_value_unique_id, - is_node_parsed, - node_device_id_and_name, - node_name, -) - -_LOGGER = logging.getLogger(__name__) - -CLASS_ID = "class_id" - -ATTR_POWER = "power_consumption" - -CONF_POLLING_INTENSITY = "polling_intensity" -CONF_IGNORED = "ignored" -CONF_INVERT_OPENCLOSE_BUTTONS = "invert_openclose_buttons" -CONF_INVERT_PERCENT = "invert_percent" -CONF_REFRESH_VALUE = "refresh_value" -CONF_REFRESH_DELAY = "delay" -CONF_DEVICE_CONFIG = "device_config" -CONF_DEVICE_CONFIG_GLOB = "device_config_glob" -CONF_DEVICE_CONFIG_DOMAIN = "device_config_domain" - -DATA_ZWAVE_CONFIG_YAML_PRESENT = "zwave_config_yaml_present" - -DEFAULT_CONF_IGNORED = False -DEFAULT_CONF_INVERT_OPENCLOSE_BUTTONS = False -DEFAULT_CONF_INVERT_PERCENT = False -DEFAULT_CONF_REFRESH_VALUE = False -DEFAULT_CONF_REFRESH_DELAY = 5 - -PLATFORMS = [ - Platform.BINARY_SENSOR, - Platform.CLIMATE, - Platform.COVER, - Platform.FAN, - Platform.LIGHT, - Platform.LOCK, - Platform.SENSOR, - Platform.SWITCH, -] - -RENAME_NODE_SCHEMA = vol.Schema( - { - vol.Required(const.ATTR_NODE_ID): vol.Coerce(int), - vol.Required(ATTR_NAME): cv.string, - vol.Optional(const.ATTR_UPDATE_IDS, default=False): cv.boolean, - } -) - -RENAME_VALUE_SCHEMA = vol.Schema( - { - vol.Required(const.ATTR_NODE_ID): vol.Coerce(int), - vol.Required(const.ATTR_VALUE_ID): vol.Coerce(int), - vol.Required(ATTR_NAME): cv.string, - vol.Optional(const.ATTR_UPDATE_IDS, default=False): cv.boolean, - } -) - -SET_CONFIG_PARAMETER_SCHEMA = vol.Schema( - { - vol.Required(const.ATTR_NODE_ID): vol.Coerce(int), - vol.Required(const.ATTR_CONFIG_PARAMETER): vol.Coerce(int), - vol.Required(const.ATTR_CONFIG_VALUE): vol.Any(vol.Coerce(int), cv.string), - vol.Optional(const.ATTR_CONFIG_SIZE, default=2): vol.Coerce(int), - } -) - -SET_NODE_VALUE_SCHEMA = vol.Schema( - { - vol.Required(const.ATTR_NODE_ID): vol.Coerce(int), - vol.Required(const.ATTR_VALUE_ID): vol.Any(vol.Coerce(int), cv.string), - vol.Required(const.ATTR_CONFIG_VALUE): vol.Any(vol.Coerce(int), cv.string), - } -) - -REFRESH_NODE_VALUE_SCHEMA = vol.Schema( - { - vol.Required(const.ATTR_NODE_ID): vol.Coerce(int), - vol.Required(const.ATTR_VALUE_ID): vol.Coerce(int), - } -) - -SET_POLL_INTENSITY_SCHEMA = vol.Schema( - { - vol.Required(const.ATTR_NODE_ID): vol.Coerce(int), - vol.Required(const.ATTR_VALUE_ID): vol.Coerce(int), - vol.Required(const.ATTR_POLL_INTENSITY): vol.Coerce(int), - } -) - -PRINT_CONFIG_PARAMETER_SCHEMA = vol.Schema( - { - vol.Required(const.ATTR_NODE_ID): vol.Coerce(int), - vol.Required(const.ATTR_CONFIG_PARAMETER): vol.Coerce(int), - } -) - -NODE_SERVICE_SCHEMA = vol.Schema({vol.Required(const.ATTR_NODE_ID): vol.Coerce(int)}) - -REFRESH_ENTITY_SCHEMA = vol.Schema({vol.Required(ATTR_ENTITY_ID): cv.entity_id}) - -RESET_NODE_METERS_SCHEMA = vol.Schema( - { - vol.Required(const.ATTR_NODE_ID): vol.Coerce(int), - vol.Optional(const.ATTR_INSTANCE, default=1): vol.Coerce(int), - } -) - -CHANGE_ASSOCIATION_SCHEMA = vol.Schema( - { - vol.Required(const.ATTR_ASSOCIATION): cv.string, - vol.Required(const.ATTR_NODE_ID): vol.Coerce(int), - vol.Required(const.ATTR_TARGET_NODE_ID): vol.Coerce(int), - vol.Required(const.ATTR_GROUP): vol.Coerce(int), - vol.Optional(const.ATTR_INSTANCE, default=0x00): vol.Coerce(int), - } -) - -SET_WAKEUP_SCHEMA = vol.Schema( - { - vol.Required(const.ATTR_NODE_ID): vol.Coerce(int), - vol.Required(const.ATTR_CONFIG_VALUE): vol.All( - vol.Coerce(int), cv.positive_int - ), - } -) - -HEAL_NODE_SCHEMA = vol.Schema( - { - vol.Required(const.ATTR_NODE_ID): vol.Coerce(int), - vol.Optional(const.ATTR_RETURN_ROUTES, default=False): cv.boolean, - } -) - -TEST_NODE_SCHEMA = vol.Schema( - { - vol.Required(const.ATTR_NODE_ID): vol.Coerce(int), - vol.Optional(const.ATTR_MESSAGES, default=1): cv.positive_int, - } -) - - -DEVICE_CONFIG_SCHEMA_ENTRY = vol.Schema( - { - vol.Optional(CONF_POLLING_INTENSITY): cv.positive_int, - vol.Optional(CONF_IGNORED, default=DEFAULT_CONF_IGNORED): cv.boolean, - vol.Optional( - CONF_INVERT_OPENCLOSE_BUTTONS, default=DEFAULT_CONF_INVERT_OPENCLOSE_BUTTONS - ): cv.boolean, - vol.Optional( - CONF_INVERT_PERCENT, default=DEFAULT_CONF_INVERT_PERCENT - ): cv.boolean, - vol.Optional( - CONF_REFRESH_VALUE, default=DEFAULT_CONF_REFRESH_VALUE - ): cv.boolean, - vol.Optional( - CONF_REFRESH_DELAY, default=DEFAULT_CONF_REFRESH_DELAY - ): cv.positive_int, - } -) - -SIGNAL_REFRESH_ENTITY_FORMAT = "zwave_refresh_entity_{}" - -CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Optional(CONF_AUTOHEAL, default=DEFAULT_CONF_AUTOHEAL): cv.boolean, - vol.Optional(CONF_CONFIG_PATH): cv.string, - vol.Optional(CONF_NETWORK_KEY): vol.All( - cv.string, vol.Match(r"(0x\w\w,\s?){15}0x\w\w") - ), - vol.Optional(CONF_DEVICE_CONFIG, default={}): vol.Schema( - {cv.entity_id: DEVICE_CONFIG_SCHEMA_ENTRY} - ), - vol.Optional(CONF_DEVICE_CONFIG_GLOB, default={}): vol.Schema( - {cv.string: DEVICE_CONFIG_SCHEMA_ENTRY} - ), - vol.Optional(CONF_DEVICE_CONFIG_DOMAIN, default={}): vol.Schema( - {cv.string: DEVICE_CONFIG_SCHEMA_ENTRY} - ), - vol.Optional(CONF_DEBUG, default=DEFAULT_DEBUG): cv.boolean, - vol.Optional( - CONF_POLLING_INTERVAL, default=DEFAULT_POLLING_INTERVAL - ): cv.positive_int, - vol.Optional(CONF_USB_STICK_PATH): cv.string, - } - ) - }, - extra=vol.ALLOW_EXTRA, -) - - -def _obj_to_dict(obj): - """Convert an object into a hash for debug.""" - return { - key: getattr(obj, key) - for key in dir(obj) - if key[0] != "_" and not callable(getattr(obj, key)) - } - - -def _value_name(value): - """Return the name of the value.""" - return f"{node_name(value.node)} {value.label}".strip() - - -def nice_print_node(node): - """Print a nice formatted node to the output (debug method).""" - node_dict = _obj_to_dict(node) - node_dict["values"] = { - value_id: _obj_to_dict(value) for value_id, value in node.values.items() - } - - _LOGGER.info("FOUND NODE %s \n%s", node.product_name, node_dict) - - -def get_config_value(node, value_index, tries=5): - """Return the current configuration value for a specific index.""" - try: - for value in node.values.values(): - if ( - value.command_class == const.COMMAND_CLASS_CONFIGURATION - and value.index == value_index - ): - return value.data - except RuntimeError: - # If we get a runtime error the dict has changed while - # we was looking for a value, just do it again - return ( - None if tries <= 0 else get_config_value(node, value_index, tries=tries - 1) - ) - return None - - -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the Z-Wave platform (generic part).""" - if discovery_info is None or DATA_NETWORK not in hass.data: - return False - - device = hass.data[DATA_DEVICES].get(discovery_info[const.DISCOVERY_DEVICE]) - if device is None: - return False - - async_add_entities([device]) - return True - - -async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up Z-Wave components.""" - if DOMAIN not in config: - return True - - conf = config[DOMAIN] - hass.data[DATA_ZWAVE_CONFIG] = conf - hass.data[DATA_ZWAVE_CONFIG_YAML_PRESENT] = 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={ - CONF_USB_STICK_PATH: conf.get( - CONF_USB_STICK_PATH, DEFAULT_CONF_USB_STICK_PATH - ), - CONF_NETWORK_KEY: conf.get(CONF_NETWORK_KEY), - }, - ) - ) - - return True - - -async def async_setup_entry( # noqa: C901 - hass: HomeAssistant, config_entry: config_entries.ConfigEntry -) -> bool: - """Set up Z-Wave from a config entry. - - Will automatically load components to support devices found on the network. - """ - from openzwave.group import ZWaveGroup - from openzwave.network import ZWaveNetwork - from openzwave.option import ZWaveOption - from pydispatch import dispatcher - - if async_is_ozw_migrated(hass) or async_is_zwave_js_migrated(hass): - - if hass.data.get(DATA_ZWAVE_CONFIG_YAML_PRESENT): - config_yaml_message = ( - ", and remove %s from configuration.yaml " - "to avoid setting up this integration on restart ", - DOMAIN, - ) - else: - config_yaml_message = "" - - _LOGGER.error( - "Migration away from legacy Z-Wave has been done. " - "Please remove the %s integration%s", - DOMAIN, - config_yaml_message, - ) - return False - - # Merge config entry and yaml config - config = config_entry.data - if DATA_ZWAVE_CONFIG in hass.data: - config = {**config, **hass.data[DATA_ZWAVE_CONFIG]} - - # Update hass.data with merged config so we can access it elsewhere - hass.data[DATA_ZWAVE_CONFIG] = config - - # Load configuration - use_debug = config.get(CONF_DEBUG, DEFAULT_DEBUG) - autoheal = config.get(CONF_AUTOHEAL, DEFAULT_CONF_AUTOHEAL) - device_config = EntityValues( - config.get(CONF_DEVICE_CONFIG), - config.get(CONF_DEVICE_CONFIG_DOMAIN), - config.get(CONF_DEVICE_CONFIG_GLOB), - ) - - usb_path = config[CONF_USB_STICK_PATH] - - _LOGGER.info("Z-Wave USB path is %s", usb_path) - - # Setup options - options = ZWaveOption( - usb_path, - user_path=hass.config.config_dir, - config_path=config.get(CONF_CONFIG_PATH), - ) - - options.set_console_output(use_debug) - - if config.get(CONF_NETWORK_KEY): - options.addOption("NetworkKey", config[CONF_NETWORK_KEY]) - - await hass.async_add_executor_job(options.lock) - network = hass.data[DATA_NETWORK] = ZWaveNetwork(options, autostart=False) - hass.data[DATA_DEVICES] = {} - hass.data[DATA_ENTITY_VALUES] = [] - - registry = await async_get_entity_registry(hass) - - wsapi.async_load_websocket_api(hass) - - if use_debug: # pragma: no cover - - def log_all(signal, value=None): - """Log all the signals.""" - print("") - print("SIGNAL *****", signal) - if value and signal in ( - ZWaveNetwork.SIGNAL_VALUE_CHANGED, - ZWaveNetwork.SIGNAL_VALUE_ADDED, - ZWaveNetwork.SIGNAL_SCENE_EVENT, - ZWaveNetwork.SIGNAL_NODE_EVENT, - ZWaveNetwork.SIGNAL_AWAKE_NODES_QUERIED, - ZWaveNetwork.SIGNAL_ALL_NODES_QUERIED, - ZWaveNetwork.SIGNAL_ALL_NODES_QUERIED_SOME_DEAD, - ): - pprint(_obj_to_dict(value)) - - print("") - - dispatcher.connect(log_all, weak=False) - - def value_added(node, value): - """Handle new added value to a node on the network.""" - # Check if this value should be tracked by an existing entity - for values in hass.data[DATA_ENTITY_VALUES]: - values.check_value(value) - - for schema in DISCOVERY_SCHEMAS: - if not check_node_schema(node, schema): - continue - if not check_value_schema( - value, schema[const.DISC_VALUES][const.DISC_PRIMARY] - ): - continue - - values = ZWaveDeviceEntityValues( - hass, schema, value, config, device_config, registry - ) - - # We create a new list and update the reference here so that - # the list can be safely iterated over in the main thread - new_values = hass.data[DATA_ENTITY_VALUES] + [values] - hass.data[DATA_ENTITY_VALUES] = new_values - - platform = EntityPlatform( - hass=hass, - logger=_LOGGER, - domain=DOMAIN, - platform_name=DOMAIN, - platform=None, - scan_interval=DEFAULT_SCAN_INTERVAL, - entity_namespace=None, - ) - platform.config_entry = config_entry - - def node_added(node): - """Handle a new node on the network.""" - entity = ZWaveNodeEntity(node, network) - - async def _add_node_to_component(): - if hass.data[DATA_DEVICES].get(entity.unique_id): - return - - name = node_name(node) - generated_id = generate_entity_id(DOMAIN + ".{}", name, []) - node_config = device_config.get(generated_id) - if node_config.get(CONF_IGNORED): - _LOGGER.info( - "Ignoring node entity %s due to device settings", generated_id - ) - return - - hass.data[DATA_DEVICES][entity.unique_id] = entity - await platform.async_add_entities([entity]) - - if entity.unique_id: - hass.create_task(_add_node_to_component()) - return - - @callback - def _on_ready(sec): - _LOGGER.info("Z-Wave node %d ready after %d seconds", entity.node_id, sec) - hass.async_add_job(_add_node_to_component) - - @callback - def _on_timeout(sec): - _LOGGER.warning( - "Z-Wave node %d not ready after %d seconds, continuing anyway", - entity.node_id, - sec, - ) - hass.async_add_job(_add_node_to_component) - - hass.add_job(check_has_unique_id, entity, _on_ready, _on_timeout) - - def node_removed(node): - node_id = node.node_id - node_key = f"node-{node_id}" - for key in list(hass.data[DATA_DEVICES]): - if key is None: - continue - if not key.startswith(f"{node_id}-"): - continue - - entity = hass.data[DATA_DEVICES][key] - _LOGGER.debug( - "Removing Entity - value: %s - entity_id: %s", key, entity.entity_id - ) - hass.add_job(entity.node_removed()) - del hass.data[DATA_DEVICES][key] - - entity = hass.data[DATA_DEVICES][node_key] - hass.add_job(entity.node_removed()) - del hass.data[DATA_DEVICES][node_key] - - hass.add_job(_remove_device(node)) - - async def _remove_device(node): - dev_reg = await async_get_device_registry(hass) - identifier, name = node_device_id_and_name(node) - device = dev_reg.async_get_device(identifiers={identifier}) - if device is not None: - _LOGGER.debug("Removing Device - %s - %s", device.id, name) - dev_reg.async_remove_device(device.id) - - def network_ready(): - """Handle the query of all awake nodes.""" - _LOGGER.info( - "Z-Wave network is ready for use. All awake nodes " - "have been queried. Sleeping nodes will be " - "queried when they awake" - ) - hass.bus.fire(const.EVENT_NETWORK_READY) - - def network_complete(): - """Handle the querying of all nodes on network.""" - _LOGGER.info( - "Z-Wave network is complete. All nodes on the network have been queried" - ) - hass.bus.fire(const.EVENT_NETWORK_COMPLETE) - - def network_complete_some_dead(): - """Handle the querying of all nodes on network.""" - _LOGGER.info( - "Z-Wave network is complete. All nodes on the network " - "have been queried, but some nodes are marked dead" - ) - hass.bus.fire(const.EVENT_NETWORK_COMPLETE_SOME_DEAD) - - dispatcher.connect(value_added, ZWaveNetwork.SIGNAL_VALUE_ADDED, weak=False) - dispatcher.connect(node_added, ZWaveNetwork.SIGNAL_NODE_ADDED, weak=False) - dispatcher.connect(node_removed, ZWaveNetwork.SIGNAL_NODE_REMOVED, weak=False) - dispatcher.connect( - network_ready, ZWaveNetwork.SIGNAL_AWAKE_NODES_QUERIED, weak=False - ) - dispatcher.connect( - network_complete, ZWaveNetwork.SIGNAL_ALL_NODES_QUERIED, weak=False - ) - dispatcher.connect( - network_complete_some_dead, - ZWaveNetwork.SIGNAL_ALL_NODES_QUERIED_SOME_DEAD, - weak=False, - ) - - def add_node(service: ServiceCall) -> None: - """Switch into inclusion mode.""" - _LOGGER.info("Z-Wave add_node have been initialized") - network.controller.add_node() - - def add_node_secure(service: ServiceCall) -> None: - """Switch into secure inclusion mode.""" - _LOGGER.info("Z-Wave add_node_secure have been initialized") - network.controller.add_node(True) - - def remove_node(service: ServiceCall) -> None: - """Switch into exclusion mode.""" - _LOGGER.info("Z-Wave remove_node have been initialized") - network.controller.remove_node() - - def cancel_command(service: ServiceCall) -> None: - """Cancel a running controller command.""" - _LOGGER.info("Cancel running Z-Wave command") - network.controller.cancel_command() - - def heal_network(service: ServiceCall) -> None: - """Heal the network.""" - _LOGGER.info("Z-Wave heal running") - network.heal() - - def soft_reset(service: ServiceCall) -> None: - """Soft reset the controller.""" - _LOGGER.info("Z-Wave soft_reset have been initialized") - network.controller.soft_reset() - - def test_network(service: ServiceCall) -> None: - """Test the network by sending commands to all the nodes.""" - _LOGGER.info("Z-Wave test_network have been initialized") - network.test() - - def stop_network(_service_or_event: Event | ServiceCall) -> None: - """Stop Z-Wave network.""" - _LOGGER.info("Stopping Z-Wave network") - network.stop() - if hass.state == CoreState.running: - hass.bus.fire(const.EVENT_NETWORK_STOP) - - async def rename_node(service: ServiceCall) -> None: - """Rename a node.""" - node_id = service.data.get(const.ATTR_NODE_ID) - node = network.nodes[node_id] # pylint: disable=unsubscriptable-object - name = service.data.get(ATTR_NAME) - node.name = name - _LOGGER.info("Renamed Z-Wave node %d to %s", node_id, name) - update_ids = service.data.get(const.ATTR_UPDATE_IDS) - # We want to rename the device, the node entity, - # and all the contained entities - node_key = f"node-{node_id}" - entity = hass.data[DATA_DEVICES][node_key] - await entity.node_renamed(update_ids) - for key in list(hass.data[DATA_DEVICES]): - if not key.startswith(f"{node_id}-"): - continue - entity = hass.data[DATA_DEVICES][key] - await entity.value_renamed(update_ids) - - async def rename_value(service: ServiceCall) -> None: - """Rename a node value.""" - node_id = service.data.get(const.ATTR_NODE_ID) - value_id = service.data.get(const.ATTR_VALUE_ID) - node = network.nodes[node_id] # pylint: disable=unsubscriptable-object - value = node.values[value_id] - name = service.data.get(ATTR_NAME) - value.label = name - _LOGGER.info( - "Renamed Z-Wave value (Node %d Value %d) to %s", node_id, value_id, name - ) - update_ids = service.data.get(const.ATTR_UPDATE_IDS) - value_key = f"{node_id}-{value_id}" - entity = hass.data[DATA_DEVICES][value_key] - await entity.value_renamed(update_ids) - - def set_poll_intensity(service: ServiceCall) -> None: - """Set the polling intensity of a node value.""" - node_id = service.data.get(const.ATTR_NODE_ID) - value_id = service.data.get(const.ATTR_VALUE_ID) - node = network.nodes[node_id] # pylint: disable=unsubscriptable-object - value = node.values[value_id] - intensity = service.data.get(const.ATTR_POLL_INTENSITY) - if intensity == 0: - if value.disable_poll(): - _LOGGER.info("Polling disabled (Node %d Value %d)", node_id, value_id) - return - _LOGGER.info( - "Polling disabled failed (Node %d Value %d)", node_id, value_id - ) - else: - if value.enable_poll(intensity): - _LOGGER.info( - "Set polling intensity (Node %d Value %d) to %s", - node_id, - value_id, - intensity, - ) - return - _LOGGER.info( - "Set polling intensity failed (Node %d Value %d)", node_id, value_id - ) - - def remove_failed_node(service: ServiceCall) -> None: - """Remove failed node.""" - node_id = service.data.get(const.ATTR_NODE_ID) - _LOGGER.info("Trying to remove zwave node %d", node_id) - network.controller.remove_failed_node(node_id) - - def replace_failed_node(service: ServiceCall) -> None: - """Replace failed node.""" - node_id = service.data.get(const.ATTR_NODE_ID) - _LOGGER.info("Trying to replace zwave node %d", node_id) - network.controller.replace_failed_node(node_id) - - def set_config_parameter(service: ServiceCall) -> None: - """Set a config parameter to a node.""" - node_id = service.data.get(const.ATTR_NODE_ID) - node = network.nodes[node_id] # pylint: disable=unsubscriptable-object - param = service.data.get(const.ATTR_CONFIG_PARAMETER) - selection = service.data.get(const.ATTR_CONFIG_VALUE) - size = service.data.get(const.ATTR_CONFIG_SIZE) - for value in node.get_values( - class_id=const.COMMAND_CLASS_CONFIGURATION - ).values(): - if value.index != param: - continue - if value.type == const.TYPE_BOOL: - value.data = int(selection == "True") - _LOGGER.info( - "Setting configuration parameter %s on Node %s with bool selection %s", - param, - node_id, - str(selection), - ) - return - if value.type == const.TYPE_LIST: - value.data = str(selection) - _LOGGER.info( - "Setting configuration parameter %s on Node %s with list selection %s", - param, - node_id, - str(selection), - ) - return - if value.type == const.TYPE_BUTTON: - network.manager.pressButton(value.value_id) - network.manager.releaseButton(value.value_id) - _LOGGER.info( - "Setting configuration parameter %s on Node %s " - "with button selection %s", - param, - node_id, - selection, - ) - return - value.data = int(selection) - _LOGGER.info( - "Setting configuration parameter %s on Node %s with selection %s", - param, - node_id, - selection, - ) - return - node.set_config_param(param, selection, size) - _LOGGER.info( - "Setting unknown configuration parameter %s on Node %s with selection %s", - param, - node_id, - selection, - ) - - def refresh_node_value(service: ServiceCall) -> None: - """Refresh the specified value from a node.""" - node_id = service.data.get(const.ATTR_NODE_ID) - value_id = service.data.get(const.ATTR_VALUE_ID) - node = network.nodes[node_id] # pylint: disable=unsubscriptable-object - node.values[value_id].refresh() - _LOGGER.info("Node %s value %s refreshed", node_id, value_id) - - def set_node_value(service: ServiceCall) -> None: - """Set the specified value on a node.""" - node_id = service.data.get(const.ATTR_NODE_ID) - value_id = service.data.get(const.ATTR_VALUE_ID) - value = service.data.get(const.ATTR_CONFIG_VALUE) - node = network.nodes[node_id] # pylint: disable=unsubscriptable-object - node.values[value_id].data = value - _LOGGER.info("Node %s value %s set to %s", node_id, value_id, value) - - def print_config_parameter(service: ServiceCall) -> None: - """Print a config parameter from a node.""" - node_id = service.data.get(const.ATTR_NODE_ID) - node = network.nodes[node_id] # pylint: disable=unsubscriptable-object - param = service.data.get(const.ATTR_CONFIG_PARAMETER) - _LOGGER.info( - "Config parameter %s on Node %s: %s", - param, - node_id, - get_config_value(node, param), - ) - - def print_node(service: ServiceCall) -> None: - """Print all information about z-wave node.""" - node_id = service.data.get(const.ATTR_NODE_ID) - node = network.nodes[node_id] # pylint: disable=unsubscriptable-object - nice_print_node(node) - - def set_wakeup(service: ServiceCall) -> None: - """Set wake-up interval of a node.""" - node_id = service.data.get(const.ATTR_NODE_ID) - node = network.nodes[node_id] # pylint: disable=unsubscriptable-object - value = service.data.get(const.ATTR_CONFIG_VALUE) - if node.can_wake_up(): - for value_id in node.get_values(class_id=const.COMMAND_CLASS_WAKE_UP): - node.values[value_id].data = value - _LOGGER.info("Node %s wake-up set to %d", node_id, value) - else: - _LOGGER.info("Node %s is not wakeable", node_id) - - def change_association(service: ServiceCall) -> None: - """Change an association in the zwave network.""" - association_type = service.data.get(const.ATTR_ASSOCIATION) - node_id = service.data.get(const.ATTR_NODE_ID) - target_node_id = service.data.get(const.ATTR_TARGET_NODE_ID) - group = service.data.get(const.ATTR_GROUP) - instance = service.data.get(const.ATTR_INSTANCE) - - node = ZWaveGroup(group, network, node_id) - if association_type == "add": - node.add_association(target_node_id, instance) - _LOGGER.info( - "Adding association for node:%s in group:%s " - "target node:%s, instance=%s", - node_id, - group, - target_node_id, - instance, - ) - if association_type == "remove": - node.remove_association(target_node_id, instance) - _LOGGER.info( - "Removing association for node:%s in group:%s " - "target node:%s, instance=%s", - node_id, - group, - target_node_id, - instance, - ) - - async def async_refresh_entity(service: ServiceCall) -> None: - """Refresh values that specific entity depends on.""" - entity_id = service.data.get(ATTR_ENTITY_ID) - async_dispatcher_send(hass, SIGNAL_REFRESH_ENTITY_FORMAT.format(entity_id)) - - def refresh_node(service: ServiceCall) -> None: - """Refresh all node info.""" - node_id = service.data.get(const.ATTR_NODE_ID) - node = network.nodes[node_id] # pylint: disable=unsubscriptable-object - node.refresh_info() - - def reset_node_meters(service: ServiceCall) -> None: - """Reset meter counters of a node.""" - node_id = service.data.get(const.ATTR_NODE_ID) - instance = service.data.get(const.ATTR_INSTANCE) - node = network.nodes[node_id] # pylint: disable=unsubscriptable-object - for value in node.get_values(class_id=const.COMMAND_CLASS_METER).values(): - if value.index != const.INDEX_METER_RESET: - continue - if value.instance != instance: - continue - network.manager.pressButton(value.value_id) - network.manager.releaseButton(value.value_id) - _LOGGER.info("Resetting meters on node %s instance %s", node_id, instance) - return - _LOGGER.info( - "Node %s on instance %s does not have resettable meters", node_id, instance - ) - - def heal_node(service: ServiceCall) -> None: - """Heal a node on the network.""" - node_id = service.data.get(const.ATTR_NODE_ID) - update_return_routes = service.data.get(const.ATTR_RETURN_ROUTES) - node = network.nodes[node_id] # pylint: disable=unsubscriptable-object - _LOGGER.info("Z-Wave node heal running for node %s", node_id) - node.heal(update_return_routes) - - def test_node(service: ServiceCall) -> None: - """Send test messages to a node on the network.""" - node_id = service.data.get(const.ATTR_NODE_ID) - messages = service.data.get(const.ATTR_MESSAGES) - node = network.nodes[node_id] # pylint: disable=unsubscriptable-object - _LOGGER.info("Sending %s test-messages to node %s", messages, node_id) - node.test(messages) - - def start_zwave(_service_or_event: ServiceCall | Event) -> None: - """Startup Z-Wave network.""" - _LOGGER.info("Starting Z-Wave network") - network.start() - hass.bus.fire(const.EVENT_NETWORK_START) - - async def _check_awaked(): - """Wait for Z-wave awaked state (or timeout) and finalize start.""" - _LOGGER.debug("network state: %d %s", network.state, network.state_str) - - start_time = dt_util.utcnow() - while True: - waited = int((dt_util.utcnow() - start_time).total_seconds()) - - if network.state >= network.STATE_AWAKED: - # Need to be in STATE_AWAKED before talking to nodes. - _LOGGER.info("Z-Wave ready after %d seconds", waited) - break - - if waited >= const.NETWORK_READY_WAIT_SECS: - # Wait up to NETWORK_READY_WAIT_SECS seconds for the Z-Wave - # network to be ready. - _LOGGER.warning( - "Z-Wave not ready after %d seconds, continuing anyway", waited - ) - _LOGGER.info( - "Final network state: %d %s", network.state, network.state_str - ) - break - - await asyncio.sleep(1) - - hass.async_add_job(_finalize_start) - - hass.add_job(_check_awaked) - - def _finalize_start(): - """Perform final initializations after Z-Wave network is awaked.""" - polling_interval = convert(config.get(CONF_POLLING_INTERVAL), int) - if polling_interval is not None: - network.set_poll_interval(polling_interval, False) - - poll_interval = network.get_poll_interval() - _LOGGER.info("Z-Wave polling interval set to %d ms", poll_interval) - - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_network) - - # Register node services for Z-Wave network - hass.services.register(DOMAIN, const.SERVICE_ADD_NODE, add_node) - hass.services.register(DOMAIN, const.SERVICE_ADD_NODE_SECURE, add_node_secure) - hass.services.register(DOMAIN, const.SERVICE_REMOVE_NODE, remove_node) - hass.services.register(DOMAIN, const.SERVICE_CANCEL_COMMAND, cancel_command) - hass.services.register(DOMAIN, const.SERVICE_HEAL_NETWORK, heal_network) - hass.services.register(DOMAIN, const.SERVICE_SOFT_RESET, soft_reset) - hass.services.register(DOMAIN, const.SERVICE_TEST_NETWORK, test_network) - hass.services.register(DOMAIN, const.SERVICE_STOP_NETWORK, stop_network) - hass.services.register( - DOMAIN, const.SERVICE_RENAME_NODE, rename_node, schema=RENAME_NODE_SCHEMA - ) - hass.services.register( - DOMAIN, const.SERVICE_RENAME_VALUE, rename_value, schema=RENAME_VALUE_SCHEMA - ) - hass.services.register( - DOMAIN, - const.SERVICE_SET_CONFIG_PARAMETER, - set_config_parameter, - schema=SET_CONFIG_PARAMETER_SCHEMA, - ) - hass.services.register( - DOMAIN, - const.SERVICE_SET_NODE_VALUE, - set_node_value, - schema=SET_NODE_VALUE_SCHEMA, - ) - hass.services.register( - DOMAIN, - const.SERVICE_REFRESH_NODE_VALUE, - refresh_node_value, - schema=REFRESH_NODE_VALUE_SCHEMA, - ) - hass.services.register( - DOMAIN, - const.SERVICE_PRINT_CONFIG_PARAMETER, - print_config_parameter, - schema=PRINT_CONFIG_PARAMETER_SCHEMA, - ) - hass.services.register( - DOMAIN, - const.SERVICE_REMOVE_FAILED_NODE, - remove_failed_node, - schema=NODE_SERVICE_SCHEMA, - ) - hass.services.register( - DOMAIN, - const.SERVICE_REPLACE_FAILED_NODE, - replace_failed_node, - schema=NODE_SERVICE_SCHEMA, - ) - - hass.services.register( - DOMAIN, - const.SERVICE_CHANGE_ASSOCIATION, - change_association, - schema=CHANGE_ASSOCIATION_SCHEMA, - ) - hass.services.register( - DOMAIN, const.SERVICE_SET_WAKEUP, set_wakeup, schema=SET_WAKEUP_SCHEMA - ) - hass.services.register( - DOMAIN, const.SERVICE_PRINT_NODE, print_node, schema=NODE_SERVICE_SCHEMA - ) - hass.services.register( - DOMAIN, - const.SERVICE_REFRESH_ENTITY, - async_refresh_entity, - schema=REFRESH_ENTITY_SCHEMA, - ) - hass.services.register( - DOMAIN, const.SERVICE_REFRESH_NODE, refresh_node, schema=NODE_SERVICE_SCHEMA - ) - hass.services.register( - DOMAIN, - const.SERVICE_RESET_NODE_METERS, - reset_node_meters, - schema=RESET_NODE_METERS_SCHEMA, - ) - hass.services.register( - DOMAIN, - const.SERVICE_SET_POLL_INTENSITY, - set_poll_intensity, - schema=SET_POLL_INTENSITY_SCHEMA, - ) - hass.services.register( - DOMAIN, const.SERVICE_HEAL_NODE, heal_node, schema=HEAL_NODE_SCHEMA - ) - hass.services.register( - DOMAIN, const.SERVICE_TEST_NODE, test_node, schema=TEST_NODE_SCHEMA - ) - - # Setup autoheal - if autoheal: - _LOGGER.info("Z-Wave network autoheal is enabled") - async_track_time_change(hass, heal_network, hour=0, minute=0, second=0) - - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, start_zwave) - - hass.services.async_register(DOMAIN, const.SERVICE_START_NETWORK, start_zwave) - - hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) - - return True - - -class ZWaveDeviceEntityValues: - """Manages entity access to the underlying zwave value objects.""" - - def __init__( - self, hass, schema, primary_value, zwave_config, device_config, registry - ): - """Initialize the values object with the passed entity schema.""" - self._hass = hass - self._zwave_config = zwave_config - self._device_config = device_config - self._schema = copy.deepcopy(schema) - self._values = {} - self._entity = None - self._workaround_ignore = False - self._registry = registry - - for name in self._schema[const.DISC_VALUES].keys(): - self._values[name] = None - self._schema[const.DISC_VALUES][name][const.DISC_INSTANCE] = [ - primary_value.instance - ] - - self._values[const.DISC_PRIMARY] = primary_value - self._node = primary_value.node - self._schema[const.DISC_NODE_ID] = [self._node.node_id] - - # Check values that have already been discovered for node - for value in self._node.values.values(): - self.check_value(value) - - self._check_entity_ready() - - def __getattr__(self, name): - """Get the specified value for this entity.""" - return self._values[name] - - def __iter__(self): - """Allow iteration over all values.""" - return iter(self._values.values()) - - def check_value(self, value): - """Check if the new value matches a missing value for this entity. - - If a match is found, it is added to the values mapping. - """ - if not check_node_schema(value.node, self._schema): - return - for name, name_value in self._values.items(): - if name_value is not None: - continue - if not check_value_schema(value, self._schema[const.DISC_VALUES][name]): - continue - self._values[name] = value - if self._entity: - self._entity.value_added() - self._entity.value_changed() - - self._check_entity_ready() - - def _check_entity_ready(self): - """Check if all required values are discovered and create entity.""" - if self._workaround_ignore: - return - if self._entity is not None: - return - - for name in self._schema[const.DISC_VALUES]: - if self._values[name] is None and not self._schema[const.DISC_VALUES][ - name - ].get(const.DISC_OPTIONAL): - return - - component = self._schema[const.DISC_COMPONENT] - - workaround_component = workaround.get_device_component_mapping(self.primary) - if workaround_component and workaround_component != component: - if workaround_component == workaround.WORKAROUND_IGNORE: - _LOGGER.info( - "Ignoring Node %d Value %d due to workaround", - self.primary.node.node_id, - self.primary.value_id, - ) - # No entity will be created for this value - self._workaround_ignore = True - return - _LOGGER.debug("Using %s instead of %s", workaround_component, component) - component = workaround_component - - entity_id = self._registry.async_get_entity_id( - component, DOMAIN, compute_value_unique_id(self._node, self.primary) - ) - if entity_id is None: - value_name = _value_name(self.primary) - entity_id = generate_entity_id(component + ".{}", value_name, []) - node_config = self._device_config.get(entity_id) - - # Configure node - _LOGGER.debug( - "Adding Node_id=%s Generic_command_class=%s, " - "Specific_command_class=%s, " - "Command_class=%s, Value type=%s, " - "Genre=%s as %s", - self._node.node_id, - self._node.generic, - self._node.specific, - self.primary.command_class, - self.primary.type, - self.primary.genre, - component, - ) - - if node_config.get(CONF_IGNORED): - _LOGGER.info("Ignoring entity %s due to device settings", entity_id) - # No entity will be created for this value - self._workaround_ignore = True - return - - polling_intensity = convert(node_config.get(CONF_POLLING_INTENSITY), int) - if polling_intensity: - self.primary.enable_poll(polling_intensity) - - platform = import_module(f".{component}", __name__) - - device = platform.get_device( - node=self._node, values=self, node_config=node_config, hass=self._hass - ) - if device is None: - # No entity will be created for this value - self._workaround_ignore = True - return - - self._entity = device - - @callback - def _on_ready(sec): - _LOGGER.info( - "Z-Wave entity %s (node_id: %d) ready after %d seconds", - device.name, - self._node.node_id, - sec, - ) - self._hass.async_add_job(discover_device, component, device) - - @callback - def _on_timeout(sec): - _LOGGER.warning( - "Z-Wave entity %s (node_id: %d) not ready after %d seconds, " - "continuing anyway", - device.name, - self._node.node_id, - sec, - ) - self._hass.async_add_job(discover_device, component, device) - - async def discover_device(component, device): - """Put device in a dictionary and call discovery on it.""" - if self._hass.data[DATA_DEVICES].get(device.unique_id): - return - - self._hass.data[DATA_DEVICES][device.unique_id] = device - if component in PLATFORMS: - async_dispatcher_send(self._hass, f"zwave_new_{component}", device) - else: - await discovery.async_load_platform( - self._hass, - component, - DOMAIN, - {const.DISCOVERY_DEVICE: device.unique_id}, - self._zwave_config, - ) - - if device.unique_id: - self._hass.add_job(discover_device, component, device) - else: - self._hass.add_job(check_has_unique_id, device, _on_ready, _on_timeout) - - -class ZWaveDeviceEntity(ZWaveBaseEntity): - """Representation of a Z-Wave node entity.""" - - def __init__(self, values, domain): - """Initialize the z-Wave device.""" - super().__init__() - from openzwave.network import ZWaveNetwork - from pydispatch import dispatcher - - self.values = values - self.node = values.primary.node - self.values.primary.set_change_verified(False) - - self._name = _value_name(self.values.primary) - self._unique_id = self._compute_unique_id() - self._update_attributes() - - dispatcher.connect( - self.network_value_changed, ZWaveNetwork.SIGNAL_VALUE_CHANGED - ) - - def network_value_changed(self, value): - """Handle a value change on the network.""" - if value.value_id in [v.value_id for v in self.values if v]: - return self.value_changed() - - def value_added(self): - """Handle a new value of this entity.""" - - def value_changed(self): - """Handle a changed value for this entity's node.""" - self._update_attributes() - self.update_properties() - self.maybe_schedule_update() - - async def value_renamed(self, update_ids=False): - """Rename the node and update any IDs.""" - self._name = _value_name(self.values.primary) - if update_ids: - # Update entity ID. - ent_reg = await async_get_entity_registry(self.hass) - new_entity_id = ent_reg.async_generate_entity_id( - self.platform.domain, - self._name, - self.platform.entities.keys() - {self.entity_id}, - ) - if new_entity_id != self.entity_id: - # Don't change the name attribute, it will be None unless - # customised and if it's been customised, keep the - # customisation. - ent_reg.async_update_entity(self.entity_id, new_entity_id=new_entity_id) - return - # else for the above two ifs, update if not using update_entity - self.async_write_ha_state() - - async def async_added_to_hass(self): - """Add device to dict.""" - async_dispatcher_connect( - self.hass, - SIGNAL_REFRESH_ENTITY_FORMAT.format(self.entity_id), - self.refresh_from_network, - ) - - # Add legacy Z-Wave migration data. - await async_add_migration_entity_value(self.hass, self.entity_id, self.values) - - def _update_attributes(self): - """Update the node attributes. May only be used inside callback.""" - self.node_id = self.node.node_id - self._name = _value_name(self.values.primary) - if not self._unique_id: - self._unique_id = self._compute_unique_id() - if self._unique_id: - self.try_remove_and_add() - - if self.values.power: - self.power_consumption = round( - self.values.power.data, self.values.power.precision - ) - else: - self.power_consumption = None - - def update_properties(self): - """Update on data changes for node values.""" - - @property - def should_poll(self): - """No polling needed.""" - return False - - @property - def unique_id(self): - """Return a unique ID.""" - return self._unique_id - - @property - def device_info(self) -> DeviceInfo: - """Return device information.""" - identifier, name = node_device_id_and_name( - self.node, self.values.primary.instance - ) - info = DeviceInfo( - name=name, - identifiers={identifier}, - manufacturer=self.node.manufacturer_name, - model=self.node.product_name, - ) - if self.values.primary.instance > 1: - info[ATTR_VIA_DEVICE] = (DOMAIN, self.node_id) - elif self.node_id > 1: - info[ATTR_VIA_DEVICE] = (DOMAIN, 1) - return info - - @property - def name(self): - """Return the name of the device.""" - return self._name - - @property - def extra_state_attributes(self): - """Return the device specific state attributes.""" - attrs = { - const.ATTR_NODE_ID: self.node_id, - const.ATTR_VALUE_INDEX: self.values.primary.index, - const.ATTR_VALUE_INSTANCE: self.values.primary.instance, - const.ATTR_VALUE_ID: str(self.values.primary.value_id), - } - - if self.power_consumption is not None: - attrs[ATTR_POWER] = self.power_consumption - - return attrs - - def refresh_from_network(self): - """Refresh all dependent values from zwave network.""" - for value in self.values: - if value is not None: - self.node.refresh_value(value.value_id) - - def _compute_unique_id(self): - if ( - is_node_parsed(self.node) and self.values.primary.label != "Unknown" - ) or self.node.is_ready: - return compute_value_unique_id(self.node, self.values.primary) - return None diff --git a/homeassistant/components/zwave/binary_sensor.py b/homeassistant/components/zwave/binary_sensor.py deleted file mode 100644 index 26944b6661d..00000000000 --- a/homeassistant/components/zwave/binary_sensor.py +++ /dev/null @@ -1,106 +0,0 @@ -"""Support for Z-Wave binary sensors.""" -import datetime -import logging - -from homeassistant.components.binary_sensor import DOMAIN, BinarySensorEntity -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.event import track_point_in_time -import homeassistant.util.dt as dt_util - -from . import ZWaveDeviceEntity, workaround -from .const import COMMAND_CLASS_SENSOR_BINARY - -_LOGGER = logging.getLogger(__name__) - - -async def async_setup_entry( - hass: HomeAssistant, - config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up Z-Wave binary sensors from Config Entry.""" - - @callback - def async_add_binary_sensor(binary_sensor): - """Add Z-Wave binary sensor.""" - async_add_entities([binary_sensor]) - - async_dispatcher_connect(hass, "zwave_new_binary_sensor", async_add_binary_sensor) - - -def get_device(values, **kwargs): - """Create Z-Wave entity device.""" - device_mapping = workaround.get_device_mapping(values.primary) - if device_mapping == workaround.WORKAROUND_NO_OFF_EVENT: - return ZWaveTriggerSensor(values, "motion") - - if workaround.get_device_component_mapping(values.primary) == DOMAIN: - return ZWaveBinarySensor(values, None) - - if values.primary.command_class == COMMAND_CLASS_SENSOR_BINARY: - return ZWaveBinarySensor(values, None) - return None - - -class ZWaveBinarySensor(BinarySensorEntity, ZWaveDeviceEntity): - """Representation of a binary sensor within Z-Wave.""" - - def __init__(self, values, device_class): - """Initialize the sensor.""" - ZWaveDeviceEntity.__init__(self, values, DOMAIN) - self._sensor_type = device_class - self._state = self.values.primary.data - - def update_properties(self): - """Handle data changes for node values.""" - self._state = self.values.primary.data - - @property - def is_on(self): - """Return true if the binary sensor is on.""" - return self._state - - @property - def device_class(self): - """Return the class of this sensor, from BinarySensorDeviceClass.""" - return self._sensor_type - - -class ZWaveTriggerSensor(ZWaveBinarySensor): - """Representation of a stateless sensor within Z-Wave.""" - - def __init__(self, values, device_class): - """Initialize the sensor.""" - super().__init__(values, device_class) - # Set default off delay to 60 sec - self.re_arm_sec = 60 - self.invalidate_after = None - - def update_properties(self): - """Handle value changes for this entity's node.""" - self._state = self.values.primary.data - _LOGGER.debug("off_delay=%s", self.values.off_delay) - # Set re_arm_sec if off_delay is provided from the sensor - if self.values.off_delay: - _LOGGER.debug("off_delay.data=%s", self.values.off_delay.data) - self.re_arm_sec = self.values.off_delay.data * 8 - # only allow this value to be true for re_arm secs - if not self.hass: - return - - self.invalidate_after = dt_util.utcnow() + datetime.timedelta( - seconds=self.re_arm_sec - ) - track_point_in_time( - self.hass, self.async_update_ha_state, self.invalidate_after - ) - - @property - def is_on(self): - """Return true if movement has happened within the rearm time.""" - return self._state and ( - self.invalidate_after is None or self.invalidate_after > dt_util.utcnow() - ) diff --git a/homeassistant/components/zwave/climate.py b/homeassistant/components/zwave/climate.py deleted file mode 100644 index d56910e1b74..00000000000 --- a/homeassistant/components/zwave/climate.py +++ /dev/null @@ -1,619 +0,0 @@ -"""Support for Z-Wave climate devices.""" -# Because we do not compile openzwave on CI -from __future__ import annotations - -import logging - -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ( - ATTR_TARGET_TEMP_HIGH, - ATTR_TARGET_TEMP_LOW, - CURRENT_HVAC_COOL, - CURRENT_HVAC_FAN, - CURRENT_HVAC_HEAT, - CURRENT_HVAC_IDLE, - CURRENT_HVAC_OFF, - DOMAIN, - HVAC_MODE_AUTO, - HVAC_MODE_COOL, - HVAC_MODE_DRY, - HVAC_MODE_FAN_ONLY, - HVAC_MODE_HEAT, - HVAC_MODE_HEAT_COOL, - HVAC_MODE_OFF, - PRESET_AWAY, - PRESET_BOOST, - PRESET_NONE, - SUPPORT_AUX_HEAT, - SUPPORT_FAN_MODE, - SUPPORT_PRESET_MODE, - SUPPORT_SWING_MODE, - SUPPORT_TARGET_TEMPERATURE, - SUPPORT_TARGET_TEMPERATURE_RANGE, -) -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity_platform import AddEntitiesCallback - -from . import ZWaveDeviceEntity, const - -_LOGGER = logging.getLogger(__name__) - -CONF_NAME = "name" -DEFAULT_NAME = "Z-Wave Climate" - -REMOTEC = 0x5254 -REMOTEC_ZXT_120 = 0x8377 -REMOTEC_ZXT_120_THERMOSTAT = (REMOTEC, REMOTEC_ZXT_120) -ATTR_OPERATING_STATE = "operating_state" -ATTR_FAN_STATE = "fan_state" -ATTR_FAN_ACTION = "fan_action" -AUX_HEAT_ZWAVE_MODE = "Aux Heat" - -# Device is in manufacturer specific mode (e.g. setting the valve manually) -PRESET_MANUFACTURER_SPECIFIC = "Manufacturer Specific" - -WORKAROUND_ZXT_120 = "zxt_120" - -DEVICE_MAPPINGS = {REMOTEC_ZXT_120_THERMOSTAT: WORKAROUND_ZXT_120} - -HVAC_STATE_MAPPINGS = { - "off": HVAC_MODE_OFF, - "heat": HVAC_MODE_HEAT, - "heat mode": HVAC_MODE_HEAT, - "heat (default)": HVAC_MODE_HEAT, - "furnace": HVAC_MODE_HEAT, - "fan only": HVAC_MODE_FAN_ONLY, - "dry air": HVAC_MODE_DRY, - "moist air": HVAC_MODE_DRY, - "cool": HVAC_MODE_COOL, - "heat_cool": HVAC_MODE_HEAT_COOL, - "auto": HVAC_MODE_HEAT_COOL, - "auto changeover": HVAC_MODE_HEAT_COOL, -} - -MODE_SETPOINT_MAPPINGS = { - "off": (), - "heat": ("setpoint_heating",), - "cool": ("setpoint_cooling",), - "auto": ("setpoint_heating", "setpoint_cooling"), - "aux heat": ("setpoint_heating",), - "furnace": ("setpoint_furnace",), - "dry air": ("setpoint_dry_air",), - "moist air": ("setpoint_moist_air",), - "auto changeover": ("setpoint_auto_changeover",), - "heat econ": ("setpoint_eco_heating",), - "cool econ": ("setpoint_eco_cooling",), - "away": ("setpoint_away_heating", "setpoint_away_cooling"), - "full power": ("setpoint_full_power",), - # aliases found in xml configs - "comfort": ("setpoint_heating",), - "heat mode": ("setpoint_heating",), - "heat (default)": ("setpoint_heating",), - "dry floor": ("setpoint_dry_air",), - "heat eco": ("setpoint_eco_heating",), - "energy saving": ("setpoint_eco_heating",), - "energy heat": ("setpoint_eco_heating",), - "vacation": ("setpoint_away_heating", "setpoint_away_cooling"), - # for tests - "heat_cool": ("setpoint_heating", "setpoint_cooling"), -} - -HVAC_CURRENT_MAPPINGS = { - "idle": CURRENT_HVAC_IDLE, - "heat": CURRENT_HVAC_HEAT, - "pending heat": CURRENT_HVAC_IDLE, - "heating": CURRENT_HVAC_HEAT, - "cool": CURRENT_HVAC_COOL, - "pending cool": CURRENT_HVAC_IDLE, - "cooling": CURRENT_HVAC_COOL, - "fan only": CURRENT_HVAC_FAN, - "vent / economiser": CURRENT_HVAC_FAN, - "off": CURRENT_HVAC_OFF, -} - -PRESET_MAPPINGS = { - "away": PRESET_AWAY, - "full power": PRESET_BOOST, - "manufacturer specific": PRESET_MANUFACTURER_SPECIFIC, -} - -DEFAULT_HVAC_MODES = [ - HVAC_MODE_HEAT_COOL, - HVAC_MODE_HEAT, - HVAC_MODE_COOL, - HVAC_MODE_FAN_ONLY, - HVAC_MODE_DRY, - HVAC_MODE_OFF, - HVAC_MODE_AUTO, -] - - -async def async_setup_entry( - hass: HomeAssistant, - config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up Z-Wave Climate device from Config Entry.""" - - @callback - def async_add_climate(climate): - """Add Z-Wave Climate Device.""" - async_add_entities([climate]) - - async_dispatcher_connect(hass, "zwave_new_climate", async_add_climate) - - -def get_device(hass, values, **kwargs): - """Create Z-Wave entity device.""" - temp_unit = hass.config.units.temperature_unit - if values.primary.command_class == const.COMMAND_CLASS_THERMOSTAT_SETPOINT: - return ZWaveClimateSingleSetpoint(values, temp_unit) - if values.primary.command_class == const.COMMAND_CLASS_THERMOSTAT_MODE: - return ZWaveClimateMultipleSetpoint(values, temp_unit) - return None - - -class ZWaveClimateBase(ZWaveDeviceEntity, ClimateEntity): - """Representation of a Z-Wave Climate device.""" - - def __init__(self, values, temp_unit): - """Initialize the Z-Wave climate device.""" - ZWaveDeviceEntity.__init__(self, values, DOMAIN) - self._target_temperature = None - self._target_temperature_range = (None, None) - self._current_temperature = None - self._hvac_action = None - self._hvac_list = None # [zwave_mode] - self._hvac_mapping = None # {ha_mode:zwave_mode} - self._hvac_mode = None # ha_mode - self._aux_heat = None - self._default_hvac_mode = None # ha_mode - self._preset_mapping = None # {ha_mode:zwave_mode} - self._preset_list = None # [zwave_mode] - self._preset_mode = None # ha_mode if exists, else zwave_mode - self._current_fan_mode = None - self._fan_modes = None - self._fan_action = None - self._current_swing_mode = None - self._swing_modes = None - self._unit = temp_unit - _LOGGER.debug("temp_unit is %s", self._unit) - self._zxt_120 = None - # Make sure that we have values for the key before converting to int - if self.node.manufacturer_id.strip() and self.node.product_id.strip(): - specific_sensor_key = ( - int(self.node.manufacturer_id, 16), - int(self.node.product_id, 16), - ) - if ( - specific_sensor_key in DEVICE_MAPPINGS - and DEVICE_MAPPINGS[specific_sensor_key] == WORKAROUND_ZXT_120 - ): - _LOGGER.debug("Remotec ZXT-120 Zwave Thermostat workaround") - self._zxt_120 = 1 - self.update_properties() - - def _mode(self) -> None: - """Return thermostat mode Z-Wave value.""" - raise NotImplementedError() - - def _current_mode_setpoints(self) -> tuple: - """Return a tuple of current setpoint Z-Wave value(s).""" - raise NotImplementedError() - - @property - def supported_features(self): - """Return the list of supported features.""" - support = SUPPORT_TARGET_TEMPERATURE - if self._hvac_list and HVAC_MODE_HEAT_COOL in self._hvac_list: - support |= SUPPORT_TARGET_TEMPERATURE_RANGE - if self._preset_list and PRESET_AWAY in self._preset_list: - support |= SUPPORT_TARGET_TEMPERATURE_RANGE - - if self.values.fan_mode: - support |= SUPPORT_FAN_MODE - if self._zxt_120 == 1 and self.values.zxt_120_swing_mode: - support |= SUPPORT_SWING_MODE - if self._aux_heat: - support |= SUPPORT_AUX_HEAT - if self._preset_list: - support |= SUPPORT_PRESET_MODE - return support - - def update_properties(self): - """Handle the data changes for node values.""" - # Operation Mode - self._update_operation_mode() - - # Current Temp - self._update_current_temp() - - # Fan Mode - self._update_fan_mode() - - # Swing mode - self._update_swing_mode() - - # Set point - self._update_target_temp() - - # Operating state - self._update_operating_state() - - # Fan operating state - self._update_fan_state() - - def _update_operation_mode(self): - """Update hvac and preset modes.""" - if self._mode(): - self._hvac_list = [] - self._hvac_mapping = {} - self._preset_list = [] - self._preset_mapping = {} - - if mode_list := self._mode().data_items: - for mode in mode_list: - ha_mode = HVAC_STATE_MAPPINGS.get(str(mode).lower()) - ha_preset = PRESET_MAPPINGS.get(str(mode).lower()) - if mode == AUX_HEAT_ZWAVE_MODE: - # Aux Heat should not be included in any mapping - self._aux_heat = True - elif ha_mode and ha_mode not in self._hvac_mapping: - self._hvac_mapping[ha_mode] = mode - self._hvac_list.append(ha_mode) - elif ha_preset and ha_preset not in self._preset_mapping: - self._preset_mapping[ha_preset] = mode - self._preset_list.append(ha_preset) - else: - # If nothing matches - self._preset_list.append(mode) - - # Default operation mode - for mode in DEFAULT_HVAC_MODES: - if mode in self._hvac_mapping: - self._default_hvac_mode = mode - break - - if self._preset_list: - # Presets are supported - self._preset_list.append(PRESET_NONE) - - current_mode = self._mode().data - _LOGGER.debug("current_mode=%s", current_mode) - _hvac_temp = next( - ( - key - for key, value in self._hvac_mapping.items() - if value == current_mode - ), - None, - ) - - if _hvac_temp is None: - # The current mode is not a hvac mode - if ( - "heat" in current_mode.lower() - and HVAC_MODE_HEAT in self._hvac_mapping - ): - # The current preset modes maps to HVAC_MODE_HEAT - _LOGGER.debug("Mapped to HEAT") - self._hvac_mode = HVAC_MODE_HEAT - elif ( - "cool" in current_mode.lower() - and HVAC_MODE_COOL in self._hvac_mapping - ): - # The current preset modes maps to HVAC_MODE_COOL - _LOGGER.debug("Mapped to COOL") - self._hvac_mode = HVAC_MODE_COOL - else: - # The current preset modes maps to self._default_hvac_mode - _LOGGER.debug("Mapped to DEFAULT") - self._hvac_mode = self._default_hvac_mode - self._preset_mode = next( - ( - key - for key, value in self._preset_mapping.items() - if value == current_mode - ), - current_mode, - ) - else: - # The current mode is a hvac mode - self._hvac_mode = _hvac_temp - self._preset_mode = PRESET_NONE - - _LOGGER.debug("self._hvac_mapping=%s", self._hvac_mapping) - _LOGGER.debug("self._hvac_list=%s", self._hvac_list) - _LOGGER.debug("self._hvac_mode=%s", self._hvac_mode) - _LOGGER.debug("self._default_hvac_mode=%s", self._default_hvac_mode) - _LOGGER.debug("self._hvac_action=%s", self._hvac_action) - _LOGGER.debug("self._aux_heat=%s", self._aux_heat) - _LOGGER.debug("self._preset_mapping=%s", self._preset_mapping) - _LOGGER.debug("self._preset_list=%s", self._preset_list) - _LOGGER.debug("self._preset_mode=%s", self._preset_mode) - - def _update_current_temp(self): - """Update current temperature.""" - if self.values.temperature: - self._current_temperature = self.values.temperature.data - device_unit = self.values.temperature.units - if device_unit is not None: - self._unit = device_unit - - def _update_fan_mode(self): - """Update fan mode.""" - if self.values.fan_mode: - self._current_fan_mode = self.values.fan_mode.data - if fan_modes := self.values.fan_mode.data_items: - self._fan_modes = list(fan_modes) - - _LOGGER.debug("self._fan_modes=%s", self._fan_modes) - _LOGGER.debug("self._current_fan_mode=%s", self._current_fan_mode) - - def _update_swing_mode(self): - """Update swing mode.""" - if self._zxt_120 == 1: - if self.values.zxt_120_swing_mode: - self._current_swing_mode = self.values.zxt_120_swing_mode.data - swing_modes = self.values.zxt_120_swing_mode.data_items - if swing_modes: - self._swing_modes = list(swing_modes) - _LOGGER.debug("self._swing_modes=%s", self._swing_modes) - _LOGGER.debug("self._current_swing_mode=%s", self._current_swing_mode) - - def _update_target_temp(self): - """Update target temperature.""" - current_setpoints = self._current_mode_setpoints() - self._target_temperature = None - self._target_temperature_range = (None, None) - if len(current_setpoints) == 1: - (setpoint,) = current_setpoints - if setpoint is not None: - self._target_temperature = round((float(setpoint.data)), 1) - elif len(current_setpoints) == 2: - (setpoint_low, setpoint_high) = current_setpoints - target_low, target_high = None, None - if setpoint_low is not None: - target_low = round((float(setpoint_low.data)), 1) - if setpoint_high is not None: - target_high = round((float(setpoint_high.data)), 1) - self._target_temperature_range = (target_low, target_high) - - def _update_operating_state(self): - """Update operating state.""" - if self.values.operating_state: - mode = self.values.operating_state.data - self._hvac_action = HVAC_CURRENT_MAPPINGS.get(str(mode).lower(), mode) - - def _update_fan_state(self): - """Update fan state.""" - if self.values.fan_action: - self._fan_action = self.values.fan_action.data - - @property - def fan_mode(self): - """Return the fan speed set.""" - return self._current_fan_mode - - @property - def fan_modes(self): - """Return a list of available fan modes.""" - return self._fan_modes - - @property - def swing_mode(self): - """Return the swing mode set.""" - return self._current_swing_mode - - @property - def swing_modes(self): - """Return a list of available swing modes.""" - return self._swing_modes - - @property - def temperature_unit(self): - """Return the unit of measurement.""" - if self._unit == "C": - return TEMP_CELSIUS - if self._unit == "F": - return TEMP_FAHRENHEIT - return self._unit - - @property - def current_temperature(self): - """Return the current temperature.""" - return self._current_temperature - - @property - def hvac_mode(self): - """Return hvac operation ie. heat, cool mode. - - Need to be one of HVAC_MODE_*. - """ - if self._mode(): - return self._hvac_mode - return self._default_hvac_mode - - @property - def hvac_modes(self): - """Return the list of available hvac operation modes. - - Need to be a subset of HVAC_MODES. - """ - if self._mode(): - return self._hvac_list - return [] - - @property - def hvac_action(self): - """Return the current running hvac operation if supported. - - Need to be one of CURRENT_HVAC_*. - """ - return self._hvac_action - - @property - def is_aux_heat(self): - """Return true if aux heater.""" - if not self._aux_heat: - return None - if self._mode().data == AUX_HEAT_ZWAVE_MODE: - return True - return False - - @property - def preset_mode(self): - """Return preset operation ie. eco, away. - - Need to be one of PRESET_*. - """ - if self._mode(): - return self._preset_mode - return PRESET_NONE - - @property - def preset_modes(self): - """Return the list of available preset operation modes. - - Need to be a subset of PRESET_MODES. - """ - if self._mode(): - return self._preset_list - return [] - - @property - def target_temperature(self): - """Return the temperature we try to reach.""" - return self._target_temperature - - @property - def target_temperature_low(self) -> float | None: - """Return the lowbound target temperature we try to reach.""" - return self._target_temperature_range[0] - - @property - def target_temperature_high(self) -> float | None: - """Return the highbound target temperature we try to reach.""" - return self._target_temperature_range[1] - - def set_temperature(self, **kwargs): - """Set new target temperature.""" - current_setpoints = self._current_mode_setpoints() - if len(current_setpoints) == 1: - (setpoint,) = current_setpoints - target_temp = kwargs.get(ATTR_TEMPERATURE) - if setpoint is not None and target_temp is not None: - _LOGGER.debug("Set temperature to %s", target_temp) - setpoint.data = target_temp - elif len(current_setpoints) == 2: - (setpoint_low, setpoint_high) = current_setpoints - target_temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW) - target_temp_high = kwargs.get(ATTR_TARGET_TEMP_HIGH) - if setpoint_low is not None and target_temp_low is not None: - _LOGGER.debug("Set low temperature to %s", target_temp_low) - setpoint_low.data = target_temp_low - if setpoint_high is not None and target_temp_high is not None: - _LOGGER.debug("Set high temperature to %s", target_temp_high) - setpoint_high.data = target_temp_high - - def set_fan_mode(self, fan_mode): - """Set new target fan mode.""" - _LOGGER.debug("Set fan mode to %s", fan_mode) - if not self.values.fan_mode: - return - self.values.fan_mode.data = fan_mode - - def set_hvac_mode(self, hvac_mode): - """Set new target hvac mode.""" - _LOGGER.debug("Set hvac_mode to %s", hvac_mode) - if not self._mode(): - return - operation_mode = self._hvac_mapping.get(hvac_mode) - _LOGGER.debug("Set operation_mode to %s", operation_mode) - self._mode().data = operation_mode - - def turn_aux_heat_on(self): - """Turn auxiliary heater on.""" - if not self._aux_heat: - return - operation_mode = AUX_HEAT_ZWAVE_MODE - _LOGGER.debug("Aux heat on. Set operation mode to %s", operation_mode) - self._mode().data = operation_mode - - def turn_aux_heat_off(self): - """Turn auxiliary heater off.""" - if not self._aux_heat: - return - if HVAC_MODE_HEAT in self._hvac_mapping: - operation_mode = self._hvac_mapping.get(HVAC_MODE_HEAT) - else: - operation_mode = self._hvac_mapping.get(HVAC_MODE_OFF) - _LOGGER.debug("Aux heat off. Set operation mode to %s", operation_mode) - self._mode().data = operation_mode - - def set_preset_mode(self, preset_mode): - """Set new target preset mode.""" - _LOGGER.debug("Set preset_mode to %s", preset_mode) - if not self._mode(): - return - if preset_mode == PRESET_NONE: - # Activate the current hvac mode - self._update_operation_mode() - operation_mode = self._hvac_mapping.get(self.hvac_mode) - _LOGGER.debug("Set operation_mode to %s", operation_mode) - self._mode().data = operation_mode - else: - operation_mode = self._preset_mapping.get(preset_mode, preset_mode) - _LOGGER.debug("Set operation_mode to %s", operation_mode) - self._mode().data = operation_mode - - def set_swing_mode(self, swing_mode): - """Set new target swing mode.""" - _LOGGER.debug("Set swing_mode to %s", swing_mode) - if self._zxt_120 == 1 and self.values.zxt_120_swing_mode: - self.values.zxt_120_swing_mode.data = swing_mode - - @property - def extra_state_attributes(self): - """Return the optional state attributes.""" - data = super().extra_state_attributes - if self._fan_action: - data[ATTR_FAN_ACTION] = self._fan_action - return data - - -class ZWaveClimateSingleSetpoint(ZWaveClimateBase): - """Representation of a single setpoint Z-Wave thermostat device.""" - - def __init__(self, values, temp_unit): - """Initialize the Z-Wave climate device.""" - ZWaveClimateBase.__init__(self, values, temp_unit) - - def _mode(self) -> None: - """Return thermostat mode Z-Wave value.""" - return self.values.mode - - def _current_mode_setpoints(self) -> tuple: - """Return a tuple of current setpoint Z-Wave value(s).""" - return (self.values.primary,) - - -class ZWaveClimateMultipleSetpoint(ZWaveClimateBase): - """Representation of a multiple setpoint Z-Wave thermostat device.""" - - def __init__(self, values, temp_unit): - """Initialize the Z-Wave climate device.""" - ZWaveClimateBase.__init__(self, values, temp_unit) - - def _mode(self) -> None: - """Return thermostat mode Z-Wave value.""" - return self.values.primary - - def _current_mode_setpoints(self) -> tuple: - """Return a tuple of current setpoint Z-Wave value(s).""" - current_mode = str(self.values.primary.data).lower() - setpoints_names = MODE_SETPOINT_MAPPINGS.get(current_mode, ()) - return tuple(getattr(self.values, name, None) for name in setpoints_names) diff --git a/homeassistant/components/zwave/config_flow.py b/homeassistant/components/zwave/config_flow.py deleted file mode 100644 index f29f2e6f6d0..00000000000 --- a/homeassistant/components/zwave/config_flow.py +++ /dev/null @@ -1,95 +0,0 @@ -"""Config flow to configure Z-Wave.""" -# pylint: disable=import-error -# pylint: disable=import-outside-toplevel -from collections import OrderedDict - -import voluptuous as vol - -from homeassistant import config_entries - -from .const import ( - CONF_NETWORK_KEY, - CONF_USB_STICK_PATH, - DEFAULT_CONF_USB_STICK_PATH, - DOMAIN, -) - - -class ZwaveFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): - """Handle a Z-Wave config flow.""" - - VERSION = 1 - - def __init__(self): - """Initialize the Z-Wave config flow.""" - self.usb_path = CONF_USB_STICK_PATH - - async def async_step_user(self, user_input=None): - """Handle a flow start.""" - if self._async_current_entries(): - return self.async_abort(reason="single_instance_allowed") - - errors = {} - - fields = OrderedDict() - fields[ - vol.Required(CONF_USB_STICK_PATH, default=DEFAULT_CONF_USB_STICK_PATH) - ] = str - fields[vol.Optional(CONF_NETWORK_KEY)] = str - - if user_input is not None: - # Check if USB path is valid - from openzwave.object import ZWaveException - from openzwave.option import ZWaveOption - - try: - from functools import partial - - option = await self.hass.async_add_executor_job( # noqa: F841 pylint: disable=unused-variable - partial( - ZWaveOption, - user_input[CONF_USB_STICK_PATH], - user_path=self.hass.config.config_dir, - ) - ) - except ZWaveException: - errors["base"] = "option_error" - return self.async_show_form( - step_id="user", data_schema=vol.Schema(fields), errors=errors - ) - - if user_input.get(CONF_NETWORK_KEY) is None: - # Generate a random key - from random import choice - - key = "" - for i in range(16): - key += "0x" - key += choice("1234567890ABCDEF") - key += choice("1234567890ABCDEF") - if i < 15: - key += ", " - user_input[CONF_NETWORK_KEY] = key - - return self.async_create_entry( - title="Z-Wave", - data={ - CONF_USB_STICK_PATH: user_input[CONF_USB_STICK_PATH], - CONF_NETWORK_KEY: user_input[CONF_NETWORK_KEY], - }, - ) - - return self.async_show_form(step_id="user", data_schema=vol.Schema(fields)) - - async def async_step_import(self, info): - """Import existing configuration from Z-Wave.""" - if self._async_current_entries(): - return self.async_abort(reason="already_setup") - - return self.async_create_entry( - title="Z-Wave (import from configuration.yaml)", - data={ - CONF_USB_STICK_PATH: info.get(CONF_USB_STICK_PATH), - CONF_NETWORK_KEY: info.get(CONF_NETWORK_KEY), - }, - ) diff --git a/homeassistant/components/zwave/const.py b/homeassistant/components/zwave/const.py deleted file mode 100644 index d11d308c490..00000000000 --- a/homeassistant/components/zwave/const.py +++ /dev/null @@ -1,395 +0,0 @@ -"""Z-Wave Constants.""" -DOMAIN = "zwave" - -ATTR_NODE_ID = "node_id" -ATTR_TARGET_NODE_ID = "target_node_id" -ATTR_ASSOCIATION = "association" -ATTR_INSTANCE = "instance" -ATTR_GROUP = "group" -ATTR_VALUE_ID = "value_id" -ATTR_MESSAGES = "messages" -ATTR_RETURN_ROUTES = "return_routes" -ATTR_SCENE_ID = "scene_id" -ATTR_SCENE_DATA = "scene_data" -ATTR_BASIC_LEVEL = "basic_level" -ATTR_CONFIG_PARAMETER = "parameter" -ATTR_CONFIG_SIZE = "size" -ATTR_CONFIG_VALUE = "value" -ATTR_POLL_INTENSITY = "poll_intensity" -ATTR_VALUE_INDEX = "value_index" -ATTR_VALUE_INSTANCE = "value_instance" -ATTR_UPDATE_IDS = "update_ids" -NETWORK_READY_WAIT_SECS = 300 -NODE_READY_WAIT_SECS = 30 - -CONF_AUTOHEAL = "autoheal" -CONF_DEBUG = "debug" -CONF_POLLING_INTERVAL = "polling_interval" -CONF_USB_STICK_PATH = "usb_path" -CONF_CONFIG_PATH = "config_path" -CONF_NETWORK_KEY = "network_key" - -DEFAULT_CONF_AUTOHEAL = False -DEFAULT_CONF_USB_STICK_PATH = "/zwaveusbstick" -DEFAULT_POLLING_INTERVAL = 60000 -DEFAULT_DEBUG = False - -DISCOVERY_DEVICE = "device" - -DATA_DEVICES = "zwave_devices" -DATA_NETWORK = "zwave_network" -DATA_ENTITY_VALUES = "zwave_entity_values" -DATA_ZWAVE_CONFIG = "zwave_config" - -SERVICE_CHANGE_ASSOCIATION = "change_association" -SERVICE_ADD_NODE = "add_node" -SERVICE_ADD_NODE_SECURE = "add_node_secure" -SERVICE_REMOVE_NODE = "remove_node" -SERVICE_CANCEL_COMMAND = "cancel_command" -SERVICE_HEAL_NETWORK = "heal_network" -SERVICE_HEAL_NODE = "heal_node" -SERVICE_SOFT_RESET = "soft_reset" -SERVICE_TEST_NODE = "test_node" -SERVICE_TEST_NETWORK = "test_network" -SERVICE_SET_CONFIG_PARAMETER = "set_config_parameter" -SERVICE_SET_NODE_VALUE = "set_node_value" -SERVICE_REFRESH_NODE_VALUE = "refresh_node_value" -SERVICE_PRINT_CONFIG_PARAMETER = "print_config_parameter" -SERVICE_PRINT_NODE = "print_node" -SERVICE_REMOVE_FAILED_NODE = "remove_failed_node" -SERVICE_REPLACE_FAILED_NODE = "replace_failed_node" -SERVICE_SET_POLL_INTENSITY = "set_poll_intensity" -SERVICE_SET_WAKEUP = "set_wakeup" -SERVICE_STOP_NETWORK = "stop_network" -SERVICE_START_NETWORK = "start_network" -SERVICE_RENAME_NODE = "rename_node" -SERVICE_RENAME_VALUE = "rename_value" -SERVICE_REFRESH_ENTITY = "refresh_entity" -SERVICE_REFRESH_NODE = "refresh_node" -SERVICE_RESET_NODE_METERS = "reset_node_meters" - -EVENT_SCENE_ACTIVATED = "zwave.scene_activated" -EVENT_NODE_EVENT = "zwave.node_event" -EVENT_NETWORK_READY = "zwave.network_ready" -EVENT_NETWORK_COMPLETE = "zwave.network_complete" -EVENT_NETWORK_COMPLETE_SOME_DEAD = "zwave.network_complete_some_dead" -EVENT_NETWORK_START = "zwave.network_start" -EVENT_NETWORK_STOP = "zwave.network_stop" - -COMMAND_CLASS_ALARM = 113 -COMMAND_CLASS_ANTITHEFT = 93 -COMMAND_CLASS_APPLICATION_CAPABILITY = 87 -COMMAND_CLASS_APPLICATION_STATUS = 34 -COMMAND_CLASS_ASSOCIATION = 133 -COMMAND_CLASS_ASSOCIATION_COMMAND_CONFIGURATION = 155 -COMMAND_CLASS_ASSOCIATION_GRP_INFO = 89 -COMMAND_CLASS_BARRIER_OPERATOR = 102 -COMMAND_CLASS_BASIC = 32 -COMMAND_CLASS_BASIC_TARIFF_INFO = 54 -COMMAND_CLASS_BASIC_WINDOW_COVERING = 80 -COMMAND_CLASS_BATTERY = 128 -COMMAND_CLASS_CENTRAL_SCENE = 91 -COMMAND_CLASS_CLIMATE_CONTROL_SCHEDULE = 70 -COMMAND_CLASS_CLOCK = 129 -COMMAND_CLASS_CONFIGURATION = 112 -COMMAND_CLASS_CONTROLLER_REPLICATION = 33 -COMMAND_CLASS_CRC_16_ENCAP = 86 -COMMAND_CLASS_DCP_CONFIG = 58 -COMMAND_CLASS_DCP_MONITOR = 59 -COMMAND_CLASS_DEVICE_RESET_LOCALLY = 90 -COMMAND_CLASS_DOOR_LOCK = 98 -COMMAND_CLASS_DOOR_LOCK_LOGGING = 76 -COMMAND_CLASS_ENERGY_PRODUCTION = 144 -COMMAND_CLASS_ENTRY_CONTROL = 111 -COMMAND_CLASS_FIRMWARE_UPDATE_MD = 122 -COMMAND_CLASS_GEOGRAPHIC_LOCATION = 140 -COMMAND_CLASS_GROUPING_NAME = 123 -COMMAND_CLASS_HAIL = 130 -COMMAND_CLASS_HRV_CONTROL = 57 -COMMAND_CLASS_HRV_STATUS = 55 -COMMAND_CLASS_HUMIDITY_CONTROL_MODE = 109 -COMMAND_CLASS_HUMIDITY_CONTROL_OPERATING_STATE = 110 -COMMAND_CLASS_HUMIDITY_CONTROL_SETPOINT = 100 -COMMAND_CLASS_INDICATOR = 135 -COMMAND_CLASS_IP_ASSOCIATION = 92 -COMMAND_CLASS_IP_CONFIGURATION = 14 -COMMAND_CLASS_IRRIGATION = 107 -COMMAND_CLASS_LANGUAGE = 137 -COMMAND_CLASS_LOCK = 118 -COMMAND_CLASS_MAILBOX = 105 -COMMAND_CLASS_MANUFACTURER_PROPRIETARY = 145 -COMMAND_CLASS_MANUFACTURER_SPECIFIC = 114 -COMMAND_CLASS_MARK = 239 -COMMAND_CLASS_METER = 50 -COMMAND_CLASS_METER_PULSE = 53 -COMMAND_CLASS_METER_TBL_CONFIG = 60 -COMMAND_CLASS_METER_TBL_MONITOR = 61 -COMMAND_CLASS_METER_TBL_PUSH = 62 -COMMAND_CLASS_MTP_WINDOW_COVERING = 81 -COMMAND_CLASS_MULTI_CHANNEL = 96 -COMMAND_CLASS_MULTI_CHANNEL_ASSOCIATION = 142 -COMMAND_CLASS_MULTI_COMMAND = 143 -COMMAND_CLASS_NETWORK_MANAGEMENT_BASIC = 77 -COMMAND_CLASS_NETWORK_MANAGEMENT_INCLUSION = 52 -COMMAND_CLASS_NETWORK_MANAGEMENT_PRIMARY = 84 -COMMAND_CLASS_NETWORK_MANAGEMENT_PROXY = 82 -COMMAND_CLASS_NO_OPERATION = 0 -COMMAND_CLASS_NODE_NAMING = 119 -COMMAND_CLASS_NON_INTEROPERABLE = 240 -COMMAND_CLASS_NOTIFICATION = 113 -COMMAND_CLASS_POWERLEVEL = 115 -COMMAND_CLASS_PREPAYMENT = 63 -COMMAND_CLASS_PREPAYMENT_ENCAPSULATION = 65 -COMMAND_CLASS_PROPRIETARY = 136 -COMMAND_CLASS_PROTECTION = 117 -COMMAND_CLASS_RATE_TBL_CONFIG = 72 -COMMAND_CLASS_RATE_TBL_MONITOR = 73 -COMMAND_CLASS_REMOTE_ASSOCIATION_ACTIVATE = 124 -COMMAND_CLASS_REMOTE_ASSOCIATION = 125 -COMMAND_CLASS_SCENE_ACTIVATION = 43 -COMMAND_CLASS_SCENE_ACTUATOR_CONF = 44 -COMMAND_CLASS_SCENE_CONTROLLER_CONF = 45 -COMMAND_CLASS_SCHEDULE = 83 -COMMAND_CLASS_SCHEDULE_ENTRY_LOCK = 78 -COMMAND_CLASS_SCREEN_ATTRIBUTES = 147 -COMMAND_CLASS_SCREEN_MD = 146 -COMMAND_CLASS_SECURITY = 152 -COMMAND_CLASS_SECURITY_SCHEME0_MARK = 61696 -COMMAND_CLASS_SENSOR_ALARM = 156 -COMMAND_CLASS_SENSOR_BINARY = 48 -COMMAND_CLASS_SENSOR_CONFIGURATION = 158 -COMMAND_CLASS_SENSOR_MULTILEVEL = 49 -COMMAND_CLASS_SILENCE_ALARM = 157 -COMMAND_CLASS_SIMPLE_AV_CONTROL = 148 -COMMAND_CLASS_SUPERVISION = 108 -COMMAND_CLASS_SWITCH_ALL = 39 -COMMAND_CLASS_SWITCH_BINARY = 37 -COMMAND_CLASS_SWITCH_COLOR = 51 -COMMAND_CLASS_SWITCH_MULTILEVEL = 38 -COMMAND_CLASS_SWITCH_TOGGLE_BINARY = 40 -COMMAND_CLASS_SWITCH_TOGGLE_MULTILEVEL = 41 -COMMAND_CLASS_TARIFF_TBL_CONFIG = 74 -COMMAND_CLASS_TARIFF_TBL_MONITOR = 75 -COMMAND_CLASS_THERMOSTAT_FAN_MODE = 68 -COMMAND_CLASS_THERMOSTAT_FAN_ACTION = 69 -COMMAND_CLASS_THERMOSTAT_MODE = 64 -COMMAND_CLASS_THERMOSTAT_OPERATING_STATE = 66 -COMMAND_CLASS_THERMOSTAT_SETBACK = 71 -COMMAND_CLASS_THERMOSTAT_SETPOINT = 67 -COMMAND_CLASS_TIME = 138 -COMMAND_CLASS_TIME_PARAMETERS = 139 -COMMAND_CLASS_TRANSPORT_SERVICE = 85 -COMMAND_CLASS_USER_CODE = 99 -COMMAND_CLASS_VERSION = 134 -COMMAND_CLASS_WAKE_UP = 132 -COMMAND_CLASS_ZIP = 35 -COMMAND_CLASS_ZIP_NAMING = 104 -COMMAND_CLASS_ZIP_ND = 88 -COMMAND_CLASS_ZIP_6LOWPAN = 79 -COMMAND_CLASS_ZIP_GATEWAY = 95 -COMMAND_CLASS_ZIP_PORTAL = 97 -COMMAND_CLASS_ZWAVEPLUS_INFO = 94 -COMMAND_CLASS_WHATEVER = None # Match ALL -COMMAND_CLASS_WINDOW_COVERING = 106 - -GENERIC_TYPE_WHATEVER = None # Match ALL -SPECIFIC_TYPE_WHATEVER = None # Match ALL -SPECIFIC_TYPE_NOT_USED = 0 # Available in all Generic types - -GENERIC_TYPE_AV_CONTROL_POINT = 3 -SPECIFIC_TYPE_DOORBELL = 18 -SPECIFIC_TYPE_SATELLITE_RECEIVER = 4 -SPECIFIC_TYPE_SATELLITE_RECEIVER_V2 = 17 - -GENERIC_TYPE_DISPLAY = 4 -SPECIFIC_TYPE_SIMPLE_DISPLAY = 1 - -GENERIC_TYPE_ENTRY_CONTROL = 64 -SPECIFIC_TYPE_DOOR_LOCK = 1 -SPECIFIC_TYPE_ADVANCED_DOOR_LOCK = 2 -SPECIFIC_TYPE_SECURE_KEYPAD_DOOR_LOCK = 3 -SPECIFIC_TYPE_SECURE_KEYPAD_DOOR_LOCK_DEADBOLT = 4 -SPECIFIC_TYPE_SECURE_DOOR = 5 -SPECIFIC_TYPE_SECURE_GATE = 6 -SPECIFIC_TYPE_SECURE_BARRIER_ADDON = 7 -SPECIFIC_TYPE_SECURE_BARRIER_OPEN_ONLY = 8 -SPECIFIC_TYPE_SECURE_BARRIER_CLOSE_ONLY = 9 -SPECIFIC_TYPE_SECURE_LOCKBOX = 10 -SPECIFIC_TYPE_SECURE_KEYPAD = 11 - -GENERIC_TYPE_GENERIC_CONTROLLER = 1 -SPECIFIC_TYPE_PORTABLE_CONTROLLER = 1 -SPECIFIC_TYPE_PORTABLE_SCENE_CONTROLLER = 2 -SPECIFIC_TYPE_PORTABLE_INSTALLER_TOOL = 3 -SPECIFIC_TYPE_REMOTE_CONTROL_AV = 4 -SPECIFIC_TYPE_REMOTE_CONTROL_SIMPLE = 6 - -GENERIC_TYPE_METER = 49 -SPECIFIC_TYPE_SIMPLE_METER = 1 -SPECIFIC_TYPE_ADV_ENERGY_CONTROL = 2 -SPECIFIC_TYPE_WHOLE_HOME_METER_SIMPLE = 3 - -GENERIC_TYPE_METER_PULSE = 48 - -GENERIC_TYPE_NON_INTEROPERABLE = 255 - -GENERIC_TYPE_REPEATER_SLAVE = 15 -SPECIFIC_TYPE_REPEATER_SLAVE = 1 -SPECIFIC_TYPE_VIRTUAL_NODE = 2 - -GENERIC_TYPE_SECURITY_PANEL = 23 -SPECIFIC_TYPE_ZONED_SECURITY_PANEL = 1 - -GENERIC_TYPE_SEMI_INTEROPERABLE = 80 -SPECIFIC_TYPE_ENERGY_PRODUCTION = 1 - -GENERIC_TYPE_SENSOR_ALARM = 161 -SPECIFIC_TYPE_ADV_ZENSOR_NET_ALARM_SENSOR = 5 -SPECIFIC_TYPE_ADV_ZENSOR_NET_SMOKE_SENSOR = 10 -SPECIFIC_TYPE_BASIC_ROUTING_ALARM_SENSOR = 1 -SPECIFIC_TYPE_BASIC_ROUTING_SMOKE_SENSOR = 6 -SPECIFIC_TYPE_BASIC_ZENSOR_NET_ALARM_SENSOR = 3 -SPECIFIC_TYPE_BASIC_ZENSOR_NET_SMOKE_SENSOR = 8 -SPECIFIC_TYPE_ROUTING_ALARM_SENSOR = 2 -SPECIFIC_TYPE_ROUTING_SMOKE_SENSOR = 7 -SPECIFIC_TYPE_ZENSOR_NET_ALARM_SENSOR = 4 -SPECIFIC_TYPE_ZENSOR_NET_SMOKE_SENSOR = 9 -SPECIFIC_TYPE_ALARM_SENSOR = 11 - -GENERIC_TYPE_SENSOR_BINARY = 32 -SPECIFIC_TYPE_ROUTING_SENSOR_BINARY = 1 - -GENERIC_TYPE_SENSOR_MULTILEVEL = 33 -SPECIFIC_TYPE_ROUTING_SENSOR_MULTILEVEL = 1 -SPECIFIC_TYPE_CHIMNEY_FAN = 2 - -GENERIC_TYPE_STATIC_CONTROLLER = 2 -SPECIFIC_TYPE_PC_CONTROLLER = 1 -SPECIFIC_TYPE_SCENE_CONTROLLER = 2 -SPECIFIC_TYPE_STATIC_INSTALLER_TOOL = 3 -SPECIFIC_TYPE_SET_TOP_BOX = 4 -SPECIFIC_TYPE_SUB_SYSTEM_CONTROLLER = 5 -SPECIFIC_TYPE_TV = 6 -SPECIFIC_TYPE_GATEWAY = 7 - -GENERIC_TYPE_SWITCH_BINARY = 16 -SPECIFIC_TYPE_POWER_SWITCH_BINARY = 1 -SPECIFIC_TYPE_SCENE_SWITCH_BINARY = 3 -SPECIFIC_TYPE_POWER_STRIP = 4 -SPECIFIC_TYPE_SIREN = 5 -SPECIFIC_TYPE_VALVE_OPEN_CLOSE = 6 -SPECIFIC_TYPE_COLOR_TUNABLE_BINARY = 2 -SPECIFIC_TYPE_IRRIGATION_CONTROLLER = 7 - -GENERIC_TYPE_SWITCH_MULTILEVEL = 17 -SPECIFIC_TYPE_CLASS_A_MOTOR_CONTROL = 5 -SPECIFIC_TYPE_CLASS_B_MOTOR_CONTROL = 6 -SPECIFIC_TYPE_CLASS_C_MOTOR_CONTROL = 7 -SPECIFIC_TYPE_MOTOR_MULTIPOSITION = 3 -SPECIFIC_TYPE_POWER_SWITCH_MULTILEVEL = 1 -SPECIFIC_TYPE_SCENE_SWITCH_MULTILEVEL = 4 -SPECIFIC_TYPE_FAN_SWITCH = 8 -SPECIFIC_TYPE_COLOR_TUNABLE_MULTILEVEL = 2 - -GENERIC_TYPE_SWITCH_REMOTE = 18 -SPECIFIC_TYPE_REMOTE_BINARY = 1 -SPECIFIC_TYPE_REMOTE_MULTILEVEL = 2 -SPECIFIC_TYPE_REMOTE_TOGGLE_BINARY = 3 -SPECIFIC_TYPE_REMOTE_TOGGLE_MULTILEVEL = 4 - -GENERIC_TYPE_SWITCH_TOGGLE = 19 -SPECIFIC_TYPE_SWITCH_TOGGLE_BINARY = 1 -SPECIFIC_TYPE_SWITCH_TOGGLE_MULTILEVEL = 2 - -GENERIC_TYPE_THERMOSTAT = 8 -SPECIFIC_TYPE_SETBACK_SCHEDULE_THERMOSTAT = 3 -SPECIFIC_TYPE_SETBACK_THERMOSTAT = 5 -SPECIFIC_TYPE_SETPOINT_THERMOSTAT = 4 -SPECIFIC_TYPE_THERMOSTAT_GENERAL = 2 -SPECIFIC_TYPE_THERMOSTAT_GENERAL_V2 = 6 -SPECIFIC_TYPE_THERMOSTAT_HEATING = 1 - -GENERIC_TYPE_VENTILATION = 22 -SPECIFIC_TYPE_RESIDENTIAL_HRV = 1 - -GENERIC_TYPE_WINDOWS_COVERING = 9 -SPECIFIC_TYPE_SIMPLE_WINDOW_COVERING = 1 - -GENERIC_TYPE_ZIP_NODE = 21 -SPECIFIC_TYPE_ZIP_ADV_NODE = 2 -SPECIFIC_TYPE_ZIP_TUN_NODE = 1 - -GENERIC_TYPE_WALL_CONTROLLER = 24 -SPECIFIC_TYPE_BASIC_WALL_CONTROLLER = 1 - -GENERIC_TYPE_NETWORK_EXTENDER = 5 -SPECIFIC_TYPE_SECURE_EXTENDER = 1 - -GENERIC_TYPE_APPLIANCE = 6 -SPECIFIC_TYPE_GENERAL_APPLIANCE = 1 -SPECIFIC_TYPE_KITCHEN_APPLIANCE = 2 -SPECIFIC_TYPE_LAUNDRY_APPLIANCE = 3 - -GENERIC_TYPE_SENSOR_NOTIFICATION = 7 -SPECIFIC_TYPE_NOTIFICATION_SENSOR = 1 - -GENRE_WHATEVER = None -GENRE_USER = "User" -GENRE_SYSTEM = "System" - -TYPE_WHATEVER = None -TYPE_BYTE = "Byte" -TYPE_BOOL = "Bool" -TYPE_DECIMAL = "Decimal" -TYPE_INT = "Int" -TYPE_LIST = "List" -TYPE_STRING = "String" -TYPE_BUTTON = "Button" - -DISC_COMMAND_CLASS = "command_class" -DISC_COMPONENT = "component" -DISC_GENERIC_DEVICE_CLASS = "generic_device_class" -DISC_GENRE = "genre" -DISC_INDEX = "index" -DISC_INSTANCE = "instance" -DISC_NODE_ID = "node_id" -DISC_OPTIONAL = "optional" -DISC_PRIMARY = "primary" -DISC_SCHEMAS = "schemas" -DISC_SPECIFIC_DEVICE_CLASS = "specific_device_class" -DISC_TYPE = "type" -DISC_VALUES = "values" - -# https://github.com/OpenZWave/open-zwave/blob/67f180eb565f0054f517ff395c71ecd706f6a837/cpp/src/command_classes/Alarm.cpp#L49 -# See also: -# https://github.com/OpenZWave/open-zwave/blob/67f180eb565f0054f517ff395c71ecd706f6a837/cpp/src/command_classes/Alarm.cpp#L275 -# https://github.com/OpenZWave/open-zwave/blob/67f180eb565f0054f517ff395c71ecd706f6a837/cpp/src/command_classes/Alarm.cpp#L278 -INDEX_ALARM_TYPE = 0 -INDEX_ALARM_LEVEL = 1 -INDEX_ALARM_ACCESS_CONTROL = 9 - -# https://github.com/OpenZWave/open-zwave/blob/de1c0e60edf1d1bee81f1ae54b1f58e66c6fd8ed/cpp/src/command_classes/BarrierOperator.cpp#L69 -INDEX_BARRIER_OPERATOR_LABEL = 1 - -# https://github.com/OpenZWave/open-zwave/blob/67f180eb565f0054f517ff395c71ecd706f6a837/cpp/src/command_classes/DoorLock.cpp#L77 -INDEX_DOOR_LOCK_LOCK = 0 - -# https://github.com/OpenZWave/open-zwave/blob/67f180eb565f0054f517ff395c71ecd706f6a837/cpp/src/command_classes/Meter.cpp#L114 -# See also: -# https://github.com/OpenZWave/open-zwave/blob/67f180eb565f0054f517ff395c71ecd706f6a837/cpp/src/command_classes/Meter.cpp#L279 -INDEX_METER_POWER = 8 -INDEX_METER_RESET = 33 - -# https://github.com/OpenZWave/open-zwave/blob/67f180eb565f0054f517ff395c71ecd706f6a837/cpp/src/command_classes/SensorMultilevel.cpp#L50 -INDEX_SENSOR_MULTILEVEL_TEMPERATURE = 1 -INDEX_SENSOR_MULTILEVEL_POWER = 4 - -# https://github.com/OpenZWave/open-zwave/blob/67f180eb565f0054f517ff395c71ecd706f6a837/cpp/src/command_classes/Color.cpp#L109 -INDEX_SWITCH_COLOR_COLOR = 0 -INDEX_SWITCH_COLOR_CHANNELS = 2 - -# https://github.com/OpenZWave/open-zwave/blob/67f180eb565f0054f517ff395c71ecd706f6a837/cpp/src/command_classes/SwitchMultilevel.cpp#L54 -INDEX_SWITCH_MULTILEVEL_LEVEL = 0 -INDEX_SWITCH_MULTILEVEL_BRIGHT = 1 -INDEX_SWITCH_MULTILEVEL_DIM = 2 -INDEX_SWITCH_MULTILEVEL_DURATION = 5 diff --git a/homeassistant/components/zwave/cover.py b/homeassistant/components/zwave/cover.py deleted file mode 100644 index 2a49a34554b..00000000000 --- a/homeassistant/components/zwave/cover.py +++ /dev/null @@ -1,216 +0,0 @@ -"""Support for Z-Wave covers.""" -import logging - -from homeassistant.components.cover import ( - ATTR_POSITION, - DOMAIN, - SUPPORT_CLOSE, - SUPPORT_OPEN, - CoverDeviceClass, - CoverEntity, -) -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity_platform import AddEntitiesCallback - -from . import ( - CONF_INVERT_OPENCLOSE_BUTTONS, - CONF_INVERT_PERCENT, - ZWaveDeviceEntity, - workaround, -) -from .const import ( - COMMAND_CLASS_BARRIER_OPERATOR, - COMMAND_CLASS_SWITCH_BINARY, - COMMAND_CLASS_SWITCH_MULTILEVEL, - DATA_NETWORK, -) - -_LOGGER = logging.getLogger(__name__) - -SUPPORT_GARAGE = SUPPORT_OPEN | SUPPORT_CLOSE - - -async def async_setup_entry( - hass: HomeAssistant, - config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up Z-Wave Cover from Config Entry.""" - - @callback - def async_add_cover(cover): - """Add Z-Wave Cover.""" - async_add_entities([cover]) - - async_dispatcher_connect(hass, "zwave_new_cover", async_add_cover) - - -def get_device(hass, values, node_config, **kwargs): - """Create Z-Wave entity device.""" - invert_buttons = node_config.get(CONF_INVERT_OPENCLOSE_BUTTONS) - invert_percent = node_config.get(CONF_INVERT_PERCENT) - if ( - values.primary.command_class == COMMAND_CLASS_SWITCH_MULTILEVEL - and values.primary.index == 0 - ): - return ZwaveRollershutter(hass, values, invert_buttons, invert_percent) - if values.primary.command_class == COMMAND_CLASS_SWITCH_BINARY: - return ZwaveGarageDoorSwitch(values) - if values.primary.command_class == COMMAND_CLASS_BARRIER_OPERATOR: - return ZwaveGarageDoorBarrier(values) - return None - - -class ZwaveRollershutter(ZWaveDeviceEntity, CoverEntity): - """Representation of an Z-Wave cover.""" - - def __init__(self, hass, values, invert_buttons, invert_percent): - """Initialize the Z-Wave rollershutter.""" - ZWaveDeviceEntity.__init__(self, values, DOMAIN) - self._network = hass.data[DATA_NETWORK] - self._open_id = None - self._close_id = None - self._current_position = None - self._invert_buttons = invert_buttons - self._invert_percent = invert_percent - - self._workaround = workaround.get_device_mapping(values.primary) - if self._workaround: - _LOGGER.debug("Using workaround %s", self._workaround) - self.update_properties() - - def update_properties(self): - """Handle data changes for node values.""" - # Position value - self._current_position = self.values.primary.data - - if ( - self.values.open - and self.values.close - and self._open_id is None - and self._close_id is None - ): - if self._invert_buttons: - self._open_id = self.values.close.value_id - self._close_id = self.values.open.value_id - else: - self._open_id = self.values.open.value_id - self._close_id = self.values.close.value_id - - @property - def is_closed(self): - """Return if the cover is closed.""" - if self.current_cover_position is None: - return None - if self.current_cover_position > 0: - return False - return True - - @property - def current_cover_position(self): - """Return the current position of Zwave roller shutter.""" - if self._workaround == workaround.WORKAROUND_NO_POSITION: - return None - - if self._current_position is not None: - if self._current_position <= 5: - return 100 if self._invert_percent else 0 - if self._current_position >= 95: - return 0 if self._invert_percent else 100 - return ( - 100 - self._current_position - if self._invert_percent - else self._current_position - ) - - def open_cover(self, **kwargs): - """Move the roller shutter up.""" - self._network.manager.pressButton(self._open_id) - - def close_cover(self, **kwargs): - """Move the roller shutter down.""" - self._network.manager.pressButton(self._close_id) - - def set_cover_position(self, **kwargs): - """Move the roller shutter to a specific position.""" - self.node.set_dimmer( - self.values.primary.value_id, - (100 - kwargs.get(ATTR_POSITION)) - if self._invert_percent - else kwargs.get(ATTR_POSITION), - ) - - def stop_cover(self, **kwargs): - """Stop the roller shutter.""" - self._network.manager.releaseButton(self._open_id) - - -class ZwaveGarageDoorBase(ZWaveDeviceEntity, CoverEntity): - """Base class for a Zwave garage door device.""" - - def __init__(self, values): - """Initialize the zwave garage door.""" - ZWaveDeviceEntity.__init__(self, values, DOMAIN) - self._state = None - self.update_properties() - - def update_properties(self): - """Handle data changes for node values.""" - self._state = self.values.primary.data - _LOGGER.debug("self._state=%s", self._state) - - @property - def device_class(self): - """Return the class of this device, from CoverDeviceClass.""" - return CoverDeviceClass.GARAGE - - @property - def supported_features(self): - """Flag supported features.""" - return SUPPORT_GARAGE - - -class ZwaveGarageDoorSwitch(ZwaveGarageDoorBase): - """Representation of a switch based Zwave garage door device.""" - - @property - def is_closed(self): - """Return the current position of Zwave garage door.""" - return not self._state - - def close_cover(self, **kwargs): - """Close the garage door.""" - self.values.primary.data = False - - def open_cover(self, **kwargs): - """Open the garage door.""" - self.values.primary.data = True - - -class ZwaveGarageDoorBarrier(ZwaveGarageDoorBase): - """Representation of a barrier operator Zwave garage door device.""" - - @property - def is_opening(self): - """Return true if cover is in an opening state.""" - return self._state == "Opening" - - @property - def is_closing(self): - """Return true if cover is in a closing state.""" - return self._state == "Closing" - - @property - def is_closed(self): - """Return the current position of Zwave garage door.""" - return self._state == "Closed" - - def close_cover(self, **kwargs): - """Close the garage door.""" - self.values.primary.data = "Closed" - - def open_cover(self, **kwargs): - """Open the garage door.""" - self.values.primary.data = "Opened" diff --git a/homeassistant/components/zwave/discovery_schemas.py b/homeassistant/components/zwave/discovery_schemas.py deleted file mode 100644 index f8674a48a32..00000000000 --- a/homeassistant/components/zwave/discovery_schemas.py +++ /dev/null @@ -1,416 +0,0 @@ -"""Z-Wave discovery schemas.""" -from . import const - -DEFAULT_VALUES_SCHEMA = { - "power": { - const.DISC_SCHEMAS: [ - { - const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_SENSOR_MULTILEVEL], - const.DISC_INDEX: [const.INDEX_SENSOR_MULTILEVEL_POWER], - }, - { - const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_METER], - const.DISC_INDEX: [const.INDEX_METER_POWER], - }, - ], - const.DISC_OPTIONAL: True, - } -} - -DISCOVERY_SCHEMAS = [ - { - const.DISC_COMPONENT: "binary_sensor", - const.DISC_GENERIC_DEVICE_CLASS: [ - const.GENERIC_TYPE_ENTRY_CONTROL, - const.GENERIC_TYPE_SENSOR_ALARM, - const.GENERIC_TYPE_SENSOR_BINARY, - const.GENERIC_TYPE_SWITCH_BINARY, - const.GENERIC_TYPE_METER, - const.GENERIC_TYPE_SENSOR_MULTILEVEL, - const.GENERIC_TYPE_SWITCH_MULTILEVEL, - const.GENERIC_TYPE_SENSOR_NOTIFICATION, - const.GENERIC_TYPE_THERMOSTAT, - ], - const.DISC_VALUES: dict( - DEFAULT_VALUES_SCHEMA, - **{ - const.DISC_PRIMARY: { - const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_SENSOR_BINARY], - const.DISC_TYPE: const.TYPE_BOOL, - const.DISC_GENRE: const.GENRE_USER, - }, - "off_delay": { - const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_CONFIGURATION], - const.DISC_INDEX: [9], - const.DISC_OPTIONAL: True, - }, - }, - ), - }, - { - const.DISC_COMPONENT: "climate", # thermostat without COMMAND_CLASS_THERMOSTAT_MODE - const.DISC_GENERIC_DEVICE_CLASS: [ - const.GENERIC_TYPE_THERMOSTAT, - const.GENERIC_TYPE_SENSOR_MULTILEVEL, - ], - const.DISC_SPECIFIC_DEVICE_CLASS: [ - const.SPECIFIC_TYPE_THERMOSTAT_HEATING, - const.SPECIFIC_TYPE_SETPOINT_THERMOSTAT, - const.SPECIFIC_TYPE_NOT_USED, - ], - const.DISC_VALUES: dict( - DEFAULT_VALUES_SCHEMA, - **{ - const.DISC_PRIMARY: { - const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_THERMOSTAT_SETPOINT] - }, - "temperature": { - const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_SENSOR_MULTILEVEL], - const.DISC_INDEX: [const.INDEX_SENSOR_MULTILEVEL_TEMPERATURE], - const.DISC_OPTIONAL: True, - }, - "fan_mode": { - const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_THERMOSTAT_FAN_MODE], - const.DISC_OPTIONAL: True, - }, - "operating_state": { - const.DISC_COMMAND_CLASS: [ - const.COMMAND_CLASS_THERMOSTAT_OPERATING_STATE - ], - const.DISC_OPTIONAL: True, - }, - "fan_action": { - const.DISC_COMMAND_CLASS: [ - const.COMMAND_CLASS_THERMOSTAT_FAN_ACTION - ], - const.DISC_OPTIONAL: True, - }, - "mode": { - const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_THERMOSTAT_MODE], - const.DISC_OPTIONAL: True, - }, - }, - ), - }, - { - const.DISC_COMPONENT: "climate", # thermostat with COMMAND_CLASS_THERMOSTAT_MODE - const.DISC_GENERIC_DEVICE_CLASS: [ - const.GENERIC_TYPE_THERMOSTAT, - const.GENERIC_TYPE_SENSOR_MULTILEVEL, - ], - const.DISC_SPECIFIC_DEVICE_CLASS: [ - const.SPECIFIC_TYPE_THERMOSTAT_GENERAL, - const.SPECIFIC_TYPE_THERMOSTAT_GENERAL_V2, - const.SPECIFIC_TYPE_SETBACK_THERMOSTAT, - ], - const.DISC_VALUES: dict( - DEFAULT_VALUES_SCHEMA, - **{ - const.DISC_PRIMARY: { - const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_THERMOSTAT_MODE] - }, - "setpoint_heating": { - const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_THERMOSTAT_SETPOINT], - const.DISC_INDEX: [1], - const.DISC_OPTIONAL: True, - }, - "setpoint_cooling": { - const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_THERMOSTAT_SETPOINT], - const.DISC_INDEX: [2], - const.DISC_OPTIONAL: True, - }, - "setpoint_furnace": { - const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_THERMOSTAT_SETPOINT], - const.DISC_INDEX: [7], - const.DISC_OPTIONAL: True, - }, - "setpoint_dry_air": { - const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_THERMOSTAT_SETPOINT], - const.DISC_INDEX: [8], - const.DISC_OPTIONAL: True, - }, - "setpoint_moist_air": { - const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_THERMOSTAT_SETPOINT], - const.DISC_INDEX: [9], - const.DISC_OPTIONAL: True, - }, - "setpoint_auto_changeover": { - const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_THERMOSTAT_SETPOINT], - const.DISC_INDEX: [10], - const.DISC_OPTIONAL: True, - }, - "setpoint_eco_heating": { - const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_THERMOSTAT_SETPOINT], - const.DISC_INDEX: [11], - const.DISC_OPTIONAL: True, - }, - "setpoint_eco_cooling": { - const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_THERMOSTAT_SETPOINT], - const.DISC_INDEX: [12], - const.DISC_OPTIONAL: True, - }, - "setpoint_away_heating": { - const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_THERMOSTAT_SETPOINT], - const.DISC_INDEX: [13], - const.DISC_OPTIONAL: True, - }, - "setpoint_away_cooling": { - const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_THERMOSTAT_SETPOINT], - const.DISC_INDEX: [14], - const.DISC_OPTIONAL: True, - }, - "setpoint_full_power": { - const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_THERMOSTAT_SETPOINT], - const.DISC_INDEX: [15], - const.DISC_OPTIONAL: True, - }, - "temperature": { - const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_SENSOR_MULTILEVEL], - const.DISC_INDEX: [const.INDEX_SENSOR_MULTILEVEL_TEMPERATURE], - const.DISC_OPTIONAL: True, - }, - "fan_mode": { - const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_THERMOSTAT_FAN_MODE], - const.DISC_OPTIONAL: True, - }, - "operating_state": { - const.DISC_COMMAND_CLASS: [ - const.COMMAND_CLASS_THERMOSTAT_OPERATING_STATE - ], - const.DISC_OPTIONAL: True, - }, - "fan_action": { - const.DISC_COMMAND_CLASS: [ - const.COMMAND_CLASS_THERMOSTAT_FAN_ACTION - ], - const.DISC_OPTIONAL: True, - }, - "zxt_120_swing_mode": { - const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_CONFIGURATION], - const.DISC_INDEX: [33], - const.DISC_OPTIONAL: True, - }, - }, - ), - }, - { - const.DISC_COMPONENT: "cover", # Rollershutter - const.DISC_GENERIC_DEVICE_CLASS: [ - const.GENERIC_TYPE_SWITCH_MULTILEVEL, - const.GENERIC_TYPE_ENTRY_CONTROL, - ], - const.DISC_SPECIFIC_DEVICE_CLASS: [ - const.SPECIFIC_TYPE_CLASS_A_MOTOR_CONTROL, - const.SPECIFIC_TYPE_CLASS_B_MOTOR_CONTROL, - const.SPECIFIC_TYPE_CLASS_C_MOTOR_CONTROL, - const.SPECIFIC_TYPE_MOTOR_MULTIPOSITION, - const.SPECIFIC_TYPE_SECURE_BARRIER_ADDON, - const.SPECIFIC_TYPE_SECURE_DOOR, - ], - const.DISC_VALUES: dict( - DEFAULT_VALUES_SCHEMA, - **{ - const.DISC_PRIMARY: { - const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_SWITCH_MULTILEVEL], - const.DISC_GENRE: const.GENRE_USER, - }, - "open": { - const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_SWITCH_MULTILEVEL], - const.DISC_INDEX: [const.INDEX_SWITCH_MULTILEVEL_BRIGHT], - const.DISC_OPTIONAL: True, - }, - "close": { - const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_SWITCH_MULTILEVEL], - const.DISC_INDEX: [const.INDEX_SWITCH_MULTILEVEL_DIM], - const.DISC_OPTIONAL: True, - }, - }, - ), - }, - { - const.DISC_COMPONENT: "cover", # Garage Door Switch - const.DISC_GENERIC_DEVICE_CLASS: [ - const.GENERIC_TYPE_SWITCH_MULTILEVEL, - const.GENERIC_TYPE_ENTRY_CONTROL, - ], - const.DISC_SPECIFIC_DEVICE_CLASS: [ - const.SPECIFIC_TYPE_CLASS_A_MOTOR_CONTROL, - const.SPECIFIC_TYPE_CLASS_B_MOTOR_CONTROL, - const.SPECIFIC_TYPE_CLASS_C_MOTOR_CONTROL, - const.SPECIFIC_TYPE_MOTOR_MULTIPOSITION, - const.SPECIFIC_TYPE_SECURE_BARRIER_ADDON, - const.SPECIFIC_TYPE_SECURE_DOOR, - ], - const.DISC_VALUES: dict( - DEFAULT_VALUES_SCHEMA, - **{ - const.DISC_PRIMARY: { - const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_SWITCH_BINARY], - const.DISC_GENRE: const.GENRE_USER, - } - }, - ), - }, - { - const.DISC_COMPONENT: "cover", # Garage Door Barrier - const.DISC_GENERIC_DEVICE_CLASS: [ - const.GENERIC_TYPE_SWITCH_MULTILEVEL, - const.GENERIC_TYPE_ENTRY_CONTROL, - ], - const.DISC_SPECIFIC_DEVICE_CLASS: [ - const.SPECIFIC_TYPE_CLASS_A_MOTOR_CONTROL, - const.SPECIFIC_TYPE_CLASS_B_MOTOR_CONTROL, - const.SPECIFIC_TYPE_CLASS_C_MOTOR_CONTROL, - const.SPECIFIC_TYPE_MOTOR_MULTIPOSITION, - const.SPECIFIC_TYPE_SECURE_BARRIER_ADDON, - const.SPECIFIC_TYPE_SECURE_DOOR, - ], - const.DISC_VALUES: dict( - DEFAULT_VALUES_SCHEMA, - **{ - const.DISC_PRIMARY: { - const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_BARRIER_OPERATOR], - const.DISC_INDEX: [const.INDEX_BARRIER_OPERATOR_LABEL], - } - }, - ), - }, - { - const.DISC_COMPONENT: "fan", - const.DISC_GENERIC_DEVICE_CLASS: [const.GENERIC_TYPE_SWITCH_MULTILEVEL], - const.DISC_SPECIFIC_DEVICE_CLASS: [const.SPECIFIC_TYPE_FAN_SWITCH], - const.DISC_VALUES: dict( - DEFAULT_VALUES_SCHEMA, - **{ - const.DISC_PRIMARY: { - const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_SWITCH_MULTILEVEL], - const.DISC_INDEX: [const.INDEX_SWITCH_MULTILEVEL_LEVEL], - const.DISC_TYPE: const.TYPE_BYTE, - } - }, - ), - }, - { - const.DISC_COMPONENT: "light", - const.DISC_GENERIC_DEVICE_CLASS: [ - const.GENERIC_TYPE_SWITCH_MULTILEVEL, - const.GENERIC_TYPE_SWITCH_REMOTE, - ], - const.DISC_SPECIFIC_DEVICE_CLASS: [ - const.SPECIFIC_TYPE_POWER_SWITCH_MULTILEVEL, - const.SPECIFIC_TYPE_SCENE_SWITCH_MULTILEVEL, - const.SPECIFIC_TYPE_NOT_USED, - ], - const.DISC_VALUES: dict( - DEFAULT_VALUES_SCHEMA, - **{ - const.DISC_PRIMARY: { - const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_SWITCH_MULTILEVEL], - const.DISC_INDEX: [const.INDEX_SWITCH_MULTILEVEL_LEVEL], - const.DISC_TYPE: const.TYPE_BYTE, - }, - "dimming_duration": { - const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_SWITCH_MULTILEVEL], - const.DISC_INDEX: [const.INDEX_SWITCH_MULTILEVEL_DURATION], - const.DISC_OPTIONAL: True, - }, - "color": { - const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_SWITCH_COLOR], - const.DISC_INDEX: [const.INDEX_SWITCH_COLOR_COLOR], - const.DISC_OPTIONAL: True, - }, - "color_channels": { - const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_SWITCH_COLOR], - const.DISC_INDEX: [const.INDEX_SWITCH_COLOR_CHANNELS], - const.DISC_OPTIONAL: True, - }, - }, - ), - }, - { - const.DISC_COMPONENT: "lock", - const.DISC_GENERIC_DEVICE_CLASS: [const.GENERIC_TYPE_ENTRY_CONTROL], - const.DISC_SPECIFIC_DEVICE_CLASS: [ - const.SPECIFIC_TYPE_DOOR_LOCK, - const.SPECIFIC_TYPE_ADVANCED_DOOR_LOCK, - const.SPECIFIC_TYPE_SECURE_KEYPAD_DOOR_LOCK, - const.SPECIFIC_TYPE_SECURE_LOCKBOX, - ], - const.DISC_VALUES: dict( - DEFAULT_VALUES_SCHEMA, - **{ - const.DISC_PRIMARY: { - const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_DOOR_LOCK], - const.DISC_INDEX: [const.INDEX_DOOR_LOCK_LOCK], - }, - "access_control": { - const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_ALARM], - const.DISC_INDEX: [const.INDEX_ALARM_ACCESS_CONTROL], - const.DISC_OPTIONAL: True, - }, - "alarm_type": { - const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_ALARM], - const.DISC_INDEX: [const.INDEX_ALARM_TYPE], - const.DISC_OPTIONAL: True, - }, - "alarm_level": { - const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_ALARM], - const.DISC_INDEX: [const.INDEX_ALARM_LEVEL], - const.DISC_OPTIONAL: True, - }, - "v2btze_advanced": { - const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_CONFIGURATION], - const.DISC_INDEX: [12], - const.DISC_OPTIONAL: True, - }, - }, - ), - }, - { - const.DISC_COMPONENT: "sensor", - const.DISC_VALUES: dict( - DEFAULT_VALUES_SCHEMA, - **{ - const.DISC_PRIMARY: { - const.DISC_COMMAND_CLASS: [ - const.COMMAND_CLASS_SENSOR_MULTILEVEL, - const.COMMAND_CLASS_METER, - const.COMMAND_CLASS_ALARM, - const.COMMAND_CLASS_SENSOR_ALARM, - const.COMMAND_CLASS_INDICATOR, - const.COMMAND_CLASS_BATTERY, - ], - const.DISC_GENRE: const.GENRE_USER, - } - }, - ), - }, - { - const.DISC_COMPONENT: "switch", - const.DISC_GENERIC_DEVICE_CLASS: [ - const.GENERIC_TYPE_METER, - const.GENERIC_TYPE_SENSOR_ALARM, - const.GENERIC_TYPE_SENSOR_BINARY, - const.GENERIC_TYPE_SWITCH_BINARY, - const.GENERIC_TYPE_ENTRY_CONTROL, - const.GENERIC_TYPE_SENSOR_MULTILEVEL, - const.GENERIC_TYPE_SWITCH_MULTILEVEL, - const.GENERIC_TYPE_SENSOR_NOTIFICATION, - const.GENERIC_TYPE_GENERIC_CONTROLLER, - const.GENERIC_TYPE_SWITCH_REMOTE, - const.GENERIC_TYPE_REPEATER_SLAVE, - const.GENERIC_TYPE_THERMOSTAT, - const.GENERIC_TYPE_WALL_CONTROLLER, - ], - const.DISC_VALUES: dict( - DEFAULT_VALUES_SCHEMA, - **{ - const.DISC_PRIMARY: { - const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_SWITCH_BINARY], - const.DISC_TYPE: const.TYPE_BOOL, - const.DISC_GENRE: const.GENRE_USER, - } - }, - ), - }, -] diff --git a/homeassistant/components/zwave/fan.py b/homeassistant/components/zwave/fan.py deleted file mode 100644 index b368e829eb7..00000000000 --- a/homeassistant/components/zwave/fan.py +++ /dev/null @@ -1,86 +0,0 @@ -"""Support for Z-Wave fans.""" -import math - -from homeassistant.components.fan import DOMAIN, SUPPORT_SET_SPEED, FanEntity -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.util.percentage import ( - int_states_in_range, - percentage_to_ranged_value, - ranged_value_to_percentage, -) - -from . import ZWaveDeviceEntity - -SUPPORTED_FEATURES = SUPPORT_SET_SPEED - -SPEED_RANGE = (1, 99) # off is not included - - -async def async_setup_entry( - hass: HomeAssistant, - config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up Z-Wave Fan from Config Entry.""" - - @callback - def async_add_fan(fan): - """Add Z-Wave Fan.""" - async_add_entities([fan]) - - async_dispatcher_connect(hass, "zwave_new_fan", async_add_fan) - - -def get_device(values, **kwargs): - """Create Z-Wave entity device.""" - return ZwaveFan(values) - - -class ZwaveFan(ZWaveDeviceEntity, FanEntity): - """Representation of a Z-Wave fan.""" - - def __init__(self, values): - """Initialize the Z-Wave fan device.""" - ZWaveDeviceEntity.__init__(self, values, DOMAIN) - self.update_properties() - - def update_properties(self): - """Handle data changes for node values.""" - self._state = self.values.primary.data - - def set_percentage(self, percentage): - """Set the speed percentage of the fan.""" - if percentage is None: - # Value 255 tells device to return to previous value - zwave_speed = 255 - elif percentage == 0: - zwave_speed = 0 - else: - zwave_speed = math.ceil(percentage_to_ranged_value(SPEED_RANGE, percentage)) - self.node.set_dimmer(self.values.primary.value_id, zwave_speed) - - def turn_on(self, speed=None, percentage=None, preset_mode=None, **kwargs): - """Turn the device on.""" - self.set_percentage(percentage) - - def turn_off(self, **kwargs): - """Turn the device off.""" - self.node.set_dimmer(self.values.primary.value_id, 0) - - @property - def percentage(self): - """Return the current speed percentage.""" - return ranged_value_to_percentage(SPEED_RANGE, self._state) - - @property - def speed_count(self) -> int: - """Return the number of speeds the fan supports.""" - return int_states_in_range(SPEED_RANGE) - - @property - def supported_features(self): - """Flag supported features.""" - return SUPPORTED_FEATURES diff --git a/homeassistant/components/zwave/light.py b/homeassistant/components/zwave/light.py deleted file mode 100644 index ea2b34a874f..00000000000 --- a/homeassistant/components/zwave/light.py +++ /dev/null @@ -1,407 +0,0 @@ -"""Support for Z-Wave lights.""" -import logging -from threading import Timer - -from homeassistant.components.light import ( - ATTR_BRIGHTNESS, - ATTR_COLOR_TEMP, - ATTR_RGB_COLOR, - ATTR_RGBW_COLOR, - ATTR_TRANSITION, - COLOR_MODE_BRIGHTNESS, - COLOR_MODE_COLOR_TEMP, - COLOR_MODE_RGB, - COLOR_MODE_RGBW, - DOMAIN, - SUPPORT_TRANSITION, - LightEntity, -) -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import STATE_OFF, STATE_ON -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity_platform import AddEntitiesCallback - -from . import CONF_REFRESH_DELAY, CONF_REFRESH_VALUE, ZWaveDeviceEntity, const - -_LOGGER = logging.getLogger(__name__) - -COLOR_CHANNEL_WARM_WHITE = 0x01 -COLOR_CHANNEL_COLD_WHITE = 0x02 -COLOR_CHANNEL_RED = 0x04 -COLOR_CHANNEL_GREEN = 0x08 -COLOR_CHANNEL_BLUE = 0x10 - -# Some bulbs have an independent warm and cool white light LEDs. These need -# to be treated differently, aka the zw098 workaround. Ensure these are added -# to DEVICE_MAPPINGS below. -# (Manufacturer ID, Product ID) from -# https://github.com/OpenZWave/open-zwave/blob/master/config/manufacturer_specific.xml -AEOTEC_ZW098_LED_BULB_LIGHT = (0x86, 0x62) -AEOTEC_ZWA001_LED_BULB_LIGHT = (0x371, 0x1) -AEOTEC_ZWA002_LED_BULB_LIGHT = (0x371, 0x2) -HANK_HKZW_RGB01_LED_BULB_LIGHT = (0x208, 0x4) -ZIPATO_RGB_BULB_2_LED_BULB_LIGHT = (0x131, 0x3) - -WORKAROUND_ZW098 = "zw098" - -DEVICE_MAPPINGS = { - AEOTEC_ZW098_LED_BULB_LIGHT: WORKAROUND_ZW098, - AEOTEC_ZWA001_LED_BULB_LIGHT: WORKAROUND_ZW098, - AEOTEC_ZWA002_LED_BULB_LIGHT: WORKAROUND_ZW098, - HANK_HKZW_RGB01_LED_BULB_LIGHT: WORKAROUND_ZW098, - ZIPATO_RGB_BULB_2_LED_BULB_LIGHT: WORKAROUND_ZW098, -} - -# Generate midpoint color temperatures for bulbs that have limited -# support for white light colors -TEMP_COLOR_MAX = 500 # mireds (inverted) -TEMP_COLOR_MIN = 154 -TEMP_MID_HASS = (TEMP_COLOR_MAX - TEMP_COLOR_MIN) / 2 + TEMP_COLOR_MIN -TEMP_WARM_HASS = (TEMP_COLOR_MAX - TEMP_COLOR_MIN) / 3 * 2 + TEMP_COLOR_MIN -TEMP_COLD_HASS = (TEMP_COLOR_MAX - TEMP_COLOR_MIN) / 3 + TEMP_COLOR_MIN - - -async def async_setup_entry( - hass: HomeAssistant, - config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up Z-Wave Light from Config Entry.""" - - @callback - def async_add_light(light): - """Add Z-Wave Light.""" - async_add_entities([light]) - - async_dispatcher_connect(hass, "zwave_new_light", async_add_light) - - -def get_device(node, values, node_config, **kwargs): - """Create Z-Wave entity device.""" - refresh = node_config.get(CONF_REFRESH_VALUE) - delay = node_config.get(CONF_REFRESH_DELAY) - _LOGGER.debug( - "node=%d value=%d node_config=%s CONF_REFRESH_VALUE=%s" - " CONF_REFRESH_DELAY=%s", - node.node_id, - values.primary.value_id, - node_config, - refresh, - delay, - ) - - if node.has_command_class(const.COMMAND_CLASS_SWITCH_COLOR): - return ZwaveColorLight(values, refresh, delay) - return ZwaveDimmer(values, refresh, delay) - - -def brightness_state(value): - """Return the brightness and state.""" - if value.data > 0: - return round((value.data / 99) * 255), STATE_ON - return 0, STATE_OFF - - -def byte_to_zwave_brightness(value): - """Convert brightness in 0-255 scale to 0-99 scale. - - `value` -- (int) Brightness byte value from 0-255. - """ - if value > 0: - return max(1, round((value / 255) * 99)) - return 0 - - -class ZwaveDimmer(ZWaveDeviceEntity, LightEntity): - """Representation of a Z-Wave dimmer.""" - - def __init__(self, values, refresh, delay): - """Initialize the light.""" - ZWaveDeviceEntity.__init__(self, values, DOMAIN) - self._brightness = None - self._state = None - self._color_mode = None - self._supported_color_modes = set() - self._supported_features = 0 - self._delay = delay - self._refresh_value = refresh - self._zw098 = None - - # Enable appropriate workaround flags for our device - # Make sure that we have values for the key before converting to int - if self.node.manufacturer_id.strip() and self.node.product_id.strip(): - specific_sensor_key = ( - int(self.node.manufacturer_id, 16), - int(self.node.product_id, 16), - ) - if ( - specific_sensor_key in DEVICE_MAPPINGS - and DEVICE_MAPPINGS[specific_sensor_key] == WORKAROUND_ZW098 - ): - _LOGGER.debug("AEOTEC ZW098 workaround enabled") - self._zw098 = 1 - - # Used for value change event handling - self._refreshing = False - self._timer = None - _LOGGER.debug( - "self._refreshing=%s self.delay=%s", self._refresh_value, self._delay - ) - self.value_added() - self.update_properties() - - def update_properties(self): - """Update internal properties based on zwave values.""" - # Brightness - self._brightness, self._state = brightness_state(self.values.primary) - - def value_added(self): - """Call when a new value is added to this entity.""" - self._supported_color_modes = {COLOR_MODE_BRIGHTNESS} - self._color_mode = COLOR_MODE_BRIGHTNESS - if self.values.dimming_duration is not None: - self._supported_features = SUPPORT_TRANSITION - - def value_changed(self): - """Call when a value for this entity's node has changed.""" - if self._refresh_value: - if self._refreshing: - self._refreshing = False - else: - - def _refresh_value(): - """Use timer callback for delayed value refresh.""" - self._refreshing = True - self.values.primary.refresh() - - if self._timer is not None and self._timer.is_alive(): - self._timer.cancel() - - self._timer = Timer(self._delay, _refresh_value) - self._timer.start() - return - super().value_changed() - - @property - def brightness(self): - """Return the brightness of this light between 0..255.""" - return self._brightness - - @property - def is_on(self): - """Return true if device is on.""" - return self._state == STATE_ON - - @property - def color_mode(self): - """Return the current color mode.""" - return self._color_mode - - @property - def supported_color_modes(self): - """Flag supported color modes.""" - return self._supported_color_modes - - @property - def supported_features(self): - """Flag supported features.""" - return self._supported_features - - def _set_duration(self, **kwargs): - """Set the transition time for the brightness value. - - Zwave Dimming Duration values: - 0x00 = instant - 0x01-0x7F = 1 second to 127 seconds - 0x80-0xFE = 1 minute to 127 minutes - 0xFF = factory default - """ - if self.values.dimming_duration is None: - if ATTR_TRANSITION in kwargs: - _LOGGER.debug("Dimming not supported by %s", self.entity_id) - return - - if ATTR_TRANSITION not in kwargs: - self.values.dimming_duration.data = 0xFF - return - - transition = kwargs[ATTR_TRANSITION] - if transition <= 127: - self.values.dimming_duration.data = int(transition) - elif transition > 7620: - self.values.dimming_duration.data = 0xFE - _LOGGER.warning("Transition clipped to 127 minutes for %s", self.entity_id) - else: - minutes = int(transition / 60) - _LOGGER.debug( - "Transition rounded to %d minutes for %s", minutes, self.entity_id - ) - self.values.dimming_duration.data = minutes + 0x7F - - def turn_on(self, **kwargs): - """Turn the device on.""" - self._set_duration(**kwargs) - - # Zwave multilevel switches use a range of [0, 99] to control - # brightness. Level 255 means to set it to previous value. - if ATTR_BRIGHTNESS in kwargs: - self._brightness = kwargs[ATTR_BRIGHTNESS] - brightness = byte_to_zwave_brightness(self._brightness) - else: - brightness = 255 - - if self.node.set_dimmer(self.values.primary.value_id, brightness): - self._state = STATE_ON - - def turn_off(self, **kwargs): - """Turn the device off.""" - self._set_duration(**kwargs) - - if self.node.set_dimmer(self.values.primary.value_id, 0): - self._state = STATE_OFF - - -class ZwaveColorLight(ZwaveDimmer): - """Representation of a Z-Wave color changing light.""" - - def __init__(self, values, refresh, delay): - """Initialize the light.""" - self._color_channels = None - self._rgb = None - self._ct = None - self._white = None - - super().__init__(values, refresh, delay) - - def value_added(self): - """Call when a new value is added to this entity.""" - if self.values.dimming_duration is not None: - self._supported_features = SUPPORT_TRANSITION - - self._supported_color_modes = {COLOR_MODE_RGB} - self._color_mode = COLOR_MODE_RGB - if self._zw098: - self._supported_color_modes.add(COLOR_MODE_COLOR_TEMP) - elif self._color_channels is not None and self._color_channels & ( - COLOR_CHANNEL_WARM_WHITE | COLOR_CHANNEL_COLD_WHITE - ): - self._supported_color_modes = {COLOR_MODE_RGBW} - self._color_mode = COLOR_MODE_RGBW - - def update_properties(self): - """Update internal properties based on zwave values.""" - super().update_properties() - - if self.values.color is None: - return - if self.values.color_channels is None: - return - - # Color Channels - self._color_channels = self.values.color_channels.data - - # Color Data String - data = self.values.color.data - - # RGB is always present in the openzwave color data string. - self._rgb = (int(data[1:3], 16), int(data[3:5], 16), int(data[5:7], 16)) - - # Parse remaining color channels. Openzwave appends white channels - # that are present. - index = 7 - - # Warm white - if self._color_channels & COLOR_CHANNEL_WARM_WHITE: - warm_white = int(data[index : index + 2], 16) - index += 2 - else: - warm_white = 0 - - # Cold white - if self._color_channels & COLOR_CHANNEL_COLD_WHITE: - cold_white = int(data[index : index + 2], 16) - index += 2 - else: - cold_white = 0 - - # Color temperature. With the AEOTEC ZW098 bulb, only two color - # temperatures are supported. The warm and cold channel values - # indicate brightness for warm/cold color temperature. - if self._zw098: - if warm_white > 0: - self._ct = TEMP_WARM_HASS - self._color_mode = COLOR_MODE_COLOR_TEMP - elif cold_white > 0: - self._ct = TEMP_COLD_HASS - self._color_mode = COLOR_MODE_COLOR_TEMP - else: - self._color_mode = COLOR_MODE_RGB - - elif self._color_channels & COLOR_CHANNEL_WARM_WHITE: - self._white = warm_white - - elif self._color_channels & COLOR_CHANNEL_COLD_WHITE: - self._white = cold_white - - # If no rgb channels supported, report None. - if not ( - self._color_channels & COLOR_CHANNEL_RED - or self._color_channels & COLOR_CHANNEL_GREEN - or self._color_channels & COLOR_CHANNEL_BLUE - ): - self._rgb = None - - @property - def rgb_color(self): - """Return the rgb color.""" - return self._rgb - - @property - def rgbw_color(self): - """Return the rgbw color.""" - if self._rgb is None: - return None - return (*self._rgb, self._white) - - @property - def color_temp(self): - """Return the color temperature.""" - return self._ct - - def turn_on(self, **kwargs): - """Turn the device on.""" - rgbw = None - - if ATTR_COLOR_TEMP in kwargs: - # Color temperature. With the AEOTEC ZW098 bulb, only two color - # temperatures are supported. The warm and cold channel values - # indicate brightness for warm/cold color temperature. - if self._zw098: - self._color_mode = COLOR_MODE_COLOR_TEMP - if kwargs[ATTR_COLOR_TEMP] > TEMP_MID_HASS: - self._ct = TEMP_WARM_HASS - rgbw = "#000000ff00" - else: - self._ct = TEMP_COLD_HASS - rgbw = "#00000000ff" - elif ATTR_RGB_COLOR in kwargs: - self._rgb = kwargs[ATTR_RGB_COLOR] - self._white = 0 - elif ATTR_RGBW_COLOR in kwargs: - self._rgb = kwargs[ATTR_RGBW_COLOR][0:3] - self._white = kwargs[ATTR_RGBW_COLOR][3] - - if ATTR_RGB_COLOR in kwargs or ATTR_RGBW_COLOR in kwargs: - rgbw = "#" - for colorval in self._rgb: - rgbw += format(colorval, "02x") - if self._white is not None: - rgbw += format(self._white, "02x") + "00" - else: - rgbw += "0000" - - if rgbw and self.values.color: - self.values.color.data = rgbw - - super().turn_on(**kwargs) diff --git a/homeassistant/components/zwave/lock.py b/homeassistant/components/zwave/lock.py deleted file mode 100644 index 06ce59a1f9e..00000000000 --- a/homeassistant/components/zwave/lock.py +++ /dev/null @@ -1,390 +0,0 @@ -"""Support for Z-Wave door locks.""" -import logging - -import voluptuous as vol - -from homeassistant.components.lock import DOMAIN, LockEntity -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant, ServiceCall, callback -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity_platform import AddEntitiesCallback - -from . import ZWaveDeviceEntity, const - -_LOGGER = logging.getLogger(__name__) - -ATTR_NOTIFICATION = "notification" -ATTR_LOCK_STATUS = "lock_status" -ATTR_CODE_SLOT = "code_slot" -ATTR_USERCODE = "usercode" -CONFIG_ADVANCED = "Advanced" - -SERVICE_SET_USERCODE = "set_usercode" -SERVICE_GET_USERCODE = "get_usercode" -SERVICE_CLEAR_USERCODE = "clear_usercode" - -POLYCONTROL = 0x10E -DANALOCK_V2_BTZE = 0x2 -POLYCONTROL_DANALOCK_V2_BTZE_LOCK = (POLYCONTROL, DANALOCK_V2_BTZE) -WORKAROUND_V2BTZE = 1 -WORKAROUND_DEVICE_STATE = 2 -WORKAROUND_TRACK_MESSAGE = 4 -WORKAROUND_ALARM_TYPE = 8 - -DEVICE_MAPPINGS = { - POLYCONTROL_DANALOCK_V2_BTZE_LOCK: WORKAROUND_V2BTZE, - # Kwikset 914TRL ZW500 99100-078 - (0x0090, 0x440): WORKAROUND_DEVICE_STATE, - (0x0090, 0x446): WORKAROUND_DEVICE_STATE, - (0x0090, 0x238): WORKAROUND_DEVICE_STATE, - # Kwikset 888ZW500-15S Smartcode 888 - (0x0090, 0x541): WORKAROUND_DEVICE_STATE, - # Kwikset 916 - (0x0090, 0x0001): WORKAROUND_DEVICE_STATE, - # Kwikset Obsidian - (0x0090, 0x0742): WORKAROUND_DEVICE_STATE, - # Yale Locks - # Yale YRD210, YRD220, YRL220 - (0x0129, 0x0000): WORKAROUND_DEVICE_STATE | WORKAROUND_ALARM_TYPE, - # Yale YRD210, YRD220 - (0x0129, 0x0209): WORKAROUND_DEVICE_STATE | WORKAROUND_ALARM_TYPE, - # Yale YRL210, YRL220 - (0x0129, 0x0409): WORKAROUND_DEVICE_STATE | WORKAROUND_ALARM_TYPE, - # Yale YRD256 - (0x0129, 0x0600): WORKAROUND_DEVICE_STATE | WORKAROUND_ALARM_TYPE, - # Yale YRD110, YRD120 - (0x0129, 0x0800): WORKAROUND_DEVICE_STATE | WORKAROUND_ALARM_TYPE, - # Yale YRD446 - (0x0129, 0x1000): WORKAROUND_DEVICE_STATE | WORKAROUND_ALARM_TYPE, - # Yale YRL220 - (0x0129, 0x2132): WORKAROUND_DEVICE_STATE | WORKAROUND_ALARM_TYPE, - (0x0129, 0x3CAC): WORKAROUND_DEVICE_STATE | WORKAROUND_ALARM_TYPE, - # Yale YRD210, YRD220 - (0x0129, 0xAA00): WORKAROUND_DEVICE_STATE | WORKAROUND_ALARM_TYPE, - # Yale YRD220 - (0x0129, 0xFFFF): WORKAROUND_DEVICE_STATE | WORKAROUND_ALARM_TYPE, - # Yale YRL256 - (0x0129, 0x0F00): WORKAROUND_DEVICE_STATE | WORKAROUND_ALARM_TYPE, - # Yale YRD220 (Older Yale products with incorrect vendor ID) - (0x0109, 0x0000): WORKAROUND_DEVICE_STATE | WORKAROUND_ALARM_TYPE, - # Schlage BE469 - (0x003B, 0x5044): WORKAROUND_DEVICE_STATE | WORKAROUND_TRACK_MESSAGE, - # Schlage FE599NX - (0x003B, 0x504C): WORKAROUND_DEVICE_STATE, -} - -LOCK_NOTIFICATION = { - "1": "Manual Lock", - "2": "Manual Unlock", - "5": "Keypad Lock", - "6": "Keypad Unlock", - "11": "Lock Jammed", - "254": "Unknown Event", -} -NOTIFICATION_RF_LOCK = "3" -NOTIFICATION_RF_UNLOCK = "4" -LOCK_NOTIFICATION[NOTIFICATION_RF_LOCK] = "RF Lock" -LOCK_NOTIFICATION[NOTIFICATION_RF_UNLOCK] = "RF Unlock" - -LOCK_ALARM_TYPE = { - "9": "Deadbolt Jammed", - "16": "Unlocked by Bluetooth ", - "18": "Locked with Keypad by user ", - "19": "Unlocked with Keypad by user ", - "21": "Manually Locked ", - "22": "Manually Unlocked ", - "27": "Auto re-lock", - "33": "User deleted: ", - "112": "Master code changed or User added: ", - "113": "Duplicate PIN code: ", - "130": "RF module, power restored", - "144": "Unlocked by NFC Tag or Card by user ", - "161": "Tamper Alarm: ", - "167": "Low Battery", - "168": "Critical Battery Level", - "169": "Battery too low to operate", -} -ALARM_RF_LOCK = "24" -ALARM_RF_UNLOCK = "25" -LOCK_ALARM_TYPE[ALARM_RF_LOCK] = "Locked by RF" -LOCK_ALARM_TYPE[ALARM_RF_UNLOCK] = "Unlocked by RF" - -MANUAL_LOCK_ALARM_LEVEL = { - "1": "by Key Cylinder or Inside thumb turn", - "2": "by Touch function (lock and leave)", -} - -TAMPER_ALARM_LEVEL = {"1": "Too many keypresses", "2": "Cover removed"} - -LOCK_STATUS = { - "1": True, - "2": False, - "3": True, - "4": False, - "5": True, - "6": False, - "9": False, - "18": True, - "19": False, - "21": True, - "22": False, - "24": True, - "25": False, - "27": True, -} - -ALARM_TYPE_STD = ["18", "19", "33", "112", "113", "144"] - -SET_USERCODE_SCHEMA = vol.Schema( - { - vol.Required(const.ATTR_NODE_ID): vol.Coerce(int), - vol.Required(ATTR_CODE_SLOT): vol.Coerce(int), - vol.Required(ATTR_USERCODE): cv.string, - } -) - -GET_USERCODE_SCHEMA = vol.Schema( - { - vol.Required(const.ATTR_NODE_ID): vol.Coerce(int), - vol.Required(ATTR_CODE_SLOT): vol.Coerce(int), - } -) - -CLEAR_USERCODE_SCHEMA = vol.Schema( - { - vol.Required(const.ATTR_NODE_ID): vol.Coerce(int), - vol.Required(ATTR_CODE_SLOT): vol.Coerce(int), - } -) - - -async def async_setup_entry( - hass: HomeAssistant, - config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up Z-Wave Lock from Config Entry.""" - - @callback - def async_add_lock(lock): - """Add Z-Wave Lock.""" - async_add_entities([lock]) - - async_dispatcher_connect(hass, "zwave_new_lock", async_add_lock) - - network = hass.data[const.DATA_NETWORK] - - def set_usercode(service: ServiceCall) -> None: - """Set the usercode to index X on the lock.""" - node_id = service.data.get(const.ATTR_NODE_ID) - lock_node = network.nodes[node_id] - code_slot = service.data.get(ATTR_CODE_SLOT) - usercode = service.data.get(ATTR_USERCODE) - - for value in lock_node.get_values( - class_id=const.COMMAND_CLASS_USER_CODE - ).values(): - if value.index != code_slot: - continue - if len(str(usercode)) < 4: - _LOGGER.error( - "Invalid code provided: (%s) " - "usercode must be at least 4 and at most" - " %s digits", - usercode, - len(value.data), - ) - break - value.data = str(usercode) - break - - def get_usercode(service: ServiceCall) -> None: - """Get a usercode at index X on the lock.""" - node_id = service.data.get(const.ATTR_NODE_ID) - lock_node = network.nodes[node_id] - code_slot = service.data.get(ATTR_CODE_SLOT) - - for value in lock_node.get_values( - class_id=const.COMMAND_CLASS_USER_CODE - ).values(): - if value.index != code_slot: - continue - _LOGGER.info("Usercode at slot %s is: %s", value.index, value.data) - break - - def clear_usercode(service: ServiceCall) -> None: - """Set usercode to slot X on the lock.""" - node_id = service.data.get(const.ATTR_NODE_ID) - lock_node = network.nodes[node_id] - code_slot = service.data.get(ATTR_CODE_SLOT) - data = "" - - for value in lock_node.get_values( - class_id=const.COMMAND_CLASS_USER_CODE - ).values(): - if value.index != code_slot: - continue - for i in range(len(value.data)): - data += "\0" - i += 1 - _LOGGER.debug("Data to clear lock: %s", data) - value.data = data - _LOGGER.info("Usercode at slot %s is cleared", value.index) - break - - hass.services.async_register( - DOMAIN, SERVICE_SET_USERCODE, set_usercode, schema=SET_USERCODE_SCHEMA - ) - hass.services.async_register( - DOMAIN, SERVICE_GET_USERCODE, get_usercode, schema=GET_USERCODE_SCHEMA - ) - hass.services.async_register( - DOMAIN, SERVICE_CLEAR_USERCODE, clear_usercode, schema=CLEAR_USERCODE_SCHEMA - ) - - -def get_device(node, values, **kwargs): - """Create Z-Wave entity device.""" - return ZwaveLock(values) - - -class ZwaveLock(ZWaveDeviceEntity, LockEntity): - """Representation of a Z-Wave Lock.""" - - def __init__(self, values): - """Initialize the Z-Wave lock device.""" - ZWaveDeviceEntity.__init__(self, values, DOMAIN) - self._state = None - self._notification = None - self._lock_status = None - self._v2btze = None - self._state_workaround = False - self._track_message_workaround = False - self._previous_message = None - self._alarm_type_workaround = False - - # Enable appropriate workaround flags for our device - # Make sure that we have values for the key before converting to int - if self.node.manufacturer_id.strip() and self.node.product_id.strip(): - specific_sensor_key = ( - int(self.node.manufacturer_id, 16), - int(self.node.product_id, 16), - ) - if specific_sensor_key in DEVICE_MAPPINGS: - workaround = DEVICE_MAPPINGS[specific_sensor_key] - if workaround & WORKAROUND_V2BTZE: - self._v2btze = 1 - _LOGGER.debug("Polycontrol Danalock v2 BTZE workaround enabled") - if workaround & WORKAROUND_DEVICE_STATE: - self._state_workaround = True - _LOGGER.debug("Notification device state workaround enabled") - if workaround & WORKAROUND_TRACK_MESSAGE: - self._track_message_workaround = True - _LOGGER.debug("Message tracking workaround enabled") - if workaround & WORKAROUND_ALARM_TYPE: - self._alarm_type_workaround = True - _LOGGER.debug("Alarm Type device state workaround enabled") - self.update_properties() - - def update_properties(self): - """Handle data changes for node values.""" - self._state = self.values.primary.data - _LOGGER.debug("lock state set to %s", self._state) - if self.values.access_control: - notification_data = self.values.access_control.data - self._notification = LOCK_NOTIFICATION.get(str(notification_data)) - if self._state_workaround: - self._state = LOCK_STATUS.get(str(notification_data)) - _LOGGER.debug("workaround: lock state set to %s", self._state) - if ( - self._v2btze - and self.values.v2btze_advanced - and self.values.v2btze_advanced.data == CONFIG_ADVANCED - ): - self._state = LOCK_STATUS.get(str(notification_data)) - _LOGGER.debug( - "Lock state set from Access Control value and is %s, get=%s", - str(notification_data), - self.state, - ) - - if self._track_message_workaround: - this_message = self.node.stats["lastReceivedMessage"][5] - - if this_message == const.COMMAND_CLASS_DOOR_LOCK: - self._state = self.values.primary.data - _LOGGER.debug("set state to %s based on message tracking", self._state) - if self._previous_message == const.COMMAND_CLASS_DOOR_LOCK: - if self._state: - self._notification = LOCK_NOTIFICATION[NOTIFICATION_RF_LOCK] - self._lock_status = LOCK_ALARM_TYPE[ALARM_RF_LOCK] - else: - self._notification = LOCK_NOTIFICATION[NOTIFICATION_RF_UNLOCK] - self._lock_status = LOCK_ALARM_TYPE[ALARM_RF_UNLOCK] - return - - self._previous_message = this_message - - if not self.values.alarm_type: - return - - alarm_type = self.values.alarm_type.data - if self.values.alarm_level: - alarm_level = self.values.alarm_level.data - else: - alarm_level = None - - if not alarm_type: - return - - if self._alarm_type_workaround: - self._state = LOCK_STATUS.get(str(alarm_type)) - _LOGGER.debug( - "workaround: lock state set to %s -- alarm type: %s", - self._state, - str(alarm_type), - ) - - if alarm_type == 21: - self._lock_status = ( - f"{LOCK_ALARM_TYPE.get(str(alarm_type))}" - f"{MANUAL_LOCK_ALARM_LEVEL.get(str(alarm_level))}" - ) - return - if str(alarm_type) in ALARM_TYPE_STD: - self._lock_status = f"{LOCK_ALARM_TYPE.get(str(alarm_type))}{alarm_level}" - return - if alarm_type == 161: - self._lock_status = ( - f"{LOCK_ALARM_TYPE.get(str(alarm_type))}" - f"{TAMPER_ALARM_LEVEL.get(str(alarm_level))}" - ) - - return - if alarm_type != 0: - self._lock_status = LOCK_ALARM_TYPE.get(str(alarm_type)) - return - - @property - def is_locked(self): - """Return true if device is locked.""" - return self._state - - def lock(self, **kwargs): - """Lock the device.""" - self.values.primary.data = True - - def unlock(self, **kwargs): - """Unlock the device.""" - self.values.primary.data = False - - @property - def extra_state_attributes(self): - """Return the device specific state attributes.""" - data = super().extra_state_attributes - if self._notification: - data[ATTR_NOTIFICATION] = self._notification - if self._lock_status: - data[ATTR_LOCK_STATUS] = self._lock_status - return data diff --git a/homeassistant/components/zwave/manifest.json b/homeassistant/components/zwave/manifest.json deleted file mode 100644 index bf3a9abe77e..00000000000 --- a/homeassistant/components/zwave/manifest.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "domain": "zwave", - "name": "Z-Wave (deprecated)", - "config_flow": true, - "documentation": "https://www.home-assistant.io/integrations/zwave", - "requirements": ["homeassistant-pyozw==0.1.10", "pydispatcher==2.0.5"], - "codeowners": ["@home-assistant/z-wave"], - "iot_class": "local_push" -} diff --git a/homeassistant/components/zwave/migration.py b/homeassistant/components/zwave/migration.py deleted file mode 100644 index 0b151d18e4b..00000000000 --- a/homeassistant/components/zwave/migration.py +++ /dev/null @@ -1,167 +0,0 @@ -"""Handle migration from legacy Z-Wave to OpenZWave and Z-Wave JS.""" -from __future__ import annotations - -from typing import TYPE_CHECKING, TypedDict, cast - -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.device_registry import async_get as async_get_device_registry -from homeassistant.helpers.entity_registry import async_get as async_get_entity_registry -from homeassistant.helpers.singleton import singleton -from homeassistant.helpers.storage import Store - -from .const import DOMAIN -from .util import node_device_id_and_name - -if TYPE_CHECKING: - from . import ZWaveDeviceEntityValues - -LEGACY_ZWAVE_MIGRATION = f"{DOMAIN}_legacy_zwave_migration" -STORAGE_WRITE_DELAY = 30 -STORAGE_KEY = f"{DOMAIN}.legacy_zwave_migration" -STORAGE_VERSION = 1 - - -class ZWaveMigrationData(TypedDict): - """Represent the Z-Wave migration data dict.""" - - node_id: int - node_instance: int - command_class: int - command_class_label: str - value_index: int - device_id: str - domain: str - entity_id: str - unique_id: str - unit_of_measurement: str | None - - -@callback -def async_is_ozw_migrated(hass): - """Return True if migration to ozw is done.""" - ozw_config_entries = hass.config_entries.async_entries("ozw") - if not ozw_config_entries: - return False - - ozw_config_entry = ozw_config_entries[0] # only one ozw entry is allowed - migrated = bool(ozw_config_entry.data.get("migrated")) - return migrated - - -@callback -def async_is_zwave_js_migrated(hass): - """Return True if migration to Z-Wave JS is done.""" - zwave_js_config_entries = hass.config_entries.async_entries("zwave_js") - if not zwave_js_config_entries: - return False - - migrated = any( - config_entry.data.get("migrated") for config_entry in zwave_js_config_entries - ) - return migrated - - -async def async_add_migration_entity_value( - hass: HomeAssistant, - entity_id: str, - entity_values: ZWaveDeviceEntityValues, -) -> None: - """Add Z-Wave entity value for legacy Z-Wave migration.""" - migration_handler: LegacyZWaveMigration = await get_legacy_zwave_migration(hass) - migration_handler.add_entity_value(entity_id, entity_values) - - -async def async_get_migration_data( - hass: HomeAssistant, config_entry: ConfigEntry -) -> dict[str, ZWaveMigrationData]: - """Return Z-Wave migration data.""" - migration_handler: LegacyZWaveMigration = await get_legacy_zwave_migration(hass) - return await migration_handler.get_data(config_entry) - - -@singleton(LEGACY_ZWAVE_MIGRATION) -async def get_legacy_zwave_migration(hass: HomeAssistant) -> LegacyZWaveMigration: - """Return legacy Z-Wave migration handler.""" - migration_handler = LegacyZWaveMigration(hass) - await migration_handler.load_data() - return migration_handler - - -class LegacyZWaveMigration: - """Handle the migration from zwave to ozw and zwave_js.""" - - def __init__(self, hass: HomeAssistant) -> None: - """Set up migration instance.""" - self._hass = hass - self._store = Store(hass, STORAGE_VERSION, STORAGE_KEY) - self._data: dict[str, dict[str, ZWaveMigrationData]] = {} - - async def load_data(self) -> None: - """Load Z-Wave migration data.""" - stored = cast(dict, await self._store.async_load()) - if stored: - self._data = stored - - @callback - def save_data( - self, config_entry_id: str, entity_id: str, data: ZWaveMigrationData - ) -> None: - """Save Z-Wave migration data.""" - if config_entry_id not in self._data: - self._data[config_entry_id] = {} - self._data[config_entry_id][entity_id] = data - self._store.async_delay_save(self._data_to_save, STORAGE_WRITE_DELAY) - - @callback - def _data_to_save(self) -> dict[str, dict[str, ZWaveMigrationData]]: - """Return data to save.""" - return self._data - - @callback - def add_entity_value( - self, - entity_id: str, - entity_values: ZWaveDeviceEntityValues, - ) -> None: - """Add info for one entity and Z-Wave value.""" - ent_reg = async_get_entity_registry(self._hass) - dev_reg = async_get_device_registry(self._hass) - - node = entity_values.primary.node - entity_entry = ent_reg.async_get(entity_id) - assert entity_entry - device_identifier, _ = node_device_id_and_name( - node, entity_values.primary.instance - ) - device_entry = dev_reg.async_get_device({device_identifier}, set()) - assert device_entry - - # Normalize unit of measurement. - if unit := entity_entry.unit_of_measurement: - unit = unit.lower() - if unit == "": - unit = None - - data: ZWaveMigrationData = { - "node_id": node.node_id, - "node_instance": entity_values.primary.instance, - "command_class": entity_values.primary.command_class, - "command_class_label": entity_values.primary.label, - "value_index": entity_values.primary.index, - "device_id": device_entry.id, - "domain": entity_entry.domain, - "entity_id": entity_id, - "unique_id": entity_entry.unique_id, - "unit_of_measurement": unit, - } - - self.save_data(entity_entry.config_entry_id, entity_id, data) - - async def get_data( - self, config_entry: ConfigEntry - ) -> dict[str, ZWaveMigrationData]: - """Return Z-Wave migration data.""" - await self.load_data() - data = self._data.get(config_entry.entry_id) - return data or {} diff --git a/homeassistant/components/zwave/node_entity.py b/homeassistant/components/zwave/node_entity.py deleted file mode 100644 index ade68284313..00000000000 --- a/homeassistant/components/zwave/node_entity.py +++ /dev/null @@ -1,381 +0,0 @@ -"""Entity class that represents Z-Wave node.""" -# pylint: disable=import-error -# pylint: disable=import-outside-toplevel -from itertools import count - -from homeassistant.const import ( - ATTR_BATTERY_LEVEL, - ATTR_ENTITY_ID, - ATTR_VIA_DEVICE, - ATTR_WAKEUP, -) -from homeassistant.core import callback -from homeassistant.helpers.device_registry import async_get_registry as get_dev_reg -from homeassistant.helpers.entity import DeviceInfo, Entity -from homeassistant.helpers.entity_registry import async_get_registry - -from .const import ( - ATTR_BASIC_LEVEL, - ATTR_NODE_ID, - ATTR_SCENE_DATA, - ATTR_SCENE_ID, - COMMAND_CLASS_CENTRAL_SCENE, - COMMAND_CLASS_VERSION, - COMMAND_CLASS_WAKE_UP, - DOMAIN, - EVENT_NODE_EVENT, - EVENT_SCENE_ACTIVATED, -) -from .util import is_node_parsed, node_device_id_and_name, node_name - -ATTR_QUERY_STAGE = "query_stage" -ATTR_AWAKE = "is_awake" -ATTR_READY = "is_ready" -ATTR_FAILED = "is_failed" -ATTR_PRODUCT_NAME = "product_name" -ATTR_MANUFACTURER_NAME = "manufacturer_name" -ATTR_NODE_NAME = "node_name" -ATTR_APPLICATION_VERSION = "application_version" - -STAGE_COMPLETE = "Complete" - -_REQUIRED_ATTRIBUTES = [ - ATTR_QUERY_STAGE, - ATTR_AWAKE, - ATTR_READY, - ATTR_FAILED, - "is_info_received", - "max_baud_rate", - "is_zwave_plus", -] -_OPTIONAL_ATTRIBUTES = ["capabilities", "neighbors", "location"] -_COMM_ATTRIBUTES = [ - "sentCnt", - "sentFailed", - "retries", - "receivedCnt", - "receivedDups", - "receivedUnsolicited", - "sentTS", - "receivedTS", - "lastRequestRTT", - "averageRequestRTT", - "lastResponseRTT", - "averageResponseRTT", -] -ATTRIBUTES = _REQUIRED_ATTRIBUTES + _OPTIONAL_ATTRIBUTES - - -class ZWaveBaseEntity(Entity): - """Base class for Z-Wave Node and Value entities.""" - - def __init__(self): - """Initialize the base Z-Wave class.""" - self._update_scheduled = False - - def maybe_schedule_update(self): - """Maybe schedule state update. - - If value changed after device was created but before setup_platform - was called - skip updating state. - """ - if self.hass and not self._update_scheduled: - self.hass.add_job(self._schedule_update) - - @callback - def _schedule_update(self): - """Schedule delayed update.""" - if self._update_scheduled: - return - - @callback - def do_update(): - """Really update.""" - self.async_write_ha_state() - self._update_scheduled = False - - self._update_scheduled = True - self.hass.loop.call_later(0.1, do_update) - - def try_remove_and_add(self): - """Remove this entity and add it back.""" - - async def _async_remove_and_add(): - await self.async_remove(force_remove=True) - self.entity_id = None - await self.platform.async_add_entities([self]) - - if self.hass and self.platform: - self.hass.add_job(_async_remove_and_add) - - async def node_removed(self): - """Call when a node is removed from the Z-Wave network.""" - await self.async_remove(force_remove=True) - - registry = await async_get_registry(self.hass) - if self.entity_id not in registry.entities: - return - - registry.async_remove(self.entity_id) - - -class ZWaveNodeEntity(ZWaveBaseEntity): - """Representation of a Z-Wave node.""" - - def __init__(self, node, network): - """Initialize node.""" - super().__init__() - from openzwave.network import ZWaveNetwork - from pydispatch import dispatcher - - self._network = network - self.node = node - self.node_id = self.node.node_id - self._name = node_name(self.node) - self._product_name = node.product_name - self._manufacturer_name = node.manufacturer_name - self._unique_id = self._compute_unique_id() - self._application_version = None - self._attributes = {} - self.wakeup_interval = None - self.location = None - self.battery_level = None - dispatcher.connect( - self.network_node_value_added, ZWaveNetwork.SIGNAL_VALUE_ADDED - ) - dispatcher.connect(self.network_node_changed, ZWaveNetwork.SIGNAL_VALUE_CHANGED) - dispatcher.connect(self.network_node_changed, ZWaveNetwork.SIGNAL_NODE) - dispatcher.connect(self.network_node_changed, ZWaveNetwork.SIGNAL_NOTIFICATION) - dispatcher.connect(self.network_node_event, ZWaveNetwork.SIGNAL_NODE_EVENT) - dispatcher.connect( - self.network_scene_activated, ZWaveNetwork.SIGNAL_SCENE_EVENT - ) - - @property - def unique_id(self): - """Return unique ID of Z-wave node.""" - return self._unique_id - - @property - def device_info(self) -> DeviceInfo: - """Return device information.""" - identifier, name = node_device_id_and_name(self.node) - info = DeviceInfo( - identifiers={identifier}, - manufacturer=self.node.manufacturer_name, - model=self.node.product_name, - name=name, - ) - if self.node_id > 1: - info[ATTR_VIA_DEVICE] = (DOMAIN, 1) - return info - - def maybe_update_application_version(self, value): - """Update application version if value is a Command Class Version, Application Value.""" - if ( - value - and value.command_class == COMMAND_CLASS_VERSION - and value.label == "Application Version" - ): - self._application_version = value.data - - def network_node_value_added(self, node=None, value=None, args=None): - """Handle a added value to a none on the network.""" - if node and node.node_id != self.node_id: - return - if args is not None and "nodeId" in args and args["nodeId"] != self.node_id: - return - - self.maybe_update_application_version(value) - - def network_node_changed(self, node=None, value=None, args=None): - """Handle a changed node on the network.""" - if node and node.node_id != self.node_id: - return - if args is not None and "nodeId" in args and args["nodeId"] != self.node_id: - return - - # Process central scene activation - if value is not None and value.command_class == COMMAND_CLASS_CENTRAL_SCENE: - self.central_scene_activated(value.index, value.data) - - self.maybe_update_application_version(value) - - self.node_changed() - - def get_node_statistics(self): - """Retrieve statistics from the node.""" - return self._network.manager.getNodeStatistics( - self._network.home_id, self.node_id - ) - - def node_changed(self): - """Update node properties.""" - attributes = {} - stats = self.get_node_statistics() - for attr in ATTRIBUTES: - value = getattr(self.node, attr) - if attr in _REQUIRED_ATTRIBUTES or value: - attributes[attr] = value - - for attr in _COMM_ATTRIBUTES: - attributes[attr] = stats[attr] - - if self.node.can_wake_up(): - for value in self.node.get_values(COMMAND_CLASS_WAKE_UP).values(): - if value.index != 0: - continue - - self.wakeup_interval = value.data - break - else: - self.wakeup_interval = None - - self.battery_level = self.node.get_battery_level() - self._product_name = self.node.product_name - self._manufacturer_name = self.node.manufacturer_name - self._name = node_name(self.node) - self._attributes = attributes - - if not self._unique_id: - self._unique_id = self._compute_unique_id() - if self._unique_id: - # Node info parsed. Remove and re-add - self.try_remove_and_add() - - self.maybe_schedule_update() - - async def node_renamed(self, update_ids=False): - """Rename the node and update any IDs.""" - identifier, self._name = node_device_id_and_name(self.node) - # Set the name in the devices. If they're customised - # the customisation will not be stored as name and will stick. - dev_reg = await get_dev_reg(self.hass) - device = dev_reg.async_get_device(identifiers={identifier}) - dev_reg.async_update_device(device.id, name=self._name) - # update sub-devices too - for i in count(2): - identifier, new_name = node_device_id_and_name(self.node, i) - device = dev_reg.async_get_device(identifiers={identifier}) - if not device: - break - dev_reg.async_update_device(device.id, name=new_name) - - # Update entity ID. - if update_ids: - ent_reg = await async_get_registry(self.hass) - new_entity_id = ent_reg.async_generate_entity_id( - DOMAIN, self._name, self.platform.entities.keys() - {self.entity_id} - ) - if new_entity_id != self.entity_id: - # Don't change the name attribute, it will be None unless - # customised and if it's been customised, keep the - # customisation. - ent_reg.async_update_entity(self.entity_id, new_entity_id=new_entity_id) - return - # else for the above two ifs, update if not using update_entity - self.async_write_ha_state() - - def network_node_event(self, node, value): - """Handle a node activated event on the network.""" - if node.node_id == self.node.node_id: - self.node_event(value) - - def node_event(self, value): - """Handle a node activated event for this node.""" - if self.hass is None: - return - - self.hass.bus.fire( - EVENT_NODE_EVENT, - { - ATTR_ENTITY_ID: self.entity_id, - ATTR_NODE_ID: self.node.node_id, - ATTR_BASIC_LEVEL: value, - }, - ) - - def network_scene_activated(self, node, scene_id): - """Handle a scene activated event on the network.""" - if node.node_id == self.node.node_id: - self.scene_activated(scene_id) - - def scene_activated(self, scene_id): - """Handle an activated scene for this node.""" - if self.hass is None: - return - - self.hass.bus.fire( - EVENT_SCENE_ACTIVATED, - { - ATTR_ENTITY_ID: self.entity_id, - ATTR_NODE_ID: self.node.node_id, - ATTR_SCENE_ID: scene_id, - }, - ) - - def central_scene_activated(self, scene_id, scene_data): - """Handle an activated central scene for this node.""" - if self.hass is None: - return - - self.hass.bus.fire( - EVENT_SCENE_ACTIVATED, - { - ATTR_ENTITY_ID: self.entity_id, - ATTR_NODE_ID: self.node_id, - ATTR_SCENE_ID: scene_id, - ATTR_SCENE_DATA: scene_data, - }, - ) - - @property - def state(self): - """Return the state.""" - if ATTR_READY not in self._attributes: - return None - - if self._attributes[ATTR_FAILED]: - return "dead" - if self._attributes[ATTR_QUERY_STAGE] != "Complete": - return "initializing" - if not self._attributes[ATTR_AWAKE]: - return "sleeping" - if self._attributes[ATTR_READY]: - return "ready" - - return None - - @property - def should_poll(self): - """No polling needed.""" - return False - - @property - def name(self): - """Return the name of the device.""" - return self._name - - @property - def extra_state_attributes(self): - """Return the device specific state attributes.""" - attrs = { - ATTR_NODE_ID: self.node_id, - ATTR_NODE_NAME: self._name, - ATTR_MANUFACTURER_NAME: self._manufacturer_name, - ATTR_PRODUCT_NAME: self._product_name, - } - attrs.update(self._attributes) - if self.battery_level is not None: - attrs[ATTR_BATTERY_LEVEL] = self.battery_level - if self.wakeup_interval is not None: - attrs[ATTR_WAKEUP] = self.wakeup_interval - if self._application_version is not None: - attrs[ATTR_APPLICATION_VERSION] = self._application_version - - return attrs - - def _compute_unique_id(self): - if is_node_parsed(self.node) or self.node.is_ready: - return f"node-{self.node_id}" - return None diff --git a/homeassistant/components/zwave/sensor.py b/homeassistant/components/zwave/sensor.py deleted file mode 100644 index 1f32f8bb681..00000000000 --- a/homeassistant/components/zwave/sensor.py +++ /dev/null @@ -1,124 +0,0 @@ -"""Support for Z-Wave sensors.""" -from homeassistant.components.sensor import DOMAIN, SensorDeviceClass, SensorEntity -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity_platform import AddEntitiesCallback - -from . import ZWaveDeviceEntity, const - - -async def async_setup_entry( - hass: HomeAssistant, - config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up Z-Wave Sensor from Config Entry.""" - - @callback - def async_add_sensor(sensor): - """Add Z-Wave Sensor.""" - async_add_entities([sensor]) - - async_dispatcher_connect(hass, "zwave_new_sensor", async_add_sensor) - - -def get_device(node, values, **kwargs): - """Create Z-Wave entity device.""" - # Generic Device mappings - if values.primary.command_class == const.COMMAND_CLASS_BATTERY: - return ZWaveBatterySensor(values) - if node.has_command_class(const.COMMAND_CLASS_SENSOR_MULTILEVEL): - return ZWaveMultilevelSensor(values) - if ( - node.has_command_class(const.COMMAND_CLASS_METER) - and values.primary.type == const.TYPE_DECIMAL - ): - return ZWaveMultilevelSensor(values) - if node.has_command_class(const.COMMAND_CLASS_ALARM) or node.has_command_class( - const.COMMAND_CLASS_SENSOR_ALARM - ): - return ZWaveAlarmSensor(values) - return None - - -class ZWaveSensor(ZWaveDeviceEntity, SensorEntity): - """Representation of a Z-Wave sensor.""" - - def __init__(self, values): - """Initialize the sensor.""" - ZWaveDeviceEntity.__init__(self, values, DOMAIN) - self.update_properties() - - def update_properties(self): - """Handle the data changes for node values.""" - self._state = self.values.primary.data - self._units = self.values.primary.units - - @property - def force_update(self): - """Return force_update.""" - return True - - @property - def native_value(self): - """Return the state of the sensor.""" - return self._state - - @property - def native_unit_of_measurement(self): - """Return the unit of measurement the value is expressed in.""" - return self._units - - -class ZWaveMultilevelSensor(ZWaveSensor): - """Representation of a multi level sensor Z-Wave sensor.""" - - @property - def native_value(self): - """Return the state of the sensor.""" - if self._units in ("C", "F"): - return round(self._state, 1) - if isinstance(self._state, float): - return round(self._state, 2) - - return self._state - - @property - def device_class(self): - """Return the class of this device.""" - if self._units in ["C", "F"]: - return SensorDeviceClass.TEMPERATURE - return None - - @property - def native_unit_of_measurement(self): - """Return the unit the value is expressed in.""" - if self._units == "C": - return TEMP_CELSIUS - if self._units == "F": - return TEMP_FAHRENHEIT - return self._units - - -class ZWaveAlarmSensor(ZWaveSensor): - """Representation of a Z-Wave sensor that sends Alarm alerts. - - Examples include certain Multisensors that have motion and vibration - capabilities. Z-Wave defines various alarm types such as Smoke, Flood, - Burglar, CarbonMonoxide, etc. - - This wraps these alarms and allows you to use them to trigger things, etc. - - COMMAND_CLASS_ALARM is what we get here. - """ - - -class ZWaveBatterySensor(ZWaveSensor): - """Representation of Z-Wave device battery level.""" - - @property - def device_class(self): - """Return the class of this device.""" - return SensorDeviceClass.BATTERY diff --git a/homeassistant/components/zwave/services.yaml b/homeassistant/components/zwave/services.yaml deleted file mode 100644 index d3063ef5d43..00000000000 --- a/homeassistant/components/zwave/services.yaml +++ /dev/null @@ -1,411 +0,0 @@ -# Describes the format for available Z-Wave services - -change_association: - name: Change association - description: Change an association in the Z-Wave network. - fields: - association: - name: Association - description: Specify add or remove association - required: true - example: add - selector: - text: - node_id: - name: Node ID - description: Node id of the node to set association for. - required: true - selector: - number: - min: 1 - max: 255 - target_node_id: - name: Target node ID - description: Node id of the node to associate to. - required: true - selector: - number: - min: 1 - max: 255 - group: - name: Group - description: Group number to set association for. - required: true - selector: - number: - min: 1 - max: 5 - instance: - name: Instance - description: Instance of multichannel association. - default: 0 - selector: - number: - min: 0 - max: 255 - -add_node: - name: Add node - description: Add a new (unsecure) node to the Z-Wave network. Refer to OZW_Log.txt for progress. - -add_node_secure: - name: Add node secure - description: Add a new node to the Z-Wave network with secure communications. Secure network key must be set, this process will fallback to add_node (unsecure) for unsupported devices. Note that unsecure devices can't directly talk to secure devices. Refer to OZW_Log.txt for progress. - -cancel_command: - name: Cancel command - description: Cancel a running Z-Wave controller command. Use this to exit add_node, if you weren't going to use it but activated it. - -heal_network: - name: Heal network - description: Start a Z-Wave network heal. This might take a while and will slow down the Z-Wave network greatly while it is being processed. Refer to OZW_Log.txt for progress. - fields: - return_routes: - name: Return routes - description: Whether or not to update the return routes from the nodes to the controller. - default: false - selector: - boolean: - -heal_node: - name: Heal node - description: Start a Z-Wave node heal. Refer to OZW_Log.txt for progress. - fields: - return_routes: - name: Return routes - description: Whether or not to update the return routes from the node to the controller. - default: false - selector: - boolean: - -remove_node: - name: Remove node - description: Remove a node from the Z-Wave network. Refer to OZW_Log.txt for progress. - -remove_failed_node: - name: Remove failed node - description: This command will remove a failed node from the network. The node should be on the controller's failed nodes list, otherwise this command will fail. Refer to OZW_Log.txt for progress. - fields: - node_id: - name: Node ID - description: Node id of the device to remove. - required: true - selector: - number: - min: 1 - max: 255 - -replace_failed_node: - name: Replace failed node - description: Replace a failed node with another. If the node is not in the controller's failed nodes list, or the node responds, this command will fail. Refer to OZW_Log.txt for progress. - fields: - node_id: - name: Node ID - description: Node id of the device to replace. - required: true - selector: - number: - min: 1 - max: 255 - -set_config_parameter: - name: Set config parameter - description: Set a config parameter to a node on the Z-Wave network. - fields: - node_id: - name: Node ID - description: Node id of the device to set config parameter to. - required: true - selector: - number: - min: 1 - max: 255 - parameter: - name: Parameter - description: Parameter number to set. - required: true - selector: - number: - min: 1 - max: 255 - value: - name: Value - description: Value to set for parameter. (String value for list and bool parameters, integer for others). - required: true - selector: - text: - size: - name: Size - description: Set the size of the parameter value. Only needed if no parameters are available. - default: 2 - selector: - number: - min: 1 - max: 255 - -set_node_value: - name: Set node value - description: Set the value for a given value_id on a Z-Wave device. - fields: - node_id: - name: Node ID - description: Node id of the device to set the value on. - required: true - selector: - number: - min: 1 - max: 255 - value_id: - name: Value ID - description: Value id of the value to set (integer or string). - required: true - selector: - text: - value: - name: Value - description: Value to set (integer or string). - required: true - selector: - text: - -refresh_node_value: - name: Refresh node value - description: Refresh the value for a given value_id on a Z-Wave device. - fields: - node_id: - name: Node ID - description: Node id of the device to refresh value from. - required: true - selector: - number: - min: 1 - max: 255 - value_id: - name: Value ID - description: Value id of the value to refresh. - required: true - selector: - text: - -set_poll_intensity: - name: Set poll intensity - description: Set the polling interval to a nodes value - fields: - node_id: - name: Node ID - description: ID of the node to set polling to. - required: true - selector: - number: - min: 1 - max: 255 - value_id: - name: Value ID - description: ID of the value to set polling to. - example: 72037594255792737 - required: true - selector: - text: - poll_intensity: - name: Poll intensity - description: The intensity to poll, 0 = disabled, 1 = Every time through list, 2 = Every second time through list... - required: true - selector: - number: - min: 0 - max: 100 - -print_config_parameter: - name: Print configuration parameter - description: Prints a Z-Wave node config parameter value to log. - fields: - node_id: - name: Node ID - description: Node id of the device to print the parameter from. - required: true - selector: - number: - min: 1 - max: 255 - parameter: - name: Parameter - description: Parameter number to print. - required: true - selector: - number: - min: 1 - max: 255 - -print_node: - name: Print node - description: Print all information about z-wave node. - fields: - node_id: - name: Node ID - description: Node id of the device to print. - required: true - selector: - number: - min: 1 - max: 255 - -refresh_entity: - name: Refresh entity - description: Refresh zwave entity. - fields: - entity_id: - name: Entity - description: Name of the entity to refresh. - required: true - selector: - entity: - integration: zwave - -refresh_node: - name: Refresh node - description: Refresh zwave node. - fields: - node_id: - name: Node ID - description: ID of the node to refresh. - required: true - selector: - number: - min: 1 - max: 255 - -set_wakeup: - name: Set wakeup - description: Sets wake-up interval of a node. - fields: - node_id: - name: Node ID - description: Node id of the device to set the wake-up interval for. - required: true - selector: - number: - min: 1 - max: 255 - value: - name: Value - description: Value of the interval to set. - required: true - selector: - text: - -start_network: - name: Start network - description: Start the Z-Wave network. This might take a while, depending on how big your Z-Wave network is. - -stop_network: - name: Stop network - description: Stop the Z-Wave network, all updates into Home Assistant will stop. - -soft_reset: - name: Soft reset - description: This will reset the controller without removing its data. Use carefully because not all controllers support this. Refer to your controller's manual. - -test_network: - name: Test network - description: This will send test to nodes in the Z-Wave network. This will greatly slow down the Z-Wave network while it is being processed. Refer to OZW_Log.txt for progress. - -test_node: - name: Test node - description: This will send test messages to a node in the Z-Wave network. This could bring back dead nodes. - fields: - node_id: - name: Node ID - description: ID of the node to send test messages to. - required: true - selector: - number: - min: 1 - max: 255 - messages: - name: Messages - description: Amount of test messages to send. - default: 1 - selector: - number: - min: 1 - max: 100 - -rename_node: - name: Rename node - description: Set the name of a node. This will also affect the IDs of all entities in the node. - fields: - node_id: - name: Node ID - description: ID of the node to rename. - required: true - selector: - number: - min: 1 - max: 255 - update_ids: - name: Update IDs - description: Rename the entity IDs for entities of this node. - default: false - selector: - boolean: - name: - name: Name - description: New Name - required: true - example: "kitchen" - selector: - text: - -rename_value: - name: Rename value - description: Set the name of a node value. This will affect the ID of the value entity. Value IDs can be queried from /api/zwave/values/{node_id} - fields: - node_id: - name: Node ID - description: ID of the node to rename. - required: true - selector: - number: - min: 1 - max: 255 - value_id: - name: Value ID - description: ID of the value to rename. - example: 72037594255792737 - required: true - selector: - text: - update_ids: - name: Update IDs - description: Update the entity ID for this value's entity. - default: false - selector: - boolean: - name: - name: Name - description: New Name - example: "Luminosity" - required: true - selector: - text: - -reset_node_meters: - name: Reset node meters - description: Resets the meter counters of a node. - fields: - node_id: - name: Node ID - description: Node id of the device to reset meters for. - required: true - selector: - number: - min: 1 - max: 255 - instance: - name: Instance - description: Instance of association. - default: 1 - selector: - number: - min: 1 - max: 100 diff --git a/homeassistant/components/zwave/strings.json b/homeassistant/components/zwave/strings.json deleted file mode 100644 index 69401b171e2..00000000000 --- a/homeassistant/components/zwave/strings.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "config": { - "step": { - "user": { - "description": "This integration is no longer maintained. For new installations, use Z-Wave JS instead.\n\nSee https://www.home-assistant.io/docs/z-wave/installation/ for information on the configuration variables", - "data": { - "usb_path": "[%key:common::config_flow::data::usb_path%]", - "network_key": "Network Key (leave blank to auto-generate)" - } - } - }, - "error": { - "option_error": "Z-Wave validation failed. Is the path to the USB stick correct?" - }, - "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" - } - }, - "state": { - "query_stage": { - "initializing": "[%key:component::zwave::state::_::initializing%]", - "dead": "[%key:component::zwave::state::_::dead%]" - }, - "_": { - "initializing": "Initializing", - "dead": "Dead", - "sleeping": "Sleeping", - "ready": "Ready" - } - } -} diff --git a/homeassistant/components/zwave/switch.py b/homeassistant/components/zwave/switch.py deleted file mode 100644 index f7d3471e2ab..00000000000 --- a/homeassistant/components/zwave/switch.py +++ /dev/null @@ -1,64 +0,0 @@ -"""Support for Z-Wave switches.""" -import time - -from homeassistant.components.switch import DOMAIN, SwitchEntity -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity_platform import AddEntitiesCallback - -from . import ZWaveDeviceEntity, workaround - - -async def async_setup_entry( - hass: HomeAssistant, - config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up Z-Wave Switch from Config Entry.""" - - @callback - def async_add_switch(switch): - """Add Z-Wave Switch.""" - async_add_entities([switch]) - - async_dispatcher_connect(hass, "zwave_new_switch", async_add_switch) - - -def get_device(values, **kwargs): - """Create zwave entity device.""" - return ZwaveSwitch(values) - - -class ZwaveSwitch(ZWaveDeviceEntity, SwitchEntity): - """Representation of a Z-Wave switch.""" - - def __init__(self, values): - """Initialize the Z-Wave switch device.""" - ZWaveDeviceEntity.__init__(self, values, DOMAIN) - self.refresh_on_update = ( - workaround.get_device_mapping(values.primary) - == workaround.WORKAROUND_REFRESH_NODE_ON_UPDATE - ) - self.last_update = time.perf_counter() - self._state = self.values.primary.data - - def update_properties(self): - """Handle data changes for node values.""" - self._state = self.values.primary.data - if self.refresh_on_update and time.perf_counter() - self.last_update > 30: - self.last_update = time.perf_counter() - self.node.request_state() - - @property - def is_on(self): - """Return true if device is on.""" - return self._state - - def turn_on(self, **kwargs): - """Turn the device on.""" - self.node.set_switch(self.values.primary.value_id, True) - - def turn_off(self, **kwargs): - """Turn the device off.""" - self.node.set_switch(self.values.primary.value_id, False) diff --git a/homeassistant/components/zwave/translations/af.json b/homeassistant/components/zwave/translations/af.json deleted file mode 100644 index 155960b3884..00000000000 --- a/homeassistant/components/zwave/translations/af.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "state": { - "_": { - "dead": "Dood", - "initializing": "Inisialiseer", - "ready": "Gereed", - "sleeping": "Aan die slaap" - }, - "query_stage": { - "dead": "Dood ({query_stage})", - "initializing": "Inisialiseer ({query_stage})" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/ar.json b/homeassistant/components/zwave/translations/ar.json deleted file mode 100644 index 5dc1469d468..00000000000 --- a/homeassistant/components/zwave/translations/ar.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "state": { - "_": { - "dead": "\u0645\u0641\u0635\u0648\u0644", - "initializing": "\u0642\u064a\u062f \u0627\u0644\u0625\u0646\u0634\u0627\u0621", - "ready": "\u062c\u0627\u0647\u0632", - "sleeping": "\u0646\u0627\u0626\u0645" - }, - "query_stage": { - "dead": "\u0645\u0641\u0635\u0648\u0644 ({query_stage})", - "initializing": "\u0642\u064a\u062f \u0627\u0644\u0625\u0646\u0634\u0627\u0621 ( {query_stage} )" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/bg.json b/homeassistant/components/zwave/translations/bg.json deleted file mode 100644 index ae82e98d705..00000000000 --- a/homeassistant/components/zwave/translations/bg.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Z-Wave \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d" - }, - "error": { - "option_error": "\u0412\u0430\u043b\u0438\u0434\u0438\u0440\u0430\u043d\u0435\u0442\u043e \u043d\u0430 Z-Wave \u043d\u0435 \u0431\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e. \u041f\u0440\u0430\u0432\u0438\u043b\u0435\u043d \u043b\u0438 \u0435 \u043f\u044a\u0442\u044f\u0442 \u043a\u044a\u043c USB \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e?" - }, - "step": { - "user": { - "data": { - "network_key": "\u041c\u0440\u0435\u0436\u043e\u0432 \u043a\u043b\u044e\u0447 (\u043e\u0441\u0442\u0430\u0432\u0435\u0442\u0435 \u043f\u0440\u0430\u0437\u043d\u043e \u0437\u0430 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u043e \u0433\u0435\u043d\u0435\u0440\u0438\u0440\u0430\u043d\u0435)", - "usb_path": "USB \u043f\u044a\u0442" - }, - "description": "\u0412\u0438\u0436\u0442\u0435 https://www.home-assistant.io/docs/z-wave/installation/ \u0437\u0430 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f \u043e\u0442\u043d\u043e\u0441\u043d\u043e \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u043e\u043d\u043d\u0438\u0442\u0435 \u043f\u0440\u043e\u043c\u0435\u043d\u043b\u0438\u0432\u0438" - } - } - }, - "state": { - "_": { - "dead": "\u041c\u044a\u0440\u0442\u044a\u0432", - "initializing": "\u0418\u043d\u0438\u0446\u0438\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f", - "ready": "\u0413\u043e\u0442\u043e\u0432", - "sleeping": "\u0421\u043f\u044f\u0449" - }, - "query_stage": { - "dead": "\u041c\u044a\u0440\u0442\u044a\u0432 ({query_stage})", - "initializing": "\u0418\u043d\u0438\u0446\u0438\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f ( {query_stage} )" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/bs.json b/homeassistant/components/zwave/translations/bs.json deleted file mode 100644 index 8d58bad5606..00000000000 --- a/homeassistant/components/zwave/translations/bs.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "state": { - "_": { - "dead": "Mrtav", - "initializing": "Inicijalizacija", - "ready": "Spreman", - "sleeping": "Spava" - }, - "query_stage": { - "dead": "Mrtav ({query_stage})", - "initializing": "Inicijalizacija ( {query_stage} )" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/ca.json b/homeassistant/components/zwave/translations/ca.json deleted file mode 100644 index 4b9e8953b8a..00000000000 --- a/homeassistant/components/zwave/translations/ca.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "El dispositiu ja est\u00e0 configurat", - "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3." - }, - "error": { - "option_error": "Ha fallat la validaci\u00f3 de Z-Wave. \u00c9s correcta la ruta al port USB on hi ha connectat el dispositiu?" - }, - "step": { - "user": { - "data": { - "network_key": "Clau de xarxa (deixa-ho en blanc per generar-la autom\u00e0ticament)", - "usb_path": "Ruta del dispositiu USB" - }, - "description": "Aquesta integraci\u00f3 ja no s'actualitzar\u00e0. Utilitza Z-Wave JS per a instal\u00b7lacions noves.\n\nConsulta https://www.home-assistant.io/docs/z-wave/installation/ per a m\u00e9s informaci\u00f3 sobre les variables de configuraci\u00f3" - } - } - }, - "state": { - "_": { - "dead": "No disponible", - "initializing": "Inicialitzant", - "ready": "A punt", - "sleeping": "Dormint" - }, - "query_stage": { - "dead": "No disponible", - "initializing": "Inicialitzant" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/cs.json b/homeassistant/components/zwave/translations/cs.json deleted file mode 100644 index 320feafe08a..00000000000 --- a/homeassistant/components/zwave/translations/cs.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", - "single_instance_allowed": "Ji\u017e nastaveno. Je mo\u017en\u00e1 pouze jedin\u00e1 konfigurace." - }, - "error": { - "option_error": "Z-Wave ov\u011b\u0159en\u00ed se nezda\u0159ilo. Je cesta k USB za\u0159\u00edzen\u00ed spr\u00e1vn\u011b?" - }, - "step": { - "user": { - "data": { - "network_key": "S\u00ed\u0165ov\u00fd kl\u00ed\u010d (ponechte pr\u00e1zdn\u00e9 pro automatick\u00e9 generov\u00e1n\u00ed)", - "usb_path": "Cesta k USB za\u0159\u00edzen\u00ed" - }, - "description": "Viz https://www.home-assistant.io/docs/z-wave/installation/ pro informace o konfigura\u010dn\u00edch prom\u011bnn\u00fdch" - } - } - }, - "state": { - "_": { - "dead": "Nereaguje", - "initializing": "Inicializace", - "ready": "P\u0159ipraveno", - "sleeping": "\u00dasporn\u00fd re\u017eim" - }, - "query_stage": { - "dead": "Nereaguje", - "initializing": "Inicializace" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/cy.json b/homeassistant/components/zwave/translations/cy.json deleted file mode 100644 index 43860e1c1fd..00000000000 --- a/homeassistant/components/zwave/translations/cy.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "state": { - "_": { - "dead": "Marw", - "initializing": "Ymgychwyn", - "ready": "Barod", - "sleeping": "Cysgu" - }, - "query_stage": { - "dead": "Marw ({query_stage})", - "initializing": "Ymgychwyn ( {query_stage} )" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/da.json b/homeassistant/components/zwave/translations/da.json deleted file mode 100644 index 7bad51c4f8e..00000000000 --- a/homeassistant/components/zwave/translations/da.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Z-Wave er allerede konfigureret" - }, - "error": { - "option_error": "Z-Wave-validering mislykkedes. Er stien til USB-enhed korrekt?" - }, - "step": { - "user": { - "data": { - "network_key": "Netv\u00e6rksn\u00f8gle (efterlad blank for autogenerering)", - "usb_path": "Sti til USB-enhed" - }, - "description": "Se https://www.home-assistant.io/docs/z-wave/installation/ for oplysninger om konfigurationsvariabler" - } - } - }, - "state": { - "_": { - "dead": "D\u00f8d", - "initializing": "Initialiserer", - "ready": "Klar", - "sleeping": "Sover" - }, - "query_stage": { - "dead": "D\u00f8d ({query_stage})", - "initializing": "Initialiserer ( {query_stage} )" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/de.json b/homeassistant/components/zwave/translations/de.json deleted file mode 100644 index 9d432edd1e5..00000000000 --- a/homeassistant/components/zwave/translations/de.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Ger\u00e4t ist bereits konfiguriert", - "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." - }, - "error": { - "option_error": "Z-Wave-Validierung fehlgeschlagen. Ist der Pfad zum USB-Stick korrekt?" - }, - "step": { - "user": { - "data": { - "network_key": "Netzwerkschl\u00fcssel (leer lassen, um automatisch zu generieren)", - "usb_path": "USB-Ger\u00e4te-Pfad" - }, - "description": "Diese Integration wird nicht mehr gepflegt. Verwende bei Neuinstallationen stattdessen Z-Wave JS.\n\nSiehe https://www.home-assistant.io/docs/z-wave/installation/ f\u00fcr Informationen zu den Konfigurationsvariablen" - } - } - }, - "state": { - "_": { - "dead": "Nicht erreichbar", - "initializing": "Initialisierend", - "ready": "Bereit", - "sleeping": "Schlafend" - }, - "query_stage": { - "dead": "Nicht erreichbar ({query_stage})", - "initializing": "Initialisierend" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/el.json b/homeassistant/components/zwave/translations/el.json deleted file mode 100644 index ca7149b26b5..00000000000 --- a/homeassistant/components/zwave/translations/el.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", - "single_instance_allowed": "\u0388\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af. \u039c\u03cc\u03bd\u03bf \u03bc\u03af\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae." - }, - "error": { - "option_error": "\u0397 \u03b5\u03c0\u03b9\u03ba\u03cd\u03c1\u03c9\u03c3\u03b7 Z-Wave \u03b1\u03c0\u03ad\u03c4\u03c5\u03c7\u03b5. \u0395\u03af\u03bd\u03b1\u03b9 \u03c3\u03c9\u03c3\u03c4\u03ae \u03b7 \u03b4\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae \u03c0\u03c1\u03bf\u03c2 \u03c4\u03bf \u03c3\u03c4\u03b9\u03ba\u03ac\u03ba\u03b9 USB;" - }, - "step": { - "user": { - "data": { - "network_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af \u03b4\u03b9\u03ba\u03c4\u03cd\u03bf\u03c5 (\u03b1\u03c6\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf \u03ba\u03b5\u03bd\u03cc \u03b3\u03b9\u03b1 \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b7 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03af\u03b1)", - "usb_path": "\u0394\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 USB" - }, - "description": "\u0391\u03c5\u03c4\u03ae \u03b7 \u03b5\u03bd\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03b4\u03b5\u03bd \u03b4\u03b9\u03b1\u03c4\u03b7\u03c1\u03b5\u03af\u03c4\u03b1\u03b9 \u03c0\u03bb\u03ad\u03bf\u03bd. \u0393\u03b9\u03b1 \u03bd\u03ad\u03b5\u03c2 \u03b5\u03b3\u03ba\u03b1\u03c4\u03b1\u03c3\u03c4\u03ac\u03c3\u03b5\u03b9\u03c2, \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf Z-Wave JS. \n\n \u0394\u03b5\u03af\u03c4\u03b5 https://www.home-assistant.io/docs/z-wave/installation/ \u03b3\u03b9\u03b1 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 \u03c3\u03c7\u03b5\u03c4\u03b9\u03ba\u03ac \u03bc\u03b5 \u03c4\u03b9\u03c2 \u03bc\u03b5\u03c4\u03b1\u03b2\u03bb\u03b7\u03c4\u03ad\u03c2 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7\u03c2" - } - } - }, - "state": { - "_": { - "dead": "\u039d\u03b5\u03ba\u03c1\u03cc", - "initializing": "\u0391\u03c1\u03c7\u03b9\u03ba\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7", - "ready": "\u0388\u03c4\u03bf\u03b9\u03bc\u03bf", - "sleeping": "\u039a\u03bf\u03b9\u03bc\u03ac\u03c4\u03b1\u03b9" - }, - "query_stage": { - "dead": "\u039d\u03b5\u03ba\u03c1\u03cc ( {query_stage} )", - "initializing": "\u0391\u03c1\u03c7\u03b9\u03ba\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 ( {query_stage} )" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/en.json b/homeassistant/components/zwave/translations/en.json deleted file mode 100644 index 5c7442fea05..00000000000 --- a/homeassistant/components/zwave/translations/en.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Device is already configured", - "single_instance_allowed": "Already configured. Only a single configuration possible." - }, - "error": { - "option_error": "Z-Wave validation failed. Is the path to the USB stick correct?" - }, - "step": { - "user": { - "data": { - "network_key": "Network Key (leave blank to auto-generate)", - "usb_path": "USB Device Path" - }, - "description": "This integration is no longer maintained. For new installations, use Z-Wave JS instead.\n\nSee https://www.home-assistant.io/docs/z-wave/installation/ for information on the configuration variables" - } - } - }, - "state": { - "_": { - "dead": "Dead", - "initializing": "Initializing", - "ready": "Ready", - "sleeping": "Sleeping" - }, - "query_stage": { - "dead": "Dead", - "initializing": "Initializing" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/es-419.json b/homeassistant/components/zwave/translations/es-419.json deleted file mode 100644 index abcf85fffa1..00000000000 --- a/homeassistant/components/zwave/translations/es-419.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Z-Wave ya est\u00e1 configurado" - }, - "error": { - "option_error": "La validaci\u00f3n de Z-Wave fall\u00f3. \u00bfEs correcta la ruta a la memoria USB?" - }, - "step": { - "user": { - "data": { - "network_key": "Clave de red (dejar en blanco para auto-generar)", - "usb_path": "Ruta USB" - }, - "description": "Consulte https://www.home-assistant.io/docs/z-wave/installation/ para obtener informaci\u00f3n sobre las variables de configuraci\u00f3n" - } - } - }, - "state": { - "_": { - "dead": "Desconectado", - "initializing": "Iniciando", - "ready": "Listo", - "sleeping": "Hibernacion" - }, - "query_stage": { - "dead": "Desconectado ({query_stage})", - "initializing": "Iniciando ( {query_stage} )" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/es.json b/homeassistant/components/zwave/translations/es.json deleted file mode 100644 index 0d8b9a3020c..00000000000 --- a/homeassistant/components/zwave/translations/es.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Z-Wave ya est\u00e1 configurado", - "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." - }, - "error": { - "option_error": "Z-Wave error de validaci\u00f3n. \u00bfLa ruta de acceso a la memoria USB escorrecta?" - }, - "step": { - "user": { - "data": { - "network_key": "Clave de red (d\u00e9jelo en blanco para generar autom\u00e1ticamente)", - "usb_path": "Ruta del dispositivo USB" - }, - "description": "Consulta https://www.home-assistant.io/docs/z-wave/installation/ para obtener informaci\u00f3n sobre las variables de configuraci\u00f3n" - } - } - }, - "state": { - "_": { - "dead": "No responde", - "initializing": "Inicializando", - "ready": "Listo", - "sleeping": "Ahorro de energ\u00eda" - }, - "query_stage": { - "dead": "No responde", - "initializing": "Inicializando" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/et.json b/homeassistant/components/zwave/translations/et.json deleted file mode 100644 index 922126e0d86..00000000000 --- a/homeassistant/components/zwave/translations/et.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Seade on juba h\u00e4\u00e4lestatud", - "single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks seadistamine." - }, - "error": { - "option_error": "Z-Wave valideerimine nurjus. Kas USB-m\u00e4lupulga tee on \u00f5ige?" - }, - "step": { - "user": { - "data": { - "network_key": "V\u00f5rguv\u00f5ti (j\u00e4ta automaatse genereerimise jaoks t\u00fchjaks)", - "usb_path": "USB seadme rada" - }, - "description": "Seda sidumist enam ei hallata. Uueks sidumiseks kasuta Z-Wave JS.\n\nKonfiguratsioonimuutujate kohta leiad teavet https://www.home-assistant.io/docs/z-wave/installation/" - } - } - }, - "state": { - "_": { - "dead": "Surnud", - "initializing": "L\u00e4htestan", - "ready": "Valmis", - "sleeping": "Ootel" - }, - "query_stage": { - "dead": "Surnud", - "initializing": "L\u00e4htestan" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/eu.json b/homeassistant/components/zwave/translations/eu.json deleted file mode 100644 index ceab4ed0d98..00000000000 --- a/homeassistant/components/zwave/translations/eu.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "state": { - "_": { - "dead": "Hilda", - "initializing": "Hasieratzen", - "ready": "Prest", - "sleeping": "Lotan" - }, - "query_stage": { - "dead": "Ez du erantzuten ({query_stage})", - "initializing": "Hasieratzen ({query_stage})" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/fa.json b/homeassistant/components/zwave/translations/fa.json deleted file mode 100644 index 21d9a0c0fb7..00000000000 --- a/homeassistant/components/zwave/translations/fa.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "state": { - "_": { - "dead": "\u0645\u0631\u062f\u0647", - "initializing": "\u062f\u0631 \u062d\u0627\u0644 \u0622\u0645\u0627\u062f\u0647 \u0634\u062f\u0646", - "ready": "\u0622\u0645\u0627\u062f\u0647", - "sleeping": "\u062f\u0631 \u062d\u0627\u0644 \u062e\u0648\u0627\u0628" - }, - "query_stage": { - "dead": "\u0645\u0631\u062f\u0647 ({query_stage})", - "initializing": "\u062f\u0631 \u062d\u0627\u0644 \u0622\u0645\u0627\u062f\u0647 \u0634\u062f\u0646 ( {query_stage} )" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/fi.json b/homeassistant/components/zwave/translations/fi.json deleted file mode 100644 index 90fb77b49e1..00000000000 --- a/homeassistant/components/zwave/translations/fi.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "config": { - "step": { - "user": { - "data": { - "usb_path": "USB-polku" - } - } - } - }, - "state": { - "_": { - "dead": "Kuollut", - "initializing": "Alustaa", - "ready": "Valmis", - "sleeping": "Lepotilassa" - }, - "query_stage": { - "dead": "Kuollut ({query_stage})", - "initializing": "Alustaa ( {query_stage} )" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/fr.json b/homeassistant/components/zwave/translations/fr.json deleted file mode 100644 index 280d86e1537..00000000000 --- a/homeassistant/components/zwave/translations/fr.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", - "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." - }, - "error": { - "option_error": "La validation Z-Wave a \u00e9chou\u00e9. Le chemin d'acc\u00e8s \u00e0 la cl\u00e9 USB est-il correct?" - }, - "step": { - "user": { - "data": { - "network_key": "Cl\u00e9 r\u00e9seau (laisser vide pour g\u00e9n\u00e9rer automatiquement)", - "usb_path": "Chemin du p\u00e9riph\u00e9rique USB" - }, - "description": "Voir https://www.home-assistant.io/docs/z-wave/installation/ pour plus d'informations sur les variables de configuration." - } - } - }, - "state": { - "_": { - "dead": "Morte", - "initializing": "Initialisation", - "ready": "Pr\u00eat", - "sleeping": "En veille" - }, - "query_stage": { - "dead": "Morte", - "initializing": "Initialisation" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/gsw.json b/homeassistant/components/zwave/translations/gsw.json deleted file mode 100644 index fb704e97c7d..00000000000 --- a/homeassistant/components/zwave/translations/gsw.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "state": { - "_": { - "dead": "Tod", - "initializing": "Inizialisi\u00e4r\u00e4", - "ready": "Parat", - "sleeping": "Schlaf\u00e4" - }, - "query_stage": { - "dead": "Tod ({query_stage})", - "initializing": "Inizialisi\u00e4r\u00e4 ( {query_stage} )" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/he.json b/homeassistant/components/zwave/translations/he.json deleted file mode 100644 index 9cbf39a6d16..00000000000 --- a/homeassistant/components/zwave/translations/he.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", - "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." - }, - "step": { - "user": { - "data": { - "usb_path": "\u05e0\u05ea\u05d9\u05d1 \u05d4\u05ea\u05e7\u05df USB" - } - } - } - }, - "state": { - "_": { - "dead": "\u05de\u05ea", - "initializing": "\u05d0\u05ea\u05d7\u05d5\u05dc", - "ready": "\u05de\u05d5\u05db\u05df", - "sleeping": "\u05d9\u05e9\u05df" - }, - "query_stage": { - "dead": "\u05de\u05ea", - "initializing": "\u05d0\u05ea\u05d7\u05d5\u05dc" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/hi.json b/homeassistant/components/zwave/translations/hi.json deleted file mode 100644 index 99e98c4aa9f..00000000000 --- a/homeassistant/components/zwave/translations/hi.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "state": { - "_": { - "dead": "\u092e\u0943\u0924", - "initializing": "\u0906\u0930\u0902\u092d", - "ready": "\u0924\u0948\u092f\u093e\u0930", - "sleeping": "\u0938\u094b\u092f\u093e \u0939\u0941\u0906" - }, - "query_stage": { - "dead": " ( {query_stage} )", - "initializing": "\u0906\u0930\u0902\u092d ({query_stage})" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/hr.json b/homeassistant/components/zwave/translations/hr.json deleted file mode 100644 index dbff348b761..00000000000 --- a/homeassistant/components/zwave/translations/hr.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "state": { - "_": { - "dead": "Mrtav", - "initializing": "Inicijalizacija", - "ready": "Spreman", - "sleeping": "Spavanje" - }, - "query_stage": { - "dead": "Mrtav ({query_stage})", - "initializing": "Inicijalizacija ( {query_stage} )" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/hu.json b/homeassistant/components/zwave/translations/hu.json deleted file mode 100644 index 4d0c6adff59..00000000000 --- a/homeassistant/components/zwave/translations/hu.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", - "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." - }, - "error": { - "option_error": "A Z-Wave \u00e9rv\u00e9nyes\u00edt\u00e9s sikertelen. Az USB-meghajt\u00f3 el\u00e9r\u00e9si \u00fatj\u00e1t helyesen adtad meg?" - }, - "step": { - "user": { - "data": { - "network_key": "H\u00e1l\u00f3zati kulcs (hagyja \u00fcresen az automatikus gener\u00e1l\u00e1shoz)", - "usb_path": "USB eszk\u00f6z el\u00e9r\u00e9si \u00fat" - }, - "description": "Ezt az integr\u00e1ci\u00f3t m\u00e1r nem tartj\u00e1k fenn. \u00daj telep\u00edt\u00e9sek eset\u00e9n haszn\u00e1lja helyette a Z-Wave JS-t.\n\nA konfigur\u00e1ci\u00f3s v\u00e1ltoz\u00f3kkal kapcsolatos inform\u00e1ci\u00f3k\u00e9rt l\u00e1sd https://www.home-assistant.io/docs/z-wave/installation/." - } - } - }, - "state": { - "_": { - "dead": "Nem ad \u00e9letjelet", - "initializing": "Inicializ\u00e1l\u00e1s", - "ready": "K\u00e9sz", - "sleeping": "Alv\u00e1s" - }, - "query_stage": { - "dead": "Nem ad \u00e9letjelet", - "initializing": "Inicializ\u00e1l\u00e1s" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/hy.json b/homeassistant/components/zwave/translations/hy.json deleted file mode 100644 index c4fa19f700a..00000000000 --- a/homeassistant/components/zwave/translations/hy.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "state": { - "_": { - "dead": "\u0544\u0565\u057c\u0561\u056e", - "initializing": "\u0546\u0561\u056d\u0561\u0571\u0565\u057c\u0576\u0578\u0572", - "ready": "\u054a\u0561\u057f\u0580\u0561\u057d\u057f \u0567", - "sleeping": "\u0554\u0576\u0565\u056c" - }, - "query_stage": { - "dead": "\u0544\u0561\u0570\u0561\u0581\u0561\u056e{query_stage})", - "initializing": "\u0546\u0561\u056d\u0561\u0571\u0565\u057c\u0576\u0578\u0582\u0569\u0575\u0578\u0582\u0576({query_stage})" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/id.json b/homeassistant/components/zwave/translations/id.json deleted file mode 100644 index 91301f6e00e..00000000000 --- a/homeassistant/components/zwave/translations/id.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Perangkat sudah dikonfigurasi", - "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan." - }, - "error": { - "option_error": "Validasi Z-Wave gagal. Apakah jalur ke stik USB sudah benar?" - }, - "step": { - "user": { - "data": { - "network_key": "Kunci Jaringan (biarkan kosong untuk dibuat secara otomatis)", - "usb_path": "Jalur Perangkat USB" - }, - "description": "Integrasi ini tidak lagi dipertahankan. Untuk instalasi baru, gunakan Z-Wave JS sebagai gantinya.\n\nBaca https://www.home-assistant.io/docs/z-wave/installation/ untuk informasi tentang variabel konfigurasi" - } - } - }, - "state": { - "_": { - "dead": "Mati", - "initializing": "Inisialisasi", - "ready": "Siap", - "sleeping": "Tidur" - }, - "query_stage": { - "dead": "Mati", - "initializing": "Inisialisasi" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/is.json b/homeassistant/components/zwave/translations/is.json deleted file mode 100644 index bb54fd48425..00000000000 --- a/homeassistant/components/zwave/translations/is.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "state": { - "_": { - "dead": "Dau\u00f0ur", - "initializing": "Frumstilli", - "ready": "Tilb\u00fainn", - "sleeping": "\u00cd dvala" - }, - "query_stage": { - "dead": "Dau\u00f0ur ({query_stage})", - "initializing": "Frumstilli ({query_stage})" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/it.json b/homeassistant/components/zwave/translations/it.json deleted file mode 100644 index 17207c23b50..00000000000 --- a/homeassistant/components/zwave/translations/it.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", - "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione." - }, - "error": { - "option_error": "Convalida Z-Wave non riuscita. Il percorso della chiavetta USB \u00e8 corretto?" - }, - "step": { - "user": { - "data": { - "network_key": "Chiave di rete (lascia vuoto per generare automaticamente)", - "usb_path": "Percorso del dispositivo USB" - }, - "description": "Questa integrazione non viene pi\u00f9 mantenuta. Per le nuove installazioni, usa invece Z-Wave JS. \n\nVedere https://www.home-assistant.io/docs/z-wave/installation/ per informazioni sulle variabili di configurazione" - } - } - }, - "state": { - "_": { - "dead": "Disattivo", - "initializing": "In avvio", - "ready": "Pronto", - "sleeping": "Dormiente" - }, - "query_stage": { - "dead": "Disattivo", - "initializing": "In avvio" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/ja.json b/homeassistant/components/zwave/translations/ja.json deleted file mode 100644 index ff0afe58c0e..00000000000 --- a/homeassistant/components/zwave/translations/ja.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" - }, - "error": { - "option_error": "Z-Wave\u306e\u691c\u8a3c\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002USB\u30b9\u30c6\u30a3\u30c3\u30af\u3078\u306e\u30d1\u30b9\u306f\u6b63\u3057\u3044\u3067\u3059\u304b\uff1f" - }, - "step": { - "user": { - "data": { - "network_key": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u30ad\u30fc(\u7a7a\u767d\u306b\u3059\u308b\u3068\u81ea\u52d5\u751f\u6210\u3055\u308c\u307e\u3059)", - "usb_path": "USB\u30c7\u30d0\u30a4\u30b9\u306e\u30d1\u30b9" - }, - "description": "\u3053\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u30e1\u30f3\u30c6\u30ca\u30f3\u30b9\u306f\u7d42\u4e86\u3057\u307e\u3057\u305f\u3002\u65b0\u898f\u306b\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u3059\u308b\u5834\u5408\u306f\u3001\u4ee3\u308f\u308a\u306bZ-Wave JS\u3092\u4f7f\u7528\u3057\u3066\u304f\u3060\u3055\u3044\u3002\n\n\u69cb\u6210\u5909\u6570\u306e\u8a73\u7d30\u306b\u3064\u3044\u3066\u306f\u3001https://www.home-assistant.io/docs/z-wave/installation/ \u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002" - } - } - }, - "state": { - "_": { - "dead": "\u30c7\u30c3\u30c9", - "initializing": "\u521d\u671f\u5316\u4e2d", - "ready": "\u6e96\u5099\u5b8c\u4e86", - "sleeping": "\u30b9\u30ea\u30fc\u30d7" - }, - "query_stage": { - "dead": "\u30c7\u30c3\u30c9", - "initializing": "\u521d\u671f\u5316\u4e2d" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/ko.json b/homeassistant/components/zwave/translations/ko.json deleted file mode 100644 index 613b4108d22..00000000000 --- a/homeassistant/components/zwave/translations/ko.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\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": { - "option_error": "Z-Wave \uc720\ud6a8\uc131 \uac80\uc0ac\uc5d0 \uc2e4\ud328\ud588\uc2b5\ub2c8\ub2e4. USB \uc2a4\ud2f1\uc758 \uacbd\ub85c\uac00 \uc815\ud655\ud569\ub2c8\uae4c?" - }, - "step": { - "user": { - "data": { - "network_key": "\ub124\ud2b8\uc6cc\ud06c \ud0a4 (\uacf5\ub780\uc73c\ub85c \ube44\uc6cc\ub450\uba74 \uc790\ub3d9 \uc0dd\uc131\ud569\ub2c8\ub2e4)", - "usb_path": "USB \uc7a5\uce58 \uacbd\ub85c" - }, - "description": "\uc774 \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub294 \ub354 \uc774\uc0c1 \uc9c0\uc6d0\ub418\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4. \uc0c8\ub85c\uc6b4 \uc124\uce58\uc758 \uacbd\uc6b0 Z-Wave JS \ub97c \uc0ac\uc6a9\ud574\uc8fc\uc138\uc694.\n\n\uad6c\uc131 \ubcc0\uc218\uc5d0 \ub300\ud55c \uc815\ubcf4\ub294 https://www.home-assistant.io/docs/z-wave/installation/ \uc744 \ucc38\uc870\ud574\uc8fc\uc138\uc694" - } - } - }, - "state": { - "_": { - "dead": "\uc751\ub2f5\uc5c6\uc74c", - "initializing": "\ucd08\uae30\ud654\uc911", - "ready": "\uc900\ube44", - "sleeping": "\uc808\uc804\ubaa8\ub4dc" - }, - "query_stage": { - "dead": "\uc751\ub2f5\uc5c6\uc74c", - "initializing": "\ucd08\uae30\ud654\uc911" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/lb.json b/homeassistant/components/zwave/translations/lb.json deleted file mode 100644 index e41d37b2025..00000000000 --- a/homeassistant/components/zwave/translations/lb.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Apparat ass scho konfigur\u00e9iert", - "single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun m\u00e9iglech." - }, - "error": { - "option_error": "Z-Wave Validatioun net g\u00eblteg. Ass de Pad zum USB Stick richteg?" - }, - "step": { - "user": { - "data": { - "network_key": "Netzwierk Schl\u00ebssel (eidel loossen fir een automatesch z'erstellen)", - "usb_path": "Pad zum USB Apparat" - }, - "description": "Lies op https://www.home-assistant.io/docs/z-wave/installation/ fir weider Informatiounen iwwert d'Konfiguratioun vun den Variabelen" - } - } - }, - "state": { - "_": { - "dead": "Doud", - "initializing": "Initialis\u00e9iert", - "ready": "Bereet", - "sleeping": "Schl\u00e9ift" - }, - "query_stage": { - "dead": "Doud", - "initializing": "Initialis\u00e9iert" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/lt.json b/homeassistant/components/zwave/translations/lt.json deleted file mode 100644 index a390b260a03..00000000000 --- a/homeassistant/components/zwave/translations/lt.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "state": { - "query_stage": { - "dead": " ({query_stage})", - "initializing": " ( {query_stage} )" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/lv.json b/homeassistant/components/zwave/translations/lv.json deleted file mode 100644 index d759c7a9213..00000000000 --- a/homeassistant/components/zwave/translations/lv.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "state": { - "_": { - "dead": "Beigta", - "initializing": "Inicializ\u0113", - "ready": "Gatavs", - "sleeping": "Gu\u013c" - }, - "query_stage": { - "dead": "Beigta ({query_stage})", - "initializing": "Inicializ\u0113 ({query_stage})" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/nb.json b/homeassistant/components/zwave/translations/nb.json deleted file mode 100644 index 9dcd1e82788..00000000000 --- a/homeassistant/components/zwave/translations/nb.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "state": { - "_": { - "dead": "D\u00f8d", - "initializing": "Initialiserer", - "ready": "Klar", - "sleeping": "Sover" - }, - "query_stage": { - "dead": "D\u00f8d ({query_stage})", - "initializing": "Initialiserer ({query_stage})" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/nl.json b/homeassistant/components/zwave/translations/nl.json deleted file mode 100644 index d8c58fe784c..00000000000 --- a/homeassistant/components/zwave/translations/nl.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Apparaat is al geconfigureerd", - "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk." - }, - "error": { - "option_error": "Z-Wave-validatie mislukt. Is het pad naar de USB-stick correct?" - }, - "step": { - "user": { - "data": { - "network_key": "Netwerksleutel (laat leeg om automatisch te genereren)", - "usb_path": "USB-apparaatpad" - }, - "description": "Deze integratie wordt niet langer onderhouden. Voor nieuwe installaties, gebruik Z-Wave JS in plaats daarvan.\n\nZie https://www.home-assistant.io/docs/z-wave/installation/ voor informatie over de configuratievariabelen" - } - } - }, - "state": { - "_": { - "dead": "Onbereikbaar", - "initializing": "Initialiseren", - "ready": "Gereed", - "sleeping": "Slaapt" - }, - "query_stage": { - "dead": "Onbereikbaar", - "initializing": "Initialiseren" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/nn.json b/homeassistant/components/zwave/translations/nn.json deleted file mode 100644 index 76ff6120d81..00000000000 --- a/homeassistant/components/zwave/translations/nn.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "config": { - "step": { - "user": { - "description": "Sj\u00e5 [www.home-assistant.io/docs/z-wave/installation/](https://www.home-assistant.io/docs/z-wave/installation/) for informasjon om konfigurasjonsvariablene." - } - } - }, - "state": { - "_": { - "dead": "D\u00f8d", - "initializing": "Initialiserer", - "ready": "Klar", - "sleeping": "S\u00f8v" - }, - "query_stage": { - "dead": "D\u00f8d ({query_stage})", - "initializing": "Initialiserer ({query_stage})" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/no.json b/homeassistant/components/zwave/translations/no.json deleted file mode 100644 index 8582f906b57..00000000000 --- a/homeassistant/components/zwave/translations/no.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Enheten er allerede konfigurert", - "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig." - }, - "error": { - "option_error": "Z-Wave-validering mislyktes. Er banen til USB dongel riktig?" - }, - "step": { - "user": { - "data": { - "network_key": "Nettverksn\u00f8kkel (la v\u00e6re tom for automatisk oppretting)", - "usb_path": "USB enhetsbane" - }, - "description": "Denne integrasjonen opprettholdes ikke lenger. For nye installasjoner, bruk Z-Wave JS i stedet. \n\n Se https://www.home-assistant.io/docs/z-wave/installation/ for informasjon om konfigurasjonsvariablene" - } - } - }, - "state": { - "_": { - "dead": "D\u00f8d", - "initializing": "Initialiserer", - "ready": "Klar", - "sleeping": "Sover" - }, - "query_stage": { - "dead": "D\u00f8d", - "initializing": "Initialiserer" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/pl.json b/homeassistant/components/zwave/translations/pl.json deleted file mode 100644 index 9706fb1721f..00000000000 --- a/homeassistant/components/zwave/translations/pl.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", - "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja." - }, - "error": { - "option_error": "Walidacja Z-Wave si\u0119 nie powiod\u0142a. Czy \u015bcie\u017cka do kontrolera Z-Wave USB jest prawid\u0142owa?" - }, - "step": { - "user": { - "data": { - "network_key": "Klucz sieciowy (pozostaw pusty, by generowa\u0107 automatycznie)", - "usb_path": "\u015acie\u017cka urz\u0105dzenia USB" - }, - "description": "Ta integracja nie jest ju\u017c wspierana. Dla nowych instalacji, u\u017cyj Z-Wave JS.\n\nPrzejd\u017a na https://www.home-assistant.io/docs/z-wave/installation/, aby uzyska\u0107 informacje na temat zmiennych konfiguracyjnych" - } - } - }, - "state": { - "_": { - "dead": "martwy", - "initializing": "inicjalizacja", - "ready": "gotowy", - "sleeping": "u\u015bpiony" - }, - "query_stage": { - "dead": "martwy", - "initializing": "inicjalizacja" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/pt-BR.json b/homeassistant/components/zwave/translations/pt-BR.json deleted file mode 100644 index 079f1ab8593..00000000000 --- a/homeassistant/components/zwave/translations/pt-BR.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", - "single_instance_allowed": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel." - }, - "error": { - "option_error": "A valida\u00e7\u00e3o Z-Wave falhou. O caminho para o USB est\u00e1 correto?" - }, - "step": { - "user": { - "data": { - "network_key": "Chave de rede (deixe em branco para gerar automaticamente)", - "usb_path": "Caminho do Dispositivo USB" - }, - "description": "Consulte https://www.home-assistant.io/docs/z-wave/installation/ para obter informa\u00e7\u00f5es sobre as vari\u00e1veis de configura\u00e7\u00e3o" - } - } - }, - "state": { - "_": { - "dead": "Morto", - "initializing": "Iniciando", - "ready": "Pronto", - "sleeping": "Dormindo" - }, - "query_stage": { - "dead": "Morto", - "initializing": "Iniciando" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/pt.json b/homeassistant/components/zwave/translations/pt.json deleted file mode 100644 index 27fc303f08b..00000000000 --- a/homeassistant/components/zwave/translations/pt.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "O Z-Wave j\u00e1 est\u00e1 configurado", - "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." - }, - "error": { - "option_error": "A valida\u00e7\u00e3o Z-Wave falhou. O caminho para o dispositivo USB est\u00e1 correto?" - }, - "step": { - "user": { - "data": { - "network_key": "Network Key (deixe em branco para auto-gera\u00e7\u00e3o)", - "usb_path": "Endere\u00e7o USB" - }, - "description": "Consulte https://www.home-assistant.io/docs/z-wave/installation/ para obter informa\u00e7\u00f5es sobre as vari\u00e1veis de configura\u00e7\u00e3o" - } - } - }, - "state": { - "_": { - "dead": "Morto", - "initializing": "A inicializar", - "ready": "Pronto", - "sleeping": "Adormecido" - }, - "query_stage": { - "dead": "Morto ({query_stage})", - "initializing": "A inicializar ({query_stage})" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/ro.json b/homeassistant/components/zwave/translations/ro.json deleted file mode 100644 index de199bea03b..00000000000 --- a/homeassistant/components/zwave/translations/ro.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Z-Wave este deja configurat" - }, - "error": { - "option_error": "Validarea Z-Wave a e\u0219uat. Este corect\u0103 calea c\u0103tre stick-ul USB?" - }, - "step": { - "user": { - "data": { - "network_key": "Cheie de re\u021bea (l\u0103sa\u021bi necompletat pentru a genera automat)", - "usb_path": "Cale USB" - }, - "description": "Vede\u021bi https://www.home-assistant.io/docs/z-wave/installation/ pentru informa\u021bii despre variabilele de configurare" - } - } - }, - "state": { - "_": { - "dead": "Inactiv", - "initializing": "Se ini\u021bializeaz\u0103", - "ready": "Disponibil", - "sleeping": "Adormit" - }, - "query_stage": { - "dead": "Inactiv ({query_stage})", - "initializing": "Se ini\u021bializeaz\u0103 ({query_stage})" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/ru.json b/homeassistant/components/zwave/translations/ru.json deleted file mode 100644 index 7b7f0c6733d..00000000000 --- a/homeassistant/components/zwave/translations/ru.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "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.", - "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": { - "option_error": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438 Z-Wave. \u041f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u043f\u0443\u0442\u044c \u043a USB-\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443." - }, - "step": { - "user": { - "data": { - "network_key": "\u041a\u043b\u044e\u0447 \u0441\u0435\u0442\u0438 (\u043e\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u043f\u0443\u0441\u0442\u044b\u043c \u0434\u043b\u044f \u0430\u0432\u0442\u043e\u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438)", - "usb_path": "\u041f\u0443\u0442\u044c \u043a USB-\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443" - }, - "description": "\u042d\u0442\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f. \u0420\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0443\u0435\u0442\u0441\u044f \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0432\u043c\u0435\u0441\u0442\u043e \u043d\u0435\u0451 Z-Wave JS.\n\n\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](https://www.home-assistant.io/docs/z-wave/installation/) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0435 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430." - } - } - }, - "state": { - "_": { - "dead": "\u041d\u0435\u0438\u0441\u043f\u0440\u0430\u0432\u043d\u043e", - "initializing": "\u0418\u043d\u0438\u0446\u0438\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f", - "ready": "\u0413\u043e\u0442\u043e\u0432", - "sleeping": "\u0420\u0435\u0436\u0438\u043c \u0441\u043d\u0430" - }, - "query_stage": { - "dead": "\u041d\u0435\u0438\u0441\u043f\u0440\u0430\u0432\u043d\u043e", - "initializing": "\u0418\u043d\u0438\u0446\u0438\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/sk.json b/homeassistant/components/zwave/translations/sk.json deleted file mode 100644 index 9819295ee1f..00000000000 --- a/homeassistant/components/zwave/translations/sk.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" - } - }, - "state": { - "_": { - "dead": "Nereaguje", - "initializing": "Inicializ\u00e1cia", - "ready": "Pripraven\u00e9", - "sleeping": "\u00dasporn\u00fd re\u017eim" - }, - "query_stage": { - "dead": "Nereaguje ({query_stage})", - "initializing": "Inicializ\u00e1cia ( {query_stage} )" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/sl.json b/homeassistant/components/zwave/translations/sl.json deleted file mode 100644 index 38e70c59652..00000000000 --- a/homeassistant/components/zwave/translations/sl.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Z-Wave je \u017ee konfiguriran" - }, - "error": { - "option_error": "Potrjevanje Z-Wave ni uspelo. Ali je pot do USB klju\u010da pravilna?" - }, - "step": { - "user": { - "data": { - "network_key": "Omre\u017eni klju\u010d (pustite prazno za samodejno generiranje)", - "usb_path": "USB Pot" - }, - "description": "Za informacije o konfiguracijskih spremenljivka si oglejte https://www.home-assistant.io/docs/z-wave/installation/" - } - } - }, - "state": { - "_": { - "dead": "Mrtva", - "initializing": "Inicializacija", - "ready": "Pripravljen", - "sleeping": "Spi" - }, - "query_stage": { - "dead": "Mrtva", - "initializing": "Inicializacija" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/sr-Latn.json b/homeassistant/components/zwave/translations/sr-Latn.json deleted file mode 100644 index a390b260a03..00000000000 --- a/homeassistant/components/zwave/translations/sr-Latn.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "state": { - "query_stage": { - "dead": " ({query_stage})", - "initializing": " ( {query_stage} )" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/sr.json b/homeassistant/components/zwave/translations/sr.json deleted file mode 100644 index 00727fbb694..00000000000 --- a/homeassistant/components/zwave/translations/sr.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "state": { - "_": { - "ready": "Spreman" - }, - "query_stage": { - "dead": " ({query_stage})", - "initializing": " ( {query_stage} )" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/sv.json b/homeassistant/components/zwave/translations/sv.json deleted file mode 100644 index 6d3af30a057..00000000000 --- a/homeassistant/components/zwave/translations/sv.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Z-Wave \u00e4r redan konfigurerat" - }, - "error": { - "option_error": "Z-Wave-valideringen misslyckades. \u00c4r s\u00f6kv\u00e4gen till USB-minnet korrekt?" - }, - "step": { - "user": { - "data": { - "network_key": "N\u00e4tverksnyckel (l\u00e4mna blank f\u00f6r automatisk generering)", - "usb_path": "USB-s\u00f6kv\u00e4g" - }, - "description": "Se https://www.home-assistant.io/docs/z-wave/installation/ f\u00f6r information om konfigurationsvariabler" - } - } - }, - "state": { - "_": { - "dead": "D\u00f6d", - "initializing": "Initierar", - "ready": "Redo", - "sleeping": "Sovande" - }, - "query_stage": { - "dead": "D\u00f6d ({query_stage})", - "initializing": "Initierar ({query_stage})" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/ta.json b/homeassistant/components/zwave/translations/ta.json deleted file mode 100644 index 9b4fa65530c..00000000000 --- a/homeassistant/components/zwave/translations/ta.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "state": { - "_": { - "dead": "\u0b87\u0bb1\u0ba8\u0bcd\u0ba4\u0bc1\u0bb5\u0bbf\u0b9f\u0bcd\u0b9f\u0ba4\u0bc1", - "initializing": "\u0ba4\u0bc1\u0bb5\u0b95\u0bcd\u0b95\u0bc1\u0b95\u0bbf\u0bb1\u0ba4\u0bc1", - "ready": "\u0ba4\u0baf\u0bbe\u0bb0\u0bcd", - "sleeping": "\u0ba4\u0bc2\u0b99\u0bcd\u0b95\u0bc1\u0b95\u0bbf\u0ba9\u0bcd\u0bb1\u0ba4\u0bc1" - }, - "query_stage": { - "dead": "\u0b87\u0bb1\u0ba8\u0bcd\u0ba4\u0bc1\u0bb5\u0bbf\u0b9f\u0bcd\u0b9f\u0ba4\u0bc1 ({query_stage})", - "initializing": "\u0ba4\u0bc1\u0bb5\u0b95\u0bcd\u0b95\u0bc1\u0b95\u0bbf\u0bb1\u0ba4\u0bc1 ( {query_stage} )" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/te.json b/homeassistant/components/zwave/translations/te.json deleted file mode 100644 index 88e4eac6961..00000000000 --- a/homeassistant/components/zwave/translations/te.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "state": { - "_": { - "dead": "\u0c2e\u0c43\u0c24 \u0c2a\u0c30\u0c3f\u0c15\u0c30\u0c02", - "initializing": "\u0c38\u0c3f\u0c26\u0c4d\u0c27\u0c02 \u0c05\u0c35\u0c41\u0c24\u0c4b\u0c02\u0c26\u0c3f", - "ready": "\u0c30\u0c46\u0c21\u0c40", - "sleeping": "\u0c28\u0c3f\u0c26\u0c4d\u0c30\u0c3f\u0c38\u0c4d\u0c24\u0c4b\u0c02\u0c26\u0c3f" - }, - "query_stage": { - "dead": "\u0c2e\u0c43\u0c24 \u0c2a\u0c30\u0c3f\u0c15\u0c30\u0c02 ({query_stage})", - "initializing": "\u0c38\u0c3f\u0c26\u0c4d\u0c27\u0c02 \u0c05\u0c35\u0c41\u0c24\u0c4b\u0c02\u0c26\u0c3f ( {query_stage} )" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/th.json b/homeassistant/components/zwave/translations/th.json deleted file mode 100644 index 51db4f5b2e1..00000000000 --- a/homeassistant/components/zwave/translations/th.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "state": { - "_": { - "dead": "\u0e44\u0e21\u0e48\u0e1e\u0e23\u0e49\u0e2d\u0e21\u0e43\u0e0a\u0e49\u0e07\u0e32\u0e19", - "initializing": "\u0e01\u0e33\u0e25\u0e31\u0e07\u0e40\u0e23\u0e34\u0e48\u0e21\u0e15\u0e49\u0e19", - "ready": "\u0e1e\u0e23\u0e49\u0e2d\u0e21\u0e43\u0e0a\u0e49\u0e07\u0e32\u0e19", - "sleeping": "\u0e01\u0e33\u0e25\u0e31\u0e07\u0e2b\u0e25\u0e31\u0e1a" - }, - "query_stage": { - "dead": "\u0e44\u0e21\u0e48\u0e1e\u0e23\u0e49\u0e2d\u0e21\u0e43\u0e0a\u0e49\u0e07\u0e32\u0e19 ({query_stage})", - "initializing": "\u0e01\u0e33\u0e25\u0e31\u0e07\u0e40\u0e23\u0e34\u0e48\u0e21\u0e15\u0e49\u0e19 ( {query_stage} )" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/tr.json b/homeassistant/components/zwave/translations/tr.json deleted file mode 100644 index b6afa368b6d..00000000000 --- a/homeassistant/components/zwave/translations/tr.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", - "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." - }, - "error": { - "option_error": "Z-Wave do\u011frulamas\u0131 ba\u015far\u0131s\u0131z oldu. USB stickin yolu do\u011fru mu?" - }, - "step": { - "user": { - "data": { - "network_key": "A\u011f Anajtar\u0131 (otomatik \u00fcretilmesi i\u00e7in bo\u015f b\u0131rak\u0131n\u0131z)", - "usb_path": "USB Cihaz Yolu" - }, - "description": "Bu entegrasyon art\u0131k korunmuyor. Yeni kurulumlar i\u00e7in bunun yerine Z-Wave JS kullan\u0131n. \n\n Yap\u0131land\u0131rma de\u011fi\u015fkenleri hakk\u0131nda bilgi i\u00e7in https://www.home-assistant.io/docs/z-wave/installation/ adresine bak\u0131n." - } - } - }, - "state": { - "_": { - "dead": "\u00d6l\u00fc", - "initializing": "Ba\u015flat\u0131l\u0131yor", - "ready": "Haz\u0131r", - "sleeping": "Uyuyor" - }, - "query_stage": { - "dead": "\u00d6l\u00fc", - "initializing": "Ba\u015flat\u0131l\u0131yor" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/uk.json b/homeassistant/components/zwave/translations/uk.json deleted file mode 100644 index 3c5b49681c8..00000000000 --- a/homeassistant/components/zwave/translations/uk.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "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.", - "single_instance_allowed": "\u0412\u0436\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u043e. \u041c\u043e\u0436\u043b\u0438\u0432\u0430 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044f." - }, - "error": { - "option_error": "\u041f\u043e\u043c\u0438\u043b\u043a\u0430 \u043f\u0435\u0440\u0435\u0432\u0456\u0440\u043a\u0438 Z-Wave. \u041f\u0435\u0440\u0435\u0432\u0456\u0440\u0442\u0435 \u0448\u043b\u044f\u0445 \u0434\u043e USB-\u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e." - }, - "step": { - "user": { - "data": { - "network_key": "\u041a\u043b\u044e\u0447 \u043c\u0435\u0440\u0435\u0436\u0456 (\u0437\u0430\u043b\u0438\u0448\u0442\u0435 \u043f\u043e\u0440\u043e\u0436\u043d\u0456\u043c \u0434\u043b\u044f \u0430\u0432\u0442\u043e\u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438)", - "usb_path": "\u0428\u043b\u044f\u0445 \u0434\u043e USB-\u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e" - }, - "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](https://www.home-assistant.io/docs/z-wave/installation/) \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0431\u0456\u043b\u044c\u0448 \u0434\u043e\u043a\u043b\u0430\u0434\u043d\u043e\u0457 \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457 \u043f\u0440\u043e \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430." - } - } - }, - "state": { - "_": { - "dead": "\u041d\u0435\u0441\u043f\u0440\u0430\u0432\u043d\u0438\u0439", - "initializing": "\u0406\u043d\u0456\u0446\u0456\u0430\u043b\u0456\u0437\u0430\u0446\u0456\u044f", - "ready": "\u0413\u043e\u0442\u043e\u0432\u0438\u0439", - "sleeping": "\u0420\u0435\u0436\u0438\u043c \u0441\u043d\u0443" - }, - "query_stage": { - "dead": "\u041d\u0435\u0441\u043f\u0440\u0430\u0432\u043d\u0438\u0439", - "initializing": "\u0406\u043d\u0456\u0446\u0456\u0430\u043b\u0456\u0437\u0430\u0446\u0456\u044f" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/vi.json b/homeassistant/components/zwave/translations/vi.json deleted file mode 100644 index 4055e09a8df..00000000000 --- a/homeassistant/components/zwave/translations/vi.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "state": { - "_": { - "dead": "\u0110\u00e3 t\u1eaft", - "initializing": "Kh\u1edfi t\u1ea1o", - "ready": "S\u1eb5n s\u00e0ng", - "sleeping": "Ng\u1ee7" - }, - "query_stage": { - "dead": "\u0110\u00e3 t\u1eaft ({query_stage})", - "initializing": "Kh\u1edfi t\u1ea1o ( {query_stage} )" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/zh-Hans.json b/homeassistant/components/zwave/translations/zh-Hans.json deleted file mode 100644 index 64af99e21ff..00000000000 --- a/homeassistant/components/zwave/translations/zh-Hans.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Z-Wave \u5df2\u914d\u7f6e\u5b8c\u6210" - }, - "error": { - "option_error": "Z-Wave \u9a8c\u8bc1\u5931\u8d25\u3002 USB \u68d2\u7684\u8def\u5f84\u662f\u5426\u6b63\u786e\uff1f" - }, - "step": { - "user": { - "data": { - "network_key": "\u7f51\u7edc\u5bc6\u94a5\uff08\u7559\u7a7a\u5c06\u81ea\u52a8\u751f\u6210\uff09", - "usb_path": "USB \u8def\u5f84" - }, - "description": "\u6709\u5173\u914d\u7f6e\u7684\u4fe1\u606f\uff0c\u8bf7\u53c2\u9605 https://www.home-assistant.io/docs/z-wave/installation/" - } - } - }, - "state": { - "_": { - "dead": "\u65ad\u5f00", - "initializing": "\u521d\u59cb\u5316", - "ready": "\u5c31\u7eea", - "sleeping": "\u4f11\u7720" - }, - "query_stage": { - "dead": "\u65ad\u5f00 ({query_stage})", - "initializing": "\u521d\u59cb\u5316 ({query_stage})" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/zh-Hant.json b/homeassistant/components/zwave/translations/zh-Hant.json deleted file mode 100644 index 9dc8810f499..00000000000 --- a/homeassistant/components/zwave/translations/zh-Hant.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", - "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" - }, - "error": { - "option_error": "Z-Wave \u9a57\u8b49\u5931\u6557\uff0c\u8acb\u78ba\u5b9a USB \u96a8\u8eab\u789f\u8def\u5f91\u6b63\u78ba\uff1f" - }, - "step": { - "user": { - "data": { - "network_key": "\u7db2\u8def\u91d1\u9470\uff08\u4fdd\u7559\u7a7a\u767d\u5c07\u6703\u81ea\u52d5\u7522\u751f\uff09", - "usb_path": "USB \u88dd\u7f6e\u8def\u5f91" - }, - "description": "\u6b64\u6574\u5408\u5df2\u7d93\u4e0d\u518d\u9032\u884c\u7dad\u8b77\uff0c\u8acb\u4f7f\u7528 Z-Wave JS \u53d6\u4ee3\u70ba\u65b0\u5b89\u88dd\u65b9\u5f0f\u3002\n\n\u8acb\u53c3\u95b1 https://www.home-assistant.io/docs/z-wave/installation/ \u4ee5\n\u7372\u5f97\u8a2d\u5b9a\u8b8a\u6578\u8cc7\u8a0a" - } - } - }, - "state": { - "_": { - "dead": "\u5931\u53bb\u9023\u7dda", - "initializing": "\u6b63\u5728\u521d\u59cb\u5316", - "ready": "\u6e96\u5099\u5c31\u7dd2", - "sleeping": "\u4f11\u7720\u4e2d" - }, - "query_stage": { - "dead": "\u5931\u53bb\u9023\u7dda", - "initializing": "\u6b63\u5728\u521d\u59cb\u5316" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zwave/util.py b/homeassistant/components/zwave/util.py deleted file mode 100644 index 19be3f7a659..00000000000 --- a/homeassistant/components/zwave/util.py +++ /dev/null @@ -1,129 +0,0 @@ -"""Zwave util methods.""" -import asyncio -import logging - -import homeassistant.util.dt as dt_util - -from . import const - -_LOGGER = logging.getLogger(__name__) - - -def check_node_schema(node, schema): - """Check if node matches the passed node schema.""" - if const.DISC_NODE_ID in schema and node.node_id not in schema[const.DISC_NODE_ID]: - _LOGGER.debug( - "node.node_id %s not in node_id %s", - node.node_id, - schema[const.DISC_NODE_ID], - ) - return False - if ( - const.DISC_GENERIC_DEVICE_CLASS in schema - and node.generic not in schema[const.DISC_GENERIC_DEVICE_CLASS] - ): - _LOGGER.debug( - "node.generic %s not in generic_device_class %s", - node.generic, - schema[const.DISC_GENERIC_DEVICE_CLASS], - ) - return False - if ( - const.DISC_SPECIFIC_DEVICE_CLASS in schema - and node.specific not in schema[const.DISC_SPECIFIC_DEVICE_CLASS] - ): - _LOGGER.debug( - "node.specific %s not in specific_device_class %s", - node.specific, - schema[const.DISC_SPECIFIC_DEVICE_CLASS], - ) - return False - return True - - -def check_value_schema(value, schema): - """Check if the value matches the passed value schema.""" - if ( - const.DISC_COMMAND_CLASS in schema - and value.command_class not in schema[const.DISC_COMMAND_CLASS] - ): - _LOGGER.debug( - "value.command_class %s not in command_class %s", - value.command_class, - schema[const.DISC_COMMAND_CLASS], - ) - return False - if const.DISC_TYPE in schema and value.type not in schema[const.DISC_TYPE]: - _LOGGER.debug( - "value.type %s not in type %s", value.type, schema[const.DISC_TYPE] - ) - return False - if const.DISC_GENRE in schema and value.genre not in schema[const.DISC_GENRE]: - _LOGGER.debug( - "value.genre %s not in genre %s", value.genre, schema[const.DISC_GENRE] - ) - return False - if const.DISC_INDEX in schema and value.index not in schema[const.DISC_INDEX]: - _LOGGER.debug( - "value.index %s not in index %s", value.index, schema[const.DISC_INDEX] - ) - return False - if ( - const.DISC_INSTANCE in schema - and value.instance not in schema[const.DISC_INSTANCE] - ): - _LOGGER.debug( - "value.instance %s not in instance %s", - value.instance, - schema[const.DISC_INSTANCE], - ) - return False - if const.DISC_SCHEMAS in schema: - found = False - for schema_item in schema[const.DISC_SCHEMAS]: - found = found or check_value_schema(value, schema_item) - if not found: - return False - - return True - - -def compute_value_unique_id(node, value): - """Compute unique_id a value would get if it were to get one.""" - return f"{node.node_id}-{value.object_id}" - - -def node_name(node): - """Return the name of the node.""" - if is_node_parsed(node): - return node.name or f"{node.manufacturer_name} {node.product_name}" - return f"Unknown Node {node.node_id}" - - -def node_device_id_and_name(node, instance=1): - """Return the name and device ID for the value with the given index.""" - name = node_name(node) - if instance == 1: - return ((const.DOMAIN, node.node_id), name) - name = f"{name} ({instance})" - return ((const.DOMAIN, node.node_id, instance), name) - - -async def check_has_unique_id(entity, ready_callback, timeout_callback): - """Wait for entity to have unique_id.""" - start_time = dt_util.utcnow() - while True: - waited = int((dt_util.utcnow() - start_time).total_seconds()) - if entity.unique_id: - ready_callback(waited) - return - if waited >= const.NODE_READY_WAIT_SECS: - # Wait up to NODE_READY_WAIT_SECS seconds for unique_id to appear. - timeout_callback(waited) - return - await asyncio.sleep(1) - - -def is_node_parsed(node): - """Check whether the node has been parsed or still waiting to be parsed.""" - return bool((node.manufacturer_name and node.product_name) or node.name) diff --git a/homeassistant/components/zwave/websocket_api.py b/homeassistant/components/zwave/websocket_api.py deleted file mode 100644 index b86e46bee98..00000000000 --- a/homeassistant/components/zwave/websocket_api.py +++ /dev/null @@ -1,90 +0,0 @@ -"""Web socket API for Z-Wave.""" -import voluptuous as vol - -from homeassistant.components import websocket_api -from homeassistant.config_entries import SOURCE_IMPORT -from homeassistant.core import callback - -from .const import ( - CONF_AUTOHEAL, - CONF_DEBUG, - CONF_NETWORK_KEY, - CONF_POLLING_INTERVAL, - CONF_USB_STICK_PATH, - DATA_NETWORK, - DATA_ZWAVE_CONFIG, -) - -TYPE = "type" -ID = "id" - - -@websocket_api.require_admin -@websocket_api.websocket_command({vol.Required(TYPE): "zwave/network_status"}) -def websocket_network_status(hass, connection, msg): - """Get Z-Wave network status.""" - network = hass.data[DATA_NETWORK] - connection.send_result(msg[ID], {"state": network.state}) - - -@websocket_api.require_admin -@websocket_api.websocket_command({vol.Required(TYPE): "zwave/get_config"}) -def websocket_get_config(hass, connection, msg): - """Get Z-Wave configuration.""" - config = hass.data[DATA_ZWAVE_CONFIG] - connection.send_result( - msg[ID], - { - CONF_AUTOHEAL: config[CONF_AUTOHEAL], - CONF_DEBUG: config[CONF_DEBUG], - CONF_POLLING_INTERVAL: config[CONF_POLLING_INTERVAL], - CONF_USB_STICK_PATH: config[CONF_USB_STICK_PATH], - }, - ) - - -@websocket_api.require_admin -@websocket_api.websocket_command({vol.Required(TYPE): "zwave/get_migration_config"}) -def websocket_get_migration_config(hass, connection, msg): - """Get Z-Wave configuration for migration.""" - config = hass.data[DATA_ZWAVE_CONFIG] - connection.send_result( - msg[ID], - { - CONF_USB_STICK_PATH: config[CONF_USB_STICK_PATH], - CONF_NETWORK_KEY: config[CONF_NETWORK_KEY], - }, - ) - - -@websocket_api.require_admin -@websocket_api.websocket_command( - {vol.Required(TYPE): "zwave/start_zwave_js_config_flow"} -) -@websocket_api.async_response -async def websocket_start_zwave_js_config_flow(hass, connection, msg): - """Start the Z-Wave JS integration config flow (for migration wizard). - - Return data with the flow id of the started Z-Wave JS config flow. - """ - config = hass.data[DATA_ZWAVE_CONFIG] - data = { - "usb_path": config[CONF_USB_STICK_PATH], - "network_key": config[CONF_NETWORK_KEY], - } - result = await hass.config_entries.flow.async_init( - "zwave_js", context={"source": SOURCE_IMPORT}, data=data - ) - connection.send_result( - msg[ID], - {"flow_id": result["flow_id"]}, - ) - - -@callback -def async_load_websocket_api(hass): - """Set up the web socket API.""" - websocket_api.async_register_command(hass, websocket_network_status) - websocket_api.async_register_command(hass, websocket_get_config) - websocket_api.async_register_command(hass, websocket_get_migration_config) - websocket_api.async_register_command(hass, websocket_start_zwave_js_config_flow) diff --git a/homeassistant/components/zwave/workaround.py b/homeassistant/components/zwave/workaround.py deleted file mode 100644 index 9ef7cde446d..00000000000 --- a/homeassistant/components/zwave/workaround.py +++ /dev/null @@ -1,170 +0,0 @@ -"""Z-Wave workarounds.""" -from . import const - -# Manufacturers -FIBARO = 0x010F -GE = 0x0063 -PHILIO = 0x013C -SOMFY = 0x0047 -WENZHOU = 0x0118 -LEVITON = 0x001D - -# Product IDs -GE_FAN_CONTROLLER_12730 = 0x3034 -GE_FAN_CONTROLLER_14287 = 0x3131 -JASCO_FAN_CONTROLLER_14314 = 0x3138 -PHILIO_SLIM_SENSOR = 0x0002 -PHILIO_3_IN_1_SENSOR_GEN_4 = 0x000D -PHILIO_PAN07 = 0x0005 -VIZIA_FAN_CONTROLLER_VRF01 = 0x0334 -LEVITON_DECORA_FAN_CONTROLLER_ZW4SF = 0x0002 - -# Product Types -FGFS101_FLOOD_SENSOR_TYPE = 0x0B00 -FGRM222_SHUTTER2 = 0x0301 -FGR222_SHUTTER2 = 0x0302 -GE_DIMMER = 0x4944 -PHILIO_SWITCH = 0x0001 -PHILIO_SENSOR = 0x0002 -SOMFY_ZRTSI = 0x5A52 -VIZIA_DIMMER = 0x1001 -LEVITON_DECORA_FAN_CONTROLLER = 0x0038 - -# Mapping devices -PHILIO_SLIM_SENSOR_MOTION_MTII = (PHILIO, PHILIO_SENSOR, PHILIO_SLIM_SENSOR, 0) -PHILIO_3_IN_1_SENSOR_GEN_4_MOTION_MTII = ( - PHILIO, - PHILIO_SENSOR, - PHILIO_3_IN_1_SENSOR_GEN_4, - 0, -) -PHILIO_PAN07_MTI_INSTANCE = (PHILIO, PHILIO_SWITCH, PHILIO_PAN07, 1) -WENZHOU_SLIM_SENSOR_MOTION_MTII = (WENZHOU, PHILIO_SENSOR, PHILIO_SLIM_SENSOR, 0) - -# Workarounds -WORKAROUND_NO_OFF_EVENT = "trigger_no_off_event" -WORKAROUND_NO_POSITION = "workaround_no_position" -WORKAROUND_REFRESH_NODE_ON_UPDATE = "refresh_node_on_update" -WORKAROUND_IGNORE = "workaround_ignore" - -# List of workarounds by (manufacturer_id, product_type, product_id, index) -DEVICE_MAPPINGS_MTII = { - PHILIO_SLIM_SENSOR_MOTION_MTII: WORKAROUND_NO_OFF_EVENT, - PHILIO_3_IN_1_SENSOR_GEN_4_MOTION_MTII: WORKAROUND_NO_OFF_EVENT, - WENZHOU_SLIM_SENSOR_MOTION_MTII: WORKAROUND_NO_OFF_EVENT, -} - -# List of workarounds by (manufacturer_id, product_type, product_id, instance) -DEVICE_MAPPINGS_MTI_INSTANCE = { - PHILIO_PAN07_MTI_INSTANCE: WORKAROUND_REFRESH_NODE_ON_UPDATE -} - -SOMFY_ZRTSI_CONTROLLER_MT = (SOMFY, SOMFY_ZRTSI) - -# List of workarounds by (manufacturer_id, product_type) -DEVICE_MAPPINGS_MT = {SOMFY_ZRTSI_CONTROLLER_MT: WORKAROUND_NO_POSITION} - -# Component mapping devices -FIBARO_FGFS101_SENSOR_ALARM = ( - FIBARO, - FGFS101_FLOOD_SENSOR_TYPE, - const.COMMAND_CLASS_SENSOR_ALARM, -) -FIBARO_FGRM222_BINARY = (FIBARO, FGRM222_SHUTTER2, const.COMMAND_CLASS_SWITCH_BINARY) -FIBARO_FGR222_BINARY = (FIBARO, FGR222_SHUTTER2, const.COMMAND_CLASS_SWITCH_BINARY) -GE_FAN_CONTROLLER_12730_MULTILEVEL = ( - GE, - GE_DIMMER, - GE_FAN_CONTROLLER_12730, - const.COMMAND_CLASS_SWITCH_MULTILEVEL, -) -GE_FAN_CONTROLLER_14287_MULTILEVEL = ( - GE, - GE_DIMMER, - GE_FAN_CONTROLLER_14287, - const.COMMAND_CLASS_SWITCH_MULTILEVEL, -) -JASCO_FAN_CONTROLLER_14314_MULTILEVEL = ( - GE, - GE_DIMMER, - JASCO_FAN_CONTROLLER_14314, - const.COMMAND_CLASS_SWITCH_MULTILEVEL, -) -VIZIA_FAN_CONTROLLER_VRF01_MULTILEVEL = ( - LEVITON, - VIZIA_DIMMER, - VIZIA_FAN_CONTROLLER_VRF01, - const.COMMAND_CLASS_SWITCH_MULTILEVEL, -) -LEVITON_FAN_CONTROLLER_ZW4SF_MULTILEVEL = ( - LEVITON, - LEVITON_DECORA_FAN_CONTROLLER, - LEVITON_DECORA_FAN_CONTROLLER_ZW4SF, - const.COMMAND_CLASS_SWITCH_MULTILEVEL, -) - -# List of component workarounds by -# (manufacturer_id, product_type, command_class) -DEVICE_COMPONENT_MAPPING = { - FIBARO_FGFS101_SENSOR_ALARM: "binary_sensor", - FIBARO_FGRM222_BINARY: WORKAROUND_IGNORE, - FIBARO_FGR222_BINARY: WORKAROUND_IGNORE, -} - -# List of component workarounds by -# (manufacturer_id, product_type, product_id, command_class) -DEVICE_COMPONENT_MAPPING_MTI = { - GE_FAN_CONTROLLER_12730_MULTILEVEL: "fan", - GE_FAN_CONTROLLER_14287_MULTILEVEL: "fan", - JASCO_FAN_CONTROLLER_14314_MULTILEVEL: "fan", - VIZIA_FAN_CONTROLLER_VRF01_MULTILEVEL: "fan", - LEVITON_FAN_CONTROLLER_ZW4SF_MULTILEVEL: "fan", -} - - -def get_device_component_mapping(value): - """Get mapping of value to another component.""" - if value.node.manufacturer_id.strip() and value.node.product_type.strip(): - manufacturer_id = int(value.node.manufacturer_id, 16) - product_type = int(value.node.product_type, 16) - product_id = int(value.node.product_id, 16) - result = DEVICE_COMPONENT_MAPPING.get( - (manufacturer_id, product_type, value.command_class) - ) - if result: - return result - - result = DEVICE_COMPONENT_MAPPING_MTI.get( - (manufacturer_id, product_type, product_id, value.command_class) - ) - if result: - return result - - return None - - -def get_device_mapping(value): - """Get mapping of value to a workaround.""" - if ( - value.node.manufacturer_id.strip() - and value.node.product_id.strip() - and value.node.product_type.strip() - ): - manufacturer_id = int(value.node.manufacturer_id, 16) - product_type = int(value.node.product_type, 16) - product_id = int(value.node.product_id, 16) - result = DEVICE_MAPPINGS_MTII.get( - (manufacturer_id, product_type, product_id, value.index) - ) - if result: - return result - - result = DEVICE_MAPPINGS_MTI_INSTANCE.get( - (manufacturer_id, product_type, product_id, value.instance) - ) - if result: - return result - - return DEVICE_MAPPINGS_MT.get((manufacturer_id, product_type)) - - return None diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index 5d294931e66..12ff9acb530 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -113,6 +113,11 @@ DATA_INVALID_SERVER_VERSION_LOGGED = "invalid_server_version_logged" async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Z-Wave JS component.""" hass.data[DOMAIN] = {} + for entry in hass.config_entries.async_entries(DOMAIN): + if not isinstance(entry.unique_id, str): + hass.config_entries.async_update_entry( + entry, unique_id=str(entry.unique_id) + ) return True diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index 4e68cc2e2dd..0e947de982b 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -60,8 +60,8 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.device_registry import DeviceEntry from homeassistant.helpers.dispatcher import async_dispatcher_connect +from .config_validation import BITMASK_SCHEMA from .const import ( - BITMASK_SCHEMA, CONF_DATA_COLLECTION_OPTED_IN, DATA_CLIENT, DOMAIN, diff --git a/homeassistant/components/zwave_js/config_flow.py b/homeassistant/components/zwave_js/config_flow.py index 60b13e135de..66cb91f9330 100644 --- a/homeassistant/components/zwave_js/config_flow.py +++ b/homeassistant/components/zwave_js/config_flow.py @@ -385,7 +385,6 @@ class ConfigFlow(BaseZwaveJSFlow, config_entries.ConfigFlow, domain=DOMAIN): return self.async_show_form( step_id="usb_confirm", description_placeholders={CONF_NAME: self._title}, - data_schema=vol.Schema({}), ) self._usb_discovery = True @@ -412,7 +411,7 @@ class ConfigFlow(BaseZwaveJSFlow, config_entries.ConfigFlow, domain=DOMAIN): errors["base"] = "unknown" else: await self.async_set_unique_id( - version_info.home_id, raise_on_progress=False + str(version_info.home_id), raise_on_progress=False ) # Make sure we disable any add-on handling # if the controller is reconfigured in a manual step. @@ -446,7 +445,7 @@ class ConfigFlow(BaseZwaveJSFlow, config_entries.ConfigFlow, domain=DOMAIN): except CannotConnect: return self.async_abort(reason="cannot_connect") - await self.async_set_unique_id(version_info.home_id) + await self.async_set_unique_id(str(version_info.home_id)) self._abort_if_unique_id_configured(updates={CONF_URL: self.ws_address}) return await self.async_step_hassio_confirm() @@ -580,7 +579,7 @@ class ConfigFlow(BaseZwaveJSFlow, config_entries.ConfigFlow, domain=DOMAIN): raise AbortFlow("cannot_connect") from err await self.async_set_unique_id( - self.version_info.home_id, raise_on_progress=False + str(self.version_info.home_id), raise_on_progress=False ) self._abort_if_unique_id_configured( @@ -668,7 +667,7 @@ class OptionsFlowHandler(BaseZwaveJSFlow, config_entries.OptionsFlow): _LOGGER.exception("Unexpected exception") errors["base"] = "unknown" else: - if self.config_entry.unique_id != version_info.home_id: + if self.config_entry.unique_id != str(version_info.home_id): return self.async_abort(reason="different_device") # Make sure we disable any add-on handling @@ -828,7 +827,7 @@ class OptionsFlowHandler(BaseZwaveJSFlow, config_entries.OptionsFlow): except CannotConnect: return await self.async_revert_addon_config(reason="cannot_connect") - if self.config_entry.unique_id != self.version_info.home_id: + if self.config_entry.unique_id != str(self.version_info.home_id): return await self.async_revert_addon_config(reason="different_device") self._async_update_entry( diff --git a/homeassistant/components/zwave_js/config_validation.py b/homeassistant/components/zwave_js/config_validation.py new file mode 100644 index 00000000000..9fc502bdafb --- /dev/null +++ b/homeassistant/components/zwave_js/config_validation.py @@ -0,0 +1,41 @@ +"""Config validation for the Z-Wave JS integration.""" +from typing import Any + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv + +# Validates that a bitmask is provided in hex form and converts it to decimal +# int equivalent since that's what the library uses +BITMASK_SCHEMA = vol.All( + cv.string, + vol.Lower, + vol.Match( + r"^(0x)?[0-9a-f]+$", + msg="Must provide an integer (e.g. 255) or a bitmask in hex form (e.g. 0xff)", + ), + lambda value: int(value, 16), +) + + +def boolean(value: Any) -> bool: + """Validate and coerce a boolean value.""" + if isinstance(value, bool): + return value + if isinstance(value, str): + value = value.lower().strip() + if value in ("true", "yes", "on", "enable"): + return True + if value in ("false", "no", "off", "disable"): + return False + raise vol.Invalid(f"invalid boolean value {value}") + + +VALUE_SCHEMA = vol.Any( + boolean, + vol.Coerce(int), + vol.Coerce(float), + BITMASK_SCHEMA, + cv.string, + dict, +) diff --git a/homeassistant/components/zwave_js/const.py b/homeassistant/components/zwave_js/const.py index 8f6fada2106..d6d63487b8a 100644 --- a/homeassistant/components/zwave_js/const.py +++ b/homeassistant/components/zwave_js/const.py @@ -1,10 +1,6 @@ """Constants for the Z-Wave JS integration.""" import logging -import voluptuous as vol - -import homeassistant.helpers.config_validation as cv - CONF_ADDON_DEVICE = "device" CONF_ADDON_EMULATE_HARDWARE = "emulate_hardware" CONF_ADDON_LOG_LEVEL = "log_level" @@ -117,26 +113,3 @@ ENTITY_DESC_KEY_TEMPERATURE = "temperature" ENTITY_DESC_KEY_TARGET_TEMPERATURE = "target_temperature" ENTITY_DESC_KEY_MEASUREMENT = "measurement" ENTITY_DESC_KEY_TOTAL_INCREASING = "total_increasing" - -# Schema Constants - -# Validates that a bitmask is provided in hex form and converts it to decimal -# int equivalent since that's what the library uses -BITMASK_SCHEMA = vol.All( - cv.string, - vol.Lower, - vol.Match( - r"^(0x)?[0-9a-f]+$", - msg="Must provide an integer (e.g. 255) or a bitmask in hex form (e.g. 0xff)", - ), - lambda value: int(value, 16), -) - -VALUE_SCHEMA = vol.Any( - bool, - vol.Coerce(int), - vol.Coerce(float), - BITMASK_SCHEMA, - cv.string, - dict, -) diff --git a/homeassistant/components/zwave_js/device_action.py b/homeassistant/components/zwave_js/device_action.py index b81d675e6fd..a67bd44e533 100644 --- a/homeassistant/components/zwave_js/device_action.py +++ b/homeassistant/components/zwave_js/device_action.py @@ -29,6 +29,7 @@ from homeassistant.helpers import entity_registry import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType +from .config_validation import VALUE_SCHEMA from .const import ( ATTR_COMMAND_CLASS, ATTR_CONFIG_PARAMETER, @@ -48,7 +49,6 @@ from .const import ( SERVICE_SET_CONFIG_PARAMETER, SERVICE_SET_LOCK_USERCODE, SERVICE_SET_VALUE, - VALUE_SCHEMA, ) from .device_automation_helpers import ( CONF_SUBTYPE, @@ -241,7 +241,7 @@ async def async_call_action_from_config( hass: HomeAssistant, config: dict, variables: dict, context: Context | None ) -> None: """Execute a device action.""" - action_type = service = config.pop(CONF_TYPE) + action_type = service = config[CONF_TYPE] if action_type not in ACTION_TYPES: raise HomeAssistantError(f"Unhandled action type {action_type}") @@ -249,10 +249,10 @@ async def async_call_action_from_config( service_data = { k: v for k, v in config.items() - if k not in (ATTR_DOMAIN, CONF_SUBTYPE) and v not in (None, "") + if k not in (ATTR_DOMAIN, CONF_TYPE, CONF_SUBTYPE) and v not in (None, "") } - # Entity services (including refresh value which is a fake entity service) expects + # Entity services (including refresh value which is a fake entity service) expect # just an entity ID if action_type in ( SERVICE_REFRESH_VALUE, diff --git a/homeassistant/components/zwave_js/device_condition.py b/homeassistant/components/zwave_js/device_condition.py index 8bb151199d7..c70371d6f8a 100644 --- a/homeassistant/components/zwave_js/device_condition.py +++ b/homeassistant/components/zwave_js/device_condition.py @@ -16,6 +16,7 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import condition, config_validation as cv from homeassistant.helpers.typing import ConfigType, TemplateVarsType +from .config_validation import VALUE_SCHEMA from .const import ( ATTR_COMMAND_CLASS, ATTR_ENDPOINT, @@ -23,7 +24,6 @@ from .const import ( ATTR_PROPERTY_KEY, ATTR_VALUE, DOMAIN, - VALUE_SCHEMA, ) from .device_automation_helpers import ( CONF_SUBTYPE, diff --git a/homeassistant/components/zwave_js/device_trigger.py b/homeassistant/components/zwave_js/device_trigger.py index 0615668cccd..89379f9a953 100644 --- a/homeassistant/components/zwave_js/device_trigger.py +++ b/homeassistant/components/zwave_js/device_trigger.py @@ -32,6 +32,7 @@ from homeassistant.helpers import ( from homeassistant.helpers.typing import ConfigType from . import trigger +from .config_validation import VALUE_SCHEMA from .const import ( ATTR_COMMAND_CLASS, ATTR_DATA_TYPE, @@ -80,14 +81,6 @@ CONFIG_PARAMETER_VALUE_UPDATED = f"{VALUE_UPDATED_PLATFORM_TYPE}.config_paramete VALUE_VALUE_UPDATED = f"{VALUE_UPDATED_PLATFORM_TYPE}.value" NODE_STATUS = "state.node_status" -VALUE_SCHEMA = vol.Any( - bool, - vol.Coerce(int), - vol.Coerce(float), - cv.boolean, - cv.string, -) - NOTIFICATION_EVENT_CC_MAPPINGS = ( (ENTRY_CONTROL_NOTIFICATION, CommandClass.ENTRY_CONTROL), diff --git a/homeassistant/components/zwave_js/diagnostics.py b/homeassistant/components/zwave_js/diagnostics.py index 8b59c38d405..f323ee0f5f9 100644 --- a/homeassistant/components/zwave_js/diagnostics.py +++ b/homeassistant/components/zwave_js/diagnostics.py @@ -1,28 +1,132 @@ """Provides diagnostics for Z-Wave JS.""" from __future__ import annotations -from zwave_js_server.client import Client -from zwave_js_server.dump import dump_msgs -from zwave_js_server.model.node import NodeDataType +from dataclasses import astuple +from typing import Any +from zwave_js_server.client import Client +from zwave_js_server.const import CommandClass +from zwave_js_server.dump import dump_msgs +from zwave_js_server.model.node import Node, NodeDataType +from zwave_js_server.model.value import ValueDataType + +from homeassistant.components.diagnostics.const import REDACTED +from homeassistant.components.diagnostics.util import async_redact_data from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_URL from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.device_registry import DeviceEntry +from homeassistant.helpers.entity_registry import async_entries_for_device, async_get from .const import DATA_CLIENT, DOMAIN -from .helpers import get_home_and_node_id_from_device_entry +from .helpers import ZwaveValueID, get_home_and_node_id_from_device_entry + +KEYS_TO_REDACT = {"homeId", "location"} + +VALUES_TO_REDACT = ( + ZwaveValueID(property_="userCode", command_class=CommandClass.USER_CODE), +) + + +def redact_value_of_zwave_value(zwave_value: ValueDataType) -> ValueDataType: + """Redact value of a Z-Wave value.""" + for value_to_redact in VALUES_TO_REDACT: + zwave_value_id = ZwaveValueID( + property_=zwave_value["property"], + command_class=CommandClass(zwave_value["commandClass"]), + endpoint=zwave_value["endpoint"], + property_key=zwave_value.get("propertyKey"), + ) + if all( + redacted_field_val is None or redacted_field_val == zwave_value_field_val + for redacted_field_val, zwave_value_field_val in zip( + astuple(value_to_redact), astuple(zwave_value_id) + ) + ): + return {**zwave_value, "value": REDACTED} + return zwave_value + + +def redact_node_state(node_state: NodeDataType) -> NodeDataType: + """Redact node state.""" + return { + **node_state, + "values": [ + redact_value_of_zwave_value(zwave_value) + for zwave_value in node_state["values"] + ], + } + + +def get_device_entities( + hass: HomeAssistant, node: Node, device: DeviceEntry +) -> list[dict[str, Any]]: + """Get entities for a device.""" + entity_entries = async_entries_for_device( + async_get(hass), device.id, include_disabled_entities=True + ) + entities = [] + for entry in entity_entries: + state_key = None + split_unique_id = entry.unique_id.split(".") + # If the unique ID has three parts, it's either one of the generic per node + # entities (node status sensor, ping button) or a binary sensor for a particular + # state. If we can get the state key, we will add it to the dictionary. + if len(split_unique_id) == 3: + try: + state_key = int(split_unique_id[-1]) + # If the third part of the unique ID isn't a state key, the entity must be a + # generic entity. We won't add those since they won't help with + # troubleshooting. + except ValueError: + continue + value_id = split_unique_id[1] + zwave_value = node.values[value_id] + primary_value_data = { + "command_class": zwave_value.command_class, + "command_class_name": zwave_value.command_class_name, + "endpoint": zwave_value.endpoint, + "property": zwave_value.property_, + "property_name": zwave_value.property_name, + "property_key": zwave_value.property_key, + "property_key_name": zwave_value.property_key_name, + } + if state_key is not None: + primary_value_data["state_key"] = state_key + entity = { + "domain": entry.domain, + "entity_id": entry.entity_id, + "original_name": entry.original_name, + "original_device_class": entry.original_device_class, + "disabled": entry.disabled, + "disabled_by": entry.disabled_by, + "hidden_by": entry.hidden_by, + "original_icon": entry.original_icon, + "entity_category": entry.entity_category, + "supported_features": entry.supported_features, + "unit_of_measurement": entry.unit_of_measurement, + "primary_value": primary_value_data, + } + entities.append(entity) + return entities async def async_get_config_entry_diagnostics( hass: HomeAssistant, config_entry: ConfigEntry ) -> list[dict]: """Return diagnostics for a config entry.""" - msgs: list[dict] = await dump_msgs( - config_entry.data[CONF_URL], async_get_clientsession(hass) + msgs: list[dict] = async_redact_data( + await dump_msgs(config_entry.data[CONF_URL], async_get_clientsession(hass)), + KEYS_TO_REDACT, ) - return msgs + handshake_msgs = msgs[:-1] + network_state = msgs[-1] + network_state["result"]["state"]["nodes"] = [ + redact_node_state(node) for node in network_state["result"]["state"]["nodes"] + ] + return [*handshake_msgs, network_state] async def async_get_device_diagnostics( @@ -35,6 +139,7 @@ async def async_get_device_diagnostics( if node_id is None or node_id not in client.driver.controller.nodes: raise ValueError(f"Node for device {device.id} can't be found") node = client.driver.controller.nodes[node_id] + entities = get_device_entities(hass, node, device) return { "versionInfo": { "driverVersion": client.version.driver_version, @@ -42,5 +147,6 @@ async def async_get_device_diagnostics( "minSchemaVersion": client.version.min_schema_version, "maxSchemaVersion": client.version.max_schema_version, }, - "state": node.data, + "entities": entities, + "state": redact_node_state(async_redact_data(node.data, KEYS_TO_REDACT)), } diff --git a/homeassistant/components/zwave_js/discovery.py b/homeassistant/components/zwave_js/discovery.py index 69a3d05539b..e94f9645444 100644 --- a/homeassistant/components/zwave_js/discovery.py +++ b/homeassistant/components/zwave_js/discovery.py @@ -34,6 +34,7 @@ from zwave_js_server.const.command_class.sound_switch import ( ) from zwave_js_server.const.command_class.thermostat import ( THERMOSTAT_CURRENT_TEMP_PROPERTY, + THERMOSTAT_FAN_MODE_PROPERTY, THERMOSTAT_MODE_PROPERTY, THERMOSTAT_SETPOINT_PROPERTY, ) @@ -48,13 +49,14 @@ from homeassistant.helpers.device_registry import DeviceEntry from .const import LOGGER from .discovery_data_template import ( BaseDiscoverySchemaDataTemplate, - ConfigurableFanSpeedDataTemplate, + ConfigurableFanValueMappingDataTemplate, CoverTiltDataTemplate, DynamicCurrentTempClimateDataTemplate, - FixedFanSpeedDataTemplate, + FanValueMapping, + FixedFanValueMappingDataTemplate, NumericSensorDataTemplate, - ZwaveValueID, ) +from .helpers import ZwaveValueID class DataclassMustHaveAtLeastOne: @@ -207,6 +209,12 @@ def get_config_parameter_discovery_schema( ) +DOOR_LOCK_CURRENT_MODE_SCHEMA = ZWaveValueDiscoverySchema( + command_class={CommandClass.DOOR_LOCK}, + property={CURRENT_MODE_PROPERTY}, + type={"number"}, +) + SWITCH_MULTILEVEL_CURRENT_VALUE_SCHEMA = ZWaveValueDiscoverySchema( command_class={CommandClass.SWITCH_MULTILEVEL}, property={CURRENT_VALUE_PROPERTY}, @@ -238,25 +246,25 @@ DISCOVERY_SCHEMAS = [ # GE/Jasco - In-Wall Smart Fan Control - 12730 / ZW4002 ZWaveDiscoverySchema( platform="fan", - hint="configured_fan_speed", + hint="has_fan_value_mapping", manufacturer_id={0x0063}, product_id={0x3034}, product_type={0x4944}, primary_value=SWITCH_MULTILEVEL_CURRENT_VALUE_SCHEMA, - data_template=FixedFanSpeedDataTemplate( - speeds=[33, 67, 99], + data_template=FixedFanValueMappingDataTemplate( + FanValueMapping(speeds=[(1, 33), (34, 67), (68, 99)]), ), ), # GE/Jasco - In-Wall Smart Fan Control - 14287 / ZW4002 ZWaveDiscoverySchema( platform="fan", - hint="configured_fan_speed", + hint="has_fan_value_mapping", manufacturer_id={0x0063}, product_id={0x3131}, product_type={0x4944}, primary_value=SWITCH_MULTILEVEL_CURRENT_VALUE_SCHEMA, - data_template=FixedFanSpeedDataTemplate( - speeds=[32, 66, 99], + data_template=FixedFanValueMappingDataTemplate( + FanValueMapping(speeds=[(1, 32), (33, 66), (67, 99)]), ), ), # GE/Jasco - In-Wall Smart Fan Control - 14314 / ZW4002 @@ -279,6 +287,7 @@ DISCOVERY_SCHEMAS = [ # The fan is endpoint 2, the light is endpoint 1. ZWaveDiscoverySchema( platform="fan", + hint="has_fan_value_mapping", manufacturer_id={0x031E}, product_id={0x0001}, product_type={0x000E}, @@ -288,20 +297,28 @@ DISCOVERY_SCHEMAS = [ property={CURRENT_VALUE_PROPERTY}, type={"number"}, ), + data_template=FixedFanValueMappingDataTemplate( + FanValueMapping( + presets={1: "breeze"}, speeds=[(2, 33), (34, 66), (67, 99)] + ), + ), ), # HomeSeer HS-FC200+ ZWaveDiscoverySchema( platform="fan", - hint="configured_fan_speed", + hint="has_fan_value_mapping", manufacturer_id={0x000C}, product_id={0x0001}, product_type={0x0203}, primary_value=SWITCH_MULTILEVEL_CURRENT_VALUE_SCHEMA, - data_template=ConfigurableFanSpeedDataTemplate( + data_template=ConfigurableFanValueMappingDataTemplate( configuration_option=ZwaveValueID( - 5, CommandClass.CONFIGURATION, endpoint=0 + property_=5, command_class=CommandClass.CONFIGURATION, endpoint=0 ), - configuration_value_to_speeds={0: [33, 66, 99], 1: [24, 49, 74, 99]}, + configuration_value_to_fan_value_mapping={ + 0: FanValueMapping(speeds=[(1, 33), (34, 66), (67, 99)]), + 1: FanValueMapping(speeds=[(1, 24), (25, 49), (50, 74), (75, 99)]), + }, ), ), # Fibaro Shutter Fibaro FGR222 @@ -314,8 +331,8 @@ DISCOVERY_SCHEMAS = [ primary_value=SWITCH_MULTILEVEL_CURRENT_VALUE_SCHEMA, data_template=CoverTiltDataTemplate( tilt_value_id=ZwaveValueID( - "fibaro", - CommandClass.MANUFACTURER_PROPRIETARY, + property_="fibaro", + command_class=CommandClass.MANUFACTURER_PROPRIETARY, endpoint=0, property_key="venetianBlindsTilt", ) @@ -380,34 +397,36 @@ DISCOVERY_SCHEMAS = [ lookup_table={ # Internal Sensor "A": ZwaveValueID( - THERMOSTAT_CURRENT_TEMP_PROPERTY, - CommandClass.SENSOR_MULTILEVEL, + property_=THERMOSTAT_CURRENT_TEMP_PROPERTY, + command_class=CommandClass.SENSOR_MULTILEVEL, endpoint=2, ), "AF": ZwaveValueID( - THERMOSTAT_CURRENT_TEMP_PROPERTY, - CommandClass.SENSOR_MULTILEVEL, + property_=THERMOSTAT_CURRENT_TEMP_PROPERTY, + command_class=CommandClass.SENSOR_MULTILEVEL, endpoint=2, ), # External Sensor "A2": ZwaveValueID( - THERMOSTAT_CURRENT_TEMP_PROPERTY, - CommandClass.SENSOR_MULTILEVEL, + property_=THERMOSTAT_CURRENT_TEMP_PROPERTY, + command_class=CommandClass.SENSOR_MULTILEVEL, endpoint=3, ), "A2F": ZwaveValueID( - THERMOSTAT_CURRENT_TEMP_PROPERTY, - CommandClass.SENSOR_MULTILEVEL, + property_=THERMOSTAT_CURRENT_TEMP_PROPERTY, + command_class=CommandClass.SENSOR_MULTILEVEL, endpoint=3, ), # Floor sensor "F": ZwaveValueID( - THERMOSTAT_CURRENT_TEMP_PROPERTY, - CommandClass.SENSOR_MULTILEVEL, + property_=THERMOSTAT_CURRENT_TEMP_PROPERTY, + command_class=CommandClass.SENSOR_MULTILEVEL, endpoint=4, ), }, - dependent_value=ZwaveValueID(2, CommandClass.CONFIGURATION, endpoint=0), + dependent_value=ZwaveValueID( + property_=2, command_class=CommandClass.CONFIGURATION, endpoint=0 + ), ), ), # Heatit Z-TRM2fx @@ -427,23 +446,25 @@ DISCOVERY_SCHEMAS = [ lookup_table={ # External Sensor "A2": ZwaveValueID( - THERMOSTAT_CURRENT_TEMP_PROPERTY, - CommandClass.SENSOR_MULTILEVEL, + property_=THERMOSTAT_CURRENT_TEMP_PROPERTY, + command_class=CommandClass.SENSOR_MULTILEVEL, endpoint=2, ), "A2F": ZwaveValueID( - THERMOSTAT_CURRENT_TEMP_PROPERTY, - CommandClass.SENSOR_MULTILEVEL, + property_=THERMOSTAT_CURRENT_TEMP_PROPERTY, + command_class=CommandClass.SENSOR_MULTILEVEL, endpoint=2, ), # Floor sensor "F": ZwaveValueID( - THERMOSTAT_CURRENT_TEMP_PROPERTY, - CommandClass.SENSOR_MULTILEVEL, + property_=THERMOSTAT_CURRENT_TEMP_PROPERTY, + command_class=CommandClass.SENSOR_MULTILEVEL, endpoint=3, ), }, - dependent_value=ZwaveValueID(2, CommandClass.CONFIGURATION, endpoint=0), + dependent_value=ZwaveValueID( + property_=2, command_class=CommandClass.CONFIGURATION, endpoint=0 + ), ), ), # FortrezZ SSA1/SSA2/SSA3 @@ -486,16 +507,17 @@ DISCOVERY_SCHEMAS = [ ), # ====== START OF GENERIC MAPPING SCHEMAS ======= # locks + # Door Lock CC + ZWaveDiscoverySchema(platform="lock", primary_value=DOOR_LOCK_CURRENT_MODE_SCHEMA), + # Only discover the Lock CC if the Door Lock CC isn't also present on the node ZWaveDiscoverySchema( platform="lock", primary_value=ZWaveValueDiscoverySchema( - command_class={ - CommandClass.LOCK, - CommandClass.DOOR_LOCK, - }, - property={CURRENT_MODE_PROPERTY, LOCKED_PROPERTY}, - type={"number", "boolean"}, + command_class={CommandClass.LOCK}, + property={LOCKED_PROPERTY}, + type={"boolean"}, ), + absent_values=[DOOR_LOCK_CURRENT_MODE_SCHEMA], ), # door lock door status ZWaveDiscoverySchema( @@ -510,6 +532,17 @@ DISCOVERY_SCHEMAS = [ type={"any"}, ), ), + # thermostat fan + ZWaveDiscoverySchema( + platform="fan", + hint="thermostat_fan", + primary_value=ZWaveValueDiscoverySchema( + command_class={CommandClass.THERMOSTAT_FAN_MODE}, + property={THERMOSTAT_FAN_MODE_PROPERTY}, + type={"number"}, + ), + entity_registry_enabled_default=False, + ), # humidifier # hygrostats supporting mode (and optional setpoint) ZWaveDiscoverySchema( diff --git a/homeassistant/components/zwave_js/discovery_data_template.py b/homeassistant/components/zwave_js/discovery_data_template.py index 3e7db7cdcd9..622a8222fa7 100644 --- a/homeassistant/components/zwave_js/discovery_data_template.py +++ b/homeassistant/components/zwave_js/discovery_data_template.py @@ -148,6 +148,7 @@ from .const import ( ENTITY_DESC_KEY_TOTAL_INCREASING, ENTITY_DESC_KEY_VOLTAGE, ) +from .helpers import ZwaveValueID METER_DEVICE_CLASS_MAP: dict[str, set[MeterScaleType]] = { ENTITY_DESC_KEY_CURRENT: CURRENT_METER_TYPES, @@ -226,16 +227,6 @@ MULTILEVEL_SENSOR_UNIT_MAP: dict[str, set[MultilevelSensorScaleType]] = { _LOGGER = logging.getLogger(__name__) -@dataclass -class ZwaveValueID: - """Class to represent a value ID.""" - - property_: str | int - command_class: int - endpoint: int | None = None - property_key: str | int | None = None - - @dataclass class BaseDiscoverySchemaDataTemplate: """Base class for discovery schema data templates.""" @@ -432,27 +423,11 @@ class CoverTiltDataTemplate(BaseDiscoverySchemaDataTemplate, TiltValueMix): @dataclass -class FanSpeedDataTemplate: - """Mixin to define get_speed_config.""" +class FanValueMapping: + """Data class to represent how a fan's values map to features.""" - def get_speed_config(self, resolved_data: dict[str, Any]) -> list[int] | None: - """ - Get the fan speed configuration for this device. - - Values should indicate the highest allowed device setting for each - actual speed, and should be sorted in ascending order. - - Empty lists are not permissible. - """ - raise NotImplementedError - - -@dataclass -class ConfigurableFanSpeedValueMix: - """Mixin data class for defining configurable fan speeds.""" - - configuration_option: ZwaveValueID - configuration_value_to_speeds: dict[int, list[int]] + presets: dict[int, str] = field(default_factory=dict) + speeds: list[tuple[int, int]] = field(default_factory=list) def __post_init__(self) -> None: """ @@ -461,14 +436,36 @@ class ConfigurableFanSpeedValueMix: These inputs are hardcoded in `discovery.py`, so these checks should only fail due to developer error. """ - for speeds in self.configuration_value_to_speeds.values(): - assert len(speeds) > 0 - assert sorted(speeds) == speeds + assert len(self.speeds) > 0, "At least one speed must be specified" + for speed_range in self.speeds: + (low, high) = speed_range + assert high >= low, "Speed range values must be ordered" @dataclass -class ConfigurableFanSpeedDataTemplate( - BaseDiscoverySchemaDataTemplate, FanSpeedDataTemplate, ConfigurableFanSpeedValueMix +class FanValueMappingDataTemplate: + """Mixin to define `get_fan_value_mapping`.""" + + def get_fan_value_mapping( + self, resolved_data: dict[str, Any] + ) -> FanValueMapping | None: + """Get the value mappings for this device.""" + raise NotImplementedError + + +@dataclass +class ConfigurableFanValueMappingValueMix: + """Mixin data class for defining fan properties that change based on a device configuration option.""" + + configuration_option: ZwaveValueID + configuration_value_to_fan_value_mapping: dict[int, FanValueMapping] + + +@dataclass +class ConfigurableFanValueMappingDataTemplate( + BaseDiscoverySchemaDataTemplate, + FanValueMappingDataTemplate, + ConfigurableFanValueMappingValueMix, ): """ Gets fan speeds based on a configuration value. @@ -476,22 +473,23 @@ class ConfigurableFanSpeedDataTemplate( Example: ZWaveDiscoverySchema( platform="fan", - hint="configured_fan_speed", + hint="has_fan_value_mapping", ... - data_template=ConfigurableFanSpeedDataTemplate( + data_template=ConfigurableFanValueMappingDataTemplate( configuration_option=ZwaveValueID( - 5, CommandClass.CONFIGURATION, endpoint=0 + property_=5, command_class=CommandClass.CONFIGURATION, endpoint=0 ), - configuration_value_to_speeds={0: [32, 65, 99], 1: [24, 49, 74, 99]}, + configuration_value_to_fan_value_mapping={ + 0: FanValueMapping(speeds=[(1,33), (34,66), (67,99)]), + 1: FanValueMapping(speeds=[(1,24), (25,49), (50,74), (75,99)]), + }, ), - ), - `configuration_option` is a reference to the setting that determines how - many speeds are supported. + `configuration_option` is a reference to the setting that determines which + value mapping to use (e.g., 3 speeds or 4 speeds). - `configuration_value_to_speeds` maps the values from `configuration_option` - to a list of speeds. The specified speeds indicate the maximum setting on - the underlying switch for each actual speed. + `configuration_value_to_fan_value_mapping` maps the values from + `configuration_option` to the value mapping object. """ def resolve_data(self, value: ZwaveValue) -> dict[str, ZwaveConfigurationValue]: @@ -507,64 +505,61 @@ class ConfigurableFanSpeedDataTemplate( resolved_data["configuration_value"], ] - def get_speed_config( + def get_fan_value_mapping( self, resolved_data: dict[str, ZwaveConfigurationValue] - ) -> list[int] | None: - """Get current speed configuration from resolved data.""" + ) -> FanValueMapping | None: + """Get current fan properties from resolved data.""" zwave_value: ZwaveValue = resolved_data["configuration_value"] + if zwave_value is None: + _LOGGER.warning("Unable to read device configuration value") + return None + if zwave_value.value is None: - _LOGGER.warning("Unable to read fan speed configuration value") + _LOGGER.warning("Fan configuration value is missing") return None - speed_config = self.configuration_value_to_speeds.get(zwave_value.value) - if speed_config is None: - _LOGGER.warning("Unrecognized speed configuration value") + fan_value_mapping = self.configuration_value_to_fan_value_mapping.get( + zwave_value.value + ) + if fan_value_mapping is None: + _LOGGER.warning("Unrecognized fan configuration value") return None - return speed_config + return fan_value_mapping @dataclass -class FixedFanSpeedValueMix: +class FixedFanValueMappingValueMix: """Mixin data class for defining supported fan speeds.""" - speeds: list[int] - - def __post_init__(self) -> None: - """ - Validate inputs. - - These inputs are hardcoded in `discovery.py`, so these checks should - only fail due to developer error. - """ - assert len(self.speeds) > 0 - assert sorted(self.speeds) == self.speeds + fan_value_mapping: FanValueMapping @dataclass -class FixedFanSpeedDataTemplate( - BaseDiscoverySchemaDataTemplate, FanSpeedDataTemplate, FixedFanSpeedValueMix +class FixedFanValueMappingDataTemplate( + BaseDiscoverySchemaDataTemplate, + FanValueMappingDataTemplate, + FixedFanValueMappingValueMix, ): """ - Specifies a fixed set of fan speeds. + Specifies a fixed set of properties for a fan. Example: ZWaveDiscoverySchema( platform="fan", - hint="configured_fan_speed", + hint="has_fan_value_mapping", ... - data_template=FixedFanSpeedDataTemplate( - speeds=[32,65,99] + data_template=FixedFanValueMappingDataTemplate( + config=FanValueMapping( + speeds=[(1, 32), (33, 65), (66, 99)] + ) ), ), - - `speeds` indicates the maximum setting on the underlying fan controller - for each actual speed. """ - def get_speed_config( + def get_fan_value_mapping( self, resolved_data: dict[str, ZwaveConfigurationValue] - ) -> list[int]: - """Get the fan speed configuration for this device.""" - return self.speeds + ) -> FanValueMapping: + """Get the fan properties for this device.""" + return self.fan_value_mapping diff --git a/homeassistant/components/zwave_js/entity.py b/homeassistant/components/zwave_js/entity.py index a61fc3765c7..c6ed1902568 100644 --- a/homeassistant/components/zwave_js/entity.py +++ b/homeassistant/components/zwave_js/entity.py @@ -223,7 +223,7 @@ class ZWaveBaseEntity(Entity): value_property: str | int, command_class: int | None = None, endpoint: int | None = None, - value_property_key: int | None = None, + value_property_key: int | str | None = None, add_to_watched_value_ids: bool = True, check_all_endpoints: bool = False, ) -> ZwaveValue | None: diff --git a/homeassistant/components/zwave_js/fan.py b/homeassistant/components/zwave_js/fan.py index cafab8b84a4..21c45bbbf42 100644 --- a/homeassistant/components/zwave_js/fan.py +++ b/homeassistant/components/zwave_js/fan.py @@ -5,15 +5,23 @@ import math from typing import Any, cast from zwave_js_server.client import Client as ZwaveClient -from zwave_js_server.const import TARGET_VALUE_PROPERTY +from zwave_js_server.const import TARGET_VALUE_PROPERTY, CommandClass +from zwave_js_server.const.command_class.thermostat import ( + THERMOSTAT_FAN_OFF_PROPERTY, + THERMOSTAT_FAN_STATE_PROPERTY, +) +from zwave_js_server.model.value import Value as ZwaveValue from homeassistant.components.fan import ( DOMAIN as FAN_DOMAIN, + SUPPORT_PRESET_MODE, SUPPORT_SET_SPEED, FanEntity, + NotValidPresetModeError, ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.percentage import ( @@ -24,13 +32,14 @@ from homeassistant.util.percentage import ( from .const import DATA_CLIENT, DOMAIN from .discovery import ZwaveDiscoveryInfo -from .discovery_data_template import FanSpeedDataTemplate +from .discovery_data_template import FanValueMapping, FanValueMappingDataTemplate from .entity import ZWaveBaseEntity - -SUPPORTED_FEATURES = SUPPORT_SET_SPEED +from .helpers import get_value_of_zwave_value DEFAULT_SPEED_RANGE = (1, 99) # off is not included +ATTR_FAN_STATE = "fan_state" + async def async_setup_entry( hass: HomeAssistant, @@ -44,8 +53,10 @@ async def async_setup_entry( def async_add_fan(info: ZwaveDiscoveryInfo) -> None: """Add Z-Wave fan.""" entities: list[ZWaveBaseEntity] = [] - if info.platform_hint == "configured_fan_speed": - entities.append(ConfiguredSpeedRangeZwaveFan(config_entry, client, info)) + if info.platform_hint == "has_fan_value_mapping": + entities.append(ValueMappingZwaveFan(config_entry, client, info)) + elif info.platform_hint == "thermostat_fan": + entities.append(ZwaveThermostatFan(config_entry, client, info)) else: entities.append(ZwaveFan(config_entry, client, info)) @@ -83,17 +94,18 @@ class ZwaveFan(ZWaveBaseEntity, FanEntity): async def async_turn_on( self, - speed: str | None = None, percentage: int | None = None, preset_mode: str | None = None, **kwargs: Any, ) -> None: """Turn the device on.""" - if percentage is None: + if percentage is not None: + await self.async_set_percentage(percentage) + elif preset_mode is not None: + await self.async_set_preset_mode(preset_mode) + else: # Value 255 tells device to return to previous value await self.info.node.async_set_value(self._target_value, 255) - else: - await self.async_set_percentage(percentage) async def async_turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" @@ -130,11 +142,11 @@ class ZwaveFan(ZWaveBaseEntity, FanEntity): @property def supported_features(self) -> int: """Flag supported features.""" - return SUPPORTED_FEATURES + return SUPPORT_SET_SPEED -class ConfiguredSpeedRangeZwaveFan(ZwaveFan): - """A Zwave fan with a configured speed range (e.g., 1-24 is low).""" +class ValueMappingZwaveFan(ZwaveFan): + """A Zwave fan with a value mapping data (e.g., 1-24 is low).""" def __init__( self, config_entry: ConfigEntry, client: ZwaveClient, info: ZwaveDiscoveryInfo @@ -142,7 +154,7 @@ class ConfiguredSpeedRangeZwaveFan(ZwaveFan): """Initialize the fan.""" super().__init__(config_entry, client, info) self.data_template = cast( - FanSpeedDataTemplate, self.info.platform_data_template + FanValueMappingDataTemplate, self.info.platform_data_template ) async def async_set_percentage(self, percentage: int) -> None: @@ -150,10 +162,21 @@ class ConfiguredSpeedRangeZwaveFan(ZwaveFan): zwave_speed = self.percentage_to_zwave_speed(percentage) await self.info.node.async_set_value(self._target_value, zwave_speed) + async def async_set_preset_mode(self, preset_mode: str) -> None: + """Set new preset mode.""" + for zwave_value, mapped_preset_mode in self.fan_value_mapping.presets.items(): + if preset_mode == mapped_preset_mode: + await self.info.node.async_set_value(self._target_value, zwave_value) + return + + raise NotValidPresetModeError( + f"The preset_mode {preset_mode} is not a valid preset_mode: {self.preset_modes}" + ) + @property def available(self) -> bool: """Return whether the entity is available.""" - return super().available and self.has_speed_configuration + return super().available and self.has_fan_value_mapping @property def percentage(self) -> int | None: @@ -162,6 +185,9 @@ class ConfiguredSpeedRangeZwaveFan(ZwaveFan): # guard missing value return None + if self.preset_mode is not None: + return None + return self.zwave_speed_to_percentage(self.info.primary_value.value) @property @@ -173,26 +199,51 @@ class ConfiguredSpeedRangeZwaveFan(ZwaveFan): return 100 / self.speed_count @property - def has_speed_configuration(self) -> bool: - """Check if the speed configuration is valid.""" - return self.data_template.get_speed_config(self.info.platform_data) is not None + def preset_modes(self) -> list[str]: + """Return the available preset modes.""" + if not self.has_fan_value_mapping: + return [] + + return list(self.fan_value_mapping.presets.values()) @property - def speed_configuration(self) -> list[int]: + def preset_mode(self) -> str | None: + """Return the current preset mode.""" + return self.fan_value_mapping.presets.get(self.info.primary_value.value) + + @property + def has_fan_value_mapping(self) -> bool: + """Check if the speed configuration is valid.""" + return ( + self.data_template.get_fan_value_mapping(self.info.platform_data) + is not None + ) + + @property + def fan_value_mapping(self) -> FanValueMapping: """Return the speed configuration for this fan.""" - speed_configuration = self.data_template.get_speed_config( + fan_value_mapping = self.data_template.get_fan_value_mapping( self.info.platform_data ) # Entity should be unavailable if this isn't set - assert speed_configuration is not None + assert fan_value_mapping is not None - return speed_configuration + return fan_value_mapping @property def speed_count(self) -> int: """Return the number of speeds the fan supports.""" - return len(self.speed_configuration) + return len(self.fan_value_mapping.speeds) + + @property + def supported_features(self) -> int: + """Flag supported features.""" + flags = SUPPORT_SET_SPEED + if self.has_fan_value_mapping and self.fan_value_mapping.presets: + flags |= SUPPORT_PRESET_MODE + + return flags def percentage_to_zwave_speed(self, percentage: int) -> int: """Map a percentage to a ZWave speed.""" @@ -201,27 +252,150 @@ class ConfiguredSpeedRangeZwaveFan(ZwaveFan): # Since the percentage steps are computed with rounding, we have to # search to find the appropriate speed. - for speed_limit in self.speed_configuration: - step_percentage = self.zwave_speed_to_percentage(speed_limit) + for speed_range in self.fan_value_mapping.speeds: + (_, max_speed) = speed_range + step_percentage = self.zwave_speed_to_percentage(max_speed) + + # zwave_speed_to_percentage will only return None if + # `self.fan_value_mapping.speeds` doesn't contain the + # specified speed. This can't happen here, because + # the input is coming from the same data structure. + assert step_percentage + if percentage <= step_percentage: - return speed_limit + return max_speed # This shouldn't actually happen; the last entry in - # `self.speed_configuration` should map to 100%. - return self.speed_configuration[-1] + # `self.fan_value_mapping.speeds` should map to 100%. + (_, last_max_speed) = self.fan_value_mapping.speeds[-1] + return last_max_speed - def zwave_speed_to_percentage(self, zwave_speed: int) -> int: - """Convert a Zwave speed to a percentage.""" + def zwave_speed_to_percentage(self, zwave_speed: int) -> int | None: + """ + Convert a Zwave speed to a percentage. + + This method may return None if the device's value mapping doesn't cover + the specified Z-Wave speed. + """ if zwave_speed == 0: return 0 percentage = 0.0 - for speed_limit in self.speed_configuration: + for speed_range in self.fan_value_mapping.speeds: + (min_speed, max_speed) = speed_range percentage += self.percentage_step - if zwave_speed <= speed_limit: - break + if min_speed <= zwave_speed <= max_speed: + # This choice of rounding function is to provide consistency with how + # the UI handles steps e.g., for a 3-speed fan, you get steps at 33, + # 67, and 100. + return round(percentage) - # This choice of rounding function is to provide consistency with how - # the UI handles steps e.g., for a 3-speed fan, you get steps at 33, - # 67, and 100. - return round(percentage) + # The specified Z-Wave device value doesn't map to a defined speed. + return None + + +class ZwaveThermostatFan(ZWaveBaseEntity, FanEntity): + """Representation of a Z-Wave thermostat fan.""" + + _fan_mode: ZwaveValue + _fan_off: ZwaveValue | None = None + _fan_state: ZwaveValue | None = None + + def __init__( + self, config_entry: ConfigEntry, client: ZwaveClient, info: ZwaveDiscoveryInfo + ) -> None: + """Initialize the thermostat fan.""" + super().__init__(config_entry, client, info) + + self._fan_mode = self.info.primary_value + + self._fan_off = self.get_zwave_value( + THERMOSTAT_FAN_OFF_PROPERTY, + CommandClass.THERMOSTAT_FAN_MODE, + add_to_watched_value_ids=True, + ) + self._fan_state = self.get_zwave_value( + THERMOSTAT_FAN_STATE_PROPERTY, + CommandClass.THERMOSTAT_FAN_STATE, + add_to_watched_value_ids=True, + ) + + async def async_turn_on( + self, + percentage: int | None = None, + preset_mode: str | None = None, + **kwargs: Any, + ) -> None: + """Turn the device on.""" + if not self._fan_off: + raise HomeAssistantError("Unhandled action turn_on") + await self.info.node.async_set_value(self._fan_off, False) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the device off.""" + if not self._fan_off: + raise HomeAssistantError("Unhandled action turn_off") + await self.info.node.async_set_value(self._fan_off, True) + + @property + def is_on(self) -> bool | None: + """Return true if device is on.""" + if (value := get_value_of_zwave_value(self._fan_off)) is None: + return None + return not cast(bool, value) + + @property + def preset_mode(self) -> str | None: + """Return the current preset mode, e.g., auto, smart, interval, favorite.""" + value = get_value_of_zwave_value(self._fan_mode) + if value is None or str(value) not in self._fan_mode.metadata.states: + return None + return cast(str, self._fan_mode.metadata.states[str(value)]) + + async def async_set_preset_mode(self, preset_mode: str) -> None: + """Set new preset mode.""" + + try: + new_state = next( + int(state) + for state, label in self._fan_mode.metadata.states.items() + if label == preset_mode + ) + except StopIteration: + raise ValueError(f"Received an invalid fan mode: {preset_mode}") from None + + await self.info.node.async_set_value(self._fan_mode, new_state) + + @property + def preset_modes(self) -> list[str] | None: + """Return a list of available preset modes.""" + if not self._fan_mode.metadata.states: + return None + return list(self._fan_mode.metadata.states.values()) + + @property + def supported_features(self) -> int: + """Flag supported features.""" + return SUPPORT_PRESET_MODE + + @property + def fan_state(self) -> str | None: + """Return the current state, Idle, Running, etc.""" + value = get_value_of_zwave_value(self._fan_state) + if ( + value is None + or self._fan_state is None + or str(value) not in self._fan_state.metadata.states + ): + return None + return cast(str, self._fan_state.metadata.states[str(value)]) + + @property + def extra_state_attributes(self) -> dict[str, str] | None: + """Return the optional state attributes.""" + attrs = {} + + if state := self.fan_state: + attrs[ATTR_FAN_STATE] = state + + return attrs diff --git a/homeassistant/components/zwave_js/helpers.py b/homeassistant/components/zwave_js/helpers.py index 05df480a487..35f278d4571 100644 --- a/homeassistant/components/zwave_js/helpers.py +++ b/homeassistant/components/zwave_js/helpers.py @@ -2,6 +2,7 @@ from __future__ import annotations from collections.abc import Callable +from dataclasses import astuple, dataclass from typing import Any, cast import voluptuous as vol @@ -14,9 +15,16 @@ from zwave_js_server.model.value import ( get_value_id, ) +from homeassistant.components.group import expand_entity_ids from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.config_entries import ConfigEntry, ConfigEntryState -from homeassistant.const import CONF_TYPE, __version__ as HA_VERSION +from homeassistant.const import ( + ATTR_AREA_ID, + ATTR_DEVICE_ID, + ATTR_ENTITY_ID, + CONF_TYPE, + __version__ as HA_VERSION, +) from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr, entity_registry as er @@ -30,9 +38,25 @@ from .const import ( CONF_DATA_COLLECTION_OPTED_IN, DATA_CLIENT, DOMAIN, + LOGGER, ) +@dataclass +class ZwaveValueID: + """Class to represent a value ID.""" + + property_: str | int | None = None + command_class: int | None = None + endpoint: int | None = None + property_key: str | int | None = None + + def __post_init__(self) -> None: + """Post initialization check.""" + if all(val is None for val in astuple(self)): + raise ValueError("At least one of the fields must be set.") + + @callback def get_value_of_zwave_value(value: ZwaveValue | None) -> Any | None: """Return the value of a ZwaveValue.""" @@ -221,6 +245,40 @@ def async_get_nodes_from_area_id( return nodes +@callback +def async_get_nodes_from_targets( + hass: HomeAssistant, + val: dict[str, Any], + ent_reg: er.EntityRegistry | None = None, + dev_reg: dr.DeviceRegistry | None = None, +) -> set[ZwaveNode]: + """ + Get nodes for all targets. + + Supports entity_id with group expansion, area_id, and device_id. + """ + nodes: set[ZwaveNode] = set() + # Convert all entity IDs to nodes + for entity_id in expand_entity_ids(hass, val.get(ATTR_ENTITY_ID, [])): + try: + nodes.add(async_get_node_from_entity_id(hass, entity_id, ent_reg, dev_reg)) + except ValueError as err: + LOGGER.warning(err.args[0]) + + # Convert all area IDs to nodes + for area_id in val.get(ATTR_AREA_ID, []): + nodes.update(async_get_nodes_from_area_id(hass, area_id, ent_reg, dev_reg)) + + # Convert all device IDs to nodes + for device_id in val.get(ATTR_DEVICE_ID, []): + try: + nodes.add(async_get_node_from_device_id(hass, device_id, dev_reg)) + except ValueError as err: + LOGGER.warning(err.args[0]) + + return nodes + + def get_zwave_value_from_config(node: ZwaveNode, config: ConfigType) -> ZwaveValue: """Get a Z-Wave JS Value from a config.""" endpoint = None diff --git a/homeassistant/components/zwave_js/manifest.json b/homeassistant/components/zwave_js/manifest.json index 8d1a9091bc0..c3968181563 100644 --- a/homeassistant/components/zwave_js/manifest.json +++ b/homeassistant/components/zwave_js/manifest.json @@ -8,9 +8,26 @@ "dependencies": ["usb", "http", "websocket_api"], "iot_class": "local_push", "usb": [ - {"vid":"0658","pid":"0200","known_devices":["Aeotec Z-Stick Gen5+", "Z-WaveMe UZB"]}, - {"vid":"10C4","pid":"8A2A","description":"*z-wave*","known_devices":["Nortek HUSBZB-1"]}, - {"vid":"10C4","pid":"EA60","known_devices":["Aeotec Z-Stick 7", "Silicon Labs UZB-7", "Zooz ZST10 700"]} + { + "vid": "0658", + "pid": "0200", + "known_devices": ["Aeotec Z-Stick Gen5+", "Z-WaveMe UZB"] + }, + { + "vid": "10C4", + "pid": "8A2A", + "description": "*z-wave*", + "known_devices": ["Nortek HUSBZB-1"] + }, + { + "vid": "10C4", + "pid": "EA60", + "known_devices": [ + "Aeotec Z-Stick 7", + "Silicon Labs UZB-7", + "Zooz ZST10 700" + ] + } ], "loggers": ["zwave_js_server"] } diff --git a/homeassistant/components/zwave_js/services.py b/homeassistant/components/zwave_js/services.py index 767516cc17c..2d31bed108f 100644 --- a/homeassistant/components/zwave_js/services.py +++ b/homeassistant/components/zwave_js/services.py @@ -25,11 +25,8 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send from . import const -from .helpers import ( - async_get_node_from_device_id, - async_get_node_from_entity_id, - async_get_nodes_from_area_id, -) +from .config_validation import BITMASK_SCHEMA, VALUE_SCHEMA +from .helpers import async_get_nodes_from_targets _LOGGER = logging.getLogger(__name__) @@ -80,38 +77,16 @@ class ZWaveServices: @callback def get_nodes_from_service_data(val: dict[str, Any]) -> dict[str, Any]: """Get nodes set from service data.""" - nodes: set[ZwaveNode] = set() - # Convert all entity IDs to nodes - for entity_id in expand_entity_ids(self._hass, val.pop(ATTR_ENTITY_ID, [])): - try: - nodes.add( - async_get_node_from_entity_id( - self._hass, entity_id, self._ent_reg, self._dev_reg - ) - ) - except ValueError as err: - const.LOGGER.warning(err.args[0]) + val[const.ATTR_NODES] = async_get_nodes_from_targets( + self._hass, val, self._ent_reg, self._dev_reg + ) + return val - # Convert all area IDs to nodes - for area_id in val.pop(ATTR_AREA_ID, []): - nodes.update( - async_get_nodes_from_area_id( - self._hass, area_id, self._ent_reg, self._dev_reg - ) - ) - - # Convert all device IDs to nodes - for device_id in val.pop(ATTR_DEVICE_ID, []): - try: - nodes.add( - async_get_node_from_device_id( - self._hass, device_id, self._dev_reg - ) - ) - except ValueError as err: - const.LOGGER.warning(err.args[0]) - - val[const.ATTR_NODES] = nodes + @callback + def has_at_least_one_node(val: dict[str, Any]) -> dict[str, Any]: + """Validate that at least one node is specified.""" + if not val.get(const.ATTR_NODES): + raise vol.Invalid(f"No {const.DOMAIN} nodes found for given targets") return val @callback @@ -120,6 +95,9 @@ class ZWaveServices: nodes: set[ZwaveNode] = val[const.ATTR_NODES] broadcast: bool = val[const.ATTR_BROADCAST] + if not broadcast: + has_at_least_one_node(val) + # User must specify a node if they are attempting a broadcast and have more # than one zwave-js network. if ( @@ -150,12 +128,20 @@ class ZWaveServices: def validate_entities(val: dict[str, Any]) -> dict[str, Any]: """Validate entities exist and are from the zwave_js platform.""" val[ATTR_ENTITY_ID] = expand_entity_ids(self._hass, val[ATTR_ENTITY_ID]) + invalid_entities = [] for entity_id in val[ATTR_ENTITY_ID]: entry = self._ent_reg.async_get(entity_id) if entry is None or entry.platform != const.DOMAIN: - raise vol.Invalid( - f"Entity {entity_id} is not a valid {const.DOMAIN} entity." + const.LOGGER.info( + "Entity %s is not a valid %s entity.", entity_id, const.DOMAIN ) + invalid_entities.append(entity_id) + + # Remove invalid entities + val[ATTR_ENTITY_ID] = list(set(val[ATTR_ENTITY_ID]) - set(invalid_entities)) + + if not val[ATTR_ENTITY_ID]: + raise vol.Invalid(f"No {const.DOMAIN} entities found in service call") return val @@ -177,10 +163,10 @@ class ZWaveServices: vol.Coerce(int), cv.string ), vol.Optional(const.ATTR_CONFIG_PARAMETER_BITMASK): vol.Any( - vol.Coerce(int), const.BITMASK_SCHEMA + vol.Coerce(int), BITMASK_SCHEMA ), vol.Required(const.ATTR_CONFIG_VALUE): vol.Any( - vol.Coerce(int), const.BITMASK_SCHEMA, cv.string + vol.Coerce(int), BITMASK_SCHEMA, cv.string ), }, cv.has_at_least_one_key( @@ -188,6 +174,7 @@ class ZWaveServices: ), parameter_name_does_not_need_bitmask, get_nodes_from_service_data, + has_at_least_one_node, ), ), ) @@ -211,10 +198,8 @@ class ZWaveServices: vol.Coerce(int), { vol.Any( - vol.Coerce(int), const.BITMASK_SCHEMA, cv.string - ): vol.Any( - vol.Coerce(int), const.BITMASK_SCHEMA, cv.string - ) + vol.Coerce(int), BITMASK_SCHEMA, cv.string + ): vol.Any(vol.Coerce(int), BITMASK_SCHEMA, cv.string) }, ), }, @@ -222,6 +207,7 @@ class ZWaveServices: ATTR_DEVICE_ID, ATTR_ENTITY_ID, ATTR_AREA_ID ), get_nodes_from_service_data, + has_at_least_one_node, ), ), ) @@ -265,16 +251,15 @@ class ZWaveServices: vol.Coerce(int), str ), vol.Optional(const.ATTR_ENDPOINT): vol.Coerce(int), - vol.Required(const.ATTR_VALUE): const.VALUE_SCHEMA, + vol.Required(const.ATTR_VALUE): VALUE_SCHEMA, vol.Optional(const.ATTR_WAIT_FOR_RESULT): cv.boolean, - vol.Optional(const.ATTR_OPTIONS): { - cv.string: const.VALUE_SCHEMA - }, + vol.Optional(const.ATTR_OPTIONS): {cv.string: VALUE_SCHEMA}, }, cv.has_at_least_one_key( ATTR_DEVICE_ID, ATTR_ENTITY_ID, ATTR_AREA_ID ), get_nodes_from_service_data, + has_at_least_one_node, ), ), ) @@ -302,10 +287,8 @@ class ZWaveServices: vol.Coerce(int), str ), vol.Optional(const.ATTR_ENDPOINT): vol.Coerce(int), - vol.Required(const.ATTR_VALUE): const.VALUE_SCHEMA, - vol.Optional(const.ATTR_OPTIONS): { - cv.string: const.VALUE_SCHEMA - }, + vol.Required(const.ATTR_VALUE): VALUE_SCHEMA, + vol.Optional(const.ATTR_OPTIONS): {cv.string: VALUE_SCHEMA}, }, vol.Any( cv.has_at_least_one_key( @@ -338,6 +321,7 @@ class ZWaveServices: ATTR_DEVICE_ID, ATTR_ENTITY_ID, ATTR_AREA_ID ), get_nodes_from_service_data, + has_at_least_one_node, ), ), ) diff --git a/homeassistant/components/zwave_js/siren.py b/homeassistant/components/zwave_js/siren.py index 4ef89b9f4cd..a3fe3dfd595 100644 --- a/homeassistant/components/zwave_js/siren.py +++ b/homeassistant/components/zwave_js/siren.py @@ -68,8 +68,10 @@ class ZwaveSirenEntity(ZWaveBaseEntity, SirenEntity): self._attr_supported_features |= SUPPORT_TONES @property - def is_on(self) -> bool: + def is_on(self) -> bool | None: """Return whether device is on.""" + if self.info.primary_value.value is None: + return None return bool(self.info.primary_value.value) async def async_set_value( diff --git a/homeassistant/components/zwave_js/translations/fr.json b/homeassistant/components/zwave_js/translations/fr.json index 0933879e517..2645e573b6d 100644 --- a/homeassistant/components/zwave_js/translations/fr.json +++ b/homeassistant/components/zwave_js/translations/fr.json @@ -15,7 +15,7 @@ "error": { "addon_start_failed": "\u00c9chec du d\u00e9marrage du module compl\u00e9mentaire Z-Wave JS. V\u00e9rifiez la configuration.", "cannot_connect": "\u00c9chec de connexion", - "invalid_ws_url": "URL websocket invalide", + "invalid_ws_url": "URL websocket non valide", "unknown": "Erreur inattendue" }, "flow_title": "{name}", @@ -65,8 +65,8 @@ "device_automation": { "action_type": { "clear_lock_usercode": "Effacer le code utilisateur sur {entity_name}", - "ping": "Pinger l'appareil", - "refresh_value": "Actualisez la ou les valeurs de {entity_name}", + "ping": "Envoyer un \u00ab\u00a0ping\u00a0\u00bb \u00e0 l'appareil", + "refresh_value": "Actualiser la ou les valeurs de {entity_name}", "reset_meter": "R\u00e9initialiser les compteurs sur {subtype}", "set_config_parameter": "D\u00e9finir la valeur du param\u00e8tre de configuration {subtype}", "set_lock_usercode": "D\u00e9finir un code utilisateur sur {entity_name}", @@ -97,11 +97,11 @@ "addon_start_failed": "\u00c9chec du d\u00e9marrage du module compl\u00e9mentaire Z-Wave JS.", "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", "cannot_connect": "\u00c9chec de connexion", - "different_device": "Le p\u00e9riph\u00e9rique USB connect\u00e9 n'est pas le m\u00eame que pr\u00e9c\u00e9demment configur\u00e9 pour cette entr\u00e9e de configuration. Veuillez plut\u00f4t cr\u00e9er une nouvelle entr\u00e9e de configuration pour le nouveau p\u00e9riph\u00e9rique." + "different_device": "Le p\u00e9riph\u00e9rique USB connect\u00e9 n'est pas le m\u00eame que celui qui \u00e9tait pr\u00e9c\u00e9demment configur\u00e9 pour cette entr\u00e9e de configuration. Veuillez \u00e0 la place cr\u00e9er une nouvelle entr\u00e9e de configuration pour le nouveau p\u00e9riph\u00e9rique." }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_ws_url": "URL websocket invalide", + "invalid_ws_url": "URL websocket non valide", "unknown": "Erreur inattendue" }, "progress": { diff --git a/homeassistant/components/zwave_js/translations/zh-Hant.json b/homeassistant/components/zwave_js/translations/zh-Hant.json index 9dd44c4457f..664cd6b48d9 100644 --- a/homeassistant/components/zwave_js/translations/zh-Hant.json +++ b/homeassistant/components/zwave_js/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "addon_get_discovery_info_failed": "\u53d6\u5f97 Z-Wave JS \u9644\u52a0\u5143\u4ef6\u63a2\u7d22\u8cc7\u8a0a\u5931\u6557\u3002", + "addon_get_discovery_info_failed": "\u53d6\u5f97 Z-Wave JS \u9644\u52a0\u5143\u4ef6\u641c\u7d22\u8cc7\u8a0a\u5931\u6557\u3002", "addon_info_failed": "\u53d6\u5f97 Z-Wave JS \u9644\u52a0\u5143\u4ef6\u8cc7\u8a0a\u5931\u6557\u3002", "addon_install_failed": "Z-Wave JS \u9644\u52a0\u5143\u4ef6\u5b89\u88dd\u5931\u6557\u3002", "addon_set_config_failed": "Z-Wave JS \u9644\u52a0\u5143\u4ef6\u8a2d\u5b9a\u5931\u6557\u3002", @@ -9,7 +9,7 @@ "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", - "discovery_requires_supervisor": "\u63a2\u7d22\u529f\u80fd\u9700\u8981 Supervisor \u6b0a\u9650\u3002", + "discovery_requires_supervisor": "\u641c\u7d22\u529f\u80fd\u9700\u8981 Supervisor \u6b0a\u9650\u3002", "not_zwave_device": "\u6240\u767c\u73fe\u7684\u88dd\u7f6e\u4e26\u975e Z-Wave \u88dd\u7f6e" }, "error": { @@ -52,7 +52,7 @@ "use_addon": "\u4f7f\u7528 Z-Wave JS Supervisor \u9644\u52a0\u5143\u4ef6" }, "description": "\u662f\u5426\u8981\u4f7f\u7528 Z-Wave JS Supervisor \u9644\u52a0\u5143\u4ef6\uff1f", - "title": "\u9078\u64c7\u9023\u7dda\u985e\u578b" + "title": "\u9078\u64c7\u9023\u7dda\u985e\u5225" }, "start_addon": { "title": "Z-Wave JS \u9644\u52a0\u5143\u4ef6\u555f\u59cb\u4e2d\u3002" @@ -90,7 +90,7 @@ }, "options": { "abort": { - "addon_get_discovery_info_failed": "\u53d6\u5f97 Z-Wave JS \u9644\u52a0\u5143\u4ef6\u63a2\u7d22\u8cc7\u8a0a\u5931\u6557\u3002", + "addon_get_discovery_info_failed": "\u53d6\u5f97 Z-Wave JS \u9644\u52a0\u5143\u4ef6\u641c\u7d22\u8cc7\u8a0a\u5931\u6557\u3002", "addon_info_failed": "\u53d6\u5f97 Z-Wave JS \u9644\u52a0\u5143\u4ef6\u8cc7\u8a0a\u5931\u6557\u3002", "addon_install_failed": "Z-Wave JS \u9644\u52a0\u5143\u4ef6\u5b89\u88dd\u5931\u6557\u3002", "addon_set_config_failed": "Z-Wave JS \u9644\u52a0\u5143\u4ef6\u8a2d\u5b9a\u5931\u6557\u3002", @@ -136,7 +136,7 @@ "use_addon": "\u4f7f\u7528 Z-Wave JS Supervisor \u9644\u52a0\u5143\u4ef6" }, "description": "\u662f\u5426\u8981\u4f7f\u7528 Z-Wave JS Supervisor \u9644\u52a0\u5143\u4ef6\uff1f", - "title": "\u9078\u64c7\u9023\u7dda\u985e\u578b" + "title": "\u9078\u64c7\u9023\u7dda\u985e\u5225" }, "start_addon": { "title": "Z-Wave JS \u9644\u52a0\u5143\u4ef6\u555f\u59cb\u4e2d\u3002" diff --git a/homeassistant/components/zwave_js/triggers/event.py b/homeassistant/components/zwave_js/triggers/event.py index 110cd21294f..fd46c89832b 100644 --- a/homeassistant/components/zwave_js/triggers/event.py +++ b/homeassistant/components/zwave_js/triggers/event.py @@ -20,13 +20,13 @@ from homeassistant.components.zwave_js.const import ( ATTR_EVENT_DATA, ATTR_EVENT_SOURCE, ATTR_NODE_ID, + ATTR_NODES, ATTR_PARTIAL_DICT_MATCH, DATA_CLIENT, DOMAIN, ) from homeassistant.components.zwave_js.helpers import ( - async_get_node_from_device_id, - async_get_node_from_entity_id, + async_get_nodes_from_targets, get_device_id, get_home_and_node_id_from_device_entry, ) @@ -111,6 +111,13 @@ async def async_validate_trigger_config( """Validate config.""" config = TRIGGER_SCHEMA(config) + if config[ATTR_EVENT_SOURCE] == "node": + config[ATTR_NODES] = async_get_nodes_from_targets(hass, config) + if not config[ATTR_NODES]: + raise vol.Invalid( + f"No nodes found for given {ATTR_DEVICE_ID}s or {ATTR_ENTITY_ID}s." + ) + if ATTR_CONFIG_ENTRY_ID not in config: return config @@ -133,21 +140,7 @@ async def async_attach_trigger( platform_type: str = PLATFORM_TYPE, ) -> CALLBACK_TYPE: """Listen for state changes based on configuration.""" - nodes: set[Node] = set() - if ATTR_DEVICE_ID in config: - nodes.update( - { - async_get_node_from_device_id(hass, device_id) - for device_id in config[ATTR_DEVICE_ID] - } - ) - if ATTR_ENTITY_ID in config: - nodes.update( - { - async_get_node_from_entity_id(hass, entity_id) - for entity_id in config[ATTR_ENTITY_ID] - } - ) + nodes: set[Node] = config.get(ATTR_NODES, {}) event_source = config[ATTR_EVENT_SOURCE] event_name = config[ATTR_EVENT] diff --git a/homeassistant/components/zwave_js/triggers/value_updated.py b/homeassistant/components/zwave_js/triggers/value_updated.py index 71223c4ef1e..8a0b287c26b 100644 --- a/homeassistant/components/zwave_js/triggers/value_updated.py +++ b/homeassistant/components/zwave_js/triggers/value_updated.py @@ -20,6 +20,7 @@ from homeassistant.components.zwave_js.const import ( ATTR_CURRENT_VALUE_RAW, ATTR_ENDPOINT, ATTR_NODE_ID, + ATTR_NODES, ATTR_PREVIOUS_VALUE, ATTR_PREVIOUS_VALUE_RAW, ATTR_PROPERTY, @@ -29,8 +30,7 @@ from homeassistant.components.zwave_js.const import ( DOMAIN, ) from homeassistant.components.zwave_js.helpers import ( - async_get_node_from_device_id, - async_get_node_from_entity_id, + async_get_nodes_from_targets, get_device_id, ) from homeassistant.const import ATTR_DEVICE_ID, ATTR_ENTITY_ID, CONF_PLATFORM, MATCH_ALL @@ -38,20 +38,14 @@ from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback from homeassistant.helpers import config_validation as cv, device_registry as dr from homeassistant.helpers.typing import ConfigType +from ..config_validation import VALUE_SCHEMA + # Platform type should be . PLATFORM_TYPE = f"{DOMAIN}.{__name__.rsplit('.', maxsplit=1)[-1]}" ATTR_FROM = "from" ATTR_TO = "to" -VALUE_SCHEMA = vol.Any( - bool, - vol.Coerce(int), - vol.Coerce(float), - cv.boolean, - cv.string, -) - TRIGGER_SCHEMA = vol.All( cv.TRIGGER_BASE_SCHEMA.extend( { @@ -76,6 +70,20 @@ TRIGGER_SCHEMA = vol.All( ) +async def async_validate_trigger_config( + hass: HomeAssistant, config: ConfigType +) -> ConfigType: + """Validate config.""" + config = TRIGGER_SCHEMA(config) + + config[ATTR_NODES] = async_get_nodes_from_targets(hass, config) + if not config[ATTR_NODES]: + raise vol.Invalid( + f"No nodes found for given {ATTR_DEVICE_ID}s or {ATTR_ENTITY_ID}s." + ) + return config + + async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, @@ -85,21 +93,7 @@ async def async_attach_trigger( platform_type: str = PLATFORM_TYPE, ) -> CALLBACK_TYPE: """Listen for state changes based on configuration.""" - nodes: set[Node] = set() - if ATTR_DEVICE_ID in config: - nodes.update( - { - async_get_node_from_device_id(hass, device_id) - for device_id in config.get(ATTR_DEVICE_ID, []) - } - ) - if ATTR_ENTITY_ID in config: - nodes.update( - { - async_get_node_from_entity_id(hass, entity_id) - for entity_id in config.get(ATTR_ENTITY_ID, []) - } - ) + nodes: set[Node] = config[ATTR_NODES] from_value = config[ATTR_FROM] to_value = config[ATTR_TO] diff --git a/homeassistant/components/zwave_me/config_flow.py b/homeassistant/components/zwave_me/config_flow.py index a4b257bdab5..4fee380ca48 100644 --- a/homeassistant/components/zwave_me/config_flow.py +++ b/homeassistant/components/zwave_me/config_flow.py @@ -26,6 +26,13 @@ class ZWaveMeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user(self, user_input=None): """Handle a flow initialized by the user or started with zeroconf.""" errors = {} + placeholders = { + "local_token": "/112f7a4a-0051-cc2b-3b61-1898181b9950", + "find_token": "0481effe8a5c6f757b455babb678dc0e764feae279/112f7a4a-0051-cc2b-3b61-1898181b9950", + "local_url": "192.168.1.39:8083", + "find_url": "wss://find.z-wave.me", + "remote_url": "wss://87.250.250.242:8083", + } if self.url is None: schema = vol.Schema( { @@ -64,6 +71,7 @@ class ZWaveMeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_show_form( step_id="user", + description_placeholders=placeholders, data_schema=schema, errors=errors, ) diff --git a/homeassistant/components/zwave_me/const.py b/homeassistant/components/zwave_me/const.py index ccbf6989f07..cbb096c91f3 100644 --- a/homeassistant/components/zwave_me/const.py +++ b/homeassistant/components/zwave_me/const.py @@ -12,10 +12,12 @@ class ZWaveMePlatform(StrEnum): BINARY_SENSOR = "sensorBinary" BUTTON = "toggleButton" CLIMATE = "thermostat" + COVER = "motor" LOCK = "doorlock" NUMBER = "switchMultilevel" SWITCH = "switchBinary" SENSOR = "sensorMultilevel" + SIREN = "siren" RGBW_LIGHT = "switchRGBW" RGB_LIGHT = "switchRGB" @@ -24,9 +26,11 @@ PLATFORMS = [ Platform.BINARY_SENSOR, Platform.BUTTON, Platform.CLIMATE, + Platform.COVER, Platform.LIGHT, Platform.LOCK, Platform.NUMBER, Platform.SENSOR, + Platform.SIREN, Platform.SWITCH, ] diff --git a/homeassistant/components/zwave_me/cover.py b/homeassistant/components/zwave_me/cover.py new file mode 100644 index 00000000000..0425a99b568 --- /dev/null +++ b/homeassistant/components/zwave_me/cover.py @@ -0,0 +1,72 @@ +"""Representation of a cover.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.components.cover import ( + ATTR_POSITION, + SUPPORT_CLOSE, + SUPPORT_OPEN, + SUPPORT_SET_POSITION, + CoverEntity, +) +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from . import ZWaveMeEntity +from .const import DOMAIN, ZWaveMePlatform + +DEVICE_NAME = ZWaveMePlatform.COVER + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the cover platform.""" + + @callback + def add_new_device(new_device): + controller = hass.data[DOMAIN][config_entry.entry_id] + cover = ZWaveMeCover(controller, new_device) + + async_add_entities( + [ + cover, + ] + ) + + config_entry.async_on_unload( + async_dispatcher_connect( + hass, f"ZWAVE_ME_NEW_{DEVICE_NAME.upper()}", add_new_device + ) + ) + + +class ZWaveMeCover(ZWaveMeEntity, CoverEntity): + """Representation of a ZWaveMe Multilevel Cover.""" + + def close_cover(self, **kwargs): + """Close cover.""" + self.controller.zwave_api.send_command(self.device.id, "exact?level=0") + + def open_cover(self, **kwargs): + """Open cover.""" + self.controller.zwave_api.send_command(self.device.id, "exact?level=99") + + def set_cover_position(self, **kwargs: Any) -> None: + """Update the current value.""" + value = kwargs[ATTR_POSITION] + self.controller.zwave_api.send_command( + self.device.id, f"exact?level={str(min(value, 99))}" + ) + + @property + def current_cover_position(self) -> int | None: + """Return current position of cover. + + None is unknown, 0 is closed, 100 is fully open. + """ + return self.device.level + + @property + def supported_features(self) -> int: + """Return the supported features.""" + return SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_SET_POSITION diff --git a/homeassistant/components/zwave_me/manifest.json b/homeassistant/components/zwave_me/manifest.json index 8863cd6ebf7..a3303bb9de4 100644 --- a/homeassistant/components/zwave_me/manifest.json +++ b/homeassistant/components/zwave_me/manifest.json @@ -3,15 +3,9 @@ "name": "Z-Wave.Me", "documentation": "https://www.home-assistant.io/integrations/zwave_me", "iot_class": "local_push", - "requirements": [ - "zwave_me_ws==0.2.1", - "url-normalize==1.4.1" - ], + "requirements": ["zwave_me_ws==0.2.3", "url-normalize==1.4.1"], "after_dependencies": ["zeroconf"], - "zeroconf": [{"type":"_hap._tcp.local.", "name": "*z.wave-me*"}], + "zeroconf": [{ "type": "_hap._tcp.local.", "name": "*z.wave-me*" }], "config_flow": true, - "codeowners": [ - "@lawfulchaos", - "@Z-Wave-Me" - ] + "codeowners": ["@lawfulchaos", "@Z-Wave-Me"] } diff --git a/homeassistant/components/zwave_me/siren.py b/homeassistant/components/zwave_me/siren.py new file mode 100644 index 00000000000..c528f610132 --- /dev/null +++ b/homeassistant/components/zwave_me/siren.py @@ -0,0 +1,49 @@ +"""Representation of a sirenBinary.""" +from typing import Any + +from homeassistant.components.siren import SirenEntity +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from . import ZWaveMeEntity +from .const import DOMAIN, ZWaveMePlatform + +DEVICE_NAME = ZWaveMePlatform.SIREN + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the siren platform.""" + + @callback + def add_new_device(new_device): + controller = hass.data[DOMAIN][config_entry.entry_id] + siren = ZWaveMeSiren(controller, new_device) + + async_add_entities( + [ + siren, + ] + ) + + config_entry.async_on_unload( + async_dispatcher_connect( + hass, f"ZWAVE_ME_NEW_{DEVICE_NAME.upper()}", add_new_device + ) + ) + + +class ZWaveMeSiren(ZWaveMeEntity, SirenEntity): + """Representation of a ZWaveMe siren.""" + + @property + def is_on(self) -> bool: + """Return the state of the siren.""" + return self.device.level == "on" + + def turn_on(self, **kwargs: Any) -> None: + """Turn the entity on.""" + self.controller.zwave_api.send_command(self.device.id, "on") + + def turn_off(self, **kwargs: Any) -> None: + """Turn the entity off.""" + self.controller.zwave_api.send_command(self.device.id, "off") diff --git a/homeassistant/components/zwave_me/strings.json b/homeassistant/components/zwave_me/strings.json index 4986de744c0..00c57bfeec7 100644 --- a/homeassistant/components/zwave_me/strings.json +++ b/homeassistant/components/zwave_me/strings.json @@ -2,10 +2,10 @@ "config": { "step": { "user": { - "description": "Input IP address of Z-Way server and Z-Way access token. IP address can be prefixed with wss:// if HTTPS should be used instead of HTTP. To get the token go to the Z-Way user interface > Menu > Settings > User > API token. It is suggested to create a new user for Home Assistant and grant access to devices you need to control from Home Assistant. It is also possible to use remote access via find.z-wave.me to connect a remote Z-Way. Input wss://find.z-wave.me in IP field and copy the token with Global scope (log-in to Z-Way via find.z-wave.me for this).", + "description": "Input IP address with port and access token of Z-Way server. To get the token go to the Z-Way user interface Smart Home UI > Menu > Settings > Users > Administrator > API token.\n\nExample of connecting to Z-Way in the local network:\nURL: {local_url}\nToken: {local_token}\n\nExample of connecting to Z-Way via remote access find.z-wave.me:\nURL: {find_url}\nToken: {find_token}\n\nExample of connecting to Z-Way with a static public IP address:\nURL: {remote_url}\nToken: {local_token}\n\nWhen connecting via find.z-wave.me you need to use a token with a global scope (log-in to Z-Way via find.z-wave.me for this).", "data": { "url": "[%key:common::config_flow::data::url%]", - "token": "Token" + "token": "[%key:common::config_flow::data::api_token%]" } } }, diff --git a/homeassistant/config.py b/homeassistant/config.py index 17a1c0fbfa1..d5f9eb91b62 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -86,7 +86,7 @@ INTEGRATION_LOAD_EXCEPTIONS = ( ) DEFAULT_CONFIG = f""" -# Configure a default setup of Home Assistant (frontend, api, etc) +# Loads default set of integrations. Do not remove. default_config: # Text to speech diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 7bc37bcd305..933cebebcf5 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -1187,7 +1187,7 @@ async def _old_conf_migrator(old_config: dict[str, Any]) -> dict[str, Any]: 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: + def __init_subclass__(cls, *, domain: str | None = None, **kwargs: Any) -> None: """Initialize a subclass, register if possible.""" super().__init_subclass__(**kwargs) if domain is not None: @@ -1230,7 +1230,7 @@ class ConfigFlow(data_entry_flow.FlowHandler): @callback def _abort_if_unique_id_configured( self, - updates: dict[Any, Any] | None = None, + updates: dict[str, Any] | None = None, reload_on_update: bool = True, ) -> None: """Abort if the unique ID is already configured.""" @@ -1455,7 +1455,7 @@ class ConfigFlow(data_entry_flow.FlowHandler): return await self.async_step_discovery(dataclasses.asdict(discovery_info)) @callback - def async_create_entry( # pylint: disable=arguments-differ + def async_create_entry( self, *, title: str, diff --git a/homeassistant/const.py b/homeassistant/const.py index e7adb5e18f8..53e52f11e67 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -6,8 +6,8 @@ from typing import Final from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 -MINOR_VERSION: Final = 3 -PATCH_VERSION: Final = "8" +MINOR_VERSION: Final = 4 +PATCH_VERSION: Final = "0" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) @@ -50,6 +50,7 @@ class Platform(StrEnum): SWITCH = "switch" TTS = "tts" VACUUM = "vacuum" + UPDATE = "update" WATER_HEATER = "water_heater" WEATHER = "weather" @@ -168,6 +169,7 @@ CONF_IP_ADDRESS: Final = "ip_address" CONF_LATITUDE: Final = "latitude" CONF_LEGACY_TEMPLATES: Final = "legacy_templates" CONF_LIGHTS: Final = "lights" +CONF_LOCATION: Final = "location" CONF_LONGITUDE: Final = "longitude" CONF_MAC: Final = "mac" CONF_MAXIMUM: Final = "maximum" @@ -175,6 +177,7 @@ CONF_MEDIA_DIRS: Final = "media_dirs" CONF_METHOD: Final = "method" CONF_MINIMUM: Final = "minimum" CONF_MODE: Final = "mode" +CONF_MODEL: Final = "model" CONF_MONITORED_CONDITIONS: Final = "monitored_conditions" CONF_MONITORED_VARIABLES: Final = "monitored_variables" CONF_NAME: Final = "name" diff --git a/homeassistant/core.py b/homeassistant/core.py index 27dba3cbc52..733281aa5e6 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -99,15 +99,13 @@ STAGE_3_SHUTDOWN_TIMEOUT = 30 block_async_io.enable() -T = TypeVar("T") +_T = TypeVar("_T") _R = TypeVar("_R") -_R_co = TypeVar("_R_co", covariant=True) # pylint: disable=invalid-name +_R_co = TypeVar("_R_co", covariant=True) # Internal; not helpers.typing.UNDEFINED due to circular dependency _UNDEF: dict[Any, Any] = {} -# pylint: disable=invalid-name -CALLABLE_T = TypeVar("CALLABLE_T", bound=Callable[..., Any]) -CALLBACK_TYPE = Callable[[], None] -# pylint: enable=invalid-name +_CallableT = TypeVar("_CallableT", bound=Callable[..., Any]) +CALLBACK_TYPE = Callable[[], None] # pylint: disable=invalid-name CORE_STORAGE_KEY = "core.config" CORE_STORAGE_VERSION = 1 @@ -165,7 +163,7 @@ def valid_state(state: str) -> bool: return len(state) <= MAX_LENGTH_STATE_STATE -def callback(func: CALLABLE_T) -> CALLABLE_T: +def callback(func: _CallableT) -> _CallableT: """Annotation to mark method as safe to call from within the event loop.""" setattr(func, "_hass_callback", True) return func @@ -480,8 +478,8 @@ class HomeAssistant: @callback def async_add_executor_job( - self, target: Callable[..., T], *args: Any - ) -> asyncio.Future[T]: + self, target: Callable[..., _T], *args: Any + ) -> asyncio.Future[_T]: """Add an executor job from within the event loop.""" task = self.loop.run_in_executor(None, target, *args) @@ -701,7 +699,7 @@ class HomeAssistant: class Context: """The context that triggered something.""" - user_id: str = attr.ib(default=None) + user_id: str | None = attr.ib(default=None) parent_id: str | None = attr.ib(default=None) id: str = attr.ib(factory=uuid_util.random_uuid_hex) diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index b69cf44dc6c..628a89dd89b 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -21,6 +21,7 @@ RESULT_TYPE_EXTERNAL_STEP = "external" RESULT_TYPE_EXTERNAL_STEP_DONE = "external_done" RESULT_TYPE_SHOW_PROGRESS = "progress" RESULT_TYPE_SHOW_PROGRESS_DONE = "progress_done" +RESULT_TYPE_MENU = "menu" # Event that is fired when a flow is progressed via external or progress source. EVENT_DATA_ENTRY_FLOW_PROGRESSED = "data_entry_flow_progressed" @@ -69,7 +70,7 @@ class FlowResult(TypedDict, total=False): title: str data: Mapping[str, Any] step_id: str - data_schema: vol.Schema + data_schema: vol.Schema | None extra: str required: bool errors: dict[str, str] | None @@ -82,6 +83,7 @@ class FlowResult(TypedDict, total=False): result: Any last_step: bool | None options: Mapping[str, Any] + menu_options: list[str] | dict[str, str] @callback @@ -249,7 +251,15 @@ class FlowManager(abc.ABC): 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) + # Handle a menu navigation choice + if cur_step["type"] == RESULT_TYPE_MENU and user_input: + result = await self._async_handle_step( + flow, user_input["next_step_id"], None + ) + else: + 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 ( @@ -343,6 +353,7 @@ class FlowManager(abc.ABC): RESULT_TYPE_EXTERNAL_STEP_DONE, RESULT_TYPE_SHOW_PROGRESS, RESULT_TYPE_SHOW_PROGRESS_DONE, + RESULT_TYPE_MENU, ): raise ValueError(f"Handler returned incorrect type: {result['type']}") @@ -352,6 +363,7 @@ class FlowManager(abc.ABC): RESULT_TYPE_EXTERNAL_STEP_DONE, RESULT_TYPE_SHOW_PROGRESS, RESULT_TYPE_SHOW_PROGRESS_DONE, + RESULT_TYPE_MENU, ): flow.cur_step = result return result @@ -408,7 +420,7 @@ class FlowHandler: self, *, step_id: str, - data_schema: vol.Schema = None, + data_schema: vol.Schema | None = None, errors: dict[str, str] | None = None, description_placeholders: dict[str, Any] | None = None, last_step: bool | None = None, @@ -507,6 +519,28 @@ class FlowHandler: "step_id": next_step_id, } + @callback + def async_show_menu( + self, + *, + step_id: str, + menu_options: list[str] | dict[str, str], + description_placeholders: dict | None = None, + ) -> FlowResult: + """Show a navigation menu to the user. + + Options dict maps step_id => i18n label + """ + return { + "type": RESULT_TYPE_MENU, + "flow_id": self.flow_id, + "handler": self.handler, + "step_id": step_id, + "data_schema": vol.Schema({"next_step_id": vol.In(menu_options)}), + "menu_options": menu_options, + "description_placeholders": description_placeholders, + } + @callback def _create_abort_data( diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 5530c73ed50..998e88000bf 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -5,386 +5,411 @@ To update, run python3 -m script.hassfest # fmt: off -FLOWS = [ - "abode", - "accuweather", - "acmeda", - "adax", - "adguard", - "advantage_air", - "aemet", - "agent_dvr", - "airly", - "airnow", - "airthings", - "airtouch4", - "airvisual", - "alarmdecoder", - "almond", - "ambee", - "amberelectric", - "ambiclimate", - "ambient_station", - "androidtv", - "apple_tv", - "arcam_fmj", - "aseko_pool_live", - "asuswrt", - "atag", - "august", - "aurora", - "aurora_abb_powerone", - "aussie_broadband", - "awair", - "axis", - "azure_devops", - "azure_event_hub", - "balboa", - "blebox", - "blink", - "bmw_connected_drive", - "bond", - "bosch_shc", - "braviatv", - "broadlink", - "brother", - "brunt", - "bsblan", - "buienradar", - "canary", - "cast", - "cert_expiry", - "climacell", - "cloudflare", - "co2signal", - "coinbase", - "control4", - "coolmaster", - "coronavirus", - "cpuspeed", - "crownstone", - "daikin", - "deconz", - "denonavr", - "devolo_home_control", - "devolo_home_network", - "dexcom", - "dialogflow", - "directv", - "dlna_dmr", - "dlna_dms", - "dnsip", - "doorbird", - "dsmr", - "dunehd", - "dynalite", - "eafm", - "ecobee", - "econet", - "efergy", - "elgato", - "elkm1", - "elmax", - "emonitor", - "emulated_roku", - "enocean", - "enphase_envoy", - "environment_canada", - "epson", - "esphome", - "evil_genius_labs", - "ezviz", - "faa_delays", - "fireservicerota", - "fivem", - "fjaraskupan", - "flick_electric", - "flipr", - "flo", - "flume", - "flunearyou", - "flux_led", - "forecast_solar", - "forked_daapd", - "foscam", - "freebox", - "freedompro", - "fritz", - "fritzbox", - "fritzbox_callmonitor", - "fronius", - "garages_amsterdam", - "gdacs", - "geofency", - "geonetnz_quakes", - "geonetnz_volcano", - "gios", - "github", - "glances", - "goalzero", - "gogogate2", - "goodwe", - "google_travel_time", - "gpslogger", - "gree", - "growatt_server", - "guardian", - "habitica", - "hangouts", - "harmony", - "heos", - "hisense_aehw4a1", - "hive", - "hlk_sw16", - "home_connect", - "home_plus_control", - "homekit", - "homekit_controller", - "homematicip_cloud", - "homewizard", - "honeywell", - "huawei_lte", - "hue", - "huisbaasje", - "hunterdouglas_powerview", - "hvv_departures", - "hyperion", - "ialarm", - "iaqualink", - "icloud", - "ifttt", - "insteon", - "intellifire", - "ios", - "iotawatt", - "ipma", - "ipp", - "iqvia", - "islamic_prayer_times", - "iss", - "isy994", - "izone", - "jellyfin", - "juicenet", - "keenetic_ndms2", - "kmtronic", - "knx", - "kodi", - "konnected", - "kostal_plenticore", - "kraken", - "kulersky", - "launch_library", - "life360", - "lifx", - "litejet", - "litterrobot", - "local_ip", - "locative", - "logi_circle", - "lookin", - "luftdaten", - "lutron_caseta", - "lyric", - "mailgun", - "mazda", - "melcloud", - "met", - "met_eireann", - "meteo_france", - "meteoclimatic", - "metoffice", - "mikrotik", - "mill", - "minecraft_server", - "mjpeg", - "mobile_app", - "modem_callerid", - "modern_forms", - "moehlenhoff_alpha2", - "monoprice", - "motion_blinds", - "motioneye", - "mqtt", - "mullvad", - "mutesync", - "myq", - "mysensors", - "nam", - "nanoleaf", - "neato", - "nest", - "netatmo", - "netgear", - "nexia", - "nfandroidtv", - "nightscout", - "nina", - "nmap_tracker", - "notion", - "nuheat", - "nuki", - "nut", - "nws", - "nzbget", - "octoprint", - "omnilogic", - "oncue", - "ondilo_ico", - "onewire", - "onvif", - "open_meteo", - "opengarage", - "opentherm_gw", - "openuv", - "openweathermap", - "overkiz", - "ovo_energy", - "owntracks", - "ozw", - "p1_monitor", - "panasonic_viera", - "philips_js", - "pi_hole", - "picnic", - "plaato", - "plex", - "plugwise", - "plum_lightpad", - "point", - "poolsense", - "powerwall", - "profiler", - "progettihwsw", - "prosegur", - "ps4", - "pure_energie", - "pvoutput", - "pvpc_hourly_pricing", - "rachio", - "radio_browser", - "rainforest_eagle", - "rainmachine", - "rdw", - "recollect_waste", - "renault", - "rfxtrx", - "ridwell", - "ring", - "risco", - "rituals_perfume_genie", - "roku", - "roomba", - "roon", - "rpi_power", - "rtsp_to_webrtc", - "ruckus_unleashed", - "samsungtv", - "screenlogic", - "sense", - "senseme", - "sensibo", - "sentry", - "sharkiq", - "shelly", - "shopping_list", - "sia", - "simplisafe", - "sleepiq", - "sma", - "smappee", - "smart_meter_texas", - "smarthab", - "smartthings", - "smarttub", - "smhi", - "sms", - "solaredge", - "solarlog", - "solax", - "soma", - "somfy", - "somfy_mylink", - "sonarr", - "songpal", - "sonos", - "speedtestdotnet", - "spider", - "spotify", - "squeezebox", - "srp_energy", - "starline", - "steamist", - "stookalert", - "subaru", - "surepetcare", - "switchbot", - "switcher_kis", - "syncthing", - "syncthru", - "synology_dsm", - "system_bridge", - "tado", - "tailscale", - "tasmota", - "tellduslive", - "tesla_wall_connector", - "tibber", - "tile", - "tolo", - "toon", - "totalconnect", - "tplink", - "traccar", - "tractive", - "tradfri", - "trafikverket_weatherstation", - "transmission", - "tuya", - "twentemilieu", - "twilio", - "twinkly", - "unifi", - "unifiprotect", - "upb", - "upcloud", - "upnp", - "uptimerobot", - "vallox", - "velbus", - "venstar", - "vera", - "verisure", - "version", - "vesync", - "vicare", - "vilfo", - "vizio", - "vlc_telnet", - "volumio", - "wallbox", - "watttime", - "waze_travel_time", - "webostv", - "wemo", - "whirlpool", - "whois", - "wiffi", - "wilight", - "withings", - "wiz", - "wled", - "wolflink", - "xbox", - "xiaomi_aqara", - "xiaomi_miio", - "yale_smart_alarm", - "yamaha_musiccast", - "yeelight", - "youless", - "zerproc", - "zha", - "zwave", - "zwave_js", - "zwave_me" -] +FLOWS = { + "integration": [ + "abode", + "accuweather", + "acmeda", + "adax", + "adguard", + "advantage_air", + "aemet", + "agent_dvr", + "airly", + "airnow", + "airthings", + "airtouch4", + "airvisual", + "airzone", + "alarmdecoder", + "almond", + "ambee", + "amberelectric", + "ambiclimate", + "ambient_station", + "androidtv", + "apple_tv", + "arcam_fmj", + "aseko_pool_live", + "asuswrt", + "atag", + "august", + "aurora", + "aurora_abb_powerone", + "aussie_broadband", + "awair", + "axis", + "azure_devops", + "azure_event_hub", + "balboa", + "blebox", + "blink", + "bmw_connected_drive", + "bond", + "bosch_shc", + "braviatv", + "broadlink", + "brother", + "brunt", + "bsblan", + "buienradar", + "canary", + "cast", + "cert_expiry", + "cloudflare", + "co2signal", + "coinbase", + "control4", + "coolmaster", + "coronavirus", + "cpuspeed", + "crownstone", + "daikin", + "deconz", + "deluge", + "denonavr", + "devolo_home_control", + "devolo_home_network", + "dexcom", + "dialogflow", + "directv", + "discord", + "dlna_dmr", + "dlna_dms", + "dnsip", + "doorbird", + "dsmr", + "dunehd", + "dynalite", + "eafm", + "ecobee", + "econet", + "efergy", + "elgato", + "elkm1", + "elmax", + "emonitor", + "emulated_roku", + "enocean", + "enphase_envoy", + "environment_canada", + "epson", + "esphome", + "evil_genius_labs", + "ezviz", + "faa_delays", + "fibaro", + "filesize", + "fireservicerota", + "fivem", + "fjaraskupan", + "flick_electric", + "flipr", + "flo", + "flume", + "flunearyou", + "flux_led", + "forecast_solar", + "forked_daapd", + "foscam", + "freebox", + "freedompro", + "fritz", + "fritzbox", + "fritzbox_callmonitor", + "fronius", + "garages_amsterdam", + "gdacs", + "generic", + "geofency", + "geonetnz_quakes", + "geonetnz_volcano", + "gios", + "github", + "glances", + "goalzero", + "gogogate2", + "goodwe", + "google", + "google_travel_time", + "gpslogger", + "gree", + "growatt_server", + "guardian", + "habitica", + "hangouts", + "harmony", + "heos", + "hisense_aehw4a1", + "hive", + "hlk_sw16", + "home_connect", + "home_plus_control", + "homekit", + "homekit_controller", + "homematicip_cloud", + "homewizard", + "honeywell", + "huawei_lte", + "hue", + "huisbaasje", + "hunterdouglas_powerview", + "hvv_departures", + "hyperion", + "ialarm", + "iaqualink", + "icloud", + "ifttt", + "insteon", + "intellifire", + "ios", + "iotawatt", + "ipma", + "ipp", + "iqvia", + "islamic_prayer_times", + "iss", + "isy994", + "izone", + "jellyfin", + "juicenet", + "kaleidescape", + "keenetic_ndms2", + "kmtronic", + "knx", + "kodi", + "konnected", + "kostal_plenticore", + "kraken", + "kulersky", + "launch_library", + "life360", + "lifx", + "litejet", + "litterrobot", + "local_ip", + "locative", + "logi_circle", + "lookin", + "luftdaten", + "lutron_caseta", + "lyric", + "mailgun", + "mazda", + "melcloud", + "met", + "met_eireann", + "meteo_france", + "meteoclimatic", + "metoffice", + "mikrotik", + "mill", + "minecraft_server", + "mjpeg", + "mobile_app", + "modem_callerid", + "modern_forms", + "moehlenhoff_alpha2", + "monoprice", + "moon", + "motion_blinds", + "motioneye", + "mqtt", + "mullvad", + "mutesync", + "myq", + "mysensors", + "nam", + "nanoleaf", + "neato", + "nest", + "netatmo", + "netgear", + "nexia", + "nfandroidtv", + "nightscout", + "nina", + "nmap_tracker", + "notion", + "nuheat", + "nuki", + "nut", + "nws", + "nzbget", + "octoprint", + "omnilogic", + "oncue", + "ondilo_ico", + "onewire", + "onvif", + "open_meteo", + "opengarage", + "opentherm_gw", + "openuv", + "openweathermap", + "overkiz", + "ovo_energy", + "owntracks", + "p1_monitor", + "panasonic_viera", + "peco", + "philips_js", + "pi_hole", + "picnic", + "plaato", + "plex", + "plugwise", + "plum_lightpad", + "point", + "poolsense", + "powerwall", + "profiler", + "progettihwsw", + "prosegur", + "ps4", + "pure_energie", + "pvoutput", + "pvpc_hourly_pricing", + "rachio", + "radio_browser", + "rainforest_eagle", + "rainmachine", + "rdw", + "recollect_waste", + "renault", + "rfxtrx", + "ridwell", + "ring", + "risco", + "rituals_perfume_genie", + "roku", + "roomba", + "roon", + "rpi_power", + "rtsp_to_webrtc", + "ruckus_unleashed", + "samsungtv", + "screenlogic", + "season", + "sense", + "senseme", + "sensibo", + "sentry", + "sharkiq", + "shelly", + "shopping_list", + "sia", + "simplisafe", + "sleepiq", + "sma", + "smappee", + "smart_meter_texas", + "smartthings", + "smarttub", + "smhi", + "sms", + "solaredge", + "solarlog", + "solax", + "soma", + "somfy", + "somfy_mylink", + "sonarr", + "songpal", + "sonos", + "speedtestdotnet", + "spider", + "spotify", + "squeezebox", + "srp_energy", + "starline", + "steamist", + "stookalert", + "subaru", + "sun", + "surepetcare", + "switchbot", + "switcher_kis", + "syncthing", + "syncthru", + "synology_dsm", + "system_bridge", + "tado", + "tailscale", + "tankerkoenig", + "tasmota", + "tellduslive", + "tesla_wall_connector", + "tibber", + "tile", + "tolo", + "tomorrowio", + "toon", + "totalconnect", + "tplink", + "traccar", + "tractive", + "tradfri", + "trafikverket_train", + "trafikverket_weatherstation", + "transmission", + "tuya", + "twentemilieu", + "twilio", + "twinkly", + "unifi", + "unifiprotect", + "upb", + "upcloud", + "upnp", + "uptime", + "uptimerobot", + "vallox", + "velbus", + "venstar", + "vera", + "verisure", + "version", + "vesync", + "vicare", + "vilfo", + "vizio", + "vlc_telnet", + "volumio", + "vulcan", + "wallbox", + "watttime", + "waze_travel_time", + "webostv", + "wemo", + "whirlpool", + "whois", + "wiffi", + "wilight", + "withings", + "wiz", + "wled", + "wolflink", + "xbox", + "xiaomi_aqara", + "xiaomi_miio", + "yale_smart_alarm", + "yamaha_musiccast", + "yeelight", + "youless", + "zerproc", + "zha", + "zwave_js", + "zwave_me" + ], + "helper": [ + "derivative", + "group", + "integration", + "min_max", + "switch_as_x", + "threshold", + "tod", + "utility_meter" + ] +} diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index db5bc2f8127..3780b7914c4 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -49,11 +49,14 @@ DHCP: list[dict[str, str | bool]] = [ {'domain': 'hunterdouglas_powerview', 'hostname': 'hunter*', 'macaddress': '002674*'}, + {'domain': 'intellifire', 'hostname': 'zentrios-*'}, {'domain': 'isy994', 'registered_devices': True}, {'domain': 'isy994', 'hostname': 'isy*', 'macaddress': '0021B9*'}, {'domain': 'lyric', 'hostname': 'lyric-*', 'macaddress': '48A2E6*'}, {'domain': 'lyric', 'hostname': 'lyric-*', 'macaddress': 'B82CA0*'}, {'domain': 'lyric', 'hostname': 'lyric-*', 'macaddress': '00D02D*'}, + {'domain': 'motion_blinds', 'registered_devices': True}, + {'domain': 'motion_blinds', 'hostname': 'motion_*'}, {'domain': 'myq', 'macaddress': '645299*'}, {'domain': 'nest', 'macaddress': '18B430*'}, {'domain': 'nest', 'macaddress': '641666*'}, @@ -75,11 +78,12 @@ DHCP: list[dict[str, str | bool]] = [ {'domain': 'roomba', 'hostname': 'roomba-*', 'macaddress': 'DCF505*'}, {'domain': 'samsungtv', 'registered_devices': True}, {'domain': 'samsungtv', 'hostname': 'tizen*'}, - {'domain': 'samsungtv', 'macaddress': '8CC8CD*'}, - {'domain': 'samsungtv', 'macaddress': '606BBD*'}, - {'domain': 'samsungtv', 'macaddress': 'F47B5E*'}, {'domain': 'samsungtv', 'macaddress': '4844F7*'}, + {'domain': 'samsungtv', 'macaddress': '606BBD*'}, + {'domain': 'samsungtv', 'macaddress': '641CB0*'}, + {'domain': 'samsungtv', 'macaddress': '8CC8CD*'}, {'domain': 'samsungtv', 'macaddress': '8CEA48*'}, + {'domain': 'samsungtv', 'macaddress': 'F47B5E*'}, {'domain': 'screenlogic', 'registered_devices': True}, {'domain': 'screenlogic', 'hostname': 'pentair*', 'macaddress': '00C033*'}, {'domain': 'sense', 'hostname': 'sense-*', 'macaddress': '009D6B*'}, diff --git a/homeassistant/generated/ssdp.py b/homeassistant/generated/ssdp.py index f0117e2a9c2..1c0876cd791 100644 --- a/homeassistant/generated/ssdp.py +++ b/homeassistant/generated/ssdp.py @@ -168,6 +168,12 @@ SSDP = { "manufacturer": "Universal Devices Inc." } ], + "kaleidescape": [ + { + "deviceType": "schemas-upnp-org:device:Basic:1", + "manufacturer": "Kaleidescape, Inc." + } + ], "keenetic_ndms2": [ { "deviceType": "urn:schemas-upnp-org:device:InternetGatewayDevice:1", @@ -219,6 +225,17 @@ SSDP = { "samsungtv": [ { "st": "urn:samsung.com:device:RemoteControlReceiver:1" + }, + { + "st": "urn:samsung.com:service:MainTVAgent2:1" + }, + { + "manufacturer": "Samsung", + "st": "urn:schemas-upnp-org:service:RenderingControl:1" + }, + { + "manufacturer": "Samsung Electronics", + "st": "urn:schemas-upnp-org:service:RenderingControl:1" } ], "songpal": [ diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py index 3355424b710..71e238938cb 100644 --- a/homeassistant/helpers/condition.py +++ b/homeassistant/helpers/condition.py @@ -897,7 +897,7 @@ def numeric_state_validate_config( registry = er.async_get(hass) config = dict(config) - config[CONF_ENTITY_ID] = er.async_resolve_entity_ids( + config[CONF_ENTITY_ID] = er.async_validate_entity_ids( registry, cv.entity_ids_or_uuids(config[CONF_ENTITY_ID]) ) return config @@ -908,7 +908,7 @@ def state_validate_config(hass: HomeAssistant, config: ConfigType) -> ConfigType registry = er.async_get(hass) config = dict(config) - config[CONF_ENTITY_ID] = er.async_resolve_entity_ids( + config[CONF_ENTITY_ID] = er.async_validate_entity_ids( registry, cv.entity_ids_or_uuids(config[CONF_ENTITY_ID]) ) return config diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 7f33ec1f1ec..fb920c4ef1b 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -102,7 +102,7 @@ sun_event = vol.All(vol.Lower, vol.Any(SUN_EVENT_SUNSET, SUN_EVENT_SUNRISE)) port = vol.All(vol.Coerce(int), vol.Range(min=1, max=65535)) # typing typevar -T = TypeVar("T") +_T = TypeVar("_T") def path(value: Any) -> str: @@ -253,20 +253,20 @@ def ensure_list(value: None) -> list[Any]: @overload -def ensure_list(value: list[T]) -> list[T]: +def ensure_list(value: list[_T]) -> list[_T]: ... @overload -def ensure_list(value: list[T] | T) -> list[T]: +def ensure_list(value: list[_T] | _T) -> list[_T]: ... -def ensure_list(value: T | None) -> list[T] | list[Any]: +def ensure_list(value: _T | None) -> list[_T] | list[Any]: """Wrap value in list if it is not one.""" if value is None: return [] - return cast("list[T]", value) if isinstance(value, list) else [value] + return cast("list[_T]", value) if isinstance(value, list) else [value] def entity_id(value: Any) -> str: @@ -467,7 +467,7 @@ def time_period_seconds(value: float | str) -> timedelta: time_period = vol.Any(time_period_str, time_period_seconds, timedelta, time_period_dict) -def match_all(value: T) -> T: +def match_all(value: _T) -> _T: """Validate that matches all values.""" return value @@ -483,7 +483,7 @@ positive_time_period_dict = vol.All(time_period_dict, positive_timedelta) positive_time_period = vol.All(time_period, positive_timedelta) -def remove_falsy(value: list[T]) -> list[T]: +def remove_falsy(value: list[_T]) -> list[_T]: """Remove falsy values from a list.""" return [v for v in value if v] @@ -510,7 +510,7 @@ def slug(value: Any) -> str: def schema_with_slug_keys( - value_schema: T | Callable, *, slug_validator: Callable[[Any], str] = slug + value_schema: _T | Callable, *, slug_validator: Callable[[Any], str] = slug ) -> Callable: """Ensure dicts have slugs as keys. @@ -729,8 +729,11 @@ _FAKE_UUID_4_HEX = re.compile(r"^[0-9a-f]{32}$") def fake_uuid4_hex(value: Any) -> str: """Validate a fake v4 UUID generated by random_uuid_hex.""" - if not _FAKE_UUID_4_HEX.match(value): - raise vol.Invalid("Invalid UUID") + try: + if not _FAKE_UUID_4_HEX.match(value): + raise vol.Invalid("Invalid UUID") + except TypeError as exc: + raise vol.Invalid("Invalid UUID") from exc return cast(str, value) # Pattern.match throws if input is not a string @@ -1313,12 +1316,25 @@ CONDITION_ACTION_SCHEMA: vol.Schema = vol.Schema( ) TRIGGER_BASE_SCHEMA = vol.Schema( - {vol.Required(CONF_PLATFORM): str, vol.Optional(CONF_ID): str} + { + vol.Required(CONF_PLATFORM): str, + vol.Optional(CONF_ID): str, + vol.Optional(CONF_VARIABLES): SCRIPT_VARIABLES_SCHEMA, + } ) -TRIGGER_SCHEMA = vol.All( - ensure_list, [TRIGGER_BASE_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA)] -) + +_base_trigger_validator_schema = TRIGGER_BASE_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA) + + +# This is first round of validation, we don't want to process the config here already, +# just ensure basics as platform and ID are there. +def _base_trigger_validator(value: Any) -> Any: + _base_trigger_validator_schema(value) + return value + + +TRIGGER_SCHEMA = vol.All(ensure_list, [_base_trigger_validator]) _SCRIPT_DELAY_SCHEMA = vol.Schema( { diff --git a/homeassistant/helpers/data_entry_flow.py b/homeassistant/helpers/data_entry_flow.py index 07f5e640ea3..808207c7f30 100644 --- a/homeassistant/helpers/data_entry_flow.py +++ b/homeassistant/helpers/data_entry_flow.py @@ -6,6 +6,7 @@ from typing import Any from aiohttp import web import voluptuous as vol +import voluptuous_serialize from homeassistant import config_entries, data_entry_flow from homeassistant.components.http import HomeAssistantView @@ -32,11 +33,9 @@ class _BaseFlowManagerView(HomeAssistantView): data.pop("data") return data - if result["type"] != data_entry_flow.RESULT_TYPE_FORM: + if "data_schema" not in result: return result - import voluptuous_serialize # pylint: disable=import-outside-toplevel - data = result.copy() if (schema := data["data_schema"]) is None: diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index cb009efeb07..c45b7d5f37b 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -43,6 +43,8 @@ CONNECTION_ZIGBEE = "zigbee" ORPHANED_DEVICE_KEEP_SECONDS = 86400 * 30 +RUNTIME_ONLY_ATTRS = {"suggested_area"} + class _DeviceIndex(NamedTuple): identifiers: dict[tuple[str, str], str] @@ -509,6 +511,15 @@ class DeviceRegistry: new = attr.evolve(old, **new_values) self._update_device(old, new) + + # If its only run time attributes (suggested_area) + # that do not get saved we do not want to write + # to disk or fire an event as we would end up + # firing events for data we have nothing to compare + # against since its never saved on disk + if RUNTIME_ONLY_ATTRS.issuperset(new_values): + return new + self.async_schedule_save() data: dict[str, Any] = { diff --git a/homeassistant/helpers/discovery.py b/homeassistant/helpers/discovery.py index 20819ac7504..93efd7e69c1 100644 --- a/homeassistant/helpers/discovery.py +++ b/homeassistant/helpers/discovery.py @@ -95,7 +95,7 @@ def async_listen_platform( hass: core.HomeAssistant, component: str, callback: Callable[[str, dict[str, Any] | None], Any], -) -> None: +) -> Callable[[], None]: """Register a platform loader listener. This method must be run in the event loop. @@ -112,7 +112,7 @@ def async_listen_platform( if task: await task - async_dispatcher_connect( + return async_dispatcher_connect( hass, SIGNAL_PLATFORM_DISCOVERED.format(service), discovery_platform_listener ) diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index a554a093c5c..f6869787f5b 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -36,7 +36,14 @@ from homeassistant.const import ( TEMP_CELSIUS, TEMP_FAHRENHEIT, ) -from homeassistant.core import CALLBACK_TYPE, Context, Event, HomeAssistant, callback +from homeassistant.core import ( + CALLBACK_TYPE, + Context, + Event, + HomeAssistant, + callback, + split_entity_id, +) from homeassistant.exceptions import HomeAssistantError, NoEntitySpecifiedError from homeassistant.loader import bind_hass from homeassistant.util import dt as dt_util, ensure_unique_string, slugify @@ -45,7 +52,6 @@ from . import entity_registry as er from .device_registry import DeviceEntryType from .entity_platform import EntityPlatform from .event import async_track_entity_registry_updated_event -from .frame import report from .typing import StateType _LOGGER = logging.getLogger(__name__) @@ -221,29 +227,6 @@ class EntityPlatformState(Enum): REMOVED = auto() -def convert_to_entity_category( - value: EntityCategory | str | None, raise_report: bool = True -) -> EntityCategory | None: - """Force incoming entity_category to be an enum.""" - - if value is None: - return value - - if not isinstance(value, EntityCategory): - if raise_report: - report( - "uses %s (%s) for entity category. This is deprecated and will " - "stop working in Home Assistant 2022.4, it should be updated to use " - "EntityCategory instead" % (type(value).__name__, value), - error_if_core=False, - ) - try: - return EntityCategory(value) - except ValueError: - return None - return value - - @dataclass class EntityDescription: """A class that describes Home Assistant entities.""" @@ -252,10 +235,7 @@ class EntityDescription: key: str device_class: str | None = None - # Type string is deprecated as of 2021.12, use EntityCategory - entity_category: EntityCategory | Literal[ - "config", "diagnostic", "system" - ] | None = None + entity_category: EntityCategory | None = None entity_registry_enabled_default: bool = True force_update: bool = False icon: str | None = None @@ -288,8 +268,8 @@ class Entity(ABC): # If we reported this entity is updated while disabled _disabled_reported = False - # If we reported this entity is using deprecated device_state_attributes - _deprecated_device_state_attributes_reported = False + # If we reported this entity is relying on deprecated temperature conversion + _temperature_reported = False # Protect for multiple updates _update_staged = False @@ -484,9 +464,8 @@ class Entity(ABC): """Return the attribution.""" return self._attr_attribution - # Type str is deprecated as of 2021.12, use EntityCategory @property - def entity_category(self) -> EntityCategory | str | None: + def entity_category(self) -> EntityCategory | None: """Return the category of the entity, if any.""" if hasattr(self, "_attr_entity_category"): return self._attr_entity_category @@ -552,9 +531,9 @@ class Entity(ABC): self._async_write_ha_state() - def _stringify_state(self) -> str: + def _stringify_state(self, available: bool) -> str: """Convert state to string.""" - if not self.available: + if not available: return STATE_UNAVAILABLE if (state := self.state) is None: return STATE_UNKNOWN @@ -587,30 +566,13 @@ class Entity(ABC): attr = self.capability_attributes attr = dict(attr) if attr else {} - state = self._stringify_state() - if self.available: + available = self.available # only call self.available once per update cycle + state = self._stringify_state(available) + if available: attr.update(self.state_attributes or {}) - extra_state_attributes = self.extra_state_attributes - # Backwards compatibility for "device_state_attributes" deprecated in 2021.4 - # Warning added in 2021.12, will be removed in 2022.4 - if ( - self.device_state_attributes is not None - and not self._deprecated_device_state_attributes_reported - ): - report_issue = self._suggest_report_issue() - _LOGGER.warning( - "Entity %s (%s) implements device_state_attributes. Please %s", - self.entity_id, - type(self), - report_issue, - ) - self._deprecated_device_state_attributes_reported = True - if extra_state_attributes is None: - extra_state_attributes = self.device_state_attributes - attr.update(extra_state_attributes or {}) + attr.update(self.extra_state_attributes or {}) - unit_of_measurement = self.unit_of_measurement - if unit_of_measurement is not None: + if (unit_of_measurement := self.unit_of_measurement) is not None: attr[ATTR_UNIT_OF_MEASUREMENT] = unit_of_measurement entry = self.registry_entry @@ -655,21 +617,57 @@ class Entity(ABC): if DATA_CUSTOMIZE in self.hass.data: attr.update(self.hass.data[DATA_CUSTOMIZE].get(self.entity_id)) - # Convert temperature if we detect one - try: + def _convert_temperature(state: str, attr: dict) -> str: + # Convert temperature if we detect one + # pylint: disable-next=import-outside-toplevel + from homeassistant.components.sensor import SensorEntity + unit_of_measure = attr.get(ATTR_UNIT_OF_MEASUREMENT) units = self.hass.config.units - if ( - unit_of_measure in (TEMP_CELSIUS, TEMP_FAHRENHEIT) - and unit_of_measure != units.temperature_unit + if unit_of_measure == units.temperature_unit or unit_of_measure not in ( + TEMP_CELSIUS, + TEMP_FAHRENHEIT, ): + return state + + domain = split_entity_id(self.entity_id)[0] + if domain != "sensor": + if not self._temperature_reported: + self._temperature_reported = True + report_issue = self._suggest_report_issue() + _LOGGER.warning( + "Entity %s (%s) relies on automatic temperature conversion, this will " + "be unsupported in Home Assistant Core 2022.7. Please %s", + self.entity_id, + type(self), + report_issue, + ) + elif not isinstance(self, SensorEntity): + if not self._temperature_reported: + self._temperature_reported = True + report_issue = self._suggest_report_issue() + _LOGGER.warning( + "Temperature sensor %s (%s) does not inherit SensorEntity, " + "this will be unsupported in Home Assistant Core 2022.7." + "Please %s", + self.entity_id, + type(self), + report_issue, + ) + else: + return state + + try: prec = len(state) - state.index(".") - 1 if "." in state else 0 temp = units.temperature(float(state), unit_of_measure) state = str(round(temp) if prec == 0 else round(temp, prec)) attr[ATTR_UNIT_OF_MEASUREMENT] = units.temperature_unit - except ValueError: - # Could not convert state to float - pass + except ValueError: + # Could not convert state to float + pass + return state + + state = _convert_temperature(state, attr) if ( self._context_set is not None @@ -847,6 +845,13 @@ class Entity(ABC): To be extended by integrations. """ + @callback + def async_registry_entry_updated(self) -> None: + """Run when the entity registry entry has been updated. + + To be extended by integrations. + """ + async def async_internal_added_to_hass(self) -> None: """Run when entity about to be added to hass. @@ -908,6 +913,7 @@ class Entity(ABC): assert old is not None if self.registry_entry.entity_id == old.entity_id: + self.async_registry_entry_updated() self.async_write_ha_state() return diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index a1dba0d6962..1cef123b292 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -201,8 +201,8 @@ class EntityComponent: async def handle_service(call: ServiceCall) -> None: """Handle the service.""" - await self.hass.helpers.service.entity_service_call( - self._platforms.values(), func, call, required_features + await service.entity_service_call( + self.hass, self._platforms.values(), func, call, required_features ) self.hass.services.async_register(self.domain, name, handle_service, schema) diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index cc252f82782..eaf0ae6d6bb 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -478,7 +478,7 @@ class EntityPlatform: "via_device", ): if key in device_info: - processed_dev_info[key] = device_info[key] # type: ignore[misc] + processed_dev_info[key] = device_info[key] # type: ignore[literal-required] if "configuration_url" in device_info: if device_info["configuration_url"] is None: diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 4d4fce6e685..f171f3f7f70 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -44,7 +44,6 @@ from homeassistant.util.yaml import load_yaml from . import device_registry as dr, storage from .device_registry import EVENT_DEVICE_REGISTRY_UPDATED -from .frame import report from .typing import UNDEFINED, UndefinedType if TYPE_CHECKING: @@ -59,7 +58,7 @@ SAVE_DELAY = 10 _LOGGER = logging.getLogger(__name__) STORAGE_VERSION_MAJOR = 1 -STORAGE_VERSION_MINOR = 5 +STORAGE_VERSION_MINOR = 6 STORAGE_KEY = "core.entity_registry" # Attributes relevant to describing entity @@ -85,22 +84,11 @@ class RegistryEntryDisabler(StrEnum): USER = "user" -# DISABLED_* are deprecated, to be removed in 2022.3 -DISABLED_CONFIG_ENTRY = RegistryEntryDisabler.CONFIG_ENTRY.value -DISABLED_DEVICE = RegistryEntryDisabler.DEVICE.value -DISABLED_HASS = RegistryEntryDisabler.HASS.value -DISABLED_INTEGRATION = RegistryEntryDisabler.INTEGRATION.value -DISABLED_USER = RegistryEntryDisabler.USER.value +class RegistryEntryHider(StrEnum): + """What hid a registry entry.""" - -def _convert_to_entity_category( - value: EntityCategory | str | None, raise_report: bool = True -) -> EntityCategory | None: - """Force incoming entity_category to be an enum.""" - # pylint: disable=import-outside-toplevel - from .entity import convert_to_entity_category - - return convert_to_entity_category(value, raise_report=raise_report) + INTEGRATION = "integration" + USER = "user" @attr.s(slots=True, frozen=True) @@ -117,9 +105,8 @@ class RegistryEntry: device_id: str | None = attr.ib(default=None) domain: str = attr.ib(init=False, repr=False) disabled_by: RegistryEntryDisabler | None = attr.ib(default=None) - entity_category: EntityCategory | None = attr.ib( - default=None, converter=_convert_to_entity_category - ) + entity_category: EntityCategory | None = attr.ib(default=None) + hidden_by: RegistryEntryHider | None = attr.ib(default=None) icon: str | None = attr.ib(default=None) id: str = attr.ib(factory=uuid_util.random_uuid_hex) name: str | None = attr.ib(default=None) @@ -143,6 +130,11 @@ class RegistryEntry: """Return if entry is disabled.""" return self.disabled_by is not None + @property + def hidden(self) -> bool: + """Return if entry is hidden.""" + return self.hidden_by is not None + @callback def write_unavailable_state(self, hass: HomeAssistant) -> None: """Write the unavailable state to the state machine.""" @@ -327,15 +319,15 @@ class EntityRegistry: # To influence entity ID generation known_object_ids: Iterable[str] | None = None, suggested_object_id: str | None = None, - # To disable an entity if it gets created + # To disable or hide an entity if it gets created disabled_by: RegistryEntryDisabler | None = None, + hidden_by: RegistryEntryHider | None = None, # Data that we want entry to have area_id: str | None = None, capabilities: Mapping[str, Any] | None = None, config_entry: ConfigEntry | None = None, device_id: str | None = None, - # Type str (ENTITY_CATEG*) is deprecated as of 2021.12, use EntityCategory - entity_category: EntityCategory | str | None = None, + entity_category: EntityCategory | None = None, original_device_class: str | None = None, original_icon: str | None = None, original_name: str | None = None, @@ -375,31 +367,30 @@ class EntityRegistry: domain, suggested_object_id or f"{platform}_{unique_id}", known_object_ids ) - if isinstance(disabled_by, str) and not isinstance( - disabled_by, RegistryEntryDisabler - ): - report( # type: ignore[unreachable] - "uses str for entity registry disabled_by. This is deprecated and will " - "stop working in Home Assistant 2022.3, it should be updated to use " - "RegistryEntryDisabler instead", - error_if_core=False, - ) - disabled_by = RegistryEntryDisabler(disabled_by) - elif ( + if disabled_by and not isinstance(disabled_by, RegistryEntryDisabler): + raise ValueError("disabled_by must be a RegistryEntryDisabler value") + + if ( disabled_by is None and config_entry and config_entry.pref_disable_new_entities ): disabled_by = RegistryEntryDisabler.INTEGRATION + from .entity import EntityCategory # pylint: disable=import-outside-toplevel + + if entity_category and not isinstance(entity_category, EntityCategory): + raise ValueError("entity_category must be a valid EntityCategory instance") + entry = RegistryEntry( area_id=area_id, capabilities=capabilities, config_entry_id=config_entry_id, device_id=device_id, disabled_by=disabled_by, - entity_category=_convert_to_entity_category(entity_category), + entity_category=entity_category, entity_id=entity_id, + hidden_by=hidden_by, original_device_class=original_device_class, original_icon=original_icon, original_name=original_name, @@ -503,8 +494,8 @@ class EntityRegistry: device_class: str | None | UndefinedType = UNDEFINED, device_id: str | None | UndefinedType = UNDEFINED, disabled_by: RegistryEntryDisabler | None | UndefinedType = UNDEFINED, - # Type str (ENTITY_CATEG*) is deprecated as of 2021.12, use EntityCategory - entity_category: EntityCategory | str | None | UndefinedType = UNDEFINED, + entity_category: EntityCategory | None | UndefinedType = UNDEFINED, + hidden_by: RegistryEntryHider | None | UndefinedType = UNDEFINED, icon: str | None | UndefinedType = UNDEFINED, name: str | None | UndefinedType = UNDEFINED, new_entity_id: str | UndefinedType = UNDEFINED, @@ -521,16 +512,21 @@ class EntityRegistry: new_values: dict[str, Any] = {} # Dict with new key/value pairs old_values: dict[str, Any] = {} # Dict with old key/value pairs - if isinstance(disabled_by, str) and not isinstance( - disabled_by, RegistryEntryDisabler + if ( + disabled_by + and disabled_by is not UNDEFINED + and not isinstance(disabled_by, RegistryEntryDisabler) ): - report( # type: ignore[unreachable] - "uses str for entity registry disabled_by. This is deprecated and will " - "stop working in Home Assistant 2022.3, it should be updated to use " - "RegistryEntryDisabler instead", - error_if_core=False, - ) - disabled_by = RegistryEntryDisabler(disabled_by) + raise ValueError("disabled_by must be a RegistryEntryDisabler value") + + from .entity import EntityCategory # pylint: disable=import-outside-toplevel + + if ( + entity_category + and entity_category is not UNDEFINED + and not isinstance(entity_category, EntityCategory) + ): + raise ValueError("entity_category must be a valid EntityCategory instance") for attr_name, value in ( ("area_id", area_id), @@ -540,6 +536,7 @@ class EntityRegistry: ("device_id", device_id), ("disabled_by", disabled_by), ("entity_category", entity_category), + ("hidden_by", hidden_by), ("icon", icon), ("name", name), ("original_device_class", original_device_class), @@ -601,11 +598,11 @@ class EntityRegistry: @callback def async_update_entity_options( self, entity_id: str, domain: str, options: dict[str, Any] - ) -> None: + ) -> RegistryEntry: """Update entity options.""" old = self.entities[entity_id] new_options: Mapping[str, Mapping[str, Any]] = {**old.options, domain: options} - self.entities[entity_id] = attr.evolve(old, options=new_options) + new = self.entities[entity_id] = attr.evolve(old, options=new_options) self.async_schedule_save() @@ -617,6 +614,8 @@ class EntityRegistry: self.hass.bus.async_fire(EVENT_ENTITY_REGISTRY_UPDATED, data) + return new + async def async_load(self) -> None: """Load the entity registry.""" async_setup_entity_restore(self.hass, self) @@ -630,6 +629,8 @@ class EntityRegistry: ) entities = EntityRegistryItems() + from .entity import EntityCategory # pylint: disable=import-outside-toplevel + if data is not None: for entity in data["entities"]: # Some old installations can have some bad entities. @@ -647,10 +648,11 @@ class EntityRegistry: disabled_by=RegistryEntryDisabler(entity["disabled_by"]) if entity["disabled_by"] else None, - entity_category=_convert_to_entity_category( - entity["entity_category"], raise_report=False - ), + entity_category=EntityCategory(entity["entity_category"]) + if entity["entity_category"] + else None, entity_id=entity["entity_id"], + hidden_by=entity["hidden_by"], icon=entity["icon"], id=entity["id"], name=entity["name"], @@ -686,6 +688,7 @@ class EntityRegistry: "disabled_by": entry.disabled_by, "entity_category": entry.entity_category, "entity_id": entry.entity_id, + "hidden_by": entry.hidden_by, "icon": entry.icon, "id": entry.id, "name": entry.name, @@ -846,6 +849,11 @@ async def _async_migrate( for entity in data["entities"]: entity["options"] = {} + if old_major_version == 1 and old_minor_version < 6: + # Version 1.6 adds hidden_by + for entity in data["entities"]: + entity["hidden_by"] = None + if old_major_version > 1: raise NotImplementedError return data @@ -924,22 +932,44 @@ async def async_migrate_entries( @callback -def async_resolve_entity_ids( +def async_validate_entity_id(registry: EntityRegistry, entity_id_or_uuid: str) -> str: + """Validate and resolve an entity id or UUID to an entity id. + + Raises vol.Invalid if the entity or UUID is invalid, or if the UUID is not + associated with an entity registry item. + """ + if valid_entity_id(entity_id_or_uuid): + return entity_id_or_uuid + if (entry := registry.entities.get_entry(entity_id_or_uuid)) is None: + raise vol.Invalid(f"Unknown entity registry entry {entity_id_or_uuid}") + return entry.entity_id + + +@callback +def async_resolve_entity_id( + registry: EntityRegistry, entity_id_or_uuid: str +) -> str | None: + """Validate and resolve an entity id or UUID to an entity id. + + Returns None if the entity or UUID is invalid, or if the UUID is not + associated with an entity registry item. + """ + if valid_entity_id(entity_id_or_uuid): + return entity_id_or_uuid + if (entry := registry.entities.get_entry(entity_id_or_uuid)) is None: + return None + return entry.entity_id + + +@callback +def async_validate_entity_ids( registry: EntityRegistry, entity_ids_or_uuids: list[str] ) -> list[str]: - """Resolve a list of entity ids or UUIDs to a list of entity ids.""" + """Validate and resolve a list of entity ids or UUIDs to a list of entity ids. - def resolve_entity(entity_id_or_uuid: str) -> str | None: - """Resolve an entity id or UUID to an entity id or None.""" - if valid_entity_id(entity_id_or_uuid): - return entity_id_or_uuid - if (entry := registry.entities.get_entry(entity_id_or_uuid)) is None: - raise vol.Invalid(f"Unknown entity registry entry {entity_id_or_uuid}") - return entry.entity_id + Returns a list with UUID resolved to entity_ids. + Raises vol.Invalid if any item is invalid, or if any a UUID is not associated with + an entity registry item. + """ - tmp = [ - resolved_item - for item in entity_ids_or_uuids - if (resolved_item := resolve_entity(item)) is not None - ] - return tmp + return [async_validate_entity_id(registry, item) for item in entity_ids_or_uuids] diff --git a/homeassistant/helpers/frame.py b/homeassistant/helpers/frame.py index 13ffea48f81..2baf7cdd713 100644 --- a/homeassistant/helpers/frame.py +++ b/homeassistant/helpers/frame.py @@ -15,7 +15,7 @@ _LOGGER = logging.getLogger(__name__) # Keep track of integrations already reported to prevent flooding _REPORTED_INTEGRATIONS: set[str] = set() -CALLABLE_T = TypeVar("CALLABLE_T", bound=Callable) # pylint: disable=invalid-name +_CallableT = TypeVar("_CallableT", bound=Callable) def get_integration_frame( @@ -113,7 +113,7 @@ def report_integration( ) -def warn_use(func: CALLABLE_T, what: str) -> CALLABLE_T: +def warn_use(func: _CallableT, what: str) -> _CallableT: """Mock a function to warn when it was about to be used.""" if asyncio.iscoroutinefunction(func): @@ -127,4 +127,4 @@ def warn_use(func: CALLABLE_T, what: str) -> CALLABLE_T: def report_use(*args: Any, **kwargs: Any) -> None: report(what) - return cast(CALLABLE_T, report_use) + return cast(_CallableT, report_use) diff --git a/homeassistant/helpers/intent.py b/homeassistant/helpers/intent.py index 44dd21d7fa3..7b0b1033016 100644 --- a/homeassistant/helpers/intent.py +++ b/homeassistant/helpers/intent.py @@ -4,12 +4,12 @@ from __future__ import annotations from collections.abc import Callable, Iterable import logging import re -from typing import Any +from typing import Any, TypeVar import voluptuous as vol from homeassistant.const import ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES -from homeassistant.core import Context, HomeAssistant, State, T, callback +from homeassistant.core import Context, HomeAssistant, State, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.loader import bind_hass @@ -17,6 +17,7 @@ from . import config_validation as cv _LOGGER = logging.getLogger(__name__) _SlotsType = dict[str, Any] +_T = TypeVar("_T") INTENT_TURN_OFF = "HassTurnOff" INTENT_TURN_ON = "HassTurnOn" @@ -163,7 +164,7 @@ class IntentHandler: return f"<{self.__class__.__name__} - {self.intent_type}>" -def _fuzzymatch(name: str, items: Iterable[T], key: Callable[[T], str]) -> T | None: +def _fuzzymatch(name: str, items: Iterable[_T], key: Callable[[_T], str]) -> _T | None: """Fuzzy matching function.""" matches = [] pattern = ".*?".join(name) diff --git a/homeassistant/helpers/network.py b/homeassistant/helpers/network.py index a8c4b3cf458..5fa10fd6fe8 100644 --- a/homeassistant/helpers/network.py +++ b/homeassistant/helpers/network.py @@ -1,6 +1,7 @@ """Network helpers.""" from __future__ import annotations +from collections.abc import Callable from contextlib import suppress from ipaddress import ip_address from typing import cast @@ -15,6 +16,7 @@ from homeassistant.util.network import is_ip_address, is_loopback, normalize_url TYPE_URL_INTERNAL = "internal_url" TYPE_URL_EXTERNAL = "external_url" +SUPERVISOR_NETWORK_HOST = "homeassistant" class NoURLAvailableError(HomeAssistantError): @@ -33,9 +35,40 @@ def is_internal_request(hass: HomeAssistant) -> bool: return False +@bind_hass +def get_supervisor_network_url( + hass: HomeAssistant, *, allow_ssl: bool = False +) -> str | None: + """Get URL for home assistant within supervisor network.""" + if hass.config.api is None or not hass.components.hassio.is_hassio(): + return None + + scheme = "http" + if hass.config.api.use_ssl: + # Certificate won't be valid for hostname so this URL usually won't work + if not allow_ssl: + return None + + scheme = "https" + + return str( + yarl.URL.build( + scheme=scheme, + host=SUPERVISOR_NETWORK_HOST, + port=hass.config.api.port, + ) + ) + + def is_hass_url(hass: HomeAssistant, url: str) -> bool: """Return if the URL points at this Home Assistant instance.""" - parsed = yarl.URL(normalize_url(url)) + parsed = yarl.URL(url) + + if not parsed.is_absolute(): + return False + + if parsed.is_default_port(): + parsed = parsed.with_port(None) def host_ip() -> str | None: if hass.config.api is None or is_loopback(ip_address(hass.config.api.local_ip)): @@ -53,11 +86,13 @@ def is_hass_url(hass: HomeAssistant, url: str) -> bool: except NoURLAvailableError: return None + potential_base_factory: Callable[[], str | None] for potential_base_factory in ( lambda: hass.config.internal_url, lambda: hass.config.external_url, cloud_url, host_ip, + lambda: get_supervisor_network_url(hass, allow_ssl=True), ): potential_base = potential_base_factory() diff --git a/homeassistant/helpers/reload.py b/homeassistant/helpers/reload.py index 5a826d4129e..83698557eb6 100644 --- a/homeassistant/helpers/reload.py +++ b/homeassistant/helpers/reload.py @@ -8,17 +8,21 @@ from typing import Any from homeassistant import config as conf_util from homeassistant.const import SERVICE_RELOAD -from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.loader import async_get_integration from homeassistant.setup import async_setup_component from . import config_per_platform +from .entity_component import EntityComponent from .entity_platform import EntityPlatform, async_get_platforms +from .service import async_register_admin_service from .typing import ConfigType _LOGGER = logging.getLogger(__name__) +PLATFORM_RESET_LOCK = "lock_async_reset_platform_{}" + async def async_reload_integration_platforms( hass: HomeAssistant, integration_name: str, integration_platforms: Iterable[str] @@ -64,7 +68,7 @@ async def _resetup_platform( if not conf: return - root_config: dict[str, Any] = {integration_platform: []} + root_config: dict[str, list[ConfigType]] = {integration_platform: []} # Extract only the config for template, ignore the rest. for p_type, p_config in config_per_platform(conf, integration_platform): if p_type != integration_name: @@ -77,8 +81,11 @@ async def _resetup_platform( if hasattr(component, "async_reset_platform"): # If the integration has its own way to reset # use this method. - await component.async_reset_platform(hass, integration_name) - await component.async_setup(hass, root_config) + async with hass.data.setdefault( + PLATFORM_RESET_LOCK.format(integration_platform), asyncio.Lock() + ): + await component.async_reset_platform(hass, integration_name) + await component.async_setup(hass, root_config) return # If it's an entity platform, we use the entity_platform @@ -113,7 +120,7 @@ async def _async_setup_platform( ) return - entity_component = hass.data[integration_platform] + entity_component: EntityComponent = hass.data[integration_platform] tasks = [ entity_component.async_setup_platform(integration_name, p_config) for p_config in platform_configs @@ -163,14 +170,12 @@ async def async_setup_reload_service( if hass.services.has_service(domain, SERVICE_RELOAD): return - async def _reload_config(call: Event) -> None: + async def _reload_config(call: ServiceCall) -> None: """Reload the platforms.""" await async_reload_integration_platforms(hass, domain, platforms) hass.bus.async_fire(f"event_{domain}_reloaded", context=call.context) - hass.helpers.service.async_register_admin_service( - domain, SERVICE_RELOAD, _reload_config - ) + async_register_admin_service(hass, domain, SERVICE_RELOAD, _reload_config) def setup_reload_service( diff --git a/homeassistant/helpers/schema_config_entry_flow.py b/homeassistant/helpers/schema_config_entry_flow.py new file mode 100644 index 00000000000..341ae605025 --- /dev/null +++ b/homeassistant/helpers/schema_config_entry_flow.py @@ -0,0 +1,386 @@ +"""Helpers for creating schema based data entry flows.""" +from __future__ import annotations + +from abc import abstractmethod +from collections.abc import Callable, Mapping +import copy +from dataclasses import dataclass +import types +from typing import Any, cast + +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.core import HomeAssistant, callback, split_entity_id +from homeassistant.data_entry_flow import FlowResult, UnknownHandler + +from . import entity_registry as er, selector + + +class SchemaFlowError(Exception): + """Validation failed.""" + + +@dataclass +class SchemaFlowFormStep: + """Define a config or options flow step.""" + + # Optional schema for requesting and validating user input. If schema validation + # fails, the step will be retried. If the schema is None, no user input is requested. + schema: vol.Schema | Callable[ + [SchemaConfigFlowHandler | SchemaOptionsFlowHandler, dict[str, Any]], + vol.Schema | None, + ] | None + + # Optional function to validate user input. + # The validate_user_input function is called if the schema validates successfully. + # The validate_user_input function is passed the user input from the current step. + # The validate_user_input should raise SchemaFlowError is user input is invalid. + validate_user_input: Callable[[dict[str, Any]], dict[str, Any]] = lambda x: x + + # Optional function to identify next step. + # The next_step function is called if the schema validates successfully or if no + # schema is defined. The next_step function is passed the union of config entry + # options and user input from previous steps. + # If next_step returns None, the flow is ended with RESULT_TYPE_CREATE_ENTRY. + next_step: Callable[[dict[str, Any]], str | None] = lambda _: None + + # Optional function to allow amending a form schema. + # The update_form_schema function is called before async_show_form is called. The + # update_form_schema function is passed the handler, which is either an instance of + # SchemaConfigFlowHandler or SchemaOptionsFlowHandler, the schema, and the union of + # config entry options and user input from previous steps. + update_form_schema: Callable[ + [ + SchemaConfigFlowHandler | SchemaOptionsFlowHandler, + vol.Schema, + dict[str, Any], + ], + vol.Schema, + ] = lambda _handler, schema, _options: schema + + +@dataclass +class SchemaFlowMenuStep: + """Define a config or options flow menu step.""" + + # Menu options + options: list[str] | dict[str, str] + + +class SchemaCommonFlowHandler: + """Handle a schema based config or options flow.""" + + def __init__( + self, + handler: SchemaConfigFlowHandler | SchemaOptionsFlowHandler, + flow: dict[str, SchemaFlowFormStep | SchemaFlowMenuStep], + config_entry: config_entries.ConfigEntry | None, + ) -> None: + """Initialize a common handler.""" + self._flow = flow + self._handler = handler + self._options = dict(config_entry.options) if config_entry is not None else {} + + async def async_step( + self, step_id: str, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle a step.""" + if isinstance(self._flow[step_id], SchemaFlowFormStep): + return await self._async_form_step(step_id, user_input) + return await self._async_menu_step(step_id, user_input) + + def _get_schema( + self, form_step: SchemaFlowFormStep, options: dict[str, Any] + ) -> vol.Schema | None: + if form_step.schema is None: + return None + if isinstance(form_step.schema, vol.Schema): + return form_step.schema + return form_step.schema(self._handler, options) + + async def _async_form_step( + self, step_id: str, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle a form step.""" + form_step: SchemaFlowFormStep = cast(SchemaFlowFormStep, self._flow[step_id]) + + if ( + user_input is not None + and (data_schema := self._get_schema(form_step, self._options)) + and data_schema.schema + and not self._handler.show_advanced_options + ): + # Add advanced field default if not set + for key in data_schema.schema.keys(): + if isinstance(key, (vol.Optional, vol.Required)): + if ( + key.description + and key.description.get("advanced") + and key.default is not vol.UNDEFINED + and key not in self._options + ): + user_input[str(key.schema)] = key.default() + + if user_input is not None and form_step.schema is not None: + # Do extra validation of user input + try: + user_input = form_step.validate_user_input(user_input) + except SchemaFlowError as exc: + return self._show_next_step(step_id, exc, user_input) + + if user_input is not None: + # User input was validated successfully, update options + self._options.update(user_input) + + next_step_id: str = step_id + if form_step.next_step and (user_input is not None or form_step.schema is None): + # Get next step + next_step_id_or_end_flow = form_step.next_step(self._options) + if next_step_id_or_end_flow is None: + # Flow done, create entry or update config entry options + return self._handler.async_create_entry(data=self._options) + + next_step_id = next_step_id_or_end_flow + + return self._show_next_step(next_step_id) + + def _show_next_step( + self, + next_step_id: str, + error: SchemaFlowError | None = None, + user_input: dict[str, Any] | None = None, + ) -> FlowResult: + """Show form for next step.""" + form_step: SchemaFlowFormStep = cast( + SchemaFlowFormStep, self._flow[next_step_id] + ) + + options = dict(self._options) + if user_input: + options.update(user_input) + + if ( + data_schema := self._get_schema(form_step, self._options) + ) and data_schema.schema: + # Make a copy of the schema with suggested values set to saved options + schema = {} + for key, val in data_schema.schema.items(): + + if isinstance(key, vol.Marker): + # Exclude advanced field + if ( + key.description + and key.description.get("advanced") + and not self._handler.show_advanced_options + ): + continue + + new_key = key + if key in options and isinstance(key, vol.Marker): + # Copy the marker to not modify the flow schema + new_key = copy.copy(key) + new_key.description = {"suggested_value": options[key]} + schema[new_key] = val + data_schema = vol.Schema(schema) + + errors = {"base": str(error)} if error else None + + # Show form for next step + return self._handler.async_show_form( + step_id=next_step_id, data_schema=data_schema, errors=errors + ) + + async def _async_menu_step( + self, step_id: str, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle a menu step.""" + form_step: SchemaFlowMenuStep = cast(SchemaFlowMenuStep, self._flow[step_id]) + return self._handler.async_show_menu( + step_id=step_id, + menu_options=form_step.options, + ) + + +class SchemaConfigFlowHandler(config_entries.ConfigFlow): + """Handle a schema based config flow.""" + + config_flow: dict[str, SchemaFlowFormStep | SchemaFlowMenuStep] + options_flow: dict[str, SchemaFlowFormStep | SchemaFlowMenuStep] | None = None + + VERSION = 1 + + def __init_subclass__(cls, **kwargs: Any) -> None: + """Initialize a subclass.""" + super().__init_subclass__(**kwargs) + + @callback + def _async_get_options_flow( + config_entry: config_entries.ConfigEntry, + ) -> config_entries.OptionsFlow: + """Get the options flow for this handler.""" + if cls.options_flow is None: + raise UnknownHandler + + return SchemaOptionsFlowHandler( + config_entry, cls.options_flow, cls.async_options_flow_finished + ) + + # Create an async_get_options_flow method + cls.async_get_options_flow = _async_get_options_flow # type: ignore[assignment] + + # Create flow step methods for each step defined in the flow schema + for step in cls.config_flow: + setattr(cls, f"async_step_{step}", cls._async_step(step)) + + def __init__(self) -> None: + """Initialize config flow.""" + self._common_handler = SchemaCommonFlowHandler(self, self.config_flow, None) + + @classmethod + @callback + def async_supports_options_flow( + cls, config_entry: config_entries.ConfigEntry + ) -> bool: + """Return options flow support for this handler.""" + return cls.options_flow is not None + + @staticmethod + def _async_step(step_id: str) -> Callable: + """Generate a step handler.""" + + async def _async_step( + self: SchemaConfigFlowHandler, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle a config flow step.""" + # pylint: disable-next=protected-access + result = await self._common_handler.async_step(step_id, user_input) + return result + + return _async_step + + @abstractmethod + @callback + def async_config_entry_title(self, options: Mapping[str, Any]) -> str: + """Return config entry title. + + The options parameter contains config entry options, which is the union of user + input from the config flow steps. + """ + + @callback + def async_config_flow_finished(self, options: Mapping[str, Any]) -> None: + """Take necessary actions after the config flow is finished, if needed. + + The options parameter contains config entry options, which is the union of user + input from the config flow steps. + """ + + @callback + @staticmethod + def async_options_flow_finished( + hass: HomeAssistant, options: Mapping[str, Any] + ) -> None: + """Take necessary actions after the options flow is finished, if needed. + + The options parameter contains config entry options, which is the union of stored + options and user input from the options flow steps. + """ + + @callback + def async_create_entry( # pylint: disable=arguments-differ + self, + data: Mapping[str, Any], + **kwargs: Any, + ) -> FlowResult: + """Finish config flow and create a config entry.""" + self.async_config_flow_finished(data) + return super().async_create_entry( + data={}, options=data, title=self.async_config_entry_title(data), **kwargs + ) + + +class SchemaOptionsFlowHandler(config_entries.OptionsFlow): + """Handle a schema based options flow.""" + + def __init__( + self, + config_entry: config_entries.ConfigEntry, + options_flow: dict[str, vol.Schema], + async_options_flow_finished: Callable[[HomeAssistant, Mapping[str, Any]], None], + ) -> None: + """Initialize options flow.""" + self._common_handler = SchemaCommonFlowHandler(self, options_flow, config_entry) + self.config_entry = config_entry + self._async_options_flow_finished = async_options_flow_finished + + for step in options_flow: + setattr( + self, + f"async_step_{step}", + types.MethodType(self._async_step(step), self), + ) + + @staticmethod + def _async_step(step_id: str) -> Callable: + """Generate a step handler.""" + + async def _async_step( + self: SchemaConfigFlowHandler, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle an options flow step.""" + # pylint: disable-next=protected-access + result = await self._common_handler.async_step(step_id, user_input) + return result + + return _async_step + + @callback + def async_create_entry( # pylint: disable=arguments-differ + self, + data: Mapping[str, Any], + **kwargs: Any, + ) -> FlowResult: + """Finish config flow and create a config entry.""" + self._async_options_flow_finished(self.hass, data) + return super().async_create_entry(title="", data=data, **kwargs) + + +@callback +def wrapped_entity_config_entry_title( + hass: HomeAssistant, entity_id_or_uuid: str +) -> str: + """Generate title for a config entry wrapping a single entity. + + If the entity is registered, use the registry entry's name. + If the entity is in the state machine, use the name from the state. + Otherwise, fall back to the object ID. + """ + registry = er.async_get(hass) + entity_id = er.async_validate_entity_id(registry, entity_id_or_uuid) + object_id = split_entity_id(entity_id)[1] + entry = registry.async_get(entity_id) + if entry: + return entry.name or entry.original_name or object_id + state = hass.states.get(entity_id) + if state: + return state.name or object_id + return object_id + + +@callback +def entity_selector_without_own_entities( + handler: SchemaOptionsFlowHandler, + entity_selector_config: dict[str, Any], +) -> vol.Schema: + """Return an entity selector which excludes own entities.""" + entity_registry = er.async_get(handler.hass) + entities = er.async_entries_for_config_entry( + entity_registry, + handler.config_entry.entry_id, # pylint: disable=protected-access + ) + entity_ids = [ent.entity_id for ent in entities] + + return selector.selector( + {"entity": {**entity_selector_config, "exclude_entities": entity_ids}} + ) diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 1eabc33b89d..1ede1d10d89 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -4,6 +4,7 @@ from __future__ import annotations import asyncio from collections.abc import Callable, Sequence from contextlib import asynccontextmanager, suppress +from contextvars import ContextVar from datetime import datetime, timedelta from functools import partial import itertools @@ -109,6 +110,7 @@ ATTR_MAX = "max" DATA_SCRIPTS = "helpers.script" DATA_SCRIPT_BREAKPOINTS = "helpers.script_breakpoints" +DATA_NEW_SCRIPT_RUNS_NOT_ALLOWED = "helpers.script_not_allowed" RUN_ID_ANY = "*" NODE_ANY = "*" @@ -126,6 +128,8 @@ SCRIPT_BREAKPOINT_HIT = "script_breakpoint_hit" SCRIPT_DEBUG_CONTINUE_STOP = "script_debug_continue_stop_{}_{}" SCRIPT_DEBUG_CONTINUE_ALL = "script_debug_continue_all" +script_stack_cv: ContextVar[list[int] | None] = ContextVar("script_stack", default=None) + def action_trace_append(variables, path): """Append a TraceElement to trace[path].""" @@ -340,6 +344,12 @@ class _ScriptRun: async def async_run(self) -> None: """Run script.""" + # Push the script to the script execution stack + if (script_stack := script_stack_cv.get()) is None: + script_stack = [] + script_stack_cv.set(script_stack) + script_stack.append(id(self._script)) + try: self._log("Running %s", self._script.running_description) for self._step, self._action in enumerate(self._script.sequence): @@ -355,6 +365,8 @@ class _ScriptRun: script_execution_set("error") raise finally: + # Pop the script from the script execution stack + script_stack.pop() self._finish() async def _async_step(self, log_exceptions): @@ -848,12 +860,13 @@ class _QueuedScriptRun(_ScriptRun): {lock_task, stop_task}, return_when=asyncio.FIRST_COMPLETED ) except asyncio.CancelledError: - lock_task.cancel() self._finish() raise + else: + self.lock_acquired = lock_task.done() and not lock_task.cancelled() finally: + lock_task.cancel() stop_task.cancel() - self.lock_acquired = lock_task.done() and not lock_task.cancelled() # If we've been told to stop, then just finish up. Otherwise, we've acquired the # lock so we can go ahead and start the run. @@ -872,6 +885,7 @@ class _QueuedScriptRun(_ScriptRun): async def _async_stop_scripts_after_shutdown(hass, point_in_time): """Stop running Script objects started after shutdown.""" + hass.data[DATA_NEW_SCRIPT_RUNS_NOT_ALLOWED] = None running_scripts = [ script for script in hass.data[DATA_SCRIPTS] if script["instance"].is_running ] @@ -1181,6 +1195,12 @@ class Script: ) context = Context() + # Prevent spawning new script runs when Home Assistant is shutting down + if DATA_NEW_SCRIPT_RUNS_NOT_ALLOWED in self._hass.data: + self._log("Home Assistant is shutting down, starting script blocked") + return + + # Prevent spawning new script runs if not allowed by script mode if self.is_running: if self.script_mode == SCRIPT_MODE_SINGLE: if self._max_exceeded != "SILENT": @@ -1218,6 +1238,18 @@ class Script: else: variables = cast(dict, run_variables) + # Prevent non-allowed recursive calls which will cause deadlocks when we try to + # stop (restart) or wait for (queued) our own script run. + script_stack = script_stack_cv.get() + if ( + self.script_mode in (SCRIPT_MODE_RESTART, SCRIPT_MODE_QUEUED) + and (script_stack := script_stack_cv.get()) is not None + and id(self) in script_stack + ): + script_execution_set("disallowed_recursion_detected") + self._log("Disallowed recursion detected", level=logging.WARNING) + return + if self.script_mode != SCRIPT_MODE_QUEUED: cls = _ScriptRun else: diff --git a/homeassistant/helpers/selector.py b/homeassistant/helpers/selector.py index f280feb83b2..b4d01ef52e0 100644 --- a/homeassistant/helpers/selector.py +++ b/homeassistant/helpers/selector.py @@ -2,13 +2,12 @@ from __future__ import annotations from collections.abc import Callable -from datetime import time as time_sys from typing import Any, cast import voluptuous as vol from homeassistant.const import CONF_MODE, CONF_UNIT_OF_MEASUREMENT -from homeassistant.core import split_entity_id +from homeassistant.core import split_entity_id, valid_entity_id from homeassistant.util import decorator from . import config_validation as cv @@ -74,109 +73,42 @@ class Selector: return {"selector": {self.selector_type: self.config}} -@SELECTORS.register("entity") -class EntitySelector(Selector): - """Selector of a single entity.""" +SINGLE_ENTITY_SELECTOR_CONFIG_SCHEMA = vol.Schema( + { + # Integration that provided the entity + vol.Optional("integration"): str, + # Domain the entity belongs to + vol.Optional("domain"): vol.Any(str, [str]), + # Device class of the entity + vol.Optional("device_class"): str, + } +) - selector_type = "entity" +SINGLE_DEVICE_SELECTOR_CONFIG_SCHEMA = vol.Schema( + { + # Integration linked to it with a config entry + vol.Optional("integration"): str, + # Manufacturer of device + vol.Optional("manufacturer"): str, + # Model of device + vol.Optional("model"): str, + # Device has to contain entities matching this selector + vol.Optional("entity"): SINGLE_ENTITY_SELECTOR_CONFIG_SCHEMA, + } +) - CONFIG_SCHEMA = vol.Schema( - { - # Integration that provided the entity - vol.Optional("integration"): str, - # Domain the entity belongs to - vol.Optional("domain"): str, - # Device class of the entity - vol.Optional("device_class"): str, - } - ) - def __call__(self, data: Any) -> str: +@SELECTORS.register("action") +class ActionSelector(Selector): + """Selector of an action sequence (script syntax).""" + + selector_type = "action" + + CONFIG_SCHEMA = vol.Schema({}) + + def __call__(self, data: Any) -> Any: """Validate the passed selection.""" - try: - entity_id = cv.entity_id(data) - domain = split_entity_id(entity_id)[0] - except vol.Invalid: - # Not a valid entity_id, maybe it's an entity entry id - return cv.entity_id_or_uuid(cv.string(data)) - else: - if "domain" in self.config and domain != self.config["domain"]: - raise vol.Invalid( - f"Entity {entity_id} belongs to domain {domain}, " - f"expected {self.config['domain']}" - ) - - return entity_id - - -@SELECTORS.register("device") -class DeviceSelector(Selector): - """Selector of a single device.""" - - selector_type = "device" - - CONFIG_SCHEMA = vol.Schema( - { - # Integration linked to it with a config entry - vol.Optional("integration"): str, - # Manufacturer of device - vol.Optional("manufacturer"): str, - # Model of device - vol.Optional("model"): str, - # Device has to contain entities matching this selector - vol.Optional("entity"): EntitySelector.CONFIG_SCHEMA, - } - ) - - def __call__(self, data: Any) -> str: - """Validate the passed selection.""" - return cv.string(data) - - -@SELECTORS.register("area") -class AreaSelector(Selector): - """Selector of a single area.""" - - selector_type = "area" - - CONFIG_SCHEMA = vol.Schema( - { - vol.Optional("entity"): EntitySelector.CONFIG_SCHEMA, - vol.Optional("device"): DeviceSelector.CONFIG_SCHEMA, - } - ) - - def __call__(self, data: Any) -> str: - """Validate the passed selection.""" - return cv.string(data) - - -@SELECTORS.register("number") -class NumberSelector(Selector): - """Selector of a numeric value.""" - - selector_type = "number" - - CONFIG_SCHEMA = vol.Schema( - { - vol.Required("min"): vol.Coerce(float), - vol.Required("max"): vol.Coerce(float), - vol.Optional("step", default=1): vol.All( - vol.Coerce(float), vol.Range(min=1e-3) - ), - vol.Optional(CONF_UNIT_OF_MEASUREMENT): str, - vol.Optional(CONF_MODE, default="slider"): vol.In(["box", "slider"]), - } - ) - - def __call__(self, data: Any) -> float: - """Validate the passed selection.""" - value: float = vol.Coerce(float)(data) - - if not self.config["min"] <= value <= self.config["max"]: - raise vol.Invalid(f"Value {value} is too small or too large") - - return value + return data @SELECTORS.register("addon") @@ -185,11 +117,55 @@ class AddonSelector(Selector): selector_type = "addon" - CONFIG_SCHEMA = vol.Schema({}) + CONFIG_SCHEMA = vol.Schema( + { + vol.Optional("name"): str, + vol.Optional("slug"): str, + } + ) def __call__(self, data: Any) -> str: """Validate the passed selection.""" - return cv.string(data) + addon: str = vol.Schema(str)(data) + return addon + + +@SELECTORS.register("area") +class AreaSelector(Selector): + """Selector of a single or list of areas.""" + + selector_type = "area" + + CONFIG_SCHEMA = vol.Schema( + { + vol.Optional("entity"): SINGLE_ENTITY_SELECTOR_CONFIG_SCHEMA, + vol.Optional("device"): SINGLE_DEVICE_SELECTOR_CONFIG_SCHEMA, + vol.Optional("multiple", default=False): cv.boolean, + } + ) + + def __call__(self, data: Any) -> str | list[str]: + """Validate the passed selection.""" + if not self.config["multiple"]: + area_id: str = vol.Schema(str)(data) + return area_id + if not isinstance(data, list): + raise vol.Invalid("Value should be a list") + return [vol.Schema(str)(val) for val in data] + + +@SELECTORS.register("attribute") +class AttributeSelector(Selector): + """Selector for an entity attribute.""" + + selector_type = "attribute" + + CONFIG_SCHEMA = vol.Schema({vol.Required("entity_id"): cv.entity_id}) + + def __call__(self, data: Any) -> str: + """Validate the passed selection.""" + attribute: str = vol.Schema(str)(data) + return attribute @SELECTORS.register("boolean") @@ -206,56 +182,269 @@ class BooleanSelector(Selector): return value -@SELECTORS.register("time") -class TimeSelector(Selector): - """Selector of a time value.""" +@SELECTORS.register("color_rgb") +class ColorRGBSelector(Selector): + """Selector of an RGB color value.""" - selector_type = "time" + selector_type = "color_rgb" CONFIG_SCHEMA = vol.Schema({}) - def __call__(self, data: Any) -> time_sys: + def __call__(self, data: Any) -> list[int]: """Validate the passed selection.""" - return cv.time(data) + value: list[int] = vol.All(list, vol.ExactSequence((cv.byte,) * 3))(data) + return value -@SELECTORS.register("target") -class TargetSelector(Selector): - """Selector of a target value (area ID, device ID, entity ID etc). +@SELECTORS.register("color_temp") +class ColorTempSelector(Selector): + """Selector of an color temperature.""" - Value should follow cv.TARGET_SERVICE_FIELDS format. - """ - - selector_type = "target" + selector_type = "color_temp" CONFIG_SCHEMA = vol.Schema( { - vol.Optional("entity"): EntitySelector.CONFIG_SCHEMA, - vol.Optional("device"): DeviceSelector.CONFIG_SCHEMA, + vol.Optional("max_mireds"): vol.Coerce(int), + vol.Optional("min_mireds"): vol.Coerce(int), } ) - TARGET_SELECTION_SCHEMA = vol.Schema(cv.TARGET_SERVICE_FIELDS) - - def __call__(self, data: Any) -> dict[str, list[str]]: + def __call__(self, data: Any) -> int: """Validate the passed selection.""" - target: dict[str, list[str]] = self.TARGET_SELECTION_SCHEMA(data) - return target + value: int = vol.All( + vol.Coerce(float), + vol.Range( + min=self.config.get("min_mireds"), + max=self.config.get("max_mireds"), + ), + )(data) + return value -@SELECTORS.register("action") -class ActionSelector(Selector): - """Selector of an action sequence (script syntax).""" +@SELECTORS.register("date") +class DateSelector(Selector): + """Selector of a date.""" - selector_type = "action" + selector_type = "date" CONFIG_SCHEMA = vol.Schema({}) def __call__(self, data: Any) -> Any: """Validate the passed selection.""" + cv.date(data) return data +@SELECTORS.register("datetime") +class DateTimeSelector(Selector): + """Selector of a datetime.""" + + selector_type = "datetime" + + CONFIG_SCHEMA = vol.Schema({}) + + def __call__(self, data: Any) -> Any: + """Validate the passed selection.""" + cv.datetime(data) + return data + + +@SELECTORS.register("device") +class DeviceSelector(Selector): + """Selector of a single or list of devices.""" + + selector_type = "device" + + CONFIG_SCHEMA = SINGLE_DEVICE_SELECTOR_CONFIG_SCHEMA.extend( + {vol.Optional("multiple", default=False): cv.boolean} + ) + + def __call__(self, data: Any) -> str | list[str]: + """Validate the passed selection.""" + if not self.config["multiple"]: + device_id: str = vol.Schema(str)(data) + return device_id + if not isinstance(data, list): + raise vol.Invalid("Value should be a list") + return [vol.Schema(str)(val) for val in data] + + +@SELECTORS.register("duration") +class DurationSelector(Selector): + """Selector for a duration.""" + + selector_type = "duration" + + CONFIG_SCHEMA = vol.Schema( + { + # Enable day field in frontend. A selection with `days` set is allowed + # even if `enable_day` is not set + vol.Optional("enable_day"): cv.boolean, + } + ) + + def __call__(self, data: Any) -> dict[str, float]: + """Validate the passed selection.""" + cv.time_period_dict(data) + return cast(dict[str, float], data) + + +@SELECTORS.register("entity") +class EntitySelector(Selector): + """Selector of a single or list of entities.""" + + selector_type = "entity" + + CONFIG_SCHEMA = SINGLE_ENTITY_SELECTOR_CONFIG_SCHEMA.extend( + { + vol.Optional("exclude_entities"): [str], + vol.Optional("include_entities"): [str], + vol.Optional("multiple", default=False): cv.boolean, + } + ) + + def __call__(self, data: Any) -> str | list[str]: + """Validate the passed selection.""" + + include_entities = self.config.get("include_entities") + exclude_entities = self.config.get("exclude_entities") + + def validate(e_or_u: str) -> str: + e_or_u = cv.entity_id_or_uuid(e_or_u) + if not valid_entity_id(e_or_u): + return e_or_u + if allowed_domains := cv.ensure_list(self.config.get("domain")): + domain = split_entity_id(e_or_u)[0] + if domain not in allowed_domains: + raise vol.Invalid( + f"Entity {e_or_u} belongs to domain {domain}, " + f"expected {allowed_domains}" + ) + if include_entities: + vol.In(include_entities)(e_or_u) + if exclude_entities: + vol.NotIn(exclude_entities)(e_or_u) + return e_or_u + + if not self.config["multiple"]: + return validate(data) + if not isinstance(data, list): + raise vol.Invalid("Value should be a list") + return cast(list, vol.Schema([validate])(data)) # Output is a list + + +@SELECTORS.register("icon") +class IconSelector(Selector): + """Selector for an icon.""" + + selector_type = "icon" + + CONFIG_SCHEMA = vol.Schema( + {vol.Optional("placeholder"): str} + # Frontend also has a fallbackPath option, this is not used by core + ) + + def __call__(self, data: Any) -> str: + """Validate the passed selection.""" + icon: str = vol.Schema(str)(data) + return icon + + +@SELECTORS.register("location") +class LocationSelector(Selector): + """Selector for a location.""" + + selector_type = "location" + + CONFIG_SCHEMA = vol.Schema( + {vol.Optional("radius"): bool, vol.Optional("icon"): str} + ) + DATA_SCHEMA = vol.Schema( + { + vol.Required("latitude"): float, + vol.Required("longitude"): float, + vol.Optional("radius"): float, + } + ) + + def __call__(self, data: Any) -> dict[str, float]: + """Validate the passed selection.""" + location: dict[str, float] = self.DATA_SCHEMA(data) + return location + + +@SELECTORS.register("media") +class MediaSelector(Selector): + """Selector for media.""" + + selector_type = "media" + + CONFIG_SCHEMA = vol.Schema({}) + DATA_SCHEMA = vol.Schema( + { + # Although marked as optional in frontend, this field is required + vol.Required("entity_id"): cv.entity_id_or_uuid, + # Although marked as optional in frontend, this field is required + vol.Required("media_content_id"): str, + # Although marked as optional in frontend, this field is required + vol.Required("media_content_type"): str, + vol.Remove("metadata"): dict, + } + ) + + def __call__(self, data: Any) -> dict[str, float]: + """Validate the passed selection.""" + media: dict[str, float] = self.DATA_SCHEMA(data) + return media + + +def has_min_max_if_slider(data: Any) -> Any: + """Validate configuration.""" + if data["mode"] == "box": + return data + + if "min" not in data or "max" not in data: + raise vol.Invalid("min and max are required in slider mode") + + return data + + +@SELECTORS.register("number") +class NumberSelector(Selector): + """Selector of a numeric value.""" + + selector_type = "number" + + CONFIG_SCHEMA = vol.All( + vol.Schema( + { + vol.Optional("min"): vol.Coerce(float), + vol.Optional("max"): vol.Coerce(float), + # Controls slider steps, and up/down keyboard binding for the box + # user input is not rounded + vol.Optional("step", default=1): vol.All( + vol.Coerce(float), vol.Range(min=1e-3) + ), + vol.Optional(CONF_UNIT_OF_MEASUREMENT): str, + vol.Optional(CONF_MODE, default="slider"): vol.In(["box", "slider"]), + } + ), + has_min_max_if_slider, + ) + + def __call__(self, data: Any) -> float: + """Validate the passed selection.""" + value: float = vol.Coerce(float)(data) + + if "min" in self.config and value < self.config["min"]: + raise vol.Invalid(f"Value {value} is too small") + + if "max" in self.config and value > self.config["max"]: + raise vol.Invalid(f"Value {value} is too large") + + return value + + @SELECTORS.register("object") class ObjectSelector(Selector): """Selector for an arbitrary object.""" @@ -269,31 +458,136 @@ class ObjectSelector(Selector): return data +select_option = vol.All( + dict, + vol.Schema( + { + vol.Required("value"): str, + vol.Required("label"): str, + } + ), +) + + +@SELECTORS.register("select") +class SelectSelector(Selector): + """Selector for an single or multi-choice input select.""" + + selector_type = "select" + + CONFIG_SCHEMA = vol.Schema( + { + vol.Required("options"): vol.All(vol.Any([str], [select_option])), + vol.Optional("multiple", default=False): cv.boolean, + vol.Optional("custom_value", default=False): cv.boolean, + vol.Optional("mode"): vol.In(("list", "dropdown")), + } + ) + + def __call__(self, data: Any) -> Any: + """Validate the passed selection.""" + options = [] + if self.config["options"]: + if isinstance(self.config["options"][0], str): + options = self.config["options"] + else: + options = [option["value"] for option in self.config["options"]] + + parent_schema = vol.In(options) + if self.config["custom_value"]: + parent_schema = vol.Any(parent_schema, str) + + if not self.config["multiple"]: + return parent_schema(vol.Schema(str)(data)) + if not isinstance(data, list): + raise vol.Invalid("Value should be a list") + return [parent_schema(vol.Schema(str)(val)) for val in data] + + @SELECTORS.register("text") class StringSelector(Selector): """Selector for a multi-line text string.""" selector_type = "text" - CONFIG_SCHEMA = vol.Schema({vol.Optional("multiline", default=False): bool}) + STRING_TYPES = [ + "number", + "text", + "search", + "tel", + "url", + "email", + "password", + "date", + "month", + "week", + "time", + "datetime-local", + "color", + ] + CONFIG_SCHEMA = vol.Schema( + { + vol.Optional("multiline", default=False): bool, + vol.Optional("suffix"): str, + # The "type" controls the input field in the browser, the resulting + # data can be any string so we don't validate it. + vol.Optional("type"): vol.In(STRING_TYPES), + } + ) def __call__(self, data: Any) -> str: """Validate the passed selection.""" - text = cv.string(data) + text: str = vol.Schema(str)(data) return text -@SELECTORS.register("select") -class SelectSelector(Selector): - """Selector for an single-choice input select.""" +@SELECTORS.register("target") +class TargetSelector(Selector): + """Selector of a target value (area ID, device ID, entity ID etc). - selector_type = "select" + Value should follow cv.TARGET_SERVICE_FIELDS format. + """ + + selector_type = "target" CONFIG_SCHEMA = vol.Schema( - {vol.Required("options"): vol.All([str], vol.Length(min=1))} + { + vol.Optional("entity"): SINGLE_ENTITY_SELECTOR_CONFIG_SCHEMA, + vol.Optional("device"): SINGLE_DEVICE_SELECTOR_CONFIG_SCHEMA, + } ) - def __call__(self, data: Any) -> Any: + TARGET_SELECTION_SCHEMA = vol.Schema(cv.TARGET_SERVICE_FIELDS) + + def __call__(self, data: Any) -> dict[str, list[str]]: """Validate the passed selection.""" - selected_option = vol.In(self.config["options"])(cv.string(data)) - return selected_option + target: dict[str, list[str]] = self.TARGET_SELECTION_SCHEMA(data) + return target + + +@SELECTORS.register("theme") +class ThemeSelector(Selector): + """Selector for an theme.""" + + selector_type = "theme" + + CONFIG_SCHEMA = vol.Schema({}) + + def __call__(self, data: Any) -> str: + """Validate the passed selection.""" + theme: str = vol.Schema(str)(data) + return theme + + +@SELECTORS.register("time") +class TimeSelector(Selector): + """Selector of a time value.""" + + selector_type = "time" + + CONFIG_SCHEMA = vol.Schema({}) + + def __call__(self, data: Any) -> str: + """Validate the passed selection.""" + cv.time(data) + return cast(str, data) diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index 2fee7f0716f..9d446f10913 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -220,7 +220,7 @@ def async_prepare_call_from_config( registry = entity_registry.async_get(hass) entity_ids = cv.comp_entity_ids_or_uuids(target[CONF_ENTITY_ID]) if entity_ids not in (ENTITY_MATCH_ALL, ENTITY_MATCH_NONE): - entity_ids = entity_registry.async_resolve_entity_ids( + entity_ids = entity_registry.async_validate_entity_ids( registry, entity_ids ) target[CONF_ENTITY_ID] = entity_ids @@ -362,7 +362,7 @@ def async_extract_referenced_entity_ids( if area_id not in area_reg.areas: selected.missing_areas.add(area_id) - # Find devices for this area + # Find devices for targeted areas selected.referenced_devices.update(selector.device_ids) for device_entry in dev_reg.devices.values(): if device_entry.area_id in selector.area_ids: @@ -372,20 +372,20 @@ def async_extract_referenced_entity_ids( return selected for ent_entry in ent_reg.entities.values(): - # Do not add config or diagnostic entities referenced by areas or devices - - if ent_entry.entity_category is not None: + # Do not add entities which are hidden or which are config or diagnostic entities + if ent_entry.entity_category is not None or ent_entry.hidden_by is not None: continue if ( - # when area matches the target area + # The entity's area matches a targeted area ent_entry.area_id in selector.area_ids - # when device matches a referenced devices with no explicitly set area + # The entity's device matches a device referenced by an area and the entity + # has no explicitly set area or ( not ent_entry.area_id and ent_entry.device_id in selected.referenced_devices ) - # when device matches target device + # The entity's device matches a targeted device or ent_entry.device_id in selector.device_ids ): selected.indirectly_referenced.add(ent_entry.entity_id) @@ -530,7 +530,7 @@ def async_set_service_schema( @bind_hass -async def entity_service_call( +async def entity_service_call( # noqa: C901 hass: HomeAssistant, platforms: Iterable[EntityPlatform], func: str | Callable[..., Any], @@ -649,6 +649,12 @@ async def entity_service_call( for feature_set in required_features ) ): + # If entity explicitly referenced, raise an error + if referenced is not None and entity.entity_id in referenced.referenced: + raise HomeAssistantError( + f"Entity {entity.entity_id} does not support this service." + ) + continue entities.append(entity) diff --git a/homeassistant/helpers/singleton.py b/homeassistant/helpers/singleton.py index 7012241fe4e..a4f6d32c303 100644 --- a/homeassistant/helpers/singleton.py +++ b/homeassistant/helpers/singleton.py @@ -9,9 +9,9 @@ from typing import TypeVar, cast from homeassistant.core import HomeAssistant from homeassistant.loader import bind_hass -T = TypeVar("T") +_T = TypeVar("_T") -FUNC = Callable[[HomeAssistant], T] +FUNC = Callable[[HomeAssistant], _T] def singleton(data_key: str) -> Callable[[FUNC], FUNC]: @@ -26,30 +26,30 @@ def singleton(data_key: str) -> Callable[[FUNC], FUNC]: @bind_hass @functools.wraps(func) - def wrapped(hass: HomeAssistant) -> T: + def wrapped(hass: HomeAssistant) -> _T: if data_key not in hass.data: hass.data[data_key] = func(hass) - return cast(T, hass.data[data_key]) + return cast(_T, hass.data[data_key]) return wrapped @bind_hass @functools.wraps(func) - async def async_wrapped(hass: HomeAssistant) -> T: + async def async_wrapped(hass: HomeAssistant) -> _T: if data_key not in hass.data: evt = hass.data[data_key] = asyncio.Event() result = await func(hass) hass.data[data_key] = result evt.set() - return cast(T, result) + return cast(_T, result) obj_or_evt = hass.data[data_key] if isinstance(obj_or_evt, asyncio.Event): await obj_or_evt.wait() - return cast(T, hass.data[data_key]) + return cast(_T, hass.data[data_key]) - return cast(T, obj_or_evt) + return cast(_T, obj_or_evt) return async_wrapped diff --git a/homeassistant/helpers/start.py b/homeassistant/helpers/start.py index 4560119a685..7f919f5351d 100644 --- a/homeassistant/helpers/start.py +++ b/homeassistant/helpers/start.py @@ -4,13 +4,13 @@ from __future__ import annotations from collections.abc import Awaitable, Callable from homeassistant.const import EVENT_HOMEASSISTANT_START -from homeassistant.core import Event, HassJob, HomeAssistant, callback +from homeassistant.core import CALLBACK_TYPE, Event, HassJob, HomeAssistant, callback @callback def async_at_start( hass: HomeAssistant, at_start_cb: Callable[[HomeAssistant], Awaitable[None] | None] -) -> None: +) -> CALLBACK_TYPE: """Execute something when Home Assistant is started. Will execute it now if Home Assistant is already started. @@ -18,10 +18,10 @@ def async_at_start( at_start_job = HassJob(at_start_cb) if hass.is_running: hass.async_run_hass_job(at_start_job, hass) - return + return lambda: None async def _matched_event(event: Event) -> None: """Call the callback when Home Assistant started.""" hass.async_run_hass_job(at_start_job, hass) - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, _matched_event) + return hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, _matched_event) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 2b93b69fe37..0d849f67d9a 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -887,6 +887,9 @@ def result_as_boolean(template_result: Any | None) -> bool: def expand(hass: HomeAssistant, *args: Any) -> Iterable[State]: """Expand out any groups into entity states.""" + # circular import. + from . import entity as entity_helper # pylint: disable=import-outside-toplevel + search = list(args) found = {} while search: @@ -904,7 +907,10 @@ def expand(hass: HomeAssistant, *args: Any) -> Iterable[State]: # ignore other types continue - if entity_id.startswith(_GROUP_DOMAIN_PREFIX): + if entity_id.startswith(_GROUP_DOMAIN_PREFIX) or ( + (source := entity_helper.entity_sources(hass).get(entity_id)) + and source["domain"] == "group" + ): # Collect state will be called in here since it's wrapped group_entities = entity.attributes.get(ATTR_ENTITY_ID) if group_entities: diff --git a/homeassistant/helpers/trigger.py b/homeassistant/helpers/trigger.py index 0b18ad9aa42..79ac9f33f24 100644 --- a/homeassistant/helpers/trigger.py +++ b/homeassistant/helpers/trigger.py @@ -3,13 +3,14 @@ from __future__ import annotations import asyncio from collections.abc import Callable +import functools import logging from typing import Any import voluptuous as vol -from homeassistant.const import CONF_ID, CONF_PLATFORM -from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback +from homeassistant.const import CONF_ID, CONF_PLATFORM, CONF_VARIABLES +from homeassistant.core import CALLBACK_TYPE, Context, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.loader import IntegrationNotFound, async_get_integration @@ -55,6 +56,25 @@ async def async_validate_trigger_config( return config +def _trigger_action_wrapper( + hass: HomeAssistant, action: Callable, conf: ConfigType +) -> Callable: + """Wrap trigger action with extra vars if configured.""" + if CONF_VARIABLES not in conf: + return action + + @functools.wraps(action) + async def with_vars( + run_variables: dict[str, Any], context: Context | None = None + ) -> None: + """Wrap action with extra vars.""" + trigger_variables = conf[CONF_VARIABLES] + run_variables.update(trigger_variables.async_render(hass, run_variables)) + await action(run_variables, context) + + return with_vars + + async def async_initialize_triggers( hass: HomeAssistant, trigger_config: list[ConfigType], @@ -80,7 +100,12 @@ async def async_initialize_triggers( "variables": variables, "trigger_data": trigger_data, } - triggers.append(platform.async_attach_trigger(hass, conf, action, info)) + + triggers.append( + platform.async_attach_trigger( + hass, conf, _trigger_action_wrapper(hass, action, conf), info + ) + ) attach_results = await asyncio.gather(*triggers, return_exceptions=True) removes: list[Callable[[], None]] = [] diff --git a/homeassistant/helpers/update_coordinator.py b/homeassistant/helpers/update_coordinator.py index d9ab337e84e..0c0d647d48a 100644 --- a/homeassistant/helpers/update_coordinator.py +++ b/homeassistant/helpers/update_coordinator.py @@ -6,7 +6,7 @@ from collections.abc import Awaitable, Callable from datetime import datetime, timedelta import logging from time import monotonic -from typing import Generic, TypeVar +from typing import Any, Generic, TypeVar # pylint: disable=unused-import import urllib.error import aiohttp @@ -23,14 +23,17 @@ from .debounce import Debouncer REQUEST_REFRESH_DEFAULT_COOLDOWN = 10 REQUEST_REFRESH_DEFAULT_IMMEDIATE = True -T = TypeVar("T") +_T = TypeVar("_T") +_DataUpdateCoordinatorT = TypeVar( + "_DataUpdateCoordinatorT", bound="DataUpdateCoordinator[Any]" +) class UpdateFailed(Exception): """Raised when an update has failed.""" -class DataUpdateCoordinator(Generic[T]): +class DataUpdateCoordinator(Generic[_T]): """Class to manage fetching data from single endpoint.""" def __init__( @@ -40,7 +43,7 @@ class DataUpdateCoordinator(Generic[T]): *, name: str, update_interval: timedelta | None = None, - update_method: Callable[[], Awaitable[T]] | None = None, + update_method: Callable[[], Awaitable[_T]] | None = None, request_refresh_debouncer: Debouncer | None = None, ) -> None: """Initialize global data updater.""" @@ -56,7 +59,7 @@ class DataUpdateCoordinator(Generic[T]): # to make sure the first update was successful. # Set type to just T to remove annoying checks that data is not None # when it was already checked during setup. - self.data: T = None # type: ignore[assignment] + self.data: _T = None # type: ignore[assignment] self._listeners: list[CALLBACK_TYPE] = [] self._job = HassJob(self._handle_refresh_interval) @@ -140,7 +143,7 @@ class DataUpdateCoordinator(Generic[T]): """ await self._debounced_refresh.async_call() - async def _async_update_data(self) -> T: + async def _async_update_data(self) -> _T: """Fetch the latest data from the source.""" if self.update_method is None: raise NotImplementedError("Update method not implemented") @@ -265,7 +268,7 @@ class DataUpdateCoordinator(Generic[T]): update_callback() @callback - def async_set_updated_data(self, data: T) -> None: + def async_set_updated_data(self, data: _T) -> None: """Manually update data, notify listeners and reset refresh interval.""" if self._unsub_refresh: self._unsub_refresh() @@ -295,10 +298,10 @@ class DataUpdateCoordinator(Generic[T]): self._unsub_refresh = None -class CoordinatorEntity(Generic[T], entity.Entity): +class CoordinatorEntity(entity.Entity, Generic[_DataUpdateCoordinatorT]): """A class for entities using DataUpdateCoordinator.""" - def __init__(self, coordinator: DataUpdateCoordinator[T]) -> None: + def __init__(self, coordinator: _DataUpdateCoordinatorT) -> None: """Create the entity with a DataUpdateCoordinator.""" self.coordinator = coordinator diff --git a/homeassistant/loader.py b/homeassistant/loader.py index f5c68897e2e..364f212a1be 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -16,7 +16,7 @@ import logging import pathlib import sys from types import ModuleType -from typing import TYPE_CHECKING, Any, TypedDict, TypeVar, cast +from typing import TYPE_CHECKING, Any, Literal, TypedDict, TypeVar, cast from awesomeversion import ( AwesomeVersion, @@ -35,9 +35,7 @@ from .util.async_ import gather_with_concurrency if TYPE_CHECKING: from .core import HomeAssistant -CALLABLE_T = TypeVar( # pylint: disable=invalid-name - "CALLABLE_T", bound=Callable[..., Any] -) +_CallableT = TypeVar("_CallableT", bound=Callable[..., Any]) _LOGGER = logging.getLogger(__name__) @@ -60,6 +58,24 @@ MAX_LOAD_CONCURRENTLY = 4 MOVED_ZEROCONF_PROPS = ("macaddress", "model", "manufacturer") +class DHCPMatcherRequired(TypedDict, total=True): + """Matcher for the dhcp integration for required fields.""" + + domain: str + + +class DHCPMatcherOptional(TypedDict, total=False): + """Matcher for the dhcp integration for optional fields.""" + + macaddress: str + hostname: str + registered_devices: bool + + +class DHCPMatcher(DHCPMatcherRequired, DHCPMatcherOptional): + """Matcher for the dhcp integration.""" + + class Manifest(TypedDict, total=False): """ Integration manifest. @@ -71,6 +87,7 @@ class Manifest(TypedDict, total=False): name: str disabled: str domain: str + integration_type: Literal["integration", "helper"] dependencies: list[str] after_dependencies: list[str] requirements: list[str] @@ -164,20 +181,29 @@ async def async_get_custom_components( return cast(dict[str, "Integration"], reg_or_evt) -async def async_get_config_flows(hass: HomeAssistant) -> set[str]: +async def async_get_config_flows( + hass: HomeAssistant, + type_filter: Literal["helper", "integration"] | None = None, +) -> set[str]: """Return cached list of config flows.""" # pylint: disable=import-outside-toplevel from .generated.config_flows import FLOWS - flows: set[str] = set() - flows.update(FLOWS) - integrations = await async_get_custom_components(hass) + flows: set[str] = set() + + if type_filter is not None: + flows.update(FLOWS[type_filter]) + else: + for type_flows in FLOWS.values(): + flows.update(type_flows) + flows.update( [ integration.domain for integration in integrations.values() if integration.config_flow + and (type_filter is None or integration.integration_type == type_filter) ] ) @@ -228,16 +254,16 @@ async def async_get_zeroconf( return zeroconf -async def async_get_dhcp(hass: HomeAssistant) -> list[dict[str, str | bool]]: +async def async_get_dhcp(hass: HomeAssistant) -> list[DHCPMatcher]: """Return cached list of dhcp types.""" - dhcp: list[dict[str, str | bool]] = DHCP.copy() + dhcp = cast(list[DHCPMatcher], 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}) + dhcp.append(cast(DHCPMatcher, {"domain": integration.domain, **entry})) return dhcp @@ -458,6 +484,11 @@ class Integration: """Return the integration IoT Class.""" return self.manifest.get("iot_class") + @property + def integration_type(self) -> Literal["integration", "helper"]: + """Return the integration type.""" + return self.manifest.get("integration_type", "integration") + @property def mqtt(self) -> list[str] | None: """Return Integration MQTT entries.""" @@ -787,7 +818,7 @@ class Helpers: return wrapped -def bind_hass(func: CALLABLE_T) -> CALLABLE_T: +def bind_hass(func: _CallableT) -> _CallableT: """Decorate function to indicate that first argument is hass.""" setattr(func, "__bind_hass", True) return func diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 7db411585ef..1ebfb713b70 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -1,10 +1,10 @@ -PyJWT==2.1.0 -PyNaCl==1.4.0 +PyJWT==2.3.0 +PyNaCl==1.5.0 aiodiscover==1.4.8 aiohttp==3.8.1 aiohttp_cors==0.7.0 astral==2.2 -async-upnp-client==0.23.5 +async-upnp-client==0.27.0 async_timeout==4.0.2 atomicwrites==1.4.0 attrs==21.2.0 @@ -13,11 +13,13 @@ bcrypt==3.1.7 certifi>=2021.5.30 ciso8601==2.2.0 cryptography==35.0.0 +fnvhash==0.1.0 hass-nabucasa==0.54.0 -home-assistant-frontend==20220301.2 -httpx==0.21.3 +home-assistant-frontend==20220405.0 +httpx==0.22.0 ifaddr==0.1.7 -jinja2==3.0.3 +jinja2==3.1.0 +lru-dict==1.1.7 paho-mqtt==1.6.1 pillow==9.0.1 pip>=21.0,<22.1 @@ -27,7 +29,7 @@ pyudev==0.22.0 pyyaml==6.0 requests==2.27.1 scapy==2.4.5 -sqlalchemy==1.4.27 +sqlalchemy==1.4.32 typing-extensions>=3.10.0.2,<5.0 voluptuous-serialize==2.5.0 voluptuous==0.12.2 @@ -48,7 +50,7 @@ httplib2>=0.19.0 # gRPC is an implicit dependency that we want to make explicit so we manage # upgrades intentionally. It is a large package to build from source and we # want to ensure we have wheels built. -grpcio==1.44.0 +grpcio==1.45.0 # libcst >=0.4.0 requires a newer Rust than we currently have available, # thus our wheels builds fail. This pins it to the last working version, @@ -77,7 +79,7 @@ regex==2021.8.28 # requirements so we can directly link HA versions to these library versions. anyio==3.5.0 h11==0.12.0 -httpcore==0.14.5 +httpcore==0.14.7 # Ensure we have a hyperframe version that works in Python 3.10 # 5.2.0 fixed a collections abc deprecation diff --git a/homeassistant/scripts/benchmark/__init__.py b/homeassistant/scripts/benchmark/__init__.py index c57d082de61..5441d39c877 100644 --- a/homeassistant/scripts/benchmark/__init__.py +++ b/homeassistant/scripts/benchmark/__init__.py @@ -22,7 +22,7 @@ from homeassistant.util import dt as dt_util # mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs # mypy: no-warn-return-any -CALLABLE_T = TypeVar("CALLABLE_T", bound=Callable) # pylint: disable=invalid-name +_CallableT = TypeVar("_CallableT", bound=Callable) BENCHMARKS: dict[str, Callable] = {} @@ -54,7 +54,7 @@ async def run_benchmark(bench): await hass.async_stop() -def benchmark(func: CALLABLE_T) -> CALLABLE_T: +def benchmark(func: _CallableT) -> _CallableT: """Decorate to mark a benchmark.""" BENCHMARKS[func.__name__] = func return func diff --git a/homeassistant/util/__init__.py b/homeassistant/util/__init__.py index 15e9254e9d8..6562ecedb4f 100644 --- a/homeassistant/util/__init__.py +++ b/homeassistant/util/__init__.py @@ -15,8 +15,8 @@ import slugify as unicode_slug from .dt import as_local, utcnow -T = TypeVar("T") -U = TypeVar("U") # pylint: disable=invalid-name +_T = TypeVar("_T") +_U = TypeVar("_U") RE_SANITIZE_FILENAME = re.compile(r"(~|\.\.|/|\\)") RE_SANITIZE_PATH = re.compile(r"(~|\.(\.)+)") @@ -63,8 +63,8 @@ def repr_helper(inp: Any) -> str: def convert( - value: T | None, to_type: Callable[[T], U], default: U | None = None -) -> U | None: + value: _T | None, to_type: Callable[[_T], _U], default: _U | None = None +) -> _U | None: """Convert value to to_type, returns default if fails.""" try: return default if value is None else to_type(value) diff --git a/homeassistant/util/location.py b/homeassistant/util/location.py index 4e76fa32de3..e653b439e0e 100644 --- a/homeassistant/util/location.py +++ b/homeassistant/util/location.py @@ -14,8 +14,8 @@ import aiohttp from homeassistant.const import __version__ as HA_VERSION -WHOAMI_URL = "https://whoami.home-assistant.io/v1" -WHOAMI_URL_DEV = "https://whoami-v1-dev.home-assistant.workers.dev/v1" +WHOAMI_URL = "https://services.home-assistant.io/whoami/v1" +WHOAMI_URL_DEV = "https://services-dev.home-assistant.workers.dev/whoami/v1" # Constants from https://github.com/maurycyp/vincenty # Earth ellipsoid according to WGS 84 diff --git a/homeassistant/util/network.py b/homeassistant/util/network.py index 396ab56b8c2..87077a0eb0a 100644 --- a/homeassistant/util/network.py +++ b/homeassistant/util/network.py @@ -82,6 +82,6 @@ def is_ipv6_address(address: str) -> bool: def normalize_url(address: str) -> str: """Normalize a given URL.""" url = yarl.URL(address.rstrip("/")) - if url.is_default_port(): + if url.is_absolute() and url.is_default_port(): return str(url.with_port(None)) return str(url) diff --git a/homeassistant/util/percentage.py b/homeassistant/util/percentage.py index d5646b44c0f..63bf78ed1f0 100644 --- a/homeassistant/util/percentage.py +++ b/homeassistant/util/percentage.py @@ -3,10 +3,10 @@ from __future__ import annotations from typing import TypeVar -T = TypeVar("T") +_T = TypeVar("_T") -def ordered_list_item_to_percentage(ordered_list: list[T], item: T) -> int: +def ordered_list_item_to_percentage(ordered_list: list[_T], item: _T) -> int: """Determine the percentage of an item in an ordered list. When using this utility for fan speeds, do not include "off" @@ -29,7 +29,7 @@ def ordered_list_item_to_percentage(ordered_list: list[T], item: T) -> int: return (list_position * 100) // list_len -def percentage_to_ordered_list_item(ordered_list: list[T], percentage: int) -> T: +def percentage_to_ordered_list_item(ordered_list: list[_T], percentage: int) -> _T: """Find the item that most closely matches the percentage in an ordered list. When using this utility for fan speeds, do not include "off" diff --git a/homeassistant/util/read_only_dict.py b/homeassistant/util/read_only_dict.py index f9cc949afdc..bb93dd41298 100644 --- a/homeassistant/util/read_only_dict.py +++ b/homeassistant/util/read_only_dict.py @@ -7,11 +7,11 @@ def _readonly(*args: Any, **kwargs: Any) -> Any: raise RuntimeError("Cannot modify ReadOnlyDict") -Key = TypeVar("Key") -Value = TypeVar("Value") +_KT = TypeVar("_KT") +_VT = TypeVar("_VT") -class ReadOnlyDict(dict[Key, Value]): +class ReadOnlyDict(dict[_KT, _VT]): """Read only version of dict that is compatible with dict types.""" __setitem__ = _readonly diff --git a/homeassistant/util/temperature.py b/homeassistant/util/temperature.py index bc3cb4c1017..d7b7597d6d0 100644 --- a/homeassistant/util/temperature.py +++ b/homeassistant/util/temperature.py @@ -7,6 +7,12 @@ from homeassistant.const import ( UNIT_NOT_RECOGNIZED_TEMPLATE, ) +VALID_UNITS: tuple[str, ...] = ( + TEMP_CELSIUS, + TEMP_FAHRENHEIT, + TEMP_KELVIN, +) + def fahrenheit_to_celsius(fahrenheit: float, interval: bool = False) -> float: """Convert a temperature in Fahrenheit to Celsius.""" diff --git a/homeassistant/util/yaml/loader.py b/homeassistant/util/yaml/loader.py index 84349ef91a9..3507ab96286 100644 --- a/homeassistant/util/yaml/loader.py +++ b/homeassistant/util/yaml/loader.py @@ -19,7 +19,7 @@ from .objects import Input, NodeListClass, NodeStrClass # mypy: allow-untyped-calls, no-warn-return-any JSON_TYPE = Union[list, dict, str] # pylint: disable=invalid-name -DICT_T = TypeVar("DICT_T", bound=dict) # pylint: disable=invalid-name +_DictT = TypeVar("_DictT", bound=dict) _LOGGER = logging.getLogger(__name__) @@ -144,8 +144,8 @@ def _add_reference( @overload def _add_reference( - obj: DICT_T, loader: SafeLineLoader, node: yaml.nodes.Node -) -> DICT_T: + obj: _DictT, loader: SafeLineLoader, node: yaml.nodes.Node +) -> _DictT: ... diff --git a/mypy.ini b/mypy.ini index 781bc5c199e..9c98db3ca10 100644 --- a/mypy.ini +++ b/mypy.ini @@ -12,6 +12,7 @@ warn_incomplete_stub = true warn_redundant_casts = true warn_unused_configs = true warn_unused_ignores = true +enable_error_code = ignore-without-code check_untyped_defs = true disallow_incomplete_defs = true disallow_subclassing_any = true @@ -109,6 +110,17 @@ warn_return_any = true warn_unreachable = true no_implicit_reexport = true +[mypy-homeassistant.components.alert.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.abode.*] check_untyped_defs = true disallow_incomplete_defs = true @@ -153,6 +165,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.adguard.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.aftership.*] check_untyped_defs = true disallow_incomplete_defs = true @@ -296,6 +319,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.backup.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.binary_sensor.*] check_untyped_defs = true disallow_incomplete_defs = true @@ -549,6 +583,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.dhcp.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.dlna_dmr.*] check_untyped_defs = true disallow_incomplete_defs = true @@ -659,6 +704,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.filesize.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.fitbit.*] check_untyped_defs = true disallow_incomplete_defs = true @@ -835,6 +891,94 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.homekit] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + +[mypy-homeassistant.components.homekit.accessories] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + +[mypy-homeassistant.components.homekit.aidmanager] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + +[mypy-homeassistant.components.homekit.config_flow] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + +[mypy-homeassistant.components.homekit.diagnostics] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + +[mypy-homeassistant.components.homekit.logbook] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + +[mypy-homeassistant.components.homekit.type_triggers] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + +[mypy-homeassistant.components.homekit.util] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.homekit_controller] check_untyped_defs = true disallow_incomplete_defs = true @@ -1055,6 +1199,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.kaleidescape.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.knx.*] check_untyped_defs = true disallow_incomplete_defs = true @@ -1176,6 +1331,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.media_source.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.mjpeg.*] check_untyped_defs = true disallow_incomplete_defs = true @@ -1209,7 +1375,7 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true -[mypy-homeassistant.components.media_source.*] +[mypy-homeassistant.components.moon.*] check_untyped_defs = true disallow_incomplete_defs = true disallow_subclassing_any = true @@ -1407,6 +1573,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.peco.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.overkiz.*] check_untyped_defs = true disallow_incomplete_defs = true @@ -1517,6 +1694,83 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.recorder] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + +[mypy-homeassistant.components.recorder.const] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + +[mypy-homeassistant.components.recorder.backup] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + +[mypy-homeassistant.components.recorder.executor] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + +[mypy-homeassistant.components.recorder.history] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + +[mypy-homeassistant.components.recorder.models] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + +[mypy-homeassistant.components.recorder.pool] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.recorder.purge] check_untyped_defs = true disallow_incomplete_defs = true @@ -1550,6 +1804,28 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.recorder.util] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + +[mypy-homeassistant.components.recorder.websocket_api] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.remote.*] check_untyped_defs = true disallow_incomplete_defs = true @@ -2023,6 +2299,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.update.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.uptime.*] check_untyped_defs = true disallow_incomplete_defs = true @@ -2045,6 +2332,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.usb.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.vacuum.*] check_untyped_defs = true disallow_incomplete_defs = true @@ -2188,6 +2486,28 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.worldclock.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + +[mypy-homeassistant.components.yale_smart_alarm.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.zodiac.*] check_untyped_defs = true disallow_incomplete_defs = true @@ -2378,15 +2698,6 @@ ignore_errors = true [mypy-homeassistant.components.home_plus_control.api] ignore_errors = true -[mypy-homeassistant.components.homekit.aidmanager] -ignore_errors = true - -[mypy-homeassistant.components.homekit.config_flow] -ignore_errors = true - -[mypy-homeassistant.components.homekit.util] -ignore_errors = true - [mypy-homeassistant.components.honeywell.climate] ignore_errors = true @@ -2480,18 +2791,6 @@ ignore_errors = true [mypy-homeassistant.components.minecraft_server.sensor] ignore_errors = true -[mypy-homeassistant.components.netgear] -ignore_errors = true - -[mypy-homeassistant.components.netgear.config_flow] -ignore_errors = true - -[mypy-homeassistant.components.netgear.device_tracker] -ignore_errors = true - -[mypy-homeassistant.components.netgear.router] -ignore_errors = true - [mypy-homeassistant.components.nilu.air_quality] ignore_errors = true @@ -2546,15 +2845,6 @@ ignore_errors = true [mypy-homeassistant.components.onvif.sensor] ignore_errors = true -[mypy-homeassistant.components.ozw] -ignore_errors = true - -[mypy-homeassistant.components.ozw.climate] -ignore_errors = true - -[mypy-homeassistant.components.ozw.entity] -ignore_errors = true - [mypy-homeassistant.components.philips_js] ignore_errors = true @@ -2603,9 +2893,6 @@ ignore_errors = true [mypy-homeassistant.components.sonos.favorites] ignore_errors = true -[mypy-homeassistant.components.sonos.helpers] -ignore_errors = true - [mypy-homeassistant.components.sonos.media_browser] ignore_errors = true @@ -2660,21 +2947,6 @@ ignore_errors = true [mypy-homeassistant.components.unifi.unifi_entity_base] ignore_errors = true -[mypy-homeassistant.components.upnp] -ignore_errors = true - -[mypy-homeassistant.components.upnp.binary_sensor] -ignore_errors = true - -[mypy-homeassistant.components.upnp.config_flow] -ignore_errors = true - -[mypy-homeassistant.components.upnp.device] -ignore_errors = true - -[mypy-homeassistant.components.upnp.sensor] -ignore_errors = true - [mypy-homeassistant.components.vizio.config_flow] ignore_errors = true @@ -2878,12 +3150,3 @@ ignore_errors = true [mypy-homeassistant.components.zha.switch] ignore_errors = true - -[mypy-homeassistant.components.zwave] -ignore_errors = true - -[mypy-homeassistant.components.zwave.migration] -ignore_errors = true - -[mypy-homeassistant.components.zwave.node_entity] -ignore_errors = true diff --git a/pyproject.toml b/pyproject.toml index c1a08a3f0c3..fbe9fa4b6e8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -67,7 +67,6 @@ good-names = [ "j", "k", "Run", - "T", "ip", ] diff --git a/requirements.txt b/requirements.txt index 4885e717b30..d86a7be1e73 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,10 +10,10 @@ awesomeversion==22.2.0 bcrypt==3.1.7 certifi>=2021.5.30 ciso8601==2.2.0 -httpx==0.21.3 +httpx==0.22.0 ifaddr==0.1.7 -jinja2==3.0.3 -PyJWT==2.1.0 +jinja2==3.1.0 +PyJWT==2.3.0 cryptography==35.0.0 pip>=21.0,<22.1 python-slugify==4.0.1 diff --git a/requirements_all.txt b/requirements_all.txt index 9351749148f..9dea0b1d92e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -4,15 +4,6 @@ # homeassistant.components.aemet AEMET-OpenData==0.2.1 -# homeassistant.components.sht31 -Adafruit-GPIO==1.0.3 - -# homeassistant.components.sht31 -Adafruit-SHT31==1.0.2 - -# homeassistant.components.bbb_gpio -# Adafruit_BBIO==1.1.1 - # homeassistant.components.adax Adax-local==0.1.3 @@ -22,9 +13,6 @@ HAP-python==4.4.0 # homeassistant.components.mastodon Mastodon.py==1.5.1 -# homeassistant.components.orangepi_gpio -OPi.GPIO==0.5.2 - # homeassistant.components.flick_electric PyFlick==0.0.2 @@ -33,7 +21,7 @@ PyMVGLive==1.1.4 # homeassistant.components.mobile_app # homeassistant.components.owntracks -PyNaCl==1.4.0 +PyNaCl==1.5.0 # homeassistant.auth.mfa_modules.totp # homeassistant.components.homekit @@ -53,7 +41,7 @@ PyTransportNSW==0.1.1 # homeassistant.components.camera # homeassistant.components.stream -PyTurboJPEG==1.6.5 +PyTurboJPEG==1.6.6 # homeassistant.components.vicare PyViCare==2.16.1 @@ -61,11 +49,7 @@ PyViCare==2.16.1 # homeassistant.components.xiaomi_aqara PyXiaomiGateway==0.13.4 -# homeassistant.components.bmp280 -# homeassistant.components.dht -# homeassistant.components.mcp23017 # homeassistant.components.rpi_gpio -# homeassistant.components.rpi_rf # RPi.GPIO==0.7.1a4 # homeassistant.components.remember_the_milk @@ -77,9 +61,6 @@ TravisPy==0.3.5 # homeassistant.components.twitter TwitterAPI==2.7.5 -# homeassistant.components.tof -# VL53L1X2==0.1.5 - # homeassistant.components.onvif WSDiscovery==2.0.0 @@ -92,20 +73,11 @@ abodepy==1.2.0 # homeassistant.components.accuweather accuweather==0.3.0 -# homeassistant.components.bmp280 -adafruit-circuitpython-bmp280==3.1.1 - -# homeassistant.components.dht -adafruit-circuitpython-dht==3.7.0 - -# homeassistant.components.mcp23017 -adafruit-circuitpython-mcp230xx==2.2.2 - # homeassistant.components.adax adax==0.2.0 # homeassistant.components.androidtv -adb-shell[async]==0.4.0 +adb-shell[async]==0.4.2 # homeassistant.components.alarmdecoder adext==0.4.2 @@ -122,6 +94,9 @@ afsapi==0.0.4 # homeassistant.components.agent_dvr agent-py==0.0.23 +# homeassistant.components.geo_json_events +aio_geojson_generic_client==0.1 + # homeassistant.components.geonetnz_quakes aio_geojson_geonetnz_quakes==0.13 @@ -134,6 +109,9 @@ aio_geojson_nsw_rfs_incidents==0.4 # homeassistant.components.gdacs aio_georss_gdacs==0.5 +# homeassistant.components.airzone +aioairzone==0.2.3 + # homeassistant.components.ambient_station aioambient==2021.11.0 @@ -175,7 +153,7 @@ aioflo==2021.11.0 aioftp==0.12.0 # homeassistant.components.github -aiogithubapi==22.2.3 +aiogithubapi==22.2.4 # homeassistant.components.guardian aioguardian==2021.11.0 @@ -251,7 +229,7 @@ aiopyarr==22.2.2 aiorecollect==1.0.8 # homeassistant.components.ridwell -aioridwell==2021.12.2 +aioridwell==2022.03.0 # homeassistant.components.senseme aiosenseme==0.6.1 @@ -281,7 +259,7 @@ aiovlc==0.1.0 aiowatttime==0.1.1 # homeassistant.components.webostv -aiowebostv==0.1.3 +aiowebostv==0.2.0 # homeassistant.components.yandex_transport aioymaps==1.2.2 @@ -305,7 +283,7 @@ alpha_vantage==2.3.1 ambee==0.4.0 # homeassistant.components.amberelectric -amberelectric==1.0.3 +amberelectric==1.0.4 # homeassistant.components.ambiclimate ambiclimate==0.2.1 @@ -314,7 +292,7 @@ ambiclimate==0.2.1 amcrest==1.9.7 # homeassistant.components.androidtv -androidtv[async]==0.0.63 +androidtv[async]==0.0.66 # homeassistant.components.anel_pwrctrl anel_pwrctrl-homeassistant==0.0.1.dev2 @@ -348,16 +326,17 @@ asterisk_mbox==0.5.0 # homeassistant.components.dlna_dmr # homeassistant.components.dlna_dms +# homeassistant.components.samsungtv # homeassistant.components.ssdp # homeassistant.components.upnp # homeassistant.components.yeelight -async-upnp-client==0.23.5 +async-upnp-client==0.27.0 # homeassistant.components.supla asyncpysupla==0.0.5 # homeassistant.components.sleepiq -asyncsleepiq==1.1.0 +asyncsleepiq==1.2.3 # homeassistant.components.aten_pe atenpdu==0.3.2 @@ -368,8 +347,9 @@ auroranoaa==0.0.2 # homeassistant.components.aurora_abb_powerone aurorapy==0.2.6 +# homeassistant.components.generic # homeassistant.components.stream -av==8.1.0 +av==9.0.0 # homeassistant.components.avea # avea==1.5.1 @@ -417,32 +397,24 @@ bizkaibus==0.1.1 blebox_uniapi==1.3.3 # homeassistant.components.blink -blinkpy==0.18.0 +blinkpy==0.19.0 # homeassistant.components.blinksticklight blinkstick==1.2.0 -# homeassistant.components.blinkt -# blinkt==0.1.0 - # homeassistant.components.bitcoin blockchain==1.4.4 # homeassistant.components.decora # homeassistant.components.miflora +# homeassistant.components.zengge # bluepy==1.3.0 -# homeassistant.components.bme280 -# bme280spi==0.2.0 - -# homeassistant.components.bme680 -# bme680==1.0.5 - # homeassistant.components.bond bond-api==0.1.16 # homeassistant.components.bosch_shc -boschshcpy==0.2.29 +boschshcpy==0.2.30 # homeassistant.components.amazon_polly # homeassistant.components.route53 @@ -452,7 +424,7 @@ boto3==1.20.24 bravia-tv==1.0.11 # homeassistant.components.broadlink -broadlink==0.18.0 +broadlink==0.18.1 # homeassistant.components.brother brother==1.1.0 @@ -621,16 +593,13 @@ enocean==0.50 enturclient==0.2.3 # homeassistant.components.environment_canada -env_canada==0.5.20 - -# homeassistant.components.envirophat -# envirophat==0.0.6 +env_canada==0.5.21 # homeassistant.components.enphase_envoy envoy_reader==0.20.1 # homeassistant.components.season -ephem==3.7.7.0 +ephem==4.1.2 # homeassistant.components.epson epson-projector==0.4.2 @@ -685,13 +654,14 @@ flipr-api==1.4.2 flux_led==0.28.27 # homeassistant.components.homekit +# homeassistant.components.recorder fnvhash==0.1.0 # homeassistant.components.foobot foobot_async==1.0.0 # homeassistant.components.forecast_solar -forecast_solar==2.1.0 +forecast_solar==2.2.0 # homeassistant.components.fortios fortiosapi==1.0.5 @@ -707,7 +677,7 @@ freesms==0.2.0 fritzconnection==1.8.0 # homeassistant.components.google_translate -gTTS==2.2.3 +gTTS==2.2.4 # homeassistant.components.garages_amsterdam garages-amsterdam==3.0.0 @@ -715,7 +685,6 @@ garages-amsterdam==3.0.0 # homeassistant.components.geniushub geniushub-client==0.6.30 -# homeassistant.components.geo_json_events # homeassistant.components.usgs_earthquakes_feed geojson_client==0.6 @@ -753,16 +722,16 @@ goalzero==0.2.1 goodwe==0.2.15 # homeassistant.components.google -google-api-python-client==1.6.4 +google-api-python-client==2.38.0 # homeassistant.components.google_pubsub -google-cloud-pubsub==2.9.0 +google-cloud-pubsub==2.11.0 # homeassistant.components.google_cloud -google-cloud-texttospeech==0.4.0 +google-cloud-texttospeech==2.11.0 # homeassistant.components.nest -google-nest-sdm==1.7.1 +google-nest-sdm==1.8.0 # homeassistant.components.google_travel_time googlemaps==2.5.1 @@ -777,7 +746,7 @@ gpiozero==1.5.1 gps3==0.33.3 # homeassistant.components.gree -greeclimate==1.0.2 +greeclimate==1.1.0 # homeassistant.components.greeneye_monitor greeneye_monitor==3.0.3 @@ -807,7 +776,7 @@ ha-philipsjs==2.7.6 habitipy==0.2.0 # homeassistant.components.hangouts -hangups==0.4.17 +hangups==0.4.18 # homeassistant.components.cloud hass-nabucasa==0.54.0 @@ -816,7 +785,7 @@ hass-nabucasa==0.54.0 hass_splunk==0.1.1 # homeassistant.components.tasmota -hatasmota==0.3.1 +hatasmota==0.4.0 # homeassistant.components.jewish_calendar hdate==0.10.4 @@ -843,10 +812,7 @@ hole==0.7.0 holidays==0.13 # homeassistant.components.frontend -home-assistant-frontend==20220301.2 - -# homeassistant.components.zwave -# homeassistant-pyozw==0.1.10 +home-assistant-frontend==20220405.0 # homeassistant.components.home_connect homeconnect==0.7.0 @@ -862,7 +828,7 @@ horimote==0.4.1 # homeassistant.components.google # homeassistant.components.remember_the_milk -httplib2==0.19.0 +httplib2==0.20.4 # homeassistant.components.huawei_lte huawei-lte-api==1.4.18 @@ -874,12 +840,7 @@ huisbaasje-client==0.1.0 hydrawiser==0.2 # homeassistant.components.hyperion -hyperion-py==0.7.4 - -# homeassistant.components.bh1750 -# homeassistant.components.bme280 -# homeassistant.components.htu21d -# i2csense==0.0.4 +hyperion-py==0.7.5 # homeassistant.components.iammeter iammeter==0.1.7 @@ -915,7 +876,7 @@ influxdb-client==1.24.0 influxdb==5.3.1 # homeassistant.components.intellifire -intellifire4py==0.9.9 +intellifire4py==1.0.2 # homeassistant.components.iotawatt iotawattpy==0.1.0 @@ -992,6 +953,9 @@ logi_circle==0.2.2 # homeassistant.components.london_underground london-tube-status==0.2 +# homeassistant.components.recorder +lru-dict==1.1.7 + # homeassistant.components.luftdaten luftdaten==0.7.2 @@ -1053,7 +1017,7 @@ mitemp_bt==0.0.5 moehlenhoff-alpha2==1.1.2 # homeassistant.components.motion_blinds -motionblinds==0.5.13 +motionblinds==0.6.2 # homeassistant.components.motioneye motioneye-client==0.3.12 @@ -1113,7 +1077,7 @@ niluclient==0.1.2 noaa-coops==0.1.8 # homeassistant.components.nfandroidtv -notifications-android-tv==0.1.3 +notifications-android-tv==0.1.5 # homeassistant.components.notify_events notify-events==1.0.4 @@ -1144,7 +1108,7 @@ oasatelematics==0.3 oauth2client==4.1.3 # homeassistant.components.profiler -objgraph==3.4.1 +objgraph==3.5.0 # homeassistant.components.oem oemthermostat==1.1.1 @@ -1213,12 +1177,12 @@ panacotta==0.1 # homeassistant.components.panasonic_viera panasonic_viera==0.3.6 -# homeassistant.components.pcal9535a -pcal9535a==0.7 - # homeassistant.components.dunehd pdunehd==1.3.2 +# homeassistant.components.peco +peco==0.0.25 + # homeassistant.components.pencom pencompy==0.0.3 @@ -1234,22 +1198,11 @@ phone_modem==0.1.1 # homeassistant.components.onewire pi1wire==0.1.0 -# homeassistant.components.pi4ioe5v9xxxx -pi4ioe5v9xxxx==0.0.2 - -# homeassistant.components.rpi_pfio -pifacecommon==4.2.2 - -# homeassistant.components.rpi_pfio -pifacedigitalio==3.0.5 - -# homeassistant.components.piglow -piglow==1.2.4 - # homeassistant.components.pilight pilight==0.1.1 # homeassistant.components.doods +# homeassistant.components.generic # homeassistant.components.image # homeassistant.components.proxy # homeassistant.components.qrcode @@ -1262,7 +1215,7 @@ pillow==9.0.1 pizzapi==0.0.3 # homeassistant.components.plex -plexapi==4.10.0 +plexapi==4.10.1 # homeassistant.components.plex plexauth==0.0.6 @@ -1271,12 +1224,11 @@ plexauth==0.0.6 plexwebsocket==0.0.13 # homeassistant.components.plugwise -plugwise==0.16.6 +plugwise==0.17.3 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 -# homeassistant.components.mhz19 # homeassistant.components.serial_pm pmsensor==0.4 @@ -1319,9 +1271,6 @@ pushover_complete==1.1.1 # homeassistant.components.pvoutput pvo==0.2.2 -# homeassistant.components.rpi_gpio_pwm -pwmled==1.6.10 - # homeassistant.components.canary py-canary==0.5.1 @@ -1338,7 +1287,7 @@ py-nightscout==1.2.2 py-schluter==0.1.7 # homeassistant.components.synology_dsm -py-synologydsm-api==1.0.7 +py-synologydsm-api==1.0.8 # homeassistant.components.zabbix py-zabbix==1.1.7 @@ -1366,7 +1315,7 @@ pyRFXtrx==0.28.0 # pySwitchmate==0.4.6 # homeassistant.components.tibber -pyTibber==0.22.1 +pyTibber==0.22.2 # homeassistant.components.dlink pyW215==0.7.0 @@ -1411,7 +1360,7 @@ pyatome==0.1.1 pyatv==0.10.0 # homeassistant.components.aussie_broadband -pyaussiebb==0.0.11 +pyaussiebb==0.0.15 # homeassistant.components.balboa pybalboa==0.13 @@ -1438,7 +1387,7 @@ pycfdns==1.2.2 pychannels==1.0.0 # homeassistant.components.cast -pychromecast==10.2.3 +pychromecast==11.0.0 # homeassistant.components.pocketcasts pycketcasts==1.0.0 @@ -1479,9 +1428,6 @@ pydelijn==1.0.0 # homeassistant.components.dexcom pydexcom==0.2.3 -# homeassistant.components.zwave -pydispatcher==2.0.5 - # homeassistant.components.doods pydoods==1.0.2 @@ -1565,7 +1511,7 @@ pygtfs==0.1.6 pygti==0.9.2 # homeassistant.components.version -pyhaversion==22.02.0 +pyhaversion==22.04.0 # homeassistant.components.heos pyheos==0.7.2 @@ -1610,11 +1556,14 @@ pyirishrail==0.0.2 pyiss==1.0.1 # homeassistant.components.isy994 -pyisy==3.0.5 +pyisy==3.0.6 # homeassistant.components.itach pyitachip2ir==0.0.7 +# homeassistant.components.kaleidescape +pykaleidescape==1.0.1 + # homeassistant.components.kira pykira==0.1.1 @@ -1652,7 +1601,7 @@ pylibrespot-java==0.1.0 pylitejet==0.3.0 # homeassistant.components.litterrobot -pylitterbot==2021.12.0 +pylitterbot==2022.3.0 # homeassistant.components.lutron_caseta pylutron-caseta==0.13.1 @@ -1703,7 +1652,7 @@ pymyq==3.1.4 pymysensors==0.22.1 # homeassistant.components.netgear -pynetgear==0.9.1 +pynetgear==0.9.4 # homeassistant.components.netio pynetio==0.1.9.1 @@ -1730,7 +1679,7 @@ pynzbgetapi==0.2.0 pyobihai==1.3.2 # homeassistant.components.octoprint -pyoctoprintapi==0.1.7 +pyoctoprintapi==0.1.8 # homeassistant.components.ombi pyombi==0.1.10 @@ -1753,7 +1702,7 @@ pyotgw==1.1b1 pyotp==2.6.0 # homeassistant.components.overkiz -pyoverkiz==1.3.9 +pyoverkiz==1.3.14 # homeassistant.components.openweathermap pyowm==3.2.0 @@ -1771,7 +1720,7 @@ pypck==0.7.14 pypjlink2==1.2.1 # homeassistant.components.plaato -pyplaato==0.0.15 +pyplaato==0.0.16 # homeassistant.components.point pypoint==2.3.0 @@ -1822,7 +1771,7 @@ pysaj==0.0.16 pysdcp==1 # homeassistant.components.sensibo -pysensibo==1.0.7 +pysensibo==1.0.9 # homeassistant.components.serial # homeassistant.components.zha @@ -1850,7 +1799,7 @@ pysignalclirestapi==0.3.18 pyskyqhub==0.1.4 # homeassistant.components.sma -pysma==0.6.10 +pysma==0.6.11 # homeassistant.components.smappee pysmappee==0.2.29 @@ -1940,13 +1889,13 @@ python-hpilo==4.3 python-izone==1.2.3 # homeassistant.components.joaoapps_join -python-join-api==0.0.6 +python-join-api==0.0.9 # homeassistant.components.juicenet -python-juicenet==1.0.2 +python-juicenet==1.1.0 # homeassistant.components.tplink -python-kasa==0.4.1 +python-kasa==0.4.3 # homeassistant.components.lirc # python-lirc==1.2.3 @@ -1963,9 +1912,6 @@ python-mystrom==1.1.2 # homeassistant.components.nest python-nest==4.2.0 -# homeassistant.components.ozw -python-openzwave-mqtt[mqtt-client]==1.4.0 - # homeassistant.components.picnic python-picnic-api==1.1.0 @@ -1976,10 +1922,7 @@ python-qbittorrent==0.4.2 python-ripple-api==0.0.3 # homeassistant.components.smarttub -python-smarttub==0.0.29 - -# homeassistant.components.sochain -python-sochain-api==0.0.2 +python-smarttub==0.0.30 # homeassistant.components.songpal python-songpal==0.14.1 @@ -1994,7 +1937,7 @@ python-telegram-bot==13.1 python-vlc==1.1.2 # homeassistant.components.awair -python_awair==0.2.1 +python_awair==0.2.3 # homeassistant.components.swiss_public_transport python_opendata_transport==0.3.0 @@ -2005,6 +1948,9 @@ pythonegardia==1.0.40 # homeassistant.components.tile pytile==2022.02.0 +# homeassistant.components.tomorrowio +pytomorrowio==0.1.0 + # homeassistant.components.touchline pytouchline==0.7 @@ -2087,9 +2033,6 @@ radiotherm==2.1.0 # homeassistant.components.raincloud raincloudy==0.0.7 -# homeassistant.components.raspihats -# raspihats==2.2.3 - # homeassistant.components.raspyrfm raspyrfm-client==1.2.8 @@ -2121,7 +2064,7 @@ rjpl==0.3.6 rocketchat-API==0.6.1 # homeassistant.components.roku -rokuecp==0.15.0 +rokuecp==0.16.0 # homeassistant.components.roomba roombapy==1.6.5 @@ -2130,14 +2073,11 @@ roombapy==1.6.5 roonapi==0.0.38 # homeassistant.components.rova -rova==0.2.1 +rova==0.3.0 # homeassistant.components.rpi_power rpi-bad-power==0.1.0 -# homeassistant.components.rpi_rf -# rpi-rf==0.9.7 - # homeassistant.components.rtsp_to_webrtc rtsp-to-webrtc==0.5.0 @@ -2154,7 +2094,7 @@ rxv==0.7.0 samsungctl[websocket]==0.7.1 # homeassistant.components.samsungtv -samsungtvws==1.7.0 +samsungtvws[async,encrypted]==2.5.0 # homeassistant.components.satel_integra satel_integra==0.3.4 @@ -2171,27 +2111,27 @@ screenlogicpy==0.5.4 # homeassistant.components.scsgate scsgate==0.1.0 +# homeassistant.components.backup +securetar==2022.2.0 + # homeassistant.components.sendgrid sendgrid==6.8.2 -# homeassistant.components.sensehat -sense-hat==2.2.0 - # homeassistant.components.emulated_kasa # homeassistant.components.sense sense_energy==0.10.4 # homeassistant.components.sentry -sentry-sdk==1.5.5 +sentry-sdk==1.5.8 # homeassistant.components.sharkiq -sharkiqpy==0.1.8 +sharkiq==0.0.1 # homeassistant.components.aquostv sharp_aquos_rc==0.3.2 # homeassistant.components.shodan -shodan==1.26.1 +shodan==1.27.0 # homeassistant.components.sighthound simplehound==0.3 @@ -2200,7 +2140,7 @@ simplehound==0.3 simplepush==1.1.4 # homeassistant.components.simplisafe -simplisafe-python==2022.02.1 +simplisafe-python==2022.03.0 # homeassistant.components.sisyphus sisyphus-control==3.1.2 @@ -2212,22 +2152,11 @@ skybellpy==0.6.3 slackclient==2.5.0 # homeassistant.components.xmpp -slixmpp==1.7.1 +slixmpp==1.8.0.1 # homeassistant.components.smart_meter_texas smart-meter-texas==0.4.7 -# homeassistant.components.smarthab -smarthab==0.21 - -# homeassistant.components.bh1750 -# homeassistant.components.bme280 -# homeassistant.components.bme680 -# homeassistant.components.envirophat -# homeassistant.components.htu21d -# homeassistant.components.raspihats -# smbus-cffi==0.5.1 - # homeassistant.components.smhi smhi-pkg==1.0.15 @@ -2235,7 +2164,7 @@ smhi-pkg==1.0.15 snapcast==2.1.3 # homeassistant.components.sonos -soco==0.26.4 +soco==0.27.1 # homeassistant.components.solaredge_local solaredge-local==0.2.0 @@ -2266,8 +2195,7 @@ spotipy==2.19.0 # homeassistant.components.recorder # homeassistant.components.sql -# homeassistant.components.webostv -sqlalchemy==1.4.27 +sqlalchemy==1.4.32 # homeassistant.components.srp_energy srpenergy==1.3.6 @@ -2297,7 +2225,7 @@ streamlabswater==1.0.1 stringcase==1.2.0 # homeassistant.components.subaru -subarulink==0.4.2 +subarulink==0.5.0 # homeassistant.components.ecovacs sucks==0.9.4 @@ -2390,7 +2318,7 @@ ttls==1.4.3 tuya-iot-py-sdk==0.6.6 # homeassistant.components.twentemilieu -twentemilieu==0.5.0 +twentemilieu==0.6.0 # homeassistant.components.twilio twilio==6.32.0 @@ -2428,7 +2356,7 @@ uvcclient==0.11.0 vallox-websocket-api==2.11.0 # homeassistant.components.rdw -vehicle==0.3.1 +vehicle==0.4.0 # homeassistant.components.velbus velbus-aio==2022.2.4 @@ -2440,10 +2368,10 @@ venstarcolortouch==0.15 vilfo-api-client==0.3.2 # homeassistant.components.volkszaehler -volkszaehler==0.2.1 +volkszaehler==0.3.2 # homeassistant.components.volvooncall -volvooncall==0.9.1 +volvooncall==0.10.0 # homeassistant.components.verisure vsure==1.7.3 @@ -2451,6 +2379,9 @@ vsure==1.7.3 # homeassistant.components.vasttrafik vtjp==0.1.14 +# homeassistant.components.vulcan +vulcan-api==2.0.3 + # homeassistant.components.vultr vultr==0.1.2 @@ -2489,7 +2420,7 @@ wirelesstagpy==0.8.1 withings-api==2.4.0 # homeassistant.components.wled -wled==0.13.0 +wled==0.13.2 # homeassistant.components.wolflink wolf_smartset==0.1.11 @@ -2501,7 +2432,7 @@ xbox-webapi==2.0.11 xboxapi==2.0.1 # homeassistant.components.knx -xknx==0.19.2 +xknx==0.20.1 # homeassistant.components.bluesound # homeassistant.components.fritz @@ -2518,10 +2449,10 @@ xs1-api-client==3.0.0 yalesmartalarmclient==0.3.8 # homeassistant.components.august -yalexs==1.1.22 +yalexs==1.1.23 # homeassistant.components.yeelight -yeelight==0.7.9 +yeelight==0.7.10 # homeassistant.components.yeelightsunflower yeelightsunflower==0.0.10 @@ -2539,7 +2470,7 @@ zengge==0.2 zeroconf==0.38.4 # homeassistant.components.zha -zha-quirks==0.0.67 +zha-quirks==0.0.69 # homeassistant.components.zhong_hong zhong_hong_hvac==1.0.9 @@ -2548,7 +2479,7 @@ zhong_hong_hvac==1.0.9 ziggo-mediabox-xl==1.1.0 # homeassistant.components.zha -zigpy-deconz==0.14.0 +zigpy-deconz==0.15.0 # homeassistant.components.zha zigpy-xbee==0.14.0 @@ -2560,7 +2491,7 @@ zigpy-zigate==0.8.0 zigpy-znp==0.7.0 # homeassistant.components.zha -zigpy==0.43.0 +zigpy==0.44.1 # homeassistant.components.zoneminder zm-py==0.5.2 @@ -2569,4 +2500,4 @@ zm-py==0.5.2 zwave-js-server-python==0.35.2 # homeassistant.components.zwave_me -zwave_me_ws==0.2.1 +zwave_me_ws==0.2.3 diff --git a/requirements_test.txt b/requirements_test.txt index bf98c8a449e..93cc10b627a 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -8,12 +8,12 @@ -c homeassistant/package_constraints.txt -r requirements_test_pre_commit.txt codecov==2.1.12 -coverage==6.3.1 -freezegun==1.1.0 +coverage==6.3.2 +freezegun==1.2.1 mock-open==1.4.0 -mypy==0.931 +mypy==0.942 pre-commit==2.17.0 -pylint==2.12.2 +pylint==2.13.3 pipdeptree==2.2.1 pylint-strict-informational==0.1 pytest-aiohttp==0.3.0 @@ -23,8 +23,8 @@ pytest-socket==0.4.1 pytest-test-groups==1.0.3 pytest-sugar==0.9.4 pytest-timeout==2.1.0 -pytest-xdist==2.4.0 -pytest==7.0.1 +pytest-xdist==2.5.0 +pytest==7.1.1 requests_mock==1.9.2 respx==0.19.0 stdlib-list==0.7.0 @@ -35,7 +35,6 @@ types-backports==0.1.3 types-certifi==0.1.4 types-chardet==0.1.5 types-decorator==0.1.7 -types-emoji==1.2.4 types-enum34==0.1.8 types-ipaddress==0.1.5 types-pkg-resources==0.1.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1a02fe43828..d9ff1a51483 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -17,7 +17,7 @@ PyFlick==0.0.2 # homeassistant.components.mobile_app # homeassistant.components.owntracks -PyNaCl==1.4.0 +PyNaCl==1.5.0 # homeassistant.auth.mfa_modules.totp # homeassistant.components.homekit @@ -26,6 +26,9 @@ PyQRCode==1.2.1 # homeassistant.components.rmvtransport PyRMVtransport==0.3.3 +# homeassistant.components.telegram_bot +PySocks==1.7.1 + # homeassistant.components.switchbot # PySwitchbot==0.13.3 @@ -34,7 +37,7 @@ PyTransportNSW==0.1.1 # homeassistant.components.camera # homeassistant.components.stream -PyTurboJPEG==1.6.5 +PyTurboJPEG==1.6.6 # homeassistant.components.vicare PyViCare==2.16.1 @@ -61,7 +64,7 @@ accuweather==0.3.0 adax==0.2.0 # homeassistant.components.androidtv -adb-shell[async]==0.4.0 +adb-shell[async]==0.4.2 # homeassistant.components.alarmdecoder adext==0.4.2 @@ -75,6 +78,9 @@ advantage_air==0.3.1 # homeassistant.components.agent_dvr agent-py==0.0.23 +# homeassistant.components.geo_json_events +aio_geojson_generic_client==0.1 + # homeassistant.components.geonetnz_quakes aio_geojson_geonetnz_quakes==0.13 @@ -87,6 +93,9 @@ aio_geojson_nsw_rfs_incidents==0.4 # homeassistant.components.gdacs aio_georss_gdacs==0.5 +# homeassistant.components.airzone +aioairzone==0.2.3 + # homeassistant.components.ambient_station aioambient==2021.11.0 @@ -125,7 +134,7 @@ aioesphomeapi==10.8.2 aioflo==2021.11.0 # homeassistant.components.github -aiogithubapi==22.2.3 +aiogithubapi==22.2.4 # homeassistant.components.guardian aioguardian==2021.11.0 @@ -186,7 +195,7 @@ aiopyarr==22.2.2 aiorecollect==1.0.8 # homeassistant.components.ridwell -aioridwell==2021.12.2 +aioridwell==2022.03.0 # homeassistant.components.senseme aiosenseme==0.6.1 @@ -216,7 +225,7 @@ aiovlc==0.1.0 aiowatttime==0.1.1 # homeassistant.components.webostv -aiowebostv==0.1.3 +aiowebostv==0.2.0 # homeassistant.components.yandex_transport aioymaps==1.2.2 @@ -234,13 +243,13 @@ airtouch4pyapi==1.0.5 ambee==0.4.0 # homeassistant.components.amberelectric -amberelectric==1.0.3 +amberelectric==1.0.4 # homeassistant.components.ambiclimate ambiclimate==0.2.1 # homeassistant.components.androidtv -androidtv[async]==0.0.63 +androidtv[async]==0.0.66 # homeassistant.components.apprise apprise==0.9.7 @@ -253,13 +262,14 @@ arcam-fmj==0.12.0 # homeassistant.components.dlna_dmr # homeassistant.components.dlna_dms +# homeassistant.components.samsungtv # homeassistant.components.ssdp # homeassistant.components.upnp # homeassistant.components.yeelight -async-upnp-client==0.23.5 +async-upnp-client==0.27.0 # homeassistant.components.sleepiq -asyncsleepiq==1.1.0 +asyncsleepiq==1.2.3 # homeassistant.components.aurora auroranoaa==0.0.2 @@ -267,8 +277,9 @@ auroranoaa==0.0.2 # homeassistant.components.aurora_abb_powerone aurorapy==0.2.6 +# homeassistant.components.generic # homeassistant.components.stream -av==8.1.0 +av==9.0.0 # homeassistant.components.axis axis==44 @@ -292,19 +303,19 @@ bimmer_connected==0.8.11 blebox_uniapi==1.3.3 # homeassistant.components.blink -blinkpy==0.18.0 +blinkpy==0.19.0 # homeassistant.components.bond bond-api==0.1.16 # homeassistant.components.bosch_shc -boschshcpy==0.2.29 +boschshcpy==0.2.30 # homeassistant.components.braviatv bravia-tv==1.0.11 # homeassistant.components.broadlink -broadlink==0.18.0 +broadlink==0.18.1 # homeassistant.components.brother brother==1.1.0 @@ -367,6 +378,9 @@ debugpy==1.5.1 # homeassistant.components.ohmconnect defusedxml==0.7.1 +# homeassistant.components.deluge +deluge-client==1.7.1 + # homeassistant.components.denonavr denonavr==0.10.10 @@ -407,13 +421,13 @@ emulated_roku==0.2.1 enocean==0.50 # homeassistant.components.environment_canada -env_canada==0.5.20 +env_canada==0.5.21 # homeassistant.components.enphase_envoy envoy_reader==0.20.1 # homeassistant.components.season -ephem==3.7.7.0 +ephem==4.1.2 # homeassistant.components.epson epson-projector==0.4.2 @@ -424,6 +438,9 @@ faadelays==0.0.7 # homeassistant.components.feedreader feedparser==6.0.2 +# homeassistant.components.fibaro +fiblary3==0.1.8 + # homeassistant.components.fivem fivem-api==0.1.2 @@ -437,13 +454,14 @@ flipr-api==1.4.2 flux_led==0.28.27 # homeassistant.components.homekit +# homeassistant.components.recorder fnvhash==0.1.0 # homeassistant.components.foobot foobot_async==1.0.0 # homeassistant.components.forecast_solar -forecast_solar==2.1.0 +forecast_solar==2.2.0 # homeassistant.components.freebox freebox-api==0.0.10 @@ -453,12 +471,11 @@ freebox-api==0.0.10 fritzconnection==1.8.0 # homeassistant.components.google_translate -gTTS==2.2.3 +gTTS==2.2.4 # homeassistant.components.garages_amsterdam garages-amsterdam==3.0.0 -# homeassistant.components.geo_json_events # homeassistant.components.usgs_earthquakes_feed geojson_client==0.6 @@ -493,19 +510,19 @@ goalzero==0.2.1 goodwe==0.2.15 # homeassistant.components.google -google-api-python-client==1.6.4 +google-api-python-client==2.38.0 # homeassistant.components.google_pubsub -google-cloud-pubsub==2.9.0 +google-cloud-pubsub==2.11.0 # homeassistant.components.nest -google-nest-sdm==1.7.1 +google-nest-sdm==1.8.0 # homeassistant.components.google_travel_time googlemaps==2.5.1 # homeassistant.components.gree -greeclimate==1.0.2 +greeclimate==1.1.0 # homeassistant.components.greeneye_monitor greeneye_monitor==3.0.3 @@ -529,13 +546,13 @@ ha-philipsjs==2.7.6 habitipy==0.2.0 # homeassistant.components.hangouts -hangups==0.4.17 +hangups==0.4.18 # homeassistant.components.cloud hass-nabucasa==0.54.0 # homeassistant.components.tasmota -hatasmota==0.3.1 +hatasmota==0.4.0 # homeassistant.components.jewish_calendar hdate==0.10.4 @@ -553,10 +570,7 @@ hole==0.7.0 holidays==0.13 # homeassistant.components.frontend -home-assistant-frontend==20220301.2 - -# homeassistant.components.zwave -# homeassistant-pyozw==0.1.10 +home-assistant-frontend==20220405.0 # homeassistant.components.home_connect homeconnect==0.7.0 @@ -569,7 +583,7 @@ homepluscontrol==0.0.5 # homeassistant.components.google # homeassistant.components.remember_the_milk -httplib2==0.19.0 +httplib2==0.20.4 # homeassistant.components.huawei_lte huawei-lte-api==1.4.18 @@ -578,7 +592,7 @@ huawei-lte-api==1.4.18 huisbaasje-client==0.1.0 # homeassistant.components.hyperion -hyperion-py==0.7.4 +hyperion-py==0.7.5 # homeassistant.components.iaqualink iaqualink==0.4.1 @@ -596,7 +610,7 @@ influxdb-client==1.24.0 influxdb==5.3.1 # homeassistant.components.intellifire -intellifire4py==0.9.9 +intellifire4py==1.0.2 # homeassistant.components.iotawatt iotawattpy==0.1.0 @@ -631,6 +645,9 @@ libsoundtouch==0.8 # homeassistant.components.logi_circle logi_circle==0.2.2 +# homeassistant.components.recorder +lru-dict==1.1.7 + # homeassistant.components.luftdaten luftdaten==0.7.2 @@ -668,7 +685,7 @@ minio==5.0.10 moehlenhoff-alpha2==1.1.2 # homeassistant.components.motion_blinds -motionblinds==0.5.13 +motionblinds==0.6.2 # homeassistant.components.motioneye motioneye-client==0.3.12 @@ -700,8 +717,11 @@ nettigo-air-monitor==1.2.1 # homeassistant.components.nexia nexia==0.9.13 +# homeassistant.components.discord +nextcord==2.0.0a8 + # homeassistant.components.nfandroidtv -notifications-android-tv==0.1.3 +notifications-android-tv==0.1.5 # homeassistant.components.notify_events notify-events==1.0.4 @@ -726,7 +746,7 @@ numpy==1.21.4 oauth2client==4.1.3 # homeassistant.components.profiler -objgraph==3.4.1 +objgraph==3.5.0 # homeassistant.components.omnilogic omnilogic==0.4.5 @@ -762,6 +782,9 @@ panasonic_viera==0.3.6 # homeassistant.components.dunehd pdunehd==1.3.2 +# homeassistant.components.peco +peco==0.0.25 + # homeassistant.components.aruba # homeassistant.components.cisco_ios # homeassistant.components.pandora @@ -778,6 +801,7 @@ pi1wire==0.1.0 pilight==0.1.1 # homeassistant.components.doods +# homeassistant.components.generic # homeassistant.components.image # homeassistant.components.proxy # homeassistant.components.qrcode @@ -787,7 +811,7 @@ pilight==0.1.1 pillow==9.0.1 # homeassistant.components.plex -plexapi==4.10.0 +plexapi==4.10.1 # homeassistant.components.plex plexauth==0.0.6 @@ -796,15 +820,11 @@ plexauth==0.0.6 plexwebsocket==0.0.13 # homeassistant.components.plugwise -plugwise==0.16.6 +plugwise==0.17.3 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 -# homeassistant.components.mhz19 -# homeassistant.components.serial_pm -pmsensor==0.4 - # homeassistant.components.poolsense poolsense==0.0.8 @@ -842,7 +862,7 @@ py-melissa-climate==2.1.4 py-nightscout==1.2.2 # homeassistant.components.synology_dsm -py-synologydsm-api==1.0.7 +py-synologydsm-api==1.0.8 # homeassistant.components.seventeentrack py17track==2021.12.2 @@ -861,7 +881,7 @@ pyMetno==0.9.0 pyRFXtrx==0.28.0 # homeassistant.components.tibber -pyTibber==0.22.1 +pyTibber==0.22.2 # homeassistant.components.nextbus py_nextbusnext==0.1.5 @@ -891,7 +911,7 @@ pyatmo==6.2.4 pyatv==0.10.0 # homeassistant.components.aussie_broadband -pyaussiebb==0.0.11 +pyaussiebb==0.0.15 # homeassistant.components.balboa pybalboa==0.13 @@ -906,7 +926,7 @@ pybotvac==0.0.23 pycfdns==1.2.2 # homeassistant.components.cast -pychromecast==10.2.3 +pychromecast==11.0.0 # homeassistant.components.climacell pyclimacell==0.18.2 @@ -926,9 +946,6 @@ pydeconz==87 # homeassistant.components.dexcom pydexcom==0.2.3 -# homeassistant.components.zwave -pydispatcher==2.0.5 - # homeassistant.components.econet pyeconet==0.1.15 @@ -982,7 +999,7 @@ pygatt[GATTTOOL]==4.0.5 pygti==0.9.2 # homeassistant.components.version -pyhaversion==22.02.0 +pyhaversion==22.04.0 # homeassistant.components.heos pyheos==0.7.2 @@ -1015,7 +1032,10 @@ pyiqvia==2021.11.0 pyiss==1.0.1 # homeassistant.components.isy994 -pyisy==3.0.5 +pyisy==3.0.6 + +# homeassistant.components.kaleidescape +pykaleidescape==1.0.1 # homeassistant.components.kira pykira==0.1.1 @@ -1045,7 +1065,7 @@ pylibrespot-java==0.1.0 pylitejet==0.3.0 # homeassistant.components.litterrobot -pylitterbot==2021.12.0 +pylitterbot==2022.3.0 # homeassistant.components.lutron_caseta pylutron-caseta==0.13.1 @@ -1084,7 +1104,7 @@ pymyq==3.1.4 pymysensors==0.22.1 # homeassistant.components.netgear -pynetgear==0.9.1 +pynetgear==0.9.4 # homeassistant.components.nina pynina==0.1.7 @@ -1105,7 +1125,7 @@ pynx584==0.5 pynzbgetapi==0.2.0 # homeassistant.components.octoprint -pyoctoprintapi==0.1.7 +pyoctoprintapi==0.1.8 # homeassistant.components.openuv pyopenuv==2021.11.0 @@ -1122,7 +1142,7 @@ pyotgw==1.1b1 pyotp==2.6.0 # homeassistant.components.overkiz -pyoverkiz==1.3.9 +pyoverkiz==1.3.14 # homeassistant.components.openweathermap pyowm==3.2.0 @@ -1134,7 +1154,7 @@ pyownet==0.10.0.post1 pypck==0.7.14 # homeassistant.components.plaato -pyplaato==0.0.15 +pyplaato==0.0.16 # homeassistant.components.point pypoint==2.3.0 @@ -1161,7 +1181,7 @@ pyrituals==0.0.6 pyruckus==0.12 # homeassistant.components.sensibo -pysensibo==1.0.7 +pysensibo==1.0.9 # homeassistant.components.serial # homeassistant.components.zha @@ -1180,7 +1200,7 @@ pysiaalarm==3.0.2 pysignalclirestapi==0.3.18 # homeassistant.components.sma -pysma==0.6.10 +pysma==0.6.11 # homeassistant.components.smappee pysmappee==0.2.29 @@ -1203,6 +1223,9 @@ pysqueezebox==0.5.5 # homeassistant.components.syncthru pysyncthru==0.7.10 +# homeassistant.components.tankerkoenig +pytankerkoenig==0.0.6 + # homeassistant.components.ecobee python-ecobee-api==0.2.14 @@ -1213,10 +1236,10 @@ python-forecastio==1.4.0 python-izone==1.2.3 # homeassistant.components.juicenet -python-juicenet==1.0.2 +python-juicenet==1.1.0 # homeassistant.components.tplink -python-kasa==0.4.1 +python-kasa==0.4.3 # homeassistant.components.xiaomi_miio python-miio==0.5.11 @@ -1224,14 +1247,11 @@ python-miio==0.5.11 # homeassistant.components.nest python-nest==4.2.0 -# homeassistant.components.ozw -python-openzwave-mqtt[mqtt-client]==1.4.0 - # homeassistant.components.picnic python-picnic-api==1.1.0 # homeassistant.components.smarttub -python-smarttub==0.0.29 +python-smarttub==0.0.30 # homeassistant.components.songpal python-songpal==0.14.1 @@ -1239,12 +1259,18 @@ python-songpal==0.14.1 # homeassistant.components.tado python-tado==0.12.0 +# homeassistant.components.telegram_bot +python-telegram-bot==13.1 + # homeassistant.components.awair -python_awair==0.2.1 +python_awair==0.2.3 # homeassistant.components.tile pytile==2022.02.0 +# homeassistant.components.tomorrowio +pytomorrowio==0.1.0 + # homeassistant.components.traccar pytraccar==0.10.0 @@ -1313,7 +1339,7 @@ rflink==0.0.62 ring_doorbell==0.7.2 # homeassistant.components.roku -rokuecp==0.15.0 +rokuecp==0.16.0 # homeassistant.components.roomba roombapy==1.6.5 @@ -1334,7 +1360,7 @@ rxv==0.7.0 samsungctl[websocket]==0.7.1 # homeassistant.components.samsungtv -samsungtvws==1.7.0 +samsungtvws[async,encrypted]==2.5.0 # homeassistant.components.dhcp scapy==2.4.5 @@ -1342,21 +1368,24 @@ scapy==2.4.5 # homeassistant.components.screenlogic screenlogicpy==0.5.4 +# homeassistant.components.backup +securetar==2022.2.0 + # homeassistant.components.emulated_kasa # homeassistant.components.sense sense_energy==0.10.4 # homeassistant.components.sentry -sentry-sdk==1.5.5 +sentry-sdk==1.5.8 # homeassistant.components.sharkiq -sharkiqpy==0.1.8 +sharkiq==0.0.1 # homeassistant.components.sighthound simplehound==0.3 # homeassistant.components.simplisafe -simplisafe-python==2022.02.1 +simplisafe-python==2022.03.0 # homeassistant.components.slack slackclient==2.5.0 @@ -1364,14 +1393,11 @@ slackclient==2.5.0 # homeassistant.components.smart_meter_texas smart-meter-texas==0.4.7 -# homeassistant.components.smarthab -smarthab==0.21 - # homeassistant.components.smhi smhi-pkg==1.0.15 # homeassistant.components.sonos -soco==0.26.4 +soco==0.27.1 # homeassistant.components.solaredge solaredge==0.0.2 @@ -1399,8 +1425,7 @@ spotipy==2.19.0 # homeassistant.components.recorder # homeassistant.components.sql -# homeassistant.components.webostv -sqlalchemy==1.4.27 +sqlalchemy==1.4.32 # homeassistant.components.srp_energy srpenergy==1.3.6 @@ -1421,7 +1446,7 @@ stookalert==0.1.4 stringcase==1.2.0 # homeassistant.components.subaru -subarulink==0.4.2 +subarulink==0.5.0 # homeassistant.components.solarlog sunwatcher==0.2.1 @@ -1444,6 +1469,9 @@ tesla-powerwall==0.3.17 # homeassistant.components.tesla_wall_connector tesla-wall-connector==1.0.1 +# homeassistant.components.todoist +todoist-python==8.0.0 + # homeassistant.components.tolo tololib==0.1.0b3 @@ -1463,7 +1491,7 @@ ttls==1.4.3 tuya-iot-py-sdk==0.6.6 # homeassistant.components.twentemilieu -twentemilieu==0.5.0 +twentemilieu==0.6.0 # homeassistant.components.twilio twilio==6.32.0 @@ -1495,7 +1523,7 @@ uvcclient==0.11.0 vallox-websocket-api==2.11.0 # homeassistant.components.rdw -vehicle==0.3.1 +vehicle==0.4.0 # homeassistant.components.velbus velbus-aio==2022.2.4 @@ -1509,6 +1537,9 @@ vilfo-api-client==0.3.2 # homeassistant.components.verisure vsure==1.7.3 +# homeassistant.components.vulcan +vulcan-api==2.0.3 + # homeassistant.components.vultr vultr==0.1.2 @@ -1535,7 +1566,7 @@ wiffi==1.1.0 withings-api==2.4.0 # homeassistant.components.wled -wled==0.13.0 +wled==0.13.2 # homeassistant.components.wolflink wolf_smartset==0.1.11 @@ -1544,7 +1575,7 @@ wolf_smartset==0.1.11 xbox-webapi==2.0.11 # homeassistant.components.knx -xknx==0.19.2 +xknx==0.20.1 # homeassistant.components.bluesound # homeassistant.components.fritz @@ -1558,10 +1589,10 @@ xmltodict==0.12.0 yalesmartalarmclient==0.3.8 # homeassistant.components.august -yalexs==1.1.22 +yalexs==1.1.23 # homeassistant.components.yeelight -yeelight==0.7.9 +yeelight==0.7.10 # homeassistant.components.youless youless-api==0.16 @@ -1570,10 +1601,10 @@ youless-api==0.16 zeroconf==0.38.4 # homeassistant.components.zha -zha-quirks==0.0.67 +zha-quirks==0.0.69 # homeassistant.components.zha -zigpy-deconz==0.14.0 +zigpy-deconz==0.15.0 # homeassistant.components.zha zigpy-xbee==0.14.0 @@ -1585,10 +1616,10 @@ zigpy-zigate==0.8.0 zigpy-znp==0.7.0 # homeassistant.components.zha -zigpy==0.43.0 +zigpy==0.44.1 # homeassistant.components.zwave_js zwave-js-server-python==0.35.2 # homeassistant.components.zwave_me -zwave_me_ws==0.2.1 +zwave_me_ws==0.2.3 diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index ca7828267b6..c62b1866e97 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -1,16 +1,16 @@ # Automatically generated from .pre-commit-config.yaml by gen_requirements_all.py, do not edit -bandit==1.7.0 -black==22.1.0 +bandit==1.7.4 +black==22.3.0 codespell==2.1.0 -flake8-comprehensions==3.7.0 +flake8-comprehensions==3.8.0 flake8-docstrings==1.6.0 flake8-noqa==1.2.1 flake8==4.0.1 -isort==5.10.0 +isort==5.10.1 mccabe==0.6.1 pycodestyle==2.8.0 pydocstyle==6.1.1 pyflakes==2.4.0 -pyupgrade==2.31.0 +pyupgrade==2.31.1 yamllint==1.26.3 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 5b61938a209..e3545ac0ece 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -18,17 +18,11 @@ COMMENT_REQUIREMENTS = ( "avion", "beacontools", "beewi_smartclim", # depends on bluepy - "blinkt", "bluepy", - "bme280spi", - "bme680", "decora", "decora_wifi", - "envirophat", "evdev", "face_recognition", - "homeassistant-pyozw", - "i2csense", "opencv-python-headless", "pybluez", "pycups", @@ -38,13 +32,9 @@ COMMENT_REQUIREMENTS = ( "python-gammu", "python-lirc", "pyuserinput", - "raspihats", - "rpi-rf", "RPi.GPIO", - "smbus-cffi", "tensorflow", "tf-models-official", - "VL53L1X2", ) COMMENT_REQUIREMENTS_NORMALIZED = { @@ -77,7 +67,7 @@ httplib2>=0.19.0 # gRPC is an implicit dependency that we want to make explicit so we manage # upgrades intentionally. It is a large package to build from source and we # want to ensure we have wheels built. -grpcio==1.44.0 +grpcio==1.45.0 # libcst >=0.4.0 requires a newer Rust than we currently have available, # thus our wheels builds fail. This pins it to the last working version, @@ -106,7 +96,7 @@ regex==2021.8.28 # requirements so we can directly link HA versions to these library versions. anyio==3.5.0 h11==0.12.0 -httpcore==0.14.5 +httpcore==0.14.7 # Ensure we have a hyperframe version that works in Python 3.10 # 5.2.0 fixed a collections abc deprecation diff --git a/script/hassfest/codeowners.py b/script/hassfest/codeowners.py index cf8fb02b989..d2ee6182f22 100644 --- a/script/hassfest/codeowners.py +++ b/script/hassfest/codeowners.py @@ -8,28 +8,34 @@ BASE = """ # People marked here will be automatically requested for a review # when the code that they own is touched. # https://github.com/blog/2392-introducing-code-owners +# https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners # Home Assistant Core setup.cfg @home-assistant/core -homeassistant/*.py @home-assistant/core -homeassistant/helpers/* @home-assistant/core -homeassistant/util/* @home-assistant/core +/homeassistant/*.py @home-assistant/core +/homeassistant/helpers/ @home-assistant/core +/homeassistant/util/ @home-assistant/core # Home Assistant Supervisor build.json @home-assistant/supervisor -machine/* @home-assistant/supervisor -rootfs/* @home-assistant/supervisor -Dockerfile @home-assistant/supervisor +/machine/ @home-assistant/supervisor +/rootfs/ @home-assistant/supervisor +/Dockerfile @home-assistant/supervisor # Other code -homeassistant/scripts/check_config.py @kellerza +/homeassistant/scripts/check_config.py @kellerza # Integrations """.strip() INDIVIDUAL_FILES = """ # Individual files -homeassistant/components/demo/weather @fabaff +/homeassistant/components/demo/weather.py @fabaff +""" + +REMOVE_CODEOWNERS = """ +# Remove codeowners from files +/homeassistant/components/*/translations/ """ @@ -54,12 +60,13 @@ def generate_and_validate(integrations: dict[str, Integration], config: Config): "codeowners", "Code owners need to be valid GitHub handles." ) - parts.append(f"homeassistant/components/{domain}/* {' '.join(codeowners)}") + parts.append(f"/homeassistant/components/{domain}/ {' '.join(codeowners)}") - if (config.root / "tests/components" / domain).exists(): - parts.append(f"tests/components/{domain}/* {' '.join(codeowners)}") + if (config.root / "tests/components" / domain / "__init__.py").exists(): + parts.append(f"/tests/components/{domain}/ {' '.join(codeowners)}") parts.append(f"\n{INDIVIDUAL_FILES.strip()}") + parts.append(f"\n{REMOVE_CODEOWNERS.strip()}") return "\n".join(parts) diff --git a/script/hassfest/config_flow.py b/script/hassfest/config_flow.py index 87e9bea6291..169ccedf4a1 100644 --- a/script/hassfest/config_flow.py +++ b/script/hassfest/config_flow.py @@ -69,7 +69,10 @@ def validate_integration(config: Config, integration: Integration): def generate_and_validate(integrations: dict[str, Integration], config: Config): """Validate and generate config flow data.""" - domains = [] + domains = { + "integration": [], + "helper": [], + } for domain in sorted(integrations): integration = integrations[domain] @@ -79,7 +82,7 @@ def generate_and_validate(integrations: dict[str, Integration], config: Config): validate_integration(config, integration) - domains.append(domain) + domains[integration.integration_type].append(domain) return BASE.format(json.dumps(domains, indent=4)) diff --git a/script/hassfest/manifest.py b/script/hassfest/manifest.py index 64239a7fae1..ca9acedd515 100644 --- a/script/hassfest/manifest.py +++ b/script/hassfest/manifest.py @@ -12,6 +12,7 @@ from awesomeversion import ( import voluptuous as vol from voluptuous.humanize import humanize_error +from homeassistant.const import Platform from homeassistant.helpers import config_validation as cv from .model import Config, Integration @@ -33,36 +34,25 @@ SUPPORTED_IOT_CLASSES = [ # List of integrations that are supposed to have no IoT class NO_IOT_CLASS = [ - "air_quality", - "alarm_control_panel", + *{platform.value for platform in Platform}, "api", "auth", "automation", - "binary_sensor", "blueprint", - "button", - "calendar", - "camera", - "climate", "color_extractor", "config", "configurator", "counter", - "cover", "default_config", "device_automation", "device_tracker", "diagnostics", "discovery", "downloader", - "fan", "ffmpeg", "frontend", - "geo_location", "history", "homeassistant", - "humidifier", - "image_processing", "image", "input_boolean", "input_button", @@ -72,18 +62,12 @@ NO_IOT_CLASS = [ "input_text", "intent_script", "intent", - "light", - "lock", "logbook", "logger", "lovelace", - "mailbox", "map", - "media_player", "media_source", "my", - "notify", - "number", "onboarding", "panel_custom", "panel_iframe", @@ -91,25 +75,14 @@ NO_IOT_CLASS = [ "profiler", "proxy", "python_script", - "remote", "safe_mode", - "scene", "script", "search", - "select", - "sensor", - "siren", - "stt", - "switch", "system_health", "system_log", "tag", "timer", "trace", - "tts", - "vacuum", - "water_heater", - "weather", "webhook", "websocket_api", "zone", @@ -179,6 +152,7 @@ MANIFEST_SCHEMA = vol.Schema( { vol.Required("domain"): str, vol.Required("name"): str, + vol.Optional("integration_type"): "helper", vol.Optional("config_flow"): bool, vol.Optional("mqtt"): [str], vol.Optional("zeroconf"): [ diff --git a/script/hassfest/model.py b/script/hassfest/model.py index 7006c1e6032..2a6ea9ca85f 100644 --- a/script/hassfest/model.py +++ b/script/hassfest/model.py @@ -112,6 +112,11 @@ class Integration: """List of dependencies.""" return self.manifest.get("dependencies", []) + @property + def integration_type(self) -> str: + """Get integration_type.""" + return self.manifest.get("integration_type", "integration") + def add_error(self, *args: Any, **kwargs: Any) -> None: """Add an error.""" self.errors.append(Error(*args, **kwargs)) diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index ef29562578e..d58406f67f9 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -60,9 +60,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.here_travel_time.sensor", "homeassistant.components.home_plus_control", "homeassistant.components.home_plus_control.api", - "homeassistant.components.homekit.aidmanager", - "homeassistant.components.homekit.config_flow", - "homeassistant.components.homekit.util", "homeassistant.components.honeywell.climate", "homeassistant.components.icloud", "homeassistant.components.icloud.account", @@ -94,10 +91,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.minecraft_server", "homeassistant.components.minecraft_server.helpers", "homeassistant.components.minecraft_server.sensor", - "homeassistant.components.netgear", - "homeassistant.components.netgear.config_flow", - "homeassistant.components.netgear.device_tracker", - "homeassistant.components.netgear.router", "homeassistant.components.nilu.air_quality", "homeassistant.components.nzbget", "homeassistant.components.nzbget.config_flow", @@ -116,9 +109,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.onvif.models", "homeassistant.components.onvif.parsers", "homeassistant.components.onvif.sensor", - "homeassistant.components.ozw", - "homeassistant.components.ozw.climate", - "homeassistant.components.ozw.entity", "homeassistant.components.philips_js", "homeassistant.components.philips_js.config_flow", "homeassistant.components.philips_js.device_trigger", @@ -135,7 +125,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.sonos.diagnostics", "homeassistant.components.sonos.entity", "homeassistant.components.sonos.favorites", - "homeassistant.components.sonos.helpers", "homeassistant.components.sonos.media_browser", "homeassistant.components.sonos.media_player", "homeassistant.components.sonos.number", @@ -154,11 +143,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.unifi.device_tracker", "homeassistant.components.unifi.diagnostics", "homeassistant.components.unifi.unifi_entity_base", - "homeassistant.components.upnp", - "homeassistant.components.upnp.binary_sensor", - "homeassistant.components.upnp.config_flow", - "homeassistant.components.upnp.device", - "homeassistant.components.upnp.sensor", "homeassistant.components.vizio.config_flow", "homeassistant.components.vizio.media_player", "homeassistant.components.withings", @@ -227,9 +211,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.zha.sensor", "homeassistant.components.zha.siren", "homeassistant.components.zha.switch", - "homeassistant.components.zwave", - "homeassistant.components.zwave.migration", - "homeassistant.components.zwave.node_entity", ] # Component modules which should set no_implicit_reexport = true. @@ -256,6 +237,7 @@ GENERAL_SETTINGS: Final[dict[str, str]] = { "warn_redundant_casts": "true", "warn_unused_configs": "true", "warn_unused_ignores": "true", + "enable_error_code": "ignore-without-code", } # This is basically the list of checks which is enabled for "strict=true". diff --git a/script/hassfest/requirements.py b/script/hassfest/requirements.py index 09afd11b147..510a70f30ce 100644 --- a/script/hassfest/requirements.py +++ b/script/hassfest/requirements.py @@ -22,7 +22,7 @@ IGNORE_PACKAGES = { commented.lower().replace("_", "-") for commented in COMMENT_REQUIREMENTS } PACKAGE_REGEX = re.compile( - r"^(?:--.+\s)?([-_\.\w\d\[\]]+)(==|>=|<=|~=|!=|<|>|===)*(.*)$" + r"^(?:--.+\s)?([-_,\.\w\d\[\]]+)(==|>=|<=|~=|!=|<|>|===)*(.*)$" ) PIP_REGEX = re.compile(r"^(--.+\s)?([-_\.\w\d]+.*(?:==|>=|<=|~=|!=|<|>|===)?.*$)") PIP_VERSION_RANGE_SEPARATOR = re.compile(r"^(==|>=|<=|~=|!=|<|>|===)?(.*)$") diff --git a/script/hassfest/translations.py b/script/hassfest/translations.py index d174f238217..0dcdbc133a6 100644 --- a/script/hassfest/translations.py +++ b/script/hassfest/translations.py @@ -26,6 +26,7 @@ ALLOW_NAME_TRANSLATION = { "cert_expiry", "cpuspeed", "emulated_roku", + "faa_delays", "garages_amsterdam", "google_travel_time", "homekit_controller", @@ -50,6 +51,16 @@ MOVED_TRANSLATIONS_DIRECTORY_MSG = ( ) +def allow_name_translation(integration: Integration): + """Validate that the translation name is not the same as the integration name.""" + # Only enforce for core because custom integrations can't be + # added to allow list. + return integration.core and ( + integration.domain in ALLOW_NAME_TRANSLATION + or integration.quality_scale == "internal" + ) + + def check_translations_directory_name(integration: Integration) -> None: """Check that the correct name is used for the translations directory.""" legacy_translations = integration.path / ".translations" @@ -100,6 +111,7 @@ def gen_data_entry_schema( integration: Integration, flow_title: int, require_step_title: bool, + mandatory_description: str | None = None, ): """Generate a data entry schema.""" step_title_class = vol.Required if require_step_title else vol.Optional @@ -110,6 +122,8 @@ def gen_data_entry_schema( step_title_class("title"): cv.string_with_no_html, vol.Optional("description"): cv.string_with_no_html, vol.Optional("data"): {str: cv.string_with_no_html}, + vol.Optional("data_description"): {str: cv.string_with_no_html}, + vol.Optional("menu_options"): {str: cv.string_with_no_html}, } }, vol.Optional("error"): {str: cv.string_with_no_html}, @@ -124,7 +138,51 @@ def gen_data_entry_schema( removed_title_validator, config, integration ) - return schema + def data_description_validator(value): + """Validate data description.""" + for step_info in value["step"].values(): + if "data_description" not in step_info: + continue + + for key in step_info["data_description"]: + if key not in step_info["data"]: + raise vol.Invalid(f"data_description key {key} is not in data") + + return value + + validators = [vol.Schema(schema), data_description_validator] + + if mandatory_description is not None: + + def validate_description_set(value): + """Validate description is set.""" + steps = value["step"] + if mandatory_description not in steps: + raise vol.Invalid(f"{mandatory_description} needs to be defined") + + if "description" not in steps[mandatory_description]: + raise vol.Invalid(f"Step {mandatory_description} needs a description") + + return value + + validators.append(validate_description_set) + + if not allow_name_translation(integration): + + def name_validator(value): + """Validate name.""" + for step_id, info in value["step"].items(): + if info.get("title") == integration.name: + raise vol.Invalid( + f"Do not set title of step {step_id} if it's a brand name " + "or add exception to ALLOW_NAME_TRANSLATION" + ) + + return value + + validators.append(name_validator) + + return vol.All(*validators) def gen_strings_schema(config: Config, integration: Integration): @@ -137,6 +195,9 @@ def gen_strings_schema(config: Config, integration: Integration): integration=integration, flow_title=REMOVED, require_step_title=False, + mandatory_description=( + "user" if integration.integration_type == "helper" else None + ), ), vol.Optional("options"): gen_data_entry_schema( config=config, @@ -280,14 +341,9 @@ def validate_translation_file(config: Config, integration: Integration, all_stri if strings_file.name == "strings.json": find_references(strings, name, references) - if ( - integration.domain not in ALLOW_NAME_TRANSLATION - # Only enforce for core because custom integratinos can't be - # added to allow list. - and integration.core - and strings.get("title") == integration.name - and integration.quality_scale != "internal" - ): + if strings.get( + "title" + ) == integration.name and not allow_name_translation(integration): integration.add_error( "translations", "Don't specify title in translation strings if it's a brand name " diff --git a/script/pip_check b/script/pip_check index fa217e89866..f29ea5a5dd0 100755 --- a/script/pip_check +++ b/script/pip_check @@ -14,6 +14,7 @@ then echo "------" echo "Requirements change added another dependency conflict." echo "Make sure to check the 'pip check' output above!" + echo "Expected $DEPENDENCY_CONFLICTS conflicts, got $LINE_COUNT." exit 1 elif [[ $((LINE_COUNT)) -lt $DEPENDENCY_CONFLICTS ]] then diff --git a/script/scaffold/__main__.py b/script/scaffold/__main__.py index 0504cdb8b37..2d4454c254b 100644 --- a/script/scaffold/__main__.py +++ b/script/scaffold/__main__.py @@ -59,7 +59,9 @@ def main(): # If it's a new integration and it's not a config flow, # create a config flow too. if not args.template.startswith("config_flow"): - if info.oauth2: + if info.helper: + template = "config_flow_helper" + elif info.oauth2: template = "config_flow_oauth2" elif info.authentication or not info.discoverable: template = "config_flow" diff --git a/script/scaffold/docs.py b/script/scaffold/docs.py index 3a871301b97..6e31f15e6d4 100644 --- a/script/scaffold/docs.py +++ b/script/scaffold/docs.py @@ -6,6 +6,10 @@ DATA = { "title": "Config Flow", "docs": "https://developers.home-assistant.io/docs/en/config_entries_config_flow_handler.html", }, + "config_flow_helper": { + "title": "Helper Config Flow", + "docs": "https://developers.home-assistant.io/docs/en/config_entries_config_flow_handler.html#helper", + }, "config_flow_discovery": { "title": "Discoverable Config Flow", "docs": "https://developers.home-assistant.io/docs/en/config_entries_config_flow_handler.html#discoverable-integrations-that-require-no-authentication", diff --git a/script/scaffold/gather_info.py b/script/scaffold/gather_info.py index 8442650dce4..85a94459d06 100644 --- a/script/scaffold/gather_info.py +++ b/script/scaffold/gather_info.py @@ -119,6 +119,11 @@ More info @ https://developers.home-assistant.io/docs/creating_integration_manif "default": "no", **YES_NO, }, + "helper": { + "prompt": "Is this a helper integration? (yes/no)", + "default": "no", + **YES_NO, + }, "oauth2": { "prompt": "Can the user authenticate the device using OAuth2? (yes/no)", "default": "no", diff --git a/script/scaffold/generate.py b/script/scaffold/generate.py index 122d8570dc1..8bde9504f41 100644 --- a/script/scaffold/generate.py +++ b/script/scaffold/generate.py @@ -150,6 +150,28 @@ def _custom_tasks(template, info: Info) -> None: }, ) + elif template == "config_flow_helper": + info.update_manifest(config_flow=True) + info.update_strings( + config={ + "step": { + "user": { + "description": "New NEW_NAME Sensor", + "data": {"entity": "Input sensor", "name": "Name"}, + }, + }, + }, + options={ + "step": { + "init": { + "data": { + "entity": "[%key:component::NEW_DOMAIN::config::step::user::description%]" + }, + }, + }, + }, + ) + elif template == "config_flow_oauth2": info.update_manifest(config_flow=True, dependencies=["http"]) info.update_strings( diff --git a/script/scaffold/model.py b/script/scaffold/model.py index 93801f973ea..2b1ee71fc63 100644 --- a/script/scaffold/model.py +++ b/script/scaffold/model.py @@ -22,6 +22,7 @@ class Info: authentication: str = attr.ib(default=None) discoverable: str = attr.ib(default=None) oauth2: str = attr.ib(default=None) + helper: str = attr.ib(default=None) files_added: set[Path] = attr.ib(factory=set) tests_added: set[Path] = attr.ib(factory=set) @@ -50,7 +51,7 @@ class Info: """Update the integration manifest.""" print(f"Updating {self.domain} manifest: {kwargs}") self.manifest_path.write_text( - json.dumps({**self.manifest(), **kwargs}, indent=2) + json.dumps({**self.manifest(), **kwargs}, indent=2) + "\n" ) @property @@ -67,4 +68,6 @@ class Info: def update_strings(self, **kwargs) -> None: """Update the integration strings.""" print(f"Updating {self.domain} strings: {list(kwargs)}") - self.strings_path.write_text(json.dumps({**self.strings(), **kwargs}, indent=2)) + self.strings_path.write_text( + json.dumps({**self.strings(), **kwargs}, indent=2) + "\n" + ) diff --git a/script/scaffold/templates/config_flow_helper/integration/__init__.py b/script/scaffold/templates/config_flow_helper/integration/__init__.py new file mode 100644 index 00000000000..e0d115559a1 --- /dev/null +++ b/script/scaffold/templates/config_flow_helper/integration/__init__.py @@ -0,0 +1,39 @@ +"""The NEW_NAME integration.""" +from __future__ import annotations + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant + +from .const import DOMAIN + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up NEW_NAME from a config entry.""" + # TODO Optionally store an object for your platforms to access + # hass.data[DOMAIN][entry.entry_id] = ... + + # TODO Optionally validate config entry options before setting up platform + + hass.config_entries.async_setup_platforms(entry, (Platform.SENSOR,)) + + # TODO Remove if the integration does not have an options flow + entry.async_on_unload(entry.add_update_listener(config_entry_update_listener)) + + return True + + +# TODO Remove if the integration does not have an options flow +async def config_entry_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Update listener, called when the config entry options are changed.""" + await hass.config_entries.async_reload(entry.entry_id) + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms( + entry, (Platform.SENSOR,) + ): + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/script/scaffold/templates/config_flow_helper/integration/config_flow.py b/script/scaffold/templates/config_flow_helper/integration/config_flow.py new file mode 100644 index 00000000000..b8a048e9dba --- /dev/null +++ b/script/scaffold/templates/config_flow_helper/integration/config_flow.py @@ -0,0 +1,51 @@ +"""Config flow for NEW_NAME integration.""" +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, cast + +import voluptuous as vol + +from homeassistant.const import CONF_ENTITY_ID +from homeassistant.helpers import selector +from homeassistant.helpers.schema_config_entry_flow import ( + SchemaConfigFlowHandler, + SchemaFlowFormStep, + SchemaFlowMenuStep, +) + +from .const import DOMAIN + +OPTIONS_SCHEMA = vol.Schema( + { + vol.Required(CONF_ENTITY_ID): selector.selector( + {"entity": {"domain": "sensor"}} + ), + } +) + +CONFIG_SCHEMA = vol.Schema( + { + vol.Required("name"): selector.selector({"text": {}}), + } +).extend(OPTIONS_SCHEMA.schema) + +CONFIG_FLOW: dict[str, SchemaFlowFormStep | SchemaFlowMenuStep] = { + "user": SchemaFlowFormStep(CONFIG_SCHEMA) +} + +OPTIONS_FLOW: dict[str, SchemaFlowFormStep | SchemaFlowMenuStep] = { + "init": SchemaFlowFormStep(OPTIONS_SCHEMA) +} + + +class ConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN): + """Handle a config or options flow for NEW_NAME.""" + + config_flow = CONFIG_FLOW + # TODO remove the options_flow if the integration does not have an options flow + options_flow = OPTIONS_FLOW + + def async_config_entry_title(self, options: Mapping[str, Any]) -> str: + """Return config entry title.""" + return cast(str, options["name"]) if "name" in options else "" diff --git a/script/scaffold/templates/config_flow_helper/integration/const.py b/script/scaffold/templates/config_flow_helper/integration/const.py new file mode 100644 index 00000000000..e8a1c494d49 --- /dev/null +++ b/script/scaffold/templates/config_flow_helper/integration/const.py @@ -0,0 +1,3 @@ +"""Constants for the NEW_NAME integration.""" + +DOMAIN = "NEW_DOMAIN" diff --git a/script/scaffold/templates/config_flow_helper/integration/sensor.py b/script/scaffold/templates/config_flow_helper/integration/sensor.py new file mode 100644 index 00000000000..fbf92bfdd6b --- /dev/null +++ b/script/scaffold/templates/config_flow_helper/integration/sensor.py @@ -0,0 +1,38 @@ +"""Sensor platform for NEW_NAME integration.""" +from __future__ import annotations + +from homeassistant.components.sensor import SensorEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_ENTITY_ID +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.entity_platform import AddEntitiesCallback + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Initialize NEW_NAME config entry.""" + registry = er.async_get(hass) + # Validate + resolve entity registry id to entity_id + entity_id = er.async_validate_entity_id( + registry, config_entry.options[CONF_ENTITY_ID] + ) + # TODO Optionally validate config entry options before creating entity + name = config_entry.title + unique_id = config_entry.entry_id + + async_add_entities([NEW_DOMAINSensorEntity(unique_id, name, entity_id)]) + + +class NEW_DOMAINSensorEntity(SensorEntity): + """NEW_DOMAIN Sensor.""" + + def __init__(self, unique_id: str, name: str, wrapped_entity_id: str) -> None: + """Initialize NEW_DOMAIN Sensor.""" + super().__init__() + self._wrapped_entity_id = wrapped_entity_id + self._attr_name = name + self._attr_unique_id = unique_id diff --git a/script/scaffold/templates/config_flow_helper/tests/test_config_flow.py b/script/scaffold/templates/config_flow_helper/tests/test_config_flow.py new file mode 100644 index 00000000000..b7bef2c63f4 --- /dev/null +++ b/script/scaffold/templates/config_flow_helper/tests/test_config_flow.py @@ -0,0 +1,116 @@ +"""Test the NEW_NAME config flow.""" +from unittest.mock import patch + +import pytest + +from homeassistant import config_entries +from homeassistant.components.NEW_DOMAIN.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM + +from tests.common import MockConfigEntry + + +@pytest.mark.parametrize("platform", ("sensor",)) +async def test_config_flow(hass: HomeAssistant, platform) -> None: + """Test the config flow.""" + input_sensor_entity_id = "sensor.input" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] is None + + with patch( + "homeassistant.components.NEW_DOMAIN.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"name": "My NEW_DOMAIN", "entity_id": input_sensor_entity_id}, + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "My NEW_DOMAIN" + assert result["data"] == {} + assert result["options"] == { + "entity_id": input_sensor_entity_id, + "name": "My NEW_DOMAIN", + } + assert len(mock_setup_entry.mock_calls) == 1 + + config_entry = hass.config_entries.async_entries(DOMAIN)[0] + assert config_entry.data == {} + assert config_entry.options == { + "entity_id": input_sensor_entity_id, + "name": "My NEW_DOMAIN", + } + assert config_entry.title == "My NEW_DOMAIN" + + +def get_suggested(schema, key): + """Get suggested value for key in voluptuous schema.""" + for k in schema.keys(): + if k == key: + if k.description is None or "suggested_value" not in k.description: + return None + return k.description["suggested_value"] + # Wanted key absent from schema + raise Exception + + +@pytest.mark.parametrize("platform", ("sensor",)) +async def test_options(hass: HomeAssistant, platform) -> None: + """Test reconfiguring.""" + input_sensor_1_entity_id = "sensor.input1" + input_sensor_2_entity_id = "sensor.input2" + + # Setup the config entry + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + "entity_id": input_sensor_1_entity_id, + "name": "My NEW_DOMAIN", + }, + title="My NEW_DOMAIN", + ) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "init" + schema = result["data_schema"].schema + assert get_suggested(schema, "entity_id") == input_sensor_1_entity_id + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + "entity_id": input_sensor_2_entity_id, + }, + ) + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["data"] == { + "entity_id": input_sensor_2_entity_id, + "name": "My NEW_DOMAIN", + } + assert config_entry.data == {} + assert config_entry.options == { + "entity_id": input_sensor_2_entity_id, + "name": "My NEW_DOMAIN", + } + assert config_entry.title == "My NEW_DOMAIN" + + # Check config entry is reloaded with new options + await hass.async_block_till_done() + + # Check the entity was updated, no new entity was created + assert len(hass.states.async_all()) == 1 + + # TODO Check the state of the entity has changed as expected + state = hass.states.get(f"{platform}.my_NEW_DOMAIN") + assert state.attributes == {} diff --git a/script/scaffold/templates/config_flow_helper/tests/test_init.py b/script/scaffold/templates/config_flow_helper/tests/test_init.py new file mode 100644 index 00000000000..0e86874c745 --- /dev/null +++ b/script/scaffold/templates/config_flow_helper/tests/test_init.py @@ -0,0 +1,50 @@ +"""Test the NEW_NAME integration.""" +import pytest + +from homeassistant.components.NEW_DOMAIN.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from tests.common import MockConfigEntry + + +@pytest.mark.parametrize("platform", ("sensor",)) +async def test_setup_and_remove_config_entry( + hass: HomeAssistant, + platform: str, +) -> None: + """Test setting up and removing a config entry.""" + input_sensor_entity_id = "sensor.input" + registry = er.async_get(hass) + NEW_DOMAIN_entity_id = f"{platform}.my_NEW_DOMAIN" + + # Setup the config entry + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + "entity_id": input_sensor_entity_id, + "name": "My NEW_DOMAIN", + }, + title="My NEW_DOMAIN", + ) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + # Check the entity is registered in the entity registry + assert registry.async_get(NEW_DOMAIN_entity_id) is not None + + # Check the platform is setup correctly + state = hass.states.get(NEW_DOMAIN_entity_id) + # TODO Check the state of the entity has changed as expected + assert state.state == "unknown" + assert state.attributes == {} + + # Remove the config entry + assert await hass.config_entries.async_remove(config_entry.entry_id) + await hass.async_block_till_done() + + # Check the state and entity registry entry are removed + assert hass.states.get(NEW_DOMAIN_entity_id) is None + assert registry.async_get(NEW_DOMAIN_entity_id) is None diff --git a/script/scaffold/templates/reproduce_state/tests/test_reproduce_state.py b/script/scaffold/templates/reproduce_state/tests/test_reproduce_state.py index 83d95570b45..cb4e37c0933 100644 --- a/script/scaffold/templates/reproduce_state/tests/test_reproduce_state.py +++ b/script/scaffold/templates/reproduce_state/tests/test_reproduce_state.py @@ -2,6 +2,7 @@ import pytest from homeassistant.core import HomeAssistant, State +from homeassistant.helpers.state import async_reproduce_state from tests.common import async_mock_service @@ -17,7 +18,8 @@ async def test_reproducing_states( turn_off_calls = async_mock_service(hass, "NEW_DOMAIN", "turn_off") # These calls should do nothing as entities already in desired state - await hass.helpers.state.async_reproduce_state( + await async_reproduce_state( + hass, [ State("NEW_DOMAIN.entity_off", "off"), State("NEW_DOMAIN.entity_on", "on", {"color": "red"}), @@ -29,8 +31,8 @@ async def test_reproducing_states( assert len(turn_off_calls) == 0 # Test invalid state is handled - await hass.helpers.state.async_reproduce_state( - [State("NEW_DOMAIN.entity_off", "not_supported")], blocking=True + await async_reproduce_state( + hass, [State("NEW_DOMAIN.entity_off", "not_supported")], blocking=True ) assert "not_supported" in caplog.text @@ -38,7 +40,8 @@ async def test_reproducing_states( assert len(turn_off_calls) == 0 # Make sure correct services are called - await hass.helpers.state.async_reproduce_state( + await async_reproduce_state( + hass, [ State("NEW_DOMAIN.entity_on", "off"), State("NEW_DOMAIN.entity_off", "on", {"color": "red"}), diff --git a/script/translations/migrate.py b/script/translations/migrate.py index c4c47600698..d3efdc28d13 100644 --- a/script/translations/migrate.py +++ b/script/translations/migrate.py @@ -237,7 +237,7 @@ STATE_REWRITE = { "[%key:state::lock::unlocked%]": "[%key:common::state::unlocked%]", } SKIP_DOMAIN = {"default", "scene"} -STATES_WITH_DEV_CLASS = {"binary_sensor", "zwave"} +STATES_WITH_DEV_CLASS = {"binary_sensor"} GROUP_DELETE = {"opening", "closing", "stopped"} # They don't exist diff --git a/setup.cfg b/setup.cfg index 970a06fcab8..779f517a1fd 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = homeassistant -version = 2022.3.8 +version = 2022.4.0 author = The Home Assistant Authors author_email = hello@home-assistant.io license = Apache-2.0 @@ -42,10 +42,10 @@ install_requires = ciso8601==2.2.0 # When bumping httpx, please check the version pins of # httpcore, anyio, and h11 in gen_requirements_all - httpx==0.21.3 + httpx==0.22.0 ifaddr==0.1.7 - jinja2==3.0.3 - PyJWT==2.1.0 + jinja2==3.1.0 + PyJWT==2.3.0 # PyJWT has loose dependency. We want the latest one. cryptography==35.0.0 pip>=21.0,<22.1 diff --git a/tests/common.py b/tests/common.py index bdebc7217a7..5968a21cddb 100644 --- a/tests/common.py +++ b/tests/common.py @@ -55,6 +55,7 @@ from homeassistant.helpers import ( restore_state, storage, ) +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.json import JSONEncoder from homeassistant.setup import async_setup_component, setup_component from homeassistant.util.async_ import run_callback_threadsafe @@ -282,7 +283,7 @@ async def async_test_home_assistant(loop, load_registries=True): hass.config.latitude = 32.87336 hass.config.longitude = -117.22743 hass.config.elevation = 0 - hass.config.time_zone = "US/Pacific" + hass.config.set_time_zone("US/Pacific") hass.config.units = METRIC_SYSTEM hass.config.media_dirs = {"local": get_test_config_dir("media")} hass.config.skip_pip = True @@ -1208,7 +1209,7 @@ def async_mock_signal(hass, signal): """Mock service call.""" calls.append(args) - hass.helpers.dispatcher.async_dispatcher_connect(signal, mock_signal_handler) + async_dispatcher_connect(hass, signal, mock_signal_handler) return calls diff --git a/tests/components/abode/fixtures/automation.json b/tests/components/abode/fixtures/automation.json index fb1c00faff9..6de09228c51 100644 --- a/tests/components/abode/fixtures/automation.json +++ b/tests/components/abode/fixtures/automation.json @@ -1,38 +1,35 @@ { - "name": "Test Automation", - "enabled": "True", - "version": 2, - "id": "47fae27488f74f55b964a81a066c3a01", - "subType": "", - "actions": [ - { - "directive": { - "trait": "panel.traits.panelMode", - "name": "panel.directives.arm", - "state": { - "panelMode": "AWAY" - } - } + "name": "Test Automation", + "enabled": "True", + "version": 2, + "id": "47fae27488f74f55b964a81a066c3a01", + "subType": "", + "actions": [ + { + "directive": { + "trait": "panel.traits.panelMode", + "name": "panel.directives.arm", + "state": { + "panelMode": "AWAY" } - ], - "conditions": {}, - "triggers": { - "operator": "OR", - "expressions": [ - { - "mobileDevices": [ - "89381", - "658" - ], - "property": { - "trait": "mobile.traits.location", - "name": "location", - "rule": { - "location": "31675", - "equalTo": "LAST_OUT" - } - } - } - ] + } } -} \ No newline at end of file + ], + "conditions": {}, + "triggers": { + "operator": "OR", + "expressions": [ + { + "mobileDevices": ["89381", "658"], + "property": { + "trait": "mobile.traits.location", + "name": "location", + "rule": { + "location": "31675", + "equalTo": "LAST_OUT" + } + } + } + ] + } +} diff --git a/tests/components/abode/fixtures/automation_changed.json b/tests/components/abode/fixtures/automation_changed.json index 39b874c4dfc..91aee084601 100644 --- a/tests/components/abode/fixtures/automation_changed.json +++ b/tests/components/abode/fixtures/automation_changed.json @@ -1,38 +1,35 @@ { - "name": "Test Automation", - "enabled": "False", - "version": 2, - "id": "47fae27488f74f55b964a81a066c3a01", - "subType": "", - "actions": [ - { - "directive": { - "trait": "panel.traits.panelMode", - "name": "panel.directives.arm", - "state": { - "panelMode": "AWAY" - } - } + "name": "Test Automation", + "enabled": "False", + "version": 2, + "id": "47fae27488f74f55b964a81a066c3a01", + "subType": "", + "actions": [ + { + "directive": { + "trait": "panel.traits.panelMode", + "name": "panel.directives.arm", + "state": { + "panelMode": "AWAY" } - ], - "conditions": {}, - "triggers": { - "operator": "OR", - "expressions": [ - { - "mobileDevices": [ - "89381", - "658" - ], - "property": { - "trait": "mobile.traits.location", - "name": "location", - "rule": { - "location": "31675", - "equalTo": "LAST_OUT" - } - } - } - ] + } } -} \ No newline at end of file + ], + "conditions": {}, + "triggers": { + "operator": "OR", + "expressions": [ + { + "mobileDevices": ["89381", "658"], + "property": { + "trait": "mobile.traits.location", + "name": "location", + "rule": { + "location": "31675", + "equalTo": "LAST_OUT" + } + } + } + ] + } +} diff --git a/tests/components/abode/fixtures/devices.json b/tests/components/abode/fixtures/devices.json index 002947f4085..6a7c01ca552 100644 --- a/tests/components/abode/fixtures/devices.json +++ b/tests/components/abode/fixtures/devices.json @@ -1,799 +1,799 @@ [ - { - "id": "RF:01430030", - "type_tag": "device_type.door_contact", - "type": "Door Contact", - "name": "Front Door", - "area": "1", - "zone": "15", - "sort_order": "", - "is_window": "1", - "bypass": "0", - "schar_24hr": "0", - "sresp_24hr": "0", - "sresp_mode_0": "3", - "sresp_entry_0": "3", - "sresp_exit_0": "0", - "group_name": "Doors and Windows", - "group_id": "397972", - "default_group_id": "1", - "sort_id": "10000", - "sresp_mode_1": "1", - "sresp_entry_1": "1", - "sresp_exit_1": "0", - "sresp_mode_2": "1", - "sresp_entry_2": "1", - "sresp_exit_2": "0", - "sresp_mode_3": "1", - "uuid": "2834013428b6035fba7d4054aa7b25a3", - "sresp_entry_3": "1", - "sresp_exit_3": "0", - "sresp_mode_4": "1", - "sresp_entry_4": "1", - "sresp_exit_4": "0", - "version": "", - "origin": "abode", - "has_subscription": null, - "onboard": "0", - "s2_grnt_keys": "", - "s2_dsk": "", - "s2_propty": "", - "s2_keys_valid": "", - "zwave_secure_protocol": "", - "control_url": "", - "deep_link": null, - "status_color": "#5cb85c", - "faults": { - "low_battery": 0, - "tempered": 0, - "supervision": 0, - "out_of_order": 0, - "no_response": 0, - "jammed": 0, - "zwave_fault": 0 - }, - "status": "Closed", - "status_display": "Closed", - "statuses": { - "open": "0" - }, - "status_ex": "", - "actions": [], - "status_icons": { - "Open": "assets/icons/WindowOpened.svg", - "Closed": "assets/icons/WindowClosed.svg" - }, - "icon": "assets/icons/doorsensor-a.svg", - "sresp_trigger": "0", - "sresp_restore": "0" + { + "id": "RF:01430030", + "type_tag": "device_type.door_contact", + "type": "Door Contact", + "name": "Front Door", + "area": "1", + "zone": "15", + "sort_order": "", + "is_window": "1", + "bypass": "0", + "schar_24hr": "0", + "sresp_24hr": "0", + "sresp_mode_0": "3", + "sresp_entry_0": "3", + "sresp_exit_0": "0", + "group_name": "Doors and Windows", + "group_id": "397972", + "default_group_id": "1", + "sort_id": "10000", + "sresp_mode_1": "1", + "sresp_entry_1": "1", + "sresp_exit_1": "0", + "sresp_mode_2": "1", + "sresp_entry_2": "1", + "sresp_exit_2": "0", + "sresp_mode_3": "1", + "uuid": "2834013428b6035fba7d4054aa7b25a3", + "sresp_entry_3": "1", + "sresp_exit_3": "0", + "sresp_mode_4": "1", + "sresp_entry_4": "1", + "sresp_exit_4": "0", + "version": "", + "origin": "abode", + "has_subscription": null, + "onboard": "0", + "s2_grnt_keys": "", + "s2_dsk": "", + "s2_propty": "", + "s2_keys_valid": "", + "zwave_secure_protocol": "", + "control_url": "", + "deep_link": null, + "status_color": "#5cb85c", + "faults": { + "low_battery": 0, + "tempered": 0, + "supervision": 0, + "out_of_order": 0, + "no_response": 0, + "jammed": 0, + "zwave_fault": 0 }, - { - "id": "RF:01c34a30", - "type_tag": "device_type.povs", - "type": "Occupancy", - "name": "Hallway Motion", - "area": "1", - "zone": "17", - "sort_order": "", - "is_window": "", - "bypass": "0", - "schar_24hr": "0", - "sresp_24hr": "0", - "sresp_mode_0": "0", - "sresp_entry_0": "0", - "sresp_exit_0": "0", - "group_name": "Ungrouped", - "group_id": "1", - "default_group_id": "1", - "sort_id": "10000", - "sresp_mode_1": "5", - "sresp_entry_1": "4", - "sresp_exit_1": "0", - "sresp_mode_2": "0", - "sresp_entry_2": "4", - "sresp_exit_2": "0", - "sresp_mode_3": "0", - "uuid": "ba2c7e8d4430da8d34c31425a2823fe0", - "sresp_entry_3": "0", - "sresp_exit_3": "0", - "sresp_mode_4": "0", - "sresp_entry_4": "0", - "sresp_exit_4": "0", - "version": "", - "origin": "abode", - "has_subscription": null, - "onboard": "0", - "s2_grnt_keys": "", - "s2_dsk": "", - "s2_propty": "", - "s2_keys_valid": "", - "zwave_secure_protocol": "", - "control_url": "", - "deep_link": null, - "status_color": "#5cb85c", - "faults": { - "low_battery": 0, - "tempered": 0, - "supervision": 0, - "out_of_order": 0, - "no_response": 0, - "jammed": 0, - "zwave_fault": 0 - }, - "status": "Online", - "status_display": "Online", - "statuses": { - "motion": "0" - }, - "status_ex": "", - "actions": [], - "status_icons": [], - "icon": "assets/icons/motioncamera-a.svg", - "sresp_trigger": "0", - "sresp_restore": "0", - "occupancy_timer": null, - "sensitivity": null, - "model": "L1", - "is_motion_sensor": true + "status": "Closed", + "status_display": "Closed", + "statuses": { + "open": "0" }, - { - "id": "SR:PIR", - "type_tag": "device_type.pir", - "type": "Motion Sensor", - "name": "Living Room Motion", - "area": "1", - "zone": "2", - "sort_order": "", - "is_window": "", - "bypass": "0", - "schar_24hr": "0", - "sresp_24hr": "0", - "sresp_mode_0": "0", - "sresp_entry_0": "0", - "sresp_exit_0": "0", - "group_name": "Motion", - "group_id": "397973", - "default_group_id": "1", - "sort_id": "10000", - "sresp_mode_1": "5", - "sresp_entry_1": "4", - "sresp_exit_1": "0", - "sresp_mode_2": "0", - "sresp_entry_2": "4", - "sresp_exit_2": "0", - "sresp_mode_3": "0", - "uuid": "2f1bc34ceadac032af4fc9189ef821a8", - "sresp_entry_3": "0", - "sresp_exit_3": "0", - "sresp_mode_4": "0", - "sresp_entry_4": "0", - "sresp_exit_4": "0", - "version": "", - "origin": "abode", - "has_subscription": null, - "onboard": "1", - "s2_grnt_keys": "", - "s2_dsk": "", - "s2_propty": "", - "s2_keys_valid": "", - "zwave_secure_protocol": "", - "control_url": "", - "deep_link": null, - "status_color": "#5cb85c", - "faults": { - "low_battery": 0, - "tempered": 0, - "supervision": 0, - "out_of_order": 0, - "no_response": 0, - "jammed": 0, - "zwave_fault": 0 - }, - "status": "Online", - "status_display": "Online", - "statuses": [], - "status_ex": "", - "actions": [], - "status_icons": [], - "icon": "assets/icons/motioncamera-a.svg", - "schar_obpir_sens": "15", - "schar_obpir_pulse": "2", - "sensitivity": "15", - "model": "L1" + "status_ex": "", + "actions": [], + "status_icons": { + "Open": "assets/icons/WindowOpened.svg", + "Closed": "assets/icons/WindowClosed.svg" }, - { - "id": "ZB:db5b1a", - "type_tag": "device_type.hue", - "type": "RGB Dimmer", - "name": "Living Room Lamp", - "area": "1", - "zone": "21", - "sort_order": "", - "is_window": "", - "bypass": "0", - "schar_24hr": "0", - "sresp_24hr": "0", - "sresp_mode_0": "0", - "sresp_entry_0": "0", - "sresp_exit_0": "0", - "group_name": "Ungrouped", - "group_id": "1", - "default_group_id": "1", - "sort_id": "10000", - "sresp_mode_1": "0", - "sresp_entry_1": "0", - "sresp_exit_1": "0", - "sresp_mode_2": "0", - "sresp_entry_2": "0", - "sresp_exit_2": "0", - "sresp_mode_3": "0", - "uuid": "741385f4388b2637df4c6b398fe50581", - "sresp_entry_3": "0", - "sresp_exit_3": "0", - "sresp_mode_4": "0", - "sresp_entry_4": "0", - "sresp_exit_4": "0", - "version": "LCT014", - "origin": "abode", - "has_subscription": null, - "onboard": "0", - "s2_grnt_keys": "", - "s2_dsk": "", - "s2_propty": "", - "s2_keys_valid": "", - "zwave_secure_protocol": "", - "control_url": "api/v1/control/light/ZB:db5b1a", - "deep_link": null, - "status_color": "#5cb85c", - "faults": { - "low_battery": 0, - "tempered": 0, - "supervision": 0, - "out_of_order": 0, - "no_response": 0, - "jammed": 0, - "zwave_fault": 0 - }, - "status": "On", - "status_display": "On", - "statuses": { - "saturation": 100, - "hue": 225, - "level": "79", - "switch": "1", - "color_temp": 3571, - "color_mode": "0" - }, - "status_ex": "", - "actions": [], - "status_icons": [], - "icon": "assets/icons/bulb-1.svg", - "statusEx": "0" + "icon": "assets/icons/doorsensor-a.svg", + "sresp_trigger": "0", + "sresp_restore": "0" + }, + { + "id": "RF:01c34a30", + "type_tag": "device_type.povs", + "type": "Occupancy", + "name": "Hallway Motion", + "area": "1", + "zone": "17", + "sort_order": "", + "is_window": "", + "bypass": "0", + "schar_24hr": "0", + "sresp_24hr": "0", + "sresp_mode_0": "0", + "sresp_entry_0": "0", + "sresp_exit_0": "0", + "group_name": "Ungrouped", + "group_id": "1", + "default_group_id": "1", + "sort_id": "10000", + "sresp_mode_1": "5", + "sresp_entry_1": "4", + "sresp_exit_1": "0", + "sresp_mode_2": "0", + "sresp_entry_2": "4", + "sresp_exit_2": "0", + "sresp_mode_3": "0", + "uuid": "ba2c7e8d4430da8d34c31425a2823fe0", + "sresp_entry_3": "0", + "sresp_exit_3": "0", + "sresp_mode_4": "0", + "sresp_entry_4": "0", + "sresp_exit_4": "0", + "version": "", + "origin": "abode", + "has_subscription": null, + "onboard": "0", + "s2_grnt_keys": "", + "s2_dsk": "", + "s2_propty": "", + "s2_keys_valid": "", + "zwave_secure_protocol": "", + "control_url": "", + "deep_link": null, + "status_color": "#5cb85c", + "faults": { + "low_battery": 0, + "tempered": 0, + "supervision": 0, + "out_of_order": 0, + "no_response": 0, + "jammed": 0, + "zwave_fault": 0 }, - { - "id": "ZB:db5b1b", - "type_tag": "device_type.hue", - "type": "Dimmer", - "name": "Test Dimmer Only Device", - "area": "1", - "zone": "21", - "sort_order": "", - "is_window": "", - "bypass": "0", - "schar_24hr": "0", - "sresp_24hr": "0", - "sresp_mode_0": "0", - "sresp_entry_0": "0", - "sresp_exit_0": "0", - "group_name": "Ungrouped", - "group_id": "1", - "default_group_id": "1", - "sort_id": "10000", - "sresp_mode_1": "0", - "sresp_entry_1": "0", - "sresp_exit_1": "0", - "sresp_mode_2": "0", - "sresp_entry_2": "0", - "sresp_exit_2": "0", - "sresp_mode_3": "0", - "uuid": "641385f4388b2637df4c6b398fe50581", - "sresp_entry_3": "0", - "sresp_exit_3": "0", - "sresp_mode_4": "0", - "sresp_entry_4": "0", - "sresp_exit_4": "0", - "version": "LCT014", - "origin": "abode", - "has_subscription": null, - "onboard": "0", - "s2_grnt_keys": "", - "s2_dsk": "", - "s2_propty": "", - "s2_keys_valid": "", - "zwave_secure_protocol": "", - "control_url": "api/v1/control/light/ZB:db5b1b", - "deep_link": null, - "status_color": "#5cb85c", - "faults": { - "low_battery": 0, - "tempered": 0, - "supervision": 0, - "out_of_order": 0, - "no_response": 0, - "jammed": 0, - "zwave_fault": 0 - }, - "status": "On", - "status_display": "On", - "statuses": { - "saturation": 100, - "hue": 225, - "level": "100", - "switch": "1", - "color_temp": 3571, - "color_mode": "2" - }, - "status_ex": "", - "actions": [], - "status_icons": [], - "icon": "assets/icons/bulb-1.svg", - "statusEx": "0" + "status": "Online", + "status_display": "Online", + "statuses": { + "motion": "0" }, - { - "id": "ZB:db5b1c", - "type_tag": "device_type.dimmer", - "type": "Light", - "name": "Test Non-dimmer Device", - "area": "1", - "zone": "21", - "sort_order": "", - "is_window": "", - "bypass": "0", - "schar_24hr": "0", - "sresp_24hr": "0", - "sresp_mode_0": "0", - "sresp_entry_0": "0", - "sresp_exit_0": "0", - "group_name": "Ungrouped", - "group_id": "1", - "default_group_id": "1", - "sort_id": "10000", - "sresp_mode_1": "0", - "sresp_entry_1": "0", - "sresp_exit_1": "0", - "sresp_mode_2": "0", - "sresp_entry_2": "0", - "sresp_exit_2": "0", - "sresp_mode_3": "0", - "uuid": "641385f4388b2637df4c6b398fe50583", - "sresp_entry_3": "0", - "sresp_exit_3": "0", - "sresp_mode_4": "0", - "sresp_entry_4": "0", - "sresp_exit_4": "0", - "version": "LCT014", - "origin": "abode", - "has_subscription": null, - "onboard": "0", - "s2_grnt_keys": "", - "s2_dsk": "", - "s2_propty": "", - "s2_keys_valid": "", - "zwave_secure_protocol": "", - "control_url": "api/v1/control/light/ZB:db5b1c", - "deep_link": null, - "status_color": "#5cb85c", - "faults": { - "low_battery": 0, - "tempered": 0, - "supervision": 0, - "out_of_order": 0, - "no_response": 0, - "jammed": 0, - "zwave_fault": 0 - }, - "status": "On", - "status_display": "On", - "statuses": { - "switch": "1" - }, - "status_ex": "", - "actions": [], - "status_icons": [], - "icon": "assets/icons/bulb-1.svg", - "statusEx": "0" + "status_ex": "", + "actions": [], + "status_icons": [], + "icon": "assets/icons/motioncamera-a.svg", + "sresp_trigger": "0", + "sresp_restore": "0", + "occupancy_timer": null, + "sensitivity": null, + "model": "L1", + "is_motion_sensor": true + }, + { + "id": "SR:PIR", + "type_tag": "device_type.pir", + "type": "Motion Sensor", + "name": "Living Room Motion", + "area": "1", + "zone": "2", + "sort_order": "", + "is_window": "", + "bypass": "0", + "schar_24hr": "0", + "sresp_24hr": "0", + "sresp_mode_0": "0", + "sresp_entry_0": "0", + "sresp_exit_0": "0", + "group_name": "Motion", + "group_id": "397973", + "default_group_id": "1", + "sort_id": "10000", + "sresp_mode_1": "5", + "sresp_entry_1": "4", + "sresp_exit_1": "0", + "sresp_mode_2": "0", + "sresp_entry_2": "4", + "sresp_exit_2": "0", + "sresp_mode_3": "0", + "uuid": "2f1bc34ceadac032af4fc9189ef821a8", + "sresp_entry_3": "0", + "sresp_exit_3": "0", + "sresp_mode_4": "0", + "sresp_entry_4": "0", + "sresp_exit_4": "0", + "version": "", + "origin": "abode", + "has_subscription": null, + "onboard": "1", + "s2_grnt_keys": "", + "s2_dsk": "", + "s2_propty": "", + "s2_keys_valid": "", + "zwave_secure_protocol": "", + "control_url": "", + "deep_link": null, + "status_color": "#5cb85c", + "faults": { + "low_battery": 0, + "tempered": 0, + "supervision": 0, + "out_of_order": 0, + "no_response": 0, + "jammed": 0, + "zwave_fault": 0 }, - { - "id": "RF:02148e70", - "type_tag": "device_type.lm", - "type": "LM", - "name": "Environment Sensor", - "area": "1", - "zone": "24", - "sort_order": "", - "is_window": "", - "bypass": "0", - "schar_24hr": "0", - "sresp_24hr": "0", - "sresp_mode_0": "0", - "sresp_entry_0": "0", - "sresp_exit_0": "0", - "group_name": "Ungrouped", - "group_id": "1", - "default_group_id": "1", - "sort_id": "10000", - "sresp_mode_1": "0", - "sresp_entry_1": "0", - "sresp_exit_1": "0", - "sresp_mode_2": "0", - "sresp_entry_2": "0", - "sresp_exit_2": "0", - "sresp_mode_3": "0", - "uuid": "13545b21f4bdcd33d9abd461f8443e65", - "sresp_entry_3": "0", - "sresp_exit_3": "0", - "sresp_mode_4": "0", - "sresp_entry_4": "0", - "sresp_exit_4": "0", - "version": "", - "origin": "abode", - "has_subscription": null, - "onboard": "0", - "s2_grnt_keys": "", - "s2_dsk": "", - "s2_propty": "", - "s2_keys_valid": "", - "zwave_secure_protocol": "", - "control_url": "", - "deep_link": null, - "status_color": "#5cb85c", - "faults": { - "low_battery": 0, - "tempered": 0, - "supervision": 0, - "out_of_order": 0, - "no_response": 0, - "jammed": 0, - "zwave_fault": 0 - }, - "status": "67 \u00b0F", - "status_display": "Online", - "statuses": { - "temperature": "67 \u00b0F", - "temp": "19.5", - "lux": "1 lx", - "humidity": "32 %" - }, - "status_ex": "", - "actions": [ - { - "label": "High Humidity Alarm", - "value": "a=1&z=24&trigger=HMH;" - }, - { - "label": "Low Humidity Alarm", - "value": "a=1&z=24&trigger=HML;" - }, - { - "label": "High Temperature Alarm", - "value": "a=1&z=24&trigger=TSH;" - }, - { - "label": "Low Temperature Alarm", - "value": "a=1&z=24&trigger=TSL;" - } - ], - "status_icons": [], - "icon": "assets/icons/occupancy-sensor.svg", - "statusEx": "1" + "status": "Online", + "status_display": "Online", + "statuses": [], + "status_ex": "", + "actions": [], + "status_icons": [], + "icon": "assets/icons/motioncamera-a.svg", + "schar_obpir_sens": "15", + "schar_obpir_pulse": "2", + "sensitivity": "15", + "model": "L1" + }, + { + "id": "ZB:db5b1a", + "type_tag": "device_type.hue", + "type": "RGB Dimmer", + "name": "Living Room Lamp", + "area": "1", + "zone": "21", + "sort_order": "", + "is_window": "", + "bypass": "0", + "schar_24hr": "0", + "sresp_24hr": "0", + "sresp_mode_0": "0", + "sresp_entry_0": "0", + "sresp_exit_0": "0", + "group_name": "Ungrouped", + "group_id": "1", + "default_group_id": "1", + "sort_id": "10000", + "sresp_mode_1": "0", + "sresp_entry_1": "0", + "sresp_exit_1": "0", + "sresp_mode_2": "0", + "sresp_entry_2": "0", + "sresp_exit_2": "0", + "sresp_mode_3": "0", + "uuid": "741385f4388b2637df4c6b398fe50581", + "sresp_entry_3": "0", + "sresp_exit_3": "0", + "sresp_mode_4": "0", + "sresp_entry_4": "0", + "sresp_exit_4": "0", + "version": "LCT014", + "origin": "abode", + "has_subscription": null, + "onboard": "0", + "s2_grnt_keys": "", + "s2_dsk": "", + "s2_propty": "", + "s2_keys_valid": "", + "zwave_secure_protocol": "", + "control_url": "api/v1/control/light/ZB:db5b1a", + "deep_link": null, + "status_color": "#5cb85c", + "faults": { + "low_battery": 0, + "tempered": 0, + "supervision": 0, + "out_of_order": 0, + "no_response": 0, + "jammed": 0, + "zwave_fault": 0 }, - { - "id": "ZW:0000000b", - "type_tag": "device_type.power_switch_sensor", - "type": "Power Switch Sensor", - "name": "Test Switch", - "area": "1", - "zone": "23", - "sort_order": "", - "is_window": "", - "bypass": "0", - "schar_24hr": "0", - "sresp_24hr": "0", - "sresp_mode_0": "0", - "sresp_entry_0": "0", - "sresp_exit_0": "0", - "group_name": "Lighting", - "group_id": "377075", - "default_group_id": "1", - "sort_id": "7", - "sresp_mode_1": "0", - "sresp_entry_1": "0", - "sresp_exit_1": "0", - "sresp_mode_2": "0", - "sresp_entry_2": "0", - "sresp_exit_2": "0", - "sresp_mode_3": "0", - "uuid": "0012a4d3614cb7e2b8c9abea31d2fb2a", - "sresp_entry_3": "0", - "sresp_exit_3": "0", - "sresp_mode_4": "0", - "sresp_entry_4": "0", - "sresp_exit_4": "0", - "version": "006349523032", - "origin": "abode", - "has_subscription": null, - "onboard": "0", - "s2_grnt_keys": "", - "s2_dsk": "", - "s2_propty": "", - "s2_keys_valid": "", - "zwave_secure_protocol": "", - "control_url": "api/v1/control/power_switch/ZW:0000000b", - "deep_link": null, - "status_color": "#5cb85c", - "faults": { - "low_battery": 0, - "tempered": 0, - "supervision": 0, - "out_of_order": 0, - "no_response": 0, - "jammed": 0, - "zwave_fault": 0 - }, - "status": "Off", - "status_display": "OFF", - "statuses": { - "switch": "0" - }, - "status_ex": "", - "actions": [], - "status_icons": [], - "icon": "assets/icons/plug.svg" + "status": "On", + "status_display": "On", + "statuses": { + "saturation": 100, + "hue": 225, + "level": "79", + "switch": "1", + "color_temp": 3571, + "color_mode": "0" }, - { - "id": "XF:b0c5ba27592a", - "type_tag": "device_type.ipcam", - "type": "IP Cam", - "name": "Test Cam", - "area": "1", - "zone": "1", - "sort_order": "", - "is_window": "", - "bypass": "0", - "schar_24hr": "1", - "sresp_24hr": "5", - "sresp_mode_0": "0", - "sresp_entry_0": "0", - "sresp_exit_0": "0", - "group_name": "Streaming Camera", - "group_id": "397893", - "default_group_id": "1", - "sort_id": "10000", - "sresp_mode_1": "0", - "sresp_entry_1": "0", - "sresp_exit_1": "0", - "sresp_mode_2": "0", - "sresp_entry_2": "0", - "sresp_exit_2": "0", - "sresp_mode_3": "0", - "uuid": "d0a3a1c316891ceb00c20118aae2a133", - "sresp_entry_3": "0", - "sresp_exit_3": "0", - "sresp_mode_4": "0", - "sresp_entry_4": "0", - "sresp_exit_4": "0", - "version": "1.0.2.22G_6.8E_homekit_2.0.9_s2 ABODE oz", - "origin": "abode", - "has_subscription": null, - "onboard": "1", - "s2_grnt_keys": "", - "s2_dsk": "", - "s2_propty": "", - "s2_keys_valid": "", - "zwave_secure_protocol": "", - "control_url": "api/v1/cams/XF:b0c5ba27592a/record", - "deep_link": null, - "status_color": "#5cb85c", - "faults": { - "low_battery": 0, - "tempered": 0, - "supervision": 0, - "out_of_order": 0, - "no_response": 0, - "jammed": 0, - "zwave_fault": 0 - }, - "status": "Online", - "status_display": "Online", - "statuses": [], - "status_ex": "", - "actions": [ - { - "label": "Capture Video", - "value": "a=1&z=1&req=vid;" - }, - { - "label": "Turn off Live Video", - "value": "a=1&z=1&privacy=on;" - }, - { - "label": "Turn on Live Video", - "value": "a=1&z=1&privacy=off;" - } - ], - "status_icons": [], - "icon": "assets/icons/streaming-camera-new.svg", - "control_url_snapshot": "api/v1/cams/XF:b0c5ba27592a/capture", - "ptt_supported": true, - "is_new_camera": 1, - "stream_quality": 2, - "camera_mac": "A0:C1:B2:C3:45:6D", - "privacy": "1", - "enable_audio": "1", - "alarm_video": "25", - "pre_alarm_video": "5", - "mic_volume": "75", - "speaker_volume": "75", - "mic_default_volume": 40, - "speaker_default_volume": 46, - "bandwidth": { - "slider_labels": [ - { - "name": "High", - "value": 3 - }, - { - "name": "Medium", - "value": 2 - }, - { - "name": "Low", - "value": 1 - } - ], - "min": 1, - "max": 3, - "step": 1 - }, - "volume": { - "min": 0, - "max": 100, - "step": 1 - }, - "video_flip": "0", - "hframe": "480P" + "status_ex": "", + "actions": [], + "status_icons": [], + "icon": "assets/icons/bulb-1.svg", + "statusEx": "0" + }, + { + "id": "ZB:db5b1b", + "type_tag": "device_type.hue", + "type": "Dimmer", + "name": "Test Dimmer Only Device", + "area": "1", + "zone": "21", + "sort_order": "", + "is_window": "", + "bypass": "0", + "schar_24hr": "0", + "sresp_24hr": "0", + "sresp_mode_0": "0", + "sresp_entry_0": "0", + "sresp_exit_0": "0", + "group_name": "Ungrouped", + "group_id": "1", + "default_group_id": "1", + "sort_id": "10000", + "sresp_mode_1": "0", + "sresp_entry_1": "0", + "sresp_exit_1": "0", + "sresp_mode_2": "0", + "sresp_entry_2": "0", + "sresp_exit_2": "0", + "sresp_mode_3": "0", + "uuid": "641385f4388b2637df4c6b398fe50581", + "sresp_entry_3": "0", + "sresp_exit_3": "0", + "sresp_mode_4": "0", + "sresp_entry_4": "0", + "sresp_exit_4": "0", + "version": "LCT014", + "origin": "abode", + "has_subscription": null, + "onboard": "0", + "s2_grnt_keys": "", + "s2_dsk": "", + "s2_propty": "", + "s2_keys_valid": "", + "zwave_secure_protocol": "", + "control_url": "api/v1/control/light/ZB:db5b1b", + "deep_link": null, + "status_color": "#5cb85c", + "faults": { + "low_battery": 0, + "tempered": 0, + "supervision": 0, + "out_of_order": 0, + "no_response": 0, + "jammed": 0, + "zwave_fault": 0 }, - { - "id": "ZW:00000004", - "type_tag": "device_type.door_lock", - "type": "Door Lock", - "name": "Test Lock", - "area": "1", - "zone": "16", - "sort_order": "", - "is_window": "", - "bypass": "0", - "schar_24hr": "0", - "sresp_24hr": "0", - "sresp_mode_0": "0", - "sresp_entry_0": "0", - "sresp_exit_0": "0", - "group_name": "Doors/Windows", - "group_id": "377028", - "default_group_id": "1", - "sort_id": "1", - "sresp_mode_1": "0", - "sresp_entry_1": "0", - "sresp_exit_1": "0", - "sresp_mode_2": "0", - "sresp_entry_2": "0", - "sresp_exit_2": "0", - "sresp_mode_3": "0", - "uuid": "51cab3b545d2o34ed7fz02731bda5324", - "sresp_entry_3": "0", - "sresp_exit_3": "0", - "sresp_mode_4": "0", - "sresp_entry_4": "0", - "sresp_exit_4": "0", - "version": "", - "origin": "abode", - "has_subscription": null, - "onboard": "0", - "s2_grnt_keys": "", - "s2_dsk": "", - "s2_propty": "", - "s2_keys_valid": "", - "zwave_secure_protocol": "", - "control_url": "api/v1/control/lock/ZW:00000004", - "deep_link": null, - "status_color": "#5cb85c", - "faults": { - "low_battery": 0, - "tempered": 0, - "supervision": 0, - "out_of_order": 0, - "no_response": 0, - "jammed": 0, - "zwave_fault": 0 - }, - "status": "LockClosed", - "status_display": "LockClosed", - "statuses": { - "open": "0" - }, - "status_ex": "", - "actions": [ - { - "label": "Lock", - "value": "a=1&z=16&sw=on;" - }, - { - "label": "Unlock", - "value": "a=1&z=16&sw=off;" - } - ], - "status_icons": { - "LockOpen": "assets/icons/unlocked-red.svg", - "LockClosed": "assets/icons/locked-green.svg" - }, - "icon": "assets/icons/automation-lock.svg", - "automation_settings": null + "status": "On", + "status_display": "On", + "statuses": { + "saturation": 100, + "hue": 225, + "level": "100", + "switch": "1", + "color_temp": 3571, + "color_mode": "2" }, - { - "id": "ZW:00000007", - "type_tag": "device_type.secure_barrier", - "type": "Secure Barrier", - "name": "Garage Door", - "area": "1", - "zone": "11", - "sort_order": "0", - "is_window": "0", - "bypass": "0", - "schar_24hr": "0", - "sresp_mode_0": "0", - "sresp_entry_0": "0", - "sresp_exit_0": "0", - "sresp_mode_1": "0", - "sresp_entry_1": "0", - "sresp_exit_1": "0", - "sresp_mode_2": "0", - "sresp_entry_2": "0", - "sresp_exit_2": "0", - "sresp_mode_3": "0", - "uuid": "61cbz3b542d2o33ed2fz02721bda3324", - "sresp_entry_3": "0", - "sresp_exit_3": "0", - "capture_mode": null, - "origin": "abode", - "control_url": "api/v1/control/power_switch/ZW:00000007", - "deep_link": null, - "status_color": "#5cb85c", - "faults": { - "low_battery": 0, - "tempered": 0, - "supervision": 0, - "out_of_order": 0, - "no_response": 0 + "status_ex": "", + "actions": [], + "status_icons": [], + "icon": "assets/icons/bulb-1.svg", + "statusEx": "0" + }, + { + "id": "ZB:db5b1c", + "type_tag": "device_type.dimmer", + "type": "Light", + "name": "Test Non-dimmer Device", + "area": "1", + "zone": "21", + "sort_order": "", + "is_window": "", + "bypass": "0", + "schar_24hr": "0", + "sresp_24hr": "0", + "sresp_mode_0": "0", + "sresp_entry_0": "0", + "sresp_exit_0": "0", + "group_name": "Ungrouped", + "group_id": "1", + "default_group_id": "1", + "sort_id": "10000", + "sresp_mode_1": "0", + "sresp_entry_1": "0", + "sresp_exit_1": "0", + "sresp_mode_2": "0", + "sresp_entry_2": "0", + "sresp_exit_2": "0", + "sresp_mode_3": "0", + "uuid": "641385f4388b2637df4c6b398fe50583", + "sresp_entry_3": "0", + "sresp_exit_3": "0", + "sresp_mode_4": "0", + "sresp_entry_4": "0", + "sresp_exit_4": "0", + "version": "LCT014", + "origin": "abode", + "has_subscription": null, + "onboard": "0", + "s2_grnt_keys": "", + "s2_dsk": "", + "s2_propty": "", + "s2_keys_valid": "", + "zwave_secure_protocol": "", + "control_url": "api/v1/control/light/ZB:db5b1c", + "deep_link": null, + "status_color": "#5cb85c", + "faults": { + "low_battery": 0, + "tempered": 0, + "supervision": 0, + "out_of_order": 0, + "no_response": 0, + "jammed": 0, + "zwave_fault": 0 + }, + "status": "On", + "status_display": "On", + "statuses": { + "switch": "1" + }, + "status_ex": "", + "actions": [], + "status_icons": [], + "icon": "assets/icons/bulb-1.svg", + "statusEx": "0" + }, + { + "id": "RF:02148e70", + "type_tag": "device_type.lm", + "type": "LM", + "name": "Environment Sensor", + "area": "1", + "zone": "24", + "sort_order": "", + "is_window": "", + "bypass": "0", + "schar_24hr": "0", + "sresp_24hr": "0", + "sresp_mode_0": "0", + "sresp_entry_0": "0", + "sresp_exit_0": "0", + "group_name": "Ungrouped", + "group_id": "1", + "default_group_id": "1", + "sort_id": "10000", + "sresp_mode_1": "0", + "sresp_entry_1": "0", + "sresp_exit_1": "0", + "sresp_mode_2": "0", + "sresp_entry_2": "0", + "sresp_exit_2": "0", + "sresp_mode_3": "0", + "uuid": "13545b21f4bdcd33d9abd461f8443e65", + "sresp_entry_3": "0", + "sresp_exit_3": "0", + "sresp_mode_4": "0", + "sresp_entry_4": "0", + "sresp_exit_4": "0", + "version": "", + "origin": "abode", + "has_subscription": null, + "onboard": "0", + "s2_grnt_keys": "", + "s2_dsk": "", + "s2_propty": "", + "s2_keys_valid": "", + "zwave_secure_protocol": "", + "control_url": "", + "deep_link": null, + "status_color": "#5cb85c", + "faults": { + "low_battery": 0, + "tempered": 0, + "supervision": 0, + "out_of_order": 0, + "no_response": 0, + "jammed": 0, + "zwave_fault": 0 + }, + "status": "67 \u00b0F", + "status_display": "Online", + "statuses": { + "temperature": "67 \u00b0F", + "temp": "19.5", + "lux": "1 lx", + "humidity": "32 %" + }, + "status_ex": "", + "actions": [ + { + "label": "High Humidity Alarm", + "value": "a=1&z=24&trigger=HMH;" + }, + { + "label": "Low Humidity Alarm", + "value": "a=1&z=24&trigger=HML;" + }, + { + "label": "High Temperature Alarm", + "value": "a=1&z=24&trigger=TSH;" + }, + { + "label": "Low Temperature Alarm", + "value": "a=1&z=24&trigger=TSL;" + } + ], + "status_icons": [], + "icon": "assets/icons/occupancy-sensor.svg", + "statusEx": "1" + }, + { + "id": "ZW:0000000b", + "type_tag": "device_type.power_switch_sensor", + "type": "Power Switch Sensor", + "name": "Test Switch", + "area": "1", + "zone": "23", + "sort_order": "", + "is_window": "", + "bypass": "0", + "schar_24hr": "0", + "sresp_24hr": "0", + "sresp_mode_0": "0", + "sresp_entry_0": "0", + "sresp_exit_0": "0", + "group_name": "Lighting", + "group_id": "377075", + "default_group_id": "1", + "sort_id": "7", + "sresp_mode_1": "0", + "sresp_entry_1": "0", + "sresp_exit_1": "0", + "sresp_mode_2": "0", + "sresp_entry_2": "0", + "sresp_exit_2": "0", + "sresp_mode_3": "0", + "uuid": "0012a4d3614cb7e2b8c9abea31d2fb2a", + "sresp_entry_3": "0", + "sresp_exit_3": "0", + "sresp_mode_4": "0", + "sresp_entry_4": "0", + "sresp_exit_4": "0", + "version": "006349523032", + "origin": "abode", + "has_subscription": null, + "onboard": "0", + "s2_grnt_keys": "", + "s2_dsk": "", + "s2_propty": "", + "s2_keys_valid": "", + "zwave_secure_protocol": "", + "control_url": "api/v1/control/power_switch/ZW:0000000b", + "deep_link": null, + "status_color": "#5cb85c", + "faults": { + "low_battery": 0, + "tempered": 0, + "supervision": 0, + "out_of_order": 0, + "no_response": 0, + "jammed": 0, + "zwave_fault": 0 + }, + "status": "Off", + "status_display": "OFF", + "statuses": { + "switch": "0" + }, + "status_ex": "", + "actions": [], + "status_icons": [], + "icon": "assets/icons/plug.svg" + }, + { + "id": "XF:b0c5ba27592a", + "type_tag": "device_type.ipcam", + "type": "IP Cam", + "name": "Test Cam", + "area": "1", + "zone": "1", + "sort_order": "", + "is_window": "", + "bypass": "0", + "schar_24hr": "1", + "sresp_24hr": "5", + "sresp_mode_0": "0", + "sresp_entry_0": "0", + "sresp_exit_0": "0", + "group_name": "Streaming Camera", + "group_id": "397893", + "default_group_id": "1", + "sort_id": "10000", + "sresp_mode_1": "0", + "sresp_entry_1": "0", + "sresp_exit_1": "0", + "sresp_mode_2": "0", + "sresp_entry_2": "0", + "sresp_exit_2": "0", + "sresp_mode_3": "0", + "uuid": "d0a3a1c316891ceb00c20118aae2a133", + "sresp_entry_3": "0", + "sresp_exit_3": "0", + "sresp_mode_4": "0", + "sresp_entry_4": "0", + "sresp_exit_4": "0", + "version": "1.0.2.22G_6.8E_homekit_2.0.9_s2 ABODE oz", + "origin": "abode", + "has_subscription": null, + "onboard": "1", + "s2_grnt_keys": "", + "s2_dsk": "", + "s2_propty": "", + "s2_keys_valid": "", + "zwave_secure_protocol": "", + "control_url": "api/v1/cams/XF:b0c5ba27592a/record", + "deep_link": null, + "status_color": "#5cb85c", + "faults": { + "low_battery": 0, + "tempered": 0, + "supervision": 0, + "out_of_order": 0, + "no_response": 0, + "jammed": 0, + "zwave_fault": 0 + }, + "status": "Online", + "status_display": "Online", + "statuses": [], + "status_ex": "", + "actions": [ + { + "label": "Capture Video", + "value": "a=1&z=1&req=vid;" + }, + { + "label": "Turn off Live Video", + "value": "a=1&z=1&privacy=on;" + }, + { + "label": "Turn on Live Video", + "value": "a=1&z=1&privacy=off;" + } + ], + "status_icons": [], + "icon": "assets/icons/streaming-camera-new.svg", + "control_url_snapshot": "api/v1/cams/XF:b0c5ba27592a/capture", + "ptt_supported": true, + "is_new_camera": 1, + "stream_quality": 2, + "camera_mac": "A0:C1:B2:C3:45:6D", + "privacy": "1", + "enable_audio": "1", + "alarm_video": "25", + "pre_alarm_video": "5", + "mic_volume": "75", + "speaker_volume": "75", + "mic_default_volume": 40, + "speaker_default_volume": 46, + "bandwidth": { + "slider_labels": [ + { + "name": "High", + "value": 3 }, - "status": "Closed", - "statuses": { - "hvac_mode": null + { + "name": "Medium", + "value": 2 }, - "status_ex": "", - "actions": [ - { - "label": "Close", - "value": "a=1&z=11&sw=off;" - }, - { - "label": "Open", - "value": "a=1&z=11&sw=on;" - } - ], - "status_icons": { - "Open": "assets/icons/garage-door-red.svg", - "Closed": "assets/icons/garage-door-green.svg" - }, - "icon": "assets/icons/garage-door.svg" - } -] \ No newline at end of file + { + "name": "Low", + "value": 1 + } + ], + "min": 1, + "max": 3, + "step": 1 + }, + "volume": { + "min": 0, + "max": 100, + "step": 1 + }, + "video_flip": "0", + "hframe": "480P" + }, + { + "id": "ZW:00000004", + "type_tag": "device_type.door_lock", + "type": "Door Lock", + "name": "Test Lock", + "area": "1", + "zone": "16", + "sort_order": "", + "is_window": "", + "bypass": "0", + "schar_24hr": "0", + "sresp_24hr": "0", + "sresp_mode_0": "0", + "sresp_entry_0": "0", + "sresp_exit_0": "0", + "group_name": "Doors/Windows", + "group_id": "377028", + "default_group_id": "1", + "sort_id": "1", + "sresp_mode_1": "0", + "sresp_entry_1": "0", + "sresp_exit_1": "0", + "sresp_mode_2": "0", + "sresp_entry_2": "0", + "sresp_exit_2": "0", + "sresp_mode_3": "0", + "uuid": "51cab3b545d2o34ed7fz02731bda5324", + "sresp_entry_3": "0", + "sresp_exit_3": "0", + "sresp_mode_4": "0", + "sresp_entry_4": "0", + "sresp_exit_4": "0", + "version": "", + "origin": "abode", + "has_subscription": null, + "onboard": "0", + "s2_grnt_keys": "", + "s2_dsk": "", + "s2_propty": "", + "s2_keys_valid": "", + "zwave_secure_protocol": "", + "control_url": "api/v1/control/lock/ZW:00000004", + "deep_link": null, + "status_color": "#5cb85c", + "faults": { + "low_battery": 0, + "tempered": 0, + "supervision": 0, + "out_of_order": 0, + "no_response": 0, + "jammed": 0, + "zwave_fault": 0 + }, + "status": "LockClosed", + "status_display": "LockClosed", + "statuses": { + "open": "0" + }, + "status_ex": "", + "actions": [ + { + "label": "Lock", + "value": "a=1&z=16&sw=on;" + }, + { + "label": "Unlock", + "value": "a=1&z=16&sw=off;" + } + ], + "status_icons": { + "LockOpen": "assets/icons/unlocked-red.svg", + "LockClosed": "assets/icons/locked-green.svg" + }, + "icon": "assets/icons/automation-lock.svg", + "automation_settings": null + }, + { + "id": "ZW:00000007", + "type_tag": "device_type.secure_barrier", + "type": "Secure Barrier", + "name": "Garage Door", + "area": "1", + "zone": "11", + "sort_order": "0", + "is_window": "0", + "bypass": "0", + "schar_24hr": "0", + "sresp_mode_0": "0", + "sresp_entry_0": "0", + "sresp_exit_0": "0", + "sresp_mode_1": "0", + "sresp_entry_1": "0", + "sresp_exit_1": "0", + "sresp_mode_2": "0", + "sresp_entry_2": "0", + "sresp_exit_2": "0", + "sresp_mode_3": "0", + "uuid": "61cbz3b542d2o33ed2fz02721bda3324", + "sresp_entry_3": "0", + "sresp_exit_3": "0", + "capture_mode": null, + "origin": "abode", + "control_url": "api/v1/control/power_switch/ZW:00000007", + "deep_link": null, + "status_color": "#5cb85c", + "faults": { + "low_battery": 0, + "tempered": 0, + "supervision": 0, + "out_of_order": 0, + "no_response": 0 + }, + "status": "Closed", + "statuses": { + "hvac_mode": null + }, + "status_ex": "", + "actions": [ + { + "label": "Close", + "value": "a=1&z=11&sw=off;" + }, + { + "label": "Open", + "value": "a=1&z=11&sw=on;" + } + ], + "status_icons": { + "Open": "assets/icons/garage-door-red.svg", + "Closed": "assets/icons/garage-door-green.svg" + }, + "icon": "assets/icons/garage-door.svg" + } +] diff --git a/tests/components/abode/fixtures/login.json b/tests/components/abode/fixtures/login.json index fb0ed1fd4ff..633da775a21 100644 --- a/tests/components/abode/fixtures/login.json +++ b/tests/components/abode/fixtures/login.json @@ -1,115 +1,115 @@ { - "token": "web-1eb04ba2236d85f49d4b9b4bb91665f2", - "expired_at": "2017-06-05 00:14:12", - "initiate_screen": "timeline", - "user": { - "id": "user@email.com", - "email": "user@email.com", - "first_name": "John", - "last_name": "Doe", - "phone": "5555551212", - "profile_pic": "https://website.com/default-image.svg", - "address": "555 None St.", - "city": "New York City", - "state": "NY", - "zip": "10108", - "country": "US", - "longitude": "0", - "latitude": "0", - "timezone": "America/New_York_City", - "verified": "1", - "plan": "Basic", - "plan_id": "0", - "plan_active": "1", - "cms_code": "1111", - "cms_active": "0", - "cms_started_at": "", - "cms_expiry": "", - "cms_ondemand": "", - "step": "-1", - "cms_permit_no": "", - "opted_plan_id": "", - "stripe_account": "1", - "plan_effective_from": "", - "agreement": "1", - "associate_users": "1", - "owner": "1" - }, - "panel": { - "version": "ABGW 0.0.2.17F ABGW-L1-XA36J 3.1.2.6.1 Z-Wave 3.95", - "report_account": "5555", - "online": "1", - "initialized": "1", - "net_version": "ABGW 0.0.2.17F", - "rf_version": "ABGW-L1-XA36J", - "zigbee_version": "3.1.2.6.1", - "z_wave_version": "Z-Wave 3.95", - "timezone": "America/New_York", - "ac_fail": "0", - "battery": "1", - "ip": "192.168.1.1", - "jam": "0", - "rssi": "2", - "setup_zone_1": "1", - "setup_zone_2": "1", - "setup_zone_3": "1", - "setup_zone_4": "1", - "setup_zone_5": "1", - "setup_zone_6": "1", - "setup_zone_7": "1", - "setup_zone_8": "1", - "setup_zone_9": "1", - "setup_zone_10": "1", - "setup_gateway": "1", - "setup_contacts": "1", - "setup_billing": "1", - "setup_users": "1", - "is_cellular": "False", - "plan_set_id": "1", - "dealer_id": "0", - "tz_diff": "-04:00", - "is_demo": "0", - "rf51_version": "ABGW-L1-XA36J", - "model": "L1", - "mac": "00:11:22:33:44:55", - "xml_version": "3", - "dealer_name": "abode", - "id": "0", - "dealer_address": "2625 Middlefield Road #900 Palo Alto CA 94306", - "dealer_domain": "https://my.goabode.com", - "domain_alias": "https://test.goabode.com", - "dealer_support_url": "https://support.goabode.com", - "app_launch_url": "https://goabode.app.link/abode", - "has_wifi": "0", - "mode": { - "area_1": "standby", - "area_2": "standby" - } - }, - "permissions": { - "premium_streaming": "0", - "guest_app": "0", - "family_app": "0", - "multiple_accounts": "1", - "google_voice": "1", - "nest": "1", - "alexa": "1", - "ifttt": "1", - "no_associates": "100", - "no_contacts": "2", - "no_devices": "155", - "no_ipcam": "100", - "no_quick_action": "25", - "no_automation": "75", - "media_storage": "3", - "cellular_backup": "0", - "cms_duration": "", - "cms_included": "0" - }, - "integrations": { - "nest": { - "is_connected": 0, - "is_home_selected": 0 - } + "token": "web-1eb04ba2236d85f49d4b9b4bb91665f2", + "expired_at": "2017-06-05 00:14:12", + "initiate_screen": "timeline", + "user": { + "id": "user@email.com", + "email": "user@email.com", + "first_name": "John", + "last_name": "Doe", + "phone": "5555551212", + "profile_pic": "https://website.com/default-image.svg", + "address": "555 None St.", + "city": "New York City", + "state": "NY", + "zip": "10108", + "country": "US", + "longitude": "0", + "latitude": "0", + "timezone": "America/New_York_City", + "verified": "1", + "plan": "Basic", + "plan_id": "0", + "plan_active": "1", + "cms_code": "1111", + "cms_active": "0", + "cms_started_at": "", + "cms_expiry": "", + "cms_ondemand": "", + "step": "-1", + "cms_permit_no": "", + "opted_plan_id": "", + "stripe_account": "1", + "plan_effective_from": "", + "agreement": "1", + "associate_users": "1", + "owner": "1" + }, + "panel": { + "version": "ABGW 0.0.2.17F ABGW-L1-XA36J 3.1.2.6.1 Z-Wave 3.95", + "report_account": "5555", + "online": "1", + "initialized": "1", + "net_version": "ABGW 0.0.2.17F", + "rf_version": "ABGW-L1-XA36J", + "zigbee_version": "3.1.2.6.1", + "z_wave_version": "Z-Wave 3.95", + "timezone": "America/New_York", + "ac_fail": "0", + "battery": "1", + "ip": "192.168.1.1", + "jam": "0", + "rssi": "2", + "setup_zone_1": "1", + "setup_zone_2": "1", + "setup_zone_3": "1", + "setup_zone_4": "1", + "setup_zone_5": "1", + "setup_zone_6": "1", + "setup_zone_7": "1", + "setup_zone_8": "1", + "setup_zone_9": "1", + "setup_zone_10": "1", + "setup_gateway": "1", + "setup_contacts": "1", + "setup_billing": "1", + "setup_users": "1", + "is_cellular": "False", + "plan_set_id": "1", + "dealer_id": "0", + "tz_diff": "-04:00", + "is_demo": "0", + "rf51_version": "ABGW-L1-XA36J", + "model": "L1", + "mac": "00:11:22:33:44:55", + "xml_version": "3", + "dealer_name": "abode", + "id": "0", + "dealer_address": "2625 Middlefield Road #900 Palo Alto CA 94306", + "dealer_domain": "https://my.goabode.com", + "domain_alias": "https://test.goabode.com", + "dealer_support_url": "https://support.goabode.com", + "app_launch_url": "https://goabode.app.link/abode", + "has_wifi": "0", + "mode": { + "area_1": "standby", + "area_2": "standby" } -} \ No newline at end of file + }, + "permissions": { + "premium_streaming": "0", + "guest_app": "0", + "family_app": "0", + "multiple_accounts": "1", + "google_voice": "1", + "nest": "1", + "alexa": "1", + "ifttt": "1", + "no_associates": "100", + "no_contacts": "2", + "no_devices": "155", + "no_ipcam": "100", + "no_quick_action": "25", + "no_automation": "75", + "media_storage": "3", + "cellular_backup": "0", + "cms_duration": "", + "cms_included": "0" + }, + "integrations": { + "nest": { + "is_connected": 0, + "is_home_selected": 0 + } + } +} diff --git a/tests/components/abode/fixtures/logout.json b/tests/components/abode/fixtures/logout.json index 53e242fced3..c34bc70caa8 100644 --- a/tests/components/abode/fixtures/logout.json +++ b/tests/components/abode/fixtures/logout.json @@ -1,4 +1,4 @@ { - "code": 200, - "message": "Logout successful." -} \ No newline at end of file + "code": 200, + "message": "Logout successful." +} diff --git a/tests/components/abode/fixtures/oauth_claims.json b/tests/components/abode/fixtures/oauth_claims.json index 2b313b9aa3e..9706ad78f54 100644 --- a/tests/components/abode/fixtures/oauth_claims.json +++ b/tests/components/abode/fixtures/oauth_claims.json @@ -1,5 +1,5 @@ { - "token_type": "Bearer", - "access_token": "ohyeahthisisanoauthtoken", - "expires_in": 3600 -} \ No newline at end of file + "token_type": "Bearer", + "access_token": "ohyeahthisisanoauthtoken", + "expires_in": 3600 +} diff --git a/tests/components/abode/fixtures/panel.json b/tests/components/abode/fixtures/panel.json index 5a50ffe6fe7..bb77c717ee5 100644 --- a/tests/components/abode/fixtures/panel.json +++ b/tests/components/abode/fixtures/panel.json @@ -1,185 +1,185 @@ { - "version": "Z3 1.0.2.22G_6.8E_homekit_2.0.9_s2 ABODE oz 19200_UITRF1BD_BL.A30.20181117 4.1.2.6.2 Z-Wave 6.02 Bridge controller library", - "report_account": "12345", - "online": "1", - "initialized": "1", - "net_version": "1.0.2.22G_6.8E_homekit_2.0.9_s2 ABODE oz", - "rf_version": "19200_UITRF1BD_BL.A30.20181117", - "zigbee_version": "4.1.2.6.2", - "z_wave_version": "Z-Wave 6.02 Bridge controller library", - "timezone": "America/Los_Angeles", - "ac_fail": "0", - "battery": "0", - "ip": "", - "jam": "0", - "rssi": "1", - "setup_zone_1": "1", - "setup_zone_2": "1", - "setup_zone_3": "1", - "setup_zone_4": "1", - "setup_zone_5": "1", - "setup_zone_6": "1", - "setup_zone_7": "1", - "setup_zone_8": "1", - "setup_zone_9": "1", - "setup_zone_10": "1", - "setup_gateway": "1", - "setup_contacts": "1", - "setup_billing": "1", - "setup_users": "1", - "is_cellular": "0", - "plan_set_id": "7", - "dealer_id": "0", - "tz_diff": "-08:00", - "model": "Z3", - "has_wifi": "1", - "has_s2_support": "1", - "mode": { - "area_1": "standby", - "area_1_label": "Standby", - "area_2": "standby", - "area_2_label": "Standby" - }, - "areas": { - "1": { - "mode": "0", - "modes": { - "0": { - "area": "1", - "mode": "0", - "read_only": "1", - "is_set": "1", - "name": "standby", - "color": null, - "icon_id": null, - "entry1": null, - "entry2": null, - "exit": null, - "icon_path": null - }, - "1": { - "area": "1", - "mode": "1", - "read_only": "1", - "is_set": "1", - "name": "away", - "color": null, - "icon_id": null, - "entry1": "30", - "entry2": "60", - "exit": "30", - "icon_path": null - }, - "2": { - "area": "1", - "mode": "2", - "read_only": "1", - "is_set": "1", - "name": "home", - "color": null, - "icon_id": null, - "entry1": "30", - "entry2": "60", - "exit": "0", - "icon_path": null - }, - "3": { - "area": "1", - "mode": "3", - "read_only": "0", - "is_set": "0", - "name": null, - "color": null, - "icon_id": null, - "entry1": "60", - "entry2": "60", - "exit": "60", - "icon_path": null - }, - "4": { - "area": "1", - "mode": "4", - "read_only": "0", - "is_set": "0", - "name": null, - "color": null, - "icon_id": null, - "entry1": "60", - "entry2": "60", - "exit": "60", - "icon_path": null - } - } - }, - "2": { - "mode": "0", - "modes": { - "0": { - "area": "2", - "mode": "0", - "read_only": "1", - "is_set": "1", - "name": "standby", - "color": null, - "icon_id": null, - "entry1": null, - "entry2": null, - "exit": null, - "icon_path": null - }, - "1": { - "area": "2", - "mode": "1", - "read_only": "1", - "is_set": "1", - "name": "away", - "color": null, - "icon_id": null, - "entry1": "60", - "entry2": "60", - "exit": "60", - "icon_path": null - }, - "2": { - "area": "2", - "mode": "2", - "read_only": "1", - "is_set": "1", - "name": "home", - "color": null, - "icon_id": null, - "entry1": "60", - "entry2": "60", - "exit": "60", - "icon_path": null - }, - "3": { - "area": "2", - "mode": "3", - "read_only": "0", - "is_set": "0", - "name": null, - "color": null, - "icon_id": null, - "entry1": "60", - "entry2": "60", - "exit": "60", - "icon_path": null - }, - "4": { - "area": "2", - "mode": "4", - "read_only": "0", - "is_set": "0", - "name": null, - "color": null, - "icon_id": null, - "entry1": "60", - "entry2": "60", - "exit": "60", - "icon_path": null - } - } + "version": "Z3 1.0.2.22G_6.8E_homekit_2.0.9_s2 ABODE oz 19200_UITRF1BD_BL.A30.20181117 4.1.2.6.2 Z-Wave 6.02 Bridge controller library", + "report_account": "12345", + "online": "1", + "initialized": "1", + "net_version": "1.0.2.22G_6.8E_homekit_2.0.9_s2 ABODE oz", + "rf_version": "19200_UITRF1BD_BL.A30.20181117", + "zigbee_version": "4.1.2.6.2", + "z_wave_version": "Z-Wave 6.02 Bridge controller library", + "timezone": "America/Los_Angeles", + "ac_fail": "0", + "battery": "0", + "ip": "", + "jam": "0", + "rssi": "1", + "setup_zone_1": "1", + "setup_zone_2": "1", + "setup_zone_3": "1", + "setup_zone_4": "1", + "setup_zone_5": "1", + "setup_zone_6": "1", + "setup_zone_7": "1", + "setup_zone_8": "1", + "setup_zone_9": "1", + "setup_zone_10": "1", + "setup_gateway": "1", + "setup_contacts": "1", + "setup_billing": "1", + "setup_users": "1", + "is_cellular": "0", + "plan_set_id": "7", + "dealer_id": "0", + "tz_diff": "-08:00", + "model": "Z3", + "has_wifi": "1", + "has_s2_support": "1", + "mode": { + "area_1": "standby", + "area_1_label": "Standby", + "area_2": "standby", + "area_2_label": "Standby" + }, + "areas": { + "1": { + "mode": "0", + "modes": { + "0": { + "area": "1", + "mode": "0", + "read_only": "1", + "is_set": "1", + "name": "standby", + "color": null, + "icon_id": null, + "entry1": null, + "entry2": null, + "exit": null, + "icon_path": null + }, + "1": { + "area": "1", + "mode": "1", + "read_only": "1", + "is_set": "1", + "name": "away", + "color": null, + "icon_id": null, + "entry1": "30", + "entry2": "60", + "exit": "30", + "icon_path": null + }, + "2": { + "area": "1", + "mode": "2", + "read_only": "1", + "is_set": "1", + "name": "home", + "color": null, + "icon_id": null, + "entry1": "30", + "entry2": "60", + "exit": "0", + "icon_path": null + }, + "3": { + "area": "1", + "mode": "3", + "read_only": "0", + "is_set": "0", + "name": null, + "color": null, + "icon_id": null, + "entry1": "60", + "entry2": "60", + "exit": "60", + "icon_path": null + }, + "4": { + "area": "1", + "mode": "4", + "read_only": "0", + "is_set": "0", + "name": null, + "color": null, + "icon_id": null, + "entry1": "60", + "entry2": "60", + "exit": "60", + "icon_path": null + } } - } -} \ No newline at end of file + }, + "2": { + "mode": "0", + "modes": { + "0": { + "area": "2", + "mode": "0", + "read_only": "1", + "is_set": "1", + "name": "standby", + "color": null, + "icon_id": null, + "entry1": null, + "entry2": null, + "exit": null, + "icon_path": null + }, + "1": { + "area": "2", + "mode": "1", + "read_only": "1", + "is_set": "1", + "name": "away", + "color": null, + "icon_id": null, + "entry1": "60", + "entry2": "60", + "exit": "60", + "icon_path": null + }, + "2": { + "area": "2", + "mode": "2", + "read_only": "1", + "is_set": "1", + "name": "home", + "color": null, + "icon_id": null, + "entry1": "60", + "entry2": "60", + "exit": "60", + "icon_path": null + }, + "3": { + "area": "2", + "mode": "3", + "read_only": "0", + "is_set": "0", + "name": null, + "color": null, + "icon_id": null, + "entry1": "60", + "entry2": "60", + "exit": "60", + "icon_path": null + }, + "4": { + "area": "2", + "mode": "4", + "read_only": "0", + "is_set": "0", + "name": null, + "color": null, + "icon_id": null, + "entry1": "60", + "entry2": "60", + "exit": "60", + "icon_path": null + } + } + } + } +} diff --git a/tests/components/accuweather/fixtures/current_conditions_data.json b/tests/components/accuweather/fixtures/current_conditions_data.json index f94ea071ee9..020345bf8fa 100644 --- a/tests/components/accuweather/fixtures/current_conditions_data.json +++ b/tests/components/accuweather/fixtures/current_conditions_data.json @@ -1,290 +1,290 @@ { - "WeatherIcon": 1, - "HasPrecipitation": false, - "PrecipitationType": null, - "Temperature": { - "Metric": { - "Value": 22.6, - "Unit": "C", - "UnitType": 17 - }, - "Imperial": { - "Value": 73.0, - "Unit": "F", - "UnitType": 18 - } - }, - "RealFeelTemperature": { - "Metric": { - "Value": 25.1, - "Unit": "C", - "UnitType": 17 - }, - "Imperial": { - "Value": 77.0, - "Unit": "F", - "UnitType": 18 - } - }, - "RealFeelTemperatureShade": { - "Metric": { - "Value": 21.1, - "Unit": "C", - "UnitType": 17 - }, - "Imperial": { - "Value": 70.0, - "Unit": "F", - "UnitType": 18 - } - }, - "RelativeHumidity": 67, - "IndoorRelativeHumidity": 67, - "DewPoint": { - "Metric": { - "Value": 16.2, - "Unit": "C", - "UnitType": 17 - }, - "Imperial": { - "Value": 61.0, - "Unit": "F", - "UnitType": 18 - } - }, - "Wind": { - "Direction": { - "Degrees": 180, - "Localized": "S", - "English": "S" - }, - "Speed": { - "Metric": { - "Value": 14.5, - "Unit": "km/h", - "UnitType": 7 - }, - "Imperial": { - "Value": 9.0, - "Unit": "mi/h", - "UnitType": 9 - } - } - }, - "WindGust": { - "Speed": { - "Metric": { - "Value": 20.3, - "Unit": "km/h", - "UnitType": 7 - }, - "Imperial": { - "Value": 12.6, - "Unit": "mi/h", - "UnitType": 9 - } - } - }, - "UVIndex": 6, - "UVIndexText": "High", - "Visibility": { - "Metric": { - "Value": 16.1, - "Unit": "km", - "UnitType": 6 - }, - "Imperial": { - "Value": 10.0, - "Unit": "mi", - "UnitType": 2 - } - }, - "ObstructionsToVisibility": "", - "CloudCover": 10, - "Ceiling": { - "Metric": { - "Value": 3200.0, - "Unit": "m", - "UnitType": 5 - }, - "Imperial": { - "Value": 10500.0, - "Unit": "ft", - "UnitType": 0 - } - }, - "Pressure": { - "Metric": { - "Value": 1012.0, - "Unit": "mb", - "UnitType": 14 - }, - "Imperial": { - "Value": 29.88, - "Unit": "inHg", - "UnitType": 12 - } - }, - "PressureTendency": { - "LocalizedText": "Falling", - "Code": "F" - }, - "Past24HourTemperatureDeparture": { - "Metric": { - "Value": 0.3, - "Unit": "C", - "UnitType": 17 - }, - "Imperial": { - "Value": 0.0, - "Unit": "F", - "UnitType": 18 - } - }, - "ApparentTemperature": { - "Metric": { - "Value": 22.8, - "Unit": "C", - "UnitType": 17 - }, - "Imperial": { - "Value": 73.0, - "Unit": "F", - "UnitType": 18 - } - }, - "WindChillTemperature": { - "Metric": { - "Value": 22.8, - "Unit": "C", - "UnitType": 17 - }, - "Imperial": { - "Value": 73.0, - "Unit": "F", - "UnitType": 18 - } - }, - "WetBulbTemperature": { - "Metric": { - "Value": 18.6, - "Unit": "C", - "UnitType": 17 - }, - "Imperial": { - "Value": 65.0, - "Unit": "F", - "UnitType": 18 - } - }, - "Precip1hr": { - "Metric": { - "Value": 0.0, - "Unit": "mm", - "UnitType": 3 - }, - "Imperial": { - "Value": 0.0, - "Unit": "in", - "UnitType": 1 - } - }, - "PrecipitationSummary": { - "Precipitation": { - "Metric": { - "Value": 0.0, - "Unit": "mm", - "UnitType": 3 - }, - "Imperial": { - "Value": 0.0, - "Unit": "in", - "UnitType": 1 - } - }, - "PastHour": { - "Metric": { - "Value": 0.0, - "Unit": "mm", - "UnitType": 3 - }, - "Imperial": { - "Value": 0.0, - "Unit": "in", - "UnitType": 1 - } - }, - "Past3Hours": { - "Metric": { - "Value": 1.3, - "Unit": "mm", - "UnitType": 3 - }, - "Imperial": { - "Value": 0.05, - "Unit": "in", - "UnitType": 1 - } - }, - "Past6Hours": { - "Metric": { - "Value": 1.3, - "Unit": "mm", - "UnitType": 3 - }, - "Imperial": { - "Value": 0.05, - "Unit": "in", - "UnitType": 1 - } - }, - "Past9Hours": { - "Metric": { - "Value": 2.5, - "Unit": "mm", - "UnitType": 3 - }, - "Imperial": { - "Value": 0.1, - "Unit": "in", - "UnitType": 1 - } - }, - "Past12Hours": { - "Metric": { - "Value": 3.8, - "Unit": "mm", - "UnitType": 3 - }, - "Imperial": { - "Value": 0.15, - "Unit": "in", - "UnitType": 1 - } - }, - "Past18Hours": { - "Metric": { - "Value": 5.1, - "Unit": "mm", - "UnitType": 3 - }, - "Imperial": { - "Value": 0.2, - "Unit": "in", - "UnitType": 1 - } - }, - "Past24Hours": { - "Metric": { - "Value": 7.6, - "Unit": "mm", - "UnitType": 3 - }, - "Imperial": { - "Value": 0.3, - "Unit": "in", - "UnitType": 1 - } - } - } -} \ No newline at end of file + "WeatherIcon": 1, + "HasPrecipitation": false, + "PrecipitationType": null, + "Temperature": { + "Metric": { + "Value": 22.6, + "Unit": "C", + "UnitType": 17 + }, + "Imperial": { + "Value": 73.0, + "Unit": "F", + "UnitType": 18 + } + }, + "RealFeelTemperature": { + "Metric": { + "Value": 25.1, + "Unit": "C", + "UnitType": 17 + }, + "Imperial": { + "Value": 77.0, + "Unit": "F", + "UnitType": 18 + } + }, + "RealFeelTemperatureShade": { + "Metric": { + "Value": 21.1, + "Unit": "C", + "UnitType": 17 + }, + "Imperial": { + "Value": 70.0, + "Unit": "F", + "UnitType": 18 + } + }, + "RelativeHumidity": 67, + "IndoorRelativeHumidity": 67, + "DewPoint": { + "Metric": { + "Value": 16.2, + "Unit": "C", + "UnitType": 17 + }, + "Imperial": { + "Value": 61.0, + "Unit": "F", + "UnitType": 18 + } + }, + "Wind": { + "Direction": { + "Degrees": 180, + "Localized": "S", + "English": "S" + }, + "Speed": { + "Metric": { + "Value": 14.5, + "Unit": "km/h", + "UnitType": 7 + }, + "Imperial": { + "Value": 9.0, + "Unit": "mi/h", + "UnitType": 9 + } + } + }, + "WindGust": { + "Speed": { + "Metric": { + "Value": 20.3, + "Unit": "km/h", + "UnitType": 7 + }, + "Imperial": { + "Value": 12.6, + "Unit": "mi/h", + "UnitType": 9 + } + } + }, + "UVIndex": 6, + "UVIndexText": "High", + "Visibility": { + "Metric": { + "Value": 16.1, + "Unit": "km", + "UnitType": 6 + }, + "Imperial": { + "Value": 10.0, + "Unit": "mi", + "UnitType": 2 + } + }, + "ObstructionsToVisibility": "", + "CloudCover": 10, + "Ceiling": { + "Metric": { + "Value": 3200.0, + "Unit": "m", + "UnitType": 5 + }, + "Imperial": { + "Value": 10500.0, + "Unit": "ft", + "UnitType": 0 + } + }, + "Pressure": { + "Metric": { + "Value": 1012.0, + "Unit": "mb", + "UnitType": 14 + }, + "Imperial": { + "Value": 29.88, + "Unit": "inHg", + "UnitType": 12 + } + }, + "PressureTendency": { + "LocalizedText": "Falling", + "Code": "F" + }, + "Past24HourTemperatureDeparture": { + "Metric": { + "Value": 0.3, + "Unit": "C", + "UnitType": 17 + }, + "Imperial": { + "Value": 0.0, + "Unit": "F", + "UnitType": 18 + } + }, + "ApparentTemperature": { + "Metric": { + "Value": 22.8, + "Unit": "C", + "UnitType": 17 + }, + "Imperial": { + "Value": 73.0, + "Unit": "F", + "UnitType": 18 + } + }, + "WindChillTemperature": { + "Metric": { + "Value": 22.8, + "Unit": "C", + "UnitType": 17 + }, + "Imperial": { + "Value": 73.0, + "Unit": "F", + "UnitType": 18 + } + }, + "WetBulbTemperature": { + "Metric": { + "Value": 18.6, + "Unit": "C", + "UnitType": 17 + }, + "Imperial": { + "Value": 65.0, + "Unit": "F", + "UnitType": 18 + } + }, + "Precip1hr": { + "Metric": { + "Value": 0.0, + "Unit": "mm", + "UnitType": 3 + }, + "Imperial": { + "Value": 0.0, + "Unit": "in", + "UnitType": 1 + } + }, + "PrecipitationSummary": { + "Precipitation": { + "Metric": { + "Value": 0.0, + "Unit": "mm", + "UnitType": 3 + }, + "Imperial": { + "Value": 0.0, + "Unit": "in", + "UnitType": 1 + } + }, + "PastHour": { + "Metric": { + "Value": 0.0, + "Unit": "mm", + "UnitType": 3 + }, + "Imperial": { + "Value": 0.0, + "Unit": "in", + "UnitType": 1 + } + }, + "Past3Hours": { + "Metric": { + "Value": 1.3, + "Unit": "mm", + "UnitType": 3 + }, + "Imperial": { + "Value": 0.05, + "Unit": "in", + "UnitType": 1 + } + }, + "Past6Hours": { + "Metric": { + "Value": 1.3, + "Unit": "mm", + "UnitType": 3 + }, + "Imperial": { + "Value": 0.05, + "Unit": "in", + "UnitType": 1 + } + }, + "Past9Hours": { + "Metric": { + "Value": 2.5, + "Unit": "mm", + "UnitType": 3 + }, + "Imperial": { + "Value": 0.1, + "Unit": "in", + "UnitType": 1 + } + }, + "Past12Hours": { + "Metric": { + "Value": 3.8, + "Unit": "mm", + "UnitType": 3 + }, + "Imperial": { + "Value": 0.15, + "Unit": "in", + "UnitType": 1 + } + }, + "Past18Hours": { + "Metric": { + "Value": 5.1, + "Unit": "mm", + "UnitType": 3 + }, + "Imperial": { + "Value": 0.2, + "Unit": "in", + "UnitType": 1 + } + }, + "Past24Hours": { + "Metric": { + "Value": 7.6, + "Unit": "mm", + "UnitType": 3 + }, + "Imperial": { + "Value": 0.3, + "Unit": "in", + "UnitType": 1 + } + } + } +} diff --git a/tests/components/accuweather/fixtures/forecast_data.json b/tests/components/accuweather/fixtures/forecast_data.json index 2de06dc66f4..70b7bd1181b 100644 --- a/tests/components/accuweather/fixtures/forecast_data.json +++ b/tests/components/accuweather/fixtures/forecast_data.json @@ -1,981 +1,981 @@ [ - { - "Date": "2020-07-26T07:00:00+02:00", - "EpochDate": 1595739600, - "HoursOfSun": 7.2, - "DegreeDaySummary": { - "Heating": { - "Value": 0.0, - "Unit": "C", - "UnitType": 17 - }, - "Cooling": { - "Value": 4.0, - "Unit": "C", - "UnitType": 17 - } - }, - "Ozone": { - "Value": 32, - "Category": "Good", - "CategoryValue": 1 - }, - "Grass": { - "Value": 0, - "Category": "Low", - "CategoryValue": 1 - }, - "Mold": { - "Value": 0, - "Category": "Low", - "CategoryValue": 1 - }, - "Ragweed": { - "Value": 0, - "Category": "Low", - "CategoryValue": 1 - }, - "Tree": { - "Value": 0, - "Category": "Low", - "CategoryValue": 1 - }, - "UVIndex": { - "Value": 5, - "Category": "Moderate", - "CategoryValue": 2 - }, - "TemperatureMin": { - "Value": 15.4, - "Unit": "C", - "UnitType": 17 - }, - "TemperatureMax": { - "Value": 29.5, - "Unit": "C", - "UnitType": 17 - }, - "RealFeelTemperatureMin": { - "Value": 15.1, - "Unit": "C", - "UnitType": 17 - }, - "RealFeelTemperatureMax": { - "Value": 29.8, - "Unit": "C", - "UnitType": 17 - }, - "RealFeelTemperatureShadeMin": { - "Value": 15.1, - "Unit": "C", - "UnitType": 17 - }, - "RealFeelTemperatureShadeMax": { - "Value": 28.0, - "Unit": "C", - "UnitType": 17 - }, - "IconDay": 17, - "IconPhraseDay": "Partly sunny w/ t-storms", - "HasPrecipitationDay": true, - "PrecipitationTypeDay": "Rain", - "PrecipitationIntensityDay": "Moderate", - "ShortPhraseDay": "A shower and t-storm around", - "LongPhraseDay": "Clouds and sunshine with a couple of showers and a thunderstorm around late this afternoon", - "PrecipitationProbabilityDay": 60, - "ThunderstormProbabilityDay": 40, - "RainProbabilityDay": 60, - "SnowProbabilityDay": 0, - "IceProbabilityDay": 0, - "WindDay": { - "Speed": { - "Value": 13.0, - "Unit": "km/h", - "UnitType": 7 - }, - "Direction": { - "Degrees": 166, - "Localized": "SSE", - "English": "SSE" - } - }, - "WindGustDay": { - "Speed": { - "Value": 29.6, - "Unit": "km/h", - "UnitType": 7 - }, - "Direction": { - "Degrees": 178, - "Localized": "S", - "English": "S" - } - }, - "TotalLiquidDay": { - "Value": 2.5, - "Unit": "mm", - "UnitType": 3 - }, - "RainDay": { - "Value": 2.5, - "Unit": "mm", - "UnitType": 3 - }, - "SnowDay": { - "Value": 0.0, - "Unit": "cm", - "UnitType": 4 - }, - "IceDay": { - "Value": 0.0, - "Unit": "mm", - "UnitType": 3 - }, - "HoursOfPrecipitationDay": 1.0, - "HoursOfRainDay": 1.0, - "HoursOfSnowDay": 0.0, - "HoursOfIceDay": 0.0, - "CloudCoverDay": 58, - "IconNight": 41, - "IconPhraseNight": "Partly cloudy w/ t-storms", - "HasPrecipitationNight": true, - "PrecipitationTypeNight": "Rain", - "PrecipitationIntensityNight": "Moderate", - "ShortPhraseNight": "Partly cloudy", - "LongPhraseNight": "Partly cloudy", - "PrecipitationProbabilityNight": 57, - "ThunderstormProbabilityNight": 40, - "RainProbabilityNight": 57, - "SnowProbabilityNight": 0, - "IceProbabilityNight": 0, - "WindNight": { - "Speed": { - "Value": 7.4, - "Unit": "km/h", - "UnitType": 7 - }, - "Direction": { - "Degrees": 289, - "Localized": "WNW", - "English": "WNW" - } - }, - "WindGustNight": { - "Speed": { - "Value": 18.5, - "Unit": "km/h", - "UnitType": 7 - }, - "Direction": { - "Degrees": 256, - "Localized": "WSW", - "English": "WSW" - } - }, - "TotalLiquidNight": { - "Value": 2.3, - "Unit": "mm", - "UnitType": 3 - }, - "RainNight": { - "Value": 2.3, - "Unit": "mm", - "UnitType": 3 - }, - "SnowNight": { - "Value": 0.0, - "Unit": "cm", - "UnitType": 4 - }, - "IceNight": { - "Value": 0.0, - "Unit": "mm", - "UnitType": 3 - }, - "HoursOfPrecipitationNight": 1.0, - "HoursOfRainNight": 1.0, - "HoursOfSnowNight": 0.0, - "HoursOfIceNight": 0.0, - "CloudCoverNight": 65 + { + "Date": "2020-07-26T07:00:00+02:00", + "EpochDate": 1595739600, + "HoursOfSun": 7.2, + "DegreeDaySummary": { + "Heating": { + "Value": 0.0, + "Unit": "C", + "UnitType": 17 + }, + "Cooling": { + "Value": 4.0, + "Unit": "C", + "UnitType": 17 + } }, - { - "Date": "2020-07-27T07:00:00+02:00", - "EpochDate": 1595826000, - "HoursOfSun": 7.4, - "DegreeDaySummary": { - "Heating": { - "Value": 0.0, - "Unit": "C", - "UnitType": 17 - }, - "Cooling": { - "Value": 3.0, - "Unit": "C", - "UnitType": 17 - } - }, - "Ozone": { - "Value": 39, - "Category": "Good", - "CategoryValue": 1 - }, - "Grass": { - "Value": 0, - "Category": "Low", - "CategoryValue": 1 - }, - "Mold": { - "Value": 0, - "Category": "Low", - "CategoryValue": 1 - }, - "Ragweed": { - "Value": 0, - "Category": "Low", - "CategoryValue": 1 - }, - "Tree": { - "Value": 0, - "Category": "Low", - "CategoryValue": 1 - }, - "UVIndex": { - "Value": 7, - "Category": "High", - "CategoryValue": 3 - }, - "TemperatureMin": { - "Value": 15.9, - "Unit": "C", - "UnitType": 17 - }, - "TemperatureMax": { - "Value": 26.2, - "Unit": "C", - "UnitType": 17 - }, - "RealFeelTemperatureMin": { - "Value": 15.8, - "Unit": "C", - "UnitType": 17 - }, - "RealFeelTemperatureMax": { - "Value": 28.9, - "Unit": "C", - "UnitType": 17 - }, - "RealFeelTemperatureShadeMin": { - "Value": 15.8, - "Unit": "C", - "UnitType": 17 - }, - "RealFeelTemperatureShadeMax": { - "Value": 25.0, - "Unit": "C", - "UnitType": 17 - }, - "IconDay": 4, - "IconPhraseDay": "Intermittent clouds", - "HasPrecipitationDay": false, - "ShortPhraseDay": "Clouds and sun", - "LongPhraseDay": "Clouds and sun", - "PrecipitationProbabilityDay": 25, - "ThunderstormProbabilityDay": 24, - "RainProbabilityDay": 25, - "SnowProbabilityDay": 0, - "IceProbabilityDay": 0, - "WindDay": { - "Speed": { - "Value": 9.3, - "Unit": "km/h", - "UnitType": 7 - }, - "Direction": { - "Degrees": 297, - "Localized": "WNW", - "English": "WNW" - } - }, - "WindGustDay": { - "Speed": { - "Value": 14.8, - "Unit": "km/h", - "UnitType": 7 - }, - "Direction": { - "Degrees": 317, - "Localized": "NW", - "English": "NW" - } - }, - "TotalLiquidDay": { - "Value": 0.0, - "Unit": "mm", - "UnitType": 3 - }, - "RainDay": { - "Value": 0.0, - "Unit": "mm", - "UnitType": 3 - }, - "SnowDay": { - "Value": 0.0, - "Unit": "cm", - "UnitType": 4 - }, - "IceDay": { - "Value": 0.0, - "Unit": "mm", - "UnitType": 3 - }, - "HoursOfPrecipitationDay": 0.0, - "HoursOfRainDay": 0.0, - "HoursOfSnowDay": 0.0, - "HoursOfIceDay": 0.0, - "CloudCoverDay": 52, - "IconNight": 36, - "IconPhraseNight": "Intermittent clouds", - "HasPrecipitationNight": false, - "ShortPhraseNight": "Partly cloudy", - "LongPhraseNight": "Partly cloudy", - "PrecipitationProbabilityNight": 6, - "ThunderstormProbabilityNight": 0, - "RainProbabilityNight": 6, - "SnowProbabilityNight": 0, - "IceProbabilityNight": 0, - "WindNight": { - "Speed": { - "Value": 7.4, - "Unit": "km/h", - "UnitType": 7 - }, - "Direction": { - "Degrees": 162, - "Localized": "SSE", - "English": "SSE" - } - }, - "WindGustNight": { - "Speed": { - "Value": 14.8, - "Unit": "km/h", - "UnitType": 7 - }, - "Direction": { - "Degrees": 175, - "Localized": "S", - "English": "S" - } - }, - "TotalLiquidNight": { - "Value": 0.0, - "Unit": "mm", - "UnitType": 3 - }, - "RainNight": { - "Value": 0.0, - "Unit": "mm", - "UnitType": 3 - }, - "SnowNight": { - "Value": 0.0, - "Unit": "cm", - "UnitType": 4 - }, - "IceNight": { - "Value": 0.0, - "Unit": "mm", - "UnitType": 3 - }, - "HoursOfPrecipitationNight": 0.0, - "HoursOfRainNight": 0.0, - "HoursOfSnowNight": 0.0, - "HoursOfIceNight": 0.0, - "CloudCoverNight": 63 + "Ozone": { + "Value": 32, + "Category": "Good", + "CategoryValue": 1 }, - { - "Date": "2020-07-28T07:00:00+02:00", - "EpochDate": 1595912400, - "HoursOfSun": 5.7, - "DegreeDaySummary": { - "Heating": { - "Value": 0.0, - "Unit": "C", - "UnitType": 17 - }, - "Cooling": { - "Value": 6.0, - "Unit": "C", - "UnitType": 17 - } - }, - "Ozone": { - "Value": 29, - "Category": "Good", - "CategoryValue": 1 - }, - "Grass": { - "Value": 0, - "Category": "Low", - "CategoryValue": 1 - }, - "Mold": { - "Value": 0, - "Category": "Low", - "CategoryValue": 1 - }, - "Ragweed": { - "Value": 0, - "Category": "Low", - "CategoryValue": 1 - }, - "Tree": { - "Value": 0, - "Category": "Low", - "CategoryValue": 1 - }, - "UVIndex": { - "Value": 7, - "Category": "High", - "CategoryValue": 3 - }, - "TemperatureMin": { - "Value": 16.8, - "Unit": "C", - "UnitType": 17 - }, - "TemperatureMax": { - "Value": 31.7, - "Unit": "C", - "UnitType": 17 - }, - "RealFeelTemperatureMin": { - "Value": 16.7, - "Unit": "C", - "UnitType": 17 - }, - "RealFeelTemperatureMax": { - "Value": 31.6, - "Unit": "C", - "UnitType": 17 - }, - "RealFeelTemperatureShadeMin": { - "Value": 16.7, - "Unit": "C", - "UnitType": 17 - }, - "RealFeelTemperatureShadeMax": { - "Value": 30.0, - "Unit": "C", - "UnitType": 17 - }, - "IconDay": 4, - "IconPhraseDay": "Intermittent clouds", - "HasPrecipitationDay": false, - "ShortPhraseDay": "Partly sunny and very warm", - "LongPhraseDay": "Very warm with a blend of sun and clouds", - "PrecipitationProbabilityDay": 10, - "ThunderstormProbabilityDay": 4, - "RainProbabilityDay": 10, - "SnowProbabilityDay": 0, - "IceProbabilityDay": 0, - "WindDay": { - "Speed": { - "Value": 16.7, - "Unit": "km/h", - "UnitType": 7 - }, - "Direction": { - "Degrees": 198, - "Localized": "SSW", - "English": "SSW" - } - }, - "WindGustDay": { - "Speed": { - "Value": 24.1, - "Unit": "km/h", - "UnitType": 7 - }, - "Direction": { - "Degrees": 198, - "Localized": "SSW", - "English": "SSW" - } - }, - "TotalLiquidDay": { - "Value": 0.0, - "Unit": "mm", - "UnitType": 3 - }, - "RainDay": { - "Value": 0.0, - "Unit": "mm", - "UnitType": 3 - }, - "SnowDay": { - "Value": 0.0, - "Unit": "cm", - "UnitType": 4 - }, - "IceDay": { - "Value": 0.0, - "Unit": "mm", - "UnitType": 3 - }, - "HoursOfPrecipitationDay": 0.0, - "HoursOfRainDay": 0.0, - "HoursOfSnowDay": 0.0, - "HoursOfIceDay": 0.0, - "CloudCoverDay": 65, - "IconNight": 36, - "IconPhraseNight": "Intermittent clouds", - "HasPrecipitationNight": false, - "ShortPhraseNight": "Partly cloudy", - "LongPhraseNight": "Partly cloudy", - "PrecipitationProbabilityNight": 25, - "ThunderstormProbabilityNight": 24, - "RainProbabilityNight": 25, - "SnowProbabilityNight": 0, - "IceProbabilityNight": 0, - "WindNight": { - "Speed": { - "Value": 9.3, - "Unit": "km/h", - "UnitType": 7 - }, - "Direction": { - "Degrees": 265, - "Localized": "W", - "English": "W" - } - }, - "WindGustNight": { - "Speed": { - "Value": 22.2, - "Unit": "km/h", - "UnitType": 7 - }, - "Direction": { - "Degrees": 271, - "Localized": "W", - "English": "W" - } - }, - "TotalLiquidNight": { - "Value": 0.0, - "Unit": "mm", - "UnitType": 3 - }, - "RainNight": { - "Value": 0.0, - "Unit": "mm", - "UnitType": 3 - }, - "SnowNight": { - "Value": 0.0, - "Unit": "cm", - "UnitType": 4 - }, - "IceNight": { - "Value": 0.0, - "Unit": "mm", - "UnitType": 3 - }, - "HoursOfPrecipitationNight": 0.0, - "HoursOfRainNight": 0.0, - "HoursOfSnowNight": 0.0, - "HoursOfIceNight": 0.0, - "CloudCoverNight": 53 + "Grass": { + "Value": 0, + "Category": "Low", + "CategoryValue": 1 }, - { - "Date": "2020-07-29T07:00:00+02:00", - "EpochDate": 1595998800, - "HoursOfSun": 9.4, - "DegreeDaySummary": { - "Heating": { - "Value": 0.0, - "Unit": "C", - "UnitType": 17 - }, - "Cooling": { - "Value": 0.0, - "Unit": "C", - "UnitType": 17 - } - }, - "Ozone": { - "Value": 18, - "Category": "Good", - "CategoryValue": 1 - }, - "Grass": { - "Value": 0, - "Category": "Low", - "CategoryValue": 1 - }, - "Mold": { - "Value": 0, - "Category": "Low", - "CategoryValue": 1 - }, - "Ragweed": { - "Value": 0, - "Category": "Low", - "CategoryValue": 1 - }, - "Tree": { - "Value": 0, - "Category": "Low", - "CategoryValue": 1 - }, - "UVIndex": { - "Value": 6, - "Category": "High", - "CategoryValue": 3 - }, - "TemperatureMin": { - "Value": 11.7, - "Unit": "C", - "UnitType": 17 - }, - "TemperatureMax": { - "Value": 24.0, - "Unit": "C", - "UnitType": 17 - }, - "RealFeelTemperatureMin": { - "Value": 10.1, - "Unit": "C", - "UnitType": 17 - }, - "RealFeelTemperatureMax": { - "Value": 26.5, - "Unit": "C", - "UnitType": 17 - }, - "RealFeelTemperatureShadeMin": { - "Value": 10.1, - "Unit": "C", - "UnitType": 17 - }, - "RealFeelTemperatureShadeMax": { - "Value": 22.5, - "Unit": "C", - "UnitType": 17 - }, - "IconDay": 3, - "IconPhraseDay": "Partly sunny", - "HasPrecipitationDay": false, - "ShortPhraseDay": "Cooler with partial sunshine", - "LongPhraseDay": "Cooler with partial sunshine", - "PrecipitationProbabilityDay": 9, - "ThunderstormProbabilityDay": 0, - "RainProbabilityDay": 9, - "SnowProbabilityDay": 0, - "IceProbabilityDay": 0, - "WindDay": { - "Speed": { - "Value": 13.0, - "Unit": "km/h", - "UnitType": 7 - }, - "Direction": { - "Degrees": 293, - "Localized": "WNW", - "English": "WNW" - } - }, - "WindGustDay": { - "Speed": { - "Value": 24.1, - "Unit": "km/h", - "UnitType": 7 - }, - "Direction": { - "Degrees": 271, - "Localized": "W", - "English": "W" - } - }, - "TotalLiquidDay": { - "Value": 0.0, - "Unit": "mm", - "UnitType": 3 - }, - "RainDay": { - "Value": 0.0, - "Unit": "mm", - "UnitType": 3 - }, - "SnowDay": { - "Value": 0.0, - "Unit": "cm", - "UnitType": 4 - }, - "IceDay": { - "Value": 0.0, - "Unit": "mm", - "UnitType": 3 - }, - "HoursOfPrecipitationDay": 0.0, - "HoursOfRainDay": 0.0, - "HoursOfSnowDay": 0.0, - "HoursOfIceDay": 0.0, - "CloudCoverDay": 45, - "IconNight": 34, - "IconPhraseNight": "Mostly clear", - "HasPrecipitationNight": false, - "ShortPhraseNight": "Mainly clear", - "LongPhraseNight": "Mainly clear", - "PrecipitationProbabilityNight": 1, - "ThunderstormProbabilityNight": 0, - "RainProbabilityNight": 1, - "SnowProbabilityNight": 0, - "IceProbabilityNight": 0, - "WindNight": { - "Speed": { - "Value": 11.1, - "Unit": "km/h", - "UnitType": 7 - }, - "Direction": { - "Degrees": 264, - "Localized": "W", - "English": "W" - } - }, - "WindGustNight": { - "Speed": { - "Value": 18.5, - "Unit": "km/h", - "UnitType": 7 - }, - "Direction": { - "Degrees": 266, - "Localized": "W", - "English": "W" - } - }, - "TotalLiquidNight": { - "Value": 0.0, - "Unit": "mm", - "UnitType": 3 - }, - "RainNight": { - "Value": 0.0, - "Unit": "mm", - "UnitType": 3 - }, - "SnowNight": { - "Value": 0.0, - "Unit": "cm", - "UnitType": 4 - }, - "IceNight": { - "Value": 0.0, - "Unit": "mm", - "UnitType": 3 - }, - "HoursOfPrecipitationNight": 0.0, - "HoursOfRainNight": 0.0, - "HoursOfSnowNight": 0.0, - "HoursOfIceNight": 0.0, - "CloudCoverNight": 27 + "Mold": { + "Value": 0, + "Category": "Low", + "CategoryValue": 1 }, - { - "Date": "2020-07-30T07:00:00+02:00", - "EpochDate": 1596085200, - "HoursOfSun": 9.2, - "DegreeDaySummary": { - "Heating": { - "Value": 1.0, - "Unit": "C", - "UnitType": 17 - }, - "Cooling": { - "Value": 0.0, - "Unit": "C", - "UnitType": 17 - } - }, - "Ozone": { - "Value": 14, - "Category": "Good", - "CategoryValue": 1 - }, - "Grass": { - "Value": 0, - "Category": "Low", - "CategoryValue": 1 - }, - "Mold": { - "Value": 0, - "Category": "Low", - "CategoryValue": 1 - }, - "Ragweed": { - "Value": 0, - "Category": "Low", - "CategoryValue": 1 - }, - "Tree": { - "Value": 0, - "Category": "Low", - "CategoryValue": 1 - }, - "UVIndex": { - "Value": 7, - "Category": "High", - "CategoryValue": 3 - }, - "TemperatureMin": { - "Value": 12.2, - "Unit": "C", - "UnitType": 17 - }, - "TemperatureMax": { - "Value": 21.4, - "Unit": "C", - "UnitType": 17 - }, - "RealFeelTemperatureMin": { - "Value": 11.3, - "Unit": "C", - "UnitType": 17 - }, - "RealFeelTemperatureMax": { - "Value": 22.2, - "Unit": "C", - "UnitType": 17 - }, - "RealFeelTemperatureShadeMin": { - "Value": 11.3, - "Unit": "C", - "UnitType": 17 - }, - "RealFeelTemperatureShadeMax": { - "Value": 19.5, - "Unit": "C", - "UnitType": 17 - }, - "IconDay": 4, - "IconPhraseDay": "Intermittent clouds", - "HasPrecipitationDay": false, - "ShortPhraseDay": "Clouds and sun", - "LongPhraseDay": "Intervals of clouds and sunshine", - "PrecipitationProbabilityDay": 1, - "ThunderstormProbabilityDay": 0, - "RainProbabilityDay": 1, - "SnowProbabilityDay": 0, - "IceProbabilityDay": 0, - "WindDay": { - "Speed": { - "Value": 18.5, - "Unit": "km/h", - "UnitType": 7 - }, - "Direction": { - "Degrees": 280, - "Localized": "W", - "English": "W" - } - }, - "WindGustDay": { - "Speed": { - "Value": 27.8, - "Unit": "km/h", - "UnitType": 7 - }, - "Direction": { - "Degrees": 273, - "Localized": "W", - "English": "W" - } - }, - "TotalLiquidDay": { - "Value": 0.0, - "Unit": "mm", - "UnitType": 3 - }, - "RainDay": { - "Value": 0.0, - "Unit": "mm", - "UnitType": 3 - }, - "SnowDay": { - "Value": 0.0, - "Unit": "cm", - "UnitType": 4 - }, - "IceDay": { - "Value": 0.0, - "Unit": "mm", - "UnitType": 3 - }, - "HoursOfPrecipitationDay": 0.0, - "HoursOfRainDay": 0.0, - "HoursOfSnowDay": 0.0, - "HoursOfIceDay": 0.0, - "CloudCoverDay": 50, - "IconNight": 34, - "IconPhraseNight": "Mostly clear", - "HasPrecipitationNight": false, - "ShortPhraseNight": "Mostly clear", - "LongPhraseNight": "Mostly clear", - "PrecipitationProbabilityNight": 3, - "ThunderstormProbabilityNight": 0, - "RainProbabilityNight": 3, - "SnowProbabilityNight": 0, - "IceProbabilityNight": 0, - "WindNight": { - "Speed": { - "Value": 9.3, - "Unit": "km/h", - "UnitType": 7 - }, - "Direction": { - "Degrees": 272, - "Localized": "W", - "English": "W" - } - }, - "WindGustNight": { - "Speed": { - "Value": 18.5, - "Unit": "km/h", - "UnitType": 7 - }, - "Direction": { - "Degrees": 274, - "Localized": "W", - "English": "W" - } - }, - "TotalLiquidNight": { - "Value": 0.0, - "Unit": "mm", - "UnitType": 3 - }, - "RainNight": { - "Value": 0.0, - "Unit": "mm", - "UnitType": 3 - }, - "SnowNight": { - "Value": 0.0, - "Unit": "cm", - "UnitType": 4 - }, - "IceNight": { - "Value": 0.0, - "Unit": "mm", - "UnitType": 3 - }, - "HoursOfPrecipitationNight": 0.0, - "HoursOfRainNight": 0.0, - "HoursOfSnowNight": 0.0, - "HoursOfIceNight": 0.0, - "CloudCoverNight": 13 - } -] \ No newline at end of file + "Ragweed": { + "Value": 0, + "Category": "Low", + "CategoryValue": 1 + }, + "Tree": { + "Value": 0, + "Category": "Low", + "CategoryValue": 1 + }, + "UVIndex": { + "Value": 5, + "Category": "Moderate", + "CategoryValue": 2 + }, + "TemperatureMin": { + "Value": 15.4, + "Unit": "C", + "UnitType": 17 + }, + "TemperatureMax": { + "Value": 29.5, + "Unit": "C", + "UnitType": 17 + }, + "RealFeelTemperatureMin": { + "Value": 15.1, + "Unit": "C", + "UnitType": 17 + }, + "RealFeelTemperatureMax": { + "Value": 29.8, + "Unit": "C", + "UnitType": 17 + }, + "RealFeelTemperatureShadeMin": { + "Value": 15.1, + "Unit": "C", + "UnitType": 17 + }, + "RealFeelTemperatureShadeMax": { + "Value": 28.0, + "Unit": "C", + "UnitType": 17 + }, + "IconDay": 17, + "IconPhraseDay": "Partly sunny w/ t-storms", + "HasPrecipitationDay": true, + "PrecipitationTypeDay": "Rain", + "PrecipitationIntensityDay": "Moderate", + "ShortPhraseDay": "A shower and t-storm around", + "LongPhraseDay": "Clouds and sunshine with a couple of showers and a thunderstorm around late this afternoon", + "PrecipitationProbabilityDay": 60, + "ThunderstormProbabilityDay": 40, + "RainProbabilityDay": 60, + "SnowProbabilityDay": 0, + "IceProbabilityDay": 0, + "WindDay": { + "Speed": { + "Value": 13.0, + "Unit": "km/h", + "UnitType": 7 + }, + "Direction": { + "Degrees": 166, + "Localized": "SSE", + "English": "SSE" + } + }, + "WindGustDay": { + "Speed": { + "Value": 29.6, + "Unit": "km/h", + "UnitType": 7 + }, + "Direction": { + "Degrees": 178, + "Localized": "S", + "English": "S" + } + }, + "TotalLiquidDay": { + "Value": 2.5, + "Unit": "mm", + "UnitType": 3 + }, + "RainDay": { + "Value": 2.5, + "Unit": "mm", + "UnitType": 3 + }, + "SnowDay": { + "Value": 0.0, + "Unit": "cm", + "UnitType": 4 + }, + "IceDay": { + "Value": 0.0, + "Unit": "mm", + "UnitType": 3 + }, + "HoursOfPrecipitationDay": 1.0, + "HoursOfRainDay": 1.0, + "HoursOfSnowDay": 0.0, + "HoursOfIceDay": 0.0, + "CloudCoverDay": 58, + "IconNight": 41, + "IconPhraseNight": "Partly cloudy w/ t-storms", + "HasPrecipitationNight": true, + "PrecipitationTypeNight": "Rain", + "PrecipitationIntensityNight": "Moderate", + "ShortPhraseNight": "Partly cloudy", + "LongPhraseNight": "Partly cloudy", + "PrecipitationProbabilityNight": 57, + "ThunderstormProbabilityNight": 40, + "RainProbabilityNight": 57, + "SnowProbabilityNight": 0, + "IceProbabilityNight": 0, + "WindNight": { + "Speed": { + "Value": 7.4, + "Unit": "km/h", + "UnitType": 7 + }, + "Direction": { + "Degrees": 289, + "Localized": "WNW", + "English": "WNW" + } + }, + "WindGustNight": { + "Speed": { + "Value": 18.5, + "Unit": "km/h", + "UnitType": 7 + }, + "Direction": { + "Degrees": 256, + "Localized": "WSW", + "English": "WSW" + } + }, + "TotalLiquidNight": { + "Value": 2.3, + "Unit": "mm", + "UnitType": 3 + }, + "RainNight": { + "Value": 2.3, + "Unit": "mm", + "UnitType": 3 + }, + "SnowNight": { + "Value": 0.0, + "Unit": "cm", + "UnitType": 4 + }, + "IceNight": { + "Value": 0.0, + "Unit": "mm", + "UnitType": 3 + }, + "HoursOfPrecipitationNight": 1.0, + "HoursOfRainNight": 1.0, + "HoursOfSnowNight": 0.0, + "HoursOfIceNight": 0.0, + "CloudCoverNight": 65 + }, + { + "Date": "2020-07-27T07:00:00+02:00", + "EpochDate": 1595826000, + "HoursOfSun": 7.4, + "DegreeDaySummary": { + "Heating": { + "Value": 0.0, + "Unit": "C", + "UnitType": 17 + }, + "Cooling": { + "Value": 3.0, + "Unit": "C", + "UnitType": 17 + } + }, + "Ozone": { + "Value": 39, + "Category": "Good", + "CategoryValue": 1 + }, + "Grass": { + "Value": 0, + "Category": "Low", + "CategoryValue": 1 + }, + "Mold": { + "Value": 0, + "Category": "Low", + "CategoryValue": 1 + }, + "Ragweed": { + "Value": 0, + "Category": "Low", + "CategoryValue": 1 + }, + "Tree": { + "Value": 0, + "Category": "Low", + "CategoryValue": 1 + }, + "UVIndex": { + "Value": 7, + "Category": "High", + "CategoryValue": 3 + }, + "TemperatureMin": { + "Value": 15.9, + "Unit": "C", + "UnitType": 17 + }, + "TemperatureMax": { + "Value": 26.2, + "Unit": "C", + "UnitType": 17 + }, + "RealFeelTemperatureMin": { + "Value": 15.8, + "Unit": "C", + "UnitType": 17 + }, + "RealFeelTemperatureMax": { + "Value": 28.9, + "Unit": "C", + "UnitType": 17 + }, + "RealFeelTemperatureShadeMin": { + "Value": 15.8, + "Unit": "C", + "UnitType": 17 + }, + "RealFeelTemperatureShadeMax": { + "Value": 25.0, + "Unit": "C", + "UnitType": 17 + }, + "IconDay": 4, + "IconPhraseDay": "Intermittent clouds", + "HasPrecipitationDay": false, + "ShortPhraseDay": "Clouds and sun", + "LongPhraseDay": "Clouds and sun", + "PrecipitationProbabilityDay": 25, + "ThunderstormProbabilityDay": 24, + "RainProbabilityDay": 25, + "SnowProbabilityDay": 0, + "IceProbabilityDay": 0, + "WindDay": { + "Speed": { + "Value": 9.3, + "Unit": "km/h", + "UnitType": 7 + }, + "Direction": { + "Degrees": 297, + "Localized": "WNW", + "English": "WNW" + } + }, + "WindGustDay": { + "Speed": { + "Value": 14.8, + "Unit": "km/h", + "UnitType": 7 + }, + "Direction": { + "Degrees": 317, + "Localized": "NW", + "English": "NW" + } + }, + "TotalLiquidDay": { + "Value": 0.0, + "Unit": "mm", + "UnitType": 3 + }, + "RainDay": { + "Value": 0.0, + "Unit": "mm", + "UnitType": 3 + }, + "SnowDay": { + "Value": 0.0, + "Unit": "cm", + "UnitType": 4 + }, + "IceDay": { + "Value": 0.0, + "Unit": "mm", + "UnitType": 3 + }, + "HoursOfPrecipitationDay": 0.0, + "HoursOfRainDay": 0.0, + "HoursOfSnowDay": 0.0, + "HoursOfIceDay": 0.0, + "CloudCoverDay": 52, + "IconNight": 36, + "IconPhraseNight": "Intermittent clouds", + "HasPrecipitationNight": false, + "ShortPhraseNight": "Partly cloudy", + "LongPhraseNight": "Partly cloudy", + "PrecipitationProbabilityNight": 6, + "ThunderstormProbabilityNight": 0, + "RainProbabilityNight": 6, + "SnowProbabilityNight": 0, + "IceProbabilityNight": 0, + "WindNight": { + "Speed": { + "Value": 7.4, + "Unit": "km/h", + "UnitType": 7 + }, + "Direction": { + "Degrees": 162, + "Localized": "SSE", + "English": "SSE" + } + }, + "WindGustNight": { + "Speed": { + "Value": 14.8, + "Unit": "km/h", + "UnitType": 7 + }, + "Direction": { + "Degrees": 175, + "Localized": "S", + "English": "S" + } + }, + "TotalLiquidNight": { + "Value": 0.0, + "Unit": "mm", + "UnitType": 3 + }, + "RainNight": { + "Value": 0.0, + "Unit": "mm", + "UnitType": 3 + }, + "SnowNight": { + "Value": 0.0, + "Unit": "cm", + "UnitType": 4 + }, + "IceNight": { + "Value": 0.0, + "Unit": "mm", + "UnitType": 3 + }, + "HoursOfPrecipitationNight": 0.0, + "HoursOfRainNight": 0.0, + "HoursOfSnowNight": 0.0, + "HoursOfIceNight": 0.0, + "CloudCoverNight": 63 + }, + { + "Date": "2020-07-28T07:00:00+02:00", + "EpochDate": 1595912400, + "HoursOfSun": 5.7, + "DegreeDaySummary": { + "Heating": { + "Value": 0.0, + "Unit": "C", + "UnitType": 17 + }, + "Cooling": { + "Value": 6.0, + "Unit": "C", + "UnitType": 17 + } + }, + "Ozone": { + "Value": 29, + "Category": "Good", + "CategoryValue": 1 + }, + "Grass": { + "Value": 0, + "Category": "Low", + "CategoryValue": 1 + }, + "Mold": { + "Value": 0, + "Category": "Low", + "CategoryValue": 1 + }, + "Ragweed": { + "Value": 0, + "Category": "Low", + "CategoryValue": 1 + }, + "Tree": { + "Value": 0, + "Category": "Low", + "CategoryValue": 1 + }, + "UVIndex": { + "Value": 7, + "Category": "High", + "CategoryValue": 3 + }, + "TemperatureMin": { + "Value": 16.8, + "Unit": "C", + "UnitType": 17 + }, + "TemperatureMax": { + "Value": 31.7, + "Unit": "C", + "UnitType": 17 + }, + "RealFeelTemperatureMin": { + "Value": 16.7, + "Unit": "C", + "UnitType": 17 + }, + "RealFeelTemperatureMax": { + "Value": 31.6, + "Unit": "C", + "UnitType": 17 + }, + "RealFeelTemperatureShadeMin": { + "Value": 16.7, + "Unit": "C", + "UnitType": 17 + }, + "RealFeelTemperatureShadeMax": { + "Value": 30.0, + "Unit": "C", + "UnitType": 17 + }, + "IconDay": 4, + "IconPhraseDay": "Intermittent clouds", + "HasPrecipitationDay": false, + "ShortPhraseDay": "Partly sunny and very warm", + "LongPhraseDay": "Very warm with a blend of sun and clouds", + "PrecipitationProbabilityDay": 10, + "ThunderstormProbabilityDay": 4, + "RainProbabilityDay": 10, + "SnowProbabilityDay": 0, + "IceProbabilityDay": 0, + "WindDay": { + "Speed": { + "Value": 16.7, + "Unit": "km/h", + "UnitType": 7 + }, + "Direction": { + "Degrees": 198, + "Localized": "SSW", + "English": "SSW" + } + }, + "WindGustDay": { + "Speed": { + "Value": 24.1, + "Unit": "km/h", + "UnitType": 7 + }, + "Direction": { + "Degrees": 198, + "Localized": "SSW", + "English": "SSW" + } + }, + "TotalLiquidDay": { + "Value": 0.0, + "Unit": "mm", + "UnitType": 3 + }, + "RainDay": { + "Value": 0.0, + "Unit": "mm", + "UnitType": 3 + }, + "SnowDay": { + "Value": 0.0, + "Unit": "cm", + "UnitType": 4 + }, + "IceDay": { + "Value": 0.0, + "Unit": "mm", + "UnitType": 3 + }, + "HoursOfPrecipitationDay": 0.0, + "HoursOfRainDay": 0.0, + "HoursOfSnowDay": 0.0, + "HoursOfIceDay": 0.0, + "CloudCoverDay": 65, + "IconNight": 36, + "IconPhraseNight": "Intermittent clouds", + "HasPrecipitationNight": false, + "ShortPhraseNight": "Partly cloudy", + "LongPhraseNight": "Partly cloudy", + "PrecipitationProbabilityNight": 25, + "ThunderstormProbabilityNight": 24, + "RainProbabilityNight": 25, + "SnowProbabilityNight": 0, + "IceProbabilityNight": 0, + "WindNight": { + "Speed": { + "Value": 9.3, + "Unit": "km/h", + "UnitType": 7 + }, + "Direction": { + "Degrees": 265, + "Localized": "W", + "English": "W" + } + }, + "WindGustNight": { + "Speed": { + "Value": 22.2, + "Unit": "km/h", + "UnitType": 7 + }, + "Direction": { + "Degrees": 271, + "Localized": "W", + "English": "W" + } + }, + "TotalLiquidNight": { + "Value": 0.0, + "Unit": "mm", + "UnitType": 3 + }, + "RainNight": { + "Value": 0.0, + "Unit": "mm", + "UnitType": 3 + }, + "SnowNight": { + "Value": 0.0, + "Unit": "cm", + "UnitType": 4 + }, + "IceNight": { + "Value": 0.0, + "Unit": "mm", + "UnitType": 3 + }, + "HoursOfPrecipitationNight": 0.0, + "HoursOfRainNight": 0.0, + "HoursOfSnowNight": 0.0, + "HoursOfIceNight": 0.0, + "CloudCoverNight": 53 + }, + { + "Date": "2020-07-29T07:00:00+02:00", + "EpochDate": 1595998800, + "HoursOfSun": 9.4, + "DegreeDaySummary": { + "Heating": { + "Value": 0.0, + "Unit": "C", + "UnitType": 17 + }, + "Cooling": { + "Value": 0.0, + "Unit": "C", + "UnitType": 17 + } + }, + "Ozone": { + "Value": 18, + "Category": "Good", + "CategoryValue": 1 + }, + "Grass": { + "Value": 0, + "Category": "Low", + "CategoryValue": 1 + }, + "Mold": { + "Value": 0, + "Category": "Low", + "CategoryValue": 1 + }, + "Ragweed": { + "Value": 0, + "Category": "Low", + "CategoryValue": 1 + }, + "Tree": { + "Value": 0, + "Category": "Low", + "CategoryValue": 1 + }, + "UVIndex": { + "Value": 6, + "Category": "High", + "CategoryValue": 3 + }, + "TemperatureMin": { + "Value": 11.7, + "Unit": "C", + "UnitType": 17 + }, + "TemperatureMax": { + "Value": 24.0, + "Unit": "C", + "UnitType": 17 + }, + "RealFeelTemperatureMin": { + "Value": 10.1, + "Unit": "C", + "UnitType": 17 + }, + "RealFeelTemperatureMax": { + "Value": 26.5, + "Unit": "C", + "UnitType": 17 + }, + "RealFeelTemperatureShadeMin": { + "Value": 10.1, + "Unit": "C", + "UnitType": 17 + }, + "RealFeelTemperatureShadeMax": { + "Value": 22.5, + "Unit": "C", + "UnitType": 17 + }, + "IconDay": 3, + "IconPhraseDay": "Partly sunny", + "HasPrecipitationDay": false, + "ShortPhraseDay": "Cooler with partial sunshine", + "LongPhraseDay": "Cooler with partial sunshine", + "PrecipitationProbabilityDay": 9, + "ThunderstormProbabilityDay": 0, + "RainProbabilityDay": 9, + "SnowProbabilityDay": 0, + "IceProbabilityDay": 0, + "WindDay": { + "Speed": { + "Value": 13.0, + "Unit": "km/h", + "UnitType": 7 + }, + "Direction": { + "Degrees": 293, + "Localized": "WNW", + "English": "WNW" + } + }, + "WindGustDay": { + "Speed": { + "Value": 24.1, + "Unit": "km/h", + "UnitType": 7 + }, + "Direction": { + "Degrees": 271, + "Localized": "W", + "English": "W" + } + }, + "TotalLiquidDay": { + "Value": 0.0, + "Unit": "mm", + "UnitType": 3 + }, + "RainDay": { + "Value": 0.0, + "Unit": "mm", + "UnitType": 3 + }, + "SnowDay": { + "Value": 0.0, + "Unit": "cm", + "UnitType": 4 + }, + "IceDay": { + "Value": 0.0, + "Unit": "mm", + "UnitType": 3 + }, + "HoursOfPrecipitationDay": 0.0, + "HoursOfRainDay": 0.0, + "HoursOfSnowDay": 0.0, + "HoursOfIceDay": 0.0, + "CloudCoverDay": 45, + "IconNight": 34, + "IconPhraseNight": "Mostly clear", + "HasPrecipitationNight": false, + "ShortPhraseNight": "Mainly clear", + "LongPhraseNight": "Mainly clear", + "PrecipitationProbabilityNight": 1, + "ThunderstormProbabilityNight": 0, + "RainProbabilityNight": 1, + "SnowProbabilityNight": 0, + "IceProbabilityNight": 0, + "WindNight": { + "Speed": { + "Value": 11.1, + "Unit": "km/h", + "UnitType": 7 + }, + "Direction": { + "Degrees": 264, + "Localized": "W", + "English": "W" + } + }, + "WindGustNight": { + "Speed": { + "Value": 18.5, + "Unit": "km/h", + "UnitType": 7 + }, + "Direction": { + "Degrees": 266, + "Localized": "W", + "English": "W" + } + }, + "TotalLiquidNight": { + "Value": 0.0, + "Unit": "mm", + "UnitType": 3 + }, + "RainNight": { + "Value": 0.0, + "Unit": "mm", + "UnitType": 3 + }, + "SnowNight": { + "Value": 0.0, + "Unit": "cm", + "UnitType": 4 + }, + "IceNight": { + "Value": 0.0, + "Unit": "mm", + "UnitType": 3 + }, + "HoursOfPrecipitationNight": 0.0, + "HoursOfRainNight": 0.0, + "HoursOfSnowNight": 0.0, + "HoursOfIceNight": 0.0, + "CloudCoverNight": 27 + }, + { + "Date": "2020-07-30T07:00:00+02:00", + "EpochDate": 1596085200, + "HoursOfSun": 9.2, + "DegreeDaySummary": { + "Heating": { + "Value": 1.0, + "Unit": "C", + "UnitType": 17 + }, + "Cooling": { + "Value": 0.0, + "Unit": "C", + "UnitType": 17 + } + }, + "Ozone": { + "Value": 14, + "Category": "Good", + "CategoryValue": 1 + }, + "Grass": { + "Value": 0, + "Category": "Low", + "CategoryValue": 1 + }, + "Mold": { + "Value": 0, + "Category": "Low", + "CategoryValue": 1 + }, + "Ragweed": { + "Value": 0, + "Category": "Low", + "CategoryValue": 1 + }, + "Tree": { + "Value": 0, + "Category": "Low", + "CategoryValue": 1 + }, + "UVIndex": { + "Value": 7, + "Category": "High", + "CategoryValue": 3 + }, + "TemperatureMin": { + "Value": 12.2, + "Unit": "C", + "UnitType": 17 + }, + "TemperatureMax": { + "Value": 21.4, + "Unit": "C", + "UnitType": 17 + }, + "RealFeelTemperatureMin": { + "Value": 11.3, + "Unit": "C", + "UnitType": 17 + }, + "RealFeelTemperatureMax": { + "Value": 22.2, + "Unit": "C", + "UnitType": 17 + }, + "RealFeelTemperatureShadeMin": { + "Value": 11.3, + "Unit": "C", + "UnitType": 17 + }, + "RealFeelTemperatureShadeMax": { + "Value": 19.5, + "Unit": "C", + "UnitType": 17 + }, + "IconDay": 4, + "IconPhraseDay": "Intermittent clouds", + "HasPrecipitationDay": false, + "ShortPhraseDay": "Clouds and sun", + "LongPhraseDay": "Intervals of clouds and sunshine", + "PrecipitationProbabilityDay": 1, + "ThunderstormProbabilityDay": 0, + "RainProbabilityDay": 1, + "SnowProbabilityDay": 0, + "IceProbabilityDay": 0, + "WindDay": { + "Speed": { + "Value": 18.5, + "Unit": "km/h", + "UnitType": 7 + }, + "Direction": { + "Degrees": 280, + "Localized": "W", + "English": "W" + } + }, + "WindGustDay": { + "Speed": { + "Value": 27.8, + "Unit": "km/h", + "UnitType": 7 + }, + "Direction": { + "Degrees": 273, + "Localized": "W", + "English": "W" + } + }, + "TotalLiquidDay": { + "Value": 0.0, + "Unit": "mm", + "UnitType": 3 + }, + "RainDay": { + "Value": 0.0, + "Unit": "mm", + "UnitType": 3 + }, + "SnowDay": { + "Value": 0.0, + "Unit": "cm", + "UnitType": 4 + }, + "IceDay": { + "Value": 0.0, + "Unit": "mm", + "UnitType": 3 + }, + "HoursOfPrecipitationDay": 0.0, + "HoursOfRainDay": 0.0, + "HoursOfSnowDay": 0.0, + "HoursOfIceDay": 0.0, + "CloudCoverDay": 50, + "IconNight": 34, + "IconPhraseNight": "Mostly clear", + "HasPrecipitationNight": false, + "ShortPhraseNight": "Mostly clear", + "LongPhraseNight": "Mostly clear", + "PrecipitationProbabilityNight": 3, + "ThunderstormProbabilityNight": 0, + "RainProbabilityNight": 3, + "SnowProbabilityNight": 0, + "IceProbabilityNight": 0, + "WindNight": { + "Speed": { + "Value": 9.3, + "Unit": "km/h", + "UnitType": 7 + }, + "Direction": { + "Degrees": 272, + "Localized": "W", + "English": "W" + } + }, + "WindGustNight": { + "Speed": { + "Value": 18.5, + "Unit": "km/h", + "UnitType": 7 + }, + "Direction": { + "Degrees": 274, + "Localized": "W", + "English": "W" + } + }, + "TotalLiquidNight": { + "Value": 0.0, + "Unit": "mm", + "UnitType": 3 + }, + "RainNight": { + "Value": 0.0, + "Unit": "mm", + "UnitType": 3 + }, + "SnowNight": { + "Value": 0.0, + "Unit": "cm", + "UnitType": 4 + }, + "IceNight": { + "Value": 0.0, + "Unit": "mm", + "UnitType": 3 + }, + "HoursOfPrecipitationNight": 0.0, + "HoursOfRainNight": 0.0, + "HoursOfSnowNight": 0.0, + "HoursOfIceNight": 0.0, + "CloudCoverNight": 13 + } +] diff --git a/tests/components/accuweather/test_diagnostics.py b/tests/components/accuweather/test_diagnostics.py new file mode 100644 index 00000000000..1936de5fad7 --- /dev/null +++ b/tests/components/accuweather/test_diagnostics.py @@ -0,0 +1,26 @@ +"""Test AccuWeather diagnostics.""" +import json + +from tests.common import load_fixture +from tests.components.accuweather import init_integration +from tests.components.diagnostics import get_diagnostics_for_config_entry + + +async def test_entry_diagnostics(hass, hass_client): + """Test config entry diagnostics.""" + entry = await init_integration(hass) + + coordinator_data = json.loads( + load_fixture("current_conditions_data.json", "accuweather") + ) + coordinator_data["forecast"] = {} + + result = await get_diagnostics_for_config_entry(hass, hass_client, entry) + + assert result["config_entry_data"] == { + "api_key": "**REDACTED**", + "latitude": "**REDACTED**", + "longitude": "**REDACTED**", + "name": "Home", + } + assert result["coordinator_data"] == coordinator_data diff --git a/tests/components/advantage_air/fixtures/getSystemData.json b/tests/components/advantage_air/fixtures/getSystemData.json index 28c28995a14..35a06c2d468 100644 --- a/tests/components/advantage_air/fixtures/getSystemData.json +++ b/tests/components/advantage_air/fixtures/getSystemData.json @@ -1,158 +1,158 @@ { - "aircons": { - "ac1": { - "info": { - "climateControlModeIsRunning": false, - "countDownToOff": 10, - "countDownToOn": 0, - "fan": "high", - "filterCleanStatus": 0, - "freshAirStatus": "off", - "mode": "vent", - "myZone": 1, - "name": "AC One", - "setTemp": 24, - "state": "on" - }, - "zones": { - "z01": { - "error": 0, - "maxDamper": 100, - "measuredTemp": 25, - "minDamper": 0, - "motion": 20, - "motionConfig": 2, - "name": "Zone open with Sensor", - "number": 1, - "rssi": 40, - "setTemp": 24, - "state": "open", - "type": 1, - "value": 100 - }, - "z02": { - "error": 0, - "maxDamper": 100, - "measuredTemp": 25, - "minDamper": 0, - "motion": 21, - "motionConfig": 2, - "name": "Zone closed with Sensor", - "number": 2, - "rssi": 10, - "setTemp": 24, - "state": "close", - "type": 1, - "value": 0 - }, - "z03": { - "error": 0, - "maxDamper": 100, - "measuredTemp": 25, - "minDamper": 0, - "motion": 22, - "motionConfig": 2, - "name": "Zone 3", - "number": 3, - "rssi": 25, - "setTemp": 24, - "state": "close", - "type": 1, - "value": 0 - }, - "z04": { - "error": 0, - "maxDamper": 100, - "measuredTemp": 25, - "minDamper": 0, - "motion": 1, - "motionConfig": 1, - "name": "Zone 4", - "number": 4, - "rssi": 75, - "setTemp": 24, - "state": "close", - "type": 1, - "value": 0 - }, - "z05": { - "error": 0, - "maxDamper": 100, - "measuredTemp": 25, - "minDamper": 0, - "motion": 5, - "motionConfig": 1, - "name": "Zone 5", - "number": 5, - "rssi": 100, - "setTemp": 24, - "state": "close", - "type": 1, - "value": 0 - } - } + "aircons": { + "ac1": { + "info": { + "climateControlModeIsRunning": false, + "countDownToOff": 10, + "countDownToOn": 0, + "fan": "high", + "filterCleanStatus": 0, + "freshAirStatus": "off", + "mode": "vent", + "myZone": 1, + "name": "AC One", + "setTemp": 24, + "state": "on" + }, + "zones": { + "z01": { + "error": 0, + "maxDamper": 100, + "measuredTemp": 25, + "minDamper": 0, + "motion": 20, + "motionConfig": 2, + "name": "Zone open with Sensor", + "number": 1, + "rssi": 40, + "setTemp": 24, + "state": "open", + "type": 1, + "value": 100 }, - "ac2": { - "info": { - "climateControlModeIsRunning": false, - "countDownToOff": 0, - "countDownToOn": 20, - "fan": "low", - "filterCleanStatus": 1, - "freshAirStatus": "none", - "mode": "myauto", - "myAutoModeCurrentSetMode": "cool", - "myAutoModeEnabled": true, - "myAutoModeIsRunning": true, - "myZone": 0, - "name": "AC Two", - "setTemp": 24, - "state": "off" - }, - "zones": { - "z01": { - "error": 0, - "maxDamper": 100, - "measuredTemp": 0, - "minDamper": 0, - "motion": 0, - "motionConfig": 0, - "name": "Zone open without sensor", - "number": 1, - "rssi": 0, - "setTemp": 24, - "state": "open", - "type": 0, - "value": 100 - }, - "z02": { - "error": 0, - "maxDamper": 100, - "measuredTemp": 0, - "minDamper": 0, - "motion": 0, - "motionConfig": 0, - "name": "Zone closed without sensor", - "number": 2, - "rssi": 0, - "setTemp": 24, - "state": "close", - "type": 0, - "value": 0 - } - } + "z02": { + "error": 0, + "maxDamper": 100, + "measuredTemp": 25, + "minDamper": 0, + "motion": 21, + "motionConfig": 2, + "name": "Zone closed with Sensor", + "number": 2, + "rssi": 10, + "setTemp": 24, + "state": "close", + "type": 1, + "value": 0 + }, + "z03": { + "error": 0, + "maxDamper": 100, + "measuredTemp": 25, + "minDamper": 0, + "motion": 22, + "motionConfig": 2, + "name": "Zone 3", + "number": 3, + "rssi": 25, + "setTemp": 24, + "state": "close", + "type": 1, + "value": 0 + }, + "z04": { + "error": 0, + "maxDamper": 100, + "measuredTemp": 25, + "minDamper": 0, + "motion": 1, + "motionConfig": 1, + "name": "Zone 4", + "number": 4, + "rssi": 75, + "setTemp": 24, + "state": "close", + "type": 1, + "value": 0 + }, + "z05": { + "error": 0, + "maxDamper": 100, + "measuredTemp": 25, + "minDamper": 0, + "motion": 5, + "motionConfig": 1, + "name": "Zone 5", + "number": 5, + "rssi": 100, + "setTemp": 24, + "state": "close", + "type": 1, + "value": 0 } + } }, - "system": { - "hasAircons": true, - "hasLights": false, - "hasSensors": false, - "hasThings": false, - "hasThingsBOG": false, - "hasThingsLight": false, - "name": "testname", - "rid": "uniqueid", - "sysType": "e-zone", - "myAppRev": "testversion" + "ac2": { + "info": { + "climateControlModeIsRunning": false, + "countDownToOff": 0, + "countDownToOn": 20, + "fan": "low", + "filterCleanStatus": 1, + "freshAirStatus": "none", + "mode": "myauto", + "myAutoModeCurrentSetMode": "cool", + "myAutoModeEnabled": true, + "myAutoModeIsRunning": true, + "myZone": 0, + "name": "AC Two", + "setTemp": 24, + "state": "off" + }, + "zones": { + "z01": { + "error": 0, + "maxDamper": 100, + "measuredTemp": 0, + "minDamper": 0, + "motion": 0, + "motionConfig": 0, + "name": "Zone open without sensor", + "number": 1, + "rssi": 0, + "setTemp": 24, + "state": "open", + "type": 0, + "value": 100 + }, + "z02": { + "error": 0, + "maxDamper": 100, + "measuredTemp": 0, + "minDamper": 0, + "motion": 0, + "motionConfig": 0, + "name": "Zone closed without sensor", + "number": 2, + "rssi": 0, + "setTemp": 24, + "state": "close", + "type": 0, + "value": 0 + } + } } -} \ No newline at end of file + }, + "system": { + "hasAircons": true, + "hasLights": false, + "hasSensors": false, + "hasThings": false, + "hasThingsBOG": false, + "hasThingsLight": false, + "name": "testname", + "rid": "uniqueid", + "sysType": "e-zone", + "myAppRev": "testversion" + } +} diff --git a/tests/components/advantage_air/fixtures/setAircon.json b/tests/components/advantage_air/fixtures/setAircon.json index ca439c142ae..00274b4c170 100644 --- a/tests/components/advantage_air/fixtures/setAircon.json +++ b/tests/components/advantage_air/fixtures/setAircon.json @@ -1,4 +1,4 @@ { - "ack": true, - "request": "setAircon" -} \ No newline at end of file + "ack": true, + "request": "setAircon" +} diff --git a/tests/components/aemet/fixtures/station-3195-data.json b/tests/components/aemet/fixtures/station-3195-data.json index 1784a5fb3a4..b050ee16d67 100644 --- a/tests/components/aemet/fixtures/station-3195-data.json +++ b/tests/components/aemet/fixtures/station-3195-data.json @@ -1,369 +1,393 @@ -[ { - "idema" : "3195", - "lon" : -3.678095, - "fint" : "2021-01-08T14:00:00", - "prec" : 1.2, - "alt" : 667.0, - "lat" : 40.411804, - "ubi" : "MADRID RETIRO", - "pres" : 929.9, - "hr" : 97.0, - "pres_nmar" : 1009.9, - "tamin" : -0.1, - "ta" : 0.1, - "tamax" : 0.2, - "tpr" : -0.3, - "rviento" : 132.0 -}, { - "idema" : "3195", - "lon" : -3.678095, - "fint" : "2021-01-08T15:00:00", - "prec" : 1.5, - "alt" : 667.0, - "lat" : 40.411804, - "ubi" : "MADRID RETIRO", - "pres" : 929.0, - "hr" : 98.0, - "pres_nmar" : 1008.9, - "tamin" : 0.1, - "ta" : 0.2, - "tamax" : 0.3, - "tpr" : 0.0, - "rviento" : 154.0 -}, { - "idema" : "3195", - "lon" : -3.678095, - "fint" : "2021-01-08T16:00:00", - "prec" : 0.7, - "alt" : 667.0, - "lat" : 40.411804, - "ubi" : "MADRID RETIRO", - "pres" : 928.8, - "hr" : 98.0, - "pres_nmar" : 1008.6, - "tamin" : 0.2, - "ta" : 0.3, - "tamax" : 0.3, - "tpr" : 0.0, - "rviento" : 177.0 -}, { - "idema" : "3195", - "lon" : -3.678095, - "fint" : "2021-01-08T17:00:00", - "prec" : 1.7, - "alt" : 667.0, - "lat" : 40.411804, - "ubi" : "MADRID RETIRO", - "pres" : 928.6, - "hr" : 99.0, - "pres_nmar" : 1008.5, - "tamin" : 0.1, - "ta" : 0.1, - "tamax" : 0.3, - "tpr" : 0.0, - "rviento" : 174.0 -}, { - "idema" : "3195", - "lon" : -3.678095, - "fint" : "2021-01-08T18:00:00", - "prec" : 1.9, - "alt" : 667.0, - "lat" : 40.411804, - "ubi" : "MADRID RETIRO", - "pres" : 928.2, - "hr" : 99.0, - "pres_nmar" : 1008.1, - "tamin" : -0.1, - "ta" : -0.1, - "tamax" : 0.1, - "tpr" : -0.3, - "rviento" : 163.0 -}, { - "idema" : "3195", - "lon" : -3.678095, - "fint" : "2021-01-08T19:00:00", - "prec" : 3.0, - "alt" : 667.0, - "lat" : 40.411804, - "ubi" : "MADRID RETIRO", - "pres" : 928.4, - "hr" : 99.0, - "pres_nmar" : 1008.4, - "tamin" : -0.3, - "ta" : -0.3, - "tamax" : 0.0, - "tpr" : -0.5, - "rviento" : 79.0 -}, { - "idema" : "3195", - "lon" : -3.678095, - "fint" : "2021-01-08T20:00:00", - "prec" : 3.5, - "alt" : 667.0, - "lat" : 40.411804, - "ubi" : "MADRID RETIRO", - "pres" : 928.4, - "hr" : 99.0, - "pres_nmar" : 1008.5, - "tamin" : -0.6, - "ta" : -0.6, - "tamax" : -0.3, - "tpr" : -0.7, - "rviento" : 0.0 -}, { - "idema" : "3195", - "lon" : -3.678095, - "fint" : "2021-01-08T21:00:00", - "prec" : 2.6, - "alt" : 667.0, - "lat" : 40.411804, - "ubi" : "MADRID RETIRO", - "pres" : 928.1, - "hr" : 99.0, - "pres_nmar" : 1008.2, - "tamin" : -0.7, - "ta" : -0.7, - "tamax" : -0.5, - "tpr" : -0.7, - "rviento" : 0.0 -}, { - "idema" : "3195", - "lon" : -3.678095, - "fint" : "2021-01-08T22:00:00", - "prec" : 3.0, - "alt" : 667.0, - "lat" : 40.411804, - "ubi" : "MADRID RETIRO", - "pres" : 927.6, - "hr" : 99.0, - "pres_nmar" : 1007.7, - "tamin" : -0.8, - "ta" : -0.8, - "tamax" : -0.7, - "tpr" : -1.0, - "rviento" : 0.0 -}, { - "idema" : "3195", - "lon" : -3.678095, - "fint" : "2021-01-08T23:00:00", - "prec" : 2.9, - "alt" : 667.0, - "lat" : 40.411804, - "ubi" : "MADRID RETIRO", - "pres" : 926.9, - "hr" : 99.0, - "pres_nmar" : 1007.0, - "tamin" : -0.9, - "ta" : -0.9, - "tamax" : -0.7, - "tpr" : -1.0, - "rviento" : 0.0 -}, { - "idema" : "3195", - "lon" : -3.678095, - "fint" : "2021-01-09T00:00:00", - "prec" : 1.4, - "alt" : 667.0, - "lat" : 40.411804, - "ubi" : "MADRID RETIRO", - "pres" : 926.5, - "hr" : 99.0, - "pres_nmar" : 1006.6, - "tamin" : -1.0, - "ta" : -1.0, - "tamax" : -0.8, - "tpr" : -1.2, - "rviento" : 0.0 -}, { - "idema" : "3195", - "lon" : -3.678095, - "fint" : "2021-01-09T01:00:00", - "prec" : 2.0, - "alt" : 667.0, - "lat" : 40.411804, - "ubi" : "MADRID RETIRO", - "pres" : 925.9, - "hr" : 99.0, - "pres_nmar" : 1006.0, - "tamin" : -1.3, - "ta" : -1.3, - "tamax" : -1.0, - "tpr" : -1.4, - "rviento" : 0.0 -}, { - "idema" : "3195", - "lon" : -3.678095, - "fint" : "2021-01-09T02:00:00", - "prec" : 1.5, - "alt" : 667.0, - "lat" : 40.411804, - "ubi" : "MADRID RETIRO", - "pres" : 925.7, - "hr" : 99.0, - "pres_nmar" : 1005.8, - "tamin" : -1.5, - "ta" : -1.4, - "tamax" : -1.3, - "tpr" : -1.4, - "rviento" : 0.0 -}, { - "idema" : "3195", - "lon" : -3.678095, - "fint" : "2021-01-09T03:00:00", - "prec" : 1.2, - "alt" : 667.0, - "lat" : 40.411804, - "ubi" : "MADRID RETIRO", - "pres" : 925.6, - "hr" : 99.0, - "pres_nmar" : 1005.7, - "tamin" : -1.5, - "ta" : -1.4, - "tamax" : -1.4, - "tpr" : -1.4, - "rviento" : 0.0 -}, { - "idema" : "3195", - "lon" : -3.678095, - "fint" : "2021-01-09T04:00:00", - "prec" : 1.1, - "alt" : 667.0, - "lat" : 40.411804, - "ubi" : "MADRID RETIRO", - "pres" : 924.9, - "hr" : 99.0, - "pres_nmar" : 1005.0, - "tamin" : -1.5, - "ta" : -1.5, - "tamax" : -1.4, - "tpr" : -1.7, - "rviento" : 0.0 -}, { - "idema" : "3195", - "lon" : -3.678095, - "fint" : "2021-01-09T05:00:00", - "prec" : 0.7, - "alt" : 667.0, - "lat" : 40.411804, - "ubi" : "MADRID RETIRO", - "pres" : 924.6, - "hr" : 99.0, - "pres_nmar" : 1004.7, - "tamin" : -1.5, - "ta" : -1.5, - "tamax" : -1.4, - "tpr" : -1.7, - "rviento" : 0.0 -}, { - "idema" : "3195", - "lon" : -3.678095, - "fint" : "2021-01-09T06:00:00", - "prec" : 0.2, - "alt" : 667.0, - "lat" : 40.411804, - "ubi" : "MADRID RETIRO", - "pres" : 924.4, - "hr" : 99.0, - "pres_nmar" : 1004.5, - "tamin" : -1.6, - "ta" : -1.6, - "tamax" : -1.5, - "tpr" : -1.7, - "rviento" : 0.0 -}, { - "idema" : "3195", - "lon" : -3.678095, - "fint" : "2021-01-09T07:00:00", - "prec" : 0.0, - "alt" : 667.0, - "lat" : 40.411804, - "ubi" : "MADRID RETIRO", - "pres" : 924.4, - "hr" : 99.0, - "pres_nmar" : 1004.5, - "tamin" : -1.6, - "ta" : -1.6, - "tamax" : -1.6, - "tpr" : -1.7, - "rviento" : 0.0 -}, { - "idema" : "3195", - "lon" : -3.678095, - "fint" : "2021-01-09T08:00:00", - "prec" : 0.1, - "alt" : 667.0, - "lat" : 40.411804, - "ubi" : "MADRID RETIRO", - "pres" : 924.8, - "hr" : 99.0, - "pres_nmar" : 1004.9, - "tamin" : -1.6, - "ta" : -1.6, - "tamax" : -1.5, - "tpr" : -1.7, - "rviento" : 0.0 -}, { - "idema" : "3195", - "lon" : -3.678095, - "fint" : "2021-01-09T09:00:00", - "prec" : 0.0, - "alt" : 667.0, - "lat" : 40.411804, - "ubi" : "MADRID RETIRO", - "pres" : 925.0, - "hr" : 99.0, - "pres_nmar" : 1005.0, - "tamin" : -1.6, - "ta" : -1.3, - "tamax" : -1.3, - "tpr" : -1.4, - "rviento" : 0.0 -}, { - "idema" : "3195", - "lon" : -3.678095, - "fint" : "2021-01-09T10:00:00", - "prec" : 0.0, - "alt" : 667.0, - "lat" : 40.411804, - "ubi" : "MADRID RETIRO", - "pres" : 925.3, - "hr" : 99.0, - "pres_nmar" : 1005.3, - "tamin" : -1.3, - "ta" : -1.2, - "tamax" : -1.1, - "tpr" : -1.4, - "rviento" : 0.0 -}, { - "idema" : "3195", - "lon" : -3.678095, - "fint" : "2021-01-09T11:00:00", - "prec" : 4.4, - "alt" : 667.0, - "lat" : 40.411804, - "ubi" : "MADRID RETIRO", - "pres" : 925.4, - "hr" : 99.0, - "pres_nmar" : 1005.4, - "tamin" : -1.2, - "ta" : -1.0, - "tamax" : -1.0, - "tpr" : -1.2, - "rviento" : 0.0 -}, { - "idema" : "3195", - "lon" : -3.678095, - "fint" : "2021-01-09T12:00:00", - "prec" : 7.0, - "alt" : 667.0, - "lat" : 40.411804, - "ubi" : "MADRID RETIRO", - "pres" : 924.6, - "hr" : 99.0, - "pres_nmar" : 1004.4, - "tamin" : -1.0, - "ta" : -0.7, - "tamax" : -0.6, - "tpr" : -0.7, - "rviento" : 0.0 -} ] +[ + { + "idema": "3195", + "lon": -3.678095, + "fint": "2021-01-08T14:00:00", + "prec": 1.2, + "alt": 667.0, + "lat": 40.411804, + "ubi": "MADRID RETIRO", + "pres": 929.9, + "hr": 97.0, + "pres_nmar": 1009.9, + "tamin": -0.1, + "ta": 0.1, + "tamax": 0.2, + "tpr": -0.3, + "rviento": 132.0 + }, + { + "idema": "3195", + "lon": -3.678095, + "fint": "2021-01-08T15:00:00", + "prec": 1.5, + "alt": 667.0, + "lat": 40.411804, + "ubi": "MADRID RETIRO", + "pres": 929.0, + "hr": 98.0, + "pres_nmar": 1008.9, + "tamin": 0.1, + "ta": 0.2, + "tamax": 0.3, + "tpr": 0.0, + "rviento": 154.0 + }, + { + "idema": "3195", + "lon": -3.678095, + "fint": "2021-01-08T16:00:00", + "prec": 0.7, + "alt": 667.0, + "lat": 40.411804, + "ubi": "MADRID RETIRO", + "pres": 928.8, + "hr": 98.0, + "pres_nmar": 1008.6, + "tamin": 0.2, + "ta": 0.3, + "tamax": 0.3, + "tpr": 0.0, + "rviento": 177.0 + }, + { + "idema": "3195", + "lon": -3.678095, + "fint": "2021-01-08T17:00:00", + "prec": 1.7, + "alt": 667.0, + "lat": 40.411804, + "ubi": "MADRID RETIRO", + "pres": 928.6, + "hr": 99.0, + "pres_nmar": 1008.5, + "tamin": 0.1, + "ta": 0.1, + "tamax": 0.3, + "tpr": 0.0, + "rviento": 174.0 + }, + { + "idema": "3195", + "lon": -3.678095, + "fint": "2021-01-08T18:00:00", + "prec": 1.9, + "alt": 667.0, + "lat": 40.411804, + "ubi": "MADRID RETIRO", + "pres": 928.2, + "hr": 99.0, + "pres_nmar": 1008.1, + "tamin": -0.1, + "ta": -0.1, + "tamax": 0.1, + "tpr": -0.3, + "rviento": 163.0 + }, + { + "idema": "3195", + "lon": -3.678095, + "fint": "2021-01-08T19:00:00", + "prec": 3.0, + "alt": 667.0, + "lat": 40.411804, + "ubi": "MADRID RETIRO", + "pres": 928.4, + "hr": 99.0, + "pres_nmar": 1008.4, + "tamin": -0.3, + "ta": -0.3, + "tamax": 0.0, + "tpr": -0.5, + "rviento": 79.0 + }, + { + "idema": "3195", + "lon": -3.678095, + "fint": "2021-01-08T20:00:00", + "prec": 3.5, + "alt": 667.0, + "lat": 40.411804, + "ubi": "MADRID RETIRO", + "pres": 928.4, + "hr": 99.0, + "pres_nmar": 1008.5, + "tamin": -0.6, + "ta": -0.6, + "tamax": -0.3, + "tpr": -0.7, + "rviento": 0.0 + }, + { + "idema": "3195", + "lon": -3.678095, + "fint": "2021-01-08T21:00:00", + "prec": 2.6, + "alt": 667.0, + "lat": 40.411804, + "ubi": "MADRID RETIRO", + "pres": 928.1, + "hr": 99.0, + "pres_nmar": 1008.2, + "tamin": -0.7, + "ta": -0.7, + "tamax": -0.5, + "tpr": -0.7, + "rviento": 0.0 + }, + { + "idema": "3195", + "lon": -3.678095, + "fint": "2021-01-08T22:00:00", + "prec": 3.0, + "alt": 667.0, + "lat": 40.411804, + "ubi": "MADRID RETIRO", + "pres": 927.6, + "hr": 99.0, + "pres_nmar": 1007.7, + "tamin": -0.8, + "ta": -0.8, + "tamax": -0.7, + "tpr": -1.0, + "rviento": 0.0 + }, + { + "idema": "3195", + "lon": -3.678095, + "fint": "2021-01-08T23:00:00", + "prec": 2.9, + "alt": 667.0, + "lat": 40.411804, + "ubi": "MADRID RETIRO", + "pres": 926.9, + "hr": 99.0, + "pres_nmar": 1007.0, + "tamin": -0.9, + "ta": -0.9, + "tamax": -0.7, + "tpr": -1.0, + "rviento": 0.0 + }, + { + "idema": "3195", + "lon": -3.678095, + "fint": "2021-01-09T00:00:00", + "prec": 1.4, + "alt": 667.0, + "lat": 40.411804, + "ubi": "MADRID RETIRO", + "pres": 926.5, + "hr": 99.0, + "pres_nmar": 1006.6, + "tamin": -1.0, + "ta": -1.0, + "tamax": -0.8, + "tpr": -1.2, + "rviento": 0.0 + }, + { + "idema": "3195", + "lon": -3.678095, + "fint": "2021-01-09T01:00:00", + "prec": 2.0, + "alt": 667.0, + "lat": 40.411804, + "ubi": "MADRID RETIRO", + "pres": 925.9, + "hr": 99.0, + "pres_nmar": 1006.0, + "tamin": -1.3, + "ta": -1.3, + "tamax": -1.0, + "tpr": -1.4, + "rviento": 0.0 + }, + { + "idema": "3195", + "lon": -3.678095, + "fint": "2021-01-09T02:00:00", + "prec": 1.5, + "alt": 667.0, + "lat": 40.411804, + "ubi": "MADRID RETIRO", + "pres": 925.7, + "hr": 99.0, + "pres_nmar": 1005.8, + "tamin": -1.5, + "ta": -1.4, + "tamax": -1.3, + "tpr": -1.4, + "rviento": 0.0 + }, + { + "idema": "3195", + "lon": -3.678095, + "fint": "2021-01-09T03:00:00", + "prec": 1.2, + "alt": 667.0, + "lat": 40.411804, + "ubi": "MADRID RETIRO", + "pres": 925.6, + "hr": 99.0, + "pres_nmar": 1005.7, + "tamin": -1.5, + "ta": -1.4, + "tamax": -1.4, + "tpr": -1.4, + "rviento": 0.0 + }, + { + "idema": "3195", + "lon": -3.678095, + "fint": "2021-01-09T04:00:00", + "prec": 1.1, + "alt": 667.0, + "lat": 40.411804, + "ubi": "MADRID RETIRO", + "pres": 924.9, + "hr": 99.0, + "pres_nmar": 1005.0, + "tamin": -1.5, + "ta": -1.5, + "tamax": -1.4, + "tpr": -1.7, + "rviento": 0.0 + }, + { + "idema": "3195", + "lon": -3.678095, + "fint": "2021-01-09T05:00:00", + "prec": 0.7, + "alt": 667.0, + "lat": 40.411804, + "ubi": "MADRID RETIRO", + "pres": 924.6, + "hr": 99.0, + "pres_nmar": 1004.7, + "tamin": -1.5, + "ta": -1.5, + "tamax": -1.4, + "tpr": -1.7, + "rviento": 0.0 + }, + { + "idema": "3195", + "lon": -3.678095, + "fint": "2021-01-09T06:00:00", + "prec": 0.2, + "alt": 667.0, + "lat": 40.411804, + "ubi": "MADRID RETIRO", + "pres": 924.4, + "hr": 99.0, + "pres_nmar": 1004.5, + "tamin": -1.6, + "ta": -1.6, + "tamax": -1.5, + "tpr": -1.7, + "rviento": 0.0 + }, + { + "idema": "3195", + "lon": -3.678095, + "fint": "2021-01-09T07:00:00", + "prec": 0.0, + "alt": 667.0, + "lat": 40.411804, + "ubi": "MADRID RETIRO", + "pres": 924.4, + "hr": 99.0, + "pres_nmar": 1004.5, + "tamin": -1.6, + "ta": -1.6, + "tamax": -1.6, + "tpr": -1.7, + "rviento": 0.0 + }, + { + "idema": "3195", + "lon": -3.678095, + "fint": "2021-01-09T08:00:00", + "prec": 0.1, + "alt": 667.0, + "lat": 40.411804, + "ubi": "MADRID RETIRO", + "pres": 924.8, + "hr": 99.0, + "pres_nmar": 1004.9, + "tamin": -1.6, + "ta": -1.6, + "tamax": -1.5, + "tpr": -1.7, + "rviento": 0.0 + }, + { + "idema": "3195", + "lon": -3.678095, + "fint": "2021-01-09T09:00:00", + "prec": 0.0, + "alt": 667.0, + "lat": 40.411804, + "ubi": "MADRID RETIRO", + "pres": 925.0, + "hr": 99.0, + "pres_nmar": 1005.0, + "tamin": -1.6, + "ta": -1.3, + "tamax": -1.3, + "tpr": -1.4, + "rviento": 0.0 + }, + { + "idema": "3195", + "lon": -3.678095, + "fint": "2021-01-09T10:00:00", + "prec": 0.0, + "alt": 667.0, + "lat": 40.411804, + "ubi": "MADRID RETIRO", + "pres": 925.3, + "hr": 99.0, + "pres_nmar": 1005.3, + "tamin": -1.3, + "ta": -1.2, + "tamax": -1.1, + "tpr": -1.4, + "rviento": 0.0 + }, + { + "idema": "3195", + "lon": -3.678095, + "fint": "2021-01-09T11:00:00", + "prec": 4.4, + "alt": 667.0, + "lat": 40.411804, + "ubi": "MADRID RETIRO", + "pres": 925.4, + "hr": 99.0, + "pres_nmar": 1005.4, + "tamin": -1.2, + "ta": -1.0, + "tamax": -1.0, + "tpr": -1.2, + "rviento": 0.0 + }, + { + "idema": "3195", + "lon": -3.678095, + "fint": "2021-01-09T12:00:00", + "prec": 7.0, + "alt": 667.0, + "lat": 40.411804, + "ubi": "MADRID RETIRO", + "pres": 924.6, + "hr": 99.0, + "pres_nmar": 1004.4, + "tamin": -1.0, + "ta": -0.7, + "tamax": -0.6, + "tpr": -0.7, + "rviento": 0.0 + } +] diff --git a/tests/components/aemet/fixtures/station-3195.json b/tests/components/aemet/fixtures/station-3195.json index f97df3bea63..cfd8c59a7ee 100644 --- a/tests/components/aemet/fixtures/station-3195.json +++ b/tests/components/aemet/fixtures/station-3195.json @@ -1,6 +1,6 @@ { - "descripcion" : "exito", - "estado" : 200, - "datos" : "https://opendata.aemet.es/opendata/sh/208c3ca3", - "metadatos" : "https://opendata.aemet.es/opendata/sh/55c2971b" + "descripcion": "exito", + "estado": 200, + "datos": "https://opendata.aemet.es/opendata/sh/208c3ca3", + "metadatos": "https://opendata.aemet.es/opendata/sh/55c2971b" } diff --git a/tests/components/aemet/fixtures/station-list-data.json b/tests/components/aemet/fixtures/station-list-data.json index 8b35bff6e4a..2507cca7328 100644 --- a/tests/components/aemet/fixtures/station-list-data.json +++ b/tests/components/aemet/fixtures/station-list-data.json @@ -1,42 +1,46 @@ -[ { - "idema" : "3194U", - "lon" : -3.724167, - "fint" : "2021-01-08T14:00:00", - "prec" : 1.3, - "alt" : 664.0, - "lat" : 40.45167, - "ubi" : "MADRID C. UNIVERSITARIA", - "hr" : 98.0, - "tamin" : 0.6, - "ta" : 0.9, - "tamax" : 1.0, - "tpr" : 0.6 -}, { - "idema" : "3194Y", - "lon" : -3.813369, - "fint" : "2021-01-08T14:00:00", - "prec" : 0.2, - "alt" : 665.0, - "lat" : 40.448437, - "ubi" : "POZUELO DE ALARCON (AUTOM�TICA)", - "hr" : 93.0, - "tamin" : 0.5, - "ta" : 0.6, - "tamax" : 0.6 -}, { - "idema" : "3195", - "lon" : -3.678095, - "fint" : "2021-01-08T14:00:00", - "prec" : 1.2, - "alt" : 667.0, - "lat" : 40.411804, - "ubi" : "MADRID RETIRO", - "pres" : 929.9, - "hr" : 97.0, - "pres_nmar" : 1009.9, - "tamin" : -0.1, - "ta" : 0.1, - "tamax" : 0.2, - "tpr" : -0.3, - "rviento" : 132.0 -} ] +[ + { + "idema": "3194U", + "lon": -3.724167, + "fint": "2021-01-08T14:00:00", + "prec": 1.3, + "alt": 664.0, + "lat": 40.45167, + "ubi": "MADRID C. UNIVERSITARIA", + "hr": 98.0, + "tamin": 0.6, + "ta": 0.9, + "tamax": 1.0, + "tpr": 0.6 + }, + { + "idema": "3194Y", + "lon": -3.813369, + "fint": "2021-01-08T14:00:00", + "prec": 0.2, + "alt": 665.0, + "lat": 40.448437, + "ubi": "POZUELO DE ALARCON (AUTOM�TICA)", + "hr": 93.0, + "tamin": 0.5, + "ta": 0.6, + "tamax": 0.6 + }, + { + "idema": "3195", + "lon": -3.678095, + "fint": "2021-01-08T14:00:00", + "prec": 1.2, + "alt": 667.0, + "lat": 40.411804, + "ubi": "MADRID RETIRO", + "pres": 929.9, + "hr": 97.0, + "pres_nmar": 1009.9, + "tamin": -0.1, + "ta": 0.1, + "tamax": 0.2, + "tpr": -0.3, + "rviento": 132.0 + } +] diff --git a/tests/components/aemet/fixtures/station-list.json b/tests/components/aemet/fixtures/station-list.json index 6e0dbc97d6d..86f79727e7f 100644 --- a/tests/components/aemet/fixtures/station-list.json +++ b/tests/components/aemet/fixtures/station-list.json @@ -1,6 +1,6 @@ { - "descripcion" : "exito", - "estado" : 200, - "datos" : "https://opendata.aemet.es/opendata/sh/2c55192f", - "metadatos" : "https://opendata.aemet.es/opendata/sh/55c2971b" + "descripcion": "exito", + "estado": 200, + "datos": "https://opendata.aemet.es/opendata/sh/2c55192f", + "metadatos": "https://opendata.aemet.es/opendata/sh/55c2971b" } diff --git a/tests/components/aemet/fixtures/town-28065-forecast-daily-data.json b/tests/components/aemet/fixtures/town-28065-forecast-daily-data.json index 77877c72f3a..23647dc0c92 100644 --- a/tests/components/aemet/fixtures/town-28065-forecast-daily-data.json +++ b/tests/components/aemet/fixtures/town-28065-forecast-daily-data.json @@ -1,625 +1,815 @@ -[ { - "origen" : { - "productor" : "Agencia Estatal de Meteorolog�a - AEMET. Gobierno de Espa�a", - "web" : "http://www.aemet.es", - "enlace" : "http://www.aemet.es/es/eltiempo/prediccion/municipios/getafe-id28065", - "language" : "es", - "copyright" : "� AEMET. Autorizado el uso de la informaci�n y su reproducci�n citando a AEMET como autora de la misma.", - "notaLegal" : "http://www.aemet.es/es/nota_legal" - }, - "elaborado" : "2021-01-09T11:54:00", - "nombre" : "Getafe", - "provincia" : "Madrid", - "prediccion" : { - "dia" : [ { - "probPrecipitacion" : [ { - "value" : 0, - "periodo" : "00-24" - }, { - "value" : 0, - "periodo" : "00-12" - }, { - "value" : 100, - "periodo" : "12-24" - }, { - "value" : 0, - "periodo" : "00-06" - }, { - "value" : 100, - "periodo" : "06-12" - }, { - "value" : 100, - "periodo" : "12-18" - }, { - "value" : 100, - "periodo" : "18-24" - } ], - "cotaNieveProv" : [ { - "value" : "", - "periodo" : "00-24" - }, { - "value" : "", - "periodo" : "00-12" - }, { - "value" : "500", - "periodo" : "12-24" - }, { - "value" : "", - "periodo" : "00-06" - }, { - "value" : "400", - "periodo" : "06-12" - }, { - "value" : "500", - "periodo" : "12-18" - }, { - "value" : "600", - "periodo" : "18-24" - } ], - "estadoCielo" : [ { - "value" : "", - "periodo" : "00-24", - "descripcion" : "" - }, { - "value" : "", - "periodo" : "00-12", - "descripcion" : "" - }, { - "value" : "36", - "periodo" : "12-24", - "descripcion" : "Cubierto con nieve" - }, { - "value" : "", - "periodo" : "00-06", - "descripcion" : "" - }, { - "value" : "36", - "periodo" : "06-12", - "descripcion" : "Cubierto con nieve" - }, { - "value" : "36", - "periodo" : "12-18", - "descripcion" : "Cubierto con nieve" - }, { - "value" : "34n", - "periodo" : "18-24", - "descripcion" : "Nuboso con nieve" - } ], - "viento" : [ { - "direccion" : "", - "velocidad" : 0, - "periodo" : "00-24" - }, { - "direccion" : "", - "velocidad" : 0, - "periodo" : "00-12" - }, { - "direccion" : "E", - "velocidad" : 15, - "periodo" : "12-24" - }, { - "direccion" : "NE", - "velocidad" : 30, - "periodo" : "00-06" - }, { - "direccion" : "E", - "velocidad" : 15, - "periodo" : "06-12" - }, { - "direccion" : "E", - "velocidad" : 5, - "periodo" : "12-18" - }, { - "direccion" : "NE", - "velocidad" : 5, - "periodo" : "18-24" - } ], - "rachaMax" : [ { - "value" : "", - "periodo" : "00-24" - }, { - "value" : "", - "periodo" : "00-12" - }, { - "value" : "", - "periodo" : "12-24" - }, { - "value" : "40", - "periodo" : "00-06" - }, { - "value" : "", - "periodo" : "06-12" - }, { - "value" : "", - "periodo" : "12-18" - }, { - "value" : "", - "periodo" : "18-24" - } ], - "temperatura" : { - "maxima" : 2, - "minima" : -1, - "dato" : [ { - "value" : -1, - "hora" : 6 - }, { - "value" : 0, - "hora" : 12 - }, { - "value" : 1, - "hora" : 18 - }, { - "value" : 1, - "hora" : 24 - } ] - }, - "sensTermica" : { - "maxima" : 1, - "minima" : -9, - "dato" : [ { - "value" : -1, - "hora" : 6 - }, { - "value" : -4, - "hora" : 12 - }, { - "value" : 1, - "hora" : 18 - }, { - "value" : 1, - "hora" : 24 - } ] - }, - "humedadRelativa" : { - "maxima" : 100, - "minima" : 75, - "dato" : [ { - "value" : 100, - "hora" : 6 - }, { - "value" : 100, - "hora" : 12 - }, { - "value" : 95, - "hora" : 18 - }, { - "value" : 75, - "hora" : 24 - } ] - }, - "uvMax" : 1, - "fecha" : "2021-01-09T00:00:00" - }, { - "probPrecipitacion" : [ { - "value" : 30, - "periodo" : "00-24" - }, { - "value" : 25, - "periodo" : "00-12" - }, { - "value" : 5, - "periodo" : "12-24" - }, { - "value" : 5, - "periodo" : "00-06" - }, { - "value" : 15, - "periodo" : "06-12" - }, { - "value" : 5, - "periodo" : "12-18" - }, { - "value" : 0, - "periodo" : "18-24" - } ], - "cotaNieveProv" : [ { - "value" : "600", - "periodo" : "00-24" - }, { - "value" : "600", - "periodo" : "00-12" - }, { - "value" : "", - "periodo" : "12-24" - }, { - "value" : "", - "periodo" : "00-06" - }, { - "value" : "600", - "periodo" : "06-12" - }, { - "value" : "", - "periodo" : "12-18" - }, { - "value" : "", - "periodo" : "18-24" - } ], - "estadoCielo" : [ { - "value" : "13", - "periodo" : "00-24", - "descripcion" : "Intervalos nubosos" - }, { - "value" : "15", - "periodo" : "00-12", - "descripcion" : "Muy nuboso" - }, { - "value" : "12", - "periodo" : "12-24", - "descripcion" : "Poco nuboso" - }, { - "value" : "14n", - "periodo" : "00-06", - "descripcion" : "Nuboso" - }, { - "value" : "15", - "periodo" : "06-12", - "descripcion" : "Muy nuboso" - }, { - "value" : "12", - "periodo" : "12-18", - "descripcion" : "Poco nuboso" - }, { - "value" : "12n", - "periodo" : "18-24", - "descripcion" : "Poco nuboso" - } ], - "viento" : [ { - "direccion" : "NE", - "velocidad" : 20, - "periodo" : "00-24" - }, { - "direccion" : "NE", - "velocidad" : 20, - "periodo" : "00-12" - }, { - "direccion" : "NE", - "velocidad" : 20, - "periodo" : "12-24" - }, { - "direccion" : "N", - "velocidad" : 10, - "periodo" : "00-06" - }, { - "direccion" : "NE", - "velocidad" : 20, - "periodo" : "06-12" - }, { - "direccion" : "NE", - "velocidad" : 15, - "periodo" : "12-18" - }, { - "direccion" : "NE", - "velocidad" : 20, - "periodo" : "18-24" - } ], - "rachaMax" : [ { - "value" : "30", - "periodo" : "00-24" - }, { - "value" : "30", - "periodo" : "00-12" - }, { - "value" : "30", - "periodo" : "12-24" - }, { - "value" : "", - "periodo" : "00-06" - }, { - "value" : "30", - "periodo" : "06-12" - }, { - "value" : "", - "periodo" : "12-18" - }, { - "value" : "", - "periodo" : "18-24" - } ], - "temperatura" : { - "maxima" : 4, - "minima" : -4, - "dato" : [ { - "value" : -1, - "hora" : 6 - }, { - "value" : 3, - "hora" : 12 - }, { - "value" : 1, - "hora" : 18 - }, { - "value" : -1, - "hora" : 24 - } ] - }, - "sensTermica" : { - "maxima" : 1, - "minima" : -7, - "dato" : [ { - "value" : -4, - "hora" : 6 - }, { - "value" : -2, - "hora" : 12 - }, { - "value" : -4, - "hora" : 18 - }, { - "value" : -6, - "hora" : 24 - } ] - }, - "humedadRelativa" : { - "maxima" : 100, - "minima" : 70, - "dato" : [ { - "value" : 90, - "hora" : 6 - }, { - "value" : 75, - "hora" : 12 - }, { - "value" : 80, - "hora" : 18 - }, { - "value" : 80, - "hora" : 24 - } ] - }, - "uvMax" : 1, - "fecha" : "2021-01-10T00:00:00" - }, { - "probPrecipitacion" : [ { - "value" : 0, - "periodo" : "00-24" - }, { - "value" : 0, - "periodo" : "00-12" - }, { - "value" : 0, - "periodo" : "12-24" - } ], - "cotaNieveProv" : [ { - "value" : "", - "periodo" : "00-24" - }, { - "value" : "", - "periodo" : "00-12" - }, { - "value" : "", - "periodo" : "12-24" - } ], - "estadoCielo" : [ { - "value" : "12", - "periodo" : "00-24", - "descripcion" : "Poco nuboso" - }, { - "value" : "12", - "periodo" : "00-12", - "descripcion" : "Poco nuboso" - }, { - "value" : "12", - "periodo" : "12-24", - "descripcion" : "Poco nuboso" - } ], - "viento" : [ { - "direccion" : "N", - "velocidad" : 5, - "periodo" : "00-24" - }, { - "direccion" : "NE", - "velocidad" : 20, - "periodo" : "00-12" - }, { - "direccion" : "NO", - "velocidad" : 10, - "periodo" : "12-24" - } ], - "rachaMax" : [ { - "value" : "", - "periodo" : "00-24" - }, { - "value" : "", - "periodo" : "00-12" - }, { - "value" : "", - "periodo" : "12-24" - } ], - "temperatura" : { - "maxima" : 3, - "minima" : -7, - "dato" : [ ] - }, - "sensTermica" : { - "maxima" : 3, - "minima" : -8, - "dato" : [ ] - }, - "humedadRelativa" : { - "maxima" : 85, - "minima" : 60, - "dato" : [ ] - }, - "uvMax" : 1, - "fecha" : "2021-01-11T00:00:00" - }, { - "probPrecipitacion" : [ { - "value" : 0, - "periodo" : "00-24" - }, { - "value" : 0, - "periodo" : "00-12" - }, { - "value" : 0, - "periodo" : "12-24" - } ], - "cotaNieveProv" : [ { - "value" : "", - "periodo" : "00-24" - }, { - "value" : "", - "periodo" : "00-12" - }, { - "value" : "", - "periodo" : "12-24" - } ], - "estadoCielo" : [ { - "value" : "12", - "periodo" : "00-24", - "descripcion" : "Poco nuboso" - }, { - "value" : "12", - "periodo" : "00-12", - "descripcion" : "Poco nuboso" - }, { - "value" : "12", - "periodo" : "12-24", - "descripcion" : "Poco nuboso" - } ], - "viento" : [ { - "direccion" : "C", - "velocidad" : 0, - "periodo" : "00-24" - }, { - "direccion" : "E", - "velocidad" : 5, - "periodo" : "00-12" - }, { - "direccion" : "C", - "velocidad" : 0, - "periodo" : "12-24" - } ], - "rachaMax" : [ { - "value" : "", - "periodo" : "00-24" - }, { - "value" : "", - "periodo" : "00-12" - }, { - "value" : "", - "periodo" : "12-24" - } ], - "temperatura" : { - "maxima" : -1, - "minima" : -13, - "dato" : [ ] - }, - "sensTermica" : { - "maxima" : -1, - "minima" : -13, - "dato" : [ ] - }, - "humedadRelativa" : { - "maxima" : 100, - "minima" : 65, - "dato" : [ ] - }, - "uvMax" : 2, - "fecha" : "2021-01-12T00:00:00" - }, { - "probPrecipitacion" : [ { - "value" : 0 - } ], - "cotaNieveProv" : [ { - "value" : "" - } ], - "estadoCielo" : [ { - "value" : "11", - "descripcion" : "Despejado" - } ], - "viento" : [ { - "direccion" : "C", - "velocidad" : 0 - } ], - "rachaMax" : [ { - "value" : "" - } ], - "temperatura" : { - "maxima" : 6, - "minima" : -11, - "dato" : [ ] - }, - "sensTermica" : { - "maxima" : 6, - "minima" : -11, - "dato" : [ ] - }, - "humedadRelativa" : { - "maxima" : 100, - "minima" : 65, - "dato" : [ ] - }, - "uvMax" : 2, - "fecha" : "2021-01-13T00:00:00" - }, { - "probPrecipitacion" : [ { - "value" : 0 - } ], - "cotaNieveProv" : [ { - "value" : "" - } ], - "estadoCielo" : [ { - "value" : "12", - "descripcion" : "Poco nuboso" - } ], - "viento" : [ { - "direccion" : "C", - "velocidad" : 0 - } ], - "rachaMax" : [ { - "value" : "" - } ], - "temperatura" : { - "maxima" : 6, - "minima" : -7, - "dato" : [ ] - }, - "sensTermica" : { - "maxima" : 6, - "minima" : -7, - "dato" : [ ] - }, - "humedadRelativa" : { - "maxima" : 100, - "minima" : 80, - "dato" : [ ] - }, - "fecha" : "2021-01-14T00:00:00" - }, { - "probPrecipitacion" : [ { - "value" : 0 - } ], - "cotaNieveProv" : [ { - "value" : "" - } ], - "estadoCielo" : [ { - "value" : "14", - "descripcion" : "Nuboso" - } ], - "viento" : [ { - "direccion" : "C", - "velocidad" : 0 - } ], - "rachaMax" : [ { - "value" : "" - } ], - "temperatura" : { - "maxima" : 5, - "minima" : -4, - "dato" : [ ] - }, - "sensTermica" : { - "maxima" : 5, - "minima" : -4, - "dato" : [ ] - }, - "humedadRelativa" : { - "maxima" : 100, - "minima" : 55, - "dato" : [ ] - }, - "fecha" : "2021-01-15T00:00:00" - } ] - }, - "id" : 28065, - "version" : 1.0 -} ] +[ + { + "origen": { + "productor": "Agencia Estatal de Meteorolog�a - AEMET. Gobierno de Espa�a", + "web": "http://www.aemet.es", + "enlace": "http://www.aemet.es/es/eltiempo/prediccion/municipios/getafe-id28065", + "language": "es", + "copyright": "� AEMET. Autorizado el uso de la informaci�n y su reproducci�n citando a AEMET como autora de la misma.", + "notaLegal": "http://www.aemet.es/es/nota_legal" + }, + "elaborado": "2021-01-09T11:54:00", + "nombre": "Getafe", + "provincia": "Madrid", + "prediccion": { + "dia": [ + { + "probPrecipitacion": [ + { + "value": 0, + "periodo": "00-24" + }, + { + "value": 0, + "periodo": "00-12" + }, + { + "value": 100, + "periodo": "12-24" + }, + { + "value": 0, + "periodo": "00-06" + }, + { + "value": 100, + "periodo": "06-12" + }, + { + "value": 100, + "periodo": "12-18" + }, + { + "value": 100, + "periodo": "18-24" + } + ], + "cotaNieveProv": [ + { + "value": "", + "periodo": "00-24" + }, + { + "value": "", + "periodo": "00-12" + }, + { + "value": "500", + "periodo": "12-24" + }, + { + "value": "", + "periodo": "00-06" + }, + { + "value": "400", + "periodo": "06-12" + }, + { + "value": "500", + "periodo": "12-18" + }, + { + "value": "600", + "periodo": "18-24" + } + ], + "estadoCielo": [ + { + "value": "", + "periodo": "00-24", + "descripcion": "" + }, + { + "value": "", + "periodo": "00-12", + "descripcion": "" + }, + { + "value": "36", + "periodo": "12-24", + "descripcion": "Cubierto con nieve" + }, + { + "value": "", + "periodo": "00-06", + "descripcion": "" + }, + { + "value": "36", + "periodo": "06-12", + "descripcion": "Cubierto con nieve" + }, + { + "value": "36", + "periodo": "12-18", + "descripcion": "Cubierto con nieve" + }, + { + "value": "34n", + "periodo": "18-24", + "descripcion": "Nuboso con nieve" + } + ], + "viento": [ + { + "direccion": "", + "velocidad": 0, + "periodo": "00-24" + }, + { + "direccion": "", + "velocidad": 0, + "periodo": "00-12" + }, + { + "direccion": "E", + "velocidad": 15, + "periodo": "12-24" + }, + { + "direccion": "NE", + "velocidad": 30, + "periodo": "00-06" + }, + { + "direccion": "E", + "velocidad": 15, + "periodo": "06-12" + }, + { + "direccion": "E", + "velocidad": 5, + "periodo": "12-18" + }, + { + "direccion": "NE", + "velocidad": 5, + "periodo": "18-24" + } + ], + "rachaMax": [ + { + "value": "", + "periodo": "00-24" + }, + { + "value": "", + "periodo": "00-12" + }, + { + "value": "", + "periodo": "12-24" + }, + { + "value": "40", + "periodo": "00-06" + }, + { + "value": "", + "periodo": "06-12" + }, + { + "value": "", + "periodo": "12-18" + }, + { + "value": "", + "periodo": "18-24" + } + ], + "temperatura": { + "maxima": 2, + "minima": -1, + "dato": [ + { + "value": -1, + "hora": 6 + }, + { + "value": 0, + "hora": 12 + }, + { + "value": 1, + "hora": 18 + }, + { + "value": 1, + "hora": 24 + } + ] + }, + "sensTermica": { + "maxima": 1, + "minima": -9, + "dato": [ + { + "value": -1, + "hora": 6 + }, + { + "value": -4, + "hora": 12 + }, + { + "value": 1, + "hora": 18 + }, + { + "value": 1, + "hora": 24 + } + ] + }, + "humedadRelativa": { + "maxima": 100, + "minima": 75, + "dato": [ + { + "value": 100, + "hora": 6 + }, + { + "value": 100, + "hora": 12 + }, + { + "value": 95, + "hora": 18 + }, + { + "value": 75, + "hora": 24 + } + ] + }, + "uvMax": 1, + "fecha": "2021-01-09T00:00:00" + }, + { + "probPrecipitacion": [ + { + "value": 30, + "periodo": "00-24" + }, + { + "value": 25, + "periodo": "00-12" + }, + { + "value": 5, + "periodo": "12-24" + }, + { + "value": 5, + "periodo": "00-06" + }, + { + "value": 15, + "periodo": "06-12" + }, + { + "value": 5, + "periodo": "12-18" + }, + { + "value": 0, + "periodo": "18-24" + } + ], + "cotaNieveProv": [ + { + "value": "600", + "periodo": "00-24" + }, + { + "value": "600", + "periodo": "00-12" + }, + { + "value": "", + "periodo": "12-24" + }, + { + "value": "", + "periodo": "00-06" + }, + { + "value": "600", + "periodo": "06-12" + }, + { + "value": "", + "periodo": "12-18" + }, + { + "value": "", + "periodo": "18-24" + } + ], + "estadoCielo": [ + { + "value": "13", + "periodo": "00-24", + "descripcion": "Intervalos nubosos" + }, + { + "value": "15", + "periodo": "00-12", + "descripcion": "Muy nuboso" + }, + { + "value": "12", + "periodo": "12-24", + "descripcion": "Poco nuboso" + }, + { + "value": "14n", + "periodo": "00-06", + "descripcion": "Nuboso" + }, + { + "value": "15", + "periodo": "06-12", + "descripcion": "Muy nuboso" + }, + { + "value": "12", + "periodo": "12-18", + "descripcion": "Poco nuboso" + }, + { + "value": "12n", + "periodo": "18-24", + "descripcion": "Poco nuboso" + } + ], + "viento": [ + { + "direccion": "NE", + "velocidad": 20, + "periodo": "00-24" + }, + { + "direccion": "NE", + "velocidad": 20, + "periodo": "00-12" + }, + { + "direccion": "NE", + "velocidad": 20, + "periodo": "12-24" + }, + { + "direccion": "N", + "velocidad": 10, + "periodo": "00-06" + }, + { + "direccion": "NE", + "velocidad": 20, + "periodo": "06-12" + }, + { + "direccion": "NE", + "velocidad": 15, + "periodo": "12-18" + }, + { + "direccion": "NE", + "velocidad": 20, + "periodo": "18-24" + } + ], + "rachaMax": [ + { + "value": "30", + "periodo": "00-24" + }, + { + "value": "30", + "periodo": "00-12" + }, + { + "value": "30", + "periodo": "12-24" + }, + { + "value": "", + "periodo": "00-06" + }, + { + "value": "30", + "periodo": "06-12" + }, + { + "value": "", + "periodo": "12-18" + }, + { + "value": "", + "periodo": "18-24" + } + ], + "temperatura": { + "maxima": 4, + "minima": -4, + "dato": [ + { + "value": -1, + "hora": 6 + }, + { + "value": 3, + "hora": 12 + }, + { + "value": 1, + "hora": 18 + }, + { + "value": -1, + "hora": 24 + } + ] + }, + "sensTermica": { + "maxima": 1, + "minima": -7, + "dato": [ + { + "value": -4, + "hora": 6 + }, + { + "value": -2, + "hora": 12 + }, + { + "value": -4, + "hora": 18 + }, + { + "value": -6, + "hora": 24 + } + ] + }, + "humedadRelativa": { + "maxima": 100, + "minima": 70, + "dato": [ + { + "value": 90, + "hora": 6 + }, + { + "value": 75, + "hora": 12 + }, + { + "value": 80, + "hora": 18 + }, + { + "value": 80, + "hora": 24 + } + ] + }, + "uvMax": 1, + "fecha": "2021-01-10T00:00:00" + }, + { + "probPrecipitacion": [ + { + "value": 0, + "periodo": "00-24" + }, + { + "value": 0, + "periodo": "00-12" + }, + { + "value": 0, + "periodo": "12-24" + } + ], + "cotaNieveProv": [ + { + "value": "", + "periodo": "00-24" + }, + { + "value": "", + "periodo": "00-12" + }, + { + "value": "", + "periodo": "12-24" + } + ], + "estadoCielo": [ + { + "value": "12", + "periodo": "00-24", + "descripcion": "Poco nuboso" + }, + { + "value": "12", + "periodo": "00-12", + "descripcion": "Poco nuboso" + }, + { + "value": "12", + "periodo": "12-24", + "descripcion": "Poco nuboso" + } + ], + "viento": [ + { + "direccion": "N", + "velocidad": 5, + "periodo": "00-24" + }, + { + "direccion": "NE", + "velocidad": 20, + "periodo": "00-12" + }, + { + "direccion": "NO", + "velocidad": 10, + "periodo": "12-24" + } + ], + "rachaMax": [ + { + "value": "", + "periodo": "00-24" + }, + { + "value": "", + "periodo": "00-12" + }, + { + "value": "", + "periodo": "12-24" + } + ], + "temperatura": { + "maxima": 3, + "minima": -7, + "dato": [] + }, + "sensTermica": { + "maxima": 3, + "minima": -8, + "dato": [] + }, + "humedadRelativa": { + "maxima": 85, + "minima": 60, + "dato": [] + }, + "uvMax": 1, + "fecha": "2021-01-11T00:00:00" + }, + { + "probPrecipitacion": [ + { + "value": 0, + "periodo": "00-24" + }, + { + "value": 0, + "periodo": "00-12" + }, + { + "value": 0, + "periodo": "12-24" + } + ], + "cotaNieveProv": [ + { + "value": "", + "periodo": "00-24" + }, + { + "value": "", + "periodo": "00-12" + }, + { + "value": "", + "periodo": "12-24" + } + ], + "estadoCielo": [ + { + "value": "12", + "periodo": "00-24", + "descripcion": "Poco nuboso" + }, + { + "value": "12", + "periodo": "00-12", + "descripcion": "Poco nuboso" + }, + { + "value": "12", + "periodo": "12-24", + "descripcion": "Poco nuboso" + } + ], + "viento": [ + { + "direccion": "C", + "velocidad": 0, + "periodo": "00-24" + }, + { + "direccion": "E", + "velocidad": 5, + "periodo": "00-12" + }, + { + "direccion": "C", + "velocidad": 0, + "periodo": "12-24" + } + ], + "rachaMax": [ + { + "value": "", + "periodo": "00-24" + }, + { + "value": "", + "periodo": "00-12" + }, + { + "value": "", + "periodo": "12-24" + } + ], + "temperatura": { + "maxima": -1, + "minima": -13, + "dato": [] + }, + "sensTermica": { + "maxima": -1, + "minima": -13, + "dato": [] + }, + "humedadRelativa": { + "maxima": 100, + "minima": 65, + "dato": [] + }, + "uvMax": 2, + "fecha": "2021-01-12T00:00:00" + }, + { + "probPrecipitacion": [ + { + "value": 0 + } + ], + "cotaNieveProv": [ + { + "value": "" + } + ], + "estadoCielo": [ + { + "value": "11", + "descripcion": "Despejado" + } + ], + "viento": [ + { + "direccion": "C", + "velocidad": 0 + } + ], + "rachaMax": [ + { + "value": "" + } + ], + "temperatura": { + "maxima": 6, + "minima": -11, + "dato": [] + }, + "sensTermica": { + "maxima": 6, + "minima": -11, + "dato": [] + }, + "humedadRelativa": { + "maxima": 100, + "minima": 65, + "dato": [] + }, + "uvMax": 2, + "fecha": "2021-01-13T00:00:00" + }, + { + "probPrecipitacion": [ + { + "value": 0 + } + ], + "cotaNieveProv": [ + { + "value": "" + } + ], + "estadoCielo": [ + { + "value": "12", + "descripcion": "Poco nuboso" + } + ], + "viento": [ + { + "direccion": "C", + "velocidad": 0 + } + ], + "rachaMax": [ + { + "value": "" + } + ], + "temperatura": { + "maxima": 6, + "minima": -7, + "dato": [] + }, + "sensTermica": { + "maxima": 6, + "minima": -7, + "dato": [] + }, + "humedadRelativa": { + "maxima": 100, + "minima": 80, + "dato": [] + }, + "fecha": "2021-01-14T00:00:00" + }, + { + "probPrecipitacion": [ + { + "value": 0 + } + ], + "cotaNieveProv": [ + { + "value": "" + } + ], + "estadoCielo": [ + { + "value": "14", + "descripcion": "Nuboso" + } + ], + "viento": [ + { + "direccion": "C", + "velocidad": 0 + } + ], + "rachaMax": [ + { + "value": "" + } + ], + "temperatura": { + "maxima": 5, + "minima": -4, + "dato": [] + }, + "sensTermica": { + "maxima": 5, + "minima": -4, + "dato": [] + }, + "humedadRelativa": { + "maxima": 100, + "minima": 55, + "dato": [] + }, + "fecha": "2021-01-15T00:00:00" + } + ] + }, + "id": 28065, + "version": 1.0 + } +] diff --git a/tests/components/aemet/fixtures/town-28065-forecast-daily.json b/tests/components/aemet/fixtures/town-28065-forecast-daily.json index 35935658c50..41103c1033f 100644 --- a/tests/components/aemet/fixtures/town-28065-forecast-daily.json +++ b/tests/components/aemet/fixtures/town-28065-forecast-daily.json @@ -1,6 +1,6 @@ { - "descripcion" : "exito", - "estado" : 200, - "datos" : "https://opendata.aemet.es/opendata/sh/64e29abb", - "metadatos" : "https://opendata.aemet.es/opendata/sh/dfd88b22" + "descripcion": "exito", + "estado": 200, + "datos": "https://opendata.aemet.es/opendata/sh/64e29abb", + "metadatos": "https://opendata.aemet.es/opendata/sh/dfd88b22" } diff --git a/tests/components/aemet/fixtures/town-28065-forecast-hourly-data.json b/tests/components/aemet/fixtures/town-28065-forecast-hourly-data.json index 2bd3a22235a..c9775b604ca 100644 --- a/tests/components/aemet/fixtures/town-28065-forecast-hourly-data.json +++ b/tests/components/aemet/fixtures/town-28065-forecast-hourly-data.json @@ -1,1416 +1,1872 @@ -[ { - "origen" : { - "productor" : "Agencia Estatal de Meteorolog�a - AEMET. Gobierno de Espa�a", - "web" : "http://www.aemet.es", - "enlace" : "http://www.aemet.es/es/eltiempo/prediccion/municipios/horas/getafe-id28065", - "language" : "es", - "copyright" : "� AEMET. Autorizado el uso de la informaci�n y su reproducci�n citando a AEMET como autora de la misma.", - "notaLegal" : "http://www.aemet.es/es/nota_legal" - }, - "elaborado" : "2021-01-09T11:47:45", - "nombre" : "Getafe", - "provincia" : "Madrid", - "prediccion" : { - "dia" : [ { - "estadoCielo" : [ { - "value" : "36n", - "periodo" : "07", - "descripcion" : "Cubierto con nieve" - }, { - "value" : "36n", - "periodo" : "08", - "descripcion" : "Cubierto con nieve" - }, { - "value" : "36", - "periodo" : "09", - "descripcion" : "Cubierto con nieve" - }, { - "value" : "36", - "periodo" : "10", - "descripcion" : "Cubierto con nieve" - }, { - "value" : "36", - "periodo" : "11", - "descripcion" : "Cubierto con nieve" - }, { - "value" : "36", - "periodo" : "12", - "descripcion" : "Cubierto con nieve" - }, { - "value" : "36", - "periodo" : "13", - "descripcion" : "Cubierto con nieve" - }, { - "value" : "46", - "periodo" : "14", - "descripcion" : "Cubierto con lluvia escasa" - }, { - "value" : "46", - "periodo" : "15", - "descripcion" : "Cubierto con lluvia escasa" - }, { - "value" : "36", - "periodo" : "16", - "descripcion" : "Cubierto con nieve" - }, { - "value" : "36", - "periodo" : "17", - "descripcion" : "Cubierto con nieve" - }, { - "value" : "74n", - "periodo" : "18", - "descripcion" : "Cubierto con nieve escasa" - }, { - "value" : "46n", - "periodo" : "19", - "descripcion" : "Cubierto con lluvia escasa" - }, { - "value" : "46n", - "periodo" : "20", - "descripcion" : "Cubierto con lluvia escasa" - }, { - "value" : "16n", - "periodo" : "21", - "descripcion" : "Cubierto" - }, { - "value" : "16n", - "periodo" : "22", - "descripcion" : "Cubierto" - }, { - "value" : "12n", - "periodo" : "23", - "descripcion" : "Poco nuboso" - } ], - "precipitacion" : [ { - "value" : "1.4", - "periodo" : "07" - }, { - "value" : "2.1", - "periodo" : "08" - }, { - "value" : "1.9", - "periodo" : "09" - }, { - "value" : "2", - "periodo" : "10" - }, { - "value" : "1.9", - "periodo" : "11" - }, { - "value" : "1.8", - "periodo" : "12" - }, { - "value" : "1.5", - "periodo" : "13" - }, { - "value" : "0.5", - "periodo" : "14" - }, { - "value" : "0.6", - "periodo" : "15" - }, { - "value" : "0.8", - "periodo" : "16" - }, { - "value" : "0.6", - "periodo" : "17" - }, { - "value" : "0.2", - "periodo" : "18" - }, { - "value" : "0.2", - "periodo" : "19" - }, { - "value" : "0.1", - "periodo" : "20" - }, { - "value" : "0", - "periodo" : "21" - }, { - "value" : "0", - "periodo" : "22" - }, { - "value" : "0", - "periodo" : "23" - } ], - "probPrecipitacion" : [ { - "value" : "", - "periodo" : "0107" - }, { - "value" : "100", - "periodo" : "0713" - }, { - "value" : "100", - "periodo" : "1319" - }, { - "value" : "100", - "periodo" : "1901" - } ], - "probTormenta" : [ { - "value" : "", - "periodo" : "0107" - }, { - "value" : "0", - "periodo" : "0713" - }, { - "value" : "0", - "periodo" : "1319" - }, { - "value" : "0", - "periodo" : "1901" - } ], - "nieve" : [ { - "value" : "1.4", - "periodo" : "07" - }, { - "value" : "2.1", - "periodo" : "08" - }, { - "value" : "1.9", - "periodo" : "09" - }, { - "value" : "2", - "periodo" : "10" - }, { - "value" : "1.9", - "periodo" : "11" - }, { - "value" : "1.8", - "periodo" : "12" - }, { - "value" : "1.2", - "periodo" : "13" - }, { - "value" : "0.1", - "periodo" : "14" - }, { - "value" : "0.2", - "periodo" : "15" - }, { - "value" : "0.6", - "periodo" : "16" - }, { - "value" : "0.6", - "periodo" : "17" - }, { - "value" : "0.2", - "periodo" : "18" - }, { - "value" : "0.1", - "periodo" : "19" - }, { - "value" : "0", - "periodo" : "20" - }, { - "value" : "0", - "periodo" : "21" - }, { - "value" : "0", - "periodo" : "22" - }, { - "value" : "0", - "periodo" : "23" - } ], - "probNieve" : [ { - "value" : "", - "periodo" : "0107" - }, { - "value" : "100", - "periodo" : "0713" - }, { - "value" : "100", - "periodo" : "1319" - }, { - "value" : "80", - "periodo" : "1901" - } ], - "temperatura" : [ { - "value" : "-1", - "periodo" : "07" - }, { - "value" : "-1", - "periodo" : "08" - }, { - "value" : "-1", - "periodo" : "09" - }, { - "value" : "-1", - "periodo" : "10" - }, { - "value" : "-1", - "periodo" : "11" - }, { - "value" : "-0", - "periodo" : "12" - }, { - "value" : "-0", - "periodo" : "13" - }, { - "value" : "0", - "periodo" : "14" - }, { - "value" : "1", - "periodo" : "15" - }, { - "value" : "1", - "periodo" : "16" - }, { - "value" : "1", - "periodo" : "17" - }, { - "value" : "1", - "periodo" : "18" - }, { - "value" : "1", - "periodo" : "19" - }, { - "value" : "1", - "periodo" : "20" - }, { - "value" : "1", - "periodo" : "21" - }, { - "value" : "1", - "periodo" : "22" - }, { - "value" : "1", - "periodo" : "23" - } ], - "sensTermica" : [ { - "value" : "-8", - "periodo" : "07" - }, { - "value" : "-7", - "periodo" : "08" - }, { - "value" : "-7", - "periodo" : "09" - }, { - "value" : "-6", - "periodo" : "10" - }, { - "value" : "-6", - "periodo" : "11" - }, { - "value" : "-4", - "periodo" : "12" - }, { - "value" : "-4", - "periodo" : "13" - }, { - "value" : "-4", - "periodo" : "14" - }, { - "value" : "-2", - "periodo" : "15" - }, { - "value" : "-2", - "periodo" : "16" - }, { - "value" : "-2", - "periodo" : "17" - }, { - "value" : "1", - "periodo" : "18" - }, { - "value" : "-2", - "periodo" : "19" - }, { - "value" : "1", - "periodo" : "20" - }, { - "value" : "1", - "periodo" : "21" - }, { - "value" : "1", - "periodo" : "22" - }, { - "value" : "-2", - "periodo" : "23" - } ], - "humedadRelativa" : [ { - "value" : "96", - "periodo" : "07" - }, { - "value" : "96", - "periodo" : "08" - }, { - "value" : "99", - "periodo" : "09" - }, { - "value" : "100", - "periodo" : "10" - }, { - "value" : "100", - "periodo" : "11" - }, { - "value" : "100", - "periodo" : "12" - }, { - "value" : "100", - "periodo" : "13" - }, { - "value" : "100", - "periodo" : "14" - }, { - "value" : "100", - "periodo" : "15" - }, { - "value" : "97", - "periodo" : "16" - }, { - "value" : "94", - "periodo" : "17" - }, { - "value" : "93", - "periodo" : "18" - }, { - "value" : "93", - "periodo" : "19" - }, { - "value" : "92", - "periodo" : "20" - }, { - "value" : "89", - "periodo" : "21" - }, { - "value" : "89", - "periodo" : "22" - }, { - "value" : "85", - "periodo" : "23" - } ], - "vientoAndRachaMax" : [ { - "direccion" : [ "NE" ], - "velocidad" : [ "28" ], - "periodo" : "07" - }, { - "value" : "41", - "periodo" : "07" - }, { - "direccion" : [ "NE" ], - "velocidad" : [ "27" ], - "periodo" : "08" - }, { - "value" : "41", - "periodo" : "08" - }, { - "direccion" : [ "NE" ], - "velocidad" : [ "25" ], - "periodo" : "09" - }, { - "value" : "39", - "periodo" : "09" - }, { - "direccion" : [ "NE" ], - "velocidad" : [ "20" ], - "periodo" : "10" - }, { - "value" : "36", - "periodo" : "10" - }, { - "direccion" : [ "NE" ], - "velocidad" : [ "17" ], - "periodo" : "11" - }, { - "value" : "29", - "periodo" : "11" - }, { - "direccion" : [ "E" ], - "velocidad" : [ "15" ], - "periodo" : "12" - }, { - "value" : "24", - "periodo" : "12" - }, { - "direccion" : [ "SE" ], - "velocidad" : [ "15" ], - "periodo" : "13" - }, { - "value" : "22", - "periodo" : "13" - }, { - "direccion" : [ "SE" ], - "velocidad" : [ "14" ], - "periodo" : "14" - }, { - "value" : "24", - "periodo" : "14" - }, { - "direccion" : [ "SE" ], - "velocidad" : [ "10" ], - "periodo" : "15" - }, { - "value" : "20", - "periodo" : "15" - }, { - "direccion" : [ "SE" ], - "velocidad" : [ "8" ], - "periodo" : "16" - }, { - "value" : "14", - "periodo" : "16" - }, { - "direccion" : [ "SE" ], - "velocidad" : [ "9" ], - "periodo" : "17" - }, { - "value" : "13", - "periodo" : "17" - }, { - "direccion" : [ "E" ], - "velocidad" : [ "7" ], - "periodo" : "18" - }, { - "value" : "13", - "periodo" : "18" - }, { - "direccion" : [ "SE" ], - "velocidad" : [ "8" ], - "periodo" : "19" - }, { - "value" : "12", - "periodo" : "19" - }, { - "direccion" : [ "SE" ], - "velocidad" : [ "6" ], - "periodo" : "20" - }, { - "value" : "12", - "periodo" : "20" - }, { - "direccion" : [ "E" ], - "velocidad" : [ "6" ], - "periodo" : "21" - }, { - "value" : "8", - "periodo" : "21" - }, { - "direccion" : [ "NE" ], - "velocidad" : [ "6" ], - "periodo" : "22" - }, { - "value" : "9", - "periodo" : "22" - }, { - "direccion" : [ "E" ], - "velocidad" : [ "8" ], - "periodo" : "23" - }, { - "value" : "11", - "periodo" : "23" - } ], - "fecha" : "2021-01-09T00:00:00", - "orto" : "08:37", - "ocaso" : "18:07" - }, { - "estadoCielo" : [ { - "value" : "12n", - "periodo" : "00", - "descripcion" : "Poco nuboso" - }, { - "value" : "81n", - "periodo" : "01", - "descripcion" : "Niebla" - }, { - "value" : "81n", - "periodo" : "02", - "descripcion" : "Niebla" - }, { - "value" : "81n", - "periodo" : "03", - "descripcion" : "Niebla" - }, { - "value" : "17n", - "periodo" : "04", - "descripcion" : "Nubes altas" - }, { - "value" : "16n", - "periodo" : "05", - "descripcion" : "Cubierto" - }, { - "value" : "16n", - "periodo" : "06", - "descripcion" : "Cubierto" - }, { - "value" : "16n", - "periodo" : "07", - "descripcion" : "Cubierto" - }, { - "value" : "16n", - "periodo" : "08", - "descripcion" : "Cubierto" - }, { - "value" : "14", - "periodo" : "09", - "descripcion" : "Nuboso" - }, { - "value" : "12", - "periodo" : "10", - "descripcion" : "Poco nuboso" - }, { - "value" : "12", - "periodo" : "11", - "descripcion" : "Poco nuboso" - }, { - "value" : "17", - "periodo" : "12", - "descripcion" : "Nubes altas" - }, { - "value" : "17", - "periodo" : "13", - "descripcion" : "Nubes altas" - }, { - "value" : "17", - "periodo" : "14", - "descripcion" : "Nubes altas" - }, { - "value" : "17", - "periodo" : "15", - "descripcion" : "Nubes altas" - }, { - "value" : "17", - "periodo" : "16", - "descripcion" : "Nubes altas" - }, { - "value" : "17", - "periodo" : "17", - "descripcion" : "Nubes altas" - }, { - "value" : "12n", - "periodo" : "18", - "descripcion" : "Poco nuboso" - }, { - "value" : "12n", - "periodo" : "19", - "descripcion" : "Poco nuboso" - }, { - "value" : "14n", - "periodo" : "20", - "descripcion" : "Nuboso" - }, { - "value" : "16n", - "periodo" : "21", - "descripcion" : "Cubierto" - }, { - "value" : "16n", - "periodo" : "22", - "descripcion" : "Cubierto" - }, { - "value" : "15n", - "periodo" : "23", - "descripcion" : "Muy nuboso" - } ], - "precipitacion" : [ { - "value" : "0", - "periodo" : "00" - }, { - "value" : "0", - "periodo" : "01" - }, { - "value" : "0", - "periodo" : "02" - }, { - "value" : "0", - "periodo" : "03" - }, { - "value" : "0", - "periodo" : "04" - }, { - "value" : "0", - "periodo" : "05" - }, { - "value" : "0", - "periodo" : "06" - }, { - "value" : "0", - "periodo" : "07" - }, { - "value" : "0", - "periodo" : "08" - }, { - "value" : "Ip", - "periodo" : "09" - }, { - "value" : "0", - "periodo" : "10" - }, { - "value" : "0", - "periodo" : "11" - }, { - "value" : "0", - "periodo" : "12" - }, { - "value" : "0", - "periodo" : "13" - }, { - "value" : "0", - "periodo" : "14" - }, { - "value" : "0", - "periodo" : "15" - }, { - "value" : "0", - "periodo" : "16" - }, { - "value" : "0", - "periodo" : "17" - }, { - "value" : "0", - "periodo" : "18" - }, { - "value" : "0", - "periodo" : "19" - }, { - "value" : "0", - "periodo" : "20" - }, { - "value" : "0", - "periodo" : "21" - }, { - "value" : "0", - "periodo" : "22" - }, { - "value" : "0", - "periodo" : "23" - } ], - "probPrecipitacion" : [ { - "value" : "10", - "periodo" : "0107" - }, { - "value" : "15", - "periodo" : "0713" - }, { - "value" : "5", - "periodo" : "1319" - }, { - "value" : "0", - "periodo" : "1901" - } ], - "probTormenta" : [ { - "value" : "0", - "periodo" : "0107" - }, { - "value" : "0", - "periodo" : "0713" - }, { - "value" : "0", - "periodo" : "1319" - }, { - "value" : "0", - "periodo" : "1901" - } ], - "nieve" : [ { - "value" : "0", - "periodo" : "00" - }, { - "value" : "0", - "periodo" : "01" - }, { - "value" : "0", - "periodo" : "02" - }, { - "value" : "0", - "periodo" : "03" - }, { - "value" : "0", - "periodo" : "04" - }, { - "value" : "0", - "periodo" : "05" - }, { - "value" : "0", - "periodo" : "06" - }, { - "value" : "0", - "periodo" : "07" - }, { - "value" : "0", - "periodo" : "08" - }, { - "value" : "Ip", - "periodo" : "09" - }, { - "value" : "0", - "periodo" : "10" - }, { - "value" : "0", - "periodo" : "11" - }, { - "value" : "0", - "periodo" : "12" - }, { - "value" : "0", - "periodo" : "13" - }, { - "value" : "0", - "periodo" : "14" - }, { - "value" : "0", - "periodo" : "15" - }, { - "value" : "0", - "periodo" : "16" - }, { - "value" : "0", - "periodo" : "17" - }, { - "value" : "0", - "periodo" : "18" - }, { - "value" : "0", - "periodo" : "19" - }, { - "value" : "0", - "periodo" : "20" - }, { - "value" : "0", - "periodo" : "21" - }, { - "value" : "0", - "periodo" : "22" - }, { - "value" : "0", - "periodo" : "23" - } ], - "probNieve" : [ { - "value" : "10", - "periodo" : "0107" - }, { - "value" : "10", - "periodo" : "0713" - }, { - "value" : "0", - "periodo" : "1319" - }, { - "value" : "0", - "periodo" : "1901" - } ], - "temperatura" : [ { - "value" : "1", - "periodo" : "00" - }, { - "value" : "0", - "periodo" : "01" - }, { - "value" : "-0", - "periodo" : "02" - }, { - "value" : "-0", - "periodo" : "03" - }, { - "value" : "-1", - "periodo" : "04" - }, { - "value" : "-1", - "periodo" : "05" - }, { - "value" : "-1", - "periodo" : "06" - }, { - "value" : "-2", - "periodo" : "07" - }, { - "value" : "-1", - "periodo" : "08" - }, { - "value" : "-1", - "periodo" : "09" - }, { - "value" : "0", - "periodo" : "10" - }, { - "value" : "2", - "periodo" : "11" - }, { - "value" : "3", - "periodo" : "12" - }, { - "value" : "3", - "periodo" : "13" - }, { - "value" : "3", - "periodo" : "14" - }, { - "value" : "4", - "periodo" : "15" - }, { - "value" : "3", - "periodo" : "16" - }, { - "value" : "2", - "periodo" : "17" - }, { - "value" : "1", - "periodo" : "18" - }, { - "value" : "1", - "periodo" : "19" - }, { - "value" : "1", - "periodo" : "20" - }, { - "value" : "1", - "periodo" : "21" - }, { - "value" : "0", - "periodo" : "22" - }, { - "value" : "-0", - "periodo" : "23" - } ], - "sensTermica" : [ { - "value" : "1", - "periodo" : "00" - }, { - "value" : "0", - "periodo" : "01" - }, { - "value" : "-0", - "periodo" : "02" - }, { - "value" : "-0", - "periodo" : "03" - }, { - "value" : "-4", - "periodo" : "04" - }, { - "value" : "-1", - "periodo" : "05" - }, { - "value" : "-4", - "periodo" : "06" - }, { - "value" : "-6", - "periodo" : "07" - }, { - "value" : "-6", - "periodo" : "08" - }, { - "value" : "-7", - "periodo" : "09" - }, { - "value" : "-5", - "periodo" : "10" - }, { - "value" : "-3", - "periodo" : "11" - }, { - "value" : "-2", - "periodo" : "12" - }, { - "value" : "-1", - "periodo" : "13" - }, { - "value" : "-1", - "periodo" : "14" - }, { - "value" : "0", - "periodo" : "15" - }, { - "value" : "-1", - "periodo" : "16" - }, { - "value" : "-2", - "periodo" : "17" - }, { - "value" : "-4", - "periodo" : "18" - }, { - "value" : "-4", - "periodo" : "19" - }, { - "value" : "-3", - "periodo" : "20" - }, { - "value" : "-4", - "periodo" : "21" - }, { - "value" : "-5", - "periodo" : "22" - }, { - "value" : "-5", - "periodo" : "23" - } ], - "humedadRelativa" : [ { - "value" : "74", - "periodo" : "00" - }, { - "value" : "71", - "periodo" : "01" - }, { - "value" : "80", - "periodo" : "02" - }, { - "value" : "84", - "periodo" : "03" - }, { - "value" : "81", - "periodo" : "04" - }, { - "value" : "78", - "periodo" : "05" - }, { - "value" : "90", - "periodo" : "06" - }, { - "value" : "100", - "periodo" : "07" - }, { - "value" : "100", - "periodo" : "08" - }, { - "value" : "93", - "periodo" : "09" - }, { - "value" : "84", - "periodo" : "10" - }, { - "value" : "78", - "periodo" : "11" - }, { - "value" : "73", - "periodo" : "12" - }, { - "value" : "74", - "periodo" : "13" - }, { - "value" : "74", - "periodo" : "14" - }, { - "value" : "73", - "periodo" : "15" - }, { - "value" : "78", - "periodo" : "16" - }, { - "value" : "79", - "periodo" : "17" - }, { - "value" : "79", - "periodo" : "18" - }, { - "value" : "77", - "periodo" : "19" - }, { - "value" : "75", - "periodo" : "20" - }, { - "value" : "77", - "periodo" : "21" - }, { - "value" : "80", - "periodo" : "22" - }, { - "value" : "80", - "periodo" : "23" - } ], - "vientoAndRachaMax" : [ { - "direccion" : [ "NE" ], - "velocidad" : [ "6" ], - "periodo" : "00" - }, { - "value" : "12", - "periodo" : "00" - }, { - "direccion" : [ "NE" ], - "velocidad" : [ "5" ], - "periodo" : "01" - }, { - "value" : "10", - "periodo" : "01" - }, { - "direccion" : [ "N" ], - "velocidad" : [ "6" ], - "periodo" : "02" - }, { - "value" : "11", - "periodo" : "02" - }, { - "direccion" : [ "N" ], - "velocidad" : [ "6" ], - "periodo" : "03" - }, { - "value" : "9", - "periodo" : "03" - }, { - "direccion" : [ "NE" ], - "velocidad" : [ "8" ], - "periodo" : "04" - }, { - "value" : "12", - "periodo" : "04" - }, { - "direccion" : [ "N" ], - "velocidad" : [ "5" ], - "periodo" : "05" - }, { - "value" : "11", - "periodo" : "05" - }, { - "direccion" : [ "N" ], - "velocidad" : [ "9" ], - "periodo" : "06" - }, { - "value" : "13", - "periodo" : "06" - }, { - "direccion" : [ "NE" ], - "velocidad" : [ "13" ], - "periodo" : "07" - }, { - "value" : "18", - "periodo" : "07" - }, { - "direccion" : [ "NE" ], - "velocidad" : [ "17" ], - "periodo" : "08" - }, { - "value" : "25", - "periodo" : "08" - }, { - "direccion" : [ "NE" ], - "velocidad" : [ "21" ], - "periodo" : "09" - }, { - "value" : "31", - "periodo" : "09" - }, { - "direccion" : [ "NE" ], - "velocidad" : [ "21" ], - "periodo" : "10" - }, { - "value" : "32", - "periodo" : "10" - }, { - "direccion" : [ "NE" ], - "velocidad" : [ "21" ], - "periodo" : "11" - }, { - "value" : "30", - "periodo" : "11" - }, { - "direccion" : [ "NE" ], - "velocidad" : [ "22" ], - "periodo" : "12" - }, { - "value" : "32", - "periodo" : "12" - }, { - "direccion" : [ "NE" ], - "velocidad" : [ "20" ], - "periodo" : "13" - }, { - "value" : "32", - "periodo" : "13" - }, { - "direccion" : [ "NE" ], - "velocidad" : [ "19" ], - "periodo" : "14" - }, { - "value" : "30", - "periodo" : "14" - }, { - "direccion" : [ "NE" ], - "velocidad" : [ "17" ], - "periodo" : "15" - }, { - "value" : "28", - "periodo" : "15" - }, { - "direccion" : [ "NE" ], - "velocidad" : [ "16" ], - "periodo" : "16" - }, { - "value" : "25", - "periodo" : "16" - }, { - "direccion" : [ "NE" ], - "velocidad" : [ "16" ], - "periodo" : "17" - }, { - "value" : "24", - "periodo" : "17" - }, { - "direccion" : [ "NE" ], - "velocidad" : [ "17" ], - "periodo" : "18" - }, { - "value" : "24", - "periodo" : "18" - }, { - "direccion" : [ "NE" ], - "velocidad" : [ "17" ], - "periodo" : "19" - }, { - "value" : "25", - "periodo" : "19" - }, { - "direccion" : [ "NE" ], - "velocidad" : [ "16" ], - "periodo" : "20" - }, { - "value" : "25", - "periodo" : "20" - }, { - "direccion" : [ "NE" ], - "velocidad" : [ "17" ], - "periodo" : "21" - }, { - "value" : "24", - "periodo" : "21" - }, { - "direccion" : [ "NE" ], - "velocidad" : [ "19" ], - "periodo" : "22" - }, { - "value" : "27", - "periodo" : "22" - }, { - "direccion" : [ "NE" ], - "velocidad" : [ "21" ], - "periodo" : "23" - }, { - "value" : "30", - "periodo" : "23" - } ], - "fecha" : "2021-01-10T00:00:00", - "orto" : "08:36", - "ocaso" : "18:08" - }, { - "estadoCielo" : [ { - "value" : "14n", - "periodo" : "00", - "descripcion" : "Nuboso" - }, { - "value" : "12n", - "periodo" : "01", - "descripcion" : "Poco nuboso" - }, { - "value" : "11n", - "periodo" : "02", - "descripcion" : "Despejado" - }, { - "value" : "11n", - "periodo" : "03", - "descripcion" : "Despejado" - }, { - "value" : "11n", - "periodo" : "04", - "descripcion" : "Despejado" - }, { - "value" : "11n", - "periodo" : "05", - "descripcion" : "Despejado" - }, { - "value" : "11n", - "periodo" : "06", - "descripcion" : "Despejado" - } ], - "precipitacion" : [ { - "value" : "0", - "periodo" : "00" - }, { - "value" : "0", - "periodo" : "01" - }, { - "value" : "0", - "periodo" : "02" - }, { - "value" : "0", - "periodo" : "03" - }, { - "value" : "0", - "periodo" : "04" - }, { - "value" : "0", - "periodo" : "05" - }, { - "value" : "0", - "periodo" : "06" - } ], - "probPrecipitacion" : [ { - "value" : "0", - "periodo" : "0107" - }, { - "value" : "", - "periodo" : "0713" - }, { - "value" : "", - "periodo" : "1319" - }, { - "value" : "", - "periodo" : "1901" - } ], - "probTormenta" : [ { - "value" : "0", - "periodo" : "0107" - }, { - "value" : "", - "periodo" : "0713" - }, { - "value" : "", - "periodo" : "1319" - }, { - "value" : "", - "periodo" : "1901" - } ], - "nieve" : [ { - "value" : "0", - "periodo" : "00" - }, { - "value" : "0", - "periodo" : "01" - }, { - "value" : "0", - "periodo" : "02" - }, { - "value" : "0", - "periodo" : "03" - }, { - "value" : "0", - "periodo" : "04" - }, { - "value" : "0", - "periodo" : "05" - }, { - "value" : "0", - "periodo" : "06" - } ], - "probNieve" : [ { - "value" : "0", - "periodo" : "0107" - }, { - "value" : "", - "periodo" : "0713" - }, { - "value" : "", - "periodo" : "1319" - }, { - "value" : "", - "periodo" : "1901" - } ], - "temperatura" : [ { - "value" : "-1", - "periodo" : "00" - }, { - "value" : "-1", - "periodo" : "01" - }, { - "value" : "-2", - "periodo" : "02" - }, { - "value" : "-2", - "periodo" : "03" - }, { - "value" : "-3", - "periodo" : "04" - }, { - "value" : "-4", - "periodo" : "05" - }, { - "value" : "-4", - "periodo" : "06" - } ], - "sensTermica" : [ { - "value" : "-6", - "periodo" : "00" - }, { - "value" : "-6", - "periodo" : "01" - }, { - "value" : "-6", - "periodo" : "02" - }, { - "value" : "-6", - "periodo" : "03" - }, { - "value" : "-7", - "periodo" : "04" - }, { - "value" : "-8", - "periodo" : "05" - }, { - "value" : "-8", - "periodo" : "06" - } ], - "humedadRelativa" : [ { - "value" : "81", - "periodo" : "00" - }, { - "value" : "79", - "periodo" : "01" - }, { - "value" : "77", - "periodo" : "02" - }, { - "value" : "76", - "periodo" : "03" - }, { - "value" : "76", - "periodo" : "04" - }, { - "value" : "76", - "periodo" : "05" - }, { - "value" : "78", - "periodo" : "06" - } ], - "vientoAndRachaMax" : [ { - "direccion" : [ "NE" ], - "velocidad" : [ "19" ], - "periodo" : "00" - }, { - "value" : "30", - "periodo" : "00" - }, { - "direccion" : [ "NE" ], - "velocidad" : [ "16" ], - "periodo" : "01" - }, { - "value" : "27", - "periodo" : "01" - }, { - "direccion" : [ "NE" ], - "velocidad" : [ "12" ], - "periodo" : "02" - }, { - "value" : "22", - "periodo" : "02" - }, { - "direccion" : [ "NE" ], - "velocidad" : [ "10" ], - "periodo" : "03" - }, { - "value" : "17", - "periodo" : "03" - }, { - "direccion" : [ "NE" ], - "velocidad" : [ "11" ], - "periodo" : "04" - }, { - "value" : "15", - "periodo" : "04" - }, { - "direccion" : [ "NE" ], - "velocidad" : [ "10" ], - "periodo" : "05" - }, { - "value" : "15", - "periodo" : "05" - }, { - "direccion" : [ "N" ], - "velocidad" : [ "10" ], - "periodo" : "06" - }, { - "value" : "15", - "periodo" : "06" - } ], - "fecha" : "2021-01-11T00:00:00", - "orto" : "08:36", - "ocaso" : "18:09" - } ] - }, - "id" : "28065", - "version" : "1.0" -} ] +[ + { + "origen": { + "productor": "Agencia Estatal de Meteorolog�a - AEMET. Gobierno de Espa�a", + "web": "http://www.aemet.es", + "enlace": "http://www.aemet.es/es/eltiempo/prediccion/municipios/horas/getafe-id28065", + "language": "es", + "copyright": "� AEMET. Autorizado el uso de la informaci�n y su reproducci�n citando a AEMET como autora de la misma.", + "notaLegal": "http://www.aemet.es/es/nota_legal" + }, + "elaborado": "2021-01-09T11:47:45", + "nombre": "Getafe", + "provincia": "Madrid", + "prediccion": { + "dia": [ + { + "estadoCielo": [ + { + "value": "36n", + "periodo": "07", + "descripcion": "Cubierto con nieve" + }, + { + "value": "36n", + "periodo": "08", + "descripcion": "Cubierto con nieve" + }, + { + "value": "36", + "periodo": "09", + "descripcion": "Cubierto con nieve" + }, + { + "value": "36", + "periodo": "10", + "descripcion": "Cubierto con nieve" + }, + { + "value": "36", + "periodo": "11", + "descripcion": "Cubierto con nieve" + }, + { + "value": "36", + "periodo": "12", + "descripcion": "Cubierto con nieve" + }, + { + "value": "36", + "periodo": "13", + "descripcion": "Cubierto con nieve" + }, + { + "value": "46", + "periodo": "14", + "descripcion": "Cubierto con lluvia escasa" + }, + { + "value": "46", + "periodo": "15", + "descripcion": "Cubierto con lluvia escasa" + }, + { + "value": "36", + "periodo": "16", + "descripcion": "Cubierto con nieve" + }, + { + "value": "36", + "periodo": "17", + "descripcion": "Cubierto con nieve" + }, + { + "value": "74n", + "periodo": "18", + "descripcion": "Cubierto con nieve escasa" + }, + { + "value": "46n", + "periodo": "19", + "descripcion": "Cubierto con lluvia escasa" + }, + { + "value": "46n", + "periodo": "20", + "descripcion": "Cubierto con lluvia escasa" + }, + { + "value": "16n", + "periodo": "21", + "descripcion": "Cubierto" + }, + { + "value": "16n", + "periodo": "22", + "descripcion": "Cubierto" + }, + { + "value": "12n", + "periodo": "23", + "descripcion": "Poco nuboso" + } + ], + "precipitacion": [ + { + "value": "1.4", + "periodo": "07" + }, + { + "value": "2.1", + "periodo": "08" + }, + { + "value": "1.9", + "periodo": "09" + }, + { + "value": "2", + "periodo": "10" + }, + { + "value": "1.9", + "periodo": "11" + }, + { + "value": "1.8", + "periodo": "12" + }, + { + "value": "1.5", + "periodo": "13" + }, + { + "value": "0.5", + "periodo": "14" + }, + { + "value": "0.6", + "periodo": "15" + }, + { + "value": "0.8", + "periodo": "16" + }, + { + "value": "0.6", + "periodo": "17" + }, + { + "value": "0.2", + "periodo": "18" + }, + { + "value": "0.2", + "periodo": "19" + }, + { + "value": "0.1", + "periodo": "20" + }, + { + "value": "0", + "periodo": "21" + }, + { + "value": "0", + "periodo": "22" + }, + { + "value": "0", + "periodo": "23" + } + ], + "probPrecipitacion": [ + { + "value": "", + "periodo": "0107" + }, + { + "value": "100", + "periodo": "0713" + }, + { + "value": "100", + "periodo": "1319" + }, + { + "value": "100", + "periodo": "1901" + } + ], + "probTormenta": [ + { + "value": "", + "periodo": "0107" + }, + { + "value": "0", + "periodo": "0713" + }, + { + "value": "0", + "periodo": "1319" + }, + { + "value": "0", + "periodo": "1901" + } + ], + "nieve": [ + { + "value": "1.4", + "periodo": "07" + }, + { + "value": "2.1", + "periodo": "08" + }, + { + "value": "1.9", + "periodo": "09" + }, + { + "value": "2", + "periodo": "10" + }, + { + "value": "1.9", + "periodo": "11" + }, + { + "value": "1.8", + "periodo": "12" + }, + { + "value": "1.2", + "periodo": "13" + }, + { + "value": "0.1", + "periodo": "14" + }, + { + "value": "0.2", + "periodo": "15" + }, + { + "value": "0.6", + "periodo": "16" + }, + { + "value": "0.6", + "periodo": "17" + }, + { + "value": "0.2", + "periodo": "18" + }, + { + "value": "0.1", + "periodo": "19" + }, + { + "value": "0", + "periodo": "20" + }, + { + "value": "0", + "periodo": "21" + }, + { + "value": "0", + "periodo": "22" + }, + { + "value": "0", + "periodo": "23" + } + ], + "probNieve": [ + { + "value": "", + "periodo": "0107" + }, + { + "value": "100", + "periodo": "0713" + }, + { + "value": "100", + "periodo": "1319" + }, + { + "value": "80", + "periodo": "1901" + } + ], + "temperatura": [ + { + "value": "-1", + "periodo": "07" + }, + { + "value": "-1", + "periodo": "08" + }, + { + "value": "-1", + "periodo": "09" + }, + { + "value": "-1", + "periodo": "10" + }, + { + "value": "-1", + "periodo": "11" + }, + { + "value": "-0", + "periodo": "12" + }, + { + "value": "-0", + "periodo": "13" + }, + { + "value": "0", + "periodo": "14" + }, + { + "value": "1", + "periodo": "15" + }, + { + "value": "1", + "periodo": "16" + }, + { + "value": "1", + "periodo": "17" + }, + { + "value": "1", + "periodo": "18" + }, + { + "value": "1", + "periodo": "19" + }, + { + "value": "1", + "periodo": "20" + }, + { + "value": "1", + "periodo": "21" + }, + { + "value": "1", + "periodo": "22" + }, + { + "value": "1", + "periodo": "23" + } + ], + "sensTermica": [ + { + "value": "-8", + "periodo": "07" + }, + { + "value": "-7", + "periodo": "08" + }, + { + "value": "-7", + "periodo": "09" + }, + { + "value": "-6", + "periodo": "10" + }, + { + "value": "-6", + "periodo": "11" + }, + { + "value": "-4", + "periodo": "12" + }, + { + "value": "-4", + "periodo": "13" + }, + { + "value": "-4", + "periodo": "14" + }, + { + "value": "-2", + "periodo": "15" + }, + { + "value": "-2", + "periodo": "16" + }, + { + "value": "-2", + "periodo": "17" + }, + { + "value": "1", + "periodo": "18" + }, + { + "value": "-2", + "periodo": "19" + }, + { + "value": "1", + "periodo": "20" + }, + { + "value": "1", + "periodo": "21" + }, + { + "value": "1", + "periodo": "22" + }, + { + "value": "-2", + "periodo": "23" + } + ], + "humedadRelativa": [ + { + "value": "96", + "periodo": "07" + }, + { + "value": "96", + "periodo": "08" + }, + { + "value": "99", + "periodo": "09" + }, + { + "value": "100", + "periodo": "10" + }, + { + "value": "100", + "periodo": "11" + }, + { + "value": "100", + "periodo": "12" + }, + { + "value": "100", + "periodo": "13" + }, + { + "value": "100", + "periodo": "14" + }, + { + "value": "100", + "periodo": "15" + }, + { + "value": "97", + "periodo": "16" + }, + { + "value": "94", + "periodo": "17" + }, + { + "value": "93", + "periodo": "18" + }, + { + "value": "93", + "periodo": "19" + }, + { + "value": "92", + "periodo": "20" + }, + { + "value": "89", + "periodo": "21" + }, + { + "value": "89", + "periodo": "22" + }, + { + "value": "85", + "periodo": "23" + } + ], + "vientoAndRachaMax": [ + { + "direccion": ["NE"], + "velocidad": ["28"], + "periodo": "07" + }, + { + "value": "41", + "periodo": "07" + }, + { + "direccion": ["NE"], + "velocidad": ["27"], + "periodo": "08" + }, + { + "value": "41", + "periodo": "08" + }, + { + "direccion": ["NE"], + "velocidad": ["25"], + "periodo": "09" + }, + { + "value": "39", + "periodo": "09" + }, + { + "direccion": ["NE"], + "velocidad": ["20"], + "periodo": "10" + }, + { + "value": "36", + "periodo": "10" + }, + { + "direccion": ["NE"], + "velocidad": ["17"], + "periodo": "11" + }, + { + "value": "29", + "periodo": "11" + }, + { + "direccion": ["E"], + "velocidad": ["15"], + "periodo": "12" + }, + { + "value": "24", + "periodo": "12" + }, + { + "direccion": ["SE"], + "velocidad": ["15"], + "periodo": "13" + }, + { + "value": "22", + "periodo": "13" + }, + { + "direccion": ["SE"], + "velocidad": ["14"], + "periodo": "14" + }, + { + "value": "24", + "periodo": "14" + }, + { + "direccion": ["SE"], + "velocidad": ["10"], + "periodo": "15" + }, + { + "value": "20", + "periodo": "15" + }, + { + "direccion": ["SE"], + "velocidad": ["8"], + "periodo": "16" + }, + { + "value": "14", + "periodo": "16" + }, + { + "direccion": ["SE"], + "velocidad": ["9"], + "periodo": "17" + }, + { + "value": "13", + "periodo": "17" + }, + { + "direccion": ["E"], + "velocidad": ["7"], + "periodo": "18" + }, + { + "value": "13", + "periodo": "18" + }, + { + "direccion": ["SE"], + "velocidad": ["8"], + "periodo": "19" + }, + { + "value": "12", + "periodo": "19" + }, + { + "direccion": ["SE"], + "velocidad": ["6"], + "periodo": "20" + }, + { + "value": "12", + "periodo": "20" + }, + { + "direccion": ["E"], + "velocidad": ["6"], + "periodo": "21" + }, + { + "value": "8", + "periodo": "21" + }, + { + "direccion": ["NE"], + "velocidad": ["6"], + "periodo": "22" + }, + { + "value": "9", + "periodo": "22" + }, + { + "direccion": ["E"], + "velocidad": ["8"], + "periodo": "23" + }, + { + "value": "11", + "periodo": "23" + } + ], + "fecha": "2021-01-09T00:00:00", + "orto": "08:37", + "ocaso": "18:07" + }, + { + "estadoCielo": [ + { + "value": "12n", + "periodo": "00", + "descripcion": "Poco nuboso" + }, + { + "value": "81n", + "periodo": "01", + "descripcion": "Niebla" + }, + { + "value": "81n", + "periodo": "02", + "descripcion": "Niebla" + }, + { + "value": "81n", + "periodo": "03", + "descripcion": "Niebla" + }, + { + "value": "17n", + "periodo": "04", + "descripcion": "Nubes altas" + }, + { + "value": "16n", + "periodo": "05", + "descripcion": "Cubierto" + }, + { + "value": "16n", + "periodo": "06", + "descripcion": "Cubierto" + }, + { + "value": "16n", + "periodo": "07", + "descripcion": "Cubierto" + }, + { + "value": "16n", + "periodo": "08", + "descripcion": "Cubierto" + }, + { + "value": "14", + "periodo": "09", + "descripcion": "Nuboso" + }, + { + "value": "12", + "periodo": "10", + "descripcion": "Poco nuboso" + }, + { + "value": "12", + "periodo": "11", + "descripcion": "Poco nuboso" + }, + { + "value": "17", + "periodo": "12", + "descripcion": "Nubes altas" + }, + { + "value": "17", + "periodo": "13", + "descripcion": "Nubes altas" + }, + { + "value": "17", + "periodo": "14", + "descripcion": "Nubes altas" + }, + { + "value": "17", + "periodo": "15", + "descripcion": "Nubes altas" + }, + { + "value": "17", + "periodo": "16", + "descripcion": "Nubes altas" + }, + { + "value": "17", + "periodo": "17", + "descripcion": "Nubes altas" + }, + { + "value": "12n", + "periodo": "18", + "descripcion": "Poco nuboso" + }, + { + "value": "12n", + "periodo": "19", + "descripcion": "Poco nuboso" + }, + { + "value": "14n", + "periodo": "20", + "descripcion": "Nuboso" + }, + { + "value": "16n", + "periodo": "21", + "descripcion": "Cubierto" + }, + { + "value": "16n", + "periodo": "22", + "descripcion": "Cubierto" + }, + { + "value": "15n", + "periodo": "23", + "descripcion": "Muy nuboso" + } + ], + "precipitacion": [ + { + "value": "0", + "periodo": "00" + }, + { + "value": "0", + "periodo": "01" + }, + { + "value": "0", + "periodo": "02" + }, + { + "value": "0", + "periodo": "03" + }, + { + "value": "0", + "periodo": "04" + }, + { + "value": "0", + "periodo": "05" + }, + { + "value": "0", + "periodo": "06" + }, + { + "value": "0", + "periodo": "07" + }, + { + "value": "0", + "periodo": "08" + }, + { + "value": "Ip", + "periodo": "09" + }, + { + "value": "0", + "periodo": "10" + }, + { + "value": "0", + "periodo": "11" + }, + { + "value": "0", + "periodo": "12" + }, + { + "value": "0", + "periodo": "13" + }, + { + "value": "0", + "periodo": "14" + }, + { + "value": "0", + "periodo": "15" + }, + { + "value": "0", + "periodo": "16" + }, + { + "value": "0", + "periodo": "17" + }, + { + "value": "0", + "periodo": "18" + }, + { + "value": "0", + "periodo": "19" + }, + { + "value": "0", + "periodo": "20" + }, + { + "value": "0", + "periodo": "21" + }, + { + "value": "0", + "periodo": "22" + }, + { + "value": "0", + "periodo": "23" + } + ], + "probPrecipitacion": [ + { + "value": "10", + "periodo": "0107" + }, + { + "value": "15", + "periodo": "0713" + }, + { + "value": "5", + "periodo": "1319" + }, + { + "value": "0", + "periodo": "1901" + } + ], + "probTormenta": [ + { + "value": "0", + "periodo": "0107" + }, + { + "value": "0", + "periodo": "0713" + }, + { + "value": "0", + "periodo": "1319" + }, + { + "value": "0", + "periodo": "1901" + } + ], + "nieve": [ + { + "value": "0", + "periodo": "00" + }, + { + "value": "0", + "periodo": "01" + }, + { + "value": "0", + "periodo": "02" + }, + { + "value": "0", + "periodo": "03" + }, + { + "value": "0", + "periodo": "04" + }, + { + "value": "0", + "periodo": "05" + }, + { + "value": "0", + "periodo": "06" + }, + { + "value": "0", + "periodo": "07" + }, + { + "value": "0", + "periodo": "08" + }, + { + "value": "Ip", + "periodo": "09" + }, + { + "value": "0", + "periodo": "10" + }, + { + "value": "0", + "periodo": "11" + }, + { + "value": "0", + "periodo": "12" + }, + { + "value": "0", + "periodo": "13" + }, + { + "value": "0", + "periodo": "14" + }, + { + "value": "0", + "periodo": "15" + }, + { + "value": "0", + "periodo": "16" + }, + { + "value": "0", + "periodo": "17" + }, + { + "value": "0", + "periodo": "18" + }, + { + "value": "0", + "periodo": "19" + }, + { + "value": "0", + "periodo": "20" + }, + { + "value": "0", + "periodo": "21" + }, + { + "value": "0", + "periodo": "22" + }, + { + "value": "0", + "periodo": "23" + } + ], + "probNieve": [ + { + "value": "10", + "periodo": "0107" + }, + { + "value": "10", + "periodo": "0713" + }, + { + "value": "0", + "periodo": "1319" + }, + { + "value": "0", + "periodo": "1901" + } + ], + "temperatura": [ + { + "value": "1", + "periodo": "00" + }, + { + "value": "0", + "periodo": "01" + }, + { + "value": "-0", + "periodo": "02" + }, + { + "value": "-0", + "periodo": "03" + }, + { + "value": "-1", + "periodo": "04" + }, + { + "value": "-1", + "periodo": "05" + }, + { + "value": "-1", + "periodo": "06" + }, + { + "value": "-2", + "periodo": "07" + }, + { + "value": "-1", + "periodo": "08" + }, + { + "value": "-1", + "periodo": "09" + }, + { + "value": "0", + "periodo": "10" + }, + { + "value": "2", + "periodo": "11" + }, + { + "value": "3", + "periodo": "12" + }, + { + "value": "3", + "periodo": "13" + }, + { + "value": "3", + "periodo": "14" + }, + { + "value": "4", + "periodo": "15" + }, + { + "value": "3", + "periodo": "16" + }, + { + "value": "2", + "periodo": "17" + }, + { + "value": "1", + "periodo": "18" + }, + { + "value": "1", + "periodo": "19" + }, + { + "value": "1", + "periodo": "20" + }, + { + "value": "1", + "periodo": "21" + }, + { + "value": "0", + "periodo": "22" + }, + { + "value": "-0", + "periodo": "23" + } + ], + "sensTermica": [ + { + "value": "1", + "periodo": "00" + }, + { + "value": "0", + "periodo": "01" + }, + { + "value": "-0", + "periodo": "02" + }, + { + "value": "-0", + "periodo": "03" + }, + { + "value": "-4", + "periodo": "04" + }, + { + "value": "-1", + "periodo": "05" + }, + { + "value": "-4", + "periodo": "06" + }, + { + "value": "-6", + "periodo": "07" + }, + { + "value": "-6", + "periodo": "08" + }, + { + "value": "-7", + "periodo": "09" + }, + { + "value": "-5", + "periodo": "10" + }, + { + "value": "-3", + "periodo": "11" + }, + { + "value": "-2", + "periodo": "12" + }, + { + "value": "-1", + "periodo": "13" + }, + { + "value": "-1", + "periodo": "14" + }, + { + "value": "0", + "periodo": "15" + }, + { + "value": "-1", + "periodo": "16" + }, + { + "value": "-2", + "periodo": "17" + }, + { + "value": "-4", + "periodo": "18" + }, + { + "value": "-4", + "periodo": "19" + }, + { + "value": "-3", + "periodo": "20" + }, + { + "value": "-4", + "periodo": "21" + }, + { + "value": "-5", + "periodo": "22" + }, + { + "value": "-5", + "periodo": "23" + } + ], + "humedadRelativa": [ + { + "value": "74", + "periodo": "00" + }, + { + "value": "71", + "periodo": "01" + }, + { + "value": "80", + "periodo": "02" + }, + { + "value": "84", + "periodo": "03" + }, + { + "value": "81", + "periodo": "04" + }, + { + "value": "78", + "periodo": "05" + }, + { + "value": "90", + "periodo": "06" + }, + { + "value": "100", + "periodo": "07" + }, + { + "value": "100", + "periodo": "08" + }, + { + "value": "93", + "periodo": "09" + }, + { + "value": "84", + "periodo": "10" + }, + { + "value": "78", + "periodo": "11" + }, + { + "value": "73", + "periodo": "12" + }, + { + "value": "74", + "periodo": "13" + }, + { + "value": "74", + "periodo": "14" + }, + { + "value": "73", + "periodo": "15" + }, + { + "value": "78", + "periodo": "16" + }, + { + "value": "79", + "periodo": "17" + }, + { + "value": "79", + "periodo": "18" + }, + { + "value": "77", + "periodo": "19" + }, + { + "value": "75", + "periodo": "20" + }, + { + "value": "77", + "periodo": "21" + }, + { + "value": "80", + "periodo": "22" + }, + { + "value": "80", + "periodo": "23" + } + ], + "vientoAndRachaMax": [ + { + "direccion": ["NE"], + "velocidad": ["6"], + "periodo": "00" + }, + { + "value": "12", + "periodo": "00" + }, + { + "direccion": ["NE"], + "velocidad": ["5"], + "periodo": "01" + }, + { + "value": "10", + "periodo": "01" + }, + { + "direccion": ["N"], + "velocidad": ["6"], + "periodo": "02" + }, + { + "value": "11", + "periodo": "02" + }, + { + "direccion": ["N"], + "velocidad": ["6"], + "periodo": "03" + }, + { + "value": "9", + "periodo": "03" + }, + { + "direccion": ["NE"], + "velocidad": ["8"], + "periodo": "04" + }, + { + "value": "12", + "periodo": "04" + }, + { + "direccion": ["N"], + "velocidad": ["5"], + "periodo": "05" + }, + { + "value": "11", + "periodo": "05" + }, + { + "direccion": ["N"], + "velocidad": ["9"], + "periodo": "06" + }, + { + "value": "13", + "periodo": "06" + }, + { + "direccion": ["NE"], + "velocidad": ["13"], + "periodo": "07" + }, + { + "value": "18", + "periodo": "07" + }, + { + "direccion": ["NE"], + "velocidad": ["17"], + "periodo": "08" + }, + { + "value": "25", + "periodo": "08" + }, + { + "direccion": ["NE"], + "velocidad": ["21"], + "periodo": "09" + }, + { + "value": "31", + "periodo": "09" + }, + { + "direccion": ["NE"], + "velocidad": ["21"], + "periodo": "10" + }, + { + "value": "32", + "periodo": "10" + }, + { + "direccion": ["NE"], + "velocidad": ["21"], + "periodo": "11" + }, + { + "value": "30", + "periodo": "11" + }, + { + "direccion": ["NE"], + "velocidad": ["22"], + "periodo": "12" + }, + { + "value": "32", + "periodo": "12" + }, + { + "direccion": ["NE"], + "velocidad": ["20"], + "periodo": "13" + }, + { + "value": "32", + "periodo": "13" + }, + { + "direccion": ["NE"], + "velocidad": ["19"], + "periodo": "14" + }, + { + "value": "30", + "periodo": "14" + }, + { + "direccion": ["NE"], + "velocidad": ["17"], + "periodo": "15" + }, + { + "value": "28", + "periodo": "15" + }, + { + "direccion": ["NE"], + "velocidad": ["16"], + "periodo": "16" + }, + { + "value": "25", + "periodo": "16" + }, + { + "direccion": ["NE"], + "velocidad": ["16"], + "periodo": "17" + }, + { + "value": "24", + "periodo": "17" + }, + { + "direccion": ["NE"], + "velocidad": ["17"], + "periodo": "18" + }, + { + "value": "24", + "periodo": "18" + }, + { + "direccion": ["NE"], + "velocidad": ["17"], + "periodo": "19" + }, + { + "value": "25", + "periodo": "19" + }, + { + "direccion": ["NE"], + "velocidad": ["16"], + "periodo": "20" + }, + { + "value": "25", + "periodo": "20" + }, + { + "direccion": ["NE"], + "velocidad": ["17"], + "periodo": "21" + }, + { + "value": "24", + "periodo": "21" + }, + { + "direccion": ["NE"], + "velocidad": ["19"], + "periodo": "22" + }, + { + "value": "27", + "periodo": "22" + }, + { + "direccion": ["NE"], + "velocidad": ["21"], + "periodo": "23" + }, + { + "value": "30", + "periodo": "23" + } + ], + "fecha": "2021-01-10T00:00:00", + "orto": "08:36", + "ocaso": "18:08" + }, + { + "estadoCielo": [ + { + "value": "14n", + "periodo": "00", + "descripcion": "Nuboso" + }, + { + "value": "12n", + "periodo": "01", + "descripcion": "Poco nuboso" + }, + { + "value": "11n", + "periodo": "02", + "descripcion": "Despejado" + }, + { + "value": "11n", + "periodo": "03", + "descripcion": "Despejado" + }, + { + "value": "11n", + "periodo": "04", + "descripcion": "Despejado" + }, + { + "value": "11n", + "periodo": "05", + "descripcion": "Despejado" + }, + { + "value": "11n", + "periodo": "06", + "descripcion": "Despejado" + } + ], + "precipitacion": [ + { + "value": "0", + "periodo": "00" + }, + { + "value": "0", + "periodo": "01" + }, + { + "value": "0", + "periodo": "02" + }, + { + "value": "0", + "periodo": "03" + }, + { + "value": "0", + "periodo": "04" + }, + { + "value": "0", + "periodo": "05" + }, + { + "value": "0", + "periodo": "06" + } + ], + "probPrecipitacion": [ + { + "value": "0", + "periodo": "0107" + }, + { + "value": "", + "periodo": "0713" + }, + { + "value": "", + "periodo": "1319" + }, + { + "value": "", + "periodo": "1901" + } + ], + "probTormenta": [ + { + "value": "0", + "periodo": "0107" + }, + { + "value": "", + "periodo": "0713" + }, + { + "value": "", + "periodo": "1319" + }, + { + "value": "", + "periodo": "1901" + } + ], + "nieve": [ + { + "value": "0", + "periodo": "00" + }, + { + "value": "0", + "periodo": "01" + }, + { + "value": "0", + "periodo": "02" + }, + { + "value": "0", + "periodo": "03" + }, + { + "value": "0", + "periodo": "04" + }, + { + "value": "0", + "periodo": "05" + }, + { + "value": "0", + "periodo": "06" + } + ], + "probNieve": [ + { + "value": "0", + "periodo": "0107" + }, + { + "value": "", + "periodo": "0713" + }, + { + "value": "", + "periodo": "1319" + }, + { + "value": "", + "periodo": "1901" + } + ], + "temperatura": [ + { + "value": "-1", + "periodo": "00" + }, + { + "value": "-1", + "periodo": "01" + }, + { + "value": "-2", + "periodo": "02" + }, + { + "value": "-2", + "periodo": "03" + }, + { + "value": "-3", + "periodo": "04" + }, + { + "value": "-4", + "periodo": "05" + }, + { + "value": "-4", + "periodo": "06" + } + ], + "sensTermica": [ + { + "value": "-6", + "periodo": "00" + }, + { + "value": "-6", + "periodo": "01" + }, + { + "value": "-6", + "periodo": "02" + }, + { + "value": "-6", + "periodo": "03" + }, + { + "value": "-7", + "periodo": "04" + }, + { + "value": "-8", + "periodo": "05" + }, + { + "value": "-8", + "periodo": "06" + } + ], + "humedadRelativa": [ + { + "value": "81", + "periodo": "00" + }, + { + "value": "79", + "periodo": "01" + }, + { + "value": "77", + "periodo": "02" + }, + { + "value": "76", + "periodo": "03" + }, + { + "value": "76", + "periodo": "04" + }, + { + "value": "76", + "periodo": "05" + }, + { + "value": "78", + "periodo": "06" + } + ], + "vientoAndRachaMax": [ + { + "direccion": ["NE"], + "velocidad": ["19"], + "periodo": "00" + }, + { + "value": "30", + "periodo": "00" + }, + { + "direccion": ["NE"], + "velocidad": ["16"], + "periodo": "01" + }, + { + "value": "27", + "periodo": "01" + }, + { + "direccion": ["NE"], + "velocidad": ["12"], + "periodo": "02" + }, + { + "value": "22", + "periodo": "02" + }, + { + "direccion": ["NE"], + "velocidad": ["10"], + "periodo": "03" + }, + { + "value": "17", + "periodo": "03" + }, + { + "direccion": ["NE"], + "velocidad": ["11"], + "periodo": "04" + }, + { + "value": "15", + "periodo": "04" + }, + { + "direccion": ["NE"], + "velocidad": ["10"], + "periodo": "05" + }, + { + "value": "15", + "periodo": "05" + }, + { + "direccion": ["N"], + "velocidad": ["10"], + "periodo": "06" + }, + { + "value": "15", + "periodo": "06" + } + ], + "fecha": "2021-01-11T00:00:00", + "orto": "08:36", + "ocaso": "18:09" + } + ] + }, + "id": "28065", + "version": "1.0" + } +] diff --git a/tests/components/aemet/fixtures/town-28065-forecast-hourly.json b/tests/components/aemet/fixtures/town-28065-forecast-hourly.json index 2fbcaaeb33e..cdcacfcb6a5 100644 --- a/tests/components/aemet/fixtures/town-28065-forecast-hourly.json +++ b/tests/components/aemet/fixtures/town-28065-forecast-hourly.json @@ -1,6 +1,6 @@ { - "descripcion" : "exito", - "estado" : 200, - "datos" : "https://opendata.aemet.es/opendata/sh/18ca1886", - "metadatos" : "https://opendata.aemet.es/opendata/sh/93a7c63d" + "descripcion": "exito", + "estado": 200, + "datos": "https://opendata.aemet.es/opendata/sh/18ca1886", + "metadatos": "https://opendata.aemet.es/opendata/sh/93a7c63d" } diff --git a/tests/components/aemet/fixtures/town-id28065.json b/tests/components/aemet/fixtures/town-id28065.json index 342b163062c..8b5f191a08c 100644 --- a/tests/components/aemet/fixtures/town-id28065.json +++ b/tests/components/aemet/fixtures/town-id28065.json @@ -1,15 +1,17 @@ -[ { - "latitud" : "40�18'14.535144\"", - "id_old" : "28325", - "url" : "getafe-id28065", - "latitud_dec" : "40.30403754", - "altitud" : "622", - "capital" : "Getafe", - "num_hab" : "173057", - "zona_comarcal" : "722802", - "destacada" : "1", - "nombre" : "Getafe", - "longitud_dec" : "-3.72935236", - "id" : "id28065", - "longitud" : "-3�43'45.668496\"" -} ] +[ + { + "latitud": "40�18'14.535144\"", + "id_old": "28325", + "url": "getafe-id28065", + "latitud_dec": "40.30403754", + "altitud": "622", + "capital": "Getafe", + "num_hab": "173057", + "zona_comarcal": "722802", + "destacada": "1", + "nombre": "Getafe", + "longitud_dec": "-3.72935236", + "id": "id28065", + "longitud": "-3�43'45.668496\"" + } +] diff --git a/tests/components/aemet/fixtures/town-list.json b/tests/components/aemet/fixtures/town-list.json index d5ed23ef935..ccadedd44fc 100644 --- a/tests/components/aemet/fixtures/town-list.json +++ b/tests/components/aemet/fixtures/town-list.json @@ -1,43 +1,47 @@ -[ { - "latitud" : "40�18'14.535144\"", - "id_old" : "28325", - "url" : "getafe-id28065", - "latitud_dec" : "40.30403754", - "altitud" : "622", - "capital" : "Getafe", - "num_hab" : "173057", - "zona_comarcal" : "722802", - "destacada" : "1", - "nombre" : "Getafe", - "longitud_dec" : "-3.72935236", - "id" : "id28065", - "longitud" : "-3�43'45.668496\"" -}, { - "latitud" : "40�19'54.277752\"", - "id_old" : "28370", - "url" : "leganes-id28074", - "latitud_dec" : "40.33174382", - "altitud" : "667", - "capital" : "Legan�s", - "num_hab" : "186696", - "zona_comarcal" : "722802", - "destacada" : "1", - "nombre" : "Legan�s", - "longitud_dec" : "-3.76655557", - "id" : "id28074", - "longitud" : "-3�45'59.600052\"" -}, { - "latitud" : "40�24'30.282876\"", - "id_old" : "28001", - "url" : "madrid-id28079", - "latitud_dec" : "40.40841191", - "altitud" : "657", - "capital" : "Madrid", - "num_hab" : "3165235", - "zona_comarcal" : "722802", - "destacada" : "1", - "nombre" : "Madrid", - "longitud_dec" : "-3.68760088", - "id" : "id28079", - "longitud" : "-3�41'15.363168\"" -} ] +[ + { + "latitud": "40�18'14.535144\"", + "id_old": "28325", + "url": "getafe-id28065", + "latitud_dec": "40.30403754", + "altitud": "622", + "capital": "Getafe", + "num_hab": "173057", + "zona_comarcal": "722802", + "destacada": "1", + "nombre": "Getafe", + "longitud_dec": "-3.72935236", + "id": "id28065", + "longitud": "-3�43'45.668496\"" + }, + { + "latitud": "40�19'54.277752\"", + "id_old": "28370", + "url": "leganes-id28074", + "latitud_dec": "40.33174382", + "altitud": "667", + "capital": "Legan�s", + "num_hab": "186696", + "zona_comarcal": "722802", + "destacada": "1", + "nombre": "Legan�s", + "longitud_dec": "-3.76655557", + "id": "id28074", + "longitud": "-3�45'59.600052\"" + }, + { + "latitud": "40�24'30.282876\"", + "id_old": "28001", + "url": "madrid-id28079", + "latitud_dec": "40.40841191", + "altitud": "657", + "capital": "Madrid", + "num_hab": "3165235", + "zona_comarcal": "722802", + "destacada": "1", + "nombre": "Madrid", + "longitud_dec": "-3.68760088", + "id": "id28079", + "longitud": "-3�41'15.363168\"" + } +] diff --git a/tests/components/aemet/test_sensor.py b/tests/components/aemet/test_sensor.py index 7887139a386..b5e7679dbe6 100644 --- a/tests/components/aemet/test_sensor.py +++ b/tests/components/aemet/test_sensor.py @@ -15,6 +15,7 @@ from .util import async_init_integration async def test_aemet_forecast_create_sensors(hass): """Test creation of forecast sensors.""" + hass.config.set_time_zone("UTC") now = dt_util.parse_datetime("2021-01-09 12:00:00+00:00") with patch("homeassistant.util.dt.now", return_value=now), patch( "homeassistant.util.dt.utcnow", return_value=now diff --git a/tests/components/aemet/test_weather.py b/tests/components/aemet/test_weather.py index 43acf4c1c87..d1f1889c807 100644 --- a/tests/components/aemet/test_weather.py +++ b/tests/components/aemet/test_weather.py @@ -30,6 +30,7 @@ from .util import async_init_integration async def test_aemet_weather(hass): """Test states of the weather.""" + hass.config.set_time_zone("UTC") now = dt_util.parse_datetime("2021-01-09 12:00:00+00:00") with patch("homeassistant.util.dt.now", return_value=now), patch( "homeassistant.util.dt.utcnow", return_value=now diff --git a/tests/components/agent_dvr/fixtures/objects.json b/tests/components/agent_dvr/fixtures/objects.json index 883679b47cb..449b0fc157c 100644 --- a/tests/components/agent_dvr/fixtures/objects.json +++ b/tests/components/agent_dvr/fixtures/objects.json @@ -1 +1,43 @@ -{"settings":{"canUpdate":true,"supportsPlugins":true,"isArmed":true,"background":"255,255,255"},"directories":[{"ID":0,"dir":"D:\\Projects\\agent-service\\AgentService\\Media\\WebServerRoot\\Media\\"}],"locations":[],"objectList": [],"profiles": [{"name":"Home","active":true,"id":0},{"name":"Away","active":false,"id":1},{"name":"Night","active":false,"id":2}],"views":[{"name":"0","mode":"column","objects":[],"maxWidth":1266,"maxHeight":1222,"backColor":"#222222","id":1,"typeID":2,"focused":false},{"name":"1","mode":"grid","objects":[]},{"name":"2","mode":"grid","objects":[]},{"name":"3","mode":"grid","objects":[]},{"name":"4","mode":"grid","objects":[]},{"name":"5","mode":"grid","objects":[]},{"name":"6","mode":"grid","objects":[]},{"name":"7","mode":"grid","objects":[]},{"name":"8","mode":"grid","objects":[]}],"rtmpStreaming":false} \ No newline at end of file +{ + "settings": { + "canUpdate": true, + "supportsPlugins": true, + "isArmed": true, + "background": "255,255,255" + }, + "directories": [ + { + "ID": 0, + "dir": "D:\\Projects\\agent-service\\AgentService\\Media\\WebServerRoot\\Media\\" + } + ], + "locations": [], + "objectList": [], + "profiles": [ + { "name": "Home", "active": true, "id": 0 }, + { "name": "Away", "active": false, "id": 1 }, + { "name": "Night", "active": false, "id": 2 } + ], + "views": [ + { + "name": "0", + "mode": "column", + "objects": [], + "maxWidth": 1266, + "maxHeight": 1222, + "backColor": "#222222", + "id": 1, + "typeID": 2, + "focused": false + }, + { "name": "1", "mode": "grid", "objects": [] }, + { "name": "2", "mode": "grid", "objects": [] }, + { "name": "3", "mode": "grid", "objects": [] }, + { "name": "4", "mode": "grid", "objects": [] }, + { "name": "5", "mode": "grid", "objects": [] }, + { "name": "6", "mode": "grid", "objects": [] }, + { "name": "7", "mode": "grid", "objects": [] }, + { "name": "8", "mode": "grid", "objects": [] } + ], + "rtmpStreaming": false +} diff --git a/tests/components/airly/fixtures/no_station.json b/tests/components/airly/fixtures/no_station.json index cc64934938f..bb14b684490 100644 --- a/tests/components/airly/fixtures/no_station.json +++ b/tests/components/airly/fixtures/no_station.json @@ -1,642 +1,790 @@ { - "current": { - "fromDateTime": "2019-10-02T05:53:00.608Z", - "tillDateTime": "2019-10-02T06:53:00.608Z", - "values": [], - "indexes": [{ - "name": "AIRLY_CAQI", - "value": null, - "level": "UNKNOWN", - "description": "There are no Airly sensors in this area yet.", - "advice": null, - "color": "#999999" - }], - "standards": [] - }, - "history": [{ - "fromDateTime": "2019-10-01T06:00:00.000Z", - "tillDateTime": "2019-10-01T07:00:00.000Z", - "values": [], - "indexes": [{ - "name": "AIRLY_CAQI", - "value": null, - "level": "UNKNOWN", - "description": "There are no Airly sensors in this area yet.", - "advice": null, - "color": "#999999" - }], - "standards": [] - }, { - "fromDateTime": "2019-10-01T07:00:00.000Z", - "tillDateTime": "2019-10-01T08:00:00.000Z", - "values": [], - "indexes": [{ - "name": "AIRLY_CAQI", - "value": null, - "level": "UNKNOWN", - "description": "There are no Airly sensors in this area yet.", - "advice": null, - "color": "#999999" - }], - "standards": [] - }, { - "fromDateTime": "2019-10-01T08:00:00.000Z", - "tillDateTime": "2019-10-01T09:00:00.000Z", - "values": [], - "indexes": [{ - "name": "AIRLY_CAQI", - "value": null, - "level": "UNKNOWN", - "description": "There are no Airly sensors in this area yet.", - "advice": null, - "color": "#999999" - }], - "standards": [] - }, { - "fromDateTime": "2019-10-01T09:00:00.000Z", - "tillDateTime": "2019-10-01T10:00:00.000Z", - "values": [], - "indexes": [{ - "name": "AIRLY_CAQI", - "value": null, - "level": "UNKNOWN", - "description": "There are no Airly sensors in this area yet.", - "advice": null, - "color": "#999999" - }], - "standards": [] - }, { - "fromDateTime": "2019-10-01T10:00:00.000Z", - "tillDateTime": "2019-10-01T11:00:00.000Z", - "values": [], - "indexes": [{ - "name": "AIRLY_CAQI", - "value": null, - "level": "UNKNOWN", - "description": "There are no Airly sensors in this area yet.", - "advice": null, - "color": "#999999" - }], - "standards": [] - }, { - "fromDateTime": "2019-10-01T11:00:00.000Z", - "tillDateTime": "2019-10-01T12:00:00.000Z", - "values": [], - "indexes": [{ - "name": "AIRLY_CAQI", - "value": null, - "level": "UNKNOWN", - "description": "There are no Airly sensors in this area yet.", - "advice": null, - "color": "#999999" - }], - "standards": [] - }, { - "fromDateTime": "2019-10-01T12:00:00.000Z", - "tillDateTime": "2019-10-01T13:00:00.000Z", - "values": [], - "indexes": [{ - "name": "AIRLY_CAQI", - "value": null, - "level": "UNKNOWN", - "description": "There are no Airly sensors in this area yet.", - "advice": null, - "color": "#999999" - }], - "standards": [] - }, { - "fromDateTime": "2019-10-01T13:00:00.000Z", - "tillDateTime": "2019-10-01T14:00:00.000Z", - "values": [], - "indexes": [{ - "name": "AIRLY_CAQI", - "value": null, - "level": "UNKNOWN", - "description": "There are no Airly sensors in this area yet.", - "advice": null, - "color": "#999999" - }], - "standards": [] - }, { - "fromDateTime": "2019-10-01T14:00:00.000Z", - "tillDateTime": "2019-10-01T15:00:00.000Z", - "values": [], - "indexes": [{ - "name": "AIRLY_CAQI", - "value": null, - "level": "UNKNOWN", - "description": "There are no Airly sensors in this area yet.", - "advice": null, - "color": "#999999" - }], - "standards": [] - }, { - "fromDateTime": "2019-10-01T15:00:00.000Z", - "tillDateTime": "2019-10-01T16:00:00.000Z", - "values": [], - "indexes": [{ - "name": "AIRLY_CAQI", - "value": null, - "level": "UNKNOWN", - "description": "There are no Airly sensors in this area yet.", - "advice": null, - "color": "#999999" - }], - "standards": [] - }, { - "fromDateTime": "2019-10-01T16:00:00.000Z", - "tillDateTime": "2019-10-01T17:00:00.000Z", - "values": [], - "indexes": [{ - "name": "AIRLY_CAQI", - "value": null, - "level": "UNKNOWN", - "description": "There are no Airly sensors in this area yet.", - "advice": null, - "color": "#999999" - }], - "standards": [] - }, { - "fromDateTime": "2019-10-01T17:00:00.000Z", - "tillDateTime": "2019-10-01T18:00:00.000Z", - "values": [], - "indexes": [{ - "name": "AIRLY_CAQI", - "value": null, - "level": "UNKNOWN", - "description": "There are no Airly sensors in this area yet.", - "advice": null, - "color": "#999999" - }], - "standards": [] - }, { - "fromDateTime": "2019-10-01T18:00:00.000Z", - "tillDateTime": "2019-10-01T19:00:00.000Z", - "values": [], - "indexes": [{ - "name": "AIRLY_CAQI", - "value": null, - "level": "UNKNOWN", - "description": "There are no Airly sensors in this area yet.", - "advice": null, - "color": "#999999" - }], - "standards": [] - }, { - "fromDateTime": "2019-10-01T19:00:00.000Z", - "tillDateTime": "2019-10-01T20:00:00.000Z", - "values": [], - "indexes": [{ - "name": "AIRLY_CAQI", - "value": null, - "level": "UNKNOWN", - "description": "There are no Airly sensors in this area yet.", - "advice": null, - "color": "#999999" - }], - "standards": [] - }, { - "fromDateTime": "2019-10-01T20:00:00.000Z", - "tillDateTime": "2019-10-01T21:00:00.000Z", - "values": [], - "indexes": [{ - "name": "AIRLY_CAQI", - "value": null, - "level": "UNKNOWN", - "description": "There are no Airly sensors in this area yet.", - "advice": null, - "color": "#999999" - }], - "standards": [] - }, { - "fromDateTime": "2019-10-01T21:00:00.000Z", - "tillDateTime": "2019-10-01T22:00:00.000Z", - "values": [], - "indexes": [{ - "name": "AIRLY_CAQI", - "value": null, - "level": "UNKNOWN", - "description": "There are no Airly sensors in this area yet.", - "advice": null, - "color": "#999999" - }], - "standards": [] - }, { - "fromDateTime": "2019-10-01T22:00:00.000Z", - "tillDateTime": "2019-10-01T23:00:00.000Z", - "values": [], - "indexes": [{ - "name": "AIRLY_CAQI", - "value": null, - "level": "UNKNOWN", - "description": "There are no Airly sensors in this area yet.", - "advice": null, - "color": "#999999" - }], - "standards": [] - }, { - "fromDateTime": "2019-10-01T23:00:00.000Z", - "tillDateTime": "2019-10-02T00:00:00.000Z", - "values": [], - "indexes": [{ - "name": "AIRLY_CAQI", - "value": null, - "level": "UNKNOWN", - "description": "There are no Airly sensors in this area yet.", - "advice": null, - "color": "#999999" - }], - "standards": [] - }, { - "fromDateTime": "2019-10-02T00:00:00.000Z", - "tillDateTime": "2019-10-02T01:00:00.000Z", - "values": [], - "indexes": [{ - "name": "AIRLY_CAQI", - "value": null, - "level": "UNKNOWN", - "description": "There are no Airly sensors in this area yet.", - "advice": null, - "color": "#999999" - }], - "standards": [] - }, { - "fromDateTime": "2019-10-02T01:00:00.000Z", - "tillDateTime": "2019-10-02T02:00:00.000Z", - "values": [], - "indexes": [{ - "name": "AIRLY_CAQI", - "value": null, - "level": "UNKNOWN", - "description": "There are no Airly sensors in this area yet.", - "advice": null, - "color": "#999999" - }], - "standards": [] - }, { - "fromDateTime": "2019-10-02T02:00:00.000Z", - "tillDateTime": "2019-10-02T03:00:00.000Z", - "values": [], - "indexes": [{ - "name": "AIRLY_CAQI", - "value": null, - "level": "UNKNOWN", - "description": "There are no Airly sensors in this area yet.", - "advice": null, - "color": "#999999" - }], - "standards": [] - }, { - "fromDateTime": "2019-10-02T03:00:00.000Z", - "tillDateTime": "2019-10-02T04:00:00.000Z", - "values": [], - "indexes": [{ - "name": "AIRLY_CAQI", - "value": null, - "level": "UNKNOWN", - "description": "There are no Airly sensors in this area yet.", - "advice": null, - "color": "#999999" - }], - "standards": [] - }, { - "fromDateTime": "2019-10-02T04:00:00.000Z", - "tillDateTime": "2019-10-02T05:00:00.000Z", - "values": [], - "indexes": [{ - "name": "AIRLY_CAQI", - "value": null, - "level": "UNKNOWN", - "description": "There are no Airly sensors in this area yet.", - "advice": null, - "color": "#999999" - }], - "standards": [] - }, { - "fromDateTime": "2019-10-02T05:00:00.000Z", - "tillDateTime": "2019-10-02T06:00:00.000Z", - "values": [], - "indexes": [{ - "name": "AIRLY_CAQI", - "value": null, - "level": "UNKNOWN", - "description": "There are no Airly sensors in this area yet.", - "advice": null, - "color": "#999999" - }], - "standards": [] - }], - "forecast": [{ - "fromDateTime": "2019-10-02T06:00:00.000Z", - "tillDateTime": "2019-10-02T07:00:00.000Z", - "values": [], - "indexes": [{ - "name": "AIRLY_CAQI", - "value": null, - "level": "UNKNOWN", - "description": "There are no Airly sensors in this area yet.", - "advice": null, - "color": "#999999" - }], - "standards": [] - }, { - "fromDateTime": "2019-10-02T07:00:00.000Z", - "tillDateTime": "2019-10-02T08:00:00.000Z", - "values": [], - "indexes": [{ - "name": "AIRLY_CAQI", - "value": null, - "level": "UNKNOWN", - "description": "There are no Airly sensors in this area yet.", - "advice": null, - "color": "#999999" - }], - "standards": [] - }, { - "fromDateTime": "2019-10-02T08:00:00.000Z", - "tillDateTime": "2019-10-02T09:00:00.000Z", - "values": [], - "indexes": [{ - "name": "AIRLY_CAQI", - "value": null, - "level": "UNKNOWN", - "description": "There are no Airly sensors in this area yet.", - "advice": null, - "color": "#999999" - }], - "standards": [] - }, { - "fromDateTime": "2019-10-02T09:00:00.000Z", - "tillDateTime": "2019-10-02T10:00:00.000Z", - "values": [], - "indexes": [{ - "name": "AIRLY_CAQI", - "value": null, - "level": "UNKNOWN", - "description": "There are no Airly sensors in this area yet.", - "advice": null, - "color": "#999999" - }], - "standards": [] - }, { - "fromDateTime": "2019-10-02T10:00:00.000Z", - "tillDateTime": "2019-10-02T11:00:00.000Z", - "values": [], - "indexes": [{ - "name": "AIRLY_CAQI", - "value": null, - "level": "UNKNOWN", - "description": "There are no Airly sensors in this area yet.", - "advice": null, - "color": "#999999" - }], - "standards": [] - }, { - "fromDateTime": "2019-10-02T11:00:00.000Z", - "tillDateTime": "2019-10-02T12:00:00.000Z", - "values": [], - "indexes": [{ - "name": "AIRLY_CAQI", - "value": null, - "level": "UNKNOWN", - "description": "There are no Airly sensors in this area yet.", - "advice": null, - "color": "#999999" - }], - "standards": [] - }, { - "fromDateTime": "2019-10-02T12:00:00.000Z", - "tillDateTime": "2019-10-02T13:00:00.000Z", - "values": [], - "indexes": [{ - "name": "AIRLY_CAQI", - "value": null, - "level": "UNKNOWN", - "description": "There are no Airly sensors in this area yet.", - "advice": null, - "color": "#999999" - }], - "standards": [] - }, { - "fromDateTime": "2019-10-02T13:00:00.000Z", - "tillDateTime": "2019-10-02T14:00:00.000Z", - "values": [], - "indexes": [{ - "name": "AIRLY_CAQI", - "value": null, - "level": "UNKNOWN", - "description": "There are no Airly sensors in this area yet.", - "advice": null, - "color": "#999999" - }], - "standards": [] - }, { - "fromDateTime": "2019-10-02T14:00:00.000Z", - "tillDateTime": "2019-10-02T15:00:00.000Z", - "values": [], - "indexes": [{ - "name": "AIRLY_CAQI", - "value": null, - "level": "UNKNOWN", - "description": "There are no Airly sensors in this area yet.", - "advice": null, - "color": "#999999" - }], - "standards": [] - }, { - "fromDateTime": "2019-10-02T15:00:00.000Z", - "tillDateTime": "2019-10-02T16:00:00.000Z", - "values": [], - "indexes": [{ - "name": "AIRLY_CAQI", - "value": null, - "level": "UNKNOWN", - "description": "There are no Airly sensors in this area yet.", - "advice": null, - "color": "#999999" - }], - "standards": [] - }, { - "fromDateTime": "2019-10-02T16:00:00.000Z", - "tillDateTime": "2019-10-02T17:00:00.000Z", - "values": [], - "indexes": [{ - "name": "AIRLY_CAQI", - "value": null, - "level": "UNKNOWN", - "description": "There are no Airly sensors in this area yet.", - "advice": null, - "color": "#999999" - }], - "standards": [] - }, { - "fromDateTime": "2019-10-02T17:00:00.000Z", - "tillDateTime": "2019-10-02T18:00:00.000Z", - "values": [], - "indexes": [{ - "name": "AIRLY_CAQI", - "value": null, - "level": "UNKNOWN", - "description": "There are no Airly sensors in this area yet.", - "advice": null, - "color": "#999999" - }], - "standards": [] - }, { - "fromDateTime": "2019-10-02T18:00:00.000Z", - "tillDateTime": "2019-10-02T19:00:00.000Z", - "values": [], - "indexes": [{ - "name": "AIRLY_CAQI", - "value": null, - "level": "UNKNOWN", - "description": "There are no Airly sensors in this area yet.", - "advice": null, - "color": "#999999" - }], - "standards": [] - }, { - "fromDateTime": "2019-10-02T19:00:00.000Z", - "tillDateTime": "2019-10-02T20:00:00.000Z", - "values": [], - "indexes": [{ - "name": "AIRLY_CAQI", - "value": null, - "level": "UNKNOWN", - "description": "There are no Airly sensors in this area yet.", - "advice": null, - "color": "#999999" - }], - "standards": [] - }, { - "fromDateTime": "2019-10-02T20:00:00.000Z", - "tillDateTime": "2019-10-02T21:00:00.000Z", - "values": [], - "indexes": [{ - "name": "AIRLY_CAQI", - "value": null, - "level": "UNKNOWN", - "description": "There are no Airly sensors in this area yet.", - "advice": null, - "color": "#999999" - }], - "standards": [] - }, { - "fromDateTime": "2019-10-02T21:00:00.000Z", - "tillDateTime": "2019-10-02T22:00:00.000Z", - "values": [], - "indexes": [{ - "name": "AIRLY_CAQI", - "value": null, - "level": "UNKNOWN", - "description": "There are no Airly sensors in this area yet.", - "advice": null, - "color": "#999999" - }], - "standards": [] - }, { - "fromDateTime": "2019-10-02T22:00:00.000Z", - "tillDateTime": "2019-10-02T23:00:00.000Z", - "values": [], - "indexes": [{ - "name": "AIRLY_CAQI", - "value": null, - "level": "UNKNOWN", - "description": "There are no Airly sensors in this area yet.", - "advice": null, - "color": "#999999" - }], - "standards": [] - }, { - "fromDateTime": "2019-10-02T23:00:00.000Z", - "tillDateTime": "2019-10-03T00:00:00.000Z", - "values": [], - "indexes": [{ - "name": "AIRLY_CAQI", - "value": null, - "level": "UNKNOWN", - "description": "There are no Airly sensors in this area yet.", - "advice": null, - "color": "#999999" - }], - "standards": [] - }, { - "fromDateTime": "2019-10-03T00:00:00.000Z", - "tillDateTime": "2019-10-03T01:00:00.000Z", - "values": [], - "indexes": [{ - "name": "AIRLY_CAQI", - "value": null, - "level": "UNKNOWN", - "description": "There are no Airly sensors in this area yet.", - "advice": null, - "color": "#999999" - }], - "standards": [] - }, { - "fromDateTime": "2019-10-03T01:00:00.000Z", - "tillDateTime": "2019-10-03T02:00:00.000Z", - "values": [], - "indexes": [{ - "name": "AIRLY_CAQI", - "value": null, - "level": "UNKNOWN", - "description": "There are no Airly sensors in this area yet.", - "advice": null, - "color": "#999999" - }], - "standards": [] - }, { - "fromDateTime": "2019-10-03T02:00:00.000Z", - "tillDateTime": "2019-10-03T03:00:00.000Z", - "values": [], - "indexes": [{ - "name": "AIRLY_CAQI", - "value": null, - "level": "UNKNOWN", - "description": "There are no Airly sensors in this area yet.", - "advice": null, - "color": "#999999" - }], - "standards": [] - }, { - "fromDateTime": "2019-10-03T03:00:00.000Z", - "tillDateTime": "2019-10-03T04:00:00.000Z", - "values": [], - "indexes": [{ - "name": "AIRLY_CAQI", - "value": null, - "level": "UNKNOWN", - "description": "There are no Airly sensors in this area yet.", - "advice": null, - "color": "#999999" - }], - "standards": [] - }, { - "fromDateTime": "2019-10-03T04:00:00.000Z", - "tillDateTime": "2019-10-03T05:00:00.000Z", - "values": [], - "indexes": [{ - "name": "AIRLY_CAQI", - "value": null, - "level": "UNKNOWN", - "description": "There are no Airly sensors in this area yet.", - "advice": null, - "color": "#999999" - }], - "standards": [] - }, { - "fromDateTime": "2019-10-03T05:00:00.000Z", - "tillDateTime": "2019-10-03T06:00:00.000Z", - "values": [], - "indexes": [{ - "name": "AIRLY_CAQI", - "value": null, - "level": "UNKNOWN", - "description": "There are no Airly sensors in this area yet.", - "advice": null, - "color": "#999999" - }], - "standards": [] - }] -} \ No newline at end of file + "current": { + "fromDateTime": "2019-10-02T05:53:00.608Z", + "tillDateTime": "2019-10-02T06:53:00.608Z", + "values": [], + "indexes": [ + { + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + } + ], + "standards": [] + }, + "history": [ + { + "fromDateTime": "2019-10-01T06:00:00.000Z", + "tillDateTime": "2019-10-01T07:00:00.000Z", + "values": [], + "indexes": [ + { + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + } + ], + "standards": [] + }, + { + "fromDateTime": "2019-10-01T07:00:00.000Z", + "tillDateTime": "2019-10-01T08:00:00.000Z", + "values": [], + "indexes": [ + { + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + } + ], + "standards": [] + }, + { + "fromDateTime": "2019-10-01T08:00:00.000Z", + "tillDateTime": "2019-10-01T09:00:00.000Z", + "values": [], + "indexes": [ + { + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + } + ], + "standards": [] + }, + { + "fromDateTime": "2019-10-01T09:00:00.000Z", + "tillDateTime": "2019-10-01T10:00:00.000Z", + "values": [], + "indexes": [ + { + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + } + ], + "standards": [] + }, + { + "fromDateTime": "2019-10-01T10:00:00.000Z", + "tillDateTime": "2019-10-01T11:00:00.000Z", + "values": [], + "indexes": [ + { + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + } + ], + "standards": [] + }, + { + "fromDateTime": "2019-10-01T11:00:00.000Z", + "tillDateTime": "2019-10-01T12:00:00.000Z", + "values": [], + "indexes": [ + { + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + } + ], + "standards": [] + }, + { + "fromDateTime": "2019-10-01T12:00:00.000Z", + "tillDateTime": "2019-10-01T13:00:00.000Z", + "values": [], + "indexes": [ + { + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + } + ], + "standards": [] + }, + { + "fromDateTime": "2019-10-01T13:00:00.000Z", + "tillDateTime": "2019-10-01T14:00:00.000Z", + "values": [], + "indexes": [ + { + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + } + ], + "standards": [] + }, + { + "fromDateTime": "2019-10-01T14:00:00.000Z", + "tillDateTime": "2019-10-01T15:00:00.000Z", + "values": [], + "indexes": [ + { + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + } + ], + "standards": [] + }, + { + "fromDateTime": "2019-10-01T15:00:00.000Z", + "tillDateTime": "2019-10-01T16:00:00.000Z", + "values": [], + "indexes": [ + { + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + } + ], + "standards": [] + }, + { + "fromDateTime": "2019-10-01T16:00:00.000Z", + "tillDateTime": "2019-10-01T17:00:00.000Z", + "values": [], + "indexes": [ + { + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + } + ], + "standards": [] + }, + { + "fromDateTime": "2019-10-01T17:00:00.000Z", + "tillDateTime": "2019-10-01T18:00:00.000Z", + "values": [], + "indexes": [ + { + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + } + ], + "standards": [] + }, + { + "fromDateTime": "2019-10-01T18:00:00.000Z", + "tillDateTime": "2019-10-01T19:00:00.000Z", + "values": [], + "indexes": [ + { + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + } + ], + "standards": [] + }, + { + "fromDateTime": "2019-10-01T19:00:00.000Z", + "tillDateTime": "2019-10-01T20:00:00.000Z", + "values": [], + "indexes": [ + { + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + } + ], + "standards": [] + }, + { + "fromDateTime": "2019-10-01T20:00:00.000Z", + "tillDateTime": "2019-10-01T21:00:00.000Z", + "values": [], + "indexes": [ + { + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + } + ], + "standards": [] + }, + { + "fromDateTime": "2019-10-01T21:00:00.000Z", + "tillDateTime": "2019-10-01T22:00:00.000Z", + "values": [], + "indexes": [ + { + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + } + ], + "standards": [] + }, + { + "fromDateTime": "2019-10-01T22:00:00.000Z", + "tillDateTime": "2019-10-01T23:00:00.000Z", + "values": [], + "indexes": [ + { + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + } + ], + "standards": [] + }, + { + "fromDateTime": "2019-10-01T23:00:00.000Z", + "tillDateTime": "2019-10-02T00:00:00.000Z", + "values": [], + "indexes": [ + { + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + } + ], + "standards": [] + }, + { + "fromDateTime": "2019-10-02T00:00:00.000Z", + "tillDateTime": "2019-10-02T01:00:00.000Z", + "values": [], + "indexes": [ + { + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + } + ], + "standards": [] + }, + { + "fromDateTime": "2019-10-02T01:00:00.000Z", + "tillDateTime": "2019-10-02T02:00:00.000Z", + "values": [], + "indexes": [ + { + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + } + ], + "standards": [] + }, + { + "fromDateTime": "2019-10-02T02:00:00.000Z", + "tillDateTime": "2019-10-02T03:00:00.000Z", + "values": [], + "indexes": [ + { + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + } + ], + "standards": [] + }, + { + "fromDateTime": "2019-10-02T03:00:00.000Z", + "tillDateTime": "2019-10-02T04:00:00.000Z", + "values": [], + "indexes": [ + { + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + } + ], + "standards": [] + }, + { + "fromDateTime": "2019-10-02T04:00:00.000Z", + "tillDateTime": "2019-10-02T05:00:00.000Z", + "values": [], + "indexes": [ + { + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + } + ], + "standards": [] + }, + { + "fromDateTime": "2019-10-02T05:00:00.000Z", + "tillDateTime": "2019-10-02T06:00:00.000Z", + "values": [], + "indexes": [ + { + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + } + ], + "standards": [] + } + ], + "forecast": [ + { + "fromDateTime": "2019-10-02T06:00:00.000Z", + "tillDateTime": "2019-10-02T07:00:00.000Z", + "values": [], + "indexes": [ + { + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + } + ], + "standards": [] + }, + { + "fromDateTime": "2019-10-02T07:00:00.000Z", + "tillDateTime": "2019-10-02T08:00:00.000Z", + "values": [], + "indexes": [ + { + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + } + ], + "standards": [] + }, + { + "fromDateTime": "2019-10-02T08:00:00.000Z", + "tillDateTime": "2019-10-02T09:00:00.000Z", + "values": [], + "indexes": [ + { + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + } + ], + "standards": [] + }, + { + "fromDateTime": "2019-10-02T09:00:00.000Z", + "tillDateTime": "2019-10-02T10:00:00.000Z", + "values": [], + "indexes": [ + { + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + } + ], + "standards": [] + }, + { + "fromDateTime": "2019-10-02T10:00:00.000Z", + "tillDateTime": "2019-10-02T11:00:00.000Z", + "values": [], + "indexes": [ + { + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + } + ], + "standards": [] + }, + { + "fromDateTime": "2019-10-02T11:00:00.000Z", + "tillDateTime": "2019-10-02T12:00:00.000Z", + "values": [], + "indexes": [ + { + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + } + ], + "standards": [] + }, + { + "fromDateTime": "2019-10-02T12:00:00.000Z", + "tillDateTime": "2019-10-02T13:00:00.000Z", + "values": [], + "indexes": [ + { + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + } + ], + "standards": [] + }, + { + "fromDateTime": "2019-10-02T13:00:00.000Z", + "tillDateTime": "2019-10-02T14:00:00.000Z", + "values": [], + "indexes": [ + { + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + } + ], + "standards": [] + }, + { + "fromDateTime": "2019-10-02T14:00:00.000Z", + "tillDateTime": "2019-10-02T15:00:00.000Z", + "values": [], + "indexes": [ + { + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + } + ], + "standards": [] + }, + { + "fromDateTime": "2019-10-02T15:00:00.000Z", + "tillDateTime": "2019-10-02T16:00:00.000Z", + "values": [], + "indexes": [ + { + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + } + ], + "standards": [] + }, + { + "fromDateTime": "2019-10-02T16:00:00.000Z", + "tillDateTime": "2019-10-02T17:00:00.000Z", + "values": [], + "indexes": [ + { + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + } + ], + "standards": [] + }, + { + "fromDateTime": "2019-10-02T17:00:00.000Z", + "tillDateTime": "2019-10-02T18:00:00.000Z", + "values": [], + "indexes": [ + { + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + } + ], + "standards": [] + }, + { + "fromDateTime": "2019-10-02T18:00:00.000Z", + "tillDateTime": "2019-10-02T19:00:00.000Z", + "values": [], + "indexes": [ + { + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + } + ], + "standards": [] + }, + { + "fromDateTime": "2019-10-02T19:00:00.000Z", + "tillDateTime": "2019-10-02T20:00:00.000Z", + "values": [], + "indexes": [ + { + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + } + ], + "standards": [] + }, + { + "fromDateTime": "2019-10-02T20:00:00.000Z", + "tillDateTime": "2019-10-02T21:00:00.000Z", + "values": [], + "indexes": [ + { + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + } + ], + "standards": [] + }, + { + "fromDateTime": "2019-10-02T21:00:00.000Z", + "tillDateTime": "2019-10-02T22:00:00.000Z", + "values": [], + "indexes": [ + { + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + } + ], + "standards": [] + }, + { + "fromDateTime": "2019-10-02T22:00:00.000Z", + "tillDateTime": "2019-10-02T23:00:00.000Z", + "values": [], + "indexes": [ + { + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + } + ], + "standards": [] + }, + { + "fromDateTime": "2019-10-02T23:00:00.000Z", + "tillDateTime": "2019-10-03T00:00:00.000Z", + "values": [], + "indexes": [ + { + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + } + ], + "standards": [] + }, + { + "fromDateTime": "2019-10-03T00:00:00.000Z", + "tillDateTime": "2019-10-03T01:00:00.000Z", + "values": [], + "indexes": [ + { + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + } + ], + "standards": [] + }, + { + "fromDateTime": "2019-10-03T01:00:00.000Z", + "tillDateTime": "2019-10-03T02:00:00.000Z", + "values": [], + "indexes": [ + { + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + } + ], + "standards": [] + }, + { + "fromDateTime": "2019-10-03T02:00:00.000Z", + "tillDateTime": "2019-10-03T03:00:00.000Z", + "values": [], + "indexes": [ + { + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + } + ], + "standards": [] + }, + { + "fromDateTime": "2019-10-03T03:00:00.000Z", + "tillDateTime": "2019-10-03T04:00:00.000Z", + "values": [], + "indexes": [ + { + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + } + ], + "standards": [] + }, + { + "fromDateTime": "2019-10-03T04:00:00.000Z", + "tillDateTime": "2019-10-03T05:00:00.000Z", + "values": [], + "indexes": [ + { + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + } + ], + "standards": [] + }, + { + "fromDateTime": "2019-10-03T05:00:00.000Z", + "tillDateTime": "2019-10-03T06:00:00.000Z", + "values": [], + "indexes": [ + { + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + } + ], + "standards": [] + } + ] +} diff --git a/tests/components/airly/fixtures/valid_station.json b/tests/components/airly/fixtures/valid_station.json index 656c62c04c2..c21c40b14a0 100644 --- a/tests/components/airly/fixtures/valid_station.json +++ b/tests/components/airly/fixtures/valid_station.json @@ -1,1726 +1,2268 @@ { - "current": { - "fromDateTime": "2019-10-02T05:54:57.204Z", - "tillDateTime": "2019-10-02T06:54:57.204Z", - "values": [{ - "name": "PM1", - "value": 9.23 - }, { - "name": "PM25", - "value": 13.71 - }, { - "name": "PM10", - "value": 18.58 - }, { - "name": "PRESSURE", - "value": 1000.87 - }, { - "name": "HUMIDITY", - "value": 92.84 - }, { - "name": "TEMPERATURE", - "value": 14.23 - }], - "indexes": [{ - "name": "AIRLY_CAQI", - "value": 22.85, - "level": "VERY_LOW", - "description": "Great air here today!", - "advice": "Great air!", - "color": "#6BC926" - }], - "standards": [{ - "name": "WHO", - "pollutant": "PM25", - "limit": 25.0, - "percent": 54.84 - }, { - "name": "WHO", - "pollutant": "PM10", - "limit": 50.0, - "percent": 37.17 - }] - }, - "history": [{ - "fromDateTime": "2019-10-01T06:00:00.000Z", - "tillDateTime": "2019-10-01T07:00:00.000Z", - "values": [{ - "name": "PM1", - "value": 5.95 - }, { - "name": "PM25", - "value": 8.54 - }, { - "name": "PM10", - "value": 11.46 - }, { - "name": "PRESSURE", - "value": 1009.61 - }, { - "name": "HUMIDITY", - "value": 97.6 - }, { - "name": "TEMPERATURE", - "value": 9.71 - }], - "indexes": [{ - "name": "AIRLY_CAQI", - "value": 14.24, - "level": "VERY_LOW", - "description": "Great air here today!", - "advice": "Green equals clean!", - "color": "#6BC926" - }], - "standards": [{ - "name": "WHO", - "pollutant": "PM25", - "limit": 25.0, - "percent": 34.18 - }, { - "name": "WHO", - "pollutant": "PM10", - "limit": 50.0, - "percent": 22.91 - }] - }, { - "fromDateTime": "2019-10-01T07:00:00.000Z", - "tillDateTime": "2019-10-01T08:00:00.000Z", - "values": [{ - "name": "PM1", - "value": 4.2 - }, { - "name": "PM25", - "value": 5.88 - }, { - "name": "PM10", - "value": 7.88 - }, { - "name": "PRESSURE", - "value": 1009.13 - }, { - "name": "HUMIDITY", - "value": 90.84 - }, { - "name": "TEMPERATURE", - "value": 12.65 - }], - "indexes": [{ - "name": "AIRLY_CAQI", - "value": 9.81, - "level": "VERY_LOW", - "description": "Great air here today!", - "advice": "Dear me, how wonderful!", - "color": "#6BC926" - }], - "standards": [{ - "name": "WHO", - "pollutant": "PM25", - "limit": 25.0, - "percent": 23.53 - }, { - "name": "WHO", - "pollutant": "PM10", - "limit": 50.0, - "percent": 15.75 - }] - }, { - "fromDateTime": "2019-10-01T08:00:00.000Z", - "tillDateTime": "2019-10-01T09:00:00.000Z", - "values": [{ - "name": "PM1", - "value": 3.63 - }, { - "name": "PM25", - "value": 5.56 - }, { - "name": "PM10", - "value": 7.71 - }, { - "name": "PRESSURE", - "value": 1008.27 - }, { - "name": "HUMIDITY", - "value": 84.61 - }, { - "name": "TEMPERATURE", - "value": 15.57 - }], - "indexes": [{ - "name": "AIRLY_CAQI", - "value": 9.26, - "level": "VERY_LOW", - "description": "Great air here today!", - "advice": "Dear me, how wonderful!", - "color": "#6BC926" - }], - "standards": [{ - "name": "WHO", - "pollutant": "PM25", - "limit": 25.0, - "percent": 22.23 - }, { - "name": "WHO", - "pollutant": "PM10", - "limit": 50.0, - "percent": 15.42 - }] - }, { - "fromDateTime": "2019-10-01T09:00:00.000Z", - "tillDateTime": "2019-10-01T10:00:00.000Z", - "values": [{ - "name": "PM1", - "value": 2.9 - }, { - "name": "PM25", - "value": 3.93 - }, { - "name": "PM10", - "value": 5.24 - }, { - "name": "PRESSURE", - "value": 1007.57 - }, { - "name": "HUMIDITY", - "value": 79.52 - }, { - "name": "TEMPERATURE", - "value": 16.57 - }], - "indexes": [{ - "name": "AIRLY_CAQI", - "value": 6.56, - "level": "VERY_LOW", - "description": "Great air here today!", - "advice": "Breathe deep! The air is clean!", - "color": "#6BC926" - }], - "standards": [{ - "name": "WHO", - "pollutant": "PM25", - "limit": 25.0, - "percent": 15.74 - }, { - "name": "WHO", - "pollutant": "PM10", - "limit": 50.0, - "percent": 10.48 - }] - }, { - "fromDateTime": "2019-10-01T10:00:00.000Z", - "tillDateTime": "2019-10-01T11:00:00.000Z", - "values": [{ - "name": "PM1", - "value": 2.45 - }, { - "name": "PM25", - "value": 3.33 - }, { - "name": "PM10", - "value": 4.52 - }, { - "name": "PRESSURE", - "value": 1006.75 - }, { - "name": "HUMIDITY", - "value": 74.09 - }, { - "name": "TEMPERATURE", - "value": 16.95 - }], - "indexes": [{ - "name": "AIRLY_CAQI", - "value": 5.55, - "level": "VERY_LOW", - "description": "Great air here today!", - "advice": "The air is grand today. ;)", - "color": "#6BC926" - }], - "standards": [{ - "name": "WHO", - "pollutant": "PM25", - "limit": 25.0, - "percent": 13.31 - }, { - "name": "WHO", - "pollutant": "PM10", - "limit": 50.0, - "percent": 9.04 - }] - }, { - "fromDateTime": "2019-10-01T11:00:00.000Z", - "tillDateTime": "2019-10-01T12:00:00.000Z", - "values": [{ - "name": "PM1", - "value": 2.0 - }, { - "name": "PM25", - "value": 2.93 - }, { - "name": "PM10", - "value": 3.98 - }, { - "name": "PRESSURE", - "value": 1005.71 - }, { - "name": "HUMIDITY", - "value": 69.06 - }, { - "name": "TEMPERATURE", - "value": 17.31 - }], - "indexes": [{ - "name": "AIRLY_CAQI", - "value": 4.89, - "level": "VERY_LOW", - "description": "Great air here today!", - "advice": "Green equals clean!", - "color": "#6BC926" - }], - "standards": [{ - "name": "WHO", - "pollutant": "PM25", - "limit": 25.0, - "percent": 11.74 - }, { - "name": "WHO", - "pollutant": "PM10", - "limit": 50.0, - "percent": 7.96 - }] - }, { - "fromDateTime": "2019-10-01T12:00:00.000Z", - "tillDateTime": "2019-10-01T13:00:00.000Z", - "values": [{ - "name": "PM1", - "value": 1.92 - }, { - "name": "PM25", - "value": 2.69 - }, { - "name": "PM10", - "value": 3.68 - }, { - "name": "PRESSURE", - "value": 1005.03 - }, { - "name": "HUMIDITY", - "value": 65.08 - }, { - "name": "TEMPERATURE", - "value": 17.47 - }], - "indexes": [{ - "name": "AIRLY_CAQI", - "value": 4.49, - "level": "VERY_LOW", - "description": "Great air here today!", - "advice": "Enjoy life!", - "color": "#6BC926" - }], - "standards": [{ - "name": "WHO", - "pollutant": "PM25", - "limit": 25.0, - "percent": 10.77 - }, { - "name": "WHO", - "pollutant": "PM10", - "limit": 50.0, - "percent": 7.36 - }] - }, { - "fromDateTime": "2019-10-01T13:00:00.000Z", - "tillDateTime": "2019-10-01T14:00:00.000Z", - "values": [{ - "name": "PM1", - "value": 1.79 - }, { - "name": "PM25", - "value": 2.57 - }, { - "name": "PM10", - "value": 3.53 - }, { - "name": "PRESSURE", - "value": 1004.26 - }, { - "name": "HUMIDITY", - "value": 63.72 - }, { - "name": "TEMPERATURE", - "value": 17.91 - }], - "indexes": [{ - "name": "AIRLY_CAQI", - "value": 4.29, - "level": "VERY_LOW", - "description": "Great air here today!", - "advice": "Great air!", - "color": "#6BC926" - }], - "standards": [{ - "name": "WHO", - "pollutant": "PM25", - "limit": 25.0, - "percent": 10.29 - }, { - "name": "WHO", - "pollutant": "PM10", - "limit": 50.0, - "percent": 7.06 - }] - }, { - "fromDateTime": "2019-10-01T14:00:00.000Z", - "tillDateTime": "2019-10-01T15:00:00.000Z", - "values": [{ - "name": "PM1", - "value": 2.06 - }, { - "name": "PM25", - "value": 3.08 - }, { - "name": "PM10", - "value": 4.23 - }, { - "name": "PRESSURE", - "value": 1003.46 - }, { - "name": "HUMIDITY", - "value": 64.44 - }, { - "name": "TEMPERATURE", - "value": 17.84 - }], - "indexes": [{ - "name": "AIRLY_CAQI", - "value": 5.14, - "level": "VERY_LOW", - "description": "Great air here today!", - "advice": "The air is grand today. ;)", - "color": "#6BC926" - }], - "standards": [{ - "name": "WHO", - "pollutant": "PM25", - "limit": 25.0, - "percent": 12.33 - }, { - "name": "WHO", - "pollutant": "PM10", - "limit": 50.0, - "percent": 8.47 - }] - }, { - "fromDateTime": "2019-10-01T15:00:00.000Z", - "tillDateTime": "2019-10-01T16:00:00.000Z", - "values": [{ - "name": "PM1", - "value": 3.17 - }, { - "name": "PM25", - "value": 4.61 - }, { - "name": "PM10", - "value": 6.25 - }, { - "name": "PRESSURE", - "value": 1003.18 - }, { - "name": "HUMIDITY", - "value": 65.32 - }, { - "name": "TEMPERATURE", - "value": 18.08 - }], - "indexes": [{ - "name": "AIRLY_CAQI", - "value": 7.68, - "level": "VERY_LOW", - "description": "Great air here today!", - "advice": "Green, green, green!", - "color": "#6BC926" - }], - "standards": [{ - "name": "WHO", - "pollutant": "PM25", - "limit": 25.0, - "percent": 18.44 - }, { - "name": "WHO", - "pollutant": "PM10", - "limit": 50.0, - "percent": 12.5 - }] - }, { - "fromDateTime": "2019-10-01T16:00:00.000Z", - "tillDateTime": "2019-10-01T17:00:00.000Z", - "values": [{ - "name": "PM1", - "value": 4.17 - }, { - "name": "PM25", - "value": 5.91 - }, { - "name": "PM10", - "value": 8.06 - }, { - "name": "PRESSURE", - "value": 1003.05 - }, { - "name": "HUMIDITY", - "value": 66.14 - }, { - "name": "TEMPERATURE", - "value": 17.04 - }], - "indexes": [{ - "name": "AIRLY_CAQI", - "value": 9.84, - "level": "VERY_LOW", - "description": "Great air here today!", - "advice": "Enjoy life!", - "color": "#6BC926" - }], - "standards": [{ - "name": "WHO", - "pollutant": "PM25", - "limit": 25.0, - "percent": 23.62 - }, { - "name": "WHO", - "pollutant": "PM10", - "limit": 50.0, - "percent": 16.11 - }] - }, { - "fromDateTime": "2019-10-01T17:00:00.000Z", - "tillDateTime": "2019-10-01T18:00:00.000Z", - "values": [{ - "name": "PM1", - "value": 6.4 - }, { - "name": "PM25", - "value": 10.93 - }, { - "name": "PM10", - "value": 15.7 - }, { - "name": "PRESSURE", - "value": 1002.85 - }, { - "name": "HUMIDITY", - "value": 68.31 - }, { - "name": "TEMPERATURE", - "value": 16.33 - }], - "indexes": [{ - "name": "AIRLY_CAQI", - "value": 18.22, - "level": "VERY_LOW", - "description": "Great air here today!", - "advice": "It couldn't be better ;)", - "color": "#6BC926" - }], - "standards": [{ - "name": "WHO", - "pollutant": "PM25", - "limit": 25.0, - "percent": 43.74 - }, { - "name": "WHO", - "pollutant": "PM10", - "limit": 50.0, - "percent": 31.4 - }] - }, { - "fromDateTime": "2019-10-01T18:00:00.000Z", - "tillDateTime": "2019-10-01T19:00:00.000Z", - "values": [{ - "name": "PM1", - "value": 4.79 - }, { - "name": "PM25", - "value": 7.41 - }, { - "name": "PM10", - "value": 10.31 - }, { - "name": "PRESSURE", - "value": 1002.52 - }, { - "name": "HUMIDITY", - "value": 69.88 - }, { - "name": "TEMPERATURE", - "value": 15.98 - }], - "indexes": [{ - "name": "AIRLY_CAQI", - "value": 12.35, - "level": "VERY_LOW", - "description": "Great air here today!", - "advice": "Enjoy life!", - "color": "#6BC926" - }], - "standards": [{ - "name": "WHO", - "pollutant": "PM25", - "limit": 25.0, - "percent": 29.65 - }, { - "name": "WHO", - "pollutant": "PM10", - "limit": 50.0, - "percent": 20.63 - }] - }, { - "fromDateTime": "2019-10-01T19:00:00.000Z", - "tillDateTime": "2019-10-01T20:00:00.000Z", - "values": [{ - "name": "PM1", - "value": 5.99 - }, { - "name": "PM25", - "value": 9.45 - }, { - "name": "PM10", - "value": 13.22 - }, { - "name": "PRESSURE", - "value": 1002.32 - }, { - "name": "HUMIDITY", - "value": 70.47 - }, { - "name": "TEMPERATURE", - "value": 15.76 - }], - "indexes": [{ - "name": "AIRLY_CAQI", - "value": 15.74, - "level": "VERY_LOW", - "description": "Great air here today!", - "advice": "Breathe deeply!", - "color": "#6BC926" - }], - "standards": [{ - "name": "WHO", - "pollutant": "PM25", - "limit": 25.0, - "percent": 37.78 - }, { - "name": "WHO", - "pollutant": "PM10", - "limit": 50.0, - "percent": 26.44 - }] - }, { - "fromDateTime": "2019-10-01T20:00:00.000Z", - "tillDateTime": "2019-10-01T21:00:00.000Z", - "values": [{ - "name": "PM1", - "value": 9.35 - }, { - "name": "PM25", - "value": 14.67 - }, { - "name": "PM10", - "value": 20.57 - }, { - "name": "PRESSURE", - "value": 1002.46 - }, { - "name": "HUMIDITY", - "value": 72.61 - }, { - "name": "TEMPERATURE", - "value": 15.47 - }], - "indexes": [{ - "name": "AIRLY_CAQI", - "value": 24.45, - "level": "VERY_LOW", - "description": "Great air here today!", - "advice": "It couldn't be better ;)", - "color": "#6BC926" - }], - "standards": [{ - "name": "WHO", - "pollutant": "PM25", - "limit": 25.0, - "percent": 58.68 - }, { - "name": "WHO", - "pollutant": "PM10", - "limit": 50.0, - "percent": 41.13 - }] - }, { - "fromDateTime": "2019-10-01T21:00:00.000Z", - "tillDateTime": "2019-10-01T22:00:00.000Z", - "values": [{ - "name": "PM1", - "value": 9.95 - }, { - "name": "PM25", - "value": 15.37 - }, { - "name": "PM10", - "value": 21.33 - }, { - "name": "PRESSURE", - "value": 1002.59 - }, { - "name": "HUMIDITY", - "value": 75.09 - }, { - "name": "TEMPERATURE", - "value": 15.17 - }], - "indexes": [{ - "name": "AIRLY_CAQI", - "value": 25.62, - "level": "LOW", - "description": "Air is quite good.", - "advice": "Take a breath!", - "color": "#D1CF1E" - }], - "standards": [{ - "name": "WHO", - "pollutant": "PM25", - "limit": 25.0, - "percent": 61.48 - }, { - "name": "WHO", - "pollutant": "PM10", - "limit": 50.0, - "percent": 42.66 - }] - }, { - "fromDateTime": "2019-10-01T22:00:00.000Z", - "tillDateTime": "2019-10-01T23:00:00.000Z", - "values": [{ - "name": "PM1", - "value": 10.16 - }, { - "name": "PM25", - "value": 15.78 - }, { - "name": "PM10", - "value": 21.97 - }, { - "name": "PRESSURE", - "value": 1002.59 - }, { - "name": "HUMIDITY", - "value": 77.68 - }, { - "name": "TEMPERATURE", - "value": 14.9 - }], - "indexes": [{ - "name": "AIRLY_CAQI", - "value": 26.31, - "level": "LOW", - "description": "Air is quite good.", - "advice": "Great air for a walk to the park!", - "color": "#D1CF1E" - }], - "standards": [{ - "name": "WHO", - "pollutant": "PM25", - "limit": 25.0, - "percent": 63.14 - }, { - "name": "WHO", - "pollutant": "PM10", - "limit": 50.0, - "percent": 43.93 - }] - }, { - "fromDateTime": "2019-10-01T23:00:00.000Z", - "tillDateTime": "2019-10-02T00:00:00.000Z", - "values": [{ - "name": "PM1", - "value": 9.86 - }, { - "name": "PM25", - "value": 15.14 - }, { - "name": "PM10", - "value": 21.07 - }, { - "name": "PRESSURE", - "value": 1002.49 - }, { - "name": "HUMIDITY", - "value": 79.86 - }, { - "name": "TEMPERATURE", - "value": 14.56 - }], - "indexes": [{ - "name": "AIRLY_CAQI", - "value": 25.24, - "level": "LOW", - "description": "Air is quite good.", - "advice": "Leave the mask at home today!", - "color": "#D1CF1E" - }], - "standards": [{ - "name": "WHO", - "pollutant": "PM25", - "limit": 25.0, - "percent": 60.57 - }, { - "name": "WHO", - "pollutant": "PM10", - "limit": 50.0, - "percent": 42.14 - }] - }, { - "fromDateTime": "2019-10-02T00:00:00.000Z", - "tillDateTime": "2019-10-02T01:00:00.000Z", - "values": [{ - "name": "PM1", - "value": 9.77 - }, { - "name": "PM25", - "value": 15.04 - }, { - "name": "PM10", - "value": 20.97 - }, { - "name": "PRESSURE", - "value": 1002.18 - }, { - "name": "HUMIDITY", - "value": 81.77 - }, { - "name": "TEMPERATURE", - "value": 14.13 - }], - "indexes": [{ - "name": "AIRLY_CAQI", - "value": 25.07, - "level": "LOW", - "description": "Air is quite good.", - "advice": "Time for a walk with friends or activities with your family - because the air is clean!", - "color": "#D1CF1E" - }], - "standards": [{ - "name": "WHO", - "pollutant": "PM25", - "limit": 25.0, - "percent": 60.18 - }, { - "name": "WHO", - "pollutant": "PM10", - "limit": 50.0, - "percent": 41.94 - }] - }, { - "fromDateTime": "2019-10-02T01:00:00.000Z", - "tillDateTime": "2019-10-02T02:00:00.000Z", - "values": [{ - "name": "PM1", - "value": 9.67 - }, { - "name": "PM25", - "value": 14.9 - }, { - "name": "PM10", - "value": 20.67 - }, { - "name": "PRESSURE", - "value": 1002.01 - }, { - "name": "HUMIDITY", - "value": 84.5 - }, { - "name": "TEMPERATURE", - "value": 13.7 - }], - "indexes": [{ - "name": "AIRLY_CAQI", - "value": 24.84, - "level": "VERY_LOW", - "description": "Great air here today!", - "advice": "Great air!", - "color": "#6BC926" - }], - "standards": [{ - "name": "WHO", - "pollutant": "PM25", - "limit": 25.0, - "percent": 59.62 - }, { - "name": "WHO", - "pollutant": "PM10", - "limit": 50.0, - "percent": 41.33 - }] - }, { - "fromDateTime": "2019-10-02T02:00:00.000Z", - "tillDateTime": "2019-10-02T03:00:00.000Z", - "values": [{ - "name": "PM1", - "value": 7.17 - }, { - "name": "PM25", - "value": 10.7 - }, { - "name": "PM10", - "value": 14.58 - }, { - "name": "PRESSURE", - "value": 1001.56 - }, { - "name": "HUMIDITY", - "value": 88.55 - }, { - "name": "TEMPERATURE", - "value": 13.44 - }], - "indexes": [{ - "name": "AIRLY_CAQI", - "value": 17.83, - "level": "VERY_LOW", - "description": "Great air here today!", - "advice": "Catch your breath!", - "color": "#6BC926" - }], - "standards": [{ - "name": "WHO", - "pollutant": "PM25", - "limit": 25.0, - "percent": 42.8 - }, { - "name": "WHO", - "pollutant": "PM10", - "limit": 50.0, - "percent": 29.17 - }] - }, { - "fromDateTime": "2019-10-02T03:00:00.000Z", - "tillDateTime": "2019-10-02T04:00:00.000Z", - "values": [{ - "name": "PM1", - "value": 6.99 - }, { - "name": "PM25", - "value": 10.23 - }, { - "name": "PM10", - "value": 13.66 - }, { - "name": "PRESSURE", - "value": 1001.34 - }, { - "name": "HUMIDITY", - "value": 90.82 - }, { - "name": "TEMPERATURE", - "value": 13.3 - }], - "indexes": [{ - "name": "AIRLY_CAQI", - "value": 17.05, - "level": "VERY_LOW", - "description": "Great air here today!", - "advice": "Perfect air for exercising! Go for it!", - "color": "#6BC926" - }], - "standards": [{ - "name": "WHO", - "pollutant": "PM25", - "limit": 25.0, - "percent": 40.91 - }, { - "name": "WHO", - "pollutant": "PM10", - "limit": 50.0, - "percent": 27.33 - }] - }, { - "fromDateTime": "2019-10-02T04:00:00.000Z", - "tillDateTime": "2019-10-02T05:00:00.000Z", - "values": [{ - "name": "PM1", - "value": 7.82 - }, { - "name": "PM25", - "value": 11.59 - }, { - "name": "PM10", - "value": 15.77 - }, { - "name": "PRESSURE", - "value": 1000.92 - }, { - "name": "HUMIDITY", - "value": 91.8 - }, { - "name": "TEMPERATURE", - "value": 13.34 - }], - "indexes": [{ - "name": "AIRLY_CAQI", - "value": 19.32, - "level": "VERY_LOW", - "description": "Great air here today!", - "advice": "Dear me, how wonderful!", - "color": "#6BC926" - }], - "standards": [{ - "name": "WHO", - "pollutant": "PM25", - "limit": 25.0, - "percent": 46.36 - }, { - "name": "WHO", - "pollutant": "PM10", - "limit": 50.0, - "percent": 31.54 - }] - }, { - "fromDateTime": "2019-10-02T05:00:00.000Z", - "tillDateTime": "2019-10-02T06:00:00.000Z", - "values": [{ - "name": "PM1", - "value": 10.16 - }, { - "name": "PM25", - "value": 15.35 - }, { - "name": "PM10", - "value": 21.45 - }, { - "name": "PRESSURE", - "value": 1000.82 - }, { - "name": "HUMIDITY", - "value": 92.15 - }, { - "name": "TEMPERATURE", - "value": 13.74 - }], - "indexes": [{ - "name": "AIRLY_CAQI", - "value": 25.59, - "level": "LOW", - "description": "Air is quite good.", - "advice": "How about going for a walk?", - "color": "#D1CF1E" - }], - "standards": [{ - "name": "WHO", - "pollutant": "PM25", - "limit": 25.0, - "percent": 61.42 - }, { - "name": "WHO", - "pollutant": "PM10", - "limit": 50.0, - "percent": 42.9 - }] - }], - "forecast": [{ - "fromDateTime": "2019-10-02T06:00:00.000Z", - "tillDateTime": "2019-10-02T07:00:00.000Z", - "values": [{ - "name": "PM25", - "value": 13.28 - }, { - "name": "PM10", - "value": 18.37 - }], - "indexes": [{ - "name": "AIRLY_CAQI", - "value": 22.14, - "level": "VERY_LOW", - "description": "Great air here today!", - "advice": "It couldn't be better ;)", - "color": "#6BC926" - }], - "standards": [{ - "name": "WHO", - "pollutant": "PM25", - "limit": 25.0, - "percent": 53.13 - }, { - "name": "WHO", - "pollutant": "PM10", - "limit": 50.0, - "percent": 36.73 - }] - }, { - "fromDateTime": "2019-10-02T07:00:00.000Z", - "tillDateTime": "2019-10-02T08:00:00.000Z", - "values": [{ - "name": "PM25", - "value": 11.19 - }, { - "name": "PM10", - "value": 15.65 - }], - "indexes": [{ - "name": "AIRLY_CAQI", - "value": 18.65, - "level": "VERY_LOW", - "description": "Great air here today!", - "advice": "Enjoy life!", - "color": "#6BC926" - }], - "standards": [{ - "name": "WHO", - "pollutant": "PM25", - "limit": 25.0, - "percent": 44.76 - }, { - "name": "WHO", - "pollutant": "PM10", - "limit": 50.0, - "percent": 31.31 - }] - }, { - "fromDateTime": "2019-10-02T08:00:00.000Z", - "tillDateTime": "2019-10-02T09:00:00.000Z", - "values": [{ - "name": "PM25", - "value": 8.79 - }, { - "name": "PM10", - "value": 12.8 - }], - "indexes": [{ - "name": "AIRLY_CAQI", - "value": 14.65, - "level": "VERY_LOW", - "description": "Great air here today!", - "advice": "Breathe deep! The air is clean!", - "color": "#6BC926" - }], - "standards": [{ - "name": "WHO", - "pollutant": "PM25", - "limit": 25.0, - "percent": 35.15 - }, { - "name": "WHO", - "pollutant": "PM10", - "limit": 50.0, - "percent": 25.59 - }] - }, { - "fromDateTime": "2019-10-02T09:00:00.000Z", - "tillDateTime": "2019-10-02T10:00:00.000Z", - "values": [{ - "name": "PM25", - "value": 5.46 - }, { - "name": "PM10", - "value": 8.91 - }], - "indexes": [{ - "name": "AIRLY_CAQI", - "value": 9.11, - "level": "VERY_LOW", - "description": "Great air here today!", - "advice": "Breathe to fill your lungs!", - "color": "#6BC926" - }], - "standards": [{ - "name": "WHO", - "pollutant": "PM25", - "limit": 25.0, - "percent": 21.86 - }, { - "name": "WHO", - "pollutant": "PM10", - "limit": 50.0, - "percent": 17.83 - }] - }, { - "fromDateTime": "2019-10-02T10:00:00.000Z", - "tillDateTime": "2019-10-02T11:00:00.000Z", - "values": [{ - "name": "PM25", - "value": 2.26 - }, { - "name": "PM10", - "value": 5.02 - }], - "indexes": [{ - "name": "AIRLY_CAQI", - "value": 5.02, - "level": "VERY_LOW", - "description": "Great air here today!", - "advice": "Enjoy life!", - "color": "#6BC926" - }], - "standards": [{ - "name": "WHO", - "pollutant": "PM25", - "limit": 25.0, - "percent": 9.06 - }, { - "name": "WHO", - "pollutant": "PM10", - "limit": 50.0, - "percent": 10.05 - }] - }, { - "fromDateTime": "2019-10-02T11:00:00.000Z", - "tillDateTime": "2019-10-02T12:00:00.000Z", - "values": [{ - "name": "PM25", - "value": 1.06 - }, { - "name": "PM10", - "value": 2.52 - }], - "indexes": [{ - "name": "AIRLY_CAQI", - "value": 2.52, - "level": "VERY_LOW", - "description": "Great air here today!", - "advice": "The air is great!", - "color": "#6BC926" - }], - "standards": [{ - "name": "WHO", - "pollutant": "PM25", - "limit": 25.0, - "percent": 4.22 - }, { - "name": "WHO", - "pollutant": "PM10", - "limit": 50.0, - "percent": 5.05 - }] - }, { - "fromDateTime": "2019-10-02T12:00:00.000Z", - "tillDateTime": "2019-10-02T13:00:00.000Z", - "values": [{ - "name": "PM25", - "value": 0.48 - }, { - "name": "PM10", - "value": 1.94 - }], - "indexes": [{ - "name": "AIRLY_CAQI", - "value": 1.94, - "level": "VERY_LOW", - "description": "Great air here today!", - "advice": "Breathe as much as you can!", - "color": "#6BC926" - }], - "standards": [{ - "name": "WHO", - "pollutant": "PM25", - "limit": 25.0, - "percent": 1.94 - }, { - "name": "WHO", - "pollutant": "PM10", - "limit": 50.0, - "percent": 3.89 - }] - }, { - "fromDateTime": "2019-10-02T13:00:00.000Z", - "tillDateTime": "2019-10-02T14:00:00.000Z", - "values": [{ - "name": "PM25", - "value": 0.63 - }, { - "name": "PM10", - "value": 2.26 - }], - "indexes": [{ - "name": "AIRLY_CAQI", - "value": 2.26, - "level": "VERY_LOW", - "description": "Great air here today!", - "advice": "Enjoy life!", - "color": "#6BC926" - }], - "standards": [{ - "name": "WHO", - "pollutant": "PM25", - "limit": 25.0, - "percent": 2.53 - }, { - "name": "WHO", - "pollutant": "PM10", - "limit": 50.0, - "percent": 4.52 - }] - }, { - "fromDateTime": "2019-10-02T14:00:00.000Z", - "tillDateTime": "2019-10-02T15:00:00.000Z", - "values": [{ - "name": "PM25", - "value": 1.47 - }, { - "name": "PM10", - "value": 3.39 - }], - "indexes": [{ - "name": "AIRLY_CAQI", - "value": 3.39, - "level": "VERY_LOW", - "description": "Great air here today!", - "advice": "Breathe as much as you can!", - "color": "#6BC926" - }], - "standards": [{ - "name": "WHO", - "pollutant": "PM25", - "limit": 25.0, - "percent": 5.87 - }, { - "name": "WHO", - "pollutant": "PM10", - "limit": 50.0, - "percent": 6.78 - }] - }, { - "fromDateTime": "2019-10-02T15:00:00.000Z", - "tillDateTime": "2019-10-02T16:00:00.000Z", - "values": [{ - "name": "PM25", - "value": 2.62 - }, { - "name": "PM10", - "value": 5.02 - }], - "indexes": [{ - "name": "AIRLY_CAQI", - "value": 5.02, - "level": "VERY_LOW", - "description": "Great air here today!", - "advice": "Great air!", - "color": "#6BC926" - }], - "standards": [{ - "name": "WHO", - "pollutant": "PM25", - "limit": 25.0, - "percent": 10.5 - }, { - "name": "WHO", - "pollutant": "PM10", - "limit": 50.0, - "percent": 10.05 - }] - }, { - "fromDateTime": "2019-10-02T16:00:00.000Z", - "tillDateTime": "2019-10-02T17:00:00.000Z", - "values": [{ - "name": "PM25", - "value": 3.89 - }, { - "name": "PM10", - "value": 8.02 - }], - "indexes": [{ - "name": "AIRLY_CAQI", - "value": 8.02, - "level": "VERY_LOW", - "description": "Great air here today!", - "advice": "Dear me, how wonderful!", - "color": "#6BC926" - }], - "standards": [{ - "name": "WHO", - "pollutant": "PM25", - "limit": 25.0, - "percent": 15.56 - }, { - "name": "WHO", - "pollutant": "PM10", - "limit": 50.0, - "percent": 16.04 - }] - }, { - "fromDateTime": "2019-10-02T17:00:00.000Z", - "tillDateTime": "2019-10-02T18:00:00.000Z", - "values": [{ - "name": "PM25", - "value": 6.26 - }, { - "name": "PM10", - "value": 11.41 - }], - "indexes": [{ - "name": "AIRLY_CAQI", - "value": 11.41, - "level": "VERY_LOW", - "description": "Great air here today!", - "advice": "The air is great!", - "color": "#6BC926" - }], - "standards": [{ - "name": "WHO", - "pollutant": "PM25", - "limit": 25.0, - "percent": 25.05 - }, { - "name": "WHO", - "pollutant": "PM10", - "limit": 50.0, - "percent": 22.83 - }] - }, { - "fromDateTime": "2019-10-02T18:00:00.000Z", - "tillDateTime": "2019-10-02T19:00:00.000Z", - "values": [{ - "name": "PM25", - "value": 8.69 - }, { - "name": "PM10", - "value": 14.48 - }], - "indexes": [{ - "name": "AIRLY_CAQI", - "value": 14.48, - "level": "VERY_LOW", - "description": "Great air here today!", - "advice": "Zero dust - zero worries!", - "color": "#6BC926" - }], - "standards": [{ - "name": "WHO", - "pollutant": "PM25", - "limit": 25.0, - "percent": 34.76 - }, { - "name": "WHO", - "pollutant": "PM10", - "limit": 50.0, - "percent": 28.96 - }] - }, { - "fromDateTime": "2019-10-02T19:00:00.000Z", - "tillDateTime": "2019-10-02T20:00:00.000Z", - "values": [{ - "name": "PM25", - "value": 10.78 - }, { - "name": "PM10", - "value": 16.86 - }], - "indexes": [{ - "name": "AIRLY_CAQI", - "value": 17.97, - "level": "VERY_LOW", - "description": "Great air here today!", - "advice": "Zero dust - zero worries!", - "color": "#6BC926" - }], - "standards": [{ - "name": "WHO", - "pollutant": "PM25", - "limit": 25.0, - "percent": 43.13 - }, { - "name": "WHO", - "pollutant": "PM10", - "limit": 50.0, - "percent": 33.72 - }] - }, { - "fromDateTime": "2019-10-02T20:00:00.000Z", - "tillDateTime": "2019-10-02T21:00:00.000Z", - "values": [{ - "name": "PM25", - "value": 12.22 - }, { - "name": "PM10", - "value": 18.19 - }], - "indexes": [{ - "name": "AIRLY_CAQI", - "value": 20.36, - "level": "VERY_LOW", - "description": "Great air here today!", - "advice": "Breathe to fill your lungs!", - "color": "#6BC926" - }], - "standards": [{ - "name": "WHO", - "pollutant": "PM25", - "limit": 25.0, - "percent": 48.88 - }, { - "name": "WHO", - "pollutant": "PM10", - "limit": 50.0, - "percent": 36.38 - }] - }, { - "fromDateTime": "2019-10-02T21:00:00.000Z", - "tillDateTime": "2019-10-02T22:00:00.000Z", - "values": [{ - "name": "PM25", - "value": 13.06 - }, { - "name": "PM10", - "value": 18.62 - }], - "indexes": [{ - "name": "AIRLY_CAQI", - "value": 21.77, - "level": "VERY_LOW", - "description": "Great air here today!", - "advice": "Dear me, how wonderful!", - "color": "#6BC926" - }], - "standards": [{ - "name": "WHO", - "pollutant": "PM25", - "limit": 25.0, - "percent": 52.25 - }, { - "name": "WHO", - "pollutant": "PM10", - "limit": 50.0, - "percent": 37.24 - }] - }, { - "fromDateTime": "2019-10-02T22:00:00.000Z", - "tillDateTime": "2019-10-02T23:00:00.000Z", - "values": [{ - "name": "PM25", - "value": 13.51 - }, { - "name": "PM10", - "value": 18.49 - }], - "indexes": [{ - "name": "AIRLY_CAQI", - "value": 22.52, - "level": "VERY_LOW", - "description": "Great air here today!", - "advice": "The air is great!", - "color": "#6BC926" - }], - "standards": [{ - "name": "WHO", - "pollutant": "PM25", - "limit": 25.0, - "percent": 54.06 - }, { - "name": "WHO", - "pollutant": "PM10", - "limit": 50.0, - "percent": 36.98 - }] - }, { - "fromDateTime": "2019-10-02T23:00:00.000Z", - "tillDateTime": "2019-10-03T00:00:00.000Z", - "values": [{ - "name": "PM25", - "value": 13.46 - }, { - "name": "PM10", - "value": 17.63 - }], - "indexes": [{ - "name": "AIRLY_CAQI", - "value": 22.44, - "level": "VERY_LOW", - "description": "Great air here today!", - "advice": "Green, green, green!", - "color": "#6BC926" - }], - "standards": [{ - "name": "WHO", - "pollutant": "PM25", - "limit": 25.0, - "percent": 53.85 - }, { - "name": "WHO", - "pollutant": "PM10", - "limit": 50.0, - "percent": 35.26 - }] - }, { - "fromDateTime": "2019-10-03T00:00:00.000Z", - "tillDateTime": "2019-10-03T01:00:00.000Z", - "values": [{ - "name": "PM25", - "value": 13.05 - }, { - "name": "PM10", - "value": 16.36 - }], - "indexes": [{ - "name": "AIRLY_CAQI", - "value": 21.74, - "level": "VERY_LOW", - "description": "Great air here today!", - "advice": "Catch your breath!", - "color": "#6BC926" - }], - "standards": [{ - "name": "WHO", - "pollutant": "PM25", - "limit": 25.0, - "percent": 52.19 - }, { - "name": "WHO", - "pollutant": "PM10", - "limit": 50.0, - "percent": 32.73 - }] - }, { - "fromDateTime": "2019-10-03T01:00:00.000Z", - "tillDateTime": "2019-10-03T02:00:00.000Z", - "values": [{ - "name": "PM25", - "value": 12.47 - }, { - "name": "PM10", - "value": 15.16 - }], - "indexes": [{ - "name": "AIRLY_CAQI", - "value": 20.79, - "level": "VERY_LOW", - "description": "Great air here today!", - "advice": "Green, green, green!", - "color": "#6BC926" - }], - "standards": [{ - "name": "WHO", - "pollutant": "PM25", - "limit": 25.0, - "percent": 49.9 - }, { - "name": "WHO", - "pollutant": "PM10", - "limit": 50.0, - "percent": 30.32 - }] - }, { - "fromDateTime": "2019-10-03T02:00:00.000Z", - "tillDateTime": "2019-10-03T03:00:00.000Z", - "values": [{ - "name": "PM25", - "value": 11.99 - }, { - "name": "PM10", - "value": 14.07 - }], - "indexes": [{ - "name": "AIRLY_CAQI", - "value": 19.98, - "level": "VERY_LOW", - "description": "Great air here today!", - "advice": "Breathe as much as you can!", - "color": "#6BC926" - }], - "standards": [{ - "name": "WHO", - "pollutant": "PM25", - "limit": 25.0, - "percent": 47.94 - }, { - "name": "WHO", - "pollutant": "PM10", - "limit": 50.0, - "percent": 28.14 - }] - }, { - "fromDateTime": "2019-10-03T03:00:00.000Z", - "tillDateTime": "2019-10-03T04:00:00.000Z", - "values": [{ - "name": "PM25", - "value": 11.74 - }, { - "name": "PM10", - "value": 13.67 - }], - "indexes": [{ - "name": "AIRLY_CAQI", - "value": 19.56, - "level": "VERY_LOW", - "description": "Great air here today!", - "advice": "Dear me, how wonderful!", - "color": "#6BC926" - }], - "standards": [{ - "name": "WHO", - "pollutant": "PM25", - "limit": 25.0, - "percent": 46.95 - }, { - "name": "WHO", - "pollutant": "PM10", - "limit": 50.0, - "percent": 27.34 - }] - }, { - "fromDateTime": "2019-10-03T04:00:00.000Z", - "tillDateTime": "2019-10-03T05:00:00.000Z", - "values": [{ - "name": "PM25", - "value": 11.44 - }, { - "name": "PM10", - "value": 13.51 - }], - "indexes": [{ - "name": "AIRLY_CAQI", - "value": 19.06, - "level": "VERY_LOW", - "description": "Great air here today!", - "advice": "Breathe to fill your lungs!", - "color": "#6BC926" - }], - "standards": [{ - "name": "WHO", - "pollutant": "PM25", - "limit": 25.0, - "percent": 45.74 - }, { - "name": "WHO", - "pollutant": "PM10", - "limit": 50.0, - "percent": 27.02 - }] - }, { - "fromDateTime": "2019-10-03T05:00:00.000Z", - "tillDateTime": "2019-10-03T06:00:00.000Z", - "values": [{ - "name": "PM25", - "value": 10.88 - }, { - "name": "PM10", - "value": 13.38 - }], - "indexes": [{ - "name": "AIRLY_CAQI", - "value": 18.13, - "level": "VERY_LOW", - "description": "Great air here today!", - "advice": "Breathe as much as you can!", - "color": "#6BC926" - }], - "standards": [{ - "name": "WHO", - "pollutant": "PM25", - "limit": 25.0, - "percent": 43.52 - }, { - "name": "WHO", - "pollutant": "PM10", - "limit": 50.0, - "percent": 26.76 - }] - }] -} \ No newline at end of file + "current": { + "fromDateTime": "2019-10-02T05:54:57.204Z", + "tillDateTime": "2019-10-02T06:54:57.204Z", + "values": [ + { + "name": "PM1", + "value": 9.23 + }, + { + "name": "PM25", + "value": 13.71 + }, + { + "name": "PM10", + "value": 18.58 + }, + { + "name": "PRESSURE", + "value": 1000.87 + }, + { + "name": "HUMIDITY", + "value": 92.84 + }, + { + "name": "TEMPERATURE", + "value": 14.23 + } + ], + "indexes": [ + { + "name": "AIRLY_CAQI", + "value": 22.85, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Great air!", + "color": "#6BC926" + } + ], + "standards": [ + { + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 54.84 + }, + { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 37.17 + } + ] + }, + "history": [ + { + "fromDateTime": "2019-10-01T06:00:00.000Z", + "tillDateTime": "2019-10-01T07:00:00.000Z", + "values": [ + { + "name": "PM1", + "value": 5.95 + }, + { + "name": "PM25", + "value": 8.54 + }, + { + "name": "PM10", + "value": 11.46 + }, + { + "name": "PRESSURE", + "value": 1009.61 + }, + { + "name": "HUMIDITY", + "value": 97.6 + }, + { + "name": "TEMPERATURE", + "value": 9.71 + } + ], + "indexes": [ + { + "name": "AIRLY_CAQI", + "value": 14.24, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Green equals clean!", + "color": "#6BC926" + } + ], + "standards": [ + { + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 34.18 + }, + { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 22.91 + } + ] + }, + { + "fromDateTime": "2019-10-01T07:00:00.000Z", + "tillDateTime": "2019-10-01T08:00:00.000Z", + "values": [ + { + "name": "PM1", + "value": 4.2 + }, + { + "name": "PM25", + "value": 5.88 + }, + { + "name": "PM10", + "value": 7.88 + }, + { + "name": "PRESSURE", + "value": 1009.13 + }, + { + "name": "HUMIDITY", + "value": 90.84 + }, + { + "name": "TEMPERATURE", + "value": 12.65 + } + ], + "indexes": [ + { + "name": "AIRLY_CAQI", + "value": 9.81, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Dear me, how wonderful!", + "color": "#6BC926" + } + ], + "standards": [ + { + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 23.53 + }, + { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 15.75 + } + ] + }, + { + "fromDateTime": "2019-10-01T08:00:00.000Z", + "tillDateTime": "2019-10-01T09:00:00.000Z", + "values": [ + { + "name": "PM1", + "value": 3.63 + }, + { + "name": "PM25", + "value": 5.56 + }, + { + "name": "PM10", + "value": 7.71 + }, + { + "name": "PRESSURE", + "value": 1008.27 + }, + { + "name": "HUMIDITY", + "value": 84.61 + }, + { + "name": "TEMPERATURE", + "value": 15.57 + } + ], + "indexes": [ + { + "name": "AIRLY_CAQI", + "value": 9.26, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Dear me, how wonderful!", + "color": "#6BC926" + } + ], + "standards": [ + { + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 22.23 + }, + { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 15.42 + } + ] + }, + { + "fromDateTime": "2019-10-01T09:00:00.000Z", + "tillDateTime": "2019-10-01T10:00:00.000Z", + "values": [ + { + "name": "PM1", + "value": 2.9 + }, + { + "name": "PM25", + "value": 3.93 + }, + { + "name": "PM10", + "value": 5.24 + }, + { + "name": "PRESSURE", + "value": 1007.57 + }, + { + "name": "HUMIDITY", + "value": 79.52 + }, + { + "name": "TEMPERATURE", + "value": 16.57 + } + ], + "indexes": [ + { + "name": "AIRLY_CAQI", + "value": 6.56, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Breathe deep! The air is clean!", + "color": "#6BC926" + } + ], + "standards": [ + { + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 15.74 + }, + { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 10.48 + } + ] + }, + { + "fromDateTime": "2019-10-01T10:00:00.000Z", + "tillDateTime": "2019-10-01T11:00:00.000Z", + "values": [ + { + "name": "PM1", + "value": 2.45 + }, + { + "name": "PM25", + "value": 3.33 + }, + { + "name": "PM10", + "value": 4.52 + }, + { + "name": "PRESSURE", + "value": 1006.75 + }, + { + "name": "HUMIDITY", + "value": 74.09 + }, + { + "name": "TEMPERATURE", + "value": 16.95 + } + ], + "indexes": [ + { + "name": "AIRLY_CAQI", + "value": 5.55, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "The air is grand today. ;)", + "color": "#6BC926" + } + ], + "standards": [ + { + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 13.31 + }, + { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 9.04 + } + ] + }, + { + "fromDateTime": "2019-10-01T11:00:00.000Z", + "tillDateTime": "2019-10-01T12:00:00.000Z", + "values": [ + { + "name": "PM1", + "value": 2.0 + }, + { + "name": "PM25", + "value": 2.93 + }, + { + "name": "PM10", + "value": 3.98 + }, + { + "name": "PRESSURE", + "value": 1005.71 + }, + { + "name": "HUMIDITY", + "value": 69.06 + }, + { + "name": "TEMPERATURE", + "value": 17.31 + } + ], + "indexes": [ + { + "name": "AIRLY_CAQI", + "value": 4.89, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Green equals clean!", + "color": "#6BC926" + } + ], + "standards": [ + { + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 11.74 + }, + { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 7.96 + } + ] + }, + { + "fromDateTime": "2019-10-01T12:00:00.000Z", + "tillDateTime": "2019-10-01T13:00:00.000Z", + "values": [ + { + "name": "PM1", + "value": 1.92 + }, + { + "name": "PM25", + "value": 2.69 + }, + { + "name": "PM10", + "value": 3.68 + }, + { + "name": "PRESSURE", + "value": 1005.03 + }, + { + "name": "HUMIDITY", + "value": 65.08 + }, + { + "name": "TEMPERATURE", + "value": 17.47 + } + ], + "indexes": [ + { + "name": "AIRLY_CAQI", + "value": 4.49, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Enjoy life!", + "color": "#6BC926" + } + ], + "standards": [ + { + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 10.77 + }, + { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 7.36 + } + ] + }, + { + "fromDateTime": "2019-10-01T13:00:00.000Z", + "tillDateTime": "2019-10-01T14:00:00.000Z", + "values": [ + { + "name": "PM1", + "value": 1.79 + }, + { + "name": "PM25", + "value": 2.57 + }, + { + "name": "PM10", + "value": 3.53 + }, + { + "name": "PRESSURE", + "value": 1004.26 + }, + { + "name": "HUMIDITY", + "value": 63.72 + }, + { + "name": "TEMPERATURE", + "value": 17.91 + } + ], + "indexes": [ + { + "name": "AIRLY_CAQI", + "value": 4.29, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Great air!", + "color": "#6BC926" + } + ], + "standards": [ + { + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 10.29 + }, + { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 7.06 + } + ] + }, + { + "fromDateTime": "2019-10-01T14:00:00.000Z", + "tillDateTime": "2019-10-01T15:00:00.000Z", + "values": [ + { + "name": "PM1", + "value": 2.06 + }, + { + "name": "PM25", + "value": 3.08 + }, + { + "name": "PM10", + "value": 4.23 + }, + { + "name": "PRESSURE", + "value": 1003.46 + }, + { + "name": "HUMIDITY", + "value": 64.44 + }, + { + "name": "TEMPERATURE", + "value": 17.84 + } + ], + "indexes": [ + { + "name": "AIRLY_CAQI", + "value": 5.14, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "The air is grand today. ;)", + "color": "#6BC926" + } + ], + "standards": [ + { + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 12.33 + }, + { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 8.47 + } + ] + }, + { + "fromDateTime": "2019-10-01T15:00:00.000Z", + "tillDateTime": "2019-10-01T16:00:00.000Z", + "values": [ + { + "name": "PM1", + "value": 3.17 + }, + { + "name": "PM25", + "value": 4.61 + }, + { + "name": "PM10", + "value": 6.25 + }, + { + "name": "PRESSURE", + "value": 1003.18 + }, + { + "name": "HUMIDITY", + "value": 65.32 + }, + { + "name": "TEMPERATURE", + "value": 18.08 + } + ], + "indexes": [ + { + "name": "AIRLY_CAQI", + "value": 7.68, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Green, green, green!", + "color": "#6BC926" + } + ], + "standards": [ + { + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 18.44 + }, + { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 12.5 + } + ] + }, + { + "fromDateTime": "2019-10-01T16:00:00.000Z", + "tillDateTime": "2019-10-01T17:00:00.000Z", + "values": [ + { + "name": "PM1", + "value": 4.17 + }, + { + "name": "PM25", + "value": 5.91 + }, + { + "name": "PM10", + "value": 8.06 + }, + { + "name": "PRESSURE", + "value": 1003.05 + }, + { + "name": "HUMIDITY", + "value": 66.14 + }, + { + "name": "TEMPERATURE", + "value": 17.04 + } + ], + "indexes": [ + { + "name": "AIRLY_CAQI", + "value": 9.84, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Enjoy life!", + "color": "#6BC926" + } + ], + "standards": [ + { + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 23.62 + }, + { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 16.11 + } + ] + }, + { + "fromDateTime": "2019-10-01T17:00:00.000Z", + "tillDateTime": "2019-10-01T18:00:00.000Z", + "values": [ + { + "name": "PM1", + "value": 6.4 + }, + { + "name": "PM25", + "value": 10.93 + }, + { + "name": "PM10", + "value": 15.7 + }, + { + "name": "PRESSURE", + "value": 1002.85 + }, + { + "name": "HUMIDITY", + "value": 68.31 + }, + { + "name": "TEMPERATURE", + "value": 16.33 + } + ], + "indexes": [ + { + "name": "AIRLY_CAQI", + "value": 18.22, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "It couldn't be better ;)", + "color": "#6BC926" + } + ], + "standards": [ + { + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 43.74 + }, + { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 31.4 + } + ] + }, + { + "fromDateTime": "2019-10-01T18:00:00.000Z", + "tillDateTime": "2019-10-01T19:00:00.000Z", + "values": [ + { + "name": "PM1", + "value": 4.79 + }, + { + "name": "PM25", + "value": 7.41 + }, + { + "name": "PM10", + "value": 10.31 + }, + { + "name": "PRESSURE", + "value": 1002.52 + }, + { + "name": "HUMIDITY", + "value": 69.88 + }, + { + "name": "TEMPERATURE", + "value": 15.98 + } + ], + "indexes": [ + { + "name": "AIRLY_CAQI", + "value": 12.35, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Enjoy life!", + "color": "#6BC926" + } + ], + "standards": [ + { + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 29.65 + }, + { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 20.63 + } + ] + }, + { + "fromDateTime": "2019-10-01T19:00:00.000Z", + "tillDateTime": "2019-10-01T20:00:00.000Z", + "values": [ + { + "name": "PM1", + "value": 5.99 + }, + { + "name": "PM25", + "value": 9.45 + }, + { + "name": "PM10", + "value": 13.22 + }, + { + "name": "PRESSURE", + "value": 1002.32 + }, + { + "name": "HUMIDITY", + "value": 70.47 + }, + { + "name": "TEMPERATURE", + "value": 15.76 + } + ], + "indexes": [ + { + "name": "AIRLY_CAQI", + "value": 15.74, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Breathe deeply!", + "color": "#6BC926" + } + ], + "standards": [ + { + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 37.78 + }, + { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 26.44 + } + ] + }, + { + "fromDateTime": "2019-10-01T20:00:00.000Z", + "tillDateTime": "2019-10-01T21:00:00.000Z", + "values": [ + { + "name": "PM1", + "value": 9.35 + }, + { + "name": "PM25", + "value": 14.67 + }, + { + "name": "PM10", + "value": 20.57 + }, + { + "name": "PRESSURE", + "value": 1002.46 + }, + { + "name": "HUMIDITY", + "value": 72.61 + }, + { + "name": "TEMPERATURE", + "value": 15.47 + } + ], + "indexes": [ + { + "name": "AIRLY_CAQI", + "value": 24.45, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "It couldn't be better ;)", + "color": "#6BC926" + } + ], + "standards": [ + { + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 58.68 + }, + { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 41.13 + } + ] + }, + { + "fromDateTime": "2019-10-01T21:00:00.000Z", + "tillDateTime": "2019-10-01T22:00:00.000Z", + "values": [ + { + "name": "PM1", + "value": 9.95 + }, + { + "name": "PM25", + "value": 15.37 + }, + { + "name": "PM10", + "value": 21.33 + }, + { + "name": "PRESSURE", + "value": 1002.59 + }, + { + "name": "HUMIDITY", + "value": 75.09 + }, + { + "name": "TEMPERATURE", + "value": 15.17 + } + ], + "indexes": [ + { + "name": "AIRLY_CAQI", + "value": 25.62, + "level": "LOW", + "description": "Air is quite good.", + "advice": "Take a breath!", + "color": "#D1CF1E" + } + ], + "standards": [ + { + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 61.48 + }, + { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 42.66 + } + ] + }, + { + "fromDateTime": "2019-10-01T22:00:00.000Z", + "tillDateTime": "2019-10-01T23:00:00.000Z", + "values": [ + { + "name": "PM1", + "value": 10.16 + }, + { + "name": "PM25", + "value": 15.78 + }, + { + "name": "PM10", + "value": 21.97 + }, + { + "name": "PRESSURE", + "value": 1002.59 + }, + { + "name": "HUMIDITY", + "value": 77.68 + }, + { + "name": "TEMPERATURE", + "value": 14.9 + } + ], + "indexes": [ + { + "name": "AIRLY_CAQI", + "value": 26.31, + "level": "LOW", + "description": "Air is quite good.", + "advice": "Great air for a walk to the park!", + "color": "#D1CF1E" + } + ], + "standards": [ + { + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 63.14 + }, + { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 43.93 + } + ] + }, + { + "fromDateTime": "2019-10-01T23:00:00.000Z", + "tillDateTime": "2019-10-02T00:00:00.000Z", + "values": [ + { + "name": "PM1", + "value": 9.86 + }, + { + "name": "PM25", + "value": 15.14 + }, + { + "name": "PM10", + "value": 21.07 + }, + { + "name": "PRESSURE", + "value": 1002.49 + }, + { + "name": "HUMIDITY", + "value": 79.86 + }, + { + "name": "TEMPERATURE", + "value": 14.56 + } + ], + "indexes": [ + { + "name": "AIRLY_CAQI", + "value": 25.24, + "level": "LOW", + "description": "Air is quite good.", + "advice": "Leave the mask at home today!", + "color": "#D1CF1E" + } + ], + "standards": [ + { + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 60.57 + }, + { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 42.14 + } + ] + }, + { + "fromDateTime": "2019-10-02T00:00:00.000Z", + "tillDateTime": "2019-10-02T01:00:00.000Z", + "values": [ + { + "name": "PM1", + "value": 9.77 + }, + { + "name": "PM25", + "value": 15.04 + }, + { + "name": "PM10", + "value": 20.97 + }, + { + "name": "PRESSURE", + "value": 1002.18 + }, + { + "name": "HUMIDITY", + "value": 81.77 + }, + { + "name": "TEMPERATURE", + "value": 14.13 + } + ], + "indexes": [ + { + "name": "AIRLY_CAQI", + "value": 25.07, + "level": "LOW", + "description": "Air is quite good.", + "advice": "Time for a walk with friends or activities with your family - because the air is clean!", + "color": "#D1CF1E" + } + ], + "standards": [ + { + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 60.18 + }, + { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 41.94 + } + ] + }, + { + "fromDateTime": "2019-10-02T01:00:00.000Z", + "tillDateTime": "2019-10-02T02:00:00.000Z", + "values": [ + { + "name": "PM1", + "value": 9.67 + }, + { + "name": "PM25", + "value": 14.9 + }, + { + "name": "PM10", + "value": 20.67 + }, + { + "name": "PRESSURE", + "value": 1002.01 + }, + { + "name": "HUMIDITY", + "value": 84.5 + }, + { + "name": "TEMPERATURE", + "value": 13.7 + } + ], + "indexes": [ + { + "name": "AIRLY_CAQI", + "value": 24.84, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Great air!", + "color": "#6BC926" + } + ], + "standards": [ + { + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 59.62 + }, + { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 41.33 + } + ] + }, + { + "fromDateTime": "2019-10-02T02:00:00.000Z", + "tillDateTime": "2019-10-02T03:00:00.000Z", + "values": [ + { + "name": "PM1", + "value": 7.17 + }, + { + "name": "PM25", + "value": 10.7 + }, + { + "name": "PM10", + "value": 14.58 + }, + { + "name": "PRESSURE", + "value": 1001.56 + }, + { + "name": "HUMIDITY", + "value": 88.55 + }, + { + "name": "TEMPERATURE", + "value": 13.44 + } + ], + "indexes": [ + { + "name": "AIRLY_CAQI", + "value": 17.83, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Catch your breath!", + "color": "#6BC926" + } + ], + "standards": [ + { + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 42.8 + }, + { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 29.17 + } + ] + }, + { + "fromDateTime": "2019-10-02T03:00:00.000Z", + "tillDateTime": "2019-10-02T04:00:00.000Z", + "values": [ + { + "name": "PM1", + "value": 6.99 + }, + { + "name": "PM25", + "value": 10.23 + }, + { + "name": "PM10", + "value": 13.66 + }, + { + "name": "PRESSURE", + "value": 1001.34 + }, + { + "name": "HUMIDITY", + "value": 90.82 + }, + { + "name": "TEMPERATURE", + "value": 13.3 + } + ], + "indexes": [ + { + "name": "AIRLY_CAQI", + "value": 17.05, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Perfect air for exercising! Go for it!", + "color": "#6BC926" + } + ], + "standards": [ + { + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 40.91 + }, + { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 27.33 + } + ] + }, + { + "fromDateTime": "2019-10-02T04:00:00.000Z", + "tillDateTime": "2019-10-02T05:00:00.000Z", + "values": [ + { + "name": "PM1", + "value": 7.82 + }, + { + "name": "PM25", + "value": 11.59 + }, + { + "name": "PM10", + "value": 15.77 + }, + { + "name": "PRESSURE", + "value": 1000.92 + }, + { + "name": "HUMIDITY", + "value": 91.8 + }, + { + "name": "TEMPERATURE", + "value": 13.34 + } + ], + "indexes": [ + { + "name": "AIRLY_CAQI", + "value": 19.32, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Dear me, how wonderful!", + "color": "#6BC926" + } + ], + "standards": [ + { + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 46.36 + }, + { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 31.54 + } + ] + }, + { + "fromDateTime": "2019-10-02T05:00:00.000Z", + "tillDateTime": "2019-10-02T06:00:00.000Z", + "values": [ + { + "name": "PM1", + "value": 10.16 + }, + { + "name": "PM25", + "value": 15.35 + }, + { + "name": "PM10", + "value": 21.45 + }, + { + "name": "PRESSURE", + "value": 1000.82 + }, + { + "name": "HUMIDITY", + "value": 92.15 + }, + { + "name": "TEMPERATURE", + "value": 13.74 + } + ], + "indexes": [ + { + "name": "AIRLY_CAQI", + "value": 25.59, + "level": "LOW", + "description": "Air is quite good.", + "advice": "How about going for a walk?", + "color": "#D1CF1E" + } + ], + "standards": [ + { + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 61.42 + }, + { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 42.9 + } + ] + } + ], + "forecast": [ + { + "fromDateTime": "2019-10-02T06:00:00.000Z", + "tillDateTime": "2019-10-02T07:00:00.000Z", + "values": [ + { + "name": "PM25", + "value": 13.28 + }, + { + "name": "PM10", + "value": 18.37 + } + ], + "indexes": [ + { + "name": "AIRLY_CAQI", + "value": 22.14, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "It couldn't be better ;)", + "color": "#6BC926" + } + ], + "standards": [ + { + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 53.13 + }, + { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 36.73 + } + ] + }, + { + "fromDateTime": "2019-10-02T07:00:00.000Z", + "tillDateTime": "2019-10-02T08:00:00.000Z", + "values": [ + { + "name": "PM25", + "value": 11.19 + }, + { + "name": "PM10", + "value": 15.65 + } + ], + "indexes": [ + { + "name": "AIRLY_CAQI", + "value": 18.65, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Enjoy life!", + "color": "#6BC926" + } + ], + "standards": [ + { + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 44.76 + }, + { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 31.31 + } + ] + }, + { + "fromDateTime": "2019-10-02T08:00:00.000Z", + "tillDateTime": "2019-10-02T09:00:00.000Z", + "values": [ + { + "name": "PM25", + "value": 8.79 + }, + { + "name": "PM10", + "value": 12.8 + } + ], + "indexes": [ + { + "name": "AIRLY_CAQI", + "value": 14.65, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Breathe deep! The air is clean!", + "color": "#6BC926" + } + ], + "standards": [ + { + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 35.15 + }, + { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 25.59 + } + ] + }, + { + "fromDateTime": "2019-10-02T09:00:00.000Z", + "tillDateTime": "2019-10-02T10:00:00.000Z", + "values": [ + { + "name": "PM25", + "value": 5.46 + }, + { + "name": "PM10", + "value": 8.91 + } + ], + "indexes": [ + { + "name": "AIRLY_CAQI", + "value": 9.11, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Breathe to fill your lungs!", + "color": "#6BC926" + } + ], + "standards": [ + { + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 21.86 + }, + { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 17.83 + } + ] + }, + { + "fromDateTime": "2019-10-02T10:00:00.000Z", + "tillDateTime": "2019-10-02T11:00:00.000Z", + "values": [ + { + "name": "PM25", + "value": 2.26 + }, + { + "name": "PM10", + "value": 5.02 + } + ], + "indexes": [ + { + "name": "AIRLY_CAQI", + "value": 5.02, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Enjoy life!", + "color": "#6BC926" + } + ], + "standards": [ + { + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 9.06 + }, + { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 10.05 + } + ] + }, + { + "fromDateTime": "2019-10-02T11:00:00.000Z", + "tillDateTime": "2019-10-02T12:00:00.000Z", + "values": [ + { + "name": "PM25", + "value": 1.06 + }, + { + "name": "PM10", + "value": 2.52 + } + ], + "indexes": [ + { + "name": "AIRLY_CAQI", + "value": 2.52, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "The air is great!", + "color": "#6BC926" + } + ], + "standards": [ + { + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 4.22 + }, + { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 5.05 + } + ] + }, + { + "fromDateTime": "2019-10-02T12:00:00.000Z", + "tillDateTime": "2019-10-02T13:00:00.000Z", + "values": [ + { + "name": "PM25", + "value": 0.48 + }, + { + "name": "PM10", + "value": 1.94 + } + ], + "indexes": [ + { + "name": "AIRLY_CAQI", + "value": 1.94, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Breathe as much as you can!", + "color": "#6BC926" + } + ], + "standards": [ + { + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 1.94 + }, + { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 3.89 + } + ] + }, + { + "fromDateTime": "2019-10-02T13:00:00.000Z", + "tillDateTime": "2019-10-02T14:00:00.000Z", + "values": [ + { + "name": "PM25", + "value": 0.63 + }, + { + "name": "PM10", + "value": 2.26 + } + ], + "indexes": [ + { + "name": "AIRLY_CAQI", + "value": 2.26, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Enjoy life!", + "color": "#6BC926" + } + ], + "standards": [ + { + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 2.53 + }, + { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 4.52 + } + ] + }, + { + "fromDateTime": "2019-10-02T14:00:00.000Z", + "tillDateTime": "2019-10-02T15:00:00.000Z", + "values": [ + { + "name": "PM25", + "value": 1.47 + }, + { + "name": "PM10", + "value": 3.39 + } + ], + "indexes": [ + { + "name": "AIRLY_CAQI", + "value": 3.39, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Breathe as much as you can!", + "color": "#6BC926" + } + ], + "standards": [ + { + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 5.87 + }, + { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 6.78 + } + ] + }, + { + "fromDateTime": "2019-10-02T15:00:00.000Z", + "tillDateTime": "2019-10-02T16:00:00.000Z", + "values": [ + { + "name": "PM25", + "value": 2.62 + }, + { + "name": "PM10", + "value": 5.02 + } + ], + "indexes": [ + { + "name": "AIRLY_CAQI", + "value": 5.02, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Great air!", + "color": "#6BC926" + } + ], + "standards": [ + { + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 10.5 + }, + { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 10.05 + } + ] + }, + { + "fromDateTime": "2019-10-02T16:00:00.000Z", + "tillDateTime": "2019-10-02T17:00:00.000Z", + "values": [ + { + "name": "PM25", + "value": 3.89 + }, + { + "name": "PM10", + "value": 8.02 + } + ], + "indexes": [ + { + "name": "AIRLY_CAQI", + "value": 8.02, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Dear me, how wonderful!", + "color": "#6BC926" + } + ], + "standards": [ + { + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 15.56 + }, + { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 16.04 + } + ] + }, + { + "fromDateTime": "2019-10-02T17:00:00.000Z", + "tillDateTime": "2019-10-02T18:00:00.000Z", + "values": [ + { + "name": "PM25", + "value": 6.26 + }, + { + "name": "PM10", + "value": 11.41 + } + ], + "indexes": [ + { + "name": "AIRLY_CAQI", + "value": 11.41, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "The air is great!", + "color": "#6BC926" + } + ], + "standards": [ + { + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 25.05 + }, + { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 22.83 + } + ] + }, + { + "fromDateTime": "2019-10-02T18:00:00.000Z", + "tillDateTime": "2019-10-02T19:00:00.000Z", + "values": [ + { + "name": "PM25", + "value": 8.69 + }, + { + "name": "PM10", + "value": 14.48 + } + ], + "indexes": [ + { + "name": "AIRLY_CAQI", + "value": 14.48, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Zero dust - zero worries!", + "color": "#6BC926" + } + ], + "standards": [ + { + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 34.76 + }, + { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 28.96 + } + ] + }, + { + "fromDateTime": "2019-10-02T19:00:00.000Z", + "tillDateTime": "2019-10-02T20:00:00.000Z", + "values": [ + { + "name": "PM25", + "value": 10.78 + }, + { + "name": "PM10", + "value": 16.86 + } + ], + "indexes": [ + { + "name": "AIRLY_CAQI", + "value": 17.97, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Zero dust - zero worries!", + "color": "#6BC926" + } + ], + "standards": [ + { + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 43.13 + }, + { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 33.72 + } + ] + }, + { + "fromDateTime": "2019-10-02T20:00:00.000Z", + "tillDateTime": "2019-10-02T21:00:00.000Z", + "values": [ + { + "name": "PM25", + "value": 12.22 + }, + { + "name": "PM10", + "value": 18.19 + } + ], + "indexes": [ + { + "name": "AIRLY_CAQI", + "value": 20.36, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Breathe to fill your lungs!", + "color": "#6BC926" + } + ], + "standards": [ + { + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 48.88 + }, + { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 36.38 + } + ] + }, + { + "fromDateTime": "2019-10-02T21:00:00.000Z", + "tillDateTime": "2019-10-02T22:00:00.000Z", + "values": [ + { + "name": "PM25", + "value": 13.06 + }, + { + "name": "PM10", + "value": 18.62 + } + ], + "indexes": [ + { + "name": "AIRLY_CAQI", + "value": 21.77, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Dear me, how wonderful!", + "color": "#6BC926" + } + ], + "standards": [ + { + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 52.25 + }, + { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 37.24 + } + ] + }, + { + "fromDateTime": "2019-10-02T22:00:00.000Z", + "tillDateTime": "2019-10-02T23:00:00.000Z", + "values": [ + { + "name": "PM25", + "value": 13.51 + }, + { + "name": "PM10", + "value": 18.49 + } + ], + "indexes": [ + { + "name": "AIRLY_CAQI", + "value": 22.52, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "The air is great!", + "color": "#6BC926" + } + ], + "standards": [ + { + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 54.06 + }, + { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 36.98 + } + ] + }, + { + "fromDateTime": "2019-10-02T23:00:00.000Z", + "tillDateTime": "2019-10-03T00:00:00.000Z", + "values": [ + { + "name": "PM25", + "value": 13.46 + }, + { + "name": "PM10", + "value": 17.63 + } + ], + "indexes": [ + { + "name": "AIRLY_CAQI", + "value": 22.44, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Green, green, green!", + "color": "#6BC926" + } + ], + "standards": [ + { + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 53.85 + }, + { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 35.26 + } + ] + }, + { + "fromDateTime": "2019-10-03T00:00:00.000Z", + "tillDateTime": "2019-10-03T01:00:00.000Z", + "values": [ + { + "name": "PM25", + "value": 13.05 + }, + { + "name": "PM10", + "value": 16.36 + } + ], + "indexes": [ + { + "name": "AIRLY_CAQI", + "value": 21.74, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Catch your breath!", + "color": "#6BC926" + } + ], + "standards": [ + { + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 52.19 + }, + { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 32.73 + } + ] + }, + { + "fromDateTime": "2019-10-03T01:00:00.000Z", + "tillDateTime": "2019-10-03T02:00:00.000Z", + "values": [ + { + "name": "PM25", + "value": 12.47 + }, + { + "name": "PM10", + "value": 15.16 + } + ], + "indexes": [ + { + "name": "AIRLY_CAQI", + "value": 20.79, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Green, green, green!", + "color": "#6BC926" + } + ], + "standards": [ + { + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 49.9 + }, + { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 30.32 + } + ] + }, + { + "fromDateTime": "2019-10-03T02:00:00.000Z", + "tillDateTime": "2019-10-03T03:00:00.000Z", + "values": [ + { + "name": "PM25", + "value": 11.99 + }, + { + "name": "PM10", + "value": 14.07 + } + ], + "indexes": [ + { + "name": "AIRLY_CAQI", + "value": 19.98, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Breathe as much as you can!", + "color": "#6BC926" + } + ], + "standards": [ + { + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 47.94 + }, + { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 28.14 + } + ] + }, + { + "fromDateTime": "2019-10-03T03:00:00.000Z", + "tillDateTime": "2019-10-03T04:00:00.000Z", + "values": [ + { + "name": "PM25", + "value": 11.74 + }, + { + "name": "PM10", + "value": 13.67 + } + ], + "indexes": [ + { + "name": "AIRLY_CAQI", + "value": 19.56, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Dear me, how wonderful!", + "color": "#6BC926" + } + ], + "standards": [ + { + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 46.95 + }, + { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 27.34 + } + ] + }, + { + "fromDateTime": "2019-10-03T04:00:00.000Z", + "tillDateTime": "2019-10-03T05:00:00.000Z", + "values": [ + { + "name": "PM25", + "value": 11.44 + }, + { + "name": "PM10", + "value": 13.51 + } + ], + "indexes": [ + { + "name": "AIRLY_CAQI", + "value": 19.06, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Breathe to fill your lungs!", + "color": "#6BC926" + } + ], + "standards": [ + { + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 45.74 + }, + { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 27.02 + } + ] + }, + { + "fromDateTime": "2019-10-03T05:00:00.000Z", + "tillDateTime": "2019-10-03T06:00:00.000Z", + "values": [ + { + "name": "PM25", + "value": 10.88 + }, + { + "name": "PM10", + "value": 13.38 + } + ], + "indexes": [ + { + "name": "AIRLY_CAQI", + "value": 18.13, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Breathe as much as you can!", + "color": "#6BC926" + } + ], + "standards": [ + { + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 43.52 + }, + { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 26.76 + } + ] + } + ] +} diff --git a/tests/components/airvisual/fixtures/data.json b/tests/components/airvisual/fixtures/data.json index ff955e5528e..ea931d2d6ee 100644 --- a/tests/components/airvisual/fixtures/data.json +++ b/tests/components/airvisual/fixtures/data.json @@ -6,10 +6,7 @@ "country": "USA", "location": { "type": "Point", - "coordinates": [ - -105.06415, - 39.75304 - ] + "coordinates": [-105.06415, 39.75304] }, "current": { "weather": { diff --git a/tests/components/airzone/__init__.py b/tests/components/airzone/__init__.py new file mode 100644 index 00000000000..1d38439991b --- /dev/null +++ b/tests/components/airzone/__init__.py @@ -0,0 +1 @@ +"""Tests for the Airzone integration.""" diff --git a/tests/components/airzone/test_binary_sensor.py b/tests/components/airzone/test_binary_sensor.py new file mode 100644 index 00000000000..ee3a8324ea4 --- /dev/null +++ b/tests/components/airzone/test_binary_sensor.py @@ -0,0 +1,56 @@ +"""The sensor tests for the Airzone platform.""" + +from homeassistant.const import STATE_OFF, STATE_ON + +from .util import async_init_integration + + +async def test_airzone_create_binary_sensors(hass): + """Test creation of binary sensors.""" + + await async_init_integration(hass) + + state = hass.states.get("binary_sensor.despacho_air_demand") + assert state.state == STATE_OFF + + state = hass.states.get("binary_sensor.despacho_floor_demand") + assert state.state == STATE_OFF + + state = hass.states.get("binary_sensor.despacho_problem") + assert state.state == STATE_OFF + + state = hass.states.get("binary_sensor.dorm_1_air_demand") + assert state.state == STATE_OFF + + state = hass.states.get("binary_sensor.dorm_1_floor_demand") + assert state.state == STATE_OFF + + state = hass.states.get("binary_sensor.dorm_1_problem") + assert state.state == STATE_OFF + + state = hass.states.get("binary_sensor.dorm_2_air_demand") + assert state.state == STATE_OFF + + state = hass.states.get("binary_sensor.dorm_2_floor_demand") + assert state.state == STATE_OFF + + state = hass.states.get("binary_sensor.dorm_2_problem") + assert state.state == STATE_OFF + + state = hass.states.get("binary_sensor.dorm_ppal_air_demand") + assert state.state == STATE_ON + + state = hass.states.get("binary_sensor.dorm_ppal_floor_demand") + assert state.state == STATE_OFF + + state = hass.states.get("binary_sensor.dorm_ppal_problem") + assert state.state == STATE_OFF + + state = hass.states.get("binary_sensor.salon_air_demand") + assert state.state == STATE_OFF + + state = hass.states.get("binary_sensor.salon_floor_demand") + assert state.state == STATE_OFF + + state = hass.states.get("binary_sensor.salon_problem") + assert state.state == STATE_OFF diff --git a/tests/components/airzone/test_climate.py b/tests/components/airzone/test_climate.py new file mode 100644 index 00000000000..b06bb1f046f --- /dev/null +++ b/tests/components/airzone/test_climate.py @@ -0,0 +1,281 @@ +"""The climate tests for the Airzone platform.""" + +from unittest.mock import patch + +from aioairzone.common import OperationMode +from aioairzone.const import ( + API_DATA, + API_MODE, + API_ON, + API_SET_POINT, + API_SYSTEM_ID, + API_ZONE_ID, +) +from aioairzone.exceptions import AirzoneError +import pytest + +from homeassistant.components.airzone.const import API_TEMPERATURE_STEP +from homeassistant.components.climate.const import ( + ATTR_CURRENT_HUMIDITY, + ATTR_CURRENT_TEMPERATURE, + ATTR_HVAC_ACTION, + ATTR_HVAC_MODE, + ATTR_HVAC_MODES, + ATTR_MAX_TEMP, + ATTR_MIN_TEMP, + ATTR_TARGET_TEMP_STEP, + CURRENT_HVAC_HEAT, + CURRENT_HVAC_IDLE, + CURRENT_HVAC_OFF, + DOMAIN as CLIMATE_DOMAIN, + HVAC_MODE_COOL, + HVAC_MODE_DRY, + HVAC_MODE_FAN_ONLY, + HVAC_MODE_HEAT, + HVAC_MODE_OFF, + SERVICE_SET_HVAC_MODE, + SERVICE_SET_TEMPERATURE, +) +from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE +from homeassistant.exceptions import HomeAssistantError + +from .util import async_init_integration + + +async def test_airzone_create_climates(hass): + """Test creation of climates.""" + + await async_init_integration(hass) + + state = hass.states.get("climate.despacho") + assert state.state == HVAC_MODE_OFF + assert state.attributes.get(ATTR_CURRENT_HUMIDITY) == 36 + assert state.attributes.get(ATTR_CURRENT_TEMPERATURE) == 21.2 + assert state.attributes.get(ATTR_HVAC_ACTION) == CURRENT_HVAC_OFF + assert state.attributes.get(ATTR_HVAC_MODES) == [ + HVAC_MODE_OFF, + HVAC_MODE_FAN_ONLY, + HVAC_MODE_COOL, + HVAC_MODE_HEAT, + HVAC_MODE_DRY, + ] + assert state.attributes.get(ATTR_MAX_TEMP) == 30 + assert state.attributes.get(ATTR_MIN_TEMP) == 15 + assert state.attributes.get(ATTR_TARGET_TEMP_STEP) == API_TEMPERATURE_STEP + assert state.attributes.get(ATTR_TEMPERATURE) == 19.4 + + state = hass.states.get("climate.dorm_1") + assert state.state == HVAC_MODE_HEAT + assert state.attributes.get(ATTR_CURRENT_HUMIDITY) == 35 + assert state.attributes.get(ATTR_CURRENT_TEMPERATURE) == 20.8 + assert state.attributes.get(ATTR_HVAC_ACTION) == CURRENT_HVAC_IDLE + assert state.attributes.get(ATTR_HVAC_MODES) == [ + HVAC_MODE_OFF, + HVAC_MODE_FAN_ONLY, + HVAC_MODE_COOL, + HVAC_MODE_HEAT, + HVAC_MODE_DRY, + ] + assert state.attributes.get(ATTR_MAX_TEMP) == 30 + assert state.attributes.get(ATTR_MIN_TEMP) == 15 + assert state.attributes.get(ATTR_TARGET_TEMP_STEP) == API_TEMPERATURE_STEP + assert state.attributes.get(ATTR_TEMPERATURE) == 19.3 + + state = hass.states.get("climate.dorm_2") + assert state.state == HVAC_MODE_OFF + assert state.attributes.get(ATTR_CURRENT_HUMIDITY) == 40 + assert state.attributes.get(ATTR_CURRENT_TEMPERATURE) == 20.5 + assert state.attributes.get(ATTR_HVAC_ACTION) == CURRENT_HVAC_OFF + assert state.attributes.get(ATTR_HVAC_MODES) == [ + HVAC_MODE_OFF, + HVAC_MODE_FAN_ONLY, + HVAC_MODE_COOL, + HVAC_MODE_HEAT, + HVAC_MODE_DRY, + ] + assert state.attributes.get(ATTR_MAX_TEMP) == 30 + assert state.attributes.get(ATTR_MIN_TEMP) == 15 + assert state.attributes.get(ATTR_TARGET_TEMP_STEP) == API_TEMPERATURE_STEP + assert state.attributes.get(ATTR_TEMPERATURE) == 19.5 + + state = hass.states.get("climate.dorm_ppal") + assert state.state == HVAC_MODE_HEAT + assert state.attributes.get(ATTR_CURRENT_HUMIDITY) == 39 + assert state.attributes.get(ATTR_CURRENT_TEMPERATURE) == 21.1 + assert state.attributes.get(ATTR_HVAC_ACTION) == CURRENT_HVAC_HEAT + assert state.attributes.get(ATTR_HVAC_MODES) == [ + HVAC_MODE_OFF, + HVAC_MODE_FAN_ONLY, + HVAC_MODE_COOL, + HVAC_MODE_HEAT, + HVAC_MODE_DRY, + ] + assert state.attributes.get(ATTR_MAX_TEMP) == 30 + assert state.attributes.get(ATTR_MIN_TEMP) == 15 + assert state.attributes.get(ATTR_TARGET_TEMP_STEP) == API_TEMPERATURE_STEP + assert state.attributes.get(ATTR_TEMPERATURE) == 19.2 + + state = hass.states.get("climate.salon") + assert state.state == HVAC_MODE_OFF + assert state.attributes.get(ATTR_CURRENT_HUMIDITY) == 34 + assert state.attributes.get(ATTR_CURRENT_TEMPERATURE) == 19.6 + assert state.attributes.get(ATTR_HVAC_ACTION) == CURRENT_HVAC_OFF + assert state.attributes.get(ATTR_HVAC_MODES) == [ + HVAC_MODE_OFF, + HVAC_MODE_FAN_ONLY, + HVAC_MODE_COOL, + HVAC_MODE_HEAT, + HVAC_MODE_DRY, + ] + assert state.attributes.get(ATTR_MAX_TEMP) == 30 + assert state.attributes.get(ATTR_MIN_TEMP) == 15 + assert state.attributes.get(ATTR_TARGET_TEMP_STEP) == API_TEMPERATURE_STEP + assert state.attributes.get(ATTR_TEMPERATURE) == 19.1 + + +async def test_airzone_climate_set_hvac_mode(hass): + """Test setting the HVAC mode.""" + + await async_init_integration(hass) + + HVAC_MOCK = { + API_DATA: [ + { + API_SYSTEM_ID: 1, + API_ZONE_ID: 1, + API_MODE: OperationMode.COOLING.value, + API_ON: 1, + } + ] + } + with patch( + "homeassistant.components.airzone.AirzoneLocalApi.http_request", + return_value=HVAC_MOCK, + ): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + { + ATTR_ENTITY_ID: "climate.salon", + ATTR_HVAC_MODE: HVAC_MODE_COOL, + }, + blocking=True, + ) + + state = hass.states.get("climate.salon") + assert state.state == HVAC_MODE_COOL + + HVAC_MOCK_2 = { + API_DATA: [ + { + API_SYSTEM_ID: 1, + API_ZONE_ID: 1, + API_ON: 0, + } + ] + } + with patch( + "homeassistant.components.airzone.AirzoneLocalApi.http_request", + return_value=HVAC_MOCK_2, + ): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + { + ATTR_ENTITY_ID: "climate.salon", + ATTR_HVAC_MODE: HVAC_MODE_OFF, + }, + blocking=True, + ) + + state = hass.states.get("climate.salon") + assert state.state == HVAC_MODE_OFF + + +async def test_airzone_climate_set_hvac_slave_error(hass): + """Test setting the HVAC mode for a slave zone.""" + + HVAC_MOCK = { + API_DATA: [ + { + API_SYSTEM_ID: 1, + API_ZONE_ID: 5, + API_ON: 1, + } + ] + } + + await async_init_integration(hass) + + with patch( + "homeassistant.components.airzone.AirzoneLocalApi.http_request", + return_value=HVAC_MOCK, + ), pytest.raises(HomeAssistantError): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + { + ATTR_ENTITY_ID: "climate.dorm_2", + ATTR_HVAC_MODE: HVAC_MODE_COOL, + }, + blocking=True, + ) + + state = hass.states.get("climate.dorm_2") + assert state.state == HVAC_MODE_OFF + + +async def test_airzone_climate_set_temp(hass): + """Test setting the target temperature.""" + + HVAC_MOCK = { + API_DATA: [ + { + API_SYSTEM_ID: 1, + API_ZONE_ID: 5, + API_SET_POINT: 20.5, + } + ] + } + + await async_init_integration(hass) + + with patch( + "homeassistant.components.airzone.AirzoneLocalApi.http_request", + return_value=HVAC_MOCK, + ): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, + { + ATTR_ENTITY_ID: "climate.dorm_2", + ATTR_TEMPERATURE: 20.5, + }, + blocking=True, + ) + + state = hass.states.get("climate.dorm_2") + assert state.attributes.get(ATTR_TEMPERATURE) == 20.5 + + +async def test_airzone_climate_set_temp_error(hass): + """Test error when setting the target temperature.""" + + await async_init_integration(hass) + + with patch( + "homeassistant.components.airzone.AirzoneLocalApi.put_hvac", + side_effect=AirzoneError, + ), pytest.raises(HomeAssistantError): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, + { + ATTR_ENTITY_ID: "climate.dorm_2", + ATTR_TEMPERATURE: 20.5, + }, + blocking=True, + ) + + state = hass.states.get("climate.dorm_2") + assert state.attributes.get(ATTR_TEMPERATURE) == 19.5 diff --git a/tests/components/airzone/test_config_flow.py b/tests/components/airzone/test_config_flow.py new file mode 100644 index 00000000000..a6612d6de9c --- /dev/null +++ b/tests/components/airzone/test_config_flow.py @@ -0,0 +1,82 @@ +"""Define tests for the Airzone config flow.""" + +from unittest.mock import MagicMock, patch + +from aiohttp.client_exceptions import ClientConnectorError + +from homeassistant import data_entry_flow +from homeassistant.components.airzone.const import DOMAIN +from homeassistant.config_entries import SOURCE_USER, ConfigEntryState +from homeassistant.const import CONF_HOST, CONF_PORT + +from .util import CONFIG, HVAC_MOCK + +from tests.common import MockConfigEntry + + +async def test_form(hass): + """Test that the form is served with valid input.""" + + with patch( + "homeassistant.components.airzone.async_setup_entry", + return_value=True, + ) as mock_setup_entry, patch( + "homeassistant.components.airzone.AirzoneLocalApi.get_hvac", + return_value=HVAC_MOCK, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == SOURCE_USER + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], CONFIG + ) + + await hass.async_block_till_done() + + conf_entries = hass.config_entries.async_entries(DOMAIN) + entry = conf_entries[0] + assert entry.state is ConfigEntryState.LOADED + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == f"Airzone {CONFIG[CONF_HOST]}:{CONFIG[CONF_PORT]}" + assert result["data"][CONF_HOST] == CONFIG[CONF_HOST] + assert result["data"][CONF_PORT] == CONFIG[CONF_PORT] + + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_duplicated_id(hass): + """Test setting up duplicated entry.""" + + entry = MockConfigEntry(domain=DOMAIN, data=CONFIG) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.airzone.AirzoneLocalApi.get_hvac", + return_value=HVAC_MOCK, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=CONFIG + ) + + assert result["type"] == "abort" + assert result["reason"] == "already_configured" + + +async def test_connection_error(hass): + """Test connection to host error.""" + + with patch( + "homeassistant.components.airzone.AirzoneLocalApi.validate_airzone", + side_effect=ClientConnectorError(MagicMock(), MagicMock()), + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=CONFIG + ) + + assert result["errors"] == {"base": "cannot_connect"} diff --git a/tests/components/airzone/test_coordinator.py b/tests/components/airzone/test_coordinator.py new file mode 100644 index 00000000000..00ef0616b3e --- /dev/null +++ b/tests/components/airzone/test_coordinator.py @@ -0,0 +1,39 @@ +"""Define tests for the Airzone coordinator.""" + +from unittest.mock import MagicMock, patch + +from aiohttp import ClientConnectorError + +from homeassistant.components.airzone.const import DOMAIN +from homeassistant.components.airzone.coordinator import SCAN_INTERVAL +from homeassistant.const import STATE_UNAVAILABLE +from homeassistant.core import HomeAssistant +from homeassistant.util.dt import utcnow + +from .util import CONFIG, HVAC_MOCK + +from tests.common import MockConfigEntry, async_fire_time_changed + + +async def test_coordinator_client_connector_error(hass: HomeAssistant): + """Test ClientConnectorError on coordinator update.""" + + entry = MockConfigEntry(domain=DOMAIN, data=CONFIG) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.airzone.AirzoneLocalApi.get_hvac", + return_value=HVAC_MOCK, + ) as mock_hvac: + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + mock_hvac.assert_called_once() + mock_hvac.reset_mock() + + mock_hvac.side_effect = ClientConnectorError(MagicMock(), MagicMock()) + async_fire_time_changed(hass, utcnow() + SCAN_INTERVAL) + await hass.async_block_till_done() + mock_hvac.assert_called_once() + + state = hass.states.get("sensor.despacho_temperature") + assert state.state == STATE_UNAVAILABLE diff --git a/tests/components/airzone/test_init.py b/tests/components/airzone/test_init.py new file mode 100644 index 00000000000..30e3ce37d6f --- /dev/null +++ b/tests/components/airzone/test_init.py @@ -0,0 +1,31 @@ +"""Define tests for the Airzone init.""" + +from unittest.mock import patch + +from homeassistant.components.airzone.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState + +from .util import CONFIG, HVAC_MOCK + +from tests.common import MockConfigEntry + + +async def test_unload_entry(hass): + """Test unload.""" + + config_entry = MockConfigEntry( + domain=DOMAIN, unique_id="airzone_unique_id", data=CONFIG + ) + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.airzone.AirzoneLocalApi.get_hvac", + return_value=HVAC_MOCK, + ): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert config_entry.state is ConfigEntryState.LOADED + + await hass.config_entries.async_unload(config_entry.entry_id) + await hass.async_block_till_done() + assert config_entry.state is ConfigEntryState.NOT_LOADED diff --git a/tests/components/airzone/test_sensor.py b/tests/components/airzone/test_sensor.py new file mode 100644 index 00000000000..fc03d8a3301 --- /dev/null +++ b/tests/components/airzone/test_sensor.py @@ -0,0 +1,39 @@ +"""The sensor tests for the Airzone platform.""" + +from .util import async_init_integration + + +async def test_airzone_create_sensors(hass): + """Test creation of sensors.""" + + await async_init_integration(hass) + + state = hass.states.get("sensor.despacho_temperature") + assert state.state == "21.2" + + state = hass.states.get("sensor.despacho_humidity") + assert state.state == "36" + + state = hass.states.get("sensor.dorm_1_temperature") + assert state.state == "20.8" + + state = hass.states.get("sensor.dorm_1_humidity") + assert state.state == "35" + + state = hass.states.get("sensor.dorm_2_temperature") + assert state.state == "20.5" + + state = hass.states.get("sensor.dorm_2_humidity") + assert state.state == "40" + + state = hass.states.get("sensor.dorm_ppal_temperature") + assert state.state == "21.1" + + state = hass.states.get("sensor.dorm_ppal_humidity") + assert state.state == "39" + + state = hass.states.get("sensor.salon_temperature") + assert state.state == "19.6" + + state = hass.states.get("sensor.salon_humidity") + assert state.state == "34" diff --git a/tests/components/airzone/util.py b/tests/components/airzone/util.py new file mode 100644 index 00000000000..2f7afb068b3 --- /dev/null +++ b/tests/components/airzone/util.py @@ -0,0 +1,164 @@ +"""Tests for the Airzone integration.""" + +from unittest.mock import patch + +from aioairzone.const import ( + API_AIR_DEMAND, + API_COLD_STAGE, + API_COLD_STAGES, + API_DATA, + API_ERRORS, + API_FLOOR_DEMAND, + API_HEAT_STAGE, + API_HEAT_STAGES, + API_HUMIDITY, + API_MAX_TEMP, + API_MIN_TEMP, + API_MODE, + API_MODES, + API_NAME, + API_ON, + API_ROOM_TEMP, + API_SET_POINT, + API_SYSTEM_ID, + API_SYSTEMS, + API_UNITS, + API_ZONE_ID, +) + +from homeassistant.components.airzone import DOMAIN +from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + +CONFIG = { + CONF_HOST: "192.168.1.100", + CONF_PORT: 3000, +} + +HVAC_MOCK = { + API_SYSTEMS: [ + { + API_DATA: [ + { + API_SYSTEM_ID: 1, + API_ZONE_ID: 1, + API_NAME: "Salon", + API_ON: 0, + API_MAX_TEMP: 30, + API_MIN_TEMP: 15, + API_SET_POINT: 19.1, + API_ROOM_TEMP: 19.6, + API_MODES: [1, 4, 2, 3, 5], + API_MODE: 3, + API_COLD_STAGES: 1, + API_COLD_STAGE: 1, + API_HEAT_STAGES: 1, + API_HEAT_STAGE: 1, + API_HUMIDITY: 34, + API_UNITS: 0, + API_ERRORS: [], + API_AIR_DEMAND: 0, + API_FLOOR_DEMAND: 0, + }, + { + API_SYSTEM_ID: 1, + API_ZONE_ID: 2, + API_NAME: "Dorm Ppal", + API_ON: 1, + API_MAX_TEMP: 30, + API_MIN_TEMP: 15, + API_SET_POINT: 19.2, + API_ROOM_TEMP: 21.1, + API_MODE: 3, + API_COLD_STAGES: 1, + API_COLD_STAGE: 1, + API_HEAT_STAGES: 1, + API_HEAT_STAGE: 1, + API_HUMIDITY: 39, + API_UNITS: 0, + API_ERRORS: [], + API_AIR_DEMAND: 1, + API_FLOOR_DEMAND: 0, + }, + { + API_SYSTEM_ID: 1, + API_ZONE_ID: 3, + API_NAME: "Dorm #1", + API_ON: 1, + API_MAX_TEMP: 30, + API_MIN_TEMP: 15, + API_SET_POINT: 19.3, + API_ROOM_TEMP: 20.8, + API_MODE: 3, + API_COLD_STAGES: 1, + API_COLD_STAGE: 1, + API_HEAT_STAGES: 1, + API_HEAT_STAGE: 1, + API_HUMIDITY: 35, + API_UNITS: 0, + API_ERRORS: [], + API_AIR_DEMAND: 0, + API_FLOOR_DEMAND: 0, + }, + { + API_SYSTEM_ID: 1, + API_ZONE_ID: 4, + API_NAME: "Despacho", + API_ON: 0, + API_MAX_TEMP: 86, + API_MIN_TEMP: 59, + API_SET_POINT: 66.92, + API_ROOM_TEMP: 70.16, + API_MODE: 3, + API_COLD_STAGES: 1, + API_COLD_STAGE: 1, + API_HEAT_STAGES: 1, + API_HEAT_STAGE: 1, + API_HUMIDITY: 36, + API_UNITS: 1, + API_ERRORS: [], + API_AIR_DEMAND: 0, + API_FLOOR_DEMAND: 0, + }, + { + API_SYSTEM_ID: 1, + API_ZONE_ID: 5, + API_NAME: "Dorm #2", + API_ON: 0, + API_MAX_TEMP: 30, + API_MIN_TEMP: 15, + API_SET_POINT: 19.5, + API_ROOM_TEMP: 20.5, + API_MODE: 3, + API_COLD_STAGES: 1, + API_COLD_STAGE: 1, + API_HEAT_STAGES: 1, + API_HEAT_STAGE: 1, + API_HUMIDITY: 40, + API_UNITS: 0, + API_ERRORS: [], + API_AIR_DEMAND: 0, + API_FLOOR_DEMAND: 0, + }, + ] + } + ] +} + + +async def async_init_integration( + hass: HomeAssistant, +): + """Set up the Airzone integration in Home Assistant.""" + + entry = MockConfigEntry(domain=DOMAIN, data=CONFIG) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.airzone.AirzoneLocalApi.get_hvac", + return_value=HVAC_MOCK, + ): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() diff --git a/tests/components/alarm_control_panel/test_reproduce_state.py b/tests/components/alarm_control_panel/test_reproduce_state.py index 0f87e2206ac..b78815c24f4 100644 --- a/tests/components/alarm_control_panel/test_reproduce_state.py +++ b/tests/components/alarm_control_panel/test_reproduce_state.py @@ -16,6 +16,7 @@ from homeassistant.const import ( STATE_ALARM_TRIGGERED, ) from homeassistant.core import State +from homeassistant.helpers.state import async_reproduce_state from tests.common import async_mock_service @@ -67,7 +68,8 @@ async def test_reproducing_states(hass, caplog): ) # These calls should do nothing as entities already in desired state - await hass.helpers.state.async_reproduce_state( + await async_reproduce_state( + hass, [ State("alarm_control_panel.entity_armed_away", STATE_ALARM_ARMED_AWAY), State( @@ -81,7 +83,7 @@ async def test_reproducing_states(hass, caplog): ), State("alarm_control_panel.entity_disarmed", STATE_ALARM_DISARMED), State("alarm_control_panel.entity_triggered", STATE_ALARM_TRIGGERED), - ] + ], ) assert len(arm_away_calls) == 0 @@ -93,8 +95,8 @@ async def test_reproducing_states(hass, caplog): assert len(trigger_calls) == 0 # Test invalid state is handled - await hass.helpers.state.async_reproduce_state( - [State("alarm_control_panel.entity_triggered", "not_supported")] + await async_reproduce_state( + hass, [State("alarm_control_panel.entity_triggered", "not_supported")] ) assert "not_supported" in caplog.text @@ -107,7 +109,8 @@ async def test_reproducing_states(hass, caplog): assert len(trigger_calls) == 0 # Make sure correct services are called - await hass.helpers.state.async_reproduce_state( + await async_reproduce_state( + hass, [ State("alarm_control_panel.entity_armed_away", STATE_ALARM_TRIGGERED), State( @@ -122,7 +125,7 @@ async def test_reproducing_states(hass, caplog): State("alarm_control_panel.entity_triggered", STATE_ALARM_DISARMED), # Should not raise State("alarm_control_panel.non_existing", "on"), - ] + ], ) assert len(arm_away_calls) == 1 diff --git a/tests/components/alert/test_reproduce_state.py b/tests/components/alert/test_reproduce_state.py index 2470106558c..83b5cf45701 100644 --- a/tests/components/alert/test_reproduce_state.py +++ b/tests/components/alert/test_reproduce_state.py @@ -1,5 +1,6 @@ """Test reproduce state for Alert.""" from homeassistant.core import State +from homeassistant.helpers.state import async_reproduce_state from tests.common import async_mock_service @@ -13,30 +14,29 @@ async def test_reproducing_states(hass, caplog): turn_off_calls = async_mock_service(hass, "alert", "turn_off") # These calls should do nothing as entities already in desired state - await hass.helpers.state.async_reproduce_state( - [State("alert.entity_off", "off"), State("alert.entity_on", "on")] + await async_reproduce_state( + hass, [State("alert.entity_off", "off"), State("alert.entity_on", "on")] ) assert len(turn_on_calls) == 0 assert len(turn_off_calls) == 0 # Test invalid state is handled - await hass.helpers.state.async_reproduce_state( - [State("alert.entity_off", "not_supported")] - ) + await async_reproduce_state(hass, [State("alert.entity_off", "not_supported")]) assert "not_supported" in caplog.text assert len(turn_on_calls) == 0 assert len(turn_off_calls) == 0 # Make sure correct services are called - await hass.helpers.state.async_reproduce_state( + await async_reproduce_state( + hass, [ State("alert.entity_on", "off"), State("alert.entity_off", "on"), # Should not raise State("alert.non_existing", "on"), - ] + ], ) assert len(turn_on_calls) == 1 diff --git a/tests/components/alexa/__init__.py b/tests/components/alexa/__init__.py index 053100d2e00..1f0854c5102 100644 --- a/tests/components/alexa/__init__.py +++ b/tests/components/alexa/__init__.py @@ -3,8 +3,10 @@ import re from unittest.mock import Mock from uuid import uuid4 -from homeassistant.components.alexa import config, smart_home +from homeassistant.components.alexa import config, smart_home, smart_home_http +from homeassistant.components.alexa.const import CONF_ENDPOINT, CONF_FILTER, CONF_LOCALE from homeassistant.core import Context, callback +from homeassistant.helpers import entityfilter from tests.common import async_mock_service @@ -13,7 +15,7 @@ TEST_TOKEN_URL = "https://api.amazon.com/auth/o2/token" TEST_LOCALE = "en-US" -class MockConfig(config.AbstractConfig): +class MockConfig(smart_home_http.AlexaConfig): """Mock Alexa config.""" entity_config = { @@ -26,7 +28,14 @@ class MockConfig(config.AbstractConfig): def __init__(self, hass): """Mock Alexa config.""" - super().__init__(hass) + super().__init__( + hass, + { + CONF_ENDPOINT: TEST_URL, + CONF_FILTER: entityfilter.FILTER_SCHEMA({}), + CONF_LOCALE: TEST_LOCALE, + }, + ) self._store = Mock(spec_set=config.AlexaConfigStore) @property @@ -34,25 +43,11 @@ class MockConfig(config.AbstractConfig): """Return if config supports auth.""" return True - @property - def endpoint(self): - """Endpoint for report state.""" - return TEST_URL - - @property - def locale(self): - """Return config locale.""" - return TEST_LOCALE - @callback def user_identifier(self): """Return an identifier for the user that represents this config.""" return "mock-user-id" - def should_expose(self, entity_id): - """If an entity should be exposed.""" - return True - @callback def async_invalidate_access_token(self): """Invalidate access token.""" @@ -65,9 +60,9 @@ class MockConfig(config.AbstractConfig): """Accept a grant.""" -def get_default_config(): +def get_default_config(hass): """Return a MockConfig instance.""" - return MockConfig(None) + return MockConfig(hass) def get_new_request(namespace, name, endpoint=None): @@ -117,7 +112,7 @@ async def assert_request_calls_service( calls = async_mock_service(hass, domain, service_name) msg = await smart_home.async_handle_message( - hass, get_default_config(), request, context + hass, get_default_config(hass), request, context ) await hass.async_block_till_done() @@ -142,7 +137,7 @@ async def assert_request_fails( domain, service_name = service_not_called.split(".") call = async_mock_service(hass, domain, service_name) - msg = await smart_home.async_handle_message(hass, get_default_config(), request) + msg = await smart_home.async_handle_message(hass, get_default_config(hass), request) await hass.async_block_till_done() assert not call @@ -201,7 +196,7 @@ async def reported_properties(hass, endpoint, return_full_response=False): assertions about the properties. """ request = get_new_request("Alexa", "ReportState", endpoint) - msg = await smart_home.async_handle_message(hass, get_default_config(), request) + msg = await smart_home.async_handle_message(hass, get_default_config(hass), request) await hass.async_block_till_done() if return_full_response: return msg diff --git a/tests/components/alexa/test_capabilities.py b/tests/components/alexa/test_capabilities.py index d24849e1006..3f76c6bee04 100644 --- a/tests/components/alexa/test_capabilities.py +++ b/tests/components/alexa/test_capabilities.py @@ -55,7 +55,7 @@ async def test_api_adjust_brightness(hass, adjust): call_light = async_mock_service(hass, "light", "turn_on") - msg = await smart_home.async_handle_message(hass, get_default_config(), request) + msg = await smart_home.async_handle_message(hass, get_default_config(hass), request) await hass.async_block_till_done() assert "event" in msg @@ -85,7 +85,7 @@ async def test_api_set_color_rgb(hass): call_light = async_mock_service(hass, "light", "turn_on") - msg = await smart_home.async_handle_message(hass, get_default_config(), request) + msg = await smart_home.async_handle_message(hass, get_default_config(hass), request) await hass.async_block_till_done() assert "event" in msg @@ -111,7 +111,7 @@ async def test_api_set_color_temperature(hass): call_light = async_mock_service(hass, "light", "turn_on") - msg = await smart_home.async_handle_message(hass, get_default_config(), request) + msg = await smart_home.async_handle_message(hass, get_default_config(hass), request) await hass.async_block_till_done() assert "event" in msg @@ -139,7 +139,7 @@ async def test_api_decrease_color_temp(hass, result, initial): call_light = async_mock_service(hass, "light", "turn_on") - msg = await smart_home.async_handle_message(hass, get_default_config(), request) + msg = await smart_home.async_handle_message(hass, get_default_config(hass), request) await hass.async_block_till_done() assert "event" in msg @@ -167,7 +167,7 @@ async def test_api_increase_color_temp(hass, result, initial): call_light = async_mock_service(hass, "light", "turn_on") - msg = await smart_home.async_handle_message(hass, get_default_config(), request) + msg = await smart_home.async_handle_message(hass, get_default_config(hass), request) await hass.async_block_till_done() assert "event" in msg diff --git a/tests/components/alexa/test_entities.py b/tests/components/alexa/test_entities.py index 54e48df8e8e..7b2a455be92 100644 --- a/tests/components/alexa/test_entities.py +++ b/tests/components/alexa/test_entities.py @@ -3,6 +3,8 @@ from unittest.mock import patch from homeassistant.components.alexa import smart_home from homeassistant.const import __version__ +from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.entity import EntityCategory from . import get_default_config, get_new_request @@ -13,7 +15,63 @@ async def test_unsupported_domain(hass): hass.states.async_set("woz.boop", "on", {"friendly_name": "Boop Woz"}) - msg = await smart_home.async_handle_message(hass, get_default_config(), request) + msg = await smart_home.async_handle_message(hass, get_default_config(hass), request) + + assert "event" in msg + msg = msg["event"] + + assert not msg["payload"]["endpoints"] + + +async def test_categorized_hidden_entities(hass): + """Discovery ignores hidden and categorized entities.""" + entity_registry = er.async_get(hass) + request = get_new_request("Alexa.Discovery", "Discover") + + entity_entry1 = entity_registry.async_get_or_create( + "switch", + "test", + "switch_config_id", + suggested_object_id="config_switch", + entity_category=EntityCategory.CONFIG, + ) + entity_entry2 = entity_registry.async_get_or_create( + "switch", + "test", + "switch_diagnostic_id", + suggested_object_id="diagnostic_switch", + entity_category=EntityCategory.DIAGNOSTIC, + ) + entity_entry3 = entity_registry.async_get_or_create( + "switch", + "test", + "switch_system_id", + suggested_object_id="system_switch", + entity_category=EntityCategory.SYSTEM, + ) + entity_entry4 = entity_registry.async_get_or_create( + "switch", + "test", + "switch_hidden_integration_id", + suggested_object_id="hidden_integration_switch", + hidden_by=er.RegistryEntryHider.INTEGRATION, + ) + entity_entry5 = entity_registry.async_get_or_create( + "switch", + "test", + "switch_hidden_user_id", + suggested_object_id="hidden_user_switch", + hidden_by=er.RegistryEntryHider.USER, + ) + + # These should not show up in the sync request + hass.states.async_set(entity_entry1.entity_id, "on") + hass.states.async_set(entity_entry2.entity_id, "something_else") + hass.states.async_set(entity_entry3.entity_id, "blah") + hass.states.async_set(entity_entry4.entity_id, "foo") + hass.states.async_set(entity_entry5.entity_id, "bar") + + msg = await smart_home.async_handle_message(hass, get_default_config(hass), request) assert "event" in msg msg = msg["event"] @@ -27,7 +85,7 @@ async def test_serialize_discovery(hass): hass.states.async_set("switch.bla", "on", {"friendly_name": "Boop Woz"}) - msg = await smart_home.async_handle_message(hass, get_default_config(), request) + msg = await smart_home.async_handle_message(hass, get_default_config(hass), request) assert "event" in msg msg = msg["event"] @@ -51,7 +109,9 @@ async def test_serialize_discovery_recovers(hass, caplog): "homeassistant.components.alexa.capabilities.AlexaPowerController.serialize_discovery", side_effect=TypeError, ): - msg = await smart_home.async_handle_message(hass, get_default_config(), request) + msg = await smart_home.async_handle_message( + hass, get_default_config(hass), request + ) assert "event" in msg msg = msg["event"] diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 7ebba26113d..9fb6584fae3 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -121,7 +121,7 @@ async def test_wrong_version(hass): msg["directive"]["header"]["payloadVersion"] = "2" with pytest.raises(AssertionError): - await smart_home.async_handle_message(hass, get_default_config(), msg) + await smart_home.async_handle_message(hass, get_default_config(hass), msg) async def discovery_test(device, hass, expected_endpoints=1): @@ -131,7 +131,7 @@ async def discovery_test(device, hass, expected_endpoints=1): # setup test devices hass.states.async_set(*device) - msg = await smart_home.async_handle_message(hass, get_default_config(), request) + msg = await smart_home.async_handle_message(hass, get_default_config(hass), request) assert "event" in msg msg = msg["event"] @@ -2308,7 +2308,7 @@ async def test_api_entity_not_exists(hass): call_switch = async_mock_service(hass, "switch", "turn_on") - msg = await smart_home.async_handle_message(hass, get_default_config(), request) + msg = await smart_home.async_handle_message(hass, get_default_config(hass), request) await hass.async_block_till_done() assert "event" in msg @@ -2323,7 +2323,7 @@ async def test_api_entity_not_exists(hass): async def test_api_function_not_implemented(hass): """Test api call that is not implemented to us.""" request = get_new_request("Alexa.HAHAAH", "Sweet") - msg = await smart_home.async_handle_message(hass, get_default_config(), request) + msg = await smart_home.async_handle_message(hass, get_default_config(hass), request) assert "event" in msg msg = msg["event"] @@ -2347,7 +2347,7 @@ async def test_api_accept_grant(hass): } # setup test devices - msg = await smart_home.async_handle_message(hass, get_default_config(), request) + msg = await smart_home.async_handle_message(hass, get_default_config(hass), request) await hass.async_block_till_done() assert "event" in msg @@ -2400,7 +2400,9 @@ async def test_logging_request(hass, events): """Test that we log requests.""" context = Context() request = get_new_request("Alexa.Discovery", "Discover") - await smart_home.async_handle_message(hass, get_default_config(), request, context) + await smart_home.async_handle_message( + hass, get_default_config(hass), request, context + ) # To trigger event listener await hass.async_block_till_done() @@ -2420,7 +2422,9 @@ async def test_logging_request_with_entity(hass, events): """Test that we log requests.""" context = Context() request = get_new_request("Alexa.PowerController", "TurnOn", "switch#xy") - await smart_home.async_handle_message(hass, get_default_config(), request, context) + await smart_home.async_handle_message( + hass, get_default_config(hass), request, context + ) # To trigger event listener await hass.async_block_till_done() @@ -2446,7 +2450,7 @@ async def test_disabled(hass): call_switch = async_mock_service(hass, "switch", "turn_on") msg = await smart_home.async_handle_message( - hass, get_default_config(), request, enabled=False + hass, get_default_config(hass), request, enabled=False ) await hass.async_block_till_done() @@ -2630,7 +2634,7 @@ async def test_range_unsupported_domain(hass): request["directive"]["header"]["instance"] = "switch.speed" msg = await smart_home.async_handle_message( - hass, get_default_config(), request, context + hass, get_default_config(hass), request, context ) assert "event" in msg @@ -2651,7 +2655,7 @@ async def test_mode_unsupported_domain(hass): request["directive"]["header"]["instance"] = "switch.direction" msg = await smart_home.async_handle_message( - hass, get_default_config(), request, context + hass, get_default_config(hass), request, context ) assert "event" in msg @@ -3393,7 +3397,7 @@ async def test_media_player_eq_bands_not_supported(hass): ) request["directive"]["payload"] = {"bands": [{"name": "BASS", "value": -2}]} msg = await smart_home.async_handle_message( - hass, get_default_config(), request, context + hass, get_default_config(hass), request, context ) assert "event" in msg @@ -3410,7 +3414,7 @@ async def test_media_player_eq_bands_not_supported(hass): "bands": [{"name": "BASS", "levelDelta": 3, "levelDirection": "UP"}] } msg = await smart_home.async_handle_message( - hass, get_default_config(), request, context + hass, get_default_config(hass), request, context ) assert "event" in msg @@ -3427,7 +3431,7 @@ async def test_media_player_eq_bands_not_supported(hass): "bands": [{"name": "BASS", "levelDelta": 3, "levelDirection": "UP"}] } msg = await smart_home.async_handle_message( - hass, get_default_config(), request, context + hass, get_default_config(hass), request, context ) assert "event" in msg @@ -3928,7 +3932,9 @@ async def test_initialize_camera_stream(hass, mock_camera, mock_stream): "homeassistant.components.demo.camera.DemoCamera.stream_source", return_value="rtsp://example.local", ): - msg = await smart_home.async_handle_message(hass, get_default_config(), request) + msg = await smart_home.async_handle_message( + hass, get_default_config(hass), request + ) await hass.async_block_till_done() assert "event" in msg @@ -3982,7 +3988,7 @@ async def test_api_message_sets_authorized(hass): msg = get_new_request("Alexa.PowerController", "TurnOn", "switch#xy") async_mock_service(hass, "switch", "turn_on") - config = get_default_config() + config = get_default_config(hass) config._store.set_authorized.assert_not_called() await smart_home.async_handle_message(hass, config, msg) config._store.set_authorized.assert_called_once_with(True) diff --git a/tests/components/alexa/test_state_report.py b/tests/components/alexa/test_state_report.py index 06c7d051798..bd47a80c18c 100644 --- a/tests/components/alexa/test_state_report.py +++ b/tests/components/alexa/test_state_report.py @@ -21,7 +21,7 @@ async def test_report_state(hass, aioclient_mock): {"friendly_name": "Test Contact Sensor", "device_class": "door"}, ) - await state_report.async_enable_proactive_mode(hass, get_default_config()) + await state_report.async_enable_proactive_mode(hass, get_default_config(hass)) hass.states.async_set( "binary_sensor.test_contact", @@ -66,7 +66,7 @@ async def test_report_state_fail(hass, aioclient_mock, caplog): {"friendly_name": "Test Contact Sensor", "device_class": "door"}, ) - await state_report.async_enable_proactive_mode(hass, get_default_config()) + await state_report.async_enable_proactive_mode(hass, get_default_config(hass)) hass.states.async_set( "binary_sensor.test_contact", @@ -100,7 +100,7 @@ async def test_report_state_timeout(hass, aioclient_mock, caplog): {"friendly_name": "Test Contact Sensor", "device_class": "door"}, ) - await state_report.async_enable_proactive_mode(hass, get_default_config()) + await state_report.async_enable_proactive_mode(hass, get_default_config(hass)) hass.states.async_set( "binary_sensor.test_contact", @@ -134,7 +134,7 @@ async def test_report_state_retry(hass, aioclient_mock): {"friendly_name": "Test Contact Sensor", "device_class": "door"}, ) - await state_report.async_enable_proactive_mode(hass, get_default_config()) + await state_report.async_enable_proactive_mode(hass, get_default_config(hass)) hass.states.async_set( "binary_sensor.test_contact", @@ -162,7 +162,7 @@ async def test_report_state_unsets_authorized_on_error(hass, aioclient_mock): {"friendly_name": "Test Contact Sensor", "device_class": "door"}, ) - config = get_default_config() + config = get_default_config(hass) await state_report.async_enable_proactive_mode(hass, config) hass.states.async_set( @@ -191,7 +191,7 @@ async def test_report_state_unsets_authorized_on_access_token_error( {"friendly_name": "Test Contact Sensor", "device_class": "door"}, ) - config = get_default_config() + config = get_default_config(hass) await state_report.async_enable_proactive_mode(hass, config) @@ -226,7 +226,7 @@ async def test_report_state_instance(hass, aioclient_mock): }, ) - await state_report.async_enable_proactive_mode(hass, get_default_config()) + await state_report.async_enable_proactive_mode(hass, get_default_config(hass)) hass.states.async_set( "fan.test_fan", @@ -296,7 +296,7 @@ async def test_send_add_or_update_message(hass, aioclient_mock): "zwave.bla", # Unsupported ] await state_report.async_send_add_or_update_message( - hass, get_default_config(), entities + hass, get_default_config(hass), entities ) assert len(aioclient_mock.mock_calls) == 1 @@ -323,7 +323,7 @@ async def test_send_delete_message(hass, aioclient_mock): ) await state_report.async_send_delete_message( - hass, get_default_config(), ["binary_sensor.test_contact", "zwave.bla"] + hass, get_default_config(hass), ["binary_sensor.test_contact", "zwave.bla"] ) assert len(aioclient_mock.mock_calls) == 1 @@ -349,7 +349,7 @@ async def test_doorbell_event(hass, aioclient_mock): {"friendly_name": "Test Doorbell Sensor", "device_class": "occupancy"}, ) - await state_report.async_enable_proactive_mode(hass, get_default_config()) + await state_report.async_enable_proactive_mode(hass, get_default_config(hass)) hass.states.async_set( "binary_sensor.test_doorbell", @@ -407,7 +407,7 @@ async def test_doorbell_event_fail(hass, aioclient_mock, caplog): {"friendly_name": "Test Doorbell Sensor", "device_class": "occupancy"}, ) - await state_report.async_enable_proactive_mode(hass, get_default_config()) + await state_report.async_enable_proactive_mode(hass, get_default_config(hass)) hass.states.async_set( "binary_sensor.test_doorbell", @@ -441,7 +441,7 @@ async def test_doorbell_event_timeout(hass, aioclient_mock, caplog): {"friendly_name": "Test Doorbell Sensor", "device_class": "occupancy"}, ) - await state_report.async_enable_proactive_mode(hass, get_default_config()) + await state_report.async_enable_proactive_mode(hass, get_default_config(hass)) hass.states.async_set( "binary_sensor.test_doorbell", @@ -464,7 +464,7 @@ async def test_doorbell_event_timeout(hass, aioclient_mock, caplog): async def test_proactive_mode_filter_states(hass, aioclient_mock): """Test all the cases that filter states.""" aioclient_mock.post(TEST_URL, text="", status=202) - config = get_default_config() + config = get_default_config(hass) await state_report.async_enable_proactive_mode(hass, config) # First state should report diff --git a/tests/components/ambee/fixtures/air_quality.json b/tests/components/ambee/fixtures/air_quality.json index 2844e38168b..be75c832949 100644 --- a/tests/components/ambee/fixtures/air_quality.json +++ b/tests/components/ambee/fixtures/air_quality.json @@ -1,28 +1,28 @@ { - "message": "success", - "stations": [ - { - "CO": 0.105, - "NO2": 0.66, - "OZONE": 17.067, - "PM10": 5.24, - "PM25": 3.14, - "SO2": 0.031, - "city": "Hellendoorn", - "countryCode": "NL", - "division": "", - "lat": 52.3981, - "lng": 6.4493, - "placeName": "Hellendoorn", - "postalCode": "7447", - "state": "Overijssel", - "updatedAt": "2021-05-29T14:00:00.000Z", - "AQI": 13, - "aqiInfo": { - "pollutant": "PM2.5", - "concentration": 3.14, - "category": "Good" - } - } - ] + "message": "success", + "stations": [ + { + "CO": 0.105, + "NO2": 0.66, + "OZONE": 17.067, + "PM10": 5.24, + "PM25": 3.14, + "SO2": 0.031, + "city": "Hellendoorn", + "countryCode": "NL", + "division": "", + "lat": 52.3981, + "lng": 6.4493, + "placeName": "Hellendoorn", + "postalCode": "7447", + "state": "Overijssel", + "updatedAt": "2021-05-29T14:00:00.000Z", + "AQI": 13, + "aqiInfo": { + "pollutant": "PM2.5", + "concentration": 3.14, + "category": "Good" + } + } + ] } diff --git a/tests/components/ambee/fixtures/pollen.json b/tests/components/ambee/fixtures/pollen.json index 95f8a96c3c8..79d581ff3e2 100644 --- a/tests/components/ambee/fixtures/pollen.json +++ b/tests/components/ambee/fixtures/pollen.json @@ -3,41 +3,41 @@ "lat": 52.42, "lng": 6.42, "data": [ - { - "Count": { - "grass_pollen": 190, - "tree_pollen": 127, - "weed_pollen": 95 - }, - "Risk": { - "grass_pollen": "High", - "tree_pollen": "Moderate", - "weed_pollen": "High" - }, - "Species": { - "Grass": { - "Grass / Poaceae": 190 - }, - "Others": 5, - "Tree": { - "Alder": 0, - "Birch": 35, - "Cypress": 0, - "Elm": 0, - "Hazel": 0, - "Oak": 55, - "Pine": 30, - "Plane": 5, - "Poplar / Cottonwood": 0 - }, - "Weed": { - "Chenopod": 0, - "Mugwort": 1, - "Nettle": 88, - "Ragweed": 3 - } - }, - "updatedAt": "2021-06-09T16:24:27.000Z" - } + { + "Count": { + "grass_pollen": 190, + "tree_pollen": 127, + "weed_pollen": 95 + }, + "Risk": { + "grass_pollen": "High", + "tree_pollen": "Moderate", + "weed_pollen": "High" + }, + "Species": { + "Grass": { + "Grass / Poaceae": 190 + }, + "Others": 5, + "Tree": { + "Alder": 0, + "Birch": 35, + "Cypress": 0, + "Elm": 0, + "Hazel": 0, + "Oak": 55, + "Pine": 30, + "Plane": 5, + "Poplar / Cottonwood": 0 + }, + "Weed": { + "Chenopod": 0, + "Mugwort": 1, + "Nettle": 88, + "Ragweed": 3 + } + }, + "updatedAt": "2021-06-09T16:24:27.000Z" + } ] -} \ No newline at end of file +} diff --git a/tests/components/amberelectric/helpers.py b/tests/components/amberelectric/helpers.py index fbb1ebfd7ad..2bc65fdd558 100644 --- a/tests/components/amberelectric/helpers.py +++ b/tests/components/amberelectric/helpers.py @@ -6,7 +6,7 @@ from amberelectric.model.actual_interval import ActualInterval from amberelectric.model.channel import ChannelType from amberelectric.model.current_interval import CurrentInterval from amberelectric.model.forecast_interval import ForecastInterval -from amberelectric.model.interval import SpikeStatus +from amberelectric.model.interval import Descriptor, SpikeStatus from dateutil import parser @@ -26,6 +26,7 @@ def generate_actual_interval( renewables=50, channel_type=channel_type.value, spike_status=SpikeStatus.NO_SPIKE.value, + descriptor=Descriptor.LOW.value, ) @@ -45,6 +46,7 @@ def generate_current_interval( renewables=50.6, channel_type=channel_type.value, spike_status=SpikeStatus.NO_SPIKE.value, + descriptor=Descriptor.EXTREMELY_LOW.value, estimate=True, ) @@ -65,6 +67,7 @@ def generate_forecast_interval( renewables=50, channel_type=channel_type.value, spike_status=SpikeStatus.NO_SPIKE.value, + descriptor=Descriptor.VERY_LOW.value, estimate=True, ) diff --git a/tests/components/amberelectric/test_binary_sensor.py b/tests/components/amberelectric/test_binary_sensor.py index 856dcdc473e..b5d6504447e 100644 --- a/tests/components/amberelectric/test_binary_sensor.py +++ b/tests/components/amberelectric/test_binary_sensor.py @@ -112,7 +112,7 @@ async def setup_spike(hass) -> AsyncGenerator: def test_no_spike_sensor(hass: HomeAssistant, setup_no_spike) -> None: """Testing the creation of the Amber renewables sensor.""" - assert len(hass.states.async_all()) == 4 + assert len(hass.states.async_all()) == 5 sensor = hass.states.get("binary_sensor.mock_title_price_spike") assert sensor assert sensor.state == "off" @@ -122,7 +122,7 @@ def test_no_spike_sensor(hass: HomeAssistant, setup_no_spike) -> None: def test_potential_spike_sensor(hass: HomeAssistant, setup_potential_spike) -> None: """Testing the creation of the Amber renewables sensor.""" - assert len(hass.states.async_all()) == 4 + assert len(hass.states.async_all()) == 5 sensor = hass.states.get("binary_sensor.mock_title_price_spike") assert sensor assert sensor.state == "off" @@ -132,7 +132,7 @@ def test_potential_spike_sensor(hass: HomeAssistant, setup_potential_spike) -> N def test_spike_sensor(hass: HomeAssistant, setup_spike) -> None: """Testing the creation of the Amber renewables sensor.""" - assert len(hass.states.async_all()) == 4 + assert len(hass.states.async_all()) == 5 sensor = hass.states.get("binary_sensor.mock_title_price_spike") assert sensor assert sensor.state == "on" diff --git a/tests/components/amberelectric/test_coordinator.py b/tests/components/amberelectric/test_coordinator.py index bc80d3674d6..924cd5249c0 100644 --- a/tests/components/amberelectric/test_coordinator.py +++ b/tests/components/amberelectric/test_coordinator.py @@ -7,12 +7,15 @@ from unittest.mock import Mock, patch from amberelectric import ApiException from amberelectric.model.channel import Channel, ChannelType from amberelectric.model.current_interval import CurrentInterval -from amberelectric.model.interval import SpikeStatus +from amberelectric.model.interval import Descriptor, SpikeStatus from amberelectric.model.site import Site from dateutil import parser import pytest -from homeassistant.components.amberelectric.coordinator import AmberUpdateCoordinator +from homeassistant.components.amberelectric.coordinator import ( + AmberUpdateCoordinator, + normalize_descriptor, +) from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import UpdateFailed @@ -63,6 +66,18 @@ def mock_api_current_price() -> Generator: yield instance +def test_normalize_descriptor() -> None: + """Test normalizing descriptors works correctly.""" + assert normalize_descriptor(None) is None + assert normalize_descriptor(Descriptor.NEGATIVE) == "negative" + assert normalize_descriptor(Descriptor.EXTREMELY_LOW) == "extremely_low" + assert normalize_descriptor(Descriptor.VERY_LOW) == "very_low" + assert normalize_descriptor(Descriptor.LOW) == "low" + assert normalize_descriptor(Descriptor.NEUTRAL) == "neutral" + assert normalize_descriptor(Descriptor.HIGH) == "high" + assert normalize_descriptor(Descriptor.SPIKE) == "spike" + + async def test_fetch_general_site(hass: HomeAssistant, current_price_api: Mock) -> None: """Test fetching a site with only a general channel.""" diff --git a/tests/components/amberelectric/test_sensor.py b/tests/components/amberelectric/test_sensor.py index fa8cffe2c73..08103576b49 100644 --- a/tests/components/amberelectric/test_sensor.py +++ b/tests/components/amberelectric/test_sensor.py @@ -101,7 +101,7 @@ async def setup_general_and_feed_in(hass) -> AsyncGenerator: async def test_general_price_sensor(hass: HomeAssistant, setup_general: Mock) -> None: """Test the General Price sensor.""" - assert len(hass.states.async_all()) == 4 + assert len(hass.states.async_all()) == 5 price = hass.states.get("sensor.mock_title_general_price") assert price assert price.state == "0.08" @@ -140,7 +140,7 @@ async def test_general_and_controlled_load_price_sensor( hass: HomeAssistant, setup_general_and_controlled_load: Mock ) -> None: """Test the Controlled Price sensor.""" - assert len(hass.states.async_all()) == 6 + assert len(hass.states.async_all()) == 8 price = hass.states.get("sensor.mock_title_controlled_load_price") assert price assert price.state == "0.08" @@ -163,7 +163,7 @@ async def test_general_and_feed_in_price_sensor( hass: HomeAssistant, setup_general_and_feed_in: Mock ) -> None: """Test the Feed In sensor.""" - assert len(hass.states.async_all()) == 6 + assert len(hass.states.async_all()) == 8 price = hass.states.get("sensor.mock_title_feed_in_price") assert price assert price.state == "-0.08" @@ -186,7 +186,7 @@ async def test_general_forecast_sensor( hass: HomeAssistant, setup_general: Mock ) -> None: """Test the General Forecast sensor.""" - assert len(hass.states.async_all()) == 4 + assert len(hass.states.async_all()) == 5 price = hass.states.get("sensor.mock_title_general_forecast") assert price assert price.state == "0.09" @@ -204,6 +204,7 @@ async def test_general_forecast_sensor( assert first_forecast["end_time"] == "2021-09-21T09:00:00+10:00" assert first_forecast["renewables"] == 50 assert first_forecast["spike_status"] == "none" + assert first_forecast["descriptor"] == "very_low" assert first_forecast.get("range_min") is None assert first_forecast.get("range_max") is None @@ -228,7 +229,7 @@ async def test_controlled_load_forecast_sensor( hass: HomeAssistant, setup_general_and_controlled_load: Mock ) -> None: """Test the Controlled Load Forecast sensor.""" - assert len(hass.states.async_all()) == 6 + assert len(hass.states.async_all()) == 8 price = hass.states.get("sensor.mock_title_controlled_load_forecast") assert price assert price.state == "0.09" @@ -246,13 +247,14 @@ async def test_controlled_load_forecast_sensor( assert first_forecast["end_time"] == "2021-09-21T09:00:00+10:00" assert first_forecast["renewables"] == 50 assert first_forecast["spike_status"] == "none" + assert first_forecast["descriptor"] == "very_low" async def test_feed_in_forecast_sensor( hass: HomeAssistant, setup_general_and_feed_in: Mock ) -> None: """Test the Feed In Forecast sensor.""" - assert len(hass.states.async_all()) == 6 + assert len(hass.states.async_all()) == 8 price = hass.states.get("sensor.mock_title_feed_in_forecast") assert price assert price.state == "-0.09" @@ -270,11 +272,42 @@ async def test_feed_in_forecast_sensor( assert first_forecast["end_time"] == "2021-09-21T09:00:00+10:00" assert first_forecast["renewables"] == 50 assert first_forecast["spike_status"] == "none" + assert first_forecast["descriptor"] == "very_low" def test_renewable_sensor(hass: HomeAssistant, setup_general) -> None: """Testing the creation of the Amber renewables sensor.""" - assert len(hass.states.async_all()) == 4 + assert len(hass.states.async_all()) == 5 sensor = hass.states.get("sensor.mock_title_renewables") assert sensor assert sensor.state == "51" + + +def test_general_price_descriptor_descriptor_sensor( + hass: HomeAssistant, setup_general: Mock +) -> None: + """Test the General Price Descriptor sensor.""" + assert len(hass.states.async_all()) == 5 + price = hass.states.get("sensor.mock_title_general_price_descriptor") + assert price + assert price.state == "extremely_low" + + +def test_general_and_controlled_load_price_descriptor_sensor( + hass: HomeAssistant, setup_general_and_controlled_load: Mock +) -> None: + """Test the Controlled Price Descriptor sensor.""" + assert len(hass.states.async_all()) == 8 + price = hass.states.get("sensor.mock_title_controlled_load_price_descriptor") + assert price + assert price.state == "extremely_low" + + +def test_general_and_feed_in_price_descriptor_sensor( + hass: HomeAssistant, setup_general_and_feed_in: Mock +) -> None: + """Test the Feed In Price Descriptor sensor.""" + assert len(hass.states.async_all()) == 8 + price = hass.states.get("sensor.mock_title_feed_in_price_descriptor") + assert price + assert price.state == "extremely_low" diff --git a/tests/components/ambient_station/fixtures/devices.json b/tests/components/ambient_station/fixtures/devices.json index cd5edc21cb0..84379d43a94 100644 --- a/tests/components/ambient_station/fixtures/devices.json +++ b/tests/components/ambient_station/fixtures/devices.json @@ -1,15 +1,17 @@ -[{ - "macAddress": "12:34:56:78:90:AB", - "lastData": { - "dateutc": 1546889640000, - "baromrelin": 30.09, - "baromabsin": 24.61, - "tempinf": 68.9, - "humidityin": 30, - "date": "2019-01-07T19:34:00.000Z" - }, - "info": { - "name": "Home", - "location": "Home" +[ + { + "macAddress": "12:34:56:78:90:AB", + "lastData": { + "dateutc": 1546889640000, + "baromrelin": 30.09, + "baromabsin": 24.61, + "tempinf": 68.9, + "humidityin": 30, + "date": "2019-01-07T19:34:00.000Z" + }, + "info": { + "name": "Home", + "location": "Home" + } } -}] +] diff --git a/tests/components/analytics/test_analytics.py b/tests/components/analytics/test_analytics.py index 2534c4678e8..f36fbdd9d79 100644 --- a/tests/components/analytics/test_analytics.py +++ b/tests/components/analytics/test_analytics.py @@ -173,6 +173,7 @@ async def test_send_usage(hass, caplog, aioclient_mock): """Test send usage preferences are defined.""" aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200) analytics = Analytics(hass) + hass.http = Mock(ssl_certificate=None) await analytics.save_preferences({ATTR_BASE: True, ATTR_USAGE: True}) assert analytics.preferences[ATTR_BASE] @@ -184,13 +185,14 @@ async def test_send_usage(hass, caplog, aioclient_mock): assert "'integrations': ['default_config']" in caplog.text assert "'integration_count':" not in caplog.text + assert "'certificate': False" in caplog.text async def test_send_usage_with_supervisor(hass, caplog, aioclient_mock): """Test send usage with supervisor preferences are defined.""" aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200) - analytics = Analytics(hass) + hass.http = Mock(ssl_certificate=None) await analytics.save_preferences({ATTR_BASE: True, ATTR_USAGE: True}) assert analytics.preferences[ATTR_BASE] assert analytics.preferences[ATTR_USAGE] @@ -365,6 +367,7 @@ async def test_custom_integrations(hass, aioclient_mock, enable_custom_integrati """Test sending custom integrations.""" aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200) analytics = Analytics(hass) + hass.http = Mock(ssl_certificate=None) assert await async_setup_component(hass, "test_package", {"test_package": {}}) await analytics.save_preferences({ATTR_BASE: True, ATTR_USAGE: True}) @@ -430,6 +433,7 @@ async def test_send_with_no_energy(hass, aioclient_mock): """Test send base preferences are defined.""" aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200) analytics = Analytics(hass) + hass.http = Mock(ssl_certificate=None) await analytics.save_preferences({ATTR_BASE: True, ATTR_USAGE: True}) @@ -493,3 +497,20 @@ async def test_send_with_energy_config(hass, aioclient_mock): postdata = aioclient_mock.mock_calls[-1][2] assert postdata["energy"]["configured"] + + +async def test_send_usage_with_certificate(hass, caplog, aioclient_mock): + """Test send usage preferences with certificate.""" + aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200) + analytics = Analytics(hass) + hass.http = Mock(ssl_certificate="/some/path/to/cert.pem") + await analytics.save_preferences({ATTR_BASE: True, ATTR_USAGE: True}) + + assert analytics.preferences[ATTR_BASE] + assert analytics.preferences[ATTR_USAGE] + hass.config.components = ["default_config"] + + with patch("homeassistant.components.analytics.analytics.HA_VERSION", MOCK_VERSION): + await analytics.send_analytics() + + assert "'certificate': True" in caplog.text diff --git a/tests/components/androidtv/test_config_flow.py b/tests/components/androidtv/test_config_flow.py index 991d3757749..a6541e53e18 100644 --- a/tests/components/androidtv/test_config_flow.py +++ b/tests/components/androidtv/test_config_flow.py @@ -1,6 +1,5 @@ """Tests for the AndroidTV config flow.""" import json -from socket import gaierror from unittest.mock import patch import pytest @@ -33,10 +32,8 @@ from homeassistant.components.androidtv.const import ( PROP_ETHMAC, PROP_WIFIMAC, ) -from homeassistant.components.media_player import DOMAIN as MP_DOMAIN -from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER -from homeassistant.const import CONF_DEVICE_CLASS, CONF_HOST, CONF_PLATFORM, CONF_PORT -from homeassistant.setup import async_setup_component +from homeassistant.config_entries import SOURCE_USER +from homeassistant.const import CONF_DEVICE_CLASS, CONF_HOST, CONF_PORT from tests.common import MockConfigEntry from tests.components.androidtv.patchers import isfile @@ -44,6 +41,7 @@ from tests.components.androidtv.patchers import isfile ADBKEY = "adbkey" ETH_MAC = "a1:b1:c1:d1:e1:f1" WIFI_MAC = "a2:b2:c2:d2:e2:f2" +INVALID_MAC = "ff:ff:ff:ff:ff:ff" HOST = "127.0.0.1" VALID_DETECT_RULE = [{"paused": {"media_session_state": 3}}] @@ -52,7 +50,6 @@ CONFIG_PYTHON_ADB = { CONF_HOST: HOST, CONF_PORT: DEFAULT_PORT, CONF_DEVICE_CLASS: "androidtv", - CONF_ADB_SERVER_PORT: DEFAULT_ADB_SERVER_PORT, } # Android TV device with ADB server @@ -70,10 +67,6 @@ CONNECT_METHOD = ( PATCH_ACCESS = patch( "homeassistant.components.androidtv.config_flow.os.access", return_value=True ) -PATCH_GET_HOST_IP = patch( - "homeassistant.components.androidtv.config_flow.socket.gethostbyname", - return_value=HOST, -) PATCH_ISFILE = patch( "homeassistant.components.androidtv.config_flow.os.path.isfile", isfile ) @@ -119,7 +112,7 @@ async def test_user(hass, config, eth_mac, wifi_mac): with patch( CONNECT_METHOD, return_value=(MockConfigDevice(eth_mac, wifi_mac), None), - ), PATCH_SETUP_ENTRY as mock_setup_entry, PATCH_GET_HOST_IP: + ), PATCH_SETUP_ENTRY as mock_setup_entry: result = await hass.config_entries.flow.async_configure( flow_result["flow_id"], user_input=config ) @@ -132,28 +125,6 @@ async def test_user(hass, config, eth_mac, wifi_mac): assert len(mock_setup_entry.mock_calls) == 1 -async def test_import(hass): - """Test import config.""" - - # test with all provided - with patch( - CONNECT_METHOD, - return_value=(MockConfigDevice(), None), - ), PATCH_SETUP_ENTRY as mock_setup_entry, PATCH_GET_HOST_IP: - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data=CONFIG_PYTHON_ADB, - ) - await hass.async_block_till_done() - - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == HOST - assert result["data"] == CONFIG_PYTHON_ADB - - assert len(mock_setup_entry.mock_calls) == 1 - - async def test_user_adbkey(hass): """Test user step with adbkey file.""" config_data = CONFIG_PYTHON_ADB.copy() @@ -162,7 +133,7 @@ async def test_user_adbkey(hass): with patch( CONNECT_METHOD, return_value=(MockConfigDevice(), None), - ), PATCH_SETUP_ENTRY as mock_setup_entry, PATCH_GET_HOST_IP, PATCH_ISFILE, PATCH_ACCESS: + ), PATCH_SETUP_ENTRY as mock_setup_entry, PATCH_ISFILE, PATCH_ACCESS: result = await hass.config_entries.flow.async_init( DOMAIN, @@ -178,25 +149,6 @@ async def test_user_adbkey(hass): assert len(mock_setup_entry.mock_calls) == 1 -async def test_import_data(hass): - """Test import from configuration file.""" - config_data = CONFIG_PYTHON_ADB.copy() - config_data[CONF_PLATFORM] = DOMAIN - config_data[CONF_ADBKEY] = ADBKEY - config_data[CONF_TURN_OFF_COMMAND] = "off" - platform_data = {MP_DOMAIN: config_data} - - with patch( - CONNECT_METHOD, - return_value=(MockConfigDevice(), None), - ), PATCH_SETUP_ENTRY as mock_setup_entry, PATCH_GET_HOST_IP, PATCH_ISFILE, PATCH_ACCESS: - - assert await async_setup_component(hass, MP_DOMAIN, platform_data) - await hass.async_block_till_done() - - assert len(mock_setup_entry.mock_calls) == 1 - - async def test_error_both_key_server(hass): """Test we abort if both adb key and server are provided.""" config_data = CONFIG_ADB_SERVER.copy() @@ -214,7 +166,7 @@ async def test_error_both_key_server(hass): with patch( CONNECT_METHOD, return_value=(MockConfigDevice(), None), - ), PATCH_SETUP_ENTRY, PATCH_GET_HOST_IP: + ), PATCH_SETUP_ENTRY: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=CONFIG_ADB_SERVER ) @@ -241,7 +193,7 @@ async def test_error_invalid_key(hass): with patch( CONNECT_METHOD, return_value=(MockConfigDevice(), None), - ), PATCH_SETUP_ENTRY, PATCH_GET_HOST_IP: + ), PATCH_SETUP_ENTRY: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=CONFIG_ADB_SERVER ) @@ -252,45 +204,27 @@ async def test_error_invalid_key(hass): assert result2["data"] == CONFIG_ADB_SERVER -async def test_error_invalid_host(hass): - """Test we abort if host name is invalid.""" +@pytest.mark.parametrize( + ["config", "eth_mac", "wifi_mac"], + [ + (CONFIG_ADB_SERVER, None, None), + (CONFIG_PYTHON_ADB, None, None), + (CONFIG_ADB_SERVER, INVALID_MAC, None), + (CONFIG_PYTHON_ADB, INVALID_MAC, None), + (CONFIG_ADB_SERVER, None, INVALID_MAC), + (CONFIG_PYTHON_ADB, None, INVALID_MAC), + ], +) +async def test_invalid_mac(hass, config, eth_mac, wifi_mac): + """Test for invalid mac address.""" with patch( - "socket.gethostbyname", - side_effect=gaierror, + CONNECT_METHOD, + return_value=(MockConfigDevice(eth_mac, wifi_mac), None), ): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_USER, "show_advanced_options": True}, - data=CONFIG_ADB_SERVER, - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["errors"] == {"base": "invalid_host"} - - with patch( - CONNECT_METHOD, - return_value=(MockConfigDevice(), None), - ), PATCH_SETUP_ENTRY, PATCH_GET_HOST_IP: - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=CONFIG_ADB_SERVER - ) - await hass.async_block_till_done() - - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result2["title"] == HOST - assert result2["data"] == CONFIG_ADB_SERVER - - -async def test_invalid_serial(hass): - """Test for invalid serialno.""" - with patch( - CONNECT_METHOD, - return_value=(MockConfigDevice(eth_mac=None), None), - ), PATCH_GET_HOST_IP: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, - data=CONFIG_ADB_SERVER, + data=config, ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT @@ -303,31 +237,12 @@ async def test_abort_if_host_exist(hass): domain=DOMAIN, data=CONFIG_ADB_SERVER, unique_id=ETH_MAC ).add_to_hass(hass) - config_data = CONFIG_ADB_SERVER.copy() - config_data[CONF_HOST] = "name" - # Should fail, same IP Address (by PATCH_GET_HOST_IP) - with PATCH_GET_HOST_IP: - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_USER}, - data=config_data, - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "already_configured" - - -async def test_abort_import_if_host_exist(hass): - """Test we abort if component is already setup.""" - MockConfigEntry( - domain=DOMAIN, data=CONFIG_ADB_SERVER, unique_id=ETH_MAC - ).add_to_hass(hass) - - # Should fail, same Host in entry + config_data = CONFIG_PYTHON_ADB + # Should fail, same HOST result = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": SOURCE_IMPORT}, - data=CONFIG_ADB_SERVER, + context={"source": SOURCE_USER}, + data=config_data, ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT @@ -346,7 +261,7 @@ async def test_abort_if_unique_exist(hass): with patch( CONNECT_METHOD, return_value=(MockConfigDevice(), None), - ), PATCH_GET_HOST_IP: + ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, @@ -364,7 +279,7 @@ async def test_on_connect_failed(hass): context={"source": SOURCE_USER, "show_advanced_options": True}, ) - with patch(CONNECT_METHOD, return_value=(None, "Error")), PATCH_GET_HOST_IP: + with patch(CONNECT_METHOD, return_value=(None, "Error")): result = await hass.config_entries.flow.async_configure( flow_result["flow_id"], user_input=CONFIG_ADB_SERVER ) @@ -374,7 +289,7 @@ async def test_on_connect_failed(hass): with patch( CONNECT_METHOD, side_effect=TypeError, - ), PATCH_GET_HOST_IP: + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=CONFIG_ADB_SERVER ) @@ -384,7 +299,7 @@ async def test_on_connect_failed(hass): with patch( CONNECT_METHOD, return_value=(MockConfigDevice(), None), - ), PATCH_SETUP_ENTRY, PATCH_GET_HOST_IP: + ), PATCH_SETUP_ENTRY: result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], user_input=CONFIG_ADB_SERVER ) diff --git a/tests/components/androidtv/test_media_player.py b/tests/components/androidtv/test_media_player.py index a0bab1736ff..d5102a06887 100644 --- a/tests/components/androidtv/test_media_player.py +++ b/tests/components/androidtv/test_media_player.py @@ -53,6 +53,7 @@ from homeassistant.const import ( ATTR_ENTITY_ID, CONF_DEVICE_CLASS, CONF_HOST, + CONF_NAME, CONF_PORT, EVENT_HOMEASSISTANT_STOP, STATE_OFF, @@ -60,6 +61,7 @@ from homeassistant.const import ( STATE_STANDBY, STATE_UNAVAILABLE, ) +from homeassistant.helpers.entity_component import async_update_entity from homeassistant.util import slugify from tests.common import MockConfigEntry @@ -85,6 +87,14 @@ CONFIG_ANDROIDTV_PYTHON_ADB = { } } +# Android TV device with Python ADB implementation imported from YAML +CONFIG_ANDROIDTV_PYTHON_ADB_YAML = { + DOMAIN: { + CONF_NAME: "ADB yaml import", + **CONFIG_ANDROIDTV_PYTHON_ADB[DOMAIN], + } +} + # Android TV device with ADB server CONFIG_ANDROIDTV_ADB_SERVER = { DOMAIN: { @@ -126,7 +136,10 @@ def _setup(config): patch_key = "server" host = config[DOMAIN][CONF_HOST] - if config[DOMAIN].get(CONF_DEVICE_CLASS) != "firetv": + # CONF_NAME available for configuration imported from YAML + if conf_name := config[DOMAIN].get(CONF_NAME): + entity_id = slugify(conf_name) + elif config[DOMAIN].get(CONF_DEVICE_CLASS) != "firetv": entity_id = slugify(f"Android TV {host}") else: entity_id = slugify(f"Fire TV {host}") @@ -146,6 +159,7 @@ def _setup(config): "config", [ CONFIG_ANDROIDTV_PYTHON_ADB, + CONFIG_ANDROIDTV_PYTHON_ADB_YAML, CONFIG_FIRETV_PYTHON_ADB, CONFIG_ANDROIDTV_ADB_SERVER, CONFIG_FIRETV_ADB_SERVER, @@ -170,7 +184,7 @@ async def test_reconnect(hass, caplog, config): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - await hass.helpers.entity_component.async_update_entity(entity_id) + await async_update_entity(hass, entity_id) state = hass.states.get(entity_id) assert state is not None assert state.state == STATE_OFF @@ -182,7 +196,7 @@ async def test_reconnect(hass, caplog, config): patch_key ], patchers.PATCH_ANDROIDTV_OPEN, patchers.PATCH_SIGNER: for _ in range(5): - await hass.helpers.entity_component.async_update_entity(entity_id) + await async_update_entity(hass, entity_id) state = hass.states.get(entity_id) assert state is not None assert state.state == STATE_UNAVAILABLE @@ -195,7 +209,7 @@ async def test_reconnect(hass, caplog, config): with patchers.patch_connect(True)[patch_key], patchers.patch_shell( SHELL_RESPONSE_STANDBY )[patch_key], patchers.PATCH_ANDROIDTV_OPEN, patchers.PATCH_SIGNER: - await hass.helpers.entity_component.async_update_entity(entity_id) + await async_update_entity(hass, entity_id) state = hass.states.get(entity_id) assert state is not None @@ -238,7 +252,7 @@ async def test_adb_shell_returns_none(hass, config): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - await hass.helpers.entity_component.async_update_entity(entity_id) + await async_update_entity(hass, entity_id) state = hass.states.get(entity_id) assert state is not None assert state.state != STATE_UNAVAILABLE @@ -246,7 +260,7 @@ async def test_adb_shell_returns_none(hass, config): with patchers.patch_shell(None)[patch_key], patchers.patch_shell(error=True)[ patch_key ], patchers.PATCH_ANDROIDTV_OPEN, patchers.PATCH_SIGNER: - await hass.helpers.entity_component.async_update_entity(entity_id) + await async_update_entity(hass, entity_id) state = hass.states.get(entity_id) assert state is not None assert state.state == STATE_UNAVAILABLE @@ -267,7 +281,7 @@ async def test_setup_with_adbkey(hass): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - await hass.helpers.entity_component.async_update_entity(entity_id) + await async_update_entity(hass, entity_id) state = hass.states.get(entity_id) assert state is not None assert state.state == STATE_OFF @@ -301,7 +315,7 @@ async def test_sources(hass, config0): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - await hass.helpers.entity_component.async_update_entity(entity_id) + await async_update_entity(hass, entity_id) state = hass.states.get(entity_id) assert state is not None assert state.state == STATE_OFF @@ -325,7 +339,7 @@ async def test_sources(hass, config0): ) with patch_update: - await hass.helpers.entity_component.async_update_entity(entity_id) + await async_update_entity(hass, entity_id) state = hass.states.get(entity_id) assert state is not None assert state.state == STATE_PLAYING @@ -351,7 +365,7 @@ async def test_sources(hass, config0): ) with patch_update: - await hass.helpers.entity_component.async_update_entity(entity_id) + await async_update_entity(hass, entity_id) state = hass.states.get(entity_id) assert state is not None assert state.state == STATE_PLAYING @@ -380,7 +394,7 @@ async def _test_exclude_sources(hass, config0, expected_sources): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - await hass.helpers.entity_component.async_update_entity(entity_id) + await async_update_entity(hass, entity_id) state = hass.states.get(entity_id) assert state is not None assert state.state == STATE_OFF @@ -416,7 +430,7 @@ async def _test_exclude_sources(hass, config0, expected_sources): ) with patch_update: - await hass.helpers.entity_component.async_update_entity(entity_id) + await async_update_entity(hass, entity_id) state = hass.states.get(entity_id) assert state is not None assert state.state == STATE_PLAYING @@ -461,7 +475,7 @@ async def _test_select_source(hass, config0, source, expected_arg, method_patch) assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - await hass.helpers.entity_component.async_update_entity(entity_id) + await async_update_entity(hass, entity_id) state = hass.states.get(entity_id) assert state is not None assert state.state == STATE_OFF @@ -688,7 +702,7 @@ async def test_setup_fail(hass, config): assert await hass.config_entries.async_setup(config_entry.entry_id) is False await hass.async_block_till_done() - await hass.helpers.entity_component.async_update_entity(entity_id) + await async_update_entity(hass, entity_id) state = hass.states.get(entity_id) assert state is None @@ -851,7 +865,7 @@ async def test_update_lock_not_acquired(hass): await hass.async_block_till_done() with patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: - await hass.helpers.entity_component.async_update_entity(entity_id) + await async_update_entity(hass, entity_id) state = hass.states.get(entity_id) assert state is not None assert state.state == STATE_OFF @@ -860,13 +874,13 @@ async def test_update_lock_not_acquired(hass): "androidtv.androidtv.androidtv_async.AndroidTVAsync.update", side_effect=LockNotAcquiredException, ), patchers.patch_shell(SHELL_RESPONSE_STANDBY)[patch_key]: - await hass.helpers.entity_component.async_update_entity(entity_id) + await async_update_entity(hass, entity_id) state = hass.states.get(entity_id) assert state is not None assert state.state == STATE_OFF with patchers.patch_shell(SHELL_RESPONSE_STANDBY)[patch_key]: - await hass.helpers.entity_component.async_update_entity(entity_id) + await async_update_entity(hass, entity_id) state = hass.states.get(entity_id) assert state is not None assert state.state == STATE_STANDBY @@ -1003,7 +1017,7 @@ async def test_get_image(hass, hass_ws_client): await hass.async_block_till_done() with patchers.patch_shell("11")[patch_key]: - await hass.helpers.entity_component.async_update_entity(entity_id) + await async_update_entity(hass, entity_id) client = await hass_ws_client(hass) @@ -1208,20 +1222,20 @@ async def test_exception(hass): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - await hass.helpers.entity_component.async_update_entity(entity_id) + await async_update_entity(hass, entity_id) state = hass.states.get(entity_id) assert state is not None assert state.state == STATE_OFF # When an unforeseen exception occurs, we close the ADB connection and raise the exception with patchers.PATCH_ANDROIDTV_UPDATE_EXCEPTION, pytest.raises(Exception): - await hass.helpers.entity_component.async_update_entity(entity_id) + await async_update_entity(hass, entity_id) state = hass.states.get(entity_id) assert state is not None assert state.state == STATE_UNAVAILABLE # On the next update, HA will reconnect to the device - await hass.helpers.entity_component.async_update_entity(entity_id) + await async_update_entity(hass, entity_id) state = hass.states.get(entity_id) assert state is not None assert state.state == STATE_OFF diff --git a/tests/components/august/fixtures/get_activity.bridge_offline.json b/tests/components/august/fixtures/get_activity.bridge_offline.json index ed4aaadaf73..9c2ded96665 100644 --- a/tests/components/august/fixtures/get_activity.bridge_offline.json +++ b/tests/components/august/fixtures/get_activity.bridge_offline.json @@ -1,34 +1,36 @@ -[{ - "entities" : { - "activity" : "mockActivity2", - "house" : "123", - "device" : "online_with_doorsense", - "callingUser" : "mockUserId2", - "otherUser" : "deleted" - }, - "callingUser" : { - "LastName" : "elven princess", - "UserID" : "mockUserId2", - "FirstName" : "Your favorite" - }, - "otherUser" : { - "LastName" : "User", - "UserName" : "deleteduser", - "FirstName" : "Unknown", - "UserID" : "deleted", - "PhoneNo" : "deleted" - }, - "deviceType" : "lock", - "deviceName" : "MockHouseTDoor", - "action" : "associated_bridge_offline", - "dateTime" : 1582007218000, - "info" : { - "remote" : true, - "DateLogActionID" : "ABC+Time" - }, - "deviceID" : "online_with_doorsense", - "house" : { - "houseName" : "MockHouse", - "houseID" : "123" - } -}] +[ + { + "entities": { + "activity": "mockActivity2", + "house": "123", + "device": "online_with_doorsense", + "callingUser": "mockUserId2", + "otherUser": "deleted" + }, + "callingUser": { + "LastName": "elven princess", + "UserID": "mockUserId2", + "FirstName": "Your favorite" + }, + "otherUser": { + "LastName": "User", + "UserName": "deleteduser", + "FirstName": "Unknown", + "UserID": "deleted", + "PhoneNo": "deleted" + }, + "deviceType": "lock", + "deviceName": "MockHouseTDoor", + "action": "associated_bridge_offline", + "dateTime": 1582007218000, + "info": { + "remote": true, + "DateLogActionID": "ABC+Time" + }, + "deviceID": "online_with_doorsense", + "house": { + "houseName": "MockHouse", + "houseID": "123" + } + } +] diff --git a/tests/components/august/fixtures/get_activity.bridge_online.json b/tests/components/august/fixtures/get_activity.bridge_online.json index db14f06cfe9..6f8b5e6a4a6 100644 --- a/tests/components/august/fixtures/get_activity.bridge_online.json +++ b/tests/components/august/fixtures/get_activity.bridge_online.json @@ -1,34 +1,36 @@ -[{ - "entities" : { - "activity" : "mockActivity2", - "house" : "123", - "device" : "online_with_doorsense", - "callingUser" : "mockUserId2", - "otherUser" : "deleted" - }, - "callingUser" : { - "LastName" : "elven princess", - "UserID" : "mockUserId2", - "FirstName" : "Your favorite" - }, - "otherUser" : { - "LastName" : "User", - "UserName" : "deleteduser", - "FirstName" : "Unknown", - "UserID" : "deleted", - "PhoneNo" : "deleted" - }, - "deviceType" : "lock", - "deviceName" : "MockHouseTDoor", - "action" : "associated_bridge_online", - "dateTime" : 1582007218000, - "info" : { - "remote" : true, - "DateLogActionID" : "ABC+Time" - }, - "deviceID" : "online_with_doorsense", - "house" : { - "houseName" : "MockHouse", - "houseID" : "123" - } -}] +[ + { + "entities": { + "activity": "mockActivity2", + "house": "123", + "device": "online_with_doorsense", + "callingUser": "mockUserId2", + "otherUser": "deleted" + }, + "callingUser": { + "LastName": "elven princess", + "UserID": "mockUserId2", + "FirstName": "Your favorite" + }, + "otherUser": { + "LastName": "User", + "UserName": "deleteduser", + "FirstName": "Unknown", + "UserID": "deleted", + "PhoneNo": "deleted" + }, + "deviceType": "lock", + "deviceName": "MockHouseTDoor", + "action": "associated_bridge_online", + "dateTime": 1582007218000, + "info": { + "remote": true, + "DateLogActionID": "ABC+Time" + }, + "deviceID": "online_with_doorsense", + "house": { + "houseName": "MockHouse", + "houseID": "123" + } + } +] diff --git a/tests/components/august/fixtures/get_activity.doorbell_motion.json b/tests/components/august/fixtures/get_activity.doorbell_motion.json index bd9c07afa26..cf0f231a49a 100644 --- a/tests/components/august/fixtures/get_activity.doorbell_motion.json +++ b/tests/components/august/fixtures/get_activity.doorbell_motion.json @@ -1,58 +1,58 @@ [ - { - "otherUser" : { - "FirstName" : "Unknown", - "UserName" : "deleteduser", - "LastName" : "User", - "UserID" : "deleted", - "PhoneNo" : "deleted" + { + "otherUser": { + "FirstName": "Unknown", + "UserName": "deleteduser", + "LastName": "User", + "UserID": "deleted", + "PhoneNo": "deleted" + }, + "dateTime": 1582663119959, + "deviceID": "K98GiDT45GUL", + "info": { + "videoUploadProgress": "in_progress", + "image": { + "resource_type": "image", + "etag": "fdsf", + "created_at": "2020-02-25T20:38:39Z", + "type": "upload", + "format": "jpg", + "version": 1582663119, + "secure_url": "https://res.cloudinary.com/updated_image.jpg", + "signature": "fdfdfd", + "url": "http://res.cloudinary.com/updated_image.jpg", + "bytes": 48545, + "placeholder": false, + "original_filename": "file", + "width": 720, + "tags": [], + "public_id": "xnsj5gphpzij9brifpf4", + "height": 576 }, - "dateTime" : 1582663119959, - "deviceID" : "K98GiDT45GUL", - "info" : { - "videoUploadProgress" : "in_progress", - "image" : { - "resource_type" : "image", - "etag" : "fdsf", - "created_at" : "2020-02-25T20:38:39Z", - "type" : "upload", - "format" : "jpg", - "version" : 1582663119, - "secure_url" : "https://res.cloudinary.com/updated_image.jpg", - "signature" : "fdfdfd", - "url" : "http://res.cloudinary.com/updated_image.jpg", - "bytes" : 48545, - "placeholder" : false, - "original_filename" : "file", - "width" : 720, - "tags" : [], - "public_id" : "xnsj5gphpzij9brifpf4", - "height" : 576 - }, - "dvrID" : "dvr", - "videoAvailable" : false, - "hasSubscription" : false - }, - "callingUser" : { - "LastName" : "User", - "UserName" : "deleteduser", - "FirstName" : "Unknown", - "UserID" : "deleted", - "PhoneNo" : "deleted" - }, - "house" : { - "houseName" : "K98GiDT45GUL", - "houseID" : "na" - }, - "action" : "doorbell_motion_detected", - "deviceType" : "doorbell", - "entities" : { - "otherUser" : "deleted", - "house" : "na", - "device" : "K98GiDT45GUL", - "activity" : "de5585cfd4eae900bb5ba3dc", - "callingUser" : "deleted" - }, - "deviceName" : "Front Door" - } + "dvrID": "dvr", + "videoAvailable": false, + "hasSubscription": false + }, + "callingUser": { + "LastName": "User", + "UserName": "deleteduser", + "FirstName": "Unknown", + "UserID": "deleted", + "PhoneNo": "deleted" + }, + "house": { + "houseName": "K98GiDT45GUL", + "houseID": "na" + }, + "action": "doorbell_motion_detected", + "deviceType": "doorbell", + "entities": { + "otherUser": "deleted", + "house": "na", + "device": "K98GiDT45GUL", + "activity": "de5585cfd4eae900bb5ba3dc", + "callingUser": "deleted" + }, + "deviceName": "Front Door" + } ] diff --git a/tests/components/august/fixtures/get_activity.jammed.json b/tests/components/august/fixtures/get_activity.jammed.json index be5b9dfa4eb..782a13f9c73 100644 --- a/tests/components/august/fixtures/get_activity.jammed.json +++ b/tests/components/august/fixtures/get_activity.jammed.json @@ -1,34 +1,36 @@ -[{ - "entities" : { - "activity" : "mockActivity2", - "house" : "123", - "device" : "online_with_doorsense", - "callingUser" : "mockUserId2", - "otherUser" : "deleted" - }, - "callingUser" : { - "LastName" : "elven princess", - "UserID" : "mockUserId2", - "FirstName" : "Your favorite" - }, - "otherUser" : { - "LastName" : "User", - "UserName" : "deleteduser", - "FirstName" : "Unknown", - "UserID" : "deleted", - "PhoneNo" : "deleted" - }, - "deviceType" : "lock", - "deviceName" : "MockHouseTDoor", - "action" : "jammed", - "dateTime" : 1582007218000, - "info" : { - "remote" : true, - "DateLogActionID" : "ABC+Time" - }, - "deviceID" : "online_with_doorsense", - "house" : { - "houseName" : "MockHouse", - "houseID" : "123" - } -}] +[ + { + "entities": { + "activity": "mockActivity2", + "house": "123", + "device": "online_with_doorsense", + "callingUser": "mockUserId2", + "otherUser": "deleted" + }, + "callingUser": { + "LastName": "elven princess", + "UserID": "mockUserId2", + "FirstName": "Your favorite" + }, + "otherUser": { + "LastName": "User", + "UserName": "deleteduser", + "FirstName": "Unknown", + "UserID": "deleted", + "PhoneNo": "deleted" + }, + "deviceType": "lock", + "deviceName": "MockHouseTDoor", + "action": "jammed", + "dateTime": 1582007218000, + "info": { + "remote": true, + "DateLogActionID": "ABC+Time" + }, + "deviceID": "online_with_doorsense", + "house": { + "houseName": "MockHouse", + "houseID": "123" + } + } +] diff --git a/tests/components/august/fixtures/get_activity.lock.json b/tests/components/august/fixtures/get_activity.lock.json index e0e61cb36b3..b40e7d61ccf 100644 --- a/tests/components/august/fixtures/get_activity.lock.json +++ b/tests/components/august/fixtures/get_activity.lock.json @@ -1,34 +1,36 @@ -[{ - "entities" : { - "activity" : "mockActivity2", - "house" : "123", - "device" : "online_with_doorsense", - "callingUser" : "mockUserId2", - "otherUser" : "deleted" - }, - "callingUser" : { - "LastName" : "elven princess", - "UserID" : "mockUserId2", - "FirstName" : "Your favorite" - }, - "otherUser" : { - "LastName" : "User", - "UserName" : "deleteduser", - "FirstName" : "Unknown", - "UserID" : "deleted", - "PhoneNo" : "deleted" - }, - "deviceType" : "lock", - "deviceName" : "MockHouseTDoor", - "action" : "lock", - "dateTime" : 1582007218000, - "info" : { - "remote" : true, - "DateLogActionID" : "ABC+Time" - }, - "deviceID" : "online_with_doorsense", - "house" : { - "houseName" : "MockHouse", - "houseID" : "123" - } -}] +[ + { + "entities": { + "activity": "mockActivity2", + "house": "123", + "device": "online_with_doorsense", + "callingUser": "mockUserId2", + "otherUser": "deleted" + }, + "callingUser": { + "LastName": "elven princess", + "UserID": "mockUserId2", + "FirstName": "Your favorite" + }, + "otherUser": { + "LastName": "User", + "UserName": "deleteduser", + "FirstName": "Unknown", + "UserID": "deleted", + "PhoneNo": "deleted" + }, + "deviceType": "lock", + "deviceName": "MockHouseTDoor", + "action": "lock", + "dateTime": 1582007218000, + "info": { + "remote": true, + "DateLogActionID": "ABC+Time" + }, + "deviceID": "online_with_doorsense", + "house": { + "houseName": "MockHouse", + "houseID": "123" + } + } +] diff --git a/tests/components/august/fixtures/get_activity.lock_from_autorelock.json b/tests/components/august/fixtures/get_activity.lock_from_autorelock.json index 1c5d19344dc..38c26ffb7dd 100644 --- a/tests/components/august/fixtures/get_activity.lock_from_autorelock.json +++ b/tests/components/august/fixtures/get_activity.lock_from_autorelock.json @@ -1,34 +1,36 @@ -[{ - "entities" : { - "activity" : "mockActivity2", - "house" : "123", - "device" : "online_with_doorsense", - "callingUser" : "mockUserId2", - "otherUser" : "deleted" - }, - "callingUser" : { - "LastName" : "Relock", - "UserID" : "automaticrelock", - "FirstName" : "Auto" - }, - "otherUser" : { - "LastName" : "User", - "UserName" : "deleteduser", - "FirstName" : "Unknown", - "UserID" : "deleted", - "PhoneNo" : "deleted" - }, - "deviceType" : "lock", - "deviceName" : "MockHouseTDoor", - "action" : "lock", - "dateTime" : 1582007218000, - "info" : { - "remote" : false, - "DateLogActionID" : "ABC+Time" - }, - "deviceID" : "online_with_doorsense", - "house" : { - "houseName" : "MockHouse", - "houseID" : "123" - } -}] +[ + { + "entities": { + "activity": "mockActivity2", + "house": "123", + "device": "online_with_doorsense", + "callingUser": "mockUserId2", + "otherUser": "deleted" + }, + "callingUser": { + "LastName": "Relock", + "UserID": "automaticrelock", + "FirstName": "Auto" + }, + "otherUser": { + "LastName": "User", + "UserName": "deleteduser", + "FirstName": "Unknown", + "UserID": "deleted", + "PhoneNo": "deleted" + }, + "deviceType": "lock", + "deviceName": "MockHouseTDoor", + "action": "lock", + "dateTime": 1582007218000, + "info": { + "remote": false, + "DateLogActionID": "ABC+Time" + }, + "deviceID": "online_with_doorsense", + "house": { + "houseName": "MockHouse", + "houseID": "123" + } + } +] diff --git a/tests/components/august/fixtures/get_activity.lock_from_bluetooth.json b/tests/components/august/fixtures/get_activity.lock_from_bluetooth.json index f48d8da1319..bfbc621e064 100644 --- a/tests/components/august/fixtures/get_activity.lock_from_bluetooth.json +++ b/tests/components/august/fixtures/get_activity.lock_from_bluetooth.json @@ -1,34 +1,36 @@ -[{ - "entities" : { - "activity" : "mockActivity2", - "house" : "123", - "device" : "online_with_doorsense", - "callingUser" : "mockUserId2", - "otherUser" : "deleted" - }, - "callingUser" : { - "LastName" : "elven princess", - "UserID" : "mockUserId2", - "FirstName" : "Your favorite" - }, - "otherUser" : { - "LastName" : "User", - "UserName" : "deleteduser", - "FirstName" : "Unknown", - "UserID" : "deleted", - "PhoneNo" : "deleted" - }, - "deviceType" : "lock", - "deviceName" : "MockHouseTDoor", - "action" : "lock", - "dateTime" : 1582007218000, - "info" : { - "remote" : false, - "DateLogActionID" : "ABC+Time" - }, - "deviceID" : "online_with_doorsense", - "house" : { - "houseName" : "MockHouse", - "houseID" : "123" - } -}] +[ + { + "entities": { + "activity": "mockActivity2", + "house": "123", + "device": "online_with_doorsense", + "callingUser": "mockUserId2", + "otherUser": "deleted" + }, + "callingUser": { + "LastName": "elven princess", + "UserID": "mockUserId2", + "FirstName": "Your favorite" + }, + "otherUser": { + "LastName": "User", + "UserName": "deleteduser", + "FirstName": "Unknown", + "UserID": "deleted", + "PhoneNo": "deleted" + }, + "deviceType": "lock", + "deviceName": "MockHouseTDoor", + "action": "lock", + "dateTime": 1582007218000, + "info": { + "remote": false, + "DateLogActionID": "ABC+Time" + }, + "deviceID": "online_with_doorsense", + "house": { + "houseName": "MockHouse", + "houseID": "123" + } + } +] diff --git a/tests/components/august/fixtures/get_activity.lock_from_keypad.json b/tests/components/august/fixtures/get_activity.lock_from_keypad.json index 4c76fc46cd8..1b1e13e67dd 100644 --- a/tests/components/august/fixtures/get_activity.lock_from_keypad.json +++ b/tests/components/august/fixtures/get_activity.lock_from_keypad.json @@ -1,35 +1,37 @@ -[{ - "entities" : { - "activity" : "mockActivity2", - "house" : "123", - "device" : "online_with_doorsense", - "callingUser" : "mockUserId2", - "otherUser" : "deleted" - }, - "callingUser" : { - "LastName" : "elven princess", - "UserID" : "mockUserId2", - "FirstName" : "Your favorite" - }, - "otherUser" : { - "LastName" : "User", - "UserName" : "deleteduser", - "FirstName" : "Unknown", - "UserID" : "deleted", - "PhoneNo" : "deleted" - }, - "deviceType" : "lock", - "deviceName" : "MockHouseTDoor", - "action" : "lock", - "dateTime" : 1582007218000, - "info" : { - "remote" : false, - "keypad" : true, - "DateLogActionID" : "ABC+Time" - }, - "deviceID" : "online_with_doorsense", - "house" : { - "houseName" : "MockHouse", - "houseID" : "123" - } -}] +[ + { + "entities": { + "activity": "mockActivity2", + "house": "123", + "device": "online_with_doorsense", + "callingUser": "mockUserId2", + "otherUser": "deleted" + }, + "callingUser": { + "LastName": "elven princess", + "UserID": "mockUserId2", + "FirstName": "Your favorite" + }, + "otherUser": { + "LastName": "User", + "UserName": "deleteduser", + "FirstName": "Unknown", + "UserID": "deleted", + "PhoneNo": "deleted" + }, + "deviceType": "lock", + "deviceName": "MockHouseTDoor", + "action": "lock", + "dateTime": 1582007218000, + "info": { + "remote": false, + "keypad": true, + "DateLogActionID": "ABC+Time" + }, + "deviceID": "online_with_doorsense", + "house": { + "houseName": "MockHouse", + "houseID": "123" + } + } +] diff --git a/tests/components/august/fixtures/get_activity.locking.json b/tests/components/august/fixtures/get_activity.locking.json index c1f07e47312..ad2df6f7e91 100644 --- a/tests/components/august/fixtures/get_activity.locking.json +++ b/tests/components/august/fixtures/get_activity.locking.json @@ -1,34 +1,36 @@ -[{ - "entities" : { - "activity" : "mockActivity2", - "house" : "123", - "device" : "online_with_doorsense", - "callingUser" : "mockUserId2", - "otherUser" : "deleted" - }, - "callingUser" : { - "LastName" : "elven princess", - "UserID" : "mockUserId2", - "FirstName" : "Your favorite" - }, - "otherUser" : { - "LastName" : "User", - "UserName" : "deleteduser", - "FirstName" : "Unknown", - "UserID" : "deleted", - "PhoneNo" : "deleted" - }, - "deviceType" : "lock", - "deviceName" : "MockHouseTDoor", - "action" : "locking", - "dateTime" : 1582007218000, - "info" : { - "remote" : true, - "DateLogActionID" : "ABC+Time" - }, - "deviceID" : "online_with_doorsense", - "house" : { - "houseName" : "MockHouse", - "houseID" : "123" - } -}] +[ + { + "entities": { + "activity": "mockActivity2", + "house": "123", + "device": "online_with_doorsense", + "callingUser": "mockUserId2", + "otherUser": "deleted" + }, + "callingUser": { + "LastName": "elven princess", + "UserID": "mockUserId2", + "FirstName": "Your favorite" + }, + "otherUser": { + "LastName": "User", + "UserName": "deleteduser", + "FirstName": "Unknown", + "UserID": "deleted", + "PhoneNo": "deleted" + }, + "deviceType": "lock", + "deviceName": "MockHouseTDoor", + "action": "locking", + "dateTime": 1582007218000, + "info": { + "remote": true, + "DateLogActionID": "ABC+Time" + }, + "deviceID": "online_with_doorsense", + "house": { + "houseName": "MockHouse", + "houseID": "123" + } + } +] diff --git a/tests/components/august/fixtures/get_activity.unlocking.json b/tests/components/august/fixtures/get_activity.unlocking.json index 788a69164aa..0fbd0be3eb8 100644 --- a/tests/components/august/fixtures/get_activity.unlocking.json +++ b/tests/components/august/fixtures/get_activity.unlocking.json @@ -1,34 +1,36 @@ -[{ - "entities" : { - "activity" : "mockActivity2", - "house" : "123", - "device" : "online_with_doorsense", - "callingUser" : "mockUserId2", - "otherUser" : "deleted" - }, - "callingUser" : { - "LastName" : "elven princess", - "UserID" : "mockUserId2", - "FirstName" : "Your favorite" - }, - "otherUser" : { - "LastName" : "User", - "UserName" : "deleteduser", - "FirstName" : "Unknown", - "UserID" : "deleted", - "PhoneNo" : "deleted" - }, - "deviceType" : "lock", - "deviceName" : "MockHouseTDoor", - "action" : "unlocking", - "dateTime" : 1582007218000, - "info" : { - "remote" : true, - "DateLogActionID" : "ABC+Time" - }, - "deviceID" : "online_with_doorsense", - "house" : { - "houseName" : "MockHouse", - "houseID" : "123" - } -}] +[ + { + "entities": { + "activity": "mockActivity2", + "house": "123", + "device": "online_with_doorsense", + "callingUser": "mockUserId2", + "otherUser": "deleted" + }, + "callingUser": { + "LastName": "elven princess", + "UserID": "mockUserId2", + "FirstName": "Your favorite" + }, + "otherUser": { + "LastName": "User", + "UserName": "deleteduser", + "FirstName": "Unknown", + "UserID": "deleted", + "PhoneNo": "deleted" + }, + "deviceType": "lock", + "deviceName": "MockHouseTDoor", + "action": "unlocking", + "dateTime": 1582007218000, + "info": { + "remote": true, + "DateLogActionID": "ABC+Time" + }, + "deviceID": "online_with_doorsense", + "house": { + "houseName": "MockHouse", + "houseID": "123" + } + } +] diff --git a/tests/components/august/fixtures/get_doorbell.json b/tests/components/august/fixtures/get_doorbell.json index fb2cd5780c9..32714211618 100644 --- a/tests/components/august/fixtures/get_doorbell.json +++ b/tests/components/august/fixtures/get_doorbell.json @@ -1,83 +1,81 @@ { - "status_timestamp" : 1512811834532, - "appID" : "august-iphone", - "LockID" : "BBBB1F5F11114C24CCCC97571DD6AAAA", - "recentImage" : { - "original_filename" : "file", - "placeholder" : false, - "bytes" : 24476, - "height" : 640, - "format" : "jpg", - "width" : 480, - "version" : 1512892814, - "resource_type" : "image", - "etag" : "54966926be2e93f77d498a55f247661f", - "tags" : [], - "public_id" : "qqqqt4ctmxwsysylaaaa", - "url" : "http://image.com/vmk16naaaa7ibuey7sar.jpg", - "created_at" : "2017-12-10T08:01:35Z", - "signature" : "75z47ca21b5e8ffda21d2134e478a2307c4625da", - "secure_url" : "https://image.com/vmk16naaaa7ibuey7sar.jpg", - "type" : "upload" - }, - "settings" : { - "keepEncoderRunning" : true, - "videoResolution" : "640x480", - "minACNoScaling" : 40, - "irConfiguration" : 8448272, - "directLink" : true, - "overlayEnabled" : true, - "notify_when_offline" : true, - "micVolume" : 100, - "bitrateCeiling" : 512000, - "initialBitrate" : 384000, - "IVAEnabled" : false, - "turnOffCamera" : false, - "ringSoundEnabled" : true, - "JPGQuality" : 70, - "motion_notifications" : true, - "speakerVolume" : 92, - "buttonpush_notifications" : true, - "ABREnabled" : true, - "debug" : false, - "batteryLowThreshold" : 3.1, - "batteryRun" : false, - "IREnabled" : true, - "batteryUseThreshold" : 3.4 - }, - "doorbellServerURL" : "https://doorbells.august.com", - "name" : "Front Door", - "createdAt" : "2016-11-26T22:27:11.176Z", - "installDate" : "2016-11-26T22:27:11.176Z", - "serialNumber" : "tBXZR0Z35E", - "dvrSubscriptionSetupDone" : true, - "caps" : [ - "reconnect" - ], - "doorbellID" : "K98GiDT45GUL", - "HouseID" : "mockhouseid1", - "telemetry" : { - "signal_level" : -56, - "date" : "2017-12-10 08:05:12", - "battery_soc" : 96, - "battery" : 4.061763, - "steady_ac_in" : 22.196405, - "BSSID" : "88:ee:00:dd:aa:11", - "SSID" : "foo_ssid", - "updated_at" : "2017-12-10T08:05:13.650Z", - "temperature" : 28.25, - "wifi_freq" : 5745, - "load_average" : "0.50 0.47 0.35 1/154 9345", - "link_quality" : 54, - "battery_soh" : 95, - "uptime" : "16168.75 13830.49", - "ip_addr" : "10.0.1.11", - "doorbell_low_battery" : false, - "ac_in" : 23.856874 - }, - "installUserID" : "c3b2a94e-373e-aaaa-bbbb-36e996827777", - "status" : "doorbell_call_status_online", - "firmwareVersion" : "2.3.0-RC153+201711151527", - "pubsubChannel" : "7c7a6672-59c8-3333-ffff-dcd98705cccc", - "updatedAt" : "2017-12-10T08:05:13.650Z" + "status_timestamp": 1512811834532, + "appID": "august-iphone", + "LockID": "BBBB1F5F11114C24CCCC97571DD6AAAA", + "recentImage": { + "original_filename": "file", + "placeholder": false, + "bytes": 24476, + "height": 640, + "format": "jpg", + "width": 480, + "version": 1512892814, + "resource_type": "image", + "etag": "54966926be2e93f77d498a55f247661f", + "tags": [], + "public_id": "qqqqt4ctmxwsysylaaaa", + "url": "http://image.com/vmk16naaaa7ibuey7sar.jpg", + "created_at": "2017-12-10T08:01:35Z", + "signature": "75z47ca21b5e8ffda21d2134e478a2307c4625da", + "secure_url": "https://image.com/vmk16naaaa7ibuey7sar.jpg", + "type": "upload" + }, + "settings": { + "keepEncoderRunning": true, + "videoResolution": "640x480", + "minACNoScaling": 40, + "irConfiguration": 8448272, + "directLink": true, + "overlayEnabled": true, + "notify_when_offline": true, + "micVolume": 100, + "bitrateCeiling": 512000, + "initialBitrate": 384000, + "IVAEnabled": false, + "turnOffCamera": false, + "ringSoundEnabled": true, + "JPGQuality": 70, + "motion_notifications": true, + "speakerVolume": 92, + "buttonpush_notifications": true, + "ABREnabled": true, + "debug": false, + "batteryLowThreshold": 3.1, + "batteryRun": false, + "IREnabled": true, + "batteryUseThreshold": 3.4 + }, + "doorbellServerURL": "https://doorbells.august.com", + "name": "Front Door", + "createdAt": "2016-11-26T22:27:11.176Z", + "installDate": "2016-11-26T22:27:11.176Z", + "serialNumber": "tBXZR0Z35E", + "dvrSubscriptionSetupDone": true, + "caps": ["reconnect"], + "doorbellID": "K98GiDT45GUL", + "HouseID": "mockhouseid1", + "telemetry": { + "signal_level": -56, + "date": "2017-12-10 08:05:12", + "battery_soc": 96, + "battery": 4.061763, + "steady_ac_in": 22.196405, + "BSSID": "88:ee:00:dd:aa:11", + "SSID": "foo_ssid", + "updated_at": "2017-12-10T08:05:13.650Z", + "temperature": 28.25, + "wifi_freq": 5745, + "load_average": "0.50 0.47 0.35 1/154 9345", + "link_quality": 54, + "battery_soh": 95, + "uptime": "16168.75 13830.49", + "ip_addr": "10.0.1.11", + "doorbell_low_battery": false, + "ac_in": 23.856874 + }, + "installUserID": "c3b2a94e-373e-aaaa-bbbb-36e996827777", + "status": "doorbell_call_status_online", + "firmwareVersion": "2.3.0-RC153+201711151527", + "pubsubChannel": "7c7a6672-59c8-3333-ffff-dcd98705cccc", + "updatedAt": "2017-12-10T08:05:13.650Z" } diff --git a/tests/components/august/fixtures/get_doorbell.nobattery.json b/tests/components/august/fixtures/get_doorbell.nobattery.json index e2a93a086cc..2a7f1e2d3b2 100644 --- a/tests/components/august/fixtures/get_doorbell.nobattery.json +++ b/tests/components/august/fixtures/get_doorbell.nobattery.json @@ -1,80 +1,78 @@ { - "status_timestamp" : 1512811834532, - "appID" : "august-iphone", - "LockID" : "BBBB1F5F11114C24CCCC97571DD6AAAA", - "recentImage" : { - "original_filename" : "file", - "placeholder" : false, - "bytes" : 24476, - "height" : 640, - "format" : "jpg", - "width" : 480, - "version" : 1512892814, - "resource_type" : "image", - "etag" : "54966926be2e93f77d498a55f247661f", - "tags" : [], - "public_id" : "qqqqt4ctmxwsysylaaaa", - "url" : "http://image.com/vmk16naaaa7ibuey7sar.jpg", - "created_at" : "2017-12-10T08:01:35Z", - "signature" : "75z47ca21b5e8ffda21d2134e478a2307c4625da", - "secure_url" : "https://image.com/vmk16naaaa7ibuey7sar.jpg", - "type" : "upload" - }, - "settings" : { - "keepEncoderRunning" : true, - "videoResolution" : "640x480", - "minACNoScaling" : 40, - "irConfiguration" : 8448272, - "directLink" : true, - "overlayEnabled" : true, - "notify_when_offline" : true, - "micVolume" : 100, - "bitrateCeiling" : 512000, - "initialBitrate" : 384000, - "IVAEnabled" : false, - "turnOffCamera" : false, - "ringSoundEnabled" : true, - "JPGQuality" : 70, - "motion_notifications" : true, - "speakerVolume" : 92, - "buttonpush_notifications" : true, - "ABREnabled" : true, - "debug" : false, - "batteryLowThreshold" : 3.1, - "batteryRun" : false, - "IREnabled" : true, - "batteryUseThreshold" : 3.4 - }, - "doorbellServerURL" : "https://doorbells.august.com", - "name" : "Front Door", - "createdAt" : "2016-11-26T22:27:11.176Z", - "installDate" : "2016-11-26T22:27:11.176Z", - "serialNumber" : "tBXZR0Z35E", - "dvrSubscriptionSetupDone" : true, - "caps" : [ - "reconnect" - ], - "doorbellID" : "K98GiDT45GUL", - "HouseID" : "3dd2accaea08", - "telemetry" : { - "signal_level" : -56, - "date" : "2017-12-10 08:05:12", - "steady_ac_in" : 22.196405, - "BSSID" : "88:ee:00:dd:aa:11", - "SSID" : "foo_ssid", - "updated_at" : "2017-12-10T08:05:13.650Z", - "temperature" : 28.25, - "wifi_freq" : 5745, - "load_average" : "0.50 0.47 0.35 1/154 9345", - "link_quality" : 54, - "uptime" : "16168.75 13830.49", - "ip_addr" : "10.0.1.11", - "doorbell_low_battery" : false, - "ac_in" : 23.856874 - }, - "installUserID" : "c3b2a94e-373e-aaaa-bbbb-36e996827777", - "status" : "doorbell_call_status_online", - "firmwareVersion" : "2.3.0-RC153+201711151527", - "pubsubChannel" : "7c7a6672-59c8-3333-ffff-dcd98705cccc", - "updatedAt" : "2017-12-10T08:05:13.650Z" + "status_timestamp": 1512811834532, + "appID": "august-iphone", + "LockID": "BBBB1F5F11114C24CCCC97571DD6AAAA", + "recentImage": { + "original_filename": "file", + "placeholder": false, + "bytes": 24476, + "height": 640, + "format": "jpg", + "width": 480, + "version": 1512892814, + "resource_type": "image", + "etag": "54966926be2e93f77d498a55f247661f", + "tags": [], + "public_id": "qqqqt4ctmxwsysylaaaa", + "url": "http://image.com/vmk16naaaa7ibuey7sar.jpg", + "created_at": "2017-12-10T08:01:35Z", + "signature": "75z47ca21b5e8ffda21d2134e478a2307c4625da", + "secure_url": "https://image.com/vmk16naaaa7ibuey7sar.jpg", + "type": "upload" + }, + "settings": { + "keepEncoderRunning": true, + "videoResolution": "640x480", + "minACNoScaling": 40, + "irConfiguration": 8448272, + "directLink": true, + "overlayEnabled": true, + "notify_when_offline": true, + "micVolume": 100, + "bitrateCeiling": 512000, + "initialBitrate": 384000, + "IVAEnabled": false, + "turnOffCamera": false, + "ringSoundEnabled": true, + "JPGQuality": 70, + "motion_notifications": true, + "speakerVolume": 92, + "buttonpush_notifications": true, + "ABREnabled": true, + "debug": false, + "batteryLowThreshold": 3.1, + "batteryRun": false, + "IREnabled": true, + "batteryUseThreshold": 3.4 + }, + "doorbellServerURL": "https://doorbells.august.com", + "name": "Front Door", + "createdAt": "2016-11-26T22:27:11.176Z", + "installDate": "2016-11-26T22:27:11.176Z", + "serialNumber": "tBXZR0Z35E", + "dvrSubscriptionSetupDone": true, + "caps": ["reconnect"], + "doorbellID": "K98GiDT45GUL", + "HouseID": "3dd2accaea08", + "telemetry": { + "signal_level": -56, + "date": "2017-12-10 08:05:12", + "steady_ac_in": 22.196405, + "BSSID": "88:ee:00:dd:aa:11", + "SSID": "foo_ssid", + "updated_at": "2017-12-10T08:05:13.650Z", + "temperature": 28.25, + "wifi_freq": 5745, + "load_average": "0.50 0.47 0.35 1/154 9345", + "link_quality": 54, + "uptime": "16168.75 13830.49", + "ip_addr": "10.0.1.11", + "doorbell_low_battery": false, + "ac_in": 23.856874 + }, + "installUserID": "c3b2a94e-373e-aaaa-bbbb-36e996827777", + "status": "doorbell_call_status_online", + "firmwareVersion": "2.3.0-RC153+201711151527", + "pubsubChannel": "7c7a6672-59c8-3333-ffff-dcd98705cccc", + "updatedAt": "2017-12-10T08:05:13.650Z" } diff --git a/tests/components/august/fixtures/get_doorbell.offline.json b/tests/components/august/fixtures/get_doorbell.offline.json index dec94374355..13a8483c995 100644 --- a/tests/components/august/fixtures/get_doorbell.offline.json +++ b/tests/components/august/fixtures/get_doorbell.offline.json @@ -1,130 +1,126 @@ { - "recentImage" : { - "tags" : [], - "height" : 576, - "public_id" : "fdsfds", - "bytes" : 50013, - "resource_type" : "image", - "original_filename" : "file", - "version" : 1582242766, - "format" : "jpg", - "signature" : "fdsfdsf", - "created_at" : "2020-02-20T23:52:46Z", - "type" : "upload", - "placeholder" : false, - "url" : "http://res.cloudinary.com/august-com/image/upload/ccc/ccccc.jpg", - "secure_url" : "https://res.cloudinary.com/august-com/image/upload/cc/cccc.jpg", - "etag" : "zds", - "width" : 720 - }, - "firmwareVersion" : "3.1.0-HYDRC75+201909251139", - "doorbellServerURL" : "https://doorbells.august.com", - "installUserID" : "mock", - "caps" : [ - "reconnect", - "webrtc", - "tcp_wakeup" - ], - "messagingProtocol" : "pubnub", - "createdAt" : "2020-02-12T03:52:28.719Z", - "invitations" : [], - "appID" : "august-iphone-v5", - "HouseID" : "houseid1", - "doorbellID" : "tmt100", - "name" : "Front Door", - "settings" : { - "batteryUseThreshold" : 3.4, - "brightness" : 50, - "batteryChargeCurrent" : 60, - "overCurrentThreshold" : -250, - "irLedBrightness" : 40, - "videoResolution" : "720x576", - "pirPulseCounter" : 1, - "contrast" : 50, - "micVolume" : 50, - "directLink" : true, - "auto_contrast_mode" : 0, - "saturation" : 50, - "motion_notifications" : true, - "pirSensitivity" : 20, - "pirBlindTime" : 7, - "notify_when_offline" : false, - "nightModeAlsThreshold" : 10, - "minACNoScaling" : 40, - "DVRRecordingTimeout" : 15, - "turnOffCamera" : false, - "debug" : false, - "keepEncoderRunning" : true, - "pirWindowTime" : 0, - "bitrateCeiling" : 2000000, - "backlight_comp" : false, - "buttonpush_notifications" : true, - "buttonpush_notifications_partners" : false, - "minimumSnapshotInterval" : 30, - "pirConfiguration" : 272, - "batteryLowThreshold" : 3.1, - "sharpness" : 50, - "ABREnabled" : true, - "hue" : 50, - "initialBitrate" : 1000000, - "ringSoundEnabled" : true, - "IVAEnabled" : false, - "overlayEnabled" : true, - "speakerVolume" : 92, - "ringRepetitions" : 3, - "powerProfilePreset" : -1, - "irConfiguration" : 16836880, - "JPGQuality" : 70, - "IREnabled" : true - }, - "updatedAt" : "2020-02-20T23:58:21.580Z", - "serialNumber" : "abc", - "installDate" : "2019-02-12T03:52:28.719Z", - "dvrSubscriptionSetupDone" : true, - "pubsubChannel" : "mock", - "chimes" : [ - { - "updatedAt" : "2020-02-12T03:55:38.805Z", - "_id" : "cccc", - "type" : 1, - "serialNumber" : "ccccc", - "doorbellID" : "tmt100", - "name" : "Living Room", - "chimeID" : "cccc", - "createdAt" : "2020-02-12T03:55:38.805Z", - "firmware" : "3.1.16" - } - ], - "telemetry" : { - "battery" : 3.985, - "battery_soc" : 81, - "load_average" : "0.45 0.18 0.07 4/98 831", - "ip_addr" : "192.168.100.174", - "BSSID" : "snp", - "uptime" : "96.55 70.59", - "SSID" : "bob", - "updated_at" : "2020-02-20T23:53:09.586Z", - "dtim_period" : 0, - "wifi_freq" : 2462, - "date" : "2020-02-20 11:47:36", - "BSSIDManufacturer" : "Ubiquiti - Ubiquiti Networks Inc.", - "battery_temp" : 22, - "battery_avg_cur" : -291, - "beacon_interval" : 0, - "signal_level" : -49, - "battery_soh" : 95, - "doorbell_low_battery" : false - }, - "secChipCertSerial" : "", - "tcpKeepAlive" : { - "keepAliveUUID" : "mock", - "wakeUp" : { - "token" : "wakemeup", - "lastUpdated" : 1582242723931 - } - }, - "statusUpdatedAtMs" : 1582243101579, - "status" : "doorbell_offline", - "type" : "hydra1", - "HouseName" : "housename" + "recentImage": { + "tags": [], + "height": 576, + "public_id": "fdsfds", + "bytes": 50013, + "resource_type": "image", + "original_filename": "file", + "version": 1582242766, + "format": "jpg", + "signature": "fdsfdsf", + "created_at": "2020-02-20T23:52:46Z", + "type": "upload", + "placeholder": false, + "url": "http://res.cloudinary.com/august-com/image/upload/ccc/ccccc.jpg", + "secure_url": "https://res.cloudinary.com/august-com/image/upload/cc/cccc.jpg", + "etag": "zds", + "width": 720 + }, + "firmwareVersion": "3.1.0-HYDRC75+201909251139", + "doorbellServerURL": "https://doorbells.august.com", + "installUserID": "mock", + "caps": ["reconnect", "webrtc", "tcp_wakeup"], + "messagingProtocol": "pubnub", + "createdAt": "2020-02-12T03:52:28.719Z", + "invitations": [], + "appID": "august-iphone-v5", + "HouseID": "houseid1", + "doorbellID": "tmt100", + "name": "Front Door", + "settings": { + "batteryUseThreshold": 3.4, + "brightness": 50, + "batteryChargeCurrent": 60, + "overCurrentThreshold": -250, + "irLedBrightness": 40, + "videoResolution": "720x576", + "pirPulseCounter": 1, + "contrast": 50, + "micVolume": 50, + "directLink": true, + "auto_contrast_mode": 0, + "saturation": 50, + "motion_notifications": true, + "pirSensitivity": 20, + "pirBlindTime": 7, + "notify_when_offline": false, + "nightModeAlsThreshold": 10, + "minACNoScaling": 40, + "DVRRecordingTimeout": 15, + "turnOffCamera": false, + "debug": false, + "keepEncoderRunning": true, + "pirWindowTime": 0, + "bitrateCeiling": 2000000, + "backlight_comp": false, + "buttonpush_notifications": true, + "buttonpush_notifications_partners": false, + "minimumSnapshotInterval": 30, + "pirConfiguration": 272, + "batteryLowThreshold": 3.1, + "sharpness": 50, + "ABREnabled": true, + "hue": 50, + "initialBitrate": 1000000, + "ringSoundEnabled": true, + "IVAEnabled": false, + "overlayEnabled": true, + "speakerVolume": 92, + "ringRepetitions": 3, + "powerProfilePreset": -1, + "irConfiguration": 16836880, + "JPGQuality": 70, + "IREnabled": true + }, + "updatedAt": "2020-02-20T23:58:21.580Z", + "serialNumber": "abc", + "installDate": "2019-02-12T03:52:28.719Z", + "dvrSubscriptionSetupDone": true, + "pubsubChannel": "mock", + "chimes": [ + { + "updatedAt": "2020-02-12T03:55:38.805Z", + "_id": "cccc", + "type": 1, + "serialNumber": "ccccc", + "doorbellID": "tmt100", + "name": "Living Room", + "chimeID": "cccc", + "createdAt": "2020-02-12T03:55:38.805Z", + "firmware": "3.1.16" + } + ], + "telemetry": { + "battery": 3.985, + "battery_soc": 81, + "load_average": "0.45 0.18 0.07 4/98 831", + "ip_addr": "192.168.100.174", + "BSSID": "snp", + "uptime": "96.55 70.59", + "SSID": "bob", + "updated_at": "2020-02-20T23:53:09.586Z", + "dtim_period": 0, + "wifi_freq": 2462, + "date": "2020-02-20 11:47:36", + "BSSIDManufacturer": "Ubiquiti - Ubiquiti Networks Inc.", + "battery_temp": 22, + "battery_avg_cur": -291, + "beacon_interval": 0, + "signal_level": -49, + "battery_soh": 95, + "doorbell_low_battery": false + }, + "secChipCertSerial": "", + "tcpKeepAlive": { + "keepAliveUUID": "mock", + "wakeUp": { + "token": "wakemeup", + "lastUpdated": 1582242723931 + } + }, + "statusUpdatedAtMs": 1582243101579, + "status": "doorbell_offline", + "type": "hydra1", + "HouseName": "housename" } diff --git a/tests/components/august/fixtures/get_lock.doorsense_init.json b/tests/components/august/fixtures/get_lock.doorsense_init.json index be60bbe6236..d85ca3b153f 100644 --- a/tests/components/august/fixtures/get_lock.doorsense_init.json +++ b/tests/components/august/fixtures/get_lock.doorsense_init.json @@ -66,10 +66,7 @@ "UserType": "superuser", "FirstName": "Foo", "LastName": "Bar", - "identifiers": [ - "email:foo@bar.com", - "phone:+177777777777" - ], + "identifiers": ["email:foo@bar.com", "phone:+177777777777"], "imageInfo": { "original": { "width": 948, diff --git a/tests/components/august/fixtures/get_lock.low_keypad_battery.json b/tests/components/august/fixtures/get_lock.low_keypad_battery.json index f848a8d30eb..b10c3f2600f 100644 --- a/tests/components/august/fixtures/get_lock.low_keypad_battery.json +++ b/tests/components/august/fixtures/get_lock.low_keypad_battery.json @@ -66,10 +66,7 @@ "UserType": "superuser", "FirstName": "Foo", "LastName": "Bar", - "identifiers": [ - "email:foo@bar.com", - "phone:+177777777777" - ], + "identifiers": ["email:foo@bar.com", "phone:+177777777777"], "imageInfo": { "original": { "width": 948, diff --git a/tests/components/august/fixtures/get_lock.offline.json b/tests/components/august/fixtures/get_lock.offline.json index 502a78674e9..753a1081918 100644 --- a/tests/components/august/fixtures/get_lock.offline.json +++ b/tests/components/august/fixtures/get_lock.offline.json @@ -1,68 +1,65 @@ { - "Calibrated" : false, - "Created" : "2000-00-00T00:00:00.447Z", - "HouseID" : "houseid", - "HouseName" : "MockName", - "LockID" : "ABC", - "LockName" : "Test", - "LockStatus" : { - "status" : "unknown" - }, - "OfflineKeys" : { - "created" : [], - "createdhk" : [ - { - "UserID" : "mock-user-id", - "created" : "2000-00-00T00:00:00.447Z", - "key" : "mockkey", - "slot" : 12 - } - ], - "deleted" : [], - "loaded" : [ - { - "UserID" : "userid", - "created" : "2000-00-00T00:00:00.447Z", - "key" : "key", - "loaded" : "2000-00-00T00:00:00.447Z", - "slot" : 1 - } - ] - }, - "SerialNumber" : "ABC", - "Type" : 3, - "Updated" : "2000-00-00T00:00:00.447Z", - "battery" : -1, - "cameras" : [], - "currentFirmwareVersion" : "undefined-1.59.0-1.13.2", - "geofenceLimits" : { - "ios" : { - "debounceInterval" : 90, - "gpsAccuracyMultiplier" : 2.5, - "maximumGeofence" : 5000, - "minGPSAccuracyRequired" : 80, - "minimumGeofence" : 100 + "Calibrated": false, + "Created": "2000-00-00T00:00:00.447Z", + "HouseID": "houseid", + "HouseName": "MockName", + "LockID": "ABC", + "LockName": "Test", + "LockStatus": { + "status": "unknown" + }, + "OfflineKeys": { + "created": [], + "createdhk": [ + { + "UserID": "mock-user-id", + "created": "2000-00-00T00:00:00.447Z", + "key": "mockkey", + "slot": 12 } - }, - "homeKitEnabled" : false, - "isGalileo" : false, - "macAddress" : "a:b:c", - "parametersToSet" : {}, - "pubsubChannel" : "mockpubsub", - "ruleHash" : {}, - "skuNumber" : "AUG-X", - "supportsEntryCodes" : false, - "users" : { - "mockuserid" : { - "FirstName" : "MockName", - "LastName" : "House", - "UserType" : "superuser", - "identifiers" : [ - "phone:+15558675309", - "email:mockme@mock.org" - ] + ], + "deleted": [], + "loaded": [ + { + "UserID": "userid", + "created": "2000-00-00T00:00:00.447Z", + "key": "key", + "loaded": "2000-00-00T00:00:00.447Z", + "slot": 1 } - }, - "zWaveDSK" : "1-2-3-4", - "zWaveEnabled" : true + ] + }, + "SerialNumber": "ABC", + "Type": 3, + "Updated": "2000-00-00T00:00:00.447Z", + "battery": -1, + "cameras": [], + "currentFirmwareVersion": "undefined-1.59.0-1.13.2", + "geofenceLimits": { + "ios": { + "debounceInterval": 90, + "gpsAccuracyMultiplier": 2.5, + "maximumGeofence": 5000, + "minGPSAccuracyRequired": 80, + "minimumGeofence": 100 + } + }, + "homeKitEnabled": false, + "isGalileo": false, + "macAddress": "a:b:c", + "parametersToSet": {}, + "pubsubChannel": "mockpubsub", + "ruleHash": {}, + "skuNumber": "AUG-X", + "supportsEntryCodes": false, + "users": { + "mockuserid": { + "FirstName": "MockName", + "LastName": "House", + "UserType": "superuser", + "identifiers": ["phone:+15558675309", "email:mockme@mock.org"] + } + }, + "zWaveDSK": "1-2-3-4", + "zWaveEnabled": true } diff --git a/tests/components/august/fixtures/get_lock.online.json b/tests/components/august/fixtures/get_lock.online.json index 8003359e589..7fa12fa8bcb 100644 --- a/tests/components/august/fixtures/get_lock.online.json +++ b/tests/components/august/fixtures/get_lock.online.json @@ -66,10 +66,7 @@ "UserType": "superuser", "FirstName": "Foo", "LastName": "Bar", - "identifiers": [ - "email:foo@bar.com", - "phone:+177777777777" - ], + "identifiers": ["email:foo@bar.com", "phone:+177777777777"], "imageInfo": { "original": { "width": 948, diff --git a/tests/components/august/fixtures/get_lock.online.unknown_state.json b/tests/components/august/fixtures/get_lock.online.unknown_state.json index ad455655902..abc8b40a132 100644 --- a/tests/components/august/fixtures/get_lock.online.unknown_state.json +++ b/tests/components/august/fixtures/get_lock.online.unknown_state.json @@ -1,59 +1,59 @@ { - "LockName": "Side Door", - "Type": 1001, - "Created": "2019-10-07T01:49:06.831Z", - "Updated": "2019-10-07T01:49:06.831Z", - "LockID": "BROKENID", - "HouseID": "abc", - "HouseName": "dog", - "Calibrated": false, - "timeZone": "America/Chicago", - "battery": 0.9524716174964851, - "hostLockInfo": { - "serialNumber": "YR", - "manufacturer": "yale", - "productID": 1536, - "productTypeID": 32770 + "LockName": "Side Door", + "Type": 1001, + "Created": "2019-10-07T01:49:06.831Z", + "Updated": "2019-10-07T01:49:06.831Z", + "LockID": "BROKENID", + "HouseID": "abc", + "HouseName": "dog", + "Calibrated": false, + "timeZone": "America/Chicago", + "battery": 0.9524716174964851, + "hostLockInfo": { + "serialNumber": "YR", + "manufacturer": "yale", + "productID": 1536, + "productTypeID": 32770 + }, + "supportsEntryCodes": true, + "skuNumber": "AUG-MD01", + "macAddress": "MAC", + "SerialNumber": "M1FXZ00EZ9", + "LockStatus": { + "status": "unknown_error_during_connect", + "dateTime": "2020-02-22T02:48:11.741Z", + "isLockStatusChanged": true, + "valid": true, + "doorState": "closed" + }, + "currentFirmwareVersion": "undefined-4.3.0-1.8.14", + "homeKitEnabled": true, + "zWaveEnabled": false, + "isGalileo": false, + "Bridge": { + "_id": "id", + "mfgBridgeID": "id", + "deviceModel": "august-connect", + "firmwareVersion": "2.2.1", + "operative": true, + "status": { + "current": "online", + "updated": "2020-02-21T15:06:47.001Z", + "lastOnline": "2020-02-21T15:06:47.001Z", + "lastOffline": "2020-02-06T17:33:21.265Z" }, - "supportsEntryCodes": true, - "skuNumber": "AUG-MD01", - "macAddress": "MAC", - "SerialNumber": "M1FXZ00EZ9", - "LockStatus": { - "status": "unknown_error_during_connect", - "dateTime": "2020-02-22T02:48:11.741Z", - "isLockStatusChanged": true, - "valid": true, - "doorState": "closed" - }, - "currentFirmwareVersion": "undefined-4.3.0-1.8.14", - "homeKitEnabled": true, - "zWaveEnabled": false, - "isGalileo": false, - "Bridge": { - "_id": "id", - "mfgBridgeID": "id", - "deviceModel": "august-connect", - "firmwareVersion": "2.2.1", - "operative": true, - "status": { - "current": "online", - "updated": "2020-02-21T15:06:47.001Z", - "lastOnline": "2020-02-21T15:06:47.001Z", - "lastOffline": "2020-02-06T17:33:21.265Z" - }, - "hyperBridge": true - }, - "parametersToSet": {}, - "ruleHash": {}, - "cameras": [], - "geofenceLimits": { - "ios": { - "debounceInterval": 90, - "gpsAccuracyMultiplier": 2.5, - "maximumGeofence": 5000, - "minimumGeofence": 100, - "minGPSAccuracyRequired": 80 - } + "hyperBridge": true + }, + "parametersToSet": {}, + "ruleHash": {}, + "cameras": [], + "geofenceLimits": { + "ios": { + "debounceInterval": 90, + "gpsAccuracyMultiplier": 2.5, + "maximumGeofence": 5000, + "minimumGeofence": 100, + "minGPSAccuracyRequired": 80 } + } } diff --git a/tests/components/august/fixtures/get_lock.online_missing_doorsense.json b/tests/components/august/fixtures/get_lock.online_missing_doorsense.json index 46971c3bbd2..84822df9b89 100644 --- a/tests/components/august/fixtures/get_lock.online_missing_doorsense.json +++ b/tests/components/august/fixtures/get_lock.online_missing_doorsense.json @@ -1,50 +1,50 @@ { - "Bridge" : { - "_id" : "bridgeid", - "deviceModel" : "august-connect", - "firmwareVersion" : "2.2.1", - "hyperBridge" : true, - "mfgBridgeID" : "C5WY200WSH", - "operative" : true, - "status" : { - "current" : "online", - "lastOffline" : "2000-00-00T00:00:00.447Z", - "lastOnline" : "2000-00-00T00:00:00.447Z", - "updated" : "2000-00-00T00:00:00.447Z" - } - }, - "Calibrated" : false, - "Created" : "2000-00-00T00:00:00.447Z", - "HouseID" : "123", - "HouseName" : "Test", - "LockID" : "missing_doorsense_id", - "LockName" : "Online door missing doorsense", - "LockStatus" : { - "dateTime" : "2017-12-10T04:48:30.272Z", - "isLockStatusChanged" : false, - "status" : "locked", - "valid" : true - }, - "SerialNumber" : "XY", - "Type" : 1001, - "Updated" : "2000-00-00T00:00:00.447Z", - "battery" : 0.922, - "currentFirmwareVersion" : "undefined-4.3.0-1.8.14", - "homeKitEnabled" : true, - "hostLockInfo" : { - "manufacturer" : "yale", - "productID" : 1536, - "productTypeID" : 32770, - "serialNumber" : "ABC" - }, - "isGalileo" : false, - "macAddress" : "12:22", - "pins" : { - "created" : [], - "loaded" : [] - }, - "skuNumber" : "AUG-MD01", - "supportsEntryCodes" : true, - "timeZone" : "Pacific/Hawaii", - "zWaveEnabled" : false + "Bridge": { + "_id": "bridgeid", + "deviceModel": "august-connect", + "firmwareVersion": "2.2.1", + "hyperBridge": true, + "mfgBridgeID": "C5WY200WSH", + "operative": true, + "status": { + "current": "online", + "lastOffline": "2000-00-00T00:00:00.447Z", + "lastOnline": "2000-00-00T00:00:00.447Z", + "updated": "2000-00-00T00:00:00.447Z" + } + }, + "Calibrated": false, + "Created": "2000-00-00T00:00:00.447Z", + "HouseID": "123", + "HouseName": "Test", + "LockID": "missing_doorsense_id", + "LockName": "Online door missing doorsense", + "LockStatus": { + "dateTime": "2017-12-10T04:48:30.272Z", + "isLockStatusChanged": false, + "status": "locked", + "valid": true + }, + "SerialNumber": "XY", + "Type": 1001, + "Updated": "2000-00-00T00:00:00.447Z", + "battery": 0.922, + "currentFirmwareVersion": "undefined-4.3.0-1.8.14", + "homeKitEnabled": true, + "hostLockInfo": { + "manufacturer": "yale", + "productID": 1536, + "productTypeID": 32770, + "serialNumber": "ABC" + }, + "isGalileo": false, + "macAddress": "12:22", + "pins": { + "created": [], + "loaded": [] + }, + "skuNumber": "AUG-MD01", + "supportsEntryCodes": true, + "timeZone": "Pacific/Hawaii", + "zWaveEnabled": false } diff --git a/tests/components/august/fixtures/get_lock.online_with_doorsense.json b/tests/components/august/fixtures/get_lock.online_with_doorsense.json index e29614c9e48..d9b413708ca 100644 --- a/tests/components/august/fixtures/get_lock.online_with_doorsense.json +++ b/tests/components/august/fixtures/get_lock.online_with_doorsense.json @@ -1,52 +1,52 @@ { - "Bridge" : { - "_id" : "bridgeid", - "deviceModel" : "august-connect", - "firmwareVersion" : "2.2.1", - "hyperBridge" : true, - "mfgBridgeID" : "C5WY200WSH", - "operative" : true, - "status" : { - "current" : "online", - "lastOffline" : "2000-00-00T00:00:00.447Z", - "lastOnline" : "2000-00-00T00:00:00.447Z", - "updated" : "2000-00-00T00:00:00.447Z" - } - }, - "pubsubChannel":"pubsub", - "Calibrated" : false, - "Created" : "2000-00-00T00:00:00.447Z", - "HouseID" : "mockhouseid1", - "HouseName" : "Test", - "LockID" : "online_with_doorsense", - "LockName" : "Online door with doorsense", - "LockStatus" : { - "dateTime" : "2017-12-10T04:48:30.272Z", - "doorState" : "open", - "isLockStatusChanged" : false, - "status" : "locked", - "valid" : true - }, - "SerialNumber" : "XY", - "Type" : 1001, - "Updated" : "2000-00-00T00:00:00.447Z", - "battery" : 0.922, - "currentFirmwareVersion" : "undefined-4.3.0-1.8.14", - "homeKitEnabled" : true, - "hostLockInfo" : { - "manufacturer" : "yale", - "productID" : 1536, - "productTypeID" : 32770, - "serialNumber" : "ABC" - }, - "isGalileo" : false, - "macAddress" : "12:22", - "pins" : { - "created" : [], - "loaded" : [] - }, - "skuNumber" : "AUG-MD01", - "supportsEntryCodes" : true, - "timeZone" : "Pacific/Hawaii", - "zWaveEnabled" : false + "Bridge": { + "_id": "bridgeid", + "deviceModel": "august-connect", + "firmwareVersion": "2.2.1", + "hyperBridge": true, + "mfgBridgeID": "C5WY200WSH", + "operative": true, + "status": { + "current": "online", + "lastOffline": "2000-00-00T00:00:00.447Z", + "lastOnline": "2000-00-00T00:00:00.447Z", + "updated": "2000-00-00T00:00:00.447Z" + } + }, + "pubsubChannel": "pubsub", + "Calibrated": false, + "Created": "2000-00-00T00:00:00.447Z", + "HouseID": "mockhouseid1", + "HouseName": "Test", + "LockID": "online_with_doorsense", + "LockName": "Online door with doorsense", + "LockStatus": { + "dateTime": "2017-12-10T04:48:30.272Z", + "doorState": "open", + "isLockStatusChanged": false, + "status": "locked", + "valid": true + }, + "SerialNumber": "XY", + "Type": 1001, + "Updated": "2000-00-00T00:00:00.447Z", + "battery": 0.922, + "currentFirmwareVersion": "undefined-4.3.0-1.8.14", + "homeKitEnabled": true, + "hostLockInfo": { + "manufacturer": "yale", + "productID": 1536, + "productTypeID": 32770, + "serialNumber": "ABC" + }, + "isGalileo": false, + "macAddress": "12:22", + "pins": { + "created": [], + "loaded": [] + }, + "skuNumber": "AUG-MD01", + "supportsEntryCodes": true, + "timeZone": "Pacific/Hawaii", + "zWaveEnabled": false } diff --git a/tests/components/august/fixtures/lock_open.json b/tests/components/august/fixtures/lock_open.json index 67e3ccfbf15..b6cfe3c90fc 100644 --- a/tests/components/august/fixtures/lock_open.json +++ b/tests/components/august/fixtures/lock_open.json @@ -1,26 +1,26 @@ { - "status" : "kAugLockState_Locked", - "resultsFromOperationCache" : false, - "retryCount" : 1, - "info" : { - "wlanRSSI" : -54, - "lockType" : "lock_version_1001", - "lockStatusChanged" : false, - "serialNumber" : "ABC", - "serial" : "123", - "action" : "lock", - "context" : { - "startDate" : "2020-02-19T01:59:39.516Z", - "retryCount" : 1, - "transactionID" : "mock" - }, - "bridgeID" : "mock", - "wlanSNR" : 41, - "startTime" : "2020-02-19T01:59:39.517Z", - "duration" : 5149, - "lockID" : "ABC", - "rssi" : -77 - }, - "totalTime" : 5162, - "doorState" : "kAugDoorState_Open" + "status": "kAugLockState_Locked", + "resultsFromOperationCache": false, + "retryCount": 1, + "info": { + "wlanRSSI": -54, + "lockType": "lock_version_1001", + "lockStatusChanged": false, + "serialNumber": "ABC", + "serial": "123", + "action": "lock", + "context": { + "startDate": "2020-02-19T01:59:39.516Z", + "retryCount": 1, + "transactionID": "mock" + }, + "bridgeID": "mock", + "wlanSNR": 41, + "startTime": "2020-02-19T01:59:39.517Z", + "duration": 5149, + "lockID": "ABC", + "rssi": -77 + }, + "totalTime": 5162, + "doorState": "kAugDoorState_Open" } diff --git a/tests/components/august/fixtures/unlock_closed.json b/tests/components/august/fixtures/unlock_closed.json index 57b712f55e1..f676c005a17 100644 --- a/tests/components/august/fixtures/unlock_closed.json +++ b/tests/components/august/fixtures/unlock_closed.json @@ -1,26 +1,26 @@ { - "status" : "kAugLockState_Unlocked", - "resultsFromOperationCache" : false, - "retryCount" : 1, - "info" : { - "wlanRSSI" : -54, - "lockType" : "lock_version_1001", - "lockStatusChanged" : false, - "serialNumber" : "ABC", - "serial" : "123", - "action" : "lock", - "context" : { - "startDate" : "2020-02-19T01:59:39.516Z", - "retryCount" : 1, - "transactionID" : "mock" - }, - "bridgeID" : "mock", - "wlanSNR" : 41, - "startTime" : "2020-02-19T01:59:39.517Z", - "duration" : 5149, - "lockID" : "ABC", - "rssi" : -77 - }, - "totalTime" : 5162, - "doorState" : "kAugDoorState_Closed" + "status": "kAugLockState_Unlocked", + "resultsFromOperationCache": false, + "retryCount": 1, + "info": { + "wlanRSSI": -54, + "lockType": "lock_version_1001", + "lockStatusChanged": false, + "serialNumber": "ABC", + "serial": "123", + "action": "lock", + "context": { + "startDate": "2020-02-19T01:59:39.516Z", + "retryCount": 1, + "transactionID": "mock" + }, + "bridgeID": "mock", + "wlanSNR": 41, + "startTime": "2020-02-19T01:59:39.517Z", + "duration": 5149, + "lockID": "ABC", + "rssi": -77 + }, + "totalTime": 5162, + "doorState": "kAugDoorState_Closed" } diff --git a/tests/components/august/test_diagnostics.py b/tests/components/august/test_diagnostics.py new file mode 100644 index 00000000000..520daa91f91 --- /dev/null +++ b/tests/components/august/test_diagnostics.py @@ -0,0 +1,139 @@ +"""Test august diagnostics.""" + +from tests.components.august.mocks import ( + _create_august_api_with_devices, + _mock_doorbell_from_fixture, + _mock_lock_from_fixture, +) +from tests.components.diagnostics import get_diagnostics_for_config_entry + + +async def test_diagnostics(hass, hass_client): + """Test generating diagnostics for a config entry.""" + lock_one = await _mock_lock_from_fixture( + hass, "get_lock.online_with_doorsense.json" + ) + doorbell_one = await _mock_doorbell_from_fixture(hass, "get_doorbell.json") + + entry, _ = await _create_august_api_with_devices(hass, [lock_one, doorbell_one]) + diag = await get_diagnostics_for_config_entry(hass, hass_client, entry) + + assert diag == { + "doorbells": { + "K98GiDT45GUL": { + "HouseID": "**REDACTED**", + "LockID": "BBBB1F5F11114C24CCCC97571DD6AAAA", + "appID": "august-iphone", + "caps": ["reconnect"], + "createdAt": "2016-11-26T22:27:11.176Z", + "doorbellID": "K98GiDT45GUL", + "doorbellServerURL": "https://doorbells.august.com", + "dvrSubscriptionSetupDone": True, + "firmwareVersion": "2.3.0-RC153+201711151527", + "installDate": "2016-11-26T22:27:11.176Z", + "installUserID": "**REDACTED**", + "name": "Front Door", + "pubsubChannel": "**REDACTED**", + "recentImage": "**REDACTED**", + "serialNumber": "tBXZR0Z35E", + "settings": { + "ABREnabled": True, + "IREnabled": True, + "IVAEnabled": False, + "JPGQuality": 70, + "batteryLowThreshold": 3.1, + "batteryRun": False, + "batteryUseThreshold": 3.4, + "bitrateCeiling": 512000, + "buttonpush_notifications": True, + "debug": False, + "directLink": True, + "initialBitrate": 384000, + "irConfiguration": 8448272, + "keepEncoderRunning": True, + "micVolume": 100, + "minACNoScaling": 40, + "motion_notifications": True, + "notify_when_offline": True, + "overlayEnabled": True, + "ringSoundEnabled": True, + "speakerVolume": 92, + "turnOffCamera": False, + "videoResolution": "640x480", + }, + "status": "doorbell_call_status_online", + "status_timestamp": 1512811834532, + "telemetry": { + "BSSID": "88:ee:00:dd:aa:11", + "SSID": "foo_ssid", + "ac_in": 23.856874, + "battery": 4.061763, + "battery_soc": 96, + "battery_soh": 95, + "date": "2017-12-10 08:05:12", + "doorbell_low_battery": False, + "ip_addr": "10.0.1.11", + "link_quality": 54, + "load_average": "0.50 0.47 0.35 " "1/154 9345", + "signal_level": -56, + "steady_ac_in": 22.196405, + "temperature": 28.25, + "updated_at": "2017-12-10T08:05:13.650Z", + "uptime": "16168.75 13830.49", + "wifi_freq": 5745, + }, + "updatedAt": "2017-12-10T08:05:13.650Z", + } + }, + "locks": { + "online_with_doorsense": { + "Bridge": { + "_id": "bridgeid", + "deviceModel": "august-connect", + "firmwareVersion": "2.2.1", + "hyperBridge": True, + "mfgBridgeID": "C5WY200WSH", + "operative": True, + "status": { + "current": "online", + "lastOffline": "2000-00-00T00:00:00.447Z", + "lastOnline": "2000-00-00T00:00:00.447Z", + "updated": "2000-00-00T00:00:00.447Z", + }, + }, + "Calibrated": False, + "Created": "2000-00-00T00:00:00.447Z", + "HouseID": "**REDACTED**", + "HouseName": "Test", + "LockID": "online_with_doorsense", + "LockName": "Online door with doorsense", + "LockStatus": { + "dateTime": "2017-12-10T04:48:30.272Z", + "doorState": "open", + "isLockStatusChanged": False, + "status": "locked", + "valid": True, + }, + "SerialNumber": "XY", + "Type": 1001, + "Updated": "2000-00-00T00:00:00.447Z", + "battery": 0.922, + "currentFirmwareVersion": "undefined-4.3.0-1.8.14", + "homeKitEnabled": True, + "hostLockInfo": { + "manufacturer": "yale", + "productID": 1536, + "productTypeID": 32770, + "serialNumber": "ABC", + }, + "isGalileo": False, + "macAddress": "12:22", + "pins": "**REDACTED**", + "pubsubChannel": "**REDACTED**", + "skuNumber": "AUG-MD01", + "supportsEntryCodes": True, + "timeZone": "Pacific/Hawaii", + "zWaveEnabled": False, + } + }, + } diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index b90c6a90819..1c90abe72ca 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -1,5 +1,6 @@ """The tests for the automation component.""" import asyncio +from datetime import timedelta import logging from unittest.mock import Mock, patch @@ -25,14 +26,30 @@ from homeassistant.const import ( STATE_OFF, STATE_ON, ) -from homeassistant.core import Context, CoreState, State, callback +from homeassistant.core import ( + Context, + CoreState, + HomeAssistant, + ServiceCall, + State, + callback, +) from homeassistant.exceptions import HomeAssistantError, Unauthorized +from homeassistant.helpers.script import ( + SCRIPT_MODE_CHOICES, + SCRIPT_MODE_PARALLEL, + SCRIPT_MODE_QUEUED, + SCRIPT_MODE_RESTART, + SCRIPT_MODE_SINGLE, + _async_stop_scripts_at_shutdown, +) from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util from tests.common import ( assert_setup_component, async_capture_events, + async_fire_time_changed, async_mock_service, mock_restore_cache, ) @@ -1570,3 +1587,191 @@ async def test_trigger_condition_explicit_id(hass, calls): await hass.async_block_till_done() assert len(calls) == 2 assert calls[-1].data.get("param") == "two" + + +@pytest.mark.parametrize( + "automation_mode,automation_runs", + ( + (SCRIPT_MODE_PARALLEL, 2), + (SCRIPT_MODE_QUEUED, 2), + (SCRIPT_MODE_RESTART, 2), + (SCRIPT_MODE_SINGLE, 1), + ), +) +@pytest.mark.parametrize( + "script_mode,script_warning_msg", + ( + (SCRIPT_MODE_PARALLEL, "script1: Maximum number of runs exceeded"), + (SCRIPT_MODE_QUEUED, "script1: Disallowed recursion detected"), + (SCRIPT_MODE_RESTART, "script1: Disallowed recursion detected"), + (SCRIPT_MODE_SINGLE, "script1: Already running"), + ), +) +async def test_recursive_automation_starting_script( + hass: HomeAssistant, + automation_mode, + automation_runs, + script_mode, + script_warning_msg, + caplog, +): + """Test starting automations does not interfere with script deadlock prevention.""" + + # Fail if additional script modes are added to + # make sure we cover all script modes in tests + assert SCRIPT_MODE_CHOICES == [ + SCRIPT_MODE_PARALLEL, + SCRIPT_MODE_QUEUED, + SCRIPT_MODE_RESTART, + SCRIPT_MODE_SINGLE, + ] + + stop_scripts_at_shutdown_called = asyncio.Event() + real_stop_scripts_at_shutdown = _async_stop_scripts_at_shutdown + + async def mock_stop_scripts_at_shutdown(*args): + await real_stop_scripts_at_shutdown(*args) + stop_scripts_at_shutdown_called.set() + + with patch( + "homeassistant.helpers.script._async_stop_scripts_at_shutdown", + wraps=mock_stop_scripts_at_shutdown, + ): + assert await async_setup_component( + hass, + "script", + { + "script": { + "script1": { + "mode": script_mode, + "sequence": [ + {"event": "trigger_automation"}, + { + "wait_template": f"{{{{ float(states('sensor.test'), 0) >= {automation_runs} }}}}" + }, + {"service": "script.script1"}, + {"service": "test.script_done"}, + ], + }, + } + }, + ) + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "mode": automation_mode, + "trigger": [ + {"platform": "event", "event_type": "trigger_automation"}, + ], + "action": [ + {"service": "test.automation_started"}, + {"service": "script.script1"}, + ], + } + }, + ) + + script_done_event = asyncio.Event() + script_done = [] + automation_started = [] + automation_triggered = [] + + async def async_service_handler(service: ServiceCall): + if service.service == "automation_started": + automation_started.append(service) + elif service.service == "script_done": + script_done.append(service) + if len(script_done) == 1: + script_done_event.set() + + async def async_automation_triggered(event): + """Listen to automation_triggered event from the automation integration.""" + automation_triggered.append(event) + hass.states.async_set("sensor.test", str(len(automation_triggered))) + + hass.services.async_register("test", "script_done", async_service_handler) + hass.services.async_register( + "test", "automation_started", async_service_handler + ) + hass.bus.async_listen("automation_triggered", async_automation_triggered) + + hass.bus.async_fire("trigger_automation") + await asyncio.wait_for(script_done_event.wait(), 1) + + # Trigger 1st stage script shutdown + hass.state = CoreState.stopping + hass.bus.async_fire("homeassistant_stop") + await asyncio.wait_for(stop_scripts_at_shutdown_called.wait(), 1) + + # Trigger 2nd stage script shutdown + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=60)) + await hass.async_block_till_done() + + assert script_warning_msg in caplog.text + + +@pytest.mark.parametrize("automation_mode", SCRIPT_MODE_CHOICES) +async def test_recursive_automation(hass: HomeAssistant, automation_mode, caplog): + """Test automation triggering itself. + + - Illegal recursion detection should not be triggered + - Home Assistant should not hang on shut down + """ + stop_scripts_at_shutdown_called = asyncio.Event() + real_stop_scripts_at_shutdown = _async_stop_scripts_at_shutdown + + async def stop_scripts_at_shutdown(*args): + await real_stop_scripts_at_shutdown(*args) + stop_scripts_at_shutdown_called.set() + + with patch( + "homeassistant.helpers.script._async_stop_scripts_at_shutdown", + wraps=stop_scripts_at_shutdown, + ): + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "mode": automation_mode, + "trigger": [ + {"platform": "event", "event_type": "trigger_automation"}, + ], + "action": [ + {"event": "trigger_automation"}, + {"service": "test.automation_done"}, + ], + } + }, + ) + + service_called = asyncio.Event() + service_called_late = [] + + async def async_service_handler(service): + if service.service == "automation_done": + service_called.set() + if service.service == "automation_started_late": + service_called_late.append(service) + + hass.services.async_register("test", "automation_done", async_service_handler) + hass.services.async_register( + "test", "automation_started_late", async_service_handler + ) + + hass.bus.async_fire("trigger_automation") + await asyncio.wait_for(service_called.wait(), 1) + + # Trigger 1st stage script shutdown + hass.state = CoreState.stopping + hass.bus.async_fire("homeassistant_stop") + await asyncio.wait_for(stop_scripts_at_shutdown_called.wait(), 1) + + # Trigger 2nd stage script shutdown + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=90)) + await hass.async_block_till_done() + + assert "Disallowed recursion detected" not in caplog.text diff --git a/tests/components/automation/test_reproduce_state.py b/tests/components/automation/test_reproduce_state.py index c00378aa369..4c596afee2d 100644 --- a/tests/components/automation/test_reproduce_state.py +++ b/tests/components/automation/test_reproduce_state.py @@ -1,5 +1,6 @@ """Test reproduce state for Automation.""" from homeassistant.core import State +from homeassistant.helpers.state import async_reproduce_state from tests.common import async_mock_service @@ -13,30 +14,30 @@ async def test_reproducing_states(hass, caplog): turn_off_calls = async_mock_service(hass, "automation", "turn_off") # These calls should do nothing as entities already in desired state - await hass.helpers.state.async_reproduce_state( - [State("automation.entity_off", "off"), State("automation.entity_on", "on")] + await async_reproduce_state( + hass, + [State("automation.entity_off", "off"), State("automation.entity_on", "on")], ) assert len(turn_on_calls) == 0 assert len(turn_off_calls) == 0 # Test invalid state is handled - await hass.helpers.state.async_reproduce_state( - [State("automation.entity_off", "not_supported")] - ) + await async_reproduce_state(hass, [State("automation.entity_off", "not_supported")]) assert "not_supported" in caplog.text assert len(turn_on_calls) == 0 assert len(turn_off_calls) == 0 # Make sure correct services are called - await hass.helpers.state.async_reproduce_state( + await async_reproduce_state( + hass, [ State("automation.entity_on", "off"), State("automation.entity_off", "on"), # Should not raise State("automation.non_existing", "on"), - ] + ], ) assert len(turn_on_calls) == 1 diff --git a/tests/components/awair/fixtures/awair-offline.json b/tests/components/awair/fixtures/awair-offline.json index f93ccdf4b7b..22f4a272fc1 100644 --- a/tests/components/awair/fixtures/awair-offline.json +++ b/tests/components/awair/fixtures/awair-offline.json @@ -1 +1 @@ -{"data":[]} +{ "data": [] } diff --git a/tests/components/awair/fixtures/awair-r2.json b/tests/components/awair/fixtures/awair-r2.json index e0150eed54f..92c5bc97b9f 100644 --- a/tests/components/awair/fixtures/awair-r2.json +++ b/tests/components/awair/fixtures/awair-r2.json @@ -1 +1,22 @@ -{"data":[{"timestamp":"2020-04-10T16:41:57.771Z","score":97.0,"sensors":[{"comp":"temp","value":18.829999923706055},{"comp":"humid","value":50.52000045776367},{"comp":"co2","value":431.0},{"comp":"voc","value":57.0},{"comp":"pm25","value":2.0}],"indices":[{"comp":"temp","value":0.0},{"comp":"humid","value":1.0},{"comp":"co2","value":0.0},{"comp":"voc","value":0.0},{"comp":"pm25","value":0.0}]}]} +{ + "data": [ + { + "timestamp": "2020-04-10T16:41:57.771Z", + "score": 97.0, + "sensors": [ + { "comp": "temp", "value": 18.829999923706055 }, + { "comp": "humid", "value": 50.52000045776367 }, + { "comp": "co2", "value": 431.0 }, + { "comp": "voc", "value": 57.0 }, + { "comp": "pm25", "value": 2.0 } + ], + "indices": [ + { "comp": "temp", "value": 0.0 }, + { "comp": "humid", "value": 1.0 }, + { "comp": "co2", "value": 0.0 }, + { "comp": "voc", "value": 0.0 }, + { "comp": "pm25", "value": 0.0 } + ] + } + ] +} diff --git a/tests/components/awair/fixtures/awair.json b/tests/components/awair/fixtures/awair.json index 590c4a08642..1b64adddc95 100644 --- a/tests/components/awair/fixtures/awair.json +++ b/tests/components/awair/fixtures/awair.json @@ -1 +1,22 @@ -{"data":[{"timestamp":"2020-04-10T15:38:24.111Z","score":88.0,"sensors":[{"comp":"temp","value":21.770000457763672},{"comp":"humid","value":41.59000015258789},{"comp":"co2","value":654.0},{"comp":"voc","value":366.0},{"comp":"dust","value":14.300000190734863}],"indices":[{"comp":"temp","value":-1.0},{"comp":"humid","value":0.0},{"comp":"co2","value":0.0},{"comp":"voc","value":1.0},{"comp":"dust","value":1.0}]}]} +{ + "data": [ + { + "timestamp": "2020-04-10T15:38:24.111Z", + "score": 88.0, + "sensors": [ + { "comp": "temp", "value": 21.770000457763672 }, + { "comp": "humid", "value": 41.59000015258789 }, + { "comp": "co2", "value": 654.0 }, + { "comp": "voc", "value": 366.0 }, + { "comp": "dust", "value": 14.300000190734863 } + ], + "indices": [ + { "comp": "temp", "value": -1.0 }, + { "comp": "humid", "value": 0.0 }, + { "comp": "co2", "value": 0.0 }, + { "comp": "voc", "value": 1.0 }, + { "comp": "dust", "value": 1.0 } + ] + } + ] +} diff --git a/tests/components/awair/fixtures/devices.json b/tests/components/awair/fixtures/devices.json index 413d488c634..ef8b4925555 100644 --- a/tests/components/awair/fixtures/devices.json +++ b/tests/components/awair/fixtures/devices.json @@ -1 +1,18 @@ -{"devices":[{"name":"Living Room","macAddress":"70886B104941","latitude":0.0,"preference":"GENERAL","timezone":"","roomType":"LIVING_ROOM","deviceType":"awair","longitude":0.0,"spaceType":"HOME","deviceUUID":"awair_24947","deviceId":24947,"locationName":"Chicago, IL"}]} +{ + "devices": [ + { + "name": "Living Room", + "macAddress": "70886B104941", + "latitude": 0.0, + "preference": "GENERAL", + "timezone": "", + "roomType": "LIVING_ROOM", + "deviceType": "awair", + "longitude": 0.0, + "spaceType": "HOME", + "deviceUUID": "awair_24947", + "deviceId": 24947, + "locationName": "Chicago, IL" + } + ] +} diff --git a/tests/components/awair/fixtures/glow.json b/tests/components/awair/fixtures/glow.json index 2274905afc7..3d156433ffe 100644 --- a/tests/components/awair/fixtures/glow.json +++ b/tests/components/awair/fixtures/glow.json @@ -1 +1,20 @@ -{"data":[{"timestamp":"2020-04-10T16:46:15.486Z","score":93.0,"sensors":[{"comp":"temp","value":21.93000030517578},{"comp":"humid","value":42.31999969482422},{"comp":"co2","value":429.0},{"comp":"voc","value":288.0}],"indices":[{"comp":"temp","value":-1.0},{"comp":"humid","value":0.0},{"comp":"co2","value":0.0},{"comp":"voc","value":0.0}]}]} +{ + "data": [ + { + "timestamp": "2020-04-10T16:46:15.486Z", + "score": 93.0, + "sensors": [ + { "comp": "temp", "value": 21.93000030517578 }, + { "comp": "humid", "value": 42.31999969482422 }, + { "comp": "co2", "value": 429.0 }, + { "comp": "voc", "value": 288.0 } + ], + "indices": [ + { "comp": "temp", "value": -1.0 }, + { "comp": "humid", "value": 0.0 }, + { "comp": "co2", "value": 0.0 }, + { "comp": "voc", "value": 0.0 } + ] + } + ] +} diff --git a/tests/components/awair/fixtures/mint.json b/tests/components/awair/fixtures/mint.json index 2a7cefa8ad7..715cfa7a53c 100644 --- a/tests/components/awair/fixtures/mint.json +++ b/tests/components/awair/fixtures/mint.json @@ -1 +1,21 @@ -{"data":[{"timestamp":"2020-04-10T16:25:03.606Z","score":98.0,"sensors":[{"comp":"temp","value":20.639999389648438},{"comp":"humid","value":45.04999923706055},{"comp":"voc","value":269.0},{"comp":"pm25","value":1.0},{"comp":"lux","value":441.70001220703125}],"indices":[{"comp":"temp","value":0.0},{"comp":"humid","value":0.0},{"comp":"voc","value":0.0},{"comp":"pm25","value":0.0}]}]} +{ + "data": [ + { + "timestamp": "2020-04-10T16:25:03.606Z", + "score": 98.0, + "sensors": [ + { "comp": "temp", "value": 20.639999389648438 }, + { "comp": "humid", "value": 45.04999923706055 }, + { "comp": "voc", "value": 269.0 }, + { "comp": "pm25", "value": 1.0 }, + { "comp": "lux", "value": 441.70001220703125 } + ], + "indices": [ + { "comp": "temp", "value": 0.0 }, + { "comp": "humid", "value": 0.0 }, + { "comp": "voc", "value": 0.0 }, + { "comp": "pm25", "value": 0.0 } + ] + } + ] +} diff --git a/tests/components/awair/fixtures/no_devices.json b/tests/components/awair/fixtures/no_devices.json index f5732d79e1e..92a95438b30 100644 --- a/tests/components/awair/fixtures/no_devices.json +++ b/tests/components/awair/fixtures/no_devices.json @@ -1 +1 @@ -{"devices":[]} +{ "devices": [] } diff --git a/tests/components/awair/fixtures/omni.json b/tests/components/awair/fixtures/omni.json index 9a3dc3dd063..c678115a9ee 100644 --- a/tests/components/awair/fixtures/omni.json +++ b/tests/components/awair/fixtures/omni.json @@ -1 +1,24 @@ -{"data":[{"timestamp":"2020-04-10T16:18:10.298Z","score":99.0,"sensors":[{"comp":"temp","value":21.40999984741211},{"comp":"humid","value":42.7400016784668},{"comp":"co2","value":436.0},{"comp":"voc","value":171.0},{"comp":"pm25","value":0.0},{"comp":"lux","value":804.9000244140625},{"comp":"spl_a","value":47.0}],"indices":[{"comp":"temp","value":0.0},{"comp":"humid","value":0.0},{"comp":"co2","value":0.0},{"comp":"voc","value":0.0},{"comp":"pm25","value":0.0}]}]} +{ + "data": [ + { + "timestamp": "2020-04-10T16:18:10.298Z", + "score": 99.0, + "sensors": [ + { "comp": "temp", "value": 21.40999984741211 }, + { "comp": "humid", "value": 42.7400016784668 }, + { "comp": "co2", "value": 436.0 }, + { "comp": "voc", "value": 171.0 }, + { "comp": "pm25", "value": 0.0 }, + { "comp": "lux", "value": 804.9000244140625 }, + { "comp": "spl_a", "value": 47.0 } + ], + "indices": [ + { "comp": "temp", "value": 0.0 }, + { "comp": "humid", "value": 0.0 }, + { "comp": "co2", "value": 0.0 }, + { "comp": "voc", "value": 0.0 }, + { "comp": "pm25", "value": 0.0 } + ] + } + ] +} diff --git a/tests/components/awair/fixtures/user.json b/tests/components/awair/fixtures/user.json index f0fe94caf6d..3ee35414e91 100644 --- a/tests/components/awair/fixtures/user.json +++ b/tests/components/awair/fixtures/user.json @@ -1 +1,38 @@ - {"dobDay":8,"usages":[{"scope":"API_USAGE","usage":302},{"scope":"USER_DEVICE_LIST","usage":50},{"scope":"USER_INFO","usage":80}],"tier":"Large_developer","email":"foo@bar.com","dobYear":2020,"permissions":[{"scope":"USER_DEVICE_LIST","quota":2147483647},{"scope":"USER_INFO","quota":2147483647},{"scope":"FIFTEEN_MIN","quota":30000},{"scope":"FIVE_MIN","quota":30000},{"scope":"RAW","quota":30000},{"scope":"LATEST","quota":30000},{"scope":"PUT_PREFERENCE","quota":30000},{"scope":"PUT_DISPLAY_MODE","quota":30000},{"scope":"PUT_LED_MODE","quota":30000},{"scope":"PUT_KNOCKING_MODE","quota":30000},{"scope":"PUT_TIMEZONE","quota":30000},{"scope":"PUT_DEVICE_NAME","quota":30000},{"scope":"PUT_LOCATION","quota":30000},{"scope":"PUT_ROOM_TYPE","quota":30000},{"scope":"PUT_SPACE_TYPE","quota":30000},{"scope":"GET_DISPLAY_MODE","quota":30000},{"scope":"GET_LED_MODE","quota":30000},{"scope":"GET_KNOCKING_MODE","quota":30000},{"scope":"GET_POWER_STATUS","quota":30000},{"scope":"GET_TIMEZONE","quota":30000}],"dobMonth":4,"sex":"MALE","lastName":"Hayworth","firstName":"Andrew","id":"32406"} +{ + "dobDay": 8, + "usages": [ + { "scope": "API_USAGE", "usage": 302 }, + { "scope": "USER_DEVICE_LIST", "usage": 50 }, + { "scope": "USER_INFO", "usage": 80 } + ], + "tier": "Large_developer", + "email": "foo@bar.com", + "dobYear": 2020, + "permissions": [ + { "scope": "USER_DEVICE_LIST", "quota": 2147483647 }, + { "scope": "USER_INFO", "quota": 2147483647 }, + { "scope": "FIFTEEN_MIN", "quota": 30000 }, + { "scope": "FIVE_MIN", "quota": 30000 }, + { "scope": "RAW", "quota": 30000 }, + { "scope": "LATEST", "quota": 30000 }, + { "scope": "PUT_PREFERENCE", "quota": 30000 }, + { "scope": "PUT_DISPLAY_MODE", "quota": 30000 }, + { "scope": "PUT_LED_MODE", "quota": 30000 }, + { "scope": "PUT_KNOCKING_MODE", "quota": 30000 }, + { "scope": "PUT_TIMEZONE", "quota": 30000 }, + { "scope": "PUT_DEVICE_NAME", "quota": 30000 }, + { "scope": "PUT_LOCATION", "quota": 30000 }, + { "scope": "PUT_ROOM_TYPE", "quota": 30000 }, + { "scope": "PUT_SPACE_TYPE", "quota": 30000 }, + { "scope": "GET_DISPLAY_MODE", "quota": 30000 }, + { "scope": "GET_LED_MODE", "quota": 30000 }, + { "scope": "GET_KNOCKING_MODE", "quota": 30000 }, + { "scope": "GET_POWER_STATUS", "quota": 30000 }, + { "scope": "GET_TIMEZONE", "quota": 30000 } + ], + "dobMonth": 4, + "sex": "MALE", + "lastName": "Hayworth", + "firstName": "Andrew", + "id": "32406" +} diff --git a/tests/components/awair/test_config_flow.py b/tests/components/awair/test_config_flow.py index 84b92229161..8afe9a1c701 100644 --- a/tests/components/awair/test_config_flow.py +++ b/tests/components/awair/test_config_flow.py @@ -6,7 +6,7 @@ from python_awair.exceptions import AuthError, AwairError from homeassistant import data_entry_flow from homeassistant.components.awair.const import DOMAIN -from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_REAUTH, SOURCE_USER +from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER from homeassistant.const import CONF_ACCESS_TOKEN from .const import CONFIG, DEVICES_FIXTURE, NO_DEVICES_FIXTURE, UNIQUE_ID, USER_FIXTURE @@ -82,64 +82,6 @@ async def test_no_devices_error(hass): assert result["reason"] == "no_devices_found" -async def test_import(hass): - """Test config.yaml import.""" - - with patch( - "python_awair.AwairClient.query", side_effect=[USER_FIXTURE, DEVICES_FIXTURE] - ), patch( - "homeassistant.components.awair.sensor.async_setup_entry", - return_value=True, - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={CONF_ACCESS_TOKEN: CONFIG[CONF_ACCESS_TOKEN]}, - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == "foo@bar.com (32406)" - assert result["data"][CONF_ACCESS_TOKEN] == CONFIG[CONF_ACCESS_TOKEN] - assert result["result"].unique_id == UNIQUE_ID - - -async def test_import_aborts_on_api_error(hass): - """Test config.yaml imports on api error.""" - - with patch("python_awair.AwairClient.query", side_effect=AwairError()): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={CONF_ACCESS_TOKEN: CONFIG[CONF_ACCESS_TOKEN]}, - ) - - assert result["type"] == "abort" - assert result["reason"] == "unknown" - - -async def test_import_aborts_if_configured(hass): - """Test config import doesn't re-import unnecessarily.""" - - with patch( - "python_awair.AwairClient.query", side_effect=[USER_FIXTURE, DEVICES_FIXTURE] - ), patch( - "homeassistant.components.awair.sensor.async_setup_entry", - return_value=True, - ): - MockConfigEntry(domain=DOMAIN, unique_id=UNIQUE_ID, data=CONFIG).add_to_hass( - hass - ) - - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={CONF_ACCESS_TOKEN: CONFIG[CONF_ACCESS_TOKEN]}, - ) - - assert result["type"] == "abort" - assert result["reason"] == "already_setup" - - async def test_reauth(hass): """Test reauth flow.""" with patch( diff --git a/tests/components/awair/test_sensor.py b/tests/components/awair/test_sensor.py index 658ba802e8e..5f30be81d6f 100644 --- a/tests/components/awair/test_sensor.py +++ b/tests/components/awair/test_sensor.py @@ -28,6 +28,7 @@ from homeassistant.const import ( TEMP_CELSIUS, ) from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.entity_component import async_update_entity from .const import ( AWAIR_UUID, @@ -336,9 +337,7 @@ async def test_awair_unavailable(hass): ) with patch("python_awair.AwairClient.query", side_effect=OFFLINE_FIXTURE): - await hass.helpers.entity_component.async_update_entity( - "sensor.living_room_awair_score" - ) + await async_update_entity(hass, "sensor.living_room_awair_score") assert_expected_properties( hass, registry, diff --git a/tests/components/axis/test_config_flow.py b/tests/components/axis/test_config_flow.py index 112fa57fa64..f09e87020c3 100644 --- a/tests/components/axis/test_config_flow.py +++ b/tests/components/axis/test_config_flow.py @@ -9,7 +9,6 @@ from homeassistant.components import dhcp, ssdp, zeroconf from homeassistant.components.axis import config_flow from homeassistant.components.axis.const import ( CONF_EVENTS, - CONF_MODEL, CONF_STREAM_PROFILE, CONF_VIDEO_SOURCE, DEFAULT_STREAM_PROFILE, @@ -26,6 +25,7 @@ from homeassistant.config_entries import ( ) from homeassistant.const import ( CONF_HOST, + CONF_MODEL, CONF_NAME, CONF_PASSWORD, CONF_PORT, diff --git a/tests/components/axis/test_device.py b/tests/components/axis/test_device.py index cca62babbb5..4717e2915c1 100644 --- a/tests/components/axis/test_device.py +++ b/tests/components/axis/test_device.py @@ -9,15 +9,12 @@ import pytest import respx from homeassistant.components import axis, zeroconf -from homeassistant.components.axis.const import ( - CONF_EVENTS, - CONF_MODEL, - DOMAIN as AXIS_DOMAIN, -) +from homeassistant.components.axis.const import CONF_EVENTS, DOMAIN as AXIS_DOMAIN from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.config_entries import SOURCE_ZEROCONF from homeassistant.const import ( CONF_HOST, + CONF_MODEL, CONF_NAME, CONF_PASSWORD, CONF_PORT, diff --git a/tests/components/axis/test_init.py b/tests/components/axis/test_init.py index 5ca9fb1eb8d..94f8bf88a8d 100644 --- a/tests/components/axis/test_init.py +++ b/tests/components/axis/test_init.py @@ -2,12 +2,13 @@ from unittest.mock import AsyncMock, Mock, patch from homeassistant.components import axis -from homeassistant.components.axis.const import CONF_MODEL, DOMAIN as AXIS_DOMAIN +from homeassistant.components.axis.const import DOMAIN as AXIS_DOMAIN from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.const import ( CONF_DEVICE, CONF_HOST, CONF_MAC, + CONF_MODEL, CONF_NAME, CONF_PASSWORD, CONF_PORT, diff --git a/tests/components/backup/__init__.py b/tests/components/backup/__init__.py new file mode 100644 index 00000000000..0c5dcea461a --- /dev/null +++ b/tests/components/backup/__init__.py @@ -0,0 +1 @@ +"""Tests for the Backup integration.""" diff --git a/tests/components/backup/common.py b/tests/components/backup/common.py new file mode 100644 index 00000000000..824057b6500 --- /dev/null +++ b/tests/components/backup/common.py @@ -0,0 +1,29 @@ +"""Common helpers for the Backup integration tests.""" +from __future__ import annotations + +from pathlib import Path +from unittest.mock import patch + +from homeassistant.components.backup import DOMAIN +from homeassistant.components.backup.manager import Backup +from homeassistant.core import HomeAssistant +from homeassistant.helpers.typing import ConfigType +from homeassistant.setup import async_setup_component + +TEST_BACKUP = Backup( + slug="abc123", + name="Test", + date="1970-01-01T00:00:00.000Z", + path=Path("abc123.tar"), + size=0.0, +) + + +async def setup_backup_integration( + hass: HomeAssistant, + with_hassio: bool = False, + configuration: ConfigType | None = None, +) -> bool: + """Set up the Backup integration.""" + with patch("homeassistant.components.backup.is_hassio", return_value=with_hassio): + return await async_setup_component(hass, DOMAIN, configuration or {}) diff --git a/tests/components/backup/test_http.py b/tests/components/backup/test_http.py new file mode 100644 index 00000000000..708abec057b --- /dev/null +++ b/tests/components/backup/test_http.py @@ -0,0 +1,60 @@ +"""Tests for the Backup integration.""" +from collections.abc import Awaitable, Callable +from unittest.mock import patch + +from aiohttp import ClientSession, web + +from homeassistant.core import HomeAssistant + +from .common import TEST_BACKUP, setup_backup_integration + +from tests.common import MockUser + + +async def test_downloading_backup( + hass: HomeAssistant, + hass_client: Callable[..., Awaitable[ClientSession]], +) -> None: + """Test downloading a backup file.""" + await setup_backup_integration(hass) + + client = await hass_client() + + with patch( + "homeassistant.components.backup.http.BackupManager.get_backup", + return_value=TEST_BACKUP, + ), patch("pathlib.Path.exists", return_value=True), patch( + "homeassistant.components.backup.http.FileResponse", + return_value=web.Response(text=""), + ): + + resp = await client.get("/api/backup/download/abc123") + assert resp.status == 200 + + +async def test_downloading_backup_not_found( + hass: HomeAssistant, + hass_client: Callable[..., Awaitable[ClientSession]], +) -> None: + """Test downloading a backup file that does not exist.""" + await setup_backup_integration(hass) + + client = await hass_client() + + resp = await client.get("/api/backup/download/abc123") + assert resp.status == 404 + + +async def test_non_admin( + hass: HomeAssistant, + hass_client: Callable[..., Awaitable[ClientSession]], + hass_admin_user: MockUser, +) -> None: + """Test downloading a backup file that does not exist.""" + hass_admin_user.groups = [] + await setup_backup_integration(hass) + + client = await hass_client() + + resp = await client.get("/api/backup/download/abc123") + assert resp.status == 401 diff --git a/tests/components/backup/test_init.py b/tests/components/backup/test_init.py new file mode 100644 index 00000000000..a9e2fe20c6b --- /dev/null +++ b/tests/components/backup/test_init.py @@ -0,0 +1,18 @@ +"""Tests for the Backup integration.""" +import pytest + +from homeassistant.core import HomeAssistant + +from .common import setup_backup_integration + + +async def test_setup_with_hassio( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test the setup of the integration with hassio enabled.""" + assert not await setup_backup_integration(hass=hass, with_hassio=True) + assert ( + "The backup integration is not supported on this installation method, please remove it from your configuration" + in caplog.text + ) diff --git a/tests/components/backup/test_manager.py b/tests/components/backup/test_manager.py new file mode 100644 index 00000000000..7edd64e0cb0 --- /dev/null +++ b/tests/components/backup/test_manager.py @@ -0,0 +1,269 @@ +"""Tests for the Backup integration.""" +from __future__ import annotations + +from pathlib import Path +from unittest.mock import AsyncMock, MagicMock, Mock, patch + +import pytest + +from homeassistant.components.backup import BackupManager +from homeassistant.components.backup.manager import BackupPlatformProtocol +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.setup import async_setup_component + +from .common import TEST_BACKUP + +from tests.common import MockPlatform, mock_platform + + +async def _mock_backup_generation(manager: BackupManager): + """Mock backup generator.""" + + def _mock_iterdir(path: Path) -> list[Path]: + if not path.name.endswith("testing_config"): + return [] + return [ + Path("test.txt"), + Path(".DS_Store"), + Path(".storage"), + ] + + with patch("tarfile.open", MagicMock()) as mocked_tarfile, patch( + "pathlib.Path.iterdir", _mock_iterdir + ), patch("pathlib.Path.stat", MagicMock(st_size=123)), patch( + "pathlib.Path.is_file", lambda x: x.name != ".storage" + ), patch( + "pathlib.Path.is_dir", + lambda x: x.name == ".storage", + ), patch( + "pathlib.Path.exists", + lambda x: x != manager.backup_dir, + ), patch( + "pathlib.Path.is_symlink", + lambda _: False, + ), patch( + "pathlib.Path.mkdir", + MagicMock(), + ), patch( + "homeassistant.components.backup.manager.json_util.save_json" + ) as mocked_json_util, patch( + "homeassistant.components.backup.manager.HAVERSION", + "2025.1.0", + ): + await manager.generate_backup() + + assert mocked_json_util.call_count == 1 + assert mocked_json_util.call_args[0][1]["homeassistant"] == { + "version": "2025.1.0" + } + + assert ( + manager.backup_dir.as_posix() + in mocked_tarfile.call_args_list[0].kwargs["name"] + ) + + +async def _setup_mock_domain( + hass: HomeAssistant, + platform: BackupPlatformProtocol | None = None, +) -> None: + """Set up a mock domain.""" + mock_platform(hass, "some_domain.backup", platform or MockPlatform()) + assert await async_setup_component(hass, "some_domain", {}) + + +async def test_constructor(hass: HomeAssistant) -> None: + """Test BackupManager constructor.""" + manager = BackupManager(hass) + assert manager.backup_dir.as_posix() == hass.config.path("backups") + + +async def test_load_backups(hass: HomeAssistant) -> None: + """Test loading backups.""" + manager = BackupManager(hass) + with patch("pathlib.Path.glob", return_value=[TEST_BACKUP.path]), patch( + "tarfile.open", return_value=MagicMock() + ), patch( + "json.loads", + return_value={ + "slug": TEST_BACKUP.slug, + "name": TEST_BACKUP.name, + "date": TEST_BACKUP.date, + }, + ), patch( + "pathlib.Path.stat", return_value=MagicMock(st_size=TEST_BACKUP.size) + ): + await manager.load_backups() + backups = await manager.get_backups() + assert backups == {TEST_BACKUP.slug: TEST_BACKUP} + + +async def test_load_backups_with_exception( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test loading backups with exception.""" + manager = BackupManager(hass) + with patch("pathlib.Path.glob", return_value=[TEST_BACKUP.path]), patch( + "tarfile.open", side_effect=OSError("Test ecxeption") + ): + await manager.load_backups() + backups = await manager.get_backups() + assert f"Unable to read backup {TEST_BACKUP.path}: Test ecxeption" in caplog.text + assert backups == {} + + +async def test_removing_backup( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test removing backup.""" + manager = BackupManager(hass) + manager.backups = {TEST_BACKUP.slug: TEST_BACKUP} + manager.loaded_backups = True + + with patch("pathlib.Path.exists", return_value=True): + await manager.remove_backup(TEST_BACKUP.slug) + assert "Removed backup located at" in caplog.text + + +async def test_removing_non_existing_backup( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test removing not existing backup.""" + manager = BackupManager(hass) + + await manager.remove_backup("non_existing") + assert "Removed backup located at" not in caplog.text + + +async def test_getting_backup_that_does_not_exist( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, +): + """Test getting backup that does not exist.""" + manager = BackupManager(hass) + manager.backups = {TEST_BACKUP.slug: TEST_BACKUP} + manager.loaded_backups = True + + with patch("pathlib.Path.exists", return_value=False): + backup = await manager.get_backup(TEST_BACKUP.slug) + assert backup is None + + assert ( + f"Removing tracked backup ({TEST_BACKUP.slug}) that " + f"does not exists on the expected path {TEST_BACKUP.path}" in caplog.text + ) + + +async def test_generate_backup_when_backing_up(hass: HomeAssistant) -> None: + """Test generate backup.""" + manager = BackupManager(hass) + manager.backing_up = True + with pytest.raises(HomeAssistantError, match="Backup already in progress"): + await manager.generate_backup() + + +async def test_generate_backup( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test generate backup.""" + manager = BackupManager(hass) + manager.loaded_backups = True + + await _mock_backup_generation(manager) + + assert "Generated new backup with slug " in caplog.text + assert "Creating backup directory" in caplog.text + assert "Loaded 0 platforms" in caplog.text + + +async def test_loading_platforms( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test loading backup platforms.""" + manager = BackupManager(hass) + + assert not manager.loaded_platforms + assert not manager.platforms + + await _setup_mock_domain( + hass, + Mock( + async_pre_backup=AsyncMock(), + async_post_backup=AsyncMock(), + ), + ) + await manager.load_platforms() + + assert manager.loaded_platforms + assert len(manager.platforms) == 1 + + assert "Loaded 1 platforms" in caplog.text + + +async def test_not_loading_bad_platforms( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test loading backup platforms.""" + manager = BackupManager(hass) + + assert not manager.loaded_platforms + assert not manager.platforms + + await _setup_mock_domain(hass) + await manager.load_platforms() + + assert manager.loaded_platforms + assert len(manager.platforms) == 0 + + assert "Loaded 0 platforms" in caplog.text + assert ( + "some_domain does not implement required functions for the backup platform" + in caplog.text + ) + + +async def test_exception_plaform_pre(hass: HomeAssistant) -> None: + """Test exception in pre step.""" + manager = BackupManager(hass) + manager.loaded_backups = True + + async def _mock_step(hass: HomeAssistant) -> None: + raise HomeAssistantError("Test exception") + + await _setup_mock_domain( + hass, + Mock( + async_pre_backup=_mock_step, + async_post_backup=AsyncMock(), + ), + ) + + with pytest.raises(HomeAssistantError): + await _mock_backup_generation(manager) + + +async def test_exception_plaform_post(hass: HomeAssistant) -> None: + """Test exception in post step.""" + manager = BackupManager(hass) + manager.loaded_backups = True + + async def _mock_step(hass: HomeAssistant) -> None: + raise HomeAssistantError("Test exception") + + await _setup_mock_domain( + hass, + Mock( + async_pre_backup=AsyncMock(), + async_post_backup=_mock_step, + ), + ) + + with pytest.raises(HomeAssistantError): + await _mock_backup_generation(manager) diff --git a/tests/components/backup/test_websocket.py b/tests/components/backup/test_websocket.py new file mode 100644 index 00000000000..4179eb026c5 --- /dev/null +++ b/tests/components/backup/test_websocket.py @@ -0,0 +1,76 @@ +"""Tests for the Backup integration.""" +from collections.abc import Awaitable, Callable +from unittest.mock import patch + +from aiohttp import ClientWebSocketResponse +import pytest + +from homeassistant.core import HomeAssistant + +from .common import TEST_BACKUP, setup_backup_integration + + +async def test_info( + hass: HomeAssistant, + hass_ws_client: Callable[[HomeAssistant], Awaitable[ClientWebSocketResponse]], +) -> None: + """Test getting backup info.""" + await setup_backup_integration(hass) + + client = await hass_ws_client(hass) + await hass.async_block_till_done() + + with patch( + "homeassistant.components.backup.websocket.BackupManager.get_backups", + return_value={TEST_BACKUP.slug: TEST_BACKUP}, + ): + + await client.send_json({"id": 1, "type": "backup/info"}) + msg = await client.receive_json() + + assert msg["id"] == 1 + assert msg["success"] + assert msg["result"] == {"backing_up": False, "backups": [TEST_BACKUP.as_dict()]} + + +async def test_remove( + hass: HomeAssistant, + hass_ws_client: Callable[[HomeAssistant], Awaitable[ClientWebSocketResponse]], + caplog: pytest.LogCaptureFixture, +) -> None: + """Test removing a backup file.""" + await setup_backup_integration(hass) + + client = await hass_ws_client(hass) + await hass.async_block_till_done() + + with patch( + "homeassistant.components.backup.websocket.BackupManager.remove_backup", + ): + await client.send_json({"id": 1, "type": "backup/remove", "slug": "abc123"}) + msg = await client.receive_json() + + assert msg["id"] == 1 + assert msg["success"] + + +async def test_generate( + hass: HomeAssistant, + hass_ws_client: Callable[[HomeAssistant], Awaitable[ClientWebSocketResponse]], +) -> None: + """Test removing a backup file.""" + await setup_backup_integration(hass) + + client = await hass_ws_client(hass) + await hass.async_block_till_done() + + with patch( + "homeassistant.components.backup.websocket.BackupManager.generate_backup", + return_value=TEST_BACKUP, + ): + await client.send_json({"id": 1, "type": "backup/generate"}) + msg = await client.receive_json() + + assert msg["id"] == 1 + assert msg["success"] + assert msg["result"] == TEST_BACKUP.as_dict() diff --git a/tests/components/bayesian/fixtures/configuration.yaml b/tests/components/bayesian/fixtures/configuration.yaml index 56a490d4aec..099350eaa78 100644 --- a/tests/components/bayesian/fixtures/configuration.yaml +++ b/tests/components/bayesian/fixtures/configuration.yaml @@ -2,9 +2,9 @@ binary_sensor: - platform: bayesian prior: 0.1 observations: - - entity_id: 'switch.kitchen_lights' + - entity_id: "switch.kitchen_lights" prob_given_true: 0.6 prob_given_false: 0.2 - platform: 'state' - to_state: 'on' + platform: "state" + to_state: "on" name: test2 diff --git a/tests/components/blueprint/fixtures/community_post.json b/tests/components/blueprint/fixtures/community_post.json index 121d53ad94e..725d50bbdc7 100644 --- a/tests/components/blueprint/fixtures/community_post.json +++ b/tests/components/blueprint/fixtures/community_post.json @@ -97,16 +97,9 @@ "accepted_answer": false } ], - "stream": [ - 1216212 - ] + "stream": [1216212] }, - "timeline_lookup": [ - [ - 1, - 0 - ] - ], + "timeline_lookup": [[1, 0]], "suggested_topics": [ { "id": 168593, @@ -315,9 +308,7 @@ "bookmarked": false, "liked": false, "thumbnails": null, - "tags": [ - "alexa" - ], + "tags": ["alexa"], "like_count": 1092, "views": 179580, "category_id": 47, @@ -549,10 +540,7 @@ ] } ], - "tags": [ - "blueprint", - "zha" - ], + "tags": ["blueprint", "zha"], "id": 253804, "title": "ZHA - IKEA five button remote for lights", "fancy_title": "ZHA - IKEA five button remote for lights", diff --git a/tests/components/blueprint/fixtures/github_gist.json b/tests/components/blueprint/fixtures/github_gist.json index 208e8b54a71..f808d4e0ce3 100644 --- a/tests/components/blueprint/fixtures/github_gist.json +++ b/tests/components/blueprint/fixtures/github_gist.json @@ -45,9 +45,7 @@ "type": "User", "site_admin": false }, - "forks": [ - - ], + "forks": [], "history": [ { "user": { diff --git a/tests/components/blueprint/test_importer.py b/tests/components/blueprint/test_importer.py index 382363aa560..0e1e66405e6 100644 --- a/tests/components/blueprint/test_importer.py +++ b/tests/components/blueprint/test_importer.py @@ -25,6 +25,7 @@ COMMUNITY_POST_INPUTS = { "integration": "zha", "manufacturer": "IKEA of Sweden", "model": "TRADFRI remote control", + "multiple": False, } }, }, @@ -218,10 +219,17 @@ async def test_fetch_blueprint_from_github_gist_url(hass, aioclient_mock): "motion_entity": { "name": "Motion Sensor", "selector": { - "entity": {"domain": "binary_sensor", "device_class": "motion"} + "entity": { + "domain": "binary_sensor", + "device_class": "motion", + "multiple": False, + } }, }, - "light_entity": {"name": "Light", "selector": {"entity": {"domain": "light"}}}, + "light_entity": { + "name": "Light", + "selector": {"entity": {"domain": "light", "multiple": False}}, + }, } assert imported_blueprint.suggested_filename == "balloob/motion_light" assert imported_blueprint.blueprint.metadata["source_url"] == url diff --git a/tests/components/bmw_connected_drive/test_config_flow.py b/tests/components/bmw_connected_drive/test_config_flow.py index b0bc3ce292c..644da56a91d 100644 --- a/tests/components/bmw_connected_drive/test_config_flow.py +++ b/tests/components/bmw_connected_drive/test_config_flow.py @@ -68,8 +68,6 @@ async def test_full_user_flow_implementation(hass): "bimmer_connected.account.ConnectedDriveAccount._get_vehicles", return_value=[], ), patch( - "homeassistant.components.bmw_connected_drive.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.bmw_connected_drive.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -82,32 +80,6 @@ async def test_full_user_flow_implementation(hass): assert result2["title"] == FIXTURE_COMPLETE_ENTRY[CONF_USERNAME] assert result2["data"] == FIXTURE_COMPLETE_ENTRY - assert len(mock_setup.mock_calls) == 1 - assert len(mock_setup_entry.mock_calls) == 1 - - -async def test_full_config_flow_implementation(hass): - """Test registering an integration and finishing flow works.""" - with patch( - "bimmer_connected.account.ConnectedDriveAccount._get_vehicles", - return_value=[], - ), patch( - "homeassistant.components.bmw_connected_drive.async_setup", return_value=True - ) as mock_setup, patch( - "homeassistant.components.bmw_connected_drive.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data=FIXTURE_USER_INPUT, - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == FIXTURE_IMPORT_ENTRY[CONF_USERNAME] - assert result["data"] == FIXTURE_IMPORT_ENTRY - - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -117,8 +89,6 @@ async def test_options_flow_implementation(hass): "bimmer_connected.account.ConnectedDriveAccount._get_vehicles", return_value=[], ), patch( - "homeassistant.components.bmw_connected_drive.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.bmw_connected_drive.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -143,5 +113,4 @@ async def test_options_flow_implementation(hass): CONF_READ_ONLY: False, } - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/bond/common.py b/tests/components/bond/common.py index 0400b466e34..9c53c0afb8b 100644 --- a/tests/components/bond/common.py +++ b/tests/components/bond/common.py @@ -8,6 +8,7 @@ from typing import Any from unittest.mock import MagicMock, patch from aiohttp.client_exceptions import ClientResponseError +from bond_api import DeviceType from homeassistant import core from homeassistant.components.bond.const import DOMAIN as BOND_DOMAIN @@ -18,6 +19,15 @@ from homeassistant.util import utcnow from tests.common import MockConfigEntry, async_fire_time_changed +def ceiling_fan_with_breeze(name: str): + """Create a ceiling fan with given name with breeze support.""" + return { + "name": name, + "type": DeviceType.CEILING_FAN, + "actions": ["SetSpeed", "SetDirection", "BreezeOn"], + } + + def patch_setup_entry(domain: str, *, enabled: bool = True): """Patch async_setup_entry for specified domain.""" if not enabled: diff --git a/tests/components/bond/test_diagnostics.py b/tests/components/bond/test_diagnostics.py new file mode 100644 index 00000000000..88d33ff2cc0 --- /dev/null +++ b/tests/components/bond/test_diagnostics.py @@ -0,0 +1,43 @@ +"""Test bond diagnostics.""" + +from homeassistant.components.fan import DOMAIN as FAN_DOMAIN + +from .common import ceiling_fan_with_breeze, setup_platform + +from tests.components.diagnostics import get_diagnostics_for_config_entry + + +async def test_diagnostics(hass, hass_client): + """Test generating diagnostics for a config entry.""" + + entry = await setup_platform( + hass, + FAN_DOMAIN, + ceiling_fan_with_breeze("name-1"), + bond_device_id="test-device-id", + props={"max_speed": 6}, + ) + diag = await get_diagnostics_for_config_entry(hass, hass_client, entry) + mock_device = diag["devices"][0] + mock_device["attrs"]["actions"] = set(mock_device["attrs"]["actions"]) + mock_device["supported_actions"] = set(mock_device["supported_actions"]) + + assert diag == { + "devices": [ + { + "attrs": { + "actions": {"SetSpeed", "SetDirection", "BreezeOn"}, + "name": "name-1", + "type": "CF", + }, + "device_id": "test-device-id", + "props": {"max_speed": 6}, + "supported_actions": {"BreezeOn", "SetSpeed", "SetDirection"}, + } + ], + "entry": { + "data": {"access_token": "**REDACTED**", "host": "some host"}, + "title": "Mock Title", + }, + "hub": {"version": {"bondid": "test-bond-id"}}, + } diff --git a/tests/components/bond/test_fan.py b/tests/components/bond/test_fan.py index 4168cbd35d2..061e94595bf 100644 --- a/tests/components/bond/test_fan.py +++ b/tests/components/bond/test_fan.py @@ -16,17 +16,15 @@ from homeassistant.components.bond.const import ( from homeassistant.components.bond.fan import PRESET_MODE_BREEZE from homeassistant.components.fan import ( ATTR_DIRECTION, + ATTR_PERCENTAGE, ATTR_PRESET_MODE, ATTR_PRESET_MODES, - ATTR_SPEED, - ATTR_SPEED_LIST, DIRECTION_FORWARD, DIRECTION_REVERSE, DOMAIN as FAN_DOMAIN, SERVICE_SET_DIRECTION, + SERVICE_SET_PERCENTAGE, SERVICE_SET_PRESET_MODE, - SERVICE_SET_SPEED, - SPEED_OFF, ) from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON from homeassistant.exceptions import HomeAssistantError @@ -66,7 +64,6 @@ def ceiling_fan_with_breeze(name: str): async def turn_fan_on( hass: core.HomeAssistant, fan_id: str, - speed: str | None = None, percentage: int | None = None, preset_mode: str | None = None, ) -> None: @@ -74,9 +71,7 @@ async def turn_fan_on( service_data = {ATTR_ENTITY_ID: fan_id} if preset_mode: service_data[fan.ATTR_PRESET_MODE] = preset_mode - if speed: - service_data[fan.ATTR_SPEED] = speed - if percentage: + if percentage is not None: service_data[fan.ATTR_PERCENTAGE] = percentage await hass.services.async_call( FAN_DOMAIN, @@ -116,35 +111,27 @@ async def test_non_standard_speed_list(hass: core.HomeAssistant): props={"max_speed": 6}, ) - actual_speeds = hass.states.get("fan.name_1").attributes[ATTR_SPEED_LIST] - assert actual_speeds == [ - fan.SPEED_OFF, - fan.SPEED_LOW, - fan.SPEED_MEDIUM, - fan.SPEED_HIGH, - ] - with patch_bond_device_state(): with patch_bond_action() as mock_set_speed_low: - await turn_fan_on(hass, "fan.name_1", fan.SPEED_LOW) + await turn_fan_on(hass, "fan.name_1", percentage=100 / 6 * 2) mock_set_speed_low.assert_called_once_with( "test-device-id", Action.set_speed(2) ) with patch_bond_action() as mock_set_speed_medium: - await turn_fan_on(hass, "fan.name_1", fan.SPEED_MEDIUM) + await turn_fan_on(hass, "fan.name_1", percentage=100 / 6 * 4) mock_set_speed_medium.assert_called_once_with( "test-device-id", Action.set_speed(4) ) with patch_bond_action() as mock_set_speed_high: - await turn_fan_on(hass, "fan.name_1", fan.SPEED_HIGH) + await turn_fan_on(hass, "fan.name_1", percentage=100) mock_set_speed_high.assert_called_once_with( "test-device-id", Action.set_speed(6) ) -async def test_fan_speed_with_no_max_seed(hass: core.HomeAssistant): +async def test_fan_speed_with_no_max_speed(hass: core.HomeAssistant): """Tests that fans without max speed (increase/decrease controls) map speed to HA standard.""" await setup_platform( hass, @@ -155,7 +142,7 @@ async def test_fan_speed_with_no_max_seed(hass: core.HomeAssistant): state={"power": 1, "speed": 14}, ) - assert hass.states.get("fan.name_1").attributes["speed"] == fan.SPEED_HIGH + assert hass.states.get("fan.name_1").attributes["percentage"] == 100 async def test_turn_on_fan_with_speed(hass: core.HomeAssistant): @@ -165,7 +152,7 @@ async def test_turn_on_fan_with_speed(hass: core.HomeAssistant): ) with patch_bond_action() as mock_set_speed, patch_bond_device_state(): - await turn_fan_on(hass, "fan.name_1", fan.SPEED_LOW) + await turn_fan_on(hass, "fan.name_1", percentage=1) mock_set_speed.assert_called_with("test-device-id", Action.set_speed(1)) @@ -264,9 +251,7 @@ async def test_turn_on_fan_preset_mode_not_supported(hass: core.HomeAssistant): props={"max_speed": 6}, ) - with patch_bond_action(), patch_bond_device_state(), pytest.raises( - fan.NotValidPresetModeError - ): + with patch_bond_action(), patch_bond_device_state(), pytest.raises(ValueError): await turn_fan_on(hass, "fan.name_1", preset_mode=PRESET_MODE_BREEZE) with patch_bond_action(), patch_bond_device_state(), pytest.raises(ValueError): @@ -296,7 +281,7 @@ async def test_turn_on_fan_with_off_with_breeze(hass: core.HomeAssistant): ) with patch_bond_action() as mock_actions, patch_bond_device_state(): - await turn_fan_on(hass, "fan.name_1", fan.SPEED_OFF) + await turn_fan_on(hass, "fan.name_1", percentage=0) assert mock_actions.mock_calls == [ call("test-device-id", Action(Action.BREEZE_OFF)), @@ -316,14 +301,14 @@ async def test_turn_on_fan_without_speed(hass: core.HomeAssistant): mock_turn_on.assert_called_with("test-device-id", Action.turn_on()) -async def test_turn_on_fan_with_off_speed(hass: core.HomeAssistant): +async def test_turn_on_fan_with_off_percentage(hass: core.HomeAssistant): """Tests that turn off command delegates to turn off API.""" await setup_platform( hass, FAN_DOMAIN, ceiling_fan("name-1"), bond_device_id="test-device-id" ) with patch_bond_action() as mock_turn_off, patch_bond_device_state(): - await turn_fan_on(hass, "fan.name_1", fan.SPEED_OFF) + await turn_fan_on(hass, "fan.name_1", percentage=0) mock_turn_off.assert_called_with("test-device-id", Action.turn_off()) @@ -337,8 +322,8 @@ async def test_set_speed_off(hass: core.HomeAssistant): with patch_bond_action() as mock_turn_off, patch_bond_device_state(): await hass.services.async_call( FAN_DOMAIN, - SERVICE_SET_SPEED, - service_data={ATTR_ENTITY_ID: "fan.name_1", ATTR_SPEED: SPEED_OFF}, + SERVICE_SET_PERCENTAGE, + service_data={ATTR_ENTITY_ID: "fan.name_1", ATTR_PERCENTAGE: 0}, blocking=True, ) await hass.async_block_till_done() @@ -374,7 +359,7 @@ async def test_set_speed_belief_speed_zero(hass: core.HomeAssistant): await hass.services.async_call( BOND_DOMAIN, SERVICE_SET_FAN_SPEED_TRACKED_STATE, - {ATTR_ENTITY_ID: "fan.name_1", ATTR_SPEED: 0}, + {ATTR_ENTITY_ID: "fan.name_1", "speed": 0}, blocking=True, ) await hass.async_block_till_done() @@ -396,7 +381,7 @@ async def test_set_speed_belief_speed_api_error(hass: core.HomeAssistant): await hass.services.async_call( BOND_DOMAIN, SERVICE_SET_FAN_SPEED_TRACKED_STATE, - {ATTR_ENTITY_ID: "fan.name_1", ATTR_SPEED: 100}, + {ATTR_ENTITY_ID: "fan.name_1", "speed": 100}, blocking=True, ) await hass.async_block_till_done() @@ -412,7 +397,7 @@ async def test_set_speed_belief_speed_100(hass: core.HomeAssistant): await hass.services.async_call( BOND_DOMAIN, SERVICE_SET_FAN_SPEED_TRACKED_STATE, - {ATTR_ENTITY_ID: "fan.name_1", ATTR_SPEED: 100}, + {ATTR_ENTITY_ID: "fan.name_1", "speed": 100}, blocking=True, ) await hass.async_block_till_done() diff --git a/tests/components/broadlink/test_sensors.py b/tests/components/broadlink/test_sensors.py index 28caa212278..b5a49fdae15 100644 --- a/tests/components/broadlink/test_sensors.py +++ b/tests/components/broadlink/test_sensors.py @@ -4,6 +4,7 @@ from datetime import timedelta from homeassistant.components.broadlink.const import DOMAIN from homeassistant.components.broadlink.updater import BroadlinkSP4UpdateManager from homeassistant.const import Platform +from homeassistant.helpers.entity_component import async_update_entity from homeassistant.helpers.entity_registry import async_entries_for_device from homeassistant.util import dt @@ -81,9 +82,7 @@ async def test_a1_sensor_update(hass): "light": 3, "noise": 2, } - await hass.helpers.entity_component.async_update_entity( - next(iter(sensors)).entity_id - ) + await async_update_entity(hass, next(iter(sensors)).entity_id) assert mock_setup.api.check_sensors_raw.call_count == 2 sensors_and_states = { @@ -144,9 +143,7 @@ async def test_rm_pro_sensor_update(hass): assert len(sensors) == 1 mock_setup.api.check_sensors.return_value = {"temperature": 25.8} - await hass.helpers.entity_component.async_update_entity( - next(iter(sensors)).entity_id - ) + await async_update_entity(hass, next(iter(sensors)).entity_id) assert mock_setup.api.check_sensors.call_count == 2 sensors_and_states = { @@ -178,9 +175,7 @@ async def test_rm_pro_filter_crazy_temperature(hass): assert len(sensors) == 1 mock_setup.api.check_sensors.return_value = {"temperature": -7} - await hass.helpers.entity_component.async_update_entity( - next(iter(sensors)).entity_id - ) + await async_update_entity(hass, next(iter(sensors)).entity_id) assert mock_setup.api.check_sensors.call_count == 2 sensors_and_states = { @@ -258,9 +253,7 @@ async def test_rm4_pro_hts2_sensor_update(hass): assert len(sensors) == 2 mock_setup.api.check_sensors.return_value = {"temperature": 16.8, "humidity": 34.0} - await hass.helpers.entity_component.async_update_entity( - next(iter(sensors)).entity_id - ) + await async_update_entity(hass, next(iter(sensors)).entity_id) assert mock_setup.api.check_sensors.call_count == 2 sensors_and_states = { diff --git a/tests/components/brother/fixtures/diagnostics_data.json b/tests/components/brother/fixtures/diagnostics_data.json index 1a8458e1caf..0199acdd722 100644 --- a/tests/components/brother/fixtures/diagnostics_data.json +++ b/tests/components/brother/fixtures/diagnostics_data.json @@ -1,45 +1,45 @@ { - "b/w_counter": 709, - "belt_unit_remaining_life": 97, - "belt_unit_remaining_pages": 48436, - "black_drum_counter": 1611, - "black_drum_remaining_life": 92, - "black_drum_remaining_pages": 16389, - "black_toner": 80, - "black_toner_remaining": 75, - "black_toner_status": 1, - "color_counter": 902, - "cyan_drum_counter": 1611, - "cyan_drum_remaining_life": 92, - "cyan_drum_remaining_pages": 16389, - "cyan_toner": 10, - "cyan_toner_remaining": 10, - "cyan_toner_status": 1, - "drum_counter": 986, - "drum_remaining_life": 92, - "drum_remaining_pages": 11014, - "drum_status": 1, - "duplex_unit_pages_counter": 538, - "firmware": "1.17", - "fuser_remaining_life": 97, - "laser_unit_remaining_pages": 48389, - "magenta_drum_counter": 1611, - "magenta_drum_remaining_life": 92, - "magenta_drum_remaining_pages": 16389, - "magenta_toner": 10, - "magenta_toner_remaining": 8, - "magenta_toner_status": 2, - "model": "HL-L2340DW", - "page_counter": 986, - "pf_kit_1_remaining_life": 98, - "pf_kit_1_remaining_pages": 48741, - "serial": "0123456789", - "status": "waiting", - "uptime": "2019-09-24T12:14:56+00:00", - "yellow_drum_counter": 1611, - "yellow_drum_remaining_life": 92, - "yellow_drum_remaining_pages": 16389, - "yellow_toner": 10, - "yellow_toner_remaining": 2, - "yellow_toner_status": 2 + "b/w_counter": 709, + "belt_unit_remaining_life": 97, + "belt_unit_remaining_pages": 48436, + "black_drum_counter": 1611, + "black_drum_remaining_life": 92, + "black_drum_remaining_pages": 16389, + "black_toner": 80, + "black_toner_remaining": 75, + "black_toner_status": 1, + "color_counter": 902, + "cyan_drum_counter": 1611, + "cyan_drum_remaining_life": 92, + "cyan_drum_remaining_pages": 16389, + "cyan_toner": 10, + "cyan_toner_remaining": 10, + "cyan_toner_status": 1, + "drum_counter": 986, + "drum_remaining_life": 92, + "drum_remaining_pages": 11014, + "drum_status": 1, + "duplex_unit_pages_counter": 538, + "firmware": "1.17", + "fuser_remaining_life": 97, + "laser_unit_remaining_pages": 48389, + "magenta_drum_counter": 1611, + "magenta_drum_remaining_life": 92, + "magenta_drum_remaining_pages": 16389, + "magenta_toner": 10, + "magenta_toner_remaining": 8, + "magenta_toner_status": 2, + "model": "HL-L2340DW", + "page_counter": 986, + "pf_kit_1_remaining_life": 98, + "pf_kit_1_remaining_pages": 48741, + "serial": "0123456789", + "status": "waiting", + "uptime": "2019-09-24T12:14:56+00:00", + "yellow_drum_counter": 1611, + "yellow_drum_remaining_life": 92, + "yellow_drum_remaining_pages": 16389, + "yellow_toner": 10, + "yellow_toner_remaining": 2, + "yellow_toner_status": 2 } diff --git a/tests/components/brother/fixtures/printer_data.json b/tests/components/brother/fixtures/printer_data.json index a70d87673d0..c6fd5042330 100644 --- a/tests/components/brother/fixtures/printer_data.json +++ b/tests/components/brother/fixtures/printer_data.json @@ -1,76 +1,76 @@ { - "1.3.6.1.2.1.1.3.0": "413613515", - "1.3.6.1.2.1.43.10.2.1.4.1.1": "986", - "1.3.6.1.4.1.2435.2.3.9.4.2.1.5.5.10.0": [ - "000104000003da", - "010104000002c5", - "02010400000386", - "0601040000021a", - "0701040000012d", - "080104000000ed" - ], - "1.3.6.1.4.1.2435.2.3.9.4.2.1.5.5.17.0": "1.17", - "1.3.6.1.4.1.2435.2.3.9.4.2.1.5.5.8.0": [ - "110104000003da", - "31010400000001", - "32010400000001", - "33010400000002", - "34010400000002", - "35010400000001", - "410104000023f0", - "54010400000001", - "55010400000001", - "63010400000001", - "68010400000001", - "690104000025e4", - "6a0104000025e4", - "6d010400002648", - "6f010400001d4c", - "700104000003e8", - "71010400000320", - "720104000000c8", - "7301040000064b", - "7401040000064b", - "7501040000064b", - "76010400000001", - "77010400000001", - "78010400000001", - "790104000023f0", - "7a0104000023f0", - "7b0104000023f0", - "7e01040000064b", - "800104000023f0", - "81010400000050", - "8201040000000a", - "8301040000000a", - "8401040000000a", - "8601040000000a" - ], - "1.3.6.1.4.1.2435.2.3.9.1.1.7.0": "MFG:Brother;CMD:PJL,HBP,URF;MDL:HL-L2340DW series;CLS:PRINTER;CID:Brother Laser Type1;URF:W8,CP1,IS4-1,MT1-3-4-5-8,OB10,PQ4,RS300-600,V1.3,DM1;", - "1.3.6.1.4.1.2435.2.3.9.4.2.1.5.5.11.0": [ - "7301040000bd05", - "7701040000be65", - "82010400002b06", - "8801040000bd34", - "a4010400004005", - "a5010400004005", - "a6010400004005", - "a7010400004005" - ], - "1.3.6.1.4.1.2435.2.3.9.4.2.1.5.5.21.0": [ - "00002302000025", - "00020016010200", - "00210200022202", - "020000a1040000" - ], - "1.3.6.1.4.1.2435.2.3.9.4.2.1.5.5.20.0": [ - "00a40100a50100", - "0100a301008801", - "01017301007701", - "870100a10100a2", - "a60100a70100a0" - ], - "1.3.6.1.4.1.2435.2.3.9.4.2.1.5.5.1.0": "0123456789", - "1.3.6.1.4.1.2435.2.3.9.4.2.1.5.4.5.2.0": "WAITING ", - "1.3.6.1.2.1.43.7.1.1.4.1.1": "2004" - } \ No newline at end of file + "1.3.6.1.2.1.1.3.0": "413613515", + "1.3.6.1.2.1.43.10.2.1.4.1.1": "986", + "1.3.6.1.4.1.2435.2.3.9.4.2.1.5.5.10.0": [ + "000104000003da", + "010104000002c5", + "02010400000386", + "0601040000021a", + "0701040000012d", + "080104000000ed" + ], + "1.3.6.1.4.1.2435.2.3.9.4.2.1.5.5.17.0": "1.17", + "1.3.6.1.4.1.2435.2.3.9.4.2.1.5.5.8.0": [ + "110104000003da", + "31010400000001", + "32010400000001", + "33010400000002", + "34010400000002", + "35010400000001", + "410104000023f0", + "54010400000001", + "55010400000001", + "63010400000001", + "68010400000001", + "690104000025e4", + "6a0104000025e4", + "6d010400002648", + "6f010400001d4c", + "700104000003e8", + "71010400000320", + "720104000000c8", + "7301040000064b", + "7401040000064b", + "7501040000064b", + "76010400000001", + "77010400000001", + "78010400000001", + "790104000023f0", + "7a0104000023f0", + "7b0104000023f0", + "7e01040000064b", + "800104000023f0", + "81010400000050", + "8201040000000a", + "8301040000000a", + "8401040000000a", + "8601040000000a" + ], + "1.3.6.1.4.1.2435.2.3.9.1.1.7.0": "MFG:Brother;CMD:PJL,HBP,URF;MDL:HL-L2340DW series;CLS:PRINTER;CID:Brother Laser Type1;URF:W8,CP1,IS4-1,MT1-3-4-5-8,OB10,PQ4,RS300-600,V1.3,DM1;", + "1.3.6.1.4.1.2435.2.3.9.4.2.1.5.5.11.0": [ + "7301040000bd05", + "7701040000be65", + "82010400002b06", + "8801040000bd34", + "a4010400004005", + "a5010400004005", + "a6010400004005", + "a7010400004005" + ], + "1.3.6.1.4.1.2435.2.3.9.4.2.1.5.5.21.0": [ + "00002302000025", + "00020016010200", + "00210200022202", + "020000a1040000" + ], + "1.3.6.1.4.1.2435.2.3.9.4.2.1.5.5.20.0": [ + "00a40100a50100", + "0100a301008801", + "01017301007701", + "870100a10100a2", + "a60100a70100a0" + ], + "1.3.6.1.4.1.2435.2.3.9.4.2.1.5.5.1.0": "0123456789", + "1.3.6.1.4.1.2435.2.3.9.4.2.1.5.4.5.2.0": "WAITING ", + "1.3.6.1.2.1.43.7.1.1.4.1.1": "2004" +} diff --git a/tests/components/brunt/test_config_flow.py b/tests/components/brunt/test_config_flow.py index f053a6d18b0..4a949911ca6 100644 --- a/tests/components/brunt/test_config_flow.py +++ b/tests/components/brunt/test_config_flow.py @@ -41,54 +41,6 @@ async def test_form(hass): assert len(mock_setup_entry.mock_calls) == 1 -async def test_import(hass): - """Test we get the form.""" - - with patch( - "homeassistant.components.brunt.config_flow.BruntClientAsync.async_login", - return_value=None, - ), patch( - "homeassistant.components.brunt.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=CONFIG - ) - - await hass.async_block_till_done() - - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == "test-username" - assert result["data"] == CONFIG - assert len(mock_setup_entry.mock_calls) == 1 - - -async def test_import_duplicate_login(hass): - """Test uniqueness of username.""" - entry = MockConfigEntry( - domain=DOMAIN, - data=CONFIG, - title="test-username", - unique_id="test-username", - ) - entry.add_to_hass(hass) - - with patch( - "homeassistant.components.brunt.config_flow.BruntClientAsync.async_login", - return_value=None, - ), patch( - "homeassistant.components.brunt.async_setup_entry", - return_value=True, - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=CONFIG - ) - await hass.async_block_till_done() - - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "already_configured" - - async def test_form_duplicate_login(hass): """Test uniqueness of username.""" entry = MockConfigEntry( diff --git a/tests/components/bsblan/fixtures/info.json b/tests/components/bsblan/fixtures/info.json index 82c8b919cc9..08ae7e46247 100644 --- a/tests/components/bsblan/fixtures/info.json +++ b/tests/components/bsblan/fixtures/info.json @@ -20,4 +20,4 @@ "desc": "", "dataType": 0 } -} \ No newline at end of file +} diff --git a/tests/components/caldav/test_calendar.py b/tests/components/caldav/test_calendar.py index 6993aa97081..cb23381d1c6 100644 --- a/tests/components/caldav/test_calendar.py +++ b/tests/components/caldav/test_calendar.py @@ -1,5 +1,6 @@ """The tests for the webdav calendar component.""" import datetime +from http import HTTPStatus from unittest.mock import MagicMock, Mock, patch from caldav.objects import Event @@ -222,15 +223,6 @@ CALDAV_CONFIG = { "custom_calendars": [], } -ORIG_TZ = dt.DEFAULT_TIME_ZONE - - -@pytest.fixture(autouse=True) -def reset_tz(): - """Restore the default TZ after test runs.""" - yield - dt.DEFAULT_TIME_ZONE = ORIG_TZ - @pytest.fixture def set_tz(request): @@ -239,21 +231,21 @@ def set_tz(request): @pytest.fixture -def utc(): +def utc(hass): """Set the default TZ to UTC.""" - dt.set_default_time_zone(dt.get_time_zone("UTC")) + hass.config.set_time_zone("UTC") @pytest.fixture -def new_york(): +def new_york(hass): """Set the default TZ to America/New_York.""" - dt.set_default_time_zone(dt.get_time_zone("America/New_York")) + hass.config.set_time_zone("America/New_York") @pytest.fixture -def baghdad(): +def baghdad(hass): """Set the default TZ to Asia/Baghdad.""" - dt.set_default_time_zone(dt.get_time_zone("Asia/Baghdad")) + hass.config.set_time_zone("Asia/Baghdad") @pytest.fixture(autouse=True) @@ -283,6 +275,23 @@ def mock_private_cal(): yield _calendar +@pytest.fixture +def get_api_events(hass_client): + """Fixture to return events for a specific calendar using the API.""" + + async def api_call(entity_id): + client = await hass_client() + response = await client.get( + # The start/end times are arbitrary since they are ignored by `_mock_calendar` + # which just returns all events for the calendar. + f"/api/calendars/{entity_id}?start=2022-01-01&end=2022-01-01" + ) + assert response.status == HTTPStatus.OK + return await response.json() + + return api_call + + def _local_datetime(hours, minutes): """Build a datetime object for testing in the correct timezone.""" return dt.as_local(datetime.datetime(2017, 11, 27, hours, minutes, 0)) @@ -364,8 +373,9 @@ async def test_setup_component_with_one_custom_calendar(hass, mock_dav_client): assert state.name == "HomeOffice" +@pytest.mark.parametrize("set_tz", ["utc"], indirect=True) @patch("homeassistant.util.dt.now", return_value=_local_datetime(17, 45)) -async def test_ongoing_event(mock_now, hass, calendar): +async def test_ongoing_event(mock_now, hass, calendar, set_tz): """Test that the ongoing event is returned.""" assert await async_setup_component(hass, "calendar", {"calendar": CALDAV_CONFIG}) await hass.async_block_till_done() @@ -385,8 +395,9 @@ async def test_ongoing_event(mock_now, hass, calendar): } +@pytest.mark.parametrize("set_tz", ["utc"], indirect=True) @patch("homeassistant.util.dt.now", return_value=_local_datetime(17, 30)) -async def test_just_ended_event(mock_now, hass, calendar): +async def test_just_ended_event(mock_now, hass, calendar, set_tz): """Test that the next ongoing event is returned.""" assert await async_setup_component(hass, "calendar", {"calendar": CALDAV_CONFIG}) await hass.async_block_till_done() @@ -406,8 +417,9 @@ async def test_just_ended_event(mock_now, hass, calendar): } +@pytest.mark.parametrize("set_tz", ["utc"], indirect=True) @patch("homeassistant.util.dt.now", return_value=_local_datetime(17, 00)) -async def test_ongoing_event_different_tz(mock_now, hass, calendar): +async def test_ongoing_event_different_tz(mock_now, hass, calendar, set_tz): """Test that the ongoing event with another timezone is returned.""" assert await async_setup_component(hass, "calendar", {"calendar": CALDAV_CONFIG}) await hass.async_block_till_done() @@ -427,8 +439,9 @@ async def test_ongoing_event_different_tz(mock_now, hass, calendar): } +@pytest.mark.parametrize("set_tz", ["utc"], indirect=True) @patch("homeassistant.util.dt.now", return_value=_local_datetime(19, 10)) -async def test_ongoing_floating_event_returned(mock_now, hass, calendar): +async def test_ongoing_floating_event_returned(mock_now, hass, calendar, set_tz): """Test that floating events without timezones work.""" assert await async_setup_component(hass, "calendar", {"calendar": CALDAV_CONFIG}) await hass.async_block_till_done() @@ -448,8 +461,9 @@ async def test_ongoing_floating_event_returned(mock_now, hass, calendar): } +@pytest.mark.parametrize("set_tz", ["utc"], indirect=True) @patch("homeassistant.util.dt.now", return_value=_local_datetime(8, 30)) -async def test_ongoing_event_with_offset(mock_now, hass, calendar): +async def test_ongoing_event_with_offset(mock_now, hass, calendar, set_tz): """Test that the offset is taken into account.""" assert await async_setup_component(hass, "calendar", {"calendar": CALDAV_CONFIG}) await hass.async_block_till_done() @@ -469,8 +483,9 @@ async def test_ongoing_event_with_offset(mock_now, hass, calendar): } +@pytest.mark.parametrize("set_tz", ["utc"], indirect=True) @patch("homeassistant.util.dt.now", return_value=_local_datetime(12, 00)) -async def test_matching_filter(mock_now, hass, calendar): +async def test_matching_filter(mock_now, hass, calendar, set_tz): """Test that the matching event is returned.""" config = dict(CALDAV_CONFIG) config["custom_calendars"] = [ @@ -495,8 +510,9 @@ async def test_matching_filter(mock_now, hass, calendar): } +@pytest.mark.parametrize("set_tz", ["utc"], indirect=True) @patch("homeassistant.util.dt.now", return_value=_local_datetime(12, 00)) -async def test_matching_filter_real_regexp(mock_now, hass, calendar): +async def test_matching_filter_real_regexp(mock_now, hass, calendar, set_tz): """Test that the event matching the regexp is returned.""" config = dict(CALDAV_CONFIG) config["custom_calendars"] = [ @@ -625,8 +641,9 @@ async def test_all_day_event_returned_late(hass, calendar, set_tz): ) +@pytest.mark.parametrize("set_tz", ["utc"], indirect=True) @patch("homeassistant.util.dt.now", return_value=_local_datetime(21, 45)) -async def test_event_rrule(mock_now, hass, calendar): +async def test_event_rrule(mock_now, hass, calendar, set_tz): """Test that the future recurring event is returned.""" assert await async_setup_component(hass, "calendar", {"calendar": CALDAV_CONFIG}) await hass.async_block_till_done() @@ -646,8 +663,9 @@ async def test_event_rrule(mock_now, hass, calendar): } +@pytest.mark.parametrize("set_tz", ["utc"], indirect=True) @patch("homeassistant.util.dt.now", return_value=_local_datetime(22, 15)) -async def test_event_rrule_ongoing(mock_now, hass, calendar): +async def test_event_rrule_ongoing(mock_now, hass, calendar, set_tz): """Test that the current recurring event is returned.""" assert await async_setup_component(hass, "calendar", {"calendar": CALDAV_CONFIG}) await hass.async_block_till_done() @@ -667,8 +685,9 @@ async def test_event_rrule_ongoing(mock_now, hass, calendar): } +@pytest.mark.parametrize("set_tz", ["utc"], indirect=True) @patch("homeassistant.util.dt.now", return_value=_local_datetime(22, 45)) -async def test_event_rrule_duration(mock_now, hass, calendar): +async def test_event_rrule_duration(mock_now, hass, calendar, set_tz): """Test that the future recurring event is returned.""" assert await async_setup_component(hass, "calendar", {"calendar": CALDAV_CONFIG}) await hass.async_block_till_done() @@ -688,8 +707,9 @@ async def test_event_rrule_duration(mock_now, hass, calendar): } +@pytest.mark.parametrize("set_tz", ["utc"], indirect=True) @patch("homeassistant.util.dt.now", return_value=_local_datetime(23, 15)) -async def test_event_rrule_duration_ongoing(mock_now, hass, calendar): +async def test_event_rrule_duration_ongoing(mock_now, hass, calendar, set_tz): """Test that the ongoing recurring event is returned.""" assert await async_setup_component(hass, "calendar", {"calendar": CALDAV_CONFIG}) await hass.async_block_till_done() @@ -709,8 +729,9 @@ async def test_event_rrule_duration_ongoing(mock_now, hass, calendar): } +@pytest.mark.parametrize("set_tz", ["utc"], indirect=True) @patch("homeassistant.util.dt.now", return_value=_local_datetime(23, 37)) -async def test_event_rrule_endless(mock_now, hass, calendar): +async def test_event_rrule_endless(mock_now, hass, calendar, set_tz): """Test that the endless recurring event is returned.""" assert await async_setup_component(hass, "calendar", {"calendar": CALDAV_CONFIG}) await hass.async_block_till_done() @@ -798,11 +819,12 @@ async def test_event_rrule_all_day_late(hass, calendar, set_tz): ) +@pytest.mark.parametrize("set_tz", ["utc"], indirect=True) @patch( "homeassistant.util.dt.now", return_value=dt.as_local(datetime.datetime(2015, 11, 27, 0, 15)), ) -async def test_event_rrule_hourly_on_first(mock_now, hass, calendar): +async def test_event_rrule_hourly_on_first(mock_now, hass, calendar, set_tz): """Test that the endless recurring event is returned.""" assert await async_setup_component(hass, "calendar", {"calendar": CALDAV_CONFIG}) await hass.async_block_till_done() @@ -822,11 +844,12 @@ async def test_event_rrule_hourly_on_first(mock_now, hass, calendar): } +@pytest.mark.parametrize("set_tz", ["utc"], indirect=True) @patch( "homeassistant.util.dt.now", return_value=dt.as_local(datetime.datetime(2015, 11, 27, 11, 15)), ) -async def test_event_rrule_hourly_on_last(mock_now, hass, calendar): +async def test_event_rrule_hourly_on_last(mock_now, hass, calendar, set_tz): """Test that the endless recurring event is returned.""" assert await async_setup_component(hass, "calendar", {"calendar": CALDAV_CONFIG}) await hass.async_block_till_done() @@ -888,18 +911,17 @@ async def test_event_rrule_hourly_ended(mock_now, hass, calendar): assert state.state == STATE_OFF -async def test_get_events(hass, calendar): +async def test_get_events(hass, calendar, get_api_events): """Test that all events are returned on API.""" assert await async_setup_component(hass, "calendar", {"calendar": CALDAV_CONFIG}) await hass.async_block_till_done() - entity = hass.data["calendar"].get_entity("calendar.private") - events = await entity.async_get_events( - hass, datetime.date(2015, 11, 27), datetime.date(2015, 11, 28) - ) + + events = await get_api_events("calendar.private") assert len(events) == 14 + assert calendar.call -async def test_get_events_custom_calendars(hass, calendar): +async def test_get_events_custom_calendars(hass, calendar, get_api_events): """Test that only searched events are returned on API.""" config = dict(CALDAV_CONFIG) config["custom_calendars"] = [ @@ -909,9 +931,14 @@ async def test_get_events_custom_calendars(hass, calendar): assert await async_setup_component(hass, "calendar", {"calendar": config}) await hass.async_block_till_done() - entity = hass.data["calendar"].get_entity("calendar.private_private") - events = await entity.async_get_events( - hass, datetime.date(2015, 11, 27), datetime.date(2015, 11, 28) - ) - assert len(events) == 1 - assert events[0]["summary"] == "This is a normal event" + events = await get_api_events("calendar.private_private") + assert events == [ + { + "description": "Surprisingly rainy", + "end": "2017-11-27T10:00:00-08:00", + "location": "Hamburg", + "start": "2017-11-27T09:00:00-08:00", + "summary": "This is a normal event", + "uid": "1", + } + ] diff --git a/tests/components/camera/test_init.py b/tests/components/camera/test_init.py index 0e53e163404..e72941ef488 100644 --- a/tests/components/camera/test_init.py +++ b/tests/components/camera/test_init.py @@ -108,28 +108,6 @@ async def test_get_image_from_camera(hass, image_mock_url): assert image.content == b"Test" -async def test_legacy_async_get_image_signature_warns_only_once( - hass, image_mock_url, caplog -): - """Test that we only warn once when we encounter a legacy async_get_image function signature.""" - - async def _legacy_async_camera_image(self): - return b"Image" - - with patch( - "homeassistant.components.demo.camera.DemoCamera.async_camera_image", - new=_legacy_async_camera_image, - ): - image = await camera.async_get_image(hass, "camera.demo_camera") - assert image.content == b"Image" - assert "does not support requesting width and height" in caplog.text - caplog.clear() - - image = await camera.async_get_image(hass, "camera.demo_camera") - assert image.content == b"Image" - assert "does not support requesting width and height" not in caplog.text - - async def test_get_image_from_camera_with_width_height(hass, image_mock_url): """Grab an image from camera entity with width and height.""" diff --git a/tests/components/camera/test_recorder.py b/tests/components/camera/test_recorder.py new file mode 100644 index 00000000000..76654a2dd9c --- /dev/null +++ b/tests/components/camera/test_recorder.py @@ -0,0 +1,50 @@ +"""The tests for camera recorder.""" +from __future__ import annotations + +from datetime import timedelta + +from homeassistant.components import camera +from homeassistant.components.recorder.models import StateAttributes, States +from homeassistant.components.recorder.util import session_scope +from homeassistant.const import ( + ATTR_ATTRIBUTION, + ATTR_ENTITY_PICTURE, + ATTR_FRIENDLY_NAME, + ATTR_SUPPORTED_FEATURES, +) +from homeassistant.core import State +from homeassistant.setup import async_setup_component +from homeassistant.util import dt as dt_util + +from tests.common import async_fire_time_changed, async_init_recorder_component +from tests.components.recorder.common import async_wait_recording_done_without_instance + + +async def test_exclude_attributes(hass): + """Test camera registered attributes to be excluded.""" + await async_init_recorder_component(hass) + await async_setup_component( + hass, camera.DOMAIN, {camera.DOMAIN: {"platform": "demo"}} + ) + await hass.async_block_till_done() + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5)) + await hass.async_block_till_done() + await async_wait_recording_done_without_instance(hass) + + def _fetch_camera_states() -> list[State]: + with session_scope(hass=hass) as session: + native_states = [] + for db_state, db_state_attributes in session.query(States, StateAttributes): + state = db_state.to_native() + state.attributes = db_state_attributes.to_native() + native_states.append(state) + return native_states + + states: list[State] = await hass.async_add_executor_job(_fetch_camera_states) + assert len(states) > 1 + for state in states: + assert "access_token" not in state.attributes + assert ATTR_ENTITY_PICTURE not in state.attributes + assert ATTR_ATTRIBUTION not in state.attributes + assert ATTR_SUPPORTED_FEATURES not in state.attributes + assert ATTR_FRIENDLY_NAME in state.attributes diff --git a/tests/components/canary/test_alarm_control_panel.py b/tests/components/canary/test_alarm_control_panel.py index 84cef7e81ff..5034792d389 100644 --- a/tests/components/canary/test_alarm_control_panel.py +++ b/tests/components/canary/test_alarm_control_panel.py @@ -16,6 +16,7 @@ from homeassistant.const import ( STATE_ALARM_DISARMED, STATE_UNKNOWN, ) +from homeassistant.helpers.entity_component import async_update_entity from homeassistant.setup import async_setup_component from . import mock_device, mock_location, mock_mode @@ -59,7 +60,7 @@ async def test_alarm_control_panel(hass, canary) -> None: # test private system type(mocked_location).is_private = PropertyMock(return_value=True) - await hass.helpers.entity_component.async_update_entity(entity_id) + await async_update_entity(hass, entity_id) await hass.async_block_till_done() state = hass.states.get(entity_id) @@ -74,7 +75,7 @@ async def test_alarm_control_panel(hass, canary) -> None: return_value=mock_mode(4, LOCATION_MODE_HOME) ) - await hass.helpers.entity_component.async_update_entity(entity_id) + await async_update_entity(hass, entity_id) await hass.async_block_till_done() state = hass.states.get(entity_id) @@ -86,7 +87,7 @@ async def test_alarm_control_panel(hass, canary) -> None: return_value=mock_mode(5, LOCATION_MODE_AWAY) ) - await hass.helpers.entity_component.async_update_entity(entity_id) + await async_update_entity(hass, entity_id) await hass.async_block_till_done() state = hass.states.get(entity_id) @@ -98,7 +99,7 @@ async def test_alarm_control_panel(hass, canary) -> None: return_value=mock_mode(6, LOCATION_MODE_NIGHT) ) - await hass.helpers.entity_component.async_update_entity(entity_id) + await async_update_entity(hass, entity_id) await hass.async_block_till_done() state = hass.states.get(entity_id) diff --git a/tests/components/canary/test_sensor.py b/tests/components/canary/test_sensor.py index 0c95152338c..d9946cab6a0 100644 --- a/tests/components/canary/test_sensor.py +++ b/tests/components/canary/test_sensor.py @@ -16,6 +16,7 @@ from homeassistant.const import ( SIGNAL_STRENGTH_DECIBELS_MILLIWATT, TEMP_CELSIUS, ) +from homeassistant.helpers.entity_component import async_update_entity from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow @@ -126,7 +127,7 @@ async def test_sensors_attributes_pro(hass, canary) -> None: future = utcnow() + timedelta(seconds=30) async_fire_time_changed(hass, future) - await hass.helpers.entity_component.async_update_entity(entity_id) + await async_update_entity(hass, entity_id) await hass.async_block_till_done() state2 = hass.states.get(entity_id) @@ -142,7 +143,7 @@ async def test_sensors_attributes_pro(hass, canary) -> None: future += timedelta(seconds=30) async_fire_time_changed(hass, future) - await hass.helpers.entity_component.async_update_entity(entity_id) + await async_update_entity(hass, entity_id) await hass.async_block_till_done() state3 = hass.states.get(entity_id) diff --git a/tests/components/cast/test_media_player.py b/tests/components/cast/test_media_player.py index 40a1269557d..2e6fafb0287 100644 --- a/tests/components/cast/test_media_player.py +++ b/tests/components/cast/test_media_player.py @@ -39,6 +39,7 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP, ) from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr, entity_registry as er, network from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.setup import async_setup_component @@ -633,8 +634,10 @@ async def test_entity_availability(hass: HomeAssistant): @pytest.mark.parametrize("port,entry_type", ((8009, None), (12345, None))) -async def test_device_registry(hass: HomeAssistant, port, entry_type): +async def test_device_registry(hass: HomeAssistant, hass_ws_client, port, entry_type): """Test device registry integration.""" + assert await async_setup_component(hass, "config", {}) + entity_id = "media_player.speaker" reg = er.async_get(hass) dev_reg = dr.async_get(hass) @@ -657,19 +660,32 @@ async def test_device_registry(hass: HomeAssistant, port, entry_type): assert state.state == "off" assert entity_id == reg.async_get_entity_id("media_player", "cast", str(info.uuid)) entity_entry = reg.async_get(entity_id) - assert entity_entry.device_id is not None device_entry = dev_reg.async_get(entity_entry.device_id) + assert entity_entry.device_id == device_entry.id assert device_entry.entry_type == entry_type # Check that the chromecast object is torn down when the device is removed chromecast.disconnect.assert_not_called() - dev_reg.async_update_device( - device_entry.id, remove_config_entry_id=cast_entry.entry_id + + client = await hass_ws_client(hass) + await client.send_json( + { + "id": 5, + "type": "config/device_registry/remove_config_entry", + "config_entry_id": cast_entry.entry_id, + "device_id": device_entry.id, + } ) + response = await client.receive_json() + assert response["success"] + await hass.async_block_till_done() await hass.async_block_till_done() chromecast.disconnect.assert_called_once() + assert reg.async_get(entity_id) is None + assert dev_reg.async_get(entity_entry.device_id) is None + async def test_entity_cast_status(hass: HomeAssistant): """Test handling of cast status.""" @@ -958,7 +974,7 @@ async def test_entity_play_media(hass: HomeAssistant, quick_play_mock): { ATTR_ENTITY_ID: entity_id, media_player.ATTR_MEDIA_CONTENT_TYPE: "audio", - media_player.ATTR_MEDIA_CONTENT_ID: "best.mp3", + media_player.ATTR_MEDIA_CONTENT_ID: "http://example.com/best.mp3", media_player.ATTR_MEDIA_EXTRA: {"metadata": {"metadatatype": 3}}, }, blocking=True, @@ -969,7 +985,7 @@ async def test_entity_play_media(hass: HomeAssistant, quick_play_mock): chromecast, "default_media_receiver", { - "media_id": "best.mp3", + "media_id": "http://example.com/best.mp3", "media_type": "audio", "metadata": {"metadatatype": 3}, }, @@ -1210,15 +1226,18 @@ async def test_entity_control(hass: HomeAssistant): chromecast.media_controller.pause.assert_called_once_with() # Media previous - await common.async_media_previous_track(hass, entity_id) + with pytest.raises(HomeAssistantError): + await common.async_media_previous_track(hass, entity_id) chromecast.media_controller.queue_prev.assert_not_called() # Media next - await common.async_media_next_track(hass, entity_id) + with pytest.raises(HomeAssistantError): + await common.async_media_next_track(hass, entity_id) chromecast.media_controller.queue_next.assert_not_called() # Media seek - await common.async_media_seek(hass, 123, entity_id) + with pytest.raises(HomeAssistantError): + await common.async_media_seek(hass, 123, entity_id) chromecast.media_controller.seek.assert_not_called() # Enable support for queue and seek @@ -1504,13 +1523,15 @@ async def test_group_media_control(hass, mz_mock, quick_play_mock): assert not chromecast.media_controller.stop.called # Verify play_media is not forwarded - await common.async_play_media(hass, "music", "best.mp3", entity_id) + await common.async_play_media( + hass, "music", "http://example.com/best.mp3", entity_id + ) assert not grp_media.play_media.called assert not chromecast.media_controller.play_media.called quick_play_mock.assert_called_once_with( chromecast, "default_media_receiver", - {"media_id": "best.mp3", "media_type": "music"}, + {"media_id": "http://example.com/best.mp3", "media_type": "music"}, ) @@ -1784,7 +1805,7 @@ async def test_cast_platform_play_media(hass: HomeAssistant, quick_play_mock, ca { ATTR_ENTITY_ID: entity_id, media_player.ATTR_MEDIA_CONTENT_TYPE: "audio", - media_player.ATTR_MEDIA_CONTENT_ID: "best.mp3", + media_player.ATTR_MEDIA_CONTENT_ID: "http://example.com/best.mp3", media_player.ATTR_MEDIA_EXTRA: {"metadata": {"metadatatype": 3}}, }, blocking=True, @@ -1792,7 +1813,7 @@ async def test_cast_platform_play_media(hass: HomeAssistant, quick_play_mock, ca # Assert the media player attempt to play media through the cast platform cast_platform_mock.async_play_media.assert_called_once_with( - hass, entity_id, chromecast, "audio", "best.mp3" + hass, entity_id, chromecast, "audio", "http://example.com/best.mp3" ) # Assert pychromecast is used to play media diff --git a/tests/components/climacell/conftest.py b/tests/components/climacell/conftest.py index 88640c69c14..f762dc8d6f9 100644 --- a/tests/components/climacell/conftest.py +++ b/tests/components/climacell/conftest.py @@ -7,36 +7,20 @@ import pytest from tests.common import load_fixture -@pytest.fixture(name="climacell_config_flow_connect", autouse=True) -def climacell_config_flow_connect(): - """Mock valid climacell config flow setup.""" - with patch( - "homeassistant.components.climacell.config_flow.ClimaCellV3.realtime", - return_value={}, - ), patch( - "homeassistant.components.climacell.config_flow.ClimaCellV4.realtime", - return_value={}, - ): - yield - - @pytest.fixture(name="climacell_config_entry_update") def climacell_config_entry_update_fixture(): """Mock valid climacell config entry setup.""" with patch( "homeassistant.components.climacell.ClimaCellV3.realtime", - return_value=json.loads(load_fixture("climacell/v3_realtime.json")), + return_value=json.loads(load_fixture("v3_realtime.json", "climacell")), ), patch( "homeassistant.components.climacell.ClimaCellV3.forecast_hourly", - return_value=json.loads(load_fixture("climacell/v3_forecast_hourly.json")), + return_value=json.loads(load_fixture("v3_forecast_hourly.json", "climacell")), ), patch( "homeassistant.components.climacell.ClimaCellV3.forecast_daily", - return_value=json.loads(load_fixture("climacell/v3_forecast_daily.json")), + return_value=json.loads(load_fixture("v3_forecast_daily.json", "climacell")), ), patch( "homeassistant.components.climacell.ClimaCellV3.forecast_nowcast", - return_value=json.loads(load_fixture("climacell/v3_forecast_nowcast.json")), - ), patch( - "homeassistant.components.climacell.ClimaCellV4.realtime_and_all_forecasts", - return_value=json.loads(load_fixture("climacell/v4.json")), + return_value=json.loads(load_fixture("v3_forecast_nowcast.json", "climacell")), ): yield diff --git a/tests/components/climacell/const.py b/tests/components/climacell/const.py index be933ecde29..d6487bfa6ce 100644 --- a/tests/components/climacell/const.py +++ b/tests/components/climacell/const.py @@ -10,29 +10,10 @@ from homeassistant.const import ( API_KEY = "aa" -MIN_CONFIG = { - CONF_API_KEY: API_KEY, -} - -V1_ENTRY_DATA = { - CONF_NAME: "ClimaCell", - CONF_API_KEY: API_KEY, - CONF_LATITUDE: 80, - CONF_LONGITUDE: 80, -} - API_V3_ENTRY_DATA = { CONF_NAME: "ClimaCell", CONF_API_KEY: API_KEY, - CONF_LATITUDE: 80, - CONF_LONGITUDE: 80, + CONF_LATITUDE: 80.0, + CONF_LONGITUDE: 80.0, CONF_API_VERSION: 3, } - -API_V4_ENTRY_DATA = { - CONF_NAME: "ClimaCell", - CONF_API_KEY: API_KEY, - CONF_LATITUDE: 80, - CONF_LONGITUDE: 80, - CONF_API_VERSION: 4, -} diff --git a/tests/components/climacell/fixtures/v3_forecast_daily.json b/tests/components/climacell/fixtures/v3_forecast_daily.json index 18f2d77e0cf..4cf4527f6d9 100644 --- a/tests/components/climacell/fixtures/v3_forecast_daily.json +++ b/tests/components/climacell/fixtures/v3_forecast_daily.json @@ -989,4 +989,4 @@ "lat": 38.90694, "lon": -77.03012 } -] \ No newline at end of file +] diff --git a/tests/components/climacell/fixtures/v3_forecast_hourly.json b/tests/components/climacell/fixtures/v3_forecast_hourly.json index a550c7f4302..e6c18890809 100644 --- a/tests/components/climacell/fixtures/v3_forecast_hourly.json +++ b/tests/components/climacell/fixtures/v3_forecast_hourly.json @@ -749,4 +749,4 @@ "value": "2021-03-08T18:00:00.000Z" } } -] \ No newline at end of file +] diff --git a/tests/components/climacell/fixtures/v3_forecast_nowcast.json b/tests/components/climacell/fixtures/v3_forecast_nowcast.json index 23372eae0f9..9439f944401 100644 --- a/tests/components/climacell/fixtures/v3_forecast_nowcast.json +++ b/tests/components/climacell/fixtures/v3_forecast_nowcast.json @@ -779,4 +779,4 @@ "value": "clear" } } -] \ No newline at end of file +] diff --git a/tests/components/climacell/fixtures/v3_realtime.json b/tests/components/climacell/fixtures/v3_realtime.json index b7801d78160..4c3880b139a 100644 --- a/tests/components/climacell/fixtures/v3_realtime.json +++ b/tests/components/climacell/fixtures/v3_realtime.json @@ -37,11 +37,11 @@ "units": "mph" }, "precipitation_type": { - "value": "rain" + "value": "rain" }, "cloud_cover": { - "value": 100, - "units": "%" + "value": 100, + "units": "%" }, "fire_index": { "value": 9 @@ -99,4 +99,4 @@ "observation_time": { "value": "2021-03-07T18:54:06.055Z" } -} \ No newline at end of file +} diff --git a/tests/components/climacell/test_config_flow.py b/tests/components/climacell/test_config_flow.py index 476f2ba3bee..9aa16b8a7c5 100644 --- a/tests/components/climacell/test_config_flow.py +++ b/tests/components/climacell/test_config_flow.py @@ -1,179 +1,27 @@ """Test the ClimaCell config flow.""" -from unittest.mock import patch - -from pyclimacell.exceptions import ( - CantConnectException, - InvalidAPIKeyException, - RateLimitedException, - UnknownException, -) - from homeassistant import data_entry_flow -from homeassistant.components.climacell.config_flow import ( - _get_config_schema, - _get_unique_id, -) from homeassistant.components.climacell.const import ( CONF_TIMESTEP, - DEFAULT_NAME, DEFAULT_TIMESTEP, DOMAIN, ) from homeassistant.config_entries import SOURCE_USER -from homeassistant.const import ( - CONF_API_KEY, - CONF_API_VERSION, - CONF_LATITUDE, - CONF_LONGITUDE, - CONF_NAME, -) from homeassistant.core import HomeAssistant -from .const import API_KEY, MIN_CONFIG +from .const import API_V3_ENTRY_DATA from tests.common import MockConfigEntry -async def test_user_flow_minimum_fields(hass: HomeAssistant) -> None: - """Test user config flow with minimum fields.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER} - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "user" - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input=_get_config_schema(hass, MIN_CONFIG)(MIN_CONFIG), - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == DEFAULT_NAME - assert result["data"][CONF_NAME] == DEFAULT_NAME - assert result["data"][CONF_API_KEY] == API_KEY - assert result["data"][CONF_API_VERSION] == 4 - assert result["data"][CONF_LATITUDE] == hass.config.latitude - assert result["data"][CONF_LONGITUDE] == hass.config.longitude - - -async def test_user_flow_v3(hass: HomeAssistant) -> None: - """Test user config flow with v3 API.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER} - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "user" - - data = _get_config_schema(hass, MIN_CONFIG)(MIN_CONFIG) - data[CONF_API_VERSION] = 3 - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input=data, - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == DEFAULT_NAME - assert result["data"][CONF_NAME] == DEFAULT_NAME - assert result["data"][CONF_API_KEY] == API_KEY - assert result["data"][CONF_API_VERSION] == 3 - assert result["data"][CONF_LATITUDE] == hass.config.latitude - assert result["data"][CONF_LONGITUDE] == hass.config.longitude - - -async def test_user_flow_same_unique_ids(hass: HomeAssistant) -> None: - """Test user config flow with the same unique ID as an existing entry.""" - user_input = _get_config_schema(hass, MIN_CONFIG)(MIN_CONFIG) - MockConfigEntry( - domain=DOMAIN, - data=user_input, - source=SOURCE_USER, - unique_id=_get_unique_id(hass, user_input), - version=2, - ).add_to_hass(hass) - - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_USER}, - data=user_input, - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "already_configured" - - -async def test_user_flow_cannot_connect(hass: HomeAssistant) -> None: - """Test user config flow when ClimaCell can't connect.""" - with patch( - "homeassistant.components.climacell.config_flow.ClimaCellV4.realtime", - side_effect=CantConnectException, - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_USER}, - data=_get_config_schema(hass, MIN_CONFIG)(MIN_CONFIG), - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["errors"] == {"base": "cannot_connect"} - - -async def test_user_flow_invalid_api(hass: HomeAssistant) -> None: - """Test user config flow when API key is invalid.""" - with patch( - "homeassistant.components.climacell.config_flow.ClimaCellV4.realtime", - side_effect=InvalidAPIKeyException, - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_USER}, - data=_get_config_schema(hass, MIN_CONFIG)(MIN_CONFIG), - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["errors"] == {CONF_API_KEY: "invalid_api_key"} - - -async def test_user_flow_rate_limited(hass: HomeAssistant) -> None: - """Test user config flow when API key is rate limited.""" - with patch( - "homeassistant.components.climacell.config_flow.ClimaCellV4.realtime", - side_effect=RateLimitedException, - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_USER}, - data=_get_config_schema(hass, MIN_CONFIG)(MIN_CONFIG), - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["errors"] == {CONF_API_KEY: "rate_limited"} - - -async def test_user_flow_unknown_exception(hass: HomeAssistant) -> None: - """Test user config flow when unknown error occurs.""" - with patch( - "homeassistant.components.climacell.config_flow.ClimaCellV4.realtime", - side_effect=UnknownException, - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_USER}, - data=_get_config_schema(hass, MIN_CONFIG)(MIN_CONFIG), - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["errors"] == {"base": "unknown"} - - -async def test_options_flow(hass: HomeAssistant) -> None: +async def test_options_flow( + hass: HomeAssistant, climacell_config_entry_update: None +) -> None: """Test options config flow for climacell.""" - user_config = _get_config_schema(hass)(MIN_CONFIG) entry = MockConfigEntry( domain=DOMAIN, - data=user_config, + data=API_V3_ENTRY_DATA, source=SOURCE_USER, - unique_id=_get_unique_id(hass, user_config), + unique_id="test", version=1, ) entry.add_to_hass(hass) diff --git a/tests/components/climacell/test_init.py b/tests/components/climacell/test_init.py index 5ee50c6d0ec..baddd46c19d 100644 --- a/tests/components/climacell/test_init.py +++ b/tests/components/climacell/test_init.py @@ -1,16 +1,14 @@ """Tests for Climacell init.""" +from unittest.mock import patch + import pytest -from homeassistant.components.climacell.config_flow import ( - _get_config_schema, - _get_unique_id, -) from homeassistant.components.climacell.const import CONF_TIMESTEP, DOMAIN from homeassistant.components.weather import DOMAIN as WEATHER_DOMAIN from homeassistant.const import CONF_API_VERSION from homeassistant.core import HomeAssistant -from .const import API_V3_ENTRY_DATA, MIN_CONFIG, V1_ENTRY_DATA +from .const import API_V3_ENTRY_DATA from tests.common import MockConfigEntry @@ -20,11 +18,10 @@ async def test_load_and_unload( climacell_config_entry_update: pytest.fixture, ) -> None: """Test loading and unloading entry.""" - data = _get_config_schema(hass)(MIN_CONFIG) config_entry = MockConfigEntry( domain=DOMAIN, - data=data, - unique_id=_get_unique_id(hass, data), + data=API_V3_ENTRY_DATA, + unique_id="test", version=1, ) config_entry.add_to_hass(hass) @@ -42,11 +39,10 @@ async def test_v3_load_and_unload( climacell_config_entry_update: pytest.fixture, ) -> None: """Test loading and unloading v3 entry.""" - data = _get_config_schema(hass)(API_V3_ENTRY_DATA) config_entry = MockConfigEntry( domain=DOMAIN, - data=data, - unique_id=_get_unique_id(hass, data), + data={k: v for k, v in API_V3_ENTRY_DATA.items() if k != CONF_API_VERSION}, + unique_id="test", version=1, ) config_entry.add_to_hass(hass) @@ -59,6 +55,29 @@ async def test_v3_load_and_unload( assert len(hass.states.async_entity_ids(WEATHER_DOMAIN)) == 0 +async def test_v4_load_and_unload( + hass: HomeAssistant, + climacell_config_entry_update: pytest.fixture, +) -> None: + """Test loading and unloading v3 entry.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_API_VERSION: 4, + **{k: v for k, v in API_V3_ENTRY_DATA.items() if k != CONF_API_VERSION}, + }, + unique_id="test", + version=1, + ) + config_entry.add_to_hass(hass) + with patch( + "homeassistant.components.tomorrowio.async_setup_entry", return_value=True + ): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert len(hass.states.async_entity_ids(WEATHER_DOMAIN)) == 0 + + @pytest.mark.parametrize( "old_timestep, new_timestep", [(2, 1), (7, 5), (20, 15), (21, 30)] ) @@ -71,9 +90,9 @@ async def test_migrate_timestep( """Test migration to standardized timestep.""" config_entry = MockConfigEntry( domain=DOMAIN, - data=V1_ENTRY_DATA, + data=API_V3_ENTRY_DATA, options={CONF_TIMESTEP: old_timestep}, - unique_id=_get_unique_id(hass, V1_ENTRY_DATA), + unique_id="test", version=1, ) config_entry.add_to_hass(hass) diff --git a/tests/components/climacell/test_sensor.py b/tests/components/climacell/test_sensor.py index 8c075942cea..3412a5c35f0 100644 --- a/tests/components/climacell/test_sensor.py +++ b/tests/components/climacell/test_sensor.py @@ -7,10 +7,6 @@ from unittest.mock import patch import pytest -from homeassistant.components.climacell.config_flow import ( - _get_config_schema, - _get_unique_id, -) from homeassistant.components.climacell.const import ATTRIBUTION, DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.const import ATTR_ATTRIBUTION @@ -18,7 +14,7 @@ from homeassistant.core import HomeAssistant, State, callback from homeassistant.helpers.entity_registry import async_get from homeassistant.util import dt as dt_util -from .const import API_V3_ENTRY_DATA, API_V4_ENTRY_DATA +from .const import API_V3_ENTRY_DATA from tests.common import MockConfigEntry @@ -105,11 +101,10 @@ async def _setup( "homeassistant.util.dt.utcnow", return_value=datetime(2021, 3, 6, 23, 59, 59, tzinfo=dt_util.UTC), ): - data = _get_config_schema(hass)(config) config_entry = MockConfigEntry( domain=DOMAIN, - data=data, - unique_id=_get_unique_id(hass, data), + data=config, + unique_id="test", version=1, ) config_entry.add_to_hass(hass) @@ -151,36 +146,3 @@ async def test_v3_sensor( check_sensor_state(hass, GRASS_POLLEN, "minimal_to_none") check_sensor_state(hass, WEED_POLLEN, "minimal_to_none") check_sensor_state(hass, TREE_POLLEN, "minimal_to_none") - - -async def test_v4_sensor( - hass: HomeAssistant, - climacell_config_entry_update: pytest.fixture, -) -> None: - """Test v4 sensor data.""" - await _setup(hass, V4_FIELDS, API_V4_ENTRY_DATA) - check_sensor_state(hass, O3, "46.53") - check_sensor_state(hass, CO, "0.63") - check_sensor_state(hass, NO2, "10.67") - check_sensor_state(hass, SO2, "1.65") - check_sensor_state(hass, PM25, "5.2972") - check_sensor_state(hass, PM10, "20.1294") - check_sensor_state(hass, MEP_AQI, "23") - check_sensor_state(hass, MEP_HEALTH_CONCERN, "good") - check_sensor_state(hass, MEP_PRIMARY_POLLUTANT, "pm10") - check_sensor_state(hass, EPA_AQI, "24") - check_sensor_state(hass, EPA_HEALTH_CONCERN, "good") - check_sensor_state(hass, EPA_PRIMARY_POLLUTANT, "pm25") - check_sensor_state(hass, FIRE_INDEX, "10") - check_sensor_state(hass, GRASS_POLLEN, "none") - check_sensor_state(hass, WEED_POLLEN, "none") - check_sensor_state(hass, TREE_POLLEN, "none") - check_sensor_state(hass, FEELS_LIKE, "38.5") - check_sensor_state(hass, DEW_POINT, "22.6778") - check_sensor_state(hass, PRESSURE_SURFACE_LEVEL, "997.9688") - check_sensor_state(hass, GHI, "0.0") - check_sensor_state(hass, CLOUD_BASE, "1.1909") - check_sensor_state(hass, CLOUD_COVER, "100") - check_sensor_state(hass, CLOUD_CEILING, "1.1909") - check_sensor_state(hass, WIND_GUST, "5.6506") - check_sensor_state(hass, PRECIPITATION_TYPE, "rain") diff --git a/tests/components/climacell/test_weather.py b/tests/components/climacell/test_weather.py index e3e15889e44..3c02f6b9b1f 100644 --- a/tests/components/climacell/test_weather.py +++ b/tests/components/climacell/test_weather.py @@ -7,10 +7,6 @@ from unittest.mock import patch import pytest -from homeassistant.components.climacell.config_flow import ( - _get_config_schema, - _get_unique_id, -) from homeassistant.components.climacell.const import ( ATTR_CLOUD_COVER, ATTR_PRECIPITATION_TYPE, @@ -30,8 +26,6 @@ from homeassistant.components.weather import ( ATTR_FORECAST_TEMP, ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_TIME, - ATTR_FORECAST_WIND_BEARING, - ATTR_FORECAST_WIND_SPEED, ATTR_WEATHER_HUMIDITY, ATTR_WEATHER_OZONE, ATTR_WEATHER_PRESSURE, @@ -46,7 +40,7 @@ from homeassistant.core import HomeAssistant, State, callback from homeassistant.helpers.entity_registry import async_get from homeassistant.util import dt as dt_util -from .const import API_V3_ENTRY_DATA, API_V4_ENTRY_DATA +from .const import API_V3_ENTRY_DATA from tests.common import MockConfigEntry @@ -69,11 +63,10 @@ async def _setup(hass: HomeAssistant, config: dict[str, Any]) -> State: "homeassistant.util.dt.utcnow", return_value=datetime(2021, 3, 6, 23, 59, 59, tzinfo=dt_util.UTC), ): - data = _get_config_schema(hass)(config) config_entry = MockConfigEntry( domain=DOMAIN, - data=data, - unique_id=_get_unique_id(hass, data), + data=config, + unique_id="test", version=1, ) config_entry.add_to_hass(hass) @@ -98,296 +91,133 @@ async def test_v3_weather( assert weather_state.attributes[ATTR_FORECAST] == [ { ATTR_FORECAST_CONDITION: ATTR_CONDITION_SUNNY, - ATTR_FORECAST_TIME: "2021-03-07T00:00:00+00:00", + ATTR_FORECAST_TIME: "2021-03-07T00:00:00-08:00", ATTR_FORECAST_PRECIPITATION: 0, ATTR_FORECAST_PRECIPITATION_PROBABILITY: 0, - ATTR_FORECAST_TEMP: 7, - ATTR_FORECAST_TEMP_LOW: -5, + ATTR_FORECAST_TEMP: 7.2, + ATTR_FORECAST_TEMP_LOW: -4.7, }, { ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY, - ATTR_FORECAST_TIME: "2021-03-08T00:00:00+00:00", + ATTR_FORECAST_TIME: "2021-03-08T00:00:00-08:00", ATTR_FORECAST_PRECIPITATION: 0, ATTR_FORECAST_PRECIPITATION_PROBABILITY: 0, - ATTR_FORECAST_TEMP: 10, - ATTR_FORECAST_TEMP_LOW: -4, + ATTR_FORECAST_TEMP: 9.7, + ATTR_FORECAST_TEMP_LOW: -4.0, }, { ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY, - ATTR_FORECAST_TIME: "2021-03-09T00:00:00+00:00", + ATTR_FORECAST_TIME: "2021-03-09T00:00:00-08:00", ATTR_FORECAST_PRECIPITATION: 0, ATTR_FORECAST_PRECIPITATION_PROBABILITY: 0, - ATTR_FORECAST_TEMP: 19, - ATTR_FORECAST_TEMP_LOW: 0, + ATTR_FORECAST_TEMP: 19.4, + ATTR_FORECAST_TEMP_LOW: -0.3, }, { ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY, - ATTR_FORECAST_TIME: "2021-03-10T00:00:00+00:00", + ATTR_FORECAST_TIME: "2021-03-10T00:00:00-08:00", ATTR_FORECAST_PRECIPITATION: 0, ATTR_FORECAST_PRECIPITATION_PROBABILITY: 0, - ATTR_FORECAST_TEMP: 18, - ATTR_FORECAST_TEMP_LOW: 3, + ATTR_FORECAST_TEMP: 18.5, + ATTR_FORECAST_TEMP_LOW: 3.0, }, { ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY, - ATTR_FORECAST_TIME: "2021-03-11T00:00:00+00:00", + ATTR_FORECAST_TIME: "2021-03-11T00:00:00-08:00", ATTR_FORECAST_PRECIPITATION: 0, ATTR_FORECAST_PRECIPITATION_PROBABILITY: 5, - ATTR_FORECAST_TEMP: 20, - ATTR_FORECAST_TEMP_LOW: 9, + ATTR_FORECAST_TEMP: 19.7, + ATTR_FORECAST_TEMP_LOW: 9.3, }, { ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY, - ATTR_FORECAST_TIME: "2021-03-12T00:00:00+00:00", + ATTR_FORECAST_TIME: "2021-03-12T00:00:00-08:00", ATTR_FORECAST_PRECIPITATION: 0.0457, ATTR_FORECAST_PRECIPITATION_PROBABILITY: 25, - ATTR_FORECAST_TEMP: 20, - ATTR_FORECAST_TEMP_LOW: 12, + ATTR_FORECAST_TEMP: 19.9, + ATTR_FORECAST_TEMP_LOW: 12.1, }, { ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY, - ATTR_FORECAST_TIME: "2021-03-13T00:00:00+00:00", + ATTR_FORECAST_TIME: "2021-03-13T00:00:00-08:00", ATTR_FORECAST_PRECIPITATION: 0, ATTR_FORECAST_PRECIPITATION_PROBABILITY: 25, - ATTR_FORECAST_TEMP: 16, - ATTR_FORECAST_TEMP_LOW: 7, + ATTR_FORECAST_TEMP: 15.8, + ATTR_FORECAST_TEMP_LOW: 7.5, }, { ATTR_FORECAST_CONDITION: ATTR_CONDITION_RAINY, - ATTR_FORECAST_TIME: "2021-03-14T00:00:00+00:00", + ATTR_FORECAST_TIME: "2021-03-14T00:00:00-08:00", ATTR_FORECAST_PRECIPITATION: 1.0744, ATTR_FORECAST_PRECIPITATION_PROBABILITY: 75, - ATTR_FORECAST_TEMP: 6, - ATTR_FORECAST_TEMP_LOW: 3, + ATTR_FORECAST_TEMP: 6.4, + ATTR_FORECAST_TEMP_LOW: 3.2, }, { ATTR_FORECAST_CONDITION: ATTR_CONDITION_SNOWY, - ATTR_FORECAST_TIME: "2021-03-15T00:00:00+00:00", + ATTR_FORECAST_TIME: "2021-03-15T00:00:00-07:00", # DST starts ATTR_FORECAST_PRECIPITATION: 7.3050, ATTR_FORECAST_PRECIPITATION_PROBABILITY: 95, - ATTR_FORECAST_TEMP: 1, - ATTR_FORECAST_TEMP_LOW: 0, + ATTR_FORECAST_TEMP: 1.2, + ATTR_FORECAST_TEMP_LOW: 0.2, }, { ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY, - ATTR_FORECAST_TIME: "2021-03-16T00:00:00+00:00", + ATTR_FORECAST_TIME: "2021-03-16T00:00:00-07:00", ATTR_FORECAST_PRECIPITATION: 0.0051, ATTR_FORECAST_PRECIPITATION_PROBABILITY: 5, - ATTR_FORECAST_TEMP: 6, - ATTR_FORECAST_TEMP_LOW: -2, + ATTR_FORECAST_TEMP: 6.1, + ATTR_FORECAST_TEMP_LOW: -1.6, }, { ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY, - ATTR_FORECAST_TIME: "2021-03-17T00:00:00+00:00", + ATTR_FORECAST_TIME: "2021-03-17T00:00:00-07:00", ATTR_FORECAST_PRECIPITATION: 0, ATTR_FORECAST_PRECIPITATION_PROBABILITY: 0, - ATTR_FORECAST_TEMP: 11, - ATTR_FORECAST_TEMP_LOW: 1, + ATTR_FORECAST_TEMP: 11.3, + ATTR_FORECAST_TEMP_LOW: 1.3, }, { ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY, - ATTR_FORECAST_TIME: "2021-03-18T00:00:00+00:00", + ATTR_FORECAST_TIME: "2021-03-18T00:00:00-07:00", ATTR_FORECAST_PRECIPITATION: 0, ATTR_FORECAST_PRECIPITATION_PROBABILITY: 5, - ATTR_FORECAST_TEMP: 12, - ATTR_FORECAST_TEMP_LOW: 6, + ATTR_FORECAST_TEMP: 12.3, + ATTR_FORECAST_TEMP_LOW: 5.6, }, { ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY, - ATTR_FORECAST_TIME: "2021-03-19T00:00:00+00:00", + ATTR_FORECAST_TIME: "2021-03-19T00:00:00-07:00", ATTR_FORECAST_PRECIPITATION: 0.1778, ATTR_FORECAST_PRECIPITATION_PROBABILITY: 45, - ATTR_FORECAST_TEMP: 9, - ATTR_FORECAST_TEMP_LOW: 5, + ATTR_FORECAST_TEMP: 9.4, + ATTR_FORECAST_TEMP_LOW: 4.7, }, { ATTR_FORECAST_CONDITION: ATTR_CONDITION_RAINY, - ATTR_FORECAST_TIME: "2021-03-20T00:00:00+00:00", + ATTR_FORECAST_TIME: "2021-03-20T00:00:00-07:00", ATTR_FORECAST_PRECIPITATION: 1.2319, ATTR_FORECAST_PRECIPITATION_PROBABILITY: 55, - ATTR_FORECAST_TEMP: 5, - ATTR_FORECAST_TEMP_LOW: 3, + ATTR_FORECAST_TEMP: 5.0, + ATTR_FORECAST_TEMP_LOW: 3.1, }, { ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY, - ATTR_FORECAST_TIME: "2021-03-21T00:00:00+00:00", + ATTR_FORECAST_TIME: "2021-03-21T00:00:00-07:00", ATTR_FORECAST_PRECIPITATION: 0.0432, ATTR_FORECAST_PRECIPITATION_PROBABILITY: 20, - ATTR_FORECAST_TEMP: 7, - ATTR_FORECAST_TEMP_LOW: 1, + ATTR_FORECAST_TEMP: 6.8, + ATTR_FORECAST_TEMP_LOW: 0.9, }, ] assert weather_state.attributes[ATTR_FRIENDLY_NAME] == "ClimaCell - Daily" assert weather_state.attributes[ATTR_WEATHER_HUMIDITY] == 24 assert weather_state.attributes[ATTR_WEATHER_OZONE] == 52.625 assert weather_state.attributes[ATTR_WEATHER_PRESSURE] == 1028.1246 - assert weather_state.attributes[ATTR_WEATHER_TEMPERATURE] == 7 + assert weather_state.attributes[ATTR_WEATHER_TEMPERATURE] == 6.6 assert weather_state.attributes[ATTR_WEATHER_VISIBILITY] == 9.9940 assert weather_state.attributes[ATTR_WEATHER_WIND_BEARING] == 320.31 assert weather_state.attributes[ATTR_WEATHER_WIND_SPEED] == 14.6289 assert weather_state.attributes[ATTR_CLOUD_COVER] == 100 assert weather_state.attributes[ATTR_WIND_GUST] == 24.0758 assert weather_state.attributes[ATTR_PRECIPITATION_TYPE] == "rain" - - -async def test_v4_weather( - hass: HomeAssistant, - climacell_config_entry_update: pytest.fixture, -) -> None: - """Test v4 weather data.""" - weather_state = await _setup(hass, API_V4_ENTRY_DATA) - assert weather_state.state == ATTR_CONDITION_SUNNY - assert weather_state.attributes[ATTR_ATTRIBUTION] == ATTRIBUTION - assert weather_state.attributes[ATTR_FORECAST] == [ - { - ATTR_FORECAST_CONDITION: ATTR_CONDITION_SUNNY, - ATTR_FORECAST_TIME: "2021-03-07T11:00:00+00:00", - ATTR_FORECAST_PRECIPITATION: 0, - ATTR_FORECAST_PRECIPITATION_PROBABILITY: 0, - ATTR_FORECAST_TEMP: 8, - ATTR_FORECAST_TEMP_LOW: -3, - ATTR_FORECAST_WIND_BEARING: 239.6, - ATTR_FORECAST_WIND_SPEED: 15.2727, - }, - { - ATTR_FORECAST_CONDITION: "cloudy", - ATTR_FORECAST_TIME: "2021-03-08T11:00:00+00:00", - ATTR_FORECAST_PRECIPITATION: 0, - ATTR_FORECAST_PRECIPITATION_PROBABILITY: 0, - ATTR_FORECAST_TEMP: 10, - ATTR_FORECAST_TEMP_LOW: -3, - ATTR_FORECAST_WIND_BEARING: 262.82, - ATTR_FORECAST_WIND_SPEED: 11.6517, - }, - { - ATTR_FORECAST_CONDITION: "cloudy", - ATTR_FORECAST_TIME: "2021-03-09T11:00:00+00:00", - ATTR_FORECAST_PRECIPITATION: 0, - ATTR_FORECAST_PRECIPITATION_PROBABILITY: 0, - ATTR_FORECAST_TEMP: 19, - ATTR_FORECAST_TEMP_LOW: 0, - ATTR_FORECAST_WIND_BEARING: 229.3, - ATTR_FORECAST_WIND_SPEED: 11.3459, - }, - { - ATTR_FORECAST_CONDITION: "cloudy", - ATTR_FORECAST_TIME: "2021-03-10T11:00:00+00:00", - ATTR_FORECAST_PRECIPITATION: 0, - ATTR_FORECAST_PRECIPITATION_PROBABILITY: 0, - ATTR_FORECAST_TEMP: 18, - ATTR_FORECAST_TEMP_LOW: 3, - ATTR_FORECAST_WIND_BEARING: 149.91, - ATTR_FORECAST_WIND_SPEED: 17.1234, - }, - { - ATTR_FORECAST_CONDITION: "cloudy", - ATTR_FORECAST_TIME: "2021-03-11T11:00:00+00:00", - ATTR_FORECAST_PRECIPITATION: 0, - ATTR_FORECAST_PRECIPITATION_PROBABILITY: 0, - ATTR_FORECAST_TEMP: 19, - ATTR_FORECAST_TEMP_LOW: 9, - ATTR_FORECAST_WIND_BEARING: 210.45, - ATTR_FORECAST_WIND_SPEED: 25.2506, - }, - { - ATTR_FORECAST_CONDITION: "rainy", - ATTR_FORECAST_TIME: "2021-03-12T11:00:00+00:00", - ATTR_FORECAST_PRECIPITATION: 0.1219, - ATTR_FORECAST_PRECIPITATION_PROBABILITY: 25, - ATTR_FORECAST_TEMP: 20, - ATTR_FORECAST_TEMP_LOW: 12, - ATTR_FORECAST_WIND_BEARING: 217.98, - ATTR_FORECAST_WIND_SPEED: 19.7949, - }, - { - ATTR_FORECAST_CONDITION: "cloudy", - ATTR_FORECAST_TIME: "2021-03-13T11:00:00+00:00", - ATTR_FORECAST_PRECIPITATION: 0, - ATTR_FORECAST_PRECIPITATION_PROBABILITY: 25, - ATTR_FORECAST_TEMP: 12, - ATTR_FORECAST_TEMP_LOW: 6, - ATTR_FORECAST_WIND_BEARING: 58.79, - ATTR_FORECAST_WIND_SPEED: 15.6428, - }, - { - ATTR_FORECAST_CONDITION: "snowy", - ATTR_FORECAST_TIME: "2021-03-14T10:00:00+00:00", - ATTR_FORECAST_PRECIPITATION: 23.9573, - ATTR_FORECAST_PRECIPITATION_PROBABILITY: 95, - ATTR_FORECAST_TEMP: 6, - ATTR_FORECAST_TEMP_LOW: 1, - ATTR_FORECAST_WIND_BEARING: 70.25, - ATTR_FORECAST_WIND_SPEED: 26.1518, - }, - { - ATTR_FORECAST_CONDITION: "snowy", - ATTR_FORECAST_TIME: "2021-03-15T10:00:00+00:00", - ATTR_FORECAST_PRECIPITATION: 1.4630, - ATTR_FORECAST_PRECIPITATION_PROBABILITY: 55, - ATTR_FORECAST_TEMP: 6, - ATTR_FORECAST_TEMP_LOW: -1, - ATTR_FORECAST_WIND_BEARING: 84.47, - ATTR_FORECAST_WIND_SPEED: 25.5725, - }, - { - ATTR_FORECAST_CONDITION: "cloudy", - ATTR_FORECAST_TIME: "2021-03-16T10:00:00+00:00", - ATTR_FORECAST_PRECIPITATION: 0, - ATTR_FORECAST_PRECIPITATION_PROBABILITY: 0, - ATTR_FORECAST_TEMP: 6, - ATTR_FORECAST_TEMP_LOW: -2, - ATTR_FORECAST_WIND_BEARING: 103.85, - ATTR_FORECAST_WIND_SPEED: 10.7987, - }, - { - ATTR_FORECAST_CONDITION: "cloudy", - ATTR_FORECAST_TIME: "2021-03-17T10:00:00+00:00", - ATTR_FORECAST_PRECIPITATION: 0, - ATTR_FORECAST_PRECIPITATION_PROBABILITY: 0, - ATTR_FORECAST_TEMP: 11, - ATTR_FORECAST_TEMP_LOW: 1, - ATTR_FORECAST_WIND_BEARING: 145.41, - ATTR_FORECAST_WIND_SPEED: 11.6999, - }, - { - ATTR_FORECAST_CONDITION: "cloudy", - ATTR_FORECAST_TIME: "2021-03-18T10:00:00+00:00", - ATTR_FORECAST_PRECIPITATION: 0, - ATTR_FORECAST_PRECIPITATION_PROBABILITY: 10, - ATTR_FORECAST_TEMP: 12, - ATTR_FORECAST_TEMP_LOW: 5, - ATTR_FORECAST_WIND_BEARING: 62.99, - ATTR_FORECAST_WIND_SPEED: 10.5895, - }, - { - ATTR_FORECAST_CONDITION: "rainy", - ATTR_FORECAST_TIME: "2021-03-19T10:00:00+00:00", - ATTR_FORECAST_PRECIPITATION: 2.9261, - ATTR_FORECAST_PRECIPITATION_PROBABILITY: 55, - ATTR_FORECAST_TEMP: 9, - ATTR_FORECAST_TEMP_LOW: 4, - ATTR_FORECAST_WIND_BEARING: 68.54, - ATTR_FORECAST_WIND_SPEED: 22.3860, - }, - { - ATTR_FORECAST_CONDITION: "snowy", - ATTR_FORECAST_TIME: "2021-03-20T10:00:00+00:00", - ATTR_FORECAST_PRECIPITATION: 1.2192, - ATTR_FORECAST_PRECIPITATION_PROBABILITY: 33.3, - ATTR_FORECAST_TEMP: 5, - ATTR_FORECAST_TEMP_LOW: 2, - ATTR_FORECAST_WIND_BEARING: 56.98, - ATTR_FORECAST_WIND_SPEED: 27.9221, - }, - ] - assert weather_state.attributes[ATTR_FRIENDLY_NAME] == "ClimaCell - Daily" - assert weather_state.attributes[ATTR_WEATHER_HUMIDITY] == 23 - assert weather_state.attributes[ATTR_WEATHER_OZONE] == 46.53 - assert weather_state.attributes[ATTR_WEATHER_PRESSURE] == 1027.7691 - assert weather_state.attributes[ATTR_WEATHER_TEMPERATURE] == 7 - assert weather_state.attributes[ATTR_WEATHER_VISIBILITY] == 13.1162 - assert weather_state.attributes[ATTR_WEATHER_WIND_BEARING] == 315.14 - assert weather_state.attributes[ATTR_WEATHER_WIND_SPEED] == 15.0152 - assert weather_state.attributes[ATTR_CLOUD_COVER] == 100 - assert weather_state.attributes[ATTR_WIND_GUST] == 20.3421 - assert weather_state.attributes[ATTR_PRECIPITATION_TYPE] == "rain" diff --git a/tests/components/climate/test_recorder.py b/tests/components/climate/test_recorder.py new file mode 100644 index 00000000000..f1642fca240 --- /dev/null +++ b/tests/components/climate/test_recorder.py @@ -0,0 +1,61 @@ +"""The tests for climate recorder.""" +from __future__ import annotations + +from datetime import timedelta + +from homeassistant.components import climate +from homeassistant.components.climate.const import ( + ATTR_FAN_MODES, + ATTR_HVAC_MODES, + ATTR_MAX_HUMIDITY, + ATTR_MAX_TEMP, + ATTR_MIN_HUMIDITY, + ATTR_MIN_TEMP, + ATTR_PRESET_MODES, + ATTR_SWING_MODES, + ATTR_TARGET_TEMP_STEP, +) +from homeassistant.components.recorder.models import StateAttributes, States +from homeassistant.components.recorder.util import session_scope +from homeassistant.const import ATTR_FRIENDLY_NAME +from homeassistant.core import State +from homeassistant.setup import async_setup_component +from homeassistant.util import dt as dt_util + +from tests.common import async_fire_time_changed, async_init_recorder_component +from tests.components.recorder.common import async_wait_recording_done_without_instance + + +async def test_exclude_attributes(hass): + """Test climate registered attributes to be excluded.""" + await async_init_recorder_component(hass) + await async_setup_component( + hass, climate.DOMAIN, {climate.DOMAIN: {"platform": "demo"}} + ) + await hass.async_block_till_done() + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5)) + await hass.async_block_till_done() + await async_wait_recording_done_without_instance(hass) + + def _fetch_states() -> list[State]: + with session_scope(hass=hass) as session: + native_states = [] + for db_state, db_state_attributes in session.query(States, StateAttributes): + state = db_state.to_native() + state.attributes = db_state_attributes.to_native() + native_states.append(state) + return native_states + + states: list[State] = await hass.async_add_executor_job(_fetch_states) + assert len(states) > 1 + for state in states: + assert ATTR_PRESET_MODES not in state.attributes + assert ATTR_HVAC_MODES not in state.attributes + assert ATTR_FAN_MODES not in state.attributes + assert ATTR_SWING_MODES not in state.attributes + assert ATTR_MIN_TEMP not in state.attributes + assert ATTR_MAX_TEMP not in state.attributes + assert ATTR_MIN_HUMIDITY not in state.attributes + assert ATTR_MAX_HUMIDITY not in state.attributes + assert ATTR_TARGET_TEMP_STEP not in state.attributes + assert ATTR_FRIENDLY_NAME in state.attributes diff --git a/tests/components/cloud/test_alexa_config.py b/tests/components/cloud/test_alexa_config.py index 9e24eaa764d..115d39d3aeb 100644 --- a/tests/components/cloud/test_alexa_config.py +++ b/tests/components/cloud/test_alexa_config.py @@ -6,8 +6,9 @@ import pytest from homeassistant.components.alexa import errors from homeassistant.components.cloud import ALEXA_SCHEMA, alexa_config +from homeassistant.helpers import entity_registry as er from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.entity_registry import EVENT_ENTITY_REGISTRY_UPDATED +from homeassistant.helpers.entity import EntityCategory from homeassistant.util.dt import utcnow from tests.common import async_fire_time_changed, mock_registry @@ -28,21 +29,35 @@ async def test_alexa_config_expose_entity_prefs(hass, cloud_prefs, cloud_stub): "test", "light_config_id", suggested_object_id="config_light", - entity_category="config", + entity_category=EntityCategory.CONFIG, ) entity_entry2 = entity_registry.async_get_or_create( "light", "test", "light_diagnostic_id", suggested_object_id="diagnostic_light", - entity_category="diagnostic", + entity_category=EntityCategory.DIAGNOSTIC, ) entity_entry3 = entity_registry.async_get_or_create( "light", "test", "light_system_id", suggested_object_id="system_light", - entity_category="system", + entity_category=EntityCategory.SYSTEM, + ) + entity_entry4 = entity_registry.async_get_or_create( + "light", + "test", + "light_hidden_integration_id", + suggested_object_id="hidden_integration_light", + hidden_by=er.RegistryEntryHider.INTEGRATION, + ) + entity_entry5 = entity_registry.async_get_or_create( + "light", + "test", + "light_hidden_user_id", + suggested_object_id="hidden_user_light", + hidden_by=er.RegistryEntryHider.USER, ) entity_conf = {"should_expose": False} @@ -61,20 +76,26 @@ async def test_alexa_config_expose_entity_prefs(hass, cloud_prefs, cloud_stub): assert not conf.should_expose(entity_entry1.entity_id) assert not conf.should_expose(entity_entry2.entity_id) assert not conf.should_expose(entity_entry3.entity_id) + assert not conf.should_expose(entity_entry4.entity_id) + assert not conf.should_expose(entity_entry5.entity_id) entity_conf["should_expose"] = True assert conf.should_expose("light.kitchen") - # config and diagnostic entities should not be exposed + # categorized and hidden entities should not be exposed assert not conf.should_expose(entity_entry1.entity_id) assert not conf.should_expose(entity_entry2.entity_id) assert not conf.should_expose(entity_entry3.entity_id) + assert not conf.should_expose(entity_entry4.entity_id) + assert not conf.should_expose(entity_entry5.entity_id) entity_conf["should_expose"] = None assert conf.should_expose("light.kitchen") - # config and diagnostic entities should not be exposed + # categorized and hidden entities should not be exposed assert not conf.should_expose(entity_entry1.entity_id) assert not conf.should_expose(entity_entry2.entity_id) assert not conf.should_expose(entity_entry3.entity_id) + assert not conf.should_expose(entity_entry4.entity_id) + assert not conf.should_expose(entity_entry5.entity_id) assert "alexa" not in hass.config.components await cloud_prefs.async_update( @@ -324,7 +345,7 @@ async def test_alexa_entity_registry_sync(hass, mock_cloud_login, cloud_prefs): with patch_sync_helper() as (to_update, to_remove): hass.bus.async_fire( - EVENT_ENTITY_REGISTRY_UPDATED, + er.EVENT_ENTITY_REGISTRY_UPDATED, {"action": "create", "entity_id": "light.kitchen"}, ) await hass.async_block_till_done() @@ -334,7 +355,7 @@ async def test_alexa_entity_registry_sync(hass, mock_cloud_login, cloud_prefs): with patch_sync_helper() as (to_update, to_remove): hass.bus.async_fire( - EVENT_ENTITY_REGISTRY_UPDATED, + er.EVENT_ENTITY_REGISTRY_UPDATED, {"action": "remove", "entity_id": "light.kitchen"}, ) await hass.async_block_till_done() @@ -344,7 +365,7 @@ async def test_alexa_entity_registry_sync(hass, mock_cloud_login, cloud_prefs): with patch_sync_helper() as (to_update, to_remove): hass.bus.async_fire( - EVENT_ENTITY_REGISTRY_UPDATED, + er.EVENT_ENTITY_REGISTRY_UPDATED, { "action": "update", "entity_id": "light.kitchen", @@ -359,7 +380,7 @@ async def test_alexa_entity_registry_sync(hass, mock_cloud_login, cloud_prefs): with patch_sync_helper() as (to_update, to_remove): hass.bus.async_fire( - EVENT_ENTITY_REGISTRY_UPDATED, + er.EVENT_ENTITY_REGISTRY_UPDATED, {"action": "update", "entity_id": "light.kitchen", "changes": ["icon"]}, ) await hass.async_block_till_done() diff --git a/tests/components/cloud/test_google_config.py b/tests/components/cloud/test_google_config.py index 478fa22f66c..e7867f52e6c 100644 --- a/tests/components/cloud/test_google_config.py +++ b/tests/components/cloud/test_google_config.py @@ -9,7 +9,8 @@ from homeassistant.components.cloud.google_config import CloudGoogleConfig from homeassistant.components.google_assistant import helpers as ga_helpers from homeassistant.const import EVENT_HOMEASSISTANT_STARTED from homeassistant.core import CoreState, State -from homeassistant.helpers.entity_registry import EVENT_ENTITY_REGISTRY_UPDATED +from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.entity import EntityCategory from homeassistant.util.dt import utcnow from tests.common import async_fire_time_changed, mock_registry @@ -141,7 +142,7 @@ async def test_google_entity_registry_sync(hass, mock_cloud_login, cloud_prefs): ): # Created entity hass.bus.async_fire( - EVENT_ENTITY_REGISTRY_UPDATED, + er.EVENT_ENTITY_REGISTRY_UPDATED, {"action": "create", "entity_id": "light.kitchen"}, ) await hass.async_block_till_done() @@ -150,7 +151,7 @@ async def test_google_entity_registry_sync(hass, mock_cloud_login, cloud_prefs): # Removed entity hass.bus.async_fire( - EVENT_ENTITY_REGISTRY_UPDATED, + er.EVENT_ENTITY_REGISTRY_UPDATED, {"action": "remove", "entity_id": "light.kitchen"}, ) await hass.async_block_till_done() @@ -159,7 +160,7 @@ async def test_google_entity_registry_sync(hass, mock_cloud_login, cloud_prefs): # Entity registry updated with relevant changes hass.bus.async_fire( - EVENT_ENTITY_REGISTRY_UPDATED, + er.EVENT_ENTITY_REGISTRY_UPDATED, { "action": "update", "entity_id": "light.kitchen", @@ -172,7 +173,7 @@ async def test_google_entity_registry_sync(hass, mock_cloud_login, cloud_prefs): # Entity registry updated with non-relevant changes hass.bus.async_fire( - EVENT_ENTITY_REGISTRY_UPDATED, + er.EVENT_ENTITY_REGISTRY_UPDATED, {"action": "update", "entity_id": "light.kitchen", "changes": ["icon"]}, ) await hass.async_block_till_done() @@ -182,7 +183,7 @@ async def test_google_entity_registry_sync(hass, mock_cloud_login, cloud_prefs): # When hass is not started yet we wait till started hass.state = CoreState.starting hass.bus.async_fire( - EVENT_ENTITY_REGISTRY_UPDATED, + er.EVENT_ENTITY_REGISTRY_UPDATED, {"action": "create", "entity_id": "light.kitchen"}, ) await hass.async_block_till_done() @@ -227,21 +228,35 @@ async def test_google_config_expose_entity_prefs(hass, mock_conf, cloud_prefs): "test", "light_config_id", suggested_object_id="config_light", - entity_category="config", + entity_category=EntityCategory.CONFIG, ) entity_entry2 = entity_registry.async_get_or_create( "light", "test", "light_diagnostic_id", suggested_object_id="diagnostic_light", - entity_category="diagnostic", + entity_category=EntityCategory.DIAGNOSTIC, ) entity_entry3 = entity_registry.async_get_or_create( "light", "test", "light_system_id", suggested_object_id="system_light", - entity_category="system", + entity_category=EntityCategory.SYSTEM, + ) + entity_entry4 = entity_registry.async_get_or_create( + "light", + "test", + "light_hidden_integration_id", + suggested_object_id="hidden_integration_light", + hidden_by=er.RegistryEntryHider.INTEGRATION, + ) + entity_entry5 = entity_registry.async_get_or_create( + "light", + "test", + "light_hidden_user_id", + suggested_object_id="hidden_user_light", + hidden_by=er.RegistryEntryHider.USER, ) entity_conf = {"should_expose": False} @@ -254,25 +269,33 @@ async def test_google_config_expose_entity_prefs(hass, mock_conf, cloud_prefs): state_config = State(entity_entry1.entity_id, "on") state_diagnostic = State(entity_entry2.entity_id, "on") state_system = State(entity_entry3.entity_id, "on") + state_hidden_integration = State(entity_entry4.entity_id, "on") + state_hidden_user = State(entity_entry5.entity_id, "on") assert not mock_conf.should_expose(state) assert not mock_conf.should_expose(state_config) assert not mock_conf.should_expose(state_diagnostic) assert not mock_conf.should_expose(state_system) + assert not mock_conf.should_expose(state_hidden_integration) + assert not mock_conf.should_expose(state_hidden_user) entity_conf["should_expose"] = True assert mock_conf.should_expose(state) - # config and diagnostic entities should not be exposed + # categorized and hidden entities should not be exposed assert not mock_conf.should_expose(state_config) assert not mock_conf.should_expose(state_diagnostic) assert not mock_conf.should_expose(state_system) + assert not mock_conf.should_expose(state_hidden_integration) + assert not mock_conf.should_expose(state_hidden_user) entity_conf["should_expose"] = None assert mock_conf.should_expose(state) - # config and diagnostic entities should not be exposed + # categorized and hidden entities should not be exposed assert not mock_conf.should_expose(state_config) assert not mock_conf.should_expose(state_diagnostic) assert not mock_conf.should_expose(state_system) + assert not mock_conf.should_expose(state_hidden_integration) + assert not mock_conf.should_expose(state_hidden_user) await cloud_prefs.async_update( google_default_expose=["sensor"], diff --git a/tests/components/cloud/test_http_api.py b/tests/components/cloud/test_http_api.py index 23605268649..4d0729d72b2 100644 --- a/tests/components/cloud/test_http_api.py +++ b/tests/components/cloud/test_http_api.py @@ -430,6 +430,7 @@ async def test_websocket_status( "remote_connected": False, "remote_certificate": None, "http_use_ssl": False, + "active_subscription": False, } @@ -438,7 +439,11 @@ async def test_websocket_status_not_logged_in(hass, hass_ws_client): client = await hass_ws_client(hass) await client.send_json({"id": 5, "type": "cloud/status"}) response = await client.receive_json() - assert response["result"] == {"logged_in": False, "cloud": "disconnected"} + assert response["result"] == { + "logged_in": False, + "cloud": "disconnected", + "http_use_ssl": False, + } async def test_websocket_subscription_info( diff --git a/tests/components/coinbase/test_config_flow.py b/tests/components/coinbase/test_config_flow.py index 3eac65e04e2..b4927ca1b66 100644 --- a/tests/components/coinbase/test_config_flow.py +++ b/tests/components/coinbase/test_config_flow.py @@ -8,6 +8,7 @@ from requests.models import Response from homeassistant import config_entries from homeassistant.components.coinbase.const import ( CONF_CURRENCIES, + CONF_EXCHANGE_PRECISION, CONF_EXCHANGE_RATES, CONF_YAML_API_TOKEN, DOMAIN, @@ -211,6 +212,7 @@ async def test_option_form(hass): user_input={ CONF_CURRENCIES: [GOOD_CURRENCY], CONF_EXCHANGE_RATES: [GOOD_EXCHANGE_RATE], + CONF_EXCHANGE_PRECISION: 5, }, ) assert result2["type"] == "create_entry" @@ -237,6 +239,7 @@ async def test_form_bad_account_currency(hass): user_input={ CONF_CURRENCIES: [BAD_CURRENCY], CONF_EXCHANGE_RATES: [], + CONF_EXCHANGE_PRECISION: 5, }, ) @@ -263,6 +266,7 @@ async def test_form_bad_exchange_rate(hass): user_input={ CONF_CURRENCIES: [], CONF_EXCHANGE_RATES: [BAD_EXCHANGE_RATE], + CONF_EXCHANGE_PRECISION: 5, }, ) assert result2["type"] == "form" @@ -293,6 +297,7 @@ async def test_option_catch_all_exception(hass): user_input={ CONF_CURRENCIES: [], CONF_EXCHANGE_RATES: ["ETH"], + CONF_EXCHANGE_PRECISION: 5, }, ) diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index 06b9f1ae7f6..6366eca4c6d 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -23,6 +23,13 @@ from tests.common import ( ) +@pytest.fixture +def clear_handlers(): + """Clear config entry handlers.""" + with patch.dict(HANDLERS, clear=True): + yield + + @pytest.fixture(autouse=True) def mock_test_component(hass): """Ensure a component called 'test' exists.""" @@ -30,104 +37,133 @@ def mock_test_component(hass): @pytest.fixture -def client(hass, hass_client): +async def client(hass, hass_client): """Fixture that can interact with the config manager API.""" - hass.loop.run_until_complete(async_setup_component(hass, "http", {})) - hass.loop.run_until_complete(config_entries.async_setup(hass)) - yield hass.loop.run_until_complete(hass_client()) + await async_setup_component(hass, "http", {}) + await config_entries.async_setup(hass) + return await hass_client() -async def test_get_entries(hass, client): +async def test_get_entries(hass, client, clear_handlers): """Test get entries.""" - with patch.dict(HANDLERS, clear=True): + mock_integration(hass, MockModule("comp1")) + mock_integration( + hass, MockModule("comp2", partial_manifest={"integration_type": "helper"}) + ) + mock_integration(hass, MockModule("comp3")) - @HANDLERS.register("comp1") - class Comp1ConfigFlow: - """Config flow with options flow.""" + @HANDLERS.register("comp1") + class Comp1ConfigFlow: + """Config flow with options flow.""" - @staticmethod - @callback - def async_get_options_flow(config_entry): - """Get options flow.""" - pass + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Get options flow.""" + pass - @classmethod - @callback - def async_supports_options_flow(cls, config_entry): - """Return options flow support for this handler.""" - return True + @classmethod + @callback + def async_supports_options_flow(cls, config_entry): + """Return options flow support for this handler.""" + return True - hass.helpers.config_entry_flow.register_discovery_flow( - "comp2", "Comp 2", lambda: None - ) + hass.helpers.config_entry_flow.register_discovery_flow( + "comp2", "Comp 2", lambda: None + ) - entry = MockConfigEntry( - domain="comp1", - title="Test 1", - source="bla", - ) - entry.supports_unload = True - entry.add_to_hass(hass) - MockConfigEntry( - domain="comp2", - title="Test 2", - source="bla2", - state=core_ce.ConfigEntryState.SETUP_ERROR, - reason="Unsupported API", - ).add_to_hass(hass) - MockConfigEntry( - domain="comp3", - title="Test 3", - source="bla3", - disabled_by=core_ce.ConfigEntryDisabler.USER, - ).add_to_hass(hass) + entry = MockConfigEntry( + domain="comp1", + title="Test 1", + source="bla", + ) + entry.supports_unload = True + entry.add_to_hass(hass) + MockConfigEntry( + domain="comp2", + title="Test 2", + source="bla2", + state=core_ce.ConfigEntryState.SETUP_ERROR, + reason="Unsupported API", + ).add_to_hass(hass) + MockConfigEntry( + domain="comp3", + title="Test 3", + source="bla3", + disabled_by=core_ce.ConfigEntryDisabler.USER, + ).add_to_hass(hass) - resp = await client.get("/api/config/config_entries/entry") - assert resp.status == HTTPStatus.OK - data = await resp.json() - for entry in data: - entry.pop("entry_id") - assert data == [ - { - "domain": "comp1", - "title": "Test 1", - "source": "bla", - "state": core_ce.ConfigEntryState.NOT_LOADED.value, - "supports_options": True, - "supports_remove_device": False, - "supports_unload": True, - "pref_disable_new_entities": False, - "pref_disable_polling": False, - "disabled_by": None, - "reason": None, - }, - { - "domain": "comp2", - "title": "Test 2", - "source": "bla2", - "state": core_ce.ConfigEntryState.SETUP_ERROR.value, - "supports_options": False, - "supports_remove_device": False, - "supports_unload": False, - "pref_disable_new_entities": False, - "pref_disable_polling": False, - "disabled_by": None, - "reason": "Unsupported API", - }, - { - "domain": "comp3", - "title": "Test 3", - "source": "bla3", - "state": core_ce.ConfigEntryState.NOT_LOADED.value, - "supports_options": False, - "supports_remove_device": False, - "supports_unload": False, - "pref_disable_new_entities": False, - "pref_disable_polling": False, - "disabled_by": core_ce.ConfigEntryDisabler.USER, - "reason": None, - }, - ] + resp = await client.get("/api/config/config_entries/entry") + assert resp.status == HTTPStatus.OK + data = await resp.json() + for entry in data: + entry.pop("entry_id") + assert data == [ + { + "domain": "comp1", + "title": "Test 1", + "source": "bla", + "state": core_ce.ConfigEntryState.NOT_LOADED.value, + "supports_options": True, + "supports_remove_device": False, + "supports_unload": True, + "pref_disable_new_entities": False, + "pref_disable_polling": False, + "disabled_by": None, + "reason": None, + }, + { + "domain": "comp2", + "title": "Test 2", + "source": "bla2", + "state": core_ce.ConfigEntryState.SETUP_ERROR.value, + "supports_options": False, + "supports_remove_device": False, + "supports_unload": False, + "pref_disable_new_entities": False, + "pref_disable_polling": False, + "disabled_by": None, + "reason": "Unsupported API", + }, + { + "domain": "comp3", + "title": "Test 3", + "source": "bla3", + "state": core_ce.ConfigEntryState.NOT_LOADED.value, + "supports_options": False, + "supports_remove_device": False, + "supports_unload": False, + "pref_disable_new_entities": False, + "pref_disable_polling": False, + "disabled_by": core_ce.ConfigEntryDisabler.USER, + "reason": None, + }, + ] + + resp = await client.get("/api/config/config_entries/entry?domain=comp3") + assert resp.status == HTTPStatus.OK + data = await resp.json() + assert len(data) == 1 + assert data[0]["domain"] == "comp3" + + resp = await client.get("/api/config/config_entries/entry?domain=comp3&type=helper") + assert resp.status == HTTPStatus.OK + data = await resp.json() + assert len(data) == 0 + + resp = await client.get( + "/api/config/config_entries/entry?domain=comp3&type=integration" + ) + assert resp.status == HTTPStatus.OK + data = await resp.json() + assert len(data) == 1 + + resp = await client.get("/api/config/config_entries/entry?type=integration") + assert resp.status == HTTPStatus.OK + data = await resp.json() + assert len(data) == 2 + assert data[0]["domain"] == "comp1" + assert data[1]["domain"] == "comp3" async def test_remove_entry(hass, client): @@ -224,13 +260,28 @@ async def test_reload_entry_in_setup_retry(hass, client, hass_admin_user): assert len(hass.config_entries.async_entries()) == 1 -async def test_available_flows(hass, client): +@pytest.mark.parametrize( + "type_filter,result", + ( + (None, {"hello", "another", "world"}), + ("integration", {"hello", "another"}), + ("helper", {"world"}), + ), +) +async def test_available_flows(hass, client, type_filter, result): """Test querying the available flows.""" - with patch.object(config_flows, "FLOWS", ["hello", "world"]): - resp = await client.get("/api/config/config_entries/flow_handlers") + with patch.object( + config_flows, + "FLOWS", + {"integration": ["hello", "another"], "helper": ["world"]}, + ): + resp = await client.get( + "/api/config/config_entries/flow_handlers", + params={"type": type_filter} if type_filter else {}, + ) assert resp.status == HTTPStatus.OK data = await resp.json() - assert set(data) == {"hello", "world"} + assert set(data) == result ############################ @@ -1017,7 +1068,7 @@ async def test_ignore_flow(hass, hass_ws_client): async def async_step_user(self, user_input=None): await self.async_set_unique_id("mock-unique-id") - return self.async_show_form(step_id="account", data_schema=vol.Schema({})) + return self.async_show_form(step_id="account") ws_client = await hass_ws_client(hass) diff --git a/tests/components/config/test_core.py b/tests/components/config/test_core.py index b78ed50cdf2..33309f6b6c6 100644 --- a/tests/components/config/test_core.py +++ b/tests/components/config/test_core.py @@ -10,8 +10,6 @@ from homeassistant.components.websocket_api.const import TYPE_RESULT from homeassistant.const import CONF_UNIT_SYSTEM, CONF_UNIT_SYSTEM_IMPERIAL from homeassistant.util import dt as dt_util, location -ORIG_TIME_ZONE = dt_util.DEFAULT_TIME_ZONE - @pytest.fixture async def client(hass, hass_ws_client): diff --git a/tests/components/config/test_entity_registry.py b/tests/components/config/test_entity_registry.py index b4065d855ff..e74e43de701 100644 --- a/tests/components/config/test_entity_registry.py +++ b/tests/components/config/test_entity_registry.py @@ -4,7 +4,11 @@ import pytest from homeassistant.components.config import entity_registry from homeassistant.const import ATTR_ICON from homeassistant.helpers.device_registry import DeviceEntryDisabler -from homeassistant.helpers.entity_registry import RegistryEntry, RegistryEntryDisabler +from homeassistant.helpers.entity_registry import ( + RegistryEntry, + RegistryEntryDisabler, + RegistryEntryHider, +) from tests.common import ( MockConfigEntry, @@ -57,6 +61,7 @@ async def test_list_entities(hass, client): "area_id": None, "disabled_by": None, "entity_id": "test_domain.name", + "hidden_by": None, "name": "Hello World", "icon": None, "platform": "test_platform", @@ -68,6 +73,7 @@ async def test_list_entities(hass, client): "area_id": None, "disabled_by": None, "entity_id": "test_domain.no_name", + "hidden_by": None, "name": None, "icon": None, "platform": "test_platform", @@ -109,8 +115,10 @@ async def test_get_entity(hass, client): "disabled_by": None, "entity_category": None, "entity_id": "test_domain.name", + "hidden_by": None, "icon": None, "name": "Hello World", + "options": {}, "original_device_class": None, "original_icon": None, "original_name": None, @@ -136,8 +144,10 @@ async def test_get_entity(hass, client): "disabled_by": None, "entity_category": None, "entity_id": "test_domain.no_name", + "hidden_by": None, "icon": None, "name": None, + "options": {}, "original_device_class": None, "original_icon": None, "original_name": None, @@ -170,7 +180,7 @@ async def test_update_entity(hass, client): assert state.name == "before update" assert state.attributes[ATTR_ICON] == "icon:before update" - # UPDATE AREA, DEVICE_CLASS, ICON AND NAME + # UPDATE AREA, DEVICE_CLASS, HIDDEN_BY, ICON AND NAME await client.send_json( { "id": 6, @@ -178,6 +188,7 @@ async def test_update_entity(hass, client): "entity_id": "test_domain.world", "area_id": "mock-area-id", "device_class": "custom_device_class", + "hidden_by": "user", # We exchange strings over the WS API, not enums "icon": "icon:after update", "name": "after update", } @@ -195,8 +206,10 @@ async def test_update_entity(hass, client): "disabled_by": None, "entity_category": None, "entity_id": "test_domain.world", + "hidden_by": "user", # We exchange strings over the WS API, not enums "icon": "icon:after update", "name": "after update", + "options": {}, "original_device_class": None, "original_icon": None, "original_name": None, @@ -209,17 +222,33 @@ async def test_update_entity(hass, client): assert state.name == "after update" assert state.attributes[ATTR_ICON] == "icon:after update" - # UPDATE DISABLED_BY TO USER + # UPDATE HIDDEN_BY TO ILLEGAL VALUE await client.send_json( { "id": 7, "type": "config/entity_registry/update", "entity_id": "test_domain.world", - "disabled_by": RegistryEntryDisabler.USER, + "hidden_by": "ivy", } ) msg = await client.receive_json() + assert not msg["success"] + + assert registry.entities["test_domain.world"].hidden_by is RegistryEntryHider.USER + + # UPDATE DISABLED_BY TO USER + await client.send_json( + { + "id": 8, + "type": "config/entity_registry/update", + "entity_id": "test_domain.world", + "disabled_by": "user", # We exchange strings over the WS API, not enums + } + ) + + msg = await client.receive_json() + assert msg["success"] assert hass.states.get("test_domain.world") is None assert ( @@ -229,7 +258,7 @@ async def test_update_entity(hass, client): # UPDATE DISABLED_BY TO NONE await client.send_json( { - "id": 8, + "id": 9, "type": "config/entity_registry/update", "entity_id": "test_domain.world", "disabled_by": None, @@ -248,8 +277,10 @@ async def test_update_entity(hass, client): "disabled_by": None, "entity_category": None, "entity_id": "test_domain.world", + "hidden_by": "user", # We exchange strings over the WS API, not enums "icon": "icon:after update", "name": "after update", + "options": {}, "original_device_class": None, "original_icon": None, "original_name": None, @@ -259,6 +290,41 @@ async def test_update_entity(hass, client): "reload_delay": 30, } + # UPDATE ENTITY OPTION + await client.send_json( + { + "id": 10, + "type": "config/entity_registry/update", + "entity_id": "test_domain.world", + "options_domain": "sensor", + "options": {"unit_of_measurement": "beard_second"}, + } + ) + + msg = await client.receive_json() + + assert msg["result"] == { + "entity_entry": { + "area_id": "mock-area-id", + "capabilities": None, + "config_entry_id": None, + "device_class": "custom_device_class", + "device_id": None, + "disabled_by": None, + "entity_category": None, + "entity_id": "test_domain.world", + "hidden_by": "user", # We exchange strings over the WS API, not enums + "icon": "icon:after update", + "name": "after update", + "options": {"sensor": {"unit_of_measurement": "beard_second"}}, + "original_device_class": None, + "original_icon": None, + "original_name": None, + "platform": "test_platform", + "unique_id": "1234", + }, + } + async def test_update_entity_require_restart(hass, client): """Test updating entity.""" @@ -306,7 +372,9 @@ async def test_update_entity_require_restart(hass, client): "entity_category": None, "entity_id": "test_domain.world", "icon": None, + "hidden_by": None, "name": None, + "options": {}, "original_device_class": None, "original_icon": None, "original_name": None, @@ -409,8 +477,10 @@ async def test_update_entity_no_changes(hass, client): "disabled_by": None, "entity_category": None, "entity_id": "test_domain.world", + "hidden_by": None, "icon": None, "name": "name of entity", + "options": {}, "original_device_class": None, "original_icon": None, "original_name": None, @@ -492,8 +562,10 @@ async def test_update_entity_id(hass, client): "disabled_by": None, "entity_category": None, "entity_id": "test_domain.planet", + "hidden_by": None, "icon": None, "name": None, + "options": {}, "original_device_class": None, "original_icon": None, "original_name": None, diff --git a/tests/components/config/test_init.py b/tests/components/config/test_init.py index dd3e294bac3..2d5610cadfb 100644 --- a/tests/components/config/test_init.py +++ b/tests/components/config/test_init.py @@ -1,49 +1,8 @@ """Test config init.""" - -from unittest.mock import patch - -from homeassistant.components import config -from homeassistant.const import EVENT_COMPONENT_LOADED -from homeassistant.setup import ATTR_COMPONENT, async_setup_component - -from tests.common import mock_component +from homeassistant.setup import async_setup_component async def test_config_setup(hass, loop): """Test it sets up hassbian.""" await async_setup_component(hass, "config", {}) assert "config" in hass.config.components - - -async def test_load_on_demand_already_loaded(hass, aiohttp_client): - """Test getting suites.""" - mock_component(hass, "zwave") - - with patch.object(config, "SECTIONS", []), patch.object( - config, "ON_DEMAND", ["zwave"] - ), patch( - "homeassistant.components.config.zwave.async_setup", return_value=True - ) as stp: - - await async_setup_component(hass, "config", {}) - - await hass.async_block_till_done() - assert stp.called - - -async def test_load_on_demand_on_load(hass, aiohttp_client): - """Test getting suites.""" - with patch.object(config, "SECTIONS", []), patch.object( - config, "ON_DEMAND", ["zwave"] - ): - await async_setup_component(hass, "config", {}) - - assert "config.zwave" not in hass.config.components - - with patch( - "homeassistant.components.config.zwave.async_setup", return_value=True - ) as stp: - hass.bus.async_fire(EVENT_COMPONENT_LOADED, {ATTR_COMPONENT: "zwave"}) - await hass.async_block_till_done() - - assert stp.called diff --git a/tests/components/config/test_zwave.py b/tests/components/config/test_zwave.py deleted file mode 100644 index bc7f22c104f..00000000000 --- a/tests/components/config/test_zwave.py +++ /dev/null @@ -1,542 +0,0 @@ -"""Test Z-Wave config panel.""" -from http import HTTPStatus -import json -from unittest.mock import MagicMock, patch - -import pytest - -from homeassistant.bootstrap import async_setup_component -from homeassistant.components import config -from homeassistant.components.zwave import DATA_NETWORK, const - -from tests.mock.zwave import MockEntityValues, MockNode, MockValue - -VIEW_NAME = "api:config:zwave:device_config" - - -@pytest.fixture -def client(loop, hass, hass_client): - """Client to communicate with Z-Wave config views.""" - with patch.object(config, "SECTIONS", ["zwave"]): - loop.run_until_complete(async_setup_component(hass, "config", {})) - - return loop.run_until_complete(hass_client()) - - -async def test_get_device_config(client): - """Test getting device config.""" - - def mock_read(path): - """Mock reading data.""" - return {"hello.beer": {"free": "beer"}, "other.entity": {"do": "something"}} - - with patch("homeassistant.components.config._read", mock_read): - resp = await client.get("/api/config/zwave/device_config/hello.beer") - - assert resp.status == HTTPStatus.OK - result = await resp.json() - - assert result == {"free": "beer"} - - -async def test_update_device_config(client): - """Test updating device config.""" - orig_data = { - "hello.beer": {"ignored": True}, - "other.entity": {"polling_intensity": 2}, - } - - def mock_read(path): - """Mock reading data.""" - return orig_data - - written = [] - - def mock_write(path, data): - """Mock writing data.""" - written.append(data) - - with patch("homeassistant.components.config._read", mock_read), patch( - "homeassistant.components.config._write", mock_write - ): - resp = await client.post( - "/api/config/zwave/device_config/hello.beer", - data=json.dumps({"polling_intensity": 2}), - ) - - assert resp.status == HTTPStatus.OK - result = await resp.json() - assert result == {"result": "ok"} - - orig_data["hello.beer"]["polling_intensity"] = 2 - - assert written[0] == orig_data - - -async def test_update_device_config_invalid_key(client): - """Test updating device config.""" - resp = await client.post( - "/api/config/zwave/device_config/invalid_entity", - data=json.dumps({"polling_intensity": 2}), - ) - - assert resp.status == HTTPStatus.BAD_REQUEST - - -async def test_update_device_config_invalid_data(client): - """Test updating device config.""" - resp = await client.post( - "/api/config/zwave/device_config/hello.beer", - data=json.dumps({"invalid_option": 2}), - ) - - assert resp.status == HTTPStatus.BAD_REQUEST - - -async def test_update_device_config_invalid_json(client): - """Test updating device config.""" - resp = await client.post( - "/api/config/zwave/device_config/hello.beer", data="not json" - ) - - assert resp.status == HTTPStatus.BAD_REQUEST - - -async def test_get_values(hass, client): - """Test getting values on node.""" - node = MockNode(node_id=1) - value = MockValue( - value_id=123456, - node=node, - label="Test Label", - instance=1, - index=2, - poll_intensity=4, - ) - values = MockEntityValues(primary=value) - node2 = MockNode(node_id=2) - value2 = MockValue(value_id=234567, node=node2, label="Test Label 2") - values2 = MockEntityValues(primary=value2) - hass.data[const.DATA_ENTITY_VALUES] = [values, values2] - - resp = await client.get("/api/zwave/values/1") - - assert resp.status == HTTPStatus.OK - result = await resp.json() - - assert result == { - "123456": { - "label": "Test Label", - "instance": 1, - "index": 2, - "poll_intensity": 4, - } - } - - -async def test_get_groups(hass, client): - """Test getting groupdata on node.""" - network = hass.data[DATA_NETWORK] = MagicMock() - node = MockNode(node_id=2) - node.groups.associations = "assoc" - node.groups.associations_instances = "inst" - node.groups.label = "the label" - node.groups.max_associations = "max" - node.groups = {1: node.groups} - network.nodes = {2: node} - - resp = await client.get("/api/zwave/groups/2") - - assert resp.status == HTTPStatus.OK - result = await resp.json() - - assert result == { - "1": { - "association_instances": "inst", - "associations": "assoc", - "label": "the label", - "max_associations": "max", - } - } - - -async def test_get_groups_nogroups(hass, client): - """Test getting groupdata on node with no groups.""" - network = hass.data[DATA_NETWORK] = MagicMock() - node = MockNode(node_id=2) - - network.nodes = {2: node} - - resp = await client.get("/api/zwave/groups/2") - - assert resp.status == HTTPStatus.OK - result = await resp.json() - - assert result == {} - - -async def test_get_groups_nonode(hass, client): - """Test getting groupdata on nonexisting node.""" - network = hass.data[DATA_NETWORK] = MagicMock() - network.nodes = {1: 1, 5: 5} - - resp = await client.get("/api/zwave/groups/2") - - assert resp.status == HTTPStatus.NOT_FOUND - result = await resp.json() - - assert result == {"message": "Node not found"} - - -async def test_get_config(hass, client): - """Test getting config on node.""" - network = hass.data[DATA_NETWORK] = MagicMock() - node = MockNode(node_id=2) - value = MockValue(index=12, command_class=const.COMMAND_CLASS_CONFIGURATION) - value.label = "label" - value.help = "help" - value.type = "type" - value.data = "data" - value.data_items = ["item1", "item2"] - value.max = "max" - value.min = "min" - node.values = {12: value} - network.nodes = {2: node} - node.get_values.return_value = node.values - - resp = await client.get("/api/zwave/config/2") - - assert resp.status == HTTPStatus.OK - result = await resp.json() - - assert result == { - "12": { - "data": "data", - "data_items": ["item1", "item2"], - "help": "help", - "label": "label", - "max": "max", - "min": "min", - "type": "type", - } - } - - -async def test_get_config_noconfig_node(hass, client): - """Test getting config on node without config.""" - network = hass.data[DATA_NETWORK] = MagicMock() - node = MockNode(node_id=2) - - network.nodes = {2: node} - node.get_values.return_value = node.values - - resp = await client.get("/api/zwave/config/2") - - assert resp.status == HTTPStatus.OK - result = await resp.json() - - assert result == {} - - -async def test_get_config_nonode(hass, client): - """Test getting config on nonexisting node.""" - network = hass.data[DATA_NETWORK] = MagicMock() - network.nodes = {1: 1, 5: 5} - - resp = await client.get("/api/zwave/config/2") - - assert resp.status == HTTPStatus.NOT_FOUND - result = await resp.json() - - assert result == {"message": "Node not found"} - - -async def test_get_usercodes_nonode(hass, client): - """Test getting usercodes on nonexisting node.""" - network = hass.data[DATA_NETWORK] = MagicMock() - network.nodes = {1: 1, 5: 5} - - resp = await client.get("/api/zwave/usercodes/2") - - assert resp.status == HTTPStatus.NOT_FOUND - result = await resp.json() - - assert result == {"message": "Node not found"} - - -async def test_get_usercodes(hass, client): - """Test getting usercodes on node.""" - network = hass.data[DATA_NETWORK] = MagicMock() - node = MockNode(node_id=18, command_classes=[const.COMMAND_CLASS_USER_CODE]) - value = MockValue(index=0, command_class=const.COMMAND_CLASS_USER_CODE) - value.genre = const.GENRE_USER - value.label = "label" - value.data = "1234" - node.values = {0: value} - network.nodes = {18: node} - node.get_values.return_value = node.values - - resp = await client.get("/api/zwave/usercodes/18") - - assert resp.status == HTTPStatus.OK - result = await resp.json() - - assert result == {"0": {"code": "1234", "label": "label", "length": 4}} - - -async def test_get_usercode_nousercode_node(hass, client): - """Test getting usercodes on node without usercodes.""" - network = hass.data[DATA_NETWORK] = MagicMock() - node = MockNode(node_id=18) - - network.nodes = {18: node} - node.get_values.return_value = node.values - - resp = await client.get("/api/zwave/usercodes/18") - - assert resp.status == HTTPStatus.OK - result = await resp.json() - - assert result == {} - - -async def test_get_usercodes_no_genreuser(hass, client): - """Test getting usercodes on node missing genre user.""" - network = hass.data[DATA_NETWORK] = MagicMock() - node = MockNode(node_id=18, command_classes=[const.COMMAND_CLASS_USER_CODE]) - value = MockValue(index=0, command_class=const.COMMAND_CLASS_USER_CODE) - value.genre = const.GENRE_SYSTEM - value.label = "label" - value.data = "1234" - node.values = {0: value} - network.nodes = {18: node} - node.get_values.return_value = node.values - - resp = await client.get("/api/zwave/usercodes/18") - - assert resp.status == HTTPStatus.OK - result = await resp.json() - - assert result == {} - - -async def test_save_config_no_network(hass, client): - """Test saving configuration without network data.""" - resp = await client.post("/api/zwave/saveconfig") - - assert resp.status == HTTPStatus.NOT_FOUND - result = await resp.json() - assert result == {"message": "No Z-Wave network data found"} - - -async def test_save_config(hass, client): - """Test saving configuration.""" - network = hass.data[DATA_NETWORK] = MagicMock() - - resp = await client.post("/api/zwave/saveconfig") - - assert resp.status == HTTPStatus.OK - result = await resp.json() - assert network.write_config.called - assert result == {"message": "Z-Wave configuration saved to file"} - - -async def test_get_protection_values(hass, client): - """Test getting protection values on node.""" - network = hass.data[DATA_NETWORK] = MagicMock() - node = MockNode(node_id=18, command_classes=[const.COMMAND_CLASS_PROTECTION]) - value = MockValue( - value_id=123456, - index=0, - instance=1, - command_class=const.COMMAND_CLASS_PROTECTION, - ) - value.label = "Protection Test" - value.data_items = [ - "Unprotected", - "Protection by Sequence", - "No Operation Possible", - ] - value.data = "Unprotected" - network.nodes = {18: node} - node.value = value - - node.get_protection_item.return_value = "Unprotected" - node.get_protection_items.return_value = value.data_items - node.get_protections.return_value = {value.value_id: "Object"} - - resp = await client.get("/api/zwave/protection/18") - - assert resp.status == HTTPStatus.OK - result = await resp.json() - assert node.get_protections.called - assert node.get_protection_item.called - assert node.get_protection_items.called - assert result == { - "value_id": "123456", - "selected": "Unprotected", - "options": ["Unprotected", "Protection by Sequence", "No Operation Possible"], - } - - -async def test_get_protection_values_nonexisting_node(hass, client): - """Test getting protection values on node with wrong nodeid.""" - network = hass.data[DATA_NETWORK] = MagicMock() - node = MockNode(node_id=18, command_classes=[const.COMMAND_CLASS_PROTECTION]) - value = MockValue( - value_id=123456, - index=0, - instance=1, - command_class=const.COMMAND_CLASS_PROTECTION, - ) - value.label = "Protection Test" - value.data_items = [ - "Unprotected", - "Protection by Sequence", - "No Operation Possible", - ] - value.data = "Unprotected" - network.nodes = {17: node} - node.value = value - - resp = await client.get("/api/zwave/protection/18") - - assert resp.status == HTTPStatus.NOT_FOUND - result = await resp.json() - assert not node.get_protections.called - assert not node.get_protection_item.called - assert not node.get_protection_items.called - assert result == {"message": "Node not found"} - - -async def test_get_protection_values_without_protectionclass(hass, client): - """Test getting protection values on node without protectionclass.""" - network = hass.data[DATA_NETWORK] = MagicMock() - node = MockNode(node_id=18) - value = MockValue(value_id=123456, index=0, instance=1) - network.nodes = {18: node} - node.value = value - - resp = await client.get("/api/zwave/protection/18") - - assert resp.status == HTTPStatus.OK - result = await resp.json() - assert not node.get_protections.called - assert not node.get_protection_item.called - assert not node.get_protection_items.called - assert result == {} - - -async def test_set_protection_value(hass, client): - """Test setting protection value on node.""" - network = hass.data[DATA_NETWORK] = MagicMock() - node = MockNode(node_id=18, command_classes=[const.COMMAND_CLASS_PROTECTION]) - value = MockValue( - value_id=123456, - index=0, - instance=1, - command_class=const.COMMAND_CLASS_PROTECTION, - ) - value.label = "Protection Test" - value.data_items = [ - "Unprotected", - "Protection by Sequence", - "No Operation Possible", - ] - value.data = "Unprotected" - network.nodes = {18: node} - node.value = value - - resp = await client.post( - "/api/zwave/protection/18", - data=json.dumps({"value_id": "123456", "selection": "Protection by Sequence"}), - ) - - assert resp.status == HTTPStatus.OK - result = await resp.json() - assert node.set_protection.called - assert result == {"message": "Protection setting successfully set"} - - -async def test_set_protection_value_failed(hass, client): - """Test setting protection value failed on node.""" - network = hass.data[DATA_NETWORK] = MagicMock() - node = MockNode(node_id=18, command_classes=[const.COMMAND_CLASS_PROTECTION]) - value = MockValue( - value_id=123456, - index=0, - instance=1, - command_class=const.COMMAND_CLASS_PROTECTION, - ) - value.label = "Protection Test" - value.data_items = [ - "Unprotected", - "Protection by Sequence", - "No Operation Possible", - ] - value.data = "Unprotected" - network.nodes = {18: node} - node.value = value - node.set_protection.return_value = False - - resp = await client.post( - "/api/zwave/protection/18", - data=json.dumps({"value_id": "123456", "selection": "Protecton by Sequence"}), - ) - - assert resp.status == HTTPStatus.ACCEPTED - result = await resp.json() - assert node.set_protection.called - assert result == {"message": "Protection setting did not complete"} - - -async def test_set_protection_value_nonexisting_node(hass, client): - """Test setting protection value on nonexisting node.""" - network = hass.data[DATA_NETWORK] = MagicMock() - node = MockNode(node_id=17, command_classes=[const.COMMAND_CLASS_PROTECTION]) - value = MockValue( - value_id=123456, - index=0, - instance=1, - command_class=const.COMMAND_CLASS_PROTECTION, - ) - value.label = "Protection Test" - value.data_items = [ - "Unprotected", - "Protection by Sequence", - "No Operation Possible", - ] - value.data = "Unprotected" - network.nodes = {17: node} - node.value = value - node.set_protection.return_value = False - - resp = await client.post( - "/api/zwave/protection/18", - data=json.dumps({"value_id": "123456", "selection": "Protecton by Sequence"}), - ) - - assert resp.status == HTTPStatus.NOT_FOUND - result = await resp.json() - assert not node.set_protection.called - assert result == {"message": "Node not found"} - - -async def test_set_protection_value_missing_class(hass, client): - """Test setting protection value on node without protectionclass.""" - network = hass.data[DATA_NETWORK] = MagicMock() - node = MockNode(node_id=17) - value = MockValue(value_id=123456, index=0, instance=1) - network.nodes = {17: node} - node.value = value - node.set_protection.return_value = False - - resp = await client.post( - "/api/zwave/protection/17", - data=json.dumps({"value_id": "123456", "selection": "Protecton by Sequence"}), - ) - - assert resp.status == HTTPStatus.NOT_FOUND - result = await resp.json() - assert not node.set_protection.called - assert result == {"message": "No protection commandclass on this node"} diff --git a/tests/components/conftest.py b/tests/components/conftest.py index 9e029e159a1..f153263cbc6 100644 --- a/tests/components/conftest.py +++ b/tests/components/conftest.py @@ -1,5 +1,6 @@ """Fixtures for component testing.""" -from unittest.mock import patch +from collections.abc import Generator +from unittest.mock import AsyncMock, patch import pytest @@ -22,3 +23,13 @@ def prevent_io(): return_value=[], ): yield + + +@pytest.fixture +def entity_registry_enabled_by_default() -> Generator[AsyncMock, None, None]: + """Test fixture that ensures all entities are enabled in the registry.""" + with patch( + "homeassistant.helpers.entity.Entity.entity_registry_enabled_default", + return_value=True, + ) as mock_entity_registry_enabled_by_default: + yield mock_entity_registry_enabled_by_default diff --git a/tests/components/counter/test_reproduce_state.py b/tests/components/counter/test_reproduce_state.py index 7b7816f8272..876c441d60c 100644 --- a/tests/components/counter/test_reproduce_state.py +++ b/tests/components/counter/test_reproduce_state.py @@ -1,5 +1,6 @@ """Test reproduce state for Counter.""" from homeassistant.core import State +from homeassistant.helpers.state import async_reproduce_state from tests.common import async_mock_service @@ -16,7 +17,8 @@ async def test_reproducing_states(hass, caplog): configure_calls = async_mock_service(hass, "counter", "configure") # These calls should do nothing as entities already in desired state - await hass.helpers.state.async_reproduce_state( + await async_reproduce_state( + hass, [ State("counter.entity", "5"), State( @@ -24,21 +26,20 @@ async def test_reproducing_states(hass, caplog): "8", {"initial": 12, "minimum": 5, "maximum": 15, "step": 3}, ), - ] + ], ) assert len(configure_calls) == 0 # Test invalid state is handled - await hass.helpers.state.async_reproduce_state( - [State("counter.entity", "not_supported")] - ) + await async_reproduce_state(hass, [State("counter.entity", "not_supported")]) assert "not_supported" in caplog.text assert len(configure_calls) == 0 # Make sure correct services are called - await hass.helpers.state.async_reproduce_state( + await async_reproduce_state( + hass, [ State("counter.entity", "2"), State( @@ -48,7 +49,7 @@ async def test_reproducing_states(hass, caplog): ), # Should not raise State("counter.non_existing", "6"), - ] + ], ) valid_calls = [ diff --git a/tests/components/cover/test_reproduce_state.py b/tests/components/cover/test_reproduce_state.py index aec4c8c2324..d51f7ed06e2 100644 --- a/tests/components/cover/test_reproduce_state.py +++ b/tests/components/cover/test_reproduce_state.py @@ -16,6 +16,7 @@ from homeassistant.const import ( STATE_OPEN, ) from homeassistant.core import State +from homeassistant.helpers.state import async_reproduce_state from tests.common import async_mock_service @@ -61,7 +62,8 @@ async def test_reproducing_states(hass, caplog): ) # These calls should do nothing as entities already in desired state - await hass.helpers.state.async_reproduce_state( + await async_reproduce_state( + hass, [ State("cover.entity_close", STATE_CLOSED), State( @@ -93,7 +95,7 @@ async def test_reproducing_states(hass, caplog): STATE_OPEN, {ATTR_CURRENT_POSITION: 100, ATTR_CURRENT_TILT_POSITION: 100}, ), - ] + ], ) assert len(close_calls) == 0 @@ -104,9 +106,7 @@ async def test_reproducing_states(hass, caplog): assert len(position_tilt_calls) == 0 # Test invalid state is handled - await hass.helpers.state.async_reproduce_state( - [State("cover.entity_close", "not_supported")] - ) + await async_reproduce_state(hass, [State("cover.entity_close", "not_supported")]) assert "not_supported" in caplog.text assert len(close_calls) == 0 @@ -117,7 +117,8 @@ async def test_reproducing_states(hass, caplog): assert len(position_tilt_calls) == 0 # Make sure correct services are called - await hass.helpers.state.async_reproduce_state( + await async_reproduce_state( + hass, [ State("cover.entity_close", STATE_OPEN), State( diff --git a/tests/components/cpuspeed/test_config_flow.py b/tests/components/cpuspeed/test_config_flow.py index 14563c82bff..8f12092f389 100644 --- a/tests/components/cpuspeed/test_config_flow.py +++ b/tests/components/cpuspeed/test_config_flow.py @@ -3,8 +3,7 @@ from unittest.mock import AsyncMock, MagicMock from homeassistant.components.cpuspeed.const import DOMAIN -from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER -from homeassistant.const import CONF_NAME +from homeassistant.config_entries import SOURCE_USER from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import ( RESULT_TYPE_ABORT, @@ -62,26 +61,6 @@ async def test_already_configured( assert len(mock_cpuinfo_config_flow.mock_calls) == 0 -async def test_import_flow( - hass: HomeAssistant, - mock_cpuinfo_config_flow: MagicMock, - mock_setup_entry: AsyncMock, -) -> None: - """Test the import configuration flow.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={CONF_NAME: "Frenck's CPU"}, - ) - - assert result.get("type") == RESULT_TYPE_CREATE_ENTRY - assert result.get("title") == "Frenck's CPU" - assert result.get("data") == {} - - assert len(mock_setup_entry.mock_calls) == 1 - assert len(mock_cpuinfo_config_flow.mock_calls) == 1 - - async def test_not_compatible( hass: HomeAssistant, mock_cpuinfo_config_flow: MagicMock, diff --git a/tests/components/cpuspeed/test_init.py b/tests/components/cpuspeed/test_init.py index 2352e411b8e..cdb86ba2f46 100644 --- a/tests/components/cpuspeed/test_init.py +++ b/tests/components/cpuspeed/test_init.py @@ -4,10 +4,8 @@ from unittest.mock import MagicMock import pytest from homeassistant.components.cpuspeed.const import DOMAIN -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant -from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry @@ -48,19 +46,3 @@ async def test_config_entry_not_compatible( assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR assert len(mock_cpuinfo.mock_calls) == 1 assert "is not compatible with your system" in caplog.text - - -async def test_import_config( - hass: HomeAssistant, - mock_cpuinfo: MagicMock, - caplog: pytest.LogCaptureFixture, -) -> None: - """Test the CPU Speed being set up from config via import.""" - assert await async_setup_component( - hass, SENSOR_DOMAIN, {SENSOR_DOMAIN: {"platform": DOMAIN}} - ) - await hass.async_block_till_done() - - assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert len(mock_cpuinfo.mock_calls) == 3 - assert "the CPU Speed platform in YAML is deprecated" in caplog.text diff --git a/tests/components/deconz/test_device_trigger.py b/tests/components/deconz/test_device_trigger.py index 4ae8fd32e45..de6022fa56d 100644 --- a/tests/components/deconz/test_device_trigger.py +++ b/tests/components/deconz/test_device_trigger.py @@ -5,6 +5,13 @@ from unittest.mock import Mock, patch import pytest from homeassistant.components.automation import DOMAIN as AUTOMATION_DOMAIN +from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN +from homeassistant.components.binary_sensor.device_trigger import ( + CONF_BAT_LOW, + CONF_NOT_BAT_LOW, + CONF_NOT_TAMPERED, + CONF_TAMPERED, +) from homeassistant.components.deconz import device_trigger from homeassistant.components.deconz.const import DOMAIN as DECONZ_DOMAIN from homeassistant.components.deconz.device_trigger import CONF_SUBTYPE @@ -18,6 +25,7 @@ from homeassistant.const import ( CONF_PLATFORM, CONF_TYPE, ) +from homeassistant.helpers import device_registry as dr from homeassistant.helpers.trigger import async_initialize_triggers from homeassistant.setup import async_setup_component @@ -65,7 +73,7 @@ async def test_get_triggers(hass, aioclient_mock): with patch.dict(DECONZ_WEB_REQUEST, data): await setup_deconz_integration(hass, aioclient_mock) - device_registry = await hass.helpers.device_registry.async_get_registry() + device_registry = dr.async_get(hass) device = device_registry.async_get_device( identifiers={(DECONZ_DOMAIN, "d0:cf:5e:ff:fe:71:a4:3a")} ) @@ -129,6 +137,91 @@ async def test_get_triggers(hass, aioclient_mock): assert_lists_same(triggers, expected_triggers) +async def test_get_triggers_for_alarm_event(hass, aioclient_mock): + """Test triggers work.""" + data = { + "sensors": { + "1": { + "config": { + "battery": 95, + "enrolled": 1, + "on": True, + "pending": [], + "reachable": True, + }, + "ep": 1, + "etag": "5aaa1c6bae8501f59929539c6e8f44d6", + "lastseen": "2021-07-25T18:07Z", + "manufacturername": "lk", + "modelid": "ZB-KeypadGeneric-D0002", + "name": "Keypad", + "state": { + "action": "armed_stay", + "lastupdated": "2021-07-25T18:02:51.172", + "lowbattery": False, + "panel": "exit_delay", + "seconds_remaining": 55, + "tampered": False, + }, + "swversion": "3.13", + "type": "ZHAAncillaryControl", + "uniqueid": "00:00:00:00:00:00:00:00-00", + } + } + } + with patch.dict(DECONZ_WEB_REQUEST, data): + await setup_deconz_integration(hass, aioclient_mock) + + device_registry = dr.async_get(hass) + device = device_registry.async_get_device( + identifiers={(DECONZ_DOMAIN, "00:00:00:00:00:00:00:00")} + ) + + triggers = await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, device.id + ) + + expected_triggers = [ + { + CONF_DEVICE_ID: device.id, + CONF_DOMAIN: BINARY_SENSOR_DOMAIN, + ATTR_ENTITY_ID: "binary_sensor.keypad_low_battery", + CONF_PLATFORM: "device", + CONF_TYPE: CONF_BAT_LOW, + }, + { + CONF_DEVICE_ID: device.id, + CONF_DOMAIN: BINARY_SENSOR_DOMAIN, + ATTR_ENTITY_ID: "binary_sensor.keypad_low_battery", + CONF_PLATFORM: "device", + CONF_TYPE: CONF_NOT_BAT_LOW, + }, + { + CONF_DEVICE_ID: device.id, + CONF_DOMAIN: BINARY_SENSOR_DOMAIN, + ATTR_ENTITY_ID: "binary_sensor.keypad_tampered", + CONF_PLATFORM: "device", + CONF_TYPE: CONF_TAMPERED, + }, + { + CONF_DEVICE_ID: device.id, + CONF_DOMAIN: BINARY_SENSOR_DOMAIN, + ATTR_ENTITY_ID: "binary_sensor.keypad_tampered", + CONF_PLATFORM: "device", + CONF_TYPE: CONF_NOT_TAMPERED, + }, + { + CONF_DEVICE_ID: device.id, + CONF_DOMAIN: SENSOR_DOMAIN, + ATTR_ENTITY_ID: "sensor.keypad_battery", + CONF_PLATFORM: "device", + CONF_TYPE: ATTR_BATTERY_LEVEL, + }, + ] + + assert_lists_same(triggers, expected_triggers) + + async def test_get_triggers_manage_unsupported_remotes(hass, aioclient_mock): """Verify no triggers for an unsupported remote.""" data = { @@ -156,7 +249,7 @@ async def test_get_triggers_manage_unsupported_remotes(hass, aioclient_mock): with patch.dict(DECONZ_WEB_REQUEST, data): await setup_deconz_integration(hass, aioclient_mock) - device_registry = await hass.helpers.device_registry.async_get_registry() + device_registry = dr.async_get(hass) device = device_registry.async_get_device( identifiers={(DECONZ_DOMAIN, "d0:cf:5e:ff:fe:71:a4:3a")} ) @@ -201,7 +294,7 @@ async def test_functional_device_trigger( with patch.dict(DECONZ_WEB_REQUEST, data): await setup_deconz_integration(hass, aioclient_mock) - device_registry = await hass.helpers.device_registry.async_get_registry() + device_registry = dr.async_get(hass) device = device_registry.async_get_device( identifiers={(DECONZ_DOMAIN, "d0:cf:5e:ff:fe:71:a4:3a")} ) @@ -244,9 +337,7 @@ async def test_functional_device_trigger( assert automation_calls[0].data["some"] == "test_trigger_button_press" -async def test_validate_trigger_unknown_device( - hass, aioclient_mock, mock_deconz_websocket -): +async def test_validate_trigger_unknown_device(hass, aioclient_mock): """Test unknown device does not return a trigger config.""" await setup_deconz_integration(hass, aioclient_mock) @@ -276,13 +367,11 @@ async def test_validate_trigger_unknown_device( assert len(hass.states.async_entity_ids(AUTOMATION_DOMAIN)) == 0 -async def test_validate_trigger_unsupported_device( - hass, aioclient_mock, mock_deconz_websocket -): +async def test_validate_trigger_unsupported_device(hass, aioclient_mock): """Test unsupported device doesn't return a trigger config.""" config_entry = await setup_deconz_integration(hass, aioclient_mock) - device_registry = await hass.helpers.device_registry.async_get_registry() + device_registry = dr.async_get(hass) device = device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, identifiers={(DECONZ_DOMAIN, "d0:cf:5e:ff:fe:71:a4:3a")}, @@ -315,13 +404,11 @@ async def test_validate_trigger_unsupported_device( assert len(hass.states.async_entity_ids(AUTOMATION_DOMAIN)) == 0 -async def test_validate_trigger_unsupported_trigger( - hass, aioclient_mock, mock_deconz_websocket -): +async def test_validate_trigger_unsupported_trigger(hass, aioclient_mock): """Test unsupported trigger does not return a trigger config.""" config_entry = await setup_deconz_integration(hass, aioclient_mock) - device_registry = await hass.helpers.device_registry.async_get_registry() + device_registry = dr.async_get(hass) device = device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, identifiers={(DECONZ_DOMAIN, "d0:cf:5e:ff:fe:71:a4:3a")}, @@ -356,13 +443,11 @@ async def test_validate_trigger_unsupported_trigger( assert len(hass.states.async_entity_ids(AUTOMATION_DOMAIN)) == 0 -async def test_attach_trigger_no_matching_event( - hass, aioclient_mock, mock_deconz_websocket -): +async def test_attach_trigger_no_matching_event(hass, aioclient_mock): """Test no matching event for device doesn't return a trigger config.""" config_entry = await setup_deconz_integration(hass, aioclient_mock) - device_registry = await hass.helpers.device_registry.async_get_registry() + device_registry = dr.async_get(hass) device = device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, identifiers={(DECONZ_DOMAIN, "d0:cf:5e:ff:fe:71:a4:3a")}, diff --git a/tests/components/deconz/test_fan.py b/tests/components/deconz/test_fan.py index ddd4a4e46f4..6d6877c4500 100644 --- a/tests/components/deconz/test_fan.py +++ b/tests/components/deconz/test_fan.py @@ -3,19 +3,14 @@ from unittest.mock import patch import pytest +from voluptuous.error import MultipleInvalid from homeassistant.components.fan import ( ATTR_PERCENTAGE, - ATTR_SPEED, DOMAIN as FAN_DOMAIN, SERVICE_SET_PERCENTAGE, - SERVICE_SET_SPEED, SERVICE_TURN_OFF, SERVICE_TURN_ON, - SPEED_HIGH, - SPEED_LOW, - SPEED_MEDIUM, - SPEED_OFF, ) from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON, STATE_UNAVAILABLE @@ -212,7 +207,7 @@ async def test_fans(hass, aioclient_mock, mock_deconz_websocket): {ATTR_ENTITY_ID: "fan.ceiling_fan", ATTR_PERCENTAGE: 0}, blocking=True, ) - assert aioclient_mock.mock_calls[8][2] == {"speed": 1} + assert aioclient_mock.mock_calls[8][2] == {"speed": 0} # Events with an unsupported speed does not get converted @@ -273,7 +268,6 @@ async def test_fans_legacy_speed_modes(hass, aioclient_mock, mock_deconz_websock assert len(hass.states.async_all()) == 2 # Light and fan assert hass.states.get("fan.ceiling_fan").state == STATE_ON - assert hass.states.get("fan.ceiling_fan").attributes[ATTR_SPEED] == SPEED_HIGH # Test states @@ -289,7 +283,6 @@ async def test_fans_legacy_speed_modes(hass, aioclient_mock, mock_deconz_websock assert hass.states.get("fan.ceiling_fan").state == STATE_ON assert hass.states.get("fan.ceiling_fan").attributes[ATTR_PERCENTAGE] == 25 - assert hass.states.get("fan.ceiling_fan").attributes[ATTR_SPEED] == SPEED_LOW event_changed_light = { "t": "event", @@ -303,7 +296,6 @@ async def test_fans_legacy_speed_modes(hass, aioclient_mock, mock_deconz_websock assert hass.states.get("fan.ceiling_fan").state == STATE_ON assert hass.states.get("fan.ceiling_fan").attributes[ATTR_PERCENTAGE] == 50 - assert hass.states.get("fan.ceiling_fan").attributes[ATTR_SPEED] == SPEED_MEDIUM event_changed_light = { "t": "event", @@ -317,7 +309,6 @@ async def test_fans_legacy_speed_modes(hass, aioclient_mock, mock_deconz_websock assert hass.states.get("fan.ceiling_fan").state == STATE_ON assert hass.states.get("fan.ceiling_fan").attributes[ATTR_PERCENTAGE] == 75 - assert hass.states.get("fan.ceiling_fan").attributes[ATTR_SPEED] == SPEED_MEDIUM event_changed_light = { "t": "event", @@ -331,7 +322,6 @@ async def test_fans_legacy_speed_modes(hass, aioclient_mock, mock_deconz_websock assert hass.states.get("fan.ceiling_fan").state == STATE_ON assert hass.states.get("fan.ceiling_fan").attributes[ATTR_PERCENTAGE] == 100 - assert hass.states.get("fan.ceiling_fan").attributes[ATTR_SPEED] == SPEED_HIGH event_changed_light = { "t": "event", @@ -345,7 +335,6 @@ async def test_fans_legacy_speed_modes(hass, aioclient_mock, mock_deconz_websock assert hass.states.get("fan.ceiling_fan").state == STATE_OFF assert hass.states.get("fan.ceiling_fan").attributes[ATTR_PERCENTAGE] == 0 - assert hass.states.get("fan.ceiling_fan").attributes[ATTR_SPEED] == SPEED_OFF # Test service calls @@ -367,99 +356,99 @@ async def test_fans_legacy_speed_modes(hass, aioclient_mock, mock_deconz_websock await hass.services.async_call( FAN_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "fan.ceiling_fan", ATTR_SPEED: SPEED_OFF}, + {ATTR_ENTITY_ID: "fan.ceiling_fan", ATTR_PERCENTAGE: 0}, blocking=True, ) - assert aioclient_mock.mock_calls[2][2] == {"speed": 1} + assert aioclient_mock.mock_calls[2][2] == {"speed": 0} # Service turn on fan with bad speed # async_turn_on_compat use speed_to_percentage which will convert to SPEED_MEDIUM -> 2 - await hass.services.async_call( - FAN_DOMAIN, - SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "fan.ceiling_fan", ATTR_SPEED: "bad"}, - blocking=True, - ) - assert aioclient_mock.mock_calls[3][2] == {"speed": 2} + with pytest.raises(MultipleInvalid): + await hass.services.async_call( + FAN_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "fan.ceiling_fan", ATTR_PERCENTAGE: "bad"}, + blocking=True, + ) # Service turn on fan to low speed await hass.services.async_call( FAN_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "fan.ceiling_fan", ATTR_SPEED: SPEED_LOW}, + {ATTR_ENTITY_ID: "fan.ceiling_fan", ATTR_PERCENTAGE: 25}, blocking=True, ) - assert aioclient_mock.mock_calls[4][2] == {"speed": 1} + assert aioclient_mock.mock_calls[3][2] == {"speed": 1} # Service turn on fan to medium speed await hass.services.async_call( FAN_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "fan.ceiling_fan", ATTR_SPEED: SPEED_MEDIUM}, + {ATTR_ENTITY_ID: "fan.ceiling_fan", ATTR_PERCENTAGE: 50}, blocking=True, ) - assert aioclient_mock.mock_calls[5][2] == {"speed": 2} + assert aioclient_mock.mock_calls[4][2] == {"speed": 2} # Service turn on fan to high speed await hass.services.async_call( FAN_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "fan.ceiling_fan", ATTR_SPEED: SPEED_HIGH}, + {ATTR_ENTITY_ID: "fan.ceiling_fan", ATTR_PERCENTAGE: 100}, blocking=True, ) - assert aioclient_mock.mock_calls[6][2] == {"speed": 4} + assert aioclient_mock.mock_calls[5][2] == {"speed": 4} # Service set fan speed to low await hass.services.async_call( FAN_DOMAIN, - SERVICE_SET_SPEED, - {ATTR_ENTITY_ID: "fan.ceiling_fan", ATTR_SPEED: SPEED_LOW}, + SERVICE_SET_PERCENTAGE, + {ATTR_ENTITY_ID: "fan.ceiling_fan", ATTR_PERCENTAGE: 25}, blocking=True, ) - assert aioclient_mock.mock_calls[7][2] == {"speed": 1} + assert aioclient_mock.mock_calls[6][2] == {"speed": 1} # Service set fan speed to medium await hass.services.async_call( FAN_DOMAIN, - SERVICE_SET_SPEED, - {ATTR_ENTITY_ID: "fan.ceiling_fan", ATTR_SPEED: SPEED_MEDIUM}, + SERVICE_SET_PERCENTAGE, + {ATTR_ENTITY_ID: "fan.ceiling_fan", ATTR_PERCENTAGE: 50}, blocking=True, ) - assert aioclient_mock.mock_calls[8][2] == {"speed": 2} + assert aioclient_mock.mock_calls[7][2] == {"speed": 2} # Service set fan speed to high await hass.services.async_call( FAN_DOMAIN, - SERVICE_SET_SPEED, - {ATTR_ENTITY_ID: "fan.ceiling_fan", ATTR_SPEED: SPEED_HIGH}, + SERVICE_SET_PERCENTAGE, + {ATTR_ENTITY_ID: "fan.ceiling_fan", ATTR_PERCENTAGE: 100}, blocking=True, ) - assert aioclient_mock.mock_calls[9][2] == {"speed": 4} + assert aioclient_mock.mock_calls[8][2] == {"speed": 4} # Service set fan speed to off await hass.services.async_call( FAN_DOMAIN, - SERVICE_SET_SPEED, - {ATTR_ENTITY_ID: "fan.ceiling_fan", ATTR_SPEED: SPEED_OFF}, + SERVICE_SET_PERCENTAGE, + {ATTR_ENTITY_ID: "fan.ceiling_fan", ATTR_PERCENTAGE: 0}, blocking=True, ) - assert aioclient_mock.mock_calls[10][2] == {"speed": 0} + assert aioclient_mock.mock_calls[9][2] == {"speed": 0} # Service set fan speed to unsupported value - with pytest.raises(ValueError): + with pytest.raises(MultipleInvalid): await hass.services.async_call( FAN_DOMAIN, - SERVICE_SET_SPEED, - {ATTR_ENTITY_ID: "fan.ceiling_fan", ATTR_SPEED: "bad value"}, + SERVICE_SET_PERCENTAGE, + {ATTR_ENTITY_ID: "fan.ceiling_fan", ATTR_PERCENTAGE: "bad value"}, blocking=True, ) @@ -476,7 +465,7 @@ async def test_fans_legacy_speed_modes(hass, aioclient_mock, mock_deconz_websock await hass.async_block_till_done() assert hass.states.get("fan.ceiling_fan").state == STATE_ON - assert hass.states.get("fan.ceiling_fan").attributes[ATTR_SPEED] == SPEED_MEDIUM + assert hass.states.get("fan.ceiling_fan").attributes[ATTR_PERCENTAGE] == 75 await hass.config_entries.async_unload(config_entry.entry_id) diff --git a/tests/components/deluge/__init__.py b/tests/components/deluge/__init__.py new file mode 100644 index 00000000000..47339f8dfd5 --- /dev/null +++ b/tests/components/deluge/__init__.py @@ -0,0 +1,32 @@ +"""Tests for the Deluge integration.""" + +from homeassistant.components.deluge.const import ( + CONF_WEB_PORT, + DEFAULT_RPC_PORT, + DEFAULT_WEB_PORT, +) +from homeassistant.const import ( + CONF_HOST, + CONF_MONITORED_VARIABLES, + CONF_NAME, + CONF_PASSWORD, + CONF_PORT, + CONF_USERNAME, +) + +CONF_DATA = { + CONF_HOST: "1.2.3.4", + CONF_USERNAME: "user", + CONF_PASSWORD: "password", + CONF_PORT: DEFAULT_RPC_PORT, + CONF_WEB_PORT: DEFAULT_WEB_PORT, +} + +IMPORT_DATA = { + CONF_HOST: "1.2.3.4", + CONF_NAME: "Deluge Torrent", + CONF_MONITORED_VARIABLES: ["current_status", "download_speed", "upload_speed"], + CONF_USERNAME: "user", + CONF_PASSWORD: "password", + CONF_PORT: DEFAULT_RPC_PORT, +} diff --git a/tests/components/deluge/test_config_flow.py b/tests/components/deluge/test_config_flow.py new file mode 100644 index 00000000000..56dbac55674 --- /dev/null +++ b/tests/components/deluge/test_config_flow.py @@ -0,0 +1,160 @@ +"""Test Deluge config flow.""" +from unittest.mock import patch + +import pytest + +from homeassistant.components.deluge.const import DEFAULT_NAME, DOMAIN +from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_REAUTH, SOURCE_USER +from homeassistant.const import CONF_SOURCE +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import ( + RESULT_TYPE_ABORT, + RESULT_TYPE_CREATE_ENTRY, + RESULT_TYPE_FORM, +) + +from . import CONF_DATA, IMPORT_DATA + +from tests.common import MockConfigEntry + + +@pytest.fixture(name="api") +def mock_deluge_api(): + """Mock an api.""" + with patch("deluge_client.client.DelugeRPCClient.connect"), patch( + "deluge_client.client.DelugeRPCClient._create_socket" + ): + yield + + +@pytest.fixture(name="conn_error") +def mock_api_connection_error(): + """Mock an api.""" + with patch( + "deluge_client.client.DelugeRPCClient.connect", + side_effect=ConnectionRefusedError("111: Connection refused"), + ), patch("deluge_client.client.DelugeRPCClient._create_socket"): + yield + + +@pytest.fixture(name="unknown_error") +def mock_api_unknown_error(): + """Mock an api.""" + with patch( + "deluge_client.client.DelugeRPCClient.connect", side_effect=Exception + ), patch("deluge_client.client.DelugeRPCClient._create_socket"): + yield + + +@pytest.fixture(name="deluge_setup", autouse=True) +def deluge_setup_fixture(): + """Mock deluge entry setup.""" + with patch("homeassistant.components.deluge.async_setup_entry", return_value=True): + yield + + +async def test_flow_user(hass: HomeAssistant, api): + """Test user initialized flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data=CONF_DATA, + ) + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["title"] == DEFAULT_NAME + assert result["data"] == CONF_DATA + + +async def test_flow_user_already_configured(hass: HomeAssistant, api): + """Test user initialized flow with duplicate server.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=CONF_DATA, + ) + + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data=CONF_DATA + ) + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_flow_user_cannot_connect(hass: HomeAssistant, conn_error): + """Test user initialized flow with unreachable server.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data=CONF_DATA + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"] == {"base": "cannot_connect"} + + +async def test_flow_user_unknown_error(hass: HomeAssistant, unknown_error): + """Test user initialized flow with unreachable server.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data=CONF_DATA + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"] == {"base": "unknown"} + + +async def test_flow_import(hass: HomeAssistant, api): + """Test import step.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={CONF_SOURCE: SOURCE_IMPORT}, data=IMPORT_DATA + ) + + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "Deluge Torrent" + assert result["data"] == CONF_DATA + + +async def test_flow_import_already_configured(hass: HomeAssistant, api): + """Test import step already configured.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=CONF_DATA, + ) + + entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={CONF_SOURCE: SOURCE_IMPORT}, data=IMPORT_DATA + ) + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_flow_reauth(hass: HomeAssistant, api): + """Test reauth step.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=CONF_DATA, + ) + + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + CONF_SOURCE: SOURCE_REAUTH, + "entry_id": entry.entry_id, + "unique_id": entry.unique_id, + }, + data=CONF_DATA, + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=CONF_DATA, + ) + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "reauth_successful" + assert entry.data == CONF_DATA diff --git a/tests/components/demo/test_fan.py b/tests/components/demo/test_fan.py index 4767a99d0b3..9efcdb26de3 100644 --- a/tests/components/demo/test_fan.py +++ b/tests/components/demo/test_fan.py @@ -55,39 +55,6 @@ async def test_turn_on_with_speed_and_percentage(hass, fan_entity_id): """Test turning on the device.""" state = hass.states.get(fan_entity_id) assert state.state == STATE_OFF - await hass.services.async_call( - fan.DOMAIN, - SERVICE_TURN_ON, - {ATTR_ENTITY_ID: fan_entity_id, fan.ATTR_SPEED: fan.SPEED_HIGH}, - blocking=True, - ) - state = hass.states.get(fan_entity_id) - assert state.state == STATE_ON - assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_HIGH - assert state.attributes[fan.ATTR_PERCENTAGE] == 100 - - await hass.services.async_call( - fan.DOMAIN, - SERVICE_TURN_ON, - {ATTR_ENTITY_ID: fan_entity_id, fan.ATTR_SPEED: fan.SPEED_MEDIUM}, - blocking=True, - ) - state = hass.states.get(fan_entity_id) - assert state.state == STATE_ON - assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_MEDIUM - assert state.attributes[fan.ATTR_PERCENTAGE] == 66 - - await hass.services.async_call( - fan.DOMAIN, - SERVICE_TURN_ON, - {ATTR_ENTITY_ID: fan_entity_id, fan.ATTR_SPEED: fan.SPEED_LOW}, - blocking=True, - ) - state = hass.states.get(fan_entity_id) - assert state.state == STATE_ON - assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_LOW - assert state.attributes[fan.ATTR_PERCENTAGE] == 33 - await hass.services.async_call( fan.DOMAIN, SERVICE_TURN_ON, @@ -96,7 +63,6 @@ async def test_turn_on_with_speed_and_percentage(hass, fan_entity_id): ) state = hass.states.get(fan_entity_id) assert state.state == STATE_ON - assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_HIGH assert state.attributes[fan.ATTR_PERCENTAGE] == 100 await hass.services.async_call( @@ -107,7 +73,6 @@ async def test_turn_on_with_speed_and_percentage(hass, fan_entity_id): ) state = hass.states.get(fan_entity_id) assert state.state == STATE_ON - assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_MEDIUM assert state.attributes[fan.ATTR_PERCENTAGE] == 66 await hass.services.async_call( @@ -118,7 +83,36 @@ async def test_turn_on_with_speed_and_percentage(hass, fan_entity_id): ) state = hass.states.get(fan_entity_id) assert state.state == STATE_ON - assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_LOW + assert state.attributes[fan.ATTR_PERCENTAGE] == 33 + + await hass.services.async_call( + fan.DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: fan_entity_id, fan.ATTR_PERCENTAGE: 100}, + blocking=True, + ) + state = hass.states.get(fan_entity_id) + assert state.state == STATE_ON + assert state.attributes[fan.ATTR_PERCENTAGE] == 100 + + await hass.services.async_call( + fan.DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: fan_entity_id, fan.ATTR_PERCENTAGE: 66}, + blocking=True, + ) + state = hass.states.get(fan_entity_id) + assert state.state == STATE_ON + assert state.attributes[fan.ATTR_PERCENTAGE] == 66 + + await hass.services.async_call( + fan.DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: fan_entity_id, fan.ATTR_PERCENTAGE: 33}, + blocking=True, + ) + state = hass.states.get(fan_entity_id) + assert state.state == STATE_ON assert state.attributes[fan.ATTR_PERCENTAGE] == 33 await hass.services.async_call( @@ -129,7 +123,6 @@ async def test_turn_on_with_speed_and_percentage(hass, fan_entity_id): ) state = hass.states.get(fan_entity_id) assert state.state == STATE_OFF - assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_OFF assert state.attributes[fan.ATTR_PERCENTAGE] == 0 @@ -198,19 +191,8 @@ async def test_turn_on_with_preset_mode_and_speed(hass, fan_entity_id): ) state = hass.states.get(fan_entity_id) assert state.state == STATE_ON - assert state.attributes[fan.ATTR_SPEED] == PRESET_MODE_AUTO assert state.attributes[fan.ATTR_PERCENTAGE] is None assert state.attributes[fan.ATTR_PRESET_MODE] == PRESET_MODE_AUTO - assert state.attributes[fan.ATTR_SPEED_LIST] == [ - fan.SPEED_OFF, - fan.SPEED_LOW, - fan.SPEED_MEDIUM, - fan.SPEED_HIGH, - PRESET_MODE_AUTO, - PRESET_MODE_SMART, - PRESET_MODE_SLEEP, - PRESET_MODE_ON, - ] assert state.attributes[fan.ATTR_PRESET_MODES] == [ PRESET_MODE_AUTO, PRESET_MODE_SMART, @@ -226,7 +208,6 @@ async def test_turn_on_with_preset_mode_and_speed(hass, fan_entity_id): ) state = hass.states.get(fan_entity_id) assert state.state == STATE_ON - assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_HIGH assert state.attributes[fan.ATTR_PERCENTAGE] == 100 assert state.attributes[fan.ATTR_PRESET_MODE] is None @@ -238,7 +219,6 @@ async def test_turn_on_with_preset_mode_and_speed(hass, fan_entity_id): ) state = hass.states.get(fan_entity_id) assert state.state == STATE_ON - assert state.attributes[fan.ATTR_SPEED] == PRESET_MODE_SMART assert state.attributes[fan.ATTR_PERCENTAGE] is None assert state.attributes[fan.ATTR_PRESET_MODE] == PRESET_MODE_SMART @@ -247,7 +227,6 @@ async def test_turn_on_with_preset_mode_and_speed(hass, fan_entity_id): ) state = hass.states.get(fan_entity_id) assert state.state == STATE_OFF - assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_OFF assert state.attributes[fan.ATTR_PERCENTAGE] == 0 assert state.attributes[fan.ATTR_PRESET_MODE] is None @@ -262,7 +241,6 @@ async def test_turn_on_with_preset_mode_and_speed(hass, fan_entity_id): state = hass.states.get(fan_entity_id) assert state.state == STATE_OFF - assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_OFF assert state.attributes[fan.ATTR_PERCENTAGE] == 0 assert state.attributes[fan.ATTR_PRESET_MODE] is None @@ -321,50 +299,6 @@ async def test_set_direction(hass, fan_entity_id): assert state.attributes[fan.ATTR_DIRECTION] == fan.DIRECTION_REVERSE -@pytest.mark.parametrize("fan_entity_id", LIMITED_AND_FULL_FAN_ENTITY_IDS) -async def test_set_speed(hass, fan_entity_id): - """Test setting the speed of the device.""" - state = hass.states.get(fan_entity_id) - assert state.state == STATE_OFF - - await hass.services.async_call( - fan.DOMAIN, - fan.SERVICE_SET_SPEED, - {ATTR_ENTITY_ID: fan_entity_id, fan.ATTR_SPEED: fan.SPEED_LOW}, - blocking=True, - ) - state = hass.states.get(fan_entity_id) - assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_LOW - - await hass.services.async_call( - fan.DOMAIN, - fan.SERVICE_SET_SPEED, - {ATTR_ENTITY_ID: fan_entity_id, fan.ATTR_SPEED: fan.SPEED_OFF}, - blocking=True, - ) - state = hass.states.get(fan_entity_id) - assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_OFF - - -@pytest.mark.parametrize("fan_entity_id", FANS_WITH_PRESET_MODES) -async def test_set_preset_mode_with_legacy_speed_service(hass, fan_entity_id): - """Test setting the preset mode is possible with the legacy service for backwards compat.""" - state = hass.states.get(fan_entity_id) - assert state.state == STATE_OFF - - await hass.services.async_call( - fan.DOMAIN, - fan.SERVICE_SET_SPEED, - {ATTR_ENTITY_ID: fan_entity_id, fan.ATTR_SPEED: PRESET_MODE_AUTO}, - blocking=True, - ) - state = hass.states.get(fan_entity_id) - assert state.state == STATE_ON - assert state.attributes[fan.ATTR_SPEED] == PRESET_MODE_AUTO - assert state.attributes[fan.ATTR_PERCENTAGE] is None - assert state.attributes[fan.ATTR_PRESET_MODE] == PRESET_MODE_AUTO - - @pytest.mark.parametrize("fan_entity_id", FANS_WITH_PRESET_MODES) async def test_set_preset_mode(hass, fan_entity_id): """Test setting the preset mode of the device.""" @@ -379,7 +313,6 @@ async def test_set_preset_mode(hass, fan_entity_id): ) state = hass.states.get(fan_entity_id) assert state.state == STATE_ON - assert state.attributes[fan.ATTR_SPEED] == PRESET_MODE_AUTO assert state.attributes[fan.ATTR_PERCENTAGE] is None assert state.attributes[fan.ATTR_PRESET_MODE] == PRESET_MODE_AUTO @@ -422,7 +355,6 @@ async def test_set_percentage(hass, fan_entity_id): blocking=True, ) state = hass.states.get(fan_entity_id) - assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_LOW assert state.attributes[fan.ATTR_PERCENTAGE] == 33 @@ -440,7 +372,6 @@ async def test_increase_decrease_speed(hass, fan_entity_id): blocking=True, ) state = hass.states.get(fan_entity_id) - assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_LOW assert state.attributes[fan.ATTR_PERCENTAGE] == 33 await hass.services.async_call( @@ -450,7 +381,6 @@ async def test_increase_decrease_speed(hass, fan_entity_id): blocking=True, ) state = hass.states.get(fan_entity_id) - assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_MEDIUM assert state.attributes[fan.ATTR_PERCENTAGE] == 66 await hass.services.async_call( @@ -460,7 +390,6 @@ async def test_increase_decrease_speed(hass, fan_entity_id): blocking=True, ) state = hass.states.get(fan_entity_id) - assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_HIGH assert state.attributes[fan.ATTR_PERCENTAGE] == 100 await hass.services.async_call( @@ -470,7 +399,6 @@ async def test_increase_decrease_speed(hass, fan_entity_id): blocking=True, ) state = hass.states.get(fan_entity_id) - assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_HIGH assert state.attributes[fan.ATTR_PERCENTAGE] == 100 await hass.services.async_call( @@ -481,7 +409,6 @@ async def test_increase_decrease_speed(hass, fan_entity_id): ) state = hass.states.get(fan_entity_id) assert state.attributes[fan.ATTR_PERCENTAGE] == 66 - assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_MEDIUM await hass.services.async_call( fan.DOMAIN, @@ -490,7 +417,6 @@ async def test_increase_decrease_speed(hass, fan_entity_id): blocking=True, ) state = hass.states.get(fan_entity_id) - assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_LOW assert state.attributes[fan.ATTR_PERCENTAGE] == 33 await hass.services.async_call( @@ -500,7 +426,6 @@ async def test_increase_decrease_speed(hass, fan_entity_id): blocking=True, ) state = hass.states.get(fan_entity_id) - assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_OFF assert state.attributes[fan.ATTR_PERCENTAGE] == 0 await hass.services.async_call( @@ -510,7 +435,6 @@ async def test_increase_decrease_speed(hass, fan_entity_id): blocking=True, ) state = hass.states.get(fan_entity_id) - assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_OFF assert state.attributes[fan.ATTR_PERCENTAGE] == 0 @@ -524,7 +448,6 @@ async def test_increase_decrease_speed_with_percentage_step(hass, fan_entity_id) blocking=True, ) state = hass.states.get(fan_entity_id) - assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_LOW assert state.attributes[fan.ATTR_PERCENTAGE] == 25 await hass.services.async_call( @@ -534,7 +457,6 @@ async def test_increase_decrease_speed_with_percentage_step(hass, fan_entity_id) blocking=True, ) state = hass.states.get(fan_entity_id) - assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_MEDIUM assert state.attributes[fan.ATTR_PERCENTAGE] == 50 await hass.services.async_call( @@ -544,7 +466,6 @@ async def test_increase_decrease_speed_with_percentage_step(hass, fan_entity_id) blocking=True, ) state = hass.states.get(fan_entity_id) - assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_HIGH assert state.attributes[fan.ATTR_PERCENTAGE] == 75 diff --git a/tests/components/demo/test_init.py b/tests/components/demo/test_init.py index a446856de7b..beffa3ba332 100644 --- a/tests/components/demo/test_init.py +++ b/tests/components/demo/test_init.py @@ -7,11 +7,12 @@ import pytest from homeassistant.components.demo import DOMAIN from homeassistant.components.device_tracker.legacy import YAML_DEVICES +from homeassistant.components.recorder import get_instance from homeassistant.components.recorder.statistics import list_statistic_ids from homeassistant.helpers.json import JSONEncoder -from homeassistant.setup import async_setup_component, setup_component +from homeassistant.setup import async_setup_component -from tests.components.recorder.common import wait_recording_done +from tests.components.recorder.common import async_wait_recording_done_without_instance @pytest.fixture(autouse=True) @@ -45,23 +46,27 @@ async def test_setting_up_demo(hass): ) -def test_demo_statistics(hass_recorder): +async def test_demo_statistics(hass, recorder_mock): """Test that the demo components makes some statistics available.""" - hass = hass_recorder() + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + await hass.async_block_till_done() + await hass.async_start() + await async_wait_recording_done_without_instance(hass) - assert setup_component(hass, DOMAIN, {DOMAIN: {}}) - hass.block_till_done() - hass.start() - wait_recording_done(hass) - - statistic_ids = list_statistic_ids(hass) + statistic_ids = await get_instance(hass).async_add_executor_job( + list_statistic_ids, hass + ) assert { + "has_mean": True, + "has_sum": False, "name": None, "source": "demo", "statistic_id": "demo:temperature_outdoor", "unit_of_measurement": "°C", } in statistic_ids assert { + "has_mean": False, + "has_sum": True, "name": None, "source": "demo", "statistic_id": "demo:energy_consumption", diff --git a/tests/components/demo/test_update.py b/tests/components/demo/test_update.py new file mode 100644 index 00000000000..35780114f29 --- /dev/null +++ b/tests/components/demo/test_update.py @@ -0,0 +1,183 @@ +"""The tests for the demo update platform.""" +from unittest.mock import patch + +import pytest + +from homeassistant.components.update import DOMAIN, SERVICE_INSTALL, UpdateDeviceClass +from homeassistant.components.update.const import ( + ATTR_IN_PROGRESS, + ATTR_INSTALLED_VERSION, + ATTR_LATEST_VERSION, + ATTR_RELEASE_SUMMARY, + ATTR_RELEASE_URL, + ATTR_TITLE, +) +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_ENTITY_ID, + ATTR_ENTITY_PICTURE, + STATE_OFF, + STATE_ON, +) +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.event import async_track_state_change_event +from homeassistant.setup import async_setup_component + + +@pytest.fixture(autouse=True) +async def setup_demo_update(hass: HomeAssistant) -> None: + """Initialize setup demo update entity.""" + assert await async_setup_component(hass, DOMAIN, {"update": {"platform": "demo"}}) + await hass.async_block_till_done() + + +def test_setup_params(hass: HomeAssistant) -> None: + """Test the initial parameters.""" + state = hass.states.get("update.demo_update_no_install") + assert state + assert state.state == STATE_ON + assert state.attributes[ATTR_TITLE] == "Awesomesoft Inc." + assert state.attributes[ATTR_INSTALLED_VERSION] == "1.0.0" + assert state.attributes[ATTR_LATEST_VERSION] == "1.0.1" + assert ( + state.attributes[ATTR_RELEASE_SUMMARY] == "Awesome update, fixing everything!" + ) + assert state.attributes[ATTR_RELEASE_URL] == "https://www.example.com/release/1.0.1" + assert ( + state.attributes[ATTR_ENTITY_PICTURE] + == "https://brands.home-assistant.io/_/demo/icon.png" + ) + + state = hass.states.get("update.demo_no_update") + assert state + assert state.state == STATE_OFF + assert state.attributes[ATTR_TITLE] == "AdGuard Home" + assert state.attributes[ATTR_INSTALLED_VERSION] == "1.0.0" + assert state.attributes[ATTR_LATEST_VERSION] == "1.0.0" + assert state.attributes[ATTR_RELEASE_SUMMARY] is None + assert state.attributes[ATTR_RELEASE_URL] is None + assert ( + state.attributes[ATTR_ENTITY_PICTURE] + == "https://brands.home-assistant.io/_/demo/icon.png" + ) + + state = hass.states.get("update.demo_add_on") + assert state + assert state.state == STATE_ON + assert state.attributes[ATTR_TITLE] == "AdGuard Home" + assert state.attributes[ATTR_INSTALLED_VERSION] == "1.0.0" + assert state.attributes[ATTR_LATEST_VERSION] == "1.0.1" + assert ( + state.attributes[ATTR_RELEASE_SUMMARY] == "Awesome update, fixing everything!" + ) + assert state.attributes[ATTR_RELEASE_URL] == "https://www.example.com/release/1.0.1" + assert ( + state.attributes[ATTR_ENTITY_PICTURE] + == "https://brands.home-assistant.io/_/demo/icon.png" + ) + + state = hass.states.get("update.demo_living_room_bulb_update") + assert state + assert state.state == STATE_ON + assert state.attributes[ATTR_TITLE] == "Philips Lamps Firmware" + assert state.attributes[ATTR_INSTALLED_VERSION] == "1.93.3" + assert state.attributes[ATTR_LATEST_VERSION] == "1.94.2" + assert state.attributes[ATTR_RELEASE_SUMMARY] == "Added support for effects" + assert ( + state.attributes[ATTR_RELEASE_URL] == "https://www.example.com/release/1.93.3" + ) + assert state.attributes[ATTR_DEVICE_CLASS] == UpdateDeviceClass.FIRMWARE + assert ( + state.attributes[ATTR_ENTITY_PICTURE] + == "https://brands.home-assistant.io/_/demo/icon.png" + ) + + state = hass.states.get("update.demo_update_with_progress") + assert state + assert state.state == STATE_ON + assert state.attributes[ATTR_TITLE] == "Philips Lamps Firmware" + assert state.attributes[ATTR_INSTALLED_VERSION] == "1.93.3" + assert state.attributes[ATTR_LATEST_VERSION] == "1.94.2" + assert state.attributes[ATTR_RELEASE_SUMMARY] == "Added support for effects" + assert ( + state.attributes[ATTR_RELEASE_URL] == "https://www.example.com/release/1.93.3" + ) + assert state.attributes[ATTR_DEVICE_CLASS] == UpdateDeviceClass.FIRMWARE + assert ( + state.attributes[ATTR_ENTITY_PICTURE] + == "https://brands.home-assistant.io/_/demo/icon.png" + ) + + +async def test_update_with_progress(hass: HomeAssistant) -> None: + """Test update with progress.""" + state = hass.states.get("update.demo_update_with_progress") + assert state + assert state.state == STATE_ON + assert state.attributes[ATTR_IN_PROGRESS] is False + + events = [] + async_track_state_change_event( + hass, + "update.demo_update_with_progress", + callback(lambda event: events.append(event)), + ) + + with patch("homeassistant.components.demo.update.FAKE_INSTALL_SLEEP_TIME", new=0): + await hass.services.async_call( + DOMAIN, + SERVICE_INSTALL, + {ATTR_ENTITY_ID: "update.demo_update_with_progress"}, + blocking=True, + ) + + assert len(events) == 10 + assert events[0].data["new_state"].state == STATE_ON + assert events[0].data["new_state"].attributes[ATTR_IN_PROGRESS] == 10 + assert events[1].data["new_state"].attributes[ATTR_IN_PROGRESS] == 20 + assert events[2].data["new_state"].attributes[ATTR_IN_PROGRESS] == 30 + assert events[3].data["new_state"].attributes[ATTR_IN_PROGRESS] == 40 + assert events[4].data["new_state"].attributes[ATTR_IN_PROGRESS] == 50 + assert events[5].data["new_state"].attributes[ATTR_IN_PROGRESS] == 60 + assert events[6].data["new_state"].attributes[ATTR_IN_PROGRESS] == 70 + assert events[7].data["new_state"].attributes[ATTR_IN_PROGRESS] == 80 + assert events[8].data["new_state"].attributes[ATTR_IN_PROGRESS] == 90 + assert events[9].data["new_state"].attributes[ATTR_IN_PROGRESS] is False + assert events[9].data["new_state"].state == STATE_OFF + + +async def test_update_with_progress_raising(hass: HomeAssistant) -> None: + """Test update with progress failing to install.""" + state = hass.states.get("update.demo_update_with_progress") + assert state + assert state.state == STATE_ON + assert state.attributes[ATTR_IN_PROGRESS] is False + + events = [] + async_track_state_change_event( + hass, + "update.demo_update_with_progress", + callback(lambda event: events.append(event)), + ) + + with patch( + "homeassistant.components.demo.update._fake_install", + side_effect=[None, None, None, None, RuntimeError], + ) as fake_sleep, pytest.raises(RuntimeError): + await hass.services.async_call( + DOMAIN, + SERVICE_INSTALL, + {ATTR_ENTITY_ID: "update.demo_update_with_progress"}, + blocking=True, + ) + await hass.async_block_till_done() + + assert fake_sleep.call_count == 5 + assert len(events) == 5 + assert events[0].data["new_state"].state == STATE_ON + assert events[0].data["new_state"].attributes[ATTR_IN_PROGRESS] == 10 + assert events[1].data["new_state"].attributes[ATTR_IN_PROGRESS] == 20 + assert events[2].data["new_state"].attributes[ATTR_IN_PROGRESS] == 30 + assert events[3].data["new_state"].attributes[ATTR_IN_PROGRESS] == 40 + assert events[4].data["new_state"].attributes[ATTR_IN_PROGRESS] is False + assert events[4].data["new_state"].state == STATE_ON diff --git a/tests/components/demo/test_weather.py b/tests/components/demo/test_weather.py index c4ae8fcd79c..db3f3441df1 100644 --- a/tests/components/demo/test_weather.py +++ b/tests/components/demo/test_weather.py @@ -69,4 +69,4 @@ async def test_temperature_convert(hass): assert state.state == "rainy" data = state.attributes - assert data.get(ATTR_WEATHER_TEMPERATURE) == -24 + assert data.get(ATTR_WEATHER_TEMPERATURE) == -24.4 diff --git a/tests/components/denonavr/test_config_flow.py b/tests/components/denonavr/test_config_flow.py index ff811e0d235..91197927e95 100644 --- a/tests/components/denonavr/test_config_flow.py +++ b/tests/components/denonavr/test_config_flow.py @@ -7,7 +7,6 @@ from homeassistant import config_entries, data_entry_flow from homeassistant.components import ssdp from homeassistant.components.denonavr.config_flow import ( CONF_MANUFACTURER, - CONF_MODEL, CONF_SERIAL_NUMBER, CONF_SHOW_ALL_SOURCES, CONF_TYPE, @@ -17,7 +16,7 @@ from homeassistant.components.denonavr.config_flow import ( DOMAIN, AvrTimoutError, ) -from homeassistant.const import CONF_HOST +from homeassistant.const import CONF_HOST, CONF_MODEL from tests.common import MockConfigEntry diff --git a/tests/components/denonavr/test_media_player.py b/tests/components/denonavr/test_media_player.py index 0607e7d42f7..4497025c11c 100644 --- a/tests/components/denonavr/test_media_player.py +++ b/tests/components/denonavr/test_media_player.py @@ -6,7 +6,6 @@ import pytest from homeassistant.components import media_player from homeassistant.components.denonavr.config_flow import ( CONF_MANUFACTURER, - CONF_MODEL, CONF_SERIAL_NUMBER, CONF_TYPE, DOMAIN, @@ -18,7 +17,7 @@ from homeassistant.components.denonavr.media_player import ( SERVICE_SET_DYNAMIC_EQ, SERVICE_UPDATE_AUDYSSEY, ) -from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST +from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST, CONF_MODEL from tests.common import MockConfigEntry diff --git a/tests/components/derivative/test_config_flow.py b/tests/components/derivative/test_config_flow.py new file mode 100644 index 00000000000..61ab7251f8a --- /dev/null +++ b/tests/components/derivative/test_config_flow.py @@ -0,0 +1,149 @@ +"""Test the Derivative config flow.""" +from unittest.mock import patch + +import pytest + +from homeassistant import config_entries +from homeassistant.components.derivative.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM + +from tests.common import MockConfigEntry + + +@pytest.mark.parametrize("platform", ("sensor",)) +async def test_config_flow(hass: HomeAssistant, platform) -> None: + """Test the config flow.""" + input_sensor_entity_id = "sensor.input" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] is None + + with patch( + "homeassistant.components.derivative.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "name": "My derivative", + "round": 1, + "source": input_sensor_entity_id, + "time_window": {"seconds": 0}, + "unit_prefix": "none", + "unit_time": "min", + }, + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "My derivative" + assert result["data"] == {} + assert result["options"] == { + "name": "My derivative", + "round": 1.0, + "source": "sensor.input", + "time_window": {"seconds": 0.0}, + "unit_prefix": "none", + "unit_time": "min", + } + assert len(mock_setup_entry.mock_calls) == 1 + + config_entry = hass.config_entries.async_entries(DOMAIN)[0] + assert config_entry.data == {} + assert config_entry.options == { + "name": "My derivative", + "round": 1.0, + "source": "sensor.input", + "time_window": {"seconds": 0.0}, + "unit_prefix": "none", + "unit_time": "min", + } + assert config_entry.title == "My derivative" + + +def get_suggested(schema, key): + """Get suggested value for key in voluptuous schema.""" + for k in schema.keys(): + if k == key: + if k.description is None or "suggested_value" not in k.description: + return None + return k.description["suggested_value"] + # Wanted key absent from schema + raise Exception + + +@pytest.mark.parametrize("platform", ("sensor",)) +async def test_options(hass: HomeAssistant, platform) -> None: + """Test reconfiguring.""" + # Setup the config entry + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + "name": "My derivative", + "round": 1.0, + "source": "sensor.input", + "time_window": {"seconds": 0.0}, + "unit_prefix": "k", + "unit_time": "min", + }, + title="My derivative", + ) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "init" + schema = result["data_schema"].schema + assert get_suggested(schema, "round") == 1.0 + assert get_suggested(schema, "time_window") == {"seconds": 0.0} + assert get_suggested(schema, "unit_prefix") == "k" + assert get_suggested(schema, "unit_time") == "min" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + "round": 2.0, + "time_window": {"seconds": 10.0}, + "unit_prefix": "none", + "unit_time": "h", + }, + ) + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["data"] == { + "name": "My derivative", + "round": 2.0, + "source": "sensor.input", + "time_window": {"seconds": 10.0}, + "unit_prefix": "none", + "unit_time": "h", + } + assert config_entry.data == {} + assert config_entry.options == { + "name": "My derivative", + "round": 2.0, + "source": "sensor.input", + "time_window": {"seconds": 10.0}, + "unit_prefix": "none", + "unit_time": "h", + } + assert config_entry.title == "My derivative" + + # Check config entry is reloaded with new options + await hass.async_block_till_done() + + # Check the entity was updated, no new entity was created + assert len(hass.states.async_all()) == 1 + + # Check the state of the entity has changed as expected + hass.states.async_set("sensor.input", 10, {"unit_of_measurement": "cat"}) + hass.states.async_set("sensor.input", 11, {"unit_of_measurement": "cat"}) + await hass.async_block_till_done() + state = hass.states.get(f"{platform}.my_derivative") + assert state.attributes["unit_of_measurement"] == "cat/h" diff --git a/tests/components/derivative/test_init.py b/tests/components/derivative/test_init.py new file mode 100644 index 00000000000..fef13109007 --- /dev/null +++ b/tests/components/derivative/test_init.py @@ -0,0 +1,61 @@ +"""Test the Derivative integration.""" +import pytest + +from homeassistant.components.derivative.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from tests.common import MockConfigEntry + + +@pytest.mark.parametrize("platform", ("sensor",)) +async def test_setup_and_remove_config_entry( + hass: HomeAssistant, + platform: str, +) -> None: + """Test setting up and removing a config entry.""" + input_sensor_entity_id = "sensor.input" + registry = er.async_get(hass) + derivative_entity_id = f"{platform}.my_derivative" + + # Setup the config entry + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + "name": "My derivative", + "round": 1.0, + "source": "sensor.input", + "time_window": {"seconds": 0.0}, + "unit_prefix": "k", + "unit_time": "min", + }, + title="My derivative", + ) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + # Check the entity is registered in the entity registry + assert registry.async_get(derivative_entity_id) is not None + + # Check the platform is setup correctly + state = hass.states.get(derivative_entity_id) + assert state.state == "0" + assert "unit_of_measurement" not in state.attributes + assert state.attributes["source"] == "sensor.input" + + hass.states.async_set(input_sensor_entity_id, 10, {"unit_of_measurement": "dog"}) + hass.states.async_set(input_sensor_entity_id, 11, {"unit_of_measurement": "dog"}) + await hass.async_block_till_done() + state = hass.states.get(derivative_entity_id) + assert state.state != "0" + assert state.attributes["unit_of_measurement"] == "kdog/min" + + # Remove the config entry + assert await hass.config_entries.async_remove(config_entry.entry_id) + await hass.async_block_till_done() + + # Check the state and entity registry entry are removed + assert hass.states.get(derivative_entity_id) is None + assert registry.async_get(derivative_entity_id) is None diff --git a/tests/components/device_automation/test_init.py b/tests/components/device_automation/test_init.py index f7b1339014a..ad6a08814ff 100644 --- a/tests/components/device_automation/test_init.py +++ b/tests/components/device_automation/test_init.py @@ -404,13 +404,6 @@ async def test_async_get_device_automations_single_device_trigger( assert device_entry.id in result assert len(result[device_entry.id]) == 3 - # Test deprecated str automation_type works, to be removed in 2022.4 - result = await device_automation.async_get_device_automations( - hass, "trigger", [device_entry.id] - ) - assert device_entry.id in result - assert len(result[device_entry.id]) == 3 # toggled, turned_on, turned_off - async def test_async_get_device_automations_all_devices_trigger( hass, device_reg, entity_reg diff --git a/tests/components/dexcom/test_sensor.py b/tests/components/dexcom/test_sensor.py index 15de72e9c95..ff1256a9bc0 100644 --- a/tests/components/dexcom/test_sensor.py +++ b/tests/components/dexcom/test_sensor.py @@ -10,6 +10,7 @@ from homeassistant.const import ( STATE_UNAVAILABLE, STATE_UNKNOWN, ) +from homeassistant.helpers.entity_component import async_update_entity from tests.components.dexcom import GLUCOSE_READING, init_integration @@ -36,12 +37,8 @@ async def test_sensors_unknown(hass): "homeassistant.components.dexcom.Dexcom.get_current_glucose_reading", return_value=None, ): - await hass.helpers.entity_component.async_update_entity( - "sensor.dexcom_test_username_glucose_value" - ) - await hass.helpers.entity_component.async_update_entity( - "sensor.dexcom_test_username_glucose_trend" - ) + await async_update_entity(hass, "sensor.dexcom_test_username_glucose_value") + await async_update_entity(hass, "sensor.dexcom_test_username_glucose_trend") test_username_glucose_value = hass.states.get( "sensor.dexcom_test_username_glucose_value" @@ -61,12 +58,8 @@ async def test_sensors_update_failed(hass): "homeassistant.components.dexcom.Dexcom.get_current_glucose_reading", side_effect=SessionError, ): - await hass.helpers.entity_component.async_update_entity( - "sensor.dexcom_test_username_glucose_value" - ) - await hass.helpers.entity_component.async_update_entity( - "sensor.dexcom_test_username_glucose_trend" - ) + await async_update_entity(hass, "sensor.dexcom_test_username_glucose_value") + await async_update_entity(hass, "sensor.dexcom_test_username_glucose_trend") test_username_glucose_value = hass.states.get( "sensor.dexcom_test_username_glucose_value" diff --git a/tests/components/discord/__init__.py b/tests/components/discord/__init__.py new file mode 100644 index 00000000000..ebc23360555 --- /dev/null +++ b/tests/components/discord/__init__.py @@ -0,0 +1,60 @@ +"""Tests for the Discord integration.""" + +from unittest.mock import AsyncMock, Mock, patch + +import nextcord + +from homeassistant.components.discord.const import DOMAIN +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_API_TOKEN, CONF_NAME, CONF_TOKEN +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + +TOKEN = "abc123" +NAME = "Discord Bot" + +CONF_INPUT = {CONF_API_TOKEN: TOKEN} + +CONF_DATA = { + CONF_API_TOKEN: TOKEN, + CONF_NAME: NAME, +} + +CONF_IMPORT_DATA_NO_NAME = {CONF_TOKEN: TOKEN} + +CONF_IMPORT_DATA = CONF_IMPORT_DATA_NO_NAME | {CONF_NAME: NAME} + + +def create_entry(hass: HomeAssistant) -> ConfigEntry: + """Add config entry in Home Assistant.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=CONF_DATA, + unique_id="1234567890", + ) + entry.add_to_hass(hass) + return entry + + +def mocked_discord_info(): + """Create mocked discord.""" + mocked_discord = AsyncMock() + mocked_discord.id = "1234567890" + mocked_discord.name = NAME + return patch( + "homeassistant.components.discord.config_flow.nextcord.Client.application_info", + return_value=mocked_discord, + ) + + +def patch_discord_login(): + """Patch discord info.""" + return patch("homeassistant.components.discord.config_flow.nextcord.Client.login") + + +def mock_exception(): + """Mock response.""" + response = Mock() + response.status = 404 + return nextcord.HTTPException(response, "") diff --git a/tests/components/discord/test_config_flow.py b/tests/components/discord/test_config_flow.py new file mode 100644 index 00000000000..64588b052fe --- /dev/null +++ b/tests/components/discord/test_config_flow.py @@ -0,0 +1,206 @@ +"""Test Discord config flow.""" +import nextcord +from pytest import LogCaptureFixture + +from homeassistant import config_entries, data_entry_flow +from homeassistant.components.discord.const import DOMAIN +from homeassistant.const import CONF_API_TOKEN, CONF_SOURCE +from homeassistant.core import HomeAssistant + +from . import ( + CONF_DATA, + CONF_IMPORT_DATA, + CONF_IMPORT_DATA_NO_NAME, + CONF_INPUT, + NAME, + create_entry, + mock_exception, + mocked_discord_info, + patch_discord_login, +) + + +async def test_flow_user(hass: HomeAssistant) -> None: + """Test user initialized flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + with mocked_discord_info(), patch_discord_login(): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=CONF_INPUT, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == NAME + assert result["data"] == CONF_DATA + + +async def test_flow_user_already_configured(hass: HomeAssistant) -> None: + """Test user initialized flow with duplicate server.""" + create_entry(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + with mocked_discord_info(), patch_discord_login(): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=CONF_INPUT, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_flow_user_invalid_auth(hass: HomeAssistant) -> None: + """Test user initialized flow with invalid token.""" + with patch_discord_login() as mock: + mock.side_effect = nextcord.LoginFailure + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data=CONF_DATA, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"] == {"base": "invalid_auth"} + + with mocked_discord_info(), patch_discord_login(): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=CONF_INPUT, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == NAME + assert result["data"] == CONF_DATA + + +async def test_flow_user_cannot_connect(hass: HomeAssistant) -> None: + """Test user initialized flow with unreachable server.""" + with patch_discord_login() as mock: + mock.side_effect = mock_exception() + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data=CONF_DATA, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"] == {"base": "cannot_connect"} + + with mocked_discord_info(), patch_discord_login(): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=CONF_INPUT, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == NAME + assert result["data"] == CONF_DATA + + +async def test_flow_user_unknown_error(hass: HomeAssistant) -> None: + """Test user initialized flow with unreachable server.""" + with patch_discord_login() as mock: + mock.side_effect = Exception + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data=CONF_DATA, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"] == {"base": "unknown"} + + with mocked_discord_info(), patch_discord_login(): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=CONF_INPUT, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == NAME + assert result["data"] == CONF_DATA + + +async def test_flow_import(hass: HomeAssistant, caplog: LogCaptureFixture) -> None: + """Test an import flow.""" + with mocked_discord_info(), patch_discord_login(): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=CONF_IMPORT_DATA.copy(), + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == NAME + assert result["data"] == CONF_DATA + assert "Discord integration in YAML" in caplog.text + + +async def test_flow_import_no_name(hass: HomeAssistant) -> None: + """Test import flow with no name in config.""" + with mocked_discord_info(), patch_discord_login(): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=CONF_IMPORT_DATA_NO_NAME, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == NAME + assert result["data"] == CONF_DATA + + +async def test_flow_import_already_configured(hass: HomeAssistant) -> None: + """Test an import flow already configured.""" + create_entry(hass) + with mocked_discord_info(), patch_discord_login(): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=CONF_IMPORT_DATA, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_flow_reauth(hass: HomeAssistant) -> None: + """Test a reauth flow.""" + entry = create_entry(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + CONF_SOURCE: config_entries.SOURCE_REAUTH, + "entry_id": entry.entry_id, + "unique_id": entry.unique_id, + }, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "reauth" + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={}, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "reauth_confirm" + + new_conf = {CONF_API_TOKEN: "1234567890123"} + with patch_discord_login() as mock: + mock.side_effect = nextcord.LoginFailure + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=new_conf, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "reauth_confirm" + assert result["errors"] == {"base": "invalid_auth"} + + with mocked_discord_info(), patch_discord_login(): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=new_conf, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "reauth_successful" + assert entry.data == CONF_DATA | new_conf diff --git a/tests/components/dlna_dmr/conftest.py b/tests/components/dlna_dmr/conftest.py index 3a9025a9a29..84aec044caf 100644 --- a/tests/components/dlna_dmr/conftest.py +++ b/tests/components/dlna_dmr/conftest.py @@ -5,7 +5,8 @@ from collections.abc import Iterable from socket import AddressFamily # pylint: disable=no-name-in-module from unittest.mock import Mock, create_autospec, patch, seal -from async_upnp_client import UpnpDevice, UpnpFactory, UpnpService +from async_upnp_client.client import UpnpDevice, UpnpService +from async_upnp_client.client_factory import UpnpFactory import pytest from homeassistant.components.dlna_dmr.const import DOMAIN as DLNA_DOMAIN diff --git a/tests/components/dlna_dmr/test_config_flow.py b/tests/components/dlna_dmr/test_config_flow.py index fb0c416c086..44ab4ea313f 100644 --- a/tests/components/dlna_dmr/test_config_flow.py +++ b/tests/components/dlna_dmr/test_config_flow.py @@ -4,7 +4,8 @@ from __future__ import annotations import dataclasses from unittest.mock import Mock -from async_upnp_client import UpnpDevice, UpnpError +from async_upnp_client.client import UpnpDevice +from async_upnp_client.exceptions import UpnpError import pytest from homeassistant import config_entries, data_entry_flow diff --git a/tests/components/dlna_dmr/test_data.py b/tests/components/dlna_dmr/test_data.py index 469cfcece88..5b2c0b1815c 100644 --- a/tests/components/dlna_dmr/test_data.py +++ b/tests/components/dlna_dmr/test_data.py @@ -4,8 +4,8 @@ from __future__ import annotations from collections.abc import Iterable from unittest.mock import ANY, Mock, patch -from async_upnp_client import UpnpEventHandler from async_upnp_client.aiohttp import AiohttpNotifyServer +from async_upnp_client.event_handler import UpnpEventHandler import pytest from homeassistant.components.dlna_dmr.const import DOMAIN @@ -37,7 +37,10 @@ def aiohttp_notify_servers_mock() -> Iterable[Mock]: # Every server must be stopped if it was started for server in servers: - assert server.start_server.call_count == server.stop_server.call_count + assert ( + server.async_start_server.call_count + == server.async_stop_server.call_count + ) async def test_get_domain_data(hass: HomeAssistant) -> None: @@ -60,7 +63,7 @@ async def test_event_notifier( # Check that the parameters were passed through to the AiohttpNotifyServer aiohttp_notify_servers_mock.assert_called_with( - requester=ANY, listen_port=0, listen_host=None, callback_url=None, loop=ANY + requester=ANY, source=("0.0.0.0", 0), callback_url=None, loop=ANY ) # Same address should give same notifier @@ -79,8 +82,7 @@ async def test_event_notifier( # Check that the parameters were passed through to the AiohttpNotifyServer aiohttp_notify_servers_mock.assert_called_with( requester=ANY, - listen_port=9999, - listen_host="192.88.99.4", + source=("192.88.99.4", 9999), callback_url="http://192.88.99.4:9999/notify", loop=ANY, ) diff --git a/tests/components/dlna_dmr/test_media_player.py b/tests/components/dlna_dmr/test_media_player.py index a9ac5946f30..bed89f3db9d 100644 --- a/tests/components/dlna_dmr/test_media_player.py +++ b/tests/components/dlna_dmr/test_media_player.py @@ -9,7 +9,7 @@ from types import MappingProxyType from typing import Any from unittest.mock import ANY, DEFAULT, Mock, patch -from async_upnp_client import UpnpService, UpnpStateVariable +from async_upnp_client.client import UpnpService, UpnpStateVariable from async_upnp_client.exceptions import ( UpnpConnectionError, UpnpError, diff --git a/tests/components/dlna_dms/conftest.py b/tests/components/dlna_dms/conftest.py index 6764001be31..4dcd135ea86 100644 --- a/tests/components/dlna_dms/conftest.py +++ b/tests/components/dlna_dms/conftest.py @@ -1,18 +1,23 @@ """Fixtures for DLNA DMS tests.""" from __future__ import annotations -from collections.abc import AsyncGenerator, Iterable -from typing import Final +from collections.abc import AsyncIterable, Iterable +from typing import Final, cast from unittest.mock import Mock, create_autospec, patch, seal -from async_upnp_client import UpnpDevice, UpnpService +from async_upnp_client.client import UpnpDevice, UpnpService from async_upnp_client.utils import absolute_url import pytest -from homeassistant.components.dlna_dms.const import DOMAIN -from homeassistant.components.dlna_dms.dms import DlnaDmsData, get_domain_data +from homeassistant.components.dlna_dms.const import ( + CONF_SOURCE_ID, + CONFIG_VERSION, + DOMAIN, +) +from homeassistant.components.dlna_dms.dms import DlnaDmsData from homeassistant.const import CONF_DEVICE_ID, CONF_URL from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry @@ -31,6 +36,12 @@ EVENT_CALLBACK_URL: Final = "http://192.88.99.1/notify" NEW_DEVICE_LOCATION: Final = "http://192.88.99.7" + "/dmr_description.xml" +@pytest.fixture +async def setup_media_source(hass) -> None: + """Set up media source.""" + assert await async_setup_component(hass, "media_source", {}) + + @pytest.fixture def upnp_factory_mock() -> Iterable[Mock]: """Mock the UpnpFactory class to construct DMS-style UPnP devices.""" @@ -69,21 +80,13 @@ def upnp_factory_mock() -> Iterable[Mock]: yield upnp_factory_instance -@pytest.fixture -async def domain_data_mock( - hass: HomeAssistant, aioclient_mock, upnp_factory_mock -) -> AsyncGenerator[DlnaDmsData, None]: - """Mock some global data used by this component. - - This includes network clients and library object factories. Mocking it - prevents network use. - - Yields the actual domain data, for ease of access - """ +@pytest.fixture(autouse=True, scope="module") +def aiohttp_session_requester_mock() -> Iterable[Mock]: + """Mock the AiohttpSessionRequester to prevent network use.""" with patch( "homeassistant.components.dlna_dms.dms.AiohttpSessionRequester", autospec=True - ): - yield get_domain_data(hass) + ) as requester_mock: + yield requester_mock @pytest.fixture @@ -92,9 +95,11 @@ def config_entry_mock() -> MockConfigEntry: mock_entry = MockConfigEntry( unique_id=MOCK_DEVICE_USN, domain=DOMAIN, + version=CONFIG_VERSION, data={ CONF_URL: MOCK_DEVICE_LOCATION, CONF_DEVICE_ID: MOCK_DEVICE_USN, + CONF_SOURCE_ID: MOCK_SOURCE_ID, }, title=MOCK_DEVICE_NAME, ) @@ -129,3 +134,40 @@ def ssdp_scanner_mock() -> Iterable[Mock]: reg_callback = mock_scanner.return_value.async_register_callback reg_callback.return_value = Mock(return_value=None) yield mock_scanner.return_value + + +@pytest.fixture +async def device_source_mock( + hass: HomeAssistant, + config_entry_mock: MockConfigEntry, + ssdp_scanner_mock: Mock, + dms_device_mock: Mock, +) -> AsyncIterable[None]: + """Fixture to set up a DmsDeviceSource in a connected state and cleanup at completion.""" + config_entry_mock.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry_mock.entry_id) + await hass.async_block_till_done() + + # Check the DmsDeviceSource has registered all needed listeners + assert len(config_entry_mock.update_listeners) == 0 + assert ssdp_scanner_mock.async_register_callback.await_count == 2 + assert ssdp_scanner_mock.async_register_callback.return_value.call_count == 0 + + # Run the test + yield None + + # Unload config entry to clean up + assert await hass.config_entries.async_remove(config_entry_mock.entry_id) == { + "require_restart": False + } + + # Check DmsDeviceSource has cleaned up its resources + assert not config_entry_mock.update_listeners + assert ( + ssdp_scanner_mock.async_register_callback.await_count + == ssdp_scanner_mock.async_register_callback.return_value.call_count + ) + + domain_data = cast(DlnaDmsData, hass.data[DOMAIN]) + assert MOCK_DEVICE_USN not in domain_data.devices + assert MOCK_SOURCE_ID not in domain_data.sources diff --git a/tests/components/dlna_dms/test_config_flow.py b/tests/components/dlna_dms/test_config_flow.py index df8d55dbc25..521c3169aa5 100644 --- a/tests/components/dlna_dms/test_config_flow.py +++ b/tests/components/dlna_dms/test_config_flow.py @@ -1,16 +1,17 @@ """Test the DLNA DMS config flow.""" from __future__ import annotations +from collections.abc import Iterable import dataclasses from typing import Final -from unittest.mock import Mock +from unittest.mock import Mock, patch -from async_upnp_client import UpnpError +from async_upnp_client.exceptions import UpnpError import pytest from homeassistant import config_entries, data_entry_flow from homeassistant.components import ssdp -from homeassistant.components.dlna_dms.const import DOMAIN +from homeassistant.components.dlna_dms.const import CONF_SOURCE_ID, DOMAIN from homeassistant.const import CONF_DEVICE_ID, CONF_HOST, CONF_URL from homeassistant.core import HomeAssistant @@ -21,17 +22,12 @@ from .conftest import ( MOCK_DEVICE_TYPE, MOCK_DEVICE_UDN, MOCK_DEVICE_USN, + MOCK_SOURCE_ID, NEW_DEVICE_LOCATION, ) from tests.common import MockConfigEntry -# Auto-use the domain_data_mock and dms_device_mock fixtures for every test in this module -pytestmark = [ - pytest.mark.usefixtures("domain_data_mock"), - pytest.mark.usefixtures("dms_device_mock"), -] - WRONG_DEVICE_TYPE: Final = "urn:schemas-upnp-org:device:InternetGatewayDevice:1" MOCK_ROOT_DEVICE_UDN: Final = "ROOT_DEVICE" @@ -68,6 +64,16 @@ MOCK_DISCOVERY: Final = ssdp.SsdpServiceInfo( ) +@pytest.fixture(autouse=True) +def mock_setup_entry() -> Iterable[Mock]: + """Avoid setting up the entire integration.""" + with patch( + "homeassistant.components.dlna_dms.async_setup_entry", + return_value=True, + ) as mock: + yield mock + + async def test_user_flow(hass: HomeAssistant, ssdp_scanner_mock: Mock) -> None: """Test user-init'd flow, user selects discovered device.""" ssdp_scanner_mock.async_get_discovery_info_by_st.side_effect = [ @@ -87,17 +93,17 @@ async def test_user_flow(hass: HomeAssistant, ssdp_scanner_mock: Mock) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_HOST: MOCK_DEVICE_HOST} ) + await hass.async_block_till_done() assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["title"] == MOCK_DEVICE_NAME assert result["data"] == { CONF_URL: MOCK_DEVICE_LOCATION, CONF_DEVICE_ID: MOCK_DEVICE_USN, + CONF_SOURCE_ID: MOCK_SOURCE_ID, } assert result["options"] == {} - await hass.async_block_till_done() - async def test_user_flow_no_devices( hass: HomeAssistant, ssdp_scanner_mock: Mock @@ -137,12 +143,13 @@ async def test_ssdp_flow_success(hass: HomeAssistant) -> None: assert result["data"] == { CONF_URL: MOCK_DEVICE_LOCATION, CONF_DEVICE_ID: MOCK_DEVICE_USN, + CONF_SOURCE_ID: MOCK_SOURCE_ID, } assert result["options"] == {} async def test_ssdp_flow_unavailable( - hass: HomeAssistant, domain_data_mock: Mock + hass: HomeAssistant, upnp_factory_mock: Mock ) -> None: """Test that SSDP discovery with an unavailable device still succeeds. @@ -157,7 +164,7 @@ async def test_ssdp_flow_unavailable( assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "confirm" - domain_data_mock.upnp_factory.async_create_device.side_effect = UpnpError + upnp_factory_mock.async_create_device.side_effect = UpnpError result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} @@ -169,6 +176,7 @@ async def test_ssdp_flow_unavailable( assert result["data"] == { CONF_URL: MOCK_DEVICE_LOCATION, CONF_DEVICE_ID: MOCK_DEVICE_USN, + CONF_SOURCE_ID: MOCK_SOURCE_ID, } assert result["options"] == {} @@ -213,9 +221,7 @@ async def test_ssdp_flow_duplicate_location( assert config_entry_mock.data[CONF_URL] == MOCK_DEVICE_LOCATION -async def test_ssdp_flow_bad_data( - hass: HomeAssistant, config_entry_mock: MockConfigEntry -) -> None: +async def test_ssdp_flow_bad_data(hass: HomeAssistant) -> None: """Test bad SSDP discovery information is rejected cleanly.""" # Missing location discovery = dataclasses.replace(MOCK_DISCOVERY, ssdp_location="") @@ -241,15 +247,16 @@ async def test_ssdp_flow_bad_data( async def test_duplicate_name( hass: HomeAssistant, config_entry_mock: MockConfigEntry ) -> None: - """Test device with name same as another results in no error.""" + """Test device with name same as other devices results in no error.""" + # Add two entries to test generate_source_id() tries for no collisions config_entry_mock.add_to_hass(hass) - mock_entry_1 = MockConfigEntry( unique_id="mock_entry_1", domain=DOMAIN, data={ CONF_URL: "not-important", CONF_DEVICE_ID: "not-important", + CONF_SOURCE_ID: f"{MOCK_SOURCE_ID}_1", }, title=MOCK_DEVICE_NAME, ) @@ -286,6 +293,7 @@ async def test_duplicate_name( assert result["data"] == { CONF_URL: new_device_location, CONF_DEVICE_ID: new_device_usn, + CONF_SOURCE_ID: f"{MOCK_SOURCE_ID}_2", } assert result["options"] == {} diff --git a/tests/components/dlna_dms/test_device_availability.py b/tests/components/dlna_dms/test_device_availability.py index a0cfb3ab2d2..67ad1024709 100644 --- a/tests/components/dlna_dms/test_device_availability.py +++ b/tests/components/dlna_dms/test_device_availability.py @@ -4,81 +4,56 @@ from __future__ import annotations import asyncio from collections.abc import AsyncIterable import logging -from unittest.mock import ANY, DEFAULT, Mock, patch +from typing import Final +from unittest.mock import ANY, DEFAULT, Mock from async_upnp_client.exceptions import UpnpConnectionError, UpnpError from didl_lite import didl_lite import pytest -from homeassistant.components import ssdp +from homeassistant.components import media_source, ssdp from homeassistant.components.dlna_dms.const import DOMAIN -from homeassistant.components.dlna_dms.dms import DmsDeviceSource, get_domain_data +from homeassistant.components.dlna_dms.dms import get_domain_data from homeassistant.components.media_player.errors import BrowseError from homeassistant.components.media_source.error import Unresolvable from homeassistant.core import HomeAssistant -from homeassistant.setup import async_setup_component from .conftest import ( MOCK_DEVICE_LOCATION, - MOCK_DEVICE_NAME, MOCK_DEVICE_TYPE, MOCK_DEVICE_UDN, MOCK_DEVICE_USN, + MOCK_SOURCE_ID, NEW_DEVICE_LOCATION, ) from tests.common import MockConfigEntry -# Auto-use the domain_data_mock for every test in this module +DUMMY_OBJECT_ID: Final = "123" + +# Auto-use a few fixtures from conftest pytestmark = [ - pytest.mark.usefixtures("domain_data_mock"), + # Block network access + pytest.mark.usefixtures("aiohttp_session_requester_mock"), + pytest.mark.usefixtures("dms_device_mock"), + # Setup the media_source platform + pytest.mark.usefixtures("setup_media_source"), ] -async def setup_mock_component( - hass: HomeAssistant, mock_entry: MockConfigEntry -) -> DmsDeviceSource: - """Set up a mock DlnaDmrEntity with the given configuration.""" - mock_entry.add_to_hass(hass) - assert await async_setup_component(hass, DOMAIN, {}) is True - await hass.async_block_till_done() - - domain_data = get_domain_data(hass) - return next(iter(domain_data.devices.values())) - - @pytest.fixture async def connected_source_mock( - hass: HomeAssistant, - config_entry_mock: MockConfigEntry, - ssdp_scanner_mock: Mock, - dms_device_mock: Mock, -) -> AsyncIterable[DmsDeviceSource]: - """Fixture to set up a mock DmsDeviceSource in a connected state. - - Yields the entity. Cleans up the entity after the test is complete. - """ - entity = await setup_mock_component(hass, config_entry_mock) - - # Check the entity has registered all needed listeners - assert len(config_entry_mock.update_listeners) == 1 - assert ssdp_scanner_mock.async_register_callback.await_count == 2 - assert ssdp_scanner_mock.async_register_callback.return_value.call_count == 0 - - # Run the test - yield entity - - # Unload config entry to clean up - assert await hass.config_entries.async_remove(config_entry_mock.entry_id) == { - "require_restart": False - } - - # Check entity has cleaned up its resources - assert not config_entry_mock.update_listeners - assert ( - ssdp_scanner_mock.async_register_callback.await_count - == ssdp_scanner_mock.async_register_callback.return_value.call_count + dms_device_mock: Mock, device_source_mock: None +) -> None: + """Fixture to set up a mock DmsDeviceSource in a connected state.""" + # Make async_browse_metadata work for assert_source_available + didl_item = didl_lite.Item( + id=DUMMY_OBJECT_ID, + restricted=False, + title="Object", + res=[didl_lite.Resource(uri="foo/bar", protocol_info="http-get:*:audio/mpeg:")], ) + dms_device_mock.async_browse_metadata.return_value = didl_item @pytest.fixture @@ -88,30 +63,39 @@ async def disconnected_source_mock( config_entry_mock: MockConfigEntry, ssdp_scanner_mock: Mock, dms_device_mock: Mock, -) -> AsyncIterable[DmsDeviceSource]: - """Fixture to set up a mock DmsDeviceSource in a disconnected state. - - Yields the entity. Cleans up the entity after the test is complete. - """ +) -> AsyncIterable[None]: + """Fixture to set up a mock DmsDeviceSource in a disconnected state.""" # Cause the connection attempt to fail upnp_factory_mock.async_create_device.side_effect = UpnpConnectionError - entity = await setup_mock_component(hass, config_entry_mock) + config_entry_mock.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry_mock.entry_id) + await hass.async_block_till_done() - # Check the entity has registered all needed listeners - assert len(config_entry_mock.update_listeners) == 1 + # Check the DmsDeviceSource has registered all needed listeners + assert len(config_entry_mock.update_listeners) == 0 assert ssdp_scanner_mock.async_register_callback.await_count == 2 assert ssdp_scanner_mock.async_register_callback.return_value.call_count == 0 + # Make async_browse_metadata work for assert_source_available when this + # source is connected + didl_item = didl_lite.Item( + id=DUMMY_OBJECT_ID, + restricted=False, + title="Object", + res=[didl_lite.Resource(uri="foo/bar", protocol_info="http-get:*:audio/mpeg:")], + ) + dms_device_mock.async_browse_metadata.return_value = didl_item + # Run the test - yield entity + yield # Unload config entry to clean up assert await hass.config_entries.async_remove(config_entry_mock.entry_id) == { "require_restart": False } - # Check entity has cleaned up its resources + # Check device source has cleaned up its resources assert not config_entry_mock.update_listeners assert ( ssdp_scanner_mock.async_register_callback.await_count @@ -119,24 +103,28 @@ async def disconnected_source_mock( ) +async def assert_source_available(hass: HomeAssistant) -> None: + """Assert that the DmsDeviceSource under test can be used.""" + assert await media_source.async_browse_media( + hass, f"media-source://{DOMAIN}/{MOCK_SOURCE_ID}/:{DUMMY_OBJECT_ID}" + ) + + +async def assert_source_unavailable(hass: HomeAssistant) -> None: + """Assert that the DmsDeviceSource under test cannot be used.""" + with pytest.raises(Unresolvable, match="DMS is not connected"): + await media_source.async_browse_media( + hass, f"media-source://{DOMAIN}/{MOCK_SOURCE_ID}/:{DUMMY_OBJECT_ID}" + ) + + async def test_unavailable_device( hass: HomeAssistant, upnp_factory_mock: Mock, ssdp_scanner_mock: Mock, - config_entry_mock: MockConfigEntry, + disconnected_source_mock: None, ) -> None: """Test a DlnaDmsEntity with out a connected DmsDevice.""" - # Cause connection attempts to fail - upnp_factory_mock.async_create_device.side_effect = UpnpConnectionError - - with patch( - "homeassistant.components.dlna_dms.dms.DmsDevice", autospec=True - ) as dms_device_constructor_mock: - connected_source_mock = await setup_mock_component(hass, config_entry_mock) - - # Check device is not created - dms_device_constructor_mock.assert_not_called() - # Check attempt was made to create a device from the supplied URL upnp_factory_mock.async_create_device.assert_awaited_once_with(MOCK_DEVICE_LOCATION) # Check SSDP notifications are registered @@ -147,46 +135,42 @@ async def test_unavailable_device( ANY, {"_udn": MOCK_DEVICE_UDN, "NTS": "ssdp:byebye"} ) # Quick check of the state to verify the entity has no connected DmsDevice - assert not connected_source_mock.available - # Check the name matches that supplied - assert connected_source_mock.name == MOCK_DEVICE_NAME + await assert_source_unavailable(hass) # Check attempts to browse and resolve media give errors - with pytest.raises(BrowseError): - await connected_source_mock.async_browse_media("/browse_path") - with pytest.raises(BrowseError): - await connected_source_mock.async_browse_media(":browse_object") - with pytest.raises(BrowseError): - await connected_source_mock.async_browse_media("?browse_search") + with pytest.raises(BrowseError, match="DMS is not connected"): + await media_source.async_browse_media( + hass, f"media-source://{DOMAIN}/{MOCK_SOURCE_ID}//browse_path" + ) + with pytest.raises(BrowseError, match="DMS is not connected"): + await media_source.async_browse_media( + hass, f"media-source://{DOMAIN}/{MOCK_SOURCE_ID}/:browse_object" + ) + with pytest.raises(BrowseError, match="DMS is not connected"): + await media_source.async_browse_media( + hass, f"media-source://{DOMAIN}/{MOCK_SOURCE_ID}/?browse_search" + ) + with pytest.raises(Unresolvable, match="DMS is not connected"): + await media_source.async_resolve_media( + hass, f"media-source://{DOMAIN}/{MOCK_SOURCE_ID}//resolve_path" + ) + with pytest.raises(Unresolvable, match="DMS is not connected"): + await media_source.async_resolve_media( + hass, f"media-source://{DOMAIN}/{MOCK_SOURCE_ID}/:resolve_object" + ) with pytest.raises(Unresolvable): - await connected_source_mock.async_resolve_media("/resolve_path") - with pytest.raises(Unresolvable): - await connected_source_mock.async_resolve_media(":resolve_object") - with pytest.raises(Unresolvable): - await connected_source_mock.async_resolve_media("?resolve_search") - - # Unload config entry to clean up - assert await hass.config_entries.async_remove(config_entry_mock.entry_id) == { - "require_restart": False - } - - # Confirm SSDP notifications unregistered - assert ssdp_scanner_mock.async_register_callback.return_value.call_count == 2 + await media_source.async_resolve_media( + hass, f"media-source://{DOMAIN}/{MOCK_SOURCE_ID}/?resolve_search" + ) async def test_become_available( hass: HomeAssistant, upnp_factory_mock: Mock, ssdp_scanner_mock: Mock, - config_entry_mock: MockConfigEntry, - dms_device_mock: Mock, + disconnected_source_mock: None, ) -> None: """Test a device becoming available after the entity is constructed.""" - # Cause connection attempts to fail before adding the entity - upnp_factory_mock.async_create_device.side_effect = UpnpConnectionError - connected_source_mock = await setup_mock_component(hass, config_entry_mock) - assert not connected_source_mock.available - # Mock device is now available. upnp_factory_mock.async_create_device.side_effect = None upnp_factory_mock.async_create_device.reset_mock() @@ -207,22 +191,14 @@ async def test_become_available( # Check device was created from the supplied URL upnp_factory_mock.async_create_device.assert_awaited_once_with(NEW_DEVICE_LOCATION) # Quick check of the state to verify the entity has a connected DmsDevice - assert connected_source_mock.available - - # Unload config entry to clean up - assert await hass.config_entries.async_remove(config_entry_mock.entry_id) == { - "require_restart": False - } - - # Confirm SSDP notifications unregistered - assert ssdp_scanner_mock.async_register_callback.return_value.call_count == 2 + await assert_source_available(hass) async def test_alive_but_gone( hass: HomeAssistant, upnp_factory_mock: Mock, ssdp_scanner_mock: Mock, - disconnected_source_mock: DmsDeviceSource, + disconnected_source_mock: None, ) -> None: """Test a device sending an SSDP alive announcement, but not being connectable.""" upnp_factory_mock.async_create_device.side_effect = UpnpError @@ -245,7 +221,7 @@ async def test_alive_but_gone( upnp_factory_mock.async_create_device.assert_awaited() # Device should still be unavailable - assert not disconnected_source_mock.available + await assert_source_unavailable(hass) # Send the same SSDP notification, expecting no extra connection attempts upnp_factory_mock.async_create_device.reset_mock() @@ -262,7 +238,7 @@ async def test_alive_but_gone( await hass.async_block_till_done() upnp_factory_mock.async_create_device.assert_not_called() upnp_factory_mock.async_create_device.assert_not_awaited() - assert not disconnected_source_mock.available + await assert_source_unavailable(hass) # Send an SSDP notification with a new BOOTID, indicating the device has rebooted upnp_factory_mock.async_create_device.reset_mock() @@ -280,7 +256,7 @@ async def test_alive_but_gone( # Rebooted device (seen via BOOTID) should mean a new connection attempt upnp_factory_mock.async_create_device.assert_awaited() - assert not disconnected_source_mock.available + await assert_source_unavailable(hass) # Send byebye message to indicate device is going away. Next alive message # should result in a reconnect attempt even with same BOOTID. @@ -307,14 +283,14 @@ async def test_alive_but_gone( # Rebooted device (seen via byebye/alive) should mean a new connection attempt upnp_factory_mock.async_create_device.assert_awaited() - assert not disconnected_source_mock.available + await assert_source_unavailable(hass) async def test_multiple_ssdp_alive( hass: HomeAssistant, upnp_factory_mock: Mock, ssdp_scanner_mock: Mock, - disconnected_source_mock: DmsDeviceSource, + disconnected_source_mock: None, ) -> None: """Test multiple SSDP alive notifications is ok, only connects to device once.""" upnp_factory_mock.async_create_device.reset_mock() @@ -356,13 +332,13 @@ async def test_multiple_ssdp_alive( upnp_factory_mock.async_create_device.assert_awaited_once_with(NEW_DEVICE_LOCATION) # Device should be available - assert disconnected_source_mock.available + await assert_source_available(hass) async def test_ssdp_byebye( hass: HomeAssistant, ssdp_scanner_mock: Mock, - connected_source_mock: DmsDeviceSource, + connected_source_mock: None, ) -> None: """Test device is disconnected when byebye is received.""" # First byebye will cause a disconnect @@ -379,7 +355,7 @@ async def test_ssdp_byebye( ) # Device should be gone - assert not connected_source_mock.available + await assert_source_unavailable(hass) # Second byebye will do nothing await ssdp_callback( @@ -398,12 +374,11 @@ async def test_ssdp_update_seen_bootid( hass: HomeAssistant, ssdp_scanner_mock: Mock, upnp_factory_mock: Mock, - disconnected_source_mock: DmsDeviceSource, + disconnected_source_mock: None, ) -> None: """Test device does not reconnect when it gets ssdp:update with next bootid.""" # Start with a disconnected device - entity = disconnected_source_mock - assert not entity.available + await assert_source_unavailable(hass) # "Reconnect" the device upnp_factory_mock.async_create_device.reset_mock() @@ -424,7 +399,7 @@ async def test_ssdp_update_seen_bootid( await hass.async_block_till_done() # Device should be connected - assert entity.available + await assert_source_available(hass) assert upnp_factory_mock.async_create_device.await_count == 1 # Send SSDP update with next boot ID @@ -445,7 +420,7 @@ async def test_ssdp_update_seen_bootid( await hass.async_block_till_done() # Device was not reconnected, even with a new boot ID - assert entity.available + await assert_source_available(hass) assert upnp_factory_mock.async_create_device.await_count == 1 # Send SSDP update with same next boot ID, again @@ -466,7 +441,7 @@ async def test_ssdp_update_seen_bootid( await hass.async_block_till_done() # Nothing should change - assert entity.available + await assert_source_available(hass) assert upnp_factory_mock.async_create_device.await_count == 1 # Send SSDP update with bad next boot ID @@ -487,7 +462,7 @@ async def test_ssdp_update_seen_bootid( await hass.async_block_till_done() # Nothing should change - assert entity.available + await assert_source_available(hass) assert upnp_factory_mock.async_create_device.await_count == 1 # Send a new SSDP alive with the new boot ID, device should not reconnect @@ -503,7 +478,7 @@ async def test_ssdp_update_seen_bootid( ) await hass.async_block_till_done() - assert entity.available + await assert_source_available(hass) assert upnp_factory_mock.async_create_device.await_count == 1 @@ -511,12 +486,11 @@ async def test_ssdp_update_missed_bootid( hass: HomeAssistant, ssdp_scanner_mock: Mock, upnp_factory_mock: Mock, - disconnected_source_mock: DmsDeviceSource, + disconnected_source_mock: None, ) -> None: """Test device disconnects when it gets ssdp:update bootid it wasn't expecting.""" # Start with a disconnected device - entity = disconnected_source_mock - assert not entity.available + await assert_source_unavailable(hass) # "Reconnect" the device upnp_factory_mock.async_create_device.reset_mock() @@ -537,7 +511,7 @@ async def test_ssdp_update_missed_bootid( await hass.async_block_till_done() # Device should be connected - assert entity.available + await assert_source_available(hass) assert upnp_factory_mock.async_create_device.await_count == 1 # Send SSDP update with skipped boot ID (not previously seen) @@ -558,7 +532,7 @@ async def test_ssdp_update_missed_bootid( await hass.async_block_till_done() # Device should not *re*-connect yet - assert entity.available + await assert_source_available(hass) assert upnp_factory_mock.async_create_device.await_count == 1 # Send a new SSDP alive with the new boot ID, device should reconnect @@ -574,7 +548,7 @@ async def test_ssdp_update_missed_bootid( ) await hass.async_block_till_done() - assert entity.available + await assert_source_available(hass) assert upnp_factory_mock.async_create_device.await_count == 2 @@ -582,12 +556,11 @@ async def test_ssdp_bootid( hass: HomeAssistant, upnp_factory_mock: Mock, ssdp_scanner_mock: Mock, - disconnected_source_mock: DmsDeviceSource, + disconnected_source_mock: None, ) -> None: """Test an alive with a new BOOTID.UPNP.ORG header causes a reconnect.""" # Start with a disconnected device - entity = disconnected_source_mock - assert not entity.available + await assert_source_unavailable(hass) # "Reconnect" the device upnp_factory_mock.async_create_device.side_effect = None @@ -607,7 +580,7 @@ async def test_ssdp_bootid( ) await hass.async_block_till_done() - assert entity.available + await assert_source_available(hass) assert upnp_factory_mock.async_create_device.await_count == 1 # Send SSDP alive with same boot ID, nothing should happen @@ -623,7 +596,7 @@ async def test_ssdp_bootid( ) await hass.async_block_till_done() - assert entity.available + await assert_source_available(hass) assert upnp_factory_mock.async_create_device.await_count == 1 # Send a new SSDP alive with an incremented boot ID, device should be dis/reconnected @@ -639,44 +612,32 @@ async def test_ssdp_bootid( ) await hass.async_block_till_done() - assert entity.available + await assert_source_available(hass) assert upnp_factory_mock.async_create_device.await_count == 2 async def test_repeated_connect( caplog: pytest.LogCaptureFixture, - connected_source_mock: DmsDeviceSource, + hass: HomeAssistant, upnp_factory_mock: Mock, + connected_source_mock: None, ) -> None: """Test trying to connect an already connected device is safely ignored.""" upnp_factory_mock.async_create_device.reset_mock() - # Calling internal function directly to skip trying to time 2 SSDP messages carefully - with caplog.at_level(logging.DEBUG): - await connected_source_mock.device_connect() - assert ( - "Trying to connect when device already connected" == caplog.records[-1].message - ) - assert not upnp_factory_mock.async_create_device.await_count - -async def test_connect_no_location( - caplog: pytest.LogCaptureFixture, - disconnected_source_mock: DmsDeviceSource, - upnp_factory_mock: Mock, -) -> None: - """Test trying to connect without a location is safely ignored.""" - disconnected_source_mock.location = "" - upnp_factory_mock.async_create_device.reset_mock() # Calling internal function directly to skip trying to time 2 SSDP messages carefully + domain_data = get_domain_data(hass) + device_source = domain_data.sources[MOCK_SOURCE_ID] with caplog.at_level(logging.DEBUG): - await disconnected_source_mock.device_connect() - assert "Not connecting because location is not known" == caplog.records[-1].message + await device_source.device_connect() + assert not upnp_factory_mock.async_create_device.await_count + await assert_source_available(hass) async def test_become_unavailable( hass: HomeAssistant, - connected_source_mock: DmsDeviceSource, + connected_source_mock: None, dms_device_mock: Mock, ) -> None: """Test a device becoming unavailable.""" @@ -689,17 +650,18 @@ async def test_become_unavailable( ) # Check async_resolve_object currently works - await connected_source_mock.async_resolve_media(":object_id") + assert await media_source.async_resolve_media( + hass, f"media-source://{DOMAIN}/{MOCK_SOURCE_ID}/:object_id" + ) # Now break the network connection dms_device_mock.async_browse_metadata.side_effect = UpnpConnectionError - # The device should be considered available until next contacted - assert connected_source_mock.available - # async_resolve_object should fail with pytest.raises(Unresolvable): - await connected_source_mock.async_resolve_media(":object_id") + await media_source.async_resolve_media( + hass, f"media-source://{DOMAIN}/{MOCK_SOURCE_ID}/:object_id" + ) # The device should now be unavailable - assert not connected_source_mock.available + await assert_source_unavailable(hass) diff --git a/tests/components/dlna_dms/test_dms_device_source.py b/tests/components/dlna_dms/test_dms_device_source.py index d6fcdb267d6..5e4021a5dda 100644 --- a/tests/components/dlna_dms/test_dms_device_source.py +++ b/tests/components/dlna_dms/test_dms_device_source.py @@ -1,5 +1,6 @@ -"""Test the interface methods of DmsDeviceSource, except availability.""" -from collections.abc import AsyncIterable +"""Test the browse and resolve methods of DmsDeviceSource.""" +from __future__ import annotations + from typing import Final, Union from unittest.mock import ANY, Mock, call @@ -8,215 +9,155 @@ from async_upnp_client.profiles.dlna import ContentDirectoryErrorCode, DmsDevice from didl_lite import didl_lite import pytest +from homeassistant.components import media_source, ssdp from homeassistant.components.dlna_dms.const import DLNA_SORT_CRITERIA, DOMAIN -from homeassistant.components.dlna_dms.dms import ( - ActionError, - DeviceConnectionError, - DlnaDmsData, - DmsDeviceSource, -) +from homeassistant.components.dlna_dms.dms import DidlPlayMedia from homeassistant.components.media_player.errors import BrowseError from homeassistant.components.media_source.error import Unresolvable from homeassistant.components.media_source.models import BrowseMediaSource -from homeassistant.const import CONF_DEVICE_ID, CONF_URL from homeassistant.core import HomeAssistant from .conftest import ( MOCK_DEVICE_BASE_URL, MOCK_DEVICE_NAME, MOCK_DEVICE_TYPE, + MOCK_DEVICE_UDN, MOCK_DEVICE_USN, MOCK_SOURCE_ID, ) -from tests.common import MockConfigEntry +# Auto-use a few fixtures from conftest +pytestmark = [ + # Block network access + pytest.mark.usefixtures("aiohttp_session_requester_mock"), + # Setup the media_source platform + pytest.mark.usefixtures("setup_media_source"), + # Have a connected device so that test can successfully call browse and resolve + pytest.mark.usefixtures("device_source_mock"), +] + BrowseResultList = list[Union[didl_lite.DidlObject, didl_lite.Descriptor]] -@pytest.fixture -async def device_source_mock( - hass: HomeAssistant, - config_entry_mock: MockConfigEntry, - ssdp_scanner_mock: Mock, - dms_device_mock: Mock, - domain_data_mock: DlnaDmsData, -) -> AsyncIterable[DmsDeviceSource]: - """Fixture to set up a DmsDeviceSource in a connected state and cleanup at completion.""" - await hass.config_entries.async_add(config_entry_mock) - await hass.async_block_till_done() - - mock_entity = domain_data_mock.devices[MOCK_DEVICE_USN] - - # Check the DmsDeviceSource has registered all needed listeners - assert len(config_entry_mock.update_listeners) == 1 - assert ssdp_scanner_mock.async_register_callback.await_count == 2 - assert ssdp_scanner_mock.async_register_callback.return_value.call_count == 0 - - # Run the test - yield mock_entity - - # Unload config entry to clean up - assert await hass.config_entries.async_remove(config_entry_mock.entry_id) == { - "require_restart": False - } - - # Check DmsDeviceSource has cleaned up its resources - assert not config_entry_mock.update_listeners - assert ( - ssdp_scanner_mock.async_register_callback.await_count - == ssdp_scanner_mock.async_register_callback.return_value.call_count +async def async_resolve_media( + hass: HomeAssistant, media_content_id: str +) -> DidlPlayMedia: + """Call media_source.async_resolve_media with the test source's ID.""" + result = await media_source.async_resolve_media( + hass, f"media-source://{DOMAIN}/{MOCK_SOURCE_ID}/{media_content_id}" ) - assert MOCK_DEVICE_USN not in domain_data_mock.devices - assert MOCK_SOURCE_ID not in domain_data_mock.sources + assert isinstance(result, DidlPlayMedia) + return result -async def test_update_source_id( +async def async_browse_media( hass: HomeAssistant, - config_entry_mock: MockConfigEntry, - device_source_mock: DmsDeviceSource, - domain_data_mock: DlnaDmsData, -) -> None: - """Test the config listener updates the source_id and source list upon title change.""" - new_title: Final = "New Name" - new_source_id: Final = "new_name" - assert domain_data_mock.sources.keys() == {MOCK_SOURCE_ID} - hass.config_entries.async_update_entry(config_entry_mock, title=new_title) - await hass.async_block_till_done() - - assert device_source_mock.source_id == new_source_id - assert domain_data_mock.sources.keys() == {new_source_id} - - -async def test_update_existing_source_id( - caplog: pytest.LogCaptureFixture, - hass: HomeAssistant, - config_entry_mock: MockConfigEntry, - device_source_mock: DmsDeviceSource, - domain_data_mock: DlnaDmsData, -) -> None: - """Test the config listener gracefully handles colliding source_id.""" - new_title: Final = "New Name" - new_source_id: Final = "new_name" - new_source_id_2: Final = "new_name_1" - # Set up another config entry to collide with the new source_id - colliding_entry = MockConfigEntry( - unique_id=f"different-udn::{MOCK_DEVICE_TYPE}", - domain=DOMAIN, - data={ - CONF_URL: "http://192.88.99.22/dms_description.xml", - CONF_DEVICE_ID: f"different-udn::{MOCK_DEVICE_TYPE}", - }, - title=new_title, + media_content_id: str | None, +) -> BrowseMediaSource: + """Call media_source.async_browse_media with the test source's ID.""" + return await media_source.async_browse_media( + hass, f"media-source://{DOMAIN}/{MOCK_SOURCE_ID}/{media_content_id}" ) - await hass.config_entries.async_add(colliding_entry) - await hass.async_block_till_done() - - assert device_source_mock.source_id == MOCK_SOURCE_ID - assert domain_data_mock.sources.keys() == {MOCK_SOURCE_ID, new_source_id} - assert domain_data_mock.sources[MOCK_SOURCE_ID] is device_source_mock - - # Update the existing entry to match the other entry's name - hass.config_entries.async_update_entry(config_entry_mock, title=new_title) - await hass.async_block_till_done() - - # The existing device's source ID should be a newly generated slug - assert device_source_mock.source_id == new_source_id_2 - assert domain_data_mock.sources.keys() == {new_source_id, new_source_id_2} - assert domain_data_mock.sources[new_source_id_2] is device_source_mock - - # Changing back to the old name should not cause issues - hass.config_entries.async_update_entry(config_entry_mock, title=MOCK_DEVICE_NAME) - await hass.async_block_till_done() - - assert device_source_mock.source_id == MOCK_SOURCE_ID - assert domain_data_mock.sources.keys() == {MOCK_SOURCE_ID, new_source_id} - assert domain_data_mock.sources[MOCK_SOURCE_ID] is device_source_mock - - # Remove the collision and try again - await hass.config_entries.async_remove(colliding_entry.entry_id) - assert domain_data_mock.sources.keys() == {MOCK_SOURCE_ID} - - hass.config_entries.async_update_entry(config_entry_mock, title=new_title) - await hass.async_block_till_done() - - assert device_source_mock.source_id == new_source_id - assert domain_data_mock.sources.keys() == {new_source_id} async def test_catch_request_error_unavailable( - device_source_mock: DmsDeviceSource, + hass: HomeAssistant, ssdp_scanner_mock: Mock ) -> None: """Test the device is checked for availability before trying requests.""" - device_source_mock._device = None + # DmsDevice notifies of disconnect via SSDP + ssdp_callback = ssdp_scanner_mock.async_register_callback.call_args.args[0] + await ssdp_callback( + ssdp.SsdpServiceInfo( + ssdp_usn=MOCK_DEVICE_USN, + ssdp_udn=MOCK_DEVICE_UDN, + ssdp_headers={"NTS": "ssdp:byebye"}, + ssdp_st=MOCK_DEVICE_TYPE, + upnp={}, + ), + ssdp.SsdpChange.BYEBYE, + ) - with pytest.raises(DeviceConnectionError): - await device_source_mock.async_resolve_object("id") - with pytest.raises(DeviceConnectionError): - await device_source_mock.async_resolve_path("path") - with pytest.raises(DeviceConnectionError): - await device_source_mock.async_resolve_search("query") - with pytest.raises(DeviceConnectionError): - await device_source_mock.async_browse_object("object_id") - with pytest.raises(DeviceConnectionError): - await device_source_mock.async_browse_search("query") + # All attempts to use the device should give an error + with pytest.raises(Unresolvable, match="DMS is not connected"): + # Resolve object + await async_resolve_media(hass, ":id") + with pytest.raises(Unresolvable, match="DMS is not connected"): + # Resolve path + await async_resolve_media(hass, "/path") + with pytest.raises(Unresolvable, match="DMS is not connected"): + # Resolve search + await async_resolve_media(hass, "?query") + with pytest.raises(BrowseError, match="DMS is not connected"): + # Browse object + await async_browse_media(hass, ":id") + with pytest.raises(BrowseError, match="DMS is not connected"): + # Browse path + await async_browse_media(hass, "/path") + with pytest.raises(BrowseError, match="DMS is not connected"): + # Browse search + await async_browse_media(hass, "?query") -async def test_catch_request_error( - device_source_mock: DmsDeviceSource, dms_device_mock: Mock -) -> None: +async def test_catch_request_error(hass: HomeAssistant, dms_device_mock: Mock) -> None: """Test errors when making requests to the device are handled.""" dms_device_mock.async_browse_metadata.side_effect = UpnpActionError( error_code=ContentDirectoryErrorCode.NO_SUCH_OBJECT ) - with pytest.raises(ActionError, match="No such object: bad_id"): - await device_source_mock.async_resolve_media(":bad_id") + with pytest.raises(Unresolvable, match="No such object: bad_id"): + await async_resolve_media(hass, ":bad_id") dms_device_mock.async_search_directory.side_effect = UpnpActionError( error_code=ContentDirectoryErrorCode.INVALID_SEARCH_CRITERIA ) - with pytest.raises(ActionError, match="Invalid query: bad query"): - await device_source_mock.async_resolve_media("?bad query") + with pytest.raises(Unresolvable, match="Invalid query: bad query"): + await async_resolve_media(hass, "?bad query") dms_device_mock.async_browse_metadata.side_effect = UpnpActionError( error_code=ContentDirectoryErrorCode.CANNOT_PROCESS_REQUEST ) - with pytest.raises(DeviceConnectionError, match="Server failure: "): - await device_source_mock.async_resolve_media(":good_id") + with pytest.raises(BrowseError, match="Server failure: "): + await async_resolve_media(hass, ":good_id") dms_device_mock.async_browse_metadata.side_effect = UpnpError with pytest.raises( - DeviceConnectionError, match="Server communication failure: UpnpError(.*)" + BrowseError, match="Server communication failure: UpnpError(.*)" ): - await device_source_mock.async_resolve_media(":bad_id") + await async_resolve_media(hass, ":bad_id") - # UpnpConnectionErrors will cause the device_source_mock to disconnect from the device - assert device_source_mock.available + +async def test_catch_upnp_connection_error( + hass: HomeAssistant, dms_device_mock: Mock +) -> None: + """Test UpnpConnectionError causes the device source to disconnect from the device.""" + # First check the source can be used + object_id = "foo" + didl_item = didl_lite.Item( + id=object_id, + restricted="false", + title="Object", + res=[didl_lite.Resource(uri="foo", protocol_info="http-get:*:audio/mpeg")], + ) + dms_device_mock.async_browse_metadata.return_value = didl_item + await async_browse_media(hass, f":{object_id}") + dms_device_mock.async_browse_metadata.assert_awaited_once_with( + object_id, metadata_filter=ANY + ) + + # Cause a UpnpConnectionError when next browsing dms_device_mock.async_browse_metadata.side_effect = UpnpConnectionError with pytest.raises( - DeviceConnectionError, match="Server disconnected: UpnpConnectionError(.*)" + BrowseError, match="Server disconnected: UpnpConnectionError(.*)" ): - await device_source_mock.async_resolve_media(":bad_id") - assert not device_source_mock.available + await async_browse_media(hass, f":{object_id}") + + # Clear the error, but the device should be disconnected + dms_device_mock.async_browse_metadata.side_effect = None + with pytest.raises(BrowseError, match="DMS is not connected"): + await async_browse_media(hass, f":{object_id}") -async def test_icon(device_source_mock: DmsDeviceSource, dms_device_mock: Mock) -> None: - """Test the device's icon URL is returned.""" - assert device_source_mock.icon == dms_device_mock.icon - - device_source_mock._device = None - assert device_source_mock.icon is None - - -async def test_resolve_media_invalid(device_source_mock: DmsDeviceSource) -> None: - """Test async_resolve_media will raise Unresolvable when an identifier isn't supplied.""" - with pytest.raises(Unresolvable, match="Invalid identifier.*"): - await device_source_mock.async_resolve_media("") - - -async def test_resolve_media_object( - device_source_mock: DmsDeviceSource, dms_device_mock: Mock -) -> None: +async def test_resolve_media_object(hass: HomeAssistant, dms_device_mock: Mock) -> None: """Test the async_resolve_object method via async_resolve_media.""" object_id: Final = "123" res_url: Final = "foo/bar" @@ -230,7 +171,7 @@ async def test_resolve_media_object( res=[didl_lite.Resource(uri=res_url, protocol_info=f"http-get:*:{res_mime}:")], ) dms_device_mock.async_browse_metadata.return_value = didl_item - result = await device_source_mock.async_resolve_media(f":{object_id}") + result = await async_resolve_media(hass, f":{object_id}") dms_device_mock.async_browse_metadata.assert_awaited_once_with( object_id, metadata_filter="*" ) @@ -251,7 +192,7 @@ async def test_resolve_media_object( ], ) dms_device_mock.async_browse_metadata.return_value = didl_item - result = await device_source_mock.async_resolve_media(f":{object_id}") + result = await async_resolve_media(hass, f":{object_id}") assert result.url == res_abs_url assert result.mime_type == res_mime assert result.didl_metadata is didl_item @@ -268,7 +209,7 @@ async def test_resolve_media_object( ], ) dms_device_mock.async_browse_metadata.return_value = didl_item - result = await device_source_mock.async_resolve_media(f":{object_id}") + result = await async_resolve_media(hass, f":{object_id}") assert result.url == res_abs_url assert result.mime_type == res_mime assert result.didl_metadata is didl_item @@ -282,7 +223,7 @@ async def test_resolve_media_object( ) dms_device_mock.async_browse_metadata.return_value = didl_item with pytest.raises(Unresolvable, match="Object has no resources"): - await device_source_mock.async_resolve_media(f":{object_id}") + await async_resolve_media(hass, f":{object_id}") # Failure case: resources are not playable didl_item = didl_lite.Item( @@ -293,13 +234,13 @@ async def test_resolve_media_object( ) dms_device_mock.async_browse_metadata.return_value = didl_item with pytest.raises(Unresolvable, match="Object has no playable resources"): - await device_source_mock.async_resolve_media(f":{object_id}") + await async_resolve_media(hass, f":{object_id}") -async def test_resolve_media_path( - device_source_mock: DmsDeviceSource, dms_device_mock: Mock -) -> None: +async def test_resolve_media_path(hass: HomeAssistant, dms_device_mock: Mock) -> None: """Test the async_resolve_path method via async_resolve_media.""" + # Path resolution involves searching each component of the path, then + # browsing the metadata of the final object found. path: Final = "path/to/thing" object_ids: Final = ["path_id", "to_id", "thing_id"] res_url: Final = "foo/bar" @@ -324,7 +265,7 @@ async def test_resolve_media_path( title="thing", res=[didl_lite.Resource(uri=res_url, protocol_info=f"http-get:*:{res_mime}:")], ) - result = await device_source_mock.async_resolve_media(f"/{path}") + result = await async_resolve_media(hass, f"/{path}") assert dms_device_mock.async_search_directory.await_args_list == [ call( parent_id, @@ -340,7 +281,7 @@ async def test_resolve_media_path( # Test a path starting with a / (first / is path action, second / is root of path) dms_device_mock.async_search_directory.reset_mock() dms_device_mock.async_search_directory.side_effect = search_directory_result - result = await device_source_mock.async_resolve_media(f"//{path}") + result = await async_resolve_media(hass, f"//{path}") assert dms_device_mock.async_search_directory.await_args_list == [ call( parent_id, @@ -354,44 +295,14 @@ async def test_resolve_media_path( assert result.mime_type == res_mime -async def test_resolve_path_simple( - device_source_mock: DmsDeviceSource, dms_device_mock: Mock -) -> None: - """Test async_resolve_path for simple success as for test_resolve_media_path.""" - path: Final = "path/to/thing" - object_ids: Final = ["path_id", "to_id", "thing_id"] - search_directory_result = [] - for ob_id, ob_title in zip(object_ids, path.split("/")): - didl_item = didl_lite.Item( - id=ob_id, - restricted="false", - title=ob_title, - res=[], - ) - search_directory_result.append(DmsDevice.BrowseResult([didl_item], 1, 1, 0)) - - dms_device_mock.async_search_directory.side_effect = search_directory_result - result = await device_source_mock.async_resolve_path(path) - assert dms_device_mock.async_search_directory.call_args_list == [ - call( - parent_id, - search_criteria=f'@parentID="{parent_id}" and dc:title="{title}"', - metadata_filter=["id", "upnp:class", "dc:title"], - requested_count=1, - ) - for parent_id, title in zip(["0"] + object_ids[:-1], path.split("/")) - ] - assert result == object_ids[-1] - assert not dms_device_mock.async_browse_direct_children.await_count - - -async def test_resolve_path_browsed( - device_source_mock: DmsDeviceSource, dms_device_mock: Mock -) -> None: +async def test_resolve_path_browsed(hass: HomeAssistant, dms_device_mock: Mock) -> None: """Test async_resolve_path: action error results in browsing.""" path: Final = "path/to/thing" object_ids: Final = ["path_id", "to_id", "thing_id"] + res_url: Final = "foo/bar" + res_mime: Final = "audio/mpeg" + # Setup expected calls search_directory_result = [] for ob_id, ob_title in zip(object_ids, path.split("/")): didl_item = didl_lite.Item( @@ -417,7 +328,15 @@ async def test_resolve_path_browsed( DmsDevice.BrowseResult(browse_children_result, 3, 3, 0) ] - result = await device_source_mock.async_resolve_path(path) + dms_device_mock.async_browse_metadata.return_value = didl_lite.Item( + id=object_ids[-1], + restricted="false", + title="thing", + res=[didl_lite.Resource(uri=res_url, protocol_info=f"http-get:*:{res_mime}:")], + ) + + # Perform the action to test + result = await async_resolve_media(hass, path) # All levels should have an attempted search assert dms_device_mock.async_search_directory.await_args_list == [ call( @@ -428,7 +347,7 @@ async def test_resolve_path_browsed( ) for parent_id, title in zip(["0"] + object_ids[:-1], path.split("/")) ] - assert result == object_ids[-1] + assert result.didl_metadata.id == object_ids[-1] # 2nd level should also be browsed assert dms_device_mock.async_browse_direct_children.await_args_list == [ call("path_id", metadata_filter=["id", "upnp:class", "dc:title"]) @@ -436,7 +355,7 @@ async def test_resolve_path_browsed( async def test_resolve_path_browsed_nothing( - device_source_mock: DmsDeviceSource, dms_device_mock: Mock + hass: HomeAssistant, dms_device_mock: Mock ) -> None: """Test async_resolve_path: action error results in browsing, but nothing found.""" dms_device_mock.async_search_directory.side_effect = UpnpActionError() @@ -445,7 +364,7 @@ async def test_resolve_path_browsed_nothing( DmsDevice.BrowseResult([], 0, 0, 0) ] with pytest.raises(Unresolvable, match="No contents for thing in thing/other"): - await device_source_mock.async_resolve_path(r"thing/other") + await async_resolve_media(hass, "thing/other") # There are children, but they don't match dms_device_mock.async_browse_direct_children.side_effect = [ @@ -461,12 +380,10 @@ async def test_resolve_path_browsed_nothing( ) ] with pytest.raises(Unresolvable, match="Nothing found for thing in thing/other"): - await device_source_mock.async_resolve_path(r"thing/other") + await async_resolve_media(hass, "thing/other") -async def test_resolve_path_quoted( - device_source_mock: DmsDeviceSource, dms_device_mock: Mock -) -> None: +async def test_resolve_path_quoted(hass: HomeAssistant, dms_device_mock: Mock) -> None: """Test async_resolve_path: quotes and backslashes in the path get escaped correctly.""" dms_device_mock.async_search_directory.side_effect = [ DmsDevice.BrowseResult( @@ -484,8 +401,8 @@ async def test_resolve_path_quoted( ), UpnpError("Quick abort"), ] - with pytest.raises(DeviceConnectionError): - await device_source_mock.async_resolve_path(r'path/quote"back\slash') + with pytest.raises(Unresolvable): + await async_resolve_media(hass, r'path/quote"back\slash') assert dms_device_mock.async_search_directory.await_args_list == [ call( "0", @@ -503,7 +420,7 @@ async def test_resolve_path_quoted( async def test_resolve_path_ambiguous( - device_source_mock: DmsDeviceSource, dms_device_mock: Mock + hass: HomeAssistant, dms_device_mock: Mock ) -> None: """Test async_resolve_path: ambiguous results (too many matches) gives error.""" dms_device_mock.async_search_directory.side_effect = [ @@ -530,23 +447,21 @@ async def test_resolve_path_ambiguous( with pytest.raises( Unresolvable, match="Too many items found for thing in thing/other" ): - await device_source_mock.async_resolve_path(r"thing/other") + await async_resolve_media(hass, "thing/other") async def test_resolve_path_no_such_container( - device_source_mock: DmsDeviceSource, dms_device_mock: Mock + hass: HomeAssistant, dms_device_mock: Mock ) -> None: """Test async_resolve_path: Explicit check for NO_SUCH_CONTAINER.""" dms_device_mock.async_search_directory.side_effect = UpnpActionError( error_code=ContentDirectoryErrorCode.NO_SUCH_CONTAINER ) with pytest.raises(Unresolvable, match="No such container: 0"): - await device_source_mock.async_resolve_path(r"thing/other") + await async_resolve_media(hass, "thing/other") -async def test_resolve_media_search( - device_source_mock: DmsDeviceSource, dms_device_mock: Mock -) -> None: +async def test_resolve_media_search(hass: HomeAssistant, dms_device_mock: Mock) -> None: """Test the async_resolve_search method via async_resolve_media.""" res_url: Final = "foo/bar" res_abs_url: Final = f"{MOCK_DEVICE_BASE_URL}/{res_url}" @@ -557,7 +472,7 @@ async def test_resolve_media_search( [], 0, 0, 0 ) with pytest.raises(Unresolvable, match='Nothing found for dc:title="thing"'): - await device_source_mock.async_resolve_media('?dc:title="thing"') + await async_resolve_media(hass, '?dc:title="thing"') assert dms_device_mock.async_search_directory.await_args_list == [ call( container_id="0", @@ -578,7 +493,7 @@ async def test_resolve_media_search( dms_device_mock.async_search_directory.return_value = DmsDevice.BrowseResult( [didl_item], 1, 1, 0 ) - result = await device_source_mock.async_resolve_media('?dc:title="thing"') + result = await async_resolve_media(hass, '?dc:title="thing"') assert result.url == res_abs_url assert result.mime_type == res_mime assert result.didl_metadata is didl_item @@ -590,7 +505,7 @@ async def test_resolve_media_search( dms_device_mock.async_search_directory.return_value = DmsDevice.BrowseResult( [didl_item], 1, 2, 0 ) - result = await device_source_mock.async_resolve_media('?dc:title="thing"') + result = await async_resolve_media(hass, '?dc:title="thing"') assert result.url == res_abs_url assert result.mime_type == res_mime assert result.didl_metadata is didl_item @@ -600,12 +515,10 @@ async def test_resolve_media_search( [didl_lite.Descriptor("id", "namespace")], 1, 1, 0 ) with pytest.raises(Unresolvable, match="Descriptor.* is not a DidlObject"): - await device_source_mock.async_resolve_media('?dc:title="thing"') + await async_resolve_media(hass, '?dc:title="thing"') -async def test_browse_media_root( - device_source_mock: DmsDeviceSource, dms_device_mock: Mock -) -> None: +async def test_browse_media_root(hass: HomeAssistant, dms_device_mock: Mock) -> None: """Test async_browse_media with no identifier will browse the root of the device.""" dms_device_mock.async_browse_metadata.return_value = didl_lite.DidlObject( id="0", restricted="false", title="root" @@ -613,8 +526,25 @@ async def test_browse_media_root( dms_device_mock.async_browse_direct_children.return_value = DmsDevice.BrowseResult( [], 0, 0, 0 ) + # No identifier (first opened in media browser) - result = await device_source_mock.async_browse_media(None) + result = await media_source.async_browse_media(hass, f"media-source://{DOMAIN}") + assert result.identifier == f"{MOCK_SOURCE_ID}/:0" + assert result.title == MOCK_DEVICE_NAME + dms_device_mock.async_browse_metadata.assert_awaited_once_with( + "0", metadata_filter=ANY + ) + dms_device_mock.async_browse_direct_children.assert_awaited_once_with( + "0", metadata_filter=ANY, sort_criteria=ANY + ) + + dms_device_mock.async_browse_metadata.reset_mock() + dms_device_mock.async_browse_direct_children.reset_mock() + + # Only source ID, no object ID + result = await media_source.async_browse_media( + hass, f"media-source://{DOMAIN}/{MOCK_SOURCE_ID}" + ) assert result.identifier == f"{MOCK_SOURCE_ID}/:0" assert result.title == MOCK_DEVICE_NAME dms_device_mock.async_browse_metadata.assert_awaited_once_with( @@ -627,7 +557,9 @@ async def test_browse_media_root( dms_device_mock.async_browse_metadata.reset_mock() dms_device_mock.async_browse_direct_children.reset_mock() # Empty string identifier - result = await device_source_mock.async_browse_media("") + result = await media_source.async_browse_media( + hass, f"media-source://{DOMAIN}/{MOCK_SOURCE_ID}/" + ) assert result.identifier == f"{MOCK_SOURCE_ID}/:0" assert result.title == MOCK_DEVICE_NAME dms_device_mock.async_browse_metadata.assert_awaited_once_with( @@ -638,9 +570,7 @@ async def test_browse_media_root( ) -async def test_browse_media_object( - device_source_mock: DmsDeviceSource, dms_device_mock: Mock -) -> None: +async def test_browse_media_object(hass: HomeAssistant, dms_device_mock: Mock) -> None: """Test async_browse_object via async_browse_media.""" object_id = "1234" child_titles = ("Item 1", "Thing", "Item 2") @@ -663,7 +593,7 @@ async def test_browse_media_object( ) dms_device_mock.async_browse_direct_children.return_value = children_result - result = await device_source_mock.async_browse_media(f":{object_id}") + result = await async_browse_media(hass, f":{object_id}") dms_device_mock.async_browse_metadata.assert_awaited_once_with( object_id, metadata_filter=ANY ) @@ -687,7 +617,7 @@ async def test_browse_media_object( async def test_browse_object_sort_anything( - device_source_mock: DmsDeviceSource, dms_device_mock: Mock + hass: HomeAssistant, dms_device_mock: Mock ) -> None: """Test sort criteria for children where device allows anything.""" dms_device_mock.sort_capabilities = ["*"] @@ -699,7 +629,7 @@ async def test_browse_object_sort_anything( dms_device_mock.async_browse_direct_children.return_value = DmsDevice.BrowseResult( [], 0, 0, 0 ) - await device_source_mock.async_browse_object("0") + await async_browse_media(hass, ":0") # Sort criteria should be dlna_dms's default dms_device_mock.async_browse_direct_children.assert_awaited_once_with( @@ -708,7 +638,7 @@ async def test_browse_object_sort_anything( async def test_browse_object_sort_superset( - device_source_mock: DmsDeviceSource, dms_device_mock: Mock + hass: HomeAssistant, dms_device_mock: Mock ) -> None: """Test sorting where device allows superset of integration's criteria.""" dms_device_mock.sort_capabilities = [ @@ -727,7 +657,7 @@ async def test_browse_object_sort_superset( dms_device_mock.async_browse_direct_children.return_value = DmsDevice.BrowseResult( [], 0, 0, 0 ) - await device_source_mock.async_browse_object("0") + await async_browse_media(hass, ":0") # Sort criteria should be dlna_dms's default dms_device_mock.async_browse_direct_children.assert_awaited_once_with( @@ -736,7 +666,7 @@ async def test_browse_object_sort_superset( async def test_browse_object_sort_subset( - device_source_mock: DmsDeviceSource, dms_device_mock: Mock + hass: HomeAssistant, dms_device_mock: Mock ) -> None: """Test sorting where device allows subset of integration's criteria.""" dms_device_mock.sort_capabilities = [ @@ -751,7 +681,7 @@ async def test_browse_object_sort_subset( dms_device_mock.async_browse_direct_children.return_value = DmsDevice.BrowseResult( [], 0, 0, 0 ) - await device_source_mock.async_browse_object("0") + await async_browse_media(hass, ":0") # Sort criteria should be reduced to only those allowed, # and in the order specified by DLNA_SORT_CRITERIA @@ -761,9 +691,7 @@ async def test_browse_object_sort_subset( ) -async def test_browse_media_path( - device_source_mock: DmsDeviceSource, dms_device_mock: Mock -) -> None: +async def test_browse_media_path(hass: HomeAssistant, dms_device_mock: Mock) -> None: """Test async_browse_media with a path.""" title = "folder" con_id = "123" @@ -776,7 +704,7 @@ async def test_browse_media_path( [], 0, 0, 0 ) - result = await device_source_mock.async_browse_media(f"{title}") + result = await async_browse_media(hass, title) assert result.identifier == f"{MOCK_SOURCE_ID}/:{con_id}" assert result.title == title @@ -794,9 +722,7 @@ async def test_browse_media_path( ) -async def test_browse_media_search( - device_source_mock: DmsDeviceSource, dms_device_mock: Mock -) -> None: +async def test_browse_media_search(hass: HomeAssistant, dms_device_mock: Mock) -> None: """Test async_browse_media with a search query.""" query = 'dc:title contains "FooBar"' object_details = (("111", "FooBar baz"), ("432", "Not FooBar"), ("99", "FooBar")) @@ -814,7 +740,7 @@ async def test_browse_media_search( 1, didl_lite.Descriptor("id", "name_space") ) - result = await device_source_mock.async_browse_media(f"?{query}") + result = await async_browse_media(hass, f"?{query}") assert result.identifier == f"{MOCK_SOURCE_ID}/?{query}" assert result.title == "Search results" assert result.children @@ -827,7 +753,7 @@ async def test_browse_media_search( async def test_browse_search_invalid( - device_source_mock: DmsDeviceSource, dms_device_mock: Mock + hass: HomeAssistant, dms_device_mock: Mock ) -> None: """Test searching with an invalid query gives a BrowseError.""" query = "title == FooBar" @@ -835,11 +761,11 @@ async def test_browse_search_invalid( error_code=ContentDirectoryErrorCode.INVALID_SEARCH_CRITERIA ) with pytest.raises(BrowseError, match=f"Invalid query: {query}"): - await device_source_mock.async_browse_media(f"?{query}") + await async_browse_media(hass, f"?{query}") async def test_browse_search_no_results( - device_source_mock: DmsDeviceSource, dms_device_mock: Mock + hass: HomeAssistant, dms_device_mock: Mock ) -> None: """Test a search with no results does not give an error.""" query = 'dc:title contains "FooBar"' @@ -847,15 +773,13 @@ async def test_browse_search_no_results( [], 0, 0, 0 ) - result = await device_source_mock.async_browse_media(f"?{query}") + result = await async_browse_media(hass, f"?{query}") assert result.identifier == f"{MOCK_SOURCE_ID}/?{query}" assert result.title == "Search results" assert not result.children -async def test_thumbnail( - device_source_mock: DmsDeviceSource, dms_device_mock: Mock -) -> None: +async def test_thumbnail(hass: HomeAssistant, dms_device_mock: Mock) -> None: """Test getting thumbnails URLs for items.""" # Use browse_search to get multiple items at once for least effort dms_device_mock.async_search_directory.return_value = DmsDevice.BrowseResult( @@ -901,16 +825,14 @@ async def test_thumbnail( 0, ) - result = await device_source_mock.async_browse_media("?query") + result = await async_browse_media(hass, "?query") assert result.children assert result.children[0].thumbnail == f"{MOCK_DEVICE_BASE_URL}/a_thumb.jpg" assert result.children[1].thumbnail == f"{MOCK_DEVICE_BASE_URL}/b_thumb.png" assert result.children[2].thumbnail is None -async def test_can_play( - device_source_mock: DmsDeviceSource, dms_device_mock: Mock -) -> None: +async def test_can_play(hass: HomeAssistant, dms_device_mock: Mock) -> None: """Test determination of playability for items.""" protocol_infos = [ # No protocol info for resource @@ -945,7 +867,7 @@ async def test_can_play( search_results, len(search_results), len(search_results), 0 ) - result = await device_source_mock.async_browse_media("?query") + result = await async_browse_media(hass, "?query") assert result.children assert not result.children[0].can_play for idx, info_can_play in enumerate(protocol_infos): diff --git a/tests/components/dlna_dms/test_init.py b/tests/components/dlna_dms/test_init.py index 16254adca89..a5bac73efb2 100644 --- a/tests/components/dlna_dms/test_init.py +++ b/tests/components/dlna_dms/test_init.py @@ -1,18 +1,32 @@ """Test the DLNA DMS component setup, cleanup, and module-level functions.""" +from typing import cast from unittest.mock import Mock -from homeassistant.components.dlna_dms.const import DOMAIN +from homeassistant.components.dlna_dms.const import ( + CONF_SOURCE_ID, + CONFIG_VERSION, + DOMAIN, +) from homeassistant.components.dlna_dms.dms import DlnaDmsData +from homeassistant.const import CONF_DEVICE_ID, CONF_URL from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component +from .conftest import ( + MOCK_DEVICE_LOCATION, + MOCK_DEVICE_NAME, + MOCK_DEVICE_TYPE, + MOCK_DEVICE_USN, + MOCK_SOURCE_ID, +) + from tests.common import MockConfigEntry async def test_resource_lifecycle( hass: HomeAssistant, - domain_data_mock: DlnaDmsData, + aiohttp_session_requester_mock: Mock, config_entry_mock: MockConfigEntry, ssdp_scanner_mock: Mock, dms_device_mock: Mock, @@ -23,14 +37,15 @@ async def test_resource_lifecycle( assert await async_setup_component(hass, DOMAIN, {}) is True await hass.async_block_till_done() - # Check the entity is created and working - assert len(domain_data_mock.devices) == 1 - assert len(domain_data_mock.sources) == 1 - entity = next(iter(domain_data_mock.devices.values())) + # Check the device source is created and working + domain_data = cast(DlnaDmsData, hass.data[DOMAIN]) + assert len(domain_data.devices) == 1 + assert len(domain_data.sources) == 1 + entity = next(iter(domain_data.devices.values())) assert entity.available is True - # Check update listeners are subscribed - assert len(config_entry_mock.update_listeners) == 1 + # Check listener subscriptions + assert len(config_entry_mock.update_listeners) == 0 assert ssdp_scanner_mock.async_register_callback.await_count == 2 assert ssdp_scanner_mock.async_register_callback.return_value.call_count == 0 @@ -54,6 +69,63 @@ async def test_resource_lifecycle( assert dms_device_mock.async_unsubscribe_services.await_count == 0 assert dms_device_mock.on_event is None - # Check entity is gone - assert not domain_data_mock.devices - assert not domain_data_mock.sources + # Check device source is gone + assert not domain_data.devices + assert not domain_data.sources + + +async def test_migrate_entry(hass: HomeAssistant) -> None: + """Test migrating a config entry from version 1 to version 2.""" + # Create mock entry with version 1 + mock_entry = MockConfigEntry( + unique_id=MOCK_DEVICE_USN, + domain=DOMAIN, + version=1, + data={ + CONF_URL: MOCK_DEVICE_LOCATION, + CONF_DEVICE_ID: MOCK_DEVICE_USN, + }, + title=MOCK_DEVICE_NAME, + ) + + # Set it up + mock_entry.add_to_hass(hass) + assert await async_setup_component(hass, DOMAIN, {}) is True + await hass.async_block_till_done() + + # Check that it has a source_id now + updated_entry = hass.config_entries.async_get_entry(mock_entry.entry_id) + assert updated_entry + assert updated_entry.version == CONFIG_VERSION + assert updated_entry.data.get(CONF_SOURCE_ID) == MOCK_SOURCE_ID + + +async def test_migrate_entry_collision( + hass: HomeAssistant, config_entry_mock: MockConfigEntry +) -> None: + """Test migrating a config entry with a potentially colliding source ID.""" + # Use existing mock entry + config_entry_mock.add_to_hass(hass) + + # Create mock entry with same name, and old version, that needs migrating + mock_entry = MockConfigEntry( + unique_id=f"udn-migrating::{MOCK_DEVICE_TYPE}", + domain=DOMAIN, + version=1, + data={ + CONF_URL: "http://192.88.99.22/dms_description.xml", + CONF_DEVICE_ID: f"different-udn::{MOCK_DEVICE_TYPE}", + }, + title=MOCK_DEVICE_NAME, + ) + mock_entry.add_to_hass(hass) + + # Set the integration up + assert await async_setup_component(hass, DOMAIN, {}) is True + await hass.async_block_till_done() + + # Check that it has a source_id now + updated_entry = hass.config_entries.async_get_entry(mock_entry.entry_id) + assert updated_entry + assert updated_entry.version == CONFIG_VERSION + assert updated_entry.data.get(CONF_SOURCE_ID) == f"{MOCK_SOURCE_ID}_1" diff --git a/tests/components/dlna_dms/test_media_source.py b/tests/components/dlna_dms/test_media_source.py index 4b43402ecbd..f2c3011e274 100644 --- a/tests/components/dlna_dms/test_media_source.py +++ b/tests/components/dlna_dms/test_media_source.py @@ -5,8 +5,9 @@ from async_upnp_client.exceptions import UpnpError from didl_lite import didl_lite import pytest +from homeassistant.components import media_source from homeassistant.components.dlna_dms.const import DOMAIN -from homeassistant.components.dlna_dms.dms import DlnaDmsData, DmsDeviceSource +from homeassistant.components.dlna_dms.dms import DidlPlayMedia from homeassistant.components.dlna_dms.media_source import ( DmsMediaSource, async_get_media_source, @@ -24,30 +25,18 @@ from .conftest import ( MOCK_DEVICE_BASE_URL, MOCK_DEVICE_NAME, MOCK_DEVICE_TYPE, - MOCK_DEVICE_USN, MOCK_SOURCE_ID, ) from tests.common import MockConfigEntry - -@pytest.fixture -async def entity( - hass: HomeAssistant, - config_entry_mock: MockConfigEntry, - dms_device_mock: Mock, - domain_data_mock: DlnaDmsData, -) -> DmsDeviceSource: - """Fixture to set up a DmsDeviceSource in a connected state and cleanup at completion.""" - await hass.config_entries.async_add(config_entry_mock) - await hass.async_block_till_done() - return domain_data_mock.devices[MOCK_DEVICE_USN] - - -@pytest.fixture -async def dms_source(hass: HomeAssistant, entity: DmsDeviceSource) -> DmsMediaSource: - """Fixture providing a pre-constructed DmsMediaSource with a single device.""" - return DmsMediaSource(hass) +# Auto-use a few fixtures from conftest +pytestmark = [ + # Block network access + pytest.mark.usefixtures("aiohttp_session_requester_mock"), + # Setup the media_source platform + pytest.mark.usefixtures("setup_media_source"), +] async def test_get_media_source(hass: HomeAssistant) -> None: @@ -66,41 +55,44 @@ async def test_resolve_media_unconfigured(hass: HomeAssistant) -> None: async def test_resolve_media_bad_identifier( - hass: HomeAssistant, dms_source: DmsMediaSource + hass: HomeAssistant, device_source_mock: None ) -> None: """Test trying to resolve an item that has an unresolvable identifier.""" # Empty identifier - item = MediaSourceItem(hass, DOMAIN, "") with pytest.raises(Unresolvable, match="No source ID.*"): - await dms_source.async_resolve_media(item) + await media_source.async_resolve_media(hass, f"media-source://{DOMAIN}") # Identifier has media_id but no source_id - item = MediaSourceItem(hass, DOMAIN, "/media_id") - with pytest.raises(Unresolvable, match="No source ID.*"): - await dms_source.async_resolve_media(item) + # media_source.URI_SCHEME_REGEX won't let the ID through to dlna_dms + with pytest.raises(Unresolvable, match="Invalid media source URI"): + await media_source.async_resolve_media( + hass, f"media-source://{DOMAIN}//media_id" + ) # Identifier has source_id but no media_id - item = MediaSourceItem(hass, DOMAIN, "source_id/") with pytest.raises(Unresolvable, match="No media ID.*"): - await dms_source.async_resolve_media(item) + await media_source.async_resolve_media( + hass, f"media-source://{DOMAIN}/source_id/" + ) # Identifier is missing source_id/media_id separator - item = MediaSourceItem(hass, DOMAIN, "source_id") with pytest.raises(Unresolvable, match="No media ID.*"): - await dms_source.async_resolve_media(item) + await media_source.async_resolve_media( + hass, f"media-source://{DOMAIN}/source_id" + ) # Identifier has an unknown source_id - item = MediaSourceItem(hass, DOMAIN, "unknown_source/media_id") with pytest.raises(Unresolvable, match="Unknown source ID: unknown_source"): - await dms_source.async_resolve_media(item) + await media_source.async_resolve_media( + hass, f"media-source://{DOMAIN}/unknown_source/media_id" + ) async def test_resolve_media_success( - hass: HomeAssistant, dms_source: DmsMediaSource, dms_device_mock: Mock + hass: HomeAssistant, dms_device_mock: Mock, device_source_mock: None ) -> None: """Test resolving an item via a DmsDeviceSource.""" object_id = "123" - item = MediaSourceItem(hass, DOMAIN, f"{MOCK_SOURCE_ID}/:{object_id}") res_url = "foo/bar" res_mime = "audio/mpeg" @@ -112,7 +104,10 @@ async def test_resolve_media_success( ) dms_device_mock.async_browse_metadata.return_value = didl_item - result = await dms_source.async_resolve_media(item) + result = await media_source.async_resolve_media( + hass, f"media-source://{DOMAIN}/{MOCK_SOURCE_ID}/:{object_id}" + ) + assert isinstance(result, DidlPlayMedia) assert result.url == f"{MOCK_DEVICE_BASE_URL}/{res_url}" assert result.mime_type == res_mime assert result.didl_metadata is didl_item @@ -131,43 +126,42 @@ async def test_browse_media_unconfigured(hass: HomeAssistant) -> None: async def test_browse_media_bad_identifier( - hass: HomeAssistant, dms_source: DmsMediaSource + hass: HomeAssistant, device_source_mock: None ) -> None: """Test browse_media with a bad source_id.""" - item = MediaSourceItem(hass, DOMAIN, "bad-id/media_id") with pytest.raises(BrowseError, match="Unknown source ID: bad-id"): - await dms_source.async_browse_media(item) + await media_source.async_browse_media( + hass, f"media-source://{DOMAIN}/bad-id/media_id" + ) async def test_browse_media_single_source_no_identifier( - hass: HomeAssistant, dms_source: DmsMediaSource, dms_device_mock: Mock + hass: HomeAssistant, dms_device_mock: Mock, device_source_mock: None ) -> None: """Test browse_media without a source_id, with a single device registered.""" # Fast bail-out, mock will be checked after dms_device_mock.async_browse_metadata.side_effect = UpnpError # No source_id nor media_id - item = MediaSourceItem(hass, DOMAIN, "") with pytest.raises(BrowseError): - await dms_source.async_browse_media(item) + await media_source.async_browse_media(hass, f"media-source://{DOMAIN}") # Mock device should've been browsed for the root directory dms_device_mock.async_browse_metadata.assert_awaited_once_with( "0", metadata_filter=ANY ) # No source_id but a media_id - item = MediaSourceItem(hass, DOMAIN, "/:media-item-id") + # media_source.URI_SCHEME_REGEX won't let the ID through to dlna_dms dms_device_mock.async_browse_metadata.reset_mock() - with pytest.raises(BrowseError): - await dms_source.async_browse_media(item) - # Mock device should've been browsed for the root directory - dms_device_mock.async_browse_metadata.assert_awaited_once_with( - "media-item-id", metadata_filter=ANY - ) + with pytest.raises(BrowseError, match="Invalid media source URI"): + await media_source.async_browse_media( + hass, f"media-source://{DOMAIN}//:media-item-id" + ) + assert dms_device_mock.async_browse_metadata.await_count == 0 async def test_browse_media_multiple_sources( - hass: HomeAssistant, dms_source: DmsMediaSource, dms_device_mock: Mock + hass: HomeAssistant, dms_device_mock: Mock, device_source_mock: None ) -> None: """Test browse_media without a source_id, with multiple devices registered.""" # Set up a second source @@ -182,12 +176,12 @@ async def test_browse_media_multiple_sources( }, title=other_source_title, ) - await hass.config_entries.async_add(other_config_entry) + other_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(other_config_entry.entry_id) await hass.async_block_till_done() # No source_id nor media_id - item = MediaSourceItem(hass, DOMAIN, "") - result = await dms_source.async_browse_media(item) + result = await media_source.async_browse_media(hass, f"media-source://{DOMAIN}") # Mock device should not have been browsed assert dms_device_mock.async_browse_metadata.await_count == 0 # Result will be a list of available devices @@ -196,31 +190,28 @@ async def test_browse_media_multiple_sources( assert isinstance(result.children[0], BrowseMediaSource) assert result.children[0].identifier == f"{MOCK_SOURCE_ID}/:0" assert result.children[0].title == MOCK_DEVICE_NAME + assert result.children[0].thumbnail == dms_device_mock.icon assert isinstance(result.children[1], BrowseMediaSource) assert result.children[1].identifier == f"{other_source_id}/:0" assert result.children[1].title == other_source_title - # No source_id but a media_id - will give the exact same list of all devices - item = MediaSourceItem(hass, DOMAIN, "/:media-item-id") - result = await dms_source.async_browse_media(item) + # No source_id but a media_id + # media_source.URI_SCHEME_REGEX won't let the ID through to dlna_dms + with pytest.raises(BrowseError, match="Invalid media source URI"): + result = await media_source.async_browse_media( + hass, f"media-source://{DOMAIN}//:media-item-id" + ) # Mock device should not have been browsed assert dms_device_mock.async_browse_metadata.await_count == 0 - # Result will be a list of available devices - assert result.title == "DLNA Servers" - assert result.children - assert isinstance(result.children[0], BrowseMediaSource) - assert result.children[0].identifier == f"{MOCK_SOURCE_ID}/:0" - assert result.children[0].title == MOCK_DEVICE_NAME - assert isinstance(result.children[1], BrowseMediaSource) - assert result.children[1].identifier == f"{other_source_id}/:0" - assert result.children[1].title == other_source_title + + # Clean up, to fulfil ssdp_scanner post-condition of every callback being cleared + await hass.config_entries.async_remove(other_config_entry.entry_id) async def test_browse_media_source_id( hass: HomeAssistant, config_entry_mock: MockConfigEntry, dms_device_mock: Mock, - domain_data_mock: DlnaDmsData, ) -> None: """Test browse_media with an explicit source_id.""" # Set up a second device first, then the primary mock device. @@ -235,10 +226,13 @@ async def test_browse_media_source_id( }, title=other_source_title, ) - await hass.config_entries.async_add(other_config_entry) - await hass.async_block_till_done() - await hass.config_entries.async_add(config_entry_mock) + other_config_entry.add_to_hass(hass) + config_entry_mock.add_to_hass(hass) + + # Setting up either config entry will result in the dlna_dms component being + # loaded, and both config entries will be setup + await hass.config_entries.async_setup(other_config_entry.entry_id) await hass.async_block_till_done() # Fast bail-out, mock will be checked after diff --git a/tests/components/dnsip/test_config_flow.py b/tests/components/dnsip/test_config_flow.py index f4684eb1cc4..51e169b8bb5 100644 --- a/tests/components/dnsip/test_config_flow.py +++ b/tests/components/dnsip/test_config_flow.py @@ -146,66 +146,6 @@ async def test_form_error(hass: HomeAssistant) -> None: assert result2["errors"] == {"base": "invalid_hostname"} -@pytest.mark.parametrize( - "p_input,p_output,p_options", - [ - ( - {CONF_HOSTNAME: "home-assistant.io"}, - { - "hostname": "home-assistant.io", - "name": "home-assistant.io", - "ipv4": True, - "ipv6": True, - }, - { - "resolver": "208.67.222.222", - "resolver_ipv6": "2620:0:ccc::2", - }, - ), - ( - {}, - { - "hostname": "myip.opendns.com", - "name": "myip", - "ipv4": True, - "ipv6": True, - }, - { - "resolver": "208.67.222.222", - "resolver_ipv6": "2620:0:ccc::2", - }, - ), - ], -) -async def test_import_flow_success( - hass: HomeAssistant, - p_input: dict[str, str], - p_output: dict[str, str], - p_options: dict[str, str], -) -> None: - """Test a successful import of YAML.""" - - with patch( - "homeassistant.components.dnsip.config_flow.aiodns.DNSResolver", - return_value=RetrieveDNS(), - ), patch( - "homeassistant.components.dnsip.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result2 = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data=p_input, - ) - await hass.async_block_till_done() - - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY - assert result2["title"] == p_output["name"] - assert result2["data"] == p_output - assert result2["options"] == p_options - assert len(mock_setup_entry.mock_calls) == 1 - - async def test_flow_already_exist(hass: HomeAssistant) -> None: """Test flow when unique id already exist.""" diff --git a/tests/components/dsmr/conftest.py b/tests/components/dsmr/conftest.py index e0299d68f2b..8c94c756edc 100644 --- a/tests/components/dsmr/conftest.py +++ b/tests/components/dsmr/conftest.py @@ -3,6 +3,7 @@ import asyncio from unittest.mock import MagicMock, patch from dsmr_parser.clients.protocol import DSMRProtocol +from dsmr_parser.clients.rfxtrx_protocol import RFXtrxDSMRProtocol from dsmr_parser.obis_references import ( EQUIPMENT_IDENTIFIER, EQUIPMENT_IDENTIFIER_GAS, @@ -36,6 +37,29 @@ async def dsmr_connection_fixture(hass): yield (connection_factory, transport, protocol) +@pytest.fixture +async def rfxtrx_dsmr_connection_fixture(hass): + """Fixture that mocks RFXtrx connection.""" + + transport = MagicMock(spec=asyncio.Transport) + protocol = MagicMock(spec=RFXtrxDSMRProtocol) + + async def connection_factory(*args, **kwargs): + """Return mocked out Asyncio classes.""" + return (transport, protocol) + + connection_factory = MagicMock(wraps=connection_factory) + + with patch( + "homeassistant.components.dsmr.sensor.create_rfxtrx_dsmr_reader", + connection_factory, + ), patch( + "homeassistant.components.dsmr.sensor.create_rfxtrx_tcp_dsmr_reader", + connection_factory, + ): + yield (connection_factory, transport, protocol) + + @pytest.fixture async def dsmr_connection_send_validate_fixture(hass): """Fixture that mocks serial connection.""" @@ -95,3 +119,43 @@ async def dsmr_connection_send_validate_fixture(hass): connection_factory, ): yield (connection_factory, transport, protocol) + + +@pytest.fixture +async def rfxtrx_dsmr_connection_send_validate_fixture(hass): + """Fixture that mocks serial connection.""" + + transport = MagicMock(spec=asyncio.Transport) + protocol = MagicMock(spec=RFXtrxDSMRProtocol) + + protocol.telegram = { + EQUIPMENT_IDENTIFIER: CosemObject([{"value": "12345678", "unit": ""}]), + EQUIPMENT_IDENTIFIER_GAS: CosemObject([{"value": "123456789", "unit": ""}]), + P1_MESSAGE_TIMESTAMP: CosemObject([{"value": "12345678", "unit": ""}]), + } + + async def connection_factory(*args, **kwargs): + return (transport, protocol) + + connection_factory = MagicMock(wraps=connection_factory) + + async def wait_closed(): + if isinstance(connection_factory.call_args_list[0][0][2], str): + # TCP + telegram_callback = connection_factory.call_args_list[0][0][3] + else: + # Serial + telegram_callback = connection_factory.call_args_list[0][0][2] + + telegram_callback(protocol.telegram) + + protocol.wait_closed = wait_closed + + with patch( + "homeassistant.components.dsmr.config_flow.create_rfxtrx_dsmr_reader", + connection_factory, + ), patch( + "homeassistant.components.dsmr.config_flow.create_rfxtrx_tcp_dsmr_reader", + connection_factory, + ): + yield (connection_factory, transport, protocol) diff --git a/tests/components/dsmr/test_config_flow.py b/tests/components/dsmr/test_config_flow.py index 669fcfac386..ddec7bda888 100644 --- a/tests/components/dsmr/test_config_flow.py +++ b/tests/components/dsmr/test_config_flow.py @@ -49,13 +49,70 @@ async def test_setup_network(hass, dsmr_connection_send_validate_fixture): with patch("homeassistant.components.dsmr.async_setup_entry", return_value=True): result = await hass.config_entries.flow.async_configure( result["flow_id"], - {"host": "10.10.0.1", "port": 1234, "dsmr_version": "2.2"}, + { + "host": "10.10.0.1", + "port": 1234, + "dsmr_version": "2.2", + }, ) + await hass.async_block_till_done() entry_data = { "host": "10.10.0.1", "port": 1234, "dsmr_version": "2.2", + "protocol": "dsmr_protocol", + } + + assert result["type"] == "create_entry" + assert result["title"] == "10.10.0.1:1234" + assert result["data"] == {**entry_data, **SERIAL_DATA} + + +async def test_setup_network_rfxtrx( + hass, + dsmr_connection_send_validate_fixture, + rfxtrx_dsmr_connection_send_validate_fixture, +): + """Test we can setup network.""" + (connection_factory, transport, protocol) = dsmr_connection_send_validate_fixture + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + assert result["errors"] is None + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"type": "Network"}, + ) + + assert result["type"] == "form" + assert result["step_id"] == "setup_network" + assert result["errors"] == {} + + # set-up DSMRProtocol to yield no valid telegram, this will retry with RFXtrxDSMRProtocol + protocol.telegram = {} + + with patch("homeassistant.components.dsmr.async_setup_entry", return_value=True): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "10.10.0.1", + "port": 1234, + "dsmr_version": "2.2", + }, + ) + await hass.async_block_till_done() + + entry_data = { + "host": "10.10.0.1", + "port": 1234, + "dsmr_version": "2.2", + "protocol": "rfxtrx_dsmr_protocol", } assert result["type"] == "create_entry" @@ -87,12 +144,65 @@ async def test_setup_serial(com_mock, hass, dsmr_connection_send_validate_fixtur with patch("homeassistant.components.dsmr.async_setup_entry", return_value=True): result = await hass.config_entries.flow.async_configure( - result["flow_id"], {"port": port.device, "dsmr_version": "2.2"} + result["flow_id"], + {"port": port.device, "dsmr_version": "2.2"}, ) + await hass.async_block_till_done() entry_data = { "port": port.device, "dsmr_version": "2.2", + "protocol": "dsmr_protocol", + } + + assert result["type"] == "create_entry" + assert result["title"] == port.device + assert result["data"] == {**entry_data, **SERIAL_DATA} + + +@patch("serial.tools.list_ports.comports", return_value=[com_port()]) +async def test_setup_serial_rfxtrx( + com_mock, + hass, + dsmr_connection_send_validate_fixture, + rfxtrx_dsmr_connection_send_validate_fixture, +): + """Test we can setup serial.""" + (connection_factory, transport, protocol) = dsmr_connection_send_validate_fixture + + port = com_port() + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + assert result["errors"] is None + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"type": "Serial"}, + ) + + assert result["type"] == "form" + assert result["step_id"] == "setup_serial" + assert result["errors"] == {} + + # set-up DSMRProtocol to yield no valid telegram, this will retry with RFXtrxDSMRProtocol + protocol.telegram = {} + + with patch("homeassistant.components.dsmr.async_setup_entry", return_value=True): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"port": port.device, "dsmr_version": "2.2"}, + ) + await hass.async_block_till_done() + + entry_data = { + "port": port.device, + "dsmr_version": "2.2", + "protocol": "rfxtrx_dsmr_protocol", } assert result["type"] == "create_entry" @@ -124,12 +234,15 @@ async def test_setup_5L(com_mock, hass, dsmr_connection_send_validate_fixture): with patch("homeassistant.components.dsmr.async_setup_entry", return_value=True): result = await hass.config_entries.flow.async_configure( - result["flow_id"], {"port": port.device, "dsmr_version": "5L"} + result["flow_id"], + {"port": port.device, "dsmr_version": "5L"}, ) + await hass.async_block_till_done() entry_data = { "port": port.device, "dsmr_version": "5L", + "protocol": "dsmr_protocol", "serial_id": "12345678", "serial_id_gas": "123456789", } @@ -165,10 +278,12 @@ async def test_setup_5S(com_mock, hass, dsmr_connection_send_validate_fixture): result = await hass.config_entries.flow.async_configure( result["flow_id"], {"port": port.device, "dsmr_version": "5S"} ) + await hass.async_block_till_done() entry_data = { "port": port.device, "dsmr_version": "5S", + "protocol": "dsmr_protocol", "serial_id": None, "serial_id_gas": None, } @@ -202,12 +317,15 @@ async def test_setup_Q3D(com_mock, hass, dsmr_connection_send_validate_fixture): with patch("homeassistant.components.dsmr.async_setup_entry", return_value=True): result = await hass.config_entries.flow.async_configure( - result["flow_id"], {"port": port.device, "dsmr_version": "Q3D"} + result["flow_id"], + {"port": port.device, "dsmr_version": "Q3D"}, ) + await hass.async_block_till_done() entry_data = { "port": port.device, "dsmr_version": "Q3D", + "protocol": "dsmr_protocol", "serial_id": "12345678", "serial_id_gas": None, } @@ -240,7 +358,8 @@ async def test_setup_serial_manual( assert result["errors"] == {} result = await hass.config_entries.flow.async_configure( - result["flow_id"], {"port": "Enter Manually", "dsmr_version": "2.2"} + result["flow_id"], + {"port": "Enter Manually", "dsmr_version": "2.2"}, ) assert result["type"] == "form" @@ -251,10 +370,12 @@ async def test_setup_serial_manual( result = await hass.config_entries.flow.async_configure( result["flow_id"], {"port": "/dev/ttyUSB0"} ) + await hass.async_block_till_done() entry_data = { "port": "/dev/ttyUSB0", "dsmr_version": "2.2", + "protocol": "dsmr_protocol", } assert result["type"] == "create_entry" @@ -297,7 +418,8 @@ async def test_setup_serial_fail(com_mock, hass, dsmr_connection_send_validate_f first_fail_connection_factory, ): result = await hass.config_entries.flow.async_configure( - result["flow_id"], {"port": port.device, "dsmr_version": "2.2"} + result["flow_id"], + {"port": port.device, "dsmr_version": "2.2"}, ) assert result["type"] == "form" @@ -307,10 +429,18 @@ async def test_setup_serial_fail(com_mock, hass, dsmr_connection_send_validate_f @patch("serial.tools.list_ports.comports", return_value=[com_port()]) async def test_setup_serial_timeout( - com_mock, hass, dsmr_connection_send_validate_fixture + com_mock, + hass, + dsmr_connection_send_validate_fixture, + rfxtrx_dsmr_connection_send_validate_fixture, ): """Test failed serial connection.""" (connection_factory, transport, protocol) = dsmr_connection_send_validate_fixture + ( + connection_factory, + transport, + rfxtrx_protocol, + ) = rfxtrx_dsmr_connection_send_validate_fixture port = com_port() @@ -324,6 +454,12 @@ async def test_setup_serial_timeout( ) protocol.wait_closed = first_timeout_wait_closed + first_timeout_wait_closed = AsyncMock( + return_value=True, + side_effect=chain([asyncio.TimeoutError], repeat(DEFAULT)), + ) + rfxtrx_protocol.wait_closed = first_timeout_wait_closed + assert result["type"] == "form" assert result["step_id"] == "user" assert result["errors"] is None @@ -349,10 +485,18 @@ async def test_setup_serial_timeout( @patch("serial.tools.list_ports.comports", return_value=[com_port()]) async def test_setup_serial_wrong_telegram( - com_mock, hass, dsmr_connection_send_validate_fixture + com_mock, + hass, + dsmr_connection_send_validate_fixture, + rfxtrx_dsmr_connection_send_validate_fixture, ): """Test failed telegram data.""" (connection_factory, transport, protocol) = dsmr_connection_send_validate_fixture + ( + rfxtrx_connection_factory, + transport, + rfxtrx_protocol, + ) = rfxtrx_dsmr_connection_send_validate_fixture port = com_port() @@ -360,8 +504,6 @@ async def test_setup_serial_wrong_telegram( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - protocol.telegram = {} - assert result["type"] == "form" assert result["step_id"] == "user" assert result["errors"] is None @@ -375,8 +517,12 @@ async def test_setup_serial_wrong_telegram( assert result["step_id"] == "setup_serial" assert result["errors"] == {} + protocol.telegram = {} + rfxtrx_protocol.telegram = {} + result = await hass.config_entries.flow.async_configure( - result["flow_id"], {"port": port.device, "dsmr_version": "2.2"} + result["flow_id"], + {"port": port.device, "dsmr_version": "2.2"}, ) assert result["type"] == "form" diff --git a/tests/components/dsmr/test_sensor.py b/tests/components/dsmr/test_sensor.py index 65c52e14d39..93dd78034cc 100644 --- a/tests/components/dsmr/test_sensor.py +++ b/tests/components/dsmr/test_sensor.py @@ -658,6 +658,35 @@ async def test_tcp(hass, dsmr_connection_fixture): "host": "localhost", "port": "1234", "dsmr_version": "2.2", + "protocol": "dsmr_protocol", + "precision": 4, + "reconnect_interval": 30, + "serial_id": "1234", + "serial_id_gas": "5678", + } + + mock_entry = MockConfigEntry( + domain="dsmr", unique_id="/dev/ttyUSB0", data=entry_data + ) + + mock_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + assert connection_factory.call_args_list[0][0][0] == "localhost" + assert connection_factory.call_args_list[0][0][1] == "1234" + + +async def test_rfxtrx_tcp(hass, rfxtrx_dsmr_connection_fixture): + """If proper config provided RFXtrx TCP connection should be made.""" + (connection_factory, transport, protocol) = rfxtrx_dsmr_connection_fixture + + entry_data = { + "host": "localhost", + "port": "1234", + "dsmr_version": "2.2", + "protocol": "rfxtrx_dsmr_protocol", "precision": 4, "reconnect_interval": 30, "serial_id": "1234", diff --git a/tests/components/dynalite/test_cover.py b/tests/components/dynalite/test_cover.py index 4f696d905d3..fd671365ba1 100644 --- a/tests/components/dynalite/test_cover.py +++ b/tests/components/dynalite/test_cover.py @@ -3,6 +3,7 @@ from dynalite_devices_lib.cover import DynaliteTimeCoverWithTiltDevice import pytest from homeassistant.const import ATTR_DEVICE_CLASS, ATTR_FRIENDLY_NAME +from homeassistant.exceptions import HomeAssistantError from .common import ( ATTR_ARGS, @@ -65,9 +66,10 @@ async def test_cover_without_tilt(hass, mock_device): """Test a cover with no tilt.""" mock_device.has_tilt = False await create_entity_from_device(hass, mock_device) - await hass.services.async_call( - "cover", "open_cover_tilt", {"entity_id": "cover.name"}, blocking=True - ) + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + "cover", "open_cover_tilt", {"entity_id": "cover.name"}, blocking=True + ) await hass.async_block_till_done() mock_device.async_open_cover_tilt.assert_not_called() diff --git a/tests/components/ecobee/fixtures/ecobee-data.json b/tests/components/ecobee/fixtures/ecobee-data.json index a4caa72798d..5dd4dd0d4bd 100644 --- a/tests/components/ecobee/fixtures/ecobee-data.json +++ b/tests/components/ecobee/fixtures/ecobee-data.json @@ -1,68 +1,68 @@ { - "thermostatList": [ + "thermostatList": [ + { + "identifier": 8675309, + "name": "ecobee", + "modelNumber": "athenaSmart", + "program": { + "climates": [ + { "name": "Climate1", "climateRef": "c1" }, + { "name": "Climate2", "climateRef": "c2" } + ], + "currentClimateRef": "c1" + }, + "runtime": { + "connected": true, + "actualTemperature": 300, + "actualHumidity": 15, + "desiredHeat": 400, + "desiredCool": 200, + "desiredFanMode": "on", + "desiredHumidity": 40 + }, + "settings": { + "hvacMode": "auto", + "heatStages": 1, + "coolStages": 1, + "fanMinOnTime": 10, + "heatCoolMinDelta": 50, + "holdAction": "nextTransition", + "hasHumidifier": true, + "humidifierMode": "manual", + "humidity": "30" + }, + "equipmentStatus": "fan", + "events": [ { - "identifier": 8675309, - "name": "ecobee", - "modelNumber": "athenaSmart", - "program": { - "climates": [ - {"name": "Climate1", "climateRef": "c1"}, - {"name": "Climate2", "climateRef": "c2"} - ], - "currentClimateRef": "c1" - }, - "runtime": { - "connected": true, - "actualTemperature": 300, - "actualHumidity": 15, - "desiredHeat": 400, - "desiredCool": 200, - "desiredFanMode": "on", - "desiredHumidity": 40 - }, - "settings": { - "hvacMode": "auto", - "heatStages": 1, - "coolStages": 1, - "fanMinOnTime": 10, - "heatCoolMinDelta": 50, - "holdAction": "nextTransition", - "hasHumidifier": true, - "humidifierMode": "manual", - "humidity": "30" - }, - "equipmentStatus": "fan", - "events": [ - { - "name": "Event1", - "running": true, - "type": "hold", - "holdClimateRef": "away", - "endDate": "2022-01-01 10:00:00", - "startDate": "2022-02-02 11:00:00" - } - ], - "remoteSensors": [ - { - "id": "rs:100", - "name": "Remote Sensor 1", - "type": "ecobee3_remote_sensor", - "code": "WKRP", - "inUse": false, - "capability": [ - { - "id": "1", - "type": "temperature", - "value": "782" - }, { - "id": "2", - "type": "occupancy", - "value": "false" - } - ] - } - ] + "name": "Event1", + "running": true, + "type": "hold", + "holdClimateRef": "away", + "endDate": "2022-01-01 10:00:00", + "startDate": "2022-02-02 11:00:00" } - ] + ], + "remoteSensors": [ + { + "id": "rs:100", + "name": "Remote Sensor 1", + "type": "ecobee3_remote_sensor", + "code": "WKRP", + "inUse": false, + "capability": [ + { + "id": "1", + "type": "temperature", + "value": "782" + }, + { + "id": "2", + "type": "occupancy", + "value": "false" + } + ] + } + ] + } + ] } - diff --git a/tests/components/ecobee/fixtures/ecobee-token.json b/tests/components/ecobee/fixtures/ecobee-token.json index 6ee8305a592..8a8b95a3aaf 100644 --- a/tests/components/ecobee/fixtures/ecobee-token.json +++ b/tests/components/ecobee/fixtures/ecobee-token.json @@ -1,7 +1,7 @@ { - "access_token": "Rc7JE8P7XUgSCPogLOx2VLMfITqQQrjg", - "token_type": "Bearer", - "expires_in": 3599, - "refresh_token": "og2Obost3ucRo1ofo0EDoslGltmFMe2g", - "scope": "smartWrite" -} \ No newline at end of file + "access_token": "Rc7JE8P7XUgSCPogLOx2VLMfITqQQrjg", + "token_type": "Bearer", + "expires_in": 3599, + "refresh_token": "og2Obost3ucRo1ofo0EDoslGltmFMe2g", + "scope": "smartWrite" +} diff --git a/tests/components/efergy/fixtures/budget.json b/tests/components/efergy/fixtures/budget.json index 73fc9b549b6..0495dd6224f 100644 --- a/tests/components/efergy/fixtures/budget.json +++ b/tests/components/efergy/fixtures/budget.json @@ -1,4 +1,4 @@ { "status": "ok", - "monthly_budget": 250.0000 -} \ No newline at end of file + "monthly_budget": 250.0 +} diff --git a/tests/components/efergy/fixtures/current_values_multi.json b/tests/components/efergy/fixtures/current_values_multi.json index 95ee28a6102..8c773f68c84 100644 --- a/tests/components/efergy/fixtures/current_values_multi.json +++ b/tests/components/efergy/fixtures/current_values_multi.json @@ -32,4 +32,4 @@ "units": null, "age": 5 } -] \ No newline at end of file +] diff --git a/tests/components/efergy/fixtures/current_values_single.json b/tests/components/efergy/fixtures/current_values_single.json index df9e5b9ecb4..2489752d51c 100644 --- a/tests/components/efergy/fixtures/current_values_single.json +++ b/tests/components/efergy/fixtures/current_values_single.json @@ -10,4 +10,4 @@ "units": "kWm", "age": 5 } -] \ No newline at end of file +] diff --git a/tests/components/efergy/fixtures/daily_cost.json b/tests/components/efergy/fixtures/daily_cost.json index 41150a30e87..e5e31c4e57e 100644 --- a/tests/components/efergy/fixtures/daily_cost.json +++ b/tests/components/efergy/fixtures/daily_cost.json @@ -2,4 +2,4 @@ "sum": "5.27", "duration": 70320, "units": "GBP" -} \ No newline at end of file +} diff --git a/tests/components/efergy/fixtures/daily_energy.json b/tests/components/efergy/fixtures/daily_energy.json index f1c1ce248be..28eae0c31ba 100644 --- a/tests/components/efergy/fixtures/daily_energy.json +++ b/tests/components/efergy/fixtures/daily_energy.json @@ -2,4 +2,4 @@ "sum": "38.21", "duration": 70320, "units": "kWh" -} \ No newline at end of file +} diff --git a/tests/components/efergy/fixtures/instant.json b/tests/components/efergy/fixtures/instant.json index e66bc4312c9..aa1deb4d090 100644 --- a/tests/components/efergy/fixtures/instant.json +++ b/tests/components/efergy/fixtures/instant.json @@ -2,4 +2,4 @@ "age": 1, "last_reading_time": 1486247836000, "reading": 1580 -} \ No newline at end of file +} diff --git a/tests/components/efergy/fixtures/monthly_cost.json b/tests/components/efergy/fixtures/monthly_cost.json index a3b499cd181..d5211fd6a63 100644 --- a/tests/components/efergy/fixtures/monthly_cost.json +++ b/tests/components/efergy/fixtures/monthly_cost.json @@ -2,4 +2,4 @@ "sum": "147.56", "duration": 2537340, "units": "GBP" -} \ No newline at end of file +} diff --git a/tests/components/efergy/fixtures/monthly_energy.json b/tests/components/efergy/fixtures/monthly_energy.json index ab4603e8959..d3a952e19bb 100644 --- a/tests/components/efergy/fixtures/monthly_energy.json +++ b/tests/components/efergy/fixtures/monthly_energy.json @@ -2,4 +2,4 @@ "sum": "1069.88", "duration": 2537340, "units": "kWh" -} \ No newline at end of file +} diff --git a/tests/components/efergy/fixtures/status.json b/tests/components/efergy/fixtures/status.json index 2e38374831a..748dc4a8142 100644 --- a/tests/components/efergy/fixtures/status.json +++ b/tests/components/efergy/fixtures/status.json @@ -1,31 +1,31 @@ { - "hid":"1234567890abcdef1234567890abcdef", - "listOfMacs":[ + "hid": "1234567890abcdef1234567890abcdef", + "listOfMacs": [ { - "listofchannels":[ + "listofchannels": [ { - "assoc":1, - "cid":"cid.ffffffffffff", - "reading":null, - "ts":1632961265, - "tsDelta":1, - "tsHuman":"Thu Sep 30 00:00:00 2021", - "type":{ - "battery":5, - "falseBattery":0, - "id":null, - "name":"EFCT" + "assoc": 1, + "cid": "cid.ffffffffffff", + "reading": null, + "ts": 1632961265, + "tsDelta": 1, + "tsHuman": "Thu Sep 30 00:00:00 2021", + "type": { + "battery": 5, + "falseBattery": 0, + "id": null, + "name": "EFCT" } } ], - "mac":"ffffffffffff", - "personality":"E1", - "status":"on", - "ts":1632961265, - "tsDelta":1, - "tsHuman":"Thu Sep 30 00:00:00 2021", - "type":"EEEHub", - "version":"2.3.7" + "mac": "ffffffffffff", + "personality": "E1", + "status": "on", + "ts": 1632961265, + "tsDelta": 1, + "tsHuman": "Thu Sep 30 00:00:00 2021", + "type": "EEEHub", + "version": "2.3.7" } ] -} \ No newline at end of file +} diff --git a/tests/components/efergy/fixtures/weekly_cost.json b/tests/components/efergy/fixtures/weekly_cost.json index f5267f70d2d..cd4dc572c0e 100644 --- a/tests/components/efergy/fixtures/weekly_cost.json +++ b/tests/components/efergy/fixtures/weekly_cost.json @@ -2,4 +2,4 @@ "sum": "36.89", "duration": 377280, "units": "GBP" -} \ No newline at end of file +} diff --git a/tests/components/efergy/fixtures/weekly_energy.json b/tests/components/efergy/fixtures/weekly_energy.json index f4ae92c0af2..163f3c9c650 100644 --- a/tests/components/efergy/fixtures/weekly_energy.json +++ b/tests/components/efergy/fixtures/weekly_energy.json @@ -2,4 +2,4 @@ "sum": "267.47", "duration": 377280, "units": "kWh" -} \ No newline at end of file +} diff --git a/tests/components/efergy/fixtures/yearly_cost.json b/tests/components/efergy/fixtures/yearly_cost.json index 375dbde542e..3bf2af1e27e 100644 --- a/tests/components/efergy/fixtures/yearly_cost.json +++ b/tests/components/efergy/fixtures/yearly_cost.json @@ -2,4 +2,4 @@ "sum": "1844.50", "duration": 23532540, "units": "GBP" -} \ No newline at end of file +} diff --git a/tests/components/efergy/fixtures/yearly_energy.json b/tests/components/efergy/fixtures/yearly_energy.json index b6026d8ea2d..a1bec7a203c 100644 --- a/tests/components/efergy/fixtures/yearly_energy.json +++ b/tests/components/efergy/fixtures/yearly_energy.json @@ -2,4 +2,4 @@ "sum": "13373.50", "duration": 23532540, "units": "kWh" -} \ No newline at end of file +} diff --git a/tests/components/elmax/fixtures/get_panel.json b/tests/components/elmax/fixtures/get_panel.json index 04fcfd48605..b97ab3b6c30 100644 --- a/tests/components/elmax/fixtures/get_panel.json +++ b/tests/components/elmax/fixtures/get_panel.json @@ -1,126 +1,126 @@ { - "release": 11.7, - "tappFeature": true, - "sceneFeature": true, - "zone": [ - { - "endpointId": "2db3dae30b9102de4d078706f94d0708-zona-0", - "visibile": true, - "indice": 0, - "nome": "Feed zone 0", - "aperta": false, - "esclusa": false - }, - { - "endpointId": "2db3dae30b9102de4d078706f94d0708-zona-1", - "visibile": true, - "indice": 1, - "nome": "Feed Zone 1", - "aperta": false, - "esclusa": false - }, - { - "endpointId": "2db3dae30b9102de4d078706f94d0708-zona-2", - "visibile": true, - "indice": 2, - "nome": "Feed Zone 2", - "aperta": false, - "esclusa": false - } - ], - "uscite": [ - { - "endpointId": "2db3dae30b9102de4d078706f94d0708-uscita-0", - "visibile": true, - "indice": 0, - "nome": "Actuator 0", - "aperta": false - }, - { - "endpointId": "2db3dae30b9102de4d078706f94d0708-uscita-1", - "visibile": true, - "indice": 1, - "nome": "Actuator 1", - "aperta": false - }, - { - "endpointId": "2db3dae30b9102de4d078706f94d0708-uscita-2", - "visibile": true, - "indice": 2, - "nome": "Actuator 2", - "aperta": true - } - ], - "aree": [ - { - "endpointId": "2db3dae30b9102de4d078706f94d0708-area-0", - "visibile": true, - "indice": 0, - "nome": "AREA 0", - "statiDisponibili": [0, 1, 2, 3, 4], - "statiSessioneDisponibili": [0, 1, 2, 3], - "stato": 0, - "statoSessione": 0 - }, - { - "endpointId": "2db3dae30b9102de4d078706f94d0708-area-1", - "visibile": true, - "indice": 1, - "nome": "AREA 1", - "statiDisponibili": [0, 1, 2, 3, 4], - "statiSessioneDisponibili": [0, 1, 2, 3], - "stato": 0, - "statoSessione": 0 - }, - { - "endpointId": "2db3dae30b9102de4d078706f94d0708-area-2", - "visibile": false, - "indice": 2, - "nome": "AREA 2", - "statiDisponibili": [0, 1, 2, 3, 4], - "statiSessioneDisponibili": [0, 1, 2, 3], - "stato": 0, - "statoSessione": 0 - } - ], - "tapparelle": [ - { - "endpointId": "2db3dae30b9102de4d078706f94d0708-tapparella-0", - "visibile": true, - "indice": 0, - "stato": "stop", - "posizione": 100, - "nome": "Cover 0" - } - ], - "gruppi": [ - { - "endpointId": "2db3dae30b9102de4d078706f94d0708-gruppo-0", - "visibile": true, - "indice": 0, - "nome": "Group 0" - }, - { - "endpointId": "2db3dae30b9102de4d078706f94d0708-gruppo-1", - "visibile": false, - "indice": 1, - "nome": "Group 1" - } - ], - "scenari": [ - { - "endpointId": "2db3dae30b9102de4d078706f94d0708-scenario-0", - "visibile": true, - "indice": 0, - "nome": "Automation 0" - }, - { - "endpointId": "2db3dae30b9102de4d078706f94d0708-scenario-2", - "visibile": true, - "indice": 2, - "nome": "Automation 2" - } - ], - "utente": "this.is@test.com", - "centrale": "2db3dae30b9102de4d078706f94d0708" -} \ No newline at end of file + "release": 11.7, + "tappFeature": true, + "sceneFeature": true, + "zone": [ + { + "endpointId": "2db3dae30b9102de4d078706f94d0708-zona-0", + "visibile": true, + "indice": 0, + "nome": "Feed zone 0", + "aperta": false, + "esclusa": false + }, + { + "endpointId": "2db3dae30b9102de4d078706f94d0708-zona-1", + "visibile": true, + "indice": 1, + "nome": "Feed Zone 1", + "aperta": false, + "esclusa": false + }, + { + "endpointId": "2db3dae30b9102de4d078706f94d0708-zona-2", + "visibile": true, + "indice": 2, + "nome": "Feed Zone 2", + "aperta": false, + "esclusa": false + } + ], + "uscite": [ + { + "endpointId": "2db3dae30b9102de4d078706f94d0708-uscita-0", + "visibile": true, + "indice": 0, + "nome": "Actuator 0", + "aperta": false + }, + { + "endpointId": "2db3dae30b9102de4d078706f94d0708-uscita-1", + "visibile": true, + "indice": 1, + "nome": "Actuator 1", + "aperta": false + }, + { + "endpointId": "2db3dae30b9102de4d078706f94d0708-uscita-2", + "visibile": true, + "indice": 2, + "nome": "Actuator 2", + "aperta": true + } + ], + "aree": [ + { + "endpointId": "2db3dae30b9102de4d078706f94d0708-area-0", + "visibile": true, + "indice": 0, + "nome": "AREA 0", + "statiDisponibili": [0, 1, 2, 3, 4], + "statiSessioneDisponibili": [0, 1, 2, 3], + "stato": 0, + "statoSessione": 0 + }, + { + "endpointId": "2db3dae30b9102de4d078706f94d0708-area-1", + "visibile": true, + "indice": 1, + "nome": "AREA 1", + "statiDisponibili": [0, 1, 2, 3, 4], + "statiSessioneDisponibili": [0, 1, 2, 3], + "stato": 0, + "statoSessione": 0 + }, + { + "endpointId": "2db3dae30b9102de4d078706f94d0708-area-2", + "visibile": false, + "indice": 2, + "nome": "AREA 2", + "statiDisponibili": [0, 1, 2, 3, 4], + "statiSessioneDisponibili": [0, 1, 2, 3], + "stato": 0, + "statoSessione": 0 + } + ], + "tapparelle": [ + { + "endpointId": "2db3dae30b9102de4d078706f94d0708-tapparella-0", + "visibile": true, + "indice": 0, + "stato": "stop", + "posizione": 100, + "nome": "Cover 0" + } + ], + "gruppi": [ + { + "endpointId": "2db3dae30b9102de4d078706f94d0708-gruppo-0", + "visibile": true, + "indice": 0, + "nome": "Group 0" + }, + { + "endpointId": "2db3dae30b9102de4d078706f94d0708-gruppo-1", + "visibile": false, + "indice": 1, + "nome": "Group 1" + } + ], + "scenari": [ + { + "endpointId": "2db3dae30b9102de4d078706f94d0708-scenario-0", + "visibile": true, + "indice": 0, + "nome": "Automation 0" + }, + { + "endpointId": "2db3dae30b9102de4d078706f94d0708-scenario-2", + "visibile": true, + "indice": 2, + "nome": "Automation 2" + } + ], + "utente": "this.is@test.com", + "centrale": "2db3dae30b9102de4d078706f94d0708" +} diff --git a/tests/components/elmax/fixtures/list_devices.json b/tests/components/elmax/fixtures/list_devices.json index 19cb1c44ed9..9a3091f371d 100644 --- a/tests/components/elmax/fixtures/list_devices.json +++ b/tests/components/elmax/fixtures/list_devices.json @@ -1,11 +1,12 @@ [ - { - "centrale_online": true, - "hash": "2db3dae30b9102de4d078706f94d0708", - "username": [{"name": "this.is@test.com", "label": "Test Panel Name"}] - },{ - "centrale_online": true, - "hash": "d8e8fca2dc0f896fd7cb4cb0031ba249", - "username": [{"name": "this.is@test.com", "label": "Test Panel Name"}] - } -] \ No newline at end of file + { + "centrale_online": true, + "hash": "2db3dae30b9102de4d078706f94d0708", + "username": [{ "name": "this.is@test.com", "label": "Test Panel Name" }] + }, + { + "centrale_online": true, + "hash": "d8e8fca2dc0f896fd7cb4cb0031ba249", + "username": [{ "name": "this.is@test.com", "label": "Test Panel Name" }] + } +] diff --git a/tests/components/elmax/fixtures/login.json b/tests/components/elmax/fixtures/login.json index 59f4aba559d..87b1af3f295 100644 --- a/tests/components/elmax/fixtures/login.json +++ b/tests/components/elmax/fixtures/login.json @@ -1,8 +1,8 @@ { - "token": "JWT eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiIxYjExYmIxMWJiYjExMTExYjFiMTFiMWIiLCJlbWFpbCI6InRoaXMuaXNAdGVzdC5jb20iLCJyb2xlIjoidXNlciIsImlhdCI6MTYzNjE5OTk5OCwiZXhwIjoxNjM2MjM1OTk4fQ.1C7lXuKyX1HEGOfMxNwxJ2n-CjoW4rwvNRITQxLICv0", - "user": { - "_id": "1b11bb11bbb11111b1b11b1b", - "email": "this.is@test.com", - "role": "user" - } -} \ No newline at end of file + "token": "JWT eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiIxYjExYmIxMWJiYjExMTExYjFiMTFiMWIiLCJlbWFpbCI6InRoaXMuaXNAdGVzdC5jb20iLCJyb2xlIjoidXNlciIsImlhdCI6MTYzNjE5OTk5OCwiZXhwIjoxNjM2MjM1OTk4fQ.1C7lXuKyX1HEGOfMxNwxJ2n-CjoW4rwvNRITQxLICv0", + "user": { + "_id": "1b11bb11bbb11111b1b11b1b", + "email": "this.is@test.com", + "role": "user" + } +} diff --git a/tests/components/emulated_hue/test_hue_api.py b/tests/components/emulated_hue/test_hue_api.py index 19a31b4566b..dd27eed9771 100644 --- a/tests/components/emulated_hue/test_hue_api.py +++ b/tests/components/emulated_hue/test_hue_api.py @@ -1033,17 +1033,14 @@ async def test_put_light_state_fan(hass_hue, hue_client): living_room_fan = hass_hue.states.get("fan.living_room_fan") assert living_room_fan.state == "on" - assert living_room_fan.attributes[fan.ATTR_SPEED] == fan.SPEED_MEDIUM + assert living_room_fan.attributes[fan.ATTR_PERCENTAGE] == 43 # Check setting the brightness of a fan to 0, 33%, 66% and 100% will respectively turn it off, low, medium or high # We also check non-cached GET value to exercise the code. await perform_put_light_state( hass_hue, hue_client, "fan.living_room_fan", True, brightness=0 ) - assert ( - hass_hue.states.get("fan.living_room_fan").attributes[fan.ATTR_SPEED] - == fan.SPEED_OFF - ) + assert hass_hue.states.get("fan.living_room_fan").state == STATE_OFF await perform_put_light_state( hass_hue, hue_client, @@ -1052,8 +1049,7 @@ async def test_put_light_state_fan(hass_hue, hue_client): brightness=round(33 * 254 / 100), ) assert ( - hass_hue.states.get("fan.living_room_fan").attributes[fan.ATTR_SPEED] - == fan.SPEED_LOW + hass_hue.states.get("fan.living_room_fan").attributes[fan.ATTR_PERCENTAGE] == 33 ) with patch.object(hue_api, "STATE_CACHED_TIMEOUT", 0.000001): await asyncio.sleep(0.000001) @@ -1070,8 +1066,7 @@ async def test_put_light_state_fan(hass_hue, hue_client): brightness=round(66 * 254 / 100), ) assert ( - hass_hue.states.get("fan.living_room_fan").attributes[fan.ATTR_SPEED] - == fan.SPEED_MEDIUM + hass_hue.states.get("fan.living_room_fan").attributes[fan.ATTR_PERCENTAGE] == 66 ) with patch.object(hue_api, "STATE_CACHED_TIMEOUT", 0.000001): await asyncio.sleep(0.000001) @@ -1079,7 +1074,7 @@ async def test_put_light_state_fan(hass_hue, hue_client): hue_client, "fan.living_room_fan", HTTPStatus.OK ) assert ( - round(fan_json["state"][HUE_API_STATE_BRI] * 100 / 254) == 67 + round(fan_json["state"][HUE_API_STATE_BRI] * 100 / 254) == 66 ) # small rounding error in inverse operation await perform_put_light_state( @@ -1090,8 +1085,8 @@ async def test_put_light_state_fan(hass_hue, hue_client): brightness=round(100 * 254 / 100), ) assert ( - hass_hue.states.get("fan.living_room_fan").attributes[fan.ATTR_SPEED] - == fan.SPEED_HIGH + hass_hue.states.get("fan.living_room_fan").attributes[fan.ATTR_PERCENTAGE] + == 100 ) with patch.object(hue_api, "STATE_CACHED_TIMEOUT", 0.000001): await asyncio.sleep(0.000001) diff --git a/tests/components/emulated_kasa/test_init.py b/tests/components/emulated_kasa/test_init.py index d68221d84fd..4295d162aa5 100644 --- a/tests/components/emulated_kasa/test_init.py +++ b/tests/components/emulated_kasa/test_init.py @@ -9,19 +9,15 @@ from homeassistant.components.emulated_kasa.const import ( DOMAIN, ) from homeassistant.components.fan import ( - ATTR_SPEED, + ATTR_PERCENTAGE, DOMAIN as FAN_DOMAIN, - SERVICE_SET_SPEED, + SERVICE_SET_PERCENTAGE, ) from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN -from homeassistant.components.switch import ( - ATTR_CURRENT_POWER_W, - DOMAIN as SWITCH_DOMAIN, -) +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.const import ( ATTR_ENTITY_ID, - ATTR_FRIENDLY_NAME, CONF_ENTITIES, CONF_NAME, SERVICE_TURN_OFF, @@ -57,15 +53,15 @@ CONFIG = { ENTITY_FAN: { CONF_POWER: "{% if is_state_attr('" + ENTITY_FAN - + "','speed', 'low') %} " + + "','percentage', 33) %} " + str(ENTITY_FAN_SPEED_LOW) + "{% elif is_state_attr('" + ENTITY_FAN - + "','speed', 'medium') %} " + + "','percentage', 66) %} " + str(ENTITY_FAN_SPEED_MED) + "{% elif is_state_attr('" + ENTITY_FAN - + "','speed', 'high') %} " + + "','percentage', 100) %} " + str(ENTITY_FAN_SPEED_HIGH) + "{% endif %}" }, @@ -109,15 +105,15 @@ CONFIG_FAN = { ENTITY_FAN: { CONF_POWER: "{% if is_state_attr('" + ENTITY_FAN - + "','speed', 'low') %} " + + "','percentage', 33) %} " + str(ENTITY_FAN_SPEED_LOW) + "{% elif is_state_attr('" + ENTITY_FAN - + "','speed', 'medium') %} " + + "','percentage', 66) %} " + str(ENTITY_FAN_SPEED_MED) + "{% elif is_state_attr('" + ENTITY_FAN - + "','speed', 'high') %} " + + "','percentage', 100) %} " + str(ENTITY_FAN_SPEED_HIGH) + "{% endif %}" }, @@ -125,6 +121,7 @@ CONFIG_FAN = { } } + CONFIG_SENSOR = { DOMAIN: { CONF_ENTITIES: { @@ -217,38 +214,6 @@ async def test_switch_power(hass): SWITCH_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: ENTITY_SWITCH}, blocking=True ) - hass.states.async_set( - ENTITY_SWITCH, - STATE_ON, - attributes={ATTR_CURRENT_POWER_W: 100, ATTR_FRIENDLY_NAME: "AC"}, - ) - - switch = hass.states.get(ENTITY_SWITCH) - assert switch.state == STATE_ON - power = switch.attributes[ATTR_CURRENT_POWER_W] - assert power == 100 - assert switch.name == "AC" - - plug_it = emulated_kasa.get_plug_devices(hass, config) - plug = next(plug_it).generate_response() - - assert nested_value(plug, "system", "get_sysinfo", "alias") == "AC" - power = nested_value(plug, "emeter", "get_realtime", "power") - assert math.isclose(power, power) - - hass.states.async_set( - ENTITY_SWITCH, - STATE_ON, - attributes={ATTR_CURRENT_POWER_W: 120, ATTR_FRIENDLY_NAME: "AC"}, - ) - - plug_it = emulated_kasa.get_plug_devices(hass, config) - plug = next(plug_it).generate_response() - - assert nested_value(plug, "system", "get_sysinfo", "alias") == "AC" - power = nested_value(plug, "emeter", "get_realtime", "power") - assert math.isclose(power, 120) - # Turn off await hass.services.async_call( SWITCH_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_SWITCH}, blocking=True @@ -281,8 +246,8 @@ async def test_template(hass): ) await hass.services.async_call( FAN_DOMAIN, - SERVICE_SET_SPEED, - {ATTR_ENTITY_ID: ENTITY_FAN, ATTR_SPEED: "low"}, + SERVICE_SET_PERCENTAGE, + {ATTR_ENTITY_ID: ENTITY_FAN, ATTR_PERCENTAGE: 33}, blocking=True, ) @@ -299,8 +264,8 @@ async def test_template(hass): # Fan High: await hass.services.async_call( FAN_DOMAIN, - SERVICE_SET_SPEED, - {ATTR_ENTITY_ID: ENTITY_FAN, ATTR_SPEED: "high"}, + SERVICE_SET_PERCENTAGE, + {ATTR_ENTITY_ID: ENTITY_FAN, ATTR_PERCENTAGE: 100}, blocking=True, ) plug_it = emulated_kasa.get_plug_devices(hass, config) @@ -462,8 +427,8 @@ async def test_multiple_devices(hass): ) await hass.services.async_call( FAN_DOMAIN, - SERVICE_SET_SPEED, - {ATTR_ENTITY_ID: ENTITY_FAN, ATTR_SPEED: "medium"}, + SERVICE_SET_PERCENTAGE, + {ATTR_ENTITY_ID: ENTITY_FAN, ATTR_PERCENTAGE: 66}, blocking=True, ) diff --git a/tests/components/ezviz/__init__.py b/tests/components/ezviz/__init__.py index b8dc04ef790..bfb30b893eb 100644 --- a/tests/components/ezviz/__init__.py +++ b/tests/components/ezviz/__init__.py @@ -3,9 +3,7 @@ from unittest.mock import patch from homeassistant.components.ezviz.const import ( ATTR_SERIAL, - ATTR_TYPE_CAMERA, ATTR_TYPE_CLOUD, - CONF_CAMERAS, CONF_FFMPEG_ARGUMENTS, DEFAULT_FFMPEG_ARGUMENTS, DEFAULT_TIMEOUT, @@ -48,37 +46,6 @@ USER_INPUT = { CONF_TYPE: ATTR_TYPE_CLOUD, } -USER_INPUT_CAMERA_VALIDATE = { - ATTR_SERIAL: "C666666", - CONF_PASSWORD: "test-password", - CONF_USERNAME: "test-username", -} - -USER_INPUT_CAMERA = { - CONF_PASSWORD: "test-password", - CONF_USERNAME: "test-username", - CONF_TYPE: ATTR_TYPE_CAMERA, -} - -YAML_CONFIG = { - CONF_USERNAME: "test-username", - CONF_PASSWORD: "test-password", - CONF_URL: "apiieu.ezvizlife.com", - CONF_CAMERAS: { - "C666666": {CONF_USERNAME: "test-username", CONF_PASSWORD: "test-password"} - }, -} - -YAML_INVALID = { - "C666666": {CONF_USERNAME: "test-username", CONF_PASSWORD: "test-password"} -} - -YAML_CONFIG_CAMERA = { - ATTR_SERIAL: "C666666", - CONF_USERNAME: "test-username", - CONF_PASSWORD: "test-password", -} - DISCOVERY_INFO = { ATTR_SERIAL: "C666666", CONF_USERNAME: None, diff --git a/tests/components/ezviz/test_config_flow.py b/tests/components/ezviz/test_config_flow.py index 9a3129b7dd6..4ba3b5911f6 100644 --- a/tests/components/ezviz/test_config_flow.py +++ b/tests/components/ezviz/test_config_flow.py @@ -19,11 +19,7 @@ from homeassistant.components.ezviz.const import ( DEFAULT_TIMEOUT, DOMAIN, ) -from homeassistant.config_entries import ( - SOURCE_IMPORT, - SOURCE_INTEGRATION_DISCOVERY, - SOURCE_USER, -) +from homeassistant.config_entries import SOURCE_INTEGRATION_DISCOVERY, SOURCE_USER from homeassistant.const import ( CONF_CUSTOMIZE, CONF_IP_ADDRESS, @@ -42,12 +38,7 @@ from homeassistant.data_entry_flow import ( from . import ( DISCOVERY_INFO, USER_INPUT, - USER_INPUT_CAMERA, - USER_INPUT_CAMERA_VALIDATE, USER_INPUT_VALIDATE, - YAML_CONFIG, - YAML_CONFIG_CAMERA, - YAML_INVALID, _patch_async_setup_entry, init_integration, ) @@ -115,66 +106,6 @@ async def test_user_custom_url(hass, ezviz_config_flow): assert len(mock_setup_entry.mock_calls) == 1 -async def test_async_step_import(hass, ezviz_config_flow): - """Test the config import flow.""" - - with _patch_async_setup_entry() as mock_setup_entry: - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=YAML_CONFIG - ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY - assert result["data"] == USER_INPUT - - assert len(mock_setup_entry.mock_calls) == 1 - - -async def test_async_step_import_camera(hass, ezviz_config_flow): - """Test the config import camera flow.""" - - with _patch_async_setup_entry() as mock_setup_entry: - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=YAML_CONFIG_CAMERA - ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY - assert result["data"] == USER_INPUT_CAMERA - - assert len(mock_setup_entry.mock_calls) == 1 - - -async def test_async_step_import_2nd_form_returns_camera(hass, ezviz_config_flow): - """Test we get the user initiated form.""" - - with _patch_async_setup_entry() as mock_setup_entry: - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=YAML_CONFIG - ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY - assert result["data"] == USER_INPUT - - assert len(mock_setup_entry.mock_calls) == 1 - - with _patch_async_setup_entry() as mock_setup_entry: - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=USER_INPUT_CAMERA_VALIDATE - ) - await hass.async_block_till_done() - - assert result["type"] == RESULT_TYPE_CREATE_ENTRY - assert result["data"] == USER_INPUT_CAMERA - - assert len(mock_setup_entry.mock_calls) == 1 - - -async def test_async_step_import_abort(hass, ezviz_config_flow): - """Test the config import flow with invalid data.""" - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=YAML_INVALID - ) - assert result["type"] == RESULT_TYPE_ABORT - assert result["reason"] == "unknown" - - async def test_step_discovery_abort_if_cloud_account_missing(hass): """Test discovery and confirm step, abort if cloud account was removed.""" @@ -308,45 +239,6 @@ async def test_user_form_exception(hass, ezviz_config_flow): assert result["reason"] == "unknown" -async def test_import_exception(hass, ezviz_config_flow): - """Test we handle unexpected exception on import.""" - ezviz_config_flow.side_effect = PyEzvizError - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=YAML_CONFIG - ) - - assert result["type"] == RESULT_TYPE_ABORT - assert result["reason"] == "invalid_auth" - - ezviz_config_flow.side_effect = InvalidURL - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=YAML_CONFIG - ) - - assert result["type"] == RESULT_TYPE_ABORT - assert result["reason"] == "invalid_host" - - ezviz_config_flow.side_effect = HTTPError - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=YAML_CONFIG - ) - - assert result["type"] == RESULT_TYPE_ABORT - assert result["reason"] == "cannot_connect" - - ezviz_config_flow.side_effect = Exception - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=YAML_CONFIG - ) - - assert result["type"] == RESULT_TYPE_ABORT - assert result["reason"] == "unknown" - - async def test_discover_exception_step1( hass, ezviz_config_flow, diff --git a/tests/components/fan/common.py b/tests/components/fan/common.py index c32686b9311..58264d80817 100644 --- a/tests/components/fan/common.py +++ b/tests/components/fan/common.py @@ -9,7 +9,6 @@ from homeassistant.components.fan import ( ATTR_PERCENTAGE, ATTR_PERCENTAGE_STEP, ATTR_PRESET_MODE, - ATTR_SPEED, DOMAIN, SERVICE_DECREASE_SPEED, SERVICE_INCREASE_SPEED, @@ -17,7 +16,6 @@ from homeassistant.components.fan import ( SERVICE_SET_DIRECTION, SERVICE_SET_PERCENTAGE, SERVICE_SET_PRESET_MODE, - SERVICE_SET_SPEED, ) from homeassistant.const import ( ATTR_ENTITY_ID, @@ -30,7 +28,6 @@ from homeassistant.const import ( async def async_turn_on( hass, entity_id=ENTITY_MATCH_ALL, - speed: str = None, percentage: int = None, preset_mode: str = None, ) -> None: @@ -39,7 +36,6 @@ async def async_turn_on( key: value for key, value in [ (ATTR_ENTITY_ID, entity_id), - (ATTR_SPEED, speed), (ATTR_PERCENTAGE, percentage), (ATTR_PRESET_MODE, preset_mode), ] @@ -72,17 +68,6 @@ async def async_oscillate( await hass.services.async_call(DOMAIN, SERVICE_OSCILLATE, data, blocking=True) -async def async_set_speed(hass, entity_id=ENTITY_MATCH_ALL, speed: str = None) -> None: - """Set speed for all or specified fan.""" - data = { - key: value - for key, value in [(ATTR_ENTITY_ID, entity_id), (ATTR_SPEED, speed)] - if value is not None - } - - await hass.services.async_call(DOMAIN, SERVICE_SET_SPEED, data, blocking=True) - - async def async_set_preset_mode( hass, entity_id=ENTITY_MATCH_ALL, preset_mode: str = None ) -> None: diff --git a/tests/components/fan/test_init.py b/tests/components/fan/test_init.py index d29d4378941..f7021c0fb66 100644 --- a/tests/components/fan/test_init.py +++ b/tests/components/fan/test_init.py @@ -16,7 +16,6 @@ def test_fanentity(): """Test fan entity methods.""" fan = BaseFan() assert fan.state == "off" - assert len(fan.speed_list) == 4 # legacy compat off,low,medium,high assert fan.preset_modes is None assert fan.supported_features == 0 assert fan.percentage_step == 1 @@ -25,7 +24,7 @@ def test_fanentity(): # Test set_speed not required with pytest.raises(NotImplementedError): fan.oscillate(True) - with pytest.raises(NotImplementedError): + with pytest.raises(AttributeError): fan.set_speed("low") with pytest.raises(NotImplementedError): fan.set_percentage(0) @@ -42,7 +41,6 @@ async def test_async_fanentity(hass): fan = BaseFan() fan.hass = hass assert fan.state == "off" - assert len(fan.speed_list) == 4 # legacy compat off,low,medium,high assert fan.preset_modes is None assert fan.supported_features == 0 assert fan.percentage_step == 1 @@ -51,7 +49,7 @@ async def test_async_fanentity(hass): # Test set_speed not required with pytest.raises(NotImplementedError): await fan.async_oscillate(True) - with pytest.raises(NotImplementedError): + with pytest.raises(AttributeError): await fan.async_set_speed("low") with pytest.raises(NotImplementedError): await fan.async_set_percentage(0) diff --git a/tests/components/fan/test_recorder.py b/tests/components/fan/test_recorder.py new file mode 100644 index 00000000000..360e03d2491 --- /dev/null +++ b/tests/components/fan/test_recorder.py @@ -0,0 +1,41 @@ +"""The tests for fan recorder.""" +from __future__ import annotations + +from datetime import timedelta + +from homeassistant.components import fan +from homeassistant.components.fan import ATTR_PRESET_MODES +from homeassistant.components.recorder.models import StateAttributes, States +from homeassistant.components.recorder.util import session_scope +from homeassistant.const import ATTR_FRIENDLY_NAME +from homeassistant.core import State +from homeassistant.setup import async_setup_component +from homeassistant.util import dt as dt_util + +from tests.common import async_fire_time_changed, async_init_recorder_component +from tests.components.recorder.common import async_wait_recording_done_without_instance + + +async def test_exclude_attributes(hass): + """Test fan registered attributes to be excluded.""" + await async_init_recorder_component(hass) + await async_setup_component(hass, fan.DOMAIN, {fan.DOMAIN: {"platform": "demo"}}) + await hass.async_block_till_done() + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5)) + await hass.async_block_till_done() + await async_wait_recording_done_without_instance(hass) + + def _fetch_states() -> list[State]: + with session_scope(hass=hass) as session: + native_states = [] + for db_state, db_state_attributes in session.query(States, StateAttributes): + state = db_state.to_native() + state.attributes = db_state_attributes.to_native() + native_states.append(state) + return native_states + + states: list[State] = await hass.async_add_executor_job(_fetch_states) + assert len(states) > 1 + for state in states: + assert ATTR_PRESET_MODES not in state.attributes + assert ATTR_FRIENDLY_NAME in state.attributes diff --git a/tests/components/fan/test_reproduce_state.py b/tests/components/fan/test_reproduce_state.py index 149332b3fa8..fe1f27ba625 100644 --- a/tests/components/fan/test_reproduce_state.py +++ b/tests/components/fan/test_reproduce_state.py @@ -1,5 +1,6 @@ """Test reproduce state for Fan.""" from homeassistant.core import State +from homeassistant.helpers.state import async_reproduce_state from tests.common import async_mock_service @@ -8,7 +9,7 @@ async def test_reproducing_states(hass, caplog): """Test reproducing Fan states.""" hass.states.async_set("fan.entity_off", "off", {}) hass.states.async_set("fan.entity_on", "on", {}) - hass.states.async_set("fan.entity_speed", "on", {"speed": "high"}) + hass.states.async_set("fan.entity_speed", "on", {"percentage": 100}) hass.states.async_set("fan.entity_oscillating", "on", {"oscillating": True}) hass.states.async_set("fan.entity_direction", "on", {"direction": "forward"}) @@ -16,14 +17,15 @@ async def test_reproducing_states(hass, caplog): turn_off_calls = async_mock_service(hass, "fan", "turn_off") set_direction_calls = async_mock_service(hass, "fan", "set_direction") oscillate_calls = async_mock_service(hass, "fan", "oscillate") - set_speed_calls = async_mock_service(hass, "fan", "set_speed") + set_percentage_calls = async_mock_service(hass, "fan", "set_percentage") # These calls should do nothing as entities already in desired state - await hass.helpers.state.async_reproduce_state( + await async_reproduce_state( + hass, [ State("fan.entity_off", "off"), State("fan.entity_on", "on"), - State("fan.entity_speed", "on", {"speed": "high"}), + State("fan.entity_speed", "on", {"percentage": 100}), State("fan.entity_oscillating", "on", {"oscillating": True}), State("fan.entity_direction", "on", {"direction": "forward"}), ], @@ -33,26 +35,24 @@ async def test_reproducing_states(hass, caplog): assert len(turn_off_calls) == 0 assert len(set_direction_calls) == 0 assert len(oscillate_calls) == 0 - assert len(set_speed_calls) == 0 # Test invalid state is handled - await hass.helpers.state.async_reproduce_state( - [State("fan.entity_off", "not_supported")] - ) + await async_reproduce_state(hass, [State("fan.entity_off", "not_supported")]) assert "not_supported" in caplog.text assert len(turn_on_calls) == 0 assert len(turn_off_calls) == 0 assert len(set_direction_calls) == 0 assert len(oscillate_calls) == 0 - assert len(set_speed_calls) == 0 + assert len(set_percentage_calls) == 0 # Make sure correct services are called - await hass.helpers.state.async_reproduce_state( + await async_reproduce_state( + hass, [ State("fan.entity_on", "off"), State("fan.entity_off", "on"), - State("fan.entity_speed", "on", {"speed": "low"}), + State("fan.entity_speed", "on", {"percentage": 25}), State("fan.entity_oscillating", "on", {"oscillating": False}), State("fan.entity_direction", "on", {"direction": "reverse"}), # Should not raise @@ -78,9 +78,12 @@ async def test_reproducing_states(hass, caplog): "oscillating": False, } - assert len(set_speed_calls) == 1 - assert set_speed_calls[0].domain == "fan" - assert set_speed_calls[0].data == {"entity_id": "fan.entity_speed", "speed": "low"} + assert len(set_percentage_calls) == 1 + assert set_percentage_calls[0].domain == "fan" + assert set_percentage_calls[0].data == { + "entity_id": "fan.entity_speed", + "percentage": 25, + } assert len(turn_off_calls) == 1 assert turn_off_calls[0].domain == "fan" diff --git a/tests/components/fibaro/__init__.py b/tests/components/fibaro/__init__.py new file mode 100644 index 00000000000..3787f00202e --- /dev/null +++ b/tests/components/fibaro/__init__.py @@ -0,0 +1 @@ +"""Tests for the Fibaro integration.""" diff --git a/tests/components/fibaro/test_config_flow.py b/tests/components/fibaro/test_config_flow.py new file mode 100644 index 00000000000..6f3e035a2f7 --- /dev/null +++ b/tests/components/fibaro/test_config_flow.py @@ -0,0 +1,204 @@ +"""Test the Fibaro config flow.""" +from unittest.mock import Mock, patch + +from fiblary3.common.exceptions import HTTPException +import pytest + +from homeassistant import config_entries +from homeassistant.components.fibaro import DOMAIN +from homeassistant.components.fibaro.const import CONF_IMPORT_PLUGINS +from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME + +TEST_SERIALNUMBER = "HC2-111111" +TEST_NAME = "my_fibaro_home_center" +TEST_URL = "http://192.168.1.1/api/" +TEST_USERNAME = "user" +TEST_PASSWORD = "password" + + +@pytest.fixture(name="fibaro_client", autouse=True) +def fibaro_client_fixture(): + """Mock common methods and attributes of fibaro client.""" + info_mock = Mock() + info_mock.get.return_value = Mock(serialNumber=TEST_SERIALNUMBER, hcName=TEST_NAME) + + array_mock = Mock() + array_mock.list.return_value = [] + + with patch("fiblary3.client.v4.client.Client.__init__", return_value=None,), patch( + "fiblary3.client.v4.client.Client.info", + info_mock, + create=True, + ), patch("fiblary3.client.v4.client.Client.rooms", array_mock, create=True,), patch( + "fiblary3.client.v4.client.Client.devices", + array_mock, + create=True, + ), patch( + "fiblary3.client.v4.client.Client.scenes", + array_mock, + create=True, + ): + yield + + +async def test_config_flow_user_initiated_success(hass): + """Successful flow manually initialized by the user.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + assert result["errors"] == {} + + login_mock = Mock() + login_mock.get.return_value = Mock(status=True) + with patch("fiblary3.client.v4.client.Client.login", login_mock, create=True): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_URL: TEST_URL, + CONF_USERNAME: TEST_USERNAME, + CONF_PASSWORD: TEST_PASSWORD, + }, + ) + + assert result["type"] == "create_entry" + assert result["title"] == TEST_NAME + assert result["data"] == { + CONF_URL: TEST_URL, + CONF_USERNAME: TEST_USERNAME, + CONF_PASSWORD: TEST_PASSWORD, + CONF_IMPORT_PLUGINS: False, + } + + +async def test_config_flow_user_initiated_connect_failure(hass): + """Connect failure in flow manually initialized by the user.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + assert result["errors"] == {} + + login_mock = Mock() + login_mock.get.return_value = Mock(status=False) + with patch("fiblary3.client.v4.client.Client.login", login_mock, create=True): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_URL: TEST_URL, + CONF_USERNAME: TEST_USERNAME, + CONF_PASSWORD: TEST_PASSWORD, + }, + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + assert result["errors"] == {"base": "cannot_connect"} + + +async def test_config_flow_user_initiated_auth_failure(hass): + """Authentication failure in flow manually initialized by the user.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + assert result["errors"] == {} + + login_mock = Mock() + login_mock.get.side_effect = HTTPException(details="Forbidden") + with patch("fiblary3.client.v4.client.Client.login", login_mock, create=True): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_URL: TEST_URL, + CONF_USERNAME: TEST_USERNAME, + CONF_PASSWORD: TEST_PASSWORD, + }, + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + assert result["errors"] == {"base": "invalid_auth"} + + +async def test_config_flow_user_initiated_unknown_failure_1(hass): + """Unknown failure in flow manually initialized by the user.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + assert result["errors"] == {} + + login_mock = Mock() + login_mock.get.side_effect = HTTPException(details="Any") + with patch("fiblary3.client.v4.client.Client.login", login_mock, create=True): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_URL: TEST_URL, + CONF_USERNAME: TEST_USERNAME, + CONF_PASSWORD: TEST_PASSWORD, + }, + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + assert result["errors"] == {"base": "cannot_connect"} + + +async def test_config_flow_user_initiated_unknown_failure_2(hass): + """Unknown failure in flow manually initialized by the user.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_URL: TEST_URL, + CONF_USERNAME: TEST_USERNAME, + CONF_PASSWORD: TEST_PASSWORD, + }, + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + assert result["errors"] == {"base": "cannot_connect"} + + +async def test_config_flow_import(hass): + """Test for importing config from configuration.yaml.""" + login_mock = Mock() + login_mock.get.return_value = Mock(status=True) + with patch("fiblary3.client.v4.client.Client.login", login_mock, create=True): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + CONF_URL: TEST_URL, + CONF_USERNAME: TEST_USERNAME, + CONF_PASSWORD: TEST_PASSWORD, + CONF_IMPORT_PLUGINS: False, + }, + ) + + assert result["type"] == "create_entry" + assert result["title"] == TEST_NAME + assert result["data"] == { + CONF_URL: TEST_URL, + CONF_USERNAME: TEST_USERNAME, + CONF_PASSWORD: TEST_PASSWORD, + CONF_IMPORT_PLUGINS: False, + } diff --git a/tests/components/filesize/__init__.py b/tests/components/filesize/__init__.py index 02876267482..d7aa8dd2481 100644 --- a/tests/components/filesize/__init__.py +++ b/tests/components/filesize/__init__.py @@ -1 +1,14 @@ """Tests for the filesize component.""" +import os + +TEST_DIR = os.path.join(os.path.dirname(__file__)) +TEST_FILE_NAME = "mock_file_test_filesize.txt" +TEST_FILE_NAME2 = "mock_file_test_filesize2.txt" +TEST_FILE = os.path.join(TEST_DIR, TEST_FILE_NAME) +TEST_FILE2 = os.path.join(TEST_DIR, TEST_FILE_NAME2) + + +def create_file(path) -> None: + """Create a test file.""" + with open(path, "w", encoding="utf-8") as test_file: + test_file.write("test") diff --git a/tests/components/filesize/conftest.py b/tests/components/filesize/conftest.py new file mode 100644 index 00000000000..6584ebc95df --- /dev/null +++ b/tests/components/filesize/conftest.py @@ -0,0 +1,45 @@ +"""Fixtures for Filesize integration tests.""" +from __future__ import annotations + +from collections.abc import Generator +import os +from unittest.mock import patch + +import pytest + +from homeassistant.components.filesize.const import DOMAIN +from homeassistant.const import CONF_FILE_PATH + +from . import TEST_FILE, TEST_FILE2, TEST_FILE_NAME + +from tests.common import MockConfigEntry + + +@pytest.fixture +def mock_config_entry() -> MockConfigEntry: + """Return the default mocked config entry.""" + return MockConfigEntry( + title=TEST_FILE_NAME, + domain=DOMAIN, + data={CONF_FILE_PATH: TEST_FILE}, + unique_id=TEST_FILE, + ) + + +@pytest.fixture +def mock_setup_entry() -> Generator[None, None, None]: + """Mock setting up a config entry.""" + with patch( + "homeassistant.components.filesize.async_setup_entry", return_value=True + ): + yield + + +@pytest.fixture(autouse=True) +def remove_file() -> None: + """Remove test file.""" + yield + if os.path.isfile(TEST_FILE): + os.remove(TEST_FILE) + if os.path.isfile(TEST_FILE2): + os.remove(TEST_FILE2) diff --git a/tests/components/filesize/fixtures/configuration.yaml b/tests/components/filesize/fixtures/configuration.yaml deleted file mode 100644 index ea73be72b80..00000000000 --- a/tests/components/filesize/fixtures/configuration.yaml +++ /dev/null @@ -1,4 +0,0 @@ -sensor: - - platform: filesize - file_paths: - - "/dev/null" diff --git a/tests/components/filesize/test_config_flow.py b/tests/components/filesize/test_config_flow.py new file mode 100644 index 00000000000..0209444ec42 --- /dev/null +++ b/tests/components/filesize/test_config_flow.py @@ -0,0 +1,130 @@ +"""Tests for the Filesize config flow.""" +from unittest.mock import patch + +import pytest + +from homeassistant.components.filesize.const import DOMAIN +from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER +from homeassistant.const import CONF_FILE_PATH +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import ( + RESULT_TYPE_ABORT, + RESULT_TYPE_CREATE_ENTRY, + RESULT_TYPE_FORM, +) + +from . import TEST_DIR, TEST_FILE, TEST_FILE_NAME, create_file + +from tests.common import MockConfigEntry + + +async def test_full_user_flow(hass: HomeAssistant) -> None: + """Test the full user configuration flow.""" + create_file(TEST_FILE) + hass.config.allowlist_external_dirs = {TEST_DIR} + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result.get("type") == RESULT_TYPE_FORM + assert result.get("step_id") == SOURCE_USER + assert "flow_id" in result + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_FILE_PATH: TEST_FILE}, + ) + + assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("title") == TEST_FILE_NAME + assert result2.get("data") == {CONF_FILE_PATH: TEST_FILE} + + +@pytest.mark.parametrize("source", [SOURCE_USER, SOURCE_IMPORT]) +async def test_unique_path( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + source: str, +) -> None: + """Test we abort if already setup.""" + hass.config.allowlist_external_dirs = {TEST_DIR} + mock_config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": source}, data={CONF_FILE_PATH: TEST_FILE} + ) + + assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("reason") == "already_configured" + + +async def test_import_flow(hass: HomeAssistant) -> None: + """Test the import configuration flow.""" + create_file(TEST_FILE) + hass.config.allowlist_external_dirs = {TEST_DIR} + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={CONF_FILE_PATH: TEST_FILE}, + ) + + assert result.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result.get("title") == TEST_FILE_NAME + assert result.get("data") == {CONF_FILE_PATH: TEST_FILE} + + +async def test_flow_fails_on_validation(hass: HomeAssistant) -> None: + """Test config flow errors.""" + create_file(TEST_FILE) + hass.config.allowlist_external_dirs = {} + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == SOURCE_USER + + with patch( + "homeassistant.components.filesize.config_flow.pathlib.Path", + side_effect=OSError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_FILE_PATH: TEST_FILE, + }, + ) + + assert result2["errors"] == {"base": "not_valid"} + + with patch("homeassistant.components.filesize.config_flow.pathlib.Path",), patch( + "homeassistant.components.filesize.async_setup_entry", + return_value=True, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_FILE_PATH: TEST_FILE, + }, + ) + + assert result2["errors"] == {"base": "not_allowed"} + + hass.config.allowlist_external_dirs = {TEST_DIR} + with patch("homeassistant.components.filesize.config_flow.pathlib.Path",), patch( + "homeassistant.components.filesize.async_setup_entry", + return_value=True, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_FILE_PATH: TEST_FILE, + }, + ) + + assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["title"] == TEST_FILE_NAME + assert result2["data"] == { + CONF_FILE_PATH: TEST_FILE, + } diff --git a/tests/components/filesize/test_init.py b/tests/components/filesize/test_init.py new file mode 100644 index 00000000000..fecd3033c91 --- /dev/null +++ b/tests/components/filesize/test_init.py @@ -0,0 +1,110 @@ +"""Tests for the Filesize integration.""" +from unittest.mock import AsyncMock + +from homeassistant.components.filesize.const import CONF_FILE_PATHS, DOMAIN +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import CONF_FILE_PATH +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from . import ( + TEST_DIR, + TEST_FILE, + TEST_FILE2, + TEST_FILE_NAME, + TEST_FILE_NAME2, + create_file, +) + +from tests.common import MockConfigEntry + + +async def test_load_unload_config_entry( + hass: HomeAssistant, mock_config_entry: MockConfigEntry, tmpdir: str +) -> None: + """Test the Filesize configuration entry loading/unloading.""" + testfile = f"{tmpdir}/file.txt" + create_file(testfile) + hass.config.allowlist_external_dirs = {tmpdir} + mock_config_entry.add_to_hass(hass) + hass.config_entries.async_update_entry( + mock_config_entry, unique_id=testfile, data={CONF_FILE_PATH: testfile} + ) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert mock_config_entry.state is ConfigEntryState.LOADED + + await hass.config_entries.async_unload(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert not hass.data.get(DOMAIN) + assert mock_config_entry.state is ConfigEntryState.NOT_LOADED + + +async def test_cannot_access_file( + hass: HomeAssistant, mock_config_entry: MockConfigEntry, tmpdir: str +) -> None: + """Test that an file not exist is caught.""" + mock_config_entry.add_to_hass(hass) + testfile = f"{tmpdir}/file_not_exist.txt" + hass.config.allowlist_external_dirs = {tmpdir} + hass.config_entries.async_update_entry( + mock_config_entry, unique_id=testfile, data={CONF_FILE_PATH: testfile} + ) + + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY + + +async def test_not_valid_path_to_file( + hass: HomeAssistant, mock_config_entry: MockConfigEntry, tmpdir: str +) -> None: + """Test that an invalid path is caught.""" + testfile = f"{tmpdir}/file.txt" + create_file(testfile) + mock_config_entry.add_to_hass(hass) + hass.config_entries.async_update_entry( + mock_config_entry, unique_id=testfile, data={CONF_FILE_PATH: testfile} + ) + + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY + + +async def test_import_config( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, +) -> None: + """Test Filesize being set up from config via import.""" + create_file(TEST_FILE) + create_file(TEST_FILE2) + hass.config.allowlist_external_dirs = {TEST_DIR} + assert await async_setup_component( + hass, + SENSOR_DOMAIN, + { + SENSOR_DOMAIN: { + "platform": DOMAIN, + CONF_FILE_PATHS: [TEST_FILE, TEST_FILE2], + } + }, + ) + await hass.async_block_till_done() + + config_entries = hass.config_entries.async_entries(DOMAIN) + assert len(config_entries) == 2 + + entry = config_entries[0] + assert entry.title == TEST_FILE_NAME + assert entry.unique_id == TEST_FILE + assert entry.data == {CONF_FILE_PATH: TEST_FILE} + entry2 = config_entries[1] + assert entry2.title == TEST_FILE_NAME2 + assert entry2.unique_id == TEST_FILE2 + assert entry2.data == {CONF_FILE_PATH: TEST_FILE2} diff --git a/tests/components/filesize/test_sensor.py b/tests/components/filesize/test_sensor.py index fa85ce41437..6f21119f95f 100644 --- a/tests/components/filesize/test_sensor.py +++ b/tests/components/filesize/test_sensor.py @@ -1,130 +1,93 @@ """The tests for the filesize sensor.""" import os -from unittest.mock import patch -import pytest - -from homeassistant import config as hass_config -from homeassistant.components.filesize import DOMAIN -from homeassistant.components.filesize.sensor import CONF_FILE_PATHS -from homeassistant.const import SERVICE_RELOAD, STATE_UNKNOWN +from homeassistant.components.filesize.const import DOMAIN +from homeassistant.const import CONF_FILE_PATH, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_component import async_update_entity from homeassistant.setup import async_setup_component -from tests.common import get_fixture_path +from . import TEST_FILE, TEST_FILE_NAME, create_file -TEST_DIR = os.path.join(os.path.dirname(__file__)) -TEST_FILE = os.path.join(TEST_DIR, "mock_file_test_filesize.txt") +from tests.common import MockConfigEntry -def create_file(path) -> None: - """Create a test file.""" - with open(path, "w") as test_file: - test_file.write("test") - - -@pytest.fixture(autouse=True) -def remove_file() -> None: - """Remove test file.""" - yield - if os.path.isfile(TEST_FILE): - os.remove(TEST_FILE) - - -async def test_invalid_path(hass: HomeAssistant) -> None: +async def test_invalid_path( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> None: """Test that an invalid path is caught.""" - config = {"sensor": {"platform": "filesize", CONF_FILE_PATHS: ["invalid_path"]}} - assert await async_setup_component(hass, "sensor", config) - await hass.async_block_till_done() - assert len(hass.states.async_entity_ids("sensor")) == 0 + mock_config_entry.add_to_hass(hass) + hass.config_entries.async_update_entry( + mock_config_entry, unique_id=TEST_FILE, data={CONF_FILE_PATH: TEST_FILE} + ) + + state = hass.states.get("sensor." + TEST_FILE_NAME) + assert not state -async def test_cannot_access_file(hass: HomeAssistant) -> None: - """Test that an invalid path is caught.""" - config = {"sensor": {"platform": "filesize", CONF_FILE_PATHS: [TEST_FILE]}} - - with patch( - "homeassistant.components.filesize.sensor.pathlib", - side_effect=OSError("Can not access"), - ): - assert await async_setup_component(hass, "sensor", config) - await hass.async_block_till_done() - - assert len(hass.states.async_entity_ids("sensor")) == 0 - - -async def test_valid_path(hass: HomeAssistant) -> None: +async def test_valid_path( + hass: HomeAssistant, tmpdir: str, mock_config_entry: MockConfigEntry +) -> None: """Test for a valid path.""" - create_file(TEST_FILE) - config = {"sensor": {"platform": "filesize", CONF_FILE_PATHS: [TEST_FILE]}} - hass.config.allowlist_external_dirs = {TEST_DIR} - assert await async_setup_component(hass, "sensor", config) + testfile = f"{tmpdir}/file.txt" + create_file(testfile) + hass.config.allowlist_external_dirs = {tmpdir} + mock_config_entry.add_to_hass(hass) + hass.config_entries.async_update_entry( + mock_config_entry, unique_id=testfile, data={CONF_FILE_PATH: testfile} + ) + + await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() - assert len(hass.states.async_entity_ids("sensor")) == 1 - state = hass.states.get("sensor.mock_file_test_filesize_txt") + + state = hass.states.get("sensor.file_txt_size") + assert state assert state.state == "0.0" - assert state.attributes.get("bytes") == 4 - - -async def test_state_unknown(hass: HomeAssistant, tmpdir: str) -> None: - """Verify we handle state unavailable.""" - create_file(TEST_FILE) - testfile = f"{tmpdir}/file" - await hass.async_add_executor_job(create_file, testfile) - with patch.object(hass.config, "is_allowed_path", return_value=True): - await async_setup_component( - hass, - "sensor", - { - "sensor": { - "platform": "filesize", - "file_paths": [testfile], - } - }, - ) - await hass.async_block_till_done() - - assert hass.states.get("sensor.file") await hass.async_add_executor_job(os.remove, testfile) - await async_update_entity(hass, "sensor.file") - - state = hass.states.get("sensor.file") - assert state.state == STATE_UNKNOWN -async def test_reload(hass: HomeAssistant, tmpdir: str) -> None: - """Verify we can reload filesize sensors.""" - testfile = f"{tmpdir}/file" - await hass.async_add_executor_job(create_file, testfile) - with patch.object(hass.config, "is_allowed_path", return_value=True): - await async_setup_component( - hass, - "sensor", - { - "sensor": { - "platform": "filesize", - "file_paths": [testfile], - } - }, - ) - await hass.async_block_till_done() +async def test_state_unavailable( + hass: HomeAssistant, tmpdir: str, mock_config_entry: MockConfigEntry +) -> None: + """Verify we handle state unavailable.""" + testfile = f"{tmpdir}/file.txt" + create_file(testfile) + hass.config.allowlist_external_dirs = {tmpdir} + mock_config_entry.add_to_hass(hass) + hass.config_entries.async_update_entry( + mock_config_entry, unique_id=testfile, data={CONF_FILE_PATH: testfile} + ) - assert len(hass.states.async_all()) == 1 + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() - assert hass.states.get("sensor.file") + state = hass.states.get("sensor.file_txt_size") + assert state + assert state.state == "0.0" - yaml_path = get_fixture_path("configuration.yaml", "filesize") - with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path), patch.object( - hass.config, "is_allowed_path", return_value=True - ): - await hass.services.async_call( - DOMAIN, - SERVICE_RELOAD, - {}, - blocking=True, - ) - await hass.async_block_till_done() + await hass.async_add_executor_job(os.remove, testfile) + await async_update_entity(hass, "sensor.file_txt_size") - assert hass.states.get("sensor.file") is None + state = hass.states.get("sensor.file_txt_size") + assert state.state == STATE_UNAVAILABLE + + +async def test_import_query(hass: HomeAssistant, tmpdir: str) -> None: + """Test import from yaml.""" + testfile = f"{tmpdir}/file.txt" + create_file(testfile) + hass.config.allowlist_external_dirs = {tmpdir} + config = { + "sensor": { + "platform": "filesize", + "file_paths": [testfile], + } + } + + assert await async_setup_component(hass, "sensor", config) + await hass.async_block_till_done() + + assert hass.config_entries.async_entries(DOMAIN) + data = hass.config_entries.async_entries(DOMAIN)[0].data + assert data[CONF_FILE_PATH] == testfile diff --git a/tests/components/flo/fixtures/device_info_response_detector.json b/tests/components/flo/fixtures/device_info_response_detector.json index aac24ab5e68..02aa75f923d 100644 --- a/tests/components/flo/fixtures/device_info_response_detector.json +++ b/tests/components/flo/fixtures/device_info_response_detector.json @@ -1,161 +1,161 @@ { - "actionRules": [], + "actionRules": [], + "battery": { + "level": 100, + "updated": "2021-03-01T12:05:00Z" + }, + "connectivity": { + "ssid": "SOMESSID" + }, + "deviceModel": "puck_v1", + "deviceType": "puck_oem", + "fwProperties": { + "alert_battery_active": false, + "alert_humidity_high_active": false, + "alert_humidity_high_count": 0, + "alert_humidity_low_active": false, + "alert_humidity_low_count": 1, + "alert_state": "inactive", + "alert_temperature_high_active": false, + "alert_temperature_high_count": 0, + "alert_temperature_low_active": false, + "alert_temperature_low_count": 0, + "alert_water_active": false, + "alert_water_count": 0, + "ap_mode_count": 1, + "beep_pattern": "off", + "button_click_count": 1, + "date": "2021-03-07T14:00:05.054Z", + "deep_sleep_count": 8229, + "device_boot_count": 25, + "device_boot_reason": "wakeup_timer", + "device_count": 8230, + "device_failed_count": 36, + "device_id": "1a2b3c4d5e6f", + "device_time_total": 405336, + "device_time_up": 1502, + "device_uuid": "32839", + "device_wakeup_count": 8254, + "flosense_shut_off_level": 2, + "fw_name": "1.1.15", + "fw_version": 10115, + "led_pattern": "led_blue_solid", + "limit_ota_battery_min": 30, + "pairing_state": "configured", + "reason": "heartbeat", + "serial_number": "111111111112", + "telemetry_battery_percent": 100, + "telemetry_battery_voltage": 2.9896278381347656, + "telemetry_count": 8224, + "telemetry_failed_count": 27, + "telemetry_humidity": 43.21965408325195, + "telemetry_rssi": 100, + "telemetry_temperature": 61.43144607543945, + "telemetry_water": false, + "timer_alarm_active": 10, + "timer_heartbeat_battery_low": 3600, + "timer_heartbeat_battery_ok": 1740, + "timer_heartbeat_last": 1740, + "timer_heartbeat_not_configured": 10, + "timer_heartbeat_retry_attempts": 3, + "timer_heartbeat_retry_delay": 600, + "timer_water_debounce": 2000, + "timer_wifi_ap_timeout": 600000, + "wifi_ap_ssid": "FloDetector-a123", + "wifi_sta_enc": "wpa2-psk", + "wifi_sta_failed_count": 21, + "wifi_sta_mac": "50:01:01:01:01:44", + "wifi_sta_ssid": "SOMESSID" + }, + "fwVersion": "1.1.15", + "hardwareThresholds": { "battery": { - "level": 100, - "updated": "2021-03-01T12:05:00Z" + "maxValue": 100, + "minValue": 0, + "okMax": 100, + "okMin": 20 }, - "connectivity": { - "ssid": "SOMESSID" + "batteryEnabled": true, + "humidity": { + "maxValue": 100, + "minValue": 0, + "okMax": 85, + "okMin": 15 }, - "deviceModel": "puck_v1", - "deviceType": "puck_oem", - "fwProperties": { - "alert_battery_active": false, - "alert_humidity_high_active": false, - "alert_humidity_high_count": 0, - "alert_humidity_low_active": false, - "alert_humidity_low_count": 1, - "alert_state": "inactive", - "alert_temperature_high_active": false, - "alert_temperature_high_count": 0, - "alert_temperature_low_active": false, - "alert_temperature_low_count": 0, - "alert_water_active": false, - "alert_water_count": 0, - "ap_mode_count": 1, - "beep_pattern": "off", - "button_click_count": 1, - "date": "2021-03-07T14:00:05.054Z", - "deep_sleep_count": 8229, - "device_boot_count": 25, - "device_boot_reason": "wakeup_timer", - "device_count": 8230, - "device_failed_count": 36, - "device_id": "1a2b3c4d5e6f", - "device_time_total": 405336, - "device_time_up": 1502, - "device_uuid": "32839", - "device_wakeup_count": 8254, - "flosense_shut_off_level": 2, - "fw_name": "1.1.15", - "fw_version": 10115, - "led_pattern": "led_blue_solid", - "limit_ota_battery_min": 30, - "pairing_state": "configured", - "reason": "heartbeat", - "serial_number": "111111111112", - "telemetry_battery_percent": 100, - "telemetry_battery_voltage": 2.9896278381347656, - "telemetry_count": 8224, - "telemetry_failed_count": 27, - "telemetry_humidity": 43.21965408325195, - "telemetry_rssi": 100, - "telemetry_temperature": 61.43144607543945, - "telemetry_water": false, - "timer_alarm_active": 10, - "timer_heartbeat_battery_low": 3600, - "timer_heartbeat_battery_ok": 1740, - "timer_heartbeat_last": 1740, - "timer_heartbeat_not_configured": 10, - "timer_heartbeat_retry_attempts": 3, - "timer_heartbeat_retry_delay": 600, - "timer_water_debounce": 2000, - "timer_wifi_ap_timeout": 600000, - "wifi_ap_ssid": "FloDetector-a123", - "wifi_sta_enc": "wpa2-psk", - "wifi_sta_failed_count": 21, - "wifi_sta_mac": "50:01:01:01:01:44", - "wifi_sta_ssid": "SOMESSID" + "humidityEnabled": true, + "tempC": { + "maxValue": 60, + "minValue": -17.77777777777778, + "okMax": 37.77777777777778, + "okMin": 0 }, - "fwVersion": "1.1.15", - "hardwareThresholds": { - "battery": { - "maxValue": 100, - "minValue": 0, - "okMax": 100, - "okMin": 20 - }, - "batteryEnabled": true, - "humidity": { - "maxValue": 100, - "minValue": 0, - "okMax": 85, - "okMin": 15 - }, - "humidityEnabled": true, - "tempC": { - "maxValue": 60, - "minValue": -17.77777777777778, - "okMax": 37.77777777777778, - "okMin": 0 - }, - "tempEnabled": true, - "tempF": { - "maxValue": 140, - "minValue": 0, - "okMax": 100, - "okMin": 32 + "tempEnabled": true, + "tempF": { + "maxValue": 140, + "minValue": 0, + "okMax": 100, + "okMin": 32 + } + }, + "id": "32839", + "installStatus": { + "isInstalled": false + }, + "isConnected": true, + "isPaired": true, + "lastHeardFromTime": "2021-03-07T14:05:00Z", + "location": { + "id": "mmnnoopp" + }, + "macAddress": "1a2b3c4d5e6f", + "nickname": "Kitchen Sink", + "notifications": { + "pending": { + "alarmCount": [], + "critical": { + "count": 0, + "devices": { + "absolute": 0, + "count": 0 } - }, - "id": "32839", - "installStatus": { - "isInstalled": false - }, - "isConnected": true, - "isPaired": true, - "lastHeardFromTime": "2021-03-07T14:05:00Z", - "location": { - "id": "mmnnoopp" - }, - "macAddress": "1a2b3c4d5e6f", - "nickname": "Kitchen Sink", - "notifications": { - "pending": { - "alarmCount": [], - "critical": { - "count": 0, - "devices": { - "absolute": 0, - "count": 0 - } - }, - "criticalCount": 0, - "info": { - "count": 0, - "devices": { - "absolute": 0, - "count": 0 - } - }, - "infoCount": 0, - "warning": { - "count": 0, - "devices": { - "absolute": 0, - "count": 0 - } - }, - "warningCount": 0 + }, + "criticalCount": 0, + "info": { + "count": 0, + "devices": { + "absolute": 0, + "count": 0 } - }, - "puckConfig": { - "configuredAt": "2020-09-01T18:15:12.216Z", - "isConfigured": true - }, - "serialNumber": "111111111112", - "shutoff": { - "scheduledAt": "1970-01-01T00:00:00.000Z" - }, - "systemMode": { - "isLocked": false, - "shouldInherit": true - }, - "telemetry": { - "current": { - "humidity": 43, - "tempF": 61, - "updated": "2021-03-07T14:05:00Z" + }, + "infoCount": 0, + "warning": { + "count": 0, + "devices": { + "absolute": 0, + "count": 0 } - }, - "valve": {} + }, + "warningCount": 0 + } + }, + "puckConfig": { + "configuredAt": "2020-09-01T18:15:12.216Z", + "isConfigured": true + }, + "serialNumber": "111111111112", + "shutoff": { + "scheduledAt": "1970-01-01T00:00:00.000Z" + }, + "systemMode": { + "isLocked": false, + "shouldInherit": true + }, + "telemetry": { + "current": { + "humidity": 43, + "tempF": 61, + "updated": "2021-03-07T14:05:00Z" + } + }, + "valve": {} } diff --git a/tests/components/flo/fixtures/location_info_base_response.json b/tests/components/flo/fixtures/location_info_base_response.json index a5a25da2d6c..74f709f89c8 100644 --- a/tests/components/flo/fixtures/location_info_base_response.json +++ b/tests/components/flo/fixtures/location_info_base_response.json @@ -18,9 +18,7 @@ "userRoles": [ { "userId": "12345abcde", - "roles": [ - "owner" - ] + "roles": ["owner"] } ], "address": "123 Main Street", @@ -45,9 +43,7 @@ "waterShutoffKnown": "unsure", "indoorAmenities": [], "outdoorAmenities": [], - "plumbingAppliances": [ - "exp_tank" - ], + "plumbingAppliances": ["exp_tank"], "notifications": { "pending": { "infoCount": 0, diff --git a/tests/components/flo/fixtures/location_info_expand_devices_response.json b/tests/components/flo/fixtures/location_info_expand_devices_response.json index 138de88db25..cee83d6d706 100644 --- a/tests/components/flo/fixtures/location_info_expand_devices_response.json +++ b/tests/components/flo/fixtures/location_info_expand_devices_response.json @@ -233,9 +233,7 @@ "userRoles": [ { "userId": "12345abcde", - "roles": [ - "owner" - ] + "roles": ["owner"] } ], "address": "123 Main Street", @@ -260,9 +258,7 @@ "waterShutoffKnown": "unsure", "indoorAmenities": [], "outdoorAmenities": [], - "plumbingAppliances": [ - "exp_tank" - ], + "plumbingAppliances": ["exp_tank"], "notifications": { "pending": { "infoCount": 0, diff --git a/tests/components/flo/fixtures/user_info_base_response.json b/tests/components/flo/fixtures/user_info_base_response.json index 646b62ee834..6f8bd415524 100644 --- a/tests/components/flo/fixtures/user_info_base_response.json +++ b/tests/components/flo/fixtures/user_info_base_response.json @@ -16,16 +16,12 @@ "locationRoles": [ { "locationId": "mmnnoopp", - "roles": [ - "owner" - ] + "roles": ["owner"] } ], "accountRole": { "accountId": "aabbccdd", - "roles": [ - "owner" - ] + "roles": ["owner"] }, "account": { "id": "aabbccdd" diff --git a/tests/components/flo/fixtures/user_info_expand_locations_response.json b/tests/components/flo/fixtures/user_info_expand_locations_response.json index 18643e049ba..8718dc59eb7 100644 --- a/tests/components/flo/fixtures/user_info_expand_locations_response.json +++ b/tests/components/flo/fixtures/user_info_expand_locations_response.json @@ -28,9 +28,7 @@ "userRoles": [ { "userId": "12345abcde", - "roles": [ - "owner" - ] + "roles": ["owner"] } ], "address": "123 Main Stree", @@ -55,9 +53,7 @@ "waterShutoffKnown": "unsure", "indoorAmenities": [], "outdoorAmenities": [], - "plumbingAppliances": [ - "exp_tank" - ], + "plumbingAppliances": ["exp_tank"], "notifications": { "pending": { "infoCount": 0, @@ -106,16 +102,12 @@ "locationRoles": [ { "locationId": "mmnnoopp", - "roles": [ - "owner" - ] + "roles": ["owner"] } ], "accountRole": { "accountId": "aabbccdd", - "roles": [ - "owner" - ] + "roles": ["owner"] }, "account": { "id": "aabbccdd" diff --git a/tests/components/flunearyou/fixtures/user_data.json b/tests/components/flunearyou/fixtures/user_data.json index 47d54d1c41e..79f86dfb5ab 100644 --- a/tests/components/flunearyou/fixtures/user_data.json +++ b/tests/components/flunearyou/fixtures/user_data.json @@ -48,4 +48,3 @@ "icon": "1" } ] - diff --git a/tests/components/flux/test_switch.py b/tests/components/flux/test_switch.py index 275c10c5592..19d5c064e82 100644 --- a/tests/components/flux/test_switch.py +++ b/tests/components/flux/test_switch.py @@ -23,6 +23,12 @@ from tests.common import ( ) +@pytest.fixture(autouse=True) +def set_utc(hass): + """Set timezone to UTC.""" + hass.config.set_time_zone("UTC") + + async def test_valid_config(hass): """Test configuration.""" assert await async_setup_component( diff --git a/tests/components/flux_led/__init__.py b/tests/components/flux_led/__init__.py index 65fb704e4c7..2f4e5d62dcc 100644 --- a/tests/components/flux_led/__init__.py +++ b/tests/components/flux_led/__init__.py @@ -39,6 +39,8 @@ MODEL_NUM = 0x35 MODEL = "AK001-ZJ2149" MODEL_DESCRIPTION = "Bulb RGBCW" MAC_ADDRESS = "aa:bb:cc:dd:ee:ff" +MAC_ADDRESS_ONE_OFF = "aa:bb:cc:dd:ee:fe" + FLUX_MAC_ADDRESS = "AABBCCDDEEFF" SHORT_MAC_ADDRESS = "DDEEFF" diff --git a/tests/components/flux_led/test_config_flow.py b/tests/components/flux_led/test_config_flow.py index b858f6d995a..9750600518e 100644 --- a/tests/components/flux_led/test_config_flow.py +++ b/tests/components/flux_led/test_config_flow.py @@ -12,7 +12,6 @@ from homeassistant.components.flux_led.const import ( CONF_CUSTOM_EFFECT_SPEED_PCT, CONF_CUSTOM_EFFECT_TRANSITION, CONF_MINOR_VERSION, - CONF_MODEL, CONF_MODEL_DESCRIPTION, CONF_MODEL_INFO, CONF_MODEL_NUM, @@ -23,7 +22,7 @@ from homeassistant.components.flux_led.const import ( TRANSITION_JUMP, TRANSITION_STROBE, ) -from homeassistant.const import CONF_DEVICE, CONF_HOST +from homeassistant.const import CONF_DEVICE, CONF_HOST, CONF_MODEL from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, RESULT_TYPE_FORM @@ -34,6 +33,7 @@ from . import ( FLUX_DISCOVERY_PARTIAL, IP_ADDRESS, MAC_ADDRESS, + MAC_ADDRESS_ONE_OFF, MODEL, MODEL_DESCRIPTION, MODEL_NUM, @@ -546,6 +546,7 @@ async def test_discovered_by_dhcp_partial_udp_response_fallback_tcp(hass): CONF_MODEL_NUM: MODEL_NUM, CONF_MODEL_DESCRIPTION: MODEL_DESCRIPTION, } + assert result2["title"] == "Bulb RGBCW DDEEFF" assert mock_async_setup.called assert mock_async_setup_entry.called @@ -589,6 +590,46 @@ async def test_discovered_by_dhcp_or_discovery_adds_missing_unique_id( assert config_entry.unique_id == MAC_ADDRESS +async def test_mac_address_off_by_one_updated_via_discovery(hass): + """Test the mac address is updated when its off by one from integration discovery.""" + config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: IP_ADDRESS}, unique_id=MAC_ADDRESS_ONE_OFF + ) + config_entry.add_to_hass(hass) + + with _patch_discovery(), _patch_wifibulb(): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, + data=FLUX_DISCOVERY, + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + assert config_entry.unique_id == MAC_ADDRESS + + +async def test_mac_address_off_by_one_not_updated_from_dhcp(hass): + """Test the mac address is NOT updated when its off by one from dhcp discovery.""" + config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: IP_ADDRESS}, unique_id=MAC_ADDRESS_ONE_OFF + ) + config_entry.add_to_hass(hass) + + with _patch_discovery(), _patch_wifibulb(): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=DHCP_DISCOVERY + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + assert config_entry.unique_id == MAC_ADDRESS_ONE_OFF + + @pytest.mark.parametrize( "source, data", [ diff --git a/tests/components/flux_led/test_init.py b/tests/components/flux_led/test_init.py index 489f6c932c2..b0a2c5dd33b 100644 --- a/tests/components/flux_led/test_init.py +++ b/tests/components/flux_led/test_init.py @@ -14,7 +14,12 @@ from homeassistant.components.flux_led.const import ( DOMAIN, ) from homeassistant.config_entries import ConfigEntryState -from homeassistant.const import CONF_HOST, CONF_NAME, EVENT_HOMEASSISTANT_STARTED +from homeassistant.const import ( + ATTR_FRIENDLY_NAME, + CONF_HOST, + CONF_NAME, + EVENT_HOMEASSISTANT_STARTED, +) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component @@ -26,6 +31,7 @@ from . import ( FLUX_DISCOVERY_PARTIAL, IP_ADDRESS, MAC_ADDRESS, + MAC_ADDRESS_ONE_OFF, _mocked_bulb, _patch_discovery, _patch_wifibulb, @@ -82,7 +88,9 @@ async def test_configuring_flux_led_causes_discovery_multiple_addresses( async def test_config_entry_reload(hass: HomeAssistant) -> None: """Test that a config entry can be reloaded.""" - config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=MAC_ADDRESS) + config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: IP_ADDRESS}, unique_id=MAC_ADDRESS + ) config_entry.add_to_hass(hass) with _patch_discovery(), _patch_wifibulb(): await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) @@ -126,11 +134,11 @@ async def test_config_entry_fills_unique_id_with_directed_discovery( # Only return discovery results when doing directed discovery nonlocal last_address last_address = address - return [FLUX_DISCOVERY] if address == IP_ADDRESS else [] + return [discovery] if address == IP_ADDRESS else [] def _mock_getBulbInfo(*args, **kwargs): nonlocal last_address - return [FLUX_DISCOVERY] if last_address == IP_ADDRESS else [] + return [discovery] if last_address == IP_ADDRESS else [] with patch( "homeassistant.components.flux_led.discovery.AIOBulbScanner.async_scan", @@ -149,7 +157,9 @@ async def test_config_entry_fills_unique_id_with_directed_discovery( async def test_time_sync_startup_and_next_day(hass: HomeAssistant) -> None: """Test that time is synced on startup and next day.""" - config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=MAC_ADDRESS) + config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: IP_ADDRESS}, unique_id=MAC_ADDRESS + ) config_entry.add_to_hass(hass) bulb = _mocked_bulb() with _patch_discovery(), _patch_wifibulb(device=bulb): @@ -204,3 +214,99 @@ async def test_unique_id_migrate_when_mac_discovered(hass: HomeAssistant) -> Non entity_registry.async_get("switch.bulb_rgbcw_ddeeff_remote_access").unique_id == f"{config_entry.unique_id}_remote_access" ) + + +async def test_unique_id_migrate_when_mac_discovered_via_discovery( + hass: HomeAssistant, +) -> None: + """Test unique id migrated when mac discovered via discovery and the mac address from dhcp was one off.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_REMOTE_ACCESS_HOST: "any", + CONF_REMOTE_ACCESS_ENABLED: True, + CONF_REMOTE_ACCESS_PORT: 1234, + CONF_HOST: IP_ADDRESS, + CONF_NAME: DEFAULT_ENTRY_TITLE, + }, + unique_id=MAC_ADDRESS_ONE_OFF, + ) + config_entry.add_to_hass(hass) + bulb = _mocked_bulb() + with _patch_discovery(no_device=True), _patch_wifibulb(device=bulb): + await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) + await hass.async_block_till_done() + + assert config_entry.unique_id == MAC_ADDRESS_ONE_OFF + entity_registry = er.async_get(hass) + assert ( + entity_registry.async_get("light.bulb_rgbcw_ddeeff").unique_id + == MAC_ADDRESS_ONE_OFF + ) + assert ( + entity_registry.async_get("switch.bulb_rgbcw_ddeeff_remote_access").unique_id + == f"{MAC_ADDRESS_ONE_OFF}_remote_access" + ) + + for _ in range(2): + with _patch_discovery(), _patch_wifibulb(device=bulb): + await hass.config_entries.async_reload(config_entry.entry_id) + await hass.async_block_till_done() + + assert ( + entity_registry.async_get("light.bulb_rgbcw_ddeeff").unique_id + == config_entry.unique_id + ) + assert ( + entity_registry.async_get( + "switch.bulb_rgbcw_ddeeff_remote_access" + ).unique_id + == f"{config_entry.unique_id}_remote_access" + ) + + +async def test_name_removed_when_it_matches_entry_title(hass: HomeAssistant) -> None: + """Test name is removed when it matches the entry title.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_REMOTE_ACCESS_HOST: "any", + CONF_REMOTE_ACCESS_ENABLED: True, + CONF_REMOTE_ACCESS_PORT: 1234, + CONF_HOST: IP_ADDRESS, + CONF_NAME: DEFAULT_ENTRY_TITLE, + }, + title=DEFAULT_ENTRY_TITLE, + ) + config_entry.add_to_hass(hass) + with _patch_discovery(), _patch_wifibulb(): + await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) + await hass.async_block_till_done() + assert CONF_NAME not in config_entry.data + + +async def test_entry_is_reloaded_when_title_changes(hass: HomeAssistant) -> None: + """Test the entry gets reloaded when the title changes.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_REMOTE_ACCESS_HOST: "any", + CONF_REMOTE_ACCESS_ENABLED: True, + CONF_REMOTE_ACCESS_PORT: 1234, + CONF_HOST: IP_ADDRESS, + }, + title=DEFAULT_ENTRY_TITLE, + ) + config_entry.add_to_hass(hass) + with _patch_discovery(), _patch_wifibulb(): + await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) + await hass.async_block_till_done() + + hass.config_entries.async_update_entry(config_entry, title="Shop Light") + assert config_entry.title == "Shop Light" + await hass.async_block_till_done() + + assert ( + hass.states.get("light.bulb_rgbcw_ddeeff").attributes[ATTR_FRIENDLY_NAME] + == "Shop Light" + ) diff --git a/tests/components/foobot/fixtures/data.json b/tests/components/foobot/fixtures/data.json index 93518614c42..a1a2703a239 100644 --- a/tests/components/foobot/fixtures/data.json +++ b/tests/components/foobot/fixtures/data.json @@ -1,34 +1,10 @@ { - "uuid": "32463564765421243", - "start": 1518134963, - "end": 1518134963, - "sensors": [ - "time", - "pm", - "tmp", - "hum", - "co2", - "voc", - "allpollu" - ], - "units": [ - "s", - "ugm3", - "C", - "pc", - "ppm", - "ppb", - "%" - ], - "datapoints": [ - [ - 1518134963, - 144.76668, - 21.064333, - 49.474, - 1232.0, - 340.66666, - 138.93651 - ] - ] + "uuid": "32463564765421243", + "start": 1518134963, + "end": 1518134963, + "sensors": ["time", "pm", "tmp", "hum", "co2", "voc", "allpollu"], + "units": ["s", "ugm3", "C", "pc", "ppm", "ppb", "%"], + "datapoints": [ + [1518134963, 144.76668, 21.064333, 49.474, 1232.0, 340.66666, 138.93651] + ] } diff --git a/tests/components/foobot/fixtures/devices.json b/tests/components/foobot/fixtures/devices.json index fffc8e151cc..e53794f75a6 100644 --- a/tests/components/foobot/fixtures/devices.json +++ b/tests/components/foobot/fixtures/devices.json @@ -1,8 +1,8 @@ [ - { - "uuid": "231425657665645342", - "userId": 6545342, - "mac": "A2D3F1", - "name": "Happybot" - } + { + "uuid": "231425657665645342", + "userId": 6545342, + "mac": "A2D3F1", + "name": "Happybot" + } ] diff --git a/tests/components/forecast_solar/conftest.py b/tests/components/forecast_solar/conftest.py index 6e1adc14b7f..31256c95866 100644 --- a/tests/components/forecast_solar/conftest.py +++ b/tests/components/forecast_solar/conftest.py @@ -11,6 +11,7 @@ from homeassistant.components.forecast_solar.const import ( CONF_AZIMUTH, CONF_DAMPING, CONF_DECLINATION, + CONF_INVERTER_SIZE, CONF_MODULES_POWER, DOMAIN, ) @@ -38,13 +39,17 @@ def mock_config_entry() -> MockConfigEntry: CONF_AZIMUTH: 190, CONF_MODULES_POWER: 5100, CONF_DAMPING: 0.5, + CONF_INVERTER_SIZE: 2000, }, ) @pytest.fixture -def mock_forecast_solar() -> Generator[None, MagicMock, None]: - """Return a mocked Forecast.Solar client.""" +def mock_forecast_solar(hass) -> Generator[None, MagicMock, None]: + """Return a mocked Forecast.Solar client. + + hass fixture included because it sets the time zone. + """ with patch( "homeassistant.components.forecast_solar.ForecastSolar", autospec=True ) as forecast_solar_mock: @@ -54,6 +59,7 @@ def mock_forecast_solar() -> Generator[None, MagicMock, None]: estimate = MagicMock(spec=models.Estimate) estimate.now.return_value = now estimate.timezone = "Europe/Amsterdam" + estimate.api_rate_limit = 60 estimate.account_type.value = "public" estimate.energy_production_today = 100000 estimate.energy_production_tomorrow = 200000 @@ -75,6 +81,18 @@ def mock_forecast_solar() -> Generator[None, MagicMock, None]: estimate.sum_energy_production.side_effect = { 1: 900000, }.get + estimate.watts = { + datetime(2021, 6, 27, 13, 0, tzinfo=dt_util.DEFAULT_TIME_ZONE): 10, + datetime(2022, 6, 27, 13, 0, tzinfo=dt_util.DEFAULT_TIME_ZONE): 100, + } + estimate.wh_days = { + datetime(2021, 6, 27, 13, 0, tzinfo=dt_util.DEFAULT_TIME_ZONE): 20, + datetime(2022, 6, 27, 13, 0, tzinfo=dt_util.DEFAULT_TIME_ZONE): 200, + } + estimate.wh_hours = { + datetime(2021, 6, 27, 13, 0, tzinfo=dt_util.DEFAULT_TIME_ZONE): 30, + datetime(2022, 6, 27, 13, 0, tzinfo=dt_util.DEFAULT_TIME_ZONE): 300, + } forecast_solar.estimate.return_value = estimate yield forecast_solar diff --git a/tests/components/forecast_solar/test_config_flow.py b/tests/components/forecast_solar/test_config_flow.py index d175be986ca..f0611d1678d 100644 --- a/tests/components/forecast_solar/test_config_flow.py +++ b/tests/components/forecast_solar/test_config_flow.py @@ -5,6 +5,7 @@ from homeassistant.components.forecast_solar.const import ( CONF_AZIMUTH, CONF_DAMPING, CONF_DECLINATION, + CONF_INVERTER_SIZE, CONF_MODULES_POWER, DOMAIN, ) @@ -81,6 +82,7 @@ async def test_options_flow( CONF_AZIMUTH: 22, CONF_MODULES_POWER: 2122, CONF_DAMPING: 0.25, + CONF_INVERTER_SIZE: 2000, }, ) @@ -91,4 +93,5 @@ async def test_options_flow( CONF_AZIMUTH: 22, CONF_MODULES_POWER: 2122, CONF_DAMPING: 0.25, + CONF_INVERTER_SIZE: 2000, } diff --git a/tests/components/forecast_solar/test_diagnostics.py b/tests/components/forecast_solar/test_diagnostics.py new file mode 100644 index 00000000000..3c388951009 --- /dev/null +++ b/tests/components/forecast_solar/test_diagnostics.py @@ -0,0 +1,58 @@ +"""Tests for the diagnostics data provided by the Forecast.Solar integration.""" +from aiohttp import ClientSession + +from homeassistant.components.diagnostics import REDACTED +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry +from tests.components.diagnostics import get_diagnostics_for_config_entry + + +async def test_diagnostics( + hass: HomeAssistant, + hass_client: ClientSession, + init_integration: MockConfigEntry, +): + """Test diagnostics.""" + assert await get_diagnostics_for_config_entry( + hass, hass_client, init_integration + ) == { + "entry": { + "title": "Green House", + "data": { + "latitude": REDACTED, + "longitude": REDACTED, + }, + "options": { + "api_key": REDACTED, + "declination": 30, + "azimuth": 190, + "modules power": 5100, + "damping": 0.5, + "inverter_size": 2000, + }, + }, + "data": { + "energy_production_today": 100000, + "energy_production_tomorrow": 200000, + "energy_current_hour": 800000, + "power_production_now": 300000, + "watts": { + "2021-06-27T13:00:00-07:00": 10, + "2022-06-27T13:00:00-07:00": 100, + }, + "wh_days": { + "2021-06-27T13:00:00-07:00": 20, + "2022-06-27T13:00:00-07:00": 200, + }, + "wh_hours": { + "2021-06-27T13:00:00-07:00": 30, + "2022-06-27T13:00:00-07:00": 300, + }, + }, + "account": { + "type": "public", + "rate_limit": 60, + "timezone": "Europe/Amsterdam", + }, + } diff --git a/tests/components/forecast_solar/test_sensor.py b/tests/components/forecast_solar/test_sensor.py index ee8afe5794b..a05acb5bc16 100644 --- a/tests/components/forecast_solar/test_sensor.py +++ b/tests/components/forecast_solar/test_sensor.py @@ -68,7 +68,7 @@ async def test_sensors( assert entry assert state assert entry.unique_id == f"{entry_id}_power_highest_peak_time_today" - assert state.state == "2021-06-27T13:00:00+00:00" + assert state.state == "2021-06-27T20:00:00+00:00" # Timestamp sensor is UTC assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Highest Power Peak Time - Today" assert state.attributes.get(ATTR_STATE_CLASS) is None assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TIMESTAMP @@ -80,7 +80,7 @@ async def test_sensors( assert entry assert state assert entry.unique_id == f"{entry_id}_power_highest_peak_time_tomorrow" - assert state.state == "2021-06-27T14:00:00+00:00" + assert state.state == "2021-06-27T21:00:00+00:00" # Timestamp sensor is UTC assert ( state.attributes.get(ATTR_FRIENDLY_NAME) == "Highest Power Peak Time - Tomorrow" ) diff --git a/tests/components/forked_daapd/test_media_player.py b/tests/components/forked_daapd/test_media_player.py index a2e0050c3d9..c18b8df3f1c 100644 --- a/tests/components/forked_daapd/test_media_player.py +++ b/tests/components/forked_daapd/test_media_player.py @@ -557,7 +557,7 @@ async def test_async_play_media_from_paused(hass, mock_api_object): SERVICE_PLAY_MEDIA, { ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MUSIC, - ATTR_MEDIA_CONTENT_ID: "somefile.mp3", + ATTR_MEDIA_CONTENT_ID: "http://example.com/somefile.mp3", }, ) state = hass.states.get(TEST_MASTER_ENTITY_NAME) @@ -581,7 +581,7 @@ async def test_async_play_media_from_stopped( SERVICE_PLAY_MEDIA, { ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MUSIC, - ATTR_MEDIA_CONTENT_ID: "somefile.mp3", + ATTR_MEDIA_CONTENT_ID: "http://example.com/somefile.mp3", }, ) state = hass.states.get(TEST_MASTER_ENTITY_NAME) @@ -616,7 +616,7 @@ async def test_async_play_media_tts_timeout(hass, mock_api_object): SERVICE_PLAY_MEDIA, { ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MUSIC, - ATTR_MEDIA_CONTENT_ID: "somefile.mp3", + ATTR_MEDIA_CONTENT_ID: "http://example.com/somefile.mp3", }, ) state = hass.states.get(TEST_MASTER_ENTITY_NAME) @@ -725,7 +725,7 @@ async def test_librespot_java_play_media(hass, pipe_control_api_object): SERVICE_PLAY_MEDIA, { ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MUSIC, - ATTR_MEDIA_CONTENT_ID: "somefile.mp3", + ATTR_MEDIA_CONTENT_ID: "http://example.com/somefile.mp3", }, ) state = hass.states.get(TEST_MASTER_ENTITY_NAME) @@ -747,7 +747,7 @@ async def test_librespot_java_play_media_pause_timeout(hass, pipe_control_api_ob SERVICE_PLAY_MEDIA, { ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MUSIC, - ATTR_MEDIA_CONTENT_ID: "somefile.mp3", + ATTR_MEDIA_CONTENT_ID: "http://example.com/somefile.mp3", }, ) state = hass.states.get(TEST_MASTER_ENTITY_NAME) diff --git a/tests/components/freedompro/test_cover.py b/tests/components/freedompro/test_cover.py index 11dc35b374c..ed14da90789 100644 --- a/tests/components/freedompro/test_cover.py +++ b/tests/components/freedompro/test_cover.py @@ -14,6 +14,7 @@ from homeassistant.const import ( STATE_OPEN, ) from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers.entity_component import async_update_entity from homeassistant.util.dt import utcnow from tests.common import async_fire_time_changed @@ -149,7 +150,7 @@ async def test_cover_close( "homeassistant.components.freedompro.get_states", return_value=states_response, ): - await hass.helpers.entity_component.async_update_entity(entity_id) + await async_update_entity(hass, entity_id) async_fire_time_changed(hass, utcnow() + timedelta(hours=2)) await hass.async_block_till_done() diff --git a/tests/components/freedompro/test_fan.py b/tests/components/freedompro/test_fan.py index 3404c5d17e4..3192398a323 100644 --- a/tests/components/freedompro/test_fan.py +++ b/tests/components/freedompro/test_fan.py @@ -10,6 +10,7 @@ from homeassistant.components.fan import ( ) from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, STATE_OFF, STATE_ON from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers.entity_component import async_update_entity from homeassistant.util.dt import utcnow from tests.common import async_fire_time_changed @@ -78,7 +79,7 @@ async def test_fan_set_off(hass, init_integration): "homeassistant.components.freedompro.get_states", return_value=states_response, ): - await hass.helpers.entity_component.async_update_entity(entity_id) + await async_update_entity(hass, entity_id) async_fire_time_changed(hass, utcnow() + timedelta(hours=2)) await hass.async_block_till_done() @@ -107,7 +108,7 @@ async def test_fan_set_off(hass, init_integration): "homeassistant.components.freedompro.get_states", return_value=states_response, ): - await hass.helpers.entity_component.async_update_entity(entity_id) + await async_update_entity(hass, entity_id) async_fire_time_changed(hass, utcnow() + timedelta(hours=2)) await hass.async_block_till_done() diff --git a/tests/components/freedompro/test_lock.py b/tests/components/freedompro/test_lock.py index e0d25ce91d9..44811faffe8 100644 --- a/tests/components/freedompro/test_lock.py +++ b/tests/components/freedompro/test_lock.py @@ -9,6 +9,7 @@ from homeassistant.components.lock import ( ) from homeassistant.const import ATTR_ENTITY_ID, STATE_LOCKED, STATE_UNLOCKED from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers.entity_component import async_update_entity from homeassistant.util.dt import utcnow from tests.common import async_fire_time_changed @@ -73,7 +74,7 @@ async def test_lock_set_unlock(hass, init_integration): "homeassistant.components.freedompro.get_states", return_value=states_response, ): - await hass.helpers.entity_component.async_update_entity(entity_id) + await async_update_entity(hass, entity_id) async_fire_time_changed(hass, utcnow() + timedelta(hours=2)) await hass.async_block_till_done() diff --git a/tests/components/freedompro/test_switch.py b/tests/components/freedompro/test_switch.py index 6b62e028d72..f71ec4b896f 100644 --- a/tests/components/freedompro/test_switch.py +++ b/tests/components/freedompro/test_switch.py @@ -5,6 +5,7 @@ from unittest.mock import ANY, patch from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN, SERVICE_TURN_ON from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, STATE_OFF, STATE_ON from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.entity_component import async_update_entity from homeassistant.util.dt import utcnow from tests.common import async_fire_time_changed @@ -61,7 +62,7 @@ async def test_switch_set_off(hass, init_integration): "homeassistant.components.freedompro.get_states", return_value=states_response, ): - await hass.helpers.entity_component.async_update_entity(entity_id) + await async_update_entity(hass, entity_id) async_fire_time_changed(hass, utcnow() + timedelta(hours=2)) await hass.async_block_till_done() diff --git a/tests/components/fritz/conftest.py b/tests/components/fritz/conftest.py index 139a8448b08..b073335f20a 100644 --- a/tests/components/fritz/conftest.py +++ b/tests/components/fritz/conftest.py @@ -1,6 +1,6 @@ """Common stuff for AVM Fritz!Box tests.""" import logging -from unittest.mock import patch +from unittest.mock import MagicMock, patch from fritzconnection.core.processor import Service from fritzconnection.lib.fritzhosts import FritzHosts @@ -25,7 +25,7 @@ class FritzConnectionMock: # pylint: disable=too-few-public-methods """FritzConnection mocking.""" def __init__(self, services): - """Inint Mocking class.""" + """Init Mocking class.""" self.modelname = MOCK_MODELNAME self.call_action = self._call_action self._services = services @@ -36,6 +36,13 @@ class FritzConnectionMock: # pylint: disable=too-few-public-methods LOGGER.debug("-" * 80) LOGGER.debug("FritzConnectionMock - services: %s", self.services) + def call_action_side_effect(self, side_effect=None) -> None: + """Set or unset a side_effect for call_action.""" + if side_effect is not None: + self.call_action = MagicMock(side_effect=side_effect) + else: + self.call_action = self._call_action + def _call_action(self, service: str, action: str, **kwargs): LOGGER.debug( "_call_action service: %s, action: %s, **kwargs: %s", diff --git a/tests/components/fritz/test_config_flow.py b/tests/components/fritz/test_config_flow.py index 502757c2c67..6d014782842 100644 --- a/tests/components/fritz/test_config_flow.py +++ b/tests/components/fritz/test_config_flow.py @@ -38,9 +38,9 @@ from tests.common import MockConfigEntry async def test_user(hass: HomeAssistant, fc_class_mock, mock_get_source_ip): """Test starting a flow by user.""" with patch( - "homeassistant.components.fritz.common.FritzConnection", + "homeassistant.components.fritz.config_flow.FritzConnection", side_effect=fc_class_mock, - ), patch("homeassistant.components.fritz.common.FritzStatus"), patch( + ), patch( "homeassistant.components.fritz.common.FritzBoxTools._update_device_info", return_value=MOCK_FIRMWARE_INFO, ), patch( @@ -91,9 +91,9 @@ async def test_user_already_configured( mock_config.add_to_hass(hass) with patch( - "homeassistant.components.fritz.common.FritzConnection", + "homeassistant.components.fritz.config_flow.FritzConnection", side_effect=fc_class_mock, - ), patch("homeassistant.components.fritz.common.FritzStatus"), patch( + ), patch( "homeassistant.components.fritz.common.FritzBoxTools._update_device_info", return_value=MOCK_FIRMWARE_INFO, ), patch( @@ -134,9 +134,9 @@ async def test_exception_security(hass: HomeAssistant, mock_get_source_ip): assert result["step_id"] == "user" with patch( - "homeassistant.components.fritz.common.FritzConnection", + "homeassistant.components.fritz.config_flow.FritzConnection", side_effect=FritzSecurityError, - ), patch("homeassistant.components.fritz.common.FritzStatus"): + ): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_USER_DATA @@ -157,9 +157,9 @@ async def test_exception_connection(hass: HomeAssistant, mock_get_source_ip): assert result["step_id"] == "user" with patch( - "homeassistant.components.fritz.common.FritzConnection", + "homeassistant.components.fritz.config_flow.FritzConnection", side_effect=FritzConnectionException, - ), patch("homeassistant.components.fritz.common.FritzStatus"): + ): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_USER_DATA @@ -180,9 +180,9 @@ async def test_exception_unknown(hass: HomeAssistant, mock_get_source_ip): assert result["step_id"] == "user" with patch( - "homeassistant.components.fritz.common.FritzConnection", + "homeassistant.components.fritz.config_flow.FritzConnection", side_effect=OSError, - ), patch("homeassistant.components.fritz.common.FritzStatus"): + ): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_USER_DATA @@ -202,9 +202,9 @@ async def test_reauth_successful( mock_config.add_to_hass(hass) with patch( - "homeassistant.components.fritz.common.FritzConnection", + "homeassistant.components.fritz.config_flow.FritzConnection", side_effect=fc_class_mock, - ), patch("homeassistant.components.fritz.common.FritzStatus"), patch( + ), patch( "homeassistant.components.fritz.common.FritzBoxTools._update_device_info", return_value=MOCK_FIRMWARE_INFO, ), patch( @@ -252,9 +252,9 @@ async def test_reauth_not_successful( mock_config.add_to_hass(hass) with patch( - "homeassistant.components.fritz.common.FritzConnection", + "homeassistant.components.fritz.config_flow.FritzConnection", side_effect=FritzConnectionException, - ), patch("homeassistant.components.fritz.common.FritzStatus"): + ): result = await hass.config_entries.flow.async_init( DOMAIN, @@ -291,9 +291,9 @@ async def test_ssdp_already_configured( mock_config.add_to_hass(hass) with patch( - "homeassistant.components.fritz.common.FritzConnection", + "homeassistant.components.fritz.config_flow.FritzConnection", side_effect=fc_class_mock, - ), patch("homeassistant.components.fritz.common.FritzStatus"), patch( + ), patch( "homeassistant.components.fritz.config_flow.socket.gethostbyname", return_value=MOCK_IPS["fritz.box"], ): @@ -318,9 +318,9 @@ async def test_ssdp_already_configured_host( mock_config.add_to_hass(hass) with patch( - "homeassistant.components.fritz.common.FritzConnection", + "homeassistant.components.fritz.config_flow.FritzConnection", side_effect=fc_class_mock, - ), patch("homeassistant.components.fritz.common.FritzStatus"), patch( + ), patch( "homeassistant.components.fritz.config_flow.socket.gethostbyname", return_value=MOCK_IPS["fritz.box"], ): @@ -345,9 +345,9 @@ async def test_ssdp_already_configured_host_uuid( mock_config.add_to_hass(hass) with patch( - "homeassistant.components.fritz.common.FritzConnection", + "homeassistant.components.fritz.config_flow.FritzConnection", side_effect=fc_class_mock, - ), patch("homeassistant.components.fritz.common.FritzStatus"), patch( + ), patch( "homeassistant.components.fritz.config_flow.socket.gethostbyname", return_value=MOCK_IPS["fritz.box"], ): @@ -364,9 +364,9 @@ async def test_ssdp_already_in_progress_host( ): """Test starting a flow from discovery twice.""" with patch( - "homeassistant.components.fritz.common.FritzConnection", + "homeassistant.components.fritz.config_flow.FritzConnection", side_effect=fc_class_mock, - ), patch("homeassistant.components.fritz.common.FritzStatus"): + ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA @@ -387,9 +387,9 @@ async def test_ssdp_already_in_progress_host( async def test_ssdp(hass: HomeAssistant, fc_class_mock, mock_get_source_ip): """Test starting a flow from discovery.""" with patch( - "homeassistant.components.fritz.common.FritzConnection", + "homeassistant.components.fritz.config_flow.FritzConnection", side_effect=fc_class_mock, - ), patch("homeassistant.components.fritz.common.FritzStatus"), patch( + ), patch( "homeassistant.components.fritz.common.FritzBoxTools._update_device_info", return_value=MOCK_FIRMWARE_INFO, ), patch( @@ -430,9 +430,9 @@ async def test_ssdp(hass: HomeAssistant, fc_class_mock, mock_get_source_ip): async def test_ssdp_exception(hass: HomeAssistant, mock_get_source_ip): """Test starting a flow from discovery but no device found.""" with patch( - "homeassistant.components.fritz.common.FritzConnection", + "homeassistant.components.fritz.config_flow.FritzConnection", side_effect=FritzConnectionException, - ), patch("homeassistant.components.fritz.common.FritzStatus"): + ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA @@ -459,11 +459,9 @@ async def test_options_flow(hass: HomeAssistant, fc_class_mock, mock_get_source_ mock_config.add_to_hass(hass) with patch( - "homeassistant.components.fritz.common.FritzConnection", + "homeassistant.components.fritz.config_flow.FritzConnection", side_effect=fc_class_mock, - ), patch("homeassistant.components.fritz.common.FritzStatus"), patch( - "homeassistant.components.fritz.common.FritzBoxTools" - ): + ), patch("homeassistant.components.fritz.common.FritzBoxTools"): result = await hass.config_entries.options.async_init(mock_config.entry_id) assert result["type"] == RESULT_TYPE_FORM assert result["step_id"] == "init" diff --git a/tests/components/fritz/test_sensor.py b/tests/components/fritz/test_sensor.py new file mode 100644 index 00000000000..31e142a3e47 --- /dev/null +++ b/tests/components/fritz/test_sensor.py @@ -0,0 +1,151 @@ +"""Tests for Shelly button platform.""" +from __future__ import annotations + +from datetime import timedelta +from typing import Any + +from fritzconnection.core.exceptions import FritzConnectionException + +from homeassistant.components.fritz.const import DOMAIN +from homeassistant.components.fritz.sensor import SENSOR_TYPES +from homeassistant.components.sensor import ( + ATTR_STATE_CLASS, + DEVICE_CLASS_TIMESTAMP, + DOMAIN as SENSOR_DOMAIN, + STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, +) +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_ICON, + ATTR_STATE, + ATTR_UNIT_OF_MEASUREMENT, + STATE_UNAVAILABLE, +) +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component +import homeassistant.util.dt as dt_util + +from .const import MOCK_USER_DATA + +from tests.common import MockConfigEntry, async_fire_time_changed + +SENSOR_STATES: dict[str, dict[str, Any]] = { + "sensor.mock_title_external_ip": { + ATTR_STATE: "1.2.3.4", + ATTR_ICON: "mdi:earth", + }, + "sensor.mock_title_device_uptime": { + # ATTR_STATE: "2022-02-05T17:46:04+00:00", + ATTR_DEVICE_CLASS: DEVICE_CLASS_TIMESTAMP, + }, + "sensor.mock_title_connection_uptime": { + # ATTR_STATE: "2022-03-06T11:27:16+00:00", + ATTR_DEVICE_CLASS: DEVICE_CLASS_TIMESTAMP, + }, + "sensor.mock_title_upload_throughput": { + ATTR_STATE: "3.4", + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_UNIT_OF_MEASUREMENT: "kB/s", + ATTR_ICON: "mdi:upload", + }, + "sensor.mock_title_download_throughput": { + ATTR_STATE: "67.6", + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_UNIT_OF_MEASUREMENT: "kB/s", + ATTR_ICON: "mdi:download", + }, + "sensor.mock_title_max_connection_upload_throughput": { + ATTR_STATE: "2105.0", + ATTR_UNIT_OF_MEASUREMENT: "kbit/s", + ATTR_ICON: "mdi:upload", + }, + "sensor.mock_title_max_connection_download_throughput": { + ATTR_STATE: "10087.0", + ATTR_UNIT_OF_MEASUREMENT: "kbit/s", + ATTR_ICON: "mdi:download", + }, + "sensor.mock_title_gb_sent": { + ATTR_STATE: "1.7", + ATTR_STATE_CLASS: STATE_CLASS_TOTAL_INCREASING, + ATTR_UNIT_OF_MEASUREMENT: "GB", + ATTR_ICON: "mdi:upload", + }, + "sensor.mock_title_gb_received": { + ATTR_STATE: "5.2", + ATTR_STATE_CLASS: STATE_CLASS_TOTAL_INCREASING, + ATTR_UNIT_OF_MEASUREMENT: "GB", + ATTR_ICON: "mdi:download", + }, + "sensor.mock_title_link_upload_throughput": { + ATTR_STATE: "51805.0", + ATTR_UNIT_OF_MEASUREMENT: "kbit/s", + ATTR_ICON: "mdi:upload", + }, + "sensor.mock_title_link_download_throughput": { + ATTR_STATE: "318557.0", + ATTR_UNIT_OF_MEASUREMENT: "kbit/s", + ATTR_ICON: "mdi:download", + }, + "sensor.mock_title_link_upload_noise_margin": { + ATTR_STATE: "9.0", + ATTR_UNIT_OF_MEASUREMENT: "dB", + ATTR_ICON: "mdi:upload", + }, + "sensor.mock_title_link_download_noise_margin": { + ATTR_STATE: "8.0", + ATTR_UNIT_OF_MEASUREMENT: "dB", + ATTR_ICON: "mdi:download", + }, + "sensor.mock_title_link_upload_power_attenuation": { + ATTR_STATE: "7.0", + ATTR_UNIT_OF_MEASUREMENT: "dB", + ATTR_ICON: "mdi:upload", + }, + "sensor.mock_title_link_download_power_attenuation": { + ATTR_STATE: "12.0", + ATTR_UNIT_OF_MEASUREMENT: "dB", + ATTR_ICON: "mdi:download", + }, +} + + +async def test_sensor_setup(hass: HomeAssistant, fc_class_mock, fh_class_mock): + """Test setup of Fritz!Tools sesnors.""" + + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) + entry.add_to_hass(hass) + + assert await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + assert entry.state == ConfigEntryState.LOADED + + sensors = hass.states.async_all(SENSOR_DOMAIN) + assert len(sensors) == len(SENSOR_TYPES) + + for sensor in sensors: + assert SENSOR_STATES.get(sensor.entity_id) is not None + for key, val in SENSOR_STATES[sensor.entity_id].items(): + if key == ATTR_STATE: + assert sensor.state == val + else: + assert sensor.attributes.get(key) == val + + +async def test_sensor_update_fail(hass: HomeAssistant, fc_class_mock, fh_class_mock): + """Test failed update of Fritz!Tools sesnors.""" + + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) + entry.add_to_hass(hass) + + assert await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + + fc_class_mock().call_action_side_effect(FritzConnectionException) + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=300)) + await hass.async_block_till_done() + + sensors = hass.states.async_all(SENSOR_DOMAIN) + for sensor in sensors: + assert sensor.state == STATE_UNAVAILABLE diff --git a/tests/components/fronius/fixtures/gen24/GetAPIVersion.json b/tests/components/fronius/fixtures/gen24/GetAPIVersion.json index a76e4813e5f..7a0f745b469 100644 --- a/tests/components/fronius/fixtures/gen24/GetAPIVersion.json +++ b/tests/components/fronius/fixtures/gen24/GetAPIVersion.json @@ -1,5 +1,5 @@ { - "APIVersion" : 1, - "BaseURL" : "/solar_api/v1/", - "CompatibilityRange" : "1.7-3" + "APIVersion": 1, + "BaseURL": "/solar_api/v1/", + "CompatibilityRange": "1.7-3" } diff --git a/tests/components/fronius/fixtures/gen24/GetInverterInfo.json b/tests/components/fronius/fixtures/gen24/GetInverterInfo.json index 8a20c6c806b..1d157c5d7ca 100644 --- a/tests/components/fronius/fixtures/gen24/GetInverterInfo.json +++ b/tests/components/fronius/fixtures/gen24/GetInverterInfo.json @@ -1,25 +1,25 @@ { - "Body" : { - "Data" : { - "1" : { - "CustomName" : "Inverter name", - "DT" : 1, - "ErrorCode" : 0, - "InverterState" : "Running", - "PVPower" : 9360, - "Show" : 1, - "StatusCode" : 7, - "UniqueID" : "12345678" - } + "Body": { + "Data": { + "1": { + "CustomName": "Inverter name", + "DT": 1, + "ErrorCode": 0, + "InverterState": "Running", + "PVPower": 9360, + "Show": 1, + "StatusCode": 7, + "UniqueID": "12345678" } - }, - "Head" : { - "RequestArguments" : {}, - "Status" : { - "Code" : 0, - "Reason" : "", - "UserMessage" : "" - }, - "Timestamp" : "2021-11-26T11:07:53+00:00" - } + } + }, + "Head": { + "RequestArguments": {}, + "Status": { + "Code": 0, + "Reason": "", + "UserMessage": "" + }, + "Timestamp": "2021-11-26T11:07:53+00:00" + } } diff --git a/tests/components/fronius/fixtures/gen24/GetInverterRealtimeData_Device_1.json b/tests/components/fronius/fixtures/gen24/GetInverterRealtimeData_Device_1.json index 35e7b84f452..d3c0bd79ee0 100644 --- a/tests/components/fronius/fixtures/gen24/GetInverterRealtimeData_Device_1.json +++ b/tests/components/fronius/fixtures/gen24/GetInverterRealtimeData_Device_1.json @@ -1,80 +1,80 @@ { - "Body" : { - "Data" : { - "DAY_ENERGY" : { - "Unit" : "Wh", - "Value" : null - }, - "DeviceStatus" : { - "ErrorCode" : 0, - "InverterState" : "Running", - "StatusCode" : 7 - }, - "FAC" : { - "Unit" : "Hz", - "Value" : 49.99169921875 - }, - "IAC" : { - "Unit" : "A", - "Value" : 0.15894582867622375 - }, - "IDC" : { - "Unit" : "A", - "Value" : 0.078323781490325928 - }, - "IDC_2" : { - "Unit" : "A", - "Value" : 0.075399093329906464 - }, - "IDC_3" : { - "Unit" : "A", - "Value" : null - }, - "PAC" : { - "Unit" : "W", - "Value" : 37.320449829101562 - }, - "SAC" : { - "Unit" : "VA", - "Value" : 37.40447998046875 - }, - "TOTAL_ENERGY" : { - "Unit" : "Wh", - "Value" : 1530193.4199999999 - }, - "UAC" : { - "Unit" : "V", - "Value" : 234.91676330566406 - }, - "UDC" : { - "Unit" : "V", - "Value" : 411.38107299804688 - }, - "UDC_2" : { - "Unit" : "V", - "Value" : 403.43124389648438 - }, - "UDC_3" : { - "Unit" : "V", - "Value" : null - }, - "YEAR_ENERGY" : { - "Unit" : "Wh", - "Value" : null - } + "Body": { + "Data": { + "DAY_ENERGY": { + "Unit": "Wh", + "Value": null + }, + "DeviceStatus": { + "ErrorCode": 0, + "InverterState": "Running", + "StatusCode": 7 + }, + "FAC": { + "Unit": "Hz", + "Value": 49.99169921875 + }, + "IAC": { + "Unit": "A", + "Value": 0.15894582867622375 + }, + "IDC": { + "Unit": "A", + "Value": 0.078323781490325928 + }, + "IDC_2": { + "Unit": "A", + "Value": 0.075399093329906464 + }, + "IDC_3": { + "Unit": "A", + "Value": null + }, + "PAC": { + "Unit": "W", + "Value": 37.320449829101562 + }, + "SAC": { + "Unit": "VA", + "Value": 37.40447998046875 + }, + "TOTAL_ENERGY": { + "Unit": "Wh", + "Value": 1530193.4199999999 + }, + "UAC": { + "Unit": "V", + "Value": 234.91676330566406 + }, + "UDC": { + "Unit": "V", + "Value": 411.38107299804688 + }, + "UDC_2": { + "Unit": "V", + "Value": 403.43124389648438 + }, + "UDC_3": { + "Unit": "V", + "Value": null + }, + "YEAR_ENERGY": { + "Unit": "Wh", + "Value": null } - }, - "Head" : { - "RequestArguments" : { - "DataCollection" : "CommonInverterData", - "DeviceId" : "1", - "Scope" : "Device" - }, - "Status" : { - "Code" : 0, - "Reason" : "", - "UserMessage" : "" - }, - "Timestamp" : "2021-11-26T11:07:53+00:00" - } + } + }, + "Head": { + "RequestArguments": { + "DataCollection": "CommonInverterData", + "DeviceId": "1", + "Scope": "Device" + }, + "Status": { + "Code": 0, + "Reason": "", + "UserMessage": "" + }, + "Timestamp": "2021-11-26T11:07:53+00:00" + } } diff --git a/tests/components/fronius/fixtures/gen24/GetLoggerInfo.json b/tests/components/fronius/fixtures/gen24/GetLoggerInfo.json index 103da09d9ba..1e8062c7c92 100644 --- a/tests/components/fronius/fixtures/gen24/GetLoggerInfo.json +++ b/tests/components/fronius/fixtures/gen24/GetLoggerInfo.json @@ -1,14 +1,14 @@ { - "Body" : { - "Data" : {} - }, - "Head" : { - "RequestArguments" : {}, - "Status" : { - "Code" : 11, - "Reason" : "v1/GetLoggerInfo.cgi request is not supported.", - "UserMessage" : "" - }, - "Timestamp" : "2021-11-26T11:07:53+00:00" - } + "Body": { + "Data": {} + }, + "Head": { + "RequestArguments": {}, + "Status": { + "Code": 11, + "Reason": "v1/GetLoggerInfo.cgi request is not supported.", + "UserMessage": "" + }, + "Timestamp": "2021-11-26T11:07:53+00:00" + } } diff --git a/tests/components/fronius/fixtures/gen24/GetMeterRealtimeData.json b/tests/components/fronius/fixtures/gen24/GetMeterRealtimeData.json index 2fcc208468a..cafbea5579d 100644 --- a/tests/components/fronius/fixtures/gen24/GetMeterRealtimeData.json +++ b/tests/components/fronius/fixtures/gen24/GetMeterRealtimeData.json @@ -1,61 +1,61 @@ { - "Body" : { - "Data" : { - "0" : { - "Current_AC_Phase_1" : 1.145, - "Current_AC_Phase_2" : 2.3300000000000001, - "Current_AC_Phase_3" : 1.825, - "Current_AC_Sum" : 5.2999999999999998, - "Details" : { - "Manufacturer" : "Fronius", - "Model" : "Smart Meter TS 65A-3", - "Serial" : "1234567890" - }, - "Enable" : 1, - "EnergyReactive_VArAC_Sum_Consumed" : 88221.0, - "EnergyReactive_VArAC_Sum_Produced" : 1989125.0, - "EnergyReal_WAC_Minus_Absolute" : 3863340.0, - "EnergyReal_WAC_Plus_Absolute" : 2013105.0, - "EnergyReal_WAC_Sum_Consumed" : 2013105.0, - "EnergyReal_WAC_Sum_Produced" : 3863340.0, - "Frequency_Phase_Average" : 49.899999999999999, - "Meter_Location_Current" : 0.0, - "PowerApparent_S_Phase_1" : 243.30000000000001, - "PowerApparent_S_Phase_2" : 323.39999999999998, - "PowerApparent_S_Phase_3" : 301.19999999999999, - "PowerApparent_S_Sum" : 868.0, - "PowerFactor_Phase_1" : 0.441, - "PowerFactor_Phase_2" : 0.93400000000000005, - "PowerFactor_Phase_3" : 0.83199999999999996, - "PowerFactor_Sum" : 0.82799999999999996, - "PowerReactive_Q_Phase_1" : -218.59999999999999, - "PowerReactive_Q_Phase_2" : -132.80000000000001, - "PowerReactive_Q_Phase_3" : -166.0, - "PowerReactive_Q_Sum" : -517.39999999999998, - "PowerReal_P_Phase_1" : 106.8, - "PowerReal_P_Phase_2" : 294.89999999999998, - "PowerReal_P_Phase_3" : 251.30000000000001, - "PowerReal_P_Sum" : 653.10000000000002, - "TimeStamp" : 1637924872.0, - "Visible" : 1.0, - "Voltage_AC_PhaseToPhase_12" : 408.69999999999999, - "Voltage_AC_PhaseToPhase_23" : 409.60000000000002, - "Voltage_AC_PhaseToPhase_31" : 409.39999999999998, - "Voltage_AC_Phase_1" : 235.90000000000001, - "Voltage_AC_Phase_2" : 236.09999999999999, - "Voltage_AC_Phase_3" : 236.90000000000001 - } + "Body": { + "Data": { + "0": { + "Current_AC_Phase_1": 1.145, + "Current_AC_Phase_2": 2.3300000000000001, + "Current_AC_Phase_3": 1.825, + "Current_AC_Sum": 5.2999999999999998, + "Details": { + "Manufacturer": "Fronius", + "Model": "Smart Meter TS 65A-3", + "Serial": "1234567890" + }, + "Enable": 1, + "EnergyReactive_VArAC_Sum_Consumed": 88221.0, + "EnergyReactive_VArAC_Sum_Produced": 1989125.0, + "EnergyReal_WAC_Minus_Absolute": 3863340.0, + "EnergyReal_WAC_Plus_Absolute": 2013105.0, + "EnergyReal_WAC_Sum_Consumed": 2013105.0, + "EnergyReal_WAC_Sum_Produced": 3863340.0, + "Frequency_Phase_Average": 49.899999999999999, + "Meter_Location_Current": 0.0, + "PowerApparent_S_Phase_1": 243.30000000000001, + "PowerApparent_S_Phase_2": 323.39999999999998, + "PowerApparent_S_Phase_3": 301.19999999999999, + "PowerApparent_S_Sum": 868.0, + "PowerFactor_Phase_1": 0.441, + "PowerFactor_Phase_2": 0.93400000000000005, + "PowerFactor_Phase_3": 0.83199999999999996, + "PowerFactor_Sum": 0.82799999999999996, + "PowerReactive_Q_Phase_1": -218.59999999999999, + "PowerReactive_Q_Phase_2": -132.80000000000001, + "PowerReactive_Q_Phase_3": -166.0, + "PowerReactive_Q_Sum": -517.39999999999998, + "PowerReal_P_Phase_1": 106.8, + "PowerReal_P_Phase_2": 294.89999999999998, + "PowerReal_P_Phase_3": 251.30000000000001, + "PowerReal_P_Sum": 653.10000000000002, + "TimeStamp": 1637924872.0, + "Visible": 1.0, + "Voltage_AC_PhaseToPhase_12": 408.69999999999999, + "Voltage_AC_PhaseToPhase_23": 409.60000000000002, + "Voltage_AC_PhaseToPhase_31": 409.39999999999998, + "Voltage_AC_Phase_1": 235.90000000000001, + "Voltage_AC_Phase_2": 236.09999999999999, + "Voltage_AC_Phase_3": 236.90000000000001 } - }, - "Head" : { - "RequestArguments" : { - "Scope" : "System" - }, - "Status" : { - "Code" : 0, - "Reason" : "", - "UserMessage" : "" - }, - "Timestamp" : "2021-11-26T11:07:52+00:00" - } + } + }, + "Head": { + "RequestArguments": { + "Scope": "System" + }, + "Status": { + "Code": 0, + "Reason": "", + "UserMessage": "" + }, + "Timestamp": "2021-11-26T11:07:52+00:00" + } } diff --git a/tests/components/fronius/fixtures/gen24/GetOhmPilotRealtimeData.json b/tests/components/fronius/fixtures/gen24/GetOhmPilotRealtimeData.json index 87d53b13d8b..81fda80830d 100644 --- a/tests/components/fronius/fixtures/gen24/GetOhmPilotRealtimeData.json +++ b/tests/components/fronius/fixtures/gen24/GetOhmPilotRealtimeData.json @@ -1,16 +1,16 @@ { - "Body" : { - "Data" : {} - }, - "Head" : { - "RequestArguments" : { - "Scope" : "System" - }, - "Status" : { - "Code" : 0, - "Reason" : "", - "UserMessage" : "" - }, - "Timestamp" : "2021-11-26T11:07:53+00:00" - } + "Body": { + "Data": {} + }, + "Head": { + "RequestArguments": { + "Scope": "System" + }, + "Status": { + "Code": 0, + "Reason": "", + "UserMessage": "" + }, + "Timestamp": "2021-11-26T11:07:53+00:00" + } } diff --git a/tests/components/fronius/fixtures/gen24/GetPowerFlowRealtimeData.json b/tests/components/fronius/fixtures/gen24/GetPowerFlowRealtimeData.json index 86fd9f12aff..83914552ee8 100644 --- a/tests/components/fronius/fixtures/gen24/GetPowerFlowRealtimeData.json +++ b/tests/components/fronius/fixtures/gen24/GetPowerFlowRealtimeData.json @@ -1,45 +1,45 @@ { - "Body" : { - "Data" : { - "Inverters" : { - "1" : { - "Battery_Mode" : "disabled", - "DT" : 1, - "E_Day" : null, - "E_Total" : 1530193.4199999999, - "E_Year" : null, - "P" : 37.320449829101562, - "SOC" : 0.0 - } - }, - "Site" : { - "BackupMode" : false, - "BatteryStandby" : false, - "E_Day" : null, - "E_Total" : 1530193.4199999999, - "E_Year" : null, - "Meter_Location" : "grid", - "Mode" : "meter", - "P_Akku" : null, - "P_Grid" : 658.39999999999998, - "P_Load" : -695.68274917602537, - "P_PV" : 62.948148727416992, - "rel_Autonomy" : 5.3591596485874495, - "rel_SelfConsumption" : 100.0 - }, - "Smartloads" : { - "Ohmpilots" : {} - }, - "Version" : "12" - } - }, - "Head" : { - "RequestArguments" : {}, - "Status" : { - "Code" : 0, - "Reason" : "", - "UserMessage" : "" + "Body": { + "Data": { + "Inverters": { + "1": { + "Battery_Mode": "disabled", + "DT": 1, + "E_Day": null, + "E_Total": 1530193.4199999999, + "E_Year": null, + "P": 37.320449829101562, + "SOC": 0.0 + } }, - "Timestamp" : "2021-11-26T11:07:53+00:00" - } + "Site": { + "BackupMode": false, + "BatteryStandby": false, + "E_Day": null, + "E_Total": 1530193.4199999999, + "E_Year": null, + "Meter_Location": "grid", + "Mode": "meter", + "P_Akku": null, + "P_Grid": 658.39999999999998, + "P_Load": -695.68274917602537, + "P_PV": 62.948148727416992, + "rel_Autonomy": 5.3591596485874495, + "rel_SelfConsumption": 100.0 + }, + "Smartloads": { + "Ohmpilots": {} + }, + "Version": "12" + } + }, + "Head": { + "RequestArguments": {}, + "Status": { + "Code": 0, + "Reason": "", + "UserMessage": "" + }, + "Timestamp": "2021-11-26T11:07:53+00:00" + } } diff --git a/tests/components/fronius/fixtures/gen24/GetStorageRealtimeData.json b/tests/components/fronius/fixtures/gen24/GetStorageRealtimeData.json index 573a97b7a61..c210042258f 100644 --- a/tests/components/fronius/fixtures/gen24/GetStorageRealtimeData.json +++ b/tests/components/fronius/fixtures/gen24/GetStorageRealtimeData.json @@ -1,16 +1,16 @@ { - "Body" : { - "Data" : {} - }, - "Head" : { - "RequestArguments" : { - "Scope" : "System" - }, - "Status" : { - "Code" : 0, - "Reason" : "", - "UserMessage" : "" - }, - "Timestamp" : "2021-11-26T11:07:52+00:00" - } + "Body": { + "Data": {} + }, + "Head": { + "RequestArguments": { + "Scope": "System" + }, + "Status": { + "Code": 0, + "Reason": "", + "UserMessage": "" + }, + "Timestamp": "2021-11-26T11:07:52+00:00" + } } diff --git a/tests/components/fronius/fixtures/gen24_storage/GetAPIVersion.json b/tests/components/fronius/fixtures/gen24_storage/GetAPIVersion.json index a76e4813e5f..7a0f745b469 100644 --- a/tests/components/fronius/fixtures/gen24_storage/GetAPIVersion.json +++ b/tests/components/fronius/fixtures/gen24_storage/GetAPIVersion.json @@ -1,5 +1,5 @@ { - "APIVersion" : 1, - "BaseURL" : "/solar_api/v1/", - "CompatibilityRange" : "1.7-3" + "APIVersion": 1, + "BaseURL": "/solar_api/v1/", + "CompatibilityRange": "1.7-3" } diff --git a/tests/components/fronius/fixtures/gen24_storage/GetInverterInfo.json b/tests/components/fronius/fixtures/gen24_storage/GetInverterInfo.json index f96f13ae0e8..2dc331823df 100644 --- a/tests/components/fronius/fixtures/gen24_storage/GetInverterInfo.json +++ b/tests/components/fronius/fixtures/gen24_storage/GetInverterInfo.json @@ -1,25 +1,25 @@ { - "Body" : { - "Data" : { - "1" : { - "CustomName" : "Gen24 Storage", - "DT" : 1, - "ErrorCode" : 0, - "InverterState" : "Running", - "PVPower" : 13930, - "Show" : 1, - "StatusCode" : 7, - "UniqueID" : "12345678" - } + "Body": { + "Data": { + "1": { + "CustomName": "Gen24 Storage", + "DT": 1, + "ErrorCode": 0, + "InverterState": "Running", + "PVPower": 13930, + "Show": 1, + "StatusCode": 7, + "UniqueID": "12345678" } - }, - "Head" : { - "RequestArguments" : {}, - "Status" : { - "Code" : 0, - "Reason" : "", - "UserMessage" : "" - }, - "Timestamp" : "2021-11-28T13:12:59+00:00" - } + } + }, + "Head": { + "RequestArguments": {}, + "Status": { + "Code": 0, + "Reason": "", + "UserMessage": "" + }, + "Timestamp": "2021-11-28T13:12:59+00:00" + } } diff --git a/tests/components/fronius/fixtures/gen24_storage/GetInverterRealtimeData_Device_1.json b/tests/components/fronius/fixtures/gen24_storage/GetInverterRealtimeData_Device_1.json index 0b7c01f56eb..0435ca98603 100644 --- a/tests/components/fronius/fixtures/gen24_storage/GetInverterRealtimeData_Device_1.json +++ b/tests/components/fronius/fixtures/gen24_storage/GetInverterRealtimeData_Device_1.json @@ -1,80 +1,80 @@ { - "Body" : { - "Data" : { - "DAY_ENERGY" : { - "Unit" : "Wh", - "Value" : null - }, - "DeviceStatus" : { - "ErrorCode" : 0, - "InverterState" : "Running", - "StatusCode" : 7 - }, - "FAC" : { - "Unit" : "Hz", - "Value" : 49.981552124023438 - }, - "IAC" : { - "Unit" : "A", - "Value" : 1.1086627244949341 - }, - "IDC" : { - "Unit" : "A", - "Value" : 0.39519637823104858 - }, - "IDC_2" : { - "Unit" : "A", - "Value" : 0.35640031099319458 - }, - "IDC_3" : { - "Unit" : "A", - "Value" : null - }, - "PAC" : { - "Unit" : "W", - "Value" : 250.90925598144531 - }, - "SAC" : { - "Unit" : "VA", - "Value" : 250.9410400390625 - }, - "TOTAL_ENERGY" : { - "Unit" : "Wh", - "Value" : 7512794.0116666667 - }, - "UAC" : { - "Unit" : "V", - "Value" : 227.35398864746094 - }, - "UDC" : { - "Unit" : "V", - "Value" : 419.10092163085938 - }, - "UDC_2" : { - "Unit" : "V", - "Value" : 318.81027221679688 - }, - "UDC_3" : { - "Unit" : "V", - "Value" : null - }, - "YEAR_ENERGY" : { - "Unit" : "Wh", - "Value" : null - } + "Body": { + "Data": { + "DAY_ENERGY": { + "Unit": "Wh", + "Value": null + }, + "DeviceStatus": { + "ErrorCode": 0, + "InverterState": "Running", + "StatusCode": 7 + }, + "FAC": { + "Unit": "Hz", + "Value": 49.981552124023438 + }, + "IAC": { + "Unit": "A", + "Value": 1.1086627244949341 + }, + "IDC": { + "Unit": "A", + "Value": 0.39519637823104858 + }, + "IDC_2": { + "Unit": "A", + "Value": 0.35640031099319458 + }, + "IDC_3": { + "Unit": "A", + "Value": null + }, + "PAC": { + "Unit": "W", + "Value": 250.90925598144531 + }, + "SAC": { + "Unit": "VA", + "Value": 250.9410400390625 + }, + "TOTAL_ENERGY": { + "Unit": "Wh", + "Value": 7512794.0116666667 + }, + "UAC": { + "Unit": "V", + "Value": 227.35398864746094 + }, + "UDC": { + "Unit": "V", + "Value": 419.10092163085938 + }, + "UDC_2": { + "Unit": "V", + "Value": 318.81027221679688 + }, + "UDC_3": { + "Unit": "V", + "Value": null + }, + "YEAR_ENERGY": { + "Unit": "Wh", + "Value": null } - }, - "Head" : { - "RequestArguments" : { - "DataCollection" : "CommonInverterData", - "DeviceId" : "1", - "Scope" : "Device" - }, - "Status" : { - "Code" : 0, - "Reason" : "", - "UserMessage" : "" - }, - "Timestamp" : "2021-11-28T13:51:45+00:00" - } + } + }, + "Head": { + "RequestArguments": { + "DataCollection": "CommonInverterData", + "DeviceId": "1", + "Scope": "Device" + }, + "Status": { + "Code": 0, + "Reason": "", + "UserMessage": "" + }, + "Timestamp": "2021-11-28T13:51:45+00:00" + } } diff --git a/tests/components/fronius/fixtures/gen24_storage/GetLoggerInfo.json b/tests/components/fronius/fixtures/gen24_storage/GetLoggerInfo.json index 55ad726cd83..327064a74a1 100644 --- a/tests/components/fronius/fixtures/gen24_storage/GetLoggerInfo.json +++ b/tests/components/fronius/fixtures/gen24_storage/GetLoggerInfo.json @@ -1,14 +1,14 @@ { - "Body" : { - "Data" : {} - }, - "Head" : { - "RequestArguments" : {}, - "Status" : { - "Code" : 11, - "Reason" : "v1/GetLoggerInfo.cgi request is not supported.", - "UserMessage" : "" - }, - "Timestamp" : "2021-11-28T13:17:43+00:00" - } + "Body": { + "Data": {} + }, + "Head": { + "RequestArguments": {}, + "Status": { + "Code": 11, + "Reason": "v1/GetLoggerInfo.cgi request is not supported.", + "UserMessage": "" + }, + "Timestamp": "2021-11-28T13:17:43+00:00" + } } diff --git a/tests/components/fronius/fixtures/gen24_storage/GetMeterRealtimeData.json b/tests/components/fronius/fixtures/gen24_storage/GetMeterRealtimeData.json index f32ba740eaa..7f4879b06c4 100644 --- a/tests/components/fronius/fixtures/gen24_storage/GetMeterRealtimeData.json +++ b/tests/components/fronius/fixtures/gen24_storage/GetMeterRealtimeData.json @@ -1,61 +1,61 @@ { - "Body" : { - "Data" : { - "0" : { - "Current_AC_Phase_1" : 1.7010000000000001, - "Current_AC_Phase_2" : 1.8320000000000001, - "Current_AC_Phase_3" : 0.64500000000000002, - "Current_AC_Sum" : 4.1780000000000008, - "Details" : { - "Manufacturer" : "Fronius", - "Model" : "Smart Meter TS 65A-3", - "Serial" : "1234567890" - }, - "Enable" : 1, - "EnergyReactive_VArAC_Sum_Consumed" : 5482.0, - "EnergyReactive_VArAC_Sum_Produced" : 3266105.0, - "EnergyReal_WAC_Minus_Absolute" : 1705128.0, - "EnergyReal_WAC_Plus_Absolute" : 1247204.0, - "EnergyReal_WAC_Sum_Consumed" : 1247204.0, - "EnergyReal_WAC_Sum_Produced" : 1705128.0, - "Frequency_Phase_Average" : 49.899999999999999, - "Meter_Location_Current" : 0.0, - "PowerApparent_S_Phase_1" : 319.5, - "PowerApparent_S_Phase_2" : 383.89999999999998, - "PowerApparent_S_Phase_3" : 118.40000000000001, - "PowerApparent_S_Sum" : 821.89999999999998, - "PowerFactor_Phase_1" : 0.995, - "PowerFactor_Phase_2" : 0.38900000000000001, - "PowerFactor_Phase_3" : 0.16300000000000001, - "PowerFactor_Sum" : 0.69799999999999995, - "PowerReactive_Q_Phase_1" : -31.300000000000001, - "PowerReactive_Q_Phase_2" : -353.39999999999998, - "PowerReactive_Q_Phase_3" : -116.7, - "PowerReactive_Q_Sum" : -501.5, - "PowerReal_P_Phase_1" : 317.89999999999998, - "PowerReal_P_Phase_2" : 150.0, - "PowerReal_P_Phase_3" : 19.600000000000001, - "PowerReal_P_Sum" : 487.69999999999999, - "TimeStamp" : 1638104813.0, - "Visible" : 1.0, - "Voltage_AC_PhaseToPhase_12" : 396.0, - "Voltage_AC_PhaseToPhase_23" : 393.0, - "Voltage_AC_PhaseToPhase_31" : 394.30000000000001, - "Voltage_AC_Phase_1" : 229.40000000000001, - "Voltage_AC_Phase_2" : 225.59999999999999, - "Voltage_AC_Phase_3" : 228.30000000000001 - } + "Body": { + "Data": { + "0": { + "Current_AC_Phase_1": 1.7010000000000001, + "Current_AC_Phase_2": 1.8320000000000001, + "Current_AC_Phase_3": 0.64500000000000002, + "Current_AC_Sum": 4.1780000000000008, + "Details": { + "Manufacturer": "Fronius", + "Model": "Smart Meter TS 65A-3", + "Serial": "1234567890" + }, + "Enable": 1, + "EnergyReactive_VArAC_Sum_Consumed": 5482.0, + "EnergyReactive_VArAC_Sum_Produced": 3266105.0, + "EnergyReal_WAC_Minus_Absolute": 1705128.0, + "EnergyReal_WAC_Plus_Absolute": 1247204.0, + "EnergyReal_WAC_Sum_Consumed": 1247204.0, + "EnergyReal_WAC_Sum_Produced": 1705128.0, + "Frequency_Phase_Average": 49.899999999999999, + "Meter_Location_Current": 0.0, + "PowerApparent_S_Phase_1": 319.5, + "PowerApparent_S_Phase_2": 383.89999999999998, + "PowerApparent_S_Phase_3": 118.40000000000001, + "PowerApparent_S_Sum": 821.89999999999998, + "PowerFactor_Phase_1": 0.995, + "PowerFactor_Phase_2": 0.38900000000000001, + "PowerFactor_Phase_3": 0.16300000000000001, + "PowerFactor_Sum": 0.69799999999999995, + "PowerReactive_Q_Phase_1": -31.300000000000001, + "PowerReactive_Q_Phase_2": -353.39999999999998, + "PowerReactive_Q_Phase_3": -116.7, + "PowerReactive_Q_Sum": -501.5, + "PowerReal_P_Phase_1": 317.89999999999998, + "PowerReal_P_Phase_2": 150.0, + "PowerReal_P_Phase_3": 19.600000000000001, + "PowerReal_P_Sum": 487.69999999999999, + "TimeStamp": 1638104813.0, + "Visible": 1.0, + "Voltage_AC_PhaseToPhase_12": 396.0, + "Voltage_AC_PhaseToPhase_23": 393.0, + "Voltage_AC_PhaseToPhase_31": 394.30000000000001, + "Voltage_AC_Phase_1": 229.40000000000001, + "Voltage_AC_Phase_2": 225.59999999999999, + "Voltage_AC_Phase_3": 228.30000000000001 } - }, - "Head" : { - "RequestArguments" : { - "Scope" : "System" - }, - "Status" : { - "Code" : 0, - "Reason" : "", - "UserMessage" : "" - }, - "Timestamp" : "2021-11-28T13:06:54+00:00" - } + } + }, + "Head": { + "RequestArguments": { + "Scope": "System" + }, + "Status": { + "Code": 0, + "Reason": "", + "UserMessage": "" + }, + "Timestamp": "2021-11-28T13:06:54+00:00" + } } diff --git a/tests/components/fronius/fixtures/gen24_storage/GetOhmPilotRealtimeData.json b/tests/components/fronius/fixtures/gen24_storage/GetOhmPilotRealtimeData.json index 0215201ad6e..281d0347892 100644 --- a/tests/components/fronius/fixtures/gen24_storage/GetOhmPilotRealtimeData.json +++ b/tests/components/fronius/fixtures/gen24_storage/GetOhmPilotRealtimeData.json @@ -1,30 +1,30 @@ { - "Body" : { - "Data" : { - "0" : { - "CodeOfState" : 0.0, - "Details" : { - "Hardware" : "6", - "Manufacturer" : "Fronius", - "Model" : "Ohmpilot", - "Serial" : "23456789", - "Software" : "1.0.25-3" - }, - "EnergyReal_WAC_Sum_Consumed" : 1233295.0, - "PowerReal_PAC_Sum" : 0.0, - "Temperature_Channel_1" : 38.899999999999999 - } + "Body": { + "Data": { + "0": { + "CodeOfState": 0.0, + "Details": { + "Hardware": "6", + "Manufacturer": "Fronius", + "Model": "Ohmpilot", + "Serial": "23456789", + "Software": "1.0.25-3" + }, + "EnergyReal_WAC_Sum_Consumed": 1233295.0, + "PowerReal_PAC_Sum": 0.0, + "Temperature_Channel_1": 38.899999999999999 } - }, - "Head" : { - "RequestArguments" : { - "Scope" : "System" - }, - "Status" : { - "Code" : 0, - "Reason" : "", - "UserMessage" : "" - }, - "Timestamp" : "2021-11-28T13:11:42+00:00" - } + } + }, + "Head": { + "RequestArguments": { + "Scope": "System" + }, + "Status": { + "Code": 0, + "Reason": "", + "UserMessage": "" + }, + "Timestamp": "2021-11-28T13:11:42+00:00" + } } diff --git a/tests/components/fronius/fixtures/gen24_storage/GetPowerFlowRealtimeData.json b/tests/components/fronius/fixtures/gen24_storage/GetPowerFlowRealtimeData.json index fcd82a9bf1d..a7f7a2e50fa 100644 --- a/tests/components/fronius/fixtures/gen24_storage/GetPowerFlowRealtimeData.json +++ b/tests/components/fronius/fixtures/gen24_storage/GetPowerFlowRealtimeData.json @@ -1,51 +1,51 @@ { - "Body" : { - "Data" : { - "Inverters" : { - "1" : { - "Battery_Mode" : "suspended", - "DT" : 1, - "E_Day" : null, - "E_Total" : 7512664.4041666668, - "E_Year" : null, - "P" : 186.54914855957031, - "SOC" : 4.5999999999999996 - } - }, - "Site" : { - "BackupMode" : true, - "BatteryStandby" : false, - "E_Day" : null, - "E_Total" : 7512664.4041666668, - "E_Year" : null, - "Meter_Location" : "grid", - "Mode" : "bidirectional", - "P_Akku" : 0.15907810628414154, - "P_Grid" : 2274.9000000000001, - "P_Load" : -2459.3092254638673, - "P_PV" : 216.43276786804199, - "rel_Autonomy" : 7.4984155532163506, - "rel_SelfConsumption" : 100.0 - }, - "Smartloads" : { - "Ohmpilots" : { - "0" : { - "P_AC_Total" : 0.0, - "State" : "normal", - "Temperature" : 38.799999999999997 - } - } - }, - "Version" : "12" - } - }, - "Head" : { - "RequestArguments" : {}, - "Status" : { - "Code" : 0, - "Reason" : "", - "UserMessage" : "" + "Body": { + "Data": { + "Inverters": { + "1": { + "Battery_Mode": "suspended", + "DT": 1, + "E_Day": null, + "E_Total": 7512664.4041666668, + "E_Year": null, + "P": 186.54914855957031, + "SOC": 4.5999999999999996 + } }, - "Timestamp" : "2021-11-28T13:12:20+00:00" - } + "Site": { + "BackupMode": true, + "BatteryStandby": false, + "E_Day": null, + "E_Total": 7512664.4041666668, + "E_Year": null, + "Meter_Location": "grid", + "Mode": "bidirectional", + "P_Akku": 0.15907810628414154, + "P_Grid": 2274.9000000000001, + "P_Load": -2459.3092254638673, + "P_PV": 216.43276786804199, + "rel_Autonomy": 7.4984155532163506, + "rel_SelfConsumption": 100.0 + }, + "Smartloads": { + "Ohmpilots": { + "0": { + "P_AC_Total": 0.0, + "State": "normal", + "Temperature": 38.799999999999997 + } + } + }, + "Version": "12" + } + }, + "Head": { + "RequestArguments": {}, + "Status": { + "Code": 0, + "Reason": "", + "UserMessage": "" + }, + "Timestamp": "2021-11-28T13:12:20+00:00" + } } diff --git a/tests/components/fronius/fixtures/gen24_storage/GetStorageRealtimeData.json b/tests/components/fronius/fixtures/gen24_storage/GetStorageRealtimeData.json index d810962c66c..e6c4fdab5d9 100644 --- a/tests/components/fronius/fixtures/gen24_storage/GetStorageRealtimeData.json +++ b/tests/components/fronius/fixtures/gen24_storage/GetStorageRealtimeData.json @@ -1,36 +1,36 @@ { - "Body" : { - "Data" : { - "0" : { - "Controller" : { - "Capacity_Maximum" : 16588, - "Current_DC" : 0.0, - "DesignedCapacity" : 16588, - "Details" : { - "Manufacturer" : "BYD", - "Model" : "BYD Battery-Box Premium HV", - "Serial" : "P030T020Z2001234567 " - }, - "Enable" : 1, - "StateOfCharge_Relative" : 4.5999999999999996, - "Status_BatteryCell" : 0.0, - "Temperature_Cell" : 21.5, - "TimeStamp" : 1638105056.0, - "Voltage_DC" : 0.0 - }, - "Modules" : [] - } + "Body": { + "Data": { + "0": { + "Controller": { + "Capacity_Maximum": 16588, + "Current_DC": 0.0, + "DesignedCapacity": 16588, + "Details": { + "Manufacturer": "BYD", + "Model": "BYD Battery-Box Premium HV", + "Serial": "P030T020Z2001234567 " + }, + "Enable": 1, + "StateOfCharge_Relative": 4.5999999999999996, + "Status_BatteryCell": 0.0, + "Temperature_Cell": 21.5, + "TimeStamp": 1638105056.0, + "Voltage_DC": 0.0 + }, + "Modules": [] } - }, - "Head" : { - "RequestArguments" : { - "Scope" : "System" - }, - "Status" : { - "Code" : 0, - "Reason" : "", - "UserMessage" : "" - }, - "Timestamp" : "2021-11-28T13:10:57+00:00" - } + } + }, + "Head": { + "RequestArguments": { + "Scope": "System" + }, + "Status": { + "Code": 0, + "Reason": "", + "UserMessage": "" + }, + "Timestamp": "2021-11-28T13:10:57+00:00" + } } diff --git a/tests/components/fronius/fixtures/primo_s0/GetAPIVersion.json b/tests/components/fronius/fixtures/primo_s0/GetAPIVersion.json index 2051b4d58e3..2f0ae53baf0 100644 --- a/tests/components/fronius/fixtures/primo_s0/GetAPIVersion.json +++ b/tests/components/fronius/fixtures/primo_s0/GetAPIVersion.json @@ -1,5 +1,5 @@ { - "APIVersion" : 1, - "BaseURL" : "/solar_api/v1/", - "CompatibilityRange" : "1.6-3" + "APIVersion": 1, + "BaseURL": "/solar_api/v1/", + "CompatibilityRange": "1.6-3" } diff --git a/tests/components/fronius/fixtures/primo_s0/GetInverterInfo.json b/tests/components/fronius/fixtures/primo_s0/GetInverterInfo.json index 5ac293653c0..33157caab04 100644 --- a/tests/components/fronius/fixtures/primo_s0/GetInverterInfo.json +++ b/tests/components/fronius/fixtures/primo_s0/GetInverterInfo.json @@ -1,33 +1,33 @@ { - "Body" : { - "Data" : { - "1" : { - "CustomName" : "Primo 5.0-1", - "DT" : 76, - "ErrorCode" : 0, - "PVPower" : 5160, - "Show" : 1, - "StatusCode" : 7, - "UniqueID" : "123456" - }, - "2" : { - "CustomName" : "Primo 3.0-1", - "DT" : 81, - "ErrorCode" : 0, - "PVPower" : 3240, - "Show" : 1, - "StatusCode" : 7, - "UniqueID" : "234567" - } - } - }, - "Head" : { - "RequestArguments" : {}, - "Status" : { - "Code" : 0, - "Reason" : "", - "UserMessage" : "" + "Body": { + "Data": { + "1": { + "CustomName": "Primo 5.0-1", + "DT": 76, + "ErrorCode": 0, + "PVPower": 5160, + "Show": 1, + "StatusCode": 7, + "UniqueID": "123456" }, - "Timestamp" : "2021-12-09T15:34:06-03:00" - } + "2": { + "CustomName": "Primo 3.0-1", + "DT": 81, + "ErrorCode": 0, + "PVPower": 3240, + "Show": 1, + "StatusCode": 7, + "UniqueID": "234567" + } + } + }, + "Head": { + "RequestArguments": {}, + "Status": { + "Code": 0, + "Reason": "", + "UserMessage": "" + }, + "Timestamp": "2021-12-09T15:34:06-03:00" + } } diff --git a/tests/components/fronius/fixtures/primo_s0/GetInverterRealtimeData_Device_1.json b/tests/components/fronius/fixtures/primo_s0/GetInverterRealtimeData_Device_1.json index e54366a5008..08797917f83 100644 --- a/tests/components/fronius/fixtures/primo_s0/GetInverterRealtimeData_Device_1.json +++ b/tests/components/fronius/fixtures/primo_s0/GetInverterRealtimeData_Device_1.json @@ -1,64 +1,64 @@ { - "Body" : { - "Data" : { - "DAY_ENERGY" : { - "Unit" : "Wh", - "Value" : 22504 - }, - "DeviceStatus" : { - "ErrorCode" : 0, - "LEDColor" : 2, - "LEDState" : 0, - "MgmtTimerRemainingTime" : -1, - "StateToReset" : false, - "StatusCode" : 7 - }, - "FAC" : { - "Unit" : "Hz", - "Value" : 60 - }, - "IAC" : { - "Unit" : "A", - "Value" : 3.8500000000000001 - }, - "IDC" : { - "Unit" : "A", - "Value" : 4.2300000000000004 - }, - "PAC" : { - "Unit" : "W", - "Value" : 862 - }, - "TOTAL_ENERGY" : { - "Unit" : "Wh", - "Value" : 17114940 - }, - "UAC" : { - "Unit" : "V", - "Value" : 223.90000000000001 - }, - "UDC" : { - "Unit" : "V", - "Value" : 452.30000000000001 - }, - "YEAR_ENERGY" : { - "Unit" : "Wh", - "Value" : 7532755.5 - } + "Body": { + "Data": { + "DAY_ENERGY": { + "Unit": "Wh", + "Value": 22504 + }, + "DeviceStatus": { + "ErrorCode": 0, + "LEDColor": 2, + "LEDState": 0, + "MgmtTimerRemainingTime": -1, + "StateToReset": false, + "StatusCode": 7 + }, + "FAC": { + "Unit": "Hz", + "Value": 60 + }, + "IAC": { + "Unit": "A", + "Value": 3.8500000000000001 + }, + "IDC": { + "Unit": "A", + "Value": 4.2300000000000004 + }, + "PAC": { + "Unit": "W", + "Value": 862 + }, + "TOTAL_ENERGY": { + "Unit": "Wh", + "Value": 17114940 + }, + "UAC": { + "Unit": "V", + "Value": 223.90000000000001 + }, + "UDC": { + "Unit": "V", + "Value": 452.30000000000001 + }, + "YEAR_ENERGY": { + "Unit": "Wh", + "Value": 7532755.5 } - }, - "Head" : { - "RequestArguments" : { - "DataCollection" : "CommonInverterData", - "DeviceClass" : "Inverter", - "DeviceId" : "1", - "Scope" : "Device" - }, - "Status" : { - "Code" : 0, - "Reason" : "", - "UserMessage" : "" - }, - "Timestamp" : "2021-12-09T15:34:08-03:00" - } + } + }, + "Head": { + "RequestArguments": { + "DataCollection": "CommonInverterData", + "DeviceClass": "Inverter", + "DeviceId": "1", + "Scope": "Device" + }, + "Status": { + "Code": 0, + "Reason": "", + "UserMessage": "" + }, + "Timestamp": "2021-12-09T15:34:08-03:00" + } } diff --git a/tests/components/fronius/fixtures/primo_s0/GetInverterRealtimeData_Device_2.json b/tests/components/fronius/fixtures/primo_s0/GetInverterRealtimeData_Device_2.json index dd1e22c0a7a..9330de9653a 100644 --- a/tests/components/fronius/fixtures/primo_s0/GetInverterRealtimeData_Device_2.json +++ b/tests/components/fronius/fixtures/primo_s0/GetInverterRealtimeData_Device_2.json @@ -1,64 +1,64 @@ { - "Body" : { - "Data" : { - "DAY_ENERGY" : { - "Unit" : "Wh", - "Value" : 14237 - }, - "DeviceStatus" : { - "ErrorCode" : 0, - "LEDColor" : 2, - "LEDState" : 0, - "MgmtTimerRemainingTime" : -1, - "StateToReset" : false, - "StatusCode" : 7 - }, - "FAC" : { - "Unit" : "Hz", - "Value" : 60.009999999999998 - }, - "IAC" : { - "Unit" : "A", - "Value" : 1.3200000000000001 - }, - "IDC" : { - "Unit" : "A", - "Value" : 0.96999999999999997 - }, - "PAC" : { - "Unit" : "W", - "Value" : 296 - }, - "TOTAL_ENERGY" : { - "Unit" : "Wh", - "Value" : 5796010 - }, - "UAC" : { - "Unit" : "V", - "Value" : 223.59999999999999 - }, - "UDC" : { - "Unit" : "V", - "Value" : 329.5 - }, - "YEAR_ENERGY" : { - "Unit" : "Wh", - "Value" : 3596193.25 - } + "Body": { + "Data": { + "DAY_ENERGY": { + "Unit": "Wh", + "Value": 14237 + }, + "DeviceStatus": { + "ErrorCode": 0, + "LEDColor": 2, + "LEDState": 0, + "MgmtTimerRemainingTime": -1, + "StateToReset": false, + "StatusCode": 7 + }, + "FAC": { + "Unit": "Hz", + "Value": 60.009999999999998 + }, + "IAC": { + "Unit": "A", + "Value": 1.3200000000000001 + }, + "IDC": { + "Unit": "A", + "Value": 0.96999999999999997 + }, + "PAC": { + "Unit": "W", + "Value": 296 + }, + "TOTAL_ENERGY": { + "Unit": "Wh", + "Value": 5796010 + }, + "UAC": { + "Unit": "V", + "Value": 223.59999999999999 + }, + "UDC": { + "Unit": "V", + "Value": 329.5 + }, + "YEAR_ENERGY": { + "Unit": "Wh", + "Value": 3596193.25 } - }, - "Head" : { - "RequestArguments" : { - "DataCollection" : "CommonInverterData", - "DeviceClass" : "Inverter", - "DeviceId" : "2", - "Scope" : "Device" - }, - "Status" : { - "Code" : 0, - "Reason" : "", - "UserMessage" : "" - }, - "Timestamp" : "2021-12-09T15:36:15-03:00" - } + } + }, + "Head": { + "RequestArguments": { + "DataCollection": "CommonInverterData", + "DeviceClass": "Inverter", + "DeviceId": "2", + "Scope": "Device" + }, + "Status": { + "Code": 0, + "Reason": "", + "UserMessage": "" + }, + "Timestamp": "2021-12-09T15:36:15-03:00" + } } diff --git a/tests/components/fronius/fixtures/primo_s0/GetLoggerInfo.json b/tests/components/fronius/fixtures/primo_s0/GetLoggerInfo.json index 1fb0bbc8577..cb63b93ec4e 100644 --- a/tests/components/fronius/fixtures/primo_s0/GetLoggerInfo.json +++ b/tests/components/fronius/fixtures/primo_s0/GetLoggerInfo.json @@ -1,29 +1,29 @@ { - "Body" : { - "LoggerInfo" : { - "CO2Factor" : 0.52999997138977051, - "CO2Unit" : "kg", - "CashCurrency" : "BRL", - "CashFactor" : 1, - "DefaultLanguage" : "en", - "DeliveryFactor" : 1, - "HWVersion" : "2.4E", - "PlatformID" : "wilma", - "ProductID" : "fronius-datamanager-card", - "SWVersion" : "3.18.7-1", - "TimezoneLocation" : "Sao_Paulo", - "TimezoneName" : "-03", - "UTCOffset" : 4294956496, - "UniqueID" : "123.4567890" - } - }, - "Head" : { - "RequestArguments" : {}, - "Status" : { - "Code" : 0, - "Reason" : "", - "UserMessage" : "" - }, - "Timestamp" : "2021-12-09T15:34:09-03:00" - } + "Body": { + "LoggerInfo": { + "CO2Factor": 0.52999997138977051, + "CO2Unit": "kg", + "CashCurrency": "BRL", + "CashFactor": 1, + "DefaultLanguage": "en", + "DeliveryFactor": 1, + "HWVersion": "2.4E", + "PlatformID": "wilma", + "ProductID": "fronius-datamanager-card", + "SWVersion": "3.18.7-1", + "TimezoneLocation": "Sao_Paulo", + "TimezoneName": "-03", + "UTCOffset": 4294956496, + "UniqueID": "123.4567890" + } + }, + "Head": { + "RequestArguments": {}, + "Status": { + "Code": 0, + "Reason": "", + "UserMessage": "" + }, + "Timestamp": "2021-12-09T15:34:09-03:00" + } } diff --git a/tests/components/fronius/fixtures/primo_s0/GetMeterRealtimeData.json b/tests/components/fronius/fixtures/primo_s0/GetMeterRealtimeData.json index aa308bb3b69..5055368a9f1 100644 --- a/tests/components/fronius/fixtures/primo_s0/GetMeterRealtimeData.json +++ b/tests/components/fronius/fixtures/primo_s0/GetMeterRealtimeData.json @@ -1,31 +1,31 @@ { - "Body" : { - "Data" : { - "0" : { - "Details" : { - "Manufacturer" : "Fronius", - "Model" : "S0 Meter at inverter 1", - "Serial" : "n.a." - }, - "Enable" : 1, - "EnergyReal_WAC_Minus_Relative" : 191.25, - "Meter_Location_Current" : 1, - "PowerReal_P_Sum" : -2216.7486858112229, - "TimeStamp" : 1639074843, - "Visible" : 1 - } + "Body": { + "Data": { + "0": { + "Details": { + "Manufacturer": "Fronius", + "Model": "S0 Meter at inverter 1", + "Serial": "n.a." + }, + "Enable": 1, + "EnergyReal_WAC_Minus_Relative": 191.25, + "Meter_Location_Current": 1, + "PowerReal_P_Sum": -2216.7486858112229, + "TimeStamp": 1639074843, + "Visible": 1 } - }, - "Head" : { - "RequestArguments" : { - "DeviceClass" : "Meter", - "Scope" : "System" - }, - "Status" : { - "Code" : 0, - "Reason" : "", - "UserMessage" : "" - }, - "Timestamp" : "2021-12-09T15:34:04-03:00" - } + } + }, + "Head": { + "RequestArguments": { + "DeviceClass": "Meter", + "Scope": "System" + }, + "Status": { + "Code": 0, + "Reason": "", + "UserMessage": "" + }, + "Timestamp": "2021-12-09T15:34:04-03:00" + } } diff --git a/tests/components/fronius/fixtures/primo_s0/GetOhmPilotRealtimeData.json b/tests/components/fronius/fixtures/primo_s0/GetOhmPilotRealtimeData.json index 4562b45efb0..bf0e2f69148 100644 --- a/tests/components/fronius/fixtures/primo_s0/GetOhmPilotRealtimeData.json +++ b/tests/components/fronius/fixtures/primo_s0/GetOhmPilotRealtimeData.json @@ -1,17 +1,17 @@ { - "Body" : { - "Data" : {} - }, - "Head" : { - "RequestArguments" : { - "DeviceClass" : "OhmPilot", - "Scope" : "System" - }, - "Status" : { - "Code" : 0, - "Reason" : "", - "UserMessage" : "" - }, - "Timestamp" : "2021-12-09T15:34:05-03:00" - } + "Body": { + "Data": {} + }, + "Head": { + "RequestArguments": { + "DeviceClass": "OhmPilot", + "Scope": "System" + }, + "Status": { + "Code": 0, + "Reason": "", + "UserMessage": "" + }, + "Timestamp": "2021-12-09T15:34:05-03:00" + } } diff --git a/tests/components/fronius/fixtures/primo_s0/GetPowerFlowRealtimeData.json b/tests/components/fronius/fixtures/primo_s0/GetPowerFlowRealtimeData.json index 4bbee2aec28..0c58e5fd24a 100644 --- a/tests/components/fronius/fixtures/primo_s0/GetPowerFlowRealtimeData.json +++ b/tests/components/fronius/fixtures/primo_s0/GetPowerFlowRealtimeData.json @@ -1,45 +1,45 @@ { - "Body" : { - "Data" : { - "Inverters" : { - "1" : { - "DT" : 76, - "E_Day" : 22502, - "E_Total" : 17114930, - "E_Year" : 7532753.5, - "P" : 886 - }, - "2" : { - "DT" : 81, - "E_Day" : 14222, - "E_Total" : 5795989.5, - "E_Year" : 3596179.75, - "P" : 948 - } - }, - "Site" : { - "E_Day" : 36724, - "E_Total" : 22910919.5, - "E_Year" : 11128933.25, - "Meter_Location" : "load", - "Mode" : "vague-meter", - "P_Akku" : null, - "P_Grid" : 384.93491437299008, - "P_Load" : -2218.9349143729901, - "P_PV" : 1834, - "rel_Autonomy" : 82.652266550064084, - "rel_SelfConsumption" : 100 - }, - "Version" : "12" - } - }, - "Head" : { - "RequestArguments" : {}, - "Status" : { - "Code" : 0, - "Reason" : "", - "UserMessage" : "" + "Body": { + "Data": { + "Inverters": { + "1": { + "DT": 76, + "E_Day": 22502, + "E_Total": 17114930, + "E_Year": 7532753.5, + "P": 886 + }, + "2": { + "DT": 81, + "E_Day": 14222, + "E_Total": 5795989.5, + "E_Year": 3596179.75, + "P": 948 + } }, - "Timestamp" : "2021-12-09T15:34:06-03:00" - } + "Site": { + "E_Day": 36724, + "E_Total": 22910919.5, + "E_Year": 11128933.25, + "Meter_Location": "load", + "Mode": "vague-meter", + "P_Akku": null, + "P_Grid": 384.93491437299008, + "P_Load": -2218.9349143729901, + "P_PV": 1834, + "rel_Autonomy": 82.652266550064084, + "rel_SelfConsumption": 100 + }, + "Version": "12" + } + }, + "Head": { + "RequestArguments": {}, + "Status": { + "Code": 0, + "Reason": "", + "UserMessage": "" + }, + "Timestamp": "2021-12-09T15:34:06-03:00" + } } diff --git a/tests/components/fronius/fixtures/primo_s0/GetStorageRealtimeData.json b/tests/components/fronius/fixtures/primo_s0/GetStorageRealtimeData.json index 8743a2c6d68..7f33f016b57 100644 --- a/tests/components/fronius/fixtures/primo_s0/GetStorageRealtimeData.json +++ b/tests/components/fronius/fixtures/primo_s0/GetStorageRealtimeData.json @@ -1,14 +1,14 @@ { - "Body" : { - "Data" : {} - }, - "Head" : { - "RequestArguments" : {}, - "Status" : { - "Code" : 255, - "Reason" : "GetStorageRealtimeData request is not supported by this device.", - "UserMessage" : "" - }, - "Timestamp" : "2021-12-09T15:34:05-03:00" - } + "Body": { + "Data": {} + }, + "Head": { + "RequestArguments": {}, + "Status": { + "Code": 255, + "Reason": "GetStorageRealtimeData request is not supported by this device.", + "UserMessage": "" + }, + "Timestamp": "2021-12-09T15:34:05-03:00" + } } diff --git a/tests/components/fronius/fixtures/symo/GetAPIVersion.json b/tests/components/fronius/fixtures/symo/GetAPIVersion.json index 59c1dbb1c5c..2f0ae53baf0 100644 --- a/tests/components/fronius/fixtures/symo/GetAPIVersion.json +++ b/tests/components/fronius/fixtures/symo/GetAPIVersion.json @@ -1,5 +1,5 @@ { - "APIVersion": 1, - "BaseURL": "/solar_api/v1/", - "CompatibilityRange": "1.6-3" -} \ No newline at end of file + "APIVersion": 1, + "BaseURL": "/solar_api/v1/", + "CompatibilityRange": "1.6-3" +} diff --git a/tests/components/fronius/fixtures/symo/GetInverterInfo.json b/tests/components/fronius/fixtures/symo/GetInverterInfo.json index 8bbf01a6cb4..5b2676c3a3f 100644 --- a/tests/components/fronius/fixtures/symo/GetInverterInfo.json +++ b/tests/components/fronius/fixtures/symo/GetInverterInfo.json @@ -1,24 +1,24 @@ { - "Body": { - "Data": { - "1": { - "CustomName": "Symo 20", - "DT": 121, - "ErrorCode": 0, - "PVPower": 23100, - "Show": 1, - "StatusCode": 7, - "UniqueID": "1234567" - } - } - }, - "Head": { - "RequestArguments": {}, - "Status": { - "Code": 0, - "Reason": "", - "UserMessage": "" - }, - "Timestamp": "2021-10-07T13:41:00+02:00" + "Body": { + "Data": { + "1": { + "CustomName": "Symo 20", + "DT": 121, + "ErrorCode": 0, + "PVPower": 23100, + "Show": 1, + "StatusCode": 7, + "UniqueID": "1234567" + } } -} \ No newline at end of file + }, + "Head": { + "RequestArguments": {}, + "Status": { + "Code": 0, + "Reason": "", + "UserMessage": "" + }, + "Timestamp": "2021-10-07T13:41:00+02:00" + } +} diff --git a/tests/components/fronius/fixtures/symo/GetInverterRealtimeData_Device_1.json b/tests/components/fronius/fixtures/symo/GetInverterRealtimeData_Device_1.json index d504b125a62..1a95de39cad 100644 --- a/tests/components/fronius/fixtures/symo/GetInverterRealtimeData_Device_1.json +++ b/tests/components/fronius/fixtures/symo/GetInverterRealtimeData_Device_1.json @@ -1,64 +1,64 @@ { - "Body": { - "Data": { - "DAY_ENERGY": { - "Unit": "Wh", - "Value": 1113 - }, - "DeviceStatus": { - "ErrorCode": 0, - "LEDColor": 2, - "LEDState": 0, - "MgmtTimerRemainingTime": -1, - "StateToReset": false, - "StatusCode": 7 - }, - "FAC": { - "Unit": "Hz", - "Value": 49.939999999999998 - }, - "IAC": { - "Unit": "A", - "Value": 5.1900000000000004 - }, - "IDC": { - "Unit": "A", - "Value": 2.1899999999999999 - }, - "PAC": { - "Unit": "W", - "Value": 1190 - }, - "TOTAL_ENERGY": { - "Unit": "Wh", - "Value": 44188000 - }, - "UAC": { - "Unit": "V", - "Value": 227.90000000000001 - }, - "UDC": { - "Unit": "V", - "Value": 518 - }, - "YEAR_ENERGY": { - "Unit": "Wh", - "Value": 25508798 - } - } - }, - "Head": { - "RequestArguments": { - "DataCollection": "CommonInverterData", - "DeviceClass": "Inverter", - "DeviceId": "1", - "Scope": "Device" - }, - "Status": { - "Code": 0, - "Reason": "", - "UserMessage": "" - }, - "Timestamp": "2021-10-07T10:01:17+02:00" + "Body": { + "Data": { + "DAY_ENERGY": { + "Unit": "Wh", + "Value": 1113 + }, + "DeviceStatus": { + "ErrorCode": 0, + "LEDColor": 2, + "LEDState": 0, + "MgmtTimerRemainingTime": -1, + "StateToReset": false, + "StatusCode": 7 + }, + "FAC": { + "Unit": "Hz", + "Value": 49.939999999999998 + }, + "IAC": { + "Unit": "A", + "Value": 5.1900000000000004 + }, + "IDC": { + "Unit": "A", + "Value": 2.1899999999999999 + }, + "PAC": { + "Unit": "W", + "Value": 1190 + }, + "TOTAL_ENERGY": { + "Unit": "Wh", + "Value": 44188000 + }, + "UAC": { + "Unit": "V", + "Value": 227.90000000000001 + }, + "UDC": { + "Unit": "V", + "Value": 518 + }, + "YEAR_ENERGY": { + "Unit": "Wh", + "Value": 25508798 + } } -} \ No newline at end of file + }, + "Head": { + "RequestArguments": { + "DataCollection": "CommonInverterData", + "DeviceClass": "Inverter", + "DeviceId": "1", + "Scope": "Device" + }, + "Status": { + "Code": 0, + "Reason": "", + "UserMessage": "" + }, + "Timestamp": "2021-10-07T10:01:17+02:00" + } +} diff --git a/tests/components/fronius/fixtures/symo/GetInverterRealtimeData_Device_1_night.json b/tests/components/fronius/fixtures/symo/GetInverterRealtimeData_Device_1_night.json index d3c5091fc00..797eda2bf76 100644 --- a/tests/components/fronius/fixtures/symo/GetInverterRealtimeData_Device_1_night.json +++ b/tests/components/fronius/fixtures/symo/GetInverterRealtimeData_Device_1_night.json @@ -1,48 +1,48 @@ { - "Body": { - "Data": { - "DAY_ENERGY": { - "Unit": "Wh", - "Value": 10828 - }, - "DeviceStatus": { - "ErrorCode": 307, - "LEDColor": 1, - "LEDState": 0, - "MgmtTimerRemainingTime": 17, - "StateToReset": false, - "StatusCode": 3 - }, - "IDC": { - "Unit": "A", - "Value": 0 - }, - "TOTAL_ENERGY": { - "Unit": "Wh", - "Value": 44186900 - }, - "UDC": { - "Unit": "V", - "Value": 16 - }, - "YEAR_ENERGY": { - "Unit": "Wh", - "Value": 25507686 - } - } - }, - "Head": { - "RequestArguments": { - "DataCollection": "CommonInverterData", - "DeviceClass": "Inverter", - "DeviceId": "1", - "Scope": "Device" - }, - "Status": { - "Code": 0, - "Reason": "", - "UserMessage": "" - }, - "Timestamp": "2021-10-06T21:16:59+02:00" + "Body": { + "Data": { + "DAY_ENERGY": { + "Unit": "Wh", + "Value": 10828 + }, + "DeviceStatus": { + "ErrorCode": 307, + "LEDColor": 1, + "LEDState": 0, + "MgmtTimerRemainingTime": 17, + "StateToReset": false, + "StatusCode": 3 + }, + "IDC": { + "Unit": "A", + "Value": 0 + }, + "TOTAL_ENERGY": { + "Unit": "Wh", + "Value": 44186900 + }, + "UDC": { + "Unit": "V", + "Value": 16 + }, + "YEAR_ENERGY": { + "Unit": "Wh", + "Value": 25507686 + } } -} \ No newline at end of file + }, + "Head": { + "RequestArguments": { + "DataCollection": "CommonInverterData", + "DeviceClass": "Inverter", + "DeviceId": "1", + "Scope": "Device" + }, + "Status": { + "Code": 0, + "Reason": "", + "UserMessage": "" + }, + "Timestamp": "2021-10-06T21:16:59+02:00" + } +} diff --git a/tests/components/fronius/fixtures/symo/GetLoggerInfo.json b/tests/components/fronius/fixtures/symo/GetLoggerInfo.json index f977823674e..33e6b2dce3f 100644 --- a/tests/components/fronius/fixtures/symo/GetLoggerInfo.json +++ b/tests/components/fronius/fixtures/symo/GetLoggerInfo.json @@ -1,29 +1,29 @@ { - "Body": { - "LoggerInfo": { - "CO2Factor": 0.52999997138977051, - "CO2Unit": "kg", - "CashCurrency": "EUR", - "CashFactor": 0.078000001609325409, - "DefaultLanguage": "en", - "DeliveryFactor": 0.15000000596046448, - "HWVersion": "2.4E", - "PlatformID": "wilma", - "ProductID": "fronius-datamanager-card", - "SWVersion": "3.18.7-1", - "TimezoneLocation": "Vienna", - "TimezoneName": "CEST", - "UTCOffset": 7200, - "UniqueID": "123.4567890" - } - }, - "Head": { - "RequestArguments": {}, - "Status": { - "Code": 0, - "Reason": "", - "UserMessage": "" - }, - "Timestamp": "2021-10-06T23:56:32+02:00" + "Body": { + "LoggerInfo": { + "CO2Factor": 0.52999997138977051, + "CO2Unit": "kg", + "CashCurrency": "EUR", + "CashFactor": 0.078000001609325409, + "DefaultLanguage": "en", + "DeliveryFactor": 0.15000000596046448, + "HWVersion": "2.4E", + "PlatformID": "wilma", + "ProductID": "fronius-datamanager-card", + "SWVersion": "3.18.7-1", + "TimezoneLocation": "Vienna", + "TimezoneName": "CEST", + "UTCOffset": 7200, + "UniqueID": "123.4567890" } -} \ No newline at end of file + }, + "Head": { + "RequestArguments": {}, + "Status": { + "Code": 0, + "Reason": "", + "UserMessage": "" + }, + "Timestamp": "2021-10-06T23:56:32+02:00" + } +} diff --git a/tests/components/fronius/fixtures/symo/GetMeterRealtimeData.json b/tests/components/fronius/fixtures/symo/GetMeterRealtimeData.json index 6c13116f351..2efc03922d2 100644 --- a/tests/components/fronius/fixtures/symo/GetMeterRealtimeData.json +++ b/tests/components/fronius/fixtures/symo/GetMeterRealtimeData.json @@ -1,61 +1,61 @@ { - "Body": { - "Data": { - "0": { - "Current_AC_Phase_1": 7.7549999999999999, - "Current_AC_Phase_2": 6.6799999999999997, - "Current_AC_Phase_3": 10.102, - "Details": { - "Manufacturer": "Fronius", - "Model": "Smart Meter 63A", - "Serial": "12345678" - }, - "Enable": 1, - "EnergyReactive_VArAC_Sum_Consumed": 59960790, - "EnergyReactive_VArAC_Sum_Produced": 723160, - "EnergyReal_WAC_Minus_Absolute": 35623065, - "EnergyReal_WAC_Plus_Absolute": 15303334, - "EnergyReal_WAC_Sum_Consumed": 15303334, - "EnergyReal_WAC_Sum_Produced": 35623065, - "Frequency_Phase_Average": 50, - "Meter_Location_Current": 0, - "PowerApparent_S_Phase_1": 1772.7929999999999, - "PowerApparent_S_Phase_2": 1527.048, - "PowerApparent_S_Phase_3": 2333.5619999999999, - "PowerApparent_S_Sum": 5592.5699999999997, - "PowerFactor_Phase_1": -0.98999999999999999, - "PowerFactor_Phase_2": -0.98999999999999999, - "PowerFactor_Phase_3": 0.98999999999999999, - "PowerFactor_Sum": 1, - "PowerReactive_Q_Phase_1": 51.479999999999997, - "PowerReactive_Q_Phase_2": 115.63, - "PowerReactive_Q_Phase_3": -164.24000000000001, - "PowerReactive_Q_Sum": 2.8700000000000001, - "PowerReal_P_Phase_1": 1765.55, - "PowerReal_P_Phase_2": 1515.8, - "PowerReal_P_Phase_3": 2311.2199999999998, - "PowerReal_P_Sum": 5592.5699999999997, - "TimeStamp": 1633977078, - "Visible": 1, - "Voltage_AC_PhaseToPhase_12": 395.89999999999998, - "Voltage_AC_PhaseToPhase_23": 398, - "Voltage_AC_PhaseToPhase_31": 398, - "Voltage_AC_Phase_1": 228.59999999999999, - "Voltage_AC_Phase_2": 228.59999999999999, - "Voltage_AC_Phase_3": 231 - } - } - }, - "Head": { - "RequestArguments": { - "DeviceClass": "Meter", - "Scope": "System" + "Body": { + "Data": { + "0": { + "Current_AC_Phase_1": 7.7549999999999999, + "Current_AC_Phase_2": 6.6799999999999997, + "Current_AC_Phase_3": 10.102, + "Details": { + "Manufacturer": "Fronius", + "Model": "Smart Meter 63A", + "Serial": "12345678" }, - "Status": { - "Code": 0, - "Reason": "", - "UserMessage": "" - }, - "Timestamp": "2021-10-11T20:31:18+02:00" + "Enable": 1, + "EnergyReactive_VArAC_Sum_Consumed": 59960790, + "EnergyReactive_VArAC_Sum_Produced": 723160, + "EnergyReal_WAC_Minus_Absolute": 35623065, + "EnergyReal_WAC_Plus_Absolute": 15303334, + "EnergyReal_WAC_Sum_Consumed": 15303334, + "EnergyReal_WAC_Sum_Produced": 35623065, + "Frequency_Phase_Average": 50, + "Meter_Location_Current": 0, + "PowerApparent_S_Phase_1": 1772.7929999999999, + "PowerApparent_S_Phase_2": 1527.048, + "PowerApparent_S_Phase_3": 2333.5619999999999, + "PowerApparent_S_Sum": 5592.5699999999997, + "PowerFactor_Phase_1": -0.98999999999999999, + "PowerFactor_Phase_2": -0.98999999999999999, + "PowerFactor_Phase_3": 0.98999999999999999, + "PowerFactor_Sum": 1, + "PowerReactive_Q_Phase_1": 51.479999999999997, + "PowerReactive_Q_Phase_2": 115.63, + "PowerReactive_Q_Phase_3": -164.24000000000001, + "PowerReactive_Q_Sum": 2.8700000000000001, + "PowerReal_P_Phase_1": 1765.55, + "PowerReal_P_Phase_2": 1515.8, + "PowerReal_P_Phase_3": 2311.2199999999998, + "PowerReal_P_Sum": 5592.5699999999997, + "TimeStamp": 1633977078, + "Visible": 1, + "Voltage_AC_PhaseToPhase_12": 395.89999999999998, + "Voltage_AC_PhaseToPhase_23": 398, + "Voltage_AC_PhaseToPhase_31": 398, + "Voltage_AC_Phase_1": 228.59999999999999, + "Voltage_AC_Phase_2": 228.59999999999999, + "Voltage_AC_Phase_3": 231 + } } -} \ No newline at end of file + }, + "Head": { + "RequestArguments": { + "DeviceClass": "Meter", + "Scope": "System" + }, + "Status": { + "Code": 0, + "Reason": "", + "UserMessage": "" + }, + "Timestamp": "2021-10-11T20:31:18+02:00" + } +} diff --git a/tests/components/fronius/fixtures/symo/GetOhmPilotRealtimeData.json b/tests/components/fronius/fixtures/symo/GetOhmPilotRealtimeData.json index 38cbde318ab..457f9981b11 100644 --- a/tests/components/fronius/fixtures/symo/GetOhmPilotRealtimeData.json +++ b/tests/components/fronius/fixtures/symo/GetOhmPilotRealtimeData.json @@ -1,17 +1,17 @@ { - "Body" : { - "Data" : {} - }, - "Head" : { - "RequestArguments" : { - "DeviceClass" : "OhmPilot", - "Scope" : "System" - }, - "Status" : { - "Code" : 0, - "Reason" : "", - "UserMessage" : "" - }, - "Timestamp" : "2021-11-28T22:36:04+01:00" - } + "Body": { + "Data": {} + }, + "Head": { + "RequestArguments": { + "DeviceClass": "OhmPilot", + "Scope": "System" + }, + "Status": { + "Code": 0, + "Reason": "", + "UserMessage": "" + }, + "Timestamp": "2021-11-28T22:36:04+01:00" + } } diff --git a/tests/components/fronius/fixtures/symo/GetPowerFlowRealtimeData.json b/tests/components/fronius/fixtures/symo/GetPowerFlowRealtimeData.json index 03e6a74ecf1..54f31536526 100644 --- a/tests/components/fronius/fixtures/symo/GetPowerFlowRealtimeData.json +++ b/tests/components/fronius/fixtures/symo/GetPowerFlowRealtimeData.json @@ -1,38 +1,38 @@ { - "Body": { - "Data": { - "Inverters": { - "1": { - "DT": 121, - "E_Day": 1101.7000732421875, - "E_Total": 44188000, - "E_Year": 25508788, - "P": 1111 - } - }, - "Site": { - "E_Day": 1101.7000732421875, - "E_Total": 44188000, - "E_Year": 25508788, - "Meter_Location": "grid", - "Mode": "meter", - "P_Akku": null, - "P_Grid": 1703.74, - "P_Load": -2814.7399999999998, - "P_PV": 1111, - "rel_Autonomy": 39.4707859340472, - "rel_SelfConsumption": 100 - }, - "Version": "12" + "Body": { + "Data": { + "Inverters": { + "1": { + "DT": 121, + "E_Day": 1101.7000732421875, + "E_Total": 44188000, + "E_Year": 25508788, + "P": 1111 } - }, - "Head": { - "RequestArguments": {}, - "Status": { - "Code": 0, - "Reason": "", - "UserMessage": "" - }, - "Timestamp": "2021-10-07T10:00:43+02:00" + }, + "Site": { + "E_Day": 1101.7000732421875, + "E_Total": 44188000, + "E_Year": 25508788, + "Meter_Location": "grid", + "Mode": "meter", + "P_Akku": null, + "P_Grid": 1703.74, + "P_Load": -2814.7399999999998, + "P_PV": 1111, + "rel_Autonomy": 39.4707859340472, + "rel_SelfConsumption": 100 + }, + "Version": "12" } -} \ No newline at end of file + }, + "Head": { + "RequestArguments": {}, + "Status": { + "Code": 0, + "Reason": "", + "UserMessage": "" + }, + "Timestamp": "2021-10-07T10:00:43+02:00" + } +} diff --git a/tests/components/fronius/fixtures/symo/GetPowerFlowRealtimeData_night.json b/tests/components/fronius/fixtures/symo/GetPowerFlowRealtimeData_night.json index 141033fe4f2..7afd933802d 100644 --- a/tests/components/fronius/fixtures/symo/GetPowerFlowRealtimeData_night.json +++ b/tests/components/fronius/fixtures/symo/GetPowerFlowRealtimeData_night.json @@ -1,38 +1,38 @@ { - "Body": { - "Data": { - "Inverters": { - "1": { - "DT": 121, - "E_Day": 10828, - "E_Total": 44186900, - "E_Year": 25507686, - "P": 0 - } - }, - "Site": { - "E_Day": 10828, - "E_Total": 44186900, - "E_Year": 25507686, - "Meter_Location": "grid", - "Mode": "meter", - "P_Akku": null, - "P_Grid": 975.30999999999995, - "P_Load": -975.30999999999995, - "P_PV": null, - "rel_Autonomy": 0, - "rel_SelfConsumption": null - }, - "Version": "12" + "Body": { + "Data": { + "Inverters": { + "1": { + "DT": 121, + "E_Day": 10828, + "E_Total": 44186900, + "E_Year": 25507686, + "P": 0 } - }, - "Head": { - "RequestArguments": {}, - "Status": { - "Code": 0, - "Reason": "", - "UserMessage": "" - }, - "Timestamp": "2021-10-06T23:49:54+02:00" + }, + "Site": { + "E_Day": 10828, + "E_Total": 44186900, + "E_Year": 25507686, + "Meter_Location": "grid", + "Mode": "meter", + "P_Akku": null, + "P_Grid": 975.30999999999995, + "P_Load": -975.30999999999995, + "P_PV": null, + "rel_Autonomy": 0, + "rel_SelfConsumption": null + }, + "Version": "12" } -} \ No newline at end of file + }, + "Head": { + "RequestArguments": {}, + "Status": { + "Code": 0, + "Reason": "", + "UserMessage": "" + }, + "Timestamp": "2021-10-06T23:49:54+02:00" + } +} diff --git a/tests/components/fronius/fixtures/symo/GetStorageRealtimeData.json b/tests/components/fronius/fixtures/symo/GetStorageRealtimeData.json index db4ab288683..358ee192c4e 100644 --- a/tests/components/fronius/fixtures/symo/GetStorageRealtimeData.json +++ b/tests/components/fronius/fixtures/symo/GetStorageRealtimeData.json @@ -1,14 +1,14 @@ { - "Body": { - "Data": {} + "Body": { + "Data": {} + }, + "Head": { + "RequestArguments": {}, + "Status": { + "Code": 255, + "Reason": "GetStorageRealtimeData request is not supported by this device.", + "UserMessage": "" }, - "Head": { - "RequestArguments": {}, - "Status": { - "Code": 255, - "Reason": "GetStorageRealtimeData request is not supported by this device.", - "UserMessage": "" - }, - "Timestamp": "2021-10-22T06:50:22+02:00" - } -} \ No newline at end of file + "Timestamp": "2021-10-22T06:50:22+02:00" + } +} diff --git a/tests/components/fronius/test_config_flow.py b/tests/components/fronius/test_config_flow.py index c6f2f69ce5f..256d64d4cbe 100644 --- a/tests/components/fronius/test_config_flow.py +++ b/tests/components/fronius/test_config_flow.py @@ -7,17 +7,15 @@ import pytest from homeassistant import config_entries from homeassistant.components.dhcp import DhcpServiceInfo from homeassistant.components.fronius.const import DOMAIN -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN -from homeassistant.const import CONF_HOST, CONF_RESOURCE +from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import ( RESULT_TYPE_ABORT, RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM, ) -from homeassistant.setup import async_setup_component -from . import MOCK_HOST, mock_responses +from . import mock_responses from tests.common import MockConfigEntry @@ -260,32 +258,6 @@ async def test_form_updates_host(hass, aioclient_mock): } -async def test_import(hass, aioclient_mock): - """Test import step.""" - mock_responses(aioclient_mock) - assert await async_setup_component( - hass, - SENSOR_DOMAIN, - { - SENSOR_DOMAIN: { - "platform": DOMAIN, - CONF_RESOURCE: MOCK_HOST, - } - }, - ) - await hass.async_block_till_done() - - fronius_entries = hass.config_entries.async_entries(DOMAIN) - assert len(fronius_entries) == 1 - - test_entry = fronius_entries[0] - assert test_entry.unique_id == "123.4567890" # has to match mocked logger unique_id - assert test_entry.data == { - "host": MOCK_HOST, - "is_logger": True, - } - - async def test_dhcp(hass, aioclient_mock): """Test starting a flow from discovery.""" with patch( diff --git a/tests/components/generic/conftest.py b/tests/components/generic/conftest.py index 04de1aedca9..9daa3574e6e 100644 --- a/tests/components/generic/conftest.py +++ b/tests/components/generic/conftest.py @@ -1,9 +1,14 @@ """Test fixtures for the generic component.""" from io import BytesIO +from unittest.mock import Mock, patch from PIL import Image import pytest +import respx + +from homeassistant import config_entries, setup +from homeassistant.components.generic.const import DOMAIN @pytest.fixture(scope="package") @@ -29,3 +34,48 @@ def fakeimgbytes_svg(): '', encoding="utf-8", ) + + +@pytest.fixture(scope="package") +def fakeimgbytes_gif(): + """Fake image in RAM for testing.""" + buf = BytesIO() # fake image in ram for testing. + Image.new("RGB", (1, 1)).save(buf, format="gif") + yield bytes(buf.getbuffer()) + + +@pytest.fixture +def fakeimg_png(fakeimgbytes_png): + """Set up respx to respond to test url with fake image bytes.""" + respx.get("http://127.0.0.1/testurl/1").respond(stream=fakeimgbytes_png) + + +@pytest.fixture +def fakeimg_gif(fakeimgbytes_gif): + """Set up respx to respond to test url with fake image bytes.""" + respx.get("http://127.0.0.1/testurl/1").respond(stream=fakeimgbytes_gif) + + +@pytest.fixture(scope="package") +def mock_av_open(): + """Fake container object with .streams.video[0] != None.""" + fake = Mock() + fake.streams.video = ["fakevid"] + return patch( + "homeassistant.components.generic.config_flow.av.open", + return_value=fake, + ) + + +@pytest.fixture +async def user_flow(hass): + """Initiate a user flow.""" + + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] == {} + + return result diff --git a/tests/components/generic/sample1_animate.png b/tests/components/generic/sample1_animate.png new file mode 100644 index 00000000000..d59a744f735 Binary files /dev/null and b/tests/components/generic/sample1_animate.png differ diff --git a/tests/components/generic/sample2_jpeg_odd_header.jpg b/tests/components/generic/sample2_jpeg_odd_header.jpg new file mode 100644 index 00000000000..80372d4edd6 Binary files /dev/null and b/tests/components/generic/sample2_jpeg_odd_header.jpg differ diff --git a/tests/components/generic/sample3_jpeg_odd_header.jpg b/tests/components/generic/sample3_jpeg_odd_header.jpg new file mode 100644 index 00000000000..ecc18d7ad9e Binary files /dev/null and b/tests/components/generic/sample3_jpeg_odd_header.jpg differ diff --git a/tests/components/generic/sample4_K5-60mileAnim-320x240.gif b/tests/components/generic/sample4_K5-60mileAnim-320x240.gif new file mode 100644 index 00000000000..b6b3ba5eff5 Binary files /dev/null and b/tests/components/generic/sample4_K5-60mileAnim-320x240.gif differ diff --git a/tests/components/generic/test_camera.py b/tests/components/generic/test_camera.py index 042cd2ee650..5a96391e10e 100644 --- a/tests/components/generic/test_camera.py +++ b/tests/components/generic/test_camera.py @@ -8,47 +8,47 @@ import httpx import pytest import respx -from homeassistant import config as hass_config from homeassistant.components.camera import async_get_mjpeg_stream -from homeassistant.components.generic import DOMAIN from homeassistant.components.websocket_api.const import TYPE_RESULT -from homeassistant.const import SERVICE_RELOAD +from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.setup import async_setup_component -from tests.common import AsyncMock, Mock, get_fixture_path +from tests.common import AsyncMock, Mock @respx.mock -async def test_fetching_url(hass, hass_client, fakeimgbytes_png): +async def test_fetching_url(hass, hass_client, fakeimgbytes_png, mock_av_open): """Test that it fetches the given url.""" respx.get("http://example.com").respond(stream=fakeimgbytes_png) - await async_setup_component( - hass, - "camera", - { - "camera": { - "name": "config_test", - "platform": "generic", - "still_image_url": "http://example.com", - "username": "user", - "password": "pass", - } - }, - ) - await hass.async_block_till_done() + with mock_av_open: + await async_setup_component( + hass, + "camera", + { + "camera": { + "name": "config_test", + "platform": "generic", + "still_image_url": "http://example.com", + "username": "user", + "password": "pass", + "authentication": "basic", + } + }, + ) + await hass.async_block_till_done() client = await hass_client() resp = await client.get("/api/camera_proxy/camera.config_test") assert resp.status == HTTPStatus.OK - assert respx.calls.call_count == 1 + assert respx.calls.call_count == 2 body = await resp.read() assert body == fakeimgbytes_png resp = await client.get("/api/camera_proxy/camera.config_test") - assert respx.calls.call_count == 2 + assert respx.calls.call_count == 3 @respx.mock @@ -110,11 +110,14 @@ async def test_fetching_url_with_verify_ssl(hass, hass_client, fakeimgbytes_png) @respx.mock async def test_limit_refetch(hass, hass_client, fakeimgbytes_png, fakeimgbytes_jpg): """Test that it fetches the given url.""" + respx.get("http://example.com/0a").respond(stream=fakeimgbytes_png) respx.get("http://example.com/5a").respond(stream=fakeimgbytes_png) respx.get("http://example.com/10a").respond(stream=fakeimgbytes_png) respx.get("http://example.com/15a").respond(stream=fakeimgbytes_jpg) respx.get("http://example.com/20a").respond(status_code=HTTPStatus.NOT_FOUND) + hass.states.async_set("sensor.temp", "0") + await async_setup_component( hass, "camera", @@ -140,19 +143,19 @@ async def test_limit_refetch(hass, hass_client, fakeimgbytes_png, fakeimgbytes_j ): resp = await client.get("/api/camera_proxy/camera.config_test") - assert respx.calls.call_count == 0 - assert resp.status == HTTPStatus.INTERNAL_SERVER_ERROR + assert respx.calls.call_count == 2 + assert resp.status == HTTPStatus.OK hass.states.async_set("sensor.temp", "10") resp = await client.get("/api/camera_proxy/camera.config_test") - assert respx.calls.call_count == 1 + assert respx.calls.call_count == 3 assert resp.status == HTTPStatus.OK body = await resp.read() assert body == fakeimgbytes_png resp = await client.get("/api/camera_proxy/camera.config_test") - assert respx.calls.call_count == 1 + assert respx.calls.call_count == 3 assert resp.status == HTTPStatus.OK body = await resp.read() assert body == fakeimgbytes_png @@ -161,7 +164,7 @@ async def test_limit_refetch(hass, hass_client, fakeimgbytes_png, fakeimgbytes_j # Url change = fetch new image resp = await client.get("/api/camera_proxy/camera.config_test") - assert respx.calls.call_count == 2 + assert respx.calls.call_count == 4 assert resp.status == HTTPStatus.OK body = await resp.read() assert body == fakeimgbytes_jpg @@ -169,31 +172,37 @@ async def test_limit_refetch(hass, hass_client, fakeimgbytes_png, fakeimgbytes_j # Cause a template render error hass.states.async_remove("sensor.temp") resp = await client.get("/api/camera_proxy/camera.config_test") - assert respx.calls.call_count == 2 + assert respx.calls.call_count == 4 assert resp.status == HTTPStatus.OK body = await resp.read() assert body == fakeimgbytes_jpg -async def test_stream_source(hass, hass_client, hass_ws_client, fakeimgbytes_png): +@respx.mock +async def test_stream_source( + hass, hass_client, hass_ws_client, fakeimgbytes_png, mock_av_open +): """Test that the stream source is rendered.""" respx.get("http://example.com").respond(stream=fakeimgbytes_png) + respx.get("http://example.com/0a").respond(stream=fakeimgbytes_png) - assert await async_setup_component( - hass, - "camera", - { - "camera": { - "name": "config_test", - "platform": "generic", - "still_image_url": "https://example.com", - "stream_source": 'http://example.com/{{ states.sensor.temp.state + "a" }}', - "limit_refetch_to_url_change": True, + hass.states.async_set("sensor.temp", "0") + with mock_av_open: + assert await async_setup_component( + hass, + "camera", + { + "camera": { + "name": "config_test", + "platform": "generic", + "still_image_url": "http://example.com", + "stream_source": 'http://example.com/{{ states.sensor.temp.state + "a" }}', + "limit_refetch_to_url_change": True, + }, }, - }, - ) - assert await async_setup_component(hass, "stream", {}) - await hass.async_block_till_done() + ) + assert await async_setup_component(hass, "stream", {}) + await hass.async_block_till_done() hass.states.async_set("sensor.temp", "5") @@ -217,26 +226,30 @@ async def test_stream_source(hass, hass_client, hass_ws_client, fakeimgbytes_png assert msg["result"]["url"][-13:] == "playlist.m3u8" -async def test_stream_source_error(hass, hass_client, hass_ws_client, fakeimgbytes_png): +@respx.mock +async def test_stream_source_error( + hass, hass_client, hass_ws_client, fakeimgbytes_png, mock_av_open +): """Test that the stream source has an error.""" respx.get("http://example.com").respond(stream=fakeimgbytes_png) - assert await async_setup_component( - hass, - "camera", - { - "camera": { - "name": "config_test", - "platform": "generic", - "still_image_url": "https://example.com", - # Does not exist - "stream_source": 'http://example.com/{{ states.sensor.temp.state + "a" }}', - "limit_refetch_to_url_change": True, + with mock_av_open: + assert await async_setup_component( + hass, + "camera", + { + "camera": { + "name": "config_test", + "platform": "generic", + "still_image_url": "http://example.com", + # Does not exist + "stream_source": 'http://example.com/{{ states.sensor.temp.state + "a" }}', + "limit_refetch_to_url_change": True, + }, }, - }, - ) - assert await async_setup_component(hass, "stream", {}) - await hass.async_block_till_done() + ) + assert await async_setup_component(hass, "stream", {}) + await hass.async_block_till_done() with patch( "homeassistant.components.camera.Stream.endpoint_url", @@ -261,31 +274,38 @@ async def test_stream_source_error(hass, hass_client, hass_ws_client, fakeimgbyt } -async def test_setup_alternative_options(hass, hass_ws_client): +@respx.mock +async def test_setup_alternative_options( + hass, hass_ws_client, fakeimgbytes_png, mock_av_open +): """Test that the stream source is setup with different config options.""" - assert await async_setup_component( - hass, - "camera", - { - "camera": { - "name": "config_test", - "platform": "generic", - "still_image_url": "https://example.com", - "authentication": "digest", - "username": "user", - "password": "pass", - "stream_source": "rtsp://example.com:554/rtsp/", - "rtsp_transport": "udp", + respx.get("https://example.com").respond(stream=fakeimgbytes_png) + + with mock_av_open: + assert await async_setup_component( + hass, + "camera", + { + "camera": { + "name": "config_test", + "platform": "generic", + "still_image_url": "https://example.com", + "authentication": "digest", + "username": "user", + "password": "pass", + "stream_source": "rtsp://example.com:554/rtsp/", + "rtsp_transport": "udp", + }, }, - }, - ) - await hass.async_block_till_done() - assert hass.data["camera"].get_entity("camera.config_test") + ) + await hass.async_block_till_done() + assert hass.states.get("camera.config_test") +@respx.mock async def test_no_stream_source(hass, hass_client, hass_ws_client, fakeimgbytes_png): """Test a stream request without stream source option set.""" - respx.get("http://example.com").respond(stream=fakeimgbytes_png) + respx.get("https://example.com").respond(stream=fakeimgbytes_png) assert await async_setup_component( hass, @@ -326,7 +346,7 @@ async def test_no_stream_source(hass, hass_client, hass_ws_client, fakeimgbytes_ @respx.mock async def test_camera_content_type( - hass, hass_client, fakeimgbytes_svg, fakeimgbytes_jpg + hass, hass_client, fakeimgbytes_svg, fakeimgbytes_jpg, mock_av_open ): """Test generic camera with custom content_type.""" urlsvg = "https://upload.wikimedia.org/wikipedia/commons/0/02/SVG_logo.svg" @@ -338,90 +358,54 @@ async def test_camera_content_type( "platform": "generic", "still_image_url": urlsvg, "content_type": "image/svg+xml", + "limit_refetch_to_url_change": False, + "framerate": 2, + "verify_ssl": True, } cam_config_jpg = { "name": "config_test_jpg", "platform": "generic", "still_image_url": urljpg, "content_type": "image/jpeg", + "limit_refetch_to_url_change": False, + "framerate": 2, + "verify_ssl": True, } - await async_setup_component( - hass, "camera", {"camera": [cam_config_svg, cam_config_jpg]} - ) - await hass.async_block_till_done() + with mock_av_open: + result1 = await hass.config_entries.flow.async_init( + "generic", + data=cam_config_jpg, + context={"source": SOURCE_IMPORT, "unique_id": 12345}, + ) + await hass.async_block_till_done() + with mock_av_open: + result2 = await hass.config_entries.flow.async_init( + "generic", + data=cam_config_svg, + context={"source": SOURCE_IMPORT, "unique_id": 54321}, + ) + await hass.async_block_till_done() + assert result1["type"] == "create_entry" + assert result2["type"] == "create_entry" client = await hass_client() resp_1 = await client.get("/api/camera_proxy/camera.config_test_svg") - assert respx.calls.call_count == 1 + assert respx.calls.call_count == 3 assert resp_1.status == HTTPStatus.OK assert resp_1.content_type == "image/svg+xml" body = await resp_1.read() assert body == fakeimgbytes_svg resp_2 = await client.get("/api/camera_proxy/camera.config_test_jpg") - assert respx.calls.call_count == 2 + assert respx.calls.call_count == 4 assert resp_2.status == HTTPStatus.OK assert resp_2.content_type == "image/jpeg" body = await resp_2.read() assert body == fakeimgbytes_jpg -@respx.mock -async def test_reloading(hass, hass_client): - """Test we can cleanly reload.""" - respx.get("http://example.com").respond(text="hello world") - - await async_setup_component( - hass, - "camera", - { - "camera": { - "name": "config_test", - "platform": "generic", - "still_image_url": "http://example.com", - "username": "user", - "password": "pass", - } - }, - ) - await hass.async_block_till_done() - - client = await hass_client() - - resp = await client.get("/api/camera_proxy/camera.config_test") - - assert resp.status == HTTPStatus.OK - assert respx.calls.call_count == 1 - body = await resp.text() - assert body == "hello world" - - yaml_path = get_fixture_path("configuration.yaml", "generic") - - with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path): - await hass.services.async_call( - DOMAIN, - SERVICE_RELOAD, - {}, - blocking=True, - ) - await hass.async_block_till_done() - - assert len(hass.states.async_all()) == 1 - - resp = await client.get("/api/camera_proxy/camera.config_test") - - assert resp.status == HTTPStatus.NOT_FOUND - - resp = await client.get("/api/camera_proxy/camera.reload") - - assert resp.status == HTTPStatus.OK - assert respx.calls.call_count == 2 - body = await resp.text() - assert body == "hello world" - - @respx.mock async def test_timeout_cancelled(hass, hass_client, fakeimgbytes_png, fakeimgbytes_jpg): """Test that timeouts and cancellations return last image.""" @@ -448,7 +432,7 @@ async def test_timeout_cancelled(hass, hass_client, fakeimgbytes_png, fakeimgbyt resp = await client.get("/api/camera_proxy/camera.config_test") assert resp.status == HTTPStatus.OK - assert respx.calls.call_count == 1 + assert respx.calls.call_count == 2 assert await resp.read() == fakeimgbytes_png respx.get("http://example.com").respond(stream=fakeimgbytes_jpg) @@ -458,7 +442,7 @@ async def test_timeout_cancelled(hass, hass_client, fakeimgbytes_png, fakeimgbyt side_effect=asyncio.CancelledError(), ): resp = await client.get("/api/camera_proxy/camera.config_test") - assert respx.calls.call_count == 1 + assert respx.calls.call_count == 2 assert resp.status == HTTPStatus.INTERNAL_SERVER_ERROR respx.get("http://example.com").side_effect = [ @@ -466,27 +450,28 @@ async def test_timeout_cancelled(hass, hass_client, fakeimgbytes_png, fakeimgbyt httpx.TimeoutException, ] - for total_calls in range(2, 3): + for total_calls in range(3, 5): resp = await client.get("/api/camera_proxy/camera.config_test") assert respx.calls.call_count == total_calls assert resp.status == HTTPStatus.OK assert await resp.read() == fakeimgbytes_png -async def test_no_still_image_url(hass, hass_client): +async def test_no_still_image_url(hass, hass_client, mock_av_open): """Test that the component can grab images from stream with no still_image_url.""" - assert await async_setup_component( - hass, - "camera", - { - "camera": { - "name": "config_test", - "platform": "generic", - "stream_source": "rtsp://example.com:554/rtsp/", + with mock_av_open: + assert await async_setup_component( + hass, + "camera", + { + "camera": { + "name": "config_test", + "platform": "generic", + "stream_source": "rtsp://example.com:554/rtsp/", + }, }, - }, - ) - await hass.async_block_till_done() + ) + await hass.async_block_till_done() client = await hass_client() @@ -518,22 +503,23 @@ async def test_no_still_image_url(hass, hass_client): assert await resp.read() == b"stream_keyframe_image" -async def test_frame_interval_property(hass): +async def test_frame_interval_property(hass, mock_av_open): """Test that the frame interval is calculated and returned correctly.""" - await async_setup_component( - hass, - "camera", - { - "camera": { - "name": "config_test", - "platform": "generic", - "stream_source": "rtsp://example.com:554/rtsp/", - "framerate": 5, + with mock_av_open: + await async_setup_component( + hass, + "camera", + { + "camera": { + "name": "config_test", + "platform": "generic", + "stream_source": "rtsp://example.com:554/rtsp/", + "framerate": 5, + }, }, - }, - ) - await hass.async_block_till_done() + ) + await hass.async_block_till_done() request = Mock() with patch( diff --git a/tests/components/generic/test_config_flow.py b/tests/components/generic/test_config_flow.py new file mode 100644 index 00000000000..7e6c55dafff --- /dev/null +++ b/tests/components/generic/test_config_flow.py @@ -0,0 +1,562 @@ +"""Test The generic (IP Camera) config flow.""" + +import errno +import os.path +from unittest.mock import patch + +import av +import httpx +import pytest +import respx + +from homeassistant import config_entries, data_entry_flow, setup +from homeassistant.components.generic.const import ( + CONF_CONTENT_TYPE, + CONF_FRAMERATE, + CONF_LIMIT_REFETCH_TO_URL_CHANGE, + CONF_RTSP_TRANSPORT, + CONF_STILL_IMAGE_URL, + CONF_STREAM_SOURCE, + DOMAIN, +) +from homeassistant.const import ( + CONF_AUTHENTICATION, + CONF_NAME, + CONF_PASSWORD, + CONF_USERNAME, + CONF_VERIFY_SSL, + HTTP_BASIC_AUTHENTICATION, +) + +from tests.common import MockConfigEntry + +TESTDATA = { + CONF_STILL_IMAGE_URL: "http://127.0.0.1/testurl/1", + CONF_STREAM_SOURCE: "http://127.0.0.1/testurl/2", + CONF_AUTHENTICATION: HTTP_BASIC_AUTHENTICATION, + CONF_USERNAME: "fred_flintstone", + CONF_PASSWORD: "bambam", + CONF_FRAMERATE: 5, + CONF_VERIFY_SSL: False, +} + +TESTDATA_OPTIONS = { + CONF_LIMIT_REFETCH_TO_URL_CHANGE: False, + **TESTDATA, +} + +TESTDATA_YAML = { + CONF_NAME: "Yaml Defined Name", + **TESTDATA, +} + + +@respx.mock +async def test_form(hass, fakeimg_png, mock_av_open, user_flow): + """Test the form with a normal set of settings.""" + + with mock_av_open as mock_setup: + result2 = await hass.config_entries.flow.async_configure( + user_flow["flow_id"], + TESTDATA, + ) + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["title"] == "127_0_0_1_testurl_1" + assert result2["options"] == { + CONF_STILL_IMAGE_URL: "http://127.0.0.1/testurl/1", + CONF_STREAM_SOURCE: "http://127.0.0.1/testurl/2", + CONF_AUTHENTICATION: HTTP_BASIC_AUTHENTICATION, + CONF_USERNAME: "fred_flintstone", + CONF_PASSWORD: "bambam", + CONF_LIMIT_REFETCH_TO_URL_CHANGE: False, + CONF_CONTENT_TYPE: "image/png", + CONF_FRAMERATE: 5, + CONF_VERIFY_SSL: False, + } + + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + + +@respx.mock +async def test_form_only_stillimage(hass, fakeimg_png, user_flow): + """Test we complete ok if the user wants still images only.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] == {} + + data = TESTDATA.copy() + data.pop(CONF_STREAM_SOURCE) + result2 = await hass.config_entries.flow.async_configure( + user_flow["flow_id"], + data, + ) + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["title"] == "127_0_0_1_testurl_1" + assert result2["options"] == { + CONF_STILL_IMAGE_URL: "http://127.0.0.1/testurl/1", + CONF_AUTHENTICATION: HTTP_BASIC_AUTHENTICATION, + CONF_USERNAME: "fred_flintstone", + CONF_PASSWORD: "bambam", + CONF_LIMIT_REFETCH_TO_URL_CHANGE: False, + CONF_CONTENT_TYPE: "image/png", + CONF_FRAMERATE: 5, + CONF_VERIFY_SSL: False, + } + + await hass.async_block_till_done() + assert respx.calls.call_count == 1 + + +@respx.mock +async def test_form_only_stillimage_gif(hass, fakeimg_gif, user_flow): + """Test we complete ok if the user wants a gif.""" + data = TESTDATA.copy() + data.pop(CONF_STREAM_SOURCE) + result2 = await hass.config_entries.flow.async_configure( + user_flow["flow_id"], + data, + ) + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["options"][CONF_CONTENT_TYPE] == "image/gif" + + +@respx.mock +async def test_form_only_svg_whitespace(hass, fakeimgbytes_svg, user_flow): + """Test we complete ok if svg starts with whitespace, issue #68889.""" + fakeimgbytes_wspace_svg = bytes(" \n ", encoding="utf-8") + fakeimgbytes_svg + respx.get("http://127.0.0.1/testurl/1").respond(stream=fakeimgbytes_wspace_svg) + data = TESTDATA.copy() + data.pop(CONF_STREAM_SOURCE) + result2 = await hass.config_entries.flow.async_configure( + user_flow["flow_id"], + data, + ) + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + +@respx.mock +@pytest.mark.parametrize( + "image_file", + [ + ("sample1_animate.png"), + ("sample2_jpeg_odd_header.jpg"), + ("sample3_jpeg_odd_header.jpg"), + ("sample4_K5-60mileAnim-320x240.gif"), + ], +) +async def test_form_only_still_sample(hass, user_flow, image_file): + """Test various sample images #69037.""" + image_path = os.path.join(os.path.dirname(__file__), image_file) + with open(image_path, "rb") as image: + respx.get("http://127.0.0.1/testurl/1").respond(stream=image.read()) + data = TESTDATA.copy() + data.pop(CONF_STREAM_SOURCE) + result2 = await hass.config_entries.flow.async_configure( + user_flow["flow_id"], + data, + ) + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + +@respx.mock +async def test_form_rtsp_mode(hass, fakeimg_png, mock_av_open, user_flow): + """Test we complete ok if the user enters a stream url.""" + with mock_av_open as mock_setup: + data = TESTDATA + data[CONF_RTSP_TRANSPORT] = "tcp" + result2 = await hass.config_entries.flow.async_configure( + user_flow["flow_id"], data + ) + assert "errors" not in result2, f"errors={result2['errors']}" + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["title"] == "127_0_0_1_testurl_1" + assert result2["options"] == { + CONF_STILL_IMAGE_URL: "http://127.0.0.1/testurl/1", + CONF_AUTHENTICATION: HTTP_BASIC_AUTHENTICATION, + CONF_STREAM_SOURCE: "http://127.0.0.1/testurl/2", + CONF_RTSP_TRANSPORT: "tcp", + CONF_USERNAME: "fred_flintstone", + CONF_PASSWORD: "bambam", + CONF_LIMIT_REFETCH_TO_URL_CHANGE: False, + CONF_CONTENT_TYPE: "image/png", + CONF_FRAMERATE: 5, + CONF_VERIFY_SSL: False, + } + + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + + +async def test_form_only_stream(hass, mock_av_open): + """Test we complete ok if the user wants stream only.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + data = TESTDATA.copy() + data.pop(CONF_STILL_IMAGE_URL) + with mock_av_open as mock_setup: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + data, + ) + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["title"] == "127_0_0_1_testurl_2" + assert result2["options"] == { + CONF_AUTHENTICATION: HTTP_BASIC_AUTHENTICATION, + CONF_STREAM_SOURCE: "http://127.0.0.1/testurl/2", + CONF_RTSP_TRANSPORT: "tcp", + CONF_USERNAME: "fred_flintstone", + CONF_PASSWORD: "bambam", + CONF_LIMIT_REFETCH_TO_URL_CHANGE: False, + CONF_CONTENT_TYPE: None, + CONF_FRAMERATE: 5, + CONF_VERIFY_SSL: False, + } + + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + + +async def test_form_still_and_stream_not_provided(hass, user_flow): + """Test we show a suitable error if neither still or stream URL are provided.""" + result2 = await hass.config_entries.flow.async_configure( + user_flow["flow_id"], + { + CONF_AUTHENTICATION: HTTP_BASIC_AUTHENTICATION, + CONF_FRAMERATE: 5, + CONF_VERIFY_SSL: False, + }, + ) + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["errors"] == {"base": "no_still_image_or_stream_url"} + + +@respx.mock +async def test_form_image_timeout(hass, mock_av_open, user_flow): + """Test we handle invalid image timeout.""" + respx.get("http://127.0.0.1/testurl/1").side_effect = [ + httpx.TimeoutException, + ] + + with mock_av_open: + result2 = await hass.config_entries.flow.async_configure( + user_flow["flow_id"], + TESTDATA, + ) + await hass.async_block_till_done() + + assert result2["type"] == "form" + assert result2["errors"] == {"still_image_url": "unable_still_load"} + + +@respx.mock +async def test_form_stream_invalidimage(hass, mock_av_open, user_flow): + """Test we handle invalid image when a stream is specified.""" + respx.get("http://127.0.0.1/testurl/1").respond(stream=b"invalid") + with mock_av_open: + result2 = await hass.config_entries.flow.async_configure( + user_flow["flow_id"], + TESTDATA, + ) + await hass.async_block_till_done() + + assert result2["type"] == "form" + assert result2["errors"] == {"still_image_url": "invalid_still_image"} + + +@respx.mock +async def test_form_stream_invalidimage2(hass, mock_av_open, user_flow): + """Test we handle invalid image when a stream is specified.""" + respx.get("http://127.0.0.1/testurl/1").respond(content=None) + with mock_av_open: + result2 = await hass.config_entries.flow.async_configure( + user_flow["flow_id"], + TESTDATA, + ) + await hass.async_block_till_done() + + assert result2["type"] == "form" + assert result2["errors"] == {"still_image_url": "unable_still_load"} + + +@respx.mock +async def test_form_stream_invalidimage3(hass, mock_av_open, user_flow): + """Test we handle invalid image when a stream is specified.""" + respx.get("http://127.0.0.1/testurl/1").respond(content=bytes([0xFF])) + with mock_av_open: + result2 = await hass.config_entries.flow.async_configure( + user_flow["flow_id"], + TESTDATA, + ) + await hass.async_block_till_done() + + assert result2["type"] == "form" + assert result2["errors"] == {"still_image_url": "invalid_still_image"} + + +@respx.mock +async def test_form_stream_file_not_found(hass, fakeimg_png, user_flow): + """Test we handle file not found.""" + with patch( + "homeassistant.components.generic.config_flow.av.open", + side_effect=av.error.FileNotFoundError(0, 0), + ): + result2 = await hass.config_entries.flow.async_configure( + user_flow["flow_id"], + TESTDATA, + ) + assert result2["type"] == "form" + assert result2["errors"] == {"stream_source": "stream_file_not_found"} + + +@respx.mock +async def test_form_stream_http_not_found(hass, fakeimg_png, user_flow): + """Test we handle invalid auth.""" + with patch( + "homeassistant.components.generic.config_flow.av.open", + side_effect=av.error.HTTPNotFoundError(0, 0), + ): + result2 = await hass.config_entries.flow.async_configure( + user_flow["flow_id"], + TESTDATA, + ) + assert result2["type"] == "form" + assert result2["errors"] == {"stream_source": "stream_http_not_found"} + + +@respx.mock +async def test_form_stream_timeout(hass, fakeimg_png, user_flow): + """Test we handle invalid auth.""" + with patch( + "homeassistant.components.generic.config_flow.av.open", + side_effect=av.error.TimeoutError(0, 0), + ): + result2 = await hass.config_entries.flow.async_configure( + user_flow["flow_id"], + TESTDATA, + ) + assert result2["type"] == "form" + assert result2["errors"] == {"stream_source": "timeout"} + + +@respx.mock +async def test_form_stream_unauthorised(hass, fakeimg_png, user_flow): + """Test we handle invalid auth.""" + with patch( + "homeassistant.components.generic.config_flow.av.open", + side_effect=av.error.HTTPUnauthorizedError(0, 0), + ): + result2 = await hass.config_entries.flow.async_configure( + user_flow["flow_id"], + TESTDATA, + ) + assert result2["type"] == "form" + assert result2["errors"] == {"stream_source": "stream_unauthorised"} + + +@respx.mock +async def test_form_stream_novideo(hass, fakeimg_png, user_flow): + """Test we handle invalid stream.""" + with patch( + "homeassistant.components.generic.config_flow.av.open", side_effect=KeyError() + ): + result2 = await hass.config_entries.flow.async_configure( + user_flow["flow_id"], + TESTDATA, + ) + assert result2["type"] == "form" + assert result2["errors"] == {"stream_source": "stream_no_video"} + + +@respx.mock +async def test_form_stream_permission_error(hass, fakeimgbytes_png, user_flow): + """Test we handle permission error.""" + respx.get("http://127.0.0.1/testurl/1").respond(stream=fakeimgbytes_png) + with patch( + "homeassistant.components.generic.config_flow.av.open", + side_effect=PermissionError(), + ): + result2 = await hass.config_entries.flow.async_configure( + user_flow["flow_id"], + TESTDATA, + ) + assert result2["type"] == "form" + assert result2["errors"] == {"stream_source": "stream_not_permitted"} + + +@respx.mock +async def test_form_no_route_to_host(hass, fakeimg_png, user_flow): + """Test we handle no route to host.""" + with patch( + "homeassistant.components.generic.config_flow.av.open", + side_effect=OSError(errno.EHOSTUNREACH, "No route to host"), + ): + result2 = await hass.config_entries.flow.async_configure( + user_flow["flow_id"], + TESTDATA, + ) + assert result2["type"] == "form" + assert result2["errors"] == {"stream_source": "stream_no_route_to_host"} + + +@respx.mock +async def test_form_stream_io_error(hass, fakeimg_png, user_flow): + """Test we handle no io error when setting up stream.""" + with patch( + "homeassistant.components.generic.config_flow.av.open", + side_effect=OSError(errno.EIO, "Input/output error"), + ): + result2 = await hass.config_entries.flow.async_configure( + user_flow["flow_id"], + TESTDATA, + ) + assert result2["type"] == "form" + assert result2["errors"] == {"stream_source": "stream_io_error"} + + +@respx.mock +async def test_form_oserror(hass, fakeimg_png, user_flow): + """Test we handle OS error when setting up stream.""" + with patch( + "homeassistant.components.generic.config_flow.av.open", + side_effect=OSError("Some other OSError"), + ), pytest.raises(OSError): + await hass.config_entries.flow.async_configure( + user_flow["flow_id"], + TESTDATA, + ) + + +@respx.mock +async def test_options_template_error(hass, fakeimgbytes_png, mock_av_open): + """Test the options flow with a template error.""" + respx.get("http://127.0.0.1/testurl/1").respond(stream=fakeimgbytes_png) + respx.get("http://127.0.0.1/testurl/2").respond(stream=fakeimgbytes_png) + await setup.async_setup_component(hass, "persistent_notification", {}) + + mock_entry = MockConfigEntry( + title="Test Camera", + domain=DOMAIN, + data={}, + options=TESTDATA, + ) + + with mock_av_open: + mock_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + result = await hass.config_entries.options.async_init(mock_entry.entry_id) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "init" + + # try updating the still image url + data = TESTDATA.copy() + data[CONF_STILL_IMAGE_URL] = "http://127.0.0.1/testurl/2" + result2 = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input=data, + ) + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + result3 = await hass.config_entries.options.async_init(mock_entry.entry_id) + assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result3["step_id"] == "init" + + # verify that an invalid template reports the correct UI error. + data[CONF_STILL_IMAGE_URL] = "http://127.0.0.1/testurl/{{1/0}}" + result4 = await hass.config_entries.options.async_configure( + result3["flow_id"], + user_input=data, + ) + assert result4.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result4["errors"] == {"still_image_url": "template_error"} + + +# These below can be deleted after deprecation period is finished. +@respx.mock +async def test_import(hass, fakeimg_png, mock_av_open): + """Test configuration.yaml import used during migration.""" + with mock_av_open: + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=TESTDATA_YAML + ) + # duplicate import should be aborted + result2 = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=TESTDATA_YAML + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "Yaml Defined Name" + await hass.async_block_till_done() + # Any name defined in yaml should end up as the entity id. + assert hass.states.get("camera.yaml_defined_name") + assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + + +@respx.mock +async def test_import_invalid_still_image(hass, mock_av_open): + """Test configuration.yaml import used during migration.""" + respx.get("http://127.0.0.1/testurl/1").respond(stream=b"invalid") + with mock_av_open: + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=TESTDATA_YAML + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "unknown" + + +@respx.mock +async def test_import_other_error(hass, fakeimgbytes_png): + """Test that non-specific import errors are raised.""" + respx.get("http://127.0.0.1/testurl/1").respond(stream=fakeimgbytes_png) + with patch( + "homeassistant.components.generic.config_flow.av.open", + side_effect=OSError("other error"), + ), pytest.raises(OSError): + await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=TESTDATA_YAML + ) + + +# These above can be deleted after deprecation period is finished. + + +async def test_unload_entry(hass, fakeimg_png, mock_av_open): + """Test unloading the generic IP Camera entry.""" + mock_entry = MockConfigEntry(domain=DOMAIN, options=TESTDATA) + mock_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + assert mock_entry.state is config_entries.ConfigEntryState.LOADED + + await hass.config_entries.async_unload(mock_entry.entry_id) + await hass.async_block_till_done() + assert mock_entry.state is config_entries.ConfigEntryState.NOT_LOADED + + +async def test_reload_on_title_change(hass) -> None: + """Test the integration gets reloaded when the title is updated.""" + + test_data = TESTDATA_OPTIONS + test_data[CONF_CONTENT_TYPE] = "image/png" + mock_entry = MockConfigEntry( + domain=DOMAIN, unique_id="54321", options=test_data, title="My Title" + ) + mock_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + assert mock_entry.state is config_entries.ConfigEntryState.LOADED + assert hass.states.get("camera.my_title").attributes["friendly_name"] == "My Title" + + hass.config_entries.async_update_entry(mock_entry, title="New Title") + assert mock_entry.title == "New Title" + await hass.async_block_till_done() + + assert hass.states.get("camera.my_title").attributes["friendly_name"] == "New Title" diff --git a/tests/components/geo_json_events/test_geo_location.py b/tests/components/geo_json_events/test_geo_location.py index 9d68e3d4973..a445673469b 100644 --- a/tests/components/geo_json_events/test_geo_location.py +++ b/tests/components/geo_json_events/test_geo_location.py @@ -1,5 +1,7 @@ """The tests for the geojson platform.""" -from unittest.mock import MagicMock, call, patch +from unittest.mock import ANY, MagicMock, call, patch + +from aio_geojson_generic_client import GenericFeed from homeassistant.components import geo_location from homeassistant.components.geo_json_events.geo_location import ( @@ -66,9 +68,9 @@ async def test_setup(hass, legacy_patchable_time): # Patching 'utcnow' to gain more control over the timed update. utcnow = dt_util.utcnow() with patch("homeassistant.util.dt.utcnow", return_value=utcnow), patch( - "geojson_client.generic_feed.GenericFeed" - ) as mock_feed: - mock_feed.return_value.update.return_value = ( + "aio_geojson_client.feed.GeoJsonFeed.update" + ) as mock_feed_update: + mock_feed_update.return_value = ( "OK", [mock_entry_1, mock_entry_2, mock_entry_3], ) @@ -124,7 +126,7 @@ async def test_setup(hass, legacy_patchable_time): # Simulate an update - one existing, one new entry, # one outdated entry - mock_feed.return_value.update.return_value = ( + mock_feed_update.return_value = ( "OK", [mock_entry_1, mock_entry_4, mock_entry_3], ) @@ -136,7 +138,7 @@ async def test_setup(hass, legacy_patchable_time): # Simulate an update - empty data, but successful update, # so no changes to entities. - mock_feed.return_value.update.return_value = "OK_NO_DATA", None + mock_feed_update.return_value = "OK_NO_DATA", None async_fire_time_changed(hass, utcnow + 2 * SCAN_INTERVAL) await hass.async_block_till_done() @@ -144,7 +146,7 @@ async def test_setup(hass, legacy_patchable_time): assert len(all_states) == 3 # Simulate an update - empty data, removes all entities - mock_feed.return_value.update.return_value = "ERROR", None + mock_feed_update.return_value = "ERROR", None async_fire_time_changed(hass, utcnow + 3 * SCAN_INTERVAL) await hass.async_block_till_done() @@ -157,8 +159,13 @@ async def test_setup_with_custom_location(hass): # Set up some mock feed entries for this test. mock_entry_1 = _generate_mock_feed_entry("1234", "Title 1", 2000.5, (-31.1, 150.1)) - with patch("geojson_client.generic_feed.GenericFeed") as mock_feed: - mock_feed.return_value.update.return_value = "OK", [mock_entry_1] + with patch( + "aio_geojson_generic_client.feed_manager.GenericFeed", + wraps=GenericFeed, + ) as mock_feed, patch( + "aio_geojson_client.feed.GeoJsonFeed.update" + ) as mock_feed_update: + mock_feed_update.return_value = "OK", [mock_entry_1] with assert_setup_component(1, geo_location.DOMAIN): assert await async_setup_component( @@ -174,7 +181,9 @@ async def test_setup_with_custom_location(hass): all_states = hass.states.async_all() assert len(all_states) == 1 - assert mock_feed.call_args == call((15.1, 25.2), URL, filter_radius=200.0) + assert mock_feed.call_args == call( + ANY, (15.1, 25.2), URL, filter_radius=200.0 + ) async def test_setup_race_condition(hass, legacy_patchable_time): @@ -197,12 +206,12 @@ async def test_setup_race_condition(hass, legacy_patchable_time): # Patching 'utcnow' to gain more control over the timed update. utcnow = dt_util.utcnow() with patch("homeassistant.util.dt.utcnow", return_value=utcnow), patch( - "geojson_client.generic_feed.GenericFeed" - ) as mock_feed, assert_setup_component(1, geo_location.DOMAIN): + "aio_geojson_client.feed.GeoJsonFeed.update" + ) as mock_feed_update, assert_setup_component(1, geo_location.DOMAIN): assert await async_setup_component(hass, geo_location.DOMAIN, CONFIG) await hass.async_block_till_done() - mock_feed.return_value.update.return_value = "OK", [mock_entry_1] + mock_feed_update.return_value = "OK", [mock_entry_1] # Artificially trigger update. hass.bus.async_fire(EVENT_HOMEASSISTANT_START) @@ -215,7 +224,7 @@ async def test_setup_race_condition(hass, legacy_patchable_time): assert len(hass.data[DATA_DISPATCHER][update_signal]) == 1 # Simulate an update - empty data, removes all entities - mock_feed.return_value.update.return_value = "ERROR", None + mock_feed_update.return_value = "ERROR", None async_fire_time_changed(hass, utcnow + SCAN_INTERVAL) await hass.async_block_till_done() @@ -225,7 +234,7 @@ async def test_setup_race_condition(hass, legacy_patchable_time): assert len(hass.data[DATA_DISPATCHER][update_signal]) == 0 # Simulate an update - 1 entry - mock_feed.return_value.update.return_value = "OK", [mock_entry_1] + mock_feed_update.return_value = "OK", [mock_entry_1] async_fire_time_changed(hass, utcnow + 2 * SCAN_INTERVAL) await hass.async_block_till_done() @@ -235,7 +244,7 @@ async def test_setup_race_condition(hass, legacy_patchable_time): assert len(hass.data[DATA_DISPATCHER][update_signal]) == 1 # Simulate an update - 1 entry - mock_feed.return_value.update.return_value = "OK", [mock_entry_1] + mock_feed_update.return_value = "OK", [mock_entry_1] async_fire_time_changed(hass, utcnow + 3 * SCAN_INTERVAL) await hass.async_block_till_done() @@ -245,7 +254,7 @@ async def test_setup_race_condition(hass, legacy_patchable_time): assert len(hass.data[DATA_DISPATCHER][update_signal]) == 1 # Simulate an update - empty data, removes all entities - mock_feed.return_value.update.return_value = "ERROR", None + mock_feed_update.return_value = "ERROR", None async_fire_time_changed(hass, utcnow + 4 * SCAN_INTERVAL) await hass.async_block_till_done() diff --git a/tests/components/gios/fixtures/indexes.json b/tests/components/gios/fixtures/indexes.json index 4fe4293706c..cee504b0cc7 100644 --- a/tests/components/gios/fixtures/indexes.json +++ b/tests/components/gios/fixtures/indexes.json @@ -1,29 +1,29 @@ { - "id": 123, - "stCalcDate": "2020-07-31 15:10:17", - "stIndexLevel": { "id": 1, "indexLevelName": "dobry" }, - "stSourceDataDate": "2020-07-31 14:00:00", - "so2CalcDate": "2020-07-31 15:10:17", - "so2IndexLevel": { "id": 0, "indexLevelName": "bardzo dobry" }, - "so2SourceDataDate": "2020-07-31 14:00:00", - "no2CalcDate": 1596201017000, - "no2IndexLevel": { "id": 0, "indexLevelName": "dobry" }, - "no2SourceDataDate": "2020-07-31 14:00:00", - "coCalcDate": "2020-07-31 15:10:17", - "coIndexLevel": { "id": 0, "indexLevelName": "dobry" }, - "coSourceDataDate": "2020-07-31 14:00:00", - "pm10CalcDate": "2020-07-31 15:10:17", - "pm10IndexLevel": { "id": 0, "indexLevelName": "dobry" }, - "pm10SourceDataDate": "2020-07-31 14:00:00", - "pm25CalcDate": "2020-07-31 15:10:17", - "pm25IndexLevel": { "id": 0, "indexLevelName": "dobry" }, - "pm25SourceDataDate": "2020-07-31 14:00:00", - "o3CalcDate": "2020-07-31 15:10:17", - "o3IndexLevel": { "id": 1, "indexLevelName": "dobry" }, - "o3SourceDataDate": "2020-07-31 14:00:00", - "c6h6CalcDate": "2020-07-31 15:10:17", - "c6h6IndexLevel": { "id": 0, "indexLevelName": "bardzo dobry" }, - "c6h6SourceDataDate": "2020-07-31 14:00:00", - "stIndexStatus": true, - "stIndexCrParam": "OZON" - } \ No newline at end of file + "id": 123, + "stCalcDate": "2020-07-31 15:10:17", + "stIndexLevel": { "id": 1, "indexLevelName": "dobry" }, + "stSourceDataDate": "2020-07-31 14:00:00", + "so2CalcDate": "2020-07-31 15:10:17", + "so2IndexLevel": { "id": 0, "indexLevelName": "bardzo dobry" }, + "so2SourceDataDate": "2020-07-31 14:00:00", + "no2CalcDate": 1596201017000, + "no2IndexLevel": { "id": 0, "indexLevelName": "dobry" }, + "no2SourceDataDate": "2020-07-31 14:00:00", + "coCalcDate": "2020-07-31 15:10:17", + "coIndexLevel": { "id": 0, "indexLevelName": "dobry" }, + "coSourceDataDate": "2020-07-31 14:00:00", + "pm10CalcDate": "2020-07-31 15:10:17", + "pm10IndexLevel": { "id": 0, "indexLevelName": "dobry" }, + "pm10SourceDataDate": "2020-07-31 14:00:00", + "pm25CalcDate": "2020-07-31 15:10:17", + "pm25IndexLevel": { "id": 0, "indexLevelName": "dobry" }, + "pm25SourceDataDate": "2020-07-31 14:00:00", + "o3CalcDate": "2020-07-31 15:10:17", + "o3IndexLevel": { "id": 1, "indexLevelName": "dobry" }, + "o3SourceDataDate": "2020-07-31 14:00:00", + "c6h6CalcDate": "2020-07-31 15:10:17", + "c6h6IndexLevel": { "id": 0, "indexLevelName": "bardzo dobry" }, + "c6h6SourceDataDate": "2020-07-31 14:00:00", + "stIndexStatus": true, + "stIndexCrParam": "OZON" +} diff --git a/tests/components/gios/fixtures/sensors.json b/tests/components/gios/fixtures/sensors.json index 62732552172..db0cf2ff849 100644 --- a/tests/components/gios/fixtures/sensors.json +++ b/tests/components/gios/fixtures/sensors.json @@ -1,51 +1,51 @@ { - "so2": { - "values": [ - { "date": "2020-07-31 15:00:00", "value": 4.35478 }, - { "date": "2020-07-31 14:00:00", "value": 4.25478 }, - { "date": "2020-07-31 13:00:00", "value": 4.34309 } - ] - }, - "c6h6": { - "values": [ - { "date": "2020-07-31 15:00:00", "value": 0.23789 }, - { "date": "2020-07-31 14:00:00", "value": 0.22789 }, - { "date": "2020-07-31 13:00:00", "value": 0.21315 } - ] - }, - "co": { - "values": [ - { "date": "2020-07-31 15:00:00", "value": 251.874 }, - { "date": "2020-07-31 14:00:00", "value": 250.874 }, - { "date": "2020-07-31 13:00:00", "value": 251.097 } - ] - }, - "no2": { - "values": [ - { "date": "2020-07-31 15:00:00", "value": 7.13411 }, - { "date": "2020-07-31 14:00:00", "value": 7.33411 }, - { "date": "2020-07-31 13:00:00", "value": 9.32578 } - ] - }, - "o3": { - "values": [ - { "date": "2020-07-31 15:00:00", "value": 95.7768 }, - { "date": "2020-07-31 14:00:00", "value": 93.7768 }, - { "date": "2020-07-31 13:00:00", "value": 89.4232 } - ] - }, - "pm2.5": { - "values": [ - { "date": "2020-07-31 15:00:00", "value": 4 }, - { "date": "2020-07-31 14:00:00", "value": 4 }, - { "date": "2020-07-31 13:00:00", "value": 5 } - ] - }, - "pm10": { - "values": [ - { "date": "2020-07-31 15:00:00", "value": 16.8344 }, - { "date": "2020-07-31 14:00:00", "value": 17.8344 }, - { "date": "2020-07-31 13:00:00", "value": 20.8094 } - ] - } - } \ No newline at end of file + "so2": { + "values": [ + { "date": "2020-07-31 15:00:00", "value": 4.35478 }, + { "date": "2020-07-31 14:00:00", "value": 4.25478 }, + { "date": "2020-07-31 13:00:00", "value": 4.34309 } + ] + }, + "c6h6": { + "values": [ + { "date": "2020-07-31 15:00:00", "value": 0.23789 }, + { "date": "2020-07-31 14:00:00", "value": 0.22789 }, + { "date": "2020-07-31 13:00:00", "value": 0.21315 } + ] + }, + "co": { + "values": [ + { "date": "2020-07-31 15:00:00", "value": 251.874 }, + { "date": "2020-07-31 14:00:00", "value": 250.874 }, + { "date": "2020-07-31 13:00:00", "value": 251.097 } + ] + }, + "no2": { + "values": [ + { "date": "2020-07-31 15:00:00", "value": 7.13411 }, + { "date": "2020-07-31 14:00:00", "value": 7.33411 }, + { "date": "2020-07-31 13:00:00", "value": 9.32578 } + ] + }, + "o3": { + "values": [ + { "date": "2020-07-31 15:00:00", "value": 95.7768 }, + { "date": "2020-07-31 14:00:00", "value": 93.7768 }, + { "date": "2020-07-31 13:00:00", "value": 89.4232 } + ] + }, + "pm2.5": { + "values": [ + { "date": "2020-07-31 15:00:00", "value": 4 }, + { "date": "2020-07-31 14:00:00", "value": 4 }, + { "date": "2020-07-31 13:00:00", "value": 5 } + ] + }, + "pm10": { + "values": [ + { "date": "2020-07-31 15:00:00", "value": 16.8344 }, + { "date": "2020-07-31 14:00:00", "value": 17.8344 }, + { "date": "2020-07-31 13:00:00", "value": 20.8094 } + ] + } +} diff --git a/tests/components/gios/fixtures/station.json b/tests/components/gios/fixtures/station.json index 0eaa98a1d3c..16cd824a489 100644 --- a/tests/components/gios/fixtures/station.json +++ b/tests/components/gios/fixtures/station.json @@ -1,72 +1,72 @@ [ - { - "id": 672, - "stationId": 117, - "param": { - "paramName": "dwutlenek siarki", - "paramFormula": "SO2", - "paramCode": "SO2", - "idParam": 1 - } - }, - { - "id": 658, - "stationId": 117, - "param": { - "paramName": "benzen", - "paramFormula": "C6H6", - "paramCode": "C6H6", - "idParam": 10 - } - }, - { - "id": 660, - "stationId": 117, - "param": { - "paramName": "tlenek węgla", - "paramFormula": "CO", - "paramCode": "CO", - "idParam": 8 - } - }, - { - "id": 665, - "stationId": 117, - "param": { - "paramName": "dwutlenek azotu", - "paramFormula": "NO2", - "paramCode": "NO2", - "idParam": 6 - } - }, - { - "id": 667, - "stationId": 117, - "param": { - "paramName": "ozon", - "paramFormula": "O3", - "paramCode": "O3", - "idParam": 5 - } - }, - { - "id": 670, - "stationId": 117, - "param": { - "paramName": "pył zawieszony PM2.5", - "paramFormula": "PM2.5", - "paramCode": "PM2.5", - "idParam": 69 - } - }, - { - "id": 14395, - "stationId": 117, - "param": { - "paramName": "pył zawieszony PM10", - "paramFormula": "PM10", - "paramCode": "PM10", - "idParam": 3 - } + { + "id": 672, + "stationId": 117, + "param": { + "paramName": "dwutlenek siarki", + "paramFormula": "SO2", + "paramCode": "SO2", + "idParam": 1 } - ] \ No newline at end of file + }, + { + "id": 658, + "stationId": 117, + "param": { + "paramName": "benzen", + "paramFormula": "C6H6", + "paramCode": "C6H6", + "idParam": 10 + } + }, + { + "id": 660, + "stationId": 117, + "param": { + "paramName": "tlenek węgla", + "paramFormula": "CO", + "paramCode": "CO", + "idParam": 8 + } + }, + { + "id": 665, + "stationId": 117, + "param": { + "paramName": "dwutlenek azotu", + "paramFormula": "NO2", + "paramCode": "NO2", + "idParam": 6 + } + }, + { + "id": 667, + "stationId": 117, + "param": { + "paramName": "ozon", + "paramFormula": "O3", + "paramCode": "O3", + "idParam": 5 + } + }, + { + "id": 670, + "stationId": 117, + "param": { + "paramName": "pył zawieszony PM2.5", + "paramFormula": "PM2.5", + "paramCode": "PM2.5", + "idParam": 69 + } + }, + { + "id": 14395, + "stationId": 117, + "param": { + "paramName": "pył zawieszony PM10", + "paramFormula": "PM10", + "paramCode": "PM10", + "idParam": 3 + } + } +] diff --git a/tests/components/github/common.py b/tests/components/github/common.py index a75a8cfaa78..4bd26183299 100644 --- a/tests/components/github/common.py +++ b/tests/components/github/common.py @@ -31,6 +31,11 @@ async def setup_github_integration( }, headers=headers, ) + aioclient_mock.get( + f"https://api.github.com/repos/{repository}/events", + json=[], + headers=headers, + ) aioclient_mock.post( "https://api.github.com/graphql", json=json.loads(load_fixture("graphql.json", DOMAIN)), diff --git a/tests/components/github/fixtures/base_headers.json b/tests/components/github/fixtures/base_headers.json index 4360f319cf1..ae5b155b362 100644 --- a/tests/components/github/fixtures/base_headers.json +++ b/tests/components/github/fixtures/base_headers.json @@ -1,29 +1,29 @@ { - "Server": "GitHub.com", - "Date": "Mon, 1 Jan 1970 00:00:00 GMT", - "Content-Type": "application/json; charset=utf-8", - "Transfer-Encoding": "chunked", - "Cache-Control": "private, max-age=60, s-maxage=60", - "Vary": "Accept, Authorization, Cookie, X-GitHub-OTP", - "Etag": "W/\"1234567890abcdefghijklmnopqrstuvwxyz\"", - "X-OAuth-Scopes": "", - "X-Accepted-OAuth-Scopes": "", - "github-authentication-token-expiration": "1970-01-01 01:00:00 UTC", - "X-GitHub-Media-Type": "github.v3; param=raw; format=json", - "X-RateLimit-Limit": "5000", - "X-RateLimit-Remaining": "4999", - "X-RateLimit-Reset": "1", - "X-RateLimit-Used": "1", - "X-RateLimit-Resource": "core", - "Access-Control-Expose-Headers": "ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, Deprecation, Sunset", - "Access-Control-Allow-Origin": "*", - "Strict-Transport-Security": "max-age=31536000; includeSubdomains; preload", - "X-Frame-Options": "deny", - "X-Content-Type-Options": "nosniff", - "X-XSS-Protection": "0", - "Referrer-Policy": "origin-when-cross-origin, strict-origin-when-cross-origin", - "Content-Security-Policy": "default-src 'none'", - "Content-Encoding": "gzip", - "Permissions-Policy": "", - "X-GitHub-Request-Id": "12A3:45BC:6D7890:12EF34:5678G901" -} \ No newline at end of file + "Server": "GitHub.com", + "Date": "Mon, 1 Jan 1970 00:00:00 GMT", + "Content-Type": "application/json; charset=utf-8", + "Transfer-Encoding": "chunked", + "Cache-Control": "private, max-age=60, s-maxage=60", + "Vary": "Accept, Authorization, Cookie, X-GitHub-OTP", + "Etag": "W/\"1234567890abcdefghijklmnopqrstuvwxyz\"", + "X-OAuth-Scopes": "", + "X-Accepted-OAuth-Scopes": "", + "github-authentication-token-expiration": "1970-01-01 01:00:00 UTC", + "X-GitHub-Media-Type": "github.v3; param=raw; format=json", + "X-RateLimit-Limit": "5000", + "X-RateLimit-Remaining": "4999", + "X-RateLimit-Reset": "1", + "X-RateLimit-Used": "1", + "X-RateLimit-Resource": "core", + "Access-Control-Expose-Headers": "ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, Deprecation, Sunset", + "Access-Control-Allow-Origin": "*", + "Strict-Transport-Security": "max-age=31536000; includeSubdomains; preload", + "X-Frame-Options": "deny", + "X-Content-Type-Options": "nosniff", + "X-XSS-Protection": "0", + "Referrer-Policy": "origin-when-cross-origin, strict-origin-when-cross-origin", + "Content-Security-Policy": "default-src 'none'", + "Content-Encoding": "gzip", + "Permissions-Policy": "", + "X-GitHub-Request-Id": "12A3:45BC:6D7890:12EF34:5678G901" +} diff --git a/tests/components/github/fixtures/graphql.json b/tests/components/github/fixtures/graphql.json index 52b0e6ccfd6..b72554c4fc0 100644 --- a/tests/components/github/fixtures/graphql.json +++ b/tests/components/github/fixtures/graphql.json @@ -1,69 +1,69 @@ { - "data": { - "rateLimit": { - "cost": 1, - "remaining": 4999 - }, - "repository": { - "default_branch_ref": { - "commit": { - "message": "Fix all the bugs", - "url": "https://github.com/octocat/Hello-World/commit/6dcb09b5b57875f334f61aebed695e2e4193db5e", - "sha": "6dcb09b5b57875f334f61aebed695e2e4193db5e" - } - }, - "stargazers_count": 9, - "forks_count": 9, - "full_name": "octocat/Hello-World", - "id": 1296269, - "watchers": { - "total": 9 - }, - "discussion": { - "total": 1, - "discussions": [ - { - "title": "First discussion", - "url": "https://github.com/octocat/Hello-World/discussions/1347", - "number": 1347 - } - ] - }, - "issue": { - "total": 1, - "issues": [ - { - "title": "Found a bug", - "url": "https://github.com/octocat/Hello-World/issues/1347", - "number": 1347 - } - ] - }, - "pull_request": { - "total": 1, - "pull_requests": [ - { - "title": "Amazing new feature", - "url": "https://github.com/octocat/Hello-World/pull/1347", - "number": 1347 - } - ] - }, - "release": { - "name": "v1.0.0", - "url": "https://github.com/octocat/Hello-World/releases/v1.0.0", - "tag": "v1.0.0" - }, - "refs": { - "tags": [ - { - "name": "v1.0.0", - "target": { - "url": "https://github.com/octocat/Hello-World/commit/6dcb09b5b57875f334f61aebed695e2e4193db5e" - } - } - ] - } + "data": { + "rateLimit": { + "cost": 1, + "remaining": 4999 + }, + "repository": { + "default_branch_ref": { + "commit": { + "message": "Fix all the bugs", + "url": "https://github.com/octocat/Hello-World/commit/6dcb09b5b57875f334f61aebed695e2e4193db5e", + "sha": "6dcb09b5b57875f334f61aebed695e2e4193db5e" } + }, + "stargazers_count": 9, + "forks_count": 9, + "full_name": "octocat/Hello-World", + "id": 1296269, + "watchers": { + "total": 9 + }, + "discussion": { + "total": 1, + "discussions": [ + { + "title": "First discussion", + "url": "https://github.com/octocat/Hello-World/discussions/1347", + "number": 1347 + } + ] + }, + "issue": { + "total": 1, + "issues": [ + { + "title": "Found a bug", + "url": "https://github.com/octocat/Hello-World/issues/1347", + "number": 1347 + } + ] + }, + "pull_request": { + "total": 1, + "pull_requests": [ + { + "title": "Amazing new feature", + "url": "https://github.com/octocat/Hello-World/pull/1347", + "number": 1347 + } + ] + }, + "release": { + "name": "v1.0.0", + "url": "https://github.com/octocat/Hello-World/releases/v1.0.0", + "tag": "v1.0.0" + }, + "refs": { + "tags": [ + { + "name": "v1.0.0", + "target": { + "url": "https://github.com/octocat/Hello-World/commit/6dcb09b5b57875f334f61aebed695e2e4193db5e" + } + } + ] + } } -} \ No newline at end of file + } +} diff --git a/tests/components/github/fixtures/repository.json b/tests/components/github/fixtures/repository.json index 7007db68593..e6392ee3500 100644 --- a/tests/components/github/fixtures/repository.json +++ b/tests/components/github/fixtures/repository.json @@ -1,27 +1,279 @@ { + "id": 1296269, + "node_id": "MDEwOlJlcG9zaXRvcnkxMjk2MjY5", + "name": "Hello-World", + "full_name": "octocat/Hello-World", + "owner": { + "login": "octocat", + "id": 1, + "node_id": "MDQ6VXNlcjE=", + "avatar_url": "https://github.com/images/error/octocat_happy.gif", + "gravatar_id": "", + "url": "https://api.github.com/users/octocat", + "html_url": "https://github.com/octocat", + "followers_url": "https://api.github.com/users/octocat/followers", + "following_url": "https://api.github.com/users/octocat/following{/other_user}", + "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", + "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", + "organizations_url": "https://api.github.com/users/octocat/orgs", + "repos_url": "https://api.github.com/users/octocat/repos", + "events_url": "https://api.github.com/users/octocat/events{/privacy}", + "received_events_url": "https://api.github.com/users/octocat/received_events", + "type": "User", + "site_admin": false + }, + "private": false, + "html_url": "https://github.com/octocat/Hello-World", + "description": "This your first repo!", + "fork": false, + "url": "https://api.github.com/repos/octocat/Hello-World", + "archive_url": "https://api.github.com/repos/octocat/Hello-World/{archive_format}{/ref}", + "assignees_url": "https://api.github.com/repos/octocat/Hello-World/assignees{/user}", + "blobs_url": "https://api.github.com/repos/octocat/Hello-World/git/blobs{/sha}", + "branches_url": "https://api.github.com/repos/octocat/Hello-World/branches{/branch}", + "collaborators_url": "https://api.github.com/repos/octocat/Hello-World/collaborators{/collaborator}", + "comments_url": "https://api.github.com/repos/octocat/Hello-World/comments{/number}", + "commits_url": "https://api.github.com/repos/octocat/Hello-World/commits{/sha}", + "compare_url": "https://api.github.com/repos/octocat/Hello-World/compare/{base}...{head}", + "contents_url": "https://api.github.com/repos/octocat/Hello-World/contents/{+path}", + "contributors_url": "https://api.github.com/repos/octocat/Hello-World/contributors", + "deployments_url": "https://api.github.com/repos/octocat/Hello-World/deployments", + "downloads_url": "https://api.github.com/repos/octocat/Hello-World/downloads", + "events_url": "https://api.github.com/repos/octocat/Hello-World/events", + "forks_url": "https://api.github.com/repos/octocat/Hello-World/forks", + "git_commits_url": "https://api.github.com/repos/octocat/Hello-World/git/commits{/sha}", + "git_refs_url": "https://api.github.com/repos/octocat/Hello-World/git/refs{/sha}", + "git_tags_url": "https://api.github.com/repos/octocat/Hello-World/git/tags{/sha}", + "git_url": "git:github.com/octocat/Hello-World.git", + "issue_comment_url": "https://api.github.com/repos/octocat/Hello-World/issues/comments{/number}", + "issue_events_url": "https://api.github.com/repos/octocat/Hello-World/issues/events{/number}", + "issues_url": "https://api.github.com/repos/octocat/Hello-World/issues{/number}", + "keys_url": "https://api.github.com/repos/octocat/Hello-World/keys{/key_id}", + "labels_url": "https://api.github.com/repos/octocat/Hello-World/labels{/name}", + "languages_url": "https://api.github.com/repos/octocat/Hello-World/languages", + "merges_url": "https://api.github.com/repos/octocat/Hello-World/merges", + "milestones_url": "https://api.github.com/repos/octocat/Hello-World/milestones{/number}", + "notifications_url": "https://api.github.com/repos/octocat/Hello-World/notifications{?since,all,participating}", + "pulls_url": "https://api.github.com/repos/octocat/Hello-World/pulls{/number}", + "releases_url": "https://api.github.com/repos/octocat/Hello-World/releases{/id}", + "ssh_url": "git@github.com:octocat/Hello-World.git", + "stargazers_url": "https://api.github.com/repos/octocat/Hello-World/stargazers", + "statuses_url": "https://api.github.com/repos/octocat/Hello-World/statuses/{sha}", + "subscribers_url": "https://api.github.com/repos/octocat/Hello-World/subscribers", + "subscription_url": "https://api.github.com/repos/octocat/Hello-World/subscription", + "tags_url": "https://api.github.com/repos/octocat/Hello-World/tags", + "teams_url": "https://api.github.com/repos/octocat/Hello-World/teams", + "trees_url": "https://api.github.com/repos/octocat/Hello-World/git/trees{/sha}", + "clone_url": "https://github.com/octocat/Hello-World.git", + "mirror_url": "git:git.example.com/octocat/Hello-World", + "hooks_url": "https://api.github.com/repos/octocat/Hello-World/hooks", + "svn_url": "https://svn.github.com/octocat/Hello-World", + "homepage": "https://github.com", + "language": null, + "forks_count": 9, + "forks": 9, + "stargazers_count": 80, + "watchers_count": 80, + "watchers": 80, + "size": 108, + "default_branch": "master", + "open_issues_count": 0, + "open_issues": 0, + "is_template": false, + "topics": ["octocat", "atom", "electron", "api"], + "has_issues": true, + "has_projects": true, + "has_wiki": true, + "has_pages": false, + "has_downloads": true, + "archived": false, + "disabled": false, + "visibility": "public", + "pushed_at": "2011-01-26T19:06:43Z", + "created_at": "2011-01-26T19:01:12Z", + "updated_at": "2011-01-26T19:14:43Z", + "permissions": { + "pull": true, + "push": false, + "admin": false + }, + "allow_rebase_merge": true, + "template_repository": { + "id": 1296269, + "node_id": "MDEwOlJlcG9zaXRvcnkxMjk2MjY5", + "name": "Hello-World-Template", + "full_name": "octocat/Hello-World-Template", + "owner": { + "login": "octocat", + "id": 1, + "node_id": "MDQ6VXNlcjE=", + "avatar_url": "https://github.com/images/error/octocat_happy.gif", + "gravatar_id": "", + "url": "https://api.github.com/users/octocat", + "html_url": "https://github.com/octocat", + "followers_url": "https://api.github.com/users/octocat/followers", + "following_url": "https://api.github.com/users/octocat/following{/other_user}", + "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", + "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", + "organizations_url": "https://api.github.com/users/octocat/orgs", + "repos_url": "https://api.github.com/users/octocat/repos", + "events_url": "https://api.github.com/users/octocat/events{/privacy}", + "received_events_url": "https://api.github.com/users/octocat/received_events", + "type": "User", + "site_admin": false + }, + "private": false, + "html_url": "https://github.com/octocat/Hello-World-Template", + "description": "This your first repo!", + "fork": false, + "url": "https://api.github.com/repos/octocat/Hello-World-Template", + "archive_url": "https://api.github.com/repos/octocat/Hello-World-Template/{archive_format}{/ref}", + "assignees_url": "https://api.github.com/repos/octocat/Hello-World-Template/assignees{/user}", + "blobs_url": "https://api.github.com/repos/octocat/Hello-World-Template/git/blobs{/sha}", + "branches_url": "https://api.github.com/repos/octocat/Hello-World-Template/branches{/branch}", + "collaborators_url": "https://api.github.com/repos/octocat/Hello-World-Template/collaborators{/collaborator}", + "comments_url": "https://api.github.com/repos/octocat/Hello-World-Template/comments{/number}", + "commits_url": "https://api.github.com/repos/octocat/Hello-World-Template/commits{/sha}", + "compare_url": "https://api.github.com/repos/octocat/Hello-World-Template/compare/{base}...{head}", + "contents_url": "https://api.github.com/repos/octocat/Hello-World-Template/contents/{+path}", + "contributors_url": "https://api.github.com/repos/octocat/Hello-World-Template/contributors", + "deployments_url": "https://api.github.com/repos/octocat/Hello-World-Template/deployments", + "downloads_url": "https://api.github.com/repos/octocat/Hello-World-Template/downloads", + "events_url": "https://api.github.com/repos/octocat/Hello-World-Template/events", + "forks_url": "https://api.github.com/repos/octocat/Hello-World-Template/forks", + "git_commits_url": "https://api.github.com/repos/octocat/Hello-World-Template/git/commits{/sha}", + "git_refs_url": "https://api.github.com/repos/octocat/Hello-World-Template/git/refs{/sha}", + "git_tags_url": "https://api.github.com/repos/octocat/Hello-World-Template/git/tags{/sha}", + "git_url": "git:github.com/octocat/Hello-World-Template.git", + "issue_comment_url": "https://api.github.com/repos/octocat/Hello-World-Template/issues/comments{/number}", + "issue_events_url": "https://api.github.com/repos/octocat/Hello-World-Template/issues/events{/number}", + "issues_url": "https://api.github.com/repos/octocat/Hello-World-Template/issues{/number}", + "keys_url": "https://api.github.com/repos/octocat/Hello-World-Template/keys{/key_id}", + "labels_url": "https://api.github.com/repos/octocat/Hello-World-Template/labels{/name}", + "languages_url": "https://api.github.com/repos/octocat/Hello-World-Template/languages", + "merges_url": "https://api.github.com/repos/octocat/Hello-World-Template/merges", + "milestones_url": "https://api.github.com/repos/octocat/Hello-World-Template/milestones{/number}", + "notifications_url": "https://api.github.com/repos/octocat/Hello-World-Template/notifications{?since,all,participating}", + "pulls_url": "https://api.github.com/repos/octocat/Hello-World-Template/pulls{/number}", + "releases_url": "https://api.github.com/repos/octocat/Hello-World-Template/releases{/id}", + "ssh_url": "git@github.com:octocat/Hello-World-Template.git", + "stargazers_url": "https://api.github.com/repos/octocat/Hello-World-Template/stargazers", + "statuses_url": "https://api.github.com/repos/octocat/Hello-World-Template/statuses/{sha}", + "subscribers_url": "https://api.github.com/repos/octocat/Hello-World-Template/subscribers", + "subscription_url": "https://api.github.com/repos/octocat/Hello-World-Template/subscription", + "tags_url": "https://api.github.com/repos/octocat/Hello-World-Template/tags", + "teams_url": "https://api.github.com/repos/octocat/Hello-World-Template/teams", + "trees_url": "https://api.github.com/repos/octocat/Hello-World-Template/git/trees{/sha}", + "clone_url": "https://github.com/octocat/Hello-World-Template.git", + "mirror_url": "git:git.example.com/octocat/Hello-World-Template", + "hooks_url": "https://api.github.com/repos/octocat/Hello-World-Template/hooks", + "svn_url": "https://svn.github.com/octocat/Hello-World-Template", + "homepage": "https://github.com", + "language": null, + "forks": 9, + "forks_count": 9, + "stargazers_count": 80, + "watchers_count": 80, + "watchers": 80, + "size": 108, + "default_branch": "master", + "open_issues": 0, + "open_issues_count": 0, + "is_template": true, + "license": { + "key": "mit", + "name": "MIT License", + "url": "https://api.github.com/licenses/mit", + "spdx_id": "MIT", + "node_id": "MDc6TGljZW5zZW1pdA==", + "html_url": "https://api.github.com/licenses/mit" + }, + "topics": ["octocat", "atom", "electron", "api"], + "has_issues": true, + "has_projects": true, + "has_wiki": true, + "has_pages": false, + "has_downloads": true, + "archived": false, + "disabled": false, + "visibility": "public", + "pushed_at": "2011-01-26T19:06:43Z", + "created_at": "2011-01-26T19:01:12Z", + "updated_at": "2011-01-26T19:14:43Z", + "permissions": { + "admin": false, + "push": false, + "pull": true + }, + "allow_rebase_merge": true, + "temp_clone_token": "ABTLWHOULUVAXGTRYU7OC2876QJ2O", + "allow_squash_merge": true, + "allow_auto_merge": false, + "delete_branch_on_merge": true, + "allow_merge_commit": true, + "subscribers_count": 42, + "network_count": 0 + }, + "temp_clone_token": "ABTLWHOULUVAXGTRYU7OC2876QJ2O", + "allow_squash_merge": true, + "allow_auto_merge": false, + "delete_branch_on_merge": true, + "allow_merge_commit": true, + "subscribers_count": 42, + "network_count": 0, + "license": { + "key": "mit", + "name": "MIT License", + "spdx_id": "MIT", + "url": "https://api.github.com/licenses/mit", + "node_id": "MDc6TGljZW5zZW1pdA==" + }, + "organization": { + "login": "octocat", + "id": 1, + "node_id": "MDQ6VXNlcjE=", + "avatar_url": "https://github.com/images/error/octocat_happy.gif", + "gravatar_id": "", + "url": "https://api.github.com/users/octocat", + "html_url": "https://github.com/octocat", + "followers_url": "https://api.github.com/users/octocat/followers", + "following_url": "https://api.github.com/users/octocat/following{/other_user}", + "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", + "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", + "organizations_url": "https://api.github.com/users/octocat/orgs", + "repos_url": "https://api.github.com/users/octocat/repos", + "events_url": "https://api.github.com/users/octocat/events{/privacy}", + "received_events_url": "https://api.github.com/users/octocat/received_events", + "type": "Organization", + "site_admin": false + }, + "parent": { "id": 1296269, "node_id": "MDEwOlJlcG9zaXRvcnkxMjk2MjY5", "name": "Hello-World", "full_name": "octocat/Hello-World", "owner": { - "login": "octocat", - "id": 1, - "node_id": "MDQ6VXNlcjE=", - "avatar_url": "https://github.com/images/error/octocat_happy.gif", - "gravatar_id": "", - "url": "https://api.github.com/users/octocat", - "html_url": "https://github.com/octocat", - "followers_url": "https://api.github.com/users/octocat/followers", - "following_url": "https://api.github.com/users/octocat/following{/other_user}", - "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", - "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", - "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", - "organizations_url": "https://api.github.com/users/octocat/orgs", - "repos_url": "https://api.github.com/users/octocat/repos", - "events_url": "https://api.github.com/users/octocat/events{/privacy}", - "received_events_url": "https://api.github.com/users/octocat/received_events", - "type": "User", - "site_admin": false + "login": "octocat", + "id": 1, + "node_id": "MDQ6VXNlcjE=", + "avatar_url": "https://github.com/images/error/octocat_happy.gif", + "gravatar_id": "", + "url": "https://api.github.com/users/octocat", + "html_url": "https://github.com/octocat", + "followers_url": "https://api.github.com/users/octocat/followers", + "following_url": "https://api.github.com/users/octocat/following{/other_user}", + "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", + "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", + "organizations_url": "https://api.github.com/users/octocat/orgs", + "repos_url": "https://api.github.com/users/octocat/repos", + "events_url": "https://api.github.com/users/octocat/events{/privacy}", + "received_events_url": "https://api.github.com/users/octocat/received_events", + "type": "User", + "site_admin": false }, "private": false, "html_url": "https://github.com/octocat/Hello-World", @@ -72,21 +324,13 @@ "homepage": "https://github.com", "language": null, "forks_count": 9, - "forks": 9, "stargazers_count": 80, "watchers_count": 80, - "watchers": 80, "size": 108, "default_branch": "master", "open_issues_count": 0, - "open_issues": 0, - "is_template": false, - "topics": [ - "octocat", - "atom", - "electron", - "api" - ], + "is_template": true, + "topics": ["octocat", "atom", "electron", "api"], "has_issues": true, "has_projects": true, "has_wiki": true, @@ -99,133 +343,11 @@ "created_at": "2011-01-26T19:01:12Z", "updated_at": "2011-01-26T19:14:43Z", "permissions": { - "pull": true, - "push": false, - "admin": false + "admin": false, + "push": false, + "pull": true }, "allow_rebase_merge": true, - "template_repository": { - "id": 1296269, - "node_id": "MDEwOlJlcG9zaXRvcnkxMjk2MjY5", - "name": "Hello-World-Template", - "full_name": "octocat/Hello-World-Template", - "owner": { - "login": "octocat", - "id": 1, - "node_id": "MDQ6VXNlcjE=", - "avatar_url": "https://github.com/images/error/octocat_happy.gif", - "gravatar_id": "", - "url": "https://api.github.com/users/octocat", - "html_url": "https://github.com/octocat", - "followers_url": "https://api.github.com/users/octocat/followers", - "following_url": "https://api.github.com/users/octocat/following{/other_user}", - "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", - "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", - "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", - "organizations_url": "https://api.github.com/users/octocat/orgs", - "repos_url": "https://api.github.com/users/octocat/repos", - "events_url": "https://api.github.com/users/octocat/events{/privacy}", - "received_events_url": "https://api.github.com/users/octocat/received_events", - "type": "User", - "site_admin": false - }, - "private": false, - "html_url": "https://github.com/octocat/Hello-World-Template", - "description": "This your first repo!", - "fork": false, - "url": "https://api.github.com/repos/octocat/Hello-World-Template", - "archive_url": "https://api.github.com/repos/octocat/Hello-World-Template/{archive_format}{/ref}", - "assignees_url": "https://api.github.com/repos/octocat/Hello-World-Template/assignees{/user}", - "blobs_url": "https://api.github.com/repos/octocat/Hello-World-Template/git/blobs{/sha}", - "branches_url": "https://api.github.com/repos/octocat/Hello-World-Template/branches{/branch}", - "collaborators_url": "https://api.github.com/repos/octocat/Hello-World-Template/collaborators{/collaborator}", - "comments_url": "https://api.github.com/repos/octocat/Hello-World-Template/comments{/number}", - "commits_url": "https://api.github.com/repos/octocat/Hello-World-Template/commits{/sha}", - "compare_url": "https://api.github.com/repos/octocat/Hello-World-Template/compare/{base}...{head}", - "contents_url": "https://api.github.com/repos/octocat/Hello-World-Template/contents/{+path}", - "contributors_url": "https://api.github.com/repos/octocat/Hello-World-Template/contributors", - "deployments_url": "https://api.github.com/repos/octocat/Hello-World-Template/deployments", - "downloads_url": "https://api.github.com/repos/octocat/Hello-World-Template/downloads", - "events_url": "https://api.github.com/repos/octocat/Hello-World-Template/events", - "forks_url": "https://api.github.com/repos/octocat/Hello-World-Template/forks", - "git_commits_url": "https://api.github.com/repos/octocat/Hello-World-Template/git/commits{/sha}", - "git_refs_url": "https://api.github.com/repos/octocat/Hello-World-Template/git/refs{/sha}", - "git_tags_url": "https://api.github.com/repos/octocat/Hello-World-Template/git/tags{/sha}", - "git_url": "git:github.com/octocat/Hello-World-Template.git", - "issue_comment_url": "https://api.github.com/repos/octocat/Hello-World-Template/issues/comments{/number}", - "issue_events_url": "https://api.github.com/repos/octocat/Hello-World-Template/issues/events{/number}", - "issues_url": "https://api.github.com/repos/octocat/Hello-World-Template/issues{/number}", - "keys_url": "https://api.github.com/repos/octocat/Hello-World-Template/keys{/key_id}", - "labels_url": "https://api.github.com/repos/octocat/Hello-World-Template/labels{/name}", - "languages_url": "https://api.github.com/repos/octocat/Hello-World-Template/languages", - "merges_url": "https://api.github.com/repos/octocat/Hello-World-Template/merges", - "milestones_url": "https://api.github.com/repos/octocat/Hello-World-Template/milestones{/number}", - "notifications_url": "https://api.github.com/repos/octocat/Hello-World-Template/notifications{?since,all,participating}", - "pulls_url": "https://api.github.com/repos/octocat/Hello-World-Template/pulls{/number}", - "releases_url": "https://api.github.com/repos/octocat/Hello-World-Template/releases{/id}", - "ssh_url": "git@github.com:octocat/Hello-World-Template.git", - "stargazers_url": "https://api.github.com/repos/octocat/Hello-World-Template/stargazers", - "statuses_url": "https://api.github.com/repos/octocat/Hello-World-Template/statuses/{sha}", - "subscribers_url": "https://api.github.com/repos/octocat/Hello-World-Template/subscribers", - "subscription_url": "https://api.github.com/repos/octocat/Hello-World-Template/subscription", - "tags_url": "https://api.github.com/repos/octocat/Hello-World-Template/tags", - "teams_url": "https://api.github.com/repos/octocat/Hello-World-Template/teams", - "trees_url": "https://api.github.com/repos/octocat/Hello-World-Template/git/trees{/sha}", - "clone_url": "https://github.com/octocat/Hello-World-Template.git", - "mirror_url": "git:git.example.com/octocat/Hello-World-Template", - "hooks_url": "https://api.github.com/repos/octocat/Hello-World-Template/hooks", - "svn_url": "https://svn.github.com/octocat/Hello-World-Template", - "homepage": "https://github.com", - "language": null, - "forks": 9, - "forks_count": 9, - "stargazers_count": 80, - "watchers_count": 80, - "watchers": 80, - "size": 108, - "default_branch": "master", - "open_issues": 0, - "open_issues_count": 0, - "is_template": true, - "license": { - "key": "mit", - "name": "MIT License", - "url": "https://api.github.com/licenses/mit", - "spdx_id": "MIT", - "node_id": "MDc6TGljZW5zZW1pdA==", - "html_url": "https://api.github.com/licenses/mit" - }, - "topics": [ - "octocat", - "atom", - "electron", - "api" - ], - "has_issues": true, - "has_projects": true, - "has_wiki": true, - "has_pages": false, - "has_downloads": true, - "archived": false, - "disabled": false, - "visibility": "public", - "pushed_at": "2011-01-26T19:06:43Z", - "created_at": "2011-01-26T19:01:12Z", - "updated_at": "2011-01-26T19:14:43Z", - "permissions": { - "admin": false, - "push": false, - "pull": true - }, - "allow_rebase_merge": true, - "temp_clone_token": "ABTLWHOULUVAXGTRYU7OC2876QJ2O", - "allow_squash_merge": true, - "allow_auto_merge": false, - "delete_branch_on_merge": true, - "allow_merge_commit": true, - "subscribers_count": 42, - "network_count": 0 - }, "temp_clone_token": "ABTLWHOULUVAXGTRYU7OC2876QJ2O", "allow_squash_merge": true, "allow_auto_merge": false, @@ -234,274 +356,132 @@ "subscribers_count": 42, "network_count": 0, "license": { - "key": "mit", - "name": "MIT License", - "spdx_id": "MIT", - "url": "https://api.github.com/licenses/mit", - "node_id": "MDc6TGljZW5zZW1pdA==" + "key": "mit", + "name": "MIT License", + "url": "https://api.github.com/licenses/mit", + "spdx_id": "MIT", + "node_id": "MDc6TGljZW5zZW1pdA==", + "html_url": "https://api.github.com/licenses/mit" }, - "organization": { - "login": "octocat", - "id": 1, - "node_id": "MDQ6VXNlcjE=", - "avatar_url": "https://github.com/images/error/octocat_happy.gif", - "gravatar_id": "", - "url": "https://api.github.com/users/octocat", - "html_url": "https://github.com/octocat", - "followers_url": "https://api.github.com/users/octocat/followers", - "following_url": "https://api.github.com/users/octocat/following{/other_user}", - "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", - "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", - "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", - "organizations_url": "https://api.github.com/users/octocat/orgs", - "repos_url": "https://api.github.com/users/octocat/repos", - "events_url": "https://api.github.com/users/octocat/events{/privacy}", - "received_events_url": "https://api.github.com/users/octocat/received_events", - "type": "Organization", - "site_admin": false + "forks": 1, + "open_issues": 1, + "watchers": 1 + }, + "source": { + "id": 1296269, + "node_id": "MDEwOlJlcG9zaXRvcnkxMjk2MjY5", + "name": "Hello-World", + "full_name": "octocat/Hello-World", + "owner": { + "login": "octocat", + "id": 1, + "node_id": "MDQ6VXNlcjE=", + "avatar_url": "https://github.com/images/error/octocat_happy.gif", + "gravatar_id": "", + "url": "https://api.github.com/users/octocat", + "html_url": "https://github.com/octocat", + "followers_url": "https://api.github.com/users/octocat/followers", + "following_url": "https://api.github.com/users/octocat/following{/other_user}", + "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", + "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", + "organizations_url": "https://api.github.com/users/octocat/orgs", + "repos_url": "https://api.github.com/users/octocat/repos", + "events_url": "https://api.github.com/users/octocat/events{/privacy}", + "received_events_url": "https://api.github.com/users/octocat/received_events", + "type": "User", + "site_admin": false }, - "parent": { - "id": 1296269, - "node_id": "MDEwOlJlcG9zaXRvcnkxMjk2MjY5", - "name": "Hello-World", - "full_name": "octocat/Hello-World", - "owner": { - "login": "octocat", - "id": 1, - "node_id": "MDQ6VXNlcjE=", - "avatar_url": "https://github.com/images/error/octocat_happy.gif", - "gravatar_id": "", - "url": "https://api.github.com/users/octocat", - "html_url": "https://github.com/octocat", - "followers_url": "https://api.github.com/users/octocat/followers", - "following_url": "https://api.github.com/users/octocat/following{/other_user}", - "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", - "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", - "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", - "organizations_url": "https://api.github.com/users/octocat/orgs", - "repos_url": "https://api.github.com/users/octocat/repos", - "events_url": "https://api.github.com/users/octocat/events{/privacy}", - "received_events_url": "https://api.github.com/users/octocat/received_events", - "type": "User", - "site_admin": false - }, - "private": false, - "html_url": "https://github.com/octocat/Hello-World", - "description": "This your first repo!", - "fork": false, - "url": "https://api.github.com/repos/octocat/Hello-World", - "archive_url": "https://api.github.com/repos/octocat/Hello-World/{archive_format}{/ref}", - "assignees_url": "https://api.github.com/repos/octocat/Hello-World/assignees{/user}", - "blobs_url": "https://api.github.com/repos/octocat/Hello-World/git/blobs{/sha}", - "branches_url": "https://api.github.com/repos/octocat/Hello-World/branches{/branch}", - "collaborators_url": "https://api.github.com/repos/octocat/Hello-World/collaborators{/collaborator}", - "comments_url": "https://api.github.com/repos/octocat/Hello-World/comments{/number}", - "commits_url": "https://api.github.com/repos/octocat/Hello-World/commits{/sha}", - "compare_url": "https://api.github.com/repos/octocat/Hello-World/compare/{base}...{head}", - "contents_url": "https://api.github.com/repos/octocat/Hello-World/contents/{+path}", - "contributors_url": "https://api.github.com/repos/octocat/Hello-World/contributors", - "deployments_url": "https://api.github.com/repos/octocat/Hello-World/deployments", - "downloads_url": "https://api.github.com/repos/octocat/Hello-World/downloads", - "events_url": "https://api.github.com/repos/octocat/Hello-World/events", - "forks_url": "https://api.github.com/repos/octocat/Hello-World/forks", - "git_commits_url": "https://api.github.com/repos/octocat/Hello-World/git/commits{/sha}", - "git_refs_url": "https://api.github.com/repos/octocat/Hello-World/git/refs{/sha}", - "git_tags_url": "https://api.github.com/repos/octocat/Hello-World/git/tags{/sha}", - "git_url": "git:github.com/octocat/Hello-World.git", - "issue_comment_url": "https://api.github.com/repos/octocat/Hello-World/issues/comments{/number}", - "issue_events_url": "https://api.github.com/repos/octocat/Hello-World/issues/events{/number}", - "issues_url": "https://api.github.com/repos/octocat/Hello-World/issues{/number}", - "keys_url": "https://api.github.com/repos/octocat/Hello-World/keys{/key_id}", - "labels_url": "https://api.github.com/repos/octocat/Hello-World/labels{/name}", - "languages_url": "https://api.github.com/repos/octocat/Hello-World/languages", - "merges_url": "https://api.github.com/repos/octocat/Hello-World/merges", - "milestones_url": "https://api.github.com/repos/octocat/Hello-World/milestones{/number}", - "notifications_url": "https://api.github.com/repos/octocat/Hello-World/notifications{?since,all,participating}", - "pulls_url": "https://api.github.com/repos/octocat/Hello-World/pulls{/number}", - "releases_url": "https://api.github.com/repos/octocat/Hello-World/releases{/id}", - "ssh_url": "git@github.com:octocat/Hello-World.git", - "stargazers_url": "https://api.github.com/repos/octocat/Hello-World/stargazers", - "statuses_url": "https://api.github.com/repos/octocat/Hello-World/statuses/{sha}", - "subscribers_url": "https://api.github.com/repos/octocat/Hello-World/subscribers", - "subscription_url": "https://api.github.com/repos/octocat/Hello-World/subscription", - "tags_url": "https://api.github.com/repos/octocat/Hello-World/tags", - "teams_url": "https://api.github.com/repos/octocat/Hello-World/teams", - "trees_url": "https://api.github.com/repos/octocat/Hello-World/git/trees{/sha}", - "clone_url": "https://github.com/octocat/Hello-World.git", - "mirror_url": "git:git.example.com/octocat/Hello-World", - "hooks_url": "https://api.github.com/repos/octocat/Hello-World/hooks", - "svn_url": "https://svn.github.com/octocat/Hello-World", - "homepage": "https://github.com", - "language": null, - "forks_count": 9, - "stargazers_count": 80, - "watchers_count": 80, - "size": 108, - "default_branch": "master", - "open_issues_count": 0, - "is_template": true, - "topics": [ - "octocat", - "atom", - "electron", - "api" - ], - "has_issues": true, - "has_projects": true, - "has_wiki": true, - "has_pages": false, - "has_downloads": true, - "archived": false, - "disabled": false, - "visibility": "public", - "pushed_at": "2011-01-26T19:06:43Z", - "created_at": "2011-01-26T19:01:12Z", - "updated_at": "2011-01-26T19:14:43Z", - "permissions": { - "admin": false, - "push": false, - "pull": true - }, - "allow_rebase_merge": true, - "temp_clone_token": "ABTLWHOULUVAXGTRYU7OC2876QJ2O", - "allow_squash_merge": true, - "allow_auto_merge": false, - "delete_branch_on_merge": true, - "allow_merge_commit": true, - "subscribers_count": 42, - "network_count": 0, - "license": { - "key": "mit", - "name": "MIT License", - "url": "https://api.github.com/licenses/mit", - "spdx_id": "MIT", - "node_id": "MDc6TGljZW5zZW1pdA==", - "html_url": "https://api.github.com/licenses/mit" - }, - "forks": 1, - "open_issues": 1, - "watchers": 1 + "private": false, + "html_url": "https://github.com/octocat/Hello-World", + "description": "This your first repo!", + "fork": false, + "url": "https://api.github.com/repos/octocat/Hello-World", + "archive_url": "https://api.github.com/repos/octocat/Hello-World/{archive_format}{/ref}", + "assignees_url": "https://api.github.com/repos/octocat/Hello-World/assignees{/user}", + "blobs_url": "https://api.github.com/repos/octocat/Hello-World/git/blobs{/sha}", + "branches_url": "https://api.github.com/repos/octocat/Hello-World/branches{/branch}", + "collaborators_url": "https://api.github.com/repos/octocat/Hello-World/collaborators{/collaborator}", + "comments_url": "https://api.github.com/repos/octocat/Hello-World/comments{/number}", + "commits_url": "https://api.github.com/repos/octocat/Hello-World/commits{/sha}", + "compare_url": "https://api.github.com/repos/octocat/Hello-World/compare/{base}...{head}", + "contents_url": "https://api.github.com/repos/octocat/Hello-World/contents/{+path}", + "contributors_url": "https://api.github.com/repos/octocat/Hello-World/contributors", + "deployments_url": "https://api.github.com/repos/octocat/Hello-World/deployments", + "downloads_url": "https://api.github.com/repos/octocat/Hello-World/downloads", + "events_url": "https://api.github.com/repos/octocat/Hello-World/events", + "forks_url": "https://api.github.com/repos/octocat/Hello-World/forks", + "git_commits_url": "https://api.github.com/repos/octocat/Hello-World/git/commits{/sha}", + "git_refs_url": "https://api.github.com/repos/octocat/Hello-World/git/refs{/sha}", + "git_tags_url": "https://api.github.com/repos/octocat/Hello-World/git/tags{/sha}", + "git_url": "git:github.com/octocat/Hello-World.git", + "issue_comment_url": "https://api.github.com/repos/octocat/Hello-World/issues/comments{/number}", + "issue_events_url": "https://api.github.com/repos/octocat/Hello-World/issues/events{/number}", + "issues_url": "https://api.github.com/repos/octocat/Hello-World/issues{/number}", + "keys_url": "https://api.github.com/repos/octocat/Hello-World/keys{/key_id}", + "labels_url": "https://api.github.com/repos/octocat/Hello-World/labels{/name}", + "languages_url": "https://api.github.com/repos/octocat/Hello-World/languages", + "merges_url": "https://api.github.com/repos/octocat/Hello-World/merges", + "milestones_url": "https://api.github.com/repos/octocat/Hello-World/milestones{/number}", + "notifications_url": "https://api.github.com/repos/octocat/Hello-World/notifications{?since,all,participating}", + "pulls_url": "https://api.github.com/repos/octocat/Hello-World/pulls{/number}", + "releases_url": "https://api.github.com/repos/octocat/Hello-World/releases{/id}", + "ssh_url": "git@github.com:octocat/Hello-World.git", + "stargazers_url": "https://api.github.com/repos/octocat/Hello-World/stargazers", + "statuses_url": "https://api.github.com/repos/octocat/Hello-World/statuses/{sha}", + "subscribers_url": "https://api.github.com/repos/octocat/Hello-World/subscribers", + "subscription_url": "https://api.github.com/repos/octocat/Hello-World/subscription", + "tags_url": "https://api.github.com/repos/octocat/Hello-World/tags", + "teams_url": "https://api.github.com/repos/octocat/Hello-World/teams", + "trees_url": "https://api.github.com/repos/octocat/Hello-World/git/trees{/sha}", + "clone_url": "https://github.com/octocat/Hello-World.git", + "mirror_url": "git:git.example.com/octocat/Hello-World", + "hooks_url": "https://api.github.com/repos/octocat/Hello-World/hooks", + "svn_url": "https://svn.github.com/octocat/Hello-World", + "homepage": "https://github.com", + "language": null, + "forks_count": 9, + "stargazers_count": 80, + "watchers_count": 80, + "size": 108, + "default_branch": "master", + "open_issues_count": 0, + "is_template": true, + "topics": ["octocat", "atom", "electron", "api"], + "has_issues": true, + "has_projects": true, + "has_wiki": true, + "has_pages": false, + "has_downloads": true, + "archived": false, + "disabled": false, + "visibility": "public", + "pushed_at": "2011-01-26T19:06:43Z", + "created_at": "2011-01-26T19:01:12Z", + "updated_at": "2011-01-26T19:14:43Z", + "permissions": { + "admin": false, + "push": false, + "pull": true }, - "source": { - "id": 1296269, - "node_id": "MDEwOlJlcG9zaXRvcnkxMjk2MjY5", - "name": "Hello-World", - "full_name": "octocat/Hello-World", - "owner": { - "login": "octocat", - "id": 1, - "node_id": "MDQ6VXNlcjE=", - "avatar_url": "https://github.com/images/error/octocat_happy.gif", - "gravatar_id": "", - "url": "https://api.github.com/users/octocat", - "html_url": "https://github.com/octocat", - "followers_url": "https://api.github.com/users/octocat/followers", - "following_url": "https://api.github.com/users/octocat/following{/other_user}", - "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", - "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", - "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", - "organizations_url": "https://api.github.com/users/octocat/orgs", - "repos_url": "https://api.github.com/users/octocat/repos", - "events_url": "https://api.github.com/users/octocat/events{/privacy}", - "received_events_url": "https://api.github.com/users/octocat/received_events", - "type": "User", - "site_admin": false - }, - "private": false, - "html_url": "https://github.com/octocat/Hello-World", - "description": "This your first repo!", - "fork": false, - "url": "https://api.github.com/repos/octocat/Hello-World", - "archive_url": "https://api.github.com/repos/octocat/Hello-World/{archive_format}{/ref}", - "assignees_url": "https://api.github.com/repos/octocat/Hello-World/assignees{/user}", - "blobs_url": "https://api.github.com/repos/octocat/Hello-World/git/blobs{/sha}", - "branches_url": "https://api.github.com/repos/octocat/Hello-World/branches{/branch}", - "collaborators_url": "https://api.github.com/repos/octocat/Hello-World/collaborators{/collaborator}", - "comments_url": "https://api.github.com/repos/octocat/Hello-World/comments{/number}", - "commits_url": "https://api.github.com/repos/octocat/Hello-World/commits{/sha}", - "compare_url": "https://api.github.com/repos/octocat/Hello-World/compare/{base}...{head}", - "contents_url": "https://api.github.com/repos/octocat/Hello-World/contents/{+path}", - "contributors_url": "https://api.github.com/repos/octocat/Hello-World/contributors", - "deployments_url": "https://api.github.com/repos/octocat/Hello-World/deployments", - "downloads_url": "https://api.github.com/repos/octocat/Hello-World/downloads", - "events_url": "https://api.github.com/repos/octocat/Hello-World/events", - "forks_url": "https://api.github.com/repos/octocat/Hello-World/forks", - "git_commits_url": "https://api.github.com/repos/octocat/Hello-World/git/commits{/sha}", - "git_refs_url": "https://api.github.com/repos/octocat/Hello-World/git/refs{/sha}", - "git_tags_url": "https://api.github.com/repos/octocat/Hello-World/git/tags{/sha}", - "git_url": "git:github.com/octocat/Hello-World.git", - "issue_comment_url": "https://api.github.com/repos/octocat/Hello-World/issues/comments{/number}", - "issue_events_url": "https://api.github.com/repos/octocat/Hello-World/issues/events{/number}", - "issues_url": "https://api.github.com/repos/octocat/Hello-World/issues{/number}", - "keys_url": "https://api.github.com/repos/octocat/Hello-World/keys{/key_id}", - "labels_url": "https://api.github.com/repos/octocat/Hello-World/labels{/name}", - "languages_url": "https://api.github.com/repos/octocat/Hello-World/languages", - "merges_url": "https://api.github.com/repos/octocat/Hello-World/merges", - "milestones_url": "https://api.github.com/repos/octocat/Hello-World/milestones{/number}", - "notifications_url": "https://api.github.com/repos/octocat/Hello-World/notifications{?since,all,participating}", - "pulls_url": "https://api.github.com/repos/octocat/Hello-World/pulls{/number}", - "releases_url": "https://api.github.com/repos/octocat/Hello-World/releases{/id}", - "ssh_url": "git@github.com:octocat/Hello-World.git", - "stargazers_url": "https://api.github.com/repos/octocat/Hello-World/stargazers", - "statuses_url": "https://api.github.com/repos/octocat/Hello-World/statuses/{sha}", - "subscribers_url": "https://api.github.com/repos/octocat/Hello-World/subscribers", - "subscription_url": "https://api.github.com/repos/octocat/Hello-World/subscription", - "tags_url": "https://api.github.com/repos/octocat/Hello-World/tags", - "teams_url": "https://api.github.com/repos/octocat/Hello-World/teams", - "trees_url": "https://api.github.com/repos/octocat/Hello-World/git/trees{/sha}", - "clone_url": "https://github.com/octocat/Hello-World.git", - "mirror_url": "git:git.example.com/octocat/Hello-World", - "hooks_url": "https://api.github.com/repos/octocat/Hello-World/hooks", - "svn_url": "https://svn.github.com/octocat/Hello-World", - "homepage": "https://github.com", - "language": null, - "forks_count": 9, - "stargazers_count": 80, - "watchers_count": 80, - "size": 108, - "default_branch": "master", - "open_issues_count": 0, - "is_template": true, - "topics": [ - "octocat", - "atom", - "electron", - "api" - ], - "has_issues": true, - "has_projects": true, - "has_wiki": true, - "has_pages": false, - "has_downloads": true, - "archived": false, - "disabled": false, - "visibility": "public", - "pushed_at": "2011-01-26T19:06:43Z", - "created_at": "2011-01-26T19:01:12Z", - "updated_at": "2011-01-26T19:14:43Z", - "permissions": { - "admin": false, - "push": false, - "pull": true - }, - "allow_rebase_merge": true, - "temp_clone_token": "ABTLWHOULUVAXGTRYU7OC2876QJ2O", - "allow_squash_merge": true, - "allow_auto_merge": false, - "delete_branch_on_merge": true, - "allow_merge_commit": true, - "subscribers_count": 42, - "network_count": 0, - "license": { - "key": "mit", - "name": "MIT License", - "url": "https://api.github.com/licenses/mit", - "spdx_id": "MIT", - "node_id": "MDc6TGljZW5zZW1pdA==", - "html_url": "https://api.github.com/licenses/mit" - }, - "forks": 1, - "open_issues": 1, - "watchers": 1 - } -} \ No newline at end of file + "allow_rebase_merge": true, + "temp_clone_token": "ABTLWHOULUVAXGTRYU7OC2876QJ2O", + "allow_squash_merge": true, + "allow_auto_merge": false, + "delete_branch_on_merge": true, + "allow_merge_commit": true, + "subscribers_count": 42, + "network_count": 0, + "license": { + "key": "mit", + "name": "MIT License", + "url": "https://api.github.com/licenses/mit", + "spdx_id": "MIT", + "node_id": "MDc6TGljZW5zZW1pdA==", + "html_url": "https://api.github.com/licenses/mit" + }, + "forks": 1, + "open_issues": 1, + "watchers": 1 + } +} diff --git a/tests/components/github/test_sensor.py b/tests/components/github/test_sensor.py index cba787cbc28..892fb8956e6 100644 --- a/tests/components/github/test_sensor.py +++ b/tests/components/github/test_sensor.py @@ -1,10 +1,12 @@ """Test GitHub sensor.""" import json -from homeassistant.components.github.const import DEFAULT_UPDATE_INTERVAL, DOMAIN +from homeassistant.components.github.const import DOMAIN, FALLBACK_UPDATE_INTERVAL from homeassistant.core import HomeAssistant from homeassistant.util import dt +from .common import TEST_REPOSITORY + from tests.common import MockConfigEntry, async_fire_time_changed, load_fixture from tests.test_util.aiohttp import AiohttpClientMocker @@ -22,15 +24,21 @@ async def test_sensor_updates_with_empty_release_array( response_json = json.loads(load_fixture("graphql.json", DOMAIN)) response_json["data"]["repository"]["release"] = None + headers = json.loads(load_fixture("base_headers.json", DOMAIN)) aioclient_mock.clear_requests() + aioclient_mock.get( + f"https://api.github.com/repos/{TEST_REPOSITORY}/events", + json=[], + headers=headers, + ) aioclient_mock.post( "https://api.github.com/graphql", json=response_json, - headers=json.loads(load_fixture("base_headers.json", DOMAIN)), + headers=headers, ) - async_fire_time_changed(hass, dt.utcnow() + DEFAULT_UPDATE_INTERVAL) + async_fire_time_changed(hass, dt.utcnow() + FALLBACK_UPDATE_INTERVAL) await hass.async_block_till_done() new_state = hass.states.get(TEST_SENSOR_ENTITY) diff --git a/tests/components/goalzero/fixtures/info_data.json b/tests/components/goalzero/fixtures/info_data.json index 6be95e6c482..2859596c06d 100644 --- a/tests/components/goalzero/fixtures/info_data.json +++ b/tests/components/goalzero/fixtures/info_data.json @@ -1,7 +1,7 @@ { - "name":"yeti123456789012", - "model":"Yeti 1400", - "firmwareVersion":"1.5.7", - "macAddress":"123456789012", - "platform":"esp32" -} \ No newline at end of file + "name": "yeti123456789012", + "model": "Yeti 1400", + "firmwareVersion": "1.5.7", + "macAddress": "123456789012", + "platform": "esp32" +} diff --git a/tests/components/goalzero/fixtures/state_change.json b/tests/components/goalzero/fixtures/state_change.json index 301a27da954..b617e0adc30 100644 --- a/tests/components/goalzero/fixtures/state_change.json +++ b/tests/components/goalzero/fixtures/state_change.json @@ -1,38 +1,38 @@ { - "thingName":"yeti123456789012", - "v12PortStatus":1, - "usbPortStatus":0, - "acPortStatus":1, - "backlight":1, - "app_online":0, - "wattsIn":0.0, - "ampsIn":0.0, - "wattsOut":50.5, - "ampsOut":2.1, - "whOut":5.23, - "whStored":1330, - "volts":12.0, - "socPercent":95, - "isCharging":0, - "inputDetected":0, - "timeToEmptyFull":-1, - "temperature":25, - "wifiStrength":-62, - "ssid":"wifi", - "ipAddr":"1.2.3.4", - "timestamp":1720984, - "firmwareVersion":"1.5.7", - "version":3, - "ota":{ - "delay":0, - "status":"000-000-100_001-000-100_002-000-100_003-000-100" + "thingName": "yeti123456789012", + "v12PortStatus": 1, + "usbPortStatus": 0, + "acPortStatus": 1, + "backlight": 1, + "app_online": 0, + "wattsIn": 0.0, + "ampsIn": 0.0, + "wattsOut": 50.5, + "ampsOut": 2.1, + "whOut": 5.23, + "whStored": 1330, + "volts": 12.0, + "socPercent": 95, + "isCharging": 0, + "inputDetected": 0, + "timeToEmptyFull": -1, + "temperature": 25, + "wifiStrength": -62, + "ssid": "wifi", + "ipAddr": "1.2.3.4", + "timestamp": 1720984, + "firmwareVersion": "1.5.7", + "version": 3, + "ota": { + "delay": 0, + "status": "000-000-100_001-000-100_002-000-100_003-000-100" }, - "notify":{ - "enabled":1048575, - "trigger":0 + "notify": { + "enabled": 1048575, + "trigger": 0 }, - "foreignAcsry":{ - "model":"Yeti MPPT", - "firmwareVersion":"1.1.2" + "foreignAcsry": { + "model": "Yeti MPPT", + "firmwareVersion": "1.1.2" } -} \ No newline at end of file +} diff --git a/tests/components/goalzero/fixtures/state_data.json b/tests/components/goalzero/fixtures/state_data.json index 455524584f7..3c1a4b4d37f 100644 --- a/tests/components/goalzero/fixtures/state_data.json +++ b/tests/components/goalzero/fixtures/state_data.json @@ -1,38 +1,38 @@ { - "thingName":"yeti123456789012", - "v12PortStatus":0, - "usbPortStatus":0, - "acPortStatus":1, - "backlight":1, - "app_online":0, - "wattsIn":0.0, - "ampsIn":0.0, - "wattsOut":50.5, - "ampsOut":2.1, - "whOut":5.23, - "whStored":1330, - "volts":12.0, - "socPercent":95, - "isCharging":0, - "inputDetected":0, - "timeToEmptyFull":-1, - "temperature":25, - "wifiStrength":-62, - "ssid":"wifi", - "ipAddr":"1.2.3.4", - "timestamp":1720984, - "firmwareVersion":"1.5.7", - "version":3, - "ota":{ - "delay":0, - "status":"000-000-100_001-000-100_002-000-100_003-000-100" + "thingName": "yeti123456789012", + "v12PortStatus": 0, + "usbPortStatus": 0, + "acPortStatus": 1, + "backlight": 1, + "app_online": 0, + "wattsIn": 0.0, + "ampsIn": 0.0, + "wattsOut": 50.5, + "ampsOut": 2.1, + "whOut": 5.23, + "whStored": 1330, + "volts": 12.0, + "socPercent": 95, + "isCharging": 0, + "inputDetected": 0, + "timeToEmptyFull": -1, + "temperature": 25, + "wifiStrength": -62, + "ssid": "wifi", + "ipAddr": "1.2.3.4", + "timestamp": 1720984, + "firmwareVersion": "1.5.7", + "version": 3, + "ota": { + "delay": 0, + "status": "000-000-100_001-000-100_002-000-100_003-000-100" }, - "notify":{ - "enabled":1048575, - "trigger":0 + "notify": { + "enabled": 1048575, + "trigger": 0 }, - "foreignAcsry":{ - "model":"Yeti MPPT", - "firmwareVersion":"1.1.2" + "foreignAcsry": { + "model": "Yeti MPPT", + "firmwareVersion": "1.1.2" } -} \ No newline at end of file +} diff --git a/tests/components/google/conftest.py b/tests/components/google/conftest.py index 59de9b2cddf..102b8a1ccc6 100644 --- a/tests/components/google/conftest.py +++ b/tests/components/google/conftest.py @@ -1,19 +1,43 @@ """Test configuration and mocks for the google integration.""" -from collections.abc import Callable +from __future__ import annotations + +from collections.abc import Awaitable, Callable +import datetime from typing import Any, Generator, TypeVar -from unittest.mock import Mock, patch +from unittest.mock import Mock, mock_open, patch +from googleapiclient import discovery as google_discovery +from oauth2client.client import Credentials, OAuth2Credentials import pytest +import yaml -from homeassistant.components.google import GoogleCalendarService +from homeassistant.components.google import CONF_TRACK_NEW, DOMAIN +from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component +from homeassistant.util.dt import utcnow + +from tests.common import MockConfigEntry ApiResult = Callable[[dict[str, Any]], None] -T = TypeVar("T") -YieldFixture = Generator[T, None, None] +ComponentSetup = Callable[[], Awaitable[bool]] +_T = TypeVar("_T") +YieldFixture = Generator[_T, None, None] CALENDAR_ID = "qwertyuiopasdfghjklzxcvbnm@import.calendar.google.com" -TEST_CALENDAR = { + +# Entities can either be created based on data directly from the API, or from +# the yaml config that overrides the entity name and other settings. A test +# can use a fixture to exercise either case. +TEST_API_ENTITY = "calendar.we_are_we_are_a_test_calendar" +TEST_API_ENTITY_NAME = "We are, we are, a... Test Calendar" +# Name of the entity when using yaml configuration overrides +TEST_YAML_ENTITY = "calendar.backyard_light" +TEST_YAML_ENTITY_NAME = "Backyard Light" + +# A calendar object returned from the API +TEST_API_CALENDAR = { "id": CALENDAR_ID, "etag": '"3584134138943410"', "timeZone": "UTC", @@ -26,34 +50,155 @@ TEST_CALENDAR = { "summary": "We are, we are, a... Test Calendar", "colorId": "8", "defaultReminders": [], - "track": True, } @pytest.fixture -def test_calendar(): - """Return a test calendar.""" - return TEST_CALENDAR +def test_api_calendar(): + """Return a test calendar object used in API responses.""" + return TEST_API_CALENDAR @pytest.fixture -def mock_next_event(): - """Mock the google calendar data.""" - patch_google_cal = patch( - "homeassistant.components.google.calendar.GoogleCalendarData" +def calendars_config_track() -> bool: + """Fixture that determines the 'track' setting in yaml config.""" + return True + + +@pytest.fixture +def calendars_config_ignore_availability() -> bool: + """Fixture that determines the 'ignore_availability' setting in yaml config.""" + return None + + +@pytest.fixture +def calendars_config_entity( + calendars_config_track: bool, calendars_config_ignore_availability: bool | None +) -> dict[str, Any]: + """Fixture that creates an entity within the yaml configuration.""" + entity = { + "device_id": "backyard_light", + "name": "Backyard Light", + "search": "#Backyard", + "track": calendars_config_track, + } + if calendars_config_ignore_availability is not None: + entity["ignore_availability"] = calendars_config_ignore_availability + return entity + + +@pytest.fixture +def calendars_config(calendars_config_entity: dict[str, Any]) -> list[dict[str, Any]]: + """Fixture that specifies the calendar yaml configuration.""" + return [ + { + "cal_id": CALENDAR_ID, + "entities": [calendars_config_entity], + } + ] + + +@pytest.fixture(autouse=True) +async def mock_calendars_yaml( + hass: HomeAssistant, + calendars_config: list[dict[str, Any]], +) -> None: + """Fixture that prepares the google_calendars.yaml mocks.""" + mocked_open_function = mock_open(read_data=yaml.dump(calendars_config)) + with patch("homeassistant.components.google.open", mocked_open_function): + yield + + +class FakeStorage: + """A fake storage object for persiting creds.""" + + def __init__(self) -> None: + """Initialize FakeStorage.""" + self._creds: Credentials | None = None + + def get(self) -> Credentials | None: + """Get credentials from storage.""" + return self._creds + + def put(self, creds: Credentials) -> None: + """Put credentials in storage.""" + self._creds = creds + + +@pytest.fixture +async def token_scopes() -> list[str]: + """Fixture for scopes used during test.""" + return ["https://www.googleapis.com/auth/calendar"] + + +@pytest.fixture +async def creds(token_scopes: list[str]) -> OAuth2Credentials: + """Fixture that defines creds used in the test.""" + token_expiry = utcnow() + datetime.timedelta(days=7) + return OAuth2Credentials( + access_token="ACCESS_TOKEN", + client_id="client-id", + client_secret="client-secret", + refresh_token="REFRESH_TOKEN", + token_expiry=token_expiry, + token_uri="http://example.com", + user_agent="n/a", + scopes=token_scopes, ) - with patch_google_cal as google_cal_data: - yield google_cal_data + + +@pytest.fixture(autouse=True) +async def storage() -> YieldFixture[FakeStorage]: + """Fixture to populate an existing token file for read on startup.""" + storage = FakeStorage() + with patch("homeassistant.components.google.Storage", return_value=storage): + yield storage + + +@pytest.fixture +async def config_entry(token_scopes: list[str]) -> MockConfigEntry: + """Fixture to create a config entry for the integration.""" + token_expiry = utcnow() + datetime.timedelta(days=7) + return MockConfigEntry( + domain=DOMAIN, + data={ + "auth_implementation": "device_auth", + "token": { + "access_token": "ACCESS_TOKEN", + "refresh_token": "REFRESH_TOKEN", + "scope": " ".join(token_scopes), + "token_type": "Bearer", + "expires_at": token_expiry.timestamp(), + }, + }, + ) + + +@pytest.fixture +async def mock_token_read( + hass: HomeAssistant, + creds: OAuth2Credentials, + storage: FakeStorage, +) -> None: + """Fixture to populate an existing token file for read on startup.""" + storage.put(creds) + + +@pytest.fixture(autouse=True) +def calendar_resource() -> YieldFixture[google_discovery.Resource]: + """Fixture to mock out the Google discovery API.""" + with patch("homeassistant.components.google.api.google_discovery.build") as mock: + yield mock @pytest.fixture def mock_events_list( - google_service: GoogleCalendarService, + calendar_resource: google_discovery.Resource, ) -> Callable[[dict[str, Any]], None]: """Fixture to construct a fake event list API response.""" def _put_result(response: dict[str, Any]) -> None: - google_service.return_value.get.return_value.events.return_value.list.return_value.execute.return_value = ( + calendar_resource.return_value.events.return_value.list.return_value.execute.return_value = ( response ) return @@ -61,14 +206,27 @@ def mock_events_list( return _put_result +@pytest.fixture +def mock_events_list_items( + mock_events_list: Callable[[dict[str, Any]], None] +) -> Callable[list[[dict[str, Any]]], None]: + """Fixture to construct an API response containing event items.""" + + def _put_items(items: list[dict[str, Any]]) -> None: + mock_events_list({"items": items}) + return + + return _put_items + + @pytest.fixture def mock_calendars_list( - google_service: GoogleCalendarService, + calendar_resource: google_discovery.Resource, ) -> ApiResult: """Fixture to construct a fake calendar list API response.""" def _put_result(response: dict[str, Any]) -> None: - google_service.return_value.get.return_value.calendarList.return_value.list.return_value.execute.return_value = ( + calendar_resource.return_value.calendarList.return_value.list.return_value.execute.return_value = ( response ) return @@ -78,11 +236,52 @@ def mock_calendars_list( @pytest.fixture def mock_insert_event( - google_service: GoogleCalendarService, + calendar_resource: google_discovery.Resource, ) -> Mock: """Fixture to create a mock to capture new events added to the API.""" insert_mock = Mock() - google_service.return_value.get.return_value.events.return_value.insert = ( - insert_mock - ) + calendar_resource.return_value.events.return_value.insert = insert_mock return insert_mock + + +@pytest.fixture(autouse=True) +def set_time_zone(hass): + """Set the time zone for the tests.""" + # Set our timezone to CST/Regina so we can check calculations + # This keeps UTC-6 all year round + hass.config.set_time_zone("America/Regina") + + +@pytest.fixture +def google_config_track_new() -> None: + """Fixture for tests to set the 'track_new' configuration.yaml setting.""" + return None + + +@pytest.fixture +def google_config(google_config_track_new: bool | None) -> dict[str, Any]: + """Fixture for overriding component config.""" + google_config = {CONF_CLIENT_ID: "client-id", CONF_CLIENT_SECRET: "client-secret"} + if google_config_track_new is not None: + google_config[CONF_TRACK_NEW] = google_config_track_new + return google_config + + +@pytest.fixture +async def config(google_config: dict[str, Any]) -> dict[str, Any]: + """Fixture for overriding component config.""" + return {DOMAIN: google_config} + + +@pytest.fixture +async def component_setup( + hass: HomeAssistant, config: dict[str, Any] +) -> ComponentSetup: + """Fixture for setting up the integration.""" + + async def _setup_func() -> bool: + result = await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done() + return result + + return _setup_func diff --git a/tests/components/google/fixtures/maps_elevation.json b/tests/components/google/fixtures/maps_elevation.json index 95eeb0fe239..738e3d7e9de 100644 --- a/tests/components/google/fixtures/maps_elevation.json +++ b/tests/components/google/fixtures/maps_elevation.json @@ -1,13 +1,13 @@ { - "results" : [ - { - "elevation" : 101.5, - "location" : { - "lat" : 32.54321, - "lng" : -117.12345 - }, - "resolution" : 4.8 - } - ], - "status" : "OK" + "results": [ + { + "elevation": 101.5, + "location": { + "lat": 32.54321, + "lng": -117.12345 + }, + "resolution": 4.8 + } + ], + "status": "OK" } diff --git a/tests/components/google/test_calendar.py b/tests/components/google/test_calendar.py index 0ee257788dd..53e7308bc6f 100644 --- a/tests/components/google/test_calendar.py +++ b/tests/components/google/test_calendar.py @@ -2,40 +2,25 @@ from __future__ import annotations -import copy +import datetime from http import HTTPStatus from typing import Any -from unittest.mock import Mock, patch +from unittest.mock import patch +import urllib import httplib2 import pytest -from homeassistant.components.google import ( - CONF_CAL_ID, - CONF_CLIENT_ID, - CONF_CLIENT_SECRET, - CONF_DEVICE_ID, - CONF_ENTITIES, - CONF_IGNORE_AVAILABILITY, - CONF_NAME, - CONF_TRACK, - DEVICE_SCHEMA, - SERVICE_SCAN_CALENDARS, - do_setup, -) from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.helpers.template import DATE_STR_FORMAT -from homeassistant.setup import async_setup_component -from homeassistant.util import slugify import homeassistant.util.dt as dt_util -from .conftest import TEST_CALENDAR +from .conftest import TEST_YAML_ENTITY, TEST_YAML_ENTITY_NAME -from tests.common import async_mock_service +from tests.common import async_fire_time_changed -GOOGLE_CONFIG = {CONF_CLIENT_ID: "client_id", CONF_CLIENT_SECRET: "client_secret"} -TEST_ENTITY = "calendar.we_are_we_are_a_test_calendar" -TEST_ENTITY_NAME = "We are, we are, a... Test Calendar" +TEST_ENTITY = TEST_YAML_ENTITY +TEST_ENTITY_NAME = TEST_YAML_ENTITY_NAME TEST_EVENT = { "summary": "Test All Day Event", @@ -67,76 +52,60 @@ TEST_EVENT = { } -def get_calendar_info(calendar): - """Convert data from Google into DEVICE_SCHEMA.""" - calendar_info = DEVICE_SCHEMA( - { - CONF_CAL_ID: calendar["id"], - CONF_ENTITIES: [ - { - CONF_TRACK: calendar["track"], - CONF_NAME: calendar["summary"], - CONF_DEVICE_ID: slugify(calendar["summary"]), - CONF_IGNORE_AVAILABILITY: calendar.get("ignore_availability", True), - } - ], - } - ) - return calendar_info - - @pytest.fixture(autouse=True) -def mock_google_setup(hass, test_calendar): - """Mock the google set up functions.""" - hass.loop.run_until_complete(async_setup_component(hass, "group", {"group": {}})) - calendar = get_calendar_info(test_calendar) - calendars = {calendar[CONF_CAL_ID]: calendar} - patch_google_auth = patch( - "homeassistant.components.google.do_authentication", side_effect=do_setup - ) - patch_google_load = patch( - "homeassistant.components.google.load_config", return_value=calendars - ) - patch_google_services = patch("homeassistant.components.google.setup_services") - async_mock_service(hass, "google", SERVICE_SCAN_CALENDARS) - - with patch_google_auth, patch_google_load, patch_google_services: - yield +def mock_test_setup( + hass, + mock_calendars_yaml, + test_api_calendar, + mock_calendars_list, + config_entry, +): + """Fixture that pulls in the default fixtures for tests in this file.""" + mock_calendars_list({"items": [test_api_calendar]}) + config_entry.add_to_hass(hass) + return -@pytest.fixture(autouse=True) -def set_time_zone(): - """Set the time zone for the tests.""" - # Set our timezone to CST/Regina so we can check calculations - # This keeps UTC-6 all year round - dt_util.set_default_time_zone(dt_util.get_time_zone("America/Regina")) - yield - dt_util.set_default_time_zone(dt_util.get_time_zone("UTC")) +def upcoming() -> dict[str, Any]: + """Create a test event with an arbitrary start/end time fetched from the api url.""" + now = dt_util.now() + return { + "start": {"dateTime": now.isoformat()}, + "end": {"dateTime": (now + datetime.timedelta(minutes=5)).isoformat()}, + } -@pytest.fixture(name="google_service") -def mock_google_service(): - """Mock google service.""" - patch_google_service = patch( - "homeassistant.components.google.calendar.GoogleCalendarService" - ) - with patch_google_service as mock_service: - yield mock_service +def upcoming_date() -> dict[str, Any]: + """Create a test event with an arbitrary start/end date fetched from the api url.""" + now = dt_util.now() + return { + "start": {"date": now.date().isoformat()}, + "end": {"date": now.date().isoformat()}, + } -async def test_all_day_event(hass, mock_next_event): +def upcoming_event_url() -> str: + """Return a calendar API to return events created by upcoming().""" + now = dt_util.now() + start = (now - datetime.timedelta(minutes=60)).isoformat() + end = (now + datetime.timedelta(minutes=60)).isoformat() + return f"/api/calendars/{TEST_ENTITY}?start={urllib.parse.quote(start)}&end={urllib.parse.quote(end)}" + + +async def test_all_day_event( + hass, mock_events_list_items, mock_token_read, component_setup +): """Test that we can create an event trigger on device.""" - week_from_today = dt_util.dt.date.today() + dt_util.dt.timedelta(days=7) - end_event = week_from_today + dt_util.dt.timedelta(days=1) - event = copy.deepcopy(TEST_EVENT) - start = week_from_today.isoformat() - end = end_event.isoformat() - event["start"]["date"] = start - event["end"]["date"] = end - mock_next_event.return_value.event = event + week_from_today = dt_util.now().date() + datetime.timedelta(days=7) + end_event = week_from_today + datetime.timedelta(days=1) + event = { + **TEST_EVENT, + "start": {"date": week_from_today.isoformat()}, + "end": {"date": end_event.isoformat()}, + } + mock_events_list_items([event]) - assert await async_setup_component(hass, "google", {"google": GOOGLE_CONFIG}) - await hass.async_block_till_done() + assert await component_setup() state = hass.states.get(TEST_ENTITY) assert state.name == TEST_ENTITY_NAME @@ -153,19 +122,18 @@ async def test_all_day_event(hass, mock_next_event): } -async def test_future_event(hass, mock_next_event): +async def test_future_event(hass, mock_events_list_items, component_setup): """Test that we can create an event trigger on device.""" - one_hour_from_now = dt_util.now() + dt_util.dt.timedelta(minutes=30) - end_event = one_hour_from_now + dt_util.dt.timedelta(minutes=60) - start = one_hour_from_now.isoformat() - end = end_event.isoformat() - event = copy.deepcopy(TEST_EVENT) - event["start"]["dateTime"] = start - event["end"]["dateTime"] = end - mock_next_event.return_value.event = event + one_hour_from_now = dt_util.now() + datetime.timedelta(minutes=30) + end_event = one_hour_from_now + datetime.timedelta(minutes=60) + event = { + **TEST_EVENT, + "start": {"dateTime": one_hour_from_now.isoformat()}, + "end": {"dateTime": end_event.isoformat()}, + } + mock_events_list_items([event]) - assert await async_setup_component(hass, "google", {"google": GOOGLE_CONFIG}) - await hass.async_block_till_done() + assert await component_setup() state = hass.states.get(TEST_ENTITY) assert state.name == TEST_ENTITY_NAME @@ -182,19 +150,18 @@ async def test_future_event(hass, mock_next_event): } -async def test_in_progress_event(hass, mock_next_event): +async def test_in_progress_event(hass, mock_events_list_items, component_setup): """Test that we can create an event trigger on device.""" - middle_of_event = dt_util.now() - dt_util.dt.timedelta(minutes=30) - end_event = middle_of_event + dt_util.dt.timedelta(minutes=60) - start = middle_of_event.isoformat() - end = end_event.isoformat() - event = copy.deepcopy(TEST_EVENT) - event["start"]["dateTime"] = start - event["end"]["dateTime"] = end - mock_next_event.return_value.event = event + middle_of_event = dt_util.now() - datetime.timedelta(minutes=30) + end_event = middle_of_event + datetime.timedelta(minutes=60) + event = { + **TEST_EVENT, + "start": {"dateTime": middle_of_event.isoformat()}, + "end": {"dateTime": end_event.isoformat()}, + } + mock_events_list_items([event]) - assert await async_setup_component(hass, "google", {"google": GOOGLE_CONFIG}) - await hass.async_block_till_done() + assert await component_setup() state = hass.states.get(TEST_ENTITY) assert state.name == TEST_ENTITY_NAME @@ -211,21 +178,20 @@ async def test_in_progress_event(hass, mock_next_event): } -async def test_offset_in_progress_event(hass, mock_next_event): +async def test_offset_in_progress_event(hass, mock_events_list_items, component_setup): """Test that we can create an event trigger on device.""" - middle_of_event = dt_util.now() + dt_util.dt.timedelta(minutes=14) - end_event = middle_of_event + dt_util.dt.timedelta(minutes=60) - start = middle_of_event.isoformat() - end = end_event.isoformat() + middle_of_event = dt_util.now() + datetime.timedelta(minutes=14) + end_event = middle_of_event + datetime.timedelta(minutes=60) event_summary = "Test Event in Progress" - event = copy.deepcopy(TEST_EVENT) - event["start"]["dateTime"] = start - event["end"]["dateTime"] = end - event["summary"] = f"{event_summary} !!-15" - mock_next_event.return_value.event = event + event = { + **TEST_EVENT, + "start": {"dateTime": middle_of_event.isoformat()}, + "end": {"dateTime": end_event.isoformat()}, + "summary": f"{event_summary} !!-15", + } + mock_events_list_items([event]) - assert await async_setup_component(hass, "google", {"google": GOOGLE_CONFIG}) - await hass.async_block_till_done() + assert await component_setup() state = hass.states.get(TEST_ENTITY) assert state.name == TEST_ENTITY_NAME @@ -242,22 +208,22 @@ async def test_offset_in_progress_event(hass, mock_next_event): } -@pytest.mark.skip -async def test_all_day_offset_in_progress_event(hass, mock_next_event): +async def test_all_day_offset_in_progress_event( + hass, mock_events_list_items, component_setup +): """Test that we can create an event trigger on device.""" - tomorrow = dt_util.dt.date.today() + dt_util.dt.timedelta(days=1) - end_event = tomorrow + dt_util.dt.timedelta(days=1) - start = tomorrow.isoformat() - end = end_event.isoformat() + tomorrow = dt_util.now().date() + datetime.timedelta(days=1) + end_event = tomorrow + datetime.timedelta(days=1) event_summary = "Test All Day Event Offset In Progress" - event = copy.deepcopy(TEST_EVENT) - event["start"]["date"] = start - event["end"]["date"] = end - event["summary"] = f"{event_summary} !!-25:0" - mock_next_event.return_value.event = event + event = { + **TEST_EVENT, + "start": {"date": tomorrow.isoformat()}, + "end": {"date": end_event.isoformat()}, + "summary": f"{event_summary} !!-25:0", + } + mock_events_list_items([event]) - assert await async_setup_component(hass, "google", {"google": GOOGLE_CONFIG}) - await hass.async_block_till_done() + assert await component_setup() state = hass.states.get(TEST_ENTITY) assert state.name == TEST_ENTITY_NAME @@ -274,22 +240,22 @@ async def test_all_day_offset_in_progress_event(hass, mock_next_event): } -async def test_all_day_offset_event(hass, mock_next_event): +async def test_all_day_offset_event(hass, mock_events_list_items, component_setup): """Test that we can create an event trigger on device.""" - tomorrow = dt_util.dt.date.today() + dt_util.dt.timedelta(days=2) - end_event = tomorrow + dt_util.dt.timedelta(days=1) - start = tomorrow.isoformat() - end = end_event.isoformat() - offset_hours = 1 + dt_util.now().hour + now = dt_util.now() + day_after_tomorrow = now.date() + datetime.timedelta(days=2) + end_event = day_after_tomorrow + datetime.timedelta(days=1) + offset_hours = 1 + now.hour event_summary = "Test All Day Event Offset" - event = copy.deepcopy(TEST_EVENT) - event["start"]["date"] = start - event["end"]["date"] = end - event["summary"] = f"{event_summary} !!-{offset_hours}:0" - mock_next_event.return_value.event = event + event = { + **TEST_EVENT, + "start": {"date": day_after_tomorrow.isoformat()}, + "end": {"date": end_event.isoformat()}, + "summary": f"{event_summary} !!-{offset_hours}:0", + } + mock_events_list_items([event]) - assert await async_setup_component(hass, "google", {"google": GOOGLE_CONFIG}) - await hass.async_block_till_done() + assert await component_setup() state = hass.states.get(TEST_ENTITY) assert state.name == TEST_ENTITY_NAME @@ -299,30 +265,86 @@ async def test_all_day_offset_event(hass, mock_next_event): "message": event_summary, "all_day": True, "offset_reached": False, - "start_time": tomorrow.strftime(DATE_STR_FORMAT), + "start_time": day_after_tomorrow.strftime(DATE_STR_FORMAT), "end_time": end_event.strftime(DATE_STR_FORMAT), "location": event["location"], "description": event["description"], } -async def test_update_error(hass, google_service): - """Test that the calendar handles a server error.""" - google_service.return_value.get = Mock( - side_effect=httplib2.ServerNotFoundError("unit test") - ) - assert await async_setup_component(hass, "google", {"google": GOOGLE_CONFIG}) - await hass.async_block_till_done() +async def test_update_error( + hass, calendar_resource, component_setup, test_api_calendar +): + """Test that the calendar update handles a server error.""" + now = dt_util.now() + with patch("homeassistant.components.google.api.google_discovery.build") as mock: + mock.return_value.calendarList.return_value.list.return_value.execute.return_value = { + "items": [test_api_calendar] + } + mock.return_value.events.return_value.list.return_value.execute.return_value = { + "items": [ + { + **TEST_EVENT, + "start": { + "dateTime": (now + datetime.timedelta(minutes=-30)).isoformat() + }, + "end": { + "dateTime": (now + datetime.timedelta(minutes=30)).isoformat() + }, + } + ] + } + assert await component_setup() + + state = hass.states.get(TEST_ENTITY) + assert state.name == TEST_ENTITY_NAME + assert state.state == "on" + + # Advance time to avoid throttling + now += datetime.timedelta(minutes=30) + with patch( + "homeassistant.components.google.api.google_discovery.build", + side_effect=httplib2.ServerNotFoundError("unit test"), + ), patch("homeassistant.util.utcnow", return_value=now): + async_fire_time_changed(hass, now) + await hass.async_block_till_done() + + # No change + state = hass.states.get(TEST_ENTITY) + assert state.name == TEST_ENTITY_NAME + assert state.state == "on" + + # Advance time beyond update/throttle point + now += datetime.timedelta(minutes=30) + with patch( + "homeassistant.components.google.api.google_discovery.build" + ) as mock, patch("homeassistant.util.utcnow", return_value=now): + mock.return_value.events.return_value.list.return_value.execute.return_value = { + "items": [ + { + **TEST_EVENT, + "start": { + "dateTime": (now + datetime.timedelta(minutes=30)).isoformat() + }, + "end": { + "dateTime": (now + datetime.timedelta(minutes=60)).isoformat() + }, + } + ] + } + async_fire_time_changed(hass, now) + await hass.async_block_till_done() + + # State updated state = hass.states.get(TEST_ENTITY) assert state.name == TEST_ENTITY_NAME assert state.state == "off" -async def test_calendars_api(hass, hass_client, google_service): +async def test_calendars_api(hass, hass_client, component_setup): """Test the Rest API returns the calendar.""" - assert await async_setup_component(hass, "google", {"google": GOOGLE_CONFIG}) - await hass.async_block_till_done() + assert await component_setup() client = await hass_client() response = await client.get("/api/calendars") @@ -336,126 +358,121 @@ async def test_calendars_api(hass, hass_client, google_service): ] -async def test_http_event_api_failure(hass, hass_client, google_service): +async def test_http_event_api_failure( + hass, hass_client, calendar_resource, component_setup +): """Test the Rest API response during a calendar failure.""" - google_service.return_value.get = Mock( - side_effect=httplib2.ServerNotFoundError("unit test") - ) - - assert await async_setup_component(hass, "google", {"google": GOOGLE_CONFIG}) - await hass.async_block_till_done() - - start = dt_util.now().isoformat() - end = (dt_util.now() + dt_util.dt.timedelta(minutes=60)).isoformat() + assert await component_setup() client = await hass_client() - response = await client.get(f"/api/calendars/{TEST_ENTITY}?start={start}&end={end}") + + calendar_resource.side_effect = httplib2.ServerNotFoundError("unit test") + + response = await client.get(upcoming_event_url()) assert response.status == HTTPStatus.OK # A failure to talk to the server results in an empty list of events events = await response.json() assert events == [] -async def test_http_api_event(hass, hass_client, google_service, mock_events_list): +@pytest.mark.freeze_time("2022-03-27 12:05:00+00:00") +async def test_http_api_event( + hass, hass_client, mock_events_list_items, component_setup +): """Test querying the API and fetching events from the server.""" - now = dt_util.now() - - mock_events_list( - { - "items": [ - { - "summary": "Event title", - "start": {"dateTime": now.isoformat()}, - "end": { - "dateTime": (now + dt_util.dt.timedelta(minutes=5)).isoformat() - }, - } - ], - } - ) - assert await async_setup_component(hass, "google", {"google": GOOGLE_CONFIG}) - await hass.async_block_till_done() - - start = (now - dt_util.dt.timedelta(minutes=60)).isoformat() - end = (now + dt_util.dt.timedelta(minutes=60)).isoformat() + hass.config.set_time_zone("Asia/Baghdad") + event = { + **TEST_EVENT, + **upcoming(), + } + mock_events_list_items([event]) + assert await component_setup() client = await hass_client() - response = await client.get(f"/api/calendars/{TEST_ENTITY}?start={start}&end={end}") + response = await client.get(upcoming_event_url()) assert response.status == HTTPStatus.OK events = await response.json() assert len(events) == 1 - assert "summary" in events[0] - assert events[0]["summary"] == "Event title" + assert {k: events[0].get(k) for k in ["summary", "start", "end"]} == { + "summary": TEST_EVENT["summary"], + "start": {"dateTime": "2022-03-27T15:05:00+03:00"}, + "end": {"dateTime": "2022-03-27T15:10:00+03:00"}, + } -def create_ignore_avail_calendar() -> dict[str, Any]: - """Create a calendar with ignore_availability set.""" - calendar = TEST_CALENDAR.copy() - calendar["ignore_availability"] = False - return calendar - - -@pytest.mark.parametrize("test_calendar", [create_ignore_avail_calendar()]) -async def test_opaque_event(hass, hass_client, google_service, mock_events_list): +@pytest.mark.freeze_time("2022-03-27 12:05:00+00:00") +async def test_http_api_all_day_event( + hass, hass_client, mock_events_list_items, component_setup +): """Test querying the API and fetching events from the server.""" - now = dt_util.now() - - mock_events_list( - { - "items": [ - { - "summary": "Event title", - "transparency": "opaque", - "start": {"dateTime": now.isoformat()}, - "end": { - "dateTime": (now + dt_util.dt.timedelta(minutes=5)).isoformat() - }, - } - ], - } - ) - assert await async_setup_component(hass, "google", {"google": GOOGLE_CONFIG}) - await hass.async_block_till_done() - - start = (now - dt_util.dt.timedelta(minutes=60)).isoformat() - end = (now + dt_util.dt.timedelta(minutes=60)).isoformat() + event = { + **TEST_EVENT, + **upcoming_date(), + } + mock_events_list_items([event]) + assert await component_setup() client = await hass_client() - response = await client.get(f"/api/calendars/{TEST_ENTITY}?start={start}&end={end}") + response = await client.get(upcoming_event_url()) assert response.status == HTTPStatus.OK events = await response.json() assert len(events) == 1 - assert "summary" in events[0] - assert events[0]["summary"] == "Event title" + assert {k: events[0].get(k) for k in ["summary", "start", "end"]} == { + "summary": TEST_EVENT["summary"], + "start": {"date": "2022-03-27"}, + "end": {"date": "2022-03-27"}, + } -@pytest.mark.parametrize("test_calendar", [create_ignore_avail_calendar()]) -async def test_transparent_event(hass, hass_client, google_service, mock_events_list): +@pytest.mark.parametrize( + "calendars_config_ignore_availability,transparency,expect_visible_event", + [ + # Look at visibility to determine if entity is created + (False, "opaque", True), + (False, "transparent", False), + # Ignoring availability and always show the entity + (True, "opaque", True), + (True, "transparency", True), + # Default to ignore availability + (None, "opaque", True), + (None, "transparency", True), + ], +) +async def test_opaque_event( + hass, + hass_client, + mock_events_list_items, + component_setup, + transparency, + expect_visible_event, +): """Test querying the API and fetching events from the server.""" - now = dt_util.now() - - mock_events_list( - { - "items": [ - { - "summary": "Event title", - "transparency": "transparent", - "start": {"dateTime": now.isoformat()}, - "end": { - "dateTime": (now + dt_util.dt.timedelta(minutes=5)).isoformat() - }, - } - ], - } - ) - assert await async_setup_component(hass, "google", {"google": GOOGLE_CONFIG}) - await hass.async_block_till_done() - - start = (now - dt_util.dt.timedelta(minutes=60)).isoformat() - end = (now + dt_util.dt.timedelta(minutes=60)).isoformat() + event = { + **TEST_EVENT, + **upcoming(), + "transparency": transparency, + } + mock_events_list_items([event]) + assert await component_setup() client = await hass_client() - response = await client.get(f"/api/calendars/{TEST_ENTITY}?start={start}&end={end}") + response = await client.get(upcoming_event_url()) assert response.status == HTTPStatus.OK events = await response.json() - assert events == [] + assert (len(events) > 0) == expect_visible_event + + +async def test_scan_calendar_error( + hass, + calendar_resource, + component_setup, + test_api_calendar, +): + """Test that the calendar update handles a server error.""" + with patch( + "homeassistant.components.google.api.google_discovery.build", + side_effect=httplib2.ServerNotFoundError("unit test"), + ): + assert await component_setup() + + assert not hass.states.get(TEST_ENTITY) diff --git a/tests/components/google/test_config_flow.py b/tests/components/google/test_config_flow.py new file mode 100644 index 00000000000..a5467b95dcb --- /dev/null +++ b/tests/components/google/test_config_flow.py @@ -0,0 +1,350 @@ +"""Test the google config flow.""" + +import datetime +from unittest.mock import Mock, patch + +from oauth2client.client import ( + FlowExchangeError, + OAuth2Credentials, + OAuth2DeviceCodeError, +) +import pytest + +from homeassistant import config_entries +from homeassistant.components.google.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.util.dt import utcnow + +from .conftest import ComponentSetup, YieldFixture + +from tests.common import MockConfigEntry, async_fire_time_changed + +CODE_CHECK_INTERVAL = 1 +CODE_CHECK_ALARM_TIMEDELTA = datetime.timedelta(seconds=CODE_CHECK_INTERVAL * 2) + + +@pytest.fixture(autouse=True) +async def request_setup(current_request_with_host) -> None: + """Request setup.""" + return + + +@pytest.fixture +async def code_expiration_delta() -> datetime.timedelta: + """Fixture for code expiration time, defaulting to the future.""" + return datetime.timedelta(minutes=3) + + +@pytest.fixture +async def mock_code_flow( + code_expiration_delta: datetime.timedelta, +) -> YieldFixture[Mock]: + """Fixture for initiating OAuth flow.""" + with patch( + "oauth2client.client.OAuth2WebServerFlow.step1_get_device_and_user_codes", + ) as mock_flow: + mock_flow.return_value.user_code_expiry = utcnow() + code_expiration_delta + mock_flow.return_value.interval = CODE_CHECK_INTERVAL + yield mock_flow + + +@pytest.fixture +async def mock_exchange(creds: OAuth2Credentials) -> YieldFixture[Mock]: + """Fixture for mocking out the exchange for credentials.""" + with patch( + "oauth2client.client.OAuth2WebServerFlow.step2_exchange", return_value=creds + ) as mock: + yield mock + + +async def fire_alarm(hass, point_in_time): + """Fire an alarm and wait for callbacks to run.""" + with patch("homeassistant.util.dt.utcnow", return_value=point_in_time): + async_fire_time_changed(hass, point_in_time) + await hass.async_block_till_done() + + +async def test_full_flow( + hass: HomeAssistant, + mock_code_flow: Mock, + mock_exchange: Mock, + component_setup: ComponentSetup, +) -> None: + """Test successful creds setup.""" + assert await component_setup() + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result.get("type") == "progress" + assert result.get("step_id") == "auth" + assert "description_placeholders" in result + assert "url" in result["description_placeholders"] + + with patch( + "homeassistant.components.google.async_setup_entry", return_value=True + ) as mock_setup: + # Run one tick to invoke the credential exchange check + now = utcnow() + await fire_alarm(hass, now + CODE_CHECK_ALARM_TIMEDELTA) + await hass.async_block_till_done() + result = await hass.config_entries.flow.async_configure( + flow_id=result["flow_id"] + ) + + assert result.get("type") == "create_entry" + assert result.get("title") == "Configuration.yaml" + assert "data" in result + data = result["data"] + assert "token" in data + data["token"].pop("expires_at") + data["token"].pop("expires_in") + assert data == { + "auth_implementation": "device_auth", + "token": { + "access_token": "ACCESS_TOKEN", + "refresh_token": "REFRESH_TOKEN", + "scope": "https://www.googleapis.com/auth/calendar", + "token_type": "Bearer", + }, + } + + assert len(mock_setup.mock_calls) == 1 + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + + +async def test_code_error( + hass: HomeAssistant, + mock_code_flow: Mock, + component_setup: ComponentSetup, +) -> None: + """Test successful creds setup.""" + assert await component_setup() + + with patch( + "oauth2client.client.OAuth2WebServerFlow.step1_get_device_and_user_codes", + side_effect=OAuth2DeviceCodeError("Test Failure"), + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result.get("type") == "abort" + assert result.get("reason") == "oauth_error" + + +@pytest.mark.parametrize("code_expiration_delta", [datetime.timedelta(minutes=-5)]) +async def test_expired_after_exchange( + hass: HomeAssistant, + mock_code_flow: Mock, + component_setup: ComponentSetup, +) -> None: + """Test successful creds setup.""" + assert await component_setup() + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result.get("type") == "progress" + assert result.get("step_id") == "auth" + assert "description_placeholders" in result + assert "url" in result["description_placeholders"] + + # Run one tick to invoke the credential exchange check + now = utcnow() + await fire_alarm(hass, now + CODE_CHECK_ALARM_TIMEDELTA) + await hass.async_block_till_done() + + result = await hass.config_entries.flow.async_configure(flow_id=result["flow_id"]) + assert result.get("type") == "abort" + assert result.get("reason") == "code_expired" + + +async def test_exchange_error( + hass: HomeAssistant, + mock_code_flow: Mock, + mock_exchange: Mock, + component_setup: ComponentSetup, +) -> None: + """Test an error while exchanging the code for credentials.""" + assert await component_setup() + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result.get("type") == "progress" + assert result.get("step_id") == "auth" + assert "description_placeholders" in result + assert "url" in result["description_placeholders"] + + # Run one tick to invoke the credential exchange check + now = utcnow() + with patch( + "oauth2client.client.OAuth2WebServerFlow.step2_exchange", + side_effect=FlowExchangeError(), + ): + now += CODE_CHECK_ALARM_TIMEDELTA + await fire_alarm(hass, now) + await hass.async_block_till_done() + + # Status has not updated, will retry + result = await hass.config_entries.flow.async_configure(flow_id=result["flow_id"]) + assert result.get("type") == "progress" + assert result.get("step_id") == "auth" + + # Run another tick, which attempts credential exchange again + with patch( + "homeassistant.components.google.async_setup_entry", return_value=True + ) as mock_setup: + now += CODE_CHECK_ALARM_TIMEDELTA + await fire_alarm(hass, now) + await hass.async_block_till_done() + result = await hass.config_entries.flow.async_configure( + flow_id=result["flow_id"] + ) + + assert result.get("type") == "create_entry" + assert result.get("title") == "Configuration.yaml" + assert "data" in result + data = result["data"] + assert "token" in data + data["token"].pop("expires_at") + data["token"].pop("expires_in") + assert data == { + "auth_implementation": "device_auth", + "token": { + "access_token": "ACCESS_TOKEN", + "refresh_token": "REFRESH_TOKEN", + "scope": "https://www.googleapis.com/auth/calendar", + "token_type": "Bearer", + }, + } + + assert len(mock_setup.mock_calls) == 1 + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + + +async def test_existing_config_entry( + hass: HomeAssistant, + config_entry: MockConfigEntry, + component_setup: ComponentSetup, +) -> None: + """Test can't configure when config entry already exists.""" + config_entry.add_to_hass(hass) + + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + + assert await component_setup() + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result.get("type") == "abort" + assert result.get("reason") == "already_configured" + + +async def test_missing_configuration( + hass: HomeAssistant, +) -> None: + """Test can't configure when config entry already exists.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result.get("type") == "abort" + assert result.get("reason") == "missing_configuration" + + +async def test_import_config_entry_from_existing_token( + hass: HomeAssistant, + mock_token_read: None, + component_setup: ComponentSetup, +) -> None: + """Test setup with an existing token file.""" + assert await component_setup() + + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + data = entries[0].data + assert "token" in data + data["token"].pop("expires_at") + data["token"].pop("expires_in") + assert data == { + "auth_implementation": "device_auth", + "token": { + "access_token": "ACCESS_TOKEN", + "refresh_token": "REFRESH_TOKEN", + "scope": "https://www.googleapis.com/auth/calendar", + "token_type": "Bearer", + }, + } + + +async def test_reauth_flow( + hass: HomeAssistant, + mock_code_flow: Mock, + mock_exchange: Mock, + component_setup: ComponentSetup, +) -> None: + """Test can't configure when config entry already exists.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={ + "auth_implementation": "device_auth", + "token": {"access_token": "OLD_ACCESS_TOKEN"}, + }, + ) + config_entry.add_to_hass(hass) + + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + + assert await component_setup() + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_REAUTH}, data=config_entry.data + ) + assert result["type"] == "form" + assert result["step_id"] == "reauth_confirm" + + result = await hass.config_entries.flow.async_configure( + flow_id=result["flow_id"], + user_input={}, + ) + assert result.get("type") == "progress" + assert result.get("step_id") == "auth" + assert "description_placeholders" in result + assert "url" in result["description_placeholders"] + + with patch( + "homeassistant.components.google.async_setup_entry", return_value=True + ) as mock_setup: + # Run one tick to invoke the credential exchange check + now = utcnow() + await fire_alarm(hass, now + CODE_CHECK_ALARM_TIMEDELTA) + await hass.async_block_till_done() + result = await hass.config_entries.flow.async_configure( + flow_id=result["flow_id"] + ) + + assert result.get("type") == "abort" + assert result.get("reason") == "reauth_successful" + + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + data = entries[0].data + assert "token" in data + data["token"].pop("expires_at") + data["token"].pop("expires_in") + assert data == { + "auth_implementation": "device_auth", + "token": { + "access_token": "ACCESS_TOKEN", + "refresh_token": "REFRESH_TOKEN", + "scope": "https://www.googleapis.com/auth/calendar", + "token_type": "Bearer", + }, + } + + assert len(mock_setup.mock_calls) == 1 diff --git a/tests/components/google/test_init.py b/tests/components/google/test_init.py index c3754511b04..49e10d137b6 100644 --- a/tests/components/google/test_init.py +++ b/tests/components/google/test_init.py @@ -1,302 +1,72 @@ """The tests for the Google Calendar component.""" +from __future__ import annotations + from collections.abc import Awaitable, Callable import datetime from typing import Any -from unittest.mock import Mock, call, mock_open, patch +from unittest.mock import Mock, call, patch -from oauth2client.client import ( - FlowExchangeError, - OAuth2Credentials, - OAuth2DeviceCodeError, -) import pytest -import yaml from homeassistant.components.google import ( DOMAIN, SERVICE_ADD_EVENT, - GoogleCalendarService, + SERVICE_SCAN_CALENDARS, ) -from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, STATE_OFF -from homeassistant.core import HomeAssistant -from homeassistant.setup import async_setup_component +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import STATE_OFF +from homeassistant.core import HomeAssistant, State from homeassistant.util.dt import utcnow -from .conftest import CALENDAR_ID, ApiResult, YieldFixture +from .conftest import ( + CALENDAR_ID, + TEST_API_ENTITY, + TEST_API_ENTITY_NAME, + TEST_YAML_ENTITY, + TEST_YAML_ENTITY_NAME, + ApiResult, + ComponentSetup, +) -from tests.common import async_fire_time_changed +from tests.common import MockConfigEntry # Typing helpers -ComponentSetup = Callable[[], Awaitable[bool]] HassApi = Callable[[], Awaitable[dict[str, Any]]] -CODE_CHECK_INTERVAL = 1 -CODE_CHECK_ALARM_TIMEDELTA = datetime.timedelta(seconds=CODE_CHECK_INTERVAL * 2) + +def assert_state(actual: State | None, expected: State | None) -> None: + """Assert that the two states are equal.""" + if actual is None: + assert actual == expected + return + assert actual.entity_id == expected.entity_id + assert actual.state == expected.state + assert actual.attributes == expected.attributes @pytest.fixture -async def code_expiration_delta() -> datetime.timedelta: - """Fixture for code expiration time, defaulting to the future.""" - return datetime.timedelta(minutes=3) +def setup_config_entry( + hass: HomeAssistant, config_entry: MockConfigEntry +) -> MockConfigEntry: + """Fixture to initialize the config entry.""" + config_entry.add_to_hass(hass) -@pytest.fixture -async def mock_code_flow( - code_expiration_delta: datetime.timedelta, -) -> YieldFixture[Mock]: - """Fixture for initiating OAuth flow.""" - with patch( - "oauth2client.client.OAuth2WebServerFlow.step1_get_device_and_user_codes", - ) as mock_flow: - mock_flow.return_value.user_code_expiry = utcnow() + code_expiration_delta - mock_flow.return_value.interval = CODE_CHECK_INTERVAL - yield mock_flow - - -@pytest.fixture -async def token_scopes() -> list[str]: - """Fixture for scopes used during test.""" - return ["https://www.googleapis.com/auth/calendar"] - - -@pytest.fixture -async def creds(token_scopes: list[str]) -> OAuth2Credentials: - """Fixture that defines creds used in the test.""" - token_expiry = utcnow() + datetime.timedelta(days=7) - return OAuth2Credentials( - access_token="ACCESS_TOKEN", - client_id="client-id", - client_secret="client-secret", - refresh_token="REFRESH_TOKEN", - token_expiry=token_expiry, - token_uri="http://example.com", - user_agent="n/a", - scopes=token_scopes, - ) - - -@pytest.fixture -async def mock_exchange(creds: OAuth2Credentials) -> YieldFixture[Mock]: - """Fixture for mocking out the exchange for credentials.""" - with patch( - "oauth2client.client.OAuth2WebServerFlow.step2_exchange", return_value=creds - ) as mock: - yield mock - - -@pytest.fixture(autouse=True) -async def mock_token_write(hass: HomeAssistant) -> None: - """Fixture to avoid writing token files to disk.""" - with patch( - "homeassistant.components.google.os.path.isfile", return_value=True - ), patch("homeassistant.components.google.Storage.put"): - yield - - -@pytest.fixture -async def mock_token_read( - hass: HomeAssistant, - creds: OAuth2Credentials, -) -> None: - """Fixture to populate an existing token file.""" - with patch("homeassistant.components.google.Storage.get", return_value=creds): - yield - - -@pytest.fixture -async def calendars_config() -> list[dict[str, Any]]: - """Fixture for tests to override default calendar configuration.""" - return [ - { - "cal_id": CALENDAR_ID, - "entities": [ - { - "device_id": "backyard_light", - "name": "Backyard Light", - "search": "#Backyard", - "track": True, - } - ], - } - ] - - -@pytest.fixture -async def mock_calendars_yaml( - hass: HomeAssistant, - calendars_config: list[dict[str, Any]], -) -> None: - """Fixture that prepares the calendars.yaml file.""" - mocked_open_function = mock_open(read_data=yaml.dump(calendars_config)) - with patch("homeassistant.components.google.open", mocked_open_function): - yield - - -@pytest.fixture -async def mock_notification() -> YieldFixture[Mock]: - """Fixture for capturing persistent notifications.""" - with patch("homeassistant.components.persistent_notification.create") as mock: - yield mock - - -@pytest.fixture -async def config() -> dict[str, Any]: - """Fixture for overriding component config.""" - return {DOMAIN: {CONF_CLIENT_ID: "client-id", CONF_CLIENT_SECRET: "client-ecret"}} - - -@pytest.fixture -async def component_setup( - hass: HomeAssistant, config: dict[str, Any] -) -> ComponentSetup: - """Fixture for setting up the integration.""" - - async def _setup_func() -> bool: - result = await async_setup_component(hass, DOMAIN, config) - await hass.async_block_till_done() - return result - - return _setup_func - - -@pytest.fixture -async def google_service() -> YieldFixture[GoogleCalendarService]: - """Fixture to capture service calls.""" - with patch("homeassistant.components.google.GoogleCalendarService") as mock, patch( - "homeassistant.components.google.calendar.GoogleCalendarService", mock - ): - yield mock - - -async def fire_alarm(hass, point_in_time): - """Fire an alarm and wait for callbacks to run.""" - with patch("homeassistant.util.dt.utcnow", return_value=point_in_time): - async_fire_time_changed(hass, point_in_time) - await hass.async_block_till_done() - - -@pytest.mark.parametrize("config", [{}]) -async def test_setup_config_empty( +async def test_unload_entry( hass: HomeAssistant, component_setup: ComponentSetup, - mock_notification: Mock, -): - """Test setup component with an empty configuruation.""" - assert await component_setup() - - mock_notification.assert_not_called() - - assert not hass.states.get("calendar.backyard_light") - - -async def test_init_success( - hass: HomeAssistant, - google_service: GoogleCalendarService, - mock_code_flow: Mock, - mock_exchange: Mock, - mock_notification: Mock, - mock_calendars_yaml: None, - component_setup: ComponentSetup, + setup_config_entry: MockConfigEntry, ) -> None: - """Test successful creds setup.""" - assert await component_setup() + """Test load and unload of a ConfigEntry.""" + await component_setup() - # Run one tick to invoke the credential exchange check - now = utcnow() - await fire_alarm(hass, now + CODE_CHECK_ALARM_TIMEDELTA) + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + entry = entries[0] + assert entry.state is ConfigEntryState.LOADED - state = hass.states.get("calendar.backyard_light") - assert state - assert state.name == "Backyard Light" - assert state.state == STATE_OFF - - mock_notification.assert_called() - assert "We are all setup now" in mock_notification.call_args[0][1] - - -async def test_code_error( - hass: HomeAssistant, - mock_code_flow: Mock, - component_setup: ComponentSetup, - mock_notification: Mock, -) -> None: - """Test loading the integration with no existing credentials.""" - - with patch( - "oauth2client.client.OAuth2WebServerFlow.step1_get_device_and_user_codes", - side_effect=OAuth2DeviceCodeError("Test Failure"), - ): - assert await component_setup() - - assert not hass.states.get("calendar.backyard_light") - - mock_notification.assert_called() - assert "Error: Test Failure" in mock_notification.call_args[0][1] - - -@pytest.mark.parametrize("code_expiration_delta", [datetime.timedelta(minutes=-5)]) -async def test_expired_after_exchange( - hass: HomeAssistant, - mock_code_flow: Mock, - component_setup: ComponentSetup, - mock_notification: Mock, -) -> None: - """Test loading the integration with no existing credentials.""" - - assert await component_setup() - - now = utcnow() - await fire_alarm(hass, now + CODE_CHECK_ALARM_TIMEDELTA) - - assert not hass.states.get("calendar.backyard_light") - - mock_notification.assert_called() - assert ( - "Authentication code expired, please restart Home-Assistant and try again" - in mock_notification.call_args[0][1] - ) - - -async def test_exchange_error( - hass: HomeAssistant, - mock_code_flow: Mock, - component_setup: ComponentSetup, - mock_notification: Mock, -) -> None: - """Test an error while exchanging the code for credentials.""" - - with patch( - "oauth2client.client.OAuth2WebServerFlow.step2_exchange", - side_effect=FlowExchangeError(), - ): - assert await component_setup() - - now = utcnow() - await fire_alarm(hass, now + CODE_CHECK_ALARM_TIMEDELTA) - - assert not hass.states.get("calendar.backyard_light") - - mock_notification.assert_called() - assert "In order to authorize Home-Assistant" in mock_notification.call_args[0][1] - - -async def test_existing_token( - hass: HomeAssistant, - mock_token_read: None, - component_setup: ComponentSetup, - google_service: GoogleCalendarService, - mock_calendars_yaml: None, - mock_notification: Mock, -) -> None: - """Test setup with an existing token file.""" - assert await component_setup() - - state = hass.states.get("calendar.backyard_light") - assert state - assert state.name == "Backyard Light" - assert state.state == STATE_OFF - - mock_notification.assert_not_called() + assert await hass.config_entries.async_unload(entry.entry_id) + assert entry.state == ConfigEntryState.NOT_LOADED @pytest.mark.parametrize( @@ -305,100 +75,185 @@ async def test_existing_token( async def test_existing_token_missing_scope( hass: HomeAssistant, token_scopes: list[str], - mock_token_read: None, component_setup: ComponentSetup, - google_service: GoogleCalendarService, - mock_calendars_yaml: None, - mock_notification: Mock, - mock_code_flow: Mock, - mock_exchange: Mock, + config_entry: MockConfigEntry, ) -> None: """Test setup where existing token does not have sufficient scopes.""" + config_entry.add_to_hass(hass) assert await component_setup() - # Run one tick to invoke the credential exchange check - now = utcnow() - await fire_alarm(hass, now + CODE_CHECK_ALARM_TIMEDELTA) - assert len(mock_exchange.mock_calls) == 1 + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + assert entries[0].state is ConfigEntryState.SETUP_ERROR - state = hass.states.get("calendar.backyard_light") - assert state - assert state.name == "Backyard Light" - assert state.state == STATE_OFF - - # No notifications on success - mock_notification.assert_called() - assert "We are all setup now" in mock_notification.call_args[0][1] + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + assert flows[0]["step_id"] == "reauth_confirm" @pytest.mark.parametrize("calendars_config", [[{"cal_id": "invalid-schema"}]]) async def test_calendar_yaml_missing_required_fields( hass: HomeAssistant, - mock_token_read: None, component_setup: ComponentSetup, - google_service: GoogleCalendarService, calendars_config: list[dict[str, Any]], mock_calendars_yaml: None, - mock_notification: Mock, + setup_config_entry: MockConfigEntry, ) -> None: """Test setup with a missing schema fields, ignores the error and continues.""" assert await component_setup() - assert not hass.states.get("calendar.backyard_light") - - mock_notification.assert_not_called() + assert not hass.states.get(TEST_YAML_ENTITY) @pytest.mark.parametrize("calendars_config", [[{"missing-cal_id": "invalid-schema"}]]) async def test_invalid_calendar_yaml( hass: HomeAssistant, - mock_token_read: None, component_setup: ComponentSetup, - google_service: GoogleCalendarService, calendars_config: list[dict[str, Any]], mock_calendars_yaml: None, - mock_notification: Mock, + setup_config_entry: MockConfigEntry, ) -> None: - """Test setup with missing entity id fields fails to setup the integration.""" - + """Test setup with missing entity id fields fails to setup the config entry.""" # Integration fails to setup - assert not await component_setup() + assert await component_setup() - assert not hass.states.get("calendar.backyard_light") + # XXX No config entries - mock_notification.assert_not_called() + assert not hass.states.get(TEST_YAML_ENTITY) +async def test_calendar_yaml_error( + hass: HomeAssistant, + component_setup: ComponentSetup, + mock_calendars_list: ApiResult, + test_api_calendar: dict[str, Any], + setup_config_entry: MockConfigEntry, +) -> None: + """Test setup with yaml file not found.""" + mock_calendars_list({"items": [test_api_calendar]}) + + with patch("homeassistant.components.google.open", side_effect=FileNotFoundError()): + assert await component_setup() + + assert not hass.states.get(TEST_YAML_ENTITY) + assert hass.states.get(TEST_API_ENTITY) + + +@pytest.mark.parametrize( + "google_config_track_new,calendars_config,expected_state", + [ + ( + None, + [], + State( + TEST_API_ENTITY, + STATE_OFF, + attributes={ + "offset_reached": False, + "friendly_name": TEST_API_ENTITY_NAME, + }, + ), + ), + ( + True, + [], + State( + TEST_API_ENTITY, + STATE_OFF, + attributes={ + "offset_reached": False, + "friendly_name": TEST_API_ENTITY_NAME, + }, + ), + ), + (False, [], None), + ], + ids=["default", "True", "False"], +) +async def test_track_new( + hass: HomeAssistant, + component_setup: ComponentSetup, + mock_calendars_list: ApiResult, + test_api_calendar: dict[str, Any], + mock_calendars_yaml: None, + expected_state: State, + setup_config_entry: MockConfigEntry, +) -> None: + """Test behavior of configuration.yaml settings for tracking new calendars not in the config.""" + + mock_calendars_list({"items": [test_api_calendar]}) + assert await component_setup() + + state = hass.states.get(TEST_API_ENTITY) + assert_state(state, expected_state) + + +@pytest.mark.parametrize("calendars_config", [[]]) async def test_found_calendar_from_api( hass: HomeAssistant, - mock_token_read: None, component_setup: ComponentSetup, - google_service: GoogleCalendarService, + mock_calendars_yaml: None, mock_calendars_list: ApiResult, - test_calendar: dict[str, Any], + test_api_calendar: dict[str, Any], + setup_config_entry: MockConfigEntry, ) -> None: """Test finding a calendar from the API.""" - mock_calendars_list({"items": [test_calendar]}) + mock_calendars_list({"items": [test_api_calendar]}) + assert await component_setup() - mocked_open_function = mock_open(read_data=yaml.dump([])) - with patch("homeassistant.components.google.open", mocked_open_function): - assert await component_setup() - - state = hass.states.get("calendar.we_are_we_are_a_test_calendar") + state = hass.states.get(TEST_API_ENTITY) assert state - assert state.name == "We are, we are, a... Test Calendar" + assert state.name == TEST_API_ENTITY_NAME assert state.state == STATE_OFF + # No yaml config loaded that overwrites the entity name + assert not hass.states.get(TEST_YAML_ENTITY) + + +@pytest.mark.parametrize( + "calendars_config_track,expected_state", + [ + ( + True, + State( + TEST_YAML_ENTITY, + STATE_OFF, + attributes={ + "offset_reached": False, + "friendly_name": TEST_YAML_ENTITY_NAME, + }, + ), + ), + (False, None), + ], +) +async def test_calendar_config_track_new( + hass: HomeAssistant, + component_setup: ComponentSetup, + mock_calendars_yaml: None, + mock_calendars_list: ApiResult, + test_api_calendar: dict[str, Any], + calendars_config_track: bool, + expected_state: State, + setup_config_entry: MockConfigEntry, +) -> None: + """Test calendar config that overrides whether or not a calendar is tracked.""" + + mock_calendars_list({"items": [test_api_calendar]}) + assert await component_setup() + + state = hass.states.get(TEST_YAML_ENTITY) + assert_state(state, expected_state) + async def test_add_event( hass: HomeAssistant, - mock_token_read: None, component_setup: ComponentSetup, - google_service: GoogleCalendarService, mock_calendars_list: ApiResult, - test_calendar: dict[str, Any], + test_api_calendar: dict[str, Any], mock_insert_event: Mock, + setup_config_entry: MockConfigEntry, ) -> None: """Test service call that adds an event.""" @@ -439,31 +294,19 @@ async def test_add_event( datetime.timedelta(days=7), datetime.timedelta(days=8), ), - ( - { - "start_date": datetime.date.today().isoformat(), - "end_date": ( - datetime.date.today() + datetime.timedelta(days=2) - ).isoformat(), - }, - datetime.timedelta(days=0), - datetime.timedelta(days=2), - ), ], - ids=["in_days", "in_weeks", "explit_date"], + ids=["in_days", "in_weeks"], ) -async def test_add_event_date_ranges( +async def test_add_event_date_in_x( hass: HomeAssistant, - mock_token_read: None, - calendars_config: list[dict[str, Any]], component_setup: ComponentSetup, - google_service: GoogleCalendarService, mock_calendars_list: ApiResult, - test_calendar: dict[str, Any], + test_api_calendar: dict[str, Any], mock_insert_event: Mock, date_fields: dict[str, Any], start_timedelta: datetime.timedelta, end_timedelta: datetime.timedelta, + setup_config_entry: MockConfigEntry, ) -> None: """Test service call that adds an event with various time ranges.""" @@ -495,3 +338,134 @@ async def test_add_event_date_ranges( "end": {"date": end_date.date().isoformat()}, }, ) + + +async def test_add_event_date( + hass: HomeAssistant, + component_setup: ComponentSetup, + mock_calendars_list: ApiResult, + mock_insert_event: Mock, + setup_config_entry: MockConfigEntry, +) -> None: + """Test service call that sets a date range.""" + + assert await component_setup() + + now = utcnow() + today = now.date() + end_date = today + datetime.timedelta(days=2) + + await hass.services.async_call( + DOMAIN, + SERVICE_ADD_EVENT, + { + "calendar_id": CALENDAR_ID, + "summary": "Summary", + "description": "Description", + "start_date": today.isoformat(), + "end_date": end_date.isoformat(), + }, + blocking=True, + ) + mock_insert_event.assert_called() + + assert mock_insert_event.mock_calls[0] == call( + calendarId=CALENDAR_ID, + body={ + "summary": "Summary", + "description": "Description", + "start": {"date": today.isoformat()}, + "end": {"date": end_date.isoformat()}, + }, + ) + + +async def test_add_event_date_time( + hass: HomeAssistant, + component_setup: ComponentSetup, + mock_calendars_list: ApiResult, + test_api_calendar: dict[str, Any], + mock_insert_event: Mock, + setup_config_entry: MockConfigEntry, +) -> None: + """Test service call that adds an event with a date time range.""" + + assert await component_setup() + + start_datetime = datetime.datetime.now() + delta = datetime.timedelta(days=3, hours=3) + end_datetime = start_datetime + delta + + await hass.services.async_call( + DOMAIN, + SERVICE_ADD_EVENT, + { + "calendar_id": CALENDAR_ID, + "summary": "Summary", + "description": "Description", + "start_date_time": start_datetime.isoformat(), + "end_date_time": end_datetime.isoformat(), + }, + blocking=True, + ) + mock_insert_event.assert_called() + + assert mock_insert_event.mock_calls[0] == call( + calendarId=CALENDAR_ID, + body={ + "summary": "Summary", + "description": "Description", + "start": { + "dateTime": start_datetime.isoformat(timespec="seconds"), + "timeZone": "America/Regina", + }, + "end": { + "dateTime": end_datetime.isoformat(timespec="seconds"), + "timeZone": "America/Regina", + }, + }, + ) + + +async def test_scan_calendars( + hass: HomeAssistant, + component_setup: ComponentSetup, + mock_calendars_list: ApiResult, + test_api_calendar: dict[str, Any], + setup_config_entry: MockConfigEntry, +) -> None: + """Test finding a calendar from the API.""" + + assert await component_setup() + + calendar_1 = { + "id": "calendar-id-1", + "summary": "Calendar 1", + } + calendar_2 = { + "id": "calendar-id-2", + "summary": "Calendar 2", + } + + mock_calendars_list({"items": [calendar_1]}) + await hass.services.async_call(DOMAIN, SERVICE_SCAN_CALENDARS, {}, blocking=True) + await hass.async_block_till_done() + + state = hass.states.get("calendar.calendar_1") + assert state + assert state.name == "Calendar 1" + assert state.state == STATE_OFF + assert not hass.states.get("calendar.calendar_2") + + mock_calendars_list({"items": [calendar_1, calendar_2]}) + await hass.services.async_call(DOMAIN, SERVICE_SCAN_CALENDARS, {}, blocking=True) + await hass.async_block_till_done() + + state = hass.states.get("calendar.calendar_1") + assert state + assert state.name == "Calendar 1" + assert state.state == STATE_OFF + state = hass.states.get("calendar.calendar_2") + assert state + assert state.name == "Calendar 2" + assert state.state == STATE_OFF diff --git a/tests/components/google_assistant/test_google_assistant.py b/tests/components/google_assistant/test_google_assistant.py index 0a916c1e184..642ef15451a 100644 --- a/tests/components/google_assistant/test_google_assistant.py +++ b/tests/components/google_assistant/test_google_assistant.py @@ -21,6 +21,8 @@ from homeassistant.components import ( from homeassistant.components.climate import const as climate from homeassistant.components.humidifier import const as humidifier from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES +from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.entity import EntityCategory from . import DEMO_DEVICES @@ -135,27 +137,43 @@ async def test_sync_request(hass_fixture, assistant_client, auth_header): "test", "switch_config_id", suggested_object_id="config_switch", - entity_category="config", + entity_category=EntityCategory.CONFIG, ) entity_entry2 = entity_registry.async_get_or_create( "switch", "test", "switch_diagnostic_id", suggested_object_id="diagnostic_switch", - entity_category="diagnostic", + entity_category=EntityCategory.DIAGNOSTIC, ) entity_entry3 = entity_registry.async_get_or_create( "switch", "test", "switch_system_id", suggested_object_id="system_switch", - entity_category="system", + entity_category=EntityCategory.SYSTEM, + ) + entity_entry4 = entity_registry.async_get_or_create( + "switch", + "test", + "switch_hidden_integration_id", + suggested_object_id="hidden_integration_switch", + hidden_by=er.RegistryEntryHider.INTEGRATION, + ) + entity_entry5 = entity_registry.async_get_or_create( + "switch", + "test", + "switch_hidden_user_id", + suggested_object_id="hidden_user_switch", + hidden_by=er.RegistryEntryHider.USER, ) # These should not show up in the sync request hass_fixture.states.async_set(entity_entry1.entity_id, "on") hass_fixture.states.async_set(entity_entry2.entity_id, "something_else") hass_fixture.states.async_set(entity_entry3.entity_id, "blah") + hass_fixture.states.async_set(entity_entry4.entity_id, "foo") + hass_fixture.states.async_set(entity_entry5.entity_id, "bar") reqid = "5711642932632160983" data = {"requestId": reqid, "inputs": [{"intent": "action.devices.SYNC"}]} diff --git a/tests/components/google_assistant/test_helpers.py b/tests/components/google_assistant/test_helpers.py index dc29e5df4ab..4490f0e3963 100644 --- a/tests/components/google_assistant/test_helpers.py +++ b/tests/components/google_assistant/test_helpers.py @@ -47,30 +47,29 @@ async def test_google_entity_sync_serialize_with_local_sdk(hass): ) entity = helpers.GoogleEntity(hass, config, hass.states.get("light.ceiling_lights")) - serialized = await entity.sync_serialize(None) + serialized = entity.sync_serialize(None, "mock-uuid") assert "otherDeviceIds" not in serialized assert "customData" not in serialized config.async_enable_local_sdk() - with patch("homeassistant.helpers.instance_id.async_get", return_value="abcdef"): - serialized = await entity.sync_serialize("mock-user-id") - assert serialized["otherDeviceIds"] == [{"deviceId": "light.ceiling_lights"}] - assert serialized["customData"] == { - "httpPort": 1234, - "httpSSL": False, - "proxyDeviceId": "mock-user-id", - "webhookId": "mock-webhook-id", - "baseUrl": "https://hostname:1234", - "uuid": "abcdef", - } + serialized = entity.sync_serialize("mock-user-id", "abcdef") + assert serialized["otherDeviceIds"] == [{"deviceId": "light.ceiling_lights"}] + assert serialized["customData"] == { + "httpPort": 1234, + "httpSSL": False, + "proxyDeviceId": "mock-user-id", + "webhookId": "mock-webhook-id", + "baseUrl": "https://hostname:1234", + "uuid": "abcdef", + } for device_type in NOT_EXPOSE_LOCAL: with patch( "homeassistant.components.google_assistant.helpers.get_google_type", return_value=device_type, ): - serialized = await entity.sync_serialize(None) + serialized = entity.sync_serialize(None, "mock-uuid") assert "otherDeviceIds" not in serialized assert "customData" not in serialized diff --git a/tests/components/google_assistant/test_smart_home.py b/tests/components/google_assistant/test_smart_home.py index 3398fdca926..c3bbd9336f4 100644 --- a/tests/components/google_assistant/test_smart_home.py +++ b/tests/components/google_assistant/test_smart_home.py @@ -875,7 +875,7 @@ async def test_serialize_input_boolean(hass): state = State("input_boolean.bla", "on") # pylint: disable=protected-access entity = sh.GoogleEntity(hass, BASIC_CONFIG, state) - result = await entity.sync_serialize(None) + result = entity.sync_serialize(None, "mock-uuid") assert result == { "id": "input_boolean.bla", "attributes": {}, diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index a56a8f967e6..0012826074b 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -1589,7 +1589,7 @@ async def test_fan_speed(hass): hass, State( "fan.living_room_fan", - fan.SPEED_HIGH, + STATE_ON, attributes={ "percentage": 33, "percentage_step": 1.0, @@ -1633,7 +1633,7 @@ async def test_fan_reverse(hass, direction_state, direction_call): hass, State( "fan.living_room_fan", - fan.SPEED_HIGH, + STATE_ON, attributes={ "percentage": 33, "percentage_step": 1.0, diff --git a/tests/components/google_pubsub/test_init.py b/tests/components/google_pubsub/test_init.py index d31d28e7302..1b6d1dbf4b4 100644 --- a/tests/components/google_pubsub/test_init.py +++ b/tests/components/google_pubsub/test_init.py @@ -42,10 +42,9 @@ async def test_nested(): @pytest.fixture(autouse=True, name="mock_client") def mock_client_fixture(): """Mock the pubsub client.""" - with mock.patch(f"{GOOGLE_PUBSUB_PATH}.pubsub_v1") as client: - client.PublisherClient = mock.MagicMock() + with mock.patch(f"{GOOGLE_PUBSUB_PATH}.PublisherClient") as client: setattr( - client.PublisherClient, + client, "from_service_account_json", mock.MagicMock(return_value=mock.MagicMock()), ) @@ -83,10 +82,10 @@ async def test_minimal_config(hass, mock_client): await hass.async_block_till_done() assert hass.bus.listen.called assert hass.bus.listen.call_args_list[0][0][0] == EVENT_STATE_CHANGED - assert mock_client.PublisherClient.from_service_account_json.call_count == 1 - assert mock_client.PublisherClient.from_service_account_json.call_args[0][ - 0 - ] == os.path.join(hass.config.config_dir, "creds") + assert mock_client.from_service_account_json.call_count == 1 + assert mock_client.from_service_account_json.call_args[0][0] == os.path.join( + hass.config.config_dir, "creds" + ) async def test_full_config(hass, mock_client): @@ -110,10 +109,10 @@ async def test_full_config(hass, mock_client): await hass.async_block_till_done() assert hass.bus.listen.called assert hass.bus.listen.call_args_list[0][0][0] == EVENT_STATE_CHANGED - assert mock_client.PublisherClient.from_service_account_json.call_count == 1 - assert mock_client.PublisherClient.from_service_account_json.call_args[0][ - 0 - ] == os.path.join(hass.config.config_dir, "creds") + assert mock_client.from_service_account_json.call_count == 1 + assert mock_client.from_service_account_json.call_args[0][0] == os.path.join( + hass.config.config_dir, "creds" + ) def make_event(entity_id): @@ -154,7 +153,7 @@ async def test_allowlist(hass, mock_client): "include_entities": ["binary_sensor.included"], }, ) - publish_client = mock_client.PublisherClient.from_service_account_json("path") + publish_client = mock_client.from_service_account_json("path") tests = [ FilterTest("climate.excluded", False), @@ -184,7 +183,7 @@ async def test_denylist(hass, mock_client): "exclude_entities": ["binary_sensor.excluded"], }, ) - publish_client = mock_client.PublisherClient.from_service_account_json("path") + publish_client = mock_client.from_service_account_json("path") tests = [ FilterTest("climate.excluded", False), @@ -216,7 +215,7 @@ async def test_filtered_allowlist(hass, mock_client): "exclude_entities": ["light.excluded"], }, ) - publish_client = mock_client.PublisherClient.from_service_account_json("path") + publish_client = mock_client.from_service_account_json("path") tests = [ FilterTest("light.included", True), @@ -246,7 +245,7 @@ async def test_filtered_denylist(hass, mock_client): "exclude_entities": ["light.excluded"], }, ) - publish_client = mock_client.PublisherClient.from_service_account_json("path") + publish_client = mock_client.from_service_account_json("path") tests = [ FilterTest("climate.excluded", False), diff --git a/tests/components/google_wifi/test_sensor.py b/tests/components/google_wifi/test_sensor.py index add8ec04cbe..ae0715c640b 100644 --- a/tests/components/google_wifi/test_sensor.py +++ b/tests/components/google_wifi/test_sensor.py @@ -108,9 +108,9 @@ def test_name(requests_mock): assert test_name == sensor.name -def test_unit_of_measurement(requests_mock): +def test_unit_of_measurement(hass, requests_mock): """Test the unit of measurement.""" - api, sensor_dict = setup_api(None, MOCK_DATA, requests_mock) + api, sensor_dict = setup_api(hass, MOCK_DATA, requests_mock) for name in sensor_dict: sensor = sensor_dict[name]["sensor"] assert sensor_dict[name]["units"] == sensor.unit_of_measurement diff --git a/tests/components/group/fixtures/configuration.yaml b/tests/components/group/fixtures/configuration.yaml index 0a5c9e18bd1..7b3d3c2cd9c 100644 --- a/tests/components/group/fixtures/configuration.yaml +++ b/tests/components/group/fixtures/configuration.yaml @@ -10,6 +10,30 @@ light: - light.outside_patio_lights - light.outside_patio_lights_2 +lock: + - platform: group + name: Inside Locks G + entities: + - lock.front_lock + - lock.back_lock + - platform: group + name: Outside Locks G + entities: + - lock.outside_lock + - lock.outside_lock_2 + +switch: + - platform: group + name: Master Switches G + entities: + - switch.master_switch + - switch.master_switch_2 + - platform: group + name: Outside Switches G + entities: + - switch.outside_switch + - switch.outside_switch_2 + notify: - platform: group name: new_group_notify diff --git a/tests/components/group/test_config_flow.py b/tests/components/group/test_config_flow.py new file mode 100644 index 00000000000..83741a2e851 --- /dev/null +++ b/tests/components/group/test_config_flow.py @@ -0,0 +1,428 @@ +"""Test the Switch config flow.""" +from unittest.mock import patch + +import pytest + +from homeassistant import config_entries +from homeassistant.components.group import DOMAIN, async_setup_entry +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import ( + RESULT_TYPE_CREATE_ENTRY, + RESULT_TYPE_FORM, + RESULT_TYPE_MENU, +) +from homeassistant.helpers import entity_registry as er + +from tests.common import MockConfigEntry + + +@pytest.mark.parametrize( + "group_type,group_state,member_state,member_attributes,extra_input,extra_options,extra_attrs", + ( + ("binary_sensor", "on", "on", {}, {}, {"all": False}, {}), + ("binary_sensor", "on", "on", {}, {"all": True}, {"all": True}, {}), + ("cover", "open", "open", {}, {}, {}, {}), + ("fan", "on", "on", {}, {}, {}, {}), + ("light", "on", "on", {}, {}, {}, {}), + ("lock", "locked", "locked", {}, {}, {}, {}), + ("media_player", "on", "on", {}, {}, {}, {}), + ("switch", "on", "on", {}, {}, {}, {}), + ), +) +async def test_config_flow( + hass: HomeAssistant, + group_type, + group_state, + member_state, + member_attributes, + extra_input, + extra_options, + extra_attrs, +) -> None: + """Test the config flow.""" + members = [f"{group_type}.one", f"{group_type}.two"] + for member in members: + hass.states.async_set(member, member_state, member_attributes) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_MENU + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"next_step_id": group_type}, + ) + await hass.async_block_till_done() + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == group_type + + with patch( + "homeassistant.components.group.async_setup_entry", wraps=async_setup_entry + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "name": "Living Room", + "entities": members, + **extra_input, + }, + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "Living Room" + assert result["data"] == {} + assert result["options"] == { + "entities": members, + "group_type": group_type, + "hide_members": False, + "name": "Living Room", + **extra_options, + } + assert len(mock_setup_entry.mock_calls) == 1 + + config_entry = hass.config_entries.async_entries(DOMAIN)[0] + assert config_entry.data == {} + assert config_entry.options == { + "entities": members, + "group_type": group_type, + "hide_members": False, + "name": "Living Room", + **extra_options, + } + + state = hass.states.get(f"{group_type}.living_room") + assert state.state == group_state + assert state.attributes["entity_id"] == members + for key in extra_attrs: + assert state.attributes[key] == extra_attrs[key] + + +@pytest.mark.parametrize( + "hide_members,hidden_by", ((False, None), (True, "integration")) +) +@pytest.mark.parametrize( + "group_type,extra_input", + ( + ("binary_sensor", {"all": False}), + ("cover", {}), + ("fan", {}), + ("light", {}), + ("lock", {}), + ("media_player", {}), + ("switch", {}), + ), +) +async def test_config_flow_hides_members( + hass: HomeAssistant, group_type, extra_input, hide_members, hidden_by +) -> None: + """Test the config flow hides members if requested.""" + fake_uuid = "a266a680b608c32770e6c45bfe6b8411" + registry = er.async_get(hass) + entry = registry.async_get_or_create( + group_type, "test", "unique", suggested_object_id="one" + ) + assert entry.entity_id == f"{group_type}.one" + assert entry.hidden_by is None + + entry = registry.async_get_or_create( + group_type, "test", "unique3", suggested_object_id="three" + ) + assert entry.entity_id == f"{group_type}.three" + assert entry.hidden_by is None + + members = [f"{group_type}.one", f"{group_type}.two", fake_uuid, entry.id] + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_MENU + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"next_step_id": group_type}, + ) + await hass.async_block_till_done() + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == group_type + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "name": "Living Room", + "entities": members, + "hide_members": hide_members, + **extra_input, + }, + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + + assert registry.async_get(f"{group_type}.one").hidden_by == hidden_by + assert registry.async_get(f"{group_type}.three").hidden_by == hidden_by + + +def get_suggested(schema, key): + """Get suggested value for key in voluptuous schema.""" + for k in schema.keys(): + if k == key: + if k.description is None or "suggested_value" not in k.description: + return None + return k.description["suggested_value"] + # Wanted key absent from schema + raise Exception + + +@pytest.mark.parametrize( + "group_type,member_state,extra_options", + ( + ("binary_sensor", "on", {"all": False}), + ("cover", "open", {}), + ("fan", "on", {}), + ("light", "on", {"all": False}), + ("lock", "locked", {}), + ("media_player", "on", {}), + ("switch", "on", {"all": False}), + ), +) +async def test_options( + hass: HomeAssistant, group_type, member_state, extra_options +) -> None: + """Test reconfiguring.""" + members1 = [f"{group_type}.one", f"{group_type}.two"] + members2 = [f"{group_type}.four", f"{group_type}.five"] + + for member in members1: + hass.states.async_set(member, member_state, {}) + for member in members2: + hass.states.async_set(member, member_state, {}) + + group_config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + "entities": members1, + "group_type": group_type, + "name": "Bed Room", + **extra_options, + }, + title="Bed Room", + ) + group_config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(group_config_entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get(f"{group_type}.bed_room") + assert state.attributes["entity_id"] == members1 + + config_entry = hass.config_entries.async_entries(DOMAIN)[0] + + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == group_type + assert get_suggested(result["data_schema"].schema, "entities") == members1 + assert "name" not in result["data_schema"].schema + assert result["data_schema"].schema["entities"].config["exclude_entities"] == [ + f"{group_type}.bed_room" + ] + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + "entities": members2, + }, + ) + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["data"] == { + "entities": members2, + "group_type": group_type, + "hide_members": False, + "name": "Bed Room", + **extra_options, + } + assert config_entry.data == {} + assert config_entry.options == { + "entities": members2, + "group_type": group_type, + "hide_members": False, + "name": "Bed Room", + **extra_options, + } + assert config_entry.title == "Bed Room" + + # Check config entry is reloaded with new options + await hass.async_block_till_done() + state = hass.states.get(f"{group_type}.bed_room") + assert state.attributes["entity_id"] == members2 + + # Check we don't get suggestions from another entry + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_MENU + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"next_step_id": group_type}, + ) + await hass.async_block_till_done() + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == group_type + + assert get_suggested(result["data_schema"].schema, "entities") is None + assert get_suggested(result["data_schema"].schema, "name") is None + + +@pytest.mark.parametrize( + "group_type,extra_options,extra_options_after,advanced", + ( + ("light", {"all": False}, {"all": False}, False), + ("light", {"all": True}, {"all": True}, False), + ("light", {"all": False}, {"all": False}, True), + ("light", {"all": True}, {"all": False}, True), + ("switch", {"all": False}, {"all": False}, False), + ("switch", {"all": True}, {"all": True}, False), + ("switch", {"all": False}, {"all": False}, True), + ("switch", {"all": True}, {"all": False}, True), + ), +) +async def test_all_options( + hass: HomeAssistant, group_type, extra_options, extra_options_after, advanced +) -> None: + """Test reconfiguring.""" + members1 = [f"{group_type}.one", f"{group_type}.two"] + members2 = [f"{group_type}.four", f"{group_type}.five"] + + group_config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + "entities": members1, + "group_type": group_type, + "name": "Bed Room", + **extra_options, + }, + title="Bed Room", + ) + group_config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(group_config_entry.entry_id) + await hass.async_block_till_done() + + assert hass.states.get(f"{group_type}.bed_room") + + config_entry = hass.config_entries.async_entries(DOMAIN)[0] + + result = await hass.config_entries.options.async_init( + config_entry.entry_id, context={"show_advanced_options": advanced} + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == group_type + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + "entities": members2, + }, + ) + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["data"] == { + "entities": members2, + "group_type": group_type, + "hide_members": False, + "name": "Bed Room", + **extra_options_after, + } + assert config_entry.data == {} + assert config_entry.options == { + "entities": members2, + "group_type": group_type, + "hide_members": False, + "name": "Bed Room", + **extra_options_after, + } + assert config_entry.title == "Bed Room" + + +@pytest.mark.parametrize( + "hide_members,hidden_by_initial,hidden_by", + ((False, "integration", None), (True, None, "integration")), +) +@pytest.mark.parametrize( + "group_type,extra_input", + ( + ("binary_sensor", {"all": False}), + ("cover", {}), + ("fan", {}), + ("light", {}), + ("lock", {}), + ("media_player", {}), + ("switch", {}), + ), +) +async def test_options_flow_hides_members( + hass: HomeAssistant, + group_type, + extra_input, + hide_members, + hidden_by_initial, + hidden_by, +) -> None: + """Test the options flow hides or unhides members if requested.""" + fake_uuid = "a266a680b608c32770e6c45bfe6b8411" + registry = er.async_get(hass) + entry = registry.async_get_or_create( + group_type, + "test", + "unique1", + suggested_object_id="one", + hidden_by=hidden_by_initial, + ) + assert entry.entity_id == f"{group_type}.one" + + entry = registry.async_get_or_create( + group_type, + "test", + "unique3", + suggested_object_id="three", + hidden_by=hidden_by_initial, + ) + assert entry.entity_id == f"{group_type}.three" + + members = [f"{group_type}.one", f"{group_type}.two", fake_uuid, entry.id] + + group_config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + "entities": members, + "group_type": group_type, + "hide_members": False, + "name": "Bed Room", + **extra_input, + }, + title="Bed Room", + ) + group_config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(group_config_entry.entry_id) + await hass.async_block_till_done() + + result = await hass.config_entries.options.async_init(group_config_entry.entry_id) + assert result["type"] == RESULT_TYPE_FORM + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + "entities": members, + "hide_members": hide_members, + }, + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + + assert registry.async_get(f"{group_type}.one").hidden_by == hidden_by + assert registry.async_get(f"{group_type}.three").hidden_by == hidden_by diff --git a/tests/components/group/test_init.py b/tests/components/group/test_init.py index 4eb0e455422..56553ff263c 100644 --- a/tests/components/group/test_init.py +++ b/tests/components/group/test_init.py @@ -1,6 +1,9 @@ """The tests for the Group components.""" # pylint: disable=protected-access +from __future__ import annotations + from collections import OrderedDict +from typing import Any from unittest.mock import patch import pytest @@ -18,11 +21,12 @@ from homeassistant.const import ( STATE_ON, STATE_UNKNOWN, ) -from homeassistant.core import CoreState +from homeassistant.core import CoreState, HomeAssistant +from homeassistant.helpers import entity_registry as er from homeassistant.helpers.event import TRACK_STATE_CHANGE_CALLBACKS from homeassistant.setup import async_setup_component -from tests.common import assert_setup_component +from tests.common import MockConfigEntry, assert_setup_component from tests.components.group import common @@ -1358,3 +1362,151 @@ async def test_plant_group(hass): await hass.async_block_till_done() assert hass.states.get("group.plants").state == "problem" assert hass.states.get("group.plant_with_binary_sensors").state == "on" + + +@pytest.mark.parametrize( + "group_type,member_state,extra_options", + ( + ("binary_sensor", "on", {"all": False}), + ("cover", "open", {}), + ("fan", "on", {}), + ("light", "on", {"all": False}), + ("media_player", "on", {}), + ), +) +async def test_setup_and_remove_config_entry( + hass: HomeAssistant, + group_type: str, + member_state: str, + extra_options: dict[str, Any], +) -> None: + """Test removing a config entry.""" + registry = er.async_get(hass) + + members1 = [f"{group_type}.one", f"{group_type}.two"] + + for member in members1: + hass.states.async_set(member, member_state, {}) + + # Setup the config entry + group_config_entry = MockConfigEntry( + data={}, + domain=group.DOMAIN, + options={ + "entities": members1, + "group_type": group_type, + "name": "Bed Room", + **extra_options, + }, + title="Bed Room", + ) + group_config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(group_config_entry.entry_id) + await hass.async_block_till_done() + + # Check the state and entity registry entry are present + state = hass.states.get(f"{group_type}.bed_room") + assert state.attributes["entity_id"] == members1 + assert registry.async_get(f"{group_type}.bed_room") is not None + + # Remove the config entry + assert await hass.config_entries.async_remove(group_config_entry.entry_id) + await hass.async_block_till_done() + + # Check the state and entity registry entry are removed + assert hass.states.get(f"{group_type}.bed_room") is None + assert registry.async_get(f"{group_type}.bed_room") is None + + +@pytest.mark.parametrize( + "hide_members,hidden_by_initial,hidden_by", + ( + (False, "integration", "integration"), + (False, None, None), + (False, "user", "user"), + (True, "integration", None), + (True, None, None), + (True, "user", "user"), + ), +) +@pytest.mark.parametrize( + "group_type,extra_options", + ( + ("binary_sensor", {"all": False}), + ("cover", {}), + ("fan", {}), + ("light", {"all": False}), + ("media_player", {}), + ), +) +async def test_unhide_members_on_remove( + hass: HomeAssistant, + group_type: str, + extra_options: dict[str, Any], + hide_members: bool, + hidden_by_initial: str, + hidden_by: str, +) -> None: + """Test removing a config entry.""" + registry = er.async_get(hass) + + registry = er.async_get(hass) + entry1 = registry.async_get_or_create( + group_type, + "test", + "unique1", + suggested_object_id="one", + hidden_by=hidden_by_initial, + ) + assert entry1.entity_id == f"{group_type}.one" + + entry3 = registry.async_get_or_create( + group_type, + "test", + "unique3", + suggested_object_id="three", + hidden_by=hidden_by_initial, + ) + assert entry3.entity_id == f"{group_type}.three" + + entry4 = registry.async_get_or_create( + group_type, + "test", + "unique4", + suggested_object_id="four", + ) + assert entry4.entity_id == f"{group_type}.four" + + members = [f"{group_type}.one", f"{group_type}.two", entry3.id, entry4.id] + + # Setup the config entry + group_config_entry = MockConfigEntry( + data={}, + domain=group.DOMAIN, + options={ + "entities": members, + "group_type": group_type, + "hide_members": hide_members, + "name": "Bed Room", + **extra_options, + }, + title="Bed Room", + ) + group_config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(group_config_entry.entry_id) + await hass.async_block_till_done() + + # Check the state is present + assert hass.states.get(f"{group_type}.bed_room") + + # Remove one entity registry entry, to make sure this does not trip up config entry + # removal + registry.async_remove(entry4.entity_id) + + # Remove the config entry + assert await hass.config_entries.async_remove(group_config_entry.entry_id) + await hass.async_block_till_done() + + # Check the group members are unhidden + assert registry.async_get(f"{group_type}.one").hidden_by == hidden_by + assert registry.async_get(f"{group_type}.three").hidden_by == hidden_by diff --git a/tests/components/group/test_light.py b/tests/components/group/test_light.py index d356b20b40f..0f125dd3e88 100644 --- a/tests/components/group/test_light.py +++ b/tests/components/group/test_light.py @@ -49,6 +49,7 @@ from homeassistant.const import ( STATE_OFF, STATE_ON, STATE_UNAVAILABLE, + STATE_UNKNOWN, ) from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component @@ -68,6 +69,7 @@ async def test_default_state(hass): "entities": ["light.kitchen", "light.bedroom"], "name": "Bedroom Group", "unique_id": "unique_identifier", + "all": "false", } }, ) @@ -102,6 +104,7 @@ async def test_state_reporting(hass): LIGHT_DOMAIN: { "platform": DOMAIN, "entities": ["light.test1", "light.test2"], + "all": "false", } }, ) @@ -130,6 +133,49 @@ async def test_state_reporting(hass): assert hass.states.get("light.light_group").state == STATE_UNAVAILABLE +async def test_state_reporting_all(hass): + """Test the state reporting.""" + await async_setup_component( + hass, + LIGHT_DOMAIN, + { + LIGHT_DOMAIN: { + "platform": DOMAIN, + "entities": ["light.test1", "light.test2"], + "all": "true", + } + }, + ) + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + hass.states.async_set("light.test1", STATE_ON) + hass.states.async_set("light.test2", STATE_UNAVAILABLE) + await hass.async_block_till_done() + assert hass.states.get("light.light_group").state == STATE_UNKNOWN + + hass.states.async_set("light.test1", STATE_ON) + hass.states.async_set("light.test2", STATE_OFF) + await hass.async_block_till_done() + assert hass.states.get("light.light_group").state == STATE_OFF + + hass.states.async_set("light.test1", STATE_OFF) + hass.states.async_set("light.test2", STATE_OFF) + await hass.async_block_till_done() + assert hass.states.get("light.light_group").state == STATE_OFF + + hass.states.async_set("light.test1", STATE_ON) + hass.states.async_set("light.test2", STATE_ON) + await hass.async_block_till_done() + assert hass.states.get("light.light_group").state == STATE_ON + + hass.states.async_set("light.test1", STATE_UNAVAILABLE) + hass.states.async_set("light.test2", STATE_UNAVAILABLE) + await hass.async_block_till_done() + assert hass.states.get("light.light_group").state == STATE_UNAVAILABLE + + async def test_brightness(hass, enable_custom_integrations): """Test brightness reporting.""" platform = getattr(hass.components, "test.light") @@ -155,6 +201,7 @@ async def test_brightness(hass, enable_custom_integrations): { "platform": DOMAIN, "entities": ["light.test1", "light.test2"], + "all": "false", }, ] }, @@ -225,6 +272,7 @@ async def test_color_hs(hass, enable_custom_integrations): { "platform": DOMAIN, "entities": ["light.test1", "light.test2"], + "all": "false", }, ] }, @@ -296,6 +344,7 @@ async def test_color_rgb(hass, enable_custom_integrations): { "platform": DOMAIN, "entities": ["light.test1", "light.test2"], + "all": "false", }, ] }, @@ -367,6 +416,7 @@ async def test_color_rgbw(hass, enable_custom_integrations): { "platform": DOMAIN, "entities": ["light.test1", "light.test2"], + "all": "false", }, ] }, @@ -438,6 +488,7 @@ async def test_color_rgbww(hass, enable_custom_integrations): { "platform": DOMAIN, "entities": ["light.test1", "light.test2"], + "all": "false", }, ] }, @@ -489,6 +540,7 @@ async def test_white_value(hass): LIGHT_DOMAIN: { "platform": DOMAIN, "entities": ["light.test1", "light.test2"], + "all": "false", } }, ) @@ -548,6 +600,7 @@ async def test_white(hass, enable_custom_integrations): { "platform": DOMAIN, "entities": ["light.test1", "light.test2"], + "all": "false", }, ] }, @@ -603,6 +656,7 @@ async def test_color_temp(hass, enable_custom_integrations): { "platform": DOMAIN, "entities": ["light.test1", "light.test2"], + "all": "false", }, ] }, @@ -674,6 +728,7 @@ async def test_emulated_color_temp_group(hass, enable_custom_integrations): { "platform": DOMAIN, "entities": ["light.test1", "light.test2", "light.test3"], + "all": "false", }, ] }, @@ -739,6 +794,7 @@ async def test_min_max_mireds(hass, enable_custom_integrations): { "platform": DOMAIN, "entities": ["light.test1", "light.test2"], + "all": "false", }, ] }, @@ -784,6 +840,7 @@ async def test_effect_list(hass): LIGHT_DOMAIN: { "platform": DOMAIN, "entities": ["light.test1", "light.test2"], + "all": "false", } }, ) @@ -843,6 +900,7 @@ async def test_effect(hass): LIGHT_DOMAIN: { "platform": DOMAIN, "entities": ["light.test1", "light.test2", "light.test3"], + "all": "false", } }, ) @@ -909,6 +967,7 @@ async def test_supported_color_modes(hass, enable_custom_integrations): { "platform": DOMAIN, "entities": ["light.test1", "light.test2", "light.test3"], + "all": "false", }, ] }, @@ -957,6 +1016,7 @@ async def test_color_mode(hass, enable_custom_integrations): { "platform": DOMAIN, "entities": ["light.test1", "light.test2", "light.test3"], + "all": "false", }, ] }, @@ -1051,6 +1111,7 @@ async def test_color_mode2(hass, enable_custom_integrations): "light.test5", "light.test6", ], + "all": "false", }, ] }, @@ -1082,6 +1143,7 @@ async def test_supported_features(hass): LIGHT_DOMAIN: { "platform": DOMAIN, "entities": ["light.test1", "light.test2"], + "all": "false", } }, ) @@ -1157,6 +1219,7 @@ async def test_service_calls(hass, enable_custom_integrations, supported_color_m "light.ceiling_lights", "light.kitchen_lights", ], + "all": "false", }, ] }, @@ -1269,6 +1332,7 @@ async def test_service_call_effect(hass): "light.ceiling_lights", "light.kitchen_lights", ], + "all": "false", }, ] }, @@ -1369,6 +1433,7 @@ async def test_reload(hass): "light.ceiling_lights", "light.kitchen_lights", ], + "all": "false", }, ] }, @@ -1481,11 +1546,13 @@ async def test_nested_group(hass): "platform": DOMAIN, "entities": ["light.bedroom_group"], "name": "Nested Group", + "all": "false", }, { "platform": DOMAIN, "entities": ["light.bed_light", "light.kitchen_lights"], "name": "Bedroom Group", + "all": "false", }, ] }, diff --git a/tests/components/group/test_lock.py b/tests/components/group/test_lock.py new file mode 100644 index 00000000000..8db28fab18e --- /dev/null +++ b/tests/components/group/test_lock.py @@ -0,0 +1,336 @@ +"""The tests for the Group Lock platform.""" +from unittest.mock import patch + +from homeassistant import config as hass_config +from homeassistant.components.demo import lock as demo_lock +from homeassistant.components.group import DOMAIN, SERVICE_RELOAD +from homeassistant.components.lock import ( + DOMAIN as LOCK_DOMAIN, + SERVICE_LOCK, + SERVICE_OPEN, + SERVICE_UNLOCK, +) +from homeassistant.const import ( + ATTR_ENTITY_ID, + STATE_JAMMED, + STATE_LOCKED, + STATE_LOCKING, + STATE_UNAVAILABLE, + STATE_UNKNOWN, + STATE_UNLOCKED, + STATE_UNLOCKING, +) +from homeassistant.helpers import entity_registry as er +from homeassistant.setup import async_setup_component + +from tests.common import get_fixture_path + + +async def test_default_state(hass): + """Test lock group default state.""" + hass.states.async_set("lock.front", "locked") + await async_setup_component( + hass, + LOCK_DOMAIN, + { + LOCK_DOMAIN: { + "platform": DOMAIN, + "entities": ["lock.front", "lock.back"], + "name": "Door Group", + "unique_id": "unique_identifier", + } + }, + ) + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + state = hass.states.get("lock.door_group") + assert state is not None + assert state.state == STATE_LOCKED + assert state.attributes.get(ATTR_ENTITY_ID) == ["lock.front", "lock.back"] + + entity_registry = er.async_get(hass) + entry = entity_registry.async_get("lock.door_group") + assert entry + assert entry.unique_id == "unique_identifier" + + +async def test_state_reporting(hass): + """Test the state reporting.""" + await async_setup_component( + hass, + LOCK_DOMAIN, + { + LOCK_DOMAIN: { + "platform": DOMAIN, + "entities": ["lock.test1", "lock.test2"], + } + }, + ) + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + hass.states.async_set("lock.test1", STATE_LOCKED) + hass.states.async_set("lock.test2", STATE_UNAVAILABLE) + await hass.async_block_till_done() + assert hass.states.get("lock.lock_group").state == STATE_UNKNOWN + + hass.states.async_set("lock.test1", STATE_LOCKED) + hass.states.async_set("lock.test2", STATE_UNLOCKED) + await hass.async_block_till_done() + assert hass.states.get("lock.lock_group").state == STATE_UNLOCKED + + hass.states.async_set("lock.test1", STATE_LOCKED) + hass.states.async_set("lock.test2", STATE_LOCKED) + await hass.async_block_till_done() + assert hass.states.get("lock.lock_group").state == STATE_LOCKED + + hass.states.async_set("lock.test1", STATE_UNLOCKED) + hass.states.async_set("lock.test2", STATE_UNLOCKED) + await hass.async_block_till_done() + assert hass.states.get("lock.lock_group").state == STATE_UNLOCKED + + hass.states.async_set("lock.test1", STATE_UNLOCKED) + hass.states.async_set("lock.test2", STATE_JAMMED) + await hass.async_block_till_done() + assert hass.states.get("lock.lock_group").state == STATE_JAMMED + + hass.states.async_set("lock.test1", STATE_LOCKED) + hass.states.async_set("lock.test2", STATE_UNLOCKING) + await hass.async_block_till_done() + assert hass.states.get("lock.lock_group").state == STATE_UNLOCKING + + hass.states.async_set("lock.test1", STATE_UNLOCKED) + hass.states.async_set("lock.test2", STATE_LOCKING) + await hass.async_block_till_done() + assert hass.states.get("lock.lock_group").state == STATE_LOCKING + + hass.states.async_set("lock.test1", STATE_UNAVAILABLE) + hass.states.async_set("lock.test2", STATE_UNAVAILABLE) + await hass.async_block_till_done() + assert hass.states.get("lock.lock_group").state == STATE_UNAVAILABLE + + +@patch.object(demo_lock, "LOCK_UNLOCK_DELAY", 0) +async def test_service_calls(hass, enable_custom_integrations): + """Test service calls.""" + await async_setup_component( + hass, + LOCK_DOMAIN, + { + LOCK_DOMAIN: [ + {"platform": "demo"}, + { + "platform": DOMAIN, + "entities": [ + "lock.front_door", + "lock.kitchen_door", + ], + }, + ] + }, + ) + await hass.async_block_till_done() + + group_state = hass.states.get("lock.lock_group") + assert group_state.state == STATE_UNLOCKED + assert hass.states.get("lock.front_door").state == STATE_LOCKED + assert hass.states.get("lock.kitchen_door").state == STATE_UNLOCKED + + await hass.services.async_call( + LOCK_DOMAIN, + SERVICE_OPEN, + {ATTR_ENTITY_ID: "lock.lock_group"}, + blocking=True, + ) + assert hass.states.get("lock.front_door").state == STATE_UNLOCKED + assert hass.states.get("lock.kitchen_door").state == STATE_UNLOCKED + + await hass.services.async_call( + LOCK_DOMAIN, + SERVICE_LOCK, + {ATTR_ENTITY_ID: "lock.lock_group"}, + blocking=True, + ) + assert hass.states.get("lock.front_door").state == STATE_LOCKED + assert hass.states.get("lock.kitchen_door").state == STATE_LOCKED + + await hass.services.async_call( + LOCK_DOMAIN, + SERVICE_UNLOCK, + {ATTR_ENTITY_ID: "lock.lock_group"}, + blocking=True, + ) + assert hass.states.get("lock.front_door").state == STATE_UNLOCKED + assert hass.states.get("lock.kitchen_door").state == STATE_UNLOCKED + + +async def test_reload(hass): + """Test the ability to reload locks.""" + await async_setup_component( + hass, + LOCK_DOMAIN, + { + LOCK_DOMAIN: [ + {"platform": "demo"}, + { + "platform": DOMAIN, + "entities": [ + "lock.front_door", + "lock.kitchen_door", + ], + }, + ] + }, + ) + await hass.async_block_till_done() + + await hass.async_block_till_done() + await hass.async_start() + + await hass.async_block_till_done() + assert hass.states.get("lock.lock_group").state == STATE_UNLOCKED + + yaml_path = get_fixture_path("configuration.yaml", "group") + with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path): + await hass.services.async_call( + DOMAIN, + SERVICE_RELOAD, + {}, + blocking=True, + ) + await hass.async_block_till_done() + + assert hass.states.get("lock.lock_group") is None + assert hass.states.get("lock.inside_locks_g") is not None + assert hass.states.get("lock.outside_locks_g") is not None + + +async def test_reload_with_platform_not_setup(hass): + """Test the ability to reload locks.""" + hass.states.async_set("lock.something", STATE_UNLOCKED) + await async_setup_component( + hass, + LOCK_DOMAIN, + { + LOCK_DOMAIN: [ + {"platform": "demo"}, + ] + }, + ) + assert await async_setup_component( + hass, + "group", + { + "group": { + "group_zero": {"entities": "lock.something", "icon": "mdi:work"}, + } + }, + ) + await hass.async_block_till_done() + + yaml_path = get_fixture_path("configuration.yaml", "group") + with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path): + await hass.services.async_call( + DOMAIN, + SERVICE_RELOAD, + {}, + blocking=True, + ) + await hass.async_block_till_done() + + assert hass.states.get("lock.lock_group") is None + assert hass.states.get("lock.inside_locks_g") is not None + assert hass.states.get("lock.outside_locks_g") is not None + + +async def test_reload_with_base_integration_platform_not_setup(hass): + """Test the ability to reload locks.""" + assert await async_setup_component( + hass, + "group", + { + "group": { + "group_zero": {"entities": "lock.something", "icon": "mdi:work"}, + } + }, + ) + await hass.async_block_till_done() + hass.states.async_set("lock.front_lock", STATE_LOCKED) + hass.states.async_set("lock.back_lock", STATE_UNLOCKED) + + hass.states.async_set("lock.outside_lock", STATE_LOCKED) + hass.states.async_set("lock.outside_lock_2", STATE_LOCKED) + + yaml_path = get_fixture_path("configuration.yaml", "group") + with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path): + await hass.services.async_call( + DOMAIN, + SERVICE_RELOAD, + {}, + blocking=True, + ) + await hass.async_block_till_done() + + assert hass.states.get("lock.lock_group") is None + assert hass.states.get("lock.inside_locks_g") is not None + assert hass.states.get("lock.outside_locks_g") is not None + assert hass.states.get("lock.inside_locks_g").state == STATE_UNLOCKED + assert hass.states.get("lock.outside_locks_g").state == STATE_LOCKED + + +@patch.object(demo_lock, "LOCK_UNLOCK_DELAY", 0) +async def test_nested_group(hass): + """Test nested lock group.""" + await async_setup_component( + hass, + LOCK_DOMAIN, + { + LOCK_DOMAIN: [ + {"platform": "demo"}, + { + "platform": DOMAIN, + "entities": ["lock.some_group"], + "name": "Nested Group", + }, + { + "platform": DOMAIN, + "entities": [ + "lock.front_door", + "lock.kitchen_door", + ], + "name": "Some Group", + }, + ] + }, + ) + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + state = hass.states.get("lock.some_group") + assert state is not None + assert state.state == STATE_UNLOCKED + assert state.attributes.get(ATTR_ENTITY_ID) == [ + "lock.front_door", + "lock.kitchen_door", + ] + + state = hass.states.get("lock.nested_group") + assert state is not None + assert state.state == STATE_UNLOCKED + assert state.attributes.get(ATTR_ENTITY_ID) == ["lock.some_group"] + + # Test controlling the nested group + await hass.services.async_call( + LOCK_DOMAIN, + SERVICE_LOCK, + {ATTR_ENTITY_ID: "lock.nested_group"}, + blocking=True, + ) + assert hass.states.get("lock.front_door").state == STATE_LOCKED + assert hass.states.get("lock.kitchen_door").state == STATE_LOCKED + assert hass.states.get("lock.some_group").state == STATE_LOCKED + assert hass.states.get("lock.nested_group").state == STATE_LOCKED diff --git a/tests/components/group/test_switch.py b/tests/components/group/test_switch.py new file mode 100644 index 00000000000..5df2542d101 --- /dev/null +++ b/tests/components/group/test_switch.py @@ -0,0 +1,359 @@ +"""The tests for the Group Switch platform.""" +from unittest.mock import patch + +import async_timeout + +from homeassistant import config as hass_config +from homeassistant.components.group import DOMAIN, SERVICE_RELOAD +from homeassistant.components.switch import ( + DOMAIN as SWITCH_DOMAIN, + SERVICE_TOGGLE, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, +) +from homeassistant.const import ( + ATTR_ENTITY_ID, + STATE_OFF, + STATE_ON, + STATE_UNAVAILABLE, + STATE_UNKNOWN, +) +from homeassistant.helpers import entity_registry as er +from homeassistant.setup import async_setup_component + +from tests.common import get_fixture_path + + +async def test_default_state(hass): + """Test switch group default state.""" + hass.states.async_set("switch.tv", "on") + await async_setup_component( + hass, + SWITCH_DOMAIN, + { + SWITCH_DOMAIN: { + "platform": DOMAIN, + "entities": ["switch.tv", "switch.soundbar"], + "name": "Multimedia Group", + "unique_id": "unique_identifier", + "all": "false", + } + }, + ) + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + state = hass.states.get("switch.multimedia_group") + assert state is not None + assert state.state == STATE_ON + assert state.attributes.get(ATTR_ENTITY_ID) == ["switch.tv", "switch.soundbar"] + + entity_registry = er.async_get(hass) + entry = entity_registry.async_get("switch.multimedia_group") + assert entry + assert entry.unique_id == "unique_identifier" + + +async def test_state_reporting(hass): + """Test the state reporting.""" + await async_setup_component( + hass, + SWITCH_DOMAIN, + { + SWITCH_DOMAIN: { + "platform": DOMAIN, + "entities": ["switch.test1", "switch.test2"], + "all": "false", + } + }, + ) + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + hass.states.async_set("switch.test1", STATE_ON) + hass.states.async_set("switch.test2", STATE_UNAVAILABLE) + await hass.async_block_till_done() + assert hass.states.get("switch.switch_group").state == STATE_ON + + hass.states.async_set("switch.test1", STATE_ON) + hass.states.async_set("switch.test2", STATE_OFF) + await hass.async_block_till_done() + assert hass.states.get("switch.switch_group").state == STATE_ON + + hass.states.async_set("switch.test1", STATE_OFF) + hass.states.async_set("switch.test2", STATE_OFF) + await hass.async_block_till_done() + assert hass.states.get("switch.switch_group").state == STATE_OFF + + hass.states.async_set("switch.test1", STATE_UNAVAILABLE) + hass.states.async_set("switch.test2", STATE_UNAVAILABLE) + await hass.async_block_till_done() + assert hass.states.get("switch.switch_group").state == STATE_UNAVAILABLE + + +async def test_state_reporting_all(hass): + """Test the state reporting.""" + await async_setup_component( + hass, + SWITCH_DOMAIN, + { + SWITCH_DOMAIN: { + "platform": DOMAIN, + "entities": ["switch.test1", "switch.test2"], + "all": "true", + } + }, + ) + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + hass.states.async_set("switch.test1", STATE_ON) + hass.states.async_set("switch.test2", STATE_UNAVAILABLE) + await hass.async_block_till_done() + assert hass.states.get("switch.switch_group").state == STATE_UNKNOWN + + hass.states.async_set("switch.test1", STATE_ON) + hass.states.async_set("switch.test2", STATE_OFF) + await hass.async_block_till_done() + assert hass.states.get("switch.switch_group").state == STATE_OFF + + hass.states.async_set("switch.test1", STATE_OFF) + hass.states.async_set("switch.test2", STATE_OFF) + await hass.async_block_till_done() + assert hass.states.get("switch.switch_group").state == STATE_OFF + + hass.states.async_set("switch.test1", STATE_ON) + hass.states.async_set("switch.test2", STATE_ON) + await hass.async_block_till_done() + assert hass.states.get("switch.switch_group").state == STATE_ON + + hass.states.async_set("switch.test1", STATE_UNAVAILABLE) + hass.states.async_set("switch.test2", STATE_UNAVAILABLE) + await hass.async_block_till_done() + assert hass.states.get("switch.switch_group").state == STATE_UNAVAILABLE + + +async def test_service_calls(hass, enable_custom_integrations): + """Test service calls.""" + await async_setup_component( + hass, + SWITCH_DOMAIN, + { + SWITCH_DOMAIN: [ + {"platform": "demo"}, + { + "platform": DOMAIN, + "entities": [ + "switch.ac", + "switch.decorative_lights", + ], + "all": "false", + }, + ] + }, + ) + await hass.async_block_till_done() + + group_state = hass.states.get("switch.switch_group") + assert group_state.state == STATE_ON + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TOGGLE, + {ATTR_ENTITY_ID: "switch.switch_group"}, + blocking=True, + ) + assert hass.states.get("switch.ac").state == STATE_OFF + assert hass.states.get("switch.decorative_lights").state == STATE_OFF + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "switch.switch_group"}, + blocking=True, + ) + + assert hass.states.get("switch.ac").state == STATE_ON + assert hass.states.get("switch.decorative_lights").state == STATE_ON + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "switch.switch_group"}, + blocking=True, + ) + + assert hass.states.get("switch.ac").state == STATE_OFF + assert hass.states.get("switch.decorative_lights").state == STATE_OFF + + +async def test_reload(hass): + """Test the ability to reload switches.""" + await async_setup_component( + hass, + SWITCH_DOMAIN, + { + SWITCH_DOMAIN: [ + {"platform": "demo"}, + { + "platform": DOMAIN, + "entities": [ + "switch.ac", + "switch.decorative_lights", + ], + "all": "false", + }, + ] + }, + ) + await hass.async_block_till_done() + + await hass.async_block_till_done() + await hass.async_start() + + await hass.async_block_till_done() + assert hass.states.get("switch.switch_group").state == STATE_ON + + yaml_path = get_fixture_path("configuration.yaml", "group") + with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path): + await hass.services.async_call( + DOMAIN, + SERVICE_RELOAD, + {}, + blocking=True, + ) + await hass.async_block_till_done() + + assert hass.states.get("switch.switch_group") is None + assert hass.states.get("switch.master_switches_g") is not None + assert hass.states.get("switch.outside_switches_g") is not None + + +async def test_reload_with_platform_not_setup(hass): + """Test the ability to reload switches.""" + hass.states.async_set("switch.something", STATE_ON) + await async_setup_component( + hass, + SWITCH_DOMAIN, + { + SWITCH_DOMAIN: [ + {"platform": "demo"}, + ] + }, + ) + assert await async_setup_component( + hass, + "group", + { + "group": { + "group_zero": {"entities": "switch.something", "icon": "mdi:work"}, + } + }, + ) + await hass.async_block_till_done() + + yaml_path = get_fixture_path("configuration.yaml", "group") + with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path): + await hass.services.async_call( + DOMAIN, + SERVICE_RELOAD, + {}, + blocking=True, + ) + await hass.async_block_till_done() + + assert hass.states.get("switch.switch_group") is None + assert hass.states.get("switch.master_switches_g") is not None + assert hass.states.get("switch.outside_switches_g") is not None + + +async def test_reload_with_base_integration_platform_not_setup(hass): + """Test the ability to reload switches.""" + assert await async_setup_component( + hass, + "group", + { + "group": { + "group_zero": {"entities": "switch.something", "icon": "mdi:work"}, + } + }, + ) + await hass.async_block_till_done() + hass.states.async_set("switch.master_switch", STATE_ON) + hass.states.async_set("switch.master_switch_2", STATE_OFF) + + hass.states.async_set("switch.outside_switch", STATE_OFF) + hass.states.async_set("switch.outside_switch_2", STATE_OFF) + + yaml_path = get_fixture_path("configuration.yaml", "group") + with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path): + await hass.services.async_call( + DOMAIN, + SERVICE_RELOAD, + {}, + blocking=True, + ) + await hass.async_block_till_done() + + assert hass.states.get("switch.switch_group") is None + assert hass.states.get("switch.master_switches_g") is not None + assert hass.states.get("switch.outside_switches_g") is not None + assert hass.states.get("switch.master_switches_g").state == STATE_ON + assert hass.states.get("switch.outside_switches_g").state == STATE_OFF + + +async def test_nested_group(hass): + """Test nested switch group.""" + await async_setup_component( + hass, + SWITCH_DOMAIN, + { + SWITCH_DOMAIN: [ + {"platform": "demo"}, + { + "platform": DOMAIN, + "entities": ["switch.some_group"], + "name": "Nested Group", + "all": "false", + }, + { + "platform": DOMAIN, + "entities": ["switch.ac", "switch.decorative_lights"], + "name": "Some Group", + "all": "false", + }, + ] + }, + ) + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + state = hass.states.get("switch.some_group") + assert state is not None + assert state.state == STATE_ON + assert state.attributes.get(ATTR_ENTITY_ID) == [ + "switch.ac", + "switch.decorative_lights", + ] + + state = hass.states.get("switch.nested_group") + assert state is not None + assert state.state == STATE_ON + assert state.attributes.get(ATTR_ENTITY_ID) == ["switch.some_group"] + + # Test controlling the nested group + async with async_timeout.timeout(0.5): + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TOGGLE, + {ATTR_ENTITY_ID: "switch.nested_group"}, + blocking=True, + ) + assert hass.states.get("switch.ac").state == STATE_OFF + assert hass.states.get("switch.decorative_lights").state == STATE_OFF + assert hass.states.get("switch.some_group").state == STATE_OFF + assert hass.states.get("switch.nested_group").state == STATE_OFF diff --git a/tests/components/guardian/fixtures/sensor_pair_dump_data.json b/tests/components/guardian/fixtures/sensor_pair_dump_data.json index 2186b987cc9..ec674cc74cf 100644 --- a/tests/components/guardian/fixtures/sensor_pair_dump_data.json +++ b/tests/components/guardian/fixtures/sensor_pair_dump_data.json @@ -3,8 +3,6 @@ "status": "ok", "data": { "pair_count": 1, - "paired_uids": [ - "6309FB799CDE" - ] + "paired_uids": ["6309FB799CDE"] } } diff --git a/tests/components/hassio/test_binary_sensor.py b/tests/components/hassio/test_binary_sensor.py index e4263eb5529..a49b27ba4e7 100644 --- a/tests/components/hassio/test_binary_sensor.py +++ b/tests/components/hassio/test_binary_sensor.py @@ -69,6 +69,7 @@ def mock_all(aioclient_mock, request): "result": "ok", "data": { "result": "ok", + "version": "1.0.0", "version_latest": "1.0.0", "addons": [ { @@ -113,9 +114,20 @@ def mock_all(aioclient_mock, request): }, }, ) + aioclient_mock.get("http://127.0.0.1/addons/test/changelog", text="") + aioclient_mock.get( + "http://127.0.0.1/addons/test/info", + json={"result": "ok", "data": {"auto_update": True}}, + ) + aioclient_mock.get("http://127.0.0.1/addons/test2/changelog", text="") + aioclient_mock.get( + "http://127.0.0.1/addons/test2/info", + json={"result": "ok", "data": {"auto_update": False}}, + ) aioclient_mock.get( "http://127.0.0.1/ingress/panels", json={"result": "ok", "data": {"panels": {}}} ) + aioclient_mock.post("http://127.0.0.1/refresh_updates", json={"result": "ok"}) @pytest.mark.parametrize( diff --git a/tests/components/hassio/test_init.py b/tests/components/hassio/test_init.py index 689ec138043..527c98b615e 100644 --- a/tests/components/hassio/test_init.py +++ b/tests/components/hassio/test_init.py @@ -9,6 +9,7 @@ from homeassistant.auth.const import GROUP_ID_ADMIN from homeassistant.components import frontend from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.components.hassio import ADDONS_COORDINATOR, DOMAIN, STORAGE_KEY +from homeassistant.components.hassio.handler import HassioAPIError from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.helpers.device_registry import async_get from homeassistant.setup import async_setup_component @@ -55,7 +56,7 @@ def mock_all(aioclient_mock, request): ) aioclient_mock.get( "http://127.0.0.1/core/info", - json={"result": "ok", "data": {"version_latest": "1.0.0"}}, + json={"result": "ok", "data": {"version_latest": "1.0.0", "version": "1.0.0"}}, ) aioclient_mock.get( "http://127.0.0.1/os/info", @@ -65,7 +66,7 @@ def mock_all(aioclient_mock, request): "http://127.0.0.1/supervisor/info", json={ "result": "ok", - "data": {"version_latest": "1.0.0"}, + "data": {"version_latest": "1.0.0", "version": "1.0.0"}, "addons": [ { "name": "test", @@ -138,9 +139,20 @@ def mock_all(aioclient_mock, request): }, }, ) + aioclient_mock.get("http://127.0.0.1/addons/test/changelog", text="") + aioclient_mock.get( + "http://127.0.0.1/addons/test/info", + json={"result": "ok", "data": {"auto_update": True}}, + ) + aioclient_mock.get("http://127.0.0.1/addons/test2/changelog", text="") + aioclient_mock.get( + "http://127.0.0.1/addons/test2/info", + json={"result": "ok", "data": {"auto_update": False}}, + ) aioclient_mock.get( "http://127.0.0.1/ingress/panels", json={"result": "ok", "data": {"panels": {}}} ) + aioclient_mock.post("http://127.0.0.1/refresh_updates", json={"result": "ok"}) async def test_setup_api_ping(hass, aioclient_mock): @@ -149,7 +161,7 @@ async def test_setup_api_ping(hass, aioclient_mock): result = await async_setup_component(hass, "hassio", {}) assert result - assert aioclient_mock.call_count == 10 + assert aioclient_mock.call_count == 15 assert hass.components.hassio.get_core_info()["version_latest"] == "1.0.0" assert hass.components.hassio.is_hassio() @@ -188,7 +200,7 @@ async def test_setup_api_push_api_data(hass, aioclient_mock): ) assert result - assert aioclient_mock.call_count == 10 + assert aioclient_mock.call_count == 15 assert not aioclient_mock.mock_calls[1][2]["ssl"] assert aioclient_mock.mock_calls[1][2]["port"] == 9999 assert aioclient_mock.mock_calls[1][2]["watchdog"] @@ -204,7 +216,7 @@ async def test_setup_api_push_api_data_server_host(hass, aioclient_mock): ) assert result - assert aioclient_mock.call_count == 10 + assert aioclient_mock.call_count == 15 assert not aioclient_mock.mock_calls[1][2]["ssl"] assert aioclient_mock.mock_calls[1][2]["port"] == 9999 assert not aioclient_mock.mock_calls[1][2]["watchdog"] @@ -216,7 +228,7 @@ async def test_setup_api_push_api_data_default(hass, aioclient_mock, hass_storag result = await async_setup_component(hass, "hassio", {"http": {}, "hassio": {}}) assert result - assert aioclient_mock.call_count == 10 + assert aioclient_mock.call_count == 15 assert not aioclient_mock.mock_calls[1][2]["ssl"] assert aioclient_mock.mock_calls[1][2]["port"] == 8123 refresh_token = aioclient_mock.mock_calls[1][2]["refresh_token"] @@ -283,7 +295,7 @@ async def test_setup_api_existing_hassio_user(hass, aioclient_mock, hass_storage result = await async_setup_component(hass, "hassio", {"http": {}, "hassio": {}}) assert result - assert aioclient_mock.call_count == 10 + assert aioclient_mock.call_count == 15 assert not aioclient_mock.mock_calls[1][2]["ssl"] assert aioclient_mock.mock_calls[1][2]["port"] == 8123 assert aioclient_mock.mock_calls[1][2]["refresh_token"] == token.token @@ -297,7 +309,7 @@ async def test_setup_core_push_timezone(hass, aioclient_mock): result = await async_setup_component(hass, "hassio", {"hassio": {}}) assert result - assert aioclient_mock.call_count == 10 + assert aioclient_mock.call_count == 15 assert aioclient_mock.mock_calls[2][2]["timezone"] == "testzone" with patch("homeassistant.util.dt.set_default_time_zone"): @@ -314,7 +326,7 @@ async def test_setup_hassio_no_additional_data(hass, aioclient_mock): result = await async_setup_component(hass, "hassio", {"hassio": {}}) assert result - assert aioclient_mock.call_count == 10 + assert aioclient_mock.call_count == 15 assert aioclient_mock.mock_calls[-1][3]["X-Hassio-Key"] == "123456" @@ -496,12 +508,15 @@ async def test_device_registry_calls(hass): """Test device registry entries for hassio.""" dev_reg = async_get(hass) supervisor_mock_data = { + "version": "1.0.0", + "version_latest": "1.0.0", "addons": [ { "name": "test", "state": "started", "slug": "test", "installed": True, + "icon": False, "update_available": False, "version": "1.0.0", "version_latest": "1.0.0", @@ -513,12 +528,13 @@ async def test_device_registry_calls(hass): "state": "started", "slug": "test2", "installed": True, + "icon": False, "update_available": False, "version": "1.0.0", "version_latest": "1.0.0", "url": "https://github.com", }, - ] + ], } os_mock_data = { "board": "odroid-n2", @@ -539,21 +555,24 @@ async def test_device_registry_calls(hass): config_entry.add_to_hass(hass) assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert len(dev_reg.devices) == 3 + assert len(dev_reg.devices) == 5 supervisor_mock_data = { + "version": "1.0.0", + "version_latest": "1.0.0", "addons": [ { "name": "test2", "state": "started", "slug": "test2", "installed": True, + "icon": False, "update_available": False, "version": "1.0.0", "version_latest": "1.0.0", "url": "https://github.com", }, - ] + ], } # Test that when addon is removed, next update will remove the add-on and subsequent updates won't @@ -566,19 +585,22 @@ async def test_device_registry_calls(hass): ): async_fire_time_changed(hass, dt_util.now() + timedelta(hours=1)) await hass.async_block_till_done() - assert len(dev_reg.devices) == 2 + assert len(dev_reg.devices) == 4 async_fire_time_changed(hass, dt_util.now() + timedelta(hours=2)) await hass.async_block_till_done() - assert len(dev_reg.devices) == 2 + assert len(dev_reg.devices) == 4 supervisor_mock_data = { + "version": "1.0.0", + "version_latest": "1.0.0", "addons": [ { "name": "test2", "slug": "test2", "state": "started", "installed": True, + "icon": False, "update_available": False, "version": "1.0.0", "version_latest": "1.0.0", @@ -589,12 +611,13 @@ async def test_device_registry_calls(hass): "slug": "test3", "state": "stopped", "installed": True, + "icon": False, "update_available": False, "version": "1.0.0", "version_latest": "1.0.0", "url": "https://github.com", }, - ] + ], } # Test that when addon is added, next update will reload the entry so we register @@ -608,4 +631,28 @@ async def test_device_registry_calls(hass): ): async_fire_time_changed(hass, dt_util.now() + timedelta(hours=3)) await hass.async_block_till_done() - assert len(dev_reg.devices) == 3 + assert len(dev_reg.devices) == 5 + + +async def test_coordinator_updates(hass, caplog): + """Test coordinator.""" + with patch.dict(os.environ, MOCK_ENVIRON), patch( + "homeassistant.components.hassio.HassIO.refresh_updates" + ) as refresh_updates_mock: + config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert refresh_updates_mock.call_count == 1 + + with patch( + "homeassistant.components.hassio.HassIO.refresh_updates", + side_effect=HassioAPIError("Unknown"), + ) as refresh_updates_mock: + async_fire_time_changed(hass, dt_util.now() + timedelta(minutes=5)) + await hass.async_block_till_done() + assert refresh_updates_mock.call_count == 1 + assert ( + "Error fetching hassio data: Error on Supervisor API: Unknown" + in caplog.text + ) diff --git a/tests/components/hassio/test_sensor.py b/tests/components/hassio/test_sensor.py index 481ba1b578f..9dc620ba94f 100644 --- a/tests/components/hassio/test_sensor.py +++ b/tests/components/hassio/test_sensor.py @@ -62,6 +62,7 @@ def mock_all(aioclient_mock, request): "result": "ok", "data": { "result": "ok", + "version": "1.0.0", "version_latest": "1.0.0", "addons": [ { @@ -106,9 +107,20 @@ def mock_all(aioclient_mock, request): }, }, ) + aioclient_mock.get("http://127.0.0.1/addons/test/changelog", text="") + aioclient_mock.get( + "http://127.0.0.1/addons/test/info", + json={"result": "ok", "data": {"auto_update": True}}, + ) + aioclient_mock.get("http://127.0.0.1/addons/test2/changelog", text="") + aioclient_mock.get( + "http://127.0.0.1/addons/test2/info", + json={"result": "ok", "data": {"auto_update": False}}, + ) aioclient_mock.get( "http://127.0.0.1/ingress/panels", json={"result": "ok", "data": {"panels": {}}} ) + aioclient_mock.post("http://127.0.0.1/refresh_updates", json={"result": "ok"}) @pytest.mark.parametrize( diff --git a/tests/components/hassio/test_update.py b/tests/components/hassio/test_update.py new file mode 100644 index 00000000000..e682562297d --- /dev/null +++ b/tests/components/hassio/test_update.py @@ -0,0 +1,485 @@ +"""The tests for the hassio update entities.""" + +import os +from unittest.mock import patch + +import pytest + +from homeassistant.components.hassio import DOMAIN +from homeassistant.components.hassio.handler import HassioAPIError +from homeassistant.exceptions import HomeAssistantError +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry + +MOCK_ENVIRON = {"HASSIO": "127.0.0.1", "HASSIO_TOKEN": "abcdefgh"} + + +@pytest.fixture(autouse=True) +def mock_all(aioclient_mock, request): + """Mock all setup requests.""" + aioclient_mock.post("http://127.0.0.1/homeassistant/options", json={"result": "ok"}) + aioclient_mock.get("http://127.0.0.1/supervisor/ping", json={"result": "ok"}) + aioclient_mock.post("http://127.0.0.1/supervisor/options", json={"result": "ok"}) + aioclient_mock.get( + "http://127.0.0.1/info", + json={ + "result": "ok", + "data": {"supervisor": "222", "homeassistant": "0.110.0", "hassos": None}, + }, + ) + aioclient_mock.get( + "http://127.0.0.1/store", + json={ + "result": "ok", + "data": {"addons": [], "repositories": []}, + }, + ) + aioclient_mock.get( + "http://127.0.0.1/host/info", + json={ + "result": "ok", + "data": { + "result": "ok", + "data": { + "chassis": "vm", + "operating_system": "Debian GNU/Linux 10 (buster)", + "kernel": "4.19.0-6-amd64", + }, + }, + }, + ) + aioclient_mock.get( + "http://127.0.0.1/core/info", + json={ + "result": "ok", + "data": {"version_latest": "1.0.0dev222", "version": "1.0.0dev221"}, + }, + ) + aioclient_mock.get( + "http://127.0.0.1/os/info", + json={ + "result": "ok", + "data": { + "version_latest": "1.0.0dev2222", + "version": "1.0.0dev2221", + "update_available": False, + }, + }, + ) + aioclient_mock.get( + "http://127.0.0.1/supervisor/info", + json={ + "result": "ok", + "data": { + "result": "ok", + "version": "1.0.0", + "version_latest": "1.0.1dev222", + "addons": [ + { + "name": "test", + "state": "started", + "slug": "test", + "installed": True, + "update_available": True, + "icon": False, + "version": "2.0.0", + "version_latest": "2.0.1", + "repository": "core", + "url": "https://github.com/home-assistant/addons/test", + }, + { + "name": "test2", + "state": "stopped", + "slug": "test2", + "installed": True, + "update_available": False, + "icon": True, + "version": "3.1.0", + "version_latest": "3.1.0", + "repository": "core", + "url": "https://github.com", + }, + ], + }, + }, + ) + aioclient_mock.get( + "http://127.0.0.1/addons/test/stats", + json={ + "result": "ok", + "data": { + "cpu_percent": 0.99, + "memory_usage": 182611968, + "memory_limit": 3977146368, + "memory_percent": 4.59, + "network_rx": 362570232, + "network_tx": 82374138, + "blk_read": 46010945536, + "blk_write": 15051526144, + }, + }, + ) + aioclient_mock.get("http://127.0.0.1/addons/test/changelog", text="") + aioclient_mock.get( + "http://127.0.0.1/addons/test/info", + json={"result": "ok", "data": {"auto_update": True}}, + ) + aioclient_mock.get("http://127.0.0.1/addons/test2/changelog", text="") + aioclient_mock.get( + "http://127.0.0.1/addons/test2/info", + json={"result": "ok", "data": {"auto_update": False}}, + ) + aioclient_mock.get( + "http://127.0.0.1/ingress/panels", json={"result": "ok", "data": {"panels": {}}} + ) + aioclient_mock.post("http://127.0.0.1/refresh_updates", json={"result": "ok"}) + + +@pytest.mark.parametrize( + "entity_id,expected_state, auto_update", + [ + ("update.home_assistant_operating_system_update", "on", False), + ("update.home_assistant_supervisor_update", "on", True), + ("update.home_assistant_core_update", "on", False), + ("update.test_update", "on", True), + ("update.test2_update", "off", False), + ], +) +async def test_update_entities( + hass, + entity_id, + expected_state, + auto_update, + aioclient_mock, +): + """Test update entities.""" + config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN) + config_entry.add_to_hass(hass) + + with patch.dict(os.environ, MOCK_ENVIRON): + result = await async_setup_component( + hass, + "hassio", + {"http": {"server_port": 9999, "server_host": "127.0.0.1"}, "hassio": {}}, + ) + assert result + await hass.async_block_till_done() + + # Verify that the entity have the expected state. + state = hass.states.get(entity_id) + assert state.state == expected_state + + # Verify that the auto_update attribute is correct + assert state.attributes["auto_update"] is auto_update + + +async def test_update_addon(hass, aioclient_mock): + """Test updating addon update entity.""" + config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN) + config_entry.add_to_hass(hass) + + with patch.dict(os.environ, MOCK_ENVIRON): + result = await async_setup_component( + hass, + "hassio", + {"http": {"server_port": 9999, "server_host": "127.0.0.1"}, "hassio": {}}, + ) + assert result + await hass.async_block_till_done() + + aioclient_mock.post( + "http://127.0.0.1/addons/test/update", + json={"result": "ok", "data": {}}, + ) + + assert await hass.services.async_call( + "update", + "install", + {"entity_id": "update.test_update"}, + blocking=True, + ) + + +async def test_update_os(hass, aioclient_mock): + """Test updating OS update entity.""" + config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN) + config_entry.add_to_hass(hass) + + with patch.dict(os.environ, MOCK_ENVIRON): + result = await async_setup_component( + hass, + "hassio", + {"http": {"server_port": 9999, "server_host": "127.0.0.1"}, "hassio": {}}, + ) + assert result + await hass.async_block_till_done() + + aioclient_mock.post( + "http://127.0.0.1/os/update", + json={"result": "ok", "data": {}}, + ) + + assert await hass.services.async_call( + "update", + "install", + {"entity_id": "update.home_assistant_operating_system_update"}, + blocking=True, + ) + + +async def test_update_core(hass, aioclient_mock): + """Test updating core update entity.""" + config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN) + config_entry.add_to_hass(hass) + + with patch.dict(os.environ, MOCK_ENVIRON): + result = await async_setup_component( + hass, + "hassio", + {"http": {"server_port": 9999, "server_host": "127.0.0.1"}, "hassio": {}}, + ) + assert result + await hass.async_block_till_done() + + aioclient_mock.post( + "http://127.0.0.1/core/update", + json={"result": "ok", "data": {}}, + ) + + assert await hass.services.async_call( + "update", + "install", + {"entity_id": "update.home_assistant_os_update"}, + blocking=True, + ) + + +async def test_update_supervisor(hass, aioclient_mock): + """Test updating supervisor update entity.""" + config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN) + config_entry.add_to_hass(hass) + + with patch.dict(os.environ, MOCK_ENVIRON): + result = await async_setup_component( + hass, + "hassio", + {"http": {"server_port": 9999, "server_host": "127.0.0.1"}, "hassio": {}}, + ) + assert result + await hass.async_block_till_done() + + aioclient_mock.post( + "http://127.0.0.1/supervisor/update", + json={"result": "ok", "data": {}}, + ) + + assert await hass.services.async_call( + "update", + "install", + {"entity_id": "update.home_assistant_supervisor_update"}, + blocking=True, + ) + + +async def test_update_addon_with_error(hass, aioclient_mock): + """Test updating addon update entity with error.""" + config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN) + config_entry.add_to_hass(hass) + + with patch.dict(os.environ, MOCK_ENVIRON): + assert await async_setup_component( + hass, + "hassio", + {"http": {"server_port": 9999, "server_host": "127.0.0.1"}, "hassio": {}}, + ) + await hass.async_block_till_done() + + aioclient_mock.post( + "http://127.0.0.1/addons/test/update", + exc=HassioAPIError, + ) + + with pytest.raises(HomeAssistantError): + assert not await hass.services.async_call( + "update", + "install", + {"entity_id": "update.test_update"}, + blocking=True, + ) + + +async def test_update_os_with_error(hass, aioclient_mock): + """Test updating OS update entity with error.""" + config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN) + config_entry.add_to_hass(hass) + + with patch.dict(os.environ, MOCK_ENVIRON): + assert await async_setup_component( + hass, + "hassio", + {"http": {"server_port": 9999, "server_host": "127.0.0.1"}, "hassio": {}}, + ) + await hass.async_block_till_done() + + aioclient_mock.post( + "http://127.0.0.1/os/update", + exc=HassioAPIError, + ) + + with pytest.raises(HomeAssistantError): + assert not await hass.services.async_call( + "update", + "install", + {"entity_id": "update.home_assistant_operating_system_update"}, + blocking=True, + ) + + +async def test_update_supervisor_with_error(hass, aioclient_mock): + """Test updating supervisor update entity with error.""" + config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN) + config_entry.add_to_hass(hass) + + with patch.dict(os.environ, MOCK_ENVIRON): + assert await async_setup_component( + hass, + "hassio", + {"http": {"server_port": 9999, "server_host": "127.0.0.1"}, "hassio": {}}, + ) + await hass.async_block_till_done() + + aioclient_mock.post( + "http://127.0.0.1/supervisor/update", + exc=HassioAPIError, + ) + + with pytest.raises(HomeAssistantError): + assert not await hass.services.async_call( + "update", + "install", + {"entity_id": "update.home_assistant_supervisor_update"}, + blocking=True, + ) + + +async def test_update_core_with_error(hass, aioclient_mock): + """Test updating core update entity with error.""" + config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN) + config_entry.add_to_hass(hass) + + with patch.dict(os.environ, MOCK_ENVIRON): + assert await async_setup_component( + hass, + "hassio", + {"http": {"server_port": 9999, "server_host": "127.0.0.1"}, "hassio": {}}, + ) + await hass.async_block_till_done() + + aioclient_mock.post( + "http://127.0.0.1/core/update", + exc=HassioAPIError, + ) + + with pytest.raises(HomeAssistantError): + assert not await hass.services.async_call( + "update", + "install", + {"entity_id": "update.home_assistant_core_update"}, + blocking=True, + ) + + +async def test_release_notes_between_versions(hass, aioclient_mock, hass_ws_client): + """Test release notes between versions.""" + config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN) + config_entry.add_to_hass(hass) + + with patch.dict(os.environ, MOCK_ENVIRON), patch( + "homeassistant.components.hassio.get_addons_changelogs", + return_value={"test": "# 2.0.1\nNew updates\n# 2.0.0\nOld updates"}, + ): + result = await async_setup_component( + hass, + "hassio", + {"http": {"server_port": 9999, "server_host": "127.0.0.1"}, "hassio": {}}, + ) + assert result + await hass.async_block_till_done() + + client = await hass_ws_client(hass) + await hass.async_block_till_done() + + await client.send_json( + { + "id": 1, + "type": "update/release_notes", + "entity_id": "update.test_update", + } + ) + result = await client.receive_json() + assert "Old updates" not in result["result"] + assert "New updates" in result["result"] + + +async def test_release_notes_full(hass, aioclient_mock, hass_ws_client): + """Test release notes no match.""" + config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN) + config_entry.add_to_hass(hass) + + with patch.dict(os.environ, MOCK_ENVIRON), patch( + "homeassistant.components.hassio.get_addons_changelogs", + return_value={"test": "# 2.0.0\nNew updates\n# 2.0.0\nOld updates"}, + ): + result = await async_setup_component( + hass, + "hassio", + {"http": {"server_port": 9999, "server_host": "127.0.0.1"}, "hassio": {}}, + ) + assert result + await hass.async_block_till_done() + + client = await hass_ws_client(hass) + await hass.async_block_till_done() + + await client.send_json( + { + "id": 1, + "type": "update/release_notes", + "entity_id": "update.test_update", + } + ) + result = await client.receive_json() + assert "Old updates" in result["result"] + assert "New updates" in result["result"] + + +async def test_not_release_notes(hass, aioclient_mock, hass_ws_client): + """Test handling where there are no release notes.""" + config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN) + config_entry.add_to_hass(hass) + + with patch.dict(os.environ, MOCK_ENVIRON), patch( + "homeassistant.components.hassio.get_addons_changelogs", + return_value={"test": None}, + ): + result = await async_setup_component( + hass, + "hassio", + {"http": {"server_port": 9999, "server_host": "127.0.0.1"}, "hassio": {}}, + ) + assert result + await hass.async_block_till_done() + + client = await hass_ws_client(hass) + await hass.async_block_till_done() + + await client.send_json( + { + "id": 1, + "type": "update/release_notes", + "entity_id": "update.test_update", + } + ) + result = await client.receive_json() + assert result["result"] is None diff --git a/tests/components/heos/test_media_player.py b/tests/components/heos/test_media_player.py index d134840d652..ee75793df8e 100644 --- a/tests/components/heos/test_media_player.py +++ b/tests/components/heos/test_media_player.py @@ -58,6 +58,7 @@ from homeassistant.const import ( STATE_UNAVAILABLE, ) from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.setup import async_setup_component @@ -155,7 +156,7 @@ async def test_updates_from_connection_event( async def set_signal(): event.set() - hass.helpers.dispatcher.async_dispatcher_connect(SIGNAL_HEOS_UPDATED, set_signal) + async_dispatcher_connect(hass, SIGNAL_HEOS_UPDATED, set_signal) # Connected player.available = True @@ -201,7 +202,7 @@ async def test_updates_from_sources_updated( async def set_signal(): event.set() - hass.helpers.dispatcher.async_dispatcher_connect(SIGNAL_HEOS_UPDATED, set_signal) + async_dispatcher_connect(hass, SIGNAL_HEOS_UPDATED, set_signal) input_sources.clear() player.heos.dispatcher.send( @@ -225,7 +226,7 @@ async def test_updates_from_players_changed( async def set_signal(): event.set() - hass.helpers.dispatcher.async_dispatcher_connect(SIGNAL_HEOS_UPDATED, set_signal) + async_dispatcher_connect(hass, SIGNAL_HEOS_UPDATED, set_signal) assert hass.states.get("media_player.test_player").state == STATE_IDLE player.state = const.PLAY_STATE_PLAY @@ -259,7 +260,7 @@ async def test_updates_from_players_changed_new_ids( async def set_signal(): event.set() - hass.helpers.dispatcher.async_dispatcher_connect(SIGNAL_HEOS_UPDATED, set_signal) + async_dispatcher_connect(hass, SIGNAL_HEOS_UPDATED, set_signal) player.heos.dispatcher.send( const.SIGNAL_CONTROLLER_EVENT, const.EVENT_PLAYERS_CHANGED, @@ -287,7 +288,7 @@ async def test_updates_from_user_changed(hass, config_entry, config, controller) async def set_signal(): event.set() - hass.helpers.dispatcher.async_dispatcher_connect(SIGNAL_HEOS_UPDATED, set_signal) + async_dispatcher_connect(hass, SIGNAL_HEOS_UPDATED, set_signal) controller.is_signed_in = False controller.signed_in_username = None diff --git a/tests/components/here_travel_time/conftest.py b/tests/components/here_travel_time/conftest.py index 2d5af2b0186..83f0659f516 100644 --- a/tests/components/here_travel_time/conftest.py +++ b/tests/components/here_travel_time/conftest.py @@ -19,5 +19,5 @@ def valid_response_fixture(): with patch( "herepy.RoutingApi.public_transport_timetable", return_value=RESPONSE, - ): - yield + ) as mock: + yield mock diff --git a/tests/components/here_travel_time/fixtures/car_response.json b/tests/components/here_travel_time/fixtures/car_response.json index ef050b78362..cd479b2c947 100644 --- a/tests/components/here_travel_time/fixtures/car_response.json +++ b/tests/components/here_travel_time/fixtures/car_response.json @@ -1,308 +1,304 @@ { - "response": { - "metaInfo": { - "timestamp": "2019-07-19T07:38:39Z", - "mapVersion": "8.30.98.154", - "moduleVersion": "7.2.201928-4446", - "interfaceVersion": "2.6.64", - "availableMapVersion": [ - "8.30.98.154" - ] - }, - "route": [ - { - "waypoint": [ - { - "linkId": "+732182239", - "mappedPosition": { - "latitude": 38.9, - "longitude": -77.0488358 - }, - "originalPosition": { - "latitude": 38.9, - "longitude": -77.0483301 - }, - "type": "stopOver", - "spot": 0.4946237, - "sideOfStreet": "right", - "mappedRoadName": "22nd St NW", - "label": "22nd St NW", - "shapeIndex": 0, - "source": "user" - }, - { - "linkId": "+942865877", - "mappedPosition": { - "latitude": 38.9999735, - "longitude": -77.100141 - }, - "originalPosition": { - "latitude": 38.9999999, - "longitude": -77.1000001 - }, - "type": "stopOver", - "spot": 1, - "sideOfStreet": "left", - "mappedRoadName": "Service Rd S", - "label": "Service Rd S", - "shapeIndex": 279, - "source": "user" - } - ], - "mode": { - "type": "fastest", - "transportModes": [ - "car" - ], - "trafficMode": "enabled", - "feature": [] - }, - "leg": [ - { - "start": { - "linkId": "+732182239", - "mappedPosition": { - "latitude": 38.9, - "longitude": -77.0488358 - }, - "originalPosition": { - "latitude": 38.9, - "longitude": -77.0483301 - }, - "type": "stopOver", - "spot": 0.4946237, - "sideOfStreet": "right", - "mappedRoadName": "22nd St NW", - "label": "22nd St NW", - "shapeIndex": 0, - "source": "user" - }, - "end": { - "linkId": "+942865877", - "mappedPosition": { - "latitude": 38.9999735, - "longitude": -77.100141 - }, - "originalPosition": { - "latitude": 38.9999999, - "longitude": -77.1000001 - }, - "type": "stopOver", - "spot": 1, - "sideOfStreet": "left", - "mappedRoadName": "Service Rd S", - "label": "Service Rd S", - "shapeIndex": 279, - "source": "user" - }, - "length": 23903, - "travelTime": 1884, - "maneuver": [ - { - "position": { - "latitude": 38.9, - "longitude": -77.0488358 - }, - "instruction": "Head toward I St NW on 22nd St NW. Go for 279 m.", - "travelTime": 95, - "length": 279, - "id": "M1", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 38.9021051, - "longitude": -77.048825 - }, - "instruction": "Turn left toward Pennsylvania Ave NW. Go for 71 m.", - "travelTime": 21, - "length": 71, - "id": "M2", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 38.902545, - "longitude": -77.0494151 - }, - "instruction": "Take the 3rd exit from Washington Cir NW roundabout onto K St NW. Go for 352 m.", - "travelTime": 90, - "length": 352, - "id": "M3", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 38.9026523, - "longitude": -77.0529449 - }, - "instruction": "Keep left onto K St NW (US-29). Go for 201 m.", - "travelTime": 30, - "length": 201, - "id": "M4", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 38.9025235, - "longitude": -77.0552516 - }, - "instruction": "Keep right onto Whitehurst Fwy (US-29). Go for 1.4 km.", - "travelTime": 131, - "length": 1381, - "id": "M5", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 38.9050448, - "longitude": -77.0701969 - }, - "instruction": "Turn left onto M St NW. Go for 784 m.", - "travelTime": 78, - "length": 784, - "id": "M6", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 38.9060318, - "longitude": -77.0790696 - }, - "instruction": "Turn slightly left onto Canal Rd NW. Go for 4.2 km.", - "travelTime": 277, - "length": 4230, - "id": "M7", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 38.9303219, - "longitude": -77.1117926 - }, - "instruction": "Continue on Clara Barton Pkwy. Go for 844 m.", - "travelTime": 55, - "length": 844, - "id": "M8", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 38.9368558, - "longitude": -77.1166742 - }, - "instruction": "Continue on Clara Barton Pkwy. Go for 4.7 km.", - "travelTime": 298, - "length": 4652, - "id": "M9", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 38.9706838, - "longitude": -77.1461463 - }, - "instruction": "Keep right onto Cabin John Pkwy N toward I-495 N. Go for 2.1 km.", - "travelTime": 91, - "length": 2069, - "id": "M10", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 38.9858222, - "longitude": -77.1571326 - }, - "instruction": "Take left ramp onto I-495 N (Capital Beltway). Go for 5.5 km.", - "travelTime": 238, - "length": 5538, - "id": "M11", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 39.0153587, - "longitude": -77.1221781 - }, - "instruction": "Take exit 36 toward Bethesda onto MD-187 S (Old Georgetown Rd). Go for 2.4 km.", - "travelTime": 211, - "length": 2365, - "id": "M12", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 38.9981818, - "longitude": -77.1093571 - }, - "instruction": "Turn left onto Lincoln Dr. Go for 506 m.", - "travelTime": 127, - "length": 506, - "id": "M13", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 38.9987397, - "longitude": -77.1037138 - }, - "instruction": "Turn right onto Service Rd W. Go for 121 m.", - "travelTime": 36, - "length": 121, - "id": "M14", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 38.9976454, - "longitude": -77.1036172 - }, - "instruction": "Turn left onto Service Rd S. Go for 510 m.", - "travelTime": 106, - "length": 510, - "id": "M15", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 38.9999735, - "longitude": -77.100141 - }, - "instruction": "Arrive at Service Rd S. Your destination is on the left.", - "travelTime": 0, - "length": 0, - "id": "M16", - "_type": "PrivateTransportManeuverType" - } - ] - } - ], - "summary": { - "distance": 23903, - "trafficTime": 1861, - "baseTime": 1803, - "flags": [ - "noThroughRoad", - "motorway", - "builtUpArea", - "park", - "privateRoad" - ], - "text": "The trip takes 23.9 km and 31 mins.", - "travelTime": 1861, - "_type": "RouteSummaryType" - } - } + "response": { + "metaInfo": { + "timestamp": "2019-07-19T07:38:39Z", + "mapVersion": "8.30.98.154", + "moduleVersion": "7.2.201928-4446", + "interfaceVersion": "2.6.64", + "availableMapVersion": ["8.30.98.154"] + }, + "route": [ + { + "waypoint": [ + { + "linkId": "+732182239", + "mappedPosition": { + "latitude": 38.9, + "longitude": -77.0488358 + }, + "originalPosition": { + "latitude": 38.9, + "longitude": -77.0483301 + }, + "type": "stopOver", + "spot": 0.4946237, + "sideOfStreet": "right", + "mappedRoadName": "22nd St NW", + "label": "22nd St NW", + "shapeIndex": 0, + "source": "user" + }, + { + "linkId": "+942865877", + "mappedPosition": { + "latitude": 38.9999735, + "longitude": -77.100141 + }, + "originalPosition": { + "latitude": 38.9999999, + "longitude": -77.1000001 + }, + "type": "stopOver", + "spot": 1, + "sideOfStreet": "left", + "mappedRoadName": "Service Rd S", + "label": "Service Rd S", + "shapeIndex": 279, + "source": "user" + } ], - "language": "en-us", - "sourceAttribution": { - "attribution": "With the support of HERE Technologies. All information is provided without warranty of any kind.", - "supplier": [ - { - "title": "HERE Technologies", - "href": "https://transit.api.here.com/r?appId=Mt1bOYh3m9uxE7r3wuUx&u=https://wego.here.com" - } + "mode": { + "type": "fastest", + "transportModes": ["car"], + "trafficMode": "enabled", + "feature": [] + }, + "leg": [ + { + "start": { + "linkId": "+732182239", + "mappedPosition": { + "latitude": 38.9, + "longitude": -77.0488358 + }, + "originalPosition": { + "latitude": 38.9, + "longitude": -77.0483301 + }, + "type": "stopOver", + "spot": 0.4946237, + "sideOfStreet": "right", + "mappedRoadName": "22nd St NW", + "label": "22nd St NW", + "shapeIndex": 0, + "source": "user" + }, + "end": { + "linkId": "+942865877", + "mappedPosition": { + "latitude": 38.9999735, + "longitude": -77.100141 + }, + "originalPosition": { + "latitude": 38.9999999, + "longitude": -77.1000001 + }, + "type": "stopOver", + "spot": 1, + "sideOfStreet": "left", + "mappedRoadName": "Service Rd S", + "label": "Service Rd S", + "shapeIndex": 279, + "source": "user" + }, + "length": 23903, + "travelTime": 1884, + "maneuver": [ + { + "position": { + "latitude": 38.9, + "longitude": -77.0488358 + }, + "instruction": "Head toward I St NW on 22nd St NW. Go for 279 m.", + "travelTime": 95, + "length": 279, + "id": "M1", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 38.9021051, + "longitude": -77.048825 + }, + "instruction": "Turn left toward Pennsylvania Ave NW. Go for 71 m.", + "travelTime": 21, + "length": 71, + "id": "M2", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 38.902545, + "longitude": -77.0494151 + }, + "instruction": "Take the 3rd exit from Washington Cir NW roundabout onto K St NW. Go for 352 m.", + "travelTime": 90, + "length": 352, + "id": "M3", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 38.9026523, + "longitude": -77.0529449 + }, + "instruction": "Keep left onto K St NW (US-29). Go for 201 m.", + "travelTime": 30, + "length": 201, + "id": "M4", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 38.9025235, + "longitude": -77.0552516 + }, + "instruction": "Keep right onto Whitehurst Fwy (US-29). Go for 1.4 km.", + "travelTime": 131, + "length": 1381, + "id": "M5", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 38.9050448, + "longitude": -77.0701969 + }, + "instruction": "Turn left onto M St NW. Go for 784 m.", + "travelTime": 78, + "length": 784, + "id": "M6", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 38.9060318, + "longitude": -77.0790696 + }, + "instruction": "Turn slightly left onto Canal Rd NW. Go for 4.2 km.", + "travelTime": 277, + "length": 4230, + "id": "M7", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 38.9303219, + "longitude": -77.1117926 + }, + "instruction": "Continue on Clara Barton Pkwy. Go for 844 m.", + "travelTime": 55, + "length": 844, + "id": "M8", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 38.9368558, + "longitude": -77.1166742 + }, + "instruction": "Continue on Clara Barton Pkwy. Go for 4.7 km.", + "travelTime": 298, + "length": 4652, + "id": "M9", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 38.9706838, + "longitude": -77.1461463 + }, + "instruction": "Keep right onto Cabin John Pkwy N toward I-495 N. Go for 2.1 km.", + "travelTime": 91, + "length": 2069, + "id": "M10", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 38.9858222, + "longitude": -77.1571326 + }, + "instruction": "Take left ramp onto I-495 N (Capital Beltway). Go for 5.5 km.", + "travelTime": 238, + "length": 5538, + "id": "M11", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 39.0153587, + "longitude": -77.1221781 + }, + "instruction": "Take exit 36 toward Bethesda onto MD-187 S (Old Georgetown Rd). Go for 2.4 km.", + "travelTime": 211, + "length": 2365, + "id": "M12", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 38.9981818, + "longitude": -77.1093571 + }, + "instruction": "Turn left onto Lincoln Dr. Go for 506 m.", + "travelTime": 127, + "length": 506, + "id": "M13", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 38.9987397, + "longitude": -77.1037138 + }, + "instruction": "Turn right onto Service Rd W. Go for 121 m.", + "travelTime": 36, + "length": 121, + "id": "M14", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 38.9976454, + "longitude": -77.1036172 + }, + "instruction": "Turn left onto Service Rd S. Go for 510 m.", + "travelTime": 106, + "length": 510, + "id": "M15", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 38.9999735, + "longitude": -77.100141 + }, + "instruction": "Arrive at Service Rd S. Your destination is on the left.", + "travelTime": 0, + "length": 0, + "id": "M16", + "_type": "PrivateTransportManeuverType" + } ] + } + ], + "summary": { + "distance": 23903, + "trafficTime": 1861, + "baseTime": 1803, + "flags": [ + "noThroughRoad", + "motorway", + "builtUpArea", + "park", + "privateRoad" + ], + "text": "The trip takes 23.9 km and 31 mins.", + "travelTime": 1861, + "_type": "RouteSummaryType" } + } + ], + "language": "en-us", + "sourceAttribution": { + "attribution": "With the support of HERE Technologies. All information is provided without warranty of any kind.", + "supplier": [ + { + "title": "HERE Technologies", + "href": "https://transit.api.here.com/r?appId=Mt1bOYh3m9uxE7r3wuUx&u=https://wego.here.com" + } + ] } -} \ No newline at end of file + } +} diff --git a/tests/components/here_travel_time/test_sensor.py b/tests/components/here_travel_time/test_sensor.py index 03d2313da2e..d8fd35e4ce8 100644 --- a/tests/components/here_travel_time/test_sensor.py +++ b/tests/components/here_travel_time/test_sensor.py @@ -1,9 +1,14 @@ """The test for the HERE Travel Time sensor platform.""" -from unittest.mock import patch +from unittest.mock import MagicMock, patch +from herepy.here_enum import RouteMode from herepy.routing_api import InvalidCredentialsError, NoRouteFoundError import pytest +from homeassistant.components.here_travel_time.const import ( + ROUTE_MODE_FASTEST, + TRAFFIC_MODE_ENABLED, +) from homeassistant.components.here_travel_time.sensor import ( ATTR_ATTRIBUTION, ATTR_DESTINATION, @@ -49,16 +54,50 @@ PLATFORM = "here_travel_time" @pytest.mark.parametrize( - "mode,icon,traffic_mode,unit_system", + "mode,icon,traffic_mode,unit_system,expected_state,expected_distance,expected_duration_in_traffic", [ - (TRAVEL_MODE_CAR, ICON_CAR, True, "metric"), - (TRAVEL_MODE_BICYCLE, ICON_BICYCLE, False, "metric"), - (TRAVEL_MODE_PEDESTRIAN, ICON_PEDESTRIAN, False, "imperial"), - (TRAVEL_MODE_PUBLIC_TIME_TABLE, ICON_PUBLIC, False, "imperial"), - (TRAVEL_MODE_TRUCK, ICON_TRUCK, True, "metric"), + (TRAVEL_MODE_CAR, ICON_CAR, True, "metric", "31", 23.903, 31.016666666666666), + (TRAVEL_MODE_BICYCLE, ICON_BICYCLE, False, "metric", "30", 23.903, 30.05), + ( + TRAVEL_MODE_PEDESTRIAN, + ICON_PEDESTRIAN, + False, + "imperial", + "30", + 14.852635608048994, + 30.05, + ), + ( + TRAVEL_MODE_PUBLIC_TIME_TABLE, + ICON_PUBLIC, + False, + "imperial", + "30", + 14.852635608048994, + 30.05, + ), + ( + TRAVEL_MODE_TRUCK, + ICON_TRUCK, + True, + "metric", + "31", + 23.903, + 31.016666666666666, + ), ], ) -async def test_sensor(hass, mode, icon, traffic_mode, unit_system, valid_response): +async def test_sensor( + hass, + mode, + icon, + traffic_mode, + unit_system, + expected_state, + expected_distance, + expected_duration_in_traffic, + valid_response, +): """Test that sensor works.""" config = { DOMAIN: { @@ -85,25 +124,18 @@ async def test_sensor(hass, mode, icon, traffic_mode, unit_system, valid_respons sensor.attributes.get(ATTR_ATTRIBUTION) == "With the support of HERE Technologies. All information is provided without warranty of any kind." ) - if traffic_mode: - assert sensor.state == "31" - else: - assert sensor.state == "30" + assert sensor.state == expected_state assert sensor.attributes.get(ATTR_DURATION) == 30.05 - if unit_system == "metric": - assert sensor.attributes.get(ATTR_DISTANCE) == 23.903 - else: - assert sensor.attributes.get(ATTR_DISTANCE) == 14.852635608048994 + assert sensor.attributes.get(ATTR_DISTANCE) == expected_distance assert sensor.attributes.get(ATTR_ROUTE) == ( "US-29 - K St NW; US-29 - Whitehurst Fwy; " "I-495 N - Capital Beltway; MD-187 S - Old Georgetown Rd" ) assert sensor.attributes.get(CONF_UNIT_SYSTEM) == unit_system - if mode in TRAVEL_MODES_VEHICLE: - assert sensor.attributes.get(ATTR_DURATION_IN_TRAFFIC) == 31.016666666666666 - else: - assert sensor.attributes.get(ATTR_DURATION_IN_TRAFFIC) == 30.05 + assert ( + sensor.attributes.get(ATTR_DURATION_IN_TRAFFIC) == expected_duration_in_traffic + ) assert sensor.attributes.get(ATTR_ORIGIN) == ",".join( [CAR_ORIGIN_LATITUDE, CAR_ORIGIN_LONGITUDE] ) @@ -124,20 +156,13 @@ async def test_sensor(hass, mode, icon, traffic_mode, unit_system, valid_respons ) -async def test_entity_ids(hass, valid_response): - """Test that origin/destination supplied by a zone works.""" +async def test_entity_ids(hass, valid_response: MagicMock): + """Test that origin/destination supplied by entities works.""" utcnow = dt_util.utcnow() # Patching 'utcnow' to gain more control over the timed update. with patch("homeassistant.util.dt.utcnow", return_value=utcnow): zone_config = { "zone": [ - { - "name": "Destination", - "latitude": CAR_DESTINATION_LATITUDE, - "longitude": CAR_DESTINATION_LONGITUDE, - "radius": 250, - "passive": False, - }, { "name": "Origin", "latitude": CAR_ORIGIN_LATITUDE, @@ -147,17 +172,25 @@ async def test_entity_ids(hass, valid_response): }, ] } + assert await async_setup_component(hass, "zone", zone_config) + hass.states.async_set( + "device_tracker.test", + "not_home", + { + "latitude": float(CAR_DESTINATION_LATITUDE), + "longitude": float(CAR_DESTINATION_LONGITUDE), + }, + ) config = { DOMAIN: { "platform": PLATFORM, "name": "test", "origin_entity_id": "zone.origin", - "destination_entity_id": "zone.destination", + "destination_entity_id": "device_tracker.test", "api_key": API_KEY, "mode": TRAVEL_MODE_TRUCK, } } - assert await async_setup_component(hass, "zone", zone_config) assert await async_setup_component(hass, DOMAIN, config) await hass.async_block_till_done() @@ -167,8 +200,21 @@ async def test_entity_ids(hass, valid_response): sensor = hass.states.get("sensor.test") assert sensor.attributes.get(ATTR_DISTANCE) == 23.903 + valid_response.assert_called_with( + [CAR_ORIGIN_LATITUDE, CAR_ORIGIN_LONGITUDE], + [CAR_DESTINATION_LATITUDE, CAR_DESTINATION_LONGITUDE], + True, + [ + RouteMode[ROUTE_MODE_FASTEST], + RouteMode[TRAVEL_MODE_TRUCK], + RouteMode[TRAFFIC_MODE_ENABLED], + ], + arrival=None, + departure="now", + ) -async def test_route_not_found(hass, caplog, valid_response): + +async def test_route_not_found(hass, caplog): """Test that route not found error is correctly handled.""" config = { DOMAIN: { @@ -181,12 +227,15 @@ async def test_route_not_found(hass, caplog, valid_response): "api_key": API_KEY, } } - assert await async_setup_component(hass, DOMAIN, config) - await hass.async_block_till_done() with patch( + "homeassistant.components.here_travel_time.sensor._are_valid_client_credentials", + return_value=True, + ), patch( "herepy.RoutingApi.public_transport_timetable", side_effect=NoRouteFoundError, ): + assert await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done() hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() diff --git a/tests/components/history/test_init.py b/tests/components/history/test_init.py index 18fa3dc7625..f9fb7b49fb0 100644 --- a/tests/components/history/test_init.py +++ b/tests/components/history/test_init.py @@ -17,8 +17,12 @@ from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM -from tests.common import init_recorder_component -from tests.components.recorder.common import trigger_db_commit, wait_recording_done +from tests.common import async_init_recorder_component, init_recorder_component +from tests.components.recorder.common import ( + async_wait_recording_done_without_instance, + trigger_db_commit, + wait_recording_done, +) @pytest.mark.usefixtures("hass_history") @@ -604,14 +608,36 @@ async def test_fetch_period_api_with_use_include_order(hass, hass_client): async def test_fetch_period_api_with_minimal_response(hass, hass_client): """Test the fetch period view for history with minimal_response.""" - await hass.async_add_executor_job(init_recorder_component, hass) + await async_init_recorder_component(hass) + now = dt_util.utcnow() await async_setup_component(hass, "history", {}) - await hass.async_add_executor_job(hass.data[recorder.DATA_INSTANCE].block_till_done) + + hass.states.async_set("sensor.power", 0, {"attr": "any"}) + await async_wait_recording_done_without_instance(hass) + hass.states.async_set("sensor.power", 50, {"attr": "any"}) + await async_wait_recording_done_without_instance(hass) + hass.states.async_set("sensor.power", 23, {"attr": "any"}) + await async_wait_recording_done_without_instance(hass) client = await hass_client() response = await client.get( - f"/api/history/period/{dt_util.utcnow().isoformat()}?minimal_response" + f"/api/history/period/{now.isoformat()}?filter_entity_id=sensor.power&minimal_response&no_attributes" ) assert response.status == HTTPStatus.OK + response_json = await response.json() + assert len(response_json[0]) == 3 + state_list = response_json[0] + + assert state_list[0]["entity_id"] == "sensor.power" + assert state_list[0]["attributes"] == {} + assert state_list[0]["state"] == "0" + + assert "attributes" not in state_list[1] + assert "entity_id" not in state_list[1] + assert state_list[1]["state"] == "50" + + assert state_list[2]["entity_id"] == "sensor.power" + assert state_list[2]["attributes"] == {} + assert state_list[2]["state"] == "23" async def test_fetch_period_api_with_no_timestamp(hass, hass_client): @@ -1012,6 +1038,8 @@ async def test_list_statistic_ids(hass, hass_ws_client, units, attributes, unit) assert response["result"] == [ { "statistic_id": "sensor.test", + "has_mean": True, + "has_sum": False, "name": None, "source": "recorder", "unit_of_measurement": unit, @@ -1030,6 +1058,8 @@ async def test_list_statistic_ids(hass, hass_ws_client, units, attributes, unit) assert response["result"] == [ { "statistic_id": "sensor.test", + "has_mean": True, + "has_sum": False, "name": None, "source": "recorder", "unit_of_measurement": unit, @@ -1050,6 +1080,8 @@ async def test_list_statistic_ids(hass, hass_ws_client, units, attributes, unit) assert response["result"] == [ { "statistic_id": "sensor.test", + "has_mean": True, + "has_sum": False, "name": None, "source": "recorder", "unit_of_measurement": unit, diff --git a/tests/components/history_stats/test_sensor.py b/tests/components/history_stats/test_sensor.py index 105943d4444..c018553efc6 100644 --- a/tests/components/history_stats/test_sensor.py +++ b/tests/components/history_stats/test_sensor.py @@ -11,6 +11,7 @@ from homeassistant.components.history_stats import DOMAIN from homeassistant.components.history_stats.sensor import HistoryStatsSensor from homeassistant.const import SERVICE_RELOAD, STATE_UNKNOWN import homeassistant.core as ha +from homeassistant.helpers.entity_component import async_update_entity from homeassistant.helpers.template import Template from homeassistant.setup import async_setup_component, setup_component import homeassistant.util.dt as dt_util @@ -339,7 +340,7 @@ async def test_measure_multiple(hass): return_value=fake_states, ), patch("homeassistant.components.recorder.history.get_state", return_value=None): for i in range(1, 5): - await hass.helpers.entity_component.async_update_entity(f"sensor.sensor{i}") + await async_update_entity(hass, f"sensor.sensor{i}") await hass.async_block_till_done() assert hass.states.get("sensor.sensor1").state == "0.5" @@ -416,7 +417,7 @@ async def async_test_measure(hass): return_value=fake_states, ), patch("homeassistant.components.recorder.history.get_state", return_value=None): for i in range(1, 5): - await hass.helpers.entity_component.async_update_entity(f"sensor.sensor{i}") + await async_update_entity(hass, f"sensor.sensor{i}") await hass.async_block_till_done() assert hass.states.get("sensor.sensor1").state == "0.5" diff --git a/tests/components/homeassistant/test_init.py b/tests/components/homeassistant/test_init.py index fb4a0f4c1da..8e89be00433 100644 --- a/tests/components/homeassistant/test_init.py +++ b/tests/components/homeassistant/test_init.py @@ -280,7 +280,7 @@ async def test_entity_update(hass): await async_setup_component(hass, "homeassistant", {}) with patch( - "homeassistant.helpers.entity_component.async_update_entity", + "homeassistant.components.homeassistant.async_update_entity", return_value=None, ) as mock_update: await hass.services.async_call( diff --git a/tests/components/homekit/test_config_flow.py b/tests/components/homekit/test_config_flow.py index b4600c3190e..fc3ef3e2710 100644 --- a/tests/components/homekit/test_config_flow.py +++ b/tests/components/homekit/test_config_flow.py @@ -13,7 +13,7 @@ from homeassistant.components.homekit.const import ( from homeassistant.config_entries import SOURCE_IGNORE, SOURCE_IMPORT from homeassistant.const import CONF_NAME, CONF_PORT from homeassistant.helpers.entity import EntityCategory -from homeassistant.helpers.entity_registry import RegistryEntry +from homeassistant.helpers.entity_registry import RegistryEntry, RegistryEntryHider from homeassistant.helpers.entityfilter import CONF_INCLUDE_DOMAINS from homeassistant.setup import async_setup_component @@ -1403,3 +1403,159 @@ async def test_options_flow_exclude_mode_skips_category_entities( "include_entities": [], }, } + + +@patch(f"{PATH_HOMEKIT}.async_port_is_available", return_value=True) +async def test_options_flow_exclude_mode_skips_hidden_entities( + port_mock, hass, mock_get_source_ip, hk_driver, mock_async_zeroconf, entity_reg +): + """Ensure exclude mode does not offer hidden entities.""" + config_entry = _mock_config_entry_with_options_populated() + await async_init_entry(hass, config_entry) + + hass.states.async_set("media_player.tv", "off") + hass.states.async_set("media_player.sonos", "off") + hass.states.async_set("switch.other", "off") + + sonos_hidden_switch: RegistryEntry = entity_reg.async_get_or_create( + "switch", + "sonos", + "config", + device_id="1234", + hidden_by=RegistryEntryHider.INTEGRATION, + ) + hass.states.async_set(sonos_hidden_switch.entity_id, "off") + await hass.async_block_till_done() + + result = await hass.config_entries.options.async_init( + config_entry.entry_id, context={"show_advanced_options": False} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "init" + assert result["data_schema"]({}) == { + "domains": [ + "fan", + "humidifier", + "vacuum", + "media_player", + "climate", + "alarm_control_panel", + ], + "mode": "bridge", + "include_exclude_mode": "exclude", + } + + result2 = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + "domains": ["media_player", "switch"], + "mode": "bridge", + "include_exclude_mode": "exclude", + }, + ) + + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["step_id"] == "exclude" + assert _get_schema_default(result2["data_schema"].schema, "entities") == [] + + # sonos_hidden_switch.entity_id is a hidden entity + # so it should not be selectable since it will always be excluded + with pytest.raises(voluptuous.error.MultipleInvalid): + await hass.config_entries.options.async_configure( + result2["flow_id"], + user_input={"entities": [sonos_hidden_switch.entity_id]}, + ) + + result4 = await hass.config_entries.options.async_configure( + result2["flow_id"], + user_input={"entities": ["media_player.tv", "switch.other"]}, + ) + assert result4["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert config_entry.options == { + "mode": "bridge", + "filter": { + "exclude_domains": [], + "exclude_entities": ["media_player.tv", "switch.other"], + "include_domains": ["media_player", "switch"], + "include_entities": [], + }, + } + + +@patch(f"{PATH_HOMEKIT}.async_port_is_available", return_value=True) +async def test_options_flow_include_mode_skips_hidden_entities( + port_mock, hass, mock_get_source_ip, hk_driver, mock_async_zeroconf, entity_reg +): + """Ensure include mode does not offer hidden entities.""" + config_entry = _mock_config_entry_with_options_populated() + await async_init_entry(hass, config_entry) + + hass.states.async_set("media_player.tv", "off") + hass.states.async_set("media_player.sonos", "off") + hass.states.async_set("switch.other", "off") + + sonos_hidden_switch: RegistryEntry = entity_reg.async_get_or_create( + "switch", + "sonos", + "config", + device_id="1234", + hidden_by=RegistryEntryHider.INTEGRATION, + ) + hass.states.async_set(sonos_hidden_switch.entity_id, "off") + await hass.async_block_till_done() + + result = await hass.config_entries.options.async_init( + config_entry.entry_id, context={"show_advanced_options": False} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "init" + assert result["data_schema"]({}) == { + "domains": [ + "fan", + "humidifier", + "vacuum", + "media_player", + "climate", + "alarm_control_panel", + ], + "mode": "bridge", + "include_exclude_mode": "exclude", + } + + result2 = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + "domains": ["media_player", "switch"], + "mode": "bridge", + "include_exclude_mode": "include", + }, + ) + + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["step_id"] == "include" + assert _get_schema_default(result2["data_schema"].schema, "entities") == [] + + # sonos_hidden_switch.entity_id is a hidden entity + # so it should not be selectable since it will always be excluded + with pytest.raises(voluptuous.error.MultipleInvalid): + await hass.config_entries.options.async_configure( + result2["flow_id"], + user_input={"entities": [sonos_hidden_switch.entity_id]}, + ) + + result4 = await hass.config_entries.options.async_configure( + result2["flow_id"], + user_input={"entities": ["media_player.tv", "switch.other"]}, + ) + assert result4["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert config_entry.options == { + "mode": "bridge", + "filter": { + "exclude_domains": [], + "exclude_entities": [], + "include_domains": [], + "include_entities": ["media_player.tv", "switch.other"], + }, + } diff --git a/tests/components/homekit/test_get_accessories.py b/tests/components/homekit/test_get_accessories.py index 31f7b0f3bcc..32f4abe98f1 100644 --- a/tests/components/homekit/test_get_accessories.py +++ b/tests/components/homekit/test_get_accessories.py @@ -212,8 +212,20 @@ def test_type_media_player(type_name, entity_id, state, attrs, config): ("BinarySensor", "binary_sensor.opening", "on", {ATTR_DEVICE_CLASS: "opening"}), ("BinarySensor", "device_tracker.someone", "not_home", {}), ("BinarySensor", "person.someone", "home", {}), - ("AirQualitySensor", "sensor.air_quality_pm25", "40", {}), - ("AirQualitySensor", "sensor.air_quality", "40", {ATTR_DEVICE_CLASS: "pm25"}), + ("PM10Sensor", "sensor.air_quality_pm10", "30", {}), + ( + "PM10Sensor", + "sensor.air_quality", + "30", + {ATTR_DEVICE_CLASS: "pm10"}, + ), + ("PM25Sensor", "sensor.air_quality_pm25", "40", {}), + ( + "PM25Sensor", + "sensor.air_quality", + "40", + {ATTR_DEVICE_CLASS: "pm25"}, + ), ( "CarbonMonoxideSensor", "sensor.co", diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index 5c79e764af1..8a8c32d3272 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -49,7 +49,7 @@ from homeassistant.const import ( STATE_ON, ) from homeassistant.core import HomeAssistantError, State -from homeassistant.helpers import device_registry +from homeassistant.helpers import device_registry, entity_registry as er from homeassistant.helpers.entityfilter import ( CONF_EXCLUDE_DOMAINS, CONF_EXCLUDE_ENTITIES, @@ -485,6 +485,61 @@ async def test_homekit_entity_glob_filter_with_config_entities( assert hass.states.get("select.keep") in filtered_states +async def test_homekit_entity_glob_filter_with_hidden_entities( + hass, mock_async_zeroconf, entity_reg +): + """Test the entity filter with hidden entities.""" + entry = await async_init_integration(hass) + + from homeassistant.helpers.entity_registry import RegistryEntry + + select_config_entity: RegistryEntry = entity_reg.async_get_or_create( + "select", + "any", + "any", + device_id="1234", + hidden_by=er.RegistryEntryHider.INTEGRATION, + ) + hass.states.async_set(select_config_entity.entity_id, "off") + + switch_config_entity: RegistryEntry = entity_reg.async_get_or_create( + "switch", + "any", + "any", + device_id="1234", + hidden_by=er.RegistryEntryHider.INTEGRATION, + ) + hass.states.async_set(switch_config_entity.entity_id, "off") + hass.states.async_set("select.keep", "open") + + hass.states.async_set("cover.excluded_test", "open") + hass.states.async_set("light.included_test", "on") + + entity_filter = generate_filter( + ["select"], + ["switch.test", switch_config_entity.entity_id], + [], + [], + ["*.included_*"], + ["*.excluded_*"], + ) + homekit = _mock_homekit(hass, entry, HOMEKIT_MODE_BRIDGE, entity_filter) + + homekit.bridge = Mock() + homekit.bridge.accessories = {} + + filtered_states = await homekit.async_configure_accessories() + assert ( + hass.states.get(switch_config_entity.entity_id) in filtered_states + ) # explicitly included + assert ( + hass.states.get(select_config_entity.entity_id) not in filtered_states + ) # not explicted included and its a hidden entity + assert hass.states.get("cover.excluded_test") not in filtered_states + assert hass.states.get("light.included_test") in filtered_states + assert hass.states.get("select.keep") in filtered_states + + async def test_homekit_start(hass, hk_driver, mock_async_zeroconf, device_reg): """Test HomeKit start method.""" entry = await async_init_integration(hass) diff --git a/tests/components/homekit/test_type_sensors.py b/tests/components/homekit/test_type_sensors.py index 9b6d1c9cee2..75069ce9467 100644 --- a/tests/components/homekit/test_type_sensors.py +++ b/tests/components/homekit/test_type_sensors.py @@ -15,6 +15,8 @@ from homeassistant.components.homekit.type_sensors import ( CarbonMonoxideSensor, HumiditySensor, LightSensor, + PM10Sensor, + PM25Sensor, TemperatureSensor, ) from homeassistant.const import ( @@ -132,6 +134,100 @@ async def test_air_quality(hass, hk_driver): assert acc.char_quality.value == 5 +async def test_pm10(hass, hk_driver): + """Test if accessory is updated after state change.""" + entity_id = "sensor.air_quality_pm10" + + hass.states.async_set(entity_id, None) + await hass.async_block_till_done() + acc = PM10Sensor(hass, hk_driver, "PM10 Sensor", entity_id, 2, None) + await acc.run() + await hass.async_block_till_done() + + assert acc.aid == 2 + assert acc.category == 10 # Sensor + + assert acc.char_density.value == 0 + assert acc.char_quality.value == 0 + + hass.states.async_set(entity_id, STATE_UNKNOWN) + await hass.async_block_till_done() + assert acc.char_density.value == 0 + assert acc.char_quality.value == 0 + + hass.states.async_set(entity_id, "34") + await hass.async_block_till_done() + assert acc.char_density.value == 34 + assert acc.char_quality.value == 1 + + hass.states.async_set(entity_id, "70") + await hass.async_block_till_done() + assert acc.char_density.value == 70 + assert acc.char_quality.value == 2 + + hass.states.async_set(entity_id, "110") + await hass.async_block_till_done() + assert acc.char_density.value == 110 + assert acc.char_quality.value == 3 + + hass.states.async_set(entity_id, "200") + await hass.async_block_till_done() + assert acc.char_density.value == 200 + assert acc.char_quality.value == 4 + + hass.states.async_set(entity_id, "400") + await hass.async_block_till_done() + assert acc.char_density.value == 400 + assert acc.char_quality.value == 5 + + +async def test_pm25(hass, hk_driver): + """Test if accessory is updated after state change.""" + entity_id = "sensor.air_quality_pm25" + + hass.states.async_set(entity_id, None) + await hass.async_block_till_done() + acc = PM25Sensor(hass, hk_driver, "PM25 Sensor", entity_id, 2, None) + await acc.run() + await hass.async_block_till_done() + + assert acc.aid == 2 + assert acc.category == 10 # Sensor + + assert acc.char_density.value == 0 + assert acc.char_quality.value == 0 + + hass.states.async_set(entity_id, STATE_UNKNOWN) + await hass.async_block_till_done() + assert acc.char_density.value == 0 + assert acc.char_quality.value == 0 + + hass.states.async_set(entity_id, "23") + await hass.async_block_till_done() + assert acc.char_density.value == 23 + assert acc.char_quality.value == 1 + + hass.states.async_set(entity_id, "34") + await hass.async_block_till_done() + assert acc.char_density.value == 34 + assert acc.char_quality.value == 2 + + hass.states.async_set(entity_id, "90") + await hass.async_block_till_done() + assert acc.char_density.value == 90 + assert acc.char_quality.value == 3 + + hass.states.async_set(entity_id, "200") + await hass.async_block_till_done() + assert acc.char_density.value == 200 + assert acc.char_quality.value == 4 + + hass.states.async_set(entity_id, "400") + await hass.async_block_till_done() + assert acc.char_density.value == 400 + assert acc.char_quality.value == 5 + + async def test_co(hass, hk_driver): """Test if accessory is updated after state change.""" entity_id = "sensor.co" diff --git a/tests/components/homekit_controller/fixtures/anker_eufycam.json b/tests/components/homekit_controller/fixtures/anker_eufycam.json index b3ebfcf7c9f..7a7bad266c2 100644 --- a/tests/components/homekit_controller/fixtures/anker_eufycam.json +++ b/tests/components/homekit_controller/fixtures/anker_eufycam.json @@ -1,2073 +1,1628 @@ [ - { - "aid": 1, - "services": [ - { - "iid": 1, - "type": "0000003E-0000-1000-8000-0026BB765291", - "primary": false, - "hidden": false, - "linked": [], - "characteristics": [ - { - "iid": 2, - "type": "00000014-0000-1000-8000-0026BB765291", - "format": "bool", - "perms": [ - "pw" - ] - }, - { - "iid": 3, - "type": "00000020-0000-1000-8000-0026BB765291", - "format": "string", - "value": "Anker", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 4, - "type": "00000021-0000-1000-8000-0026BB765291", - "format": "string", - "value": "T8010", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 5, - "type": "00000023-0000-1000-8000-0026BB765291", - "format": "string", - "value": "eufy HomeBase2-0AAA", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 6, - "type": "00000030-0000-1000-8000-0026BB765291", - "format": "string", - "value": "A0000A000000000A", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 7, - "type": "00000052-0000-1000-8000-0026BB765291", - "format": "string", - "value": "2.1.6", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 8, - "type": "00000053-0000-1000-8000-0026BB765291", - "format": "string", - "value": "2.0.0", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 9, - "type": "34AB8811-AC7F-4340-BAC3-FD6A85F9943B", - "format": "string", - "value": "3.0;17A93g", - "perms": [ - "pr", - "hd" - ], - "ev": false - } - ], - "stype": "accessory-information" - }, - { - "iid": 19, - "type": "80CF79D6-9D29-4268-83F7-58FA0244B7CE", - "primary": false, - "hidden": false, - "linked": [], - "characteristics": [ - { - "iid": 20, - "type": "B6704D2D-682B-4CB5-9150-AF94EFD18C22", - "format": "string", - "perms": [ - "pw" - ], - "maxLen": 256 - }, - { - "iid": 21, - "type": "489F0737-E399-41C1-A38A-BC2C152DC88D", - "format": "string", - "value": "0|0", - "perms": [ - "pr", - "ev" - ], - "ev": false - }, - { - "iid": 22, - "type": "DAC539C4-2E71-4C5F-97BE-47A11B41DE4A", - "format": "string", - "value": "0", - "perms": [ - "pr", - "ev" - ], - "ev": false - }, - { - "iid": 23, - "type": "7BD15050-677E-446B-983F-CA276A96ECDF", - "format": "string", - "value": "0", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 24, - "type": "DBE912DF-223D-4038-8116-D0DFA1B6E3DF", - "format": "string", - "value": "T8010N2319490CEB", - "perms": [ - "pr" - ], - "ev": false - } - ], - "stype": "Unknown Service: 80CF79D6-9D29-4268-83F7-58FA0244B7CE" - }, - { - "iid": 16, - "type": "000000A2-0000-1000-8000-0026BB765291", - "primary": false, - "hidden": false, - "linked": [], - "characteristics": [ - { - "iid": 18, - "type": "00000037-0000-1000-8000-0026BB765291", - "format": "string", - "value": "1.1.0", - "perms": [ - "pr" - ], - "ev": false - } - ], - "stype": "service" - } - ] - }, - { - "aid": 2, - "services": [ - { - "iid": 1, - "type": "0000003E-0000-1000-8000-0026BB765291", - "primary": false, - "hidden": false, - "linked": [], - "characteristics": [ - { - "iid": 2, - "type": "00000014-0000-1000-8000-0026BB765291", - "format": "bool", - "perms": [ - "pw" - ] - }, - { - "iid": 3, - "type": "00000020-0000-1000-8000-0026BB765291", - "format": "string", - "value": "Anker", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 4, - "type": "00000021-0000-1000-8000-0026BB765291", - "format": "string", - "value": "T8113", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 5, - "type": "00000023-0000-1000-8000-0026BB765291", - "format": "string", - "value": "eufyCam2-000A", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 6, - "type": "00000030-0000-1000-8000-0026BB765291", - "format": "string", - "value": "A0000A000000000B", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 7, - "type": "00000052-0000-1000-8000-0026BB765291", - "format": "string", - "value": "1.6.7", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 8, - "type": "00000053-0000-1000-8000-0026BB765291", - "format": "string", - "value": "1.0.0", - "perms": [ - "pr" - ], - "ev": false - } - ], - "stype": "accessory-information" - }, - { - "iid": 48, - "type": "00000110-0000-1000-8000-0026BB765291", - "primary": true, - "hidden": false, - "linked": [], - "characteristics": [ - { - "iid": 50, - "type": "000000B0-0000-1000-8000-0026BB765291", - "format": "uint8", - "value": 1, - "perms": [ - "pr", - "pw", - "ev", - "tw" - ], - "ev": false, - "minValue": 0, - "maxValue": 1, - "minStep": 1 - }, - { - "iid": 51, - "type": "00000120-0000-1000-8000-0026BB765291", - "format": "tlv8", - "value": "AQEA", - "perms": [ - "pr", - "ev" - ], - "ev": false - }, - { - "iid": 52, - "type": "00000114-0000-1000-8000-0026BB765291", - "format": "tlv8", - "value": "AaoBAQACEQEBAQIBAAAAAgECAwEABAEAAwsBAoAHAgI4BAMBHgAAAwsBAgAFAgLQAgMBHgAAAwsBAoACAgJoAQMBHgAAAwsBAuABAgIOAQMBHgAAAwsBAkABAgK0AAMBHgAAAwsBAgAFAgLAAwMBHgAAAwsBAgAEAgIAAwMBHgAAAwsBAoACAgLgAQMBHgAAAwsBAuABAgJoAQMBHgAAAwsBAkABAgLwAAMBHg==", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 53, - "type": "00000115-0000-1000-8000-0026BB765291", - "format": "tlv8", - "value": "AQ4BAQICCQEBAQIBAAMBAQIBAA==", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 54, - "type": "00000116-0000-1000-8000-0026BB765291", - "format": "tlv8", - "value": "AgEAAAACAQEAAAIBAg==", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 55, - "type": "00000118-0000-1000-8000-0026BB765291", - "format": "tlv8", - "value": "", - "perms": [ - "pr", - "pw" - ], - "ev": false - }, - { - "iid": 56, - "type": "00000117-0000-1000-8000-0026BB765291", - "format": "tlv8", - "value": "", - "perms": [ - "pr", - "pw" - ], - "ev": false - } - ], - "stype": "camera-rtp-stream-management" - }, - { - "iid": 64, - "type": "00000110-0000-1000-8000-0026BB765291", - "primary": false, - "hidden": false, - "linked": [], - "characteristics": [ - { - "iid": 66, - "type": "000000B0-0000-1000-8000-0026BB765291", - "format": "uint8", - "value": 1, - "perms": [ - "pr", - "pw", - "ev", - "tw" - ], - "ev": false, - "minValue": 0, - "maxValue": 1, - "minStep": 1 - }, - { - "iid": 67, - "type": "00000120-0000-1000-8000-0026BB765291", - "format": "tlv8", - "value": "AQEA", - "perms": [ - "pr", - "ev" - ], - "ev": false - }, - { - "iid": 68, - "type": "00000114-0000-1000-8000-0026BB765291", - "format": "tlv8", - "value": "AYwBAQACEQEBAQIBAAAAAgECAwEABAEAAwsBAgAFAgLQAgMBHgAAAwsBAoACAgJoAQMBHgAAAwsBAuABAgIOAQMBHgAAAwsBAkABAgK0AAMBHgAAAwsBAgAEAgIAAwMBHgAAAwsBAoACAgLgAQMBHgAAAwsBAuABAgJoAQMBHgAAAwsBAkABAgLwAAMBHg==", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 69, - "type": "00000115-0000-1000-8000-0026BB765291", - "format": "tlv8", - "value": "AQ4BAQICCQEBAQIBAAMBAQIBAA==", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 70, - "type": "00000116-0000-1000-8000-0026BB765291", - "format": "tlv8", - "value": "AgEAAAACAQEAAAIBAg==", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 71, - "type": "00000118-0000-1000-8000-0026BB765291", - "format": "tlv8", - "value": "", - "perms": [ - "pr", - "pw" - ], - "ev": false - }, - { - "iid": 72, - "type": "00000117-0000-1000-8000-0026BB765291", - "format": "tlv8", - "value": "", - "perms": [ - "pr", - "pw" - ], - "ev": false - } - ], - "stype": "camera-rtp-stream-management" - }, - { - "iid": 128, - "type": "204", - "primary": false, - "hidden": false, - "linked": [ - 112, - 160 - ], - "characteristics": [ - { - "iid": 130, - "type": "000000B0-0000-1000-8000-0026BB765291", - "format": "uint8", - "value": 0, - "perms": [ - "pr", - "pw", - "ev", - "tw" - ], - "ev": false, - "minValue": 0, - "maxValue": 1, - "minStep": 1 - }, - { - "iid": 131, - "type": "205", - "format": "tlv8", - "value": "AQQAAAAAAggBAAAAAAAAAAMLAQEAAgYBBKAPAAA=", - "perms": [ - "pr", - "ev" - ], - "ev": false - }, - { - "iid": 132, - "type": "206", - "format": "tlv8", - "value": "AaQBAQACCwEBAQIBAAAAAgECAwsBAoAHAgI4BAMBHgAAAwsBAgAFAgLQAgMBHgAAAwsBAoACAgJoAQMBHgAAAwsBAuABAgIOAQMBHgAAAwsBAkABAgK0AAMBHgAAAwsBAoAHAgI4BAMBDwAAAwsBAgAFAgLQAgMBDwAAAwsBAoACAgJoAQMBDwAAAwsBAuABAgIOAQMBDwAAAwsBAkABAgK0AAMBDw==", - "perms": [ - "pr", - "ev" - ], - "ev": false - }, - { - "iid": 133, - "type": "207", - "format": "tlv8", - "value": "AQ4BAQECCQEBAQIBAAMBAQ==", - "perms": [ - "pr", - "ev" - ], - "ev": false - }, - { - "iid": 134, - "type": "209", - "format": "tlv8", - "value": "AR0BBAAAAAACCAEAAAAAAAAAAwsBAQACBgEEoA8AAAIkAQEAAhIBAQECAQIDBNAHAAAEBKAPAAADCwECgAcCAjgEAwEeAxQBAQECDwEBAQIBAAMBAQQEEAAAAA==", - "perms": [ - "pr", - "pw", - "ev" - ], - "ev": false - }, - { - "iid": 135, - "type": "226", - "format": "uint8", - "value": 1, - "perms": [ - "pr", - "pw", - "ev", - "tw" - ], - "ev": false, - "minValue": 0, - "maxValue": 1, - "minStep": 1 - } - ], - "stype": "Unknown Service: 204" - }, - { - "iid": 112, - "type": "00000129-0000-1000-8000-0026BB765291", - "primary": false, - "hidden": false, - "linked": [], - "characteristics": [ - { - "iid": 114, - "type": "00000130-0000-1000-8000-0026BB765291", - "format": "tlv8", - "value": "AQMBAQA=", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 115, - "type": "00000131-0000-1000-8000-0026BB765291", - "format": "tlv8", - "value": "", - "perms": [ - "pr", - "pw", - "wr" - ], - "ev": false - }, - { - "iid": 116, - "type": "00000037-0000-1000-8000-0026BB765291", - "format": "string", - "value": "1.0", - "perms": [ - "pr" - ], - "ev": false - } - ], - "stype": "data-stream-transport-management" - }, - { - "iid": 144, - "type": "21A", - "primary": false, - "hidden": false, - "linked": [], - "characteristics": [ - { - "iid": 146, - "type": "223", - "format": "uint8", - "value": 1, - "perms": [ - "pr", - "pw", - "ev" - ], - "ev": false, - "minValue": 0, - "maxValue": 1, - "minStep": 1 - }, - { - "iid": 147, - "type": "225", - "format": "uint8", - "value": 1, - "perms": [ - "pr", - "pw", - "ev" - ], - "ev": false, - "minValue": 0, - "maxValue": 1, - "minStep": 1 - }, - { - "iid": 148, - "type": "21B", - "format": "uint8", - "value": 1, - "perms": [ - "pr", - "pw", - "ev" - ], - "ev": false, - "minValue": 0, - "maxValue": 1, - "minStep": 1 - }, - { - "iid": 149, - "type": "21C", - "format": "uint8", - "value": 1, - "perms": [ - "pr", - "ev" - ], - "ev": false, - "minValue": 0, - "maxValue": 1, - "minStep": 1 - }, - { - "iid": 150, - "type": "21D", - "format": "uint8", - "value": 0, - "perms": [ - "pr", - "pw", - "ev" - ], - "ev": false, - "minValue": 0, - "maxValue": 1, - "minStep": 1 - }, - { - "iid": 152, - "type": "0000011B-0000-1000-8000-0026BB765291", - "format": "bool", - "value": 1, - "perms": [ - "pr", - "pw", - "ev" - ], - "ev": false - } - ], - "stype": "Unknown Service: 21A" - }, - { - "iid": 80, - "type": "00000112-0000-1000-8000-0026BB765291", - "primary": false, - "hidden": false, - "linked": [], - "characteristics": [ - { - "iid": 82, - "type": "00000023-0000-1000-8000-0026BB765291", - "format": "string", - "value": "Microphone", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 83, - "type": "0000011A-0000-1000-8000-0026BB765291", - "format": "bool", - "value": 0, - "perms": [ - "pr", - "pw", - "ev" - ], - "ev": false - }, - { - "iid": 84, - "type": "00000119-0000-1000-8000-0026BB765291", - "format": "uint8", - "value": 100, - "perms": [ - "pr", - "pw", - "ev" - ], - "ev": false, - "unit": "percentage", - "minValue": 0, - "maxValue": 100, - "minStep": 1 - } - ], - "stype": "microphone" - }, - { - "iid": 160, - "type": "00000085-0000-1000-8000-0026BB765291", - "primary": false, - "hidden": false, - "linked": [], - "characteristics": [ - { - "iid": 162, - "type": "00000022-0000-1000-8000-0026BB765291", - "format": "bool", - "value": 0, - "perms": [ - "pr", - "ev" - ], - "ev": true - }, - { - "iid": 163, - "type": "00000023-0000-1000-8000-0026BB765291", - "format": "string", - "value": "Motion Sensor", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 164, - "type": "00000075-0000-1000-8000-0026BB765291", - "format": "bool", - "value": 1, - "perms": [ - "pr", - "ev" - ], - "ev": false - } - ], - "stype": "motion" - }, - { - "iid": 101, - "type": "00000096-0000-1000-8000-0026BB765291", - "primary": false, - "hidden": false, - "linked": [], - "characteristics": [ - { - "iid": 102, - "type": "00000068-0000-1000-8000-0026BB765291", - "format": "uint8", - "value": 38, - "perms": [ - "pr", - "ev" - ], - "ev": true, - "unit": "percentage", - "minValue": 0, - "maxValue": 100, - "minStep": 1 - }, - { - "iid": 103, - "type": "0000008F-0000-1000-8000-0026BB765291", - "format": "uint8", - "value": 0, - "perms": [ - "pr", - "ev" - ], - "ev": true, - "minValue": 0, - "maxValue": 2, - "minStep": 1 - }, - { - "iid": 104, - "type": "00000079-0000-1000-8000-0026BB765291", - "format": "uint8", - "value": 0, - "perms": [ - "pr", - "ev" - ], - "ev": true, - "minValue": 0, - "maxValue": 1, - "minStep": 1 - } - ], - "stype": "battery" - } - ] - }, - { - "aid": 3, - "services": [ - { - "iid": 1, - "type": "0000003E-0000-1000-8000-0026BB765291", - "primary": false, - "hidden": false, - "linked": [], - "characteristics": [ - { - "iid": 2, - "type": "00000014-0000-1000-8000-0026BB765291", - "format": "bool", - "perms": [ - "pw" - ] - }, - { - "iid": 3, - "type": "00000020-0000-1000-8000-0026BB765291", - "format": "string", - "value": "Anker", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 4, - "type": "00000021-0000-1000-8000-0026BB765291", - "format": "string", - "value": "T8113", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 5, - "type": "00000023-0000-1000-8000-0026BB765291", - "format": "string", - "value": "eufyCam2-000A", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 6, - "type": "00000030-0000-1000-8000-0026BB765291", - "format": "string", - "value": "A0000A000000000C", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 7, - "type": "00000052-0000-1000-8000-0026BB765291", - "format": "string", - "value": "1.6.7", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 8, - "type": "00000053-0000-1000-8000-0026BB765291", - "format": "string", - "value": "1.0.0", - "perms": [ - "pr" - ], - "ev": false - } - ], - "stype": "accessory-information" - }, - { - "iid": 48, - "type": "00000110-0000-1000-8000-0026BB765291", - "primary": true, - "hidden": false, - "linked": [], - "characteristics": [ - { - "iid": 50, - "type": "000000B0-0000-1000-8000-0026BB765291", - "format": "uint8", - "value": 1, - "perms": [ - "pr", - "pw", - "ev", - "tw" - ], - "ev": false, - "minValue": 0, - "maxValue": 1, - "minStep": 1 - }, - { - "iid": 51, - "type": "00000120-0000-1000-8000-0026BB765291", - "format": "tlv8", - "value": "AQEA", - "perms": [ - "pr", - "ev" - ], - "ev": false - }, - { - "iid": 52, - "type": "00000114-0000-1000-8000-0026BB765291", - "format": "tlv8", - "value": "AaoBAQACEQEBAQIBAAAAAgECAwEABAEAAwsBAoAHAgI4BAMBHgAAAwsBAgAFAgLQAgMBHgAAAwsBAoACAgJoAQMBHgAAAwsBAuABAgIOAQMBHgAAAwsBAkABAgK0AAMBHgAAAwsBAgAFAgLAAwMBHgAAAwsBAgAEAgIAAwMBHgAAAwsBAoACAgLgAQMBHgAAAwsBAuABAgJoAQMBHgAAAwsBAkABAgLwAAMBHg==", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 53, - "type": "00000115-0000-1000-8000-0026BB765291", - "format": "tlv8", - "value": "AQ4BAQICCQEBAQIBAAMBAQIBAA==", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 54, - "type": "00000116-0000-1000-8000-0026BB765291", - "format": "tlv8", - "value": "AgEAAAACAQEAAAIBAg==", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 55, - "type": "00000118-0000-1000-8000-0026BB765291", - "format": "tlv8", - "value": "", - "perms": [ - "pr", - "pw" - ], - "ev": false - }, - { - "iid": 56, - "type": "00000117-0000-1000-8000-0026BB765291", - "format": "tlv8", - "value": "", - "perms": [ - "pr", - "pw" - ], - "ev": false - } - ], - "stype": "camera-rtp-stream-management" - }, - { - "iid": 64, - "type": "00000110-0000-1000-8000-0026BB765291", - "primary": false, - "hidden": false, - "linked": [], - "characteristics": [ - { - "iid": 66, - "type": "000000B0-0000-1000-8000-0026BB765291", - "format": "uint8", - "value": 1, - "perms": [ - "pr", - "pw", - "ev", - "tw" - ], - "ev": false, - "minValue": 0, - "maxValue": 1, - "minStep": 1 - }, - { - "iid": 67, - "type": "00000120-0000-1000-8000-0026BB765291", - "format": "tlv8", - "value": "AQEA", - "perms": [ - "pr", - "ev" - ], - "ev": false - }, - { - "iid": 68, - "type": "00000114-0000-1000-8000-0026BB765291", - "format": "tlv8", - "value": "AYwBAQACEQEBAQIBAAAAAgECAwEABAEAAwsBAgAFAgLQAgMBHgAAAwsBAoACAgJoAQMBHgAAAwsBAuABAgIOAQMBHgAAAwsBAkABAgK0AAMBHgAAAwsBAgAEAgIAAwMBHgAAAwsBAoACAgLgAQMBHgAAAwsBAuABAgJoAQMBHgAAAwsBAkABAgLwAAMBHg==", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 69, - "type": "00000115-0000-1000-8000-0026BB765291", - "format": "tlv8", - "value": "AQ4BAQICCQEBAQIBAAMBAQIBAA==", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 70, - "type": "00000116-0000-1000-8000-0026BB765291", - "format": "tlv8", - "value": "AgEAAAACAQEAAAIBAg==", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 71, - "type": "00000118-0000-1000-8000-0026BB765291", - "format": "tlv8", - "value": "", - "perms": [ - "pr", - "pw" - ], - "ev": false - }, - { - "iid": 72, - "type": "00000117-0000-1000-8000-0026BB765291", - "format": "tlv8", - "value": "", - "perms": [ - "pr", - "pw" - ], - "ev": false - } - ], - "stype": "camera-rtp-stream-management" - }, - { - "iid": 128, - "type": "204", - "primary": false, - "hidden": false, - "linked": [ - 112, - 160 - ], - "characteristics": [ - { - "iid": 130, - "type": "000000B0-0000-1000-8000-0026BB765291", - "format": "uint8", - "value": 0, - "perms": [ - "pr", - "pw", - "ev", - "tw" - ], - "ev": false, - "minValue": 0, - "maxValue": 1, - "minStep": 1 - }, - { - "iid": 131, - "type": "205", - "format": "tlv8", - "value": "AQQAAAAAAggBAAAAAAAAAAMLAQEAAgYBBKAPAAA=", - "perms": [ - "pr", - "ev" - ], - "ev": false - }, - { - "iid": 132, - "type": "206", - "format": "tlv8", - "value": "AaQBAQACCwEBAQIBAAAAAgECAwsBAoAHAgI4BAMBHgAAAwsBAgAFAgLQAgMBHgAAAwsBAoACAgJoAQMBHgAAAwsBAuABAgIOAQMBHgAAAwsBAkABAgK0AAMBHgAAAwsBAoAHAgI4BAMBDwAAAwsBAgAFAgLQAgMBDwAAAwsBAoACAgJoAQMBDwAAAwsBAuABAgIOAQMBDwAAAwsBAkABAgK0AAMBDw==", - "perms": [ - "pr", - "ev" - ], - "ev": false - }, - { - "iid": 133, - "type": "207", - "format": "tlv8", - "value": "AQ4BAQECCQEBAQIBAAMBAQ==", - "perms": [ - "pr", - "ev" - ], - "ev": false - }, - { - "iid": 134, - "type": "209", - "format": "tlv8", - "value": "", - "perms": [ - "pr", - "pw", - "ev" - ], - "ev": false - }, - { - "iid": 135, - "type": "226", - "format": "uint8", - "value": 1, - "perms": [ - "pr", - "pw", - "ev", - "tw" - ], - "ev": false, - "minValue": 0, - "maxValue": 1, - "minStep": 1 - } - ], - "stype": "Unknown Service: 204" - }, - { - "iid": 112, - "type": "00000129-0000-1000-8000-0026BB765291", - "primary": false, - "hidden": false, - "linked": [], - "characteristics": [ - { - "iid": 114, - "type": "00000130-0000-1000-8000-0026BB765291", - "format": "tlv8", - "value": "AQMBAQA=", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 115, - "type": "00000131-0000-1000-8000-0026BB765291", - "format": "tlv8", - "value": "", - "perms": [ - "pr", - "pw", - "wr" - ], - "ev": false - }, - { - "iid": 116, - "type": "00000037-0000-1000-8000-0026BB765291", - "format": "string", - "value": "1.0", - "perms": [ - "pr" - ], - "ev": false - } - ], - "stype": "data-stream-transport-management" - }, - { - "iid": 144, - "type": "21A", - "primary": false, - "hidden": false, - "linked": [], - "characteristics": [ - { - "iid": 146, - "type": "223", - "format": "uint8", - "value": 1, - "perms": [ - "pr", - "pw", - "ev" - ], - "ev": false, - "minValue": 0, - "maxValue": 1, - "minStep": 1 - }, - { - "iid": 147, - "type": "225", - "format": "uint8", - "value": 1, - "perms": [ - "pr", - "pw", - "ev" - ], - "ev": false, - "minValue": 0, - "maxValue": 1, - "minStep": 1 - }, - { - "iid": 148, - "type": "21B", - "format": "uint8", - "value": 1, - "perms": [ - "pr", - "pw", - "ev" - ], - "ev": false, - "minValue": 0, - "maxValue": 1, - "minStep": 1 - }, - { - "iid": 149, - "type": "21C", - "format": "uint8", - "value": 1, - "perms": [ - "pr", - "ev" - ], - "ev": false, - "minValue": 0, - "maxValue": 1, - "minStep": 1 - }, - { - "iid": 150, - "type": "21D", - "format": "uint8", - "value": 0, - "perms": [ - "pr", - "pw", - "ev" - ], - "ev": false, - "minValue": 0, - "maxValue": 1, - "minStep": 1 - }, - { - "iid": 152, - "type": "0000011B-0000-1000-8000-0026BB765291", - "format": "bool", - "value": null, - "perms": [ - "pr", - "pw", - "ev" - ], - "ev": false - } - ], - "stype": "Unknown Service: 21A" - }, - { - "iid": 80, - "type": "00000112-0000-1000-8000-0026BB765291", - "primary": false, - "hidden": false, - "linked": [], - "characteristics": [ - { - "iid": 82, - "type": "00000023-0000-1000-8000-0026BB765291", - "format": "string", - "value": "Microphone", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 83, - "type": "0000011A-0000-1000-8000-0026BB765291", - "format": "bool", - "value": null, - "perms": [ - "pr", - "pw", - "ev" - ], - "ev": false - }, - { - "iid": 84, - "type": "00000119-0000-1000-8000-0026BB765291", - "format": "uint8", - "value": null, - "perms": [ - "pr", - "pw", - "ev" - ], - "ev": false, - "unit": "percentage", - "minValue": 0, - "maxValue": 100, - "minStep": 1 - } - ], - "stype": "microphone" - }, - { - "iid": 160, - "type": "00000085-0000-1000-8000-0026BB765291", - "primary": false, - "hidden": false, - "linked": [], - "characteristics": [ - { - "iid": 162, - "type": "00000022-0000-1000-8000-0026BB765291", - "format": "bool", - "value": null, - "perms": [ - "pr", - "ev" - ], - "ev": true - }, - { - "iid": 163, - "type": "00000023-0000-1000-8000-0026BB765291", - "format": "string", - "value": "Motion Sensor", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 164, - "type": "00000075-0000-1000-8000-0026BB765291", - "format": "bool", - "value": null, - "perms": [ - "pr", - "ev" - ], - "ev": false - } - ], - "stype": "motion" - }, - { - "iid": 101, - "type": "00000096-0000-1000-8000-0026BB765291", - "primary": false, - "hidden": false, - "linked": [], - "characteristics": [ - { - "iid": 102, - "type": "00000068-0000-1000-8000-0026BB765291", - "format": "uint8", - "value": null, - "perms": [ - "pr", - "ev" - ], - "ev": true, - "unit": "percentage", - "minValue": 0, - "maxValue": 100, - "minStep": 1 - }, - { - "iid": 103, - "type": "0000008F-0000-1000-8000-0026BB765291", - "format": "uint8", - "value": null, - "perms": [ - "pr", - "ev" - ], - "ev": true, - "minValue": 0, - "maxValue": 2, - "minStep": 1 - }, - { - "iid": 104, - "type": "00000079-0000-1000-8000-0026BB765291", - "format": "uint8", - "value": null, - "perms": [ - "pr", - "ev" - ], - "ev": true, - "minValue": 0, - "maxValue": 1, - "minStep": 1 - } - ], - "stype": "battery" - } - ] - }, - { - "aid": 4, - "services": [ - { - "iid": 1, - "type": "0000003E-0000-1000-8000-0026BB765291", - "primary": false, - "hidden": false, - "linked": [], - "characteristics": [ - { - "iid": 2, - "type": "00000014-0000-1000-8000-0026BB765291", - "format": "bool", - "perms": [ - "pw" - ] - }, - { - "iid": 3, - "type": "00000020-0000-1000-8000-0026BB765291", - "format": "string", - "value": "Anker", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 4, - "type": "00000021-0000-1000-8000-0026BB765291", - "format": "string", - "value": "T8113", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 5, - "type": "00000023-0000-1000-8000-0026BB765291", - "format": "string", - "value": "eufyCam2-0000", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 6, - "type": "00000030-0000-1000-8000-0026BB765291", - "format": "string", - "value": "A0000A000000000D", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 7, - "type": "00000052-0000-1000-8000-0026BB765291", - "format": "string", - "value": "1.6.7", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 8, - "type": "00000053-0000-1000-8000-0026BB765291", - "format": "string", - "value": "1.0.0", - "perms": [ - "pr" - ], - "ev": false - } - ], - "stype": "accessory-information" - }, - { - "iid": 48, - "type": "00000110-0000-1000-8000-0026BB765291", - "primary": true, - "hidden": false, - "linked": [], - "characteristics": [ - { - "iid": 50, - "type": "000000B0-0000-1000-8000-0026BB765291", - "format": "uint8", - "value": 1, - "perms": [ - "pr", - "pw", - "ev", - "tw" - ], - "ev": false, - "minValue": 0, - "maxValue": 1, - "minStep": 1 - }, - { - "iid": 51, - "type": "00000120-0000-1000-8000-0026BB765291", - "format": "tlv8", - "value": "AQEA", - "perms": [ - "pr", - "ev" - ], - "ev": false - }, - { - "iid": 52, - "type": "00000114-0000-1000-8000-0026BB765291", - "format": "tlv8", - "value": "AaoBAQACEQEBAQIBAAAAAgECAwEABAEAAwsBAoAHAgI4BAMBHgAAAwsBAgAFAgLQAgMBHgAAAwsBAoACAgJoAQMBHgAAAwsBAuABAgIOAQMBHgAAAwsBAkABAgK0AAMBHgAAAwsBAgAFAgLAAwMBHgAAAwsBAgAEAgIAAwMBHgAAAwsBAoACAgLgAQMBHgAAAwsBAuABAgJoAQMBHgAAAwsBAkABAgLwAAMBHg==", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 53, - "type": "00000115-0000-1000-8000-0026BB765291", - "format": "tlv8", - "value": "AQ4BAQICCQEBAQIBAAMBAQIBAA==", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 54, - "type": "00000116-0000-1000-8000-0026BB765291", - "format": "tlv8", - "value": "AgEAAAACAQEAAAIBAg==", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 55, - "type": "00000118-0000-1000-8000-0026BB765291", - "format": "tlv8", - "value": "", - "perms": [ - "pr", - "pw" - ], - "ev": false - }, - { - "iid": 56, - "type": "00000117-0000-1000-8000-0026BB765291", - "format": "tlv8", - "value": "", - "perms": [ - "pr", - "pw" - ], - "ev": false - } - ], - "stype": "camera-rtp-stream-management" - }, - { - "iid": 64, - "type": "00000110-0000-1000-8000-0026BB765291", - "primary": false, - "hidden": false, - "linked": [], - "characteristics": [ - { - "iid": 66, - "type": "000000B0-0000-1000-8000-0026BB765291", - "format": "uint8", - "value": 1, - "perms": [ - "pr", - "pw", - "ev", - "tw" - ], - "ev": false, - "minValue": 0, - "maxValue": 1, - "minStep": 1 - }, - { - "iid": 67, - "type": "00000120-0000-1000-8000-0026BB765291", - "format": "tlv8", - "value": "AQEA", - "perms": [ - "pr", - "ev" - ], - "ev": false - }, - { - "iid": 68, - "type": "00000114-0000-1000-8000-0026BB765291", - "format": "tlv8", - "value": "AYwBAQACEQEBAQIBAAAAAgECAwEABAEAAwsBAgAFAgLQAgMBHgAAAwsBAoACAgJoAQMBHgAAAwsBAuABAgIOAQMBHgAAAwsBAkABAgK0AAMBHgAAAwsBAgAEAgIAAwMBHgAAAwsBAoACAgLgAQMBHgAAAwsBAuABAgJoAQMBHgAAAwsBAkABAgLwAAMBHg==", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 69, - "type": "00000115-0000-1000-8000-0026BB765291", - "format": "tlv8", - "value": "AQ4BAQICCQEBAQIBAAMBAQIBAA==", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 70, - "type": "00000116-0000-1000-8000-0026BB765291", - "format": "tlv8", - "value": "AgEAAAACAQEAAAIBAg==", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 71, - "type": "00000118-0000-1000-8000-0026BB765291", - "format": "tlv8", - "value": "", - "perms": [ - "pr", - "pw" - ], - "ev": false - }, - { - "iid": 72, - "type": "00000117-0000-1000-8000-0026BB765291", - "format": "tlv8", - "value": "", - "perms": [ - "pr", - "pw" - ], - "ev": false - } - ], - "stype": "camera-rtp-stream-management" - }, - { - "iid": 128, - "type": "204", - "primary": false, - "hidden": false, - "linked": [ - 112, - 160 - ], - "characteristics": [ - { - "iid": 130, - "type": "000000B0-0000-1000-8000-0026BB765291", - "format": "uint8", - "value": 1, - "perms": [ - "pr", - "pw", - "ev", - "tw" - ], - "ev": false, - "minValue": 0, - "maxValue": 1, - "minStep": 1 - }, - { - "iid": 131, - "type": "205", - "format": "tlv8", - "value": "AQQAAAAAAggBAAAAAAAAAAMLAQEAAgYBBKAPAAA=", - "perms": [ - "pr", - "ev" - ], - "ev": false - }, - { - "iid": 132, - "type": "206", - "format": "tlv8", - "value": "AaQBAQACCwEBAQIBAAAAAgECAwsBAoAHAgI4BAMBHgAAAwsBAgAFAgLQAgMBHgAAAwsBAoACAgJoAQMBHgAAAwsBAuABAgIOAQMBHgAAAwsBAkABAgK0AAMBHgAAAwsBAoAHAgI4BAMBDwAAAwsBAgAFAgLQAgMBDwAAAwsBAoACAgJoAQMBDwAAAwsBAuABAgIOAQMBDwAAAwsBAkABAgK0AAMBDw==", - "perms": [ - "pr", - "ev" - ], - "ev": false - }, - { - "iid": 133, - "type": "207", - "format": "tlv8", - "value": "AQ4BAQECCQEBAQIBAAMBAQ==", - "perms": [ - "pr", - "ev" - ], - "ev": false - }, - { - "iid": 134, - "type": "209", - "format": "tlv8", - "value": "AR0BBAAAAAACCAEAAAAAAAAAAwsBAQACBgEEoA8AAAIkAQEAAhIBAQECAQIDBNAHAAAEBKAPAAADCwECgAcCAjgEAwEeAxQBAQECDwEBAQIBAAMBAQQEEAAAAA==", - "perms": [ - "pr", - "pw", - "ev" - ], - "ev": false - }, - { - "iid": 135, - "type": "226", - "format": "uint8", - "value": 1, - "perms": [ - "pr", - "pw", - "ev", - "tw" - ], - "ev": false, - "minValue": 0, - "maxValue": 1, - "minStep": 1 - } - ], - "stype": "Unknown Service: 204" - }, - { - "iid": 112, - "type": "00000129-0000-1000-8000-0026BB765291", - "primary": false, - "hidden": false, - "linked": [], - "characteristics": [ - { - "iid": 114, - "type": "00000130-0000-1000-8000-0026BB765291", - "format": "tlv8", - "value": "AQMBAQA=", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 115, - "type": "00000131-0000-1000-8000-0026BB765291", - "format": "tlv8", - "value": "", - "perms": [ - "pr", - "pw", - "wr" - ], - "ev": false - }, - { - "iid": 116, - "type": "00000037-0000-1000-8000-0026BB765291", - "format": "string", - "value": "1.0", - "perms": [ - "pr" - ], - "ev": false - } - ], - "stype": "data-stream-transport-management" - }, - { - "iid": 144, - "type": "21A", - "primary": false, - "hidden": false, - "linked": [], - "characteristics": [ - { - "iid": 146, - "type": "223", - "format": "uint8", - "value": 1, - "perms": [ - "pr", - "pw", - "ev" - ], - "ev": false, - "minValue": 0, - "maxValue": 1, - "minStep": 1 - }, - { - "iid": 147, - "type": "225", - "format": "uint8", - "value": 1, - "perms": [ - "pr", - "pw", - "ev" - ], - "ev": false, - "minValue": 0, - "maxValue": 1, - "minStep": 1 - }, - { - "iid": 148, - "type": "21B", - "format": "uint8", - "value": 1, - "perms": [ - "pr", - "pw", - "ev" - ], - "ev": false, - "minValue": 0, - "maxValue": 1, - "minStep": 1 - }, - { - "iid": 149, - "type": "21C", - "format": "uint8", - "value": 1, - "perms": [ - "pr", - "ev" - ], - "ev": false, - "minValue": 0, - "maxValue": 1, - "minStep": 1 - }, - { - "iid": 150, - "type": "21D", - "format": "uint8", - "value": 0, - "perms": [ - "pr", - "pw", - "ev" - ], - "ev": false, - "minValue": 0, - "maxValue": 1, - "minStep": 1 - }, - { - "iid": 152, - "type": "0000011B-0000-1000-8000-0026BB765291", - "format": "bool", - "value": 1, - "perms": [ - "pr", - "pw", - "ev" - ], - "ev": false - } - ], - "stype": "Unknown Service: 21A" - }, - { - "iid": 80, - "type": "00000112-0000-1000-8000-0026BB765291", - "primary": false, - "hidden": false, - "linked": [], - "characteristics": [ - { - "iid": 82, - "type": "00000023-0000-1000-8000-0026BB765291", - "format": "string", - "value": "Microphone", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 83, - "type": "0000011A-0000-1000-8000-0026BB765291", - "format": "bool", - "value": 0, - "perms": [ - "pr", - "pw", - "ev" - ], - "ev": false - }, - { - "iid": 84, - "type": "00000119-0000-1000-8000-0026BB765291", - "format": "uint8", - "value": 50, - "perms": [ - "pr", - "pw", - "ev" - ], - "ev": false, - "unit": "percentage", - "minValue": 0, - "maxValue": 100, - "minStep": 1 - } - ], - "stype": "microphone" - }, - { - "iid": 160, - "type": "00000085-0000-1000-8000-0026BB765291", - "primary": false, - "hidden": false, - "linked": [], - "characteristics": [ - { - "iid": 162, - "type": "00000022-0000-1000-8000-0026BB765291", - "format": "bool", - "value": 0, - "perms": [ - "pr", - "ev" - ], - "ev": true - }, - { - "iid": 163, - "type": "00000023-0000-1000-8000-0026BB765291", - "format": "string", - "value": "Motion Sensor", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 164, - "type": "00000075-0000-1000-8000-0026BB765291", - "format": "bool", - "value": 1, - "perms": [ - "pr", - "ev" - ], - "ev": false - } - ], - "stype": "motion" - }, - { - "iid": 101, - "type": "00000096-0000-1000-8000-0026BB765291", - "primary": false, - "hidden": false, - "linked": [], - "characteristics": [ - { - "iid": 102, - "type": "00000068-0000-1000-8000-0026BB765291", - "format": "uint8", - "value": 17, - "perms": [ - "pr", - "ev" - ], - "ev": true, - "unit": "percentage", - "minValue": 0, - "maxValue": 100, - "minStep": 1 - }, - { - "iid": 103, - "type": "0000008F-0000-1000-8000-0026BB765291", - "format": "uint8", - "value": 0, - "perms": [ - "pr", - "ev" - ], - "ev": true, - "minValue": 0, - "maxValue": 2, - "minStep": 1 - }, - { - "iid": 104, - "type": "00000079-0000-1000-8000-0026BB765291", - "format": "uint8", - "value": 0, - "perms": [ - "pr", - "ev" - ], - "ev": true, - "minValue": 0, - "maxValue": 1, - "minStep": 1 - } - ], - "stype": "battery" - } - ] - } -] \ No newline at end of file + { + "aid": 1, + "services": [ + { + "iid": 1, + "type": "0000003E-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 2, + "type": "00000014-0000-1000-8000-0026BB765291", + "format": "bool", + "perms": ["pw"] + }, + { + "iid": 3, + "type": "00000020-0000-1000-8000-0026BB765291", + "format": "string", + "value": "Anker", + "perms": ["pr"], + "ev": false + }, + { + "iid": 4, + "type": "00000021-0000-1000-8000-0026BB765291", + "format": "string", + "value": "T8010", + "perms": ["pr"], + "ev": false + }, + { + "iid": 5, + "type": "00000023-0000-1000-8000-0026BB765291", + "format": "string", + "value": "eufy HomeBase2-0AAA", + "perms": ["pr"], + "ev": false + }, + { + "iid": 6, + "type": "00000030-0000-1000-8000-0026BB765291", + "format": "string", + "value": "A0000A000000000A", + "perms": ["pr"], + "ev": false + }, + { + "iid": 7, + "type": "00000052-0000-1000-8000-0026BB765291", + "format": "string", + "value": "2.1.6", + "perms": ["pr"], + "ev": false + }, + { + "iid": 8, + "type": "00000053-0000-1000-8000-0026BB765291", + "format": "string", + "value": "2.0.0", + "perms": ["pr"], + "ev": false + }, + { + "iid": 9, + "type": "34AB8811-AC7F-4340-BAC3-FD6A85F9943B", + "format": "string", + "value": "3.0;17A93g", + "perms": ["pr", "hd"], + "ev": false + } + ], + "stype": "accessory-information" + }, + { + "iid": 19, + "type": "80CF79D6-9D29-4268-83F7-58FA0244B7CE", + "primary": false, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 20, + "type": "B6704D2D-682B-4CB5-9150-AF94EFD18C22", + "format": "string", + "perms": ["pw"], + "maxLen": 256 + }, + { + "iid": 21, + "type": "489F0737-E399-41C1-A38A-BC2C152DC88D", + "format": "string", + "value": "0|0", + "perms": ["pr", "ev"], + "ev": false + }, + { + "iid": 22, + "type": "DAC539C4-2E71-4C5F-97BE-47A11B41DE4A", + "format": "string", + "value": "0", + "perms": ["pr", "ev"], + "ev": false + }, + { + "iid": 23, + "type": "7BD15050-677E-446B-983F-CA276A96ECDF", + "format": "string", + "value": "0", + "perms": ["pr"], + "ev": false + }, + { + "iid": 24, + "type": "DBE912DF-223D-4038-8116-D0DFA1B6E3DF", + "format": "string", + "value": "T8010N2319490CEB", + "perms": ["pr"], + "ev": false + } + ], + "stype": "Unknown Service: 80CF79D6-9D29-4268-83F7-58FA0244B7CE" + }, + { + "iid": 16, + "type": "000000A2-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 18, + "type": "00000037-0000-1000-8000-0026BB765291", + "format": "string", + "value": "1.1.0", + "perms": ["pr"], + "ev": false + } + ], + "stype": "service" + } + ] + }, + { + "aid": 2, + "services": [ + { + "iid": 1, + "type": "0000003E-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 2, + "type": "00000014-0000-1000-8000-0026BB765291", + "format": "bool", + "perms": ["pw"] + }, + { + "iid": 3, + "type": "00000020-0000-1000-8000-0026BB765291", + "format": "string", + "value": "Anker", + "perms": ["pr"], + "ev": false + }, + { + "iid": 4, + "type": "00000021-0000-1000-8000-0026BB765291", + "format": "string", + "value": "T8113", + "perms": ["pr"], + "ev": false + }, + { + "iid": 5, + "type": "00000023-0000-1000-8000-0026BB765291", + "format": "string", + "value": "eufyCam2-000A", + "perms": ["pr"], + "ev": false + }, + { + "iid": 6, + "type": "00000030-0000-1000-8000-0026BB765291", + "format": "string", + "value": "A0000A000000000B", + "perms": ["pr"], + "ev": false + }, + { + "iid": 7, + "type": "00000052-0000-1000-8000-0026BB765291", + "format": "string", + "value": "1.6.7", + "perms": ["pr"], + "ev": false + }, + { + "iid": 8, + "type": "00000053-0000-1000-8000-0026BB765291", + "format": "string", + "value": "1.0.0", + "perms": ["pr"], + "ev": false + } + ], + "stype": "accessory-information" + }, + { + "iid": 48, + "type": "00000110-0000-1000-8000-0026BB765291", + "primary": true, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 50, + "type": "000000B0-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 1, + "perms": ["pr", "pw", "ev", "tw"], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 51, + "type": "00000120-0000-1000-8000-0026BB765291", + "format": "tlv8", + "value": "AQEA", + "perms": ["pr", "ev"], + "ev": false + }, + { + "iid": 52, + "type": "00000114-0000-1000-8000-0026BB765291", + "format": "tlv8", + "value": "AaoBAQACEQEBAQIBAAAAAgECAwEABAEAAwsBAoAHAgI4BAMBHgAAAwsBAgAFAgLQAgMBHgAAAwsBAoACAgJoAQMBHgAAAwsBAuABAgIOAQMBHgAAAwsBAkABAgK0AAMBHgAAAwsBAgAFAgLAAwMBHgAAAwsBAgAEAgIAAwMBHgAAAwsBAoACAgLgAQMBHgAAAwsBAuABAgJoAQMBHgAAAwsBAkABAgLwAAMBHg==", + "perms": ["pr"], + "ev": false + }, + { + "iid": 53, + "type": "00000115-0000-1000-8000-0026BB765291", + "format": "tlv8", + "value": "AQ4BAQICCQEBAQIBAAMBAQIBAA==", + "perms": ["pr"], + "ev": false + }, + { + "iid": 54, + "type": "00000116-0000-1000-8000-0026BB765291", + "format": "tlv8", + "value": "AgEAAAACAQEAAAIBAg==", + "perms": ["pr"], + "ev": false + }, + { + "iid": 55, + "type": "00000118-0000-1000-8000-0026BB765291", + "format": "tlv8", + "value": "", + "perms": ["pr", "pw"], + "ev": false + }, + { + "iid": 56, + "type": "00000117-0000-1000-8000-0026BB765291", + "format": "tlv8", + "value": "", + "perms": ["pr", "pw"], + "ev": false + } + ], + "stype": "camera-rtp-stream-management" + }, + { + "iid": 64, + "type": "00000110-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 66, + "type": "000000B0-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 1, + "perms": ["pr", "pw", "ev", "tw"], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 67, + "type": "00000120-0000-1000-8000-0026BB765291", + "format": "tlv8", + "value": "AQEA", + "perms": ["pr", "ev"], + "ev": false + }, + { + "iid": 68, + "type": "00000114-0000-1000-8000-0026BB765291", + "format": "tlv8", + "value": "AYwBAQACEQEBAQIBAAAAAgECAwEABAEAAwsBAgAFAgLQAgMBHgAAAwsBAoACAgJoAQMBHgAAAwsBAuABAgIOAQMBHgAAAwsBAkABAgK0AAMBHgAAAwsBAgAEAgIAAwMBHgAAAwsBAoACAgLgAQMBHgAAAwsBAuABAgJoAQMBHgAAAwsBAkABAgLwAAMBHg==", + "perms": ["pr"], + "ev": false + }, + { + "iid": 69, + "type": "00000115-0000-1000-8000-0026BB765291", + "format": "tlv8", + "value": "AQ4BAQICCQEBAQIBAAMBAQIBAA==", + "perms": ["pr"], + "ev": false + }, + { + "iid": 70, + "type": "00000116-0000-1000-8000-0026BB765291", + "format": "tlv8", + "value": "AgEAAAACAQEAAAIBAg==", + "perms": ["pr"], + "ev": false + }, + { + "iid": 71, + "type": "00000118-0000-1000-8000-0026BB765291", + "format": "tlv8", + "value": "", + "perms": ["pr", "pw"], + "ev": false + }, + { + "iid": 72, + "type": "00000117-0000-1000-8000-0026BB765291", + "format": "tlv8", + "value": "", + "perms": ["pr", "pw"], + "ev": false + } + ], + "stype": "camera-rtp-stream-management" + }, + { + "iid": 128, + "type": "204", + "primary": false, + "hidden": false, + "linked": [112, 160], + "characteristics": [ + { + "iid": 130, + "type": "000000B0-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 0, + "perms": ["pr", "pw", "ev", "tw"], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 131, + "type": "205", + "format": "tlv8", + "value": "AQQAAAAAAggBAAAAAAAAAAMLAQEAAgYBBKAPAAA=", + "perms": ["pr", "ev"], + "ev": false + }, + { + "iid": 132, + "type": "206", + "format": "tlv8", + "value": "AaQBAQACCwEBAQIBAAAAAgECAwsBAoAHAgI4BAMBHgAAAwsBAgAFAgLQAgMBHgAAAwsBAoACAgJoAQMBHgAAAwsBAuABAgIOAQMBHgAAAwsBAkABAgK0AAMBHgAAAwsBAoAHAgI4BAMBDwAAAwsBAgAFAgLQAgMBDwAAAwsBAoACAgJoAQMBDwAAAwsBAuABAgIOAQMBDwAAAwsBAkABAgK0AAMBDw==", + "perms": ["pr", "ev"], + "ev": false + }, + { + "iid": 133, + "type": "207", + "format": "tlv8", + "value": "AQ4BAQECCQEBAQIBAAMBAQ==", + "perms": ["pr", "ev"], + "ev": false + }, + { + "iid": 134, + "type": "209", + "format": "tlv8", + "value": "AR0BBAAAAAACCAEAAAAAAAAAAwsBAQACBgEEoA8AAAIkAQEAAhIBAQECAQIDBNAHAAAEBKAPAAADCwECgAcCAjgEAwEeAxQBAQECDwEBAQIBAAMBAQQEEAAAAA==", + "perms": ["pr", "pw", "ev"], + "ev": false + }, + { + "iid": 135, + "type": "226", + "format": "uint8", + "value": 1, + "perms": ["pr", "pw", "ev", "tw"], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + } + ], + "stype": "Unknown Service: 204" + }, + { + "iid": 112, + "type": "00000129-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 114, + "type": "00000130-0000-1000-8000-0026BB765291", + "format": "tlv8", + "value": "AQMBAQA=", + "perms": ["pr"], + "ev": false + }, + { + "iid": 115, + "type": "00000131-0000-1000-8000-0026BB765291", + "format": "tlv8", + "value": "", + "perms": ["pr", "pw", "wr"], + "ev": false + }, + { + "iid": 116, + "type": "00000037-0000-1000-8000-0026BB765291", + "format": "string", + "value": "1.0", + "perms": ["pr"], + "ev": false + } + ], + "stype": "data-stream-transport-management" + }, + { + "iid": 144, + "type": "21A", + "primary": false, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 146, + "type": "223", + "format": "uint8", + "value": 1, + "perms": ["pr", "pw", "ev"], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 147, + "type": "225", + "format": "uint8", + "value": 1, + "perms": ["pr", "pw", "ev"], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 148, + "type": "21B", + "format": "uint8", + "value": 1, + "perms": ["pr", "pw", "ev"], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 149, + "type": "21C", + "format": "uint8", + "value": 1, + "perms": ["pr", "ev"], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 150, + "type": "21D", + "format": "uint8", + "value": 0, + "perms": ["pr", "pw", "ev"], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 152, + "type": "0000011B-0000-1000-8000-0026BB765291", + "format": "bool", + "value": 1, + "perms": ["pr", "pw", "ev"], + "ev": false + } + ], + "stype": "Unknown Service: 21A" + }, + { + "iid": 80, + "type": "00000112-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 82, + "type": "00000023-0000-1000-8000-0026BB765291", + "format": "string", + "value": "Microphone", + "perms": ["pr"], + "ev": false + }, + { + "iid": 83, + "type": "0000011A-0000-1000-8000-0026BB765291", + "format": "bool", + "value": 0, + "perms": ["pr", "pw", "ev"], + "ev": false + }, + { + "iid": 84, + "type": "00000119-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 100, + "perms": ["pr", "pw", "ev"], + "ev": false, + "unit": "percentage", + "minValue": 0, + "maxValue": 100, + "minStep": 1 + } + ], + "stype": "microphone" + }, + { + "iid": 160, + "type": "00000085-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 162, + "type": "00000022-0000-1000-8000-0026BB765291", + "format": "bool", + "value": 0, + "perms": ["pr", "ev"], + "ev": true + }, + { + "iid": 163, + "type": "00000023-0000-1000-8000-0026BB765291", + "format": "string", + "value": "Motion Sensor", + "perms": ["pr"], + "ev": false + }, + { + "iid": 164, + "type": "00000075-0000-1000-8000-0026BB765291", + "format": "bool", + "value": 1, + "perms": ["pr", "ev"], + "ev": false + } + ], + "stype": "motion" + }, + { + "iid": 101, + "type": "00000096-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 102, + "type": "00000068-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 38, + "perms": ["pr", "ev"], + "ev": true, + "unit": "percentage", + "minValue": 0, + "maxValue": 100, + "minStep": 1 + }, + { + "iid": 103, + "type": "0000008F-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 0, + "perms": ["pr", "ev"], + "ev": true, + "minValue": 0, + "maxValue": 2, + "minStep": 1 + }, + { + "iid": 104, + "type": "00000079-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 0, + "perms": ["pr", "ev"], + "ev": true, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + } + ], + "stype": "battery" + } + ] + }, + { + "aid": 3, + "services": [ + { + "iid": 1, + "type": "0000003E-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 2, + "type": "00000014-0000-1000-8000-0026BB765291", + "format": "bool", + "perms": ["pw"] + }, + { + "iid": 3, + "type": "00000020-0000-1000-8000-0026BB765291", + "format": "string", + "value": "Anker", + "perms": ["pr"], + "ev": false + }, + { + "iid": 4, + "type": "00000021-0000-1000-8000-0026BB765291", + "format": "string", + "value": "T8113", + "perms": ["pr"], + "ev": false + }, + { + "iid": 5, + "type": "00000023-0000-1000-8000-0026BB765291", + "format": "string", + "value": "eufyCam2-000A", + "perms": ["pr"], + "ev": false + }, + { + "iid": 6, + "type": "00000030-0000-1000-8000-0026BB765291", + "format": "string", + "value": "A0000A000000000C", + "perms": ["pr"], + "ev": false + }, + { + "iid": 7, + "type": "00000052-0000-1000-8000-0026BB765291", + "format": "string", + "value": "1.6.7", + "perms": ["pr"], + "ev": false + }, + { + "iid": 8, + "type": "00000053-0000-1000-8000-0026BB765291", + "format": "string", + "value": "1.0.0", + "perms": ["pr"], + "ev": false + } + ], + "stype": "accessory-information" + }, + { + "iid": 48, + "type": "00000110-0000-1000-8000-0026BB765291", + "primary": true, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 50, + "type": "000000B0-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 1, + "perms": ["pr", "pw", "ev", "tw"], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 51, + "type": "00000120-0000-1000-8000-0026BB765291", + "format": "tlv8", + "value": "AQEA", + "perms": ["pr", "ev"], + "ev": false + }, + { + "iid": 52, + "type": "00000114-0000-1000-8000-0026BB765291", + "format": "tlv8", + "value": "AaoBAQACEQEBAQIBAAAAAgECAwEABAEAAwsBAoAHAgI4BAMBHgAAAwsBAgAFAgLQAgMBHgAAAwsBAoACAgJoAQMBHgAAAwsBAuABAgIOAQMBHgAAAwsBAkABAgK0AAMBHgAAAwsBAgAFAgLAAwMBHgAAAwsBAgAEAgIAAwMBHgAAAwsBAoACAgLgAQMBHgAAAwsBAuABAgJoAQMBHgAAAwsBAkABAgLwAAMBHg==", + "perms": ["pr"], + "ev": false + }, + { + "iid": 53, + "type": "00000115-0000-1000-8000-0026BB765291", + "format": "tlv8", + "value": "AQ4BAQICCQEBAQIBAAMBAQIBAA==", + "perms": ["pr"], + "ev": false + }, + { + "iid": 54, + "type": "00000116-0000-1000-8000-0026BB765291", + "format": "tlv8", + "value": "AgEAAAACAQEAAAIBAg==", + "perms": ["pr"], + "ev": false + }, + { + "iid": 55, + "type": "00000118-0000-1000-8000-0026BB765291", + "format": "tlv8", + "value": "", + "perms": ["pr", "pw"], + "ev": false + }, + { + "iid": 56, + "type": "00000117-0000-1000-8000-0026BB765291", + "format": "tlv8", + "value": "", + "perms": ["pr", "pw"], + "ev": false + } + ], + "stype": "camera-rtp-stream-management" + }, + { + "iid": 64, + "type": "00000110-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 66, + "type": "000000B0-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 1, + "perms": ["pr", "pw", "ev", "tw"], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 67, + "type": "00000120-0000-1000-8000-0026BB765291", + "format": "tlv8", + "value": "AQEA", + "perms": ["pr", "ev"], + "ev": false + }, + { + "iid": 68, + "type": "00000114-0000-1000-8000-0026BB765291", + "format": "tlv8", + "value": "AYwBAQACEQEBAQIBAAAAAgECAwEABAEAAwsBAgAFAgLQAgMBHgAAAwsBAoACAgJoAQMBHgAAAwsBAuABAgIOAQMBHgAAAwsBAkABAgK0AAMBHgAAAwsBAgAEAgIAAwMBHgAAAwsBAoACAgLgAQMBHgAAAwsBAuABAgJoAQMBHgAAAwsBAkABAgLwAAMBHg==", + "perms": ["pr"], + "ev": false + }, + { + "iid": 69, + "type": "00000115-0000-1000-8000-0026BB765291", + "format": "tlv8", + "value": "AQ4BAQICCQEBAQIBAAMBAQIBAA==", + "perms": ["pr"], + "ev": false + }, + { + "iid": 70, + "type": "00000116-0000-1000-8000-0026BB765291", + "format": "tlv8", + "value": "AgEAAAACAQEAAAIBAg==", + "perms": ["pr"], + "ev": false + }, + { + "iid": 71, + "type": "00000118-0000-1000-8000-0026BB765291", + "format": "tlv8", + "value": "", + "perms": ["pr", "pw"], + "ev": false + }, + { + "iid": 72, + "type": "00000117-0000-1000-8000-0026BB765291", + "format": "tlv8", + "value": "", + "perms": ["pr", "pw"], + "ev": false + } + ], + "stype": "camera-rtp-stream-management" + }, + { + "iid": 128, + "type": "204", + "primary": false, + "hidden": false, + "linked": [112, 160], + "characteristics": [ + { + "iid": 130, + "type": "000000B0-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 0, + "perms": ["pr", "pw", "ev", "tw"], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 131, + "type": "205", + "format": "tlv8", + "value": "AQQAAAAAAggBAAAAAAAAAAMLAQEAAgYBBKAPAAA=", + "perms": ["pr", "ev"], + "ev": false + }, + { + "iid": 132, + "type": "206", + "format": "tlv8", + "value": "AaQBAQACCwEBAQIBAAAAAgECAwsBAoAHAgI4BAMBHgAAAwsBAgAFAgLQAgMBHgAAAwsBAoACAgJoAQMBHgAAAwsBAuABAgIOAQMBHgAAAwsBAkABAgK0AAMBHgAAAwsBAoAHAgI4BAMBDwAAAwsBAgAFAgLQAgMBDwAAAwsBAoACAgJoAQMBDwAAAwsBAuABAgIOAQMBDwAAAwsBAkABAgK0AAMBDw==", + "perms": ["pr", "ev"], + "ev": false + }, + { + "iid": 133, + "type": "207", + "format": "tlv8", + "value": "AQ4BAQECCQEBAQIBAAMBAQ==", + "perms": ["pr", "ev"], + "ev": false + }, + { + "iid": 134, + "type": "209", + "format": "tlv8", + "value": "", + "perms": ["pr", "pw", "ev"], + "ev": false + }, + { + "iid": 135, + "type": "226", + "format": "uint8", + "value": 1, + "perms": ["pr", "pw", "ev", "tw"], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + } + ], + "stype": "Unknown Service: 204" + }, + { + "iid": 112, + "type": "00000129-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 114, + "type": "00000130-0000-1000-8000-0026BB765291", + "format": "tlv8", + "value": "AQMBAQA=", + "perms": ["pr"], + "ev": false + }, + { + "iid": 115, + "type": "00000131-0000-1000-8000-0026BB765291", + "format": "tlv8", + "value": "", + "perms": ["pr", "pw", "wr"], + "ev": false + }, + { + "iid": 116, + "type": "00000037-0000-1000-8000-0026BB765291", + "format": "string", + "value": "1.0", + "perms": ["pr"], + "ev": false + } + ], + "stype": "data-stream-transport-management" + }, + { + "iid": 144, + "type": "21A", + "primary": false, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 146, + "type": "223", + "format": "uint8", + "value": 1, + "perms": ["pr", "pw", "ev"], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 147, + "type": "225", + "format": "uint8", + "value": 1, + "perms": ["pr", "pw", "ev"], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 148, + "type": "21B", + "format": "uint8", + "value": 1, + "perms": ["pr", "pw", "ev"], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 149, + "type": "21C", + "format": "uint8", + "value": 1, + "perms": ["pr", "ev"], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 150, + "type": "21D", + "format": "uint8", + "value": 0, + "perms": ["pr", "pw", "ev"], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 152, + "type": "0000011B-0000-1000-8000-0026BB765291", + "format": "bool", + "value": null, + "perms": ["pr", "pw", "ev"], + "ev": false + } + ], + "stype": "Unknown Service: 21A" + }, + { + "iid": 80, + "type": "00000112-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 82, + "type": "00000023-0000-1000-8000-0026BB765291", + "format": "string", + "value": "Microphone", + "perms": ["pr"], + "ev": false + }, + { + "iid": 83, + "type": "0000011A-0000-1000-8000-0026BB765291", + "format": "bool", + "value": null, + "perms": ["pr", "pw", "ev"], + "ev": false + }, + { + "iid": 84, + "type": "00000119-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": null, + "perms": ["pr", "pw", "ev"], + "ev": false, + "unit": "percentage", + "minValue": 0, + "maxValue": 100, + "minStep": 1 + } + ], + "stype": "microphone" + }, + { + "iid": 160, + "type": "00000085-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 162, + "type": "00000022-0000-1000-8000-0026BB765291", + "format": "bool", + "value": null, + "perms": ["pr", "ev"], + "ev": true + }, + { + "iid": 163, + "type": "00000023-0000-1000-8000-0026BB765291", + "format": "string", + "value": "Motion Sensor", + "perms": ["pr"], + "ev": false + }, + { + "iid": 164, + "type": "00000075-0000-1000-8000-0026BB765291", + "format": "bool", + "value": null, + "perms": ["pr", "ev"], + "ev": false + } + ], + "stype": "motion" + }, + { + "iid": 101, + "type": "00000096-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 102, + "type": "00000068-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": null, + "perms": ["pr", "ev"], + "ev": true, + "unit": "percentage", + "minValue": 0, + "maxValue": 100, + "minStep": 1 + }, + { + "iid": 103, + "type": "0000008F-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": null, + "perms": ["pr", "ev"], + "ev": true, + "minValue": 0, + "maxValue": 2, + "minStep": 1 + }, + { + "iid": 104, + "type": "00000079-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": null, + "perms": ["pr", "ev"], + "ev": true, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + } + ], + "stype": "battery" + } + ] + }, + { + "aid": 4, + "services": [ + { + "iid": 1, + "type": "0000003E-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 2, + "type": "00000014-0000-1000-8000-0026BB765291", + "format": "bool", + "perms": ["pw"] + }, + { + "iid": 3, + "type": "00000020-0000-1000-8000-0026BB765291", + "format": "string", + "value": "Anker", + "perms": ["pr"], + "ev": false + }, + { + "iid": 4, + "type": "00000021-0000-1000-8000-0026BB765291", + "format": "string", + "value": "T8113", + "perms": ["pr"], + "ev": false + }, + { + "iid": 5, + "type": "00000023-0000-1000-8000-0026BB765291", + "format": "string", + "value": "eufyCam2-0000", + "perms": ["pr"], + "ev": false + }, + { + "iid": 6, + "type": "00000030-0000-1000-8000-0026BB765291", + "format": "string", + "value": "A0000A000000000D", + "perms": ["pr"], + "ev": false + }, + { + "iid": 7, + "type": "00000052-0000-1000-8000-0026BB765291", + "format": "string", + "value": "1.6.7", + "perms": ["pr"], + "ev": false + }, + { + "iid": 8, + "type": "00000053-0000-1000-8000-0026BB765291", + "format": "string", + "value": "1.0.0", + "perms": ["pr"], + "ev": false + } + ], + "stype": "accessory-information" + }, + { + "iid": 48, + "type": "00000110-0000-1000-8000-0026BB765291", + "primary": true, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 50, + "type": "000000B0-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 1, + "perms": ["pr", "pw", "ev", "tw"], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 51, + "type": "00000120-0000-1000-8000-0026BB765291", + "format": "tlv8", + "value": "AQEA", + "perms": ["pr", "ev"], + "ev": false + }, + { + "iid": 52, + "type": "00000114-0000-1000-8000-0026BB765291", + "format": "tlv8", + "value": "AaoBAQACEQEBAQIBAAAAAgECAwEABAEAAwsBAoAHAgI4BAMBHgAAAwsBAgAFAgLQAgMBHgAAAwsBAoACAgJoAQMBHgAAAwsBAuABAgIOAQMBHgAAAwsBAkABAgK0AAMBHgAAAwsBAgAFAgLAAwMBHgAAAwsBAgAEAgIAAwMBHgAAAwsBAoACAgLgAQMBHgAAAwsBAuABAgJoAQMBHgAAAwsBAkABAgLwAAMBHg==", + "perms": ["pr"], + "ev": false + }, + { + "iid": 53, + "type": "00000115-0000-1000-8000-0026BB765291", + "format": "tlv8", + "value": "AQ4BAQICCQEBAQIBAAMBAQIBAA==", + "perms": ["pr"], + "ev": false + }, + { + "iid": 54, + "type": "00000116-0000-1000-8000-0026BB765291", + "format": "tlv8", + "value": "AgEAAAACAQEAAAIBAg==", + "perms": ["pr"], + "ev": false + }, + { + "iid": 55, + "type": "00000118-0000-1000-8000-0026BB765291", + "format": "tlv8", + "value": "", + "perms": ["pr", "pw"], + "ev": false + }, + { + "iid": 56, + "type": "00000117-0000-1000-8000-0026BB765291", + "format": "tlv8", + "value": "", + "perms": ["pr", "pw"], + "ev": false + } + ], + "stype": "camera-rtp-stream-management" + }, + { + "iid": 64, + "type": "00000110-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 66, + "type": "000000B0-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 1, + "perms": ["pr", "pw", "ev", "tw"], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 67, + "type": "00000120-0000-1000-8000-0026BB765291", + "format": "tlv8", + "value": "AQEA", + "perms": ["pr", "ev"], + "ev": false + }, + { + "iid": 68, + "type": "00000114-0000-1000-8000-0026BB765291", + "format": "tlv8", + "value": "AYwBAQACEQEBAQIBAAAAAgECAwEABAEAAwsBAgAFAgLQAgMBHgAAAwsBAoACAgJoAQMBHgAAAwsBAuABAgIOAQMBHgAAAwsBAkABAgK0AAMBHgAAAwsBAgAEAgIAAwMBHgAAAwsBAoACAgLgAQMBHgAAAwsBAuABAgJoAQMBHgAAAwsBAkABAgLwAAMBHg==", + "perms": ["pr"], + "ev": false + }, + { + "iid": 69, + "type": "00000115-0000-1000-8000-0026BB765291", + "format": "tlv8", + "value": "AQ4BAQICCQEBAQIBAAMBAQIBAA==", + "perms": ["pr"], + "ev": false + }, + { + "iid": 70, + "type": "00000116-0000-1000-8000-0026BB765291", + "format": "tlv8", + "value": "AgEAAAACAQEAAAIBAg==", + "perms": ["pr"], + "ev": false + }, + { + "iid": 71, + "type": "00000118-0000-1000-8000-0026BB765291", + "format": "tlv8", + "value": "", + "perms": ["pr", "pw"], + "ev": false + }, + { + "iid": 72, + "type": "00000117-0000-1000-8000-0026BB765291", + "format": "tlv8", + "value": "", + "perms": ["pr", "pw"], + "ev": false + } + ], + "stype": "camera-rtp-stream-management" + }, + { + "iid": 128, + "type": "204", + "primary": false, + "hidden": false, + "linked": [112, 160], + "characteristics": [ + { + "iid": 130, + "type": "000000B0-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 1, + "perms": ["pr", "pw", "ev", "tw"], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 131, + "type": "205", + "format": "tlv8", + "value": "AQQAAAAAAggBAAAAAAAAAAMLAQEAAgYBBKAPAAA=", + "perms": ["pr", "ev"], + "ev": false + }, + { + "iid": 132, + "type": "206", + "format": "tlv8", + "value": "AaQBAQACCwEBAQIBAAAAAgECAwsBAoAHAgI4BAMBHgAAAwsBAgAFAgLQAgMBHgAAAwsBAoACAgJoAQMBHgAAAwsBAuABAgIOAQMBHgAAAwsBAkABAgK0AAMBHgAAAwsBAoAHAgI4BAMBDwAAAwsBAgAFAgLQAgMBDwAAAwsBAoACAgJoAQMBDwAAAwsBAuABAgIOAQMBDwAAAwsBAkABAgK0AAMBDw==", + "perms": ["pr", "ev"], + "ev": false + }, + { + "iid": 133, + "type": "207", + "format": "tlv8", + "value": "AQ4BAQECCQEBAQIBAAMBAQ==", + "perms": ["pr", "ev"], + "ev": false + }, + { + "iid": 134, + "type": "209", + "format": "tlv8", + "value": "AR0BBAAAAAACCAEAAAAAAAAAAwsBAQACBgEEoA8AAAIkAQEAAhIBAQECAQIDBNAHAAAEBKAPAAADCwECgAcCAjgEAwEeAxQBAQECDwEBAQIBAAMBAQQEEAAAAA==", + "perms": ["pr", "pw", "ev"], + "ev": false + }, + { + "iid": 135, + "type": "226", + "format": "uint8", + "value": 1, + "perms": ["pr", "pw", "ev", "tw"], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + } + ], + "stype": "Unknown Service: 204" + }, + { + "iid": 112, + "type": "00000129-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 114, + "type": "00000130-0000-1000-8000-0026BB765291", + "format": "tlv8", + "value": "AQMBAQA=", + "perms": ["pr"], + "ev": false + }, + { + "iid": 115, + "type": "00000131-0000-1000-8000-0026BB765291", + "format": "tlv8", + "value": "", + "perms": ["pr", "pw", "wr"], + "ev": false + }, + { + "iid": 116, + "type": "00000037-0000-1000-8000-0026BB765291", + "format": "string", + "value": "1.0", + "perms": ["pr"], + "ev": false + } + ], + "stype": "data-stream-transport-management" + }, + { + "iid": 144, + "type": "21A", + "primary": false, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 146, + "type": "223", + "format": "uint8", + "value": 1, + "perms": ["pr", "pw", "ev"], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 147, + "type": "225", + "format": "uint8", + "value": 1, + "perms": ["pr", "pw", "ev"], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 148, + "type": "21B", + "format": "uint8", + "value": 1, + "perms": ["pr", "pw", "ev"], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 149, + "type": "21C", + "format": "uint8", + "value": 1, + "perms": ["pr", "ev"], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 150, + "type": "21D", + "format": "uint8", + "value": 0, + "perms": ["pr", "pw", "ev"], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 152, + "type": "0000011B-0000-1000-8000-0026BB765291", + "format": "bool", + "value": 1, + "perms": ["pr", "pw", "ev"], + "ev": false + } + ], + "stype": "Unknown Service: 21A" + }, + { + "iid": 80, + "type": "00000112-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 82, + "type": "00000023-0000-1000-8000-0026BB765291", + "format": "string", + "value": "Microphone", + "perms": ["pr"], + "ev": false + }, + { + "iid": 83, + "type": "0000011A-0000-1000-8000-0026BB765291", + "format": "bool", + "value": 0, + "perms": ["pr", "pw", "ev"], + "ev": false + }, + { + "iid": 84, + "type": "00000119-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 50, + "perms": ["pr", "pw", "ev"], + "ev": false, + "unit": "percentage", + "minValue": 0, + "maxValue": 100, + "minStep": 1 + } + ], + "stype": "microphone" + }, + { + "iid": 160, + "type": "00000085-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 162, + "type": "00000022-0000-1000-8000-0026BB765291", + "format": "bool", + "value": 0, + "perms": ["pr", "ev"], + "ev": true + }, + { + "iid": 163, + "type": "00000023-0000-1000-8000-0026BB765291", + "format": "string", + "value": "Motion Sensor", + "perms": ["pr"], + "ev": false + }, + { + "iid": 164, + "type": "00000075-0000-1000-8000-0026BB765291", + "format": "bool", + "value": 1, + "perms": ["pr", "ev"], + "ev": false + } + ], + "stype": "motion" + }, + { + "iid": 101, + "type": "00000096-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 102, + "type": "00000068-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 17, + "perms": ["pr", "ev"], + "ev": true, + "unit": "percentage", + "minValue": 0, + "maxValue": 100, + "minStep": 1 + }, + { + "iid": 103, + "type": "0000008F-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 0, + "perms": ["pr", "ev"], + "ev": true, + "minValue": 0, + "maxValue": 2, + "minStep": 1 + }, + { + "iid": 104, + "type": "00000079-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 0, + "perms": ["pr", "ev"], + "ev": true, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + } + ], + "stype": "battery" + } + ] + } +] diff --git a/tests/components/homekit_controller/fixtures/aqara_e1.json b/tests/components/homekit_controller/fixtures/aqara_e1.json index 8c8ff326bd6..9b4c3be441b 100644 --- a/tests/components/homekit_controller/fixtures/aqara_e1.json +++ b/tests/components/homekit_controller/fixtures/aqara_e1.json @@ -1,646 +1,493 @@ [ - { - "aid": 1, - "services": [ - { - "iid": 1, - "type": "0000003E-0000-1000-8000-0026BB765291", - "primary": false, - "hidden": false, - "characteristics": [ - { - "iid": 65537, - "type": "00000014-0000-1000-8000-0026BB765291", - "format": "bool", - "perms": [ - "pw" - ] - }, - { - "iid": 65538, - "type": "00000020-0000-1000-8000-0026BB765291", - "format": "string", - "value": "Aqara", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 65539, - "type": "00000021-0000-1000-8000-0026BB765291", - "format": "string", - "value": "HE1-G01", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 65540, - "type": "00000023-0000-1000-8000-0026BB765291", - "format": "string", - "value": "Aqara-Hub-E1-00A0", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 65541, - "type": "00000030-0000-1000-8000-0026BB765291", - "format": "string", - "value": "00aa00000a0", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 65542, - "type": "00000052-0000-1000-8000-0026BB765291", - "format": "string", - "value": "3.3.0", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 65543, - "type": "00000053-0000-1000-8000-0026BB765291", - "format": "string", - "value": "1.0", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 65544, - "type": "34AB8811-AC7F-4340-BAC3-FD6A85F9943B", - "format": "string", - "value": "5.0;dfeceb3a", - "perms": [ - "pr", - "hd" - ], - "ev": false - }, - { - "iid": 65545, - "type": "220", - "format": "data", - "value": "xDsGO4QdTEA=", - "perms": [ - "pr" - ], - "ev": false, - "maxDataLen": 8 - } - ] - }, - { - "iid": 2, - "type": "000000A2-0000-1000-8000-0026BB765291", - "primary": false, - "hidden": false, - "characteristics": [ - { - "iid": 131074, - "type": "00000037-0000-1000-8000-0026BB765291", - "format": "string", - "value": "1.1.0", - "perms": [ - "pr" - ], - "ev": false - } - ] - }, - { - "iid": 4, - "type": "22A", - "primary": false, - "hidden": false, - "characteristics": [ - { - "iid": 262145, - "type": "22B", - "format": "bool", - "value": 1, - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 262146, - "type": "22C", - "format": "uint32", - "value": 9, - "perms": [ - "pr" - ], - "ev": false, - "minValue": 0, - "maxValue": 15, - "minStep": 1 - }, - { - "iid": 262147, - "type": "22D", - "format": "tlv8", - "value": "", - "perms": [ - "pr", - "pw", - "ev", - "tw", - "wr" - ], - "ev": false - } - ] - }, - { - "iid": 16, - "type": "0000007E-0000-1000-8000-0026BB765291", - "primary": true, - "hidden": false, - "characteristics": [ - { - "iid": 1048578, - "type": "00000023-0000-1000-8000-0026BB765291", - "format": "string", - "value": "Security System", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 1048579, - "type": "00000066-0000-1000-8000-0026BB765291", - "format": "uint8", - "value": 3, - "perms": [ - "pr", - "ev" - ], - "ev": true, - "minValue": 0, - "maxValue": 4, - "minStep": 1, - "valid-values": [ - 0, - 1, - 2, - 3, - 4 - ] - }, - { - "iid": 1048580, - "type": "00000067-0000-1000-8000-0026BB765291", - "format": "uint8", - "value": 3, - "perms": [ - "pr", - "pw", - "ev" - ], - "ev": true, - "minValue": 0, - "maxValue": 3, - "minStep": 1, - "valid-values": [ - 0, - 1, - 2, - 3 - ] - }, - { - "iid": 1048581, - "type": "60CDDE6C-42B6-4C72-9719-AB2740EABE2A", - "format": "tlv8", - "value": "AAA=", - "perms": [ - "pr", - "pw" - ], - "ev": false, - "description": "Stay Arm Trigger Devices" - }, - { - "iid": 1048582, - "type": "4AB2460A-41E4-4F05-97C3-CCFDAE1BE324", - "format": "tlv8", - "value": "AAA=", - "perms": [ - "pr", - "pw" - ], - "ev": false, - "description": "Alarm Trigger Devices" - }, - { - "iid": 1048583, - "type": "F8296386-5A30-4AA7-838C-ED0DA9D807DF", - "format": "tlv8", - "value": "AAA=", - "perms": [ - "pr", - "pw" - ], - "ev": false, - "description": "Night Arm Trigger Devices" - } - ] - }, - { - "iid": 17, - "type": "9715BF53-AB63-4449-8DC7-2785D617390A", - "primary": false, - "hidden": true, - "characteristics": [ - { - "iid": 1114114, - "type": "00000023-0000-1000-8000-0026BB765291", - "format": "string", - "value": "Gateway", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 1114115, - "type": "4CB28907-66DF-4D9C-962C-9971ABF30EDC", - "format": "string", - "value": "1970-01-01 21:01:22+8", - "perms": [ - "pr", - "pw", - "hd" - ], - "ev": false, - "description": "Date and Time" - }, - { - "iid": 1114116, - "type": "EE56B186-B0D3-488E-8C79-C21FC9BCF437", - "format": "int", - "value": 40, - "perms": [ - "pr", - "pw", - "ev", - "hd" - ], - "ev": false, - "description": "Gateway Volume", - "unit": "percentage", - "minValue": 0, - "maxValue": 100, - "minStep": 1 - }, - { - "iid": 1114117, - "type": "B1C09E4C-E202-4827-B863-B0F32F727CFF", - "format": "bool", - "value": 0, - "perms": [ - "pr", - "pw", - "ev", - "hd" - ], - "ev": false, - "description": "New Accessory Permission" - }, - { - "iid": 1114118, - "type": "2CB22739-1E4C-4798-A761-BC2FAF51AFC3", - "format": "string", - "value": "", - "perms": [ - "pr", - "ev", - "hd" - ], - "ev": false, - "description": "Accessory Joined" - }, - { - "iid": 1114119, - "type": "75D19FA9-218B-4943-997E-341E5D1C60CC", - "format": "string", - "perms": [ - "pw", - "hd" - ], - "description": "Remove Accessory" - }, - { - "iid": 1114120, - "type": "7D943F6A-E052-4E96-A176-D17BF00E32CB", - "format": "int", - "value": -1, - "perms": [ - "pr", - "ev", - "hd" - ], - "ev": false, - "description": "Firmware Update Status", - "minValue": -65535, - "maxValue": 65535, - "minStep": 1 - }, - { - "iid": 1114121, - "type": "A45EFD52-0DB5-4C1A-9727-513FBCD8185F", - "format": "string", - "perms": [ - "pw", - "hd" - ], - "description": "Firmware Update URL", - "maxLen": 256 - }, - { - "iid": 1114122, - "type": "40F0124A-579D-40E4-865E-0EF6740EA64B", - "format": "string", - "perms": [ - "pw", - "hd" - ], - "description": "Firmware Update Checksum" - }, - { - "iid": 1114123, - "type": "E1C20B22-E3A7-4B92-8BA3-C16E778648A7", - "format": "string", - "value": "", - "perms": [ - "pr", - "ev", - "hd" - ], - "ev": false, - "description": "Identify Accessory" - }, - { - "iid": 1114124, - "type": "4CF1436A-755C-4377-BDB8-30BE29EB8620", - "format": "string", - "value": "Chinese", - "perms": [ - "pr", - "pw", - "ev", - "hd" - ], - "ev": false, - "description": "Language" - }, - { - "iid": 1114125, - "type": "25D889CB-7135-4A29-B5B4-C1FFD6D2DD5C", - "format": "string", - "value": "", - "perms": [ - "pr", - "pw", - "hd" - ], - "ev": false, - "description": "Country Domain" - }, - { - "iid": 1114126, - "type": "C7EECAA7-91D9-40EB-AD0C-FFDDE3143CB9", - "format": "string", - "value": "lumi1.00aa00000a0", - "perms": [ - "pr", - "hd" - ], - "ev": false, - "description": "Lumi Did" - }, - { - "iid": 1114127, - "type": "80FA747E-CB45-45A4-B7BE-AA7D9964859E", - "format": "string", - "perms": [ - "pw", - "hd" - ], - "description": "Lumi Bindkey" - }, - { - "iid": 1114128, - "type": "C3B8A329-EF0C-4739-B773-E5B7AEA52C71", - "format": "bool", - "value": 0, - "perms": [ - "pr", - "hd" - ], - "ev": false, - "description": "Lumi Bindstate" - } - ] - } + { + "aid": 1, + "services": [ + { + "iid": 1, + "type": "0000003E-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "characteristics": [ + { + "iid": 65537, + "type": "00000014-0000-1000-8000-0026BB765291", + "format": "bool", + "perms": ["pw"] + }, + { + "iid": 65538, + "type": "00000020-0000-1000-8000-0026BB765291", + "format": "string", + "value": "Aqara", + "perms": ["pr"], + "ev": false + }, + { + "iid": 65539, + "type": "00000021-0000-1000-8000-0026BB765291", + "format": "string", + "value": "HE1-G01", + "perms": ["pr"], + "ev": false + }, + { + "iid": 65540, + "type": "00000023-0000-1000-8000-0026BB765291", + "format": "string", + "value": "Aqara-Hub-E1-00A0", + "perms": ["pr"], + "ev": false + }, + { + "iid": 65541, + "type": "00000030-0000-1000-8000-0026BB765291", + "format": "string", + "value": "00aa00000a0", + "perms": ["pr"], + "ev": false + }, + { + "iid": 65542, + "type": "00000052-0000-1000-8000-0026BB765291", + "format": "string", + "value": "3.3.0", + "perms": ["pr"], + "ev": false + }, + { + "iid": 65543, + "type": "00000053-0000-1000-8000-0026BB765291", + "format": "string", + "value": "1.0", + "perms": ["pr"], + "ev": false + }, + { + "iid": 65544, + "type": "34AB8811-AC7F-4340-BAC3-FD6A85F9943B", + "format": "string", + "value": "5.0;dfeceb3a", + "perms": ["pr", "hd"], + "ev": false + }, + { + "iid": 65545, + "type": "220", + "format": "data", + "value": "xDsGO4QdTEA=", + "perms": ["pr"], + "ev": false, + "maxDataLen": 8 + } ] - }, - { - "aid": 33, - "services": [ - { - "iid": 1, - "type": "0000003E-0000-1000-8000-0026BB765291", - "primary": false, - "hidden": false, - "characteristics": [ - { - "iid": 65537, - "type": "00000014-0000-1000-8000-0026BB765291", - "format": "bool", - "perms": [ - "pw" - ] - }, - { - "iid": 65538, - "type": "00000020-0000-1000-8000-0026BB765291", - "format": "string", - "value": "Aqara", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 65539, - "type": "00000021-0000-1000-8000-0026BB765291", - "format": "string", - "value": "AS006", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 65540, - "type": "00000023-0000-1000-8000-0026BB765291", - "format": "string", - "value": "Contact Sensor", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 65541, - "type": "00000030-0000-1000-8000-0026BB765291", - "format": "string", - "value": "158d0007c59c6a", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 65542, - "type": "00000052-0000-1000-8000-0026BB765291", - "format": "string", - "value": "0", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 65543, - "type": "00000053-0000-1000-8000-0026BB765291", - "format": "string", - "value": "1.0", - "perms": [ - "pr" - ], - "ev": false - } - ] - }, - { - "iid": 4, - "type": "00000080-0000-1000-8000-0026BB765291", - "primary": true, - "hidden": false, - "characteristics": [ - { - "iid": 262146, - "type": "00000023-0000-1000-8000-0026BB765291", - "format": "string", - "value": "Contact Sensor", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 262147, - "type": "0000006A-0000-1000-8000-0026BB765291", - "format": "uint8", - "value": 0, - "perms": [ - "pr", - "ev" - ], - "ev": true, - "minValue": 0, - "maxValue": 1, - "minStep": 1, - "valid-values": [ - 0, - 1 - ] - } - ] - }, - { - "iid": 5, - "type": "00000096-0000-1000-8000-0026BB765291", - "primary": false, - "hidden": false, - "characteristics": [ - { - "iid": 327682, - "type": "00000023-0000-1000-8000-0026BB765291", - "format": "string", - "value": "Battery Sensor", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 327683, - "type": "00000068-0000-1000-8000-0026BB765291", - "format": "uint8", - "value": 100, - "perms": [ - "pr", - "ev" - ], - "ev": true, - "unit": "percentage", - "minValue": 0, - "maxValue": 100, - "minStep": 1 - }, - { - "iid": 327685, - "type": "00000079-0000-1000-8000-0026BB765291", - "format": "uint8", - "value": 0, - "perms": [ - "pr", - "ev" - ], - "ev": true, - "minValue": 0, - "maxValue": 1, - "minStep": 1, - "valid-values": [ - 0, - 1 - ] - }, - { - "iid": 327684, - "type": "0000008F-0000-1000-8000-0026BB765291", - "format": "uint8", - "value": 2, - "perms": [ - "pr", - "ev" - ], - "ev": true, - "minValue": 2, - "maxValue": 2, - "minStep": 1, - "valid-values": [ - 2 - ] - } - ] - } + }, + { + "iid": 2, + "type": "000000A2-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "characteristics": [ + { + "iid": 131074, + "type": "00000037-0000-1000-8000-0026BB765291", + "format": "string", + "value": "1.1.0", + "perms": ["pr"], + "ev": false + } ] - } + }, + { + "iid": 4, + "type": "22A", + "primary": false, + "hidden": false, + "characteristics": [ + { + "iid": 262145, + "type": "22B", + "format": "bool", + "value": 1, + "perms": ["pr"], + "ev": false + }, + { + "iid": 262146, + "type": "22C", + "format": "uint32", + "value": 9, + "perms": ["pr"], + "ev": false, + "minValue": 0, + "maxValue": 15, + "minStep": 1 + }, + { + "iid": 262147, + "type": "22D", + "format": "tlv8", + "value": "", + "perms": ["pr", "pw", "ev", "tw", "wr"], + "ev": false + } + ] + }, + { + "iid": 16, + "type": "0000007E-0000-1000-8000-0026BB765291", + "primary": true, + "hidden": false, + "characteristics": [ + { + "iid": 1048578, + "type": "00000023-0000-1000-8000-0026BB765291", + "format": "string", + "value": "Security System", + "perms": ["pr"], + "ev": false + }, + { + "iid": 1048579, + "type": "00000066-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 3, + "perms": ["pr", "ev"], + "ev": true, + "minValue": 0, + "maxValue": 4, + "minStep": 1, + "valid-values": [0, 1, 2, 3, 4] + }, + { + "iid": 1048580, + "type": "00000067-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 3, + "perms": ["pr", "pw", "ev"], + "ev": true, + "minValue": 0, + "maxValue": 3, + "minStep": 1, + "valid-values": [0, 1, 2, 3] + }, + { + "iid": 1048581, + "type": "60CDDE6C-42B6-4C72-9719-AB2740EABE2A", + "format": "tlv8", + "value": "AAA=", + "perms": ["pr", "pw"], + "ev": false, + "description": "Stay Arm Trigger Devices" + }, + { + "iid": 1048582, + "type": "4AB2460A-41E4-4F05-97C3-CCFDAE1BE324", + "format": "tlv8", + "value": "AAA=", + "perms": ["pr", "pw"], + "ev": false, + "description": "Alarm Trigger Devices" + }, + { + "iid": 1048583, + "type": "F8296386-5A30-4AA7-838C-ED0DA9D807DF", + "format": "tlv8", + "value": "AAA=", + "perms": ["pr", "pw"], + "ev": false, + "description": "Night Arm Trigger Devices" + } + ] + }, + { + "iid": 17, + "type": "9715BF53-AB63-4449-8DC7-2785D617390A", + "primary": false, + "hidden": true, + "characteristics": [ + { + "iid": 1114114, + "type": "00000023-0000-1000-8000-0026BB765291", + "format": "string", + "value": "Gateway", + "perms": ["pr"], + "ev": false + }, + { + "iid": 1114115, + "type": "4CB28907-66DF-4D9C-962C-9971ABF30EDC", + "format": "string", + "value": "1970-01-01 21:01:22+8", + "perms": ["pr", "pw", "hd"], + "ev": false, + "description": "Date and Time" + }, + { + "iid": 1114116, + "type": "EE56B186-B0D3-488E-8C79-C21FC9BCF437", + "format": "int", + "value": 40, + "perms": ["pr", "pw", "ev", "hd"], + "ev": false, + "description": "Gateway Volume", + "unit": "percentage", + "minValue": 0, + "maxValue": 100, + "minStep": 1 + }, + { + "iid": 1114117, + "type": "B1C09E4C-E202-4827-B863-B0F32F727CFF", + "format": "bool", + "value": 0, + "perms": ["pr", "pw", "ev", "hd"], + "ev": false, + "description": "New Accessory Permission" + }, + { + "iid": 1114118, + "type": "2CB22739-1E4C-4798-A761-BC2FAF51AFC3", + "format": "string", + "value": "", + "perms": ["pr", "ev", "hd"], + "ev": false, + "description": "Accessory Joined" + }, + { + "iid": 1114119, + "type": "75D19FA9-218B-4943-997E-341E5D1C60CC", + "format": "string", + "perms": ["pw", "hd"], + "description": "Remove Accessory" + }, + { + "iid": 1114120, + "type": "7D943F6A-E052-4E96-A176-D17BF00E32CB", + "format": "int", + "value": -1, + "perms": ["pr", "ev", "hd"], + "ev": false, + "description": "Firmware Update Status", + "minValue": -65535, + "maxValue": 65535, + "minStep": 1 + }, + { + "iid": 1114121, + "type": "A45EFD52-0DB5-4C1A-9727-513FBCD8185F", + "format": "string", + "perms": ["pw", "hd"], + "description": "Firmware Update URL", + "maxLen": 256 + }, + { + "iid": 1114122, + "type": "40F0124A-579D-40E4-865E-0EF6740EA64B", + "format": "string", + "perms": ["pw", "hd"], + "description": "Firmware Update Checksum" + }, + { + "iid": 1114123, + "type": "E1C20B22-E3A7-4B92-8BA3-C16E778648A7", + "format": "string", + "value": "", + "perms": ["pr", "ev", "hd"], + "ev": false, + "description": "Identify Accessory" + }, + { + "iid": 1114124, + "type": "4CF1436A-755C-4377-BDB8-30BE29EB8620", + "format": "string", + "value": "Chinese", + "perms": ["pr", "pw", "ev", "hd"], + "ev": false, + "description": "Language" + }, + { + "iid": 1114125, + "type": "25D889CB-7135-4A29-B5B4-C1FFD6D2DD5C", + "format": "string", + "value": "", + "perms": ["pr", "pw", "hd"], + "ev": false, + "description": "Country Domain" + }, + { + "iid": 1114126, + "type": "C7EECAA7-91D9-40EB-AD0C-FFDDE3143CB9", + "format": "string", + "value": "lumi1.00aa00000a0", + "perms": ["pr", "hd"], + "ev": false, + "description": "Lumi Did" + }, + { + "iid": 1114127, + "type": "80FA747E-CB45-45A4-B7BE-AA7D9964859E", + "format": "string", + "perms": ["pw", "hd"], + "description": "Lumi Bindkey" + }, + { + "iid": 1114128, + "type": "C3B8A329-EF0C-4739-B773-E5B7AEA52C71", + "format": "bool", + "value": 0, + "perms": ["pr", "hd"], + "ev": false, + "description": "Lumi Bindstate" + } + ] + } + ] + }, + { + "aid": 33, + "services": [ + { + "iid": 1, + "type": "0000003E-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "characteristics": [ + { + "iid": 65537, + "type": "00000014-0000-1000-8000-0026BB765291", + "format": "bool", + "perms": ["pw"] + }, + { + "iid": 65538, + "type": "00000020-0000-1000-8000-0026BB765291", + "format": "string", + "value": "Aqara", + "perms": ["pr"], + "ev": false + }, + { + "iid": 65539, + "type": "00000021-0000-1000-8000-0026BB765291", + "format": "string", + "value": "AS006", + "perms": ["pr"], + "ev": false + }, + { + "iid": 65540, + "type": "00000023-0000-1000-8000-0026BB765291", + "format": "string", + "value": "Contact Sensor", + "perms": ["pr"], + "ev": false + }, + { + "iid": 65541, + "type": "00000030-0000-1000-8000-0026BB765291", + "format": "string", + "value": "158d0007c59c6a", + "perms": ["pr"], + "ev": false + }, + { + "iid": 65542, + "type": "00000052-0000-1000-8000-0026BB765291", + "format": "string", + "value": "0", + "perms": ["pr"], + "ev": false + }, + { + "iid": 65543, + "type": "00000053-0000-1000-8000-0026BB765291", + "format": "string", + "value": "1.0", + "perms": ["pr"], + "ev": false + } + ] + }, + { + "iid": 4, + "type": "00000080-0000-1000-8000-0026BB765291", + "primary": true, + "hidden": false, + "characteristics": [ + { + "iid": 262146, + "type": "00000023-0000-1000-8000-0026BB765291", + "format": "string", + "value": "Contact Sensor", + "perms": ["pr"], + "ev": false + }, + { + "iid": 262147, + "type": "0000006A-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 0, + "perms": ["pr", "ev"], + "ev": true, + "minValue": 0, + "maxValue": 1, + "minStep": 1, + "valid-values": [0, 1] + } + ] + }, + { + "iid": 5, + "type": "00000096-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "characteristics": [ + { + "iid": 327682, + "type": "00000023-0000-1000-8000-0026BB765291", + "format": "string", + "value": "Battery Sensor", + "perms": ["pr"], + "ev": false + }, + { + "iid": 327683, + "type": "00000068-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 100, + "perms": ["pr", "ev"], + "ev": true, + "unit": "percentage", + "minValue": 0, + "maxValue": 100, + "minStep": 1 + }, + { + "iid": 327685, + "type": "00000079-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 0, + "perms": ["pr", "ev"], + "ev": true, + "minValue": 0, + "maxValue": 1, + "minStep": 1, + "valid-values": [0, 1] + }, + { + "iid": 327684, + "type": "0000008F-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 2, + "perms": ["pr", "ev"], + "ev": true, + "minValue": 2, + "maxValue": 2, + "minStep": 1, + "valid-values": [2] + } + ] + } + ] + } ] diff --git a/tests/components/homekit_controller/fixtures/aqara_gateway.json b/tests/components/homekit_controller/fixtures/aqara_gateway.json index 092936f3da5..c1228de1834 100644 --- a/tests/components/homekit_controller/fixtures/aqara_gateway.json +++ b/tests/components/homekit_controller/fixtures/aqara_gateway.json @@ -1,488 +1,362 @@ [ - { - "services": [ - { - "iid": 1, - "characteristics": [ - { - "value": "Aqara", - "description": "Manufacturer", - "type": "20", - "iid": 3, - "perms": [ - "pr" - ], - "format": "string" - }, - { - "value": "ZHWA11LM", - "description": "Model", - "type": "21", - "iid": 4, - "perms": [ - "pr" - ], - "format": "string" - }, - { - "value": "Aqara Hub-1563", - "description": "Name", - "type": "23", - "iid": 5, - "perms": [ - "pr" - ], - "format": "string" - }, - { - "value": "0000000123456789", - "description": "Serial Number", - "type": "30", - "iid": 6, - "perms": [ - "pr" - ], - "format": "string" - }, - { - "description": "Identify", - "iid": 7, - "perms": [ - "pw" - ], - "type": "14", - "format": "bool" - }, - { - "value": "1.4.7", - "description": "Firmware Revision", - "type": "52", - "iid": 8, - "perms": [ - "pr" - ], - "format": "string" - } - ], - "type": "3e" - }, - { - "iid": 60, - "characteristics": [ - { - "value": "1.1.0", - "description": "Protocol Version", - "type": "37", - "iid": 62, - "perms": [ - "pr" - ], - "format": "string" - } - ], - "type": "a2" - }, - { - "hidden": true, - "iid": 65536, - "characteristics": [ - { - "value": false, - "description": "New Accessory Permission", - "type": "b1c09e4c-e202-4827-b343-b0f32f727cff", - "iid": 65538, - "perms": [ - "pr", - "pw", - "ev", - "hd" - ], - "format": "bool" - }, - { - "value": "()", - "description": "Accessory Joined", - "type": "2cb22739-1e4c-4798-a712-bc2faf51afc3", - "maxLen": 256, - "iid": 65539, - "perms": [ - "pr", - "ev", - "hd" - ], - "format": "string" - }, - { - "value": " ", - "description": "Remove Accessory", - "type": "75d19fa9-218b-4943-427e-341e5d1c60cc", - "iid": 65540, - "perms": [ - "pr", - "pw", - "ev", - "hd" - ], - "format": "string" - }, - { - "maxValue": 100, - "value": 40, - "minValue": 0, - "description": "Gateway Volume", - "type": "ee56b186-b0d3-528e-8c79-c21fc9bcf437", - "unit": "percentage", - "iid": 65541, - "minStep": 1, - "perms": [ - "pr", - "pw" - ], - "format": "int" - }, - { - "value": "Chinese", - "description": "Language", - "type": "4cf1436a-755c-1277-bdb8-30be29eb8620", - "iid": 65542, - "perms": [ - "pr", - "pw" - ], - "format": "string" - }, - { - "value": "2019-02-12 06:45:07+10", - "description": "Date and Time", - "type": "4cb28907-66df-4d9c-924c-9971abf30edc", - "iid": 65543, - "perms": [ - "pr", - "pw" - ], - "format": "string" - }, - { - "value": " ", - "description": "Identify Accessory", - "type": "e1c20b22-e3a7-4b12-8ba3-c16e778648a7", - "iid": 65544, - "perms": [ - "pr", - "ev", - "hd" - ], - "format": "string" - }, - { - "value": "aiot-coap.aqara.cn", - "description": "Country Domain", - "type": "25d889cb-7135-4a21-b5b4-c1ffd6d2dd5c", - "iid": 65545, - "perms": [ - "pr", - "pw", - "hd" - ], - "format": "string" - }, - { - "value": -1, - "description": "Firmware Update Status", - "type": "7d943f6a-e052-4e96-a124-d17bf00e32cb", - "iid": 65546, - "perms": [ - "pr", - "ev", - "hd" - ], - "format": "int" - }, - { - "description": "Firmware Update Data", - "iid": 65547, - "perms": [ - "pw", - "hd" - ], - "type": "7f51dc43-dc68-4237-bae8-d705e61139f5", - "format": "data" - }, - { - "description": "Firmware Update URL", - "type": "a45efd52-0db5-4c1a-1227-513fbcd8185f", - "maxLen": 256, - "iid": 65548, - "perms": [ - "pw", - "hd" - ], - "format": "string" - }, - { - "description": "Firmware Update Checksum", - "iid": 65549, - "perms": [ - "pw", - "hd" - ], - "type": "40f0124a-579d-40e4-245e-0ef6740ea64b", - "format": "string" - } - ], - "type": "9715bf53-ab63-4449-8dc7-2485d617390a" - }, - { - "iid": 65792, - "characteristics": [ - { - "value": "Lightbulb-1563", - "description": "Name", - "type": "23", - "iid": 65794, - "perms": [ - "pr" - ], - "format": "string" - }, - { - "value": false, - "description": "On", - "type": "25", - "iid": 65795, - "perms": [ - "pr", - "pw", - "ev" - ], - "format": "bool" - }, - { - "maxValue": 360, - "value": 0, - "minValue": 0, - "description": "Hue", - "type": "13", - "unit": "arcdegrees", - "iid": 65796, - "minStep": 1, - "perms": [ - "pr", - "pw", - "ev" - ], - "format": "float" - }, - { - "maxValue": 100, - "value": 100, - "minValue": 0, - "description": "Saturation", - "type": "2f", - "unit": "percentage", - "iid": 65797, - "minStep": 1, - "perms": [ - "pr", - "pw", - "ev" - ], - "format": "float" - }, - { - "maxValue": 100, - "value": 0, - "minValue": 0, - "description": "Brightness", - "type": "8", - "unit": "percentage", - "iid": 65798, - "minStep": 1, - "perms": [ - "pr", - "pw", - "ev" - ], - "format": "int" - }, - { - "value": "", - "description": "Timers", - "type": "232aa6bd-6ce2-4d7f-b7cf-52305f0d2bcf", - "iid": 65799, - "perms": [ - "pr", - "pw", - "hd" - ], - "format": "tlv8" - } - ], - "type": "43" - }, - { - "iid": 66048, - "characteristics": [ - { - "value": "MIIO Service", - "description": "Name", - "type": "23", - "iid": 66050, - "perms": [ - "pr" - ], - "format": "string" - }, - { - "value": false, - "description": "miio provisioned", - "type": "6ef066c1-08f8-46de-9121-b89b77e459e7", - "iid": 66051, - "perms": [ - "pr", - "hd" - ], - "format": "bool" - }, - { - "description": "miio bindkey", - "iid": 66052, - "perms": [ - "pw", - "hd" - ], - "type": "6ef066c2-08f8-46de-9121-b89b77e459e7", - "format": "string" - }, - { - "value": "152601563", - "description": "miio did", - "type": "6ef066c5-08f8-46de-9121-b89b77e459e7", - "iid": 66053, - "perms": [ - "pr", - "hd" - ], - "format": "string" - }, - { - "value": "lumi.gateway.aqhm01", - "description": "miio model", - "type": "6ef066c4-08f8-46de-9121-b89b77e459e7", - "iid": 66054, - "perms": [ - "pr", - "hd" - ], - "format": "string" - }, - { - "value": "ch", - "description": "miio country domain", - "type": "6ef066c3-08f8-46de-9121-b89b77e459e7", - "iid": 66055, - "perms": [ - "pr", - "pw", - "hd" - ], - "format": "string" - }, - { - "value": "country code", - "description": "miio country code", - "type": "6ef066d1-08f8-46de-9121-b89b77e459e7", - "iid": 66056, - "perms": [ - "pr", - "pw", - "hd" - ], - "format": "string" - }, - { - "value": "app", - "description": "miio config type", - "type": "6ef066d3-08f8-46de-9121-b89b77e459e7", - "iid": 66057, - "perms": [ - "pr", - "pw", - "hd" - ], - "format": "string" - }, - { - "value": 28800, - "description": "miio gmt offset", - "type": "6ef066d2-08f8-46de-9121-b89b77e459e7", - "unit": "seconds", - "iid": 66058, - "perms": [ - "pr", - "pw", - "hd" - ], - "format": "int" - } - ], - "type": "6ef066c0-08f8-46de-9121-b89b77e459e7" - }, - { - "iid": 66304, - "characteristics": [ - { - "value": "Security System", - "description": "Name", - "type": "23", - "iid": 66306, - "perms": [ - "pr" - ], - "format": "string" - }, - { - "maxValue": 4, - "value": 3, - "minValue": 0, - "description": "Security System Current State", - "type": "66", - "valid-values": [ - 1, - 3, - 4 - ], - "iid": 66307, - "minStep": 1, - "perms": [ - "pr", - "ev" - ], - "format": "uint8" - }, - { - "maxValue": 3, - "value": 3, - "minValue": 0, - "description": "Security System Target State", - "type": "67", - "valid-values": [ - 1, - 3 - ], - "iid": 66308, - "minStep": 1, - "perms": [ - "pr", - "pw", - "ev" - ], - "format": "uint8" - } - ], - "type": "7e" - } + { + "services": [ + { + "iid": 1, + "characteristics": [ + { + "value": "Aqara", + "description": "Manufacturer", + "type": "20", + "iid": 3, + "perms": ["pr"], + "format": "string" + }, + { + "value": "ZHWA11LM", + "description": "Model", + "type": "21", + "iid": 4, + "perms": ["pr"], + "format": "string" + }, + { + "value": "Aqara Hub-1563", + "description": "Name", + "type": "23", + "iid": 5, + "perms": ["pr"], + "format": "string" + }, + { + "value": "0000000123456789", + "description": "Serial Number", + "type": "30", + "iid": 6, + "perms": ["pr"], + "format": "string" + }, + { + "description": "Identify", + "iid": 7, + "perms": ["pw"], + "type": "14", + "format": "bool" + }, + { + "value": "1.4.7", + "description": "Firmware Revision", + "type": "52", + "iid": 8, + "perms": ["pr"], + "format": "string" + } ], - "aid": 1 - } -] \ No newline at end of file + "type": "3e" + }, + { + "iid": 60, + "characteristics": [ + { + "value": "1.1.0", + "description": "Protocol Version", + "type": "37", + "iid": 62, + "perms": ["pr"], + "format": "string" + } + ], + "type": "a2" + }, + { + "hidden": true, + "iid": 65536, + "characteristics": [ + { + "value": false, + "description": "New Accessory Permission", + "type": "b1c09e4c-e202-4827-b343-b0f32f727cff", + "iid": 65538, + "perms": ["pr", "pw", "ev", "hd"], + "format": "bool" + }, + { + "value": "()", + "description": "Accessory Joined", + "type": "2cb22739-1e4c-4798-a712-bc2faf51afc3", + "maxLen": 256, + "iid": 65539, + "perms": ["pr", "ev", "hd"], + "format": "string" + }, + { + "value": " ", + "description": "Remove Accessory", + "type": "75d19fa9-218b-4943-427e-341e5d1c60cc", + "iid": 65540, + "perms": ["pr", "pw", "ev", "hd"], + "format": "string" + }, + { + "maxValue": 100, + "value": 40, + "minValue": 0, + "description": "Gateway Volume", + "type": "ee56b186-b0d3-528e-8c79-c21fc9bcf437", + "unit": "percentage", + "iid": 65541, + "minStep": 1, + "perms": ["pr", "pw"], + "format": "int" + }, + { + "value": "Chinese", + "description": "Language", + "type": "4cf1436a-755c-1277-bdb8-30be29eb8620", + "iid": 65542, + "perms": ["pr", "pw"], + "format": "string" + }, + { + "value": "2019-02-12 06:45:07+10", + "description": "Date and Time", + "type": "4cb28907-66df-4d9c-924c-9971abf30edc", + "iid": 65543, + "perms": ["pr", "pw"], + "format": "string" + }, + { + "value": " ", + "description": "Identify Accessory", + "type": "e1c20b22-e3a7-4b12-8ba3-c16e778648a7", + "iid": 65544, + "perms": ["pr", "ev", "hd"], + "format": "string" + }, + { + "value": "aiot-coap.aqara.cn", + "description": "Country Domain", + "type": "25d889cb-7135-4a21-b5b4-c1ffd6d2dd5c", + "iid": 65545, + "perms": ["pr", "pw", "hd"], + "format": "string" + }, + { + "value": -1, + "description": "Firmware Update Status", + "type": "7d943f6a-e052-4e96-a124-d17bf00e32cb", + "iid": 65546, + "perms": ["pr", "ev", "hd"], + "format": "int" + }, + { + "description": "Firmware Update Data", + "iid": 65547, + "perms": ["pw", "hd"], + "type": "7f51dc43-dc68-4237-bae8-d705e61139f5", + "format": "data" + }, + { + "description": "Firmware Update URL", + "type": "a45efd52-0db5-4c1a-1227-513fbcd8185f", + "maxLen": 256, + "iid": 65548, + "perms": ["pw", "hd"], + "format": "string" + }, + { + "description": "Firmware Update Checksum", + "iid": 65549, + "perms": ["pw", "hd"], + "type": "40f0124a-579d-40e4-245e-0ef6740ea64b", + "format": "string" + } + ], + "type": "9715bf53-ab63-4449-8dc7-2485d617390a" + }, + { + "iid": 65792, + "characteristics": [ + { + "value": "Lightbulb-1563", + "description": "Name", + "type": "23", + "iid": 65794, + "perms": ["pr"], + "format": "string" + }, + { + "value": false, + "description": "On", + "type": "25", + "iid": 65795, + "perms": ["pr", "pw", "ev"], + "format": "bool" + }, + { + "maxValue": 360, + "value": 0, + "minValue": 0, + "description": "Hue", + "type": "13", + "unit": "arcdegrees", + "iid": 65796, + "minStep": 1, + "perms": ["pr", "pw", "ev"], + "format": "float" + }, + { + "maxValue": 100, + "value": 100, + "minValue": 0, + "description": "Saturation", + "type": "2f", + "unit": "percentage", + "iid": 65797, + "minStep": 1, + "perms": ["pr", "pw", "ev"], + "format": "float" + }, + { + "maxValue": 100, + "value": 0, + "minValue": 0, + "description": "Brightness", + "type": "8", + "unit": "percentage", + "iid": 65798, + "minStep": 1, + "perms": ["pr", "pw", "ev"], + "format": "int" + }, + { + "value": "", + "description": "Timers", + "type": "232aa6bd-6ce2-4d7f-b7cf-52305f0d2bcf", + "iid": 65799, + "perms": ["pr", "pw", "hd"], + "format": "tlv8" + } + ], + "type": "43" + }, + { + "iid": 66048, + "characteristics": [ + { + "value": "MIIO Service", + "description": "Name", + "type": "23", + "iid": 66050, + "perms": ["pr"], + "format": "string" + }, + { + "value": false, + "description": "miio provisioned", + "type": "6ef066c1-08f8-46de-9121-b89b77e459e7", + "iid": 66051, + "perms": ["pr", "hd"], + "format": "bool" + }, + { + "description": "miio bindkey", + "iid": 66052, + "perms": ["pw", "hd"], + "type": "6ef066c2-08f8-46de-9121-b89b77e459e7", + "format": "string" + }, + { + "value": "152601563", + "description": "miio did", + "type": "6ef066c5-08f8-46de-9121-b89b77e459e7", + "iid": 66053, + "perms": ["pr", "hd"], + "format": "string" + }, + { + "value": "lumi.gateway.aqhm01", + "description": "miio model", + "type": "6ef066c4-08f8-46de-9121-b89b77e459e7", + "iid": 66054, + "perms": ["pr", "hd"], + "format": "string" + }, + { + "value": "ch", + "description": "miio country domain", + "type": "6ef066c3-08f8-46de-9121-b89b77e459e7", + "iid": 66055, + "perms": ["pr", "pw", "hd"], + "format": "string" + }, + { + "value": "country code", + "description": "miio country code", + "type": "6ef066d1-08f8-46de-9121-b89b77e459e7", + "iid": 66056, + "perms": ["pr", "pw", "hd"], + "format": "string" + }, + { + "value": "app", + "description": "miio config type", + "type": "6ef066d3-08f8-46de-9121-b89b77e459e7", + "iid": 66057, + "perms": ["pr", "pw", "hd"], + "format": "string" + }, + { + "value": 28800, + "description": "miio gmt offset", + "type": "6ef066d2-08f8-46de-9121-b89b77e459e7", + "unit": "seconds", + "iid": 66058, + "perms": ["pr", "pw", "hd"], + "format": "int" + } + ], + "type": "6ef066c0-08f8-46de-9121-b89b77e459e7" + }, + { + "iid": 66304, + "characteristics": [ + { + "value": "Security System", + "description": "Name", + "type": "23", + "iid": 66306, + "perms": ["pr"], + "format": "string" + }, + { + "maxValue": 4, + "value": 3, + "minValue": 0, + "description": "Security System Current State", + "type": "66", + "valid-values": [1, 3, 4], + "iid": 66307, + "minStep": 1, + "perms": ["pr", "ev"], + "format": "uint8" + }, + { + "maxValue": 3, + "value": 3, + "minValue": 0, + "description": "Security System Target State", + "type": "67", + "valid-values": [1, 3], + "iid": 66308, + "minStep": 1, + "perms": ["pr", "pw", "ev"], + "format": "uint8" + } + ], + "type": "7e" + } + ], + "aid": 1 + } +] diff --git a/tests/components/homekit_controller/fixtures/aqara_switch.json b/tests/components/homekit_controller/fixtures/aqara_switch.json index 320478343f4..aaa0bd30fda 100644 --- a/tests/components/homekit_controller/fixtures/aqara_switch.json +++ b/tests/components/homekit_controller/fixtures/aqara_switch.json @@ -1,209 +1,166 @@ [ - { - "aid": 1, - "services": [ - { - "characteristics": [ - { - "format": "bool", - "iid": 65537, - "perms": [ - "pw" - ], - "type": "00000014-0000-1000-8000-0026BB765291" - }, - { - "ev": false, - "format": "string", - "iid": 65538, - "perms": [ - "pr" - ], - "type": "00000020-0000-1000-8000-0026BB765291", - "value": "Aqara" - }, - { - "ev": false, - "format": "string", - "iid": 65539, - "perms": [ - "pr" - ], - "type": "00000021-0000-1000-8000-0026BB765291", - "value": "AR004" - }, - { - "ev": false, - "format": "string", - "iid": 65540, - "perms": [ - "pr" - ], - "type": "00000023-0000-1000-8000-0026BB765291", - "value": "Programmable Switch" - }, - { - "ev": false, - "format": "string", - "iid": 65541, - "perms": [ - "pr" - ], - "type": "00000030-0000-1000-8000-0026BB765291", - "value": "111a1111a1a111" - }, - { - "ev": false, - "format": "string", - "iid": 65542, - "perms": [ - "pr" - ], - "type": "00000052-0000-1000-8000-0026BB765291", - "value": "9" - }, - { - "ev": false, - "format": "string", - "iid": 65543, - "perms": [ - "pr" - ], - "type": "00000053-0000-1000-8000-0026BB765291", - "value": "1.0" - } - ], - "hidden": false, - "iid": 1, - "linked": [], - "primary": false, - "stype": "accessory-information", - "type": "0000003E-0000-1000-8000-0026BB765291" - }, - { - "characteristics": [ - { - "ev": false, - "format": "string", - "iid": 262146, - "perms": [ - "pr" - ], - "type": "00000023-0000-1000-8000-0026BB765291", - "value": "Programmable Switch" - }, - { - "ev": false, - "format": "uint8", - "iid": 262147, - "maxValue": 2, - "minStep": 1, - "minValue": 0, - "perms": [ - "pr", - "ev" - ], - "type": "00000073-0000-1000-8000-0026BB765291", - "valid-values": [ - 0, - 1, - 2 - ], - "value": null - }, - { - "ev": false, - "format": "uint8", - "iid": 262148, - "maxValue": 255, - "minStep": 1, - "minValue": 1, - "perms": [ - "pr" - ], - "type": "000000CB-0000-1000-8000-0026BB765291", - "value": 1 - } - ], - "hidden": false, - "iid": 4, - "linked": [], - "primary": true, - "stype": "stateless-programmable-switch", - "type": "00000089-0000-1000-8000-0026BB765291" - }, - { - "characteristics": [ - { - "ev": false, - "format": "string", - "iid": 327682, - "perms": [ - "pr" - ], - "type": "00000023-0000-1000-8000-0026BB765291", - "value": "Battery Sensor" - }, - { - "ev": true, - "format": "uint8", - "iid": 327683, - "maxValue": 100, - "minStep": 1, - "minValue": 0, - "perms": [ - "pr", - "ev" - ], - "type": "00000068-0000-1000-8000-0026BB765291", - "unit": "percentage", - "value": 100 - }, - { - "ev": true, - "format": "uint8", - "iid": 327685, - "maxValue": 1, - "minStep": 1, - "minValue": 0, - "perms": [ - "pr", - "ev" - ], - "type": "00000079-0000-1000-8000-0026BB765291", - "valid-values": [ - 0, - 1 - ], - "value": 0 - }, - { - "ev": true, - "format": "uint8", - "iid": 327684, - "maxValue": 2, - "minStep": 1, - "minValue": 0, - "perms": [ - "pr", - "ev" - ], - "type": "0000008F-0000-1000-8000-0026BB765291", - "valid-values": [ - 0, - 1, - 2 - ], - "value": 2 - } - ], - "hidden": false, - "iid": 5, - "linked": [], - "primary": false, - "stype": "battery", - "type": "00000096-0000-1000-8000-0026BB765291" - } - ] - } -] \ No newline at end of file + { + "aid": 1, + "services": [ + { + "characteristics": [ + { + "format": "bool", + "iid": 65537, + "perms": ["pw"], + "type": "00000014-0000-1000-8000-0026BB765291" + }, + { + "ev": false, + "format": "string", + "iid": 65538, + "perms": ["pr"], + "type": "00000020-0000-1000-8000-0026BB765291", + "value": "Aqara" + }, + { + "ev": false, + "format": "string", + "iid": 65539, + "perms": ["pr"], + "type": "00000021-0000-1000-8000-0026BB765291", + "value": "AR004" + }, + { + "ev": false, + "format": "string", + "iid": 65540, + "perms": ["pr"], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Programmable Switch" + }, + { + "ev": false, + "format": "string", + "iid": 65541, + "perms": ["pr"], + "type": "00000030-0000-1000-8000-0026BB765291", + "value": "111a1111a1a111" + }, + { + "ev": false, + "format": "string", + "iid": 65542, + "perms": ["pr"], + "type": "00000052-0000-1000-8000-0026BB765291", + "value": "9" + }, + { + "ev": false, + "format": "string", + "iid": 65543, + "perms": ["pr"], + "type": "00000053-0000-1000-8000-0026BB765291", + "value": "1.0" + } + ], + "hidden": false, + "iid": 1, + "linked": [], + "primary": false, + "stype": "accessory-information", + "type": "0000003E-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "ev": false, + "format": "string", + "iid": 262146, + "perms": ["pr"], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Programmable Switch" + }, + { + "ev": false, + "format": "uint8", + "iid": 262147, + "maxValue": 2, + "minStep": 1, + "minValue": 0, + "perms": ["pr", "ev"], + "type": "00000073-0000-1000-8000-0026BB765291", + "valid-values": [0, 1, 2], + "value": null + }, + { + "ev": false, + "format": "uint8", + "iid": 262148, + "maxValue": 255, + "minStep": 1, + "minValue": 1, + "perms": ["pr"], + "type": "000000CB-0000-1000-8000-0026BB765291", + "value": 1 + } + ], + "hidden": false, + "iid": 4, + "linked": [], + "primary": true, + "stype": "stateless-programmable-switch", + "type": "00000089-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "ev": false, + "format": "string", + "iid": 327682, + "perms": ["pr"], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Battery Sensor" + }, + { + "ev": true, + "format": "uint8", + "iid": 327683, + "maxValue": 100, + "minStep": 1, + "minValue": 0, + "perms": ["pr", "ev"], + "type": "00000068-0000-1000-8000-0026BB765291", + "unit": "percentage", + "value": 100 + }, + { + "ev": true, + "format": "uint8", + "iid": 327685, + "maxValue": 1, + "minStep": 1, + "minValue": 0, + "perms": ["pr", "ev"], + "type": "00000079-0000-1000-8000-0026BB765291", + "valid-values": [0, 1], + "value": 0 + }, + { + "ev": true, + "format": "uint8", + "iid": 327684, + "maxValue": 2, + "minStep": 1, + "minValue": 0, + "perms": ["pr", "ev"], + "type": "0000008F-0000-1000-8000-0026BB765291", + "valid-values": [0, 1, 2], + "value": 2 + } + ], + "hidden": false, + "iid": 5, + "linked": [], + "primary": false, + "stype": "battery", + "type": "00000096-0000-1000-8000-0026BB765291" + } + ] + } +] diff --git a/tests/components/homekit_controller/fixtures/arlo_baby.json b/tests/components/homekit_controller/fixtures/arlo_baby.json index 6a124a5f56f..7277f916be8 100644 --- a/tests/components/homekit_controller/fixtures/arlo_baby.json +++ b/tests/components/homekit_controller/fixtures/arlo_baby.json @@ -1,484 +1,381 @@ [ - { - "aid": 1, - "services": [ - { - "type": "0000003E-0000-1000-8000-0026BB765291", - "iid": 1, - "characteristics": [ - { - "type": "00000023-0000-1000-8000-0026BB765291", - "iid": 2, - "value": "ArloBabyA0", - "perms": [ - "pr" - ], - "format": "string" - }, - { - "type": "00000020-0000-1000-8000-0026BB765291", - "iid": 3, - "value": "Netgear, Inc", - "perms": [ - "pr" - ], - "format": "string" - }, - { - "type": "00000030-0000-1000-8000-0026BB765291", - "iid": 4, - "value": "00A0000000000", - "perms": [ - "pr" - ], - "format": "string" - }, - { - "type": "00000021-0000-1000-8000-0026BB765291", - "iid": 5, - "value": "ABC1000", - "perms": [ - "pr" - ], - "format": "string" - }, - { - "type": "00000052-0000-1000-8000-0026BB765291", - "iid": 7, - "value": "1.10.931", - "perms": [ - "pr" - ], - "format": "string" - }, - { - "type": "00000014-0000-1000-8000-0026BB765291", - "iid": 6, - "perms": [ - "pw" - ], - "format": "bool" - } - ] - }, - { - "type": "000000A2-0000-1000-8000-0026BB765291", - "iid": 20, - "characteristics": [ - { - "type": "00000037-0000-1000-8000-0026BB765291", - "iid": 21, - "value": "1.1.0", - "perms": [ - "pr" - ], - "format": "string" - } - ] - }, - { - "type": "00000110-0000-1000-8000-0026BB765291", - "iid": 100, - "characteristics": [ - { - "type": "00000120-0000-1000-8000-0026BB765291", - "iid": 106, - "value": "AQEB", - "perms": [ - "pr", - "ev" - ], - "format": "tlv8" - }, - { - "type": "00000114-0000-1000-8000-0026BB765291", - "iid": 101, - "value": "AY8BAQACFQEBAAEBAQEBAQIBAAMBAAQBAAUBAQMLAQKABwICOAQDAR4DCwECAAUCAsADAwEeAwsBAgAEAgIAAwMBHgMLAQIABQIC0AIDAR4DCwECgAICAmgBAwEeAwsBAuABAgIOAQMBHgMLAQKAAgIC4AEDAR4DCwEC4AECAmgBAwEeAwsBAkABAgLwAAMBHg==", - "perms": [ - "pr" - ], - "format": "tlv8" - }, - { - "type": "00000115-0000-1000-8000-0026BB765291", - "iid": 102, - "value": "AQ4BAQMCCQEBAQIBAAMBAQIBAA==", - "perms": [ - "pr" - ], - "format": "tlv8" - }, - { - "type": "00000116-0000-1000-8000-0026BB765291", - "iid": 103, - "value": "AgEAAgEBAgEC", - "perms": [ - "pr" - ], - "format": "tlv8" - }, - { - "type": "00000117-0000-1000-8000-0026BB765291", - "iid": 104, - "value": "", - "perms": [ - "pr", - "pw" - ], - "format": "tlv8" - }, - { - "type": "00000118-0000-1000-8000-0026BB765291", - "iid": 108, - "value": "", - "perms": [ - "pr", - "pw" - ], - "format": "tlv8" - } - ] - }, - { - "type": "00000110-0000-1000-8000-0026BB765291", - "iid": 110, - "characteristics": [ - { - "type": "00000120-0000-1000-8000-0026BB765291", - "iid": 116, - "value": "AQEA", - "perms": [ - "pr", - "ev" - ], - "format": "tlv8" - }, - { - "type": "00000114-0000-1000-8000-0026BB765291", - "iid": 111, - "value": "AWgBAQACFQEBAAEBAQEBAQIBAAMBAAQBAAUBAQMLAQIABQIC0AIDAR4DCwECgAICAmgBAwEeAwsBAuABAgIOAQMBHgMLAQKAAgIC4AEDAR4DCwEC4AECAmgBAwEeAwsBAkABAgLwAAMBHg==", - "perms": [ - "pr" - ], - "format": "tlv8" - }, - { - "type": "00000115-0000-1000-8000-0026BB765291", - "iid": 112, - "value": "AQ4BAQMCCQEBAQIBAAMBAQIBAA==", - "perms": [ - "pr" - ], - "format": "tlv8" - }, - { - "type": "00000116-0000-1000-8000-0026BB765291", - "iid": 113, - "value": "AgEAAgEBAgEC", - "perms": [ - "pr" - ], - "format": "tlv8" - }, - { - "type": "00000117-0000-1000-8000-0026BB765291", - "iid": 114, - "value": "", - "perms": [ - "pr", - "pw" - ], - "format": "tlv8" - }, - { - "type": "00000118-0000-1000-8000-0026BB765291", - "iid": 118, - "value": "", - "perms": [ - "pr", - "pw" - ], - "format": "tlv8" - } - ] - }, - { - "type": "00000112-0000-1000-8000-0026BB765291", - "iid": 300, - "characteristics": [ - { - "type": "0000011A-0000-1000-8000-0026BB765291", - "iid": 302, - "value": false, - "perms": [ - "pr", - "pw", - "ev" - ], - "format": "bool" - } - ] - }, - { - "type": "00000113-0000-1000-8000-0026BB765291", - "iid": 400, - "characteristics": [ - { - "type": "0000011A-0000-1000-8000-0026BB765291", - "iid": 402, - "value": false, - "perms": [ - "pr", - "pw", - "ev" - ], - "format": "bool" - }, - { - "type": "00000119-0000-1000-8000-0026BB765291", - "iid": 403, - "value": 50, - "perms": [ - "pr", - "pw", - "ev" - ], - "format": "uint8", - "minValue": 0, - "maxValue": 100, - "minStep": 1, - "unit": "percentage" - } - ] - }, - { - "type": "00000085-0000-1000-8000-0026BB765291", - "iid": 500, - "characteristics": [ - { - "type": "00000023-0000-1000-8000-0026BB765291", - "iid": 501, - "value": "Motion", - "perms": [ - "pr" - ], - "format": "string" - }, - { - "type": "00000022-0000-1000-8000-0026BB765291", - "iid": 502, - "value": false, - "perms": [ - "pr", - "ev" - ], - "format": "bool" - } - ] - }, - { - "type": "00000096-0000-1000-8000-0026BB765291", - "iid": 700, - "characteristics": [ - { - "type": "00000068-0000-1000-8000-0026BB765291", - "iid": 701, - "value": 82, - "perms": [ - "pr", - "ev" - ], - "format": "uint8", - "minValue": 0, - "maxValue": 100, - "minStep": 1, - "unit": "percentage" - }, - { - "type": "0000008F-0000-1000-8000-0026BB765291", - "iid": 702, - "value": 0, - "perms": [ - "pr", - "ev" - ], - "format": "uint8", - "minValue": 0, - "maxValue": 2, - "minStep": 1 - }, - { - "type": "00000079-0000-1000-8000-0026BB765291", - "iid": 703, - "value": 0, - "perms": [ - "pr", - "ev" - ], - "format": "uint8", - "minValue": 0, - "maxValue": 2, - "minStep": 1 - } - ] - }, - { - "type": "0000008D-0000-1000-8000-0026BB765291", - "iid": 800, - "characteristics": [ - { - "type": "00000023-0000-1000-8000-0026BB765291", - "iid": 801, - "value": "Air Quality", - "perms": [ - "pr" - ], - "format": "string" - }, - { - "type": "00000095-0000-1000-8000-0026BB765291", - "iid": 802, - "value": 1, - "perms": [ - "pr", - "ev" - ], - "format": "uint8", - "minValue": 0, - "maxValue": 5, - "minStep": 1 - } - ] - }, - { - "type": "00000082-0000-1000-8000-0026BB765291", - "iid": 900, - "characteristics": [ - { - "type": "00000023-0000-1000-8000-0026BB765291", - "iid": 901, - "value": "Humidity", - "perms": [ - "pr" - ], - "format": "string" - }, - { - "type": "00000010-0000-1000-8000-0026BB765291", - "iid": 902, - "value": 60.099998, - "perms": [ - "pr", - "ev" - ], - "format": "float", - "minValue": 0.0, - "maxValue": 100.0, - "minStep": 1.0, - "unit": "percentage" - } - ] - }, - { - "type": "0000008A-0000-1000-8000-0026BB765291", - "iid": 1000, - "characteristics": [ - { - "type": "00000023-0000-1000-8000-0026BB765291", - "iid": 1001, - "value": "Temperature", - "perms": [ - "pr" - ], - "format": "string" - }, - { - "type": "00000011-0000-1000-8000-0026BB765291", - "iid": 1002, - "value": 24.0, - "perms": [ - "pr", - "ev" - ], - "format": "float", - "minValue": 0.0, - "maxValue": 100.0, - "minStep": 0.1, - "unit": "celsius" - } - ] - }, - { - "type": "00000043-0000-1000-8000-0026BB765291", - "iid": 1100, - "characteristics": [ - { - "type": "00000023-0000-1000-8000-0026BB765291", - "iid": 1101, - "value": "Nightlight", - "perms": [ - "pr" - ], - "format": "string" - }, - { - "type": "00000025-0000-1000-8000-0026BB765291", - "iid": 1102, - "value": false, - "perms": [ - "pr", - "pw", - "ev" - ], - "format": "bool" - }, - { - "type": "00000008-0000-1000-8000-0026BB765291", - "iid": 1103, - "value": 100, - "perms": [ - "pr", - "pw", - "ev" - ], - "format": "int", - "minValue": 0, - "maxValue": 100, - "minStep": 1, - "unit": "percentage" - }, - { - "type": "00000013-0000-1000-8000-0026BB765291", - "iid": 1104, - "value": 0.0, - "perms": [ - "pr", - "pw", - "ev" - ], - "format": "float", - "minValue": 0.0, - "maxValue": 360.0, - "minStep": 1.0, - "unit": "arcdegrees" - }, - { - "type": "0000002F-0000-1000-8000-0026BB765291", - "iid": 1105, - "value": 0.0, - "perms": [ - "pr", - "pw", - "ev" - ], - "format": "float", - "minValue": 0.0, - "maxValue": 100.0, - "minStep": 1.0, - "unit": "percentage" - } - ] - } + { + "aid": 1, + "services": [ + { + "type": "0000003E-0000-1000-8000-0026BB765291", + "iid": 1, + "characteristics": [ + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 2, + "value": "ArloBabyA0", + "perms": ["pr"], + "format": "string" + }, + { + "type": "00000020-0000-1000-8000-0026BB765291", + "iid": 3, + "value": "Netgear, Inc", + "perms": ["pr"], + "format": "string" + }, + { + "type": "00000030-0000-1000-8000-0026BB765291", + "iid": 4, + "value": "00A0000000000", + "perms": ["pr"], + "format": "string" + }, + { + "type": "00000021-0000-1000-8000-0026BB765291", + "iid": 5, + "value": "ABC1000", + "perms": ["pr"], + "format": "string" + }, + { + "type": "00000052-0000-1000-8000-0026BB765291", + "iid": 7, + "value": "1.10.931", + "perms": ["pr"], + "format": "string" + }, + { + "type": "00000014-0000-1000-8000-0026BB765291", + "iid": 6, + "perms": ["pw"], + "format": "bool" + } ] - } -] \ No newline at end of file + }, + { + "type": "000000A2-0000-1000-8000-0026BB765291", + "iid": 20, + "characteristics": [ + { + "type": "00000037-0000-1000-8000-0026BB765291", + "iid": 21, + "value": "1.1.0", + "perms": ["pr"], + "format": "string" + } + ] + }, + { + "type": "00000110-0000-1000-8000-0026BB765291", + "iid": 100, + "characteristics": [ + { + "type": "00000120-0000-1000-8000-0026BB765291", + "iid": 106, + "value": "AQEB", + "perms": ["pr", "ev"], + "format": "tlv8" + }, + { + "type": "00000114-0000-1000-8000-0026BB765291", + "iid": 101, + "value": "AY8BAQACFQEBAAEBAQEBAQIBAAMBAAQBAAUBAQMLAQKABwICOAQDAR4DCwECAAUCAsADAwEeAwsBAgAEAgIAAwMBHgMLAQIABQIC0AIDAR4DCwECgAICAmgBAwEeAwsBAuABAgIOAQMBHgMLAQKAAgIC4AEDAR4DCwEC4AECAmgBAwEeAwsBAkABAgLwAAMBHg==", + "perms": ["pr"], + "format": "tlv8" + }, + { + "type": "00000115-0000-1000-8000-0026BB765291", + "iid": 102, + "value": "AQ4BAQMCCQEBAQIBAAMBAQIBAA==", + "perms": ["pr"], + "format": "tlv8" + }, + { + "type": "00000116-0000-1000-8000-0026BB765291", + "iid": 103, + "value": "AgEAAgEBAgEC", + "perms": ["pr"], + "format": "tlv8" + }, + { + "type": "00000117-0000-1000-8000-0026BB765291", + "iid": 104, + "value": "", + "perms": ["pr", "pw"], + "format": "tlv8" + }, + { + "type": "00000118-0000-1000-8000-0026BB765291", + "iid": 108, + "value": "", + "perms": ["pr", "pw"], + "format": "tlv8" + } + ] + }, + { + "type": "00000110-0000-1000-8000-0026BB765291", + "iid": 110, + "characteristics": [ + { + "type": "00000120-0000-1000-8000-0026BB765291", + "iid": 116, + "value": "AQEA", + "perms": ["pr", "ev"], + "format": "tlv8" + }, + { + "type": "00000114-0000-1000-8000-0026BB765291", + "iid": 111, + "value": "AWgBAQACFQEBAAEBAQEBAQIBAAMBAAQBAAUBAQMLAQIABQIC0AIDAR4DCwECgAICAmgBAwEeAwsBAuABAgIOAQMBHgMLAQKAAgIC4AEDAR4DCwEC4AECAmgBAwEeAwsBAkABAgLwAAMBHg==", + "perms": ["pr"], + "format": "tlv8" + }, + { + "type": "00000115-0000-1000-8000-0026BB765291", + "iid": 112, + "value": "AQ4BAQMCCQEBAQIBAAMBAQIBAA==", + "perms": ["pr"], + "format": "tlv8" + }, + { + "type": "00000116-0000-1000-8000-0026BB765291", + "iid": 113, + "value": "AgEAAgEBAgEC", + "perms": ["pr"], + "format": "tlv8" + }, + { + "type": "00000117-0000-1000-8000-0026BB765291", + "iid": 114, + "value": "", + "perms": ["pr", "pw"], + "format": "tlv8" + }, + { + "type": "00000118-0000-1000-8000-0026BB765291", + "iid": 118, + "value": "", + "perms": ["pr", "pw"], + "format": "tlv8" + } + ] + }, + { + "type": "00000112-0000-1000-8000-0026BB765291", + "iid": 300, + "characteristics": [ + { + "type": "0000011A-0000-1000-8000-0026BB765291", + "iid": 302, + "value": false, + "perms": ["pr", "pw", "ev"], + "format": "bool" + } + ] + }, + { + "type": "00000113-0000-1000-8000-0026BB765291", + "iid": 400, + "characteristics": [ + { + "type": "0000011A-0000-1000-8000-0026BB765291", + "iid": 402, + "value": false, + "perms": ["pr", "pw", "ev"], + "format": "bool" + }, + { + "type": "00000119-0000-1000-8000-0026BB765291", + "iid": 403, + "value": 50, + "perms": ["pr", "pw", "ev"], + "format": "uint8", + "minValue": 0, + "maxValue": 100, + "minStep": 1, + "unit": "percentage" + } + ] + }, + { + "type": "00000085-0000-1000-8000-0026BB765291", + "iid": 500, + "characteristics": [ + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 501, + "value": "Motion", + "perms": ["pr"], + "format": "string" + }, + { + "type": "00000022-0000-1000-8000-0026BB765291", + "iid": 502, + "value": false, + "perms": ["pr", "ev"], + "format": "bool" + } + ] + }, + { + "type": "00000096-0000-1000-8000-0026BB765291", + "iid": 700, + "characteristics": [ + { + "type": "00000068-0000-1000-8000-0026BB765291", + "iid": 701, + "value": 82, + "perms": ["pr", "ev"], + "format": "uint8", + "minValue": 0, + "maxValue": 100, + "minStep": 1, + "unit": "percentage" + }, + { + "type": "0000008F-0000-1000-8000-0026BB765291", + "iid": 702, + "value": 0, + "perms": ["pr", "ev"], + "format": "uint8", + "minValue": 0, + "maxValue": 2, + "minStep": 1 + }, + { + "type": "00000079-0000-1000-8000-0026BB765291", + "iid": 703, + "value": 0, + "perms": ["pr", "ev"], + "format": "uint8", + "minValue": 0, + "maxValue": 2, + "minStep": 1 + } + ] + }, + { + "type": "0000008D-0000-1000-8000-0026BB765291", + "iid": 800, + "characteristics": [ + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 801, + "value": "Air Quality", + "perms": ["pr"], + "format": "string" + }, + { + "type": "00000095-0000-1000-8000-0026BB765291", + "iid": 802, + "value": 1, + "perms": ["pr", "ev"], + "format": "uint8", + "minValue": 0, + "maxValue": 5, + "minStep": 1 + } + ] + }, + { + "type": "00000082-0000-1000-8000-0026BB765291", + "iid": 900, + "characteristics": [ + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 901, + "value": "Humidity", + "perms": ["pr"], + "format": "string" + }, + { + "type": "00000010-0000-1000-8000-0026BB765291", + "iid": 902, + "value": 60.099998, + "perms": ["pr", "ev"], + "format": "float", + "minValue": 0.0, + "maxValue": 100.0, + "minStep": 1.0, + "unit": "percentage" + } + ] + }, + { + "type": "0000008A-0000-1000-8000-0026BB765291", + "iid": 1000, + "characteristics": [ + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 1001, + "value": "Temperature", + "perms": ["pr"], + "format": "string" + }, + { + "type": "00000011-0000-1000-8000-0026BB765291", + "iid": 1002, + "value": 24.0, + "perms": ["pr", "ev"], + "format": "float", + "minValue": 0.0, + "maxValue": 100.0, + "minStep": 0.1, + "unit": "celsius" + } + ] + }, + { + "type": "00000043-0000-1000-8000-0026BB765291", + "iid": 1100, + "characteristics": [ + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 1101, + "value": "Nightlight", + "perms": ["pr"], + "format": "string" + }, + { + "type": "00000025-0000-1000-8000-0026BB765291", + "iid": 1102, + "value": false, + "perms": ["pr", "pw", "ev"], + "format": "bool" + }, + { + "type": "00000008-0000-1000-8000-0026BB765291", + "iid": 1103, + "value": 100, + "perms": ["pr", "pw", "ev"], + "format": "int", + "minValue": 0, + "maxValue": 100, + "minStep": 1, + "unit": "percentage" + }, + { + "type": "00000013-0000-1000-8000-0026BB765291", + "iid": 1104, + "value": 0.0, + "perms": ["pr", "pw", "ev"], + "format": "float", + "minValue": 0.0, + "maxValue": 360.0, + "minStep": 1.0, + "unit": "arcdegrees" + }, + { + "type": "0000002F-0000-1000-8000-0026BB765291", + "iid": 1105, + "value": 0.0, + "perms": ["pr", "pw", "ev"], + "format": "float", + "minValue": 0.0, + "maxValue": 100.0, + "minStep": 1.0, + "unit": "percentage" + } + ] + } + ] + } +] diff --git a/tests/components/homekit_controller/fixtures/connectsense.json b/tests/components/homekit_controller/fixtures/connectsense.json index a2ea1c17cb0..976e07b5ca3 100644 --- a/tests/components/homekit_controller/fixtures/connectsense.json +++ b/tests/components/homekit_controller/fixtures/connectsense.json @@ -1,476 +1,378 @@ [ - { - "aid": 1, - "services": [ - { - "iid": 1, - "type": "0000003E-0000-1000-8000-0026BB765291", - "characteristics": [ - { - "iid": 2, - "type": "00000014-0000-1000-8000-0026BB765291", - "perms": [ - "pw" - ], - "ev": false, - "format": "bool" - }, - { - "iid": 3, - "value": "ConnectSense", - "type": "00000020-0000-1000-8000-0026BB765291", - "perms": [ - "pr" - ], - "ev": false, - "format": "string" - }, - { - "iid": 4, - "value": "CS-IWO", - "type": "00000021-0000-1000-8000-0026BB765291", - "perms": [ - "pr" - ], - "ev": false, - "format": "string" - }, - { - "iid": 5, - "value": "InWall Outlet-0394DE", - "type": "00000023-0000-1000-8000-0026BB765291", - "perms": [ - "pr" - ], - "ev": false, - "format": "string" - }, - { - "iid": 6, - "value": "1020301376", - "type": "00000030-0000-1000-8000-0026BB765291", - "perms": [ - "pr" - ], - "ev": false, - "format": "string" - }, - { - "iid": 7, - "value": "1.0.0", - "type": "00000052-0000-1000-8000-0026BB765291", - "perms": [ - "pr" - ], - "ev": false, - "format": "string" - } - ] - }, - { - "iid": 8, - "type": "000000A2-0000-1000-8000-0026BB765291", - "characteristics": [ - { - "iid": 9, - "value": "1.1.0", - "type": "00000037-0000-1000-8000-0026BB765291", - "perms": [ - "pr" - ], - "ev": false, - "format": "string" - } - ] - }, - { - "iid": 10, - "type": "B3BD50B1-B30B-4974-A71F-5C68AA126698", - "hidden": true, - "characteristics": [ - { - "iid": 11, - "value": 100, - "type": "00000008-0000-1000-8000-0026BB765291", - "perms": [ - "pr", - "pw", - "ev" - ], - "ev": false, - "format": "int", - "minValue": 0, - "maxValue": 100, - "minStep": 1, - "unit": "percentage" - }, - { - "iid": 12, - "value": 7250712, - "type": "00000005-0000-1000-8000-001D4B474349", - "perms": [ - "pr", - "ev" - ], - "ev": false, - "format": "uint32", - "description": "Epoch", - "unit": "seconds" - } - ] - }, - { - "iid": 13, - "type": "00000047-0000-1000-8000-0026BB765291", - "characteristics": [ - { - "iid": 14, - "value": true, - "type": "00000025-0000-1000-8000-0026BB765291", - "perms": [ - "pr", - "pw", - "ev" - ], - "ev": true, - "format": "bool" - }, - { - "iid": 15, - "value": true, - "type": "00000026-0000-1000-8000-0026BB765291", - "perms": [ - "pr", - "ev" - ], - "ev": true, - "format": "bool" - }, - { - "iid": 16, - "value": "Outlet A", - "type": "00000023-0000-1000-8000-0026BB765291", - "perms": [ - "pr" - ], - "ev": false, - "format": "string" - }, - { - "iid": 17, - "value": 126.3, - "type": "00000008-0000-1000-8000-001D4B474349", - "perms": [ - "pr", - "ev" - ], - "ev": false, - "format": "float", - "minValue": 0.0, - "maxValue": 130.0, - "minStep": 0.1, - "description": "Volts" - }, - { - "iid": 18, - "value": 0.03, - "type": "00000009-0000-1000-8000-001D4B474349", - "perms": [ - "pr", - "ev" - ], - "ev": false, - "format": "float", - "minValue": 0.0, - "maxValue": 20.0, - "minStep": 0.1, - "description": "Amps" - }, - { - "iid": 19, - "value": 0.8, - "type": "0000000A-0000-1000-8000-001D4B474349", - "perms": [ - "pr", - "ev" - ], - "ev": false, - "format": "float", - "description": "Watts" - }, - { - "iid": 20, - "value": 379.69299, - "type": "0000000C-0000-1000-8000-001D4B474349", - "perms": [ - "pr", - "ev" - ], - "ev": false, - "format": "float", - "minValue": 0.0, - "maxValue": 34028234663852885981170418348, - "minStep": 0.1, - "description": "Kilowatt-hours" - }, - { - "iid": 21, - "value": 22, - "type": "0000000B-0000-1000-8000-001D4B474349", - "perms": [ - "pr", - "ev" - ], - "ev": false, - "format": "int", - "minValue": 0, - "maxValue": 100, - "minStep": 1, - "description": "Power factor" - }, - { - "iid": 22, - "value": 390, - "type": "00000005-0000-1000-8000-001D4B474349", - "perms": [ - "pr", - "ev" - ], - "ev": false, - "format": "uint32", - "description": "State timer", - "unit": "seconds" - }, - { - "iid": 23, - "value": "Outlet A", - "type": "0000000E-0000-1000-8000-001D4B474349", - "perms": [ - "pr", - "ev" - ], - "ev": false, - "format": "string", - "description": "Assigned name" - }, - { - "iid": 24, - "value": 0, - "type": "0000000F-0000-1000-8000-001D4B474349", - "perms": [ - "pr", - "ev" - ], - "ev": false, - "format": "uint32", - "description": "Device type" - } - ] - }, - { - "iid": 25, - "type": "00000047-0000-1000-8000-0026BB765291", - "characteristics": [ - { - "iid": 26, - "value": true, - "type": "00000025-0000-1000-8000-0026BB765291", - "perms": [ - "pr", - "pw", - "ev" - ], - "ev": true, - "format": "bool" - }, - { - "iid": 27, - "value": true, - "type": "00000026-0000-1000-8000-0026BB765291", - "perms": [ - "pr", - "ev" - ], - "ev": true, - "format": "bool" - }, - { - "iid": 28, - "value": "Outlet B", - "type": "00000023-0000-1000-8000-0026BB765291", - "perms": [ - "pr" - ], - "ev": false, - "format": "string" - }, - { - "iid": 29, - "value": 126.3, - "type": "00000008-0000-1000-8000-001D4B474349", - "perms": [ - "pr", - "ev" - ], - "ev": false, - "format": "float", - "minValue": 0.0, - "maxValue": 130.0, - "minStep": 0.1, - "description": "Volts" - }, - { - "iid": 30, - "value": 0.05, - "type": "00000009-0000-1000-8000-001D4B474349", - "perms": [ - "pr", - "ev" - ], - "ev": false, - "format": "float", - "minValue": 0.0, - "maxValue": 20.0, - "minStep": 0.1, - "description": "Amps" - }, - { - "iid": 31, - "value": 0.8, - "type": "0000000A-0000-1000-8000-001D4B474349", - "perms": [ - "pr", - "ev" - ], - "ev": false, - "format": "float", - "description": "Watts" - }, - { - "iid": 32, - "value": 175.85001, - "type": "0000000C-0000-1000-8000-001D4B474349", - "perms": [ - "pr", - "ev" - ], - "ev": false, - "format": "float", - "minValue": 0.0, - "maxValue": 34028234663852885981170418348, - "minStep": 0.1, - "description": "Kilowatt-hours" - }, - { - "iid": 33, - "value": 13, - "type": "0000000B-0000-1000-8000-001D4B474349", - "perms": [ - "pr", - "ev" - ], - "ev": false, - "format": "int", - "minValue": 0, - "maxValue": 100, - "minStep": 1, - "description": "Power factor" - }, - { - "iid": 34, - "value": 390, - "type": "00000005-0000-1000-8000-001D4B474349", - "perms": [ - "pr", - "ev" - ], - "ev": false, - "format": "uint32", - "description": "State timer", - "unit": "seconds" - }, - { - "iid": 35, - "value": "Outlet B", - "type": "0000000E-0000-1000-8000-001D4B474349", - "perms": [ - "pr", - "ev" - ], - "ev": false, - "format": "string", - "description": "Assigned name" - }, - { - "iid": 36, - "value": 0, - "type": "0000000F-0000-1000-8000-001D4B474349", - "perms": [ - "pr", - "ev" - ], - "ev": false, - "format": "uint32", - "description": "Device type" - } - ] - }, - { - "iid": 37, - "type": "00000020-0000-1000-8000-001D4B474349", - "hidden": true, - "characteristics": [ - { - "iid": 38, - "value": 0, - "type": "00000021-0000-1000-8000-001D4B474349", - "perms": [ - "pr", - "ev" - ], - "ev": false, - "format": "uint8" - }, - { - "iid": 39, - "type": "00000022-0000-1000-8000-001D4B474349", - "perms": [ - "pw" - ], - "ev": false, - "format": "uint8" - }, - { - "iid": 40, - "type": "00000023-0000-1000-8000-001D4B474349", - "perms": [ - "pw" - ], - "ev": false, - "format": "string", - "maxLen": 256 - }, - { - "iid": 41, - "type": "00000024-0000-1000-8000-001D4B474349", - "perms": [ - "pw" - ], - "ev": false, - "format": "bool" - }, - { - "iid": 42, - "type": "00000300-0000-1000-8000-001D4B474349", - "perms": [ - "pw" - ], - "ev": false, - "format": "string", - "maxLen": 65 - } - ] - } - ] - } + { + "aid": 1, + "services": [ + { + "iid": 1, + "type": "0000003E-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "iid": 2, + "type": "00000014-0000-1000-8000-0026BB765291", + "perms": ["pw"], + "ev": false, + "format": "bool" + }, + { + "iid": 3, + "value": "ConnectSense", + "type": "00000020-0000-1000-8000-0026BB765291", + "perms": ["pr"], + "ev": false, + "format": "string" + }, + { + "iid": 4, + "value": "CS-IWO", + "type": "00000021-0000-1000-8000-0026BB765291", + "perms": ["pr"], + "ev": false, + "format": "string" + }, + { + "iid": 5, + "value": "InWall Outlet-0394DE", + "type": "00000023-0000-1000-8000-0026BB765291", + "perms": ["pr"], + "ev": false, + "format": "string" + }, + { + "iid": 6, + "value": "1020301376", + "type": "00000030-0000-1000-8000-0026BB765291", + "perms": ["pr"], + "ev": false, + "format": "string" + }, + { + "iid": 7, + "value": "1.0.0", + "type": "00000052-0000-1000-8000-0026BB765291", + "perms": ["pr"], + "ev": false, + "format": "string" + } + ] + }, + { + "iid": 8, + "type": "000000A2-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "iid": 9, + "value": "1.1.0", + "type": "00000037-0000-1000-8000-0026BB765291", + "perms": ["pr"], + "ev": false, + "format": "string" + } + ] + }, + { + "iid": 10, + "type": "B3BD50B1-B30B-4974-A71F-5C68AA126698", + "hidden": true, + "characteristics": [ + { + "iid": 11, + "value": 100, + "type": "00000008-0000-1000-8000-0026BB765291", + "perms": ["pr", "pw", "ev"], + "ev": false, + "format": "int", + "minValue": 0, + "maxValue": 100, + "minStep": 1, + "unit": "percentage" + }, + { + "iid": 12, + "value": 7250712, + "type": "00000005-0000-1000-8000-001D4B474349", + "perms": ["pr", "ev"], + "ev": false, + "format": "uint32", + "description": "Epoch", + "unit": "seconds" + } + ] + }, + { + "iid": 13, + "type": "00000047-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "iid": 14, + "value": true, + "type": "00000025-0000-1000-8000-0026BB765291", + "perms": ["pr", "pw", "ev"], + "ev": true, + "format": "bool" + }, + { + "iid": 15, + "value": true, + "type": "00000026-0000-1000-8000-0026BB765291", + "perms": ["pr", "ev"], + "ev": true, + "format": "bool" + }, + { + "iid": 16, + "value": "Outlet A", + "type": "00000023-0000-1000-8000-0026BB765291", + "perms": ["pr"], + "ev": false, + "format": "string" + }, + { + "iid": 17, + "value": 126.3, + "type": "00000008-0000-1000-8000-001D4B474349", + "perms": ["pr", "ev"], + "ev": false, + "format": "float", + "minValue": 0.0, + "maxValue": 130.0, + "minStep": 0.1, + "description": "Volts" + }, + { + "iid": 18, + "value": 0.03, + "type": "00000009-0000-1000-8000-001D4B474349", + "perms": ["pr", "ev"], + "ev": false, + "format": "float", + "minValue": 0.0, + "maxValue": 20.0, + "minStep": 0.1, + "description": "Amps" + }, + { + "iid": 19, + "value": 0.8, + "type": "0000000A-0000-1000-8000-001D4B474349", + "perms": ["pr", "ev"], + "ev": false, + "format": "float", + "description": "Watts" + }, + { + "iid": 20, + "value": 379.69299, + "type": "0000000C-0000-1000-8000-001D4B474349", + "perms": ["pr", "ev"], + "ev": false, + "format": "float", + "minValue": 0.0, + "maxValue": 34028234663852885981170418348, + "minStep": 0.1, + "description": "Kilowatt-hours" + }, + { + "iid": 21, + "value": 22, + "type": "0000000B-0000-1000-8000-001D4B474349", + "perms": ["pr", "ev"], + "ev": false, + "format": "int", + "minValue": 0, + "maxValue": 100, + "minStep": 1, + "description": "Power factor" + }, + { + "iid": 22, + "value": 390, + "type": "00000005-0000-1000-8000-001D4B474349", + "perms": ["pr", "ev"], + "ev": false, + "format": "uint32", + "description": "State timer", + "unit": "seconds" + }, + { + "iid": 23, + "value": "Outlet A", + "type": "0000000E-0000-1000-8000-001D4B474349", + "perms": ["pr", "ev"], + "ev": false, + "format": "string", + "description": "Assigned name" + }, + { + "iid": 24, + "value": 0, + "type": "0000000F-0000-1000-8000-001D4B474349", + "perms": ["pr", "ev"], + "ev": false, + "format": "uint32", + "description": "Device type" + } + ] + }, + { + "iid": 25, + "type": "00000047-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "iid": 26, + "value": true, + "type": "00000025-0000-1000-8000-0026BB765291", + "perms": ["pr", "pw", "ev"], + "ev": true, + "format": "bool" + }, + { + "iid": 27, + "value": true, + "type": "00000026-0000-1000-8000-0026BB765291", + "perms": ["pr", "ev"], + "ev": true, + "format": "bool" + }, + { + "iid": 28, + "value": "Outlet B", + "type": "00000023-0000-1000-8000-0026BB765291", + "perms": ["pr"], + "ev": false, + "format": "string" + }, + { + "iid": 29, + "value": 126.3, + "type": "00000008-0000-1000-8000-001D4B474349", + "perms": ["pr", "ev"], + "ev": false, + "format": "float", + "minValue": 0.0, + "maxValue": 130.0, + "minStep": 0.1, + "description": "Volts" + }, + { + "iid": 30, + "value": 0.05, + "type": "00000009-0000-1000-8000-001D4B474349", + "perms": ["pr", "ev"], + "ev": false, + "format": "float", + "minValue": 0.0, + "maxValue": 20.0, + "minStep": 0.1, + "description": "Amps" + }, + { + "iid": 31, + "value": 0.8, + "type": "0000000A-0000-1000-8000-001D4B474349", + "perms": ["pr", "ev"], + "ev": false, + "format": "float", + "description": "Watts" + }, + { + "iid": 32, + "value": 175.85001, + "type": "0000000C-0000-1000-8000-001D4B474349", + "perms": ["pr", "ev"], + "ev": false, + "format": "float", + "minValue": 0.0, + "maxValue": 34028234663852885981170418348, + "minStep": 0.1, + "description": "Kilowatt-hours" + }, + { + "iid": 33, + "value": 13, + "type": "0000000B-0000-1000-8000-001D4B474349", + "perms": ["pr", "ev"], + "ev": false, + "format": "int", + "minValue": 0, + "maxValue": 100, + "minStep": 1, + "description": "Power factor" + }, + { + "iid": 34, + "value": 390, + "type": "00000005-0000-1000-8000-001D4B474349", + "perms": ["pr", "ev"], + "ev": false, + "format": "uint32", + "description": "State timer", + "unit": "seconds" + }, + { + "iid": 35, + "value": "Outlet B", + "type": "0000000E-0000-1000-8000-001D4B474349", + "perms": ["pr", "ev"], + "ev": false, + "format": "string", + "description": "Assigned name" + }, + { + "iid": 36, + "value": 0, + "type": "0000000F-0000-1000-8000-001D4B474349", + "perms": ["pr", "ev"], + "ev": false, + "format": "uint32", + "description": "Device type" + } + ] + }, + { + "iid": 37, + "type": "00000020-0000-1000-8000-001D4B474349", + "hidden": true, + "characteristics": [ + { + "iid": 38, + "value": 0, + "type": "00000021-0000-1000-8000-001D4B474349", + "perms": ["pr", "ev"], + "ev": false, + "format": "uint8" + }, + { + "iid": 39, + "type": "00000022-0000-1000-8000-001D4B474349", + "perms": ["pw"], + "ev": false, + "format": "uint8" + }, + { + "iid": 40, + "type": "00000023-0000-1000-8000-001D4B474349", + "perms": ["pw"], + "ev": false, + "format": "string", + "maxLen": 256 + }, + { + "iid": 41, + "type": "00000024-0000-1000-8000-001D4B474349", + "perms": ["pw"], + "ev": false, + "format": "bool" + }, + { + "iid": 42, + "type": "00000300-0000-1000-8000-001D4B474349", + "perms": ["pw"], + "ev": false, + "format": "string", + "maxLen": 65 + } + ] + } + ] + } ] diff --git a/tests/components/homekit_controller/fixtures/ecobee3.json b/tests/components/homekit_controller/fixtures/ecobee3.json index 34c3fb4cdea..27ab84146ba 100644 --- a/tests/components/homekit_controller/fixtures/ecobee3.json +++ b/tests/components/homekit_controller/fixtures/ecobee3.json @@ -1,1036 +1,803 @@ [ - { - "aid": 1, - "services": [ - { - "type": "3E", - "characteristics": [ - { - "value": "HomeW", - "perms": [ - "pr" - ], - "type": "23", - "format": "string", - "iid": 2 - }, - { - "value": "ecobee Inc.", - "perms": [ - "pr" - ], - "type": "20", - "format": "string", - "iid": 3 - }, - { - "value": "123456789012", - "perms": [ - "pr" - ], - "type": "30", - "format": "string", - "iid": 4 - }, - { - "value": "ecobee3", - "perms": [ - "pr" - ], - "type": "21", - "format": "string", - "iid": 5 - }, - { - "perms": [ - "pw" - ], - "type": "14", - "format": "bool", - "iid": 6 - }, - { - "value": "4.2.394", - "perms": [ - "pr" - ], - "type": "52", - "format": "string", - "iid": 8 - }, - { - "value": 0, - "perms": [ - "pr", - "ev" - ], - "type": "A6", - "format": "uint32", - "iid": 9 - } - ], - "iid": 1 - }, - { - "type": "A2", - "characteristics": [ - { - "value": "1.1.0", - "perms": [ - "pr" - ], - "maxLen": 64, - "type": "37", - "format": "string", - "iid": 31 - } - ], - "iid": 30 - }, - { - "primary": true, - "type": "4A", - "characteristics": [ - { - "value": 1, - "maxValue": 2, - "minStep": 1, - "perms": [ - "pr", - "ev" - ], - "type": "F", - "minValue": 0, - "format": "uint8", - "iid": 17 - }, - { - "value": 1, - "maxValue": 3, - "minStep": 1, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "33", - "minValue": 0, - "format": "uint8", - "iid": 18 - }, - { - "value": 21.8, - "maxValue": 100, - "minStep": 0.1, - "perms": [ - "pr", - "ev" - ], - "unit": "celsius", - "type": "11", - "minValue": 0, - "format": "float", - "iid": 19 - }, - { - "value": 22.2, - "maxValue": 33.3, - "minStep": 0.1, - "perms": [ - "pr", - "pw", - "ev" - ], - "unit": "celsius", - "type": "35", - "minValue": 7.2, - "format": "float", - "iid": 20 - }, - { - "value": 1, - "maxValue": 1, - "minStep": 1, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "36", - "minValue": 0, - "format": "uint8", - "iid": 21 - }, - { - "value": 24.4, - "maxValue": 33.3, - "minStep": 0.1, - "perms": [ - "pr", - "pw", - "ev" - ], - "unit": "celsius", - "type": "D", - "minValue": 18.3, - "format": "float", - "iid": 22 - }, - { - "value": 22.2, - "maxValue": 26.1, - "minStep": 0.1, - "perms": [ - "pr", - "pw", - "ev" - ], - "unit": "celsius", - "type": "12", - "minValue": 7.2, - "format": "float", - "iid": 23 - }, - { - "value": 34, - "maxValue": 100, - "minStep": 1, - "perms": [ - "pr", - "ev" - ], - "unit": "percentage", - "type": "10", - "minValue": 0, - "format": "float", - "iid": 24 - }, - { - "value": 36, - "maxValue": 50, - "minStep": 1, - "perms": [ - "pr", - "pw", - "ev" - ], - "unit": "percentage", - "type": "34", - "minValue": 20, - "format": "float", - "iid": 25 - }, - { - "value": "HomeW", - "perms": [ - "pr" - ], - "type": "23", - "format": "string", - "iid": 27 - }, - { - "value": 0, - "perms": [ - "pr", - "ev" - ], - "type": "B7DDB9A3-54BB-4572-91D2-F1F5B0510F8C", - "format": "uint8", - "iid": 33 - }, - { - "value": 22.2, - "maxValue": 26.1, - "minStep": 0.1, - "perms": [ - "pr", - "pw", - "ev" - ], - "unit": "celsius", - "type": "E4489BBC-5227-4569-93E5-B345E3E5508F", - "minValue": 7.2, - "format": "float", - "iid": 34 - }, - { - "value": 24.4, - "maxValue": 33.3, - "minStep": 0.1, - "perms": [ - "pr", - "pw", - "ev" - ], - "unit": "celsius", - "type": "7D381BAA-20F9-40E5-9BE9-AEB92D4BECEF", - "minValue": 18.3, - "format": "float", - "iid": 35 - }, - { - "value": 17.8, - "maxValue": 26.1, - "minStep": 0.1, - "perms": [ - "pr", - "pw", - "ev" - ], - "unit": "celsius", - "type": "73AAB542-892A-4439-879A-D2A883724B69", - "minValue": 7.2, - "format": "float", - "iid": 36 - }, - { - "value": 27.8, - "maxValue": 33.3, - "minStep": 0.1, - "perms": [ - "pr", - "pw", - "ev" - ], - "unit": "celsius", - "type": "5DA985F0-898A-4850-B987-B76C6C78D670", - "minValue": 18.3, - "format": "float", - "iid": 37 - }, - { - "value": 18.9, - "maxValue": 26.1, - "minStep": 0.1, - "perms": [ - "pr", - "pw", - "ev" - ], - "unit": "celsius", - "type": "05B97374-6DC0-439B-A0FA-CA33F612D425", - "minValue": 7.2, - "format": "float", - "iid": 38 - }, - { - "value": 26.7, - "maxValue": 33.3, - "minStep": 0.1, - "perms": [ - "pr", - "pw", - "ev" - ], - "unit": "celsius", - "type": "A251F6E7-AC46-4190-9C5D-3D06277BDF9F", - "minValue": 18.3, - "format": "float", - "iid": 39 - }, - { - "minValue": 0, - "maxValue": 3, - "minStep": 1, - "perms": [ - "pw" - ], - "type": "1B300BC2-CFFC-47FF-89F9-BD6CCF5F2853", - "format": "uint8", - "iid": 40 - }, - { - "value": "2014-01-03T00:00:00-05:00", - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "1621F556-1367-443C-AF19-82AF018E99DE", - "format": "string", - "iid": 41 - }, - { - "perms": [ - "pw" - ], - "type": "FA128DE6-9D7D-49A4-B6D8-4E4E234DEE38", - "format": "bool", - "iid": 48 - }, - { - "value": 1, - "perms": [ - "pr", - "ev" - ], - "type": "4A6AE4F6-036C-495D-87CC-B3702B437741", - "format": "uint8", - "iid": 49 - }, - { - "value": 0, - "perms": [ - "pr", - "ev" - ], - "type": "DB7BF261-7042-4194-8BD1-3AA22830AEDD", - "format": "uint8", - "iid": 50 - }, - { - "value": false, - "perms": [ - "pr", - "ev" - ], - "type": "41935E3E-B54D-42E9-B8B9-D33C6319F0AF", - "format": "bool", - "iid": 51 - }, - { - "minValue": 0, - "maxValue": 100, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "C35DA3C0-E004-40E3-B153-46655CDD9214", - "value": 0, - "format": "uint8", - "iid": 52 - }, - { - "value": 100, - "perms": [ - "pr", - "ev" - ], - "type": "48F62AEC-4171-4B4A-8F0E-1EEB6708B3FB", - "format": "uint8", - "iid": 53 - }, - { - "value": "The Hive is humming along. You have no pending alerts or reminders.", - "perms": [ - "pr", - "ev" - ], - "iid": 54, - "type": "1B1515F2-CC45-409F-991F-C480987F92C3", - "format": "string", - "maxLen": 256 - } - ], - "iid": 16 - }, - { - "type": "85", - "characteristics": [ - { - "value": "HomeW", - "perms": [ - "pr" - ], - "type": "23", - "format": "string", - "iid": 28 - }, - { - "value": false, - "perms": [ - "pr", - "ev" - ], - "type": "22", - "format": "bool", - "iid": 66 - }, - { - "minValue": -1, - "maxValue": 86400, - "perms": [ - "pr", - "ev" - ], - "type": "BFE61C70-4A40-11E6-BDF4-0800200C9A66", - "value": 2980, - "format": "int", - "iid": 67 - } - ], - "iid": 56 - }, - { - "type": "86", - "characteristics": [ - { - "value": "HomeW", - "perms": [ - "pr" - ], - "type": "23", - "format": "string", - "iid": 29 - }, - { - "minValue": 0, - "maxValue": 1, - "minStep": 1, - "perms": [ - "pr", - "ev" - ], - "type": "71", - "value": 1, - "format": "uint8", - "iid": 65 - }, - { - "minValue": -1, - "maxValue": 86400, - "perms": [ - "pr", - "ev" - ], - "type": "A8f798E0-4A40-11E6-BDF4-0800200C9A66", - "value": 2980, - "format": "int", - "iid": 68 - } - ], - "iid": 57 - } - ] - }, - { - "aid": 2, - "services": [ - { - "type": "3E", - "characteristics": [ - { - "value": "Kitchen", - "perms": [ - "pr" - ], - "type": "23", - "format": "string", - "iid": 2049 - }, - { - "value": "ecobee Inc.", - "perms": [ - "pr" - ], - "type": "20", - "format": "string", - "iid": 2050 - }, - { - "value": "AB1C", - "perms": [ - "pr" - ], - "type": "30", - "format": "string", - "iid": 2051 - }, - { - "value": "REMOTE SENSOR", - "perms": [ - "pr" - ], - "type": "21", - "format": "string", - "iid": 2052 - }, - { - "value": "1.0.0", - "perms": [ - "pr" - ], - "type": "52", - "format": "string", - "iid": 8 - }, - { - "perms": [ - "pw" - ], - "type": "14", - "format": "bool", - "iid": 2053 - } - ], - "iid": 1 - }, - { - "type": "8A", - "characteristics": [ - { - "value": 21.5, - "maxValue": 100, - "minStep": 0.1, - "perms": [ - "pr", - "ev" - ], - "unit": "celsius", - "type": "11", - "minValue": 0, - "format": "float", - "iid": 2064 - }, - { - "value": "Kitchen", - "perms": [ - "pr" - ], - "type": "23", - "format": "string", - "iid": 2067 - }, - { - "value": true, - "perms": [ - "pr", - "ev" - ], - "type": "75", - "format": "bool", - "iid": 2066 - }, - { - "value": 0, - "maxValue": 1, - "minStep": 1, - "perms": [ - "pr", - "ev" - ], - "type": "79", - "minValue": 0, - "format": "uint8", - "iid": 2065 - } - ], - "iid": 55 - }, - { - "type": "85", - "characteristics": [ - { - "value": false, - "perms": [ - "pr", - "ev" - ], - "type": "22", - "format": "bool", - "iid": 2060 - }, - { - "value": "Kitchen", - "perms": [ - "pr" - ], - "type": "23", - "format": "string", - "iid": 2063 - }, - { - "value": true, - "perms": [ - "pr", - "ev" - ], - "type": "75", - "format": "bool", - "iid": 2062 - }, - { - "minValue": 0, - "maxValue": 1, - "minStep": 1, - "perms": [ - "pr", - "ev" - ], - "type": "79", - "value": 0, - "format": "uint8", - "iid": 2061 - }, - { - "minValue": -1, - "maxValue": 86400, - "perms": [ - "pr", - "ev" - ], - "type": "BFE61C70-4A40-11E6-BDF4-0800200C9A66", - "value": 3620, - "format": "int", - "iid": 2059 - } - ], - "iid": 56 - } - ] - }, - { - "aid": 3, - "services": [ - { - "type": "3E", - "characteristics": [ - { - "value": "Porch", - "perms": [ - "pr" - ], - "type": "23", - "format": "string", - "iid": 3073 - }, - { - "value": "ecobee Inc.", - "perms": [ - "pr" - ], - "type": "20", - "format": "string", - "iid": 3074 - }, - { - "value": "AB2C", - "perms": [ - "pr" - ], - "type": "30", - "format": "string", - "iid": 3075 - }, - { - "value": "REMOTE SENSOR", - "perms": [ - "pr" - ], - "type": "21", - "format": "string", - "iid": 3076 - }, - { - "value": "1.0.0", - "perms": [ - "pr" - ], - "type": "52", - "format": "string", - "iid": 8 - }, - { - "perms": [ - "pw" - ], - "type": "14", - "format": "bool", - "iid": 3077 - } - ], - "iid": 1 - }, - { - "type": "8A", - "characteristics": [ - { - "value": 21, - "maxValue": 100, - "minStep": 0.1, - "perms": [ - "pr", - "ev" - ], - "unit": "celsius", - "type": "11", - "minValue": 0, - "format": "float", - "iid": 3088 - }, - { - "value": "Porch", - "perms": [ - "pr" - ], - "type": "23", - "format": "string", - "iid": 3091 - }, - { - "value": true, - "perms": [ - "pr", - "ev" - ], - "type": "75", - "format": "bool", - "iid": 3090 - }, - { - "value": 0, - "maxValue": 1, - "minStep": 1, - "perms": [ - "pr", - "ev" - ], - "type": "79", - "minValue": 0, - "format": "uint8", - "iid": 3089 - } - ], - "iid": 55 - }, - { - "type": "85", - "characteristics": [ - { - "value": false, - "perms": [ - "pr", - "ev" - ], - "type": "22", - "format": "bool", - "iid": 3084 - }, - { - "value": "Porch", - "perms": [ - "pr" - ], - "type": "23", - "format": "string", - "iid": 3087 - }, - { - "value": true, - "perms": [ - "pr", - "ev" - ], - "type": "75", - "format": "bool", - "iid": 3086 - }, - { - "minValue": 0, - "maxValue": 1, - "minStep": 1, - "perms": [ - "pr", - "ev" - ], - "type": "79", - "value": 0, - "format": "uint8", - "iid": 3085 - }, - { - "minValue": -1, - "maxValue": 86400, - "perms": [ - "pr", - "ev" - ], - "type": "BFE61C70-4A40-11E6-BDF4-0800200C9A66", - "value": 5766, - "format": "int", - "iid": 3083 - } - ], - "iid": 56 - } - ] - }, - { - "aid": 4, - "services": [ - { - "type": "3E", - "characteristics": [ - { - "value": "Basement", - "perms": [ - "pr" - ], - "type": "23", - "format": "string", - "iid": 4097 - }, - { - "value": "ecobee Inc.", - "perms": [ - "pr" - ], - "type": "20", - "format": "string", - "iid": 4098 - }, - { - "value": "AB3C", - "perms": [ - "pr" - ], - "type": "30", - "format": "string", - "iid": 4099 - }, - { - "value": "REMOTE SENSOR", - "perms": [ - "pr" - ], - "type": "21", - "format": "string", - "iid": 4100 - }, - { - "value": "1.0.0", - "perms": [ - "pr" - ], - "type": "52", - "format": "string", - "iid": 8 - }, - { - "perms": [ - "pw" - ], - "type": "14", - "format": "bool", - "iid": 4101 - } - ], - "iid": 1 - }, - { - "type": "8A", - "characteristics": [ - { - "value": 20.7, - "maxValue": 100, - "minStep": 0.1, - "perms": [ - "pr", - "ev" - ], - "unit": "celsius", - "type": "11", - "minValue": 0, - "format": "float", - "iid": 4112 - }, - { - "value": "Basement", - "perms": [ - "pr" - ], - "type": "23", - "format": "string", - "iid": 4115 - }, - { - "value": true, - "perms": [ - "pr", - "ev" - ], - "type": "75", - "format": "bool", - "iid": 4114 - }, - { - "value": 0, - "maxValue": 1, - "minStep": 1, - "perms": [ - "pr", - "ev" - ], - "type": "79", - "minValue": 0, - "format": "uint8", - "iid": 4113 - } - ], - "iid": 55 - }, - { - "type": "85", - "characteristics": [ - { - "value": false, - "perms": [ - "pr", - "ev" - ], - "type": "22", - "format": "bool", - "iid": 4108 - }, - { - "value": "Basement", - "perms": [ - "pr" - ], - "type": "23", - "format": "string", - "iid": 4111 - }, - { - "value": true, - "perms": [ - "pr", - "ev" - ], - "type": "75", - "format": "bool", - "iid": 4110 - }, - { - "minValue": 0, - "maxValue": 1, - "minStep": 1, - "perms": [ - "pr", - "ev" - ], - "type": "79", - "value": 0, - "format": "uint8", - "iid": 4109 - }, - { - "minValue": -1, - "maxValue": 86400, - "perms": [ - "pr", - "ev" - ], - "type": "BFE61C70-4A40-11E6-BDF4-0800200C9A66", - "value": 5472, - "format": "int", - "iid": 4107 - } - ], - "iid": 56 - } - ] - } -] \ No newline at end of file + { + "aid": 1, + "services": [ + { + "type": "3E", + "characteristics": [ + { + "value": "HomeW", + "perms": ["pr"], + "type": "23", + "format": "string", + "iid": 2 + }, + { + "value": "ecobee Inc.", + "perms": ["pr"], + "type": "20", + "format": "string", + "iid": 3 + }, + { + "value": "123456789012", + "perms": ["pr"], + "type": "30", + "format": "string", + "iid": 4 + }, + { + "value": "ecobee3", + "perms": ["pr"], + "type": "21", + "format": "string", + "iid": 5 + }, + { + "perms": ["pw"], + "type": "14", + "format": "bool", + "iid": 6 + }, + { + "value": "4.2.394", + "perms": ["pr"], + "type": "52", + "format": "string", + "iid": 8 + }, + { + "value": 0, + "perms": ["pr", "ev"], + "type": "A6", + "format": "uint32", + "iid": 9 + } + ], + "iid": 1 + }, + { + "type": "A2", + "characteristics": [ + { + "value": "1.1.0", + "perms": ["pr"], + "maxLen": 64, + "type": "37", + "format": "string", + "iid": 31 + } + ], + "iid": 30 + }, + { + "primary": true, + "type": "4A", + "characteristics": [ + { + "value": 1, + "maxValue": 2, + "minStep": 1, + "perms": ["pr", "ev"], + "type": "F", + "minValue": 0, + "format": "uint8", + "iid": 17 + }, + { + "value": 1, + "maxValue": 3, + "minStep": 1, + "perms": ["pr", "pw", "ev"], + "type": "33", + "minValue": 0, + "format": "uint8", + "iid": 18 + }, + { + "value": 21.8, + "maxValue": 100, + "minStep": 0.1, + "perms": ["pr", "ev"], + "unit": "celsius", + "type": "11", + "minValue": 0, + "format": "float", + "iid": 19 + }, + { + "value": 22.2, + "maxValue": 33.3, + "minStep": 0.1, + "perms": ["pr", "pw", "ev"], + "unit": "celsius", + "type": "35", + "minValue": 7.2, + "format": "float", + "iid": 20 + }, + { + "value": 1, + "maxValue": 1, + "minStep": 1, + "perms": ["pr", "pw", "ev"], + "type": "36", + "minValue": 0, + "format": "uint8", + "iid": 21 + }, + { + "value": 24.4, + "maxValue": 33.3, + "minStep": 0.1, + "perms": ["pr", "pw", "ev"], + "unit": "celsius", + "type": "D", + "minValue": 18.3, + "format": "float", + "iid": 22 + }, + { + "value": 22.2, + "maxValue": 26.1, + "minStep": 0.1, + "perms": ["pr", "pw", "ev"], + "unit": "celsius", + "type": "12", + "minValue": 7.2, + "format": "float", + "iid": 23 + }, + { + "value": 34, + "maxValue": 100, + "minStep": 1, + "perms": ["pr", "ev"], + "unit": "percentage", + "type": "10", + "minValue": 0, + "format": "float", + "iid": 24 + }, + { + "value": 36, + "maxValue": 50, + "minStep": 1, + "perms": ["pr", "pw", "ev"], + "unit": "percentage", + "type": "34", + "minValue": 20, + "format": "float", + "iid": 25 + }, + { + "value": "HomeW", + "perms": ["pr"], + "type": "23", + "format": "string", + "iid": 27 + }, + { + "value": 0, + "perms": ["pr", "ev"], + "type": "B7DDB9A3-54BB-4572-91D2-F1F5B0510F8C", + "format": "uint8", + "iid": 33 + }, + { + "value": 22.2, + "maxValue": 26.1, + "minStep": 0.1, + "perms": ["pr", "pw", "ev"], + "unit": "celsius", + "type": "E4489BBC-5227-4569-93E5-B345E3E5508F", + "minValue": 7.2, + "format": "float", + "iid": 34 + }, + { + "value": 24.4, + "maxValue": 33.3, + "minStep": 0.1, + "perms": ["pr", "pw", "ev"], + "unit": "celsius", + "type": "7D381BAA-20F9-40E5-9BE9-AEB92D4BECEF", + "minValue": 18.3, + "format": "float", + "iid": 35 + }, + { + "value": 17.8, + "maxValue": 26.1, + "minStep": 0.1, + "perms": ["pr", "pw", "ev"], + "unit": "celsius", + "type": "73AAB542-892A-4439-879A-D2A883724B69", + "minValue": 7.2, + "format": "float", + "iid": 36 + }, + { + "value": 27.8, + "maxValue": 33.3, + "minStep": 0.1, + "perms": ["pr", "pw", "ev"], + "unit": "celsius", + "type": "5DA985F0-898A-4850-B987-B76C6C78D670", + "minValue": 18.3, + "format": "float", + "iid": 37 + }, + { + "value": 18.9, + "maxValue": 26.1, + "minStep": 0.1, + "perms": ["pr", "pw", "ev"], + "unit": "celsius", + "type": "05B97374-6DC0-439B-A0FA-CA33F612D425", + "minValue": 7.2, + "format": "float", + "iid": 38 + }, + { + "value": 26.7, + "maxValue": 33.3, + "minStep": 0.1, + "perms": ["pr", "pw", "ev"], + "unit": "celsius", + "type": "A251F6E7-AC46-4190-9C5D-3D06277BDF9F", + "minValue": 18.3, + "format": "float", + "iid": 39 + }, + { + "minValue": 0, + "maxValue": 3, + "minStep": 1, + "perms": ["pw"], + "type": "1B300BC2-CFFC-47FF-89F9-BD6CCF5F2853", + "format": "uint8", + "iid": 40 + }, + { + "value": "2014-01-03T00:00:00-05:00", + "perms": ["pr", "pw", "ev"], + "type": "1621F556-1367-443C-AF19-82AF018E99DE", + "format": "string", + "iid": 41 + }, + { + "perms": ["pw"], + "type": "FA128DE6-9D7D-49A4-B6D8-4E4E234DEE38", + "format": "bool", + "iid": 48 + }, + { + "value": 1, + "perms": ["pr", "ev"], + "type": "4A6AE4F6-036C-495D-87CC-B3702B437741", + "format": "uint8", + "iid": 49 + }, + { + "value": 0, + "perms": ["pr", "ev"], + "type": "DB7BF261-7042-4194-8BD1-3AA22830AEDD", + "format": "uint8", + "iid": 50 + }, + { + "value": false, + "perms": ["pr", "ev"], + "type": "41935E3E-B54D-42E9-B8B9-D33C6319F0AF", + "format": "bool", + "iid": 51 + }, + { + "minValue": 0, + "maxValue": 100, + "perms": ["pr", "pw", "ev"], + "type": "C35DA3C0-E004-40E3-B153-46655CDD9214", + "value": 0, + "format": "uint8", + "iid": 52 + }, + { + "value": 100, + "perms": ["pr", "ev"], + "type": "48F62AEC-4171-4B4A-8F0E-1EEB6708B3FB", + "format": "uint8", + "iid": 53 + }, + { + "value": "The Hive is humming along. You have no pending alerts or reminders.", + "perms": ["pr", "ev"], + "iid": 54, + "type": "1B1515F2-CC45-409F-991F-C480987F92C3", + "format": "string", + "maxLen": 256 + } + ], + "iid": 16 + }, + { + "type": "85", + "characteristics": [ + { + "value": "HomeW", + "perms": ["pr"], + "type": "23", + "format": "string", + "iid": 28 + }, + { + "value": false, + "perms": ["pr", "ev"], + "type": "22", + "format": "bool", + "iid": 66 + }, + { + "minValue": -1, + "maxValue": 86400, + "perms": ["pr", "ev"], + "type": "BFE61C70-4A40-11E6-BDF4-0800200C9A66", + "value": 2980, + "format": "int", + "iid": 67 + } + ], + "iid": 56 + }, + { + "type": "86", + "characteristics": [ + { + "value": "HomeW", + "perms": ["pr"], + "type": "23", + "format": "string", + "iid": 29 + }, + { + "minValue": 0, + "maxValue": 1, + "minStep": 1, + "perms": ["pr", "ev"], + "type": "71", + "value": 1, + "format": "uint8", + "iid": 65 + }, + { + "minValue": -1, + "maxValue": 86400, + "perms": ["pr", "ev"], + "type": "A8f798E0-4A40-11E6-BDF4-0800200C9A66", + "value": 2980, + "format": "int", + "iid": 68 + } + ], + "iid": 57 + } + ] + }, + { + "aid": 2, + "services": [ + { + "type": "3E", + "characteristics": [ + { + "value": "Kitchen", + "perms": ["pr"], + "type": "23", + "format": "string", + "iid": 2049 + }, + { + "value": "ecobee Inc.", + "perms": ["pr"], + "type": "20", + "format": "string", + "iid": 2050 + }, + { + "value": "AB1C", + "perms": ["pr"], + "type": "30", + "format": "string", + "iid": 2051 + }, + { + "value": "REMOTE SENSOR", + "perms": ["pr"], + "type": "21", + "format": "string", + "iid": 2052 + }, + { + "value": "1.0.0", + "perms": ["pr"], + "type": "52", + "format": "string", + "iid": 8 + }, + { + "perms": ["pw"], + "type": "14", + "format": "bool", + "iid": 2053 + } + ], + "iid": 1 + }, + { + "type": "8A", + "characteristics": [ + { + "value": 21.5, + "maxValue": 100, + "minStep": 0.1, + "perms": ["pr", "ev"], + "unit": "celsius", + "type": "11", + "minValue": 0, + "format": "float", + "iid": 2064 + }, + { + "value": "Kitchen", + "perms": ["pr"], + "type": "23", + "format": "string", + "iid": 2067 + }, + { + "value": true, + "perms": ["pr", "ev"], + "type": "75", + "format": "bool", + "iid": 2066 + }, + { + "value": 0, + "maxValue": 1, + "minStep": 1, + "perms": ["pr", "ev"], + "type": "79", + "minValue": 0, + "format": "uint8", + "iid": 2065 + } + ], + "iid": 55 + }, + { + "type": "85", + "characteristics": [ + { + "value": false, + "perms": ["pr", "ev"], + "type": "22", + "format": "bool", + "iid": 2060 + }, + { + "value": "Kitchen", + "perms": ["pr"], + "type": "23", + "format": "string", + "iid": 2063 + }, + { + "value": true, + "perms": ["pr", "ev"], + "type": "75", + "format": "bool", + "iid": 2062 + }, + { + "minValue": 0, + "maxValue": 1, + "minStep": 1, + "perms": ["pr", "ev"], + "type": "79", + "value": 0, + "format": "uint8", + "iid": 2061 + }, + { + "minValue": -1, + "maxValue": 86400, + "perms": ["pr", "ev"], + "type": "BFE61C70-4A40-11E6-BDF4-0800200C9A66", + "value": 3620, + "format": "int", + "iid": 2059 + } + ], + "iid": 56 + } + ] + }, + { + "aid": 3, + "services": [ + { + "type": "3E", + "characteristics": [ + { + "value": "Porch", + "perms": ["pr"], + "type": "23", + "format": "string", + "iid": 3073 + }, + { + "value": "ecobee Inc.", + "perms": ["pr"], + "type": "20", + "format": "string", + "iid": 3074 + }, + { + "value": "AB2C", + "perms": ["pr"], + "type": "30", + "format": "string", + "iid": 3075 + }, + { + "value": "REMOTE SENSOR", + "perms": ["pr"], + "type": "21", + "format": "string", + "iid": 3076 + }, + { + "value": "1.0.0", + "perms": ["pr"], + "type": "52", + "format": "string", + "iid": 8 + }, + { + "perms": ["pw"], + "type": "14", + "format": "bool", + "iid": 3077 + } + ], + "iid": 1 + }, + { + "type": "8A", + "characteristics": [ + { + "value": 21, + "maxValue": 100, + "minStep": 0.1, + "perms": ["pr", "ev"], + "unit": "celsius", + "type": "11", + "minValue": 0, + "format": "float", + "iid": 3088 + }, + { + "value": "Porch", + "perms": ["pr"], + "type": "23", + "format": "string", + "iid": 3091 + }, + { + "value": true, + "perms": ["pr", "ev"], + "type": "75", + "format": "bool", + "iid": 3090 + }, + { + "value": 0, + "maxValue": 1, + "minStep": 1, + "perms": ["pr", "ev"], + "type": "79", + "minValue": 0, + "format": "uint8", + "iid": 3089 + } + ], + "iid": 55 + }, + { + "type": "85", + "characteristics": [ + { + "value": false, + "perms": ["pr", "ev"], + "type": "22", + "format": "bool", + "iid": 3084 + }, + { + "value": "Porch", + "perms": ["pr"], + "type": "23", + "format": "string", + "iid": 3087 + }, + { + "value": true, + "perms": ["pr", "ev"], + "type": "75", + "format": "bool", + "iid": 3086 + }, + { + "minValue": 0, + "maxValue": 1, + "minStep": 1, + "perms": ["pr", "ev"], + "type": "79", + "value": 0, + "format": "uint8", + "iid": 3085 + }, + { + "minValue": -1, + "maxValue": 86400, + "perms": ["pr", "ev"], + "type": "BFE61C70-4A40-11E6-BDF4-0800200C9A66", + "value": 5766, + "format": "int", + "iid": 3083 + } + ], + "iid": 56 + } + ] + }, + { + "aid": 4, + "services": [ + { + "type": "3E", + "characteristics": [ + { + "value": "Basement", + "perms": ["pr"], + "type": "23", + "format": "string", + "iid": 4097 + }, + { + "value": "ecobee Inc.", + "perms": ["pr"], + "type": "20", + "format": "string", + "iid": 4098 + }, + { + "value": "AB3C", + "perms": ["pr"], + "type": "30", + "format": "string", + "iid": 4099 + }, + { + "value": "REMOTE SENSOR", + "perms": ["pr"], + "type": "21", + "format": "string", + "iid": 4100 + }, + { + "value": "1.0.0", + "perms": ["pr"], + "type": "52", + "format": "string", + "iid": 8 + }, + { + "perms": ["pw"], + "type": "14", + "format": "bool", + "iid": 4101 + } + ], + "iid": 1 + }, + { + "type": "8A", + "characteristics": [ + { + "value": 20.7, + "maxValue": 100, + "minStep": 0.1, + "perms": ["pr", "ev"], + "unit": "celsius", + "type": "11", + "minValue": 0, + "format": "float", + "iid": 4112 + }, + { + "value": "Basement", + "perms": ["pr"], + "type": "23", + "format": "string", + "iid": 4115 + }, + { + "value": true, + "perms": ["pr", "ev"], + "type": "75", + "format": "bool", + "iid": 4114 + }, + { + "value": 0, + "maxValue": 1, + "minStep": 1, + "perms": ["pr", "ev"], + "type": "79", + "minValue": 0, + "format": "uint8", + "iid": 4113 + } + ], + "iid": 55 + }, + { + "type": "85", + "characteristics": [ + { + "value": false, + "perms": ["pr", "ev"], + "type": "22", + "format": "bool", + "iid": 4108 + }, + { + "value": "Basement", + "perms": ["pr"], + "type": "23", + "format": "string", + "iid": 4111 + }, + { + "value": true, + "perms": ["pr", "ev"], + "type": "75", + "format": "bool", + "iid": 4110 + }, + { + "minValue": 0, + "maxValue": 1, + "minStep": 1, + "perms": ["pr", "ev"], + "type": "79", + "value": 0, + "format": "uint8", + "iid": 4109 + }, + { + "minValue": -1, + "maxValue": 86400, + "perms": ["pr", "ev"], + "type": "BFE61C70-4A40-11E6-BDF4-0800200C9A66", + "value": 5472, + "format": "int", + "iid": 4107 + } + ], + "iid": 56 + } + ] + } +] diff --git a/tests/components/homekit_controller/fixtures/ecobee3_no_sensors.json b/tests/components/homekit_controller/fixtures/ecobee3_no_sensors.json index 3d3c2ebad2b..cbde8f64b33 100644 --- a/tests/components/homekit_controller/fixtures/ecobee3_no_sensors.json +++ b/tests/components/homekit_controller/fixtures/ecobee3_no_sensors.json @@ -1,508 +1,386 @@ [ - { - "aid": 1, - "services": [ - { - "type": "3E", - "characteristics": [ - { - "value": "HomeW", - "perms": [ - "pr" - ], - "type": "23", - "format": "string", - "iid": 2 - }, - { - "value": "ecobee Inc.", - "perms": [ - "pr" - ], - "type": "20", - "format": "string", - "iid": 3 - }, - { - "value": "123456789012", - "perms": [ - "pr" - ], - "type": "30", - "format": "string", - "iid": 4 - }, - { - "value": "ecobee3", - "perms": [ - "pr" - ], - "type": "21", - "format": "string", - "iid": 5 - }, - { - "perms": [ - "pw" - ], - "type": "14", - "format": "bool", - "iid": 6 - }, - { - "value": "4.2.394", - "perms": [ - "pr" - ], - "type": "52", - "format": "string", - "iid": 8 - }, - { - "value": 0, - "perms": [ - "pr", - "ev" - ], - "type": "A6", - "format": "uint32", - "iid": 9 - } - ], - "iid": 1 - }, - { - "type": "A2", - "characteristics": [ - { - "value": "1.1.0", - "perms": [ - "pr" - ], - "maxLen": 64, - "type": "37", - "format": "string", - "iid": 31 - } - ], - "iid": 30 - }, - { - "primary": true, - "type": "4A", - "characteristics": [ - { - "value": 1, - "maxValue": 2, - "minStep": 1, - "perms": [ - "pr", - "ev" - ], - "type": "F", - "minValue": 0, - "format": "uint8", - "iid": 17 - }, - { - "value": 1, - "maxValue": 3, - "minStep": 1, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "33", - "minValue": 0, - "format": "uint8", - "iid": 18 - }, - { - "value": 21.8, - "maxValue": 100, - "minStep": 0.1, - "perms": [ - "pr", - "ev" - ], - "unit": "celsius", - "type": "11", - "minValue": 0, - "format": "float", - "iid": 19 - }, - { - "value": 22.2, - "maxValue": 33.3, - "minStep": 0.1, - "perms": [ - "pr", - "pw", - "ev" - ], - "unit": "celsius", - "type": "35", - "minValue": 7.2, - "format": "float", - "iid": 20 - }, - { - "value": 1, - "maxValue": 1, - "minStep": 1, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "36", - "minValue": 0, - "format": "uint8", - "iid": 21 - }, - { - "value": 24.4, - "maxValue": 33.3, - "minStep": 0.1, - "perms": [ - "pr", - "pw", - "ev" - ], - "unit": "celsius", - "type": "D", - "minValue": 18.3, - "format": "float", - "iid": 22 - }, - { - "value": 22.2, - "maxValue": 26.1, - "minStep": 0.1, - "perms": [ - "pr", - "pw", - "ev" - ], - "unit": "celsius", - "type": "12", - "minValue": 7.2, - "format": "float", - "iid": 23 - }, - { - "value": 34, - "maxValue": 100, - "minStep": 1, - "perms": [ - "pr", - "ev" - ], - "unit": "percentage", - "type": "10", - "minValue": 0, - "format": "float", - "iid": 24 - }, - { - "value": 36, - "maxValue": 50, - "minStep": 1, - "perms": [ - "pr", - "pw", - "ev" - ], - "unit": "percentage", - "type": "34", - "minValue": 20, - "format": "float", - "iid": 25 - }, - { - "value": "HomeW", - "perms": [ - "pr" - ], - "type": "23", - "format": "string", - "iid": 27 - }, - { - "value": 0, - "perms": [ - "pr", - "ev" - ], - "type": "B7DDB9A3-54BB-4572-91D2-F1F5B0510F8C", - "format": "uint8", - "iid": 33 - }, - { - "value": 22.2, - "maxValue": 26.1, - "minStep": 0.1, - "perms": [ - "pr", - "pw", - "ev" - ], - "unit": "celsius", - "type": "E4489BBC-5227-4569-93E5-B345E3E5508F", - "minValue": 7.2, - "format": "float", - "iid": 34 - }, - { - "value": 24.4, - "maxValue": 33.3, - "minStep": 0.1, - "perms": [ - "pr", - "pw", - "ev" - ], - "unit": "celsius", - "type": "7D381BAA-20F9-40E5-9BE9-AEB92D4BECEF", - "minValue": 18.3, - "format": "float", - "iid": 35 - }, - { - "value": 17.8, - "maxValue": 26.1, - "minStep": 0.1, - "perms": [ - "pr", - "pw", - "ev" - ], - "unit": "celsius", - "type": "73AAB542-892A-4439-879A-D2A883724B69", - "minValue": 7.2, - "format": "float", - "iid": 36 - }, - { - "value": 27.8, - "maxValue": 33.3, - "minStep": 0.1, - "perms": [ - "pr", - "pw", - "ev" - ], - "unit": "celsius", - "type": "5DA985F0-898A-4850-B987-B76C6C78D670", - "minValue": 18.3, - "format": "float", - "iid": 37 - }, - { - "value": 18.9, - "maxValue": 26.1, - "minStep": 0.1, - "perms": [ - "pr", - "pw", - "ev" - ], - "unit": "celsius", - "type": "05B97374-6DC0-439B-A0FA-CA33F612D425", - "minValue": 7.2, - "format": "float", - "iid": 38 - }, - { - "value": 26.7, - "maxValue": 33.3, - "minStep": 0.1, - "perms": [ - "pr", - "pw", - "ev" - ], - "unit": "celsius", - "type": "A251F6E7-AC46-4190-9C5D-3D06277BDF9F", - "minValue": 18.3, - "format": "float", - "iid": 39 - }, - { - "minValue": 0, - "maxValue": 3, - "minStep": 1, - "perms": [ - "pw" - ], - "type": "1B300BC2-CFFC-47FF-89F9-BD6CCF5F2853", - "format": "uint8", - "iid": 40 - }, - { - "value": "2014-01-03T00:00:00-05:00", - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "1621F556-1367-443C-AF19-82AF018E99DE", - "format": "string", - "iid": 41 - }, - { - "perms": [ - "pw" - ], - "type": "FA128DE6-9D7D-49A4-B6D8-4E4E234DEE38", - "format": "bool", - "iid": 48 - }, - { - "value": 1, - "perms": [ - "pr", - "ev" - ], - "type": "4A6AE4F6-036C-495D-87CC-B3702B437741", - "format": "uint8", - "iid": 49 - }, - { - "value": 0, - "perms": [ - "pr", - "ev" - ], - "type": "DB7BF261-7042-4194-8BD1-3AA22830AEDD", - "format": "uint8", - "iid": 50 - }, - { - "value": false, - "perms": [ - "pr", - "ev" - ], - "type": "41935E3E-B54D-42E9-B8B9-D33C6319F0AF", - "format": "bool", - "iid": 51 - }, - { - "minValue": 0, - "maxValue": 100, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "C35DA3C0-E004-40E3-B153-46655CDD9214", - "value": 0, - "format": "uint8", - "iid": 52 - }, - { - "value": 100, - "perms": [ - "pr", - "ev" - ], - "type": "48F62AEC-4171-4B4A-8F0E-1EEB6708B3FB", - "format": "uint8", - "iid": 53 - }, - { - "value": "The Hive is humming along. You have no pending alerts or reminders.", - "perms": [ - "pr", - "ev" - ], - "iid": 54, - "type": "1B1515F2-CC45-409F-991F-C480987F92C3", - "format": "string", - "maxLen": 256 - } - ], - "iid": 16 - }, - { - "type": "85", - "characteristics": [ - { - "value": "HomeW", - "perms": [ - "pr" - ], - "type": "23", - "format": "string", - "iid": 28 - }, - { - "value": false, - "perms": [ - "pr", - "ev" - ], - "type": "22", - "format": "bool", - "iid": 66 - }, - { - "minValue": -1, - "maxValue": 86400, - "perms": [ - "pr", - "ev" - ], - "type": "BFE61C70-4A40-11E6-BDF4-0800200C9A66", - "value": 2980, - "format": "int", - "iid": 67 - } - ], - "iid": 56 - }, - { - "type": "86", - "characteristics": [ - { - "value": "HomeW", - "perms": [ - "pr" - ], - "type": "23", - "format": "string", - "iid": 29 - }, - { - "minValue": 0, - "maxValue": 1, - "minStep": 1, - "perms": [ - "pr", - "ev" - ], - "type": "71", - "value": 1, - "format": "uint8", - "iid": 65 - }, - { - "minValue": -1, - "maxValue": 86400, - "perms": [ - "pr", - "ev" - ], - "type": "A8f798E0-4A40-11E6-BDF4-0800200C9A66", - "value": 2980, - "format": "int", - "iid": 68 - } - ], - "iid": 57 - } - ] - } -] \ No newline at end of file + { + "aid": 1, + "services": [ + { + "type": "3E", + "characteristics": [ + { + "value": "HomeW", + "perms": ["pr"], + "type": "23", + "format": "string", + "iid": 2 + }, + { + "value": "ecobee Inc.", + "perms": ["pr"], + "type": "20", + "format": "string", + "iid": 3 + }, + { + "value": "123456789012", + "perms": ["pr"], + "type": "30", + "format": "string", + "iid": 4 + }, + { + "value": "ecobee3", + "perms": ["pr"], + "type": "21", + "format": "string", + "iid": 5 + }, + { + "perms": ["pw"], + "type": "14", + "format": "bool", + "iid": 6 + }, + { + "value": "4.2.394", + "perms": ["pr"], + "type": "52", + "format": "string", + "iid": 8 + }, + { + "value": 0, + "perms": ["pr", "ev"], + "type": "A6", + "format": "uint32", + "iid": 9 + } + ], + "iid": 1 + }, + { + "type": "A2", + "characteristics": [ + { + "value": "1.1.0", + "perms": ["pr"], + "maxLen": 64, + "type": "37", + "format": "string", + "iid": 31 + } + ], + "iid": 30 + }, + { + "primary": true, + "type": "4A", + "characteristics": [ + { + "value": 1, + "maxValue": 2, + "minStep": 1, + "perms": ["pr", "ev"], + "type": "F", + "minValue": 0, + "format": "uint8", + "iid": 17 + }, + { + "value": 1, + "maxValue": 3, + "minStep": 1, + "perms": ["pr", "pw", "ev"], + "type": "33", + "minValue": 0, + "format": "uint8", + "iid": 18 + }, + { + "value": 21.8, + "maxValue": 100, + "minStep": 0.1, + "perms": ["pr", "ev"], + "unit": "celsius", + "type": "11", + "minValue": 0, + "format": "float", + "iid": 19 + }, + { + "value": 22.2, + "maxValue": 33.3, + "minStep": 0.1, + "perms": ["pr", "pw", "ev"], + "unit": "celsius", + "type": "35", + "minValue": 7.2, + "format": "float", + "iid": 20 + }, + { + "value": 1, + "maxValue": 1, + "minStep": 1, + "perms": ["pr", "pw", "ev"], + "type": "36", + "minValue": 0, + "format": "uint8", + "iid": 21 + }, + { + "value": 24.4, + "maxValue": 33.3, + "minStep": 0.1, + "perms": ["pr", "pw", "ev"], + "unit": "celsius", + "type": "D", + "minValue": 18.3, + "format": "float", + "iid": 22 + }, + { + "value": 22.2, + "maxValue": 26.1, + "minStep": 0.1, + "perms": ["pr", "pw", "ev"], + "unit": "celsius", + "type": "12", + "minValue": 7.2, + "format": "float", + "iid": 23 + }, + { + "value": 34, + "maxValue": 100, + "minStep": 1, + "perms": ["pr", "ev"], + "unit": "percentage", + "type": "10", + "minValue": 0, + "format": "float", + "iid": 24 + }, + { + "value": 36, + "maxValue": 50, + "minStep": 1, + "perms": ["pr", "pw", "ev"], + "unit": "percentage", + "type": "34", + "minValue": 20, + "format": "float", + "iid": 25 + }, + { + "value": "HomeW", + "perms": ["pr"], + "type": "23", + "format": "string", + "iid": 27 + }, + { + "value": 0, + "perms": ["pr", "ev"], + "type": "B7DDB9A3-54BB-4572-91D2-F1F5B0510F8C", + "format": "uint8", + "iid": 33 + }, + { + "value": 22.2, + "maxValue": 26.1, + "minStep": 0.1, + "perms": ["pr", "pw", "ev"], + "unit": "celsius", + "type": "E4489BBC-5227-4569-93E5-B345E3E5508F", + "minValue": 7.2, + "format": "float", + "iid": 34 + }, + { + "value": 24.4, + "maxValue": 33.3, + "minStep": 0.1, + "perms": ["pr", "pw", "ev"], + "unit": "celsius", + "type": "7D381BAA-20F9-40E5-9BE9-AEB92D4BECEF", + "minValue": 18.3, + "format": "float", + "iid": 35 + }, + { + "value": 17.8, + "maxValue": 26.1, + "minStep": 0.1, + "perms": ["pr", "pw", "ev"], + "unit": "celsius", + "type": "73AAB542-892A-4439-879A-D2A883724B69", + "minValue": 7.2, + "format": "float", + "iid": 36 + }, + { + "value": 27.8, + "maxValue": 33.3, + "minStep": 0.1, + "perms": ["pr", "pw", "ev"], + "unit": "celsius", + "type": "5DA985F0-898A-4850-B987-B76C6C78D670", + "minValue": 18.3, + "format": "float", + "iid": 37 + }, + { + "value": 18.9, + "maxValue": 26.1, + "minStep": 0.1, + "perms": ["pr", "pw", "ev"], + "unit": "celsius", + "type": "05B97374-6DC0-439B-A0FA-CA33F612D425", + "minValue": 7.2, + "format": "float", + "iid": 38 + }, + { + "value": 26.7, + "maxValue": 33.3, + "minStep": 0.1, + "perms": ["pr", "pw", "ev"], + "unit": "celsius", + "type": "A251F6E7-AC46-4190-9C5D-3D06277BDF9F", + "minValue": 18.3, + "format": "float", + "iid": 39 + }, + { + "minValue": 0, + "maxValue": 3, + "minStep": 1, + "perms": ["pw"], + "type": "1B300BC2-CFFC-47FF-89F9-BD6CCF5F2853", + "format": "uint8", + "iid": 40 + }, + { + "value": "2014-01-03T00:00:00-05:00", + "perms": ["pr", "pw", "ev"], + "type": "1621F556-1367-443C-AF19-82AF018E99DE", + "format": "string", + "iid": 41 + }, + { + "perms": ["pw"], + "type": "FA128DE6-9D7D-49A4-B6D8-4E4E234DEE38", + "format": "bool", + "iid": 48 + }, + { + "value": 1, + "perms": ["pr", "ev"], + "type": "4A6AE4F6-036C-495D-87CC-B3702B437741", + "format": "uint8", + "iid": 49 + }, + { + "value": 0, + "perms": ["pr", "ev"], + "type": "DB7BF261-7042-4194-8BD1-3AA22830AEDD", + "format": "uint8", + "iid": 50 + }, + { + "value": false, + "perms": ["pr", "ev"], + "type": "41935E3E-B54D-42E9-B8B9-D33C6319F0AF", + "format": "bool", + "iid": 51 + }, + { + "minValue": 0, + "maxValue": 100, + "perms": ["pr", "pw", "ev"], + "type": "C35DA3C0-E004-40E3-B153-46655CDD9214", + "value": 0, + "format": "uint8", + "iid": 52 + }, + { + "value": 100, + "perms": ["pr", "ev"], + "type": "48F62AEC-4171-4B4A-8F0E-1EEB6708B3FB", + "format": "uint8", + "iid": 53 + }, + { + "value": "The Hive is humming along. You have no pending alerts or reminders.", + "perms": ["pr", "ev"], + "iid": 54, + "type": "1B1515F2-CC45-409F-991F-C480987F92C3", + "format": "string", + "maxLen": 256 + } + ], + "iid": 16 + }, + { + "type": "85", + "characteristics": [ + { + "value": "HomeW", + "perms": ["pr"], + "type": "23", + "format": "string", + "iid": 28 + }, + { + "value": false, + "perms": ["pr", "ev"], + "type": "22", + "format": "bool", + "iid": 66 + }, + { + "minValue": -1, + "maxValue": 86400, + "perms": ["pr", "ev"], + "type": "BFE61C70-4A40-11E6-BDF4-0800200C9A66", + "value": 2980, + "format": "int", + "iid": 67 + } + ], + "iid": 56 + }, + { + "type": "86", + "characteristics": [ + { + "value": "HomeW", + "perms": ["pr"], + "type": "23", + "format": "string", + "iid": 29 + }, + { + "minValue": 0, + "maxValue": 1, + "minStep": 1, + "perms": ["pr", "ev"], + "type": "71", + "value": 1, + "format": "uint8", + "iid": 65 + }, + { + "minValue": -1, + "maxValue": 86400, + "perms": ["pr", "ev"], + "type": "A8f798E0-4A40-11E6-BDF4-0800200C9A66", + "value": 2980, + "format": "int", + "iid": 68 + } + ], + "iid": 57 + } + ] + } +] diff --git a/tests/components/homekit_controller/fixtures/ecobee_occupancy.json b/tests/components/homekit_controller/fixtures/ecobee_occupancy.json index 78c98599961..4cdc58b4cb7 100644 --- a/tests/components/homekit_controller/fixtures/ecobee_occupancy.json +++ b/tests/components/homekit_controller/fixtures/ecobee_occupancy.json @@ -1,236 +1,193 @@ [ - { - "aid": 1, - "services": [ - { - "characteristics": [ - { - "format": "string", - "iid": 2, - "perms": [ - "pr" - ], - "type": "00000023-0000-1000-8000-0026BB765291", - "value": "Master Fan" - }, - { - "format": "string", - "iid": 3, - "perms": [ - "pr" - ], - "type": "00000020-0000-1000-8000-0026BB765291", - "value": "ecobee Inc." - }, - { - "format": "string", - "iid": 4, - "perms": [ - "pr" - ], - "type": "00000030-0000-1000-8000-0026BB765291", - "value": "111111111111" - }, - { - "format": "string", - "iid": 5, - "perms": [ - "pr" - ], - "type": "00000021-0000-1000-8000-0026BB765291", - "value": "ecobee Switch+" - }, - { - "format": "bool", - "iid": 6, - "perms": [ - "pw" - ], - "type": "00000014-0000-1000-8000-0026BB765291" - }, - { - "format": "string", - "iid": 8, - "perms": [ - "pr" - ], - "type": "00000052-0000-1000-8000-0026BB765291", - "value": "4.5.130201" - }, - { - "format": "uint32", - "iid": 9, - "perms": [ - "pr", - "ev" - ], - "type": "000000A6-0000-1000-8000-0026BB765291", - "value": 0 - } - ], - "iid": 1, - "stype": "accessory-information", - "type": "0000003E-0000-1000-8000-0026BB765291" - }, - { - "characteristics": [ - { - "format": "string", - "iid": 31, - "maxLen": 64, - "perms": [ - "pr" - ], - "type": "00000037-0000-1000-8000-0026BB765291", - "value": "1.1.0" - } - ], - "iid": 30, - "stype": "service", - "type": "000000A2-0000-1000-8000-0026BB765291" - }, - { - "characteristics": [ - { - "format": "bool", - "iid": 17, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "00000025-0000-1000-8000-0026BB765291", - "value": false - }, - { - "format": "string", - "iid": 18, - "perms": [ - "pr" - ], - "type": "00000023-0000-1000-8000-0026BB765291", - "value": "Master Fan" - } - ], - "iid": 16, - "primary": true, - "stype": "switch", - "type": "00000049-0000-1000-8000-0026BB765291" - }, - { - "characteristics": [ - { - "format": "float", - "iid": 20, - "maxValue": 100000, - "minValue": 0, - "perms": [ - "pr", - "ev" - ], - "type": "0000006B-0000-1000-8000-0026BB765291", - "unit": "lux", - "value": 0 - }, - { - "format": "string", - "iid": 21, - "perms": [ - "pr" - ], - "type": "00000023-0000-1000-8000-0026BB765291", - "value": "Master Fan" - } - ], - "iid": 27, - "stype": "light", - "type": "00000084-0000-1000-8000-0026BB765291" - }, - { - "characteristics": [ - { - "format": "bool", - "iid": 66, - "perms": [ - "pr", - "ev" - ], - "type": "00000022-0000-1000-8000-0026BB765291", - "value": false - }, - { - "format": "string", - "iid": 28, - "perms": [ - "pr" - ], - "type": "00000023-0000-1000-8000-0026BB765291", - "value": "Master Fan" - } - ], - "iid": 56, - "stype": "motion", - "type": "00000085-0000-1000-8000-0026BB765291" - }, - { - "characteristics": [ - { - "format": "uint8", - "iid": 65, - "maxValue": 1, - "minStep": 1, - "minValue": 0, - "perms": [ - "pr", - "ev" - ], - "type": "00000071-0000-1000-8000-0026BB765291", - "value": 0 - }, - { - "format": "string", - "iid": 29, - "perms": [ - "pr" - ], - "type": "00000023-0000-1000-8000-0026BB765291", - "value": "Master Fan" - } - ], - "iid": 57, - "stype": "occupancy", - "type": "00000086-0000-1000-8000-0026BB765291" - }, - { - "characteristics": [ - { - "format": "float", - "iid": 19, - "maxValue": 100, - "minStep": 0.1, - "minValue": 0, - "perms": [ - "pr", - "ev" - ], - "type": "00000011-0000-1000-8000-0026BB765291", - "unit": "celsius", - "value": 25.6 - }, - { - "format": "string", - "iid": 22, - "perms": [ - "pr" - ], - "type": "00000023-0000-1000-8000-0026BB765291", - "value": "Master Fan" - } - ], - "iid": 55, - "stype": "temperature", - "type": "0000008A-0000-1000-8000-0026BB765291" - } - ] - } -] \ No newline at end of file + { + "aid": 1, + "services": [ + { + "characteristics": [ + { + "format": "string", + "iid": 2, + "perms": ["pr"], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Master Fan" + }, + { + "format": "string", + "iid": 3, + "perms": ["pr"], + "type": "00000020-0000-1000-8000-0026BB765291", + "value": "ecobee Inc." + }, + { + "format": "string", + "iid": 4, + "perms": ["pr"], + "type": "00000030-0000-1000-8000-0026BB765291", + "value": "111111111111" + }, + { + "format": "string", + "iid": 5, + "perms": ["pr"], + "type": "00000021-0000-1000-8000-0026BB765291", + "value": "ecobee Switch+" + }, + { + "format": "bool", + "iid": 6, + "perms": ["pw"], + "type": "00000014-0000-1000-8000-0026BB765291" + }, + { + "format": "string", + "iid": 8, + "perms": ["pr"], + "type": "00000052-0000-1000-8000-0026BB765291", + "value": "4.5.130201" + }, + { + "format": "uint32", + "iid": 9, + "perms": ["pr", "ev"], + "type": "000000A6-0000-1000-8000-0026BB765291", + "value": 0 + } + ], + "iid": 1, + "stype": "accessory-information", + "type": "0000003E-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "format": "string", + "iid": 31, + "maxLen": 64, + "perms": ["pr"], + "type": "00000037-0000-1000-8000-0026BB765291", + "value": "1.1.0" + } + ], + "iid": 30, + "stype": "service", + "type": "000000A2-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "format": "bool", + "iid": 17, + "perms": ["pr", "pw", "ev"], + "type": "00000025-0000-1000-8000-0026BB765291", + "value": false + }, + { + "format": "string", + "iid": 18, + "perms": ["pr"], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Master Fan" + } + ], + "iid": 16, + "primary": true, + "stype": "switch", + "type": "00000049-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "format": "float", + "iid": 20, + "maxValue": 100000, + "minValue": 0, + "perms": ["pr", "ev"], + "type": "0000006B-0000-1000-8000-0026BB765291", + "unit": "lux", + "value": 0 + }, + { + "format": "string", + "iid": 21, + "perms": ["pr"], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Master Fan" + } + ], + "iid": 27, + "stype": "light", + "type": "00000084-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "format": "bool", + "iid": 66, + "perms": ["pr", "ev"], + "type": "00000022-0000-1000-8000-0026BB765291", + "value": false + }, + { + "format": "string", + "iid": 28, + "perms": ["pr"], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Master Fan" + } + ], + "iid": 56, + "stype": "motion", + "type": "00000085-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "format": "uint8", + "iid": 65, + "maxValue": 1, + "minStep": 1, + "minValue": 0, + "perms": ["pr", "ev"], + "type": "00000071-0000-1000-8000-0026BB765291", + "value": 0 + }, + { + "format": "string", + "iid": 29, + "perms": ["pr"], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Master Fan" + } + ], + "iid": 57, + "stype": "occupancy", + "type": "00000086-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "format": "float", + "iid": 19, + "maxValue": 100, + "minStep": 0.1, + "minValue": 0, + "perms": ["pr", "ev"], + "type": "00000011-0000-1000-8000-0026BB765291", + "unit": "celsius", + "value": 25.6 + }, + { + "format": "string", + "iid": 22, + "perms": ["pr"], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Master Fan" + } + ], + "iid": 55, + "stype": "temperature", + "type": "0000008A-0000-1000-8000-0026BB765291" + } + ] + } +] diff --git a/tests/components/homekit_controller/fixtures/eve_degree.json b/tests/components/homekit_controller/fixtures/eve_degree.json index 2a1217789c4..5224f0c3397 100644 --- a/tests/components/homekit_controller/fixtures/eve_degree.json +++ b/tests/components/homekit_controller/fixtures/eve_degree.json @@ -1,382 +1,305 @@ [ - { - "aid": 1, - "services": [ - { - "characteristics": [ - { - "format": "string", - "iid": 2, - "perms": [ - "pr" - ], - "type": "00000023-0000-1000-8000-0026BB765291", - "value": "Eve Degree AA11" - }, - { - "format": "bool", - "iid": 3, - "perms": [ - "pw" - ], - "type": "00000014-0000-1000-8000-0026BB765291" - }, - { - "format": "string", - "iid": 4, - "perms": [ - "pr" - ], - "type": "00000020-0000-1000-8000-0026BB765291", - "value": "Elgato" - }, - { - "format": "string", - "iid": 5, - "perms": [ - "pr" - ], - "type": "00000021-0000-1000-8000-0026BB765291", - "value": "Eve Degree 00AAA0000" - }, - { - "format": "string", - "iid": 6, - "perms": [ - "pr" - ], - "type": "00000030-0000-1000-8000-0026BB765291", - "value": "AA00A0A00000" - }, - { - "format": "string", - "iid": 7, - "perms": [ - "pr" - ], - "type": "00000052-0000-1000-8000-0026BB765291", - "value": "1.2.8" - }, - { - "format": "string", - "iid": 8, - "perms": [ - "pr" - ], - "type": "00000053-0000-1000-8000-0026BB765291", - "value": "1.0.0" - } - ], - "iid": 1, - "type": "0000003E-0000-1000-8000-0026BB765291" - }, - { - "characteristics": [ - { - "format": "string", - "iid": 18, - "perms": [ - "pr" - ], - "type": "00000023-0000-1000-8000-0026BB765291", - "value": "Battery" - }, - { - "format": "uint8", - "iid": 19, - "maxValue": 100, - "minStep": 1, - "minValue": 0, - "perms": [ - "pr", - "ev" - ], - "type": "00000068-0000-1000-8000-0026BB765291", - "unit": "percentage", - "value": 65 - }, - { - "format": "uint8", - "iid": 20, - "maxValue": 2, - "minStep": 1, - "minValue": 0, - "perms": [ - "pr", - "ev" - ], - "type": "0000008F-0000-1000-8000-0026BB765291", - "value": 2 - }, - { - "format": "uint8", - "iid": 21, - "maxValue": 1, - "minStep": 1, - "minValue": 0, - "perms": [ - "pr", - "ev" - ], - "type": "00000079-0000-1000-8000-0026BB765291", - "value": 0 - } - ], - "iid": 17, - "type": "00000096-0000-1000-8000-0026BB765291" - }, - { - "characteristics": [ - { - "format": "string", - "iid": 23, - "perms": [ - "pr" - ], - "type": "00000023-0000-1000-8000-0026BB765291", - "value": "Eve Degree" - }, - { - "format": "float", - "iid": 24, - "maxValue": 100, - "minStep": 0.1, - "minValue": -30, - "perms": [ - "pr", - "ev" - ], - "type": "00000011-0000-1000-8000-0026BB765291", - "unit": "celsius", - "value": 22.77191162109375 - }, - { - "format": "uint8", - "iid": 25, - "maxValue": 1, - "minStep": 1, - "minValue": 0, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "00000036-0000-1000-8000-0026BB765291", - "value": 0 - } - ], - "iid": 22, - "type": "0000008A-0000-1000-8000-0026BB765291", - "primary": true - }, - { - "characteristics": [ - { - "format": "string", - "iid": 28, - "perms": [ - "pr" - ], - "type": "00000023-0000-1000-8000-0026BB765291", - "value": "Eve Degree" - }, - { - "format": "float", - "iid": 29, - "maxValue": 100, - "minStep": 1, - "minValue": 0, - "perms": [ - "pr", - "ev" - ], - "type": "00000010-0000-1000-8000-0026BB765291", - "unit": "percentage", - "value": 59.4818115234375 - } - ], - "iid": 27, - "type": "00000082-0000-1000-8000-0026BB765291" - }, - { - "characteristics": [ - { - "format": "string", - "iid": 31, - "perms": [ - "pr" - ], - "type": "00000023-0000-1000-8000-0026BB765291", - "value": "Eve Degree" - }, - { - "format": "float", - "iid": 32, - "maxValue": 1100, - "minStep": 1, - "minValue": 870, - "perms": [ - "pr" - ], - "type": "E863F10F-079E-48FF-8F27-9C2605A29F52", - "value": 1005.7000122070312 - }, - { - "format": "float", - "iid": 33, - "maxValue": 9000, - "minStep": 1, - "minValue": -450, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "E863F130-079E-48FF-8F27-9C2605A29F52", - "value": 0 - }, - { - "format": "uint8", - "iid": 34, - "maxValue": 4, - "minStep": 1, - "minValue": 0, - "perms": [ - "pr", - "ev" - ], - "type": "E863F135-079E-48FF-8F27-9C2605A29F52", - "value": 0 - } - ], - "iid": 30, - "type": "E863F00A-079E-48FF-8F27-9C2605A29F52" - }, - { - "characteristics": [ - { - "format": "string", - "iid": 36, - "perms": [ - "pr" - ], - "type": "00000023-0000-1000-8000-0026BB765291", - "value": "Logging" - }, - { - "format": "data", - "iid": 37, - "perms": [ - "pr", - "pw" - ], - "type": "E863F11E-079E-48FF-8F27-9C2605A29F52", - "value": "HwABDh4AeAQKAIDVzj5aDMB/" - }, - { - "format": "uint32", - "iid": 38, - "maxValue": 4294967295, - "minStep": 1, - "minValue": 0, - "perms": [ - "pr", - "pw" - ], - "type": "E863F112-079E-48FF-8F27-9C2605A29F52", - "value": 0 - }, - { - "format": "data", - "iid": 39, - "perms": [ - "pw" - ], - "type": "E863F11C-079E-48FF-8F27-9C2605A29F52" - }, - { - "format": "data", - "iid": 40, - "perms": [ - "pw" - ], - "type": "E863F121-079E-48FF-8F27-9C2605A29F52" - }, - { - "format": "data", - "iid": 41, - "perms": [ - "pr" - ], - "type": "E863F116-079E-48FF-8F27-9C2605A29F52", - "value": "/wkAAJEGAABnvbUmBQECAgIDAh4BJwEGAAAQuvIBAAEAAAABAA==" - }, - { - "format": "data", - "iid": 42, - "perms": [ - "pr" - ], - "type": "E863F117-079E-48FF-8F27-9C2605A29F52", - "value": "" - }, - { - "format": "tlv8", - "iid": 43, - "perms": [ - "pr" - ], - "type": "E863F131-079E-48FF-8F27-9C2605A29F52", - "value": "AAIeAAMCeAQEDFNVMTNHMUEwMDI4MAYCBgAHBLryAQALAgAABQEAAgTwKQAAXwQAAAAAGQIABRQBAw8EAAAAABoEAAAAACUE9griHtJHEAABQEJcLdwpUbihgRCESYX8bA7yLTF6IKhlxv5ohrqDkOEyRTNCM0VDNC1CNENCLTg0MjYtM0Q1QS0zMDJFNEIzRTZERDA=" - }, - { - "format": "tlv8", - "iid": 44, - "perms": [ - "pw" - ], - "type": "E863F11D-079E-48FF-8F27-9C2605A29F52" - } - ], - "iid": 35, - "type": "E863F007-079E-48FF-8F27-9C2605A29F52", - "hidden": true - }, - { - "characteristics": [ - { - "format": "string", - "iid": 100001, - "perms": [ - "pr" - ], - "type": "E863F155-079E-48FF-8F27-9C2605A29F52", - "value": "11:11:11:11:11:11" - }, - { - "format": "uint16", - "iid": 100002, - "perms": [ - "pr" - ], - "type": "E863F156-079E-48FF-8F27-9C2605A29F52", - "value": 10 - }, - { - "format": "uint8", - "iid": 100003, - "perms": [ - "pr", - "ev" - ], - "type": "E863F157-079E-48FF-8F27-9C2605A29F52", - "value": 1 - } - ], - "hidden": true, - "iid": 100000, - "type": "E863F00B-079E-48FF-8F27-9C2605A29F52" - } - ] - } -] \ No newline at end of file + { + "aid": 1, + "services": [ + { + "characteristics": [ + { + "format": "string", + "iid": 2, + "perms": ["pr"], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Eve Degree AA11" + }, + { + "format": "bool", + "iid": 3, + "perms": ["pw"], + "type": "00000014-0000-1000-8000-0026BB765291" + }, + { + "format": "string", + "iid": 4, + "perms": ["pr"], + "type": "00000020-0000-1000-8000-0026BB765291", + "value": "Elgato" + }, + { + "format": "string", + "iid": 5, + "perms": ["pr"], + "type": "00000021-0000-1000-8000-0026BB765291", + "value": "Eve Degree 00AAA0000" + }, + { + "format": "string", + "iid": 6, + "perms": ["pr"], + "type": "00000030-0000-1000-8000-0026BB765291", + "value": "AA00A0A00000" + }, + { + "format": "string", + "iid": 7, + "perms": ["pr"], + "type": "00000052-0000-1000-8000-0026BB765291", + "value": "1.2.8" + }, + { + "format": "string", + "iid": 8, + "perms": ["pr"], + "type": "00000053-0000-1000-8000-0026BB765291", + "value": "1.0.0" + } + ], + "iid": 1, + "type": "0000003E-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "format": "string", + "iid": 18, + "perms": ["pr"], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Battery" + }, + { + "format": "uint8", + "iid": 19, + "maxValue": 100, + "minStep": 1, + "minValue": 0, + "perms": ["pr", "ev"], + "type": "00000068-0000-1000-8000-0026BB765291", + "unit": "percentage", + "value": 65 + }, + { + "format": "uint8", + "iid": 20, + "maxValue": 2, + "minStep": 1, + "minValue": 0, + "perms": ["pr", "ev"], + "type": "0000008F-0000-1000-8000-0026BB765291", + "value": 2 + }, + { + "format": "uint8", + "iid": 21, + "maxValue": 1, + "minStep": 1, + "minValue": 0, + "perms": ["pr", "ev"], + "type": "00000079-0000-1000-8000-0026BB765291", + "value": 0 + } + ], + "iid": 17, + "type": "00000096-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "format": "string", + "iid": 23, + "perms": ["pr"], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Eve Degree" + }, + { + "format": "float", + "iid": 24, + "maxValue": 100, + "minStep": 0.1, + "minValue": -30, + "perms": ["pr", "ev"], + "type": "00000011-0000-1000-8000-0026BB765291", + "unit": "celsius", + "value": 22.77191162109375 + }, + { + "format": "uint8", + "iid": 25, + "maxValue": 1, + "minStep": 1, + "minValue": 0, + "perms": ["pr", "pw", "ev"], + "type": "00000036-0000-1000-8000-0026BB765291", + "value": 0 + } + ], + "iid": 22, + "type": "0000008A-0000-1000-8000-0026BB765291", + "primary": true + }, + { + "characteristics": [ + { + "format": "string", + "iid": 28, + "perms": ["pr"], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Eve Degree" + }, + { + "format": "float", + "iid": 29, + "maxValue": 100, + "minStep": 1, + "minValue": 0, + "perms": ["pr", "ev"], + "type": "00000010-0000-1000-8000-0026BB765291", + "unit": "percentage", + "value": 59.4818115234375 + } + ], + "iid": 27, + "type": "00000082-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "format": "string", + "iid": 31, + "perms": ["pr"], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Eve Degree" + }, + { + "format": "float", + "iid": 32, + "maxValue": 1100, + "minStep": 1, + "minValue": 870, + "perms": ["pr"], + "type": "E863F10F-079E-48FF-8F27-9C2605A29F52", + "value": 1005.7000122070312 + }, + { + "format": "float", + "iid": 33, + "maxValue": 9000, + "minStep": 1, + "minValue": -450, + "perms": ["pr", "pw", "ev"], + "type": "E863F130-079E-48FF-8F27-9C2605A29F52", + "value": 0 + }, + { + "format": "uint8", + "iid": 34, + "maxValue": 4, + "minStep": 1, + "minValue": 0, + "perms": ["pr", "ev"], + "type": "E863F135-079E-48FF-8F27-9C2605A29F52", + "value": 0 + } + ], + "iid": 30, + "type": "E863F00A-079E-48FF-8F27-9C2605A29F52" + }, + { + "characteristics": [ + { + "format": "string", + "iid": 36, + "perms": ["pr"], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Logging" + }, + { + "format": "data", + "iid": 37, + "perms": ["pr", "pw"], + "type": "E863F11E-079E-48FF-8F27-9C2605A29F52", + "value": "HwABDh4AeAQKAIDVzj5aDMB/" + }, + { + "format": "uint32", + "iid": 38, + "maxValue": 4294967295, + "minStep": 1, + "minValue": 0, + "perms": ["pr", "pw"], + "type": "E863F112-079E-48FF-8F27-9C2605A29F52", + "value": 0 + }, + { + "format": "data", + "iid": 39, + "perms": ["pw"], + "type": "E863F11C-079E-48FF-8F27-9C2605A29F52" + }, + { + "format": "data", + "iid": 40, + "perms": ["pw"], + "type": "E863F121-079E-48FF-8F27-9C2605A29F52" + }, + { + "format": "data", + "iid": 41, + "perms": ["pr"], + "type": "E863F116-079E-48FF-8F27-9C2605A29F52", + "value": "/wkAAJEGAABnvbUmBQECAgIDAh4BJwEGAAAQuvIBAAEAAAABAA==" + }, + { + "format": "data", + "iid": 42, + "perms": ["pr"], + "type": "E863F117-079E-48FF-8F27-9C2605A29F52", + "value": "" + }, + { + "format": "tlv8", + "iid": 43, + "perms": ["pr"], + "type": "E863F131-079E-48FF-8F27-9C2605A29F52", + "value": "AAIeAAMCeAQEDFNVMTNHMUEwMDI4MAYCBgAHBLryAQALAgAABQEAAgTwKQAAXwQAAAAAGQIABRQBAw8EAAAAABoEAAAAACUE9griHtJHEAABQEJcLdwpUbihgRCESYX8bA7yLTF6IKhlxv5ohrqDkOEyRTNCM0VDNC1CNENCLTg0MjYtM0Q1QS0zMDJFNEIzRTZERDA=" + }, + { + "format": "tlv8", + "iid": 44, + "perms": ["pw"], + "type": "E863F11D-079E-48FF-8F27-9C2605A29F52" + } + ], + "iid": 35, + "type": "E863F007-079E-48FF-8F27-9C2605A29F52", + "hidden": true + }, + { + "characteristics": [ + { + "format": "string", + "iid": 100001, + "perms": ["pr"], + "type": "E863F155-079E-48FF-8F27-9C2605A29F52", + "value": "11:11:11:11:11:11" + }, + { + "format": "uint16", + "iid": 100002, + "perms": ["pr"], + "type": "E863F156-079E-48FF-8F27-9C2605A29F52", + "value": 10 + }, + { + "format": "uint8", + "iid": 100003, + "perms": ["pr", "ev"], + "type": "E863F157-079E-48FF-8F27-9C2605A29F52", + "value": 1 + } + ], + "hidden": true, + "iid": 100000, + "type": "E863F00B-079E-48FF-8F27-9C2605A29F52" + } + ] + } +] diff --git a/tests/components/homekit_controller/fixtures/eve_energy.json b/tests/components/homekit_controller/fixtures/eve_energy.json index f798476ec14..454f86a4f09 100644 --- a/tests/components/homekit_controller/fixtures/eve_energy.json +++ b/tests/components/homekit_controller/fixtures/eve_energy.json @@ -1,299 +1,236 @@ [ - { - "aid": 1, - "services": [ - { - "characteristics": [ - { - "format": "string", - "iid": 2, - "perms": [ - "pr" - ], - "type": "00000023-0000-1000-8000-0026BB765291", - "value": "Eve Energy 50FF" - }, - { - "format": "bool", - "iid": 3, - "perms": [ - "pw" - ], - "type": "00000014-0000-1000-8000-0026BB765291" - }, - { - "format": "string", - "iid": 4, - "perms": [ - "pr" - ], - "type": "00000020-0000-1000-8000-0026BB765291", - "value": "Elgato" - }, - { - "format": "string", - "iid": 5, - "perms": [ - "pr" - ], - "type": "00000021-0000-1000-8000-0026BB765291", - "value": "Eve Energy 20EAO8601" - }, - { - "format": "string", - "iid": 6, - "perms": [ - "pr" - ], - "type": "00000030-0000-1000-8000-0026BB765291", - "value": "AA00A0A00000" - }, - { - "format": "string", - "iid": 7, - "perms": [ - "pr" - ], - "type": "00000052-0000-1000-8000-0026BB765291", - "value": "1.2.9" - }, - { - "format": "string", - "iid": 8, - "perms": [ - "pr" - ], - "type": "00000053-0000-1000-8000-0026BB765291", - "value": "1.0.0" - } - ], - "iid": 1, - "type": "0000003E-0000-1000-8000-0026BB765291" - }, - { - "characteristics": [ - { - "format": "string", - "iid": 18, - "perms": [ - "pr" - ], - "type": "00000023-0000-1000-8000-0026BB765291", - "value": "Logging" - }, - { - "format": "data", - "iid": 19, - "perms": [ - "pr", - "pw" - ], - "type": "E863F11E-079E-48FF-8F27-9C2605A29F52", - "value": "HwABDigAuAQKAGLrnNSTogf+" - }, - { - "format": "uint32", - "iid": 20, - "maxValue": 4294967295, - "minStep": 1, - "minValue": 0, - "perms": [ - "pr", - "pw" - ], - "type": "E863F112-079E-48FF-8F27-9C2605A29F52", - "value": 0 - }, - { - "format": "data", - "iid": 21, - "perms": [ - "pw" - ], - "type": "E863F11C-079E-48FF-8F27-9C2605A29F52" - }, - { - "format": "data", - "iid": 22, - "perms": [ - "pw" - ], - "type": "E863F121-079E-48FF-8F27-9C2605A29F52" - }, - { - "format": "data", - "iid": 23, - "perms": [ - "pr" - ], - "type": "E863F116-079E-48FF-8F27-9C2605A29F52", - "value": "QFUeAAAAAAAAAAAABQsCDAINAgcCDgEkDQAQaCMBAAEAAAABAA==" - }, - { - "format": "data", - "iid": 24, - "perms": [ - "pr" - ], - "type": "E863F117-079E-48FF-8F27-9C2605A29F52", - "value": "EmojAQABAAAADwAAAAAAAAAA" - }, - { - "format": "tlv8", - "iid": 25, - "perms": [ - "pr" - ], - "type": "E863F131-079E-48FF-8F27-9C2605A29F52", - "value": "AAIoAAMCuAQEDEZWMzVHMUEwMjkyOAYCJA0HBGgjAQALAgAABQEAAgRoOQAAXwQAAAAAGQKWABQBAw8EAAAAAEUFBQAAAABGCQUAAAAOAABCBkQRBRQABQMAAAAAAAAAAAAAAABHEQVzG0Uc3xy4HXgAAAA8AAAASAYFAAAAAABKBgUAAAAAABoEAAAAAGABZNAEE7sdAJsEQFUeANIA" - }, - { - "format": "tlv8", - "iid": 26, - "perms": [ - "pw" - ], - "type": "E863F11D-079E-48FF-8F27-9C2605A29F52" - } - ], - "iid": 17, - "type": "E863F007-079E-48FF-8F27-9C2605A29F52", - "hidden": true - }, - { - "characteristics": [ - { - "format": "string", - "iid": 29, - "perms": [ - "pr" - ], - "type": "00000023-0000-1000-8000-0026BB765291", - "value": "Eve Energy" - }, - { - "format": "bool", - "iid": 30, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "00000025-0000-1000-8000-0026BB765291", - "value": false - }, - { - "format": "bool", - "iid": 31, - "perms": [ - "pr", - "ev" - ], - "type": "00000026-0000-1000-8000-0026BB765291", - "value": true - }, - { - "format": "float", - "iid": 32, - "maxValue": 380, - "minStep": 0.1, - "minValue": 0, - "perms": [ - "pr" - ], - "type": "E863F10A-079E-48FF-8F27-9C2605A29F52", - "value": 0.4000000059604645 - }, - { - "format": "float", - "iid": 33, - "maxValue": 16, - "minStep": 0.1, - "minValue": 0, - "perms": [ - "pr" - ], - "type": "E863F126-079E-48FF-8F27-9C2605A29F52", - "value": 0 - }, - { - "format": "float", - "iid": 34, - "maxValue": 5000, - "minStep": 0.1, - "minValue": 0, - "perms": [ - "pr", - "ev" - ], - "type": "E863F10D-079E-48FF-8F27-9C2605A29F52", - "value": 0 - }, - { - "format": "float", - "iid": 35, - "minStep": 0.1, - "minValue": 0, - "perms": [ - "pr" - ], - "type": "E863F10C-079E-48FF-8F27-9C2605A29F52", - "value": 0.28999999165534973 - }, - { - "format": "uint8", - "iid": 36, - "maxValue": 1, - "minStep": 1, - "minValue": 0, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "000000A7-0000-1000-8000-0026BB765291", - "value": 0 - } - ], - "iid": 28, - "type": "00000047-0000-1000-8000-0026BB765291", - "primary": true - }, - { - "characteristics": [ - { - "format": "string", - "iid": 100001, - "perms": [ - "pr" - ], - "type": "E863F155-079E-48FF-8F27-9C2605A29F52", - "value": "03:29:72:EF:97:2F" - }, - { - "format": "uint16", - "iid": 100002, - "perms": [ - "pr" - ], - "type": "E863F156-079E-48FF-8F27-9C2605A29F52", - "value": 7 - }, - { - "format": "uint8", - "iid": 100003, - "perms": [ - "pr", - "ev" - ], - "type": "E863F157-079E-48FF-8F27-9C2605A29F52", - "value": 0 - } - ], - "hidden": true, - "iid": 100000, - "type": "E863F00B-079E-48FF-8F27-9C2605A29F52" - } - ] - } -] \ No newline at end of file + { + "aid": 1, + "services": [ + { + "characteristics": [ + { + "format": "string", + "iid": 2, + "perms": ["pr"], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Eve Energy 50FF" + }, + { + "format": "bool", + "iid": 3, + "perms": ["pw"], + "type": "00000014-0000-1000-8000-0026BB765291" + }, + { + "format": "string", + "iid": 4, + "perms": ["pr"], + "type": "00000020-0000-1000-8000-0026BB765291", + "value": "Elgato" + }, + { + "format": "string", + "iid": 5, + "perms": ["pr"], + "type": "00000021-0000-1000-8000-0026BB765291", + "value": "Eve Energy 20EAO8601" + }, + { + "format": "string", + "iid": 6, + "perms": ["pr"], + "type": "00000030-0000-1000-8000-0026BB765291", + "value": "AA00A0A00000" + }, + { + "format": "string", + "iid": 7, + "perms": ["pr"], + "type": "00000052-0000-1000-8000-0026BB765291", + "value": "1.2.9" + }, + { + "format": "string", + "iid": 8, + "perms": ["pr"], + "type": "00000053-0000-1000-8000-0026BB765291", + "value": "1.0.0" + } + ], + "iid": 1, + "type": "0000003E-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "format": "string", + "iid": 18, + "perms": ["pr"], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Logging" + }, + { + "format": "data", + "iid": 19, + "perms": ["pr", "pw"], + "type": "E863F11E-079E-48FF-8F27-9C2605A29F52", + "value": "HwABDigAuAQKAGLrnNSTogf+" + }, + { + "format": "uint32", + "iid": 20, + "maxValue": 4294967295, + "minStep": 1, + "minValue": 0, + "perms": ["pr", "pw"], + "type": "E863F112-079E-48FF-8F27-9C2605A29F52", + "value": 0 + }, + { + "format": "data", + "iid": 21, + "perms": ["pw"], + "type": "E863F11C-079E-48FF-8F27-9C2605A29F52" + }, + { + "format": "data", + "iid": 22, + "perms": ["pw"], + "type": "E863F121-079E-48FF-8F27-9C2605A29F52" + }, + { + "format": "data", + "iid": 23, + "perms": ["pr"], + "type": "E863F116-079E-48FF-8F27-9C2605A29F52", + "value": "QFUeAAAAAAAAAAAABQsCDAINAgcCDgEkDQAQaCMBAAEAAAABAA==" + }, + { + "format": "data", + "iid": 24, + "perms": ["pr"], + "type": "E863F117-079E-48FF-8F27-9C2605A29F52", + "value": "EmojAQABAAAADwAAAAAAAAAA" + }, + { + "format": "tlv8", + "iid": 25, + "perms": ["pr"], + "type": "E863F131-079E-48FF-8F27-9C2605A29F52", + "value": "AAIoAAMCuAQEDEZWMzVHMUEwMjkyOAYCJA0HBGgjAQALAgAABQEAAgRoOQAAXwQAAAAAGQKWABQBAw8EAAAAAEUFBQAAAABGCQUAAAAOAABCBkQRBRQABQMAAAAAAAAAAAAAAABHEQVzG0Uc3xy4HXgAAAA8AAAASAYFAAAAAABKBgUAAAAAABoEAAAAAGABZNAEE7sdAJsEQFUeANIA" + }, + { + "format": "tlv8", + "iid": 26, + "perms": ["pw"], + "type": "E863F11D-079E-48FF-8F27-9C2605A29F52" + } + ], + "iid": 17, + "type": "E863F007-079E-48FF-8F27-9C2605A29F52", + "hidden": true + }, + { + "characteristics": [ + { + "format": "string", + "iid": 29, + "perms": ["pr"], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Eve Energy" + }, + { + "format": "bool", + "iid": 30, + "perms": ["pr", "pw", "ev"], + "type": "00000025-0000-1000-8000-0026BB765291", + "value": false + }, + { + "format": "bool", + "iid": 31, + "perms": ["pr", "ev"], + "type": "00000026-0000-1000-8000-0026BB765291", + "value": true + }, + { + "format": "float", + "iid": 32, + "maxValue": 380, + "minStep": 0.1, + "minValue": 0, + "perms": ["pr"], + "type": "E863F10A-079E-48FF-8F27-9C2605A29F52", + "value": 0.4000000059604645 + }, + { + "format": "float", + "iid": 33, + "maxValue": 16, + "minStep": 0.1, + "minValue": 0, + "perms": ["pr"], + "type": "E863F126-079E-48FF-8F27-9C2605A29F52", + "value": 0 + }, + { + "format": "float", + "iid": 34, + "maxValue": 5000, + "minStep": 0.1, + "minValue": 0, + "perms": ["pr", "ev"], + "type": "E863F10D-079E-48FF-8F27-9C2605A29F52", + "value": 0 + }, + { + "format": "float", + "iid": 35, + "minStep": 0.1, + "minValue": 0, + "perms": ["pr"], + "type": "E863F10C-079E-48FF-8F27-9C2605A29F52", + "value": 0.28999999165534973 + }, + { + "format": "uint8", + "iid": 36, + "maxValue": 1, + "minStep": 1, + "minValue": 0, + "perms": ["pr", "pw", "ev"], + "type": "000000A7-0000-1000-8000-0026BB765291", + "value": 0 + } + ], + "iid": 28, + "type": "00000047-0000-1000-8000-0026BB765291", + "primary": true + }, + { + "characteristics": [ + { + "format": "string", + "iid": 100001, + "perms": ["pr"], + "type": "E863F155-079E-48FF-8F27-9C2605A29F52", + "value": "03:29:72:EF:97:2F" + }, + { + "format": "uint16", + "iid": 100002, + "perms": ["pr"], + "type": "E863F156-079E-48FF-8F27-9C2605A29F52", + "value": 7 + }, + { + "format": "uint8", + "iid": 100003, + "perms": ["pr", "ev"], + "type": "E863F157-079E-48FF-8F27-9C2605A29F52", + "value": 0 + } + ], + "hidden": true, + "iid": 100000, + "type": "E863F00B-079E-48FF-8F27-9C2605A29F52" + } + ] + } +] diff --git a/tests/components/homekit_controller/fixtures/haa_fan.json b/tests/components/homekit_controller/fixtures/haa_fan.json index b06ccdf9644..14a215d01fe 100644 --- a/tests/components/homekit_controller/fixtures/haa_fan.json +++ b/tests/components/homekit_controller/fixtures/haa_fan.json @@ -1,257 +1,211 @@ [ - { - "aid": 1, - "services": [ - { - "iid": 1, - "type": "0000003E-0000-1000-8000-0026BB765291", - "primary": false, - "hidden": false, - "characteristics": [ - { - "aid": 2, - "iid": 2, - "type": "00000023-0000-1000-8000-0026BB765291", - "perms": [ - "pr" - ], - "format": "string", - "value": "HAA-C718B3" - }, - { - "aid": 2, - "iid": 3, - "type": "00000020-0000-1000-8000-0026BB765291", - "perms": [ - "pr" - ], - "format": "string", - "value": "Jos\u00e9 A. Jim\u00e9nez Campos" - }, - { - "aid": 1, - "iid": 4, - "type": "00000030-0000-1000-8000-0026BB765291", - "perms": [ - "pr" - ], - "format": "string", - "value": "C718B3-1" - }, - { - "aid": 2, - "iid": 5, - "type": "00000021-0000-1000-8000-0026BB765291", - "perms": [ - "pr" - ], - "format": "string", - "value": "RavenSystem HAA" - }, - { - "aid": 2, - "iid": 6, - "type": "00000052-0000-1000-8000-0026BB765291", - "perms": [ - "pr" - ], - "format": "string", - "value": "5.0.18" - }, - { - "aid": 2, - "iid": 7, - "type": "00000014-0000-1000-8000-0026BB765291", - "perms": [ - "pw" - ], - "format": "bool" - } - ] - }, - { - "iid": 8, - "type": "00000040-0000-1000-8000-0026BB765291", - "primary": true, - "hidden": false, - "characteristics": [ - { - "aid": 1, - "iid": 9, - "type": "00000025-0000-1000-8000-0026BB765291", - "perms": [ - "pr", - "pw", - "ev" - ], - "ev": true, - "format": "bool", - "value": false - }, - { - "aid": 1, - "iid": 10, - "type": "00000029-0000-1000-8000-0026BB765291", - "perms": [ - "pr", - "pw", - "ev" - ], - "ev": true, - "format": "float", - "unit": "percentage", - "minValue": 0, - "maxValue": 3, - "minStep": 1, - "value": 3 - } - ] - }, - { - "iid": 1000, - "type": "000000A2-0000-1000-8000-0026BB765291", - "primary": false, - "hidden": true, - "characteristics": [ - { - "aid": 1, - "iid": 1001, - "type": "00000037-0000-1000-8000-0026BB765291", - "perms": [ - "pr" - ], - "format": "string", - "value": "1.1.0" - } - ] - }, - { - "iid": 1010, - "type": "F0000100-0218-2017-81BF-AF2B7C833922", - "primary": false, - "hidden": true, - "characteristics": [ - { - "aid": 1, - "iid": 1011, - "type": "F0000101-0218-2017-81BF-AF2B7C833922", - "perms": [ - "pr", - "pw", - "hd" - ], - "description": "Update", - "format": "string", - "value": "" - }, - { - "aid": 1, - "iid": 1012, - "type": "F0000102-0218-2017-81BF-AF2B7C833922", - "perms": [ - "pr", - "pw", - "hd" - ], - "description": "Setup", - "format": "string", - "value": "" - } - ] - } + { + "aid": 1, + "services": [ + { + "iid": 1, + "type": "0000003E-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "characteristics": [ + { + "aid": 2, + "iid": 2, + "type": "00000023-0000-1000-8000-0026BB765291", + "perms": ["pr"], + "format": "string", + "value": "HAA-C718B3" + }, + { + "aid": 2, + "iid": 3, + "type": "00000020-0000-1000-8000-0026BB765291", + "perms": ["pr"], + "format": "string", + "value": "Jos\u00e9 A. Jim\u00e9nez Campos" + }, + { + "aid": 1, + "iid": 4, + "type": "00000030-0000-1000-8000-0026BB765291", + "perms": ["pr"], + "format": "string", + "value": "C718B3-1" + }, + { + "aid": 2, + "iid": 5, + "type": "00000021-0000-1000-8000-0026BB765291", + "perms": ["pr"], + "format": "string", + "value": "RavenSystem HAA" + }, + { + "aid": 2, + "iid": 6, + "type": "00000052-0000-1000-8000-0026BB765291", + "perms": ["pr"], + "format": "string", + "value": "5.0.18" + }, + { + "aid": 2, + "iid": 7, + "type": "00000014-0000-1000-8000-0026BB765291", + "perms": ["pw"], + "format": "bool" + } ] - }, - { - "aid": 2, - "services": [ - { - "iid": 1, - "type": "0000003E-0000-1000-8000-0026BB765291", - "primary": false, - "hidden": false, - "characteristics": [ - { - "aid": 2, - "iid": 2, - "type": "00000023-0000-1000-8000-0026BB765291", - "perms": [ - "pr" - ], - "format": "string", - "value": "HAA-C718B3" - }, - { - "aid": 2, - "iid": 3, - "type": "00000020-0000-1000-8000-0026BB765291", - "perms": [ - "pr" - ], - "format": "string", - "value": "Jos\u00e9 A. Jim\u00e9nez Campos" - }, - { - "aid": 2, - "iid": 4, - "type": "00000030-0000-1000-8000-0026BB765291", - "perms": [ - "pr" - ], - "format": "string", - "value": "C718B3-2" - }, - { - "aid": 2, - "iid": 5, - "type": "00000021-0000-1000-8000-0026BB765291", - "perms": [ - "pr" - ], - "format": "string", - "value": "RavenSystem HAA" - }, - { - "aid": 2, - "iid": 6, - "type": "00000052-0000-1000-8000-0026BB765291", - "perms": [ - "pr" - ], - "format": "string", - "value": "5.0.18" - }, - { - "aid": 2, - "iid": 7, - "type": "00000014-0000-1000-8000-0026BB765291", - "perms": [ - "pw" - ], - "format": "bool" - } - ] - }, - { - "iid": 8, - "type": "00000049-0000-1000-8000-0026BB765291", - "primary": true, - "hidden": false, - "characteristics": [ - { - "aid": 2, - "iid": 9, - "type": "00000025-0000-1000-8000-0026BB765291", - "perms": [ - "pr", - "pw", - "ev" - ], - "ev": true, - "format": "bool", - "value": false - } - ] - } + }, + { + "iid": 8, + "type": "00000040-0000-1000-8000-0026BB765291", + "primary": true, + "hidden": false, + "characteristics": [ + { + "aid": 1, + "iid": 9, + "type": "00000025-0000-1000-8000-0026BB765291", + "perms": ["pr", "pw", "ev"], + "ev": true, + "format": "bool", + "value": false + }, + { + "aid": 1, + "iid": 10, + "type": "00000029-0000-1000-8000-0026BB765291", + "perms": ["pr", "pw", "ev"], + "ev": true, + "format": "float", + "unit": "percentage", + "minValue": 0, + "maxValue": 3, + "minStep": 1, + "value": 3 + } ] - } -] \ No newline at end of file + }, + { + "iid": 1000, + "type": "000000A2-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": true, + "characteristics": [ + { + "aid": 1, + "iid": 1001, + "type": "00000037-0000-1000-8000-0026BB765291", + "perms": ["pr"], + "format": "string", + "value": "1.1.0" + } + ] + }, + { + "iid": 1010, + "type": "F0000100-0218-2017-81BF-AF2B7C833922", + "primary": false, + "hidden": true, + "characteristics": [ + { + "aid": 1, + "iid": 1011, + "type": "F0000101-0218-2017-81BF-AF2B7C833922", + "perms": ["pr", "pw", "hd"], + "description": "Update", + "format": "string", + "value": "" + }, + { + "aid": 1, + "iid": 1012, + "type": "F0000102-0218-2017-81BF-AF2B7C833922", + "perms": ["pr", "pw", "hd"], + "description": "Setup", + "format": "string", + "value": "" + } + ] + } + ] + }, + { + "aid": 2, + "services": [ + { + "iid": 1, + "type": "0000003E-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "characteristics": [ + { + "aid": 2, + "iid": 2, + "type": "00000023-0000-1000-8000-0026BB765291", + "perms": ["pr"], + "format": "string", + "value": "HAA-C718B3" + }, + { + "aid": 2, + "iid": 3, + "type": "00000020-0000-1000-8000-0026BB765291", + "perms": ["pr"], + "format": "string", + "value": "Jos\u00e9 A. Jim\u00e9nez Campos" + }, + { + "aid": 2, + "iid": 4, + "type": "00000030-0000-1000-8000-0026BB765291", + "perms": ["pr"], + "format": "string", + "value": "C718B3-2" + }, + { + "aid": 2, + "iid": 5, + "type": "00000021-0000-1000-8000-0026BB765291", + "perms": ["pr"], + "format": "string", + "value": "RavenSystem HAA" + }, + { + "aid": 2, + "iid": 6, + "type": "00000052-0000-1000-8000-0026BB765291", + "perms": ["pr"], + "format": "string", + "value": "5.0.18" + }, + { + "aid": 2, + "iid": 7, + "type": "00000014-0000-1000-8000-0026BB765291", + "perms": ["pw"], + "format": "bool" + } + ] + }, + { + "iid": 8, + "type": "00000049-0000-1000-8000-0026BB765291", + "primary": true, + "hidden": false, + "characteristics": [ + { + "aid": 2, + "iid": 9, + "type": "00000025-0000-1000-8000-0026BB765291", + "perms": ["pr", "pw", "ev"], + "ev": true, + "format": "bool", + "value": false + } + ] + } + ] + } +] diff --git a/tests/components/homekit_controller/fixtures/home_assistant_bridge_fan.json b/tests/components/homekit_controller/fixtures/home_assistant_bridge_fan.json index 77a89574c42..6000d6a9805 100644 --- a/tests/components/homekit_controller/fixtures/home_assistant_bridge_fan.json +++ b/tests/components/homekit_controller/fixtures/home_assistant_bridge_fan.json @@ -1,325 +1,253 @@ [ - { - "aid": 1, - "services": [ - { - "characteristics": [ - { - "description": "Identify", - "format": "bool", - "iid": 2, - "perms": [ - "pw" - ], - "type": "00000014-0000-1000-8000-0026BB765291" - }, - { - "description": "Manufacturer", - "format": "string", - "iid": 3, - "perms": [ - "pr" - ], - "type": "00000020-0000-1000-8000-0026BB765291", - "value": "Home Assistant" - }, - { - "description": "Model", - "format": "string", - "iid": 4, - "perms": [ - "pr" - ], - "type": "00000021-0000-1000-8000-0026BB765291", - "value": "Bridge" - }, - { - "description": "Name", - "format": "string", - "iid": 5, - "perms": [ - "pr" - ], - "type": "00000023-0000-1000-8000-0026BB765291", - "value": "Home Assistant Bridge" - }, - { - "description": "SerialNumber", - "format": "string", - "iid": 6, - "perms": [ - "pr" - ], - "type": "00000030-0000-1000-8000-0026BB765291", - "value": "homekit.bridge" - }, - { - "description": "FirmwareRevision", - "format": "string", - "iid": 7, - "perms": [ - "pr" - ], - "type": "00000052-0000-1000-8000-0026BB765291", - "value": "0.104.0.dev0" - } - ], - "iid": 1, - "stype": "accessory-information", - "type": "0000003E-0000-1000-8000-0026BB765291" - } - ] - }, - { - "aid": 1256851357, - "services": [ - { - "characteristics": [ - { - "description": "Identify", - "format": "bool", - "iid": 2, - "perms": [ - "pw" - ], - "type": "00000014-0000-1000-8000-0026BB765291" - }, - { - "description": "Manufacturer", - "format": "string", - "iid": 3, - "perms": [ - "pr" - ], - "type": "00000020-0000-1000-8000-0026BB765291", - "value": "Home Assistant" - }, - { - "description": "Model", - "format": "string", - "iid": 4, - "perms": [ - "pr" - ], - "type": "00000021-0000-1000-8000-0026BB765291", - "value": "Fan" - }, - { - "description": "Name", - "format": "string", - "iid": 5, - "perms": [ - "pr" - ], - "type": "00000023-0000-1000-8000-0026BB765291", - "value": "Living Room Fan" - }, - { - "description": "SerialNumber", - "format": "string", - "iid": 6, - "perms": [ - "pr" - ], - "type": "00000030-0000-1000-8000-0026BB765291", - "value": "fan.living_room_fan" - }, - { - "description": "FirmwareRevision", - "format": "string", - "iid": 7, - "perms": [ - "pr" - ], - "type": "00000052-0000-1000-8000-0026BB765291", - "value": "0.104.0.dev0" - } - ], - "iid": 1, - "stype": "accessory-information", - "type": "0000003E-0000-1000-8000-0026BB765291" - }, - { - "characteristics": [ - { - "description": "Active", - "format": "uint8", - "iid": 9, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "000000B0-0000-1000-8000-0026BB765291", - "valid-values": [ - 0, - 1 - ], - "value": 0 - }, - { - "description": "RotationDirection", - "format": "int", - "iid": 10, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "00000028-0000-1000-8000-0026BB765291", - "valid-values": [ - 0, - 1 - ], - "value": 0 - }, - { - "description": "SwingMode", - "format": "uint8", - "iid": 11, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "000000B6-0000-1000-8000-0026BB765291", - "valid-values": [ - 0, - 1 - ], - "value": 0 - }, - { - "description": "RotationSpeed", - "format": "float", - "iid": 12, - "maxValue": 100, - "minStep": 1, - "minValue": 0, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "00000029-0000-1000-8000-0026BB765291", - "unit": "percentage", - "value": 100 - } - ], - "iid": 8, - "stype": "fanv2", - "type": "000000B7-0000-1000-8000-0026BB765291" - } - ] - }, - { - "aid": 766313939, - "services": [ - { - "characteristics": [ - { - "description": "Identify", - "format": "bool", - "iid": 2, - "perms": [ - "pw" - ], - "type": "00000014-0000-1000-8000-0026BB765291" - }, - { - "description": "Manufacturer", - "format": "string", - "iid": 3, - "perms": [ - "pr" - ], - "type": "00000020-0000-1000-8000-0026BB765291", - "value": "Home Assistant" - }, - { - "description": "Model", - "format": "string", - "iid": 4, - "perms": [ - "pr" - ], - "type": "00000021-0000-1000-8000-0026BB765291", - "value": "Fan" - }, - { - "description": "Name", - "format": "string", - "iid": 5, - "perms": [ - "pr" - ], - "type": "00000023-0000-1000-8000-0026BB765291", - "value": "Ceiling Fan" - }, - { - "description": "SerialNumber", - "format": "string", - "iid": 6, - "perms": [ - "pr" - ], - "type": "00000030-0000-1000-8000-0026BB765291", - "value": "fan.ceiling_fan" - }, - { - "description": "FirmwareRevision", - "format": "string", - "iid": 7, - "perms": [ - "pr" - ], - "type": "00000052-0000-1000-8000-0026BB765291", - "value": "0.104.0.dev0" - } - ], - "iid": 1, - "stype": "accessory-information", - "type": "0000003E-0000-1000-8000-0026BB765291" - }, - { - "characteristics": [ - { - "description": "Active", - "format": "uint8", - "iid": 9, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "000000B0-0000-1000-8000-0026BB765291", - "valid-values": [ - 0, - 1 - ], - "value": 0 - }, - { - "description": "RotationSpeed", - "format": "float", - "iid": 10, - "maxValue": 100, - "minStep": 1, - "minValue": 0, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "00000029-0000-1000-8000-0026BB765291", - "unit": "percentage", - "value": 100 - } - ], - "iid": 8, - "stype": "fanv2", - "type": "000000B7-0000-1000-8000-0026BB765291" - } - ] - } -] \ No newline at end of file + { + "aid": 1, + "services": [ + { + "characteristics": [ + { + "description": "Identify", + "format": "bool", + "iid": 2, + "perms": ["pw"], + "type": "00000014-0000-1000-8000-0026BB765291" + }, + { + "description": "Manufacturer", + "format": "string", + "iid": 3, + "perms": ["pr"], + "type": "00000020-0000-1000-8000-0026BB765291", + "value": "Home Assistant" + }, + { + "description": "Model", + "format": "string", + "iid": 4, + "perms": ["pr"], + "type": "00000021-0000-1000-8000-0026BB765291", + "value": "Bridge" + }, + { + "description": "Name", + "format": "string", + "iid": 5, + "perms": ["pr"], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Home Assistant Bridge" + }, + { + "description": "SerialNumber", + "format": "string", + "iid": 6, + "perms": ["pr"], + "type": "00000030-0000-1000-8000-0026BB765291", + "value": "homekit.bridge" + }, + { + "description": "FirmwareRevision", + "format": "string", + "iid": 7, + "perms": ["pr"], + "type": "00000052-0000-1000-8000-0026BB765291", + "value": "0.104.0.dev0" + } + ], + "iid": 1, + "stype": "accessory-information", + "type": "0000003E-0000-1000-8000-0026BB765291" + } + ] + }, + { + "aid": 1256851357, + "services": [ + { + "characteristics": [ + { + "description": "Identify", + "format": "bool", + "iid": 2, + "perms": ["pw"], + "type": "00000014-0000-1000-8000-0026BB765291" + }, + { + "description": "Manufacturer", + "format": "string", + "iid": 3, + "perms": ["pr"], + "type": "00000020-0000-1000-8000-0026BB765291", + "value": "Home Assistant" + }, + { + "description": "Model", + "format": "string", + "iid": 4, + "perms": ["pr"], + "type": "00000021-0000-1000-8000-0026BB765291", + "value": "Fan" + }, + { + "description": "Name", + "format": "string", + "iid": 5, + "perms": ["pr"], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Living Room Fan" + }, + { + "description": "SerialNumber", + "format": "string", + "iid": 6, + "perms": ["pr"], + "type": "00000030-0000-1000-8000-0026BB765291", + "value": "fan.living_room_fan" + }, + { + "description": "FirmwareRevision", + "format": "string", + "iid": 7, + "perms": ["pr"], + "type": "00000052-0000-1000-8000-0026BB765291", + "value": "0.104.0.dev0" + } + ], + "iid": 1, + "stype": "accessory-information", + "type": "0000003E-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "description": "Active", + "format": "uint8", + "iid": 9, + "perms": ["pr", "pw", "ev"], + "type": "000000B0-0000-1000-8000-0026BB765291", + "valid-values": [0, 1], + "value": 0 + }, + { + "description": "RotationDirection", + "format": "int", + "iid": 10, + "perms": ["pr", "pw", "ev"], + "type": "00000028-0000-1000-8000-0026BB765291", + "valid-values": [0, 1], + "value": 0 + }, + { + "description": "SwingMode", + "format": "uint8", + "iid": 11, + "perms": ["pr", "pw", "ev"], + "type": "000000B6-0000-1000-8000-0026BB765291", + "valid-values": [0, 1], + "value": 0 + }, + { + "description": "RotationSpeed", + "format": "float", + "iid": 12, + "maxValue": 100, + "minStep": 1, + "minValue": 0, + "perms": ["pr", "pw", "ev"], + "type": "00000029-0000-1000-8000-0026BB765291", + "unit": "percentage", + "value": 100 + } + ], + "iid": 8, + "stype": "fanv2", + "type": "000000B7-0000-1000-8000-0026BB765291" + } + ] + }, + { + "aid": 766313939, + "services": [ + { + "characteristics": [ + { + "description": "Identify", + "format": "bool", + "iid": 2, + "perms": ["pw"], + "type": "00000014-0000-1000-8000-0026BB765291" + }, + { + "description": "Manufacturer", + "format": "string", + "iid": 3, + "perms": ["pr"], + "type": "00000020-0000-1000-8000-0026BB765291", + "value": "Home Assistant" + }, + { + "description": "Model", + "format": "string", + "iid": 4, + "perms": ["pr"], + "type": "00000021-0000-1000-8000-0026BB765291", + "value": "Fan" + }, + { + "description": "Name", + "format": "string", + "iid": 5, + "perms": ["pr"], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Ceiling Fan" + }, + { + "description": "SerialNumber", + "format": "string", + "iid": 6, + "perms": ["pr"], + "type": "00000030-0000-1000-8000-0026BB765291", + "value": "fan.ceiling_fan" + }, + { + "description": "FirmwareRevision", + "format": "string", + "iid": 7, + "perms": ["pr"], + "type": "00000052-0000-1000-8000-0026BB765291", + "value": "0.104.0.dev0" + } + ], + "iid": 1, + "stype": "accessory-information", + "type": "0000003E-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "description": "Active", + "format": "uint8", + "iid": 9, + "perms": ["pr", "pw", "ev"], + "type": "000000B0-0000-1000-8000-0026BB765291", + "valid-values": [0, 1], + "value": 0 + }, + { + "description": "RotationSpeed", + "format": "float", + "iid": 10, + "maxValue": 100, + "minStep": 1, + "minValue": 0, + "perms": ["pr", "pw", "ev"], + "type": "00000029-0000-1000-8000-0026BB765291", + "unit": "percentage", + "value": 100 + } + ], + "iid": 8, + "stype": "fanv2", + "type": "000000B7-0000-1000-8000-0026BB765291" + } + ] + } +] diff --git a/tests/components/homekit_controller/fixtures/hue_bridge.json b/tests/components/homekit_controller/fixtures/hue_bridge.json index ed893cdad60..bac4c9d4162 100644 --- a/tests/components/homekit_controller/fixtures/hue_bridge.json +++ b/tests/components/homekit_controller/fixtures/hue_bridge.json @@ -1,2249 +1,1800 @@ [ - { - "aid": 6623462389072572, - "services": [ - { - "characteristics": [ - { - "format": "string", - "iid": 37, - "perms": [ - "pr" - ], - "type": "00000023-0000-1000-8000-0026BB765291", - "value": "Hue dimmer switch" - }, - { - "format": "string", - "iid": 35, - "perms": [ - "pr" - ], - "type": "00000021-0000-1000-8000-0026BB765291", - "value": "RWL021" - }, - { - "format": "string", - "iid": 34, - "perms": [ - "pr" - ], - "type": "00000020-0000-1000-8000-0026BB765291", - "value": "Philips" - }, - { - "format": "string", - "iid": 84, - "perms": [ - "pr" - ], - "type": "00000052-0000-1000-8000-0026BB765291", - "value": "45.1.17846" - }, - { - "format": "string", - "iid": 50, - "perms": [ - "pr" - ], - "type": "00000030-0000-1000-8000-0026BB765291", - "value": "6623462389072572" - }, - { - "format": "bool", - "iid": 22, - "perms": [ - "pw" - ], - "type": "00000014-0000-1000-8000-0026BB765291" - } - ], - "iid": 1, - "type": "0000003E-0000-1000-8000-0026BB765291" - }, - { - "characteristics": [ - { - "format": "string", - "iid": 65591, - "perms": [ - "pr" - ], - "type": "00000037-0000-1000-8000-0026BB765291", - "value": "1.1.0" - } - ], - "iid": 65535, - "type": "000000A2-0000-1000-8000-0026BB765291" - }, - { - "characteristics": [ - { - "format": "string", - "iid": 644245094436, - "perms": [ - "pr" - ], - "type": "00000023-0000-1000-8000-0026BB765291", - "value": "Hue dimmer switch battery" - }, - { - "format": "uint8", - "iid": 644245094505, - "maxValue": 100, - "minStep": 1, - "minValue": 0, - "perms": [ - "pr", - "ev" - ], - "type": "00000068-0000-1000-8000-0026BB765291", - "unit": "percentage", - "value": 100 - }, - { - "format": "uint8", - "iid": 644245094522, - "maxValue": 1, - "minStep": 1, - "minValue": 0, - "perms": [ - "pr", - "ev" - ], - "type": "00000079-0000-1000-8000-0026BB765291", - "value": 0 - }, - { - "format": "uint8", - "iid": 644245094544, - "maxValue": 2, - "minStep": 1, - "minValue": 0, - "perms": [ - "pr", - "ev" - ], - "type": "0000008F-0000-1000-8000-0026BB765291", - "value": 2 - }, - { - "description": "ID to uniquely identify service within a single accessory", - "format": "string", - "iid": 644245149880, - "maxLen": 64, - "perms": [ - "pr" - ], - "type": "D8B76298-42E7-5FFD-B1D6-1782D9A1F936", - "value": "6623462389072572" - } - ], - "iid": 644245094400, - "type": "00000096-0000-1000-8000-0026BB765291" - }, - { - "characteristics": [ - { - "format": "string", - "iid": 588410585124, - "perms": [ - "pr" - ], - "type": "00000023-0000-1000-8000-0026BB765291", - "value": "Hue dimmer switch button 1" - }, - { - "format": "uint8", - "iid": 588410585204, - "maxValue": 0, - "minStep": 1, - "minValue": 0, - "perms": [ - "pr", - "ev" - ], - "type": "00000073-0000-1000-8000-0026BB765291", - "value": null - }, - { - "format": "uint8", - "iid": 588410585292, - "minStep": 1, - "minValue": 1, - "perms": [ - "pr" - ], - "type": "000000CB-0000-1000-8000-0026BB765291", - "value": 1 - }, - { - "description": "ID to uniquely identify service within a single accessory", - "format": "string", - "iid": 588410640568, - "maxLen": 64, - "perms": [ - "pr" - ], - "type": "D8B76298-42E7-5FFD-B1D6-1782D9A1F936", - "value": "6623462389072572" - } - ], - "iid": 588410585088, - "linked": [ - 256 - ], - "type": "00000089-0000-1000-8000-0026BB765291" - }, - { - "characteristics": [ - { - "format": "uint8", - "iid": 462, - "maxValue": 1, - "minStep": 1, - "minValue": 0, - "perms": [ - "pr" - ], - "type": "000000CD-0000-1000-8000-0026BB765291", - "value": 1 - } - ], - "iid": 256, - "type": "000000CC-0000-1000-8000-0026BB765291" - }, - { - "characteristics": [ - { - "format": "string", - "iid": 588410650660, - "perms": [ - "pr" - ], - "type": "00000023-0000-1000-8000-0026BB765291", - "value": "Hue dimmer switch button 2" - }, - { - "format": "uint8", - "iid": 588410650740, - "maxValue": 0, - "minStep": 1, - "minValue": 0, - "perms": [ - "pr", - "ev" - ], - "type": "00000073-0000-1000-8000-0026BB765291", - "value": null - }, - { - "format": "uint8", - "iid": 588410650828, - "minStep": 1, - "minValue": 1, - "perms": [ - "pr" - ], - "type": "000000CB-0000-1000-8000-0026BB765291", - "value": 2 - }, - { - "description": "ID to uniquely identify service within a single accessory", - "format": "string", - "iid": 588410706104, - "maxLen": 64, - "perms": [ - "pr" - ], - "type": "D8B76298-42E7-5FFD-B1D6-1782D9A1F936", - "value": "6623462389072572" - } - ], - "iid": 588410650624, - "linked": [ - 256 - ], - "type": "00000089-0000-1000-8000-0026BB765291" - }, - { - "characteristics": [ - { - "format": "string", - "iid": 588410716196, - "perms": [ - "pr" - ], - "type": "00000023-0000-1000-8000-0026BB765291", - "value": "Hue dimmer switch button 3" - }, - { - "format": "uint8", - "iid": 588410716276, - "maxValue": 0, - "minStep": 1, - "minValue": 0, - "perms": [ - "pr", - "ev" - ], - "type": "00000073-0000-1000-8000-0026BB765291", - "value": null - }, - { - "format": "uint8", - "iid": 588410716364, - "minStep": 1, - "minValue": 1, - "perms": [ - "pr" - ], - "type": "000000CB-0000-1000-8000-0026BB765291", - "value": 3 - }, - { - "description": "ID to uniquely identify service within a single accessory", - "format": "string", - "iid": 588410771640, - "maxLen": 64, - "perms": [ - "pr" - ], - "type": "D8B76298-42E7-5FFD-B1D6-1782D9A1F936", - "value": "6623462389072572" - } - ], - "iid": 588410716160, - "linked": [ - 256 - ], - "type": "00000089-0000-1000-8000-0026BB765291" - }, - { - "characteristics": [ - { - "format": "string", - "iid": 588410781732, - "perms": [ - "pr" - ], - "type": "00000023-0000-1000-8000-0026BB765291", - "value": "Hue dimmer switch button 4" - }, - { - "format": "uint8", - "iid": 588410781812, - "maxValue": 0, - "minStep": 1, - "minValue": 0, - "perms": [ - "pr", - "ev" - ], - "type": "00000073-0000-1000-8000-0026BB765291", - "value": null - }, - { - "format": "uint8", - "iid": 588410781900, - "minStep": 1, - "minValue": 1, - "perms": [ - "pr" - ], - "type": "000000CB-0000-1000-8000-0026BB765291", - "value": 4 - }, - { - "description": "ID to uniquely identify service within a single accessory", - "format": "string", - "iid": 588410837176, - "maxLen": 64, - "perms": [ - "pr" - ], - "type": "D8B76298-42E7-5FFD-B1D6-1782D9A1F936", - "value": "6623462389072572" - } - ], - "iid": 588410781696, - "linked": [ - 256 - ], - "type": "00000089-0000-1000-8000-0026BB765291" - } - ] - }, - { - "aid": 1, - "services": [ - { - "characteristics": [ - { - "format": "string", - "iid": 2, - "perms": [ - "pr" - ], - "type": "00000023-0000-1000-8000-0026BB765291", - "value": "Philips hue - 482544" - }, - { - "format": "string", - "iid": 4, - "perms": [ - "pr" - ], - "type": "00000021-0000-1000-8000-0026BB765291", - "value": "BSB002" - }, - { - "format": "string", - "iid": 3, - "perms": [ - "pr" - ], - "type": "00000020-0000-1000-8000-0026BB765291", - "value": "Philips Lighting" - }, - { - "format": "string", - "iid": 8, - "perms": [ - "pr" - ], - "type": "00000052-0000-1000-8000-0026BB765291", - "value": "1.32.1932126170" - }, - { - "format": "string", - "iid": 6, - "perms": [ - "pr" - ], - "type": "00000030-0000-1000-8000-0026BB765291", - "value": "123456" - }, - { - "format": "bool", - "iid": 7, - "perms": [ - "pw" - ], - "type": "00000014-0000-1000-8000-0026BB765291" - } - ], - "iid": 1, - "type": "0000003E-0000-1000-8000-0026BB765291" - }, - { - "characteristics": [ - { - "format": "string", - "iid": 65591, - "perms": [ - "pr" - ], - "type": "00000037-0000-1000-8000-0026BB765291", - "value": "1.1.0" - } - ], - "iid": 65535, - "type": "000000A2-0000-1000-8000-0026BB765291" - } - ] - }, - { - "aid": 6623462378982941, - "services": [ - { - "characteristics": [ - { - "format": "string", - "iid": 5, - "perms": [ - "pr" - ], - "type": "00000023-0000-1000-8000-0026BB765291", - "value": "Hue white lamp" - }, - { - "format": "string", - "iid": 4, - "perms": [ - "pr" - ], - "type": "00000021-0000-1000-8000-0026BB765291", - "value": "LWB010" - }, - { - "format": "string", - "iid": 3, - "perms": [ - "pr" - ], - "type": "00000020-0000-1000-8000-0026BB765291", - "value": "Philips" - }, - { - "format": "string", - "iid": 112, - "perms": [ - "pr" - ], - "type": "00000052-0000-1000-8000-0026BB765291", - "value": "1.46.13" - }, - { - "format": "string", - "iid": 11, - "perms": [ - "pr" - ], - "type": "00000030-0000-1000-8000-0026BB765291", - "value": "6623462378982941" - }, - { - "format": "bool", - "iid": 6, - "perms": [ - "pw" - ], - "type": "00000014-0000-1000-8000-0026BB765291" - } - ], - "iid": 1, - "type": "0000003E-0000-1000-8000-0026BB765291" - }, - { - "characteristics": [ - { - "format": "string", - "iid": 65591, - "perms": [ - "pr" - ], - "type": "00000037-0000-1000-8000-0026BB765291", - "value": "1.1.0" - } - ], - "iid": 65535, - "type": "000000A2-0000-1000-8000-0026BB765291" - }, - { - "characteristics": [ - { - "format": "string", - "iid": 2817, - "perms": [ - "pr" - ], - "type": "00000023-0000-1000-8000-0026BB765291", - "value": "Hue white lamp" - }, - { - "format": "bool", - "iid": 2822, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "00000025-0000-1000-8000-0026BB765291", - "value": false - }, - { - "format": "int", - "iid": 2823, - "maxValue": 100, - "minStep": 1, - "minValue": 0, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "00000008-0000-1000-8000-0026BB765291", - "unit": "percentage", - "value": 100 - }, - { - "description": "ID to uniquely identify service within a single accessory", - "format": "string", - "iid": 2827, - "maxLen": 64, - "perms": [ - "pr" - ], - "type": "D8B76298-42E7-5FFD-B1D6-1782D9A1F936", - "value": "6623462378982941" - } - ], - "iid": 2816, - "type": "00000043-0000-1000-8000-0026BB765291" - } - ] - }, - { - "aid": 6623462378983942, - "services": [ - { - "characteristics": [ - { - "format": "string", - "iid": 5, - "perms": [ - "pr" - ], - "type": "00000023-0000-1000-8000-0026BB765291", - "value": "Hue white lamp" - }, - { - "format": "string", - "iid": 4, - "perms": [ - "pr" - ], - "type": "00000021-0000-1000-8000-0026BB765291", - "value": "LWB010" - }, - { - "format": "string", - "iid": 3, - "perms": [ - "pr" - ], - "type": "00000020-0000-1000-8000-0026BB765291", - "value": "Philips" - }, - { - "format": "string", - "iid": 112, - "perms": [ - "pr" - ], - "type": "00000052-0000-1000-8000-0026BB765291", - "value": "1.46.13" - }, - { - "format": "string", - "iid": 11, - "perms": [ - "pr" - ], - "type": "00000030-0000-1000-8000-0026BB765291", - "value": "6623462378983942" - }, - { - "format": "bool", - "iid": 6, - "perms": [ - "pw" - ], - "type": "00000014-0000-1000-8000-0026BB765291" - } - ], - "iid": 1, - "type": "0000003E-0000-1000-8000-0026BB765291" - }, - { - "characteristics": [ - { - "format": "string", - "iid": 65591, - "perms": [ - "pr" - ], - "type": "00000037-0000-1000-8000-0026BB765291", - "value": "1.1.0" - } - ], - "iid": 65535, - "type": "000000A2-0000-1000-8000-0026BB765291" - }, - { - "characteristics": [ - { - "format": "string", - "iid": 2817, - "perms": [ - "pr" - ], - "type": "00000023-0000-1000-8000-0026BB765291", - "value": "Hue white lamp" - }, - { - "format": "bool", - "iid": 2822, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "00000025-0000-1000-8000-0026BB765291", - "value": false - }, - { - "format": "int", - "iid": 2823, - "maxValue": 100, - "minStep": 1, - "minValue": 0, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "00000008-0000-1000-8000-0026BB765291", - "unit": "percentage", - "value": 100 - }, - { - "description": "ID to uniquely identify service within a single accessory", - "format": "string", - "iid": 2827, - "maxLen": 64, - "perms": [ - "pr" - ], - "type": "D8B76298-42E7-5FFD-B1D6-1782D9A1F936", - "value": "6623462378983942" - } - ], - "iid": 2816, - "type": "00000043-0000-1000-8000-0026BB765291" - } - ] - }, - { - "aid": 6623462379123707, - "services": [ - { - "characteristics": [ - { - "format": "string", - "iid": 5, - "perms": [ - "pr" - ], - "type": "00000023-0000-1000-8000-0026BB765291", - "value": "Hue white lamp" - }, - { - "format": "string", - "iid": 4, - "perms": [ - "pr" - ], - "type": "00000021-0000-1000-8000-0026BB765291", - "value": "LWB010" - }, - { - "format": "string", - "iid": 3, - "perms": [ - "pr" - ], - "type": "00000020-0000-1000-8000-0026BB765291", - "value": "Philips" - }, - { - "format": "string", - "iid": 112, - "perms": [ - "pr" - ], - "type": "00000052-0000-1000-8000-0026BB765291", - "value": "1.46.13" - }, - { - "format": "string", - "iid": 11, - "perms": [ - "pr" - ], - "type": "00000030-0000-1000-8000-0026BB765291", - "value": "6623462379123707" - }, - { - "format": "bool", - "iid": 6, - "perms": [ - "pw" - ], - "type": "00000014-0000-1000-8000-0026BB765291" - } - ], - "iid": 1, - "type": "0000003E-0000-1000-8000-0026BB765291" - }, - { - "characteristics": [ - { - "format": "string", - "iid": 65591, - "perms": [ - "pr" - ], - "type": "00000037-0000-1000-8000-0026BB765291", - "value": "1.1.0" - } - ], - "iid": 65535, - "type": "000000A2-0000-1000-8000-0026BB765291" - }, - { - "characteristics": [ - { - "format": "string", - "iid": 2817, - "perms": [ - "pr" - ], - "type": "00000023-0000-1000-8000-0026BB765291", - "value": "Hue white lamp" - }, - { - "format": "bool", - "iid": 2822, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "00000025-0000-1000-8000-0026BB765291", - "value": false - }, - { - "format": "int", - "iid": 2823, - "maxValue": 100, - "minStep": 1, - "minValue": 0, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "00000008-0000-1000-8000-0026BB765291", - "unit": "percentage", - "value": 100 - }, - { - "description": "ID to uniquely identify service within a single accessory", - "format": "string", - "iid": 2827, - "maxLen": 64, - "perms": [ - "pr" - ], - "type": "D8B76298-42E7-5FFD-B1D6-1782D9A1F936", - "value": "6623462379123707" - } - ], - "iid": 2816, - "type": "00000043-0000-1000-8000-0026BB765291" - } - ] - }, - { - "aid": 6623462379122122, - "services": [ - { - "characteristics": [ - { - "format": "string", - "iid": 5, - "perms": [ - "pr" - ], - "type": "00000023-0000-1000-8000-0026BB765291", - "value": "Hue white lamp" - }, - { - "format": "string", - "iid": 4, - "perms": [ - "pr" - ], - "type": "00000021-0000-1000-8000-0026BB765291", - "value": "LWB010" - }, - { - "format": "string", - "iid": 3, - "perms": [ - "pr" - ], - "type": "00000020-0000-1000-8000-0026BB765291", - "value": "Philips" - }, - { - "format": "string", - "iid": 112, - "perms": [ - "pr" - ], - "type": "00000052-0000-1000-8000-0026BB765291", - "value": "1.46.13" - }, - { - "format": "string", - "iid": 11, - "perms": [ - "pr" - ], - "type": "00000030-0000-1000-8000-0026BB765291", - "value": "6623462379122122" - }, - { - "format": "bool", - "iid": 6, - "perms": [ - "pw" - ], - "type": "00000014-0000-1000-8000-0026BB765291" - } - ], - "iid": 1, - "type": "0000003E-0000-1000-8000-0026BB765291" - }, - { - "characteristics": [ - { - "format": "string", - "iid": 65591, - "perms": [ - "pr" - ], - "type": "00000037-0000-1000-8000-0026BB765291", - "value": "1.1.0" - } - ], - "iid": 65535, - "type": "000000A2-0000-1000-8000-0026BB765291" - }, - { - "characteristics": [ - { - "format": "string", - "iid": 2817, - "perms": [ - "pr" - ], - "type": "00000023-0000-1000-8000-0026BB765291", - "value": "Hue white lamp" - }, - { - "format": "bool", - "iid": 2822, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "00000025-0000-1000-8000-0026BB765291", - "value": false - }, - { - "format": "int", - "iid": 2823, - "maxValue": 100, - "minStep": 1, - "minValue": 0, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "00000008-0000-1000-8000-0026BB765291", - "unit": "percentage", - "value": 70 - }, - { - "description": "ID to uniquely identify service within a single accessory", - "format": "string", - "iid": 2827, - "maxLen": 64, - "perms": [ - "pr" - ], - "type": "D8B76298-42E7-5FFD-B1D6-1782D9A1F936", - "value": "6623462379122122" - } - ], - "iid": 2816, - "type": "00000043-0000-1000-8000-0026BB765291" - } - ] - }, - { - "aid": 6623462385996792, - "services": [ - { - "characteristics": [ - { - "format": "string", - "iid": 5, - "perms": [ - "pr" - ], - "type": "00000023-0000-1000-8000-0026BB765291", - "value": "Hue white lamp" - }, - { - "format": "string", - "iid": 4, - "perms": [ - "pr" - ], - "type": "00000021-0000-1000-8000-0026BB765291", - "value": "LWB010" - }, - { - "format": "string", - "iid": 3, - "perms": [ - "pr" - ], - "type": "00000020-0000-1000-8000-0026BB765291", - "value": "Philips" - }, - { - "format": "string", - "iid": 112, - "perms": [ - "pr" - ], - "type": "00000052-0000-1000-8000-0026BB765291", - "value": "1.46.13" - }, - { - "format": "string", - "iid": 11, - "perms": [ - "pr" - ], - "type": "00000030-0000-1000-8000-0026BB765291", - "value": "6623462385996792" - }, - { - "format": "bool", - "iid": 6, - "perms": [ - "pw" - ], - "type": "00000014-0000-1000-8000-0026BB765291" - } - ], - "iid": 1, - "type": "0000003E-0000-1000-8000-0026BB765291" - }, - { - "characteristics": [ - { - "format": "string", - "iid": 65591, - "perms": [ - "pr" - ], - "type": "00000037-0000-1000-8000-0026BB765291", - "value": "1.1.0" - } - ], - "iid": 65535, - "type": "000000A2-0000-1000-8000-0026BB765291" - }, - { - "characteristics": [ - { - "format": "string", - "iid": 2817, - "perms": [ - "pr" - ], - "type": "00000023-0000-1000-8000-0026BB765291", - "value": "Hue white lamp" - }, - { - "format": "bool", - "iid": 2822, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "00000025-0000-1000-8000-0026BB765291", - "value": false - }, - { - "format": "int", - "iid": 2823, - "maxValue": 100, - "minStep": 1, - "minValue": 0, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "00000008-0000-1000-8000-0026BB765291", - "unit": "percentage", - "value": 100 - }, - { - "description": "ID to uniquely identify service within a single accessory", - "format": "string", - "iid": 2827, - "maxLen": 64, - "perms": [ - "pr" - ], - "type": "D8B76298-42E7-5FFD-B1D6-1782D9A1F936", - "value": "6623462385996792" - } - ], - "iid": 2816, - "type": "00000043-0000-1000-8000-0026BB765291" - } - ] - }, - { - "aid": 6623462383114193, - "services": [ - { - "characteristics": [ - { - "format": "string", - "iid": 5, - "perms": [ - "pr" - ], - "type": "00000023-0000-1000-8000-0026BB765291", - "value": "Hue white lamp" - }, - { - "format": "string", - "iid": 4, - "perms": [ - "pr" - ], - "type": "00000021-0000-1000-8000-0026BB765291", - "value": "LWB010" - }, - { - "format": "string", - "iid": 3, - "perms": [ - "pr" - ], - "type": "00000020-0000-1000-8000-0026BB765291", - "value": "Philips" - }, - { - "format": "string", - "iid": 112, - "perms": [ - "pr" - ], - "type": "00000052-0000-1000-8000-0026BB765291", - "value": "1.46.13" - }, - { - "format": "string", - "iid": 11, - "perms": [ - "pr" - ], - "type": "00000030-0000-1000-8000-0026BB765291", - "value": "6623462383114193" - }, - { - "format": "bool", - "iid": 6, - "perms": [ - "pw" - ], - "type": "00000014-0000-1000-8000-0026BB765291" - } - ], - "iid": 1, - "type": "0000003E-0000-1000-8000-0026BB765291" - }, - { - "characteristics": [ - { - "format": "string", - "iid": 65591, - "perms": [ - "pr" - ], - "type": "00000037-0000-1000-8000-0026BB765291", - "value": "1.1.0" - } - ], - "iid": 65535, - "type": "000000A2-0000-1000-8000-0026BB765291" - }, - { - "characteristics": [ - { - "format": "string", - "iid": 2817, - "perms": [ - "pr" - ], - "type": "00000023-0000-1000-8000-0026BB765291", - "value": "Hue white lamp" - }, - { - "format": "bool", - "iid": 2822, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "00000025-0000-1000-8000-0026BB765291", - "value": false - }, - { - "format": "int", - "iid": 2823, - "maxValue": 100, - "minStep": 1, - "minValue": 0, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "00000008-0000-1000-8000-0026BB765291", - "unit": "percentage", - "value": 20 - }, - { - "description": "ID to uniquely identify service within a single accessory", - "format": "string", - "iid": 2827, - "maxLen": 64, - "perms": [ - "pr" - ], - "type": "D8B76298-42E7-5FFD-B1D6-1782D9A1F936", - "value": "6623462383114193" - } - ], - "iid": 2816, - "type": "00000043-0000-1000-8000-0026BB765291" - } - ] - }, - { - "aid": 6623462383114163, - "services": [ - { - "characteristics": [ - { - "format": "string", - "iid": 5, - "perms": [ - "pr" - ], - "type": "00000023-0000-1000-8000-0026BB765291", - "value": "Hue white lamp" - }, - { - "format": "string", - "iid": 4, - "perms": [ - "pr" - ], - "type": "00000021-0000-1000-8000-0026BB765291", - "value": "LWB010" - }, - { - "format": "string", - "iid": 3, - "perms": [ - "pr" - ], - "type": "00000020-0000-1000-8000-0026BB765291", - "value": "Philips" - }, - { - "format": "string", - "iid": 112, - "perms": [ - "pr" - ], - "type": "00000052-0000-1000-8000-0026BB765291", - "value": "1.46.13" - }, - { - "format": "string", - "iid": 11, - "perms": [ - "pr" - ], - "type": "00000030-0000-1000-8000-0026BB765291", - "value": "6623462383114163" - }, - { - "format": "bool", - "iid": 6, - "perms": [ - "pw" - ], - "type": "00000014-0000-1000-8000-0026BB765291" - } - ], - "iid": 1, - "type": "0000003E-0000-1000-8000-0026BB765291" - }, - { - "characteristics": [ - { - "format": "string", - "iid": 65591, - "perms": [ - "pr" - ], - "type": "00000037-0000-1000-8000-0026BB765291", - "value": "1.1.0" - } - ], - "iid": 65535, - "type": "000000A2-0000-1000-8000-0026BB765291" - }, - { - "characteristics": [ - { - "format": "string", - "iid": 2817, - "perms": [ - "pr" - ], - "type": "00000023-0000-1000-8000-0026BB765291", - "value": "Hue white lamp" - }, - { - "format": "bool", - "iid": 2822, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "00000025-0000-1000-8000-0026BB765291", - "value": false - }, - { - "format": "int", - "iid": 2823, - "maxValue": 100, - "minStep": 1, - "minValue": 0, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "00000008-0000-1000-8000-0026BB765291", - "unit": "percentage", - "value": 100 - }, - { - "description": "ID to uniquely identify service within a single accessory", - "format": "string", - "iid": 2827, - "maxLen": 64, - "perms": [ - "pr" - ], - "type": "D8B76298-42E7-5FFD-B1D6-1782D9A1F936", - "value": "6623462383114163" - } - ], - "iid": 2816, - "type": "00000043-0000-1000-8000-0026BB765291" - } - ] - }, - { - "aid": 6623462412413293, - "services": [ - { - "characteristics": [ - { - "format": "string", - "iid": 5, - "perms": [ - "pr" - ], - "type": "00000023-0000-1000-8000-0026BB765291", - "value": "Hue ambiance spot" - }, - { - "format": "string", - "iid": 4, - "perms": [ - "pr" - ], - "type": "00000021-0000-1000-8000-0026BB765291", - "value": "LTW013" - }, - { - "format": "string", - "iid": 3, - "perms": [ - "pr" - ], - "type": "00000020-0000-1000-8000-0026BB765291", - "value": "Philips" - }, - { - "format": "string", - "iid": 112, - "perms": [ - "pr" - ], - "type": "00000052-0000-1000-8000-0026BB765291", - "value": "1.46.13" - }, - { - "format": "string", - "iid": 11, - "perms": [ - "pr" - ], - "type": "00000030-0000-1000-8000-0026BB765291", - "value": "6623462412413293" - }, - { - "format": "bool", - "iid": 6, - "perms": [ - "pw" - ], - "type": "00000014-0000-1000-8000-0026BB765291" - } - ], - "iid": 1, - "type": "0000003E-0000-1000-8000-0026BB765291" - }, - { - "characteristics": [ - { - "format": "string", - "iid": 65591, - "perms": [ - "pr" - ], - "type": "00000037-0000-1000-8000-0026BB765291", - "value": "1.1.0" - } - ], - "iid": 65535, - "type": "000000A2-0000-1000-8000-0026BB765291" - }, - { - "characteristics": [ - { - "format": "string", - "iid": 2817, - "perms": [ - "pr" - ], - "type": "00000023-0000-1000-8000-0026BB765291", - "value": "Hue ambiance spot" - }, - { - "format": "bool", - "iid": 2822, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "00000025-0000-1000-8000-0026BB765291", - "value": true - }, - { - "format": "int", - "iid": 2823, - "maxValue": 100, - "minStep": 1, - "minValue": 0, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "00000008-0000-1000-8000-0026BB765291", - "unit": "percentage", - "value": 100 - }, - { - "description": "Color Temperature", - "format": "uint32", - "iid": 3017, - "maxValue": 454, - "minStep": 1, - "minValue": 153, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "000000CE-0000-1000-8000-0026BB765291", - "value": 366 - }, - { - "description": "ID to uniquely identify service within a single accessory", - "format": "string", - "iid": 2827, - "maxLen": 64, - "perms": [ - "pr" - ], - "type": "D8B76298-42E7-5FFD-B1D6-1782D9A1F936", - "value": "6623462412413293" - } - ], - "iid": 2816, - "type": "00000043-0000-1000-8000-0026BB765291" - } - ] - }, - { - "aid": 6623462412411853, - "services": [ - { - "characteristics": [ - { - "format": "string", - "iid": 5, - "perms": [ - "pr" - ], - "type": "00000023-0000-1000-8000-0026BB765291", - "value": "Hue ambiance spot" - }, - { - "format": "string", - "iid": 4, - "perms": [ - "pr" - ], - "type": "00000021-0000-1000-8000-0026BB765291", - "value": "LTW013" - }, - { - "format": "string", - "iid": 3, - "perms": [ - "pr" - ], - "type": "00000020-0000-1000-8000-0026BB765291", - "value": "Philips" - }, - { - "format": "string", - "iid": 112, - "perms": [ - "pr" - ], - "type": "00000052-0000-1000-8000-0026BB765291", - "value": "1.46.13" - }, - { - "format": "string", - "iid": 11, - "perms": [ - "pr" - ], - "type": "00000030-0000-1000-8000-0026BB765291", - "value": "6623462412411853" - }, - { - "format": "bool", - "iid": 6, - "perms": [ - "pw" - ], - "type": "00000014-0000-1000-8000-0026BB765291" - } - ], - "iid": 1, - "type": "0000003E-0000-1000-8000-0026BB765291" - }, - { - "characteristics": [ - { - "format": "string", - "iid": 65591, - "perms": [ - "pr" - ], - "type": "00000037-0000-1000-8000-0026BB765291", - "value": "1.1.0" - } - ], - "iid": 65535, - "type": "000000A2-0000-1000-8000-0026BB765291" - }, - { - "characteristics": [ - { - "format": "string", - "iid": 2817, - "perms": [ - "pr" - ], - "type": "00000023-0000-1000-8000-0026BB765291", - "value": "Hue ambiance spot" - }, - { - "format": "bool", - "iid": 2822, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "00000025-0000-1000-8000-0026BB765291", - "value": true - }, - { - "format": "int", - "iid": 2823, - "maxValue": 100, - "minStep": 1, - "minValue": 0, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "00000008-0000-1000-8000-0026BB765291", - "unit": "percentage", - "value": 100 - }, - { - "description": "Color Temperature", - "format": "uint32", - "iid": 3017, - "maxValue": 454, - "minStep": 1, - "minValue": 153, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "000000CE-0000-1000-8000-0026BB765291", - "value": 366 - }, - { - "description": "ID to uniquely identify service within a single accessory", - "format": "string", - "iid": 2827, - "maxLen": 64, - "perms": [ - "pr" - ], - "type": "D8B76298-42E7-5FFD-B1D6-1782D9A1F936", - "value": "6623462412411853" - } - ], - "iid": 2816, - "type": "00000043-0000-1000-8000-0026BB765291" - } - ] - }, - { - "aid": 6623462403233419, - "services": [ - { - "characteristics": [ - { - "format": "string", - "iid": 5, - "perms": [ - "pr" - ], - "type": "00000023-0000-1000-8000-0026BB765291", - "value": "Hue ambiance candle" - }, - { - "format": "string", - "iid": 4, - "perms": [ - "pr" - ], - "type": "00000021-0000-1000-8000-0026BB765291", - "value": "LTW012" - }, - { - "format": "string", - "iid": 3, - "perms": [ - "pr" - ], - "type": "00000020-0000-1000-8000-0026BB765291", - "value": "Philips" - }, - { - "format": "string", - "iid": 112, - "perms": [ - "pr" - ], - "type": "00000052-0000-1000-8000-0026BB765291", - "value": "1.46.13" - }, - { - "format": "string", - "iid": 11, - "perms": [ - "pr" - ], - "type": "00000030-0000-1000-8000-0026BB765291", - "value": "6623462403233419" - }, - { - "format": "bool", - "iid": 6, - "perms": [ - "pw" - ], - "type": "00000014-0000-1000-8000-0026BB765291" - } - ], - "iid": 1, - "type": "0000003E-0000-1000-8000-0026BB765291" - }, - { - "characteristics": [ - { - "format": "string", - "iid": 65591, - "perms": [ - "pr" - ], - "type": "00000037-0000-1000-8000-0026BB765291", - "value": "1.1.0" - } - ], - "iid": 65535, - "type": "000000A2-0000-1000-8000-0026BB765291" - }, - { - "characteristics": [ - { - "format": "string", - "iid": 2817, - "perms": [ - "pr" - ], - "type": "00000023-0000-1000-8000-0026BB765291", - "value": "Hue ambiance candle" - }, - { - "format": "bool", - "iid": 2822, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "00000025-0000-1000-8000-0026BB765291", - "value": false - }, - { - "format": "int", - "iid": 2823, - "maxValue": 100, - "minStep": 1, - "minValue": 0, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "00000008-0000-1000-8000-0026BB765291", - "unit": "percentage", - "value": 100 - }, - { - "description": "Color Temperature", - "format": "uint32", - "iid": 3017, - "maxValue": 454, - "minStep": 1, - "minValue": 153, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "000000CE-0000-1000-8000-0026BB765291", - "value": 366 - }, - { - "description": "ID to uniquely identify service within a single accessory", - "format": "string", - "iid": 2827, - "maxLen": 64, - "perms": [ - "pr" - ], - "type": "D8B76298-42E7-5FFD-B1D6-1782D9A1F936", - "value": "6623462403233419" - } - ], - "iid": 2816, - "type": "00000043-0000-1000-8000-0026BB765291" - } - ] - }, - { - "aid": 6623462403113447, - "services": [ - { - "characteristics": [ - { - "format": "string", - "iid": 5, - "perms": [ - "pr" - ], - "type": "00000023-0000-1000-8000-0026BB765291", - "value": "Hue ambiance candle" - }, - { - "format": "string", - "iid": 4, - "perms": [ - "pr" - ], - "type": "00000021-0000-1000-8000-0026BB765291", - "value": "LTW012" - }, - { - "format": "string", - "iid": 3, - "perms": [ - "pr" - ], - "type": "00000020-0000-1000-8000-0026BB765291", - "value": "Philips" - }, - { - "format": "string", - "iid": 112, - "perms": [ - "pr" - ], - "type": "00000052-0000-1000-8000-0026BB765291", - "value": "1.46.13" - }, - { - "format": "string", - "iid": 11, - "perms": [ - "pr" - ], - "type": "00000030-0000-1000-8000-0026BB765291", - "value": "6623462403113447" - }, - { - "format": "bool", - "iid": 6, - "perms": [ - "pw" - ], - "type": "00000014-0000-1000-8000-0026BB765291" - } - ], - "iid": 1, - "type": "0000003E-0000-1000-8000-0026BB765291" - }, - { - "characteristics": [ - { - "format": "string", - "iid": 65591, - "perms": [ - "pr" - ], - "type": "00000037-0000-1000-8000-0026BB765291", - "value": "1.1.0" - } - ], - "iid": 65535, - "type": "000000A2-0000-1000-8000-0026BB765291" - }, - { - "characteristics": [ - { - "format": "string", - "iid": 2817, - "perms": [ - "pr" - ], - "type": "00000023-0000-1000-8000-0026BB765291", - "value": "Hue ambiance candle" - }, - { - "format": "bool", - "iid": 2822, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "00000025-0000-1000-8000-0026BB765291", - "value": false - }, - { - "format": "int", - "iid": 2823, - "maxValue": 100, - "minStep": 1, - "minValue": 0, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "00000008-0000-1000-8000-0026BB765291", - "unit": "percentage", - "value": 35 - }, - { - "description": "Color Temperature", - "format": "uint32", - "iid": 3017, - "maxValue": 454, - "minStep": 1, - "minValue": 153, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "000000CE-0000-1000-8000-0026BB765291", - "value": 366 - }, - { - "description": "ID to uniquely identify service within a single accessory", - "format": "string", - "iid": 2827, - "maxLen": 64, - "perms": [ - "pr" - ], - "type": "D8B76298-42E7-5FFD-B1D6-1782D9A1F936", - "value": "6623462403113447" - } - ], - "iid": 2816, - "type": "00000043-0000-1000-8000-0026BB765291" - } - ] - }, - { - "aid": 6623462395276939, - "services": [ - { - "characteristics": [ - { - "format": "string", - "iid": 5, - "perms": [ - "pr" - ], - "type": "00000023-0000-1000-8000-0026BB765291", - "value": "Hue ambiance candle" - }, - { - "format": "string", - "iid": 4, - "perms": [ - "pr" - ], - "type": "00000021-0000-1000-8000-0026BB765291", - "value": "LTW012" - }, - { - "format": "string", - "iid": 3, - "perms": [ - "pr" - ], - "type": "00000020-0000-1000-8000-0026BB765291", - "value": "Philips" - }, - { - "format": "string", - "iid": 112, - "perms": [ - "pr" - ], - "type": "00000052-0000-1000-8000-0026BB765291", - "value": "1.46.13" - }, - { - "format": "string", - "iid": 11, - "perms": [ - "pr" - ], - "type": "00000030-0000-1000-8000-0026BB765291", - "value": "6623462395276939" - }, - { - "format": "bool", - "iid": 6, - "perms": [ - "pw" - ], - "type": "00000014-0000-1000-8000-0026BB765291" - } - ], - "iid": 1, - "type": "0000003E-0000-1000-8000-0026BB765291" - }, - { - "characteristics": [ - { - "format": "string", - "iid": 65591, - "perms": [ - "pr" - ], - "type": "00000037-0000-1000-8000-0026BB765291", - "value": "1.1.0" - } - ], - "iid": 65535, - "type": "000000A2-0000-1000-8000-0026BB765291" - }, - { - "characteristics": [ - { - "format": "string", - "iid": 2817, - "perms": [ - "pr" - ], - "type": "00000023-0000-1000-8000-0026BB765291", - "value": "Hue ambiance candle" - }, - { - "format": "bool", - "iid": 2822, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "00000025-0000-1000-8000-0026BB765291", - "value": false - }, - { - "format": "int", - "iid": 2823, - "maxValue": 100, - "minStep": 1, - "minValue": 0, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "00000008-0000-1000-8000-0026BB765291", - "unit": "percentage", - "value": 100 - }, - { - "description": "Color Temperature", - "format": "uint32", - "iid": 3017, - "maxValue": 454, - "minStep": 1, - "minValue": 153, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "000000CE-0000-1000-8000-0026BB765291", - "value": 366 - }, - { - "description": "ID to uniquely identify service within a single accessory", - "format": "string", - "iid": 2827, - "maxLen": 64, - "perms": [ - "pr" - ], - "type": "D8B76298-42E7-5FFD-B1D6-1782D9A1F936", - "value": "6623462395276939" - } - ], - "iid": 2816, - "type": "00000043-0000-1000-8000-0026BB765291" - } - ] - }, - { - "aid": 6623462395276914, - "services": [ - { - "characteristics": [ - { - "format": "string", - "iid": 5, - "perms": [ - "pr" - ], - "type": "00000023-0000-1000-8000-0026BB765291", - "value": "Hue ambiance candle" - }, - { - "format": "string", - "iid": 4, - "perms": [ - "pr" - ], - "type": "00000021-0000-1000-8000-0026BB765291", - "value": "LTW012" - }, - { - "format": "string", - "iid": 3, - "perms": [ - "pr" - ], - "type": "00000020-0000-1000-8000-0026BB765291", - "value": "Philips" - }, - { - "format": "string", - "iid": 112, - "perms": [ - "pr" - ], - "type": "00000052-0000-1000-8000-0026BB765291", - "value": "1.46.13" - }, - { - "format": "string", - "iid": 11, - "perms": [ - "pr" - ], - "type": "00000030-0000-1000-8000-0026BB765291", - "value": "6623462395276914" - }, - { - "format": "bool", - "iid": 6, - "perms": [ - "pw" - ], - "type": "00000014-0000-1000-8000-0026BB765291" - } - ], - "iid": 1, - "type": "0000003E-0000-1000-8000-0026BB765291" - }, - { - "characteristics": [ - { - "format": "string", - "iid": 65591, - "perms": [ - "pr" - ], - "type": "00000037-0000-1000-8000-0026BB765291", - "value": "1.1.0" - } - ], - "iid": 65535, - "type": "000000A2-0000-1000-8000-0026BB765291" - }, - { - "characteristics": [ - { - "format": "string", - "iid": 2817, - "perms": [ - "pr" - ], - "type": "00000023-0000-1000-8000-0026BB765291", - "value": "Hue ambiance candle" - }, - { - "format": "bool", - "iid": 2822, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "00000025-0000-1000-8000-0026BB765291", - "value": false - }, - { - "format": "int", - "iid": 2823, - "maxValue": 100, - "minStep": 1, - "minValue": 0, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "00000008-0000-1000-8000-0026BB765291", - "unit": "percentage", - "value": 100 - }, - { - "description": "Color Temperature", - "format": "uint32", - "iid": 3017, - "maxValue": 454, - "minStep": 1, - "minValue": 153, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "000000CE-0000-1000-8000-0026BB765291", - "value": 366 - }, - { - "description": "ID to uniquely identify service within a single accessory", - "format": "string", - "iid": 2827, - "maxLen": 64, - "perms": [ - "pr" - ], - "type": "D8B76298-42E7-5FFD-B1D6-1782D9A1F936", - "value": "6623462395276914" - } - ], - "iid": 2816, - "type": "00000043-0000-1000-8000-0026BB765291" - } - ] - } -] \ No newline at end of file + { + "aid": 6623462389072572, + "services": [ + { + "characteristics": [ + { + "format": "string", + "iid": 37, + "perms": ["pr"], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Hue dimmer switch" + }, + { + "format": "string", + "iid": 35, + "perms": ["pr"], + "type": "00000021-0000-1000-8000-0026BB765291", + "value": "RWL021" + }, + { + "format": "string", + "iid": 34, + "perms": ["pr"], + "type": "00000020-0000-1000-8000-0026BB765291", + "value": "Philips" + }, + { + "format": "string", + "iid": 84, + "perms": ["pr"], + "type": "00000052-0000-1000-8000-0026BB765291", + "value": "45.1.17846" + }, + { + "format": "string", + "iid": 50, + "perms": ["pr"], + "type": "00000030-0000-1000-8000-0026BB765291", + "value": "6623462389072572" + }, + { + "format": "bool", + "iid": 22, + "perms": ["pw"], + "type": "00000014-0000-1000-8000-0026BB765291" + } + ], + "iid": 1, + "type": "0000003E-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "format": "string", + "iid": 65591, + "perms": ["pr"], + "type": "00000037-0000-1000-8000-0026BB765291", + "value": "1.1.0" + } + ], + "iid": 65535, + "type": "000000A2-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "format": "string", + "iid": 644245094436, + "perms": ["pr"], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Hue dimmer switch battery" + }, + { + "format": "uint8", + "iid": 644245094505, + "maxValue": 100, + "minStep": 1, + "minValue": 0, + "perms": ["pr", "ev"], + "type": "00000068-0000-1000-8000-0026BB765291", + "unit": "percentage", + "value": 100 + }, + { + "format": "uint8", + "iid": 644245094522, + "maxValue": 1, + "minStep": 1, + "minValue": 0, + "perms": ["pr", "ev"], + "type": "00000079-0000-1000-8000-0026BB765291", + "value": 0 + }, + { + "format": "uint8", + "iid": 644245094544, + "maxValue": 2, + "minStep": 1, + "minValue": 0, + "perms": ["pr", "ev"], + "type": "0000008F-0000-1000-8000-0026BB765291", + "value": 2 + }, + { + "description": "ID to uniquely identify service within a single accessory", + "format": "string", + "iid": 644245149880, + "maxLen": 64, + "perms": ["pr"], + "type": "D8B76298-42E7-5FFD-B1D6-1782D9A1F936", + "value": "6623462389072572" + } + ], + "iid": 644245094400, + "type": "00000096-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "format": "string", + "iid": 588410585124, + "perms": ["pr"], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Hue dimmer switch button 1" + }, + { + "format": "uint8", + "iid": 588410585204, + "maxValue": 0, + "minStep": 1, + "minValue": 0, + "perms": ["pr", "ev"], + "type": "00000073-0000-1000-8000-0026BB765291", + "value": null + }, + { + "format": "uint8", + "iid": 588410585292, + "minStep": 1, + "minValue": 1, + "perms": ["pr"], + "type": "000000CB-0000-1000-8000-0026BB765291", + "value": 1 + }, + { + "description": "ID to uniquely identify service within a single accessory", + "format": "string", + "iid": 588410640568, + "maxLen": 64, + "perms": ["pr"], + "type": "D8B76298-42E7-5FFD-B1D6-1782D9A1F936", + "value": "6623462389072572" + } + ], + "iid": 588410585088, + "linked": [256], + "type": "00000089-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "format": "uint8", + "iid": 462, + "maxValue": 1, + "minStep": 1, + "minValue": 0, + "perms": ["pr"], + "type": "000000CD-0000-1000-8000-0026BB765291", + "value": 1 + } + ], + "iid": 256, + "type": "000000CC-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "format": "string", + "iid": 588410650660, + "perms": ["pr"], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Hue dimmer switch button 2" + }, + { + "format": "uint8", + "iid": 588410650740, + "maxValue": 0, + "minStep": 1, + "minValue": 0, + "perms": ["pr", "ev"], + "type": "00000073-0000-1000-8000-0026BB765291", + "value": null + }, + { + "format": "uint8", + "iid": 588410650828, + "minStep": 1, + "minValue": 1, + "perms": ["pr"], + "type": "000000CB-0000-1000-8000-0026BB765291", + "value": 2 + }, + { + "description": "ID to uniquely identify service within a single accessory", + "format": "string", + "iid": 588410706104, + "maxLen": 64, + "perms": ["pr"], + "type": "D8B76298-42E7-5FFD-B1D6-1782D9A1F936", + "value": "6623462389072572" + } + ], + "iid": 588410650624, + "linked": [256], + "type": "00000089-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "format": "string", + "iid": 588410716196, + "perms": ["pr"], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Hue dimmer switch button 3" + }, + { + "format": "uint8", + "iid": 588410716276, + "maxValue": 0, + "minStep": 1, + "minValue": 0, + "perms": ["pr", "ev"], + "type": "00000073-0000-1000-8000-0026BB765291", + "value": null + }, + { + "format": "uint8", + "iid": 588410716364, + "minStep": 1, + "minValue": 1, + "perms": ["pr"], + "type": "000000CB-0000-1000-8000-0026BB765291", + "value": 3 + }, + { + "description": "ID to uniquely identify service within a single accessory", + "format": "string", + "iid": 588410771640, + "maxLen": 64, + "perms": ["pr"], + "type": "D8B76298-42E7-5FFD-B1D6-1782D9A1F936", + "value": "6623462389072572" + } + ], + "iid": 588410716160, + "linked": [256], + "type": "00000089-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "format": "string", + "iid": 588410781732, + "perms": ["pr"], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Hue dimmer switch button 4" + }, + { + "format": "uint8", + "iid": 588410781812, + "maxValue": 0, + "minStep": 1, + "minValue": 0, + "perms": ["pr", "ev"], + "type": "00000073-0000-1000-8000-0026BB765291", + "value": null + }, + { + "format": "uint8", + "iid": 588410781900, + "minStep": 1, + "minValue": 1, + "perms": ["pr"], + "type": "000000CB-0000-1000-8000-0026BB765291", + "value": 4 + }, + { + "description": "ID to uniquely identify service within a single accessory", + "format": "string", + "iid": 588410837176, + "maxLen": 64, + "perms": ["pr"], + "type": "D8B76298-42E7-5FFD-B1D6-1782D9A1F936", + "value": "6623462389072572" + } + ], + "iid": 588410781696, + "linked": [256], + "type": "00000089-0000-1000-8000-0026BB765291" + } + ] + }, + { + "aid": 1, + "services": [ + { + "characteristics": [ + { + "format": "string", + "iid": 2, + "perms": ["pr"], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Philips hue - 482544" + }, + { + "format": "string", + "iid": 4, + "perms": ["pr"], + "type": "00000021-0000-1000-8000-0026BB765291", + "value": "BSB002" + }, + { + "format": "string", + "iid": 3, + "perms": ["pr"], + "type": "00000020-0000-1000-8000-0026BB765291", + "value": "Philips Lighting" + }, + { + "format": "string", + "iid": 8, + "perms": ["pr"], + "type": "00000052-0000-1000-8000-0026BB765291", + "value": "1.32.1932126170" + }, + { + "format": "string", + "iid": 6, + "perms": ["pr"], + "type": "00000030-0000-1000-8000-0026BB765291", + "value": "123456" + }, + { + "format": "bool", + "iid": 7, + "perms": ["pw"], + "type": "00000014-0000-1000-8000-0026BB765291" + } + ], + "iid": 1, + "type": "0000003E-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "format": "string", + "iid": 65591, + "perms": ["pr"], + "type": "00000037-0000-1000-8000-0026BB765291", + "value": "1.1.0" + } + ], + "iid": 65535, + "type": "000000A2-0000-1000-8000-0026BB765291" + } + ] + }, + { + "aid": 6623462378982941, + "services": [ + { + "characteristics": [ + { + "format": "string", + "iid": 5, + "perms": ["pr"], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Hue white lamp" + }, + { + "format": "string", + "iid": 4, + "perms": ["pr"], + "type": "00000021-0000-1000-8000-0026BB765291", + "value": "LWB010" + }, + { + "format": "string", + "iid": 3, + "perms": ["pr"], + "type": "00000020-0000-1000-8000-0026BB765291", + "value": "Philips" + }, + { + "format": "string", + "iid": 112, + "perms": ["pr"], + "type": "00000052-0000-1000-8000-0026BB765291", + "value": "1.46.13" + }, + { + "format": "string", + "iid": 11, + "perms": ["pr"], + "type": "00000030-0000-1000-8000-0026BB765291", + "value": "6623462378982941" + }, + { + "format": "bool", + "iid": 6, + "perms": ["pw"], + "type": "00000014-0000-1000-8000-0026BB765291" + } + ], + "iid": 1, + "type": "0000003E-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "format": "string", + "iid": 65591, + "perms": ["pr"], + "type": "00000037-0000-1000-8000-0026BB765291", + "value": "1.1.0" + } + ], + "iid": 65535, + "type": "000000A2-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "format": "string", + "iid": 2817, + "perms": ["pr"], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Hue white lamp" + }, + { + "format": "bool", + "iid": 2822, + "perms": ["pr", "pw", "ev"], + "type": "00000025-0000-1000-8000-0026BB765291", + "value": false + }, + { + "format": "int", + "iid": 2823, + "maxValue": 100, + "minStep": 1, + "minValue": 0, + "perms": ["pr", "pw", "ev"], + "type": "00000008-0000-1000-8000-0026BB765291", + "unit": "percentage", + "value": 100 + }, + { + "description": "ID to uniquely identify service within a single accessory", + "format": "string", + "iid": 2827, + "maxLen": 64, + "perms": ["pr"], + "type": "D8B76298-42E7-5FFD-B1D6-1782D9A1F936", + "value": "6623462378982941" + } + ], + "iid": 2816, + "type": "00000043-0000-1000-8000-0026BB765291" + } + ] + }, + { + "aid": 6623462378983942, + "services": [ + { + "characteristics": [ + { + "format": "string", + "iid": 5, + "perms": ["pr"], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Hue white lamp" + }, + { + "format": "string", + "iid": 4, + "perms": ["pr"], + "type": "00000021-0000-1000-8000-0026BB765291", + "value": "LWB010" + }, + { + "format": "string", + "iid": 3, + "perms": ["pr"], + "type": "00000020-0000-1000-8000-0026BB765291", + "value": "Philips" + }, + { + "format": "string", + "iid": 112, + "perms": ["pr"], + "type": "00000052-0000-1000-8000-0026BB765291", + "value": "1.46.13" + }, + { + "format": "string", + "iid": 11, + "perms": ["pr"], + "type": "00000030-0000-1000-8000-0026BB765291", + "value": "6623462378983942" + }, + { + "format": "bool", + "iid": 6, + "perms": ["pw"], + "type": "00000014-0000-1000-8000-0026BB765291" + } + ], + "iid": 1, + "type": "0000003E-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "format": "string", + "iid": 65591, + "perms": ["pr"], + "type": "00000037-0000-1000-8000-0026BB765291", + "value": "1.1.0" + } + ], + "iid": 65535, + "type": "000000A2-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "format": "string", + "iid": 2817, + "perms": ["pr"], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Hue white lamp" + }, + { + "format": "bool", + "iid": 2822, + "perms": ["pr", "pw", "ev"], + "type": "00000025-0000-1000-8000-0026BB765291", + "value": false + }, + { + "format": "int", + "iid": 2823, + "maxValue": 100, + "minStep": 1, + "minValue": 0, + "perms": ["pr", "pw", "ev"], + "type": "00000008-0000-1000-8000-0026BB765291", + "unit": "percentage", + "value": 100 + }, + { + "description": "ID to uniquely identify service within a single accessory", + "format": "string", + "iid": 2827, + "maxLen": 64, + "perms": ["pr"], + "type": "D8B76298-42E7-5FFD-B1D6-1782D9A1F936", + "value": "6623462378983942" + } + ], + "iid": 2816, + "type": "00000043-0000-1000-8000-0026BB765291" + } + ] + }, + { + "aid": 6623462379123707, + "services": [ + { + "characteristics": [ + { + "format": "string", + "iid": 5, + "perms": ["pr"], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Hue white lamp" + }, + { + "format": "string", + "iid": 4, + "perms": ["pr"], + "type": "00000021-0000-1000-8000-0026BB765291", + "value": "LWB010" + }, + { + "format": "string", + "iid": 3, + "perms": ["pr"], + "type": "00000020-0000-1000-8000-0026BB765291", + "value": "Philips" + }, + { + "format": "string", + "iid": 112, + "perms": ["pr"], + "type": "00000052-0000-1000-8000-0026BB765291", + "value": "1.46.13" + }, + { + "format": "string", + "iid": 11, + "perms": ["pr"], + "type": "00000030-0000-1000-8000-0026BB765291", + "value": "6623462379123707" + }, + { + "format": "bool", + "iid": 6, + "perms": ["pw"], + "type": "00000014-0000-1000-8000-0026BB765291" + } + ], + "iid": 1, + "type": "0000003E-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "format": "string", + "iid": 65591, + "perms": ["pr"], + "type": "00000037-0000-1000-8000-0026BB765291", + "value": "1.1.0" + } + ], + "iid": 65535, + "type": "000000A2-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "format": "string", + "iid": 2817, + "perms": ["pr"], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Hue white lamp" + }, + { + "format": "bool", + "iid": 2822, + "perms": ["pr", "pw", "ev"], + "type": "00000025-0000-1000-8000-0026BB765291", + "value": false + }, + { + "format": "int", + "iid": 2823, + "maxValue": 100, + "minStep": 1, + "minValue": 0, + "perms": ["pr", "pw", "ev"], + "type": "00000008-0000-1000-8000-0026BB765291", + "unit": "percentage", + "value": 100 + }, + { + "description": "ID to uniquely identify service within a single accessory", + "format": "string", + "iid": 2827, + "maxLen": 64, + "perms": ["pr"], + "type": "D8B76298-42E7-5FFD-B1D6-1782D9A1F936", + "value": "6623462379123707" + } + ], + "iid": 2816, + "type": "00000043-0000-1000-8000-0026BB765291" + } + ] + }, + { + "aid": 6623462379122122, + "services": [ + { + "characteristics": [ + { + "format": "string", + "iid": 5, + "perms": ["pr"], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Hue white lamp" + }, + { + "format": "string", + "iid": 4, + "perms": ["pr"], + "type": "00000021-0000-1000-8000-0026BB765291", + "value": "LWB010" + }, + { + "format": "string", + "iid": 3, + "perms": ["pr"], + "type": "00000020-0000-1000-8000-0026BB765291", + "value": "Philips" + }, + { + "format": "string", + "iid": 112, + "perms": ["pr"], + "type": "00000052-0000-1000-8000-0026BB765291", + "value": "1.46.13" + }, + { + "format": "string", + "iid": 11, + "perms": ["pr"], + "type": "00000030-0000-1000-8000-0026BB765291", + "value": "6623462379122122" + }, + { + "format": "bool", + "iid": 6, + "perms": ["pw"], + "type": "00000014-0000-1000-8000-0026BB765291" + } + ], + "iid": 1, + "type": "0000003E-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "format": "string", + "iid": 65591, + "perms": ["pr"], + "type": "00000037-0000-1000-8000-0026BB765291", + "value": "1.1.0" + } + ], + "iid": 65535, + "type": "000000A2-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "format": "string", + "iid": 2817, + "perms": ["pr"], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Hue white lamp" + }, + { + "format": "bool", + "iid": 2822, + "perms": ["pr", "pw", "ev"], + "type": "00000025-0000-1000-8000-0026BB765291", + "value": false + }, + { + "format": "int", + "iid": 2823, + "maxValue": 100, + "minStep": 1, + "minValue": 0, + "perms": ["pr", "pw", "ev"], + "type": "00000008-0000-1000-8000-0026BB765291", + "unit": "percentage", + "value": 70 + }, + { + "description": "ID to uniquely identify service within a single accessory", + "format": "string", + "iid": 2827, + "maxLen": 64, + "perms": ["pr"], + "type": "D8B76298-42E7-5FFD-B1D6-1782D9A1F936", + "value": "6623462379122122" + } + ], + "iid": 2816, + "type": "00000043-0000-1000-8000-0026BB765291" + } + ] + }, + { + "aid": 6623462385996792, + "services": [ + { + "characteristics": [ + { + "format": "string", + "iid": 5, + "perms": ["pr"], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Hue white lamp" + }, + { + "format": "string", + "iid": 4, + "perms": ["pr"], + "type": "00000021-0000-1000-8000-0026BB765291", + "value": "LWB010" + }, + { + "format": "string", + "iid": 3, + "perms": ["pr"], + "type": "00000020-0000-1000-8000-0026BB765291", + "value": "Philips" + }, + { + "format": "string", + "iid": 112, + "perms": ["pr"], + "type": "00000052-0000-1000-8000-0026BB765291", + "value": "1.46.13" + }, + { + "format": "string", + "iid": 11, + "perms": ["pr"], + "type": "00000030-0000-1000-8000-0026BB765291", + "value": "6623462385996792" + }, + { + "format": "bool", + "iid": 6, + "perms": ["pw"], + "type": "00000014-0000-1000-8000-0026BB765291" + } + ], + "iid": 1, + "type": "0000003E-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "format": "string", + "iid": 65591, + "perms": ["pr"], + "type": "00000037-0000-1000-8000-0026BB765291", + "value": "1.1.0" + } + ], + "iid": 65535, + "type": "000000A2-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "format": "string", + "iid": 2817, + "perms": ["pr"], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Hue white lamp" + }, + { + "format": "bool", + "iid": 2822, + "perms": ["pr", "pw", "ev"], + "type": "00000025-0000-1000-8000-0026BB765291", + "value": false + }, + { + "format": "int", + "iid": 2823, + "maxValue": 100, + "minStep": 1, + "minValue": 0, + "perms": ["pr", "pw", "ev"], + "type": "00000008-0000-1000-8000-0026BB765291", + "unit": "percentage", + "value": 100 + }, + { + "description": "ID to uniquely identify service within a single accessory", + "format": "string", + "iid": 2827, + "maxLen": 64, + "perms": ["pr"], + "type": "D8B76298-42E7-5FFD-B1D6-1782D9A1F936", + "value": "6623462385996792" + } + ], + "iid": 2816, + "type": "00000043-0000-1000-8000-0026BB765291" + } + ] + }, + { + "aid": 6623462383114193, + "services": [ + { + "characteristics": [ + { + "format": "string", + "iid": 5, + "perms": ["pr"], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Hue white lamp" + }, + { + "format": "string", + "iid": 4, + "perms": ["pr"], + "type": "00000021-0000-1000-8000-0026BB765291", + "value": "LWB010" + }, + { + "format": "string", + "iid": 3, + "perms": ["pr"], + "type": "00000020-0000-1000-8000-0026BB765291", + "value": "Philips" + }, + { + "format": "string", + "iid": 112, + "perms": ["pr"], + "type": "00000052-0000-1000-8000-0026BB765291", + "value": "1.46.13" + }, + { + "format": "string", + "iid": 11, + "perms": ["pr"], + "type": "00000030-0000-1000-8000-0026BB765291", + "value": "6623462383114193" + }, + { + "format": "bool", + "iid": 6, + "perms": ["pw"], + "type": "00000014-0000-1000-8000-0026BB765291" + } + ], + "iid": 1, + "type": "0000003E-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "format": "string", + "iid": 65591, + "perms": ["pr"], + "type": "00000037-0000-1000-8000-0026BB765291", + "value": "1.1.0" + } + ], + "iid": 65535, + "type": "000000A2-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "format": "string", + "iid": 2817, + "perms": ["pr"], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Hue white lamp" + }, + { + "format": "bool", + "iid": 2822, + "perms": ["pr", "pw", "ev"], + "type": "00000025-0000-1000-8000-0026BB765291", + "value": false + }, + { + "format": "int", + "iid": 2823, + "maxValue": 100, + "minStep": 1, + "minValue": 0, + "perms": ["pr", "pw", "ev"], + "type": "00000008-0000-1000-8000-0026BB765291", + "unit": "percentage", + "value": 20 + }, + { + "description": "ID to uniquely identify service within a single accessory", + "format": "string", + "iid": 2827, + "maxLen": 64, + "perms": ["pr"], + "type": "D8B76298-42E7-5FFD-B1D6-1782D9A1F936", + "value": "6623462383114193" + } + ], + "iid": 2816, + "type": "00000043-0000-1000-8000-0026BB765291" + } + ] + }, + { + "aid": 6623462383114163, + "services": [ + { + "characteristics": [ + { + "format": "string", + "iid": 5, + "perms": ["pr"], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Hue white lamp" + }, + { + "format": "string", + "iid": 4, + "perms": ["pr"], + "type": "00000021-0000-1000-8000-0026BB765291", + "value": "LWB010" + }, + { + "format": "string", + "iid": 3, + "perms": ["pr"], + "type": "00000020-0000-1000-8000-0026BB765291", + "value": "Philips" + }, + { + "format": "string", + "iid": 112, + "perms": ["pr"], + "type": "00000052-0000-1000-8000-0026BB765291", + "value": "1.46.13" + }, + { + "format": "string", + "iid": 11, + "perms": ["pr"], + "type": "00000030-0000-1000-8000-0026BB765291", + "value": "6623462383114163" + }, + { + "format": "bool", + "iid": 6, + "perms": ["pw"], + "type": "00000014-0000-1000-8000-0026BB765291" + } + ], + "iid": 1, + "type": "0000003E-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "format": "string", + "iid": 65591, + "perms": ["pr"], + "type": "00000037-0000-1000-8000-0026BB765291", + "value": "1.1.0" + } + ], + "iid": 65535, + "type": "000000A2-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "format": "string", + "iid": 2817, + "perms": ["pr"], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Hue white lamp" + }, + { + "format": "bool", + "iid": 2822, + "perms": ["pr", "pw", "ev"], + "type": "00000025-0000-1000-8000-0026BB765291", + "value": false + }, + { + "format": "int", + "iid": 2823, + "maxValue": 100, + "minStep": 1, + "minValue": 0, + "perms": ["pr", "pw", "ev"], + "type": "00000008-0000-1000-8000-0026BB765291", + "unit": "percentage", + "value": 100 + }, + { + "description": "ID to uniquely identify service within a single accessory", + "format": "string", + "iid": 2827, + "maxLen": 64, + "perms": ["pr"], + "type": "D8B76298-42E7-5FFD-B1D6-1782D9A1F936", + "value": "6623462383114163" + } + ], + "iid": 2816, + "type": "00000043-0000-1000-8000-0026BB765291" + } + ] + }, + { + "aid": 6623462412413293, + "services": [ + { + "characteristics": [ + { + "format": "string", + "iid": 5, + "perms": ["pr"], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Hue ambiance spot" + }, + { + "format": "string", + "iid": 4, + "perms": ["pr"], + "type": "00000021-0000-1000-8000-0026BB765291", + "value": "LTW013" + }, + { + "format": "string", + "iid": 3, + "perms": ["pr"], + "type": "00000020-0000-1000-8000-0026BB765291", + "value": "Philips" + }, + { + "format": "string", + "iid": 112, + "perms": ["pr"], + "type": "00000052-0000-1000-8000-0026BB765291", + "value": "1.46.13" + }, + { + "format": "string", + "iid": 11, + "perms": ["pr"], + "type": "00000030-0000-1000-8000-0026BB765291", + "value": "6623462412413293" + }, + { + "format": "bool", + "iid": 6, + "perms": ["pw"], + "type": "00000014-0000-1000-8000-0026BB765291" + } + ], + "iid": 1, + "type": "0000003E-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "format": "string", + "iid": 65591, + "perms": ["pr"], + "type": "00000037-0000-1000-8000-0026BB765291", + "value": "1.1.0" + } + ], + "iid": 65535, + "type": "000000A2-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "format": "string", + "iid": 2817, + "perms": ["pr"], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Hue ambiance spot" + }, + { + "format": "bool", + "iid": 2822, + "perms": ["pr", "pw", "ev"], + "type": "00000025-0000-1000-8000-0026BB765291", + "value": true + }, + { + "format": "int", + "iid": 2823, + "maxValue": 100, + "minStep": 1, + "minValue": 0, + "perms": ["pr", "pw", "ev"], + "type": "00000008-0000-1000-8000-0026BB765291", + "unit": "percentage", + "value": 100 + }, + { + "description": "Color Temperature", + "format": "uint32", + "iid": 3017, + "maxValue": 454, + "minStep": 1, + "minValue": 153, + "perms": ["pr", "pw", "ev"], + "type": "000000CE-0000-1000-8000-0026BB765291", + "value": 366 + }, + { + "description": "ID to uniquely identify service within a single accessory", + "format": "string", + "iid": 2827, + "maxLen": 64, + "perms": ["pr"], + "type": "D8B76298-42E7-5FFD-B1D6-1782D9A1F936", + "value": "6623462412413293" + } + ], + "iid": 2816, + "type": "00000043-0000-1000-8000-0026BB765291" + } + ] + }, + { + "aid": 6623462412411853, + "services": [ + { + "characteristics": [ + { + "format": "string", + "iid": 5, + "perms": ["pr"], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Hue ambiance spot" + }, + { + "format": "string", + "iid": 4, + "perms": ["pr"], + "type": "00000021-0000-1000-8000-0026BB765291", + "value": "LTW013" + }, + { + "format": "string", + "iid": 3, + "perms": ["pr"], + "type": "00000020-0000-1000-8000-0026BB765291", + "value": "Philips" + }, + { + "format": "string", + "iid": 112, + "perms": ["pr"], + "type": "00000052-0000-1000-8000-0026BB765291", + "value": "1.46.13" + }, + { + "format": "string", + "iid": 11, + "perms": ["pr"], + "type": "00000030-0000-1000-8000-0026BB765291", + "value": "6623462412411853" + }, + { + "format": "bool", + "iid": 6, + "perms": ["pw"], + "type": "00000014-0000-1000-8000-0026BB765291" + } + ], + "iid": 1, + "type": "0000003E-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "format": "string", + "iid": 65591, + "perms": ["pr"], + "type": "00000037-0000-1000-8000-0026BB765291", + "value": "1.1.0" + } + ], + "iid": 65535, + "type": "000000A2-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "format": "string", + "iid": 2817, + "perms": ["pr"], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Hue ambiance spot" + }, + { + "format": "bool", + "iid": 2822, + "perms": ["pr", "pw", "ev"], + "type": "00000025-0000-1000-8000-0026BB765291", + "value": true + }, + { + "format": "int", + "iid": 2823, + "maxValue": 100, + "minStep": 1, + "minValue": 0, + "perms": ["pr", "pw", "ev"], + "type": "00000008-0000-1000-8000-0026BB765291", + "unit": "percentage", + "value": 100 + }, + { + "description": "Color Temperature", + "format": "uint32", + "iid": 3017, + "maxValue": 454, + "minStep": 1, + "minValue": 153, + "perms": ["pr", "pw", "ev"], + "type": "000000CE-0000-1000-8000-0026BB765291", + "value": 366 + }, + { + "description": "ID to uniquely identify service within a single accessory", + "format": "string", + "iid": 2827, + "maxLen": 64, + "perms": ["pr"], + "type": "D8B76298-42E7-5FFD-B1D6-1782D9A1F936", + "value": "6623462412411853" + } + ], + "iid": 2816, + "type": "00000043-0000-1000-8000-0026BB765291" + } + ] + }, + { + "aid": 6623462403233419, + "services": [ + { + "characteristics": [ + { + "format": "string", + "iid": 5, + "perms": ["pr"], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Hue ambiance candle" + }, + { + "format": "string", + "iid": 4, + "perms": ["pr"], + "type": "00000021-0000-1000-8000-0026BB765291", + "value": "LTW012" + }, + { + "format": "string", + "iid": 3, + "perms": ["pr"], + "type": "00000020-0000-1000-8000-0026BB765291", + "value": "Philips" + }, + { + "format": "string", + "iid": 112, + "perms": ["pr"], + "type": "00000052-0000-1000-8000-0026BB765291", + "value": "1.46.13" + }, + { + "format": "string", + "iid": 11, + "perms": ["pr"], + "type": "00000030-0000-1000-8000-0026BB765291", + "value": "6623462403233419" + }, + { + "format": "bool", + "iid": 6, + "perms": ["pw"], + "type": "00000014-0000-1000-8000-0026BB765291" + } + ], + "iid": 1, + "type": "0000003E-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "format": "string", + "iid": 65591, + "perms": ["pr"], + "type": "00000037-0000-1000-8000-0026BB765291", + "value": "1.1.0" + } + ], + "iid": 65535, + "type": "000000A2-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "format": "string", + "iid": 2817, + "perms": ["pr"], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Hue ambiance candle" + }, + { + "format": "bool", + "iid": 2822, + "perms": ["pr", "pw", "ev"], + "type": "00000025-0000-1000-8000-0026BB765291", + "value": false + }, + { + "format": "int", + "iid": 2823, + "maxValue": 100, + "minStep": 1, + "minValue": 0, + "perms": ["pr", "pw", "ev"], + "type": "00000008-0000-1000-8000-0026BB765291", + "unit": "percentage", + "value": 100 + }, + { + "description": "Color Temperature", + "format": "uint32", + "iid": 3017, + "maxValue": 454, + "minStep": 1, + "minValue": 153, + "perms": ["pr", "pw", "ev"], + "type": "000000CE-0000-1000-8000-0026BB765291", + "value": 366 + }, + { + "description": "ID to uniquely identify service within a single accessory", + "format": "string", + "iid": 2827, + "maxLen": 64, + "perms": ["pr"], + "type": "D8B76298-42E7-5FFD-B1D6-1782D9A1F936", + "value": "6623462403233419" + } + ], + "iid": 2816, + "type": "00000043-0000-1000-8000-0026BB765291" + } + ] + }, + { + "aid": 6623462403113447, + "services": [ + { + "characteristics": [ + { + "format": "string", + "iid": 5, + "perms": ["pr"], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Hue ambiance candle" + }, + { + "format": "string", + "iid": 4, + "perms": ["pr"], + "type": "00000021-0000-1000-8000-0026BB765291", + "value": "LTW012" + }, + { + "format": "string", + "iid": 3, + "perms": ["pr"], + "type": "00000020-0000-1000-8000-0026BB765291", + "value": "Philips" + }, + { + "format": "string", + "iid": 112, + "perms": ["pr"], + "type": "00000052-0000-1000-8000-0026BB765291", + "value": "1.46.13" + }, + { + "format": "string", + "iid": 11, + "perms": ["pr"], + "type": "00000030-0000-1000-8000-0026BB765291", + "value": "6623462403113447" + }, + { + "format": "bool", + "iid": 6, + "perms": ["pw"], + "type": "00000014-0000-1000-8000-0026BB765291" + } + ], + "iid": 1, + "type": "0000003E-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "format": "string", + "iid": 65591, + "perms": ["pr"], + "type": "00000037-0000-1000-8000-0026BB765291", + "value": "1.1.0" + } + ], + "iid": 65535, + "type": "000000A2-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "format": "string", + "iid": 2817, + "perms": ["pr"], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Hue ambiance candle" + }, + { + "format": "bool", + "iid": 2822, + "perms": ["pr", "pw", "ev"], + "type": "00000025-0000-1000-8000-0026BB765291", + "value": false + }, + { + "format": "int", + "iid": 2823, + "maxValue": 100, + "minStep": 1, + "minValue": 0, + "perms": ["pr", "pw", "ev"], + "type": "00000008-0000-1000-8000-0026BB765291", + "unit": "percentage", + "value": 35 + }, + { + "description": "Color Temperature", + "format": "uint32", + "iid": 3017, + "maxValue": 454, + "minStep": 1, + "minValue": 153, + "perms": ["pr", "pw", "ev"], + "type": "000000CE-0000-1000-8000-0026BB765291", + "value": 366 + }, + { + "description": "ID to uniquely identify service within a single accessory", + "format": "string", + "iid": 2827, + "maxLen": 64, + "perms": ["pr"], + "type": "D8B76298-42E7-5FFD-B1D6-1782D9A1F936", + "value": "6623462403113447" + } + ], + "iid": 2816, + "type": "00000043-0000-1000-8000-0026BB765291" + } + ] + }, + { + "aid": 6623462395276939, + "services": [ + { + "characteristics": [ + { + "format": "string", + "iid": 5, + "perms": ["pr"], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Hue ambiance candle" + }, + { + "format": "string", + "iid": 4, + "perms": ["pr"], + "type": "00000021-0000-1000-8000-0026BB765291", + "value": "LTW012" + }, + { + "format": "string", + "iid": 3, + "perms": ["pr"], + "type": "00000020-0000-1000-8000-0026BB765291", + "value": "Philips" + }, + { + "format": "string", + "iid": 112, + "perms": ["pr"], + "type": "00000052-0000-1000-8000-0026BB765291", + "value": "1.46.13" + }, + { + "format": "string", + "iid": 11, + "perms": ["pr"], + "type": "00000030-0000-1000-8000-0026BB765291", + "value": "6623462395276939" + }, + { + "format": "bool", + "iid": 6, + "perms": ["pw"], + "type": "00000014-0000-1000-8000-0026BB765291" + } + ], + "iid": 1, + "type": "0000003E-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "format": "string", + "iid": 65591, + "perms": ["pr"], + "type": "00000037-0000-1000-8000-0026BB765291", + "value": "1.1.0" + } + ], + "iid": 65535, + "type": "000000A2-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "format": "string", + "iid": 2817, + "perms": ["pr"], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Hue ambiance candle" + }, + { + "format": "bool", + "iid": 2822, + "perms": ["pr", "pw", "ev"], + "type": "00000025-0000-1000-8000-0026BB765291", + "value": false + }, + { + "format": "int", + "iid": 2823, + "maxValue": 100, + "minStep": 1, + "minValue": 0, + "perms": ["pr", "pw", "ev"], + "type": "00000008-0000-1000-8000-0026BB765291", + "unit": "percentage", + "value": 100 + }, + { + "description": "Color Temperature", + "format": "uint32", + "iid": 3017, + "maxValue": 454, + "minStep": 1, + "minValue": 153, + "perms": ["pr", "pw", "ev"], + "type": "000000CE-0000-1000-8000-0026BB765291", + "value": 366 + }, + { + "description": "ID to uniquely identify service within a single accessory", + "format": "string", + "iid": 2827, + "maxLen": 64, + "perms": ["pr"], + "type": "D8B76298-42E7-5FFD-B1D6-1782D9A1F936", + "value": "6623462395276939" + } + ], + "iid": 2816, + "type": "00000043-0000-1000-8000-0026BB765291" + } + ] + }, + { + "aid": 6623462395276914, + "services": [ + { + "characteristics": [ + { + "format": "string", + "iid": 5, + "perms": ["pr"], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Hue ambiance candle" + }, + { + "format": "string", + "iid": 4, + "perms": ["pr"], + "type": "00000021-0000-1000-8000-0026BB765291", + "value": "LTW012" + }, + { + "format": "string", + "iid": 3, + "perms": ["pr"], + "type": "00000020-0000-1000-8000-0026BB765291", + "value": "Philips" + }, + { + "format": "string", + "iid": 112, + "perms": ["pr"], + "type": "00000052-0000-1000-8000-0026BB765291", + "value": "1.46.13" + }, + { + "format": "string", + "iid": 11, + "perms": ["pr"], + "type": "00000030-0000-1000-8000-0026BB765291", + "value": "6623462395276914" + }, + { + "format": "bool", + "iid": 6, + "perms": ["pw"], + "type": "00000014-0000-1000-8000-0026BB765291" + } + ], + "iid": 1, + "type": "0000003E-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "format": "string", + "iid": 65591, + "perms": ["pr"], + "type": "00000037-0000-1000-8000-0026BB765291", + "value": "1.1.0" + } + ], + "iid": 65535, + "type": "000000A2-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "format": "string", + "iid": 2817, + "perms": ["pr"], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Hue ambiance candle" + }, + { + "format": "bool", + "iid": 2822, + "perms": ["pr", "pw", "ev"], + "type": "00000025-0000-1000-8000-0026BB765291", + "value": false + }, + { + "format": "int", + "iid": 2823, + "maxValue": 100, + "minStep": 1, + "minValue": 0, + "perms": ["pr", "pw", "ev"], + "type": "00000008-0000-1000-8000-0026BB765291", + "unit": "percentage", + "value": 100 + }, + { + "description": "Color Temperature", + "format": "uint32", + "iid": 3017, + "maxValue": 454, + "minStep": 1, + "minValue": 153, + "perms": ["pr", "pw", "ev"], + "type": "000000CE-0000-1000-8000-0026BB765291", + "value": 366 + }, + { + "description": "ID to uniquely identify service within a single accessory", + "format": "string", + "iid": 2827, + "maxLen": 64, + "perms": ["pr"], + "type": "D8B76298-42E7-5FFD-B1D6-1782D9A1F936", + "value": "6623462395276914" + } + ], + "iid": 2816, + "type": "00000043-0000-1000-8000-0026BB765291" + } + ] + } +] diff --git a/tests/components/homekit_controller/fixtures/koogeek_ls1.json b/tests/components/homekit_controller/fixtures/koogeek_ls1.json index 9b05ce76639..65e4e0eacb9 100644 --- a/tests/components/homekit_controller/fixtures/koogeek_ls1.json +++ b/tests/components/homekit_controller/fixtures/koogeek_ls1.json @@ -1,244 +1,192 @@ [ - { - "aid": 1, - "services": [ - { - "characteristics": [ - { - "format": "string", - "iid": 2, - "maxLen": 64, - "perms": [ - "pr" - ], - "type": "23", - "value": "Koogeek-LS1-20833F" - }, - { - "format": "string", - "iid": 3, - "maxLen": 64, - "perms": [ - "pr" - ], - "type": "20", - "value": "Koogeek" - }, - { - "format": "string", - "iid": 4, - "maxLen": 64, - "perms": [ - "pr" - ], - "type": "21", - "value": "LS1" - }, - { - "format": "string", - "iid": 5, - "maxLen": 64, - "perms": [ - "pr" - ], - "type": "30", - "value": "AAAA011111111111" - }, - { - "format": "bool", - "iid": 6, - "perms": [ - "pw" - ], - "type": "14" - }, - { - "format": "string", - "iid": 23, - "perms": [ - "pr" - ], - "type": "52", - "value": "2.2.15" - } - ], - "iid": 1, - "type": "3E" - }, - { - "characteristics": [ - { - "ev": false, - "format": "bool", - "iid": 8, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "25", - "value": false - }, - { - "ev": false, - "format": "float", - "iid": 9, - "maxValue": 359, - "minStep": 1, - "minValue": 0, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "13", - "unit": "arcdegrees", - "value": 44 - }, - { - "ev": false, - "format": "float", - "iid": 10, - "maxValue": 100, - "minStep": 1, - "minValue": 0, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "2F", - "unit": "percentage", - "value": 0 - }, - { - "ev": false, - "format": "int", - "iid": 11, - "maxValue": 100, - "minStep": 1, - "minValue": 0, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "8", - "unit": "percentage", - "value": 100 - }, - { - "format": "string", - "iid": 12, - "maxLen": 64, - "perms": [ - "pr" - ], - "type": "23", - "value": "Light Strip" - } - ], - "iid": 7, - "primary": true, - "type": "43" - }, - { - "characteristics": [ - { - "description": "TIMER_SETTINGS", - "format": "tlv8", - "iid": 14, - "perms": [ - "pr", - "pw" - ], - "type": "4aaaf942-0dec-11e5-b939-0800200c9a66", - "value": "AHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" - } - ], - "iid": 13, - "type": "4aaaf940-0dec-11e5-b939-0800200c9a66" - }, - { - "characteristics": [ - { - "description": "FW Upgrade supported types", - "format": "string", - "iid": 16, - "perms": [ - "pr", - "hd" - ], - "type": "151909D2-3802-11E4-916C-0800200C9A66", - "value": "url,data" - }, - { - "description": "FW Upgrade URL", - "format": "string", - "iid": 17, - "maxLen": 256, - "perms": [ - "pw", - "hd" - ], - "type": "151909D1-3802-11E4-916C-0800200C9A66" - }, - { - "description": "FW Upgrade Status", - "ev": false, - "format": "int", - "iid": 18, - "perms": [ - "pr", - "ev", - "hd" - ], - "type": "151909D6-3802-11E4-916C-0800200C9A66", - "value": 0 - }, - { - "description": "FW Upgrade Data", - "format": "data", - "iid": 19, - "perms": [ - "pw", - "hd" - ], - "type": "151909D7-3802-11E4-916C-0800200C9A66" - } - ], - "hidden": true, - "iid": 15, - "type": "151909D0-3802-11E4-916C-0800200C9A66" - }, - { - "characteristics": [ - { - "description": "Timezone", - "format": "int", - "iid": 21, - "perms": [ - "pr", - "pw" - ], - "type": "151909D5-3802-11E4-916C-0800200C9A66", - "value": 0 - }, - { - "description": "Time value since Epoch", - "format": "int", - "iid": 22, - "perms": [ - "pr", - "pw" - ], - "type": "151909D4-3802-11E4-916C-0800200C9A66", - "value": 1550348623 - } - ], - "iid": 20, - "type": "151909D3-3802-11E4-916C-0800200C9A66" - } - ] - } -] \ No newline at end of file + { + "aid": 1, + "services": [ + { + "characteristics": [ + { + "format": "string", + "iid": 2, + "maxLen": 64, + "perms": ["pr"], + "type": "23", + "value": "Koogeek-LS1-20833F" + }, + { + "format": "string", + "iid": 3, + "maxLen": 64, + "perms": ["pr"], + "type": "20", + "value": "Koogeek" + }, + { + "format": "string", + "iid": 4, + "maxLen": 64, + "perms": ["pr"], + "type": "21", + "value": "LS1" + }, + { + "format": "string", + "iid": 5, + "maxLen": 64, + "perms": ["pr"], + "type": "30", + "value": "AAAA011111111111" + }, + { + "format": "bool", + "iid": 6, + "perms": ["pw"], + "type": "14" + }, + { + "format": "string", + "iid": 23, + "perms": ["pr"], + "type": "52", + "value": "2.2.15" + } + ], + "iid": 1, + "type": "3E" + }, + { + "characteristics": [ + { + "ev": false, + "format": "bool", + "iid": 8, + "perms": ["pr", "pw", "ev"], + "type": "25", + "value": false + }, + { + "ev": false, + "format": "float", + "iid": 9, + "maxValue": 359, + "minStep": 1, + "minValue": 0, + "perms": ["pr", "pw", "ev"], + "type": "13", + "unit": "arcdegrees", + "value": 44 + }, + { + "ev": false, + "format": "float", + "iid": 10, + "maxValue": 100, + "minStep": 1, + "minValue": 0, + "perms": ["pr", "pw", "ev"], + "type": "2F", + "unit": "percentage", + "value": 0 + }, + { + "ev": false, + "format": "int", + "iid": 11, + "maxValue": 100, + "minStep": 1, + "minValue": 0, + "perms": ["pr", "pw", "ev"], + "type": "8", + "unit": "percentage", + "value": 100 + }, + { + "format": "string", + "iid": 12, + "maxLen": 64, + "perms": ["pr"], + "type": "23", + "value": "Light Strip" + } + ], + "iid": 7, + "primary": true, + "type": "43" + }, + { + "characteristics": [ + { + "description": "TIMER_SETTINGS", + "format": "tlv8", + "iid": 14, + "perms": ["pr", "pw"], + "type": "4aaaf942-0dec-11e5-b939-0800200c9a66", + "value": "AHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + } + ], + "iid": 13, + "type": "4aaaf940-0dec-11e5-b939-0800200c9a66" + }, + { + "characteristics": [ + { + "description": "FW Upgrade supported types", + "format": "string", + "iid": 16, + "perms": ["pr", "hd"], + "type": "151909D2-3802-11E4-916C-0800200C9A66", + "value": "url,data" + }, + { + "description": "FW Upgrade URL", + "format": "string", + "iid": 17, + "maxLen": 256, + "perms": ["pw", "hd"], + "type": "151909D1-3802-11E4-916C-0800200C9A66" + }, + { + "description": "FW Upgrade Status", + "ev": false, + "format": "int", + "iid": 18, + "perms": ["pr", "ev", "hd"], + "type": "151909D6-3802-11E4-916C-0800200C9A66", + "value": 0 + }, + { + "description": "FW Upgrade Data", + "format": "data", + "iid": 19, + "perms": ["pw", "hd"], + "type": "151909D7-3802-11E4-916C-0800200C9A66" + } + ], + "hidden": true, + "iid": 15, + "type": "151909D0-3802-11E4-916C-0800200C9A66" + }, + { + "characteristics": [ + { + "description": "Timezone", + "format": "int", + "iid": 21, + "perms": ["pr", "pw"], + "type": "151909D5-3802-11E4-916C-0800200C9A66", + "value": 0 + }, + { + "description": "Time value since Epoch", + "format": "int", + "iid": 22, + "perms": ["pr", "pw"], + "type": "151909D4-3802-11E4-916C-0800200C9A66", + "value": 1550348623 + } + ], + "iid": 20, + "type": "151909D3-3802-11E4-916C-0800200C9A66" + } + ] + } +] diff --git a/tests/components/homekit_controller/fixtures/koogeek_p1eu.json b/tests/components/homekit_controller/fixtures/koogeek_p1eu.json index d9d252b4cb7..4f9d9607669 100644 --- a/tests/components/homekit_controller/fixtures/koogeek_p1eu.json +++ b/tests/components/homekit_controller/fixtures/koogeek_p1eu.json @@ -1,392 +1,314 @@ [ - { - "aid": 1, - "services": [ - { - "characteristics": [ - { - "format": "string", - "iid": 2, - "maxLen": 64, - "perms": [ - "pr" - ], - "type": "00000023-0000-1000-8000-0026BB765291", - "value": "Koogeek-P1-A00AA0" - }, - { - "format": "string", - "iid": 3, - "maxLen": 64, - "perms": [ - "pr" - ], - "type": "00000020-0000-1000-8000-0026BB765291", - "value": "Koogeek" - }, - { - "format": "string", - "iid": 4, - "maxLen": 64, - "perms": [ - "pr" - ], - "type": "00000021-0000-1000-8000-0026BB765291", - "value": "P1EU" - }, - { - "format": "string", - "iid": 5, - "maxLen": 64, - "perms": [ - "pr" - ], - "type": "00000030-0000-1000-8000-0026BB765291", - "value": "EUCP03190xxxxx48" - }, - { - "format": "bool", - "iid": 6, - "perms": [ - "pw" - ], - "type": "00000014-0000-1000-8000-0026BB765291" - }, - { - "format": "string", - "iid": 37, - "perms": [ - "pr" - ], - "type": "00000052-0000-1000-8000-0026BB765291", - "value": "2.3.7" - } - ], - "iid": 1, - "stype": "accessory-information", - "type": "0000003E-0000-1000-8000-0026BB765291" - }, - { - "characteristics": [ - { - "ev": false, - "format": "bool", - "iid": 8, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "00000025-0000-1000-8000-0026BB765291", - "value": false - }, - { - "ev": false, - "format": "bool", - "iid": 9, - "perms": [ - "pr", - "ev" - ], - "type": "00000026-0000-1000-8000-0026BB765291", - "value": true - }, - { - "format": "string", - "iid": 10, - "maxLen": 64, - "perms": [ - "pr" - ], - "type": "00000023-0000-1000-8000-0026BB765291", - "value": "outlet" - } - ], - "iid": 7, - "primary": true, - "stype": "outlet", - "type": "00000047-0000-1000-8000-0026BB765291" - }, - { - "characteristics": [ - { - "description": "TIMER_SETTINGS", - "format": "tlv8", - "iid": 12, - "perms": [ - "pr", - "pw" - ], - "type": "4AAAF942-0DEC-11E5-B939-0800200C9A66", - "value": "AHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" - } - ], - "iid": 11, - "stype": "Unknown Service: 4AAAF940-0DEC-11E5-B939-0800200C9A66", - "type": "4AAAF940-0DEC-11E5-B939-0800200C9A66" - }, - { - "characteristics": [ - { - "description": "FW Upgrade supported types", - "format": "string", - "iid": 14, - "perms": [ - "pr", - "hd" - ], - "type": "151909D2-3802-11E4-916C-0800200C9A66", - "value": "url,data" - }, - { - "description": "FW Upgrade URL", - "format": "string", - "iid": 15, - "maxLen": 256, - "perms": [ - "pw", - "hd" - ], - "type": "151909D1-3802-11E4-916C-0800200C9A66" - }, - { - "description": "FW Upgrade Status", - "ev": false, - "format": "int", - "iid": 16, - "perms": [ - "pr", - "ev", - "hd" - ], - "type": "151909D6-3802-11E4-916C-0800200C9A66", - "value": 0 - }, - { - "description": "FW Upgrade Data", - "format": "data", - "iid": 17, - "perms": [ - "pw", - "hd" - ], - "type": "151909D7-3802-11E4-916C-0800200C9A66" - } - ], - "hidden": true, - "iid": 13, - "stype": "Unknown Service: 151909D0-3802-11E4-916C-0800200C9A66", - "type": "151909D0-3802-11E4-916C-0800200C9A66" - }, - { - "characteristics": [ - { - "description": "Timezone", - "format": "int", - "iid": 19, - "perms": [ - "pr", - "pw" - ], - "type": "151909D5-3802-11E4-916C-0800200C9A66", - "value": 0 - }, - { - "description": "Time value since Epoch", - "format": "int", - "iid": 20, - "perms": [ - "pr", - "pw" - ], - "type": "151909D4-3802-11E4-916C-0800200C9A66", - "value": 1570358601 - } - ], - "iid": 18, - "stype": "Unknown Service: 151909D3-3802-11E4-916C-0800200C9A66", - "type": "151909D3-3802-11E4-916C-0800200C9A66" - }, - { - "characteristics": [ - { - "description": "1 REALTIME_ENERGY", - "ev": false, - "format": "float", - "iid": 22, - "perms": [ - "pr", - "ev" - ], - "type": "4AAAF931-0DEC-11E5-B939-0800200C9A66", - "value": 5 - }, - { - "description": "2 CURRENT_HOUR_DATA", - "ev": false, - "format": "float", - "iid": 23, - "perms": [ - "pr", - "ev" - ], - "type": "4AAAF932-0DEC-11E5-B939-0800200C9A66", - "value": 0 - }, - { - "description": "3 HOUR_DATA_TODAY", - "format": "tlv8", - "iid": 24, - "perms": [ - "pr" - ], - "type": "4AAAF933-0DEC-11E5-B939-0800200C9A66", - "value": "AGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" - }, - { - "description": "4 HOUR_DATA_YESTERDAY", - "format": "tlv8", - "iid": 25, - "perms": [ - "pr" - ], - "type": "4AAAF934-0DEC-11E5-B939-0800200C9A66", - "value": "AGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" - }, - { - "description": "5 HOUR_DATA_2_DAYS_BEFORE", - "format": "tlv8", - "iid": 26, - "perms": [ - "pr" - ], - "type": "4AAAF935-0DEC-11E5-B939-0800200C9A66", - "value": "AGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" - }, - { - "description": "6 HOUR_DATA_3_DAYS_BEFORE", - "format": "tlv8", - "iid": 27, - "perms": [ - "pr" - ], - "type": "4AAAF936-0DEC-11E5-B939-0800200C9A66", - "value": "AGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" - }, - { - "description": "7 HOUR_DATA_4_DAYS_BEFORE", - "format": "tlv8", - "iid": 28, - "perms": [ - "pr" - ], - "type": "4AAAF937-0DEC-11E5-B939-0800200C9A66", - "value": "AGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" - }, - { - "description": "8 HOUR_DATA_5_DAYS_BEFORE", - "format": "tlv8", - "iid": 29, - "perms": [ - "pr" - ], - "type": "4AAAF938-0DEC-11E5-B939-0800200C9A66", - "value": "AGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" - }, - { - "description": "9 HOUR_DATA_6_DAYS_BEFORE", - "format": "tlv8", - "iid": 30, - "perms": [ - "pr" - ], - "type": "4AAAF939-0DEC-11E5-B939-0800200C9A66", - "value": "AGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" - }, - { - "description": "10 HOUR_DATA_7_DAYS_BEFORE", - "format": "tlv8", - "iid": 31, - "perms": [ - "pr" - ], - "type": "4AAAF93A-0DEC-11E5-B939-0800200C9A66", - "value": "AGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" - }, - { - "description": "11 DAY_DATA_THIS_MONTH", - "format": "tlv8", - "iid": 32, - "perms": [ - "pr" - ], - "type": "4AAAF93B-0DEC-11E5-B939-0800200C9A66", - "value": "AHwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" - }, - { - "description": "12 DAY_DATA_LAST_MONTH", - "format": "tlv8", - "iid": 33, - "perms": [ - "pr" - ], - "type": "4AAAF93C-0DEC-11E5-B939-0800200C9A66", - "value": "AHwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" - }, - { - "description": "13 MONTH_DATA_THIS_YEAR", - "format": "tlv8", - "iid": 34, - "perms": [ - "pr" - ], - "type": "4AAAF93D-0DEC-11E5-B939-0800200C9A66", - "value": "ADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" - }, - { - "description": "14 MONTH_DATA_LAST_YEAR", - "format": "tlv8", - "iid": 35, - "perms": [ - "pr" - ], - "type": "4AAAF93E-0DEC-11E5-B939-0800200C9A66", - "value": "ADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" - }, - { - "description": "15 RUNNING_TIME", - "ev": false, - "format": "int", - "iid": 36, - "perms": [ - "pr", - "ev" - ], - "type": "4AAAF93F-0DEC-11E5-B939-0800200C9A66", - "value": 0 - } - ], - "iid": 21, - "stype": "Unknown Service: 4AAAF930-0DEC-11E5-B939-0800200C9A66", - "type": "4AAAF930-0DEC-11E5-B939-0800200C9A66" - }, - { - "characteristics": [ - { - "format": "string", - "iid": 39, - "maxLen": 64, - "perms": [ - "pr" - ], - "type": "00000037-0000-1000-8000-0026BB765291", - "value": "1.1.0" - } - ], - "iid": 38, - "stype": "service", - "type": "000000A2-0000-1000-8000-0026BB765291" - } - ] - } -] \ No newline at end of file + { + "aid": 1, + "services": [ + { + "characteristics": [ + { + "format": "string", + "iid": 2, + "maxLen": 64, + "perms": ["pr"], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Koogeek-P1-A00AA0" + }, + { + "format": "string", + "iid": 3, + "maxLen": 64, + "perms": ["pr"], + "type": "00000020-0000-1000-8000-0026BB765291", + "value": "Koogeek" + }, + { + "format": "string", + "iid": 4, + "maxLen": 64, + "perms": ["pr"], + "type": "00000021-0000-1000-8000-0026BB765291", + "value": "P1EU" + }, + { + "format": "string", + "iid": 5, + "maxLen": 64, + "perms": ["pr"], + "type": "00000030-0000-1000-8000-0026BB765291", + "value": "EUCP03190xxxxx48" + }, + { + "format": "bool", + "iid": 6, + "perms": ["pw"], + "type": "00000014-0000-1000-8000-0026BB765291" + }, + { + "format": "string", + "iid": 37, + "perms": ["pr"], + "type": "00000052-0000-1000-8000-0026BB765291", + "value": "2.3.7" + } + ], + "iid": 1, + "stype": "accessory-information", + "type": "0000003E-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "ev": false, + "format": "bool", + "iid": 8, + "perms": ["pr", "pw", "ev"], + "type": "00000025-0000-1000-8000-0026BB765291", + "value": false + }, + { + "ev": false, + "format": "bool", + "iid": 9, + "perms": ["pr", "ev"], + "type": "00000026-0000-1000-8000-0026BB765291", + "value": true + }, + { + "format": "string", + "iid": 10, + "maxLen": 64, + "perms": ["pr"], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "outlet" + } + ], + "iid": 7, + "primary": true, + "stype": "outlet", + "type": "00000047-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "description": "TIMER_SETTINGS", + "format": "tlv8", + "iid": 12, + "perms": ["pr", "pw"], + "type": "4AAAF942-0DEC-11E5-B939-0800200C9A66", + "value": "AHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + } + ], + "iid": 11, + "stype": "Unknown Service: 4AAAF940-0DEC-11E5-B939-0800200C9A66", + "type": "4AAAF940-0DEC-11E5-B939-0800200C9A66" + }, + { + "characteristics": [ + { + "description": "FW Upgrade supported types", + "format": "string", + "iid": 14, + "perms": ["pr", "hd"], + "type": "151909D2-3802-11E4-916C-0800200C9A66", + "value": "url,data" + }, + { + "description": "FW Upgrade URL", + "format": "string", + "iid": 15, + "maxLen": 256, + "perms": ["pw", "hd"], + "type": "151909D1-3802-11E4-916C-0800200C9A66" + }, + { + "description": "FW Upgrade Status", + "ev": false, + "format": "int", + "iid": 16, + "perms": ["pr", "ev", "hd"], + "type": "151909D6-3802-11E4-916C-0800200C9A66", + "value": 0 + }, + { + "description": "FW Upgrade Data", + "format": "data", + "iid": 17, + "perms": ["pw", "hd"], + "type": "151909D7-3802-11E4-916C-0800200C9A66" + } + ], + "hidden": true, + "iid": 13, + "stype": "Unknown Service: 151909D0-3802-11E4-916C-0800200C9A66", + "type": "151909D0-3802-11E4-916C-0800200C9A66" + }, + { + "characteristics": [ + { + "description": "Timezone", + "format": "int", + "iid": 19, + "perms": ["pr", "pw"], + "type": "151909D5-3802-11E4-916C-0800200C9A66", + "value": 0 + }, + { + "description": "Time value since Epoch", + "format": "int", + "iid": 20, + "perms": ["pr", "pw"], + "type": "151909D4-3802-11E4-916C-0800200C9A66", + "value": 1570358601 + } + ], + "iid": 18, + "stype": "Unknown Service: 151909D3-3802-11E4-916C-0800200C9A66", + "type": "151909D3-3802-11E4-916C-0800200C9A66" + }, + { + "characteristics": [ + { + "description": "1 REALTIME_ENERGY", + "ev": false, + "format": "float", + "iid": 22, + "perms": ["pr", "ev"], + "type": "4AAAF931-0DEC-11E5-B939-0800200C9A66", + "value": 5 + }, + { + "description": "2 CURRENT_HOUR_DATA", + "ev": false, + "format": "float", + "iid": 23, + "perms": ["pr", "ev"], + "type": "4AAAF932-0DEC-11E5-B939-0800200C9A66", + "value": 0 + }, + { + "description": "3 HOUR_DATA_TODAY", + "format": "tlv8", + "iid": 24, + "perms": ["pr"], + "type": "4AAAF933-0DEC-11E5-B939-0800200C9A66", + "value": "AGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" + }, + { + "description": "4 HOUR_DATA_YESTERDAY", + "format": "tlv8", + "iid": 25, + "perms": ["pr"], + "type": "4AAAF934-0DEC-11E5-B939-0800200C9A66", + "value": "AGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" + }, + { + "description": "5 HOUR_DATA_2_DAYS_BEFORE", + "format": "tlv8", + "iid": 26, + "perms": ["pr"], + "type": "4AAAF935-0DEC-11E5-B939-0800200C9A66", + "value": "AGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" + }, + { + "description": "6 HOUR_DATA_3_DAYS_BEFORE", + "format": "tlv8", + "iid": 27, + "perms": ["pr"], + "type": "4AAAF936-0DEC-11E5-B939-0800200C9A66", + "value": "AGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" + }, + { + "description": "7 HOUR_DATA_4_DAYS_BEFORE", + "format": "tlv8", + "iid": 28, + "perms": ["pr"], + "type": "4AAAF937-0DEC-11E5-B939-0800200C9A66", + "value": "AGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" + }, + { + "description": "8 HOUR_DATA_5_DAYS_BEFORE", + "format": "tlv8", + "iid": 29, + "perms": ["pr"], + "type": "4AAAF938-0DEC-11E5-B939-0800200C9A66", + "value": "AGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" + }, + { + "description": "9 HOUR_DATA_6_DAYS_BEFORE", + "format": "tlv8", + "iid": 30, + "perms": ["pr"], + "type": "4AAAF939-0DEC-11E5-B939-0800200C9A66", + "value": "AGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" + }, + { + "description": "10 HOUR_DATA_7_DAYS_BEFORE", + "format": "tlv8", + "iid": 31, + "perms": ["pr"], + "type": "4AAAF93A-0DEC-11E5-B939-0800200C9A66", + "value": "AGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" + }, + { + "description": "11 DAY_DATA_THIS_MONTH", + "format": "tlv8", + "iid": 32, + "perms": ["pr"], + "type": "4AAAF93B-0DEC-11E5-B939-0800200C9A66", + "value": "AHwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + }, + { + "description": "12 DAY_DATA_LAST_MONTH", + "format": "tlv8", + "iid": 33, + "perms": ["pr"], + "type": "4AAAF93C-0DEC-11E5-B939-0800200C9A66", + "value": "AHwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + }, + { + "description": "13 MONTH_DATA_THIS_YEAR", + "format": "tlv8", + "iid": 34, + "perms": ["pr"], + "type": "4AAAF93D-0DEC-11E5-B939-0800200C9A66", + "value": "ADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" + }, + { + "description": "14 MONTH_DATA_LAST_YEAR", + "format": "tlv8", + "iid": 35, + "perms": ["pr"], + "type": "4AAAF93E-0DEC-11E5-B939-0800200C9A66", + "value": "ADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" + }, + { + "description": "15 RUNNING_TIME", + "ev": false, + "format": "int", + "iid": 36, + "perms": ["pr", "ev"], + "type": "4AAAF93F-0DEC-11E5-B939-0800200C9A66", + "value": 0 + } + ], + "iid": 21, + "stype": "Unknown Service: 4AAAF930-0DEC-11E5-B939-0800200C9A66", + "type": "4AAAF930-0DEC-11E5-B939-0800200C9A66" + }, + { + "characteristics": [ + { + "format": "string", + "iid": 39, + "maxLen": 64, + "perms": ["pr"], + "type": "00000037-0000-1000-8000-0026BB765291", + "value": "1.1.0" + } + ], + "iid": 38, + "stype": "service", + "type": "000000A2-0000-1000-8000-0026BB765291" + } + ] + } +] diff --git a/tests/components/homekit_controller/fixtures/koogeek_sw2.json b/tests/components/homekit_controller/fixtures/koogeek_sw2.json index b7807bfb6a7..1100a45e1f8 100644 --- a/tests/components/homekit_controller/fixtures/koogeek_sw2.json +++ b/tests/components/homekit_controller/fixtures/koogeek_sw2.json @@ -1,265 +1,212 @@ [ - { - "aid": 1, - "services": [ - { - "characteristics": [ - { - "format": "string", - "iid": 2, - "maxLen": 64, - "perms": [ - "pr" - ], - "type": "00000023-0000-1000-8000-0026BB765291", - "value": "Koogeek-SW2-187A91" - }, - { - "format": "string", - "iid": 3, - "maxLen": 64, - "perms": [ - "pr" - ], - "type": "00000020-0000-1000-8000-0026BB765291", - "value": "Koogeek" - }, - { - "format": "string", - "iid": 4, - "maxLen": 64, - "perms": [ - "pr" - ], - "type": "00000021-0000-1000-8000-0026BB765291", - "value": "KH02CN" - }, - { - "format": "string", - "iid": 5, - "maxLen": 64, - "perms": [ - "pr" - ], - "type": "00000030-0000-1000-8000-0026BB765291", - "value": "CNNT061751001372" - }, - { - "format": "bool", - "iid": 6, - "perms": [ - "pw" - ], - "type": "00000014-0000-1000-8000-0026BB765291" - }, - { - "format": "string", - "iid": 7, - "perms": [ - "pr" - ], - "type": "00000052-0000-1000-8000-0026BB765291", - "value": "1.0.3" - } - ], - "iid": 1, - "stype": "accessory-information", - "type": "0000003E-0000-1000-8000-0026BB765291" - }, - { - "characteristics": [ - { - "ev": true, - "format": "bool", - "iid": 9, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "00000025-0000-1000-8000-0026BB765291", - "value": false - }, - { - "format": "string", - "iid": 10, - "maxLen": 64, - "perms": [ - "pr" - ], - "type": "00000023-0000-1000-8000-0026BB765291", - "value": "Switch 1" - } - ], - "iid": 8, - "primary": true, - "stype": "switch", - "type": "00000049-0000-1000-8000-0026BB765291" - }, - { - "characteristics": [ - { - "ev": true, - "format": "bool", - "iid": 12, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "00000025-0000-1000-8000-0026BB765291", - "value": false - }, - { - "format": "string", - "iid": 13, - "maxLen": 64, - "perms": [ - "pr" - ], - "type": "00000023-0000-1000-8000-0026BB765291", - "value": "Switch 2" - } - ], - "iid": 11, - "primary": true, - "stype": "switch", - "type": "00000049-0000-1000-8000-0026BB765291" - }, - { - "characteristics": [ - { - "format": "string", - "iid": 15, - "maxLen": 64, - "perms": [ - "pr" - ], - "type": "00000023-0000-1000-8000-0026BB765291", - "value": "custom service" - }, - { - "description": "Current Time", - "format": "int", - "iid": 16, - "perms": [ - "pr" - ], - "type": "7BBBA961-EB2D-11E5-A837-0800200C9A66", - "value": 1599731035 - }, - { - "description": "Time Zone", - "format": "int", - "iid": 17, - "perms": [ - "pr", - "pw" - ], - "type": "7BBBA980-EB2D-11E5-A837-0800200C9A66", - "value": 16 - }, - { - "description": "Current Power", - "ev": false, - "format": "int", - "iid": 18, - "perms": [ - "pr", - "ev" - ], - "type": "7BBBA96E-EB2D-11E5-A837-0800200C9A66", - "value": 0 - }, - { - "description": "Power Consumption Today", - "format": "data", - "iid": 19, - "perms": [ - "pr" - ], - "type": "7BBBA96F-EB2D-11E5-A837-0800200C9A66", - "value": "9pcBAL4GAAC1BgAAtgYAAELhAABXIwAAtgYAAKcGAABHOQAA1aMAAP//////////////////////////////////////////////////////////////////////////" - }, - { - "description": "Power Consumption last 2 Month", - "format": "data", - "iid": 20, - "perms": [ - "pr" - ], - "type": "7BBBA972-EB2D-11E5-A837-0800200C9A66", - "value": "/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACCFAEA5HkIADGbAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" - }, - { - "description": "Power Consumption last 12 Month", - "format": "data", - "iid": 21, - "perms": [ - "pr" - ], - "type": "7BBBA970-EB2D-11E5-A837-0800200C9A66", - "value": "//////////////////////////////////////////+XKQ0A////////////////" - } - ], - "hidden": true, - "iid": 14, - "stype": "Unknown Service: 7BBBA977-EB2D-11E5-A837-0800200C9A66", - "type": "7BBBA977-EB2D-11E5-A837-0800200C9A66" - }, - { - "characteristics": [ - { - "description": "FW Upgrade supported types", - "format": "string", - "iid": 23, - "perms": [ - "pr", - "hd" - ], - "type": "151909D2-3802-11E4-916C-0800200C9A66", - "value": "url,data" - }, - { - "description": "FW Upgrade URL", - "format": "string", - "iid": 24, - "maxLen": 256, - "perms": [ - "pw", - "hd" - ], - "type": "151909D1-3802-11E4-916C-0800200C9A66" - }, - { - "description": "FW Upgrade Status", - "ev": false, - "format": "int", - "iid": 25, - "perms": [ - "pr", - "ev", - "hd" - ], - "type": "151909D6-3802-11E4-916C-0800200C9A66", - "value": 0 - }, - { - "description": "FW Upgrade Data", - "format": "data", - "iid": 26, - "perms": [ - "pw", - "hd" - ], - "type": "151909D7-3802-11E4-916C-0800200C9A66" - } - ], - "hidden": true, - "iid": 22, - "stype": "Unknown Service: 151909D0-3802-11E4-916C-0800200C9A66", - "type": "151909D0-3802-11E4-916C-0800200C9A66" - } - ] - } -] \ No newline at end of file + { + "aid": 1, + "services": [ + { + "characteristics": [ + { + "format": "string", + "iid": 2, + "maxLen": 64, + "perms": ["pr"], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Koogeek-SW2-187A91" + }, + { + "format": "string", + "iid": 3, + "maxLen": 64, + "perms": ["pr"], + "type": "00000020-0000-1000-8000-0026BB765291", + "value": "Koogeek" + }, + { + "format": "string", + "iid": 4, + "maxLen": 64, + "perms": ["pr"], + "type": "00000021-0000-1000-8000-0026BB765291", + "value": "KH02CN" + }, + { + "format": "string", + "iid": 5, + "maxLen": 64, + "perms": ["pr"], + "type": "00000030-0000-1000-8000-0026BB765291", + "value": "CNNT061751001372" + }, + { + "format": "bool", + "iid": 6, + "perms": ["pw"], + "type": "00000014-0000-1000-8000-0026BB765291" + }, + { + "format": "string", + "iid": 7, + "perms": ["pr"], + "type": "00000052-0000-1000-8000-0026BB765291", + "value": "1.0.3" + } + ], + "iid": 1, + "stype": "accessory-information", + "type": "0000003E-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "ev": true, + "format": "bool", + "iid": 9, + "perms": ["pr", "pw", "ev"], + "type": "00000025-0000-1000-8000-0026BB765291", + "value": false + }, + { + "format": "string", + "iid": 10, + "maxLen": 64, + "perms": ["pr"], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Switch 1" + } + ], + "iid": 8, + "primary": true, + "stype": "switch", + "type": "00000049-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "ev": true, + "format": "bool", + "iid": 12, + "perms": ["pr", "pw", "ev"], + "type": "00000025-0000-1000-8000-0026BB765291", + "value": false + }, + { + "format": "string", + "iid": 13, + "maxLen": 64, + "perms": ["pr"], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Switch 2" + } + ], + "iid": 11, + "primary": true, + "stype": "switch", + "type": "00000049-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "format": "string", + "iid": 15, + "maxLen": 64, + "perms": ["pr"], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "custom service" + }, + { + "description": "Current Time", + "format": "int", + "iid": 16, + "perms": ["pr"], + "type": "7BBBA961-EB2D-11E5-A837-0800200C9A66", + "value": 1599731035 + }, + { + "description": "Time Zone", + "format": "int", + "iid": 17, + "perms": ["pr", "pw"], + "type": "7BBBA980-EB2D-11E5-A837-0800200C9A66", + "value": 16 + }, + { + "description": "Current Power", + "ev": false, + "format": "int", + "iid": 18, + "perms": ["pr", "ev"], + "type": "7BBBA96E-EB2D-11E5-A837-0800200C9A66", + "value": 0 + }, + { + "description": "Power Consumption Today", + "format": "data", + "iid": 19, + "perms": ["pr"], + "type": "7BBBA96F-EB2D-11E5-A837-0800200C9A66", + "value": "9pcBAL4GAAC1BgAAtgYAAELhAABXIwAAtgYAAKcGAABHOQAA1aMAAP//////////////////////////////////////////////////////////////////////////" + }, + { + "description": "Power Consumption last 2 Month", + "format": "data", + "iid": 20, + "perms": ["pr"], + "type": "7BBBA972-EB2D-11E5-A837-0800200C9A66", + "value": "/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACCFAEA5HkIADGbAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" + }, + { + "description": "Power Consumption last 12 Month", + "format": "data", + "iid": 21, + "perms": ["pr"], + "type": "7BBBA970-EB2D-11E5-A837-0800200C9A66", + "value": "//////////////////////////////////////////+XKQ0A////////////////" + } + ], + "hidden": true, + "iid": 14, + "stype": "Unknown Service: 7BBBA977-EB2D-11E5-A837-0800200C9A66", + "type": "7BBBA977-EB2D-11E5-A837-0800200C9A66" + }, + { + "characteristics": [ + { + "description": "FW Upgrade supported types", + "format": "string", + "iid": 23, + "perms": ["pr", "hd"], + "type": "151909D2-3802-11E4-916C-0800200C9A66", + "value": "url,data" + }, + { + "description": "FW Upgrade URL", + "format": "string", + "iid": 24, + "maxLen": 256, + "perms": ["pw", "hd"], + "type": "151909D1-3802-11E4-916C-0800200C9A66" + }, + { + "description": "FW Upgrade Status", + "ev": false, + "format": "int", + "iid": 25, + "perms": ["pr", "ev", "hd"], + "type": "151909D6-3802-11E4-916C-0800200C9A66", + "value": 0 + }, + { + "description": "FW Upgrade Data", + "format": "data", + "iid": 26, + "perms": ["pw", "hd"], + "type": "151909D7-3802-11E4-916C-0800200C9A66" + } + ], + "hidden": true, + "iid": 22, + "stype": "Unknown Service: 151909D0-3802-11E4-916C-0800200C9A66", + "type": "151909D0-3802-11E4-916C-0800200C9A66" + } + ] + } +] diff --git a/tests/components/homekit_controller/fixtures/lennox_e30.json b/tests/components/homekit_controller/fixtures/lennox_e30.json index 9d2fe115259..42f751b010c 100644 --- a/tests/components/homekit_controller/fixtures/lennox_e30.json +++ b/tests/components/homekit_controller/fixtures/lennox_e30.json @@ -1,196 +1,153 @@ [ - { - "aid": 1, - "services": [ - { - "characteristics": [ - { - "format": "bool", - "iid": 2, - "perms": [ - "pw" - ], - "type": "14" - }, - { - "format": "string", - "iid": 3, - "perms": [ - "pr" - ], - "type": "20", - "value": "Lennox" - }, - { - "format": "string", - "iid": 4, - "perms": [ - "pr" - ], - "type": "21", - "value": "E30 2B" - }, - { - "format": "string", - "iid": 5, - "perms": [ - "pr" - ], - "type": "23", - "value": "Lennox" - }, - { - "format": "string", - "iid": 6, - "perms": [ - "pr" - ], - "type": "30", - "value": "XXXXXXXX" - }, - { - "format": "string", - "iid": 7, - "perms": [ - "pr" - ], - "type": "52", - "value": "3.40.XX" - }, - { - "format": "string", - "iid": 8, - "perms": [ - "pr" - ], - "type": "53", - "value": "3.0.XX" - } - ], - "iid": 1, - "type": "3E" - }, - { - "characteristics": [ - { - "format": "uint8", - "iid": 101, - "maxValue": 2, - "minStep": 1, - "minValue": 0, - "perms": [ - "pr", - "ev" - ], - "type": "F", - "value": 1 - }, - { - "format": "uint8", - "iid": 102, - "maxValue": 3, - "minStep": 1, - "minValue": 0, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "33", - "value": 3 - }, - { - "format": "float", - "iid": 103, - "maxValue": 100, - "minStep": 0.1, - "minValue": 0, - "perms": [ - "pr", - "ev" - ], - "type": "11", - "unit": "celsius", - "value": 20.5 - }, - { - "format": "float", - "iid": 104, - "maxValue": 32, - "minStep": 0.5, - "minValue": 4.5, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "35", - "unit": "celsius", - "value": 21 - }, - { - "format": "uint8", - "iid": 105, - "maxValue": 1, - "minStep": 1, - "minValue": 0, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "36", - "value": 0 - }, - { - "format": "float", - "iid": 106, - "maxValue": 37, - "minStep": 0.5, - "minValue": 16, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "D", - "unit": "celsius", - "value": 29.5 - }, - { - "format": "float", - "iid": 107, - "maxValue": 100, - "minStep": 1, - "minValue": 0, - "perms": [ - "pr", - "ev" - ], - "type": "10", - "unit": "percentage", - "value": 34 - }, - { - "format": "float", - "iid": 108, - "maxValue": 32, - "minStep": 0.5, - "minValue": 4.5, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "12", - "unit": "celsius", - "value": 21 - } - ], - "iid": 100, - "primary": true, - "type": "4A" - } - ] - } -] \ No newline at end of file + { + "aid": 1, + "services": [ + { + "characteristics": [ + { + "format": "bool", + "iid": 2, + "perms": ["pw"], + "type": "14" + }, + { + "format": "string", + "iid": 3, + "perms": ["pr"], + "type": "20", + "value": "Lennox" + }, + { + "format": "string", + "iid": 4, + "perms": ["pr"], + "type": "21", + "value": "E30 2B" + }, + { + "format": "string", + "iid": 5, + "perms": ["pr"], + "type": "23", + "value": "Lennox" + }, + { + "format": "string", + "iid": 6, + "perms": ["pr"], + "type": "30", + "value": "XXXXXXXX" + }, + { + "format": "string", + "iid": 7, + "perms": ["pr"], + "type": "52", + "value": "3.40.XX" + }, + { + "format": "string", + "iid": 8, + "perms": ["pr"], + "type": "53", + "value": "3.0.XX" + } + ], + "iid": 1, + "type": "3E" + }, + { + "characteristics": [ + { + "format": "uint8", + "iid": 101, + "maxValue": 2, + "minStep": 1, + "minValue": 0, + "perms": ["pr", "ev"], + "type": "F", + "value": 1 + }, + { + "format": "uint8", + "iid": 102, + "maxValue": 3, + "minStep": 1, + "minValue": 0, + "perms": ["pr", "pw", "ev"], + "type": "33", + "value": 3 + }, + { + "format": "float", + "iid": 103, + "maxValue": 100, + "minStep": 0.1, + "minValue": 0, + "perms": ["pr", "ev"], + "type": "11", + "unit": "celsius", + "value": 20.5 + }, + { + "format": "float", + "iid": 104, + "maxValue": 32, + "minStep": 0.5, + "minValue": 4.5, + "perms": ["pr", "pw", "ev"], + "type": "35", + "unit": "celsius", + "value": 21 + }, + { + "format": "uint8", + "iid": 105, + "maxValue": 1, + "minStep": 1, + "minValue": 0, + "perms": ["pr", "pw", "ev"], + "type": "36", + "value": 0 + }, + { + "format": "float", + "iid": 106, + "maxValue": 37, + "minStep": 0.5, + "minValue": 16, + "perms": ["pr", "pw", "ev"], + "type": "D", + "unit": "celsius", + "value": 29.5 + }, + { + "format": "float", + "iid": 107, + "maxValue": 100, + "minStep": 1, + "minValue": 0, + "perms": ["pr", "ev"], + "type": "10", + "unit": "percentage", + "value": 34 + }, + { + "format": "float", + "iid": 108, + "maxValue": 32, + "minStep": 0.5, + "minValue": 4.5, + "perms": ["pr", "pw", "ev"], + "type": "12", + "unit": "celsius", + "value": 21 + } + ], + "iid": 100, + "primary": true, + "type": "4A" + } + ] + } +] diff --git a/tests/components/homekit_controller/fixtures/lg_tv.json b/tests/components/homekit_controller/fixtures/lg_tv.json index 26b3557c2e6..97b1a54b216 100644 --- a/tests/components/homekit_controller/fixtures/lg_tv.json +++ b/tests/components/homekit_controller/fixtures/lg_tv.json @@ -1,1059 +1,841 @@ [ - { - "aid": 1, - "services": [ - { - "characteristics": [ - { - "format": "bool", - "iid": 2, - "perms": [ - "pw" - ], - "type": "00000014-0000-1000-8000-0026BB765291" - }, - { - "ev": false, - "format": "string", - "iid": 3, - "perms": [ - "pr" - ], - "type": "00000020-0000-1000-8000-0026BB765291", - "value": "LG Electronics" - }, - { - "ev": false, - "format": "string", - "iid": 4, - "perms": [ - "pr" - ], - "type": "00000021-0000-1000-8000-0026BB765291", - "value": "OLED55B9PUA" - }, - { - "ev": false, - "format": "string", - "iid": 5, - "perms": [ - "pr" - ], - "type": "00000023-0000-1000-8000-0026BB765291", - "value": "LG webOS TV AF80" - }, - { - "ev": false, - "format": "string", - "iid": 6, - "perms": [ - "pr" - ], - "type": "00000030-0000-1000-8000-0026BB765291", - "value": "999AAAAAA999" - }, - { - "ev": false, - "format": "string", - "iid": 7, - "perms": [ - "pr" - ], - "type": "00000052-0000-1000-8000-0026BB765291", - "value": "04.71.04" - }, - { - "ev": false, - "format": "string", - "iid": 8, - "perms": [ - "pr" - ], - "type": "00000053-0000-1000-8000-0026BB765291", - "value": "1" - }, - { - "ev": false, - "format": "string", - "iid": 9, - "perms": [ - "pr", - "hd" - ], - "type": "34AB8811-AC7F-4340-BAC3-FD6A85F9943B", - "value": "2.1;16B62a" - } - ], - "hidden": false, - "iid": 1, - "linked": [], - "primary": false, - "stype": "accessory-information", - "type": "0000003E-0000-1000-8000-0026BB765291" - }, - { - "characteristics": [ - { - "ev": false, - "format": "string", - "iid": 18, - "perms": [ - "pr" - ], - "type": "00000037-0000-1000-8000-0026BB765291", - "value": "1.1.0" - } - ], - "hidden": false, - "iid": 16, - "linked": [], - "primary": false, - "stype": "service", - "type": "000000A2-0000-1000-8000-0026BB765291" - }, - { - "characteristics": [ - { - "ev": false, - "format": "string", - "iid": 50, - "perms": [ - "pr" - ], - "type": "00000023-0000-1000-8000-0026BB765291", - "value": "LG webOS TV" - }, - { - "ev": false, - "format": "string", - "iid": 51, - "maxLen": 25, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "E3", - "value": "LG webOS TV OLED55B9PUA" - }, - { - "ev": false, - "format": "uint8", - "iid": 52, - "maxValue": 1, - "minStep": 1, - "minValue": 0, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "000000B0-0000-1000-8000-0026BB765291", - "value": 1 - }, - { - "ev": false, - "format": "uint32", - "iid": 53, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "E7", - "value": 6 - }, - { - "ev": false, - "format": "uint8", - "iid": 54, - "maxValue": 2, - "minStep": 1, - "minValue": 0, - "perms": [ - "pr", - "ev" - ], - "type": "E8", - "value": 1 - }, - { - "format": "uint8", - "iid": 57, - "maxValue": 1, - "minStep": 1, - "minValue": 0, - "perms": [ - "pw" - ], - "type": "DF" - }, - { - "format": "uint8", - "iid": 59, - "maxValue": 16, - "minStep": 1, - "minValue": 0, - "perms": [ - "pw" - ], - "type": "E1" - } - ], - "hidden": false, - "iid": 48, - "linked": [ - 64, - 80, - 384, - 256, - 272, - 288, - 304, - 320, - 336, - 352 - ], - "primary": true, - "stype": "Unknown Service: D8", - "type": "D8" - }, - { - "characteristics": [ - { - "ev": false, - "format": "uint16", - "iid": 66, - "maxValue": 2, - "minStep": 1, - "minValue": 0, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "E5", - "value": 0 - }, - { - "ev": false, - "format": "tlv8", - "iid": 67, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "E4", - "value": "AQACAQA=" - } - ], - "hidden": false, - "iid": 64, - "linked": [], - "primary": false, - "stype": "Unknown Service: DA", - "type": "DA" - }, - { - "characteristics": [ - { - "ev": false, - "format": "uint8", - "iid": 84, - "maxValue": 100, - "minStep": 1, - "minValue": 0, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "00000119-0000-1000-8000-0026BB765291", - "unit": "percentage", - "value": 0 - }, - { - "ev": false, - "format": "bool", - "iid": 82, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "0000011A-0000-1000-8000-0026BB765291", - "value": 0 - }, - { - "ev": false, - "format": "string", - "iid": 83, - "perms": [ - "pr" - ], - "type": "00000023-0000-1000-8000-0026BB765291", - "value": "Speaker" - }, - { - "ev": false, - "format": "uint8", - "iid": 85, - "maxValue": 1, - "minStep": 1, - "minValue": 0, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "000000B0-0000-1000-8000-0026BB765291", - "value": 1 - }, - { - "ev": false, - "format": "uint8", - "iid": 86, - "maxValue": 3, - "minStep": 1, - "minValue": 0, - "perms": [ - "pr", - "ev" - ], - "type": "E9", - "value": 2 - }, - { - "format": "uint8", - "iid": 87, - "maxValue": 1, - "minStep": 1, - "minValue": 0, - "perms": [ - "pw" - ], - "type": "EA" - } - ], - "hidden": false, - "iid": 80, - "linked": [], - "primary": false, - "stype": "speaker", - "type": "00000113-0000-1000-8000-0026BB765291" - }, - { - "characteristics": [ - { - "ev": false, - "format": "tlv8", - "iid": 385, - "perms": [ - "pr" - ], - "type": "222", - "value": "AQgBBnRAvoQmJQIaAQYgF0KJBUICBiAXQokFQgAAAgZ0QL6EJiQ=" - } - ], - "hidden": false, - "iid": 384, - "linked": [], - "primary": false, - "stype": "Unknown Service: 221", - "type": "221" - }, - { - "characteristics": [ - { - "ev": false, - "format": "uint8", - "iid": 258, - "maxValue": 10, - "minStep": 1, - "minValue": 0, - "perms": [ - "pr", - "ev" - ], - "type": "DB", - "value": 8 - }, - { - "ev": false, - "format": "uint8", - "iid": 259, - "maxValue": 1, - "minStep": 1, - "minValue": 0, - "perms": [ - "pr", - "ev" - ], - "type": "D6", - "value": 1 - }, - { - "ev": false, - "format": "uint32", - "iid": 260, - "perms": [ - "pr" - ], - "type": "E6", - "value": 1 - }, - { - "ev": false, - "format": "string", - "iid": 261, - "perms": [ - "pr" - ], - "type": "00000023-0000-1000-8000-0026BB765291", - "value": "AirPlay" - }, - { - "ev": false, - "format": "string", - "iid": 262, - "maxLen": 25, - "perms": [ - "pr", - "ev" - ], - "type": "E3", - "value": "AirPlay" - }, - { - "ev": false, - "format": "uint8", - "iid": 264, - "maxValue": 6, - "minStep": 1, - "minValue": 0, - "perms": [ - "pr", - "ev" - ], - "type": "DC", - "value": 0 - }, - { - "ev": false, - "format": "uint8", - "iid": 263, - "maxValue": 3, - "minStep": 1, - "minValue": 0, - "perms": [ - "pr", - "ev" - ], - "type": "135", - "value": 3 - } - ], - "hidden": false, - "iid": 256, - "linked": [], - "primary": false, - "stype": "Unknown Service: D9", - "type": "D9" - }, - { - "characteristics": [ - { - "ev": false, - "format": "uint8", - "iid": 274, - "maxValue": 10, - "minStep": 1, - "minValue": 0, - "perms": [ - "pr", - "ev" - ], - "type": "DB", - "value": 2 - }, - { - "ev": false, - "format": "uint8", - "iid": 275, - "maxValue": 1, - "minStep": 1, - "minValue": 0, - "perms": [ - "pr", - "ev" - ], - "type": "D6", - "value": 1 - }, - { - "ev": false, - "format": "uint32", - "iid": 276, - "perms": [ - "pr" - ], - "type": "E6", - "value": 2 - }, - { - "ev": false, - "format": "string", - "iid": 277, - "perms": [ - "pr" - ], - "type": "00000023-0000-1000-8000-0026BB765291", - "value": "Live TV" - }, - { - "ev": false, - "format": "string", - "iid": 278, - "maxLen": 25, - "perms": [ - "pr", - "ev" - ], - "type": "E3", - "value": "Live TV" - }, - { - "ev": false, - "format": "uint8", - "iid": 280, - "maxValue": 6, - "minStep": 1, - "minValue": 0, - "perms": [ - "pr", - "ev" - ], - "type": "DC", - "value": 3 - }, - { - "ev": false, - "format": "uint8", - "iid": 279, - "maxValue": 3, - "minStep": 1, - "minValue": 0, - "perms": [ - "pr", - "ev" - ], - "type": "135", - "value": 3 - } - ], - "hidden": false, - "iid": 272, - "linked": [], - "primary": false, - "stype": "Unknown Service: D9", - "type": "D9" - }, - { - "characteristics": [ - { - "ev": false, - "format": "uint8", - "iid": 290, - "maxValue": 10, - "minStep": 1, - "minValue": 0, - "perms": [ - "pr", - "ev" - ], - "type": "DB", - "value": 3 - }, - { - "ev": false, - "format": "uint8", - "iid": 291, - "maxValue": 1, - "minStep": 1, - "minValue": 0, - "perms": [ - "pr", - "ev" - ], - "type": "D6", - "value": 1 - }, - { - "ev": false, - "format": "uint32", - "iid": 292, - "perms": [ - "pr" - ], - "type": "E6", - "value": 3 - }, - { - "ev": false, - "format": "string", - "iid": 293, - "perms": [ - "pr" - ], - "type": "00000023-0000-1000-8000-0026BB765291", - "value": "HDMI 1" - }, - { - "ev": false, - "format": "string", - "iid": 294, - "maxLen": 25, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "E3", - "value": "HDMI 1" - }, - { - "ev": false, - "format": "uint8", - "iid": 296, - "maxValue": 6, - "minStep": 1, - "minValue": 0, - "perms": [ - "pr", - "ev" - ], - "type": "DC", - "value": 4 - }, - { - "ev": false, - "format": "uint8", - "iid": 295, - "maxValue": 3, - "minStep": 1, - "minValue": 0, - "perms": [ - "pr", - "ev" - ], - "type": "135", - "value": 1 - } - ], - "hidden": false, - "iid": 288, - "linked": [], - "primary": false, - "stype": "Unknown Service: D9", - "type": "D9" - }, - { - "characteristics": [ - { - "ev": false, - "format": "uint8", - "iid": 306, - "maxValue": 10, - "minStep": 1, - "minValue": 0, - "perms": [ - "pr", - "ev" - ], - "type": "DB", - "value": 3 - }, - { - "ev": false, - "format": "uint8", - "iid": 307, - "maxValue": 1, - "minStep": 1, - "minValue": 0, - "perms": [ - "pr", - "ev" - ], - "type": "D6", - "value": 1 - }, - { - "ev": false, - "format": "uint32", - "iid": 308, - "perms": [ - "pr" - ], - "type": "E6", - "value": 4 - }, - { - "ev": false, - "format": "string", - "iid": 309, - "perms": [ - "pr" - ], - "type": "00000023-0000-1000-8000-0026BB765291", - "value": "HDMI 2" - }, - { - "ev": false, - "format": "string", - "iid": 310, - "maxLen": 25, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "E3", - "value": "Sony" - }, - { - "ev": false, - "format": "uint8", - "iid": 312, - "maxValue": 6, - "minStep": 1, - "minValue": 0, - "perms": [ - "pr", - "ev" - ], - "type": "DC", - "value": 4 - }, - { - "ev": false, - "format": "uint8", - "iid": 311, - "maxValue": 3, - "minStep": 1, - "minValue": 0, - "perms": [ - "pr", - "ev" - ], - "type": "135", - "value": 2 - } - ], - "hidden": false, - "iid": 304, - "linked": [], - "primary": false, - "stype": "Unknown Service: D9", - "type": "D9" - }, - { - "characteristics": [ - { - "ev": false, - "format": "uint8", - "iid": 322, - "maxValue": 10, - "minStep": 1, - "minValue": 0, - "perms": [ - "pr", - "ev" - ], - "type": "DB", - "value": 3 - }, - { - "ev": false, - "format": "uint8", - "iid": 323, - "maxValue": 1, - "minStep": 1, - "minValue": 0, - "perms": [ - "pr", - "ev" - ], - "type": "D6", - "value": 1 - }, - { - "ev": false, - "format": "uint32", - "iid": 324, - "perms": [ - "pr" - ], - "type": "E6", - "value": 5 - }, - { - "ev": false, - "format": "string", - "iid": 325, - "perms": [ - "pr" - ], - "type": "00000023-0000-1000-8000-0026BB765291", - "value": "HDMI 3" - }, - { - "ev": false, - "format": "string", - "iid": 326, - "maxLen": 25, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "E3", - "value": "Apple" - }, - { - "ev": false, - "format": "uint8", - "iid": 328, - "maxValue": 6, - "minStep": 1, - "minValue": 0, - "perms": [ - "pr", - "ev" - ], - "type": "DC", - "value": 4 - }, - { - "ev": false, - "format": "uint8", - "iid": 327, - "maxValue": 3, - "minStep": 1, - "minValue": 0, - "perms": [ - "pr", - "ev" - ], - "type": "135", - "value": 2 - } - ], - "hidden": false, - "iid": 320, - "linked": [], - "primary": false, - "stype": "Unknown Service: D9", - "type": "D9" - }, - { - "characteristics": [ - { - "ev": false, - "format": "uint8", - "iid": 338, - "maxValue": 10, - "minStep": 1, - "minValue": 0, - "perms": [ - "pr", - "ev" - ], - "type": "DB", - "value": 4 - }, - { - "ev": false, - "format": "uint8", - "iid": 339, - "maxValue": 1, - "minStep": 1, - "minValue": 0, - "perms": [ - "pr", - "ev" - ], - "type": "D6", - "value": 1 - }, - { - "ev": false, - "format": "uint32", - "iid": 340, - "perms": [ - "pr" - ], - "type": "E6", - "value": 7 - }, - { - "ev": false, - "format": "string", - "iid": 341, - "perms": [ - "pr" - ], - "type": "00000023-0000-1000-8000-0026BB765291", - "value": "AV" - }, - { - "ev": false, - "format": "string", - "iid": 342, - "maxLen": 25, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "E3", - "value": "AV" - }, - { - "ev": false, - "format": "uint8", - "iid": 344, - "maxValue": 6, - "minStep": 1, - "minValue": 0, - "perms": [ - "pr", - "ev" - ], - "type": "DC", - "value": 2 - }, - { - "ev": false, - "format": "uint8", - "iid": 343, - "maxValue": 3, - "minStep": 1, - "minValue": 0, - "perms": [ - "pr", - "ev" - ], - "type": "135", - "value": 1 - } - ], - "hidden": false, - "iid": 336, - "linked": [], - "primary": false, - "stype": "Unknown Service: D9", - "type": "D9" - }, - { - "characteristics": [ - { - "ev": false, - "format": "uint8", - "iid": 354, - "maxValue": 10, - "minStep": 1, - "minValue": 0, - "perms": [ - "pr", - "ev" - ], - "type": "DB", - "value": 3 - }, - { - "ev": false, - "format": "uint8", - "iid": 355, - "maxValue": 1, - "minStep": 1, - "minValue": 0, - "perms": [ - "pr", - "ev" - ], - "type": "D6", - "value": 1 - }, - { - "ev": false, - "format": "uint32", - "iid": 356, - "perms": [ - "pr" - ], - "type": "E6", - "value": 6 - }, - { - "ev": false, - "format": "string", - "iid": 357, - "perms": [ - "pr" - ], - "type": "00000023-0000-1000-8000-0026BB765291", - "value": "HDMI 4" - }, - { - "ev": false, - "format": "string", - "iid": 358, - "maxLen": 25, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "E3", - "value": "HDMI 4" - }, - { - "ev": false, - "format": "uint8", - "iid": 360, - "maxValue": 6, - "minStep": 1, - "minValue": 0, - "perms": [ - "pr", - "ev" - ], - "type": "DC", - "value": 4 - }, - { - "ev": false, - "format": "uint8", - "iid": 359, - "maxValue": 3, - "minStep": 1, - "minValue": 0, - "perms": [ - "pr", - "ev" - ], - "type": "135", - "value": 2 - } - ], - "hidden": false, - "iid": 352, - "linked": [], - "primary": false, - "stype": "Unknown Service: D9", - "type": "D9" - } - ] - } + { + "aid": 1, + "services": [ + { + "characteristics": [ + { + "format": "bool", + "iid": 2, + "perms": ["pw"], + "type": "00000014-0000-1000-8000-0026BB765291" + }, + { + "ev": false, + "format": "string", + "iid": 3, + "perms": ["pr"], + "type": "00000020-0000-1000-8000-0026BB765291", + "value": "LG Electronics" + }, + { + "ev": false, + "format": "string", + "iid": 4, + "perms": ["pr"], + "type": "00000021-0000-1000-8000-0026BB765291", + "value": "OLED55B9PUA" + }, + { + "ev": false, + "format": "string", + "iid": 5, + "perms": ["pr"], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "LG webOS TV AF80" + }, + { + "ev": false, + "format": "string", + "iid": 6, + "perms": ["pr"], + "type": "00000030-0000-1000-8000-0026BB765291", + "value": "999AAAAAA999" + }, + { + "ev": false, + "format": "string", + "iid": 7, + "perms": ["pr"], + "type": "00000052-0000-1000-8000-0026BB765291", + "value": "04.71.04" + }, + { + "ev": false, + "format": "string", + "iid": 8, + "perms": ["pr"], + "type": "00000053-0000-1000-8000-0026BB765291", + "value": "1" + }, + { + "ev": false, + "format": "string", + "iid": 9, + "perms": ["pr", "hd"], + "type": "34AB8811-AC7F-4340-BAC3-FD6A85F9943B", + "value": "2.1;16B62a" + } + ], + "hidden": false, + "iid": 1, + "linked": [], + "primary": false, + "stype": "accessory-information", + "type": "0000003E-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "ev": false, + "format": "string", + "iid": 18, + "perms": ["pr"], + "type": "00000037-0000-1000-8000-0026BB765291", + "value": "1.1.0" + } + ], + "hidden": false, + "iid": 16, + "linked": [], + "primary": false, + "stype": "service", + "type": "000000A2-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "ev": false, + "format": "string", + "iid": 50, + "perms": ["pr"], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "LG webOS TV" + }, + { + "ev": false, + "format": "string", + "iid": 51, + "maxLen": 25, + "perms": ["pr", "pw", "ev"], + "type": "E3", + "value": "LG webOS TV OLED55B9PUA" + }, + { + "ev": false, + "format": "uint8", + "iid": 52, + "maxValue": 1, + "minStep": 1, + "minValue": 0, + "perms": ["pr", "pw", "ev"], + "type": "000000B0-0000-1000-8000-0026BB765291", + "value": 1 + }, + { + "ev": false, + "format": "uint32", + "iid": 53, + "perms": ["pr", "pw", "ev"], + "type": "E7", + "value": 6 + }, + { + "ev": false, + "format": "uint8", + "iid": 54, + "maxValue": 2, + "minStep": 1, + "minValue": 0, + "perms": ["pr", "ev"], + "type": "E8", + "value": 1 + }, + { + "format": "uint8", + "iid": 57, + "maxValue": 1, + "minStep": 1, + "minValue": 0, + "perms": ["pw"], + "type": "DF" + }, + { + "format": "uint8", + "iid": 59, + "maxValue": 16, + "minStep": 1, + "minValue": 0, + "perms": ["pw"], + "type": "E1" + } + ], + "hidden": false, + "iid": 48, + "linked": [64, 80, 384, 256, 272, 288, 304, 320, 336, 352], + "primary": true, + "stype": "Unknown Service: D8", + "type": "D8" + }, + { + "characteristics": [ + { + "ev": false, + "format": "uint16", + "iid": 66, + "maxValue": 2, + "minStep": 1, + "minValue": 0, + "perms": ["pr", "pw", "ev"], + "type": "E5", + "value": 0 + }, + { + "ev": false, + "format": "tlv8", + "iid": 67, + "perms": ["pr", "pw", "ev"], + "type": "E4", + "value": "AQACAQA=" + } + ], + "hidden": false, + "iid": 64, + "linked": [], + "primary": false, + "stype": "Unknown Service: DA", + "type": "DA" + }, + { + "characteristics": [ + { + "ev": false, + "format": "uint8", + "iid": 84, + "maxValue": 100, + "minStep": 1, + "minValue": 0, + "perms": ["pr", "pw", "ev"], + "type": "00000119-0000-1000-8000-0026BB765291", + "unit": "percentage", + "value": 0 + }, + { + "ev": false, + "format": "bool", + "iid": 82, + "perms": ["pr", "pw", "ev"], + "type": "0000011A-0000-1000-8000-0026BB765291", + "value": 0 + }, + { + "ev": false, + "format": "string", + "iid": 83, + "perms": ["pr"], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Speaker" + }, + { + "ev": false, + "format": "uint8", + "iid": 85, + "maxValue": 1, + "minStep": 1, + "minValue": 0, + "perms": ["pr", "pw", "ev"], + "type": "000000B0-0000-1000-8000-0026BB765291", + "value": 1 + }, + { + "ev": false, + "format": "uint8", + "iid": 86, + "maxValue": 3, + "minStep": 1, + "minValue": 0, + "perms": ["pr", "ev"], + "type": "E9", + "value": 2 + }, + { + "format": "uint8", + "iid": 87, + "maxValue": 1, + "minStep": 1, + "minValue": 0, + "perms": ["pw"], + "type": "EA" + } + ], + "hidden": false, + "iid": 80, + "linked": [], + "primary": false, + "stype": "speaker", + "type": "00000113-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "ev": false, + "format": "tlv8", + "iid": 385, + "perms": ["pr"], + "type": "222", + "value": "AQgBBnRAvoQmJQIaAQYgF0KJBUICBiAXQokFQgAAAgZ0QL6EJiQ=" + } + ], + "hidden": false, + "iid": 384, + "linked": [], + "primary": false, + "stype": "Unknown Service: 221", + "type": "221" + }, + { + "characteristics": [ + { + "ev": false, + "format": "uint8", + "iid": 258, + "maxValue": 10, + "minStep": 1, + "minValue": 0, + "perms": ["pr", "ev"], + "type": "DB", + "value": 8 + }, + { + "ev": false, + "format": "uint8", + "iid": 259, + "maxValue": 1, + "minStep": 1, + "minValue": 0, + "perms": ["pr", "ev"], + "type": "D6", + "value": 1 + }, + { + "ev": false, + "format": "uint32", + "iid": 260, + "perms": ["pr"], + "type": "E6", + "value": 1 + }, + { + "ev": false, + "format": "string", + "iid": 261, + "perms": ["pr"], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "AirPlay" + }, + { + "ev": false, + "format": "string", + "iid": 262, + "maxLen": 25, + "perms": ["pr", "ev"], + "type": "E3", + "value": "AirPlay" + }, + { + "ev": false, + "format": "uint8", + "iid": 264, + "maxValue": 6, + "minStep": 1, + "minValue": 0, + "perms": ["pr", "ev"], + "type": "DC", + "value": 0 + }, + { + "ev": false, + "format": "uint8", + "iid": 263, + "maxValue": 3, + "minStep": 1, + "minValue": 0, + "perms": ["pr", "ev"], + "type": "135", + "value": 3 + } + ], + "hidden": false, + "iid": 256, + "linked": [], + "primary": false, + "stype": "Unknown Service: D9", + "type": "D9" + }, + { + "characteristics": [ + { + "ev": false, + "format": "uint8", + "iid": 274, + "maxValue": 10, + "minStep": 1, + "minValue": 0, + "perms": ["pr", "ev"], + "type": "DB", + "value": 2 + }, + { + "ev": false, + "format": "uint8", + "iid": 275, + "maxValue": 1, + "minStep": 1, + "minValue": 0, + "perms": ["pr", "ev"], + "type": "D6", + "value": 1 + }, + { + "ev": false, + "format": "uint32", + "iid": 276, + "perms": ["pr"], + "type": "E6", + "value": 2 + }, + { + "ev": false, + "format": "string", + "iid": 277, + "perms": ["pr"], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Live TV" + }, + { + "ev": false, + "format": "string", + "iid": 278, + "maxLen": 25, + "perms": ["pr", "ev"], + "type": "E3", + "value": "Live TV" + }, + { + "ev": false, + "format": "uint8", + "iid": 280, + "maxValue": 6, + "minStep": 1, + "minValue": 0, + "perms": ["pr", "ev"], + "type": "DC", + "value": 3 + }, + { + "ev": false, + "format": "uint8", + "iid": 279, + "maxValue": 3, + "minStep": 1, + "minValue": 0, + "perms": ["pr", "ev"], + "type": "135", + "value": 3 + } + ], + "hidden": false, + "iid": 272, + "linked": [], + "primary": false, + "stype": "Unknown Service: D9", + "type": "D9" + }, + { + "characteristics": [ + { + "ev": false, + "format": "uint8", + "iid": 290, + "maxValue": 10, + "minStep": 1, + "minValue": 0, + "perms": ["pr", "ev"], + "type": "DB", + "value": 3 + }, + { + "ev": false, + "format": "uint8", + "iid": 291, + "maxValue": 1, + "minStep": 1, + "minValue": 0, + "perms": ["pr", "ev"], + "type": "D6", + "value": 1 + }, + { + "ev": false, + "format": "uint32", + "iid": 292, + "perms": ["pr"], + "type": "E6", + "value": 3 + }, + { + "ev": false, + "format": "string", + "iid": 293, + "perms": ["pr"], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "HDMI 1" + }, + { + "ev": false, + "format": "string", + "iid": 294, + "maxLen": 25, + "perms": ["pr", "pw", "ev"], + "type": "E3", + "value": "HDMI 1" + }, + { + "ev": false, + "format": "uint8", + "iid": 296, + "maxValue": 6, + "minStep": 1, + "minValue": 0, + "perms": ["pr", "ev"], + "type": "DC", + "value": 4 + }, + { + "ev": false, + "format": "uint8", + "iid": 295, + "maxValue": 3, + "minStep": 1, + "minValue": 0, + "perms": ["pr", "ev"], + "type": "135", + "value": 1 + } + ], + "hidden": false, + "iid": 288, + "linked": [], + "primary": false, + "stype": "Unknown Service: D9", + "type": "D9" + }, + { + "characteristics": [ + { + "ev": false, + "format": "uint8", + "iid": 306, + "maxValue": 10, + "minStep": 1, + "minValue": 0, + "perms": ["pr", "ev"], + "type": "DB", + "value": 3 + }, + { + "ev": false, + "format": "uint8", + "iid": 307, + "maxValue": 1, + "minStep": 1, + "minValue": 0, + "perms": ["pr", "ev"], + "type": "D6", + "value": 1 + }, + { + "ev": false, + "format": "uint32", + "iid": 308, + "perms": ["pr"], + "type": "E6", + "value": 4 + }, + { + "ev": false, + "format": "string", + "iid": 309, + "perms": ["pr"], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "HDMI 2" + }, + { + "ev": false, + "format": "string", + "iid": 310, + "maxLen": 25, + "perms": ["pr", "pw", "ev"], + "type": "E3", + "value": "Sony" + }, + { + "ev": false, + "format": "uint8", + "iid": 312, + "maxValue": 6, + "minStep": 1, + "minValue": 0, + "perms": ["pr", "ev"], + "type": "DC", + "value": 4 + }, + { + "ev": false, + "format": "uint8", + "iid": 311, + "maxValue": 3, + "minStep": 1, + "minValue": 0, + "perms": ["pr", "ev"], + "type": "135", + "value": 2 + } + ], + "hidden": false, + "iid": 304, + "linked": [], + "primary": false, + "stype": "Unknown Service: D9", + "type": "D9" + }, + { + "characteristics": [ + { + "ev": false, + "format": "uint8", + "iid": 322, + "maxValue": 10, + "minStep": 1, + "minValue": 0, + "perms": ["pr", "ev"], + "type": "DB", + "value": 3 + }, + { + "ev": false, + "format": "uint8", + "iid": 323, + "maxValue": 1, + "minStep": 1, + "minValue": 0, + "perms": ["pr", "ev"], + "type": "D6", + "value": 1 + }, + { + "ev": false, + "format": "uint32", + "iid": 324, + "perms": ["pr"], + "type": "E6", + "value": 5 + }, + { + "ev": false, + "format": "string", + "iid": 325, + "perms": ["pr"], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "HDMI 3" + }, + { + "ev": false, + "format": "string", + "iid": 326, + "maxLen": 25, + "perms": ["pr", "pw", "ev"], + "type": "E3", + "value": "Apple" + }, + { + "ev": false, + "format": "uint8", + "iid": 328, + "maxValue": 6, + "minStep": 1, + "minValue": 0, + "perms": ["pr", "ev"], + "type": "DC", + "value": 4 + }, + { + "ev": false, + "format": "uint8", + "iid": 327, + "maxValue": 3, + "minStep": 1, + "minValue": 0, + "perms": ["pr", "ev"], + "type": "135", + "value": 2 + } + ], + "hidden": false, + "iid": 320, + "linked": [], + "primary": false, + "stype": "Unknown Service: D9", + "type": "D9" + }, + { + "characteristics": [ + { + "ev": false, + "format": "uint8", + "iid": 338, + "maxValue": 10, + "minStep": 1, + "minValue": 0, + "perms": ["pr", "ev"], + "type": "DB", + "value": 4 + }, + { + "ev": false, + "format": "uint8", + "iid": 339, + "maxValue": 1, + "minStep": 1, + "minValue": 0, + "perms": ["pr", "ev"], + "type": "D6", + "value": 1 + }, + { + "ev": false, + "format": "uint32", + "iid": 340, + "perms": ["pr"], + "type": "E6", + "value": 7 + }, + { + "ev": false, + "format": "string", + "iid": 341, + "perms": ["pr"], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "AV" + }, + { + "ev": false, + "format": "string", + "iid": 342, + "maxLen": 25, + "perms": ["pr", "pw", "ev"], + "type": "E3", + "value": "AV" + }, + { + "ev": false, + "format": "uint8", + "iid": 344, + "maxValue": 6, + "minStep": 1, + "minValue": 0, + "perms": ["pr", "ev"], + "type": "DC", + "value": 2 + }, + { + "ev": false, + "format": "uint8", + "iid": 343, + "maxValue": 3, + "minStep": 1, + "minValue": 0, + "perms": ["pr", "ev"], + "type": "135", + "value": 1 + } + ], + "hidden": false, + "iid": 336, + "linked": [], + "primary": false, + "stype": "Unknown Service: D9", + "type": "D9" + }, + { + "characteristics": [ + { + "ev": false, + "format": "uint8", + "iid": 354, + "maxValue": 10, + "minStep": 1, + "minValue": 0, + "perms": ["pr", "ev"], + "type": "DB", + "value": 3 + }, + { + "ev": false, + "format": "uint8", + "iid": 355, + "maxValue": 1, + "minStep": 1, + "minValue": 0, + "perms": ["pr", "ev"], + "type": "D6", + "value": 1 + }, + { + "ev": false, + "format": "uint32", + "iid": 356, + "perms": ["pr"], + "type": "E6", + "value": 6 + }, + { + "ev": false, + "format": "string", + "iid": 357, + "perms": ["pr"], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "HDMI 4" + }, + { + "ev": false, + "format": "string", + "iid": 358, + "maxLen": 25, + "perms": ["pr", "pw", "ev"], + "type": "E3", + "value": "HDMI 4" + }, + { + "ev": false, + "format": "uint8", + "iid": 360, + "maxValue": 6, + "minStep": 1, + "minValue": 0, + "perms": ["pr", "ev"], + "type": "DC", + "value": 4 + }, + { + "ev": false, + "format": "uint8", + "iid": 359, + "maxValue": 3, + "minStep": 1, + "minValue": 0, + "perms": ["pr", "ev"], + "type": "135", + "value": 2 + } + ], + "hidden": false, + "iid": 352, + "linked": [], + "primary": false, + "stype": "Unknown Service: D9", + "type": "D9" + } + ] + } ] diff --git a/tests/components/homekit_controller/fixtures/mysa_living.json b/tests/components/homekit_controller/fixtures/mysa_living.json index da26b654fe5..2c73f8310b8 100644 --- a/tests/components/homekit_controller/fixtures/mysa_living.json +++ b/tests/components/homekit_controller/fixtures/mysa_living.json @@ -1,250 +1,198 @@ [ - { - "aid": 1, + { + "aid": 1, + "primary": true, + "services": [ + { + "type": "0000004A-0000-1000-8000-0026BB765291", "primary": true, - "services": [ - { - "type": "0000004A-0000-1000-8000-0026BB765291", - "primary": true, - "iid": 20, - "characteristics": [ - { - "type": "00000023-0000-1000-8000-0026BB765291", - "format": "string", - "value": "Thermostat", - "perms": [ - "pr" - ], - "iid": 24 - }, - { - "type": "00000010-0000-1000-8000-0026BB765291", - "format": "float", - "minValue": 0, - "maxValue": 100, - "stepValue": 1, - "value": 40, - "iid": 27, - "unit": "percentage", - "perms": [ - "pr", - "ev" - ] - }, - { - "type": "0000000F-0000-1000-8000-0026BB765291", - "value": 0, - "minValue": 0, - "maxValue": 2, - "stepValue": 1, - "format": "uint8", - "perms": [ - "pr", - "ev" - ], - "iid": 21 - }, - { - "type": "00000033-0000-1000-8000-0026BB765291", - "value": 0, - "minValue": 0, - "maxValue": 3, - "stepValue": 1, - "format": "uint8", - "perms": [ - "pr", - "pw", - "ev" - ], - "iid": 22 - }, - { - "type": "00000011-0000-1000-8000-0026BB765291", - "value": 24.1, - "minValue": 0, - "maxValue": 100, - "stepValue": 0.1, - "unit": "celsius", - "format": "float", - "perms": [ - "pr", - "ev" - ], - "iid": 25 - }, - { - "type": "00000035-0000-1000-8000-0026BB765291", - "value": 22, - "minValue": 5, - "maxValue": 30, - "stepValue": 0.1, - "unit": "celsius", - "format": "float", - "perms": [ - "pr", - "pw", - "ev" - ], - "iid": 23 - }, - { - "type": "00000036-0000-1000-8000-0026BB765291", - "format": "uint8", - "minValue": 0, - "maxValue": 1, - "stepValue": 1, - "value": 0, - "iid": 26, - "perms": [ - "pr", - "pw", - "ev" - ] - } - ] - }, - { - "type": "0000003E-0000-1000-8000-0026BB765291", - "iid": 1, - "characteristics": [ - { - "type": "00000014-0000-1000-8000-0026BB765291", - "perms": [ - "pw" - ], - "iid": 2, - "format": "bool" - }, - { - "type": "00000020-0000-1000-8000-0026BB765291", - "format": "string", - "value": "Empowered Homes Inc.", - "perms": [ - "pr" - ], - "iid": 3 - }, - { - "type": "00000021-0000-1000-8000-0026BB765291", - "format": "string", - "value": "v1", - "perms": [ - "pr" - ], - "iid": 4 - }, - { - "type": "00000023-0000-1000-8000-0026BB765291", - "format": "string", - "value": "Mysa-85dda9", - "perms": [ - "pr" - ], - "iid": 5 - }, - { - "type": "00000030-0000-1000-8000-0026BB765291", - "format": "string", - "value": "AAAAAAA000", - "perms": [ - "pr" - ], - "iid": 6 - }, - { - "type": "00000052-0000-1000-8000-0026BB765291", - "format": "string", - "value": "2.8.1", - "perms": [ - "pr" - ], - "iid": 7 - }, - { - "hidden": true, - "type": "22280E2C-9B79-43BD-8370-5A8F67777B29", - "format": "string", - "value": "b4e62d85dda9", - "perms": [ - "pr" - ], - "iid": 8 - } - ] - }, - { - "type": "000000A2-0000-1000-8000-0026BB765291", - "iid": 10, - "characteristics": [ - { - "type": "00000037-0000-1000-8000-0026BB765291", - "format": "string", - "value": "1.1.0", - "perms": [ - "pr" - ], - "iid": 11 - } - ] - }, - { - "type": "00000043-0000-1000-8000-0026BB765291", - "iid": 40, - "characteristics": [ - { - "type": "00000025-0000-1000-8000-0026BB765291", - "format": "bool", - "value": 0, - "perms": [ - "pr", - "pw", - "ev" - ], - "iid": 42 - }, - { - "type": "00000023-0000-1000-8000-0026BB765291", - "format": "string", - "value": "Display", - "perms": [ - "pr" - ], - "iid": 41 - }, - { - "type": "00000008-0000-1000-8000-0026BB765291", - "format": "int", - "minValue": 0, - "maxValue": 100, - "stepValue": 1, - "value": 0, - "iid": 43, - "unit": "percentage", - "perms": [ - "pr", - "pw", - "ev" - ] - } - ] - }, - { - "type": "3354EC82-AF38-4755-B4A4-4DB8E418F555", - "iid": 50, - "characteristics": [ - { - "hidden": true, - "type": "E71D8348-BB33-4C34-8C50-A64B1136EDD2", - "format": "bool", - "value": 0, - "perms": [ - "pr", - "pw" - ], - "iid": 51 - } - ] - } + "iid": 20, + "characteristics": [ + { + "type": "00000023-0000-1000-8000-0026BB765291", + "format": "string", + "value": "Thermostat", + "perms": ["pr"], + "iid": 24 + }, + { + "type": "00000010-0000-1000-8000-0026BB765291", + "format": "float", + "minValue": 0, + "maxValue": 100, + "stepValue": 1, + "value": 40, + "iid": 27, + "unit": "percentage", + "perms": ["pr", "ev"] + }, + { + "type": "0000000F-0000-1000-8000-0026BB765291", + "value": 0, + "minValue": 0, + "maxValue": 2, + "stepValue": 1, + "format": "uint8", + "perms": ["pr", "ev"], + "iid": 21 + }, + { + "type": "00000033-0000-1000-8000-0026BB765291", + "value": 0, + "minValue": 0, + "maxValue": 3, + "stepValue": 1, + "format": "uint8", + "perms": ["pr", "pw", "ev"], + "iid": 22 + }, + { + "type": "00000011-0000-1000-8000-0026BB765291", + "value": 24.1, + "minValue": 0, + "maxValue": 100, + "stepValue": 0.1, + "unit": "celsius", + "format": "float", + "perms": ["pr", "ev"], + "iid": 25 + }, + { + "type": "00000035-0000-1000-8000-0026BB765291", + "value": 22, + "minValue": 5, + "maxValue": 30, + "stepValue": 0.1, + "unit": "celsius", + "format": "float", + "perms": ["pr", "pw", "ev"], + "iid": 23 + }, + { + "type": "00000036-0000-1000-8000-0026BB765291", + "format": "uint8", + "minValue": 0, + "maxValue": 1, + "stepValue": 1, + "value": 0, + "iid": 26, + "perms": ["pr", "pw", "ev"] + } ] - } -] \ No newline at end of file + }, + { + "type": "0000003E-0000-1000-8000-0026BB765291", + "iid": 1, + "characteristics": [ + { + "type": "00000014-0000-1000-8000-0026BB765291", + "perms": ["pw"], + "iid": 2, + "format": "bool" + }, + { + "type": "00000020-0000-1000-8000-0026BB765291", + "format": "string", + "value": "Empowered Homes Inc.", + "perms": ["pr"], + "iid": 3 + }, + { + "type": "00000021-0000-1000-8000-0026BB765291", + "format": "string", + "value": "v1", + "perms": ["pr"], + "iid": 4 + }, + { + "type": "00000023-0000-1000-8000-0026BB765291", + "format": "string", + "value": "Mysa-85dda9", + "perms": ["pr"], + "iid": 5 + }, + { + "type": "00000030-0000-1000-8000-0026BB765291", + "format": "string", + "value": "AAAAAAA000", + "perms": ["pr"], + "iid": 6 + }, + { + "type": "00000052-0000-1000-8000-0026BB765291", + "format": "string", + "value": "2.8.1", + "perms": ["pr"], + "iid": 7 + }, + { + "hidden": true, + "type": "22280E2C-9B79-43BD-8370-5A8F67777B29", + "format": "string", + "value": "b4e62d85dda9", + "perms": ["pr"], + "iid": 8 + } + ] + }, + { + "type": "000000A2-0000-1000-8000-0026BB765291", + "iid": 10, + "characteristics": [ + { + "type": "00000037-0000-1000-8000-0026BB765291", + "format": "string", + "value": "1.1.0", + "perms": ["pr"], + "iid": 11 + } + ] + }, + { + "type": "00000043-0000-1000-8000-0026BB765291", + "iid": 40, + "characteristics": [ + { + "type": "00000025-0000-1000-8000-0026BB765291", + "format": "bool", + "value": 0, + "perms": ["pr", "pw", "ev"], + "iid": 42 + }, + { + "type": "00000023-0000-1000-8000-0026BB765291", + "format": "string", + "value": "Display", + "perms": ["pr"], + "iid": 41 + }, + { + "type": "00000008-0000-1000-8000-0026BB765291", + "format": "int", + "minValue": 0, + "maxValue": 100, + "stepValue": 1, + "value": 0, + "iid": 43, + "unit": "percentage", + "perms": ["pr", "pw", "ev"] + } + ] + }, + { + "type": "3354EC82-AF38-4755-B4A4-4DB8E418F555", + "iid": 50, + "characteristics": [ + { + "hidden": true, + "type": "E71D8348-BB33-4C34-8C50-A64B1136EDD2", + "format": "bool", + "value": 0, + "perms": ["pr", "pw"], + "iid": 51 + } + ] + } + ] + } +] diff --git a/tests/components/homekit_controller/fixtures/netamo_doorbell.json b/tests/components/homekit_controller/fixtures/netamo_doorbell.json index 450b419f30d..38ef1e14458 100644 --- a/tests/components/homekit_controller/fixtures/netamo_doorbell.json +++ b/tests/components/homekit_controller/fixtures/netamo_doorbell.json @@ -1,341 +1,273 @@ [ - { - "aid" : 1, - "services" : [ - { - "hidden" : true, - "iid" : 53, - "characteristics" : [ - { - "format" : "bool", - "iid" : 54, - "perms" : [ - "pw" - ], - "type" : "4D05AE82-5A22-5BD6-A730-B7F8B4F3218D" - }, - { - "value" : "g738658", - "format" : "string", - "type" : "00F44C18-042E-5C4E-9A4C-561D44DCD804", - "perms" : [ - "pr" - ], - "iid" : 55 - } - ], - "primary" : false, - "type" : "EA22EA53-6227-55EA-AC24-73ACF3EEA0E8" - }, - { - "type" : "0000003E-0000-1000-8000-0026BB765291", - "primary" : false, - "iid" : 1, - "characteristics" : [ - { - "format" : "string", - "value" : "Netatmo-Doorbell-g738658", - "iid" : 2, - "perms" : [ - "pr" - ], - "type" : "00000023-0000-1000-8000-0026BB765291" - }, - { - "iid" : 3, - "type" : "00000020-0000-1000-8000-0026BB765291", - "perms" : [ - "pr" - ], - "value" : "Netatmo", - "format" : "string" - }, - { - "format" : "string", - "value" : "Netatmo Doorbell", - "perms" : [ - "pr" - ], - "type" : "00000021-0000-1000-8000-0026BB765291", - "iid" : 4 - }, - { - "format" : "string", - "value" : "g738658", - "perms" : [ - "pr" - ], - "type" : "00000030-0000-1000-8000-0026BB765291", - "iid" : 5 - }, - { - "iid" : 6, - "perms" : [ - "pr" - ], - "type" : "00000052-0000-1000-8000-0026BB765291", - "format" : "string", - "value" : "80.0.0" - }, - { - "type" : "00000014-0000-1000-8000-0026BB765291", - "perms" : [ - "pw" - ], - "iid" : 7, - "format" : "bool" - }, - { - "value" : "+nvrOo7/HvM=", - "format" : "data", - "iid" : 56, - "type" : "220", - "perms" : [ - "pr" - ] - } - ], - "hidden" : false - }, - { - "hidden" : false, - "iid" : 29, - "characteristics" : [ - { - "format" : "string", - "value" : "1.1.0", - "perms" : [ - "pr" - ], - "type" : "00000037-0000-1000-8000-0026BB765291", - "iid" : 30 - } - ], - "type" : "000000A2-0000-1000-8000-0026BB765291", - "primary" : false - }, - { - "type" : "00000121-0000-1000-8000-0026BB765291", - "primary" : true, - "characteristics" : [ - { - "value" : null, - "format" : "uint8", - "type" : "00000073-0000-1000-8000-0026BB765291", - "perms" : [ - "pr", - "ev" - ], - "iid" : 50 - }, - { - "value" : "Doorbell", - "format" : "string", - "type" : "00000023-0000-1000-8000-0026BB765291", - "perms" : [ - "pr" - ], - "iid" : 57 - } - ], - "iid" : 49, - "hidden" : false - }, - { - "hidden" : false, - "iid" : 51, - "characteristics" : [ - { - "value" : false, - "format" : "bool", - "type" : "0000011A-0000-1000-8000-0026BB765291", - "perms" : [ - "pr", - "pw", - "ev" - ], - "iid" : 52 - } - ], - "type" : "00000113-0000-1000-8000-0026BB765291", - "primary" : false - }, - { - "hidden" : false, - "characteristics" : [ - { - "value" : false, - "format" : "bool", - "iid" : 9, - "type" : "0000011A-0000-1000-8000-0026BB765291", - "perms" : [ - "pr", - "pw", - "ev" - ] - } - ], - "iid" : 8, - "type" : "00000112-0000-1000-8000-0026BB765291", - "primary" : false - }, - { - "hidden" : false, - "iid" : 10, - "characteristics" : [ - { - "iid" : 11, - "type" : "00000022-0000-1000-8000-0026BB765291", - "perms" : [ - "pr", - "ev" - ], - "value" : false, - "format" : "bool" - }, - { - "perms" : [ - "pr" - ], - "type" : "00000023-0000-1000-8000-0026BB765291", - "iid" : 12, - "format" : "string", - "value" : "Motion Sensor" - } - ], - "type" : "00000085-0000-1000-8000-0026BB765291", - "primary" : false - }, - { - "primary" : false, - "type" : "00000110-0000-1000-8000-0026BB765291", - "characteristics" : [ - { - "format" : "tlv8", - "value" : "AQEA", - "perms" : [ - "pr", - "ev" - ], - "type" : "00000120-0000-1000-8000-0026BB765291", - "iid" : 14 - }, - { - "iid" : 15, - "type" : "00000114-0000-1000-8000-0026BB765291", - "perms" : [ - "pr" - ], - "value" : "AVUBAQACFgEBAQAAAQECAgEAAAACAQIDAQAEAQADCwECgAcCAjgEAwEeAAADCwECAAUCAtACAwEeAAADCwECgAICAmgBAwEeAAADCwECQAECAvAAAwEe", - "format" : "tlv8" - }, - { - "iid" : 16, - "type" : "00000115-0000-1000-8000-0026BB765291", - "perms" : [ - "pr" - ], - "value" : "ARMBAQMCDgEBAQIBAQAAAgEAAwEBAgEA", - "format" : "tlv8" - }, - { - "value" : "AgECAAACAQAAAAIBAQ==", - "format" : "tlv8", - "type" : "00000116-0000-1000-8000-0026BB765291", - "perms" : [ - "pr" - ], - "iid" : 17 - }, - { - "iid" : 19, - "type" : "00000117-0000-1000-8000-0026BB765291", - "perms" : [ - "pr", - "pw" - ], - "value" : "AQA=", - "format" : "tlv8" - }, - { - "iid" : 18, - "perms" : [ - "pr", - "pw" - ], - "type" : "00000118-0000-1000-8000-0026BB765291", - "format" : "tlv8", - "value" : "ARDpyds+onxNHb4xI0H6deS3AgEAAxgBAQACCzEwLjEwLjYwLjExAwJRwwQCUsMENQEBAQIgyWEU3zQuPNAsFAm1DM3ZSdp0Vh7kGuVQ+vqtS5Qa09YDDr5ebeow7eweCsu3FYh/BTUBAQECIFPPdRRI86ozZNB/WU/e8Em4N1lSsJhttOWoJly3XNEMAw7Zm8TgFdAof+wvoCQTYgYE/r0D9wcEC1Pwjg==" - } - ], - "iid" : 13, - "hidden" : false - }, - { - "iid" : 20, - "characteristics" : [ - { - "iid" : 21, - "type" : "00000120-0000-1000-8000-0026BB765291", - "perms" : [ - "pr", - "ev" - ], - "value" : "AQEA", - "format" : "tlv8" - }, - { - "format" : "tlv8", - "value" : "AVUBAQACFgEBAQAAAQECAgEAAAACAQIDAQAEAQADCwECgAcCAjgEAwEeAAADCwECAAUCAtACAwEeAAADCwECgAICAmgBAwEeAAADCwECQAECAvAAAwEe", - "perms" : [ - "pr" - ], - "type" : "00000114-0000-1000-8000-0026BB765291", - "iid" : 22 - }, - { - "format" : "tlv8", - "value" : "ARMBAQMCDgEBAQIBAQAAAgEAAwEBAgEA", - "iid" : 23, - "perms" : [ - "pr" - ], - "type" : "00000115-0000-1000-8000-0026BB765291" - }, - { - "format" : "tlv8", - "value" : "AgECAAACAQAAAAIBAQ==", - "perms" : [ - "pr" - ], - "type" : "00000116-0000-1000-8000-0026BB765291", - "iid" : 24 - }, - { - "format" : "tlv8", - "value" : "AQA=", - "perms" : [ - "pr", - "pw" - ], - "type" : "00000117-0000-1000-8000-0026BB765291", - "iid" : 26 - }, - { - "iid" : 25, - "type" : "00000118-0000-1000-8000-0026BB765291", - "perms" : [ - "pr", - "pw" - ], - "value" : "ARDu4+F49fZMSatQjcfR8FGVAgEAAxgBAQACCzEwLjEwLjYwLjExAwJbwwQCXMMENQEBAQIg9nqVm+80ccYh/S3vKKfbcUGH7VgggHRwp1e1x63+kpkDDnAxnJxfEz8KDp6xKoPhBTUBAQECILLYad+aKdzVbhGz55ywh0RYX9DTyY7HdSRf8y8tUi1kAw4DRngrGhYBdnrjELUzGgYEf+ysuwcESU05wg==", - "format" : "tlv8" - } - ], - "primary" : false, - "type" : "00000110-0000-1000-8000-0026BB765291", - "hidden" : false - } - ] - } + { + "aid": 1, + "services": [ + { + "hidden": true, + "iid": 53, + "characteristics": [ + { + "format": "bool", + "iid": 54, + "perms": ["pw"], + "type": "4D05AE82-5A22-5BD6-A730-B7F8B4F3218D" + }, + { + "value": "g738658", + "format": "string", + "type": "00F44C18-042E-5C4E-9A4C-561D44DCD804", + "perms": ["pr"], + "iid": 55 + } + ], + "primary": false, + "type": "EA22EA53-6227-55EA-AC24-73ACF3EEA0E8" + }, + { + "type": "0000003E-0000-1000-8000-0026BB765291", + "primary": false, + "iid": 1, + "characteristics": [ + { + "format": "string", + "value": "Netatmo-Doorbell-g738658", + "iid": 2, + "perms": ["pr"], + "type": "00000023-0000-1000-8000-0026BB765291" + }, + { + "iid": 3, + "type": "00000020-0000-1000-8000-0026BB765291", + "perms": ["pr"], + "value": "Netatmo", + "format": "string" + }, + { + "format": "string", + "value": "Netatmo Doorbell", + "perms": ["pr"], + "type": "00000021-0000-1000-8000-0026BB765291", + "iid": 4 + }, + { + "format": "string", + "value": "g738658", + "perms": ["pr"], + "type": "00000030-0000-1000-8000-0026BB765291", + "iid": 5 + }, + { + "iid": 6, + "perms": ["pr"], + "type": "00000052-0000-1000-8000-0026BB765291", + "format": "string", + "value": "80.0.0" + }, + { + "type": "00000014-0000-1000-8000-0026BB765291", + "perms": ["pw"], + "iid": 7, + "format": "bool" + }, + { + "value": "+nvrOo7/HvM=", + "format": "data", + "iid": 56, + "type": "220", + "perms": ["pr"] + } + ], + "hidden": false + }, + { + "hidden": false, + "iid": 29, + "characteristics": [ + { + "format": "string", + "value": "1.1.0", + "perms": ["pr"], + "type": "00000037-0000-1000-8000-0026BB765291", + "iid": 30 + } + ], + "type": "000000A2-0000-1000-8000-0026BB765291", + "primary": false + }, + { + "type": "00000121-0000-1000-8000-0026BB765291", + "primary": true, + "characteristics": [ + { + "value": null, + "format": "uint8", + "type": "00000073-0000-1000-8000-0026BB765291", + "perms": ["pr", "ev"], + "iid": 50 + }, + { + "value": "Doorbell", + "format": "string", + "type": "00000023-0000-1000-8000-0026BB765291", + "perms": ["pr"], + "iid": 57 + } + ], + "iid": 49, + "hidden": false + }, + { + "hidden": false, + "iid": 51, + "characteristics": [ + { + "value": false, + "format": "bool", + "type": "0000011A-0000-1000-8000-0026BB765291", + "perms": ["pr", "pw", "ev"], + "iid": 52 + } + ], + "type": "00000113-0000-1000-8000-0026BB765291", + "primary": false + }, + { + "hidden": false, + "characteristics": [ + { + "value": false, + "format": "bool", + "iid": 9, + "type": "0000011A-0000-1000-8000-0026BB765291", + "perms": ["pr", "pw", "ev"] + } + ], + "iid": 8, + "type": "00000112-0000-1000-8000-0026BB765291", + "primary": false + }, + { + "hidden": false, + "iid": 10, + "characteristics": [ + { + "iid": 11, + "type": "00000022-0000-1000-8000-0026BB765291", + "perms": ["pr", "ev"], + "value": false, + "format": "bool" + }, + { + "perms": ["pr"], + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 12, + "format": "string", + "value": "Motion Sensor" + } + ], + "type": "00000085-0000-1000-8000-0026BB765291", + "primary": false + }, + { + "primary": false, + "type": "00000110-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "format": "tlv8", + "value": "AQEA", + "perms": ["pr", "ev"], + "type": "00000120-0000-1000-8000-0026BB765291", + "iid": 14 + }, + { + "iid": 15, + "type": "00000114-0000-1000-8000-0026BB765291", + "perms": ["pr"], + "value": "AVUBAQACFgEBAQAAAQECAgEAAAACAQIDAQAEAQADCwECgAcCAjgEAwEeAAADCwECAAUCAtACAwEeAAADCwECgAICAmgBAwEeAAADCwECQAECAvAAAwEe", + "format": "tlv8" + }, + { + "iid": 16, + "type": "00000115-0000-1000-8000-0026BB765291", + "perms": ["pr"], + "value": "ARMBAQMCDgEBAQIBAQAAAgEAAwEBAgEA", + "format": "tlv8" + }, + { + "value": "AgECAAACAQAAAAIBAQ==", + "format": "tlv8", + "type": "00000116-0000-1000-8000-0026BB765291", + "perms": ["pr"], + "iid": 17 + }, + { + "iid": 19, + "type": "00000117-0000-1000-8000-0026BB765291", + "perms": ["pr", "pw"], + "value": "AQA=", + "format": "tlv8" + }, + { + "iid": 18, + "perms": ["pr", "pw"], + "type": "00000118-0000-1000-8000-0026BB765291", + "format": "tlv8", + "value": "ARDpyds+onxNHb4xI0H6deS3AgEAAxgBAQACCzEwLjEwLjYwLjExAwJRwwQCUsMENQEBAQIgyWEU3zQuPNAsFAm1DM3ZSdp0Vh7kGuVQ+vqtS5Qa09YDDr5ebeow7eweCsu3FYh/BTUBAQECIFPPdRRI86ozZNB/WU/e8Em4N1lSsJhttOWoJly3XNEMAw7Zm8TgFdAof+wvoCQTYgYE/r0D9wcEC1Pwjg==" + } + ], + "iid": 13, + "hidden": false + }, + { + "iid": 20, + "characteristics": [ + { + "iid": 21, + "type": "00000120-0000-1000-8000-0026BB765291", + "perms": ["pr", "ev"], + "value": "AQEA", + "format": "tlv8" + }, + { + "format": "tlv8", + "value": "AVUBAQACFgEBAQAAAQECAgEAAAACAQIDAQAEAQADCwECgAcCAjgEAwEeAAADCwECAAUCAtACAwEeAAADCwECgAICAmgBAwEeAAADCwECQAECAvAAAwEe", + "perms": ["pr"], + "type": "00000114-0000-1000-8000-0026BB765291", + "iid": 22 + }, + { + "format": "tlv8", + "value": "ARMBAQMCDgEBAQIBAQAAAgEAAwEBAgEA", + "iid": 23, + "perms": ["pr"], + "type": "00000115-0000-1000-8000-0026BB765291" + }, + { + "format": "tlv8", + "value": "AgECAAACAQAAAAIBAQ==", + "perms": ["pr"], + "type": "00000116-0000-1000-8000-0026BB765291", + "iid": 24 + }, + { + "format": "tlv8", + "value": "AQA=", + "perms": ["pr", "pw"], + "type": "00000117-0000-1000-8000-0026BB765291", + "iid": 26 + }, + { + "iid": 25, + "type": "00000118-0000-1000-8000-0026BB765291", + "perms": ["pr", "pw"], + "value": "ARDu4+F49fZMSatQjcfR8FGVAgEAAxgBAQACCzEwLjEwLjYwLjExAwJbwwQCXMMENQEBAQIg9nqVm+80ccYh/S3vKKfbcUGH7VgggHRwp1e1x63+kpkDDnAxnJxfEz8KDp6xKoPhBTUBAQECILLYad+aKdzVbhGz55ywh0RYX9DTyY7HdSRf8y8tUi1kAw4DRngrGhYBdnrjELUzGgYEf+ysuwcESU05wg==", + "format": "tlv8" + } + ], + "primary": false, + "type": "00000110-0000-1000-8000-0026BB765291", + "hidden": false + } + ] + } ] diff --git a/tests/components/homekit_controller/fixtures/rainmachine-pro-8.json b/tests/components/homekit_controller/fixtures/rainmachine-pro-8.json index 1b50063006e..c841c4feec5 100644 --- a/tests/components/homekit_controller/fixtures/rainmachine-pro-8.json +++ b/tests/components/homekit_controller/fixtures/rainmachine-pro-8.json @@ -1,1137 +1,897 @@ [ - { - "aid": 1, - "services": [ - { - "iid": 1, - "type": "0000003E-0000-1000-8000-0026BB765291", - "primary": false, - "hidden": false, - "linked": [], - "characteristics": [ - { - "iid": 2, - "type": "00000014-0000-1000-8000-0026BB765291", - "format": "bool", - "perms": [ - "pw" - ] - }, - { - "iid": 3, - "type": "00000020-0000-1000-8000-0026BB765291", - "format": "string", - "value": "Green Electronics LLC", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 4, - "type": "00000021-0000-1000-8000-0026BB765291", - "format": "string", - "value": "SPK5 Pro", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 5, - "type": "00000023-0000-1000-8000-0026BB765291", - "format": "string", - "value": "RainMachine-00ce4a", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 6, - "type": "00000030-0000-1000-8000-0026BB765291", - "format": "string", - "value": "00aa0000aa0a", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 7, - "type": "00000052-0000-1000-8000-0026BB765291", - "format": "string", - "value": "1.0.4", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 8, - "type": "00000053-0000-1000-8000-0026BB765291", - "format": "string", - "value": "1", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 9, - "type": "34AB8811-AC7F-4340-BAC3-FD6A85F9943B", - "format": "string", - "value": "2.0;16A62", - "perms": [ - "pr", - "hd" - ], - "ev": false - } - ] - }, - { - "iid": 16, - "type": "000000A2-0000-1000-8000-0026BB765291", - "primary": false, - "hidden": false, - "linked": [], - "characteristics": [ - { - "iid": 18, - "type": "00000037-0000-1000-8000-0026BB765291", - "format": "string", - "value": "1.1.0", - "perms": [ - "pr" - ], - "ev": false - } - ] - }, - { - "iid": 64, - "type": "000000CF-0000-1000-8000-0026BB765291", - "primary": true, - "hidden": false, - "linked": [ - 512, - 768, - 1024, - 1280, - 1536, - 1792, - 2048, - 2304 - ], - "characteristics": [ - { - "iid": 67, - "type": "000000B0-0000-1000-8000-0026BB765291", - "format": "uint8", - "value": 0, - "perms": [ - "pr", - "pw", - "ev" - ], - "ev": false, - "minValue": 0, - "maxValue": 1, - "minStep": 1 - }, - { - "iid": 65, - "type": "000000D1-0000-1000-8000-0026BB765291", - "format": "uint8", - "value": 0, - "perms": [ - "pr", - "ev" - ], - "ev": false, - "minValue": 0, - "maxValue": 2, - "minStep": 1 - }, - { - "iid": 68, - "type": "000000D2-0000-1000-8000-0026BB765291", - "format": "uint8", - "value": 0, - "perms": [ - "pr", - "ev" - ], - "ev": false, - "minValue": 0, - "maxValue": 1, - "minStep": 1 - }, - { - "iid": 66, - "type": "00000023-0000-1000-8000-0026BB765291", - "format": "string", - "value": "RainMachine", - "perms": [ - "pr" - ], - "ev": false - } - ] - }, - { - "iid": 512, - "type": "000000D0-0000-1000-8000-0026BB765291", - "primary": false, - "hidden": false, - "linked": [], - "characteristics": [ - { - "iid": 544, - "type": "000000B0-0000-1000-8000-0026BB765291", - "format": "uint8", - "value": 0, - "perms": [ - "pr", - "pw", - "ev" - ], - "ev": false, - "minValue": 0, - "maxValue": 1, - "minStep": 1 - }, - { - "iid": 560, - "type": "000000D2-0000-1000-8000-0026BB765291", - "format": "uint8", - "value": 0, - "perms": [ - "pr", - "ev" - ], - "ev": false, - "minValue": 0, - "maxValue": 1, - "minStep": 1 - }, - { - "iid": 576, - "type": "000000D5-0000-1000-8000-0026BB765291", - "format": "uint8", - "value": 1, - "perms": [ - "pr", - "ev" - ], - "ev": false, - "minValue": 0, - "maxValue": 3, - "minStep": 1 - }, - { - "iid": 592, - "type": "000000D6-0000-1000-8000-0026BB765291", - "format": "uint8", - "value": 1, - "perms": [ - "pr", - "pw", - "ev" - ], - "ev": false, - "minValue": 0, - "maxValue": 1, - "minStep": 1 - }, - { - "iid": 608, - "type": "000000D4-0000-1000-8000-0026BB765291", - "format": "uint32", - "value": 0, - "perms": [ - "pr", - "ev" - ], - "ev": false, - "minValue": 0, - "maxValue": 86400, - "minStep": 1 - }, - { - "iid": 624, - "type": "000000D3-0000-1000-8000-0026BB765291", - "format": "uint32", - "value": 300, - "perms": [ - "pr", - "pw", - "ev" - ], - "ev": false, - "minValue": 0, - "maxValue": 86400, - "minStep": 1 - }, - { - "iid": 640, - "type": "000000CB-0000-1000-8000-0026BB765291", - "format": "uint8", - "value": 1, - "perms": [ - "pr" - ], - "ev": false, - "minValue": 1, - "maxValue": 255, - "minStep": 1 - }, - { - "iid": 528, - "type": "00000023-0000-1000-8000-0026BB765291", - "format": "string", - "value": "", - "perms": [ - "pr" - ], - "ev": false - } - ] - }, - { - "iid": 768, - "type": "000000D0-0000-1000-8000-0026BB765291", - "primary": false, - "hidden": false, - "linked": [], - "characteristics": [ - { - "iid": 800, - "type": "000000B0-0000-1000-8000-0026BB765291", - "format": "uint8", - "value": 0, - "perms": [ - "pr", - "pw", - "ev" - ], - "ev": false, - "minValue": 0, - "maxValue": 1, - "minStep": 1 - }, - { - "iid": 816, - "type": "000000D2-0000-1000-8000-0026BB765291", - "format": "uint8", - "value": 0, - "perms": [ - "pr", - "ev" - ], - "ev": false, - "minValue": 0, - "maxValue": 1, - "minStep": 1 - }, - { - "iid": 832, - "type": "000000D5-0000-1000-8000-0026BB765291", - "format": "uint8", - "value": 1, - "perms": [ - "pr", - "ev" - ], - "ev": false, - "minValue": 0, - "maxValue": 3, - "minStep": 1 - }, - { - "iid": 848, - "type": "000000D6-0000-1000-8000-0026BB765291", - "format": "uint8", - "value": 1, - "perms": [ - "pr", - "pw", - "ev" - ], - "ev": false, - "minValue": 0, - "maxValue": 1, - "minStep": 1 - }, - { - "iid": 864, - "type": "000000D4-0000-1000-8000-0026BB765291", - "format": "uint32", - "value": 0, - "perms": [ - "pr", - "ev" - ], - "ev": false, - "minValue": 0, - "maxValue": 86400, - "minStep": 1 - }, - { - "iid": 880, - "type": "000000D3-0000-1000-8000-0026BB765291", - "format": "uint32", - "value": 300, - "perms": [ - "pr", - "pw", - "ev" - ], - "ev": false, - "minValue": 0, - "maxValue": 86400, - "minStep": 1 - }, - { - "iid": 896, - "type": "000000CB-0000-1000-8000-0026BB765291", - "format": "uint8", - "value": 2, - "perms": [ - "pr" - ], - "ev": false, - "minValue": 1, - "maxValue": 255, - "minStep": 1 - }, - { - "iid": 784, - "type": "00000023-0000-1000-8000-0026BB765291", - "format": "string", - "value": "", - "perms": [ - "pr" - ], - "ev": false - } - ] - }, - { - "iid": 1024, - "type": "000000D0-0000-1000-8000-0026BB765291", - "primary": false, - "hidden": false, - "linked": [], - "characteristics": [ - { - "iid": 1056, - "type": "000000B0-0000-1000-8000-0026BB765291", - "format": "uint8", - "value": 0, - "perms": [ - "pr", - "pw", - "ev" - ], - "ev": false, - "minValue": 0, - "maxValue": 1, - "minStep": 1 - }, - { - "iid": 1072, - "type": "000000D2-0000-1000-8000-0026BB765291", - "format": "uint8", - "value": 0, - "perms": [ - "pr", - "ev" - ], - "ev": false, - "minValue": 0, - "maxValue": 1, - "minStep": 1 - }, - { - "iid": 1088, - "type": "000000D5-0000-1000-8000-0026BB765291", - "format": "uint8", - "value": 1, - "perms": [ - "pr", - "ev" - ], - "ev": false, - "minValue": 0, - "maxValue": 3, - "minStep": 1 - }, - { - "iid": 1104, - "type": "000000D6-0000-1000-8000-0026BB765291", - "format": "uint8", - "value": 1, - "perms": [ - "pr", - "pw", - "ev" - ], - "ev": false, - "minValue": 0, - "maxValue": 1, - "minStep": 1 - }, - { - "iid": 1120, - "type": "000000D4-0000-1000-8000-0026BB765291", - "format": "uint32", - "value": 0, - "perms": [ - "pr", - "ev" - ], - "ev": false, - "minValue": 0, - "maxValue": 86400, - "minStep": 1 - }, - { - "iid": 1136, - "type": "000000D3-0000-1000-8000-0026BB765291", - "format": "uint32", - "value": 300, - "perms": [ - "pr", - "pw", - "ev" - ], - "ev": false, - "minValue": 0, - "maxValue": 86400, - "minStep": 1 - }, - { - "iid": 1152, - "type": "000000CB-0000-1000-8000-0026BB765291", - "format": "uint8", - "value": 3, - "perms": [ - "pr" - ], - "ev": false, - "minValue": 1, - "maxValue": 255, - "minStep": 1 - }, - { - "iid": 1040, - "type": "00000023-0000-1000-8000-0026BB765291", - "format": "string", - "value": "", - "perms": [ - "pr" - ], - "ev": false - } - ] - }, - { - "iid": 1280, - "type": "000000D0-0000-1000-8000-0026BB765291", - "primary": false, - "hidden": false, - "linked": [], - "characteristics": [ - { - "iid": 1312, - "type": "000000B0-0000-1000-8000-0026BB765291", - "format": "uint8", - "value": 0, - "perms": [ - "pr", - "pw", - "ev" - ], - "ev": false, - "minValue": 0, - "maxValue": 1, - "minStep": 1 - }, - { - "iid": 1328, - "type": "000000D2-0000-1000-8000-0026BB765291", - "format": "uint8", - "value": 0, - "perms": [ - "pr", - "ev" - ], - "ev": false, - "minValue": 0, - "maxValue": 1, - "minStep": 1 - }, - { - "iid": 1344, - "type": "000000D5-0000-1000-8000-0026BB765291", - "format": "uint8", - "value": 1, - "perms": [ - "pr", - "ev" - ], - "ev": false, - "minValue": 0, - "maxValue": 3, - "minStep": 1 - }, - { - "iid": 1360, - "type": "000000D6-0000-1000-8000-0026BB765291", - "format": "uint8", - "value": 1, - "perms": [ - "pr", - "pw", - "ev" - ], - "ev": false, - "minValue": 0, - "maxValue": 1, - "minStep": 1 - }, - { - "iid": 1376, - "type": "000000D4-0000-1000-8000-0026BB765291", - "format": "uint32", - "value": 0, - "perms": [ - "pr", - "ev" - ], - "ev": false, - "minValue": 0, - "maxValue": 86400, - "minStep": 1 - }, - { - "iid": 1392, - "type": "000000D3-0000-1000-8000-0026BB765291", - "format": "uint32", - "value": 300, - "perms": [ - "pr", - "pw", - "ev" - ], - "ev": false, - "minValue": 0, - "maxValue": 86400, - "minStep": 1 - }, - { - "iid": 1408, - "type": "000000CB-0000-1000-8000-0026BB765291", - "format": "uint8", - "value": 4, - "perms": [ - "pr" - ], - "ev": false, - "minValue": 1, - "maxValue": 255, - "minStep": 1 - }, - { - "iid": 1296, - "type": "00000023-0000-1000-8000-0026BB765291", - "format": "string", - "value": "", - "perms": [ - "pr" - ], - "ev": false - } - ] - }, - { - "iid": 1536, - "type": "000000D0-0000-1000-8000-0026BB765291", - "primary": false, - "hidden": false, - "linked": [], - "characteristics": [ - { - "iid": 1568, - "type": "000000B0-0000-1000-8000-0026BB765291", - "format": "uint8", - "value": 0, - "perms": [ - "pr", - "pw", - "ev" - ], - "ev": false, - "minValue": 0, - "maxValue": 1, - "minStep": 1 - }, - { - "iid": 1584, - "type": "000000D2-0000-1000-8000-0026BB765291", - "format": "uint8", - "value": 0, - "perms": [ - "pr", - "ev" - ], - "ev": false, - "minValue": 0, - "maxValue": 1, - "minStep": 1 - }, - { - "iid": 1600, - "type": "000000D5-0000-1000-8000-0026BB765291", - "format": "uint8", - "value": 1, - "perms": [ - "pr", - "ev" - ], - "ev": false, - "minValue": 0, - "maxValue": 3, - "minStep": 1 - }, - { - "iid": 1616, - "type": "000000D6-0000-1000-8000-0026BB765291", - "format": "uint8", - "value": 1, - "perms": [ - "pr", - "pw", - "ev" - ], - "ev": false, - "minValue": 0, - "maxValue": 1, - "minStep": 1 - }, - { - "iid": 1632, - "type": "000000D4-0000-1000-8000-0026BB765291", - "format": "uint32", - "value": 0, - "perms": [ - "pr", - "ev" - ], - "ev": false, - "minValue": 0, - "maxValue": 86400, - "minStep": 1 - }, - { - "iid": 1648, - "type": "000000D3-0000-1000-8000-0026BB765291", - "format": "uint32", - "value": 300, - "perms": [ - "pr", - "pw", - "ev" - ], - "ev": false, - "minValue": 0, - "maxValue": 86400, - "minStep": 1 - }, - { - "iid": 1664, - "type": "000000CB-0000-1000-8000-0026BB765291", - "format": "uint8", - "value": 5, - "perms": [ - "pr" - ], - "ev": false, - "minValue": 1, - "maxValue": 255, - "minStep": 1 - }, - { - "iid": 1552, - "type": "00000023-0000-1000-8000-0026BB765291", - "format": "string", - "value": "", - "perms": [ - "pr" - ], - "ev": false - } - ] - }, - { - "iid": 1792, - "type": "000000D0-0000-1000-8000-0026BB765291", - "primary": false, - "hidden": false, - "linked": [], - "characteristics": [ - { - "iid": 1824, - "type": "000000B0-0000-1000-8000-0026BB765291", - "format": "uint8", - "value": 0, - "perms": [ - "pr", - "pw", - "ev" - ], - "ev": false, - "minValue": 0, - "maxValue": 1, - "minStep": 1 - }, - { - "iid": 1840, - "type": "000000D2-0000-1000-8000-0026BB765291", - "format": "uint8", - "value": 0, - "perms": [ - "pr", - "ev" - ], - "ev": false, - "minValue": 0, - "maxValue": 1, - "minStep": 1 - }, - { - "iid": 1856, - "type": "000000D5-0000-1000-8000-0026BB765291", - "format": "uint8", - "value": 1, - "perms": [ - "pr", - "ev" - ], - "ev": false, - "minValue": 0, - "maxValue": 3, - "minStep": 1 - }, - { - "iid": 1872, - "type": "000000D6-0000-1000-8000-0026BB765291", - "format": "uint8", - "value": 1, - "perms": [ - "pr", - "pw", - "ev" - ], - "ev": false, - "minValue": 0, - "maxValue": 1, - "minStep": 1 - }, - { - "iid": 1888, - "type": "000000D4-0000-1000-8000-0026BB765291", - "format": "uint32", - "value": 0, - "perms": [ - "pr", - "ev" - ], - "ev": false, - "minValue": 0, - "maxValue": 86400, - "minStep": 1 - }, - { - "iid": 1904, - "type": "000000D3-0000-1000-8000-0026BB765291", - "format": "uint32", - "value": 300, - "perms": [ - "pr", - "pw", - "ev" - ], - "ev": false, - "minValue": 0, - "maxValue": 86400, - "minStep": 1 - }, - { - "iid": 1920, - "type": "000000CB-0000-1000-8000-0026BB765291", - "format": "uint8", - "value": 6, - "perms": [ - "pr" - ], - "ev": false, - "minValue": 1, - "maxValue": 255, - "minStep": 1 - }, - { - "iid": 1808, - "type": "00000023-0000-1000-8000-0026BB765291", - "format": "string", - "value": "", - "perms": [ - "pr" - ], - "ev": false - } - ] - }, - { - "iid": 2048, - "type": "000000D0-0000-1000-8000-0026BB765291", - "primary": false, - "hidden": false, - "linked": [], - "characteristics": [ - { - "iid": 2080, - "type": "000000B0-0000-1000-8000-0026BB765291", - "format": "uint8", - "value": 0, - "perms": [ - "pr", - "pw", - "ev" - ], - "ev": false, - "minValue": 0, - "maxValue": 1, - "minStep": 1 - }, - { - "iid": 2096, - "type": "000000D2-0000-1000-8000-0026BB765291", - "format": "uint8", - "value": 0, - "perms": [ - "pr", - "ev" - ], - "ev": false, - "minValue": 0, - "maxValue": 1, - "minStep": 1 - }, - { - "iid": 2112, - "type": "000000D5-0000-1000-8000-0026BB765291", - "format": "uint8", - "value": 1, - "perms": [ - "pr", - "ev" - ], - "ev": false, - "minValue": 0, - "maxValue": 3, - "minStep": 1 - }, - { - "iid": 2128, - "type": "000000D6-0000-1000-8000-0026BB765291", - "format": "uint8", - "value": 1, - "perms": [ - "pr", - "pw", - "ev" - ], - "ev": false, - "minValue": 0, - "maxValue": 1, - "minStep": 1 - }, - { - "iid": 2144, - "type": "000000D4-0000-1000-8000-0026BB765291", - "format": "uint32", - "value": 0, - "perms": [ - "pr", - "ev" - ], - "ev": false, - "minValue": 0, - "maxValue": 86400, - "minStep": 1 - }, - { - "iid": 2160, - "type": "000000D3-0000-1000-8000-0026BB765291", - "format": "uint32", - "value": 300, - "perms": [ - "pr", - "pw", - "ev" - ], - "ev": false, - "minValue": 0, - "maxValue": 86400, - "minStep": 1 - }, - { - "iid": 2176, - "type": "000000CB-0000-1000-8000-0026BB765291", - "format": "uint8", - "value": 7, - "perms": [ - "pr" - ], - "ev": false, - "minValue": 1, - "maxValue": 255, - "minStep": 1 - }, - { - "iid": 2064, - "type": "00000023-0000-1000-8000-0026BB765291", - "format": "string", - "value": "", - "perms": [ - "pr" - ], - "ev": false - } - ] - }, - { - "iid": 2304, - "type": "000000D0-0000-1000-8000-0026BB765291", - "primary": false, - "hidden": false, - "linked": [], - "characteristics": [ - { - "iid": 2336, - "type": "000000B0-0000-1000-8000-0026BB765291", - "format": "uint8", - "value": 0, - "perms": [ - "pr", - "pw", - "ev" - ], - "ev": false, - "minValue": 0, - "maxValue": 1, - "minStep": 1 - }, - { - "iid": 2352, - "type": "000000D2-0000-1000-8000-0026BB765291", - "format": "uint8", - "value": 0, - "perms": [ - "pr", - "ev" - ], - "ev": false, - "minValue": 0, - "maxValue": 1, - "minStep": 1 - }, - { - "iid": 2368, - "type": "000000D5-0000-1000-8000-0026BB765291", - "format": "uint8", - "value": 1, - "perms": [ - "pr", - "ev" - ], - "ev": false, - "minValue": 0, - "maxValue": 3, - "minStep": 1 - }, - { - "iid": 2384, - "type": "000000D6-0000-1000-8000-0026BB765291", - "format": "uint8", - "value": 1, - "perms": [ - "pr", - "pw", - "ev" - ], - "ev": false, - "minValue": 0, - "maxValue": 1, - "minStep": 1 - }, - { - "iid": 2400, - "type": "000000D4-0000-1000-8000-0026BB765291", - "format": "uint32", - "value": 0, - "perms": [ - "pr", - "ev" - ], - "ev": false, - "minValue": 0, - "maxValue": 86400, - "minStep": 1 - }, - { - "iid": 2416, - "type": "000000D3-0000-1000-8000-0026BB765291", - "format": "uint32", - "value": 300, - "perms": [ - "pr", - "pw", - "ev" - ], - "ev": false, - "minValue": 0, - "maxValue": 86400, - "minStep": 1 - }, - { - "iid": 2432, - "type": "000000CB-0000-1000-8000-0026BB765291", - "format": "uint8", - "value": 8, - "perms": [ - "pr" - ], - "ev": false, - "minValue": 1, - "maxValue": 255, - "minStep": 1 - }, - { - "iid": 2320, - "type": "00000023-0000-1000-8000-0026BB765291", - "format": "string", - "value": "", - "perms": [ - "pr" - ], - "ev": false - } - ] - } + { + "aid": 1, + "services": [ + { + "iid": 1, + "type": "0000003E-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 2, + "type": "00000014-0000-1000-8000-0026BB765291", + "format": "bool", + "perms": ["pw"] + }, + { + "iid": 3, + "type": "00000020-0000-1000-8000-0026BB765291", + "format": "string", + "value": "Green Electronics LLC", + "perms": ["pr"], + "ev": false + }, + { + "iid": 4, + "type": "00000021-0000-1000-8000-0026BB765291", + "format": "string", + "value": "SPK5 Pro", + "perms": ["pr"], + "ev": false + }, + { + "iid": 5, + "type": "00000023-0000-1000-8000-0026BB765291", + "format": "string", + "value": "RainMachine-00ce4a", + "perms": ["pr"], + "ev": false + }, + { + "iid": 6, + "type": "00000030-0000-1000-8000-0026BB765291", + "format": "string", + "value": "00aa0000aa0a", + "perms": ["pr"], + "ev": false + }, + { + "iid": 7, + "type": "00000052-0000-1000-8000-0026BB765291", + "format": "string", + "value": "1.0.4", + "perms": ["pr"], + "ev": false + }, + { + "iid": 8, + "type": "00000053-0000-1000-8000-0026BB765291", + "format": "string", + "value": "1", + "perms": ["pr"], + "ev": false + }, + { + "iid": 9, + "type": "34AB8811-AC7F-4340-BAC3-FD6A85F9943B", + "format": "string", + "value": "2.0;16A62", + "perms": ["pr", "hd"], + "ev": false + } ] - } -] \ No newline at end of file + }, + { + "iid": 16, + "type": "000000A2-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 18, + "type": "00000037-0000-1000-8000-0026BB765291", + "format": "string", + "value": "1.1.0", + "perms": ["pr"], + "ev": false + } + ] + }, + { + "iid": 64, + "type": "000000CF-0000-1000-8000-0026BB765291", + "primary": true, + "hidden": false, + "linked": [512, 768, 1024, 1280, 1536, 1792, 2048, 2304], + "characteristics": [ + { + "iid": 67, + "type": "000000B0-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 0, + "perms": ["pr", "pw", "ev"], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 65, + "type": "000000D1-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 0, + "perms": ["pr", "ev"], + "ev": false, + "minValue": 0, + "maxValue": 2, + "minStep": 1 + }, + { + "iid": 68, + "type": "000000D2-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 0, + "perms": ["pr", "ev"], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 66, + "type": "00000023-0000-1000-8000-0026BB765291", + "format": "string", + "value": "RainMachine", + "perms": ["pr"], + "ev": false + } + ] + }, + { + "iid": 512, + "type": "000000D0-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 544, + "type": "000000B0-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 0, + "perms": ["pr", "pw", "ev"], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 560, + "type": "000000D2-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 0, + "perms": ["pr", "ev"], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 576, + "type": "000000D5-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 1, + "perms": ["pr", "ev"], + "ev": false, + "minValue": 0, + "maxValue": 3, + "minStep": 1 + }, + { + "iid": 592, + "type": "000000D6-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 1, + "perms": ["pr", "pw", "ev"], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 608, + "type": "000000D4-0000-1000-8000-0026BB765291", + "format": "uint32", + "value": 0, + "perms": ["pr", "ev"], + "ev": false, + "minValue": 0, + "maxValue": 86400, + "minStep": 1 + }, + { + "iid": 624, + "type": "000000D3-0000-1000-8000-0026BB765291", + "format": "uint32", + "value": 300, + "perms": ["pr", "pw", "ev"], + "ev": false, + "minValue": 0, + "maxValue": 86400, + "minStep": 1 + }, + { + "iid": 640, + "type": "000000CB-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 1, + "perms": ["pr"], + "ev": false, + "minValue": 1, + "maxValue": 255, + "minStep": 1 + }, + { + "iid": 528, + "type": "00000023-0000-1000-8000-0026BB765291", + "format": "string", + "value": "", + "perms": ["pr"], + "ev": false + } + ] + }, + { + "iid": 768, + "type": "000000D0-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 800, + "type": "000000B0-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 0, + "perms": ["pr", "pw", "ev"], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 816, + "type": "000000D2-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 0, + "perms": ["pr", "ev"], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 832, + "type": "000000D5-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 1, + "perms": ["pr", "ev"], + "ev": false, + "minValue": 0, + "maxValue": 3, + "minStep": 1 + }, + { + "iid": 848, + "type": "000000D6-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 1, + "perms": ["pr", "pw", "ev"], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 864, + "type": "000000D4-0000-1000-8000-0026BB765291", + "format": "uint32", + "value": 0, + "perms": ["pr", "ev"], + "ev": false, + "minValue": 0, + "maxValue": 86400, + "minStep": 1 + }, + { + "iid": 880, + "type": "000000D3-0000-1000-8000-0026BB765291", + "format": "uint32", + "value": 300, + "perms": ["pr", "pw", "ev"], + "ev": false, + "minValue": 0, + "maxValue": 86400, + "minStep": 1 + }, + { + "iid": 896, + "type": "000000CB-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 2, + "perms": ["pr"], + "ev": false, + "minValue": 1, + "maxValue": 255, + "minStep": 1 + }, + { + "iid": 784, + "type": "00000023-0000-1000-8000-0026BB765291", + "format": "string", + "value": "", + "perms": ["pr"], + "ev": false + } + ] + }, + { + "iid": 1024, + "type": "000000D0-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 1056, + "type": "000000B0-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 0, + "perms": ["pr", "pw", "ev"], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 1072, + "type": "000000D2-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 0, + "perms": ["pr", "ev"], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 1088, + "type": "000000D5-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 1, + "perms": ["pr", "ev"], + "ev": false, + "minValue": 0, + "maxValue": 3, + "minStep": 1 + }, + { + "iid": 1104, + "type": "000000D6-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 1, + "perms": ["pr", "pw", "ev"], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 1120, + "type": "000000D4-0000-1000-8000-0026BB765291", + "format": "uint32", + "value": 0, + "perms": ["pr", "ev"], + "ev": false, + "minValue": 0, + "maxValue": 86400, + "minStep": 1 + }, + { + "iid": 1136, + "type": "000000D3-0000-1000-8000-0026BB765291", + "format": "uint32", + "value": 300, + "perms": ["pr", "pw", "ev"], + "ev": false, + "minValue": 0, + "maxValue": 86400, + "minStep": 1 + }, + { + "iid": 1152, + "type": "000000CB-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 3, + "perms": ["pr"], + "ev": false, + "minValue": 1, + "maxValue": 255, + "minStep": 1 + }, + { + "iid": 1040, + "type": "00000023-0000-1000-8000-0026BB765291", + "format": "string", + "value": "", + "perms": ["pr"], + "ev": false + } + ] + }, + { + "iid": 1280, + "type": "000000D0-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 1312, + "type": "000000B0-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 0, + "perms": ["pr", "pw", "ev"], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 1328, + "type": "000000D2-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 0, + "perms": ["pr", "ev"], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 1344, + "type": "000000D5-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 1, + "perms": ["pr", "ev"], + "ev": false, + "minValue": 0, + "maxValue": 3, + "minStep": 1 + }, + { + "iid": 1360, + "type": "000000D6-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 1, + "perms": ["pr", "pw", "ev"], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 1376, + "type": "000000D4-0000-1000-8000-0026BB765291", + "format": "uint32", + "value": 0, + "perms": ["pr", "ev"], + "ev": false, + "minValue": 0, + "maxValue": 86400, + "minStep": 1 + }, + { + "iid": 1392, + "type": "000000D3-0000-1000-8000-0026BB765291", + "format": "uint32", + "value": 300, + "perms": ["pr", "pw", "ev"], + "ev": false, + "minValue": 0, + "maxValue": 86400, + "minStep": 1 + }, + { + "iid": 1408, + "type": "000000CB-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 4, + "perms": ["pr"], + "ev": false, + "minValue": 1, + "maxValue": 255, + "minStep": 1 + }, + { + "iid": 1296, + "type": "00000023-0000-1000-8000-0026BB765291", + "format": "string", + "value": "", + "perms": ["pr"], + "ev": false + } + ] + }, + { + "iid": 1536, + "type": "000000D0-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 1568, + "type": "000000B0-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 0, + "perms": ["pr", "pw", "ev"], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 1584, + "type": "000000D2-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 0, + "perms": ["pr", "ev"], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 1600, + "type": "000000D5-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 1, + "perms": ["pr", "ev"], + "ev": false, + "minValue": 0, + "maxValue": 3, + "minStep": 1 + }, + { + "iid": 1616, + "type": "000000D6-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 1, + "perms": ["pr", "pw", "ev"], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 1632, + "type": "000000D4-0000-1000-8000-0026BB765291", + "format": "uint32", + "value": 0, + "perms": ["pr", "ev"], + "ev": false, + "minValue": 0, + "maxValue": 86400, + "minStep": 1 + }, + { + "iid": 1648, + "type": "000000D3-0000-1000-8000-0026BB765291", + "format": "uint32", + "value": 300, + "perms": ["pr", "pw", "ev"], + "ev": false, + "minValue": 0, + "maxValue": 86400, + "minStep": 1 + }, + { + "iid": 1664, + "type": "000000CB-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 5, + "perms": ["pr"], + "ev": false, + "minValue": 1, + "maxValue": 255, + "minStep": 1 + }, + { + "iid": 1552, + "type": "00000023-0000-1000-8000-0026BB765291", + "format": "string", + "value": "", + "perms": ["pr"], + "ev": false + } + ] + }, + { + "iid": 1792, + "type": "000000D0-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 1824, + "type": "000000B0-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 0, + "perms": ["pr", "pw", "ev"], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 1840, + "type": "000000D2-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 0, + "perms": ["pr", "ev"], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 1856, + "type": "000000D5-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 1, + "perms": ["pr", "ev"], + "ev": false, + "minValue": 0, + "maxValue": 3, + "minStep": 1 + }, + { + "iid": 1872, + "type": "000000D6-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 1, + "perms": ["pr", "pw", "ev"], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 1888, + "type": "000000D4-0000-1000-8000-0026BB765291", + "format": "uint32", + "value": 0, + "perms": ["pr", "ev"], + "ev": false, + "minValue": 0, + "maxValue": 86400, + "minStep": 1 + }, + { + "iid": 1904, + "type": "000000D3-0000-1000-8000-0026BB765291", + "format": "uint32", + "value": 300, + "perms": ["pr", "pw", "ev"], + "ev": false, + "minValue": 0, + "maxValue": 86400, + "minStep": 1 + }, + { + "iid": 1920, + "type": "000000CB-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 6, + "perms": ["pr"], + "ev": false, + "minValue": 1, + "maxValue": 255, + "minStep": 1 + }, + { + "iid": 1808, + "type": "00000023-0000-1000-8000-0026BB765291", + "format": "string", + "value": "", + "perms": ["pr"], + "ev": false + } + ] + }, + { + "iid": 2048, + "type": "000000D0-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 2080, + "type": "000000B0-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 0, + "perms": ["pr", "pw", "ev"], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 2096, + "type": "000000D2-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 0, + "perms": ["pr", "ev"], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 2112, + "type": "000000D5-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 1, + "perms": ["pr", "ev"], + "ev": false, + "minValue": 0, + "maxValue": 3, + "minStep": 1 + }, + { + "iid": 2128, + "type": "000000D6-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 1, + "perms": ["pr", "pw", "ev"], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 2144, + "type": "000000D4-0000-1000-8000-0026BB765291", + "format": "uint32", + "value": 0, + "perms": ["pr", "ev"], + "ev": false, + "minValue": 0, + "maxValue": 86400, + "minStep": 1 + }, + { + "iid": 2160, + "type": "000000D3-0000-1000-8000-0026BB765291", + "format": "uint32", + "value": 300, + "perms": ["pr", "pw", "ev"], + "ev": false, + "minValue": 0, + "maxValue": 86400, + "minStep": 1 + }, + { + "iid": 2176, + "type": "000000CB-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 7, + "perms": ["pr"], + "ev": false, + "minValue": 1, + "maxValue": 255, + "minStep": 1 + }, + { + "iid": 2064, + "type": "00000023-0000-1000-8000-0026BB765291", + "format": "string", + "value": "", + "perms": ["pr"], + "ev": false + } + ] + }, + { + "iid": 2304, + "type": "000000D0-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 2336, + "type": "000000B0-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 0, + "perms": ["pr", "pw", "ev"], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 2352, + "type": "000000D2-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 0, + "perms": ["pr", "ev"], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 2368, + "type": "000000D5-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 1, + "perms": ["pr", "ev"], + "ev": false, + "minValue": 0, + "maxValue": 3, + "minStep": 1 + }, + { + "iid": 2384, + "type": "000000D6-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 1, + "perms": ["pr", "pw", "ev"], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 2400, + "type": "000000D4-0000-1000-8000-0026BB765291", + "format": "uint32", + "value": 0, + "perms": ["pr", "ev"], + "ev": false, + "minValue": 0, + "maxValue": 86400, + "minStep": 1 + }, + { + "iid": 2416, + "type": "000000D3-0000-1000-8000-0026BB765291", + "format": "uint32", + "value": 300, + "perms": ["pr", "pw", "ev"], + "ev": false, + "minValue": 0, + "maxValue": 86400, + "minStep": 1 + }, + { + "iid": 2432, + "type": "000000CB-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 8, + "perms": ["pr"], + "ev": false, + "minValue": 1, + "maxValue": 255, + "minStep": 1 + }, + { + "iid": 2320, + "type": "00000023-0000-1000-8000-0026BB765291", + "format": "string", + "value": "", + "perms": ["pr"], + "ev": false + } + ] + } + ] + } +] diff --git a/tests/components/homekit_controller/fixtures/ryse_smart_bridge.json b/tests/components/homekit_controller/fixtures/ryse_smart_bridge.json index 6f6f818e5e2..f6a63c7079c 100644 --- a/tests/components/homekit_controller/fixtures/ryse_smart_bridge.json +++ b/tests/components/homekit_controller/fixtures/ryse_smart_bridge.json @@ -1,596 +1,484 @@ [ - { - "aid": 1, - "services": [ - { - "iid": 1, - "type": "0000003E-0000-1000-8000-0026BB765291", - "primary": false, - "hidden": false, - "linked": [], - "characteristics": [ - { - "iid": 2, - "type": "00000014-0000-1000-8000-0026BB765291", - "format": "bool", - "perms": [ - "pw" - ] - }, - { - "iid": 3, - "type": "00000020-0000-1000-8000-0026BB765291", - "format": "string", - "value": "RYSE Inc.", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 4, - "type": "00000021-0000-1000-8000-0026BB765291", - "format": "string", - "value": "RYSE SmartBridge", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 5, - "type": "00000023-0000-1000-8000-0026BB765291", - "format": "string", - "value": "RYSE SmartBridge", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 6, - "type": "00000030-0000-1000-8000-0026BB765291", - "format": "string", - "value": "0101.3521.0436", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 7, - "type": "00000052-0000-1000-8000-0026BB765291", - "format": "string", - "value": "1.3.0", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 8, - "type": "00000053-0000-1000-8000-0026BB765291", - "format": "string", - "value": "0101.3521.0436", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 9, - "type": "34AB8811-AC7F-4340-BAC3-FD6A85F9943B", - "format": "string", - "value": "4.1;3fac0fb4", - "perms": [ - "pr", - "hd" - ], - "ev": false - }, - { - "iid": 10, - "type": "220", - "format": "data", - "value": "Yhl9CmseEb8=", - "perms": [ - "pr", - "hd" - ], - "ev": false, - "maxDataLen": 8 - } - ] - }, - { - "iid": 16, - "type": "000000A2-0000-1000-8000-0026BB765291", - "primary": false, - "hidden": false, - "linked": [], - "characteristics": [ - { - "iid": 18, - "type": "00000037-0000-1000-8000-0026BB765291", - "format": "string", - "value": "1.1.0", - "perms": [ - "pr" - ], - "ev": false - } - ] - } + { + "aid": 1, + "services": [ + { + "iid": 1, + "type": "0000003E-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 2, + "type": "00000014-0000-1000-8000-0026BB765291", + "format": "bool", + "perms": ["pw"] + }, + { + "iid": 3, + "type": "00000020-0000-1000-8000-0026BB765291", + "format": "string", + "value": "RYSE Inc.", + "perms": ["pr"], + "ev": false + }, + { + "iid": 4, + "type": "00000021-0000-1000-8000-0026BB765291", + "format": "string", + "value": "RYSE SmartBridge", + "perms": ["pr"], + "ev": false + }, + { + "iid": 5, + "type": "00000023-0000-1000-8000-0026BB765291", + "format": "string", + "value": "RYSE SmartBridge", + "perms": ["pr"], + "ev": false + }, + { + "iid": 6, + "type": "00000030-0000-1000-8000-0026BB765291", + "format": "string", + "value": "0101.3521.0436", + "perms": ["pr"], + "ev": false + }, + { + "iid": 7, + "type": "00000052-0000-1000-8000-0026BB765291", + "format": "string", + "value": "1.3.0", + "perms": ["pr"], + "ev": false + }, + { + "iid": 8, + "type": "00000053-0000-1000-8000-0026BB765291", + "format": "string", + "value": "0101.3521.0436", + "perms": ["pr"], + "ev": false + }, + { + "iid": 9, + "type": "34AB8811-AC7F-4340-BAC3-FD6A85F9943B", + "format": "string", + "value": "4.1;3fac0fb4", + "perms": ["pr", "hd"], + "ev": false + }, + { + "iid": 10, + "type": "220", + "format": "data", + "value": "Yhl9CmseEb8=", + "perms": ["pr", "hd"], + "ev": false, + "maxDataLen": 8 + } ] - }, - { - "aid": 2, - "services": [ - { - "iid": 1, - "type": "0000003E-0000-1000-8000-0026BB765291", - "primary": false, - "hidden": false, - "linked": [], - "characteristics": [ - { - "iid": 2, - "type": "00000014-0000-1000-8000-0026BB765291", - "format": "bool", - "perms": [ - "pw" - ] - }, - { - "iid": 3, - "type": "00000020-0000-1000-8000-0026BB765291", - "format": "string", - "value": "RYSE Inc.", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 4, - "type": "00000021-0000-1000-8000-0026BB765291", - "format": "string", - "value": "RYSE Shade", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 5, - "type": "00000023-0000-1000-8000-0026BB765291", - "format": "string", - "value": "Master Bath South", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 6, - "type": "00000030-0000-1000-8000-0026BB765291", - "format": "string", - "value": "1.0.0", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 7, - "type": "00000052-0000-1000-8000-0026BB765291", - "format": "string", - "value": "3.0.8", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 8, - "type": "00000053-0000-1000-8000-0026BB765291", - "format": "string", - "value": "1.0.0", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 11, - "type": "000000A6-0000-1000-8000-0026BB765291", - "format": "uint32", - "value": 0, - "perms": [ - "pr", - "ev" - ], - "ev": false, - "minValue": 0, - "maxValue": 1, - "minStep": 1 - } - ] - }, - { - "iid": 48, - "type": "0000008C-0000-1000-8000-0026BB765291", - "primary": true, - "hidden": false, - "linked": [ - 64 - ], - "characteristics": [ - { - "iid": 52, - "type": "0000007C-0000-1000-8000-0026BB765291", - "format": "uint8", - "value": 0, - "perms": [ - "pr", - "pw", - "ev" - ], - "ev": true, - "unit": "percentage", - "minValue": 0, - "maxValue": 100, - "minStep": 1 - }, - { - "iid": 53, - "type": "0000006D-0000-1000-8000-0026BB765291", - "format": "uint8", - "value": 0, - "perms": [ - "pr", - "ev" - ], - "ev": true, - "unit": "percentage", - "minValue": 0, - "maxValue": 100, - "minStep": 1 - }, - { - "iid": 54, - "type": "00000072-0000-1000-8000-0026BB765291", - "format": "uint8", - "value": 2, - "perms": [ - "pr", - "ev" - ], - "ev": true, - "minValue": 0, - "maxValue": 2, - "minStep": 1 - }, - { - "iid": 50, - "type": "00000023-0000-1000-8000-0026BB765291", - "format": "string", - "value": "RYSE Shade", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 55, - "type": "00000024-0000-1000-8000-0026BB765291", - "format": "bool", - "value": 0, - "perms": [ - "pr", - "ev" - ], - "ev": true - } - ] - }, - { - "iid": 64, - "type": "00000096-0000-1000-8000-0026BB765291", - "primary": false, - "hidden": false, - "linked": [], - "characteristics": [ - { - "iid": 67, - "type": "00000068-0000-1000-8000-0026BB765291", - "format": "uint8", - "value": 100, - "perms": [ - "pr", - "ev" - ], - "ev": true, - "unit": "percentage", - "minValue": 0, - "maxValue": 100, - "minStep": 1 - }, - { - "iid": 68, - "type": "0000008F-0000-1000-8000-0026BB765291", - "format": "uint8", - "value": 2, - "perms": [ - "pr", - "ev" - ], - "ev": true, - "minValue": 0, - "maxValue": 2, - "minStep": 1 - }, - { - "iid": 70, - "type": "00000079-0000-1000-8000-0026BB765291", - "format": "uint8", - "value": 0, - "perms": [ - "pr", - "ev" - ], - "ev": true, - "minValue": 0, - "maxValue": 1, - "minStep": 1 - }, - { - "iid": 66, - "type": "00000023-0000-1000-8000-0026BB765291", - "format": "string", - "value": "RYSE Shade", - "perms": [ - "pr" - ], - "ev": false - } - ] - } + }, + { + "iid": 16, + "type": "000000A2-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 18, + "type": "00000037-0000-1000-8000-0026BB765291", + "format": "string", + "value": "1.1.0", + "perms": ["pr"], + "ev": false + } ] - }, - { - "aid": 3, - "services": [ - { - "iid": 1, - "type": "0000003E-0000-1000-8000-0026BB765291", - "primary": false, - "hidden": false, - "linked": [], - "characteristics": [ - { - "iid": 2, - "type": "00000014-0000-1000-8000-0026BB765291", - "format": "bool", - "perms": [ - "pw" - ] - }, - { - "iid": 3, - "type": "00000020-0000-1000-8000-0026BB765291", - "format": "string", - "value": "RYSE Inc.", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 4, - "type": "00000021-0000-1000-8000-0026BB765291", - "format": "string", - "value": "RYSE Shade", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 5, - "type": "00000023-0000-1000-8000-0026BB765291", - "format": "string", - "value": "RYSE SmartShade", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 6, - "type": "00000030-0000-1000-8000-0026BB765291", - "format": "string", - "value": "", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 7, - "type": "00000052-0000-1000-8000-0026BB765291", - "format": "string", - "value": "", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 8, - "type": "00000053-0000-1000-8000-0026BB765291", - "format": "string", - "value": "", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 11, - "type": "000000A6-0000-1000-8000-0026BB765291", - "format": "uint32", - "value": 1, - "perms": [ - "pr", - "ev" - ], - "ev": false, - "minValue": 0, - "maxValue": 1, - "minStep": 1 - } - ] - }, - { - "iid": 48, - "type": "0000008C-0000-1000-8000-0026BB765291", - "primary": true, - "hidden": false, - "linked": [ - 64 - ], - "characteristics": [ - { - "iid": 52, - "type": "0000007C-0000-1000-8000-0026BB765291", - "format": "uint8", - "value": 100, - "perms": [ - "pr", - "pw", - "ev" - ], - "ev": false, - "unit": "percentage", - "minValue": 0, - "maxValue": 100, - "minStep": 1 - }, - { - "iid": 53, - "type": "0000006D-0000-1000-8000-0026BB765291", - "format": "uint8", - "value": 100, - "perms": [ - "pr", - "ev" - ], - "ev": false, - "unit": "percentage", - "minValue": 0, - "maxValue": 100, - "minStep": 1 - }, - { - "iid": 54, - "type": "00000072-0000-1000-8000-0026BB765291", - "format": "uint8", - "value": 2, - "perms": [ - "pr", - "ev" - ], - "ev": false, - "minValue": 0, - "maxValue": 2, - "minStep": 1 - }, - { - "iid": 50, - "type": "00000023-0000-1000-8000-0026BB765291", - "format": "string", - "value": "RYSE Shade", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 55, - "type": "00000024-0000-1000-8000-0026BB765291", - "format": "bool", - "value": 0, - "perms": [ - "pr", - "ev" - ], - "ev": false - } - ] - }, - { - "iid": 64, - "type": "00000096-0000-1000-8000-0026BB765291", - "primary": false, - "hidden": false, - "linked": [], - "characteristics": [ - { - "iid": 67, - "type": "00000068-0000-1000-8000-0026BB765291", - "format": "uint8", - "value": 100, - "perms": [ - "pr", - "ev" - ], - "ev": false, - "unit": "percentage", - "minValue": 0, - "maxValue": 100, - "minStep": 1 - }, - { - "iid": 68, - "type": "0000008F-0000-1000-8000-0026BB765291", - "format": "uint8", - "value": 2, - "perms": [ - "pr", - "ev" - ], - "ev": false, - "minValue": 0, - "maxValue": 2, - "minStep": 1 - }, - { - "iid": 70, - "type": "00000079-0000-1000-8000-0026BB765291", - "format": "uint8", - "value": 0, - "perms": [ - "pr", - "ev" - ], - "ev": false, - "minValue": 0, - "maxValue": 1, - "minStep": 1 - }, - { - "iid": 66, - "type": "00000023-0000-1000-8000-0026BB765291", - "format": "string", - "value": "RYSE Shade", - "perms": [ - "pr" - ], - "ev": false - } - ] - } + } + ] + }, + { + "aid": 2, + "services": [ + { + "iid": 1, + "type": "0000003E-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 2, + "type": "00000014-0000-1000-8000-0026BB765291", + "format": "bool", + "perms": ["pw"] + }, + { + "iid": 3, + "type": "00000020-0000-1000-8000-0026BB765291", + "format": "string", + "value": "RYSE Inc.", + "perms": ["pr"], + "ev": false + }, + { + "iid": 4, + "type": "00000021-0000-1000-8000-0026BB765291", + "format": "string", + "value": "RYSE Shade", + "perms": ["pr"], + "ev": false + }, + { + "iid": 5, + "type": "00000023-0000-1000-8000-0026BB765291", + "format": "string", + "value": "Master Bath South", + "perms": ["pr"], + "ev": false + }, + { + "iid": 6, + "type": "00000030-0000-1000-8000-0026BB765291", + "format": "string", + "value": "1.0.0", + "perms": ["pr"], + "ev": false + }, + { + "iid": 7, + "type": "00000052-0000-1000-8000-0026BB765291", + "format": "string", + "value": "3.0.8", + "perms": ["pr"], + "ev": false + }, + { + "iid": 8, + "type": "00000053-0000-1000-8000-0026BB765291", + "format": "string", + "value": "1.0.0", + "perms": ["pr"], + "ev": false + }, + { + "iid": 11, + "type": "000000A6-0000-1000-8000-0026BB765291", + "format": "uint32", + "value": 0, + "perms": ["pr", "ev"], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + } ] - } -] \ No newline at end of file + }, + { + "iid": 48, + "type": "0000008C-0000-1000-8000-0026BB765291", + "primary": true, + "hidden": false, + "linked": [64], + "characteristics": [ + { + "iid": 52, + "type": "0000007C-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 0, + "perms": ["pr", "pw", "ev"], + "ev": true, + "unit": "percentage", + "minValue": 0, + "maxValue": 100, + "minStep": 1 + }, + { + "iid": 53, + "type": "0000006D-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 0, + "perms": ["pr", "ev"], + "ev": true, + "unit": "percentage", + "minValue": 0, + "maxValue": 100, + "minStep": 1 + }, + { + "iid": 54, + "type": "00000072-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 2, + "perms": ["pr", "ev"], + "ev": true, + "minValue": 0, + "maxValue": 2, + "minStep": 1 + }, + { + "iid": 50, + "type": "00000023-0000-1000-8000-0026BB765291", + "format": "string", + "value": "RYSE Shade", + "perms": ["pr"], + "ev": false + }, + { + "iid": 55, + "type": "00000024-0000-1000-8000-0026BB765291", + "format": "bool", + "value": 0, + "perms": ["pr", "ev"], + "ev": true + } + ] + }, + { + "iid": 64, + "type": "00000096-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 67, + "type": "00000068-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 100, + "perms": ["pr", "ev"], + "ev": true, + "unit": "percentage", + "minValue": 0, + "maxValue": 100, + "minStep": 1 + }, + { + "iid": 68, + "type": "0000008F-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 2, + "perms": ["pr", "ev"], + "ev": true, + "minValue": 0, + "maxValue": 2, + "minStep": 1 + }, + { + "iid": 70, + "type": "00000079-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 0, + "perms": ["pr", "ev"], + "ev": true, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 66, + "type": "00000023-0000-1000-8000-0026BB765291", + "format": "string", + "value": "RYSE Shade", + "perms": ["pr"], + "ev": false + } + ] + } + ] + }, + { + "aid": 3, + "services": [ + { + "iid": 1, + "type": "0000003E-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 2, + "type": "00000014-0000-1000-8000-0026BB765291", + "format": "bool", + "perms": ["pw"] + }, + { + "iid": 3, + "type": "00000020-0000-1000-8000-0026BB765291", + "format": "string", + "value": "RYSE Inc.", + "perms": ["pr"], + "ev": false + }, + { + "iid": 4, + "type": "00000021-0000-1000-8000-0026BB765291", + "format": "string", + "value": "RYSE Shade", + "perms": ["pr"], + "ev": false + }, + { + "iid": 5, + "type": "00000023-0000-1000-8000-0026BB765291", + "format": "string", + "value": "RYSE SmartShade", + "perms": ["pr"], + "ev": false + }, + { + "iid": 6, + "type": "00000030-0000-1000-8000-0026BB765291", + "format": "string", + "value": "", + "perms": ["pr"], + "ev": false + }, + { + "iid": 7, + "type": "00000052-0000-1000-8000-0026BB765291", + "format": "string", + "value": "", + "perms": ["pr"], + "ev": false + }, + { + "iid": 8, + "type": "00000053-0000-1000-8000-0026BB765291", + "format": "string", + "value": "", + "perms": ["pr"], + "ev": false + }, + { + "iid": 11, + "type": "000000A6-0000-1000-8000-0026BB765291", + "format": "uint32", + "value": 1, + "perms": ["pr", "ev"], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + } + ] + }, + { + "iid": 48, + "type": "0000008C-0000-1000-8000-0026BB765291", + "primary": true, + "hidden": false, + "linked": [64], + "characteristics": [ + { + "iid": 52, + "type": "0000007C-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 100, + "perms": ["pr", "pw", "ev"], + "ev": false, + "unit": "percentage", + "minValue": 0, + "maxValue": 100, + "minStep": 1 + }, + { + "iid": 53, + "type": "0000006D-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 100, + "perms": ["pr", "ev"], + "ev": false, + "unit": "percentage", + "minValue": 0, + "maxValue": 100, + "minStep": 1 + }, + { + "iid": 54, + "type": "00000072-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 2, + "perms": ["pr", "ev"], + "ev": false, + "minValue": 0, + "maxValue": 2, + "minStep": 1 + }, + { + "iid": 50, + "type": "00000023-0000-1000-8000-0026BB765291", + "format": "string", + "value": "RYSE Shade", + "perms": ["pr"], + "ev": false + }, + { + "iid": 55, + "type": "00000024-0000-1000-8000-0026BB765291", + "format": "bool", + "value": 0, + "perms": ["pr", "ev"], + "ev": false + } + ] + }, + { + "iid": 64, + "type": "00000096-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 67, + "type": "00000068-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 100, + "perms": ["pr", "ev"], + "ev": false, + "unit": "percentage", + "minValue": 0, + "maxValue": 100, + "minStep": 1 + }, + { + "iid": 68, + "type": "0000008F-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 2, + "perms": ["pr", "ev"], + "ev": false, + "minValue": 0, + "maxValue": 2, + "minStep": 1 + }, + { + "iid": 70, + "type": "00000079-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 0, + "perms": ["pr", "ev"], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 66, + "type": "00000023-0000-1000-8000-0026BB765291", + "format": "string", + "value": "RYSE Shade", + "perms": ["pr"], + "ev": false + } + ] + } + ] + } +] diff --git a/tests/components/homekit_controller/fixtures/ryse_smart_bridge_four_shades.json b/tests/components/homekit_controller/fixtures/ryse_smart_bridge_four_shades.json index b2e7aabd95d..74ddf5ba0c8 100644 --- a/tests/components/homekit_controller/fixtures/ryse_smart_bridge_four_shades.json +++ b/tests/components/homekit_controller/fixtures/ryse_smart_bridge_four_shades.json @@ -1,1066 +1,864 @@ [ - { - "aid": 1, - "services": [ - { - "iid": 1, - "type": "0000003E-0000-1000-8000-0026BB765291", - "primary": false, - "hidden": false, - "linked": [], - "characteristics": [ - { - "iid": 2, - "type": "00000014-0000-1000-8000-0026BB765291", - "format": "bool", - "perms": [ - "pw" - ] - }, - { - "iid": 3, - "type": "00000020-0000-1000-8000-0026BB765291", - "format": "string", - "value": "RYSE Inc.", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 4, - "type": "00000021-0000-1000-8000-0026BB765291", - "format": "string", - "value": "RYSE SmartBridge", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 5, - "type": "00000023-0000-1000-8000-0026BB765291", - "format": "string", - "value": "RYSE SmartBridge", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 6, - "type": "00000030-0000-1000-8000-0026BB765291", - "format": "string", - "value": "0401.3521.0679", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 7, - "type": "00000052-0000-1000-8000-0026BB765291", - "format": "string", - "value": "1.3.0", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 8, - "type": "00000053-0000-1000-8000-0026BB765291", - "format": "string", - "value": "0401.3521.0679", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 9, - "type": "34AB8811-AC7F-4340-BAC3-FD6A85F9943B", - "format": "string", - "value": "4.1;3fac0fb4", - "perms": [ - "pr", - "hd" - ], - "ev": false - }, - { - "iid": 10, - "type": "220", - "format": "data", - "value": "Yhl9CmseEb8=", - "perms": [ - "pr", - "hd" - ], - "ev": false, - "maxDataLen": 8 - } - ] - }, - { - "iid": 16, - "type": "000000A2-0000-1000-8000-0026BB765291", - "primary": false, - "hidden": false, - "linked": [], - "characteristics": [ - { - "iid": 18, - "type": "00000037-0000-1000-8000-0026BB765291", - "format": "string", - "value": "1.1.0", - "perms": [ - "pr" - ], - "ev": false - } - ] - } + { + "aid": 1, + "services": [ + { + "iid": 1, + "type": "0000003E-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 2, + "type": "00000014-0000-1000-8000-0026BB765291", + "format": "bool", + "perms": ["pw"] + }, + { + "iid": 3, + "type": "00000020-0000-1000-8000-0026BB765291", + "format": "string", + "value": "RYSE Inc.", + "perms": ["pr"], + "ev": false + }, + { + "iid": 4, + "type": "00000021-0000-1000-8000-0026BB765291", + "format": "string", + "value": "RYSE SmartBridge", + "perms": ["pr"], + "ev": false + }, + { + "iid": 5, + "type": "00000023-0000-1000-8000-0026BB765291", + "format": "string", + "value": "RYSE SmartBridge", + "perms": ["pr"], + "ev": false + }, + { + "iid": 6, + "type": "00000030-0000-1000-8000-0026BB765291", + "format": "string", + "value": "0401.3521.0679", + "perms": ["pr"], + "ev": false + }, + { + "iid": 7, + "type": "00000052-0000-1000-8000-0026BB765291", + "format": "string", + "value": "1.3.0", + "perms": ["pr"], + "ev": false + }, + { + "iid": 8, + "type": "00000053-0000-1000-8000-0026BB765291", + "format": "string", + "value": "0401.3521.0679", + "perms": ["pr"], + "ev": false + }, + { + "iid": 9, + "type": "34AB8811-AC7F-4340-BAC3-FD6A85F9943B", + "format": "string", + "value": "4.1;3fac0fb4", + "perms": ["pr", "hd"], + "ev": false + }, + { + "iid": 10, + "type": "220", + "format": "data", + "value": "Yhl9CmseEb8=", + "perms": ["pr", "hd"], + "ev": false, + "maxDataLen": 8 + } ] - }, - { - "aid": 2, - "services": [ - { - "iid": 1, - "type": "0000003E-0000-1000-8000-0026BB765291", - "primary": false, - "hidden": false, - "linked": [], - "characteristics": [ - { - "iid": 2, - "type": "00000014-0000-1000-8000-0026BB765291", - "format": "bool", - "perms": [ - "pw" - ] - }, - { - "iid": 3, - "type": "00000020-0000-1000-8000-0026BB765291", - "format": "string", - "value": "RYSE Inc.", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 4, - "type": "00000021-0000-1000-8000-0026BB765291", - "format": "string", - "value": "RYSE Shade", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 5, - "type": "00000023-0000-1000-8000-0026BB765291", - "format": "string", - "value": "LR Left", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 6, - "type": "00000030-0000-1000-8000-0026BB765291", - "format": "string", - "value": "1.0.0", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 7, - "type": "00000052-0000-1000-8000-0026BB765291", - "format": "string", - "value": "3.0.8", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 8, - "type": "00000053-0000-1000-8000-0026BB765291", - "format": "string", - "value": "1.0.0", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 11, - "type": "000000A6-0000-1000-8000-0026BB765291", - "format": "uint32", - "value": 0, - "perms": [ - "pr", - "ev" - ], - "ev": false, - "minValue": 0, - "maxValue": 1, - "minStep": 1 - } - ] - }, - { - "iid": 48, - "type": "0000008C-0000-1000-8000-0026BB765291", - "primary": true, - "hidden": false, - "linked": [ - 64 - ], - "characteristics": [ - { - "iid": 52, - "type": "0000007C-0000-1000-8000-0026BB765291", - "format": "uint8", - "value": 0, - "perms": [ - "pr", - "pw", - "ev" - ], - "ev": true, - "unit": "percentage", - "minValue": 0, - "maxValue": 100, - "minStep": 1 - }, - { - "iid": 53, - "type": "0000006D-0000-1000-8000-0026BB765291", - "format": "uint8", - "value": 0, - "perms": [ - "pr", - "ev" - ], - "ev": true, - "unit": "percentage", - "minValue": 0, - "maxValue": 100, - "minStep": 1 - }, - { - "iid": 54, - "type": "00000072-0000-1000-8000-0026BB765291", - "format": "uint8", - "value": 2, - "perms": [ - "pr", - "ev" - ], - "ev": true, - "minValue": 0, - "maxValue": 2, - "minStep": 1 - }, - { - "iid": 50, - "type": "00000023-0000-1000-8000-0026BB765291", - "format": "string", - "value": "RYSE Shade", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 55, - "type": "00000024-0000-1000-8000-0026BB765291", - "format": "bool", - "value": 0, - "perms": [ - "pr", - "ev" - ], - "ev": true - } - ] - }, - { - "iid": 64, - "type": "00000096-0000-1000-8000-0026BB765291", - "primary": false, - "hidden": false, - "linked": [], - "characteristics": [ - { - "iid": 67, - "type": "00000068-0000-1000-8000-0026BB765291", - "format": "uint8", - "value": 89, - "perms": [ - "pr", - "ev" - ], - "ev": true, - "unit": "percentage", - "minValue": 0, - "maxValue": 100, - "minStep": 1 - }, - { - "iid": 68, - "type": "0000008F-0000-1000-8000-0026BB765291", - "format": "uint8", - "value": 0, - "perms": [ - "pr", - "ev" - ], - "ev": true, - "minValue": 0, - "maxValue": 2, - "minStep": 1 - }, - { - "iid": 70, - "type": "00000079-0000-1000-8000-0026BB765291", - "format": "uint8", - "value": 0, - "perms": [ - "pr", - "ev" - ], - "ev": true, - "minValue": 0, - "maxValue": 1, - "minStep": 1 - }, - { - "iid": 66, - "type": "00000023-0000-1000-8000-0026BB765291", - "format": "string", - "value": "RYSE Shade", - "perms": [ - "pr" - ], - "ev": false - } - ] - } + }, + { + "iid": 16, + "type": "000000A2-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 18, + "type": "00000037-0000-1000-8000-0026BB765291", + "format": "string", + "value": "1.1.0", + "perms": ["pr"], + "ev": false + } ] - }, - { - "aid": 3, - "services": [ - { - "iid": 1, - "type": "0000003E-0000-1000-8000-0026BB765291", - "primary": false, - "hidden": false, - "linked": [], - "characteristics": [ - { - "iid": 2, - "type": "00000014-0000-1000-8000-0026BB765291", - "format": "bool", - "perms": [ - "pw" - ] - }, - { - "iid": 3, - "type": "00000020-0000-1000-8000-0026BB765291", - "format": "string", - "value": "RYSE Inc.", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 4, - "type": "00000021-0000-1000-8000-0026BB765291", - "format": "string", - "value": "RYSE Shade", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 5, - "type": "00000023-0000-1000-8000-0026BB765291", - "format": "string", - "value": "LR Right", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 6, - "type": "00000030-0000-1000-8000-0026BB765291", - "format": "string", - "value": "1.0.0", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 7, - "type": "00000052-0000-1000-8000-0026BB765291", - "format": "string", - "value": "3.0.8", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 8, - "type": "00000053-0000-1000-8000-0026BB765291", - "format": "string", - "value": "1.0.0", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 11, - "type": "000000A6-0000-1000-8000-0026BB765291", - "format": "uint32", - "value": 0, - "perms": [ - "pr", - "ev" - ], - "ev": false, - "minValue": 0, - "maxValue": 1, - "minStep": 1 - } - ] - }, - { - "iid": 48, - "type": "0000008C-0000-1000-8000-0026BB765291", - "primary": true, - "hidden": false, - "linked": [ - 64 - ], - "characteristics": [ - { - "iid": 52, - "type": "0000007C-0000-1000-8000-0026BB765291", - "format": "uint8", - "value": 0, - "perms": [ - "pr", - "pw", - "ev" - ], - "ev": false, - "unit": "percentage", - "minValue": 0, - "maxValue": 100, - "minStep": 1 - }, - { - "iid": 53, - "type": "0000006D-0000-1000-8000-0026BB765291", - "format": "uint8", - "value": 0, - "perms": [ - "pr", - "ev" - ], - "ev": false, - "unit": "percentage", - "minValue": 0, - "maxValue": 100, - "minStep": 1 - }, - { - "iid": 54, - "type": "00000072-0000-1000-8000-0026BB765291", - "format": "uint8", - "value": 2, - "perms": [ - "pr", - "ev" - ], - "ev": false, - "minValue": 0, - "maxValue": 2, - "minStep": 1 - }, - { - "iid": 50, - "type": "00000023-0000-1000-8000-0026BB765291", - "format": "string", - "value": "RYSE Shade", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 55, - "type": "00000024-0000-1000-8000-0026BB765291", - "format": "bool", - "value": 0, - "perms": [ - "pr", - "ev" - ], - "ev": false - } - ] - }, - { - "iid": 64, - "type": "00000096-0000-1000-8000-0026BB765291", - "primary": false, - "hidden": false, - "linked": [], - "characteristics": [ - { - "iid": 67, - "type": "00000068-0000-1000-8000-0026BB765291", - "format": "uint8", - "value": 100, - "perms": [ - "pr", - "ev" - ], - "ev": false, - "unit": "percentage", - "minValue": 0, - "maxValue": 100, - "minStep": 1 - }, - { - "iid": 68, - "type": "0000008F-0000-1000-8000-0026BB765291", - "format": "uint8", - "value": 2, - "perms": [ - "pr", - "ev" - ], - "ev": false, - "minValue": 0, - "maxValue": 2, - "minStep": 1 - }, - { - "iid": 70, - "type": "00000079-0000-1000-8000-0026BB765291", - "format": "uint8", - "value": 0, - "perms": [ - "pr", - "ev" - ], - "ev": false, - "minValue": 0, - "maxValue": 1, - "minStep": 1 - }, - { - "iid": 66, - "type": "00000023-0000-1000-8000-0026BB765291", - "format": "string", - "value": "RYSE Shade", - "perms": [ - "pr" - ], - "ev": false - } - ] - } + } + ] + }, + { + "aid": 2, + "services": [ + { + "iid": 1, + "type": "0000003E-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 2, + "type": "00000014-0000-1000-8000-0026BB765291", + "format": "bool", + "perms": ["pw"] + }, + { + "iid": 3, + "type": "00000020-0000-1000-8000-0026BB765291", + "format": "string", + "value": "RYSE Inc.", + "perms": ["pr"], + "ev": false + }, + { + "iid": 4, + "type": "00000021-0000-1000-8000-0026BB765291", + "format": "string", + "value": "RYSE Shade", + "perms": ["pr"], + "ev": false + }, + { + "iid": 5, + "type": "00000023-0000-1000-8000-0026BB765291", + "format": "string", + "value": "LR Left", + "perms": ["pr"], + "ev": false + }, + { + "iid": 6, + "type": "00000030-0000-1000-8000-0026BB765291", + "format": "string", + "value": "1.0.0", + "perms": ["pr"], + "ev": false + }, + { + "iid": 7, + "type": "00000052-0000-1000-8000-0026BB765291", + "format": "string", + "value": "3.0.8", + "perms": ["pr"], + "ev": false + }, + { + "iid": 8, + "type": "00000053-0000-1000-8000-0026BB765291", + "format": "string", + "value": "1.0.0", + "perms": ["pr"], + "ev": false + }, + { + "iid": 11, + "type": "000000A6-0000-1000-8000-0026BB765291", + "format": "uint32", + "value": 0, + "perms": ["pr", "ev"], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + } ] - }, - { - "aid": 4, - "services": [ - { - "iid": 1, - "type": "0000003E-0000-1000-8000-0026BB765291", - "primary": false, - "hidden": false, - "linked": [], - "characteristics": [ - { - "iid": 2, - "type": "00000014-0000-1000-8000-0026BB765291", - "format": "bool", - "perms": [ - "pw" - ] - }, - { - "iid": 3, - "type": "00000020-0000-1000-8000-0026BB765291", - "format": "string", - "value": "RYSE Inc.", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 4, - "type": "00000021-0000-1000-8000-0026BB765291", - "format": "string", - "value": "RYSE Shade", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 5, - "type": "00000023-0000-1000-8000-0026BB765291", - "format": "string", - "value": "BR Left", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 6, - "type": "00000030-0000-1000-8000-0026BB765291", - "format": "string", - "value": "1.0.0", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 7, - "type": "00000052-0000-1000-8000-0026BB765291", - "format": "string", - "value": "3.0.8", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 8, - "type": "00000053-0000-1000-8000-0026BB765291", - "format": "string", - "value": "1.0.0", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 11, - "type": "000000A6-0000-1000-8000-0026BB765291", - "format": "uint32", - "value": 0, - "perms": [ - "pr", - "ev" - ], - "ev": false, - "minValue": 0, - "maxValue": 1, - "minStep": 1 - } - ] - }, - { - "iid": 48, - "type": "0000008C-0000-1000-8000-0026BB765291", - "primary": true, - "hidden": false, - "linked": [ - 64 - ], - "characteristics": [ - { - "iid": 52, - "type": "0000007C-0000-1000-8000-0026BB765291", - "format": "uint8", - "value": 100, - "perms": [ - "pr", - "pw", - "ev" - ], - "ev": false, - "unit": "percentage", - "minValue": 0, - "maxValue": 100, - "minStep": 1 - }, - { - "iid": 53, - "type": "0000006D-0000-1000-8000-0026BB765291", - "format": "uint8", - "value": 100, - "perms": [ - "pr", - "ev" - ], - "ev": false, - "unit": "percentage", - "minValue": 0, - "maxValue": 100, - "minStep": 1 - }, - { - "iid": 54, - "type": "00000072-0000-1000-8000-0026BB765291", - "format": "uint8", - "value": 2, - "perms": [ - "pr", - "ev" - ], - "ev": false, - "minValue": 0, - "maxValue": 2, - "minStep": 1 - }, - { - "iid": 50, - "type": "00000023-0000-1000-8000-0026BB765291", - "format": "string", - "value": "RYSE Shade", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 55, - "type": "00000024-0000-1000-8000-0026BB765291", - "format": "bool", - "value": 0, - "perms": [ - "pr", - "ev" - ], - "ev": false - } - ] - }, - { - "iid": 64, - "type": "00000096-0000-1000-8000-0026BB765291", - "primary": false, - "hidden": false, - "linked": [], - "characteristics": [ - { - "iid": 67, - "type": "00000068-0000-1000-8000-0026BB765291", - "format": "uint8", - "value": 100, - "perms": [ - "pr", - "ev" - ], - "ev": false, - "unit": "percentage", - "minValue": 0, - "maxValue": 100, - "minStep": 1 - }, - { - "iid": 68, - "type": "0000008F-0000-1000-8000-0026BB765291", - "format": "uint8", - "value": 2, - "perms": [ - "pr", - "ev" - ], - "ev": false, - "minValue": 0, - "maxValue": 2, - "minStep": 1 - }, - { - "iid": 70, - "type": "00000079-0000-1000-8000-0026BB765291", - "format": "uint8", - "value": 0, - "perms": [ - "pr", - "ev" - ], - "ev": false, - "minValue": 0, - "maxValue": 1, - "minStep": 1 - }, - { - "iid": 66, - "type": "00000023-0000-1000-8000-0026BB765291", - "format": "string", - "value": "RYSE Shade", - "perms": [ - "pr" - ], - "ev": false - } - ] - } + }, + { + "iid": 48, + "type": "0000008C-0000-1000-8000-0026BB765291", + "primary": true, + "hidden": false, + "linked": [64], + "characteristics": [ + { + "iid": 52, + "type": "0000007C-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 0, + "perms": ["pr", "pw", "ev"], + "ev": true, + "unit": "percentage", + "minValue": 0, + "maxValue": 100, + "minStep": 1 + }, + { + "iid": 53, + "type": "0000006D-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 0, + "perms": ["pr", "ev"], + "ev": true, + "unit": "percentage", + "minValue": 0, + "maxValue": 100, + "minStep": 1 + }, + { + "iid": 54, + "type": "00000072-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 2, + "perms": ["pr", "ev"], + "ev": true, + "minValue": 0, + "maxValue": 2, + "minStep": 1 + }, + { + "iid": 50, + "type": "00000023-0000-1000-8000-0026BB765291", + "format": "string", + "value": "RYSE Shade", + "perms": ["pr"], + "ev": false + }, + { + "iid": 55, + "type": "00000024-0000-1000-8000-0026BB765291", + "format": "bool", + "value": 0, + "perms": ["pr", "ev"], + "ev": true + } ] - }, - { - "aid": 5, - "services": [ - { - "iid": 1, - "type": "0000003E-0000-1000-8000-0026BB765291", - "primary": false, - "hidden": false, - "linked": [], - "characteristics": [ - { - "iid": 2, - "type": "00000014-0000-1000-8000-0026BB765291", - "format": "bool", - "perms": [ - "pw" - ] - }, - { - "iid": 3, - "type": "00000020-0000-1000-8000-0026BB765291", - "format": "string", - "value": "RYSE Inc.", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 4, - "type": "00000021-0000-1000-8000-0026BB765291", - "format": "string", - "value": "RYSE Shade", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 5, - "type": "00000023-0000-1000-8000-0026BB765291", - "format": "string", - "value": "RZSS", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 6, - "type": "00000030-0000-1000-8000-0026BB765291", - "format": "string", - "value": "1.0.0", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 7, - "type": "00000052-0000-1000-8000-0026BB765291", - "format": "string", - "value": "3.0.8", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 8, - "type": "00000053-0000-1000-8000-0026BB765291", - "format": "string", - "value": "1.0.0", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 11, - "type": "000000A6-0000-1000-8000-0026BB765291", - "format": "uint32", - "value": 1, - "perms": [ - "pr", - "ev" - ], - "ev": false, - "minValue": 0, - "maxValue": 1, - "minStep": 1 - } - ] - }, - { - "iid": 48, - "type": "0000008C-0000-1000-8000-0026BB765291", - "primary": true, - "hidden": false, - "linked": [ - 64 - ], - "characteristics": [ - { - "iid": 52, - "type": "0000007C-0000-1000-8000-0026BB765291", - "format": "uint8", - "value": 100, - "perms": [ - "pr", - "pw", - "ev" - ], - "ev": false, - "unit": "percentage", - "minValue": 0, - "maxValue": 100, - "minStep": 1 - }, - { - "iid": 53, - "type": "0000006D-0000-1000-8000-0026BB765291", - "format": "uint8", - "value": 100, - "perms": [ - "pr", - "ev" - ], - "ev": false, - "unit": "percentage", - "minValue": 0, - "maxValue": 100, - "minStep": 1 - }, - { - "iid": 54, - "type": "00000072-0000-1000-8000-0026BB765291", - "format": "uint8", - "value": 2, - "perms": [ - "pr", - "ev" - ], - "ev": false, - "minValue": 0, - "maxValue": 2, - "minStep": 1 - }, - { - "iid": 50, - "type": "00000023-0000-1000-8000-0026BB765291", - "format": "string", - "value": "RYSE Shade", - "perms": [ - "pr" - ], - "ev": false - }, - { - "iid": 55, - "type": "00000024-0000-1000-8000-0026BB765291", - "format": "bool", - "value": 0, - "perms": [ - "pr", - "ev" - ], - "ev": false - } - ] - }, - { - "iid": 64, - "type": "00000096-0000-1000-8000-0026BB765291", - "primary": false, - "hidden": false, - "linked": [], - "characteristics": [ - { - "iid": 67, - "type": "00000068-0000-1000-8000-0026BB765291", - "format": "uint8", - "value": 0, - "perms": [ - "pr", - "ev" - ], - "ev": false, - "unit": "percentage", - "minValue": 0, - "maxValue": 100, - "minStep": 1 - }, - { - "iid": 68, - "type": "0000008F-0000-1000-8000-0026BB765291", - "format": "uint8", - "value": 0, - "perms": [ - "pr", - "ev" - ], - "ev": false, - "minValue": 0, - "maxValue": 2, - "minStep": 1 - }, - { - "iid": 70, - "type": "00000079-0000-1000-8000-0026BB765291", - "format": "uint8", - "value": 1, - "perms": [ - "pr", - "ev" - ], - "ev": false, - "minValue": 0, - "maxValue": 1, - "minStep": 1 - }, - { - "iid": 66, - "type": "00000023-0000-1000-8000-0026BB765291", - "format": "string", - "value": "RYSE Shade", - "perms": [ - "pr" - ], - "ev": false - } - ] - } + }, + { + "iid": 64, + "type": "00000096-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 67, + "type": "00000068-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 89, + "perms": ["pr", "ev"], + "ev": true, + "unit": "percentage", + "minValue": 0, + "maxValue": 100, + "minStep": 1 + }, + { + "iid": 68, + "type": "0000008F-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 0, + "perms": ["pr", "ev"], + "ev": true, + "minValue": 0, + "maxValue": 2, + "minStep": 1 + }, + { + "iid": 70, + "type": "00000079-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 0, + "perms": ["pr", "ev"], + "ev": true, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 66, + "type": "00000023-0000-1000-8000-0026BB765291", + "format": "string", + "value": "RYSE Shade", + "perms": ["pr"], + "ev": false + } ] - } + } + ] + }, + { + "aid": 3, + "services": [ + { + "iid": 1, + "type": "0000003E-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 2, + "type": "00000014-0000-1000-8000-0026BB765291", + "format": "bool", + "perms": ["pw"] + }, + { + "iid": 3, + "type": "00000020-0000-1000-8000-0026BB765291", + "format": "string", + "value": "RYSE Inc.", + "perms": ["pr"], + "ev": false + }, + { + "iid": 4, + "type": "00000021-0000-1000-8000-0026BB765291", + "format": "string", + "value": "RYSE Shade", + "perms": ["pr"], + "ev": false + }, + { + "iid": 5, + "type": "00000023-0000-1000-8000-0026BB765291", + "format": "string", + "value": "LR Right", + "perms": ["pr"], + "ev": false + }, + { + "iid": 6, + "type": "00000030-0000-1000-8000-0026BB765291", + "format": "string", + "value": "1.0.0", + "perms": ["pr"], + "ev": false + }, + { + "iid": 7, + "type": "00000052-0000-1000-8000-0026BB765291", + "format": "string", + "value": "3.0.8", + "perms": ["pr"], + "ev": false + }, + { + "iid": 8, + "type": "00000053-0000-1000-8000-0026BB765291", + "format": "string", + "value": "1.0.0", + "perms": ["pr"], + "ev": false + }, + { + "iid": 11, + "type": "000000A6-0000-1000-8000-0026BB765291", + "format": "uint32", + "value": 0, + "perms": ["pr", "ev"], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + } + ] + }, + { + "iid": 48, + "type": "0000008C-0000-1000-8000-0026BB765291", + "primary": true, + "hidden": false, + "linked": [64], + "characteristics": [ + { + "iid": 52, + "type": "0000007C-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 0, + "perms": ["pr", "pw", "ev"], + "ev": false, + "unit": "percentage", + "minValue": 0, + "maxValue": 100, + "minStep": 1 + }, + { + "iid": 53, + "type": "0000006D-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 0, + "perms": ["pr", "ev"], + "ev": false, + "unit": "percentage", + "minValue": 0, + "maxValue": 100, + "minStep": 1 + }, + { + "iid": 54, + "type": "00000072-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 2, + "perms": ["pr", "ev"], + "ev": false, + "minValue": 0, + "maxValue": 2, + "minStep": 1 + }, + { + "iid": 50, + "type": "00000023-0000-1000-8000-0026BB765291", + "format": "string", + "value": "RYSE Shade", + "perms": ["pr"], + "ev": false + }, + { + "iid": 55, + "type": "00000024-0000-1000-8000-0026BB765291", + "format": "bool", + "value": 0, + "perms": ["pr", "ev"], + "ev": false + } + ] + }, + { + "iid": 64, + "type": "00000096-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 67, + "type": "00000068-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 100, + "perms": ["pr", "ev"], + "ev": false, + "unit": "percentage", + "minValue": 0, + "maxValue": 100, + "minStep": 1 + }, + { + "iid": 68, + "type": "0000008F-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 2, + "perms": ["pr", "ev"], + "ev": false, + "minValue": 0, + "maxValue": 2, + "minStep": 1 + }, + { + "iid": 70, + "type": "00000079-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 0, + "perms": ["pr", "ev"], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 66, + "type": "00000023-0000-1000-8000-0026BB765291", + "format": "string", + "value": "RYSE Shade", + "perms": ["pr"], + "ev": false + } + ] + } + ] + }, + { + "aid": 4, + "services": [ + { + "iid": 1, + "type": "0000003E-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 2, + "type": "00000014-0000-1000-8000-0026BB765291", + "format": "bool", + "perms": ["pw"] + }, + { + "iid": 3, + "type": "00000020-0000-1000-8000-0026BB765291", + "format": "string", + "value": "RYSE Inc.", + "perms": ["pr"], + "ev": false + }, + { + "iid": 4, + "type": "00000021-0000-1000-8000-0026BB765291", + "format": "string", + "value": "RYSE Shade", + "perms": ["pr"], + "ev": false + }, + { + "iid": 5, + "type": "00000023-0000-1000-8000-0026BB765291", + "format": "string", + "value": "BR Left", + "perms": ["pr"], + "ev": false + }, + { + "iid": 6, + "type": "00000030-0000-1000-8000-0026BB765291", + "format": "string", + "value": "1.0.0", + "perms": ["pr"], + "ev": false + }, + { + "iid": 7, + "type": "00000052-0000-1000-8000-0026BB765291", + "format": "string", + "value": "3.0.8", + "perms": ["pr"], + "ev": false + }, + { + "iid": 8, + "type": "00000053-0000-1000-8000-0026BB765291", + "format": "string", + "value": "1.0.0", + "perms": ["pr"], + "ev": false + }, + { + "iid": 11, + "type": "000000A6-0000-1000-8000-0026BB765291", + "format": "uint32", + "value": 0, + "perms": ["pr", "ev"], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + } + ] + }, + { + "iid": 48, + "type": "0000008C-0000-1000-8000-0026BB765291", + "primary": true, + "hidden": false, + "linked": [64], + "characteristics": [ + { + "iid": 52, + "type": "0000007C-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 100, + "perms": ["pr", "pw", "ev"], + "ev": false, + "unit": "percentage", + "minValue": 0, + "maxValue": 100, + "minStep": 1 + }, + { + "iid": 53, + "type": "0000006D-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 100, + "perms": ["pr", "ev"], + "ev": false, + "unit": "percentage", + "minValue": 0, + "maxValue": 100, + "minStep": 1 + }, + { + "iid": 54, + "type": "00000072-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 2, + "perms": ["pr", "ev"], + "ev": false, + "minValue": 0, + "maxValue": 2, + "minStep": 1 + }, + { + "iid": 50, + "type": "00000023-0000-1000-8000-0026BB765291", + "format": "string", + "value": "RYSE Shade", + "perms": ["pr"], + "ev": false + }, + { + "iid": 55, + "type": "00000024-0000-1000-8000-0026BB765291", + "format": "bool", + "value": 0, + "perms": ["pr", "ev"], + "ev": false + } + ] + }, + { + "iid": 64, + "type": "00000096-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 67, + "type": "00000068-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 100, + "perms": ["pr", "ev"], + "ev": false, + "unit": "percentage", + "minValue": 0, + "maxValue": 100, + "minStep": 1 + }, + { + "iid": 68, + "type": "0000008F-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 2, + "perms": ["pr", "ev"], + "ev": false, + "minValue": 0, + "maxValue": 2, + "minStep": 1 + }, + { + "iid": 70, + "type": "00000079-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 0, + "perms": ["pr", "ev"], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 66, + "type": "00000023-0000-1000-8000-0026BB765291", + "format": "string", + "value": "RYSE Shade", + "perms": ["pr"], + "ev": false + } + ] + } + ] + }, + { + "aid": 5, + "services": [ + { + "iid": 1, + "type": "0000003E-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 2, + "type": "00000014-0000-1000-8000-0026BB765291", + "format": "bool", + "perms": ["pw"] + }, + { + "iid": 3, + "type": "00000020-0000-1000-8000-0026BB765291", + "format": "string", + "value": "RYSE Inc.", + "perms": ["pr"], + "ev": false + }, + { + "iid": 4, + "type": "00000021-0000-1000-8000-0026BB765291", + "format": "string", + "value": "RYSE Shade", + "perms": ["pr"], + "ev": false + }, + { + "iid": 5, + "type": "00000023-0000-1000-8000-0026BB765291", + "format": "string", + "value": "RZSS", + "perms": ["pr"], + "ev": false + }, + { + "iid": 6, + "type": "00000030-0000-1000-8000-0026BB765291", + "format": "string", + "value": "1.0.0", + "perms": ["pr"], + "ev": false + }, + { + "iid": 7, + "type": "00000052-0000-1000-8000-0026BB765291", + "format": "string", + "value": "3.0.8", + "perms": ["pr"], + "ev": false + }, + { + "iid": 8, + "type": "00000053-0000-1000-8000-0026BB765291", + "format": "string", + "value": "1.0.0", + "perms": ["pr"], + "ev": false + }, + { + "iid": 11, + "type": "000000A6-0000-1000-8000-0026BB765291", + "format": "uint32", + "value": 1, + "perms": ["pr", "ev"], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + } + ] + }, + { + "iid": 48, + "type": "0000008C-0000-1000-8000-0026BB765291", + "primary": true, + "hidden": false, + "linked": [64], + "characteristics": [ + { + "iid": 52, + "type": "0000007C-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 100, + "perms": ["pr", "pw", "ev"], + "ev": false, + "unit": "percentage", + "minValue": 0, + "maxValue": 100, + "minStep": 1 + }, + { + "iid": 53, + "type": "0000006D-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 100, + "perms": ["pr", "ev"], + "ev": false, + "unit": "percentage", + "minValue": 0, + "maxValue": 100, + "minStep": 1 + }, + { + "iid": 54, + "type": "00000072-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 2, + "perms": ["pr", "ev"], + "ev": false, + "minValue": 0, + "maxValue": 2, + "minStep": 1 + }, + { + "iid": 50, + "type": "00000023-0000-1000-8000-0026BB765291", + "format": "string", + "value": "RYSE Shade", + "perms": ["pr"], + "ev": false + }, + { + "iid": 55, + "type": "00000024-0000-1000-8000-0026BB765291", + "format": "bool", + "value": 0, + "perms": ["pr", "ev"], + "ev": false + } + ] + }, + { + "iid": 64, + "type": "00000096-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 67, + "type": "00000068-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 0, + "perms": ["pr", "ev"], + "ev": false, + "unit": "percentage", + "minValue": 0, + "maxValue": 100, + "minStep": 1 + }, + { + "iid": 68, + "type": "0000008F-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 0, + "perms": ["pr", "ev"], + "ev": false, + "minValue": 0, + "maxValue": 2, + "minStep": 1 + }, + { + "iid": 70, + "type": "00000079-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 1, + "perms": ["pr", "ev"], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 66, + "type": "00000023-0000-1000-8000-0026BB765291", + "format": "string", + "value": "RYSE Shade", + "perms": ["pr"], + "ev": false + } + ] + } + ] + } ] diff --git a/tests/components/homekit_controller/fixtures/simpleconnect_fan.json b/tests/components/homekit_controller/fixtures/simpleconnect_fan.json index ecdf4fe5673..da446a7f2b8 100644 --- a/tests/components/homekit_controller/fixtures/simpleconnect_fan.json +++ b/tests/components/homekit_controller/fixtures/simpleconnect_fan.json @@ -1,769 +1,562 @@ [ - { - "aid": 1, - "services": [ - { - "characteristics": [ - { - "format": "string", - "iid": 2, - "perms": [ - "pr" - ], - "type": "00000023-0000-1000-8000-0026BB765291", - "value": "SIMPLEconnect Fan-06F674" - }, - { - "format": "string", - "iid": 3, - "perms": [ - "pr" - ], - "type": "00000020-0000-1000-8000-0026BB765291", - "value": "Hunter Fan" - }, - { - "format": "string", - "iid": 4, - "perms": [ - "pr" - ], - "type": "00000021-0000-1000-8000-0026BB765291", - "value": "SIMPLEconnect" - }, - { - "format": "string", - "iid": 5, - "perms": [ - "pr" - ], - "type": "00000030-0000-1000-8000-0026BB765291", - "value": "1234567890abcd" - }, - { - "format": "bool", - "iid": 6, - "perms": [ - "pw" - ], - "type": "00000014-0000-1000-8000-0026BB765291" - }, - { - "format": "string", - "iid": 7, - "perms": [ - "pr" - ], - "type": "54", - "value": "0.22" - } - ], - "iid": 1, - "stype": "accessory-information", - "type": "0000003E-0000-1000-8000-0026BB765291" - }, - { - "characteristics": [ - { - "ev": false, - "format": "bool", - "iid": 9, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "00000025-0000-1000-8000-0026BB765291", - "value": false - }, - { - "format": "string", - "iid": 10, - "perms": [ - "pr" - ], - "type": "00000023-0000-1000-8000-0026BB765291", - "value": "Hunter Fan" - }, - { - "ev": false, - "format": "float", - "iid": 11, - "maxValue": 100.0, - "minStep": 25.0, - "minValue": 0.0, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "00000029-0000-1000-8000-0026BB765291", - "value": 0.0 - }, - { - "ev": false, - "format": "int", - "iid": 12, - "maxValue": 1, - "minValue": 0, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "00000028-0000-1000-8000-0026BB765291", - "value": 0 - }, - { - "description": "Set Fan Fast On", - "ev": false, - "format": "bool", - "iid": 13, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "2BD83CC0-6C60-11E5-A837-0800200C9A66", - "value": false - }, - { - "description": "Set Fan Fast Off", - "ev": false, - "format": "bool", - "iid": 14, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "2BD83CC1-6C60-11E5-A837-0800200C9A66", - "value": false - }, - { - "description": "Is BLDC in Scope", - "ev": false, - "format": "bool", - "iid": 15, - "perms": [ - "pr", - "ev" - ], - "type": "2BD83CC5-6C60-11E5-A837-0800200C9A66", - "value": false - }, - { - "ev": false, - "format": "bool", - "iid": 16, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "00000005-0000-1000-8000-0026BB765291", - "value": false - }, - { - "ev": false, - "format": "int", - "iid": 17, - "perms": [ - "pr", - "ev" - ], - "type": "2BD83CC4-6C60-11E5-A837-0800200C9A66", - "value": 341 - }, - { - "ev": false, - "format": "int", - "iid": 18, - "perms": [ - "pr", - "ev" - ], - "type": "2BD83CC3-6C60-11E5-A837-0800200C9A66", - "value": 0 - } - ], - "iid": 8, - "stype": "fan", - "type": "00000040-0000-1000-8000-0026BB765291" - }, - { - "characteristics": [ - { - "description": "FW Upgrade supported types", - "format": "string", - "iid": 20, - "perms": [ - "pr" - ], - "type": "151909D2-3802-11E4-916C-0800200C9A66", - "value": "url,data" - }, - { - "description": "FW Upgrade URL", - "format": "string", - "iid": 21, - "maxLen": 256, - "perms": [ - "pw" - ], - "type": "151909D1-3802-11E4-916C-0800200C9A66" - }, - { - "description": "FW Upgrade Status", - "ev": false, - "format": "int", - "iid": 22, - "perms": [ - "pr", - "ev" - ], - "type": "151909D6-3802-11E4-916C-0800200C9A66", - "value": 0 - }, - { - "description": "FW Upgrade Data", - "format": "data", - "iid": 23, - "perms": [ - "pw" - ], - "type": "151909D7-3802-11E4-916C-0800200C9A66" - } - ], - "iid": 19, - "stype": "Unknown Service: 151909D0-3802-11E4-916C-0800200C9A66", - "type": "151909D0-3802-11E4-916C-0800200C9A66" - }, - { - "characteristics": [ - { - "description": "FW Upgrade supported types", - "format": "string", - "iid": 25, - "perms": [ - "pr" - ], - "type": "151909D2-3802-11E4-916C-0800200C9A66", - "value": "url,data" - }, - { - "description": "FW Upgrade URL", - "format": "string", - "iid": 26, - "maxLen": 256, - "perms": [ - "pw" - ], - "type": "151909D1-3802-11E4-916C-0800200C9A66" - }, - { - "description": "FW Upgrade Status", - "ev": false, - "format": "int", - "iid": 27, - "perms": [ - "pr", - "ev" - ], - "type": "151909D6-3802-11E4-916C-0800200C9A66", - "value": 0 - }, - { - "description": "FW Upgrade Data", - "format": "data", - "iid": 28, - "perms": [ - "pw" - ], - "type": "151909D7-3802-11E4-916C-0800200C9A66" - } - ], - "iid": 24, - "stype": "Unknown Service: 151909D8-3802-11E4-916C-0800200C9A66", - "type": "151909D8-3802-11E4-916C-0800200C9A66" - }, - { - "characteristics": [ - { - "ev": false, - "format": "bool", - "iid": 30, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "00000025-0000-1000-8000-0026BB765291", - "value": true - }, - { - "format": "string", - "iid": 31, - "perms": [ - "pr" - ], - "type": "00000023-0000-1000-8000-0026BB765291", - "value": "Hunter Light" - }, - { - "ev": false, - "format": "int", - "iid": 32, - "maxValue": 100, - "minStep": 10, - "minValue": 0, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "00000008-0000-1000-8000-0026BB765291", - "value": 30 - }, - { - "description": "Set Light Dimming", - "ev": false, - "format": "bool", - "iid": 33, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "151909DC-3802-11E4-916C-0800200C9A66", - "value": true - }, - { - "description": "Set Light Security", - "ev": false, - "format": "bool", - "iid": 34, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "2BD815B0-6C60-11E5-A837-0800200C9A66", - "value": false - }, - { - "description": "Get Light Power", - "ev": false, - "format": "bool", - "iid": 35, - "perms": [ - "pr", - "ev" - ], - "type": "2BD815B5-6C60-11E5-A837-0800200C9A66", - "value": true - } - ], - "iid": 29, - "stype": "lightbulb", - "type": "00000043-0000-1000-8000-0026BB765291" - }, - { - "characteristics": [ - { - "ev": false, - "format": "int", - "iid": 37, - "perms": [ - "pr", - "ev" - ], - "type": "2BD83CC6-6C60-11E5-A837-0800200C9A66", - "value": -65 - }, - { - "ev": false, - "format": "bool", - "iid": 38, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "2BD83CC7-6C60-11E5-A837-0800200C9A66", - "value": false - }, - { - "ev": false, - "format": "string", - "iid": 39, - "maxLen": 256, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "0049CFF1-4B37-11E5-B970-0800200C9A66", - "value": "url, data" - }, - { - "ev": false, - "format": "string", - "iid": 40, - "maxLen": 256, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "2BD83CC2-6C60-11E5-A837-0800200C9A66", - "value": "url, data" - }, - { - "ev": false, - "format": "int", - "iid": 41, - "maxValue": 110, - "minValue": 0, - "perms": [ - "pr", - "ev" - ], - "type": "0049CFF2-4B37-11E5-B970-0800200C9A66", - "value": 0 - }, - { - "ev": false, - "format": "bool", - "iid": 42, - "perms": [ - "pr", - "ev" - ], - "type": "0049CFF3-4B37-11E5-B970-0800200C9A66", - "value": false - } - ], - "iid": 36, - "stype": "Unknown Service: 0049CFF0-4B37-11E5-B970-0800200C9A66", - "type": "0049CFF0-4B37-11E5-B970-0800200C9A66" - }, - { - "characteristics": [ - { - "ev": false, - "format": "string", - "iid": 44, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "2BD815B2-6C60-11E5-A837-0800200C9A66", - "value": "NULL" - }, - { - "ev": false, - "format": "int", - "iid": 45, - "maxValue": 63, - "minValue": 0, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "2BD815B4-6C60-11E5-A837-0800200C9A66", - "value": 7 - }, - { - "ev": false, - "format": "bool", - "iid": 46, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "2BD815B3-6C60-11E5-A837-0800200C9A66", - "value": true - } - ], - "iid": 43, - "stype": "Unknown Service: 2BD815B1-6C60-11E5-A837-0800200C9A66", - "type": "2BD815B1-6C60-11E5-A837-0800200C9A66" - }, - { - "characteristics": [ - { - "ev": false, - "format": "int", - "iid": 48, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "E836DC61-6C6E-11E5-A837-0800200C9A66", - "value": 4294967295 - }, - { - "ev": false, - "format": "int", - "iid": 49, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "E836DC62-6C6E-11E5-A837-0800200C9A66", - "value": 4294967295 - }, - { - "ev": false, - "format": "int", - "iid": 50, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "E836DC63-6C6E-11E5-A837-0800200C9A66", - "value": 4294967295 - }, - { - "ev": false, - "format": "int", - "iid": 51, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "E836DC64-6C6E-11E5-A837-0800200C9A66", - "value": 4294967295 - }, - { - "ev": false, - "format": "int", - "iid": 52, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "E836DC65-6C6E-11E5-A837-0800200C9A66", - "value": 4294967295 - }, - { - "ev": false, - "format": "int", - "iid": 53, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "E836DC66-6C6E-11E5-A837-0800200C9A66", - "value": 4294967295 - }, - { - "ev": false, - "format": "int", - "iid": 54, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "E836DC67-6C6E-11E5-A837-0800200C9A66", - "value": 4294967295 - }, - { - "ev": false, - "format": "int", - "iid": 55, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "E836DC68-6C6E-11E5-A837-0800200C9A66", - "value": 4294967295 - }, - { - "ev": false, - "format": "int", - "iid": 56, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "E836DC69-6C6E-11E5-A837-0800200C9A66", - "value": 4294967295 - }, - { - "ev": false, - "format": "int", - "iid": 57, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "E836DC6A-6C6E-11E5-A837-0800200C9A66", - "value": 4294967295 - }, - { - "ev": false, - "format": "int", - "iid": 58, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "E836DC6B-6C6E-11E5-A837-0800200C9A66", - "value": 4294967295 - }, - { - "ev": false, - "format": "int", - "iid": 59, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "E836DC6C-6C6E-11E5-A837-0800200C9A66", - "value": 4294967295 - }, - { - "ev": false, - "format": "int", - "iid": 60, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "E836DC6D-6C6E-11E5-A837-0800200C9A66", - "value": 4294967295 - }, - { - "ev": false, - "format": "int", - "iid": 61, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "E836DC6E-6C6E-11E5-A837-0800200C9A66", - "value": 4294967295 - }, - { - "ev": false, - "format": "int", - "iid": 62, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "E836DC6F-6C6E-11E5-A837-0800200C9A66", - "value": 4294967295 - }, - { - "ev": false, - "format": "int", - "iid": 63, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "E836DC70-6C6E-11E5-A837-0800200C9A66", - "value": 4294967295 - }, - { - "ev": false, - "format": "int", - "iid": 64, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "E836DC71-6C6E-11E5-A837-0800200C9A66", - "value": 4294967295 - }, - { - "ev": false, - "format": "int", - "iid": 65, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "E836DC72-6C6E-11E5-A837-0800200C9A66", - "value": 4294967295 - }, - { - "ev": false, - "format": "int", - "iid": 66, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "E836DC73-6C6E-11E5-A837-0800200C9A66", - "value": 4294967295 - }, - { - "ev": false, - "format": "int", - "iid": 67, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "E836DC74-6C6E-11E5-A837-0800200C9A66", - "value": 4294967295 - }, - { - "ev": false, - "format": "int", - "iid": 68, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "CC9EA121-FC1C-11E5-A837-0800200C9A66", - "value": 4294901760 - }, - { - "ev": false, - "format": "int", - "iid": 69, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "CC9EA120-FC1C-11E5-A837-0800200C9A66", - "value": 4294901760 - } - ], - "iid": 47, - "stype": "Unknown Service: E836DC60-6C6E-11E5-A837-0800200C9A66", - "type": "E836DC60-6C6E-11E5-A837-0800200C9A66" - } - ] - } -] \ No newline at end of file + { + "aid": 1, + "services": [ + { + "characteristics": [ + { + "format": "string", + "iid": 2, + "perms": ["pr"], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "SIMPLEconnect Fan-06F674" + }, + { + "format": "string", + "iid": 3, + "perms": ["pr"], + "type": "00000020-0000-1000-8000-0026BB765291", + "value": "Hunter Fan" + }, + { + "format": "string", + "iid": 4, + "perms": ["pr"], + "type": "00000021-0000-1000-8000-0026BB765291", + "value": "SIMPLEconnect" + }, + { + "format": "string", + "iid": 5, + "perms": ["pr"], + "type": "00000030-0000-1000-8000-0026BB765291", + "value": "1234567890abcd" + }, + { + "format": "bool", + "iid": 6, + "perms": ["pw"], + "type": "00000014-0000-1000-8000-0026BB765291" + }, + { + "format": "string", + "iid": 7, + "perms": ["pr"], + "type": "54", + "value": "0.22" + } + ], + "iid": 1, + "stype": "accessory-information", + "type": "0000003E-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "ev": false, + "format": "bool", + "iid": 9, + "perms": ["pr", "pw", "ev"], + "type": "00000025-0000-1000-8000-0026BB765291", + "value": false + }, + { + "format": "string", + "iid": 10, + "perms": ["pr"], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Hunter Fan" + }, + { + "ev": false, + "format": "float", + "iid": 11, + "maxValue": 100.0, + "minStep": 25.0, + "minValue": 0.0, + "perms": ["pr", "pw", "ev"], + "type": "00000029-0000-1000-8000-0026BB765291", + "value": 0.0 + }, + { + "ev": false, + "format": "int", + "iid": 12, + "maxValue": 1, + "minValue": 0, + "perms": ["pr", "pw", "ev"], + "type": "00000028-0000-1000-8000-0026BB765291", + "value": 0 + }, + { + "description": "Set Fan Fast On", + "ev": false, + "format": "bool", + "iid": 13, + "perms": ["pr", "pw", "ev"], + "type": "2BD83CC0-6C60-11E5-A837-0800200C9A66", + "value": false + }, + { + "description": "Set Fan Fast Off", + "ev": false, + "format": "bool", + "iid": 14, + "perms": ["pr", "pw", "ev"], + "type": "2BD83CC1-6C60-11E5-A837-0800200C9A66", + "value": false + }, + { + "description": "Is BLDC in Scope", + "ev": false, + "format": "bool", + "iid": 15, + "perms": ["pr", "ev"], + "type": "2BD83CC5-6C60-11E5-A837-0800200C9A66", + "value": false + }, + { + "ev": false, + "format": "bool", + "iid": 16, + "perms": ["pr", "pw", "ev"], + "type": "00000005-0000-1000-8000-0026BB765291", + "value": false + }, + { + "ev": false, + "format": "int", + "iid": 17, + "perms": ["pr", "ev"], + "type": "2BD83CC4-6C60-11E5-A837-0800200C9A66", + "value": 341 + }, + { + "ev": false, + "format": "int", + "iid": 18, + "perms": ["pr", "ev"], + "type": "2BD83CC3-6C60-11E5-A837-0800200C9A66", + "value": 0 + } + ], + "iid": 8, + "stype": "fan", + "type": "00000040-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "description": "FW Upgrade supported types", + "format": "string", + "iid": 20, + "perms": ["pr"], + "type": "151909D2-3802-11E4-916C-0800200C9A66", + "value": "url,data" + }, + { + "description": "FW Upgrade URL", + "format": "string", + "iid": 21, + "maxLen": 256, + "perms": ["pw"], + "type": "151909D1-3802-11E4-916C-0800200C9A66" + }, + { + "description": "FW Upgrade Status", + "ev": false, + "format": "int", + "iid": 22, + "perms": ["pr", "ev"], + "type": "151909D6-3802-11E4-916C-0800200C9A66", + "value": 0 + }, + { + "description": "FW Upgrade Data", + "format": "data", + "iid": 23, + "perms": ["pw"], + "type": "151909D7-3802-11E4-916C-0800200C9A66" + } + ], + "iid": 19, + "stype": "Unknown Service: 151909D0-3802-11E4-916C-0800200C9A66", + "type": "151909D0-3802-11E4-916C-0800200C9A66" + }, + { + "characteristics": [ + { + "description": "FW Upgrade supported types", + "format": "string", + "iid": 25, + "perms": ["pr"], + "type": "151909D2-3802-11E4-916C-0800200C9A66", + "value": "url,data" + }, + { + "description": "FW Upgrade URL", + "format": "string", + "iid": 26, + "maxLen": 256, + "perms": ["pw"], + "type": "151909D1-3802-11E4-916C-0800200C9A66" + }, + { + "description": "FW Upgrade Status", + "ev": false, + "format": "int", + "iid": 27, + "perms": ["pr", "ev"], + "type": "151909D6-3802-11E4-916C-0800200C9A66", + "value": 0 + }, + { + "description": "FW Upgrade Data", + "format": "data", + "iid": 28, + "perms": ["pw"], + "type": "151909D7-3802-11E4-916C-0800200C9A66" + } + ], + "iid": 24, + "stype": "Unknown Service: 151909D8-3802-11E4-916C-0800200C9A66", + "type": "151909D8-3802-11E4-916C-0800200C9A66" + }, + { + "characteristics": [ + { + "ev": false, + "format": "bool", + "iid": 30, + "perms": ["pr", "pw", "ev"], + "type": "00000025-0000-1000-8000-0026BB765291", + "value": true + }, + { + "format": "string", + "iid": 31, + "perms": ["pr"], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Hunter Light" + }, + { + "ev": false, + "format": "int", + "iid": 32, + "maxValue": 100, + "minStep": 10, + "minValue": 0, + "perms": ["pr", "pw", "ev"], + "type": "00000008-0000-1000-8000-0026BB765291", + "value": 30 + }, + { + "description": "Set Light Dimming", + "ev": false, + "format": "bool", + "iid": 33, + "perms": ["pr", "pw", "ev"], + "type": "151909DC-3802-11E4-916C-0800200C9A66", + "value": true + }, + { + "description": "Set Light Security", + "ev": false, + "format": "bool", + "iid": 34, + "perms": ["pr", "pw", "ev"], + "type": "2BD815B0-6C60-11E5-A837-0800200C9A66", + "value": false + }, + { + "description": "Get Light Power", + "ev": false, + "format": "bool", + "iid": 35, + "perms": ["pr", "ev"], + "type": "2BD815B5-6C60-11E5-A837-0800200C9A66", + "value": true + } + ], + "iid": 29, + "stype": "lightbulb", + "type": "00000043-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "ev": false, + "format": "int", + "iid": 37, + "perms": ["pr", "ev"], + "type": "2BD83CC6-6C60-11E5-A837-0800200C9A66", + "value": -65 + }, + { + "ev": false, + "format": "bool", + "iid": 38, + "perms": ["pr", "pw", "ev"], + "type": "2BD83CC7-6C60-11E5-A837-0800200C9A66", + "value": false + }, + { + "ev": false, + "format": "string", + "iid": 39, + "maxLen": 256, + "perms": ["pr", "pw", "ev"], + "type": "0049CFF1-4B37-11E5-B970-0800200C9A66", + "value": "url, data" + }, + { + "ev": false, + "format": "string", + "iid": 40, + "maxLen": 256, + "perms": ["pr", "pw", "ev"], + "type": "2BD83CC2-6C60-11E5-A837-0800200C9A66", + "value": "url, data" + }, + { + "ev": false, + "format": "int", + "iid": 41, + "maxValue": 110, + "minValue": 0, + "perms": ["pr", "ev"], + "type": "0049CFF2-4B37-11E5-B970-0800200C9A66", + "value": 0 + }, + { + "ev": false, + "format": "bool", + "iid": 42, + "perms": ["pr", "ev"], + "type": "0049CFF3-4B37-11E5-B970-0800200C9A66", + "value": false + } + ], + "iid": 36, + "stype": "Unknown Service: 0049CFF0-4B37-11E5-B970-0800200C9A66", + "type": "0049CFF0-4B37-11E5-B970-0800200C9A66" + }, + { + "characteristics": [ + { + "ev": false, + "format": "string", + "iid": 44, + "perms": ["pr", "pw", "ev"], + "type": "2BD815B2-6C60-11E5-A837-0800200C9A66", + "value": "NULL" + }, + { + "ev": false, + "format": "int", + "iid": 45, + "maxValue": 63, + "minValue": 0, + "perms": ["pr", "pw", "ev"], + "type": "2BD815B4-6C60-11E5-A837-0800200C9A66", + "value": 7 + }, + { + "ev": false, + "format": "bool", + "iid": 46, + "perms": ["pr", "pw", "ev"], + "type": "2BD815B3-6C60-11E5-A837-0800200C9A66", + "value": true + } + ], + "iid": 43, + "stype": "Unknown Service: 2BD815B1-6C60-11E5-A837-0800200C9A66", + "type": "2BD815B1-6C60-11E5-A837-0800200C9A66" + }, + { + "characteristics": [ + { + "ev": false, + "format": "int", + "iid": 48, + "perms": ["pr", "pw", "ev"], + "type": "E836DC61-6C6E-11E5-A837-0800200C9A66", + "value": 4294967295 + }, + { + "ev": false, + "format": "int", + "iid": 49, + "perms": ["pr", "pw", "ev"], + "type": "E836DC62-6C6E-11E5-A837-0800200C9A66", + "value": 4294967295 + }, + { + "ev": false, + "format": "int", + "iid": 50, + "perms": ["pr", "pw", "ev"], + "type": "E836DC63-6C6E-11E5-A837-0800200C9A66", + "value": 4294967295 + }, + { + "ev": false, + "format": "int", + "iid": 51, + "perms": ["pr", "pw", "ev"], + "type": "E836DC64-6C6E-11E5-A837-0800200C9A66", + "value": 4294967295 + }, + { + "ev": false, + "format": "int", + "iid": 52, + "perms": ["pr", "pw", "ev"], + "type": "E836DC65-6C6E-11E5-A837-0800200C9A66", + "value": 4294967295 + }, + { + "ev": false, + "format": "int", + "iid": 53, + "perms": ["pr", "pw", "ev"], + "type": "E836DC66-6C6E-11E5-A837-0800200C9A66", + "value": 4294967295 + }, + { + "ev": false, + "format": "int", + "iid": 54, + "perms": ["pr", "pw", "ev"], + "type": "E836DC67-6C6E-11E5-A837-0800200C9A66", + "value": 4294967295 + }, + { + "ev": false, + "format": "int", + "iid": 55, + "perms": ["pr", "pw", "ev"], + "type": "E836DC68-6C6E-11E5-A837-0800200C9A66", + "value": 4294967295 + }, + { + "ev": false, + "format": "int", + "iid": 56, + "perms": ["pr", "pw", "ev"], + "type": "E836DC69-6C6E-11E5-A837-0800200C9A66", + "value": 4294967295 + }, + { + "ev": false, + "format": "int", + "iid": 57, + "perms": ["pr", "pw", "ev"], + "type": "E836DC6A-6C6E-11E5-A837-0800200C9A66", + "value": 4294967295 + }, + { + "ev": false, + "format": "int", + "iid": 58, + "perms": ["pr", "pw", "ev"], + "type": "E836DC6B-6C6E-11E5-A837-0800200C9A66", + "value": 4294967295 + }, + { + "ev": false, + "format": "int", + "iid": 59, + "perms": ["pr", "pw", "ev"], + "type": "E836DC6C-6C6E-11E5-A837-0800200C9A66", + "value": 4294967295 + }, + { + "ev": false, + "format": "int", + "iid": 60, + "perms": ["pr", "pw", "ev"], + "type": "E836DC6D-6C6E-11E5-A837-0800200C9A66", + "value": 4294967295 + }, + { + "ev": false, + "format": "int", + "iid": 61, + "perms": ["pr", "pw", "ev"], + "type": "E836DC6E-6C6E-11E5-A837-0800200C9A66", + "value": 4294967295 + }, + { + "ev": false, + "format": "int", + "iid": 62, + "perms": ["pr", "pw", "ev"], + "type": "E836DC6F-6C6E-11E5-A837-0800200C9A66", + "value": 4294967295 + }, + { + "ev": false, + "format": "int", + "iid": 63, + "perms": ["pr", "pw", "ev"], + "type": "E836DC70-6C6E-11E5-A837-0800200C9A66", + "value": 4294967295 + }, + { + "ev": false, + "format": "int", + "iid": 64, + "perms": ["pr", "pw", "ev"], + "type": "E836DC71-6C6E-11E5-A837-0800200C9A66", + "value": 4294967295 + }, + { + "ev": false, + "format": "int", + "iid": 65, + "perms": ["pr", "pw", "ev"], + "type": "E836DC72-6C6E-11E5-A837-0800200C9A66", + "value": 4294967295 + }, + { + "ev": false, + "format": "int", + "iid": 66, + "perms": ["pr", "pw", "ev"], + "type": "E836DC73-6C6E-11E5-A837-0800200C9A66", + "value": 4294967295 + }, + { + "ev": false, + "format": "int", + "iid": 67, + "perms": ["pr", "pw", "ev"], + "type": "E836DC74-6C6E-11E5-A837-0800200C9A66", + "value": 4294967295 + }, + { + "ev": false, + "format": "int", + "iid": 68, + "perms": ["pr", "pw", "ev"], + "type": "CC9EA121-FC1C-11E5-A837-0800200C9A66", + "value": 4294901760 + }, + { + "ev": false, + "format": "int", + "iid": 69, + "perms": ["pr", "pw", "ev"], + "type": "CC9EA120-FC1C-11E5-A837-0800200C9A66", + "value": 4294901760 + } + ], + "iid": 47, + "stype": "Unknown Service: E836DC60-6C6E-11E5-A837-0800200C9A66", + "type": "E836DC60-6C6E-11E5-A837-0800200C9A66" + } + ] + } +] diff --git a/tests/components/homekit_controller/fixtures/velux_gateway.json b/tests/components/homekit_controller/fixtures/velux_gateway.json index 1a6f60537b3..c2ceb1d5014 100644 --- a/tests/components/homekit_controller/fixtures/velux_gateway.json +++ b/tests/components/homekit_controller/fixtures/velux_gateway.json @@ -1,380 +1,312 @@ [ - { - "aid": 1, - "services": [ - { - "type": "0000003E-0000-1000-8000-0026BB765291", - "iid": 1, - "characteristics": [ - { - "type": "00000023-0000-1000-8000-0026BB765291", - "iid": 2, - "perms": [ - "pr" - ], - "format": "string", - "value": "VELUX Gateway" - }, - { - "type": "00000020-0000-1000-8000-0026BB765291", - "iid": 3, - "perms": [ - "pr" - ], - "format": "string", - "value": "VELUX" - }, - { - "type": "00000021-0000-1000-8000-0026BB765291", - "iid": 4, - "perms": [ - "pr" - ], - "format": "string", - "value": "VELUX Gateway" - }, - { - "type": "00000030-0000-1000-8000-0026BB765291", - "iid": 5, - "perms": [ - "pr" - ], - "format": "string", - "value": "a1a11a1" - }, - { - "type": "00000014-0000-1000-8000-0026BB765291", - "iid": 6, - "perms": [ - "pw" - ], - "format": "bool" - }, - { - "type": "00000052-0000-1000-8000-0026BB765291", - "iid": 7, - "perms": [ - "pr" - ], - "format": "string", - "value": "70" - } - ], - "hidden": false, - "primary": false - }, - { - "type": "000000A2-0000-1000-8000-0026BB765291", - "iid": 8, - "characteristics": [ - { - "type": "00000037-0000-1000-8000-0026BB765291", - "iid": 9, - "perms": [ - "pr" - ], - "format": "string", - "value": "1.1.0" - } - ], - "hidden": false, - "primary": false - } - ] - }, - { - "aid": 2, - "services": [ - { - "type": "0000003E-0000-1000-8000-0026BB765291", - "iid": 1, - "characteristics": [ - { - "type": "00000023-0000-1000-8000-0026BB765291", - "iid": 2, - "perms": [ - "pr" - ], - "format": "string", - "value": "VELUX Sensor" - }, - { - "type": "00000020-0000-1000-8000-0026BB765291", - "iid": 3, - "perms": [ - "pr" - ], - "format": "string", - "value": "VELUX" - }, - { - "type": "00000021-0000-1000-8000-0026BB765291", - "iid": 4, - "perms": [ - "pr" - ], - "format": "string", - "value": "VELUX Sensor" - }, - { - "type": "00000030-0000-1000-8000-0026BB765291", - "iid": 5, - "perms": [ - "pr" - ], - "format": "string", - "value": "a11b111" - }, - { - "type": "00000014-0000-1000-8000-0026BB765291", - "iid": 7, - "perms": [ - "pw" - ], - "format": "bool" - }, - { - "type": "00000052-0000-1000-8000-0026BB765291", - "iid": 6, - "perms": [ - "pr" - ], - "format": "string", - "value": "16" - } - ], - "hidden": false, - "primary": false - }, - { - "type": "0000008A-0000-1000-8000-0026BB765291", - "iid": 8, - "characteristics": [ - { - "type": "00000023-0000-1000-8000-0026BB765291", - "iid": 9, - "perms": [ - "pr" - ], - "format": "string", - "value": "Temperature sensor" - }, - { - "type": "00000011-0000-1000-8000-0026BB765291", - "iid": 10, - "perms": [ - "pr", - "ev" - ], - "format": "float", - "value": 18.9, - "minValue": 0, - "maxValue": 50, - "minStep": 0.1, - "unit": "celsius" - } - ], - "hidden": false, - "primary": true - }, - { - "type": "00000082-0000-1000-8000-0026BB765291", - "iid": 11, - "characteristics": [ - { - "type": "00000023-0000-1000-8000-0026BB765291", - "iid": 12, - "perms": [ - "pr" - ], - "format": "string", - "value": "Humidity sensor" - }, - { - "type": "00000010-0000-1000-8000-0026BB765291", - "iid": 13, - "perms": [ - "pr", - "ev" - ], - "format": "float", - "value": 58, - "minValue": 0, - "maxValue": 100, - "minStep": 1, - "unit": "percentage" - } - ], - "hidden": false, - "primary": false - }, - { - "type": "00000097-0000-1000-8000-0026BB765291", - "iid": 14, - "characteristics": [ - { - "type": "00000023-0000-1000-8000-0026BB765291", - "iid": 15, - "perms": [ - "pr" - ], - "format": "string", - "value": "Carbon Dioxide sensor" - }, - { - "type": "00000092-0000-1000-8000-0026BB765291", - "iid": 16, - "perms": [ - "pr", - "ev" - ], - "format": "uint8", - "value": 0, - "maxValue": 1, - "minValue": 0, - "minStep": 1 - }, - { - "type": "00000093-0000-1000-8000-0026BB765291", - "iid": 17, - "perms": [ - "pr", - "ev" - ], - "format": "float", - "value": 400, - "minValue": 0, - "maxValue": 5000 - } - ], - "hidden": false, - "primary": false - } - ] - }, - { - "aid": 3, - "services": [ - { - "type": "0000003E-0000-1000-8000-0026BB765291", - "iid": 1, - "characteristics": [ - { - "type": "00000023-0000-1000-8000-0026BB765291", - "iid": 2, - "perms": [ - "pr" - ], - "format": "string", - "value": "VELUX Window" - }, - { - "type": "00000020-0000-1000-8000-0026BB765291", - "iid": 3, - "perms": [ - "pr" - ], - "format": "string", - "value": "VELUX" - }, - { - "type": "00000021-0000-1000-8000-0026BB765291", - "iid": 4, - "perms": [ - "pr" - ], - "format": "string", - "value": "VELUX Window" - }, - { - "type": "00000030-0000-1000-8000-0026BB765291", - "iid": 5, - "perms": [ - "pr" - ], - "format": "string", - "value": "1111111a114a111a" - }, - { - "type": "00000014-0000-1000-8000-0026BB765291", - "iid": 7, - "perms": [ - "pw" - ], - "format": "bool" - }, - { - "type": "00000052-0000-1000-8000-0026BB765291", - "iid": 6, - "perms": [ - "pr" - ], - "format": "string", - "value": "48" - } - ], - "hidden": false, - "primary": false - }, - { - "type": "0000008B-0000-1000-8000-0026BB765291", - "iid": 8, - "characteristics": [ - { - "type": "00000023-0000-1000-8000-0026BB765291", - "iid": 9, - "perms": [ - "pr" - ], - "format": "string", - "value": "Roof Window" - }, - { - "type": "0000007C-0000-1000-8000-0026BB765291", - "iid": 11, - "perms": [ - "pr", - "pw", - "ev" - ], - "format": "uint8", - "value": 0, - "maxValue": 100, - "minValue": 0, - "unit": "percentage", - "minStep": 1 - }, - { - "type": "0000006D-0000-1000-8000-0026BB765291", - "iid": 10, - "perms": [ - "pr", - "ev" - ], - "format": "uint8", - "value": 0, - "maxValue": 100, - "minValue": 0, - "unit": "percentage", - "minStep": 1 - }, - { - "type": "00000072-0000-1000-8000-0026BB765291", - "iid": 12, - "perms": [ - "pr", - "ev" - ], - "format": "uint8", - "value": 2, - "maxValue": 2, - "minValue": 0, - "minStep": 1 - } - ], - "hidden": false, - "primary": true - } - ] - } -] \ No newline at end of file + { + "aid": 1, + "services": [ + { + "type": "0000003E-0000-1000-8000-0026BB765291", + "iid": 1, + "characteristics": [ + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 2, + "perms": ["pr"], + "format": "string", + "value": "VELUX Gateway" + }, + { + "type": "00000020-0000-1000-8000-0026BB765291", + "iid": 3, + "perms": ["pr"], + "format": "string", + "value": "VELUX" + }, + { + "type": "00000021-0000-1000-8000-0026BB765291", + "iid": 4, + "perms": ["pr"], + "format": "string", + "value": "VELUX Gateway" + }, + { + "type": "00000030-0000-1000-8000-0026BB765291", + "iid": 5, + "perms": ["pr"], + "format": "string", + "value": "a1a11a1" + }, + { + "type": "00000014-0000-1000-8000-0026BB765291", + "iid": 6, + "perms": ["pw"], + "format": "bool" + }, + { + "type": "00000052-0000-1000-8000-0026BB765291", + "iid": 7, + "perms": ["pr"], + "format": "string", + "value": "70" + } + ], + "hidden": false, + "primary": false + }, + { + "type": "000000A2-0000-1000-8000-0026BB765291", + "iid": 8, + "characteristics": [ + { + "type": "00000037-0000-1000-8000-0026BB765291", + "iid": 9, + "perms": ["pr"], + "format": "string", + "value": "1.1.0" + } + ], + "hidden": false, + "primary": false + } + ] + }, + { + "aid": 2, + "services": [ + { + "type": "0000003E-0000-1000-8000-0026BB765291", + "iid": 1, + "characteristics": [ + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 2, + "perms": ["pr"], + "format": "string", + "value": "VELUX Sensor" + }, + { + "type": "00000020-0000-1000-8000-0026BB765291", + "iid": 3, + "perms": ["pr"], + "format": "string", + "value": "VELUX" + }, + { + "type": "00000021-0000-1000-8000-0026BB765291", + "iid": 4, + "perms": ["pr"], + "format": "string", + "value": "VELUX Sensor" + }, + { + "type": "00000030-0000-1000-8000-0026BB765291", + "iid": 5, + "perms": ["pr"], + "format": "string", + "value": "a11b111" + }, + { + "type": "00000014-0000-1000-8000-0026BB765291", + "iid": 7, + "perms": ["pw"], + "format": "bool" + }, + { + "type": "00000052-0000-1000-8000-0026BB765291", + "iid": 6, + "perms": ["pr"], + "format": "string", + "value": "16" + } + ], + "hidden": false, + "primary": false + }, + { + "type": "0000008A-0000-1000-8000-0026BB765291", + "iid": 8, + "characteristics": [ + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 9, + "perms": ["pr"], + "format": "string", + "value": "Temperature sensor" + }, + { + "type": "00000011-0000-1000-8000-0026BB765291", + "iid": 10, + "perms": ["pr", "ev"], + "format": "float", + "value": 18.9, + "minValue": 0, + "maxValue": 50, + "minStep": 0.1, + "unit": "celsius" + } + ], + "hidden": false, + "primary": true + }, + { + "type": "00000082-0000-1000-8000-0026BB765291", + "iid": 11, + "characteristics": [ + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 12, + "perms": ["pr"], + "format": "string", + "value": "Humidity sensor" + }, + { + "type": "00000010-0000-1000-8000-0026BB765291", + "iid": 13, + "perms": ["pr", "ev"], + "format": "float", + "value": 58, + "minValue": 0, + "maxValue": 100, + "minStep": 1, + "unit": "percentage" + } + ], + "hidden": false, + "primary": false + }, + { + "type": "00000097-0000-1000-8000-0026BB765291", + "iid": 14, + "characteristics": [ + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 15, + "perms": ["pr"], + "format": "string", + "value": "Carbon Dioxide sensor" + }, + { + "type": "00000092-0000-1000-8000-0026BB765291", + "iid": 16, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 0, + "maxValue": 1, + "minValue": 0, + "minStep": 1 + }, + { + "type": "00000093-0000-1000-8000-0026BB765291", + "iid": 17, + "perms": ["pr", "ev"], + "format": "float", + "value": 400, + "minValue": 0, + "maxValue": 5000 + } + ], + "hidden": false, + "primary": false + } + ] + }, + { + "aid": 3, + "services": [ + { + "type": "0000003E-0000-1000-8000-0026BB765291", + "iid": 1, + "characteristics": [ + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 2, + "perms": ["pr"], + "format": "string", + "value": "VELUX Window" + }, + { + "type": "00000020-0000-1000-8000-0026BB765291", + "iid": 3, + "perms": ["pr"], + "format": "string", + "value": "VELUX" + }, + { + "type": "00000021-0000-1000-8000-0026BB765291", + "iid": 4, + "perms": ["pr"], + "format": "string", + "value": "VELUX Window" + }, + { + "type": "00000030-0000-1000-8000-0026BB765291", + "iid": 5, + "perms": ["pr"], + "format": "string", + "value": "1111111a114a111a" + }, + { + "type": "00000014-0000-1000-8000-0026BB765291", + "iid": 7, + "perms": ["pw"], + "format": "bool" + }, + { + "type": "00000052-0000-1000-8000-0026BB765291", + "iid": 6, + "perms": ["pr"], + "format": "string", + "value": "48" + } + ], + "hidden": false, + "primary": false + }, + { + "type": "0000008B-0000-1000-8000-0026BB765291", + "iid": 8, + "characteristics": [ + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 9, + "perms": ["pr"], + "format": "string", + "value": "Roof Window" + }, + { + "type": "0000007C-0000-1000-8000-0026BB765291", + "iid": 11, + "perms": ["pr", "pw", "ev"], + "format": "uint8", + "value": 0, + "maxValue": 100, + "minValue": 0, + "unit": "percentage", + "minStep": 1 + }, + { + "type": "0000006D-0000-1000-8000-0026BB765291", + "iid": 10, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 0, + "maxValue": 100, + "minValue": 0, + "unit": "percentage", + "minStep": 1 + }, + { + "type": "00000072-0000-1000-8000-0026BB765291", + "iid": 12, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 2, + "maxValue": 2, + "minValue": 0, + "minStep": 1 + } + ], + "hidden": false, + "primary": true + } + ] + } +] diff --git a/tests/components/homekit_controller/fixtures/vocolinc_flowerbud.json b/tests/components/homekit_controller/fixtures/vocolinc_flowerbud.json index 012c03471f3..2ec39e2e039 100644 --- a/tests/components/homekit_controller/fixtures/vocolinc_flowerbud.json +++ b/tests/components/homekit_controller/fixtures/vocolinc_flowerbud.json @@ -1,467 +1,358 @@ [ - { - "aid": 1, - "services": [ - { - "characteristics": [ - { - "format": "bool", - "iid": 2, - "perms": [ - "pw" - ], - "type": "00000014-0000-1000-8000-0026BB765291" - }, - { - "format": "string", - "iid": 3, - "perms": [ - "pr" - ], - "type": "00000020-0000-1000-8000-0026BB765291", - "value": "VOCOlinc" - }, - { - "format": "string", - "iid": 4, - "perms": [ - "pr" - ], - "type": "00000021-0000-1000-8000-0026BB765291", - "value": "Flowerbud" - }, - { - "format": "string", - "iid": 5, - "perms": [ - "pr" - ], - "type": "00000023-0000-1000-8000-0026BB765291", - "value": "VOCOlinc-Flowerbud-0d324b" - }, - { - "format": "string", - "iid": 6, - "perms": [ - "pr" - ], - "type": "00000030-0000-1000-8000-0026BB765291", - "value": "AM01121849000327" - }, - { - "description": "", - "format": "string", - "iid": 7, - "perms": [ - "pr" - ], - "type": "00000052-0000-1000-8000-0026BB765291", - "value": "3.121.2" - }, - { - "format": "string", - "iid": 8, - "perms": [ - "pr" - ], - "type": "00000053-0000-1000-8000-0026BB765291", - "value": "0.1" - } - ], - "iid": 1, - "stype": "accessory-information", - "type": "0000003E-0000-1000-8000-0026BB765291" - }, - { - "characteristics": [ - { - "description": "rssi_report_switch", - "format": "bool", - "iid": 81, - "perms": [ - "pr", - "pw" - ], - "type": "D9959C8A-809A-4F75-92D7-71F630AC2925", - "value": 0 - }, - { - "description": "rssi_report_value", - "format": "uint8", - "iid": 82, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "8137182C-6904-4FB9-ADCC-61CECA85CE48", - "value": 0 - } - ], - "iid": 80, - "stype": "Unknown Service: C635EF5C-5BBC-4F96-B7DA-6669069A4B32", - "type": "C635EF5C-5BBC-4F96-B7DA-6669069A4B32" - }, - { - "characteristics": [ - { - "format": "string", - "iid": 31, - "perms": [ - "pr" - ], - "type": "00000023-0000-1000-8000-0026BB765291", - "value": "FLOWERBUD" - }, - { - "format": "uint8", - "iid": 32, - "maxValue": 1, - "minStep": 1, - "minValue": 0, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "000000B0-0000-1000-8000-0026BB765291", - "value": 0 - }, - { - "format": "float", - "iid": 33, - "maxValue": 100.0, - "minStep": 1.0, - "minValue": 0.0, - "perms": [ - "pr", - "ev" - ], - "type": "00000010-0000-1000-8000-0026BB765291", - "unit": "percentage", - "value": 45.0 - }, - { - "format": "uint8", - "iid": 34, - "maxValue": 2, - "minStep": 1, - "minValue": 0, - "perms": [ - "pr", - "ev" - ], - "type": "000000B3-0000-1000-8000-0026BB765291", - "value": 0 - }, - { - "format": "uint8", - "iid": 35, - "maxValue": 1, - "minStep": 1, - "minValue": 1, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "000000B4-0000-1000-8000-0026BB765291", - "value": 1 - }, - { - "format": "uint8", - "iid": 36, - "maxValue": 1, - "minStep": 1, - "minValue": 0, - "perms": [ - "pr", - "ev" - ], - "type": "36158AC8-5191-4AE2-9EF5-1D6722E88E3D", - "value": 1 - }, - { - "description": "spray quantity", - "format": "uint8", - "iid": 38, - "maxValue": 5, - "minStep": 1, - "minValue": 1, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "69D52519-0A4E-4898-8335-4739F9116D0A", - "value": 5 - }, - { - "format": "float", - "iid": 39, - "maxValue": 100.0, - "minStep": 1.0, - "minValue": 0.0, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "000000CA-0000-1000-8000-0026BB765291", - "unit": "percentage", - "value": 100.0 - }, - { - "description": "humidifier_timer_setting", - "format": "data", - "iid": 40, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "F84B3138-E44F-49B9-AA91-9E1736C247C0", - "value": "AA==" - }, - { - "description": "humidifier_countdown", - "format": "data", - "iid": 41, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "43CE176B-2933-4034-98A7-AD215BEEBF2F", - "value": "AA==" - } - ], - "iid": 30, - "stype": "humidifier-dehumidifier", - "type": "000000BD-0000-1000-8000-0026BB765291" - }, - { - "characteristics": [ - { - "format": "string", - "iid": 10, - "perms": [ - "pr" - ], - "type": "00000023-0000-1000-8000-0026BB765291", - "value": "Mood Light" - }, - { - "format": "bool", - "iid": 11, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "00000025-0000-1000-8000-0026BB765291", - "value": true - }, - { - "format": "int", - "iid": 12, - "maxValue": 100, - "minStep": 1, - "minValue": 0, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "00000008-0000-1000-8000-0026BB765291", - "unit": "percentage", - "value": 50 - }, - { - "format": "float", - "iid": 13, - "maxValue": 360.0, - "minStep": 1.0, - "minValue": 0.0, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "00000013-0000-1000-8000-0026BB765291", - "unit": "arcdegrees", - "value": 120.0 - }, - { - "format": "float", - "iid": 14, - "maxValue": 100.0, - "minStep": 1.0, - "minValue": 0.0, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "0000002F-0000-1000-8000-0026BB765291", - "unit": "percentage", - "value": 100.0 - }, - { - "description": "lb_timer_setting", - "format": "data", - "iid": 63, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "A30DFE91-271A-42A5-88BA-00E3FF5488AD", - "value": "AA==" - }, - { - "description": "light effect mode", - "format": "uint8", - "iid": 64, - "maxValue": 31, - "minStep": 1, - "minValue": 0, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "146889FC-7C42-429B-93AB-E80F79759E90", - "value": 0 - }, - { - "description": "light effect flag", - "format": "uint32", - "iid": 73, - "perms": [ - "pr" - ], - "type": "9D4B479D-9EFB-4739-98F3-B33E6543BF7B", - "value": 7 - }, - { - "description": "flashing mode", - "format": "data", - "iid": 65, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "2C42B339-6EC9-4ED5-8DBF-FFCCC721B144", - "value": "AA==" - }, - { - "description": "smoothing mode", - "format": "data", - "iid": 66, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "A3663C89-DC18-42EF-8297-910A4C0C9B61", - "value": "AA==" - }, - { - "description": "breathing mode", - "format": "data", - "iid": 67, - "perms": [ - "pr", - "pw", - "ev" - ], - "type": "6533B15C-AECB-455F-8896-20B125390F61", - "value": "AA==" - } - ], - "iid": 9, - "stype": "lightbulb", - "type": "00000043-0000-1000-8000-0026BB765291" - }, - { - "characteristics": [ - { - "description": "time_zone", - "format": "int", - "iid": 50, - "maxValue": 1400, - "minStep": 1, - "minValue": -1200, - "perms": [ - "pr", - "pw" - ], - "type": "38396B8E-161B-4A77-AF3F-C4DAC0BE9B74", - "value": 0 - }, - { - "description": "hour_date_time", - "format": "int", - "iid": 51, - "perms": [ - "pr", - "pw" - ], - "type": "71216CD3-209E-40CC-BEA0-71A2A9458E13", - "value": 0 - } - ], - "iid": 48, - "stype": "Unknown Service: 961EBB65-A1E3-4F34-BD31-86552706FE40", - "type": "961EBB65-A1E3-4F34-BD31-86552706FE40" - }, - { - "characteristics": [ - { - "description": "fm_upgrade_status", - "format": "int", - "iid": 21, - "perms": [ - "pr", - "ev" - ], - "type": "49DDDE07-C3FA-499E-8055-58E154E04F34", - "value": 0 - }, - { - "description": "fm_upgrade_url", - "format": "string", - "iid": 22, - "maxLen": 256, - "perms": [ - "pw" - ], - "type": "4C203E30-EB25-466D-9980-C6C2E14BF6AA" - } - ], - "hidden": true, - "iid": 20, - "stype": "Unknown Service: 3138B537-E830-4F52-90A7-D6FDB000BF97", - "type": "3138B537-E830-4F52-90A7-D6FDB000BF97" - }, - { - "characteristics": [ - { - "format": "string", - "iid": 24, - "perms": [ - "pr" - ], - "type": "00000037-0000-1000-8000-0026BB765291", - "value": "1.1.0" - } - ], - "iid": 23, - "stype": "service", - "type": "000000A2-0000-1000-8000-0026BB765291" - } - ] - } + { + "aid": 1, + "services": [ + { + "characteristics": [ + { + "format": "bool", + "iid": 2, + "perms": ["pw"], + "type": "00000014-0000-1000-8000-0026BB765291" + }, + { + "format": "string", + "iid": 3, + "perms": ["pr"], + "type": "00000020-0000-1000-8000-0026BB765291", + "value": "VOCOlinc" + }, + { + "format": "string", + "iid": 4, + "perms": ["pr"], + "type": "00000021-0000-1000-8000-0026BB765291", + "value": "Flowerbud" + }, + { + "format": "string", + "iid": 5, + "perms": ["pr"], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "VOCOlinc-Flowerbud-0d324b" + }, + { + "format": "string", + "iid": 6, + "perms": ["pr"], + "type": "00000030-0000-1000-8000-0026BB765291", + "value": "AM01121849000327" + }, + { + "description": "", + "format": "string", + "iid": 7, + "perms": ["pr"], + "type": "00000052-0000-1000-8000-0026BB765291", + "value": "3.121.2" + }, + { + "format": "string", + "iid": 8, + "perms": ["pr"], + "type": "00000053-0000-1000-8000-0026BB765291", + "value": "0.1" + } + ], + "iid": 1, + "stype": "accessory-information", + "type": "0000003E-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "description": "rssi_report_switch", + "format": "bool", + "iid": 81, + "perms": ["pr", "pw"], + "type": "D9959C8A-809A-4F75-92D7-71F630AC2925", + "value": 0 + }, + { + "description": "rssi_report_value", + "format": "uint8", + "iid": 82, + "perms": ["pr", "pw", "ev"], + "type": "8137182C-6904-4FB9-ADCC-61CECA85CE48", + "value": 0 + } + ], + "iid": 80, + "stype": "Unknown Service: C635EF5C-5BBC-4F96-B7DA-6669069A4B32", + "type": "C635EF5C-5BBC-4F96-B7DA-6669069A4B32" + }, + { + "characteristics": [ + { + "format": "string", + "iid": 31, + "perms": ["pr"], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "FLOWERBUD" + }, + { + "format": "uint8", + "iid": 32, + "maxValue": 1, + "minStep": 1, + "minValue": 0, + "perms": ["pr", "pw", "ev"], + "type": "000000B0-0000-1000-8000-0026BB765291", + "value": 0 + }, + { + "format": "float", + "iid": 33, + "maxValue": 100.0, + "minStep": 1.0, + "minValue": 0.0, + "perms": ["pr", "ev"], + "type": "00000010-0000-1000-8000-0026BB765291", + "unit": "percentage", + "value": 45.0 + }, + { + "format": "uint8", + "iid": 34, + "maxValue": 2, + "minStep": 1, + "minValue": 0, + "perms": ["pr", "ev"], + "type": "000000B3-0000-1000-8000-0026BB765291", + "value": 0 + }, + { + "format": "uint8", + "iid": 35, + "maxValue": 1, + "minStep": 1, + "minValue": 1, + "perms": ["pr", "pw", "ev"], + "type": "000000B4-0000-1000-8000-0026BB765291", + "value": 1 + }, + { + "format": "uint8", + "iid": 36, + "maxValue": 1, + "minStep": 1, + "minValue": 0, + "perms": ["pr", "ev"], + "type": "36158AC8-5191-4AE2-9EF5-1D6722E88E3D", + "value": 1 + }, + { + "description": "spray quantity", + "format": "uint8", + "iid": 38, + "maxValue": 5, + "minStep": 1, + "minValue": 1, + "perms": ["pr", "pw", "ev"], + "type": "69D52519-0A4E-4898-8335-4739F9116D0A", + "value": 5 + }, + { + "format": "float", + "iid": 39, + "maxValue": 100.0, + "minStep": 1.0, + "minValue": 0.0, + "perms": ["pr", "pw", "ev"], + "type": "000000CA-0000-1000-8000-0026BB765291", + "unit": "percentage", + "value": 100.0 + }, + { + "description": "humidifier_timer_setting", + "format": "data", + "iid": 40, + "perms": ["pr", "pw", "ev"], + "type": "F84B3138-E44F-49B9-AA91-9E1736C247C0", + "value": "AA==" + }, + { + "description": "humidifier_countdown", + "format": "data", + "iid": 41, + "perms": ["pr", "pw", "ev"], + "type": "43CE176B-2933-4034-98A7-AD215BEEBF2F", + "value": "AA==" + } + ], + "iid": 30, + "stype": "humidifier-dehumidifier", + "type": "000000BD-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "format": "string", + "iid": 10, + "perms": ["pr"], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Mood Light" + }, + { + "format": "bool", + "iid": 11, + "perms": ["pr", "pw", "ev"], + "type": "00000025-0000-1000-8000-0026BB765291", + "value": true + }, + { + "format": "int", + "iid": 12, + "maxValue": 100, + "minStep": 1, + "minValue": 0, + "perms": ["pr", "pw", "ev"], + "type": "00000008-0000-1000-8000-0026BB765291", + "unit": "percentage", + "value": 50 + }, + { + "format": "float", + "iid": 13, + "maxValue": 360.0, + "minStep": 1.0, + "minValue": 0.0, + "perms": ["pr", "pw", "ev"], + "type": "00000013-0000-1000-8000-0026BB765291", + "unit": "arcdegrees", + "value": 120.0 + }, + { + "format": "float", + "iid": 14, + "maxValue": 100.0, + "minStep": 1.0, + "minValue": 0.0, + "perms": ["pr", "pw", "ev"], + "type": "0000002F-0000-1000-8000-0026BB765291", + "unit": "percentage", + "value": 100.0 + }, + { + "description": "lb_timer_setting", + "format": "data", + "iid": 63, + "perms": ["pr", "pw", "ev"], + "type": "A30DFE91-271A-42A5-88BA-00E3FF5488AD", + "value": "AA==" + }, + { + "description": "light effect mode", + "format": "uint8", + "iid": 64, + "maxValue": 31, + "minStep": 1, + "minValue": 0, + "perms": ["pr", "pw", "ev"], + "type": "146889FC-7C42-429B-93AB-E80F79759E90", + "value": 0 + }, + { + "description": "light effect flag", + "format": "uint32", + "iid": 73, + "perms": ["pr"], + "type": "9D4B479D-9EFB-4739-98F3-B33E6543BF7B", + "value": 7 + }, + { + "description": "flashing mode", + "format": "data", + "iid": 65, + "perms": ["pr", "pw", "ev"], + "type": "2C42B339-6EC9-4ED5-8DBF-FFCCC721B144", + "value": "AA==" + }, + { + "description": "smoothing mode", + "format": "data", + "iid": 66, + "perms": ["pr", "pw", "ev"], + "type": "A3663C89-DC18-42EF-8297-910A4C0C9B61", + "value": "AA==" + }, + { + "description": "breathing mode", + "format": "data", + "iid": 67, + "perms": ["pr", "pw", "ev"], + "type": "6533B15C-AECB-455F-8896-20B125390F61", + "value": "AA==" + } + ], + "iid": 9, + "stype": "lightbulb", + "type": "00000043-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "description": "time_zone", + "format": "int", + "iid": 50, + "maxValue": 1400, + "minStep": 1, + "minValue": -1200, + "perms": ["pr", "pw"], + "type": "38396B8E-161B-4A77-AF3F-C4DAC0BE9B74", + "value": 0 + }, + { + "description": "hour_date_time", + "format": "int", + "iid": 51, + "perms": ["pr", "pw"], + "type": "71216CD3-209E-40CC-BEA0-71A2A9458E13", + "value": 0 + } + ], + "iid": 48, + "stype": "Unknown Service: 961EBB65-A1E3-4F34-BD31-86552706FE40", + "type": "961EBB65-A1E3-4F34-BD31-86552706FE40" + }, + { + "characteristics": [ + { + "description": "fm_upgrade_status", + "format": "int", + "iid": 21, + "perms": ["pr", "ev"], + "type": "49DDDE07-C3FA-499E-8055-58E154E04F34", + "value": 0 + }, + { + "description": "fm_upgrade_url", + "format": "string", + "iid": 22, + "maxLen": 256, + "perms": ["pw"], + "type": "4C203E30-EB25-466D-9980-C6C2E14BF6AA" + } + ], + "hidden": true, + "iid": 20, + "stype": "Unknown Service: 3138B537-E830-4F52-90A7-D6FDB000BF97", + "type": "3138B537-E830-4F52-90A7-D6FDB000BF97" + }, + { + "characteristics": [ + { + "format": "string", + "iid": 24, + "perms": ["pr"], + "type": "00000037-0000-1000-8000-0026BB765291", + "value": "1.1.0" + } + ], + "iid": 23, + "stype": "service", + "type": "000000A2-0000-1000-8000-0026BB765291" + } + ] + } ] diff --git a/tests/components/homekit_controller/fixtures/vocolinc_vp3.json b/tests/components/homekit_controller/fixtures/vocolinc_vp3.json index bc58df1623e..b064782a7c8 100644 --- a/tests/components/homekit_controller/fixtures/vocolinc_vp3.json +++ b/tests/components/homekit_controller/fixtures/vocolinc_vp3.json @@ -1,430 +1,335 @@ [ - { - "aid":1, - "services":[ - { - "iid":1, - "type":"0000003E-0000-1000-8000-0026BB765291", - "primary":false, - "hidden":false, - "linked":[ - - ], - "characteristics":[ - { - "iid":2, - "type":"00000014-0000-1000-8000-0026BB765291", - "format":"bool", - "perms":[ - "pw" - ] - }, - { - "iid":3, - "type":"00000020-0000-1000-8000-0026BB765291", - "format":"string", - "value":"VOCOlinc", - "perms":[ - "pr" - ], - "ev":false - }, - { - "iid":4, - "type":"00000021-0000-1000-8000-0026BB765291", - "format":"string", - "value":"VP3", - "perms":[ - "pr" - ], - "ev":false - }, - { - "iid":5, - "type":"00000023-0000-1000-8000-0026BB765291", - "format":"string", - "value":"VOCOlinc-VP3-123456", - "perms":[ - "pr" - ], - "ev":false - }, - { - "iid":6, - "type":"00000030-0000-1000-8000-0026BB765291", - "format":"string", - "value":"EU0121203xxxxx07", - "perms":[ - "pr" - ], - "ev":false - }, - { - "iid":7, - "type":"00000052-0000-1000-8000-0026BB765291", - "format":"string", - "value":"1.101.2", - "perms":[ - "pr" - ], - "ev":false - }, - { - "iid":8, - "type":"00000053-0000-1000-8000-0026BB765291", - "format":"string", - "value":"1.0.3", - "perms":[ - "pr" - ], - "ev":false - }, - { - "iid":9, - "type":"34AB8811-AC7F-4340-BAC3-FD6A85F9943B", - "format":"string", - "value":"3.0;17A126", - "perms":[ - "pr" - ], - "ev":false - }, - { - "iid":10, - "type":"220", - "format":"data", - "value":"wLrKXjM2g90=", - "perms":[ - "pr", - "hd" - ], - "ev":false, - "maxDataLen":8 - } - ] - }, - { - "iid":16, - "type":"000000A2-0000-1000-8000-0026BB765291", - "primary":false, - "hidden":false, - "linked":[ - - ], - "characteristics":[ - { - "iid":18, - "type":"00000037-0000-1000-8000-0026BB765291", - "format":"string", - "value":"1.1.0", - "perms":[ - "pr" - ], - "ev":false - } - ] - }, - { - "iid":48, - "type":"00000047-0000-1000-8000-0026BB765291", - "primary":true, - "hidden":false, - "linked":[ - - ], - "characteristics":[ - { - "iid":50, - "type":"00000023-0000-1000-8000-0026BB765291", - "format":"string", - "value":"Outlet", - "perms":[ - "pr" - ], - "ev":false - }, - { - "iid":51, - "type":"00000025-0000-1000-8000-0026BB765291", - "format":"bool", - "value":1, - "perms":[ - "pr", - "pw", - "ev" - ], - "ev":false - }, - { - "iid":83, - "type":"A30DFE96-271A-42A5-88BA-00E3FF5488AD", - "format":"data", - "value":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", - "perms":[ - "pr", - "pw", - "ev" - ], - "ev":false, - "maxDataLen":256 - }, - { - "iid":53, - "type":"E2E80928-E08A-472F-8AE9-70BA72E132F2", - "format":"int", - "value":1, - "perms":[ - "pr", - "pw", - "ev" - ], - "ev":false, - "minValue":1, - "maxValue":3600, - "minStep":1 - }, - { - "iid":54, - "type":"D4669376-C36E-4C43-ACA4-ED07686EAB19", - "format":"uint8", - "value":0, - "perms":[ - "pr", - "pw", - "ev" - ], - "ev":false, - "minValue":0, - "maxValue":2, - "minStep":0 - }, - { - "iid":97, - "type":"FC093458-18F0-4B1D-8360-BB68A3FCC9C5", - "format":"int", - "value":0, - "perms":[ - "pr", - "ev" - ], - "ev":false, - "minValue":0, - "maxValue":2147483647, - "minStep":1 - }, - { - "iid":98, - "type":"865AD00B-A016-416E-8918-CF8E7EC788C4", - "format":"int", - "value":2552, - "perms":[ - "pr" - ], - "ev":false, - "minValue":0, - "maxValue":2147483647, - "minStep":1 - }, - { - "iid":99, - "type":"2D5D1654-63EE-4314-9CF1-651F266D3BBE", - "format":"data", - "value":"AQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", - "perms":[ - "pr", - "ev" - ], - "ev":false, - "maxDataLen":128 - }, - { - "iid":100, - "type":"6E46AD30-6FC2-426F-9A86-C2A834DD8F29", - "format":"data", - "value":"AQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", - "perms":[ - "pr", - "ev" - ], - "ev":false, - "maxDataLen":128 - }, - { - "iid":101, - "type":"56F805A5-4B30-47D0-9908-E609B4CF18E3", - "format":"data", - "value":"AQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", - "perms":[ - "pr", - "ev" - ], - "ev":false, - "maxDataLen":128 - }, - { - "iid":102, - "type":"A121FC5E-67DB-41EC-BF4F-5A431F0DA9CB", - "format":"data", - "value":"AQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", - "perms":[ - "pr", - "ev" - ], - "ev":false, - "maxDataLen":64 - }, - { - "iid":103, - "type":"BC75E7A0-7DD8-4CBB-9DE8-93E70A04916D", - "format":"data", - "value":"AQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", - "perms":[ - "pr", - "ev" - ], - "ev":false, - "maxDataLen":64 - }, - { - "iid":89, - "type":"43CE176B-2933-4034-98A7-AD215BEEBF2F", - "format":"data", - "value":"AAAAAAAAAAAAAA==", - "perms":[ - "pr", - "pw", - "ev" - ], - "ev":false, - "description":"\"CountDown\"", - "maxDataLen":256 - } - ] - }, - { - "iid":64, - "type":"3138B537-E830-4F52-90A7-D6FDB000BF97", - "primary":false, - "hidden":true, - "linked":[ - - ], - "characteristics":[ - { - "iid":65, - "type":"00000023-0000-1000-8000-0026BB765291", - "format":"string", - "value":"FW Update", - "perms":[ - "pr" - ], - "ev":false - }, - { - "iid":66, - "type":"4C203E30-EB25-466D-9980-C6C2E14BF6AA", - "format":"string", - "perms":[ - "pw", - "hd" - ], - "description":"\"FW update\"", - "maxLen":128 - }, - { - "iid":67, - "type":"49DDDE07-C3FA-499E-8055-58E154E04F34", - "format":"int", - "value":null, - "perms":[ - "pr", - "ev" - ], - "ev":false, - "minValue":0, - "maxValue":3, - "minStep":1 - } - ] - }, - { - "iid":80, - "type":"C635EF5C-5BBC-4F96-B7DA-6669069A4B32", - "primary":false, - "hidden":true, - "linked":[ - - ], - "characteristics":[ - { - "iid":82, - "type":"8137182C-6904-4FB9-ADCC-61CECA85CE48", - "format":"uint8", - "value":27, - "perms":[ - "pr", - "ev" - ], - "ev":false - }, - { - "iid":81, - "type":"00000023-0000-1000-8000-0026BB765291", - "format":"string", - "value":"Rssi Report", - "perms":[ - "pr" - ], - "ev":false - } - ] - }, - { - "iid":84, - "type":"961BBB65-A1E3-4F34-BD31-86552706FE40", - "primary":false, - "hidden":false, - "linked":[ - - ], - "characteristics":[ - { - "iid":85, - "type":"38396B8E-161B-4A77-AF3F-C4DAC0BE9B74", - "format":"int", - "value":999, - "perms":[ - "pr", - "pw" - ], - "ev":false, - "minValue":-1200, - "maxValue":1400, - "minStep":1 - }, - { - "iid":86, - "type":"71216CD3-209E-40CC-BEA0-71A2A9458E13", - "format":"int", - "perms":[ - "pw" - ], - "minValue":0, - "maxValue":2147483647, - "minStep":1 - }, - { - "iid":87, - "type":"00000023-0000-1000-8000-0026BB765291", - "format":"string", - "value":"sync time", - "perms":[ - "pr" - ], - "ev":false - } - ] - } + { + "aid": 1, + "services": [ + { + "iid": 1, + "type": "0000003E-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 2, + "type": "00000014-0000-1000-8000-0026BB765291", + "format": "bool", + "perms": ["pw"] + }, + { + "iid": 3, + "type": "00000020-0000-1000-8000-0026BB765291", + "format": "string", + "value": "VOCOlinc", + "perms": ["pr"], + "ev": false + }, + { + "iid": 4, + "type": "00000021-0000-1000-8000-0026BB765291", + "format": "string", + "value": "VP3", + "perms": ["pr"], + "ev": false + }, + { + "iid": 5, + "type": "00000023-0000-1000-8000-0026BB765291", + "format": "string", + "value": "VOCOlinc-VP3-123456", + "perms": ["pr"], + "ev": false + }, + { + "iid": 6, + "type": "00000030-0000-1000-8000-0026BB765291", + "format": "string", + "value": "EU0121203xxxxx07", + "perms": ["pr"], + "ev": false + }, + { + "iid": 7, + "type": "00000052-0000-1000-8000-0026BB765291", + "format": "string", + "value": "1.101.2", + "perms": ["pr"], + "ev": false + }, + { + "iid": 8, + "type": "00000053-0000-1000-8000-0026BB765291", + "format": "string", + "value": "1.0.3", + "perms": ["pr"], + "ev": false + }, + { + "iid": 9, + "type": "34AB8811-AC7F-4340-BAC3-FD6A85F9943B", + "format": "string", + "value": "3.0;17A126", + "perms": ["pr"], + "ev": false + }, + { + "iid": 10, + "type": "220", + "format": "data", + "value": "wLrKXjM2g90=", + "perms": ["pr", "hd"], + "ev": false, + "maxDataLen": 8 + } ] - } + }, + { + "iid": 16, + "type": "000000A2-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 18, + "type": "00000037-0000-1000-8000-0026BB765291", + "format": "string", + "value": "1.1.0", + "perms": ["pr"], + "ev": false + } + ] + }, + { + "iid": 48, + "type": "00000047-0000-1000-8000-0026BB765291", + "primary": true, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 50, + "type": "00000023-0000-1000-8000-0026BB765291", + "format": "string", + "value": "Outlet", + "perms": ["pr"], + "ev": false + }, + { + "iid": 51, + "type": "00000025-0000-1000-8000-0026BB765291", + "format": "bool", + "value": 1, + "perms": ["pr", "pw", "ev"], + "ev": false + }, + { + "iid": 83, + "type": "A30DFE96-271A-42A5-88BA-00E3FF5488AD", + "format": "data", + "value": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", + "perms": ["pr", "pw", "ev"], + "ev": false, + "maxDataLen": 256 + }, + { + "iid": 53, + "type": "E2E80928-E08A-472F-8AE9-70BA72E132F2", + "format": "int", + "value": 1, + "perms": ["pr", "pw", "ev"], + "ev": false, + "minValue": 1, + "maxValue": 3600, + "minStep": 1 + }, + { + "iid": 54, + "type": "D4669376-C36E-4C43-ACA4-ED07686EAB19", + "format": "uint8", + "value": 0, + "perms": ["pr", "pw", "ev"], + "ev": false, + "minValue": 0, + "maxValue": 2, + "minStep": 0 + }, + { + "iid": 97, + "type": "FC093458-18F0-4B1D-8360-BB68A3FCC9C5", + "format": "int", + "value": 0, + "perms": ["pr", "ev"], + "ev": false, + "minValue": 0, + "maxValue": 2147483647, + "minStep": 1 + }, + { + "iid": 98, + "type": "865AD00B-A016-416E-8918-CF8E7EC788C4", + "format": "int", + "value": 2552, + "perms": ["pr"], + "ev": false, + "minValue": 0, + "maxValue": 2147483647, + "minStep": 1 + }, + { + "iid": 99, + "type": "2D5D1654-63EE-4314-9CF1-651F266D3BBE", + "format": "data", + "value": "AQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", + "perms": ["pr", "ev"], + "ev": false, + "maxDataLen": 128 + }, + { + "iid": 100, + "type": "6E46AD30-6FC2-426F-9A86-C2A834DD8F29", + "format": "data", + "value": "AQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", + "perms": ["pr", "ev"], + "ev": false, + "maxDataLen": 128 + }, + { + "iid": 101, + "type": "56F805A5-4B30-47D0-9908-E609B4CF18E3", + "format": "data", + "value": "AQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", + "perms": ["pr", "ev"], + "ev": false, + "maxDataLen": 128 + }, + { + "iid": 102, + "type": "A121FC5E-67DB-41EC-BF4F-5A431F0DA9CB", + "format": "data", + "value": "AQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", + "perms": ["pr", "ev"], + "ev": false, + "maxDataLen": 64 + }, + { + "iid": 103, + "type": "BC75E7A0-7DD8-4CBB-9DE8-93E70A04916D", + "format": "data", + "value": "AQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", + "perms": ["pr", "ev"], + "ev": false, + "maxDataLen": 64 + }, + { + "iid": 89, + "type": "43CE176B-2933-4034-98A7-AD215BEEBF2F", + "format": "data", + "value": "AAAAAAAAAAAAAA==", + "perms": ["pr", "pw", "ev"], + "ev": false, + "description": "\"CountDown\"", + "maxDataLen": 256 + } + ] + }, + { + "iid": 64, + "type": "3138B537-E830-4F52-90A7-D6FDB000BF97", + "primary": false, + "hidden": true, + "linked": [], + "characteristics": [ + { + "iid": 65, + "type": "00000023-0000-1000-8000-0026BB765291", + "format": "string", + "value": "FW Update", + "perms": ["pr"], + "ev": false + }, + { + "iid": 66, + "type": "4C203E30-EB25-466D-9980-C6C2E14BF6AA", + "format": "string", + "perms": ["pw", "hd"], + "description": "\"FW update\"", + "maxLen": 128 + }, + { + "iid": 67, + "type": "49DDDE07-C3FA-499E-8055-58E154E04F34", + "format": "int", + "value": null, + "perms": ["pr", "ev"], + "ev": false, + "minValue": 0, + "maxValue": 3, + "minStep": 1 + } + ] + }, + { + "iid": 80, + "type": "C635EF5C-5BBC-4F96-B7DA-6669069A4B32", + "primary": false, + "hidden": true, + "linked": [], + "characteristics": [ + { + "iid": 82, + "type": "8137182C-6904-4FB9-ADCC-61CECA85CE48", + "format": "uint8", + "value": 27, + "perms": ["pr", "ev"], + "ev": false + }, + { + "iid": 81, + "type": "00000023-0000-1000-8000-0026BB765291", + "format": "string", + "value": "Rssi Report", + "perms": ["pr"], + "ev": false + } + ] + }, + { + "iid": 84, + "type": "961BBB65-A1E3-4F34-BD31-86552706FE40", + "primary": false, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 85, + "type": "38396B8E-161B-4A77-AF3F-C4DAC0BE9B74", + "format": "int", + "value": 999, + "perms": ["pr", "pw"], + "ev": false, + "minValue": -1200, + "maxValue": 1400, + "minStep": 1 + }, + { + "iid": 86, + "type": "71216CD3-209E-40CC-BEA0-71A2A9458E13", + "format": "int", + "perms": ["pw"], + "minValue": 0, + "maxValue": 2147483647, + "minStep": 1 + }, + { + "iid": 87, + "type": "00000023-0000-1000-8000-0026BB765291", + "format": "string", + "value": "sync time", + "perms": ["pr"], + "ev": false + } + ] + } + ] + } ] diff --git a/tests/components/homekit_controller/specific_devices/test_haa_fan.py b/tests/components/homekit_controller/specific_devices/test_haa_fan.py index ae67bda2000..9d5983650d7 100644 --- a/tests/components/homekit_controller/specific_devices/test_haa_fan.py +++ b/tests/components/homekit_controller/specific_devices/test_haa_fan.py @@ -59,7 +59,6 @@ async def test_haa_fan_setup(hass): supported_features=SUPPORT_SET_SPEED, capabilities={ "preset_modes": None, - "speed_list": ["off", "low", "medium", "high"], }, ), EntityTestInfo( diff --git a/tests/components/homekit_controller/specific_devices/test_homeassistant_bridge.py b/tests/components/homekit_controller/specific_devices/test_homeassistant_bridge.py index 33e53e9413c..76fab93b013 100644 --- a/tests/components/homekit_controller/specific_devices/test_homeassistant_bridge.py +++ b/tests/components/homekit_controller/specific_devices/test_homeassistant_bridge.py @@ -55,7 +55,6 @@ async def test_homeassistant_bridge_fan_setup(hass): ), capabilities={ "preset_modes": None, - "speed_list": ["off", "low", "medium", "high"], }, state="off", ) diff --git a/tests/components/homekit_controller/specific_devices/test_simpleconnect_fan.py b/tests/components/homekit_controller/specific_devices/test_simpleconnect_fan.py index ce21d643bab..d3531e1c65f 100644 --- a/tests/components/homekit_controller/specific_devices/test_simpleconnect_fan.py +++ b/tests/components/homekit_controller/specific_devices/test_simpleconnect_fan.py @@ -40,7 +40,6 @@ async def test_simpleconnect_fan_setup(hass): supported_features=SUPPORT_DIRECTION | SUPPORT_SET_SPEED, capabilities={ "preset_modes": None, - "speed_list": ["off", "low", "medium", "high"], }, state="off", ), diff --git a/tests/components/homekit_controller/test_fan.py b/tests/components/homekit_controller/test_fan.py index 252e7f87bed..faaaa2e666f 100644 --- a/tests/components/homekit_controller/test_fan.py +++ b/tests/components/homekit_controller/test_fan.py @@ -95,7 +95,7 @@ async def test_turn_on(hass, utcnow): await hass.services.async_call( "fan", "turn_on", - {"entity_id": "fan.testdevice", "speed": "high"}, + {"entity_id": "fan.testdevice", "percentage": 100}, blocking=True, ) helper.async_assert_service_values( @@ -109,7 +109,7 @@ async def test_turn_on(hass, utcnow): await hass.services.async_call( "fan", "turn_on", - {"entity_id": "fan.testdevice", "speed": "medium"}, + {"entity_id": "fan.testdevice", "percentage": 66}, blocking=True, ) helper.async_assert_service_values( @@ -123,7 +123,7 @@ async def test_turn_on(hass, utcnow): await hass.services.async_call( "fan", "turn_on", - {"entity_id": "fan.testdevice", "speed": "low"}, + {"entity_id": "fan.testdevice", "percentage": 33}, blocking=True, ) helper.async_assert_service_values( @@ -196,8 +196,8 @@ async def test_set_speed(hass, utcnow): await hass.services.async_call( "fan", - "set_speed", - {"entity_id": "fan.testdevice", "speed": "high"}, + "set_percentage", + {"entity_id": "fan.testdevice", "percentage": 100}, blocking=True, ) helper.async_assert_service_values( @@ -209,8 +209,8 @@ async def test_set_speed(hass, utcnow): await hass.services.async_call( "fan", - "set_speed", - {"entity_id": "fan.testdevice", "speed": "medium"}, + "set_percentage", + {"entity_id": "fan.testdevice", "percentage": 66}, blocking=True, ) helper.async_assert_service_values( @@ -222,8 +222,8 @@ async def test_set_speed(hass, utcnow): await hass.services.async_call( "fan", - "set_speed", - {"entity_id": "fan.testdevice", "speed": "low"}, + "set_percentage", + {"entity_id": "fan.testdevice", "percentage": 33}, blocking=True, ) helper.async_assert_service_values( @@ -235,8 +235,8 @@ async def test_set_speed(hass, utcnow): await hass.services.async_call( "fan", - "set_speed", - {"entity_id": "fan.testdevice", "speed": "off"}, + "set_percentage", + {"entity_id": "fan.testdevice", "percentage": 0}, blocking=True, ) helper.async_assert_service_values( @@ -291,7 +291,6 @@ async def test_speed_read(hass, utcnow): CharacteristicsTypes.ROTATION_SPEED: 100, }, ) - assert state.attributes["speed"] == "high" assert state.attributes["percentage"] == 100 assert state.attributes["percentage_step"] == 1.0 @@ -301,7 +300,6 @@ async def test_speed_read(hass, utcnow): CharacteristicsTypes.ROTATION_SPEED: 50, }, ) - assert state.attributes["speed"] == "medium" assert state.attributes["percentage"] == 50 state = await helper.async_update( @@ -310,7 +308,6 @@ async def test_speed_read(hass, utcnow): CharacteristicsTypes.ROTATION_SPEED: 25, }, ) - assert state.attributes["speed"] == "low" assert state.attributes["percentage"] == 25 state = await helper.async_update( @@ -320,7 +317,6 @@ async def test_speed_read(hass, utcnow): CharacteristicsTypes.ROTATION_SPEED: 0, }, ) - assert state.attributes["speed"] == "off" assert state.attributes["percentage"] == 0 @@ -392,7 +388,7 @@ async def test_v2_turn_on(hass, utcnow): await hass.services.async_call( "fan", "turn_on", - {"entity_id": "fan.testdevice", "speed": "high"}, + {"entity_id": "fan.testdevice", "percentage": 100}, blocking=True, ) helper.async_assert_service_values( @@ -406,7 +402,7 @@ async def test_v2_turn_on(hass, utcnow): await hass.services.async_call( "fan", "turn_on", - {"entity_id": "fan.testdevice", "speed": "medium"}, + {"entity_id": "fan.testdevice", "percentage": 66}, blocking=True, ) helper.async_assert_service_values( @@ -420,7 +416,7 @@ async def test_v2_turn_on(hass, utcnow): await hass.services.async_call( "fan", "turn_on", - {"entity_id": "fan.testdevice", "speed": "low"}, + {"entity_id": "fan.testdevice", "percentage": 33}, blocking=True, ) helper.async_assert_service_values( @@ -488,8 +484,8 @@ async def test_v2_set_speed(hass, utcnow): await hass.services.async_call( "fan", - "set_speed", - {"entity_id": "fan.testdevice", "speed": "high"}, + "set_percentage", + {"entity_id": "fan.testdevice", "percentage": 100}, blocking=True, ) helper.async_assert_service_values( @@ -501,8 +497,8 @@ async def test_v2_set_speed(hass, utcnow): await hass.services.async_call( "fan", - "set_speed", - {"entity_id": "fan.testdevice", "speed": "medium"}, + "set_percentage", + {"entity_id": "fan.testdevice", "percentage": 66}, blocking=True, ) helper.async_assert_service_values( @@ -514,8 +510,8 @@ async def test_v2_set_speed(hass, utcnow): await hass.services.async_call( "fan", - "set_speed", - {"entity_id": "fan.testdevice", "speed": "low"}, + "set_percentage", + {"entity_id": "fan.testdevice", "percentage": 33}, blocking=True, ) helper.async_assert_service_values( @@ -527,8 +523,8 @@ async def test_v2_set_speed(hass, utcnow): await hass.services.async_call( "fan", - "set_speed", - {"entity_id": "fan.testdevice", "speed": "off"}, + "set_percentage", + {"entity_id": "fan.testdevice", "percentage": 0}, blocking=True, ) helper.async_assert_service_values( @@ -616,7 +612,6 @@ async def test_v2_speed_read(hass, utcnow): CharacteristicsTypes.ROTATION_SPEED: 100, }, ) - assert state.attributes["speed"] == "high" assert state.attributes["percentage"] == 100 state = await helper.async_update( @@ -625,7 +620,6 @@ async def test_v2_speed_read(hass, utcnow): CharacteristicsTypes.ROTATION_SPEED: 50, }, ) - assert state.attributes["speed"] == "medium" assert state.attributes["percentage"] == 50 state = await helper.async_update( @@ -634,7 +628,6 @@ async def test_v2_speed_read(hass, utcnow): CharacteristicsTypes.ROTATION_SPEED: 25, }, ) - assert state.attributes["speed"] == "low" assert state.attributes["percentage"] == 25 state = await helper.async_update( @@ -644,7 +637,6 @@ async def test_v2_speed_read(hass, utcnow): CharacteristicsTypes.ROTATION_SPEED: 0, }, ) - assert state.attributes["speed"] == "off" assert state.attributes["percentage"] == 0 diff --git a/tests/components/homematicip_cloud/test_light.py b/tests/components/homematicip_cloud/test_light.py index b4dbd0d140e..b3feb9586cd 100644 --- a/tests/components/homematicip_cloud/test_light.py +++ b/tests/components/homematicip_cloud/test_light.py @@ -2,10 +2,6 @@ from homematicip.base.enums import RGBColorState from homeassistant.components.homematicip_cloud import DOMAIN as HMIPC_DOMAIN -from homeassistant.components.homematicip_cloud.light import ( - ATTR_CURRENT_POWER_W, - ATTR_TODAY_ENERGY_KWH, -) from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_NAME, @@ -233,8 +229,6 @@ async def test_hmip_light_measuring(hass, default_mock_hap_factory): await async_manipulate_test_data(hass, hmip_device, "currentPowerConsumption", 50) ha_state = hass.states.get(entity_id) assert ha_state.state == STATE_ON - assert ha_state.attributes[ATTR_CURRENT_POWER_W] == 50 - assert ha_state.attributes[ATTR_TODAY_ENERGY_KWH] == 6.33 await hass.services.async_call( "light", "turn_off", {"entity_id": entity_id}, blocking=True diff --git a/tests/components/homematicip_cloud/test_switch.py b/tests/components/homematicip_cloud/test_switch.py index f2b3dfba32c..6d89ed31f5f 100644 --- a/tests/components/homematicip_cloud/test_switch.py +++ b/tests/components/homematicip_cloud/test_switch.py @@ -3,11 +3,7 @@ from homeassistant.components.homematicip_cloud import DOMAIN as HMIPC_DOMAIN from homeassistant.components.homematicip_cloud.generic_entity import ( ATTR_GROUP_MEMBER_UNREACHABLE, ) -from homeassistant.components.switch import ( - ATTR_CURRENT_POWER_W, - ATTR_TODAY_ENERGY_KWH, - DOMAIN as SWITCH_DOMAIN, -) +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.setup import async_setup_component @@ -132,12 +128,6 @@ async def test_hmip_switch_measuring(hass, default_mock_hap_factory): await async_manipulate_test_data(hass, hmip_device, "currentPowerConsumption", 50) ha_state = hass.states.get(entity_id) assert ha_state.state == STATE_ON - assert ha_state.attributes[ATTR_CURRENT_POWER_W] == 50 - assert ha_state.attributes[ATTR_TODAY_ENERGY_KWH] == 36 - - await async_manipulate_test_data(hass, hmip_device, "energyCounter", None) - ha_state = hass.states.get(entity_id) - assert not ha_state.attributes.get(ATTR_TODAY_ENERGY_KWH) async def test_hmip_group_switch(hass, default_mock_hap_factory): diff --git a/tests/components/homewizard/test_init.py b/tests/components/homewizard/test_init.py index 02e0b5c0c23..984a5431004 100644 --- a/tests/components/homewizard/test_init.py +++ b/tests/components/homewizard/test_init.py @@ -108,7 +108,7 @@ async def test_init_accepts_and_migrates_old_entry(aioclient_mock, hass): config_entry=original_entry, original_name="Switch Disabled", suggested_object_id="socket_disabled", - disabled_by=er.DISABLED_USER, + disabled_by=er.RegistryEntryDisabler.USER, ) # Update some user-customs ent_reg.async_update_entity(old_entity_active_power.entity_id, name="new_name") @@ -158,7 +158,7 @@ async def test_init_accepts_and_migrates_old_entry(aioclient_mock, hass): assert new_entity_disabled_sensor.name is None assert new_entity_disabled_sensor.original_name == "Switch Disabled" assert new_entity_disabled_sensor.unique_id == "socket_disabled_unique_id" - assert new_entity_disabled_sensor.disabled_by == er.DISABLED_USER + assert new_entity_disabled_sensor.disabled_by == er.RegistryEntryDisabler.USER async def test_load_detect_api_disabled(aioclient_mock, hass): diff --git a/tests/components/honeywell/test_config_flow.py b/tests/components/honeywell/test_config_flow.py index 47897cf246d..b93e359c1ac 100644 --- a/tests/components/honeywell/test_config_flow.py +++ b/tests/components/honeywell/test_config_flow.py @@ -4,10 +4,16 @@ from unittest.mock import patch import somecomfort from homeassistant import data_entry_flow -from homeassistant.components.honeywell.const import DOMAIN -from homeassistant.config_entries import SOURCE_USER +from homeassistant.components.honeywell.const import ( + CONF_COOL_AWAY_TEMPERATURE, + CONF_HEAT_AWAY_TEMPERATURE, + DOMAIN, +) +from homeassistant.config_entries import SOURCE_USER, ConfigEntryState from homeassistant.core import HomeAssistant +from tests.common import MockConfigEntry + FAKE_CONFIG = { "username": "fake", "password": "user", @@ -49,3 +55,44 @@ async def test_create_entry(hass: HomeAssistant) -> None: ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["data"] == FAKE_CONFIG + + +@patch("homeassistant.components.honeywell.UPDATE_LOOP_SLEEP_TIME", 0) +async def test_show_option_form( + hass: HomeAssistant, config_entry: MockConfigEntry, location +) -> None: + """Test that the option form is shown.""" + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.LOADED + + result = await hass.config_entries.options.async_init(config_entry.entry_id) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "init" + + +@patch("homeassistant.components.honeywell.UPDATE_LOOP_SLEEP_TIME", 0) +async def test_create_option_entry( + hass: HomeAssistant, config_entry: MockConfigEntry, location +) -> None: + """Test that the config entry is created.""" + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.LOADED + + options_form = await hass.config_entries.options.async_init(config_entry.entry_id) + result = await hass.config_entries.options.async_configure( + options_form["flow_id"], + user_input={CONF_COOL_AWAY_TEMPERATURE: 1, CONF_HEAT_AWAY_TEMPERATURE: 2}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert config_entry.options == { + CONF_COOL_AWAY_TEMPERATURE: 1, + CONF_HEAT_AWAY_TEMPERATURE: 2, + } diff --git a/tests/components/honeywell/test_init.py b/tests/components/honeywell/test_init.py index 49917aae151..83be3a05873 100644 --- a/tests/components/honeywell/test_init.py +++ b/tests/components/honeywell/test_init.py @@ -4,11 +4,19 @@ from unittest.mock import create_autospec, patch import somecomfort +from homeassistant.components.honeywell.const import ( + CONF_COOL_AWAY_TEMPERATURE, + CONF_HEAT_AWAY_TEMPERATURE, + DOMAIN, +) from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry +MIGRATE_OPTIONS_KEYS = {CONF_COOL_AWAY_TEMPERATURE, CONF_HEAT_AWAY_TEMPERATURE} + @patch("homeassistant.components.honeywell.UPDATE_LOOP_SLEEP_TIME", 0) async def test_setup_entry(hass: HomeAssistant, config_entry: MockConfigEntry): @@ -48,3 +56,28 @@ async def test_setup_multiple_thermostats_with_same_deviceid( assert config_entry.state is ConfigEntryState.LOADED assert hass.states.async_entity_ids_count() == 1 assert "Platform honeywell does not generate unique IDs" not in caplog.text + + +async def test_away_temps_migration(hass: HomeAssistant) -> None: + """Test away temps migrate to config options.""" + legacy_config = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_USERNAME: "fake", + CONF_PASSWORD: "user", + CONF_COOL_AWAY_TEMPERATURE: 1, + CONF_HEAT_AWAY_TEMPERATURE: 2, + }, + options={}, + ) + + with patch( + "homeassistant.components.honeywell.somecomfort.SomeComfort", + ): + legacy_config.add_to_hass(hass) + await hass.config_entries.async_setup(legacy_config.entry_id) + await hass.async_block_till_done() + assert legacy_config.options == { + CONF_COOL_AWAY_TEMPERATURE: 1, + CONF_HEAT_AWAY_TEMPERATURE: 2, + } diff --git a/tests/components/html5/test_notify.py b/tests/components/html5/test_notify.py index 116b4437d61..1614555c493 100644 --- a/tests/components/html5/test_notify.py +++ b/tests/components/html5/test_notify.py @@ -527,3 +527,46 @@ async def test_send_fcm_without_targets(hass, hass_client): assert mock_wp.mock_calls[0][1][0] == SUBSCRIPTION_5["subscription"] # Third mock_call checks the status_code of the response. assert mock_wp.mock_calls[2][0] == "().send().status_code.__eq__" + + +async def test_send_fcm_expired(hass, hass_client): + """Test that the FCM target is removed when expired.""" + registrations = {"device": SUBSCRIPTION_5} + await mock_client(hass, hass_client, registrations) + + with ( + patch("homeassistant.components.html5.notify.WebPusher") as mock_wp, + patch("homeassistant.components.html5.notify.save_json"), + ): + mock_wp().send().status_code = 410 + await hass.services.async_call( + "notify", + "notify", + {"message": "Hello", "target": ["device"], "data": {"icon": "beer.png"}}, + blocking=True, + ) + # "device" should be removed when expired. + assert "device" not in registrations + + +async def test_send_fcm_expired_save_fails(hass, hass_client): + """Test that the FCM target remains after expiry if save_json fails.""" + registrations = {"device": SUBSCRIPTION_5} + await mock_client(hass, hass_client, registrations) + + with ( + patch("homeassistant.components.html5.notify.WebPusher") as mock_wp, + patch( + "homeassistant.components.html5.notify.save_json", + side_effect=HomeAssistantError(), + ), + ): + mock_wp().send().status_code = 410 + await hass.services.async_call( + "notify", + "notify", + {"message": "Hello", "target": ["device"], "data": {"icon": "beer.png"}}, + blocking=True, + ) + # "device" should still exist if save fails. + assert "device" in registrations diff --git a/tests/components/hue/fixtures/v2_resources.json b/tests/components/hue/fixtures/v2_resources.json index c3f03d8f48a..be9509f9652 100644 --- a/tests/components/hue/fixtures/v2_resources.json +++ b/tests/components/hue/fixtures/v2_resources.json @@ -1,2107 +1,2118 @@ [ - { - "id": "9c489c26-9e34-4fcd-8324-a57e3a664cc0", - "status": "unpaired", - "status_values": ["pairing", "paired", "unpaired"], - "type": "homekit" - }, - { - "actions": [ - { - "action": { - "color": { - "xy": { - "x": 0.5058, - "y": 0.4477 - } - }, - "dimming": { - "brightness": 46.85 - }, - "on": { - "on": true + { + "id": "9c489c26-9e34-4fcd-8324-a57e3a664cc0", + "status": "unpaired", + "status_values": ["pairing", "paired", "unpaired"], + "type": "homekit" + }, + { + "actions": [ + { + "action": { + "color": { + "xy": { + "x": 0.5058, + "y": 0.4477 } }, - "target": { - "rid": "b3fe71ef-d0ef-48de-9355-d9e604377df0", - "rtype": "light" + "dimming": { + "brightness": 46.85 + }, + "on": { + "on": true } }, - { - "action": { - "dimming": { - "brightness": 46.85 - }, - "gradient": { - "points": [ - { - "color": { - "xy": { - "x": 0.4808, - "y": 0.4485 - } - } - }, - { - "color": { - "xy": { - "x": 0.4958, - "y": 0.443 - } - } - }, - { - "color": { - "xy": { - "x": 0.5058, - "y": 0.4477 - } - } - }, - { - "color": { - "xy": { - "x": 0.5586, - "y": 0.4081 - } - } - }, - { - "color": { - "xy": { - "x": 0.569, - "y": 0.4003 - } + "target": { + "rid": "b3fe71ef-d0ef-48de-9355-d9e604377df0", + "rtype": "light" + } + }, + { + "action": { + "dimming": { + "brightness": 46.85 + }, + "gradient": { + "points": [ + { + "color": { + "xy": { + "x": 0.4808, + "y": 0.4485 } } - ] - }, - "on": { - "on": true - } + }, + { + "color": { + "xy": { + "x": 0.4958, + "y": 0.443 + } + } + }, + { + "color": { + "xy": { + "x": 0.5058, + "y": 0.4477 + } + } + }, + { + "color": { + "xy": { + "x": 0.5586, + "y": 0.4081 + } + } + }, + { + "color": { + "xy": { + "x": 0.569, + "y": 0.4003 + } + } + } + ] }, - "target": { - "rid": "8015b17f-8336-415b-966a-b364bd082397", - "rtype": "light" + "on": { + "on": true } }, - { - "action": { - "color": { - "xy": { - "x": 0.5586, - "y": 0.4081 - } - }, - "dimming": { - "brightness": 46.85 - }, - "on": { - "on": true - } - }, - "target": { - "rid": "02cba059-9c2c-4d45-97e4-4f79b1bfbaa1", - "rtype": "light" - } + "target": { + "rid": "8015b17f-8336-415b-966a-b364bd082397", + "rtype": "light" } - ], - "group": { - "rid": "7cee478d-6455-483a-9e32-9f9fdcbcc4f6", - "rtype": "zone" }, - "id": "fce5eabb-2f51-461b-b112-5362da301236", - "id_v1": "/scenes/qYDehk7EfGoRvkj", - "metadata": { - "image": { - "rid": "93984a4f-2d1b-4554-b972-b60fa8e476c5", - "rtype": "public_image" + { + "action": { + "color": { + "xy": { + "x": 0.5586, + "y": 0.4081 + } + }, + "dimming": { + "brightness": 46.85 + }, + "on": { + "on": true + } }, - "name": "Dynamic Test Scene" - }, - "palette": { - "color": [ - { - "color": { - "xy": { - "x": 0.4808, - "y": 0.4485 - } - }, - "dimming": { - "brightness": 74.02 - } - }, - { - "color": { - "xy": { - "x": 0.5023, - "y": 0.4467 - } - }, - "dimming": { - "brightness": 100.0 - } - }, - { - "color": { - "xy": { - "x": 0.5615, - "y": 0.4059 - } - }, - "dimming": { - "brightness": 100.0 - } - } - ], - "color_temperature": [ - { - "color_temperature": { - "mirek": 451 - }, - "dimming": { - "brightness": 31.1 - } - } - ], - "dimming": [] - }, - "speed": 0.6269841194152832, - "type": "scene" + "target": { + "rid": "02cba059-9c2c-4d45-97e4-4f79b1bfbaa1", + "rtype": "light" + } + } + ], + "group": { + "rid": "7cee478d-6455-483a-9e32-9f9fdcbcc4f6", + "rtype": "zone" }, - { - "actions": [ + "id": "fce5eabb-2f51-461b-b112-5362da301236", + "id_v1": "/scenes/qYDehk7EfGoRvkj", + "metadata": { + "image": { + "rid": "93984a4f-2d1b-4554-b972-b60fa8e476c5", + "rtype": "public_image" + }, + "name": "Dynamic Test Scene" + }, + "palette": { + "color": [ { - "action": { - "color_temperature": { - "mirek": 156 - }, - "dimming": { - "brightness": 100.0 - }, - "on": { - "on": true + "color": { + "xy": { + "x": 0.4808, + "y": 0.4485 } }, - "target": { - "rid": "3a6710fa-4474-4eba-b533-5e6e72968feb", - "rtype": "light" + "dimming": { + "brightness": 74.02 } }, { - "action": { - "on": { - "on": true + "color": { + "xy": { + "x": 0.5023, + "y": 0.4467 } }, - "target": { - "rid": "7697ac8a-25aa-4576-bb40-0036c0db15b9", - "rtype": "light" + "dimming": { + "brightness": 100.0 + } + }, + { + "color": { + "xy": { + "x": 0.5615, + "y": 0.4059 + } + }, + "dimming": { + "brightness": 100.0 } } ], - "group": { + "color_temperature": [ + { + "color_temperature": { + "mirek": 451 + }, + "dimming": { + "brightness": 31.1 + } + } + ], + "dimming": [] + }, + "speed": 0.6269841194152832, + "type": "scene" + }, + { + "actions": [ + { + "action": { + "color_temperature": { + "mirek": 156 + }, + "dimming": { + "brightness": 100.0 + }, + "on": { + "on": true + } + }, + "target": { + "rid": "3a6710fa-4474-4eba-b533-5e6e72968feb", + "rtype": "light" + } + }, + { + "action": { + "on": { + "on": true + } + }, + "target": { + "rid": "7697ac8a-25aa-4576-bb40-0036c0db15b9", + "rtype": "light" + } + } + ], + "group": { + "rid": "6ddc9066-7e7d-4a03-a773-c73937968296", + "rtype": "room" + }, + "id": "cdbf3740-7977-4a11-8275-8c78636ad4bd", + "id_v1": "/scenes/LwgmWgRnaRUxg6K", + "metadata": { + "image": { + "rid": "7fd2ccc5-5749-4142-b7a5-66405a676f03", + "rtype": "public_image" + }, + "name": "Regular Test Scene" + }, + "palette": { + "color": [], + "color_temperature": [], + "dimming": [] + }, + "speed": 0.5, + "type": "scene" + }, + { + "id": "3ff06175-29e8-44a8-8fe7-af591b0025da", + "id_v1": "/sensors/50", + "metadata": { + "archetype": "unknown_archetype", + "name": "Wall switch with 2 controls" + }, + "product_data": { + "certified": true, + "manufacturer_name": "Signify Netherlands B.V.", + "model_id": "RDM001", + "product_archetype": "unknown_archetype", + "product_name": "Hue wall switch module", + "software_version": "1.0.3" + }, + "services": [ + { + "rid": "c658d3d8-a013-4b81-8ac6-78b248537e70", + "rtype": "button" + }, + { + "rid": "be1eb834-bdf5-4d26-8fba-7b1feaa83a9d", + "rtype": "button" + }, + { + "rid": "c1cd98a6-6c23-43bb-b6e1-08dda9e168a4", + "rtype": "device_power" + }, + { + "rid": "af520f40-e080-43b0-9bb5-41a4d5251b2b", + "rtype": "zigbee_connectivity" + } + ], + "type": "device" + }, + { + "id": "0b216218-d811-4c95-8c55-bbcda50f9d50", + "id_v1": "/lights/29", + "metadata": { + "archetype": "floor_shade", + "name": "Hue light with color and color temperature 1" + }, + "product_data": { + "certified": true, + "manufacturer_name": "Signify Netherlands B.V.", + "model_id": "4080248P9", + "product_archetype": "floor_shade", + "product_name": "Hue color floor", + "software_version": "1.88.1" + }, + "services": [ + { + "rid": "02cba059-9c2c-4d45-97e4-4f79b1bfbaa1", + "rtype": "light" + }, + { + "rid": "1987ba66-c21d-48d0-98fb-121d939a71f3", + "rtype": "zigbee_connectivity" + }, + { + "rid": "5d7b3979-b936-47ff-8458-554f8a2921db", + "rtype": "entertainment" + } + ], + "type": "device" + }, + { + "id": "60b849cc-a8b5-4034-8881-ed1cd560fd13", + "id_v1": "/lights/4", + "metadata": { + "archetype": "ceiling_round", + "name": "Hue light with color temperature only" + }, + "product_data": { + "certified": true, + "manufacturer_name": "Signify Netherlands B.V.", + "model_id": "LTC001", + "product_archetype": "ceiling_round", + "product_name": "Hue ambiance ceiling", + "software_version": "1.88.1" + }, + "services": [ + { + "rid": "3a6710fa-4474-4eba-b533-5e6e72968feb", + "rtype": "light" + }, + { + "rid": "bd878f44-feb7-406e-8af9-6a1796d1ddc9", + "rtype": "zigbee_connectivity" + } + ], + "type": "device" + }, + { + "id": "342daec9-391b-480b-abdd-87f1aa04ce3b", + "id_v1": "/sensors/10", + "metadata": { + "archetype": "unknown_archetype", + "name": "Hue Dimmer switch with 4 controls" + }, + "product_data": { + "certified": true, + "manufacturer_name": "Signify Netherlands B.V.", + "model_id": "RWL021", + "product_archetype": "unknown_archetype", + "product_name": "Hue dimmer switch", + "software_version": "1.1.28573" + }, + "services": [ + { + "rid": "f92aa267-1387-4f02-9950-210fb7ca1f5a", + "rtype": "button" + }, + { + "rid": "7f1ab9f6-cc2b-4b40-9011-65e2af153f75", + "rtype": "button" + }, + { + "rid": "b4edb2d6-55d0-47f4-bd43-7ae215ef1062", + "rtype": "button" + }, + { + "rid": "40a810bf-3d22-4c56-9334-4a59a00768ab", + "rtype": "button" + }, + { + "rid": "0bb058bc-2139-43d9-8c9b-edfb4570953b", + "rtype": "device_power" + }, + { + "rid": "db50a5d9-8cc7-486f-be06-c0b8f0d26c69", + "rtype": "zigbee_connectivity" + } + ], + "type": "device" + }, + { + "id": "b9e76da7-ac22-476a-986d-e466e62e962f", + "id_v1": "/lights/16", + "metadata": { + "archetype": "hue_lightstrip", + "name": "Hue light with color and color temperature 2" + }, + "product_data": { + "certified": true, + "manufacturer_name": "Signify Netherlands B.V.", + "model_id": "LST002", + "product_archetype": "hue_lightstrip", + "product_name": "Hue lightstrip plus", + "software_version": "67.88.1" + }, + "services": [ + { + "rid": "b3fe71ef-d0ef-48de-9355-d9e604377df0", + "rtype": "light" + }, + { + "rid": "717afeb6-b1ce-426e-96de-48e8fe037fb0", + "rtype": "zigbee_connectivity" + }, + { + "rid": "d88acc42-259c-43b5-bf5d-90c16cdb8f2f", + "rtype": "entertainment" + } + ], + "type": "device" + }, + { + "id": "fcdfab5d-8e04-4e9c-a999-7f92cb38c4fc", + "id_v1": "/lights/23", + "metadata": { + "archetype": "classic_bulb", + "name": "Hue on/off light" + }, + "product_data": { + "certified": false, + "manufacturer_name": "eWeLink", + "model_id": "SA-003-Zigbee", + "product_archetype": "classic_bulb", + "product_name": "On/Off light", + "software_version": "1.0.2" + }, + "services": [ + { + "rid": "7697ac8a-25aa-4576-bb40-0036c0db15b9", + "rtype": "light" + }, + { + "rid": "6b00ce2b-a8a5-4bab-bc5e-757a0b0338ff", + "rtype": "zigbee_connectivity" + } + ], + "type": "device" + }, + { + "id": "7745ebea-dd33-429c-a900-bae4e7ae1107", + "id_v1": "/sensors/5", + "metadata": { + "archetype": "unknown_archetype", + "name": "Hue Smart button 1 control" + }, + "product_data": { + "certified": true, + "manufacturer_name": "Signify Netherlands B.V.", + "model_id": "ROM001", + "product_archetype": "unknown_archetype", + "product_name": "Hue Smart button", + "software_version": "2.47.8" + }, + "services": [ + { + "rid": "31cffcda-efc2-401f-a152-e10db3eed232", + "rtype": "button" + }, + { + "rid": "3f219f5a-ad6c-484f-b976-769a9c267a72", + "rtype": "device_power" + }, + { + "rid": "bba44861-8222-45c9-9e6b-d7f3a6543829", + "rtype": "zigbee_connectivity" + } + ], + "type": "device" + }, + { + "id": "4a507550-8742-4087-8bf5-c2334f29891c", + "id_v1": "", + "metadata": { + "archetype": "bridge_v2", + "name": "Philips hue" + }, + "product_data": { + "certified": true, + "manufacturer_name": "Signify Netherlands B.V.", + "model_id": "BSB002", + "product_archetype": "bridge_v2", + "product_name": "Philips hue", + "software_version": "1.50.1950111030" + }, + "services": [ + { + "rid": "07dd5849-abcd-efgh-b9b9-eb540408ce00", + "rtype": "bridge" + }, + { + "rid": "6c898412-ed25-4402-9807-a0c326616b0f", + "rtype": "zigbee_connectivity" + }, + { + "rid": "b8ab0c30-b227-4d35-9c96-7cd16131fcc5", + "rtype": "entertainment" + } + ], + "type": "device" + }, + { + "id": "8d07d39c-3c19-47ce-ac7a-8bf3d8e849b9", + "id_v1": "/lights/11", + "metadata": { + "archetype": "hue_bloom", + "name": "Hue light with color only" + }, + "product_data": { + "certified": true, + "manufacturer_name": "Signify Netherlands B.V.", + "model_id": "LLC011", + "product_archetype": "hue_bloom", + "product_name": "Hue bloom", + "software_version": "67.91.1" + }, + "services": [ + { + "rid": "74a45fee-1b3d-4553-b5ab-040da8a10cfd", + "rtype": "light" + }, + { + "rid": "98baae94-76d9-4bc4-a1d1-d53f1d7b1286", + "rtype": "zigbee_connectivity" + }, + { + "rid": "8e6a4ff3-14ca-42f9-8358-9d691b9a4524", + "rtype": "entertainment" + } + ], + "type": "device" + }, + { + "id": "1c8be0d5-a68b-45c2-8f56-530d13b0c128", + "id_v1": "/lights/24", + "metadata": { + "archetype": "hue_lightstrip_tv", + "name": "Hue light with color and color temperature gradient" + }, + "product_data": { + "certified": true, + "manufacturer_name": "Signify Netherlands B.V.", + "model_id": "LCX003", + "product_archetype": "hue_lightstrip_tv", + "product_name": "Hue play gradient lightstrip", + "software_version": "1.86.7" + }, + "services": [ + { + "rid": "8015b17f-8336-415b-966a-b364bd082397", + "rtype": "light" + }, + { + "rid": "ff4e6545-341f-4b0d-9869-b6feb6e6fe87", + "rtype": "zigbee_connectivity" + }, + { + "rid": "7b03eb98-4cfd-4acf-ac11-675f51613e5e", + "rtype": "entertainment" + } + ], + "type": "device" + }, + { + "id": "2330b45d-6079-4c6e-bba6-1b68afb1a0d6", + "id_v1": "/sensors/66", + "metadata": { + "archetype": "unknown_archetype", + "name": "Hue motion sensor" + }, + "product_data": { + "certified": true, + "manufacturer_name": "Signify Netherlands B.V.", + "model_id": "SML001", + "product_archetype": "unknown_archetype", + "product_name": "Hue motion sensor", + "software_version": "1.1.27575" + }, + "services": [ + { + "rid": "b6896534-016d-4052-8cb4-ef04454df62c", + "rtype": "motion" + }, + { + "rid": "669f609d-4860-4f1c-bc25-7a9cec1c3b6c", + "rtype": "device_power" + }, + { + "rid": "ec9b5ad7-2471-4356-b757-d00537828963", + "rtype": "zigbee_connectivity" + }, + { + "rid": "d504e7a4-9a18-4854-90fd-c5b6ac102c40", + "rtype": "light_level" + }, + { + "rid": "66466e14-d2fa-4b96-b2a0-e10de9cd8b8b", + "rtype": "temperature" + } + ], + "type": "device" + }, + { + "alert": { + "action_values": ["breathe"] + }, + "color": { + "gamut": { + "blue": { + "x": 0.1532, + "y": 0.0475 + }, + "green": { + "x": 0.17, + "y": 0.7 + }, + "red": { + "x": 0.6915, + "y": 0.3083 + } + }, + "gamut_type": "C", + "xy": { + "x": 0.5614, + "y": 0.4058 + } + }, + "color_temperature": { + "mirek": null, + "mirek_schema": { + "mirek_maximum": 500, + "mirek_minimum": 153 + }, + "mirek_valid": false + }, + "dimming": { + "brightness": 46.85, + "min_dim_level": 0.10000000149011612 + }, + "dynamics": { + "speed": 0.627, + "speed_valid": true, + "status": "dynamic_palette", + "status_values": ["none", "dynamic_palette"] + }, + "effects": { + "status_values": ["no_effect", "candle", "fire"], + "status": "no_effect" + }, + "id": "02cba059-9c2c-4d45-97e4-4f79b1bfbaa1", + "id_v1": "/lights/29", + "metadata": { + "archetype": "floor_shade", + "name": "Hue light with color and color temperature 1" + }, + "mode": "normal", + "on": { + "on": true + }, + "owner": { + "rid": "0b216218-d811-4c95-8c55-bbcda50f9d50", + "rtype": "device" + }, + "type": "light" + }, + { + "alert": { + "action_values": ["breathe"] + }, + "color_temperature": { + "mirek": 369, + "mirek_schema": { + "mirek_maximum": 454, + "mirek_minimum": 153 + }, + "mirek_valid": true + }, + "dimming": { + "brightness": 59.45, + "min_dim_level": 0.10000000149011612 + }, + "dynamics": { + "speed": 0.0, + "speed_valid": false, + "status": "none", + "status_values": ["none"] + }, + "effects": { + "status_values": ["no_effect", "candle"], + "status": "no_effect" + }, + "timed_effects": { + "status_values": ["no_effect", "sunrise"], + "status": "no_effect" + }, + "id": "3a6710fa-4474-4eba-b533-5e6e72968feb", + "id_v1": "/lights/4", + "metadata": { + "archetype": "ceiling_round", + "name": "Hue light with color temperature only" + }, + "mode": "normal", + "on": { + "on": false + }, + "owner": { + "rid": "60b849cc-a8b5-4034-8881-ed1cd560fd13", + "rtype": "device" + }, + "type": "light" + }, + { + "alert": { + "action_values": ["breathe"] + }, + "color": { + "gamut": { + "blue": { + "x": 0.1532, + "y": 0.0475 + }, + "green": { + "x": 0.17, + "y": 0.7 + }, + "red": { + "x": 0.6915, + "y": 0.3083 + } + }, + "gamut_type": "C", + "xy": { + "x": 0.5022, + "y": 0.4466 + } + }, + "color_temperature": { + "mirek": null, + "mirek_schema": { + "mirek_maximum": 500, + "mirek_minimum": 153 + }, + "mirek_valid": false + }, + "dimming": { + "brightness": 46.85, + "min_dim_level": 0.02500000037252903 + }, + "dynamics": { + "speed": 0.627, + "speed_valid": true, + "status": "dynamic_palette", + "status_values": ["none", "dynamic_palette"] + }, + "id": "b3fe71ef-d0ef-48de-9355-d9e604377df0", + "id_v1": "/lights/16", + "metadata": { + "archetype": "hue_lightstrip", + "name": "Hue light with color and color temperature 2" + }, + "mode": "normal", + "on": { + "on": true + }, + "owner": { + "rid": "b9e76da7-ac22-476a-986d-e466e62e962f", + "rtype": "device" + }, + "type": "light" + }, + { + "alert": { + "action_values": ["breathe"] + }, + "dynamics": { + "speed": 0.0, + "speed_valid": false, + "status": "none", + "status_values": ["none"] + }, + "id": "7697ac8a-25aa-4576-bb40-0036c0db15b9", + "id_v1": "/lights/23", + "metadata": { + "archetype": "classic_bulb", + "name": "Hue on/off light" + }, + "mode": "normal", + "on": { + "on": false + }, + "owner": { + "rid": "fcdfab5d-8e04-4e9c-a999-7f92cb38c4fc", + "rtype": "device" + }, + "type": "light" + }, + { + "alert": { + "action_values": ["breathe"] + }, + "color": { + "gamut": { + "blue": { + "x": 0.138, + "y": 0.08 + }, + "green": { + "x": 0.2151, + "y": 0.7106 + }, + "red": { + "x": 0.704, + "y": 0.296 + } + }, + "gamut_type": "A", + "xy": { + "x": 0.4849, + "y": 0.3895 + } + }, + "dimming": { + "brightness": 50.0, + "min_dim_level": 10.0 + }, + "dynamics": { + "speed": 0.6389, + "speed_valid": true, + "status": "dynamic_palette", + "status_values": ["none", "dynamic_palette"] + }, + "id": "74a45fee-1b3d-4553-b5ab-040da8a10cfd", + "id_v1": "/lights/11", + "metadata": { + "archetype": "hue_bloom", + "name": "Hue light with color only" + }, + "mode": "normal", + "on": { + "on": true + }, + "owner": { + "rid": "8d07d39c-3c19-47ce-ac7a-8bf3d8e849b9", + "rtype": "device" + }, + "type": "light" + }, + { + "alert": { + "action_values": ["breathe"] + }, + "color": { + "gamut": { + "blue": { + "x": 0.1532, + "y": 0.0475 + }, + "green": { + "x": 0.17, + "y": 0.7 + }, + "red": { + "x": 0.6915, + "y": 0.3083 + } + }, + "gamut_type": "C", + "xy": { + "x": 0.5022, + "y": 0.4466 + } + }, + "color_temperature": { + "mirek": null, + "mirek_schema": { + "mirek_maximum": 500, + "mirek_minimum": 153 + }, + "mirek_valid": false + }, + "dimming": { + "brightness": 46.85, + "min_dim_level": 0.10000000149011612 + }, + "dynamics": { + "speed": 0.627, + "speed_valid": true, + "status": "dynamic_palette", + "status_values": ["none", "dynamic_palette"] + }, + "gradient": { + "points": [ + { + "color": { + "xy": { + "x": 0.5022, + "y": 0.4466 + } + } + }, + { + "color": { + "xy": { + "x": 0.4806, + "y": 0.4484 + } + } + }, + { + "color": { + "xy": { + "x": 0.5022, + "y": 0.4466 + } + } + }, + { + "color": { + "xy": { + "x": 0.5614, + "y": 0.4058 + } + } + }, + { + "color": { + "xy": { + "x": 0.5022, + "y": 0.4466 + } + } + } + ], + "points_capable": 5 + }, + "id": "8015b17f-8336-415b-966a-b364bd082397", + "id_v1": "/lights/24", + "metadata": { + "archetype": "hue_lightstrip_tv", + "name": "Hue light with color and color temperature gradient" + }, + "mode": "normal", + "on": { + "on": true + }, + "owner": { + "rid": "1c8be0d5-a68b-45c2-8f56-530d13b0c128", + "rtype": "device" + }, + "type": "light" + }, + { + "id": "af520f40-e080-43b0-9bb5-41a4d5251b2b", + "id_v1": "/sensors/50", + "mac_address": "00:17:88:01:0b:aa:bb:99", + "owner": { + "rid": "3ff06175-29e8-44a8-8fe7-af591b0025da", + "rtype": "device" + }, + "status": "connected", + "type": "zigbee_connectivity" + }, + { + "id": "1987ba66-c21d-48d0-98fb-121d939a71f3", + "id_v1": "/lights/29", + "mac_address": "00:17:88:01:09:aa:bb:65", + "owner": { + "rid": "0b216218-d811-4c95-8c55-bbcda50f9d50", + "rtype": "device" + }, + "status": "connected", + "type": "zigbee_connectivity" + }, + { + "id": "bd878f44-feb7-406e-8af9-6a1796d1ddc9", + "id_v1": "/lights/4", + "mac_address": "00:17:88:01:06:aa:bb:58", + "owner": { + "rid": "60b849cc-a8b5-4034-8881-ed1cd560fd13", + "rtype": "device" + }, + "status": "connected", + "type": "zigbee_connectivity" + }, + { + "id": "db50a5d9-8cc7-486f-be06-c0b8f0d26c69", + "id_v1": "/sensors/10", + "mac_address": "00:17:88:01:08:aa:cc:60", + "owner": { + "rid": "342daec9-391b-480b-abdd-87f1aa04ce3b", + "rtype": "device" + }, + "status": "connected", + "type": "zigbee_connectivity" + }, + { + "id": "717afeb6-b1ce-426e-96de-48e8fe037fb0", + "id_v1": "/lights/16", + "mac_address": "00:17:88:aa:aa:bb:0d:ab", + "owner": { + "rid": "b9e76da7-ac22-476a-986d-e466e62e962f", + "rtype": "device" + }, + "status": "connected", + "type": "zigbee_connectivity" + }, + { + "id": "6b00ce2b-a8a5-4bab-bc5e-757a0b0338ff", + "id_v1": "/lights/23", + "mac_address": "00:12:4b:00:1f:aa:bb:f3", + "owner": { + "rid": "fcdfab5d-8e04-4e9c-a999-7f92cb38c4fc", + "rtype": "device" + }, + "status": "connected", + "type": "zigbee_connectivity" + }, + { + "id": "bba44861-8222-45c9-9e6b-d7f3a6543829", + "id_v1": "/sensors/5", + "mac_address": "00:17:88:01:aa:cc:87:b6", + "owner": { + "rid": "7745ebea-dd33-429c-a900-bae4e7ae1107", + "rtype": "device" + }, + "status": "connected", + "type": "zigbee_connectivity" + }, + { + "id": "6c898412-ed25-4402-9807-a0c326616b0f", + "id_v1": "", + "mac_address": "00:17:88:01:aa:bb:fd:c7", + "owner": { + "rid": "4a507550-8742-4087-8bf5-c2334f29891c", + "rtype": "device" + }, + "status": "connected", + "type": "zigbee_connectivity" + }, + { + "id": "d2ae969a-add5-41b1-afbd-f2837b2eb551", + "id_v1": "/lights/34", + "mac_address": "00:17:88:01:aa:bb:cc:ed", + "owner": { + "rid": "5ad8326c-e51a-4594-8738-fc700b53fcc4", + "rtype": "device" + }, + "status": "connected", + "type": "zigbee_connectivity" + }, + { + "id": "98baae94-76d9-4bc4-a1d1-d53f1d7b1286", + "id_v1": "/lights/11", + "mac_address": "00:17:88:aa:bb:1e:cc:b2", + "owner": { + "rid": "8d07d39c-3c19-47ce-ac7a-8bf3d8e849b9", + "rtype": "device" + }, + "status": "connected", + "type": "zigbee_connectivity" + }, + { + "id": "ff4e6545-341f-4b0d-9869-b6feb6e6fe87", + "id_v1": "/lights/24", + "mac_address": "00:17:88:01:aa:bb:cc:3d", + "owner": { + "rid": "1c8be0d5-a68b-45c2-8f56-530d13b0c128", + "rtype": "device" + }, + "status": "connected", + "type": "zigbee_connectivity" + }, + { + "id": "ec9b5ad7-2471-4356-b757-d00537828963", + "id_v1": "/sensors/66", + "mac_address": "00:17:aa:bb:cc:09:ac:c3", + "owner": { + "rid": "2330b45d-6079-4c6e-bba6-1b68afb1a0d6", + "rtype": "device" + }, + "status": "connected", + "type": "zigbee_connectivity" + }, + { + "id": "5d7b3979-b936-47ff-8458-554f8a2921db", + "id_v1": "/lights/29", + "owner": { + "rid": "0b216218-d811-4c95-8c55-bbcda50f9d50", + "rtype": "device" + }, + "proxy": true, + "renderer": true, + "segments": { + "configurable": false, + "max_segments": 1, + "segments": [ + { + "length": 1, + "start": 0 + } + ] + }, + "type": "entertainment" + }, + { + "id": "d88acc42-259c-43b5-bf5d-90c16cdb8f2f", + "id_v1": "/lights/16", + "owner": { + "rid": "b9e76da7-ac22-476a-986d-e466e62e962f", + "rtype": "device" + }, + "proxy": true, + "renderer": true, + "segments": { + "configurable": false, + "max_segments": 1, + "segments": [ + { + "length": 1, + "start": 0 + } + ] + }, + "type": "entertainment" + }, + { + "id": "b8ab0c30-b227-4d35-9c96-7cd16131fcc5", + "id_v1": "", + "owner": { + "rid": "4a507550-8742-4087-8bf5-c2334f29891c", + "rtype": "device" + }, + "proxy": true, + "renderer": false, + "type": "entertainment" + }, + { + "id": "8e6a4ff3-14ca-42f9-8358-9d691b9a4524", + "id_v1": "/lights/11", + "owner": { + "rid": "8d07d39c-3c19-47ce-ac7a-8bf3d8e849b9", + "rtype": "device" + }, + "proxy": false, + "renderer": true, + "segments": { + "configurable": false, + "max_segments": 1, + "segments": [ + { + "length": 1, + "start": 0 + } + ] + }, + "type": "entertainment" + }, + { + "id": "7b03eb98-4cfd-4acf-ac11-675f51613e5e", + "id_v1": "/lights/24", + "owner": { + "rid": "1c8be0d5-a68b-45c2-8f56-530d13b0c128", + "rtype": "device" + }, + "proxy": true, + "renderer": true, + "segments": { + "configurable": false, + "max_segments": 10, + "segments": [ + { + "length": 2, + "start": 0 + }, + { + "length": 3, + "start": 2 + }, + { + "length": 5, + "start": 5 + }, + { + "length": 4, + "start": 10 + }, + { + "length": 5, + "start": 14 + }, + { + "length": 3, + "start": 19 + }, + { + "length": 2, + "start": 22 + } + ] + }, + "type": "entertainment" + }, + { + "button": { + "last_event": "short_release" + }, + "id": "c658d3d8-a013-4b81-8ac6-78b248537e70", + "id_v1": "/sensors/50", + "metadata": { + "control_id": 1 + }, + "owner": { + "rid": "3ff06175-29e8-44a8-8fe7-af591b0025da", + "rtype": "device" + }, + "type": "button" + }, + { + "id": "be1eb834-bdf5-4d26-8fba-7b1feaa83a9d", + "id_v1": "/sensors/50", + "metadata": { + "control_id": 2 + }, + "owner": { + "rid": "3ff06175-29e8-44a8-8fe7-af591b0025da", + "rtype": "device" + }, + "type": "button" + }, + { + "id": "f92aa267-1387-4f02-9950-210fb7ca1f5a", + "id_v1": "/sensors/10", + "metadata": { + "control_id": 1 + }, + "owner": { + "rid": "342daec9-391b-480b-abdd-87f1aa04ce3b", + "rtype": "device" + }, + "type": "button" + }, + { + "button": { + "last_event": "short_release" + }, + "id": "7f1ab9f6-cc2b-4b40-9011-65e2af153f75", + "id_v1": "/sensors/10", + "metadata": { + "control_id": 2 + }, + "owner": { + "rid": "342daec9-391b-480b-abdd-87f1aa04ce3b", + "rtype": "device" + }, + "type": "button" + }, + { + "id": "b4edb2d6-55d0-47f4-bd43-7ae215ef1062", + "id_v1": "/sensors/10", + "metadata": { + "control_id": 3 + }, + "owner": { + "rid": "342daec9-391b-480b-abdd-87f1aa04ce3b", + "rtype": "device" + }, + "type": "button" + }, + { + "id": "40a810bf-3d22-4c56-9334-4a59a00768ab", + "id_v1": "/sensors/10", + "metadata": { + "control_id": 4 + }, + "owner": { + "rid": "342daec9-391b-480b-abdd-87f1aa04ce3b", + "rtype": "device" + }, + "type": "button" + }, + { + "button": { + "last_event": "short_release" + }, + "id": "31cffcda-efc2-401f-a152-e10db3eed232", + "id_v1": "/sensors/5", + "metadata": { + "control_id": 1 + }, + "owner": { + "rid": "7745ebea-dd33-429c-a900-bae4e7ae1107", + "rtype": "device" + }, + "type": "button" + }, + { + "id": "c1cd98a6-6c23-43bb-b6e1-08dda9e168a4", + "id_v1": "/sensors/50", + "owner": { + "rid": "3ff06175-29e8-44a8-8fe7-af591b0025da", + "rtype": "device" + }, + "power_state": { + "battery_level": 100, + "battery_state": "normal" + }, + "type": "device_power" + }, + { + "id": "0bb058bc-2139-43d9-8c9b-edfb4570953b", + "id_v1": "/sensors/10", + "owner": { + "rid": "342daec9-391b-480b-abdd-87f1aa04ce3b", + "rtype": "device" + }, + "power_state": { + "battery_level": 83, + "battery_state": "normal" + }, + "type": "device_power" + }, + { + "id": "3f219f5a-ad6c-484f-b976-769a9c267a72", + "id_v1": "/sensors/5", + "owner": { + "rid": "7745ebea-dd33-429c-a900-bae4e7ae1107", + "rtype": "device" + }, + "power_state": { + "battery_level": 91, + "battery_state": "normal" + }, + "type": "device_power" + }, + { + "id": "669f609d-4860-4f1c-bc25-7a9cec1c3b6c", + "id_v1": "/sensors/66", + "owner": { + "rid": "2330b45d-6079-4c6e-bba6-1b68afb1a0d6", + "rtype": "device" + }, + "power_state": { + "battery_level": 100, + "battery_state": "normal" + }, + "type": "device_power" + }, + { + "children": [ + { + "rid": "02cba059-9c2c-4d45-97e4-4f79b1bfbaa1", + "rtype": "light" + }, + { + "rid": "b3fe71ef-d0ef-48de-9355-d9e604377df0", + "rtype": "light" + }, + { + "rid": "8015b17f-8336-415b-966a-b364bd082397", + "rtype": "light" + } + ], + "grouped_services": [ + { + "rid": "f2416154-9607-43ab-a684-4453108a200e", + "rtype": "grouped_light" + } + ], + "id": "7cee478d-6455-483a-9e32-9f9fdcbcc4f6", + "id_v1": "/groups/5", + "metadata": { + "archetype": "downstairs", + "name": "Test Zone" + }, + "services": [ + { + "rid": "02cba059-9c2c-4d45-97e4-4f79b1bfbaa1", + "rtype": "light" + }, + { + "rid": "b3fe71ef-d0ef-48de-9355-d9e604377df0", + "rtype": "light" + }, + { + "rid": "8015b17f-8336-415b-966a-b364bd082397", + "rtype": "light" + }, + { + "rid": "f2416154-9607-43ab-a684-4453108a200e", + "rtype": "grouped_light" + } + ], + "type": "zone" + }, + { + "alert": { + "action_values": ["breathe"] + }, + "id": "f2416154-9607-43ab-a684-4453108a200e", + "id_v1": "/groups/5", + "on": { + "on": true + }, + "type": "grouped_light" + }, + { + "alert": { + "action_values": ["breathe"] + }, + "id": "0a74457c-cb8d-44c2-a5a5-dcb7b3675550", + "id_v1": "/groups/0", + "on": { + "on": true + }, + "type": "grouped_light" + }, + { + "alert": { + "action_values": ["breathe"] + }, + "id": "e937f8db-2f0e-49a0-936e-027e60e15b34", + "id_v1": "/groups/3", + "on": { + "on": false + }, + "type": "grouped_light" + }, + { + "children": [ + { "rid": "6ddc9066-7e7d-4a03-a773-c73937968296", "rtype": "room" }, - "id": "cdbf3740-7977-4a11-8275-8c78636ad4bd", - "id_v1": "/scenes/LwgmWgRnaRUxg6K", - "metadata": { - "image": { - "rid": "7fd2ccc5-5749-4142-b7a5-66405a676f03", - "rtype": "public_image" - }, - "name": "Regular Test Scene" - }, - "palette": { - "color": [], - "color_temperature": [], - "dimming": [] - }, - "speed": 0.5, - "type": "scene" - }, - { - "id": "3ff06175-29e8-44a8-8fe7-af591b0025da", - "id_v1": "/sensors/50", - "metadata": { - "archetype": "unknown_archetype", - "name": "Wall switch with 2 controls" - }, - "product_data": { - "certified": true, - "manufacturer_name": "Signify Netherlands B.V.", - "model_id": "RDM001", - "product_archetype": "unknown_archetype", - "product_name": "Hue wall switch module", - "software_version": "1.0.3" - }, - "services": [ - { - "rid": "c658d3d8-a013-4b81-8ac6-78b248537e70", - "rtype": "button" - }, - { - "rid": "be1eb834-bdf5-4d26-8fba-7b1feaa83a9d", - "rtype": "button" - }, - { - "rid": "c1cd98a6-6c23-43bb-b6e1-08dda9e168a4", - "rtype": "device_power" - }, - { - "rid": "af520f40-e080-43b0-9bb5-41a4d5251b2b", - "rtype": "zigbee_connectivity" - } - ], - "type": "device" - }, - { - "id": "0b216218-d811-4c95-8c55-bbcda50f9d50", - "id_v1": "/lights/29", - "metadata": { - "archetype": "floor_shade", - "name": "Hue light with color and color temperature 1" - }, - "product_data": { - "certified": true, - "manufacturer_name": "Signify Netherlands B.V.", - "model_id": "4080248P9", - "product_archetype": "floor_shade", - "product_name": "Hue color floor", - "software_version": "1.88.1" - }, - "services": [ - { - "rid": "02cba059-9c2c-4d45-97e4-4f79b1bfbaa1", - "rtype": "light" - }, - { - "rid": "1987ba66-c21d-48d0-98fb-121d939a71f3", - "rtype": "zigbee_connectivity" - }, - { - "rid": "5d7b3979-b936-47ff-8458-554f8a2921db", - "rtype": "entertainment" - } - ], - "type": "device" - }, - { - "id": "60b849cc-a8b5-4034-8881-ed1cd560fd13", - "id_v1": "/lights/4", - "metadata": { - "archetype": "ceiling_round", - "name": "Hue light with color temperature only" - }, - "product_data": { - "certified": true, - "manufacturer_name": "Signify Netherlands B.V.", - "model_id": "LTC001", - "product_archetype": "ceiling_round", - "product_name": "Hue ambiance ceiling", - "software_version": "1.88.1" - }, - "services": [ - { - "rid": "3a6710fa-4474-4eba-b533-5e6e72968feb", - "rtype": "light" - }, - { - "rid": "bd878f44-feb7-406e-8af9-6a1796d1ddc9", - "rtype": "zigbee_connectivity" - } - ], - "type": "device" - }, - { - "id": "342daec9-391b-480b-abdd-87f1aa04ce3b", - "id_v1": "/sensors/10", - "metadata": { - "archetype": "unknown_archetype", - "name": "Hue Dimmer switch with 4 controls" - }, - "product_data": { - "certified": true, - "manufacturer_name": "Signify Netherlands B.V.", - "model_id": "RWL021", - "product_archetype": "unknown_archetype", - "product_name": "Hue dimmer switch", - "software_version": "1.1.28573" - }, - "services": [ - { - "rid": "f92aa267-1387-4f02-9950-210fb7ca1f5a", - "rtype": "button" - }, - { - "rid": "7f1ab9f6-cc2b-4b40-9011-65e2af153f75", - "rtype": "button" - }, - { - "rid": "b4edb2d6-55d0-47f4-bd43-7ae215ef1062", - "rtype": "button" - }, - { - "rid": "40a810bf-3d22-4c56-9334-4a59a00768ab", - "rtype": "button" - }, - { - "rid": "0bb058bc-2139-43d9-8c9b-edfb4570953b", - "rtype": "device_power" - }, - { - "rid": "db50a5d9-8cc7-486f-be06-c0b8f0d26c69", - "rtype": "zigbee_connectivity" - } - ], - "type": "device" - }, - { - "id": "b9e76da7-ac22-476a-986d-e466e62e962f", - "id_v1": "/lights/16", - "metadata": { - "archetype": "hue_lightstrip", - "name": "Hue light with color and color temperature 2" - }, - "product_data": { - "certified": true, - "manufacturer_name": "Signify Netherlands B.V.", - "model_id": "LST002", - "product_archetype": "hue_lightstrip", - "product_name": "Hue lightstrip plus", - "software_version": "67.88.1" - }, - "services": [ - { - "rid": "b3fe71ef-d0ef-48de-9355-d9e604377df0", - "rtype": "light" - }, - { - "rid": "717afeb6-b1ce-426e-96de-48e8fe037fb0", - "rtype": "zigbee_connectivity" - }, - { - "rid": "d88acc42-259c-43b5-bf5d-90c16cdb8f2f", - "rtype": "entertainment" - } - ], - "type": "device" - }, - { - "id": "fcdfab5d-8e04-4e9c-a999-7f92cb38c4fc", - "id_v1": "/lights/23", - "metadata": { - "archetype": "classic_bulb", - "name": "Hue on/off light" - }, - "product_data": { - "certified": false, - "manufacturer_name": "eWeLink", - "model_id": "SA-003-Zigbee", - "product_archetype": "classic_bulb", - "product_name": "On/Off light", - "software_version": "1.0.2" - }, - "services": [ - { - "rid": "7697ac8a-25aa-4576-bb40-0036c0db15b9", - "rtype": "light" - }, - { - "rid": "6b00ce2b-a8a5-4bab-bc5e-757a0b0338ff", - "rtype": "zigbee_connectivity" - } - ], - "type": "device" - }, - { - "id": "7745ebea-dd33-429c-a900-bae4e7ae1107", - "id_v1": "/sensors/5", - "metadata": { - "archetype": "unknown_archetype", - "name": "Hue Smart button 1 control" - }, - "product_data": { - "certified": true, - "manufacturer_name": "Signify Netherlands B.V.", - "model_id": "ROM001", - "product_archetype": "unknown_archetype", - "product_name": "Hue Smart button", - "software_version": "2.47.8" - }, - "services": [ - { - "rid": "31cffcda-efc2-401f-a152-e10db3eed232", - "rtype": "button" - }, - { - "rid": "3f219f5a-ad6c-484f-b976-769a9c267a72", - "rtype": "device_power" - }, - { - "rid": "bba44861-8222-45c9-9e6b-d7f3a6543829", - "rtype": "zigbee_connectivity" - } - ], - "type": "device" - }, - { - "id": "4a507550-8742-4087-8bf5-c2334f29891c", - "id_v1": "", - "metadata": { - "archetype": "bridge_v2", - "name": "Philips hue" - }, - "product_data": { - "certified": true, - "manufacturer_name": "Signify Netherlands B.V.", - "model_id": "BSB002", - "product_archetype": "bridge_v2", - "product_name": "Philips hue", - "software_version": "1.50.1950111030" - }, - "services": [ - { - "rid": "07dd5849-abcd-efgh-b9b9-eb540408ce00", - "rtype": "bridge" - }, - { - "rid": "6c898412-ed25-4402-9807-a0c326616b0f", - "rtype": "zigbee_connectivity" - }, - { - "rid": "b8ab0c30-b227-4d35-9c96-7cd16131fcc5", - "rtype": "entertainment" - } - ], - "type": "device" - }, - { - "id": "8d07d39c-3c19-47ce-ac7a-8bf3d8e849b9", - "id_v1": "/lights/11", - "metadata": { - "archetype": "hue_bloom", - "name": "Hue light with color only" - }, - "product_data": { - "certified": true, - "manufacturer_name": "Signify Netherlands B.V.", - "model_id": "LLC011", - "product_archetype": "hue_bloom", - "product_name": "Hue bloom", - "software_version": "67.91.1" - }, - "services": [ - { - "rid": "74a45fee-1b3d-4553-b5ab-040da8a10cfd", - "rtype": "light" - }, - { - "rid": "98baae94-76d9-4bc4-a1d1-d53f1d7b1286", - "rtype": "zigbee_connectivity" - }, - { - "rid": "8e6a4ff3-14ca-42f9-8358-9d691b9a4524", - "rtype": "entertainment" - } - ], - "type": "device" - }, - { - "id": "1c8be0d5-a68b-45c2-8f56-530d13b0c128", - "id_v1": "/lights/24", - "metadata": { - "archetype": "hue_lightstrip_tv", - "name": "Hue light with color and color temperature gradient" - }, - "product_data": { - "certified": true, - "manufacturer_name": "Signify Netherlands B.V.", - "model_id": "LCX003", - "product_archetype": "hue_lightstrip_tv", - "product_name": "Hue play gradient lightstrip", - "software_version": "1.86.7" - }, - "services": [ - { - "rid": "8015b17f-8336-415b-966a-b364bd082397", - "rtype": "light" - }, - { - "rid": "ff4e6545-341f-4b0d-9869-b6feb6e6fe87", - "rtype": "zigbee_connectivity" - }, - { - "rid": "7b03eb98-4cfd-4acf-ac11-675f51613e5e", - "rtype": "entertainment" - } - ], - "type": "device" - }, - { - "id": "2330b45d-6079-4c6e-bba6-1b68afb1a0d6", - "id_v1": "/sensors/66", - "metadata": { - "archetype": "unknown_archetype", - "name": "Hue motion sensor" - }, - "product_data": { - "certified": true, - "manufacturer_name": "Signify Netherlands B.V.", - "model_id": "SML001", - "product_archetype": "unknown_archetype", - "product_name": "Hue motion sensor", - "software_version": "1.1.27575" - }, - "services": [ - { - "rid": "b6896534-016d-4052-8cb4-ef04454df62c", - "rtype": "motion" - }, - { - "rid": "669f609d-4860-4f1c-bc25-7a9cec1c3b6c", - "rtype": "device_power" - }, - { - "rid": "ec9b5ad7-2471-4356-b757-d00537828963", - "rtype": "zigbee_connectivity" - }, - { - "rid": "d504e7a4-9a18-4854-90fd-c5b6ac102c40", - "rtype": "light_level" - }, - { - "rid": "66466e14-d2fa-4b96-b2a0-e10de9cd8b8b", - "rtype": "temperature" - } - ], - "type": "device" - }, - { - "alert": { - "action_values": ["breathe"] - }, - "color": { - "gamut": { - "blue": { - "x": 0.1532, - "y": 0.0475 - }, - "green": { - "x": 0.17, - "y": 0.7 - }, - "red": { - "x": 0.6915, - "y": 0.3083 - } - }, - "gamut_type": "C", - "xy": { - "x": 0.5614, - "y": 0.4058 - } - }, - "color_temperature": { - "mirek": null, - "mirek_schema": { - "mirek_maximum": 500, - "mirek_minimum": 153 - }, - "mirek_valid": false - }, - "dimming": { - "brightness": 46.85, - "min_dim_level": 0.10000000149011612 - }, - "dynamics": { - "speed": 0.627, - "speed_valid": true, - "status": "dynamic_palette", - "status_values": ["none", "dynamic_palette"] - }, - "id": "02cba059-9c2c-4d45-97e4-4f79b1bfbaa1", - "id_v1": "/lights/29", - "metadata": { - "archetype": "floor_shade", - "name": "Hue light with color and color temperature 1" - }, - "mode": "normal", - "on": { - "on": true - }, - "owner": { + { "rid": "0b216218-d811-4c95-8c55-bbcda50f9d50", "rtype": "device" }, - "type": "light" - }, - { - "alert": { - "action_values": ["breathe"] - }, - "color_temperature": { - "mirek": 369, - "mirek_schema": { - "mirek_maximum": 454, - "mirek_minimum": 153 - }, - "mirek_valid": true - }, - "dimming": { - "brightness": 59.45, - "min_dim_level": 0.10000000149011612 - }, - "dynamics": { - "speed": 0.0, - "speed_valid": false, - "status": "none", - "status_values": ["none"] - }, - "id": "3a6710fa-4474-4eba-b533-5e6e72968feb", - "id_v1": "/lights/4", - "metadata": { - "archetype": "ceiling_round", - "name": "Hue light with color temperature only" - }, - "mode": "normal", - "on": { - "on": false - }, - "owner": { - "rid": "60b849cc-a8b5-4034-8881-ed1cd560fd13", - "rtype": "device" - }, - "type": "light" - }, - { - "alert": { - "action_values": ["breathe"] - }, - "color": { - "gamut": { - "blue": { - "x": 0.1532, - "y": 0.0475 - }, - "green": { - "x": 0.17, - "y": 0.7 - }, - "red": { - "x": 0.6915, - "y": 0.3083 - } - }, - "gamut_type": "C", - "xy": { - "x": 0.5022, - "y": 0.4466 - } - }, - "color_temperature": { - "mirek": null, - "mirek_schema": { - "mirek_maximum": 500, - "mirek_minimum": 153 - }, - "mirek_valid": false - }, - "dimming": { - "brightness": 46.85, - "min_dim_level": 0.02500000037252903 - }, - "dynamics": { - "speed": 0.627, - "speed_valid": true, - "status": "dynamic_palette", - "status_values": ["none", "dynamic_palette"] - }, - "id": "b3fe71ef-d0ef-48de-9355-d9e604377df0", - "id_v1": "/lights/16", - "metadata": { - "archetype": "hue_lightstrip", - "name": "Hue light with color and color temperature 2" - }, - "mode": "normal", - "on": { - "on": true - }, - "owner": { + { "rid": "b9e76da7-ac22-476a-986d-e466e62e962f", "rtype": "device" }, - "type": "light" - }, - { - "alert": { - "action_values": ["breathe"] - }, - "dynamics": { - "speed": 0.0, - "speed_valid": false, - "status": "none", - "status_values": ["none"] - }, - "id": "7697ac8a-25aa-4576-bb40-0036c0db15b9", - "id_v1": "/lights/23", - "metadata": { - "archetype": "classic_bulb", - "name": "Hue on/off light" - }, - "mode": "normal", - "on": { - "on": false - }, - "owner": { - "rid": "fcdfab5d-8e04-4e9c-a999-7f92cb38c4fc", - "rtype": "device" - }, - "type": "light" - }, - { - "alert": { - "action_values": ["breathe"] - }, - "color": { - "gamut": { - "blue": { - "x": 0.138, - "y": 0.08 - }, - "green": { - "x": 0.2151, - "y": 0.7106 - }, - "red": { - "x": 0.704, - "y": 0.296 - } - }, - "gamut_type": "A", - "xy": { - "x": 0.4849, - "y": 0.3895 - } - }, - "dimming": { - "brightness": 50.0, - "min_dim_level": 10.0 - }, - "dynamics": { - "speed": 0.6389, - "speed_valid": true, - "status": "dynamic_palette", - "status_values": ["none", "dynamic_palette"] - }, - "id": "74a45fee-1b3d-4553-b5ab-040da8a10cfd", - "id_v1": "/lights/11", - "metadata": { - "archetype": "hue_bloom", - "name": "Hue light with color only" - }, - "mode": "normal", - "on": { - "on": true - }, - "owner": { - "rid": "8d07d39c-3c19-47ce-ac7a-8bf3d8e849b9", - "rtype": "device" - }, - "type": "light" - }, - { - "alert": { - "action_values": ["breathe"] - }, - "color": { - "gamut": { - "blue": { - "x": 0.1532, - "y": 0.0475 - }, - "green": { - "x": 0.17, - "y": 0.7 - }, - "red": { - "x": 0.6915, - "y": 0.3083 - } - }, - "gamut_type": "C", - "xy": { - "x": 0.5022, - "y": 0.4466 - } - }, - "color_temperature": { - "mirek": null, - "mirek_schema": { - "mirek_maximum": 500, - "mirek_minimum": 153 - }, - "mirek_valid": false - }, - "dimming": { - "brightness": 46.85, - "min_dim_level": 0.10000000149011612 - }, - "dynamics": { - "speed": 0.627, - "speed_valid": true, - "status": "dynamic_palette", - "status_values": ["none", "dynamic_palette"] - }, - "gradient": { - "points": [ - { - "color": { - "xy": { - "x": 0.5022, - "y": 0.4466 - } - } - }, - { - "color": { - "xy": { - "x": 0.4806, - "y": 0.4484 - } - } - }, - { - "color": { - "xy": { - "x": 0.5022, - "y": 0.4466 - } - } - }, - { - "color": { - "xy": { - "x": 0.5614, - "y": 0.4058 - } - } - }, - { - "color": { - "xy": { - "x": 0.5022, - "y": 0.4466 - } - } - } - ], - "points_capable": 5 - }, - "id": "8015b17f-8336-415b-966a-b364bd082397", - "id_v1": "/lights/24", - "metadata": { - "archetype": "hue_lightstrip_tv", - "name": "Hue light with color and color temperature gradient" - }, - "mode": "normal", - "on": { - "on": true - }, - "owner": { - "rid": "1c8be0d5-a68b-45c2-8f56-530d13b0c128", - "rtype": "device" - }, - "type": "light" - }, - { - "id": "af520f40-e080-43b0-9bb5-41a4d5251b2b", - "id_v1": "/sensors/50", - "mac_address": "00:17:88:01:0b:aa:bb:99", - "owner": { - "rid": "3ff06175-29e8-44a8-8fe7-af591b0025da", - "rtype": "device" - }, - "status": "connected", - "type": "zigbee_connectivity" - }, - { - "id": "1987ba66-c21d-48d0-98fb-121d939a71f3", - "id_v1": "/lights/29", - "mac_address": "00:17:88:01:09:aa:bb:65", - "owner": { - "rid": "0b216218-d811-4c95-8c55-bbcda50f9d50", - "rtype": "device" - }, - "status": "connected", - "type": "zigbee_connectivity" - }, - { - "id": "bd878f44-feb7-406e-8af9-6a1796d1ddc9", - "id_v1": "/lights/4", - "mac_address": "00:17:88:01:06:aa:bb:58", - "owner": { - "rid": "60b849cc-a8b5-4034-8881-ed1cd560fd13", - "rtype": "device" - }, - "status": "connected", - "type": "zigbee_connectivity" - }, - { - "id": "db50a5d9-8cc7-486f-be06-c0b8f0d26c69", - "id_v1": "/sensors/10", - "mac_address": "00:17:88:01:08:aa:cc:60", - "owner": { - "rid": "342daec9-391b-480b-abdd-87f1aa04ce3b", - "rtype": "device" - }, - "status": "connected", - "type": "zigbee_connectivity" - }, - { - "id": "717afeb6-b1ce-426e-96de-48e8fe037fb0", - "id_v1": "/lights/16", - "mac_address": "00:17:88:aa:aa:bb:0d:ab", - "owner": { - "rid": "b9e76da7-ac22-476a-986d-e466e62e962f", - "rtype": "device" - }, - "status": "connected", - "type": "zigbee_connectivity" - }, - { - "id": "6b00ce2b-a8a5-4bab-bc5e-757a0b0338ff", - "id_v1": "/lights/23", - "mac_address": "00:12:4b:00:1f:aa:bb:f3", - "owner": { - "rid": "fcdfab5d-8e04-4e9c-a999-7f92cb38c4fc", - "rtype": "device" - }, - "status": "connected", - "type": "zigbee_connectivity" - }, - { - "id": "bba44861-8222-45c9-9e6b-d7f3a6543829", - "id_v1": "/sensors/5", - "mac_address": "00:17:88:01:aa:cc:87:b6", - "owner": { - "rid": "7745ebea-dd33-429c-a900-bae4e7ae1107", - "rtype": "device" - }, - "status": "connected", - "type": "zigbee_connectivity" - }, - { - "id": "6c898412-ed25-4402-9807-a0c326616b0f", - "id_v1": "", - "mac_address": "00:17:88:01:aa:bb:fd:c7", - "owner": { - "rid": "4a507550-8742-4087-8bf5-c2334f29891c", - "rtype": "device" - }, - "status": "connected", - "type": "zigbee_connectivity" - }, - { - "id": "d2ae969a-add5-41b1-afbd-f2837b2eb551", - "id_v1": "/lights/34", - "mac_address": "00:17:88:01:aa:bb:cc:ed", - "owner": { + { "rid": "5ad8326c-e51a-4594-8738-fc700b53fcc4", "rtype": "device" }, - "status": "connected", - "type": "zigbee_connectivity" - }, - { - "id": "98baae94-76d9-4bc4-a1d1-d53f1d7b1286", - "id_v1": "/lights/11", - "mac_address": "00:17:88:aa:bb:1e:cc:b2", - "owner": { + { "rid": "8d07d39c-3c19-47ce-ac7a-8bf3d8e849b9", "rtype": "device" }, - "status": "connected", - "type": "zigbee_connectivity" - }, - { - "id": "ff4e6545-341f-4b0d-9869-b6feb6e6fe87", - "id_v1": "/lights/24", - "mac_address": "00:17:88:01:aa:bb:cc:3d", - "owner": { + { "rid": "1c8be0d5-a68b-45c2-8f56-530d13b0c128", "rtype": "device" }, - "status": "connected", - "type": "zigbee_connectivity" - }, - { - "id": "ec9b5ad7-2471-4356-b757-d00537828963", - "id_v1": "/sensors/66", - "mac_address": "00:17:aa:bb:cc:09:ac:c3", - "owner": { - "rid": "2330b45d-6079-4c6e-bba6-1b68afb1a0d6", - "rtype": "device" - }, - "status": "connected", - "type": "zigbee_connectivity" - }, - { - "id": "5d7b3979-b936-47ff-8458-554f8a2921db", - "id_v1": "/lights/29", - "owner": { - "rid": "0b216218-d811-4c95-8c55-bbcda50f9d50", - "rtype": "device" - }, - "proxy": true, - "renderer": true, - "segments": { - "configurable": false, - "max_segments": 1, - "segments": [ - { - "length": 1, - "start": 0 - } - ] - }, - "type": "entertainment" - }, - { - "id": "d88acc42-259c-43b5-bf5d-90c16cdb8f2f", - "id_v1": "/lights/16", - "owner": { - "rid": "b9e76da7-ac22-476a-986d-e466e62e962f", - "rtype": "device" - }, - "proxy": true, - "renderer": true, - "segments": { - "configurable": false, - "max_segments": 1, - "segments": [ - { - "length": 1, - "start": 0 - } - ] - }, - "type": "entertainment" - }, - { - "id": "b8ab0c30-b227-4d35-9c96-7cd16131fcc5", - "id_v1": "", - "owner": { - "rid": "4a507550-8742-4087-8bf5-c2334f29891c", - "rtype": "device" - }, - "proxy": true, - "renderer": false, - "type": "entertainment" - }, - { - "id": "8e6a4ff3-14ca-42f9-8358-9d691b9a4524", - "id_v1": "/lights/11", - "owner": { - "rid": "8d07d39c-3c19-47ce-ac7a-8bf3d8e849b9", - "rtype": "device" - }, - "proxy": false, - "renderer": true, - "segments": { - "configurable": false, - "max_segments": 1, - "segments": [ - { - "length": 1, - "start": 0 - } - ] - }, - "type": "entertainment" - }, - { - "id": "7b03eb98-4cfd-4acf-ac11-675f51613e5e", - "id_v1": "/lights/24", - "owner": { - "rid": "1c8be0d5-a68b-45c2-8f56-530d13b0c128", - "rtype": "device" - }, - "proxy": true, - "renderer": true, - "segments": { - "configurable": false, - "max_segments": 10, - "segments": [ - { - "length": 2, - "start": 0 - }, - { - "length": 3, - "start": 2 - }, - { - "length": 5, - "start": 5 - }, - { - "length": 4, - "start": 10 - }, - { - "length": 5, - "start": 14 - }, - { - "length": 3, - "start": 19 - }, - { - "length": 2, - "start": 22 - } - ] - }, - "type": "entertainment" - }, - { - "button": { - "last_event": "short_release" - }, - "id": "c658d3d8-a013-4b81-8ac6-78b248537e70", - "id_v1": "/sensors/50", - "metadata": { - "control_id": 1 - }, - "owner": { - "rid": "3ff06175-29e8-44a8-8fe7-af591b0025da", - "rtype": "device" - }, - "type": "button" - }, - { - "id": "be1eb834-bdf5-4d26-8fba-7b1feaa83a9d", - "id_v1": "/sensors/50", - "metadata": { - "control_id": 2 - }, - "owner": { - "rid": "3ff06175-29e8-44a8-8fe7-af591b0025da", - "rtype": "device" - }, - "type": "button" - }, - { - "id": "f92aa267-1387-4f02-9950-210fb7ca1f5a", - "id_v1": "/sensors/10", - "metadata": { - "control_id": 1 - }, - "owner": { - "rid": "342daec9-391b-480b-abdd-87f1aa04ce3b", - "rtype": "device" - }, - "type": "button" - }, - { - "button": { - "last_event": "short_release" - }, - "id": "7f1ab9f6-cc2b-4b40-9011-65e2af153f75", - "id_v1": "/sensors/10", - "metadata": { - "control_id": 2 - }, - "owner": { - "rid": "342daec9-391b-480b-abdd-87f1aa04ce3b", - "rtype": "device" - }, - "type": "button" - }, - { - "id": "b4edb2d6-55d0-47f4-bd43-7ae215ef1062", - "id_v1": "/sensors/10", - "metadata": { - "control_id": 3 - }, - "owner": { - "rid": "342daec9-391b-480b-abdd-87f1aa04ce3b", - "rtype": "device" - }, - "type": "button" - }, - { - "id": "40a810bf-3d22-4c56-9334-4a59a00768ab", - "id_v1": "/sensors/10", - "metadata": { - "control_id": 4 - }, - "owner": { - "rid": "342daec9-391b-480b-abdd-87f1aa04ce3b", - "rtype": "device" - }, - "type": "button" - }, - { - "button": { - "last_event": "short_release" - }, - "id": "31cffcda-efc2-401f-a152-e10db3eed232", - "id_v1": "/sensors/5", - "metadata": { - "control_id": 1 - }, - "owner": { + { "rid": "7745ebea-dd33-429c-a900-bae4e7ae1107", "rtype": "device" }, - "type": "button" - }, - { - "id": "c1cd98a6-6c23-43bb-b6e1-08dda9e168a4", - "id_v1": "/sensors/50", - "owner": { + { "rid": "3ff06175-29e8-44a8-8fe7-af591b0025da", "rtype": "device" }, - "power_state": { - "battery_level": 100, - "battery_state": "normal" - }, - "type": "device_power" - }, - { - "id": "0bb058bc-2139-43d9-8c9b-edfb4570953b", - "id_v1": "/sensors/10", - "owner": { - "rid": "342daec9-391b-480b-abdd-87f1aa04ce3b", - "rtype": "device" - }, - "power_state": { - "battery_level": 83, - "battery_state": "normal" - }, - "type": "device_power" - }, - { - "id": "3f219f5a-ad6c-484f-b976-769a9c267a72", - "id_v1": "/sensors/5", - "owner": { - "rid": "7745ebea-dd33-429c-a900-bae4e7ae1107", - "rtype": "device" - }, - "power_state": { - "battery_level": 91, - "battery_state": "normal" - }, - "type": "device_power" - }, - { - "id": "669f609d-4860-4f1c-bc25-7a9cec1c3b6c", - "id_v1": "/sensors/66", - "owner": { + { "rid": "2330b45d-6079-4c6e-bba6-1b68afb1a0d6", "rtype": "device" }, - "power_state": { - "battery_level": 100, - "battery_state": "normal" + { + "rid": "342daec9-391b-480b-abdd-87f1aa04ce3b", + "rtype": "device" + } + ], + "grouped_services": [ + { + "rid": "0a74457c-cb8d-44c2-a5a5-dcb7b3675550", + "rtype": "grouped_light" + } + ], + "id": "a3fbc86a-bf4c-4c69-899d-d6eafc37e288", + "id_v1": "/groups/0", + "services": [ + { + "rid": "3a6710fa-4474-4eba-b533-5e6e72968feb", + "rtype": "light" }, - "type": "device_power" + { + "rid": "d0df7249-02c1-4480-ba2c-d61b1e648a58", + "rtype": "light" + }, + { + "rid": "74a45fee-1b3d-4553-b5ab-040da8a10cfd", + "rtype": "light" + }, + { + "rid": "d2d48fac-df99-4f8d-8bdc-bac82d2cfb24", + "rtype": "light" + }, + { + "rid": "b3fe71ef-d0ef-48de-9355-d9e604377df0", + "rtype": "light" + }, + { + "rid": "1d1ac857-9b89-48aa-a4f3-68302e7d0998", + "rtype": "light" + }, + { + "rid": "7697ac8a-25aa-4576-bb40-0036c0db15b9", + "rtype": "light" + }, + { + "rid": "8015b17f-8336-415b-966a-b364bd082397", + "rtype": "light" + }, + { + "rid": "02cba059-9c2c-4d45-97e4-4f79b1bfbaa1", + "rtype": "light" + }, + { + "rid": "6a5d8ce8-c0a0-43bb-870e-d7e641cdb063", + "rtype": "button" + }, + { + "rid": "85fa4928-b061-4d19-8458-c5e30d375e39", + "rtype": "button" + }, + { + "rid": "a0640313-0a01-42b9-b236-c5e0a1568ef5", + "rtype": "button" + }, + { + "rid": "50fe978e-117c-4fc5-bb17-f707c1614a11", + "rtype": "button" + }, + { + "rid": "31cffcda-efc2-401f-a152-e10db3eed232", + "rtype": "button" + }, + { + "rid": "f92aa267-1387-4f02-9950-210fb7ca1f5a", + "rtype": "button" + }, + { + "rid": "7f1ab9f6-cc2b-4b40-9011-65e2af153f75", + "rtype": "button" + }, + { + "rid": "b4edb2d6-55d0-47f4-bd43-7ae215ef1062", + "rtype": "button" + }, + { + "rid": "40a810bf-3d22-4c56-9334-4a59a00768ab", + "rtype": "button" + }, + { + "rid": "487aa265-8ea1-4280-a663-cbf93a79ccd7", + "rtype": "button" + }, + { + "rid": "f6e137cf-8e93-4f6a-be9c-2f820bf6d893", + "rtype": "button" + }, + { + "rid": "c658d3d8-a013-4b81-8ac6-78b248537e70", + "rtype": "button" + }, + { + "rid": "be1eb834-bdf5-4d26-8fba-7b1feaa83a9d", + "rtype": "button" + }, + { + "rid": "b6896534-016d-4052-8cb4-ef04454df62c", + "rtype": "motion" + }, + { + "rid": "d504e7a4-9a18-4854-90fd-c5b6ac102c40", + "rtype": "light_level" + }, + { + "rid": "66466e14-d2fa-4b96-b2a0-e10de9cd8b8b", + "rtype": "temperature" + }, + { + "rid": "0a74457c-cb8d-44c2-a5a5-dcb7b3675550", + "rtype": "grouped_light" + } + ], + "type": "bridge_home" + }, + { + "children": [ + { + "rid": "60b849cc-a8b5-4034-8881-ed1cd560fd13", + "rtype": "device" + }, + { + "rid": "fcdfab5d-8e04-4e9c-a999-7f92cb38c4fc", + "rtype": "device" + } + ], + "grouped_services": [ + { + "rid": "e937f8db-2f0e-49a0-936e-027e60e15b34", + "rtype": "grouped_light" + } + ], + "id": "6ddc9066-7e7d-4a03-a773-c73937968296", + "id_v1": "/groups/3", + "metadata": { + "archetype": "bathroom", + "name": "Test Room" }, - { - "children": [ - { - "rid": "02cba059-9c2c-4d45-97e4-4f79b1bfbaa1", - "rtype": "light" - }, - { - "rid": "b3fe71ef-d0ef-48de-9355-d9e604377df0", - "rtype": "light" - }, - { - "rid": "8015b17f-8336-415b-966a-b364bd082397", - "rtype": "light" - } - ], - "grouped_services": [ - { - "rid": "f2416154-9607-43ab-a684-4453108a200e", - "rtype": "grouped_light" - } - ], - "id": "7cee478d-6455-483a-9e32-9f9fdcbcc4f6", - "id_v1": "/groups/5", - "metadata": { - "archetype": "downstairs", - "name": "Test Zone" + "services": [ + { + "rid": "7697ac8a-25aa-4576-bb40-0036c0db15b9", + "rtype": "light" }, - "services": [ - { - "rid": "02cba059-9c2c-4d45-97e4-4f79b1bfbaa1", - "rtype": "light" - }, - { - "rid": "b3fe71ef-d0ef-48de-9355-d9e604377df0", - "rtype": "light" - }, - { - "rid": "8015b17f-8336-415b-966a-b364bd082397", - "rtype": "light" - }, - { - "rid": "f2416154-9607-43ab-a684-4453108a200e", - "rtype": "grouped_light" - } - ], - "type": "zone" - }, - { - "alert": { - "action_values": ["breathe"] + { + "rid": "3a6710fa-4474-4eba-b533-5e6e72968feb", + "rtype": "light" }, - "id": "f2416154-9607-43ab-a684-4453108a200e", - "id_v1": "/groups/5", - "on": { - "on": true - }, - "type": "grouped_light" - }, - { - "alert": { - "action_values": ["breathe"] - }, - "id": "0a74457c-cb8d-44c2-a5a5-dcb7b3675550", - "id_v1": "/groups/0", - "on": { - "on": true - }, - "type": "grouped_light" - }, - { - "alert": { - "action_values": ["breathe"] - }, - "id": "e937f8db-2f0e-49a0-936e-027e60e15b34", - "id_v1": "/groups/3", - "on": { - "on": false - }, - "type": "grouped_light" - }, - { - "children": [ - { - "rid": "6ddc9066-7e7d-4a03-a773-c73937968296", - "rtype": "room" - }, - { - "rid": "0b216218-d811-4c95-8c55-bbcda50f9d50", - "rtype": "device" - }, - { - "rid": "b9e76da7-ac22-476a-986d-e466e62e962f", - "rtype": "device" - }, - { - "rid": "5ad8326c-e51a-4594-8738-fc700b53fcc4", - "rtype": "device" - }, - { - "rid": "8d07d39c-3c19-47ce-ac7a-8bf3d8e849b9", - "rtype": "device" - }, - { - "rid": "1c8be0d5-a68b-45c2-8f56-530d13b0c128", - "rtype": "device" - }, - { - "rid": "7745ebea-dd33-429c-a900-bae4e7ae1107", - "rtype": "device" - }, - { - "rid": "3ff06175-29e8-44a8-8fe7-af591b0025da", - "rtype": "device" - }, - { - "rid": "2330b45d-6079-4c6e-bba6-1b68afb1a0d6", - "rtype": "device" - }, - { - "rid": "342daec9-391b-480b-abdd-87f1aa04ce3b", - "rtype": "device" - } - ], - "grouped_services": [ - { - "rid": "0a74457c-cb8d-44c2-a5a5-dcb7b3675550", - "rtype": "grouped_light" - } - ], - "id": "a3fbc86a-bf4c-4c69-899d-d6eafc37e288", - "id_v1": "/groups/0", - "services": [ - { - "rid": "3a6710fa-4474-4eba-b533-5e6e72968feb", - "rtype": "light" - }, - { - "rid": "d0df7249-02c1-4480-ba2c-d61b1e648a58", - "rtype": "light" - }, - { - "rid": "74a45fee-1b3d-4553-b5ab-040da8a10cfd", - "rtype": "light" - }, - { - "rid": "d2d48fac-df99-4f8d-8bdc-bac82d2cfb24", - "rtype": "light" - }, - { - "rid": "b3fe71ef-d0ef-48de-9355-d9e604377df0", - "rtype": "light" - }, - { - "rid": "1d1ac857-9b89-48aa-a4f3-68302e7d0998", - "rtype": "light" - }, - { - "rid": "7697ac8a-25aa-4576-bb40-0036c0db15b9", - "rtype": "light" - }, - { - "rid": "8015b17f-8336-415b-966a-b364bd082397", - "rtype": "light" - }, - { - "rid": "02cba059-9c2c-4d45-97e4-4f79b1bfbaa1", - "rtype": "light" - }, - { - "rid": "6a5d8ce8-c0a0-43bb-870e-d7e641cdb063", - "rtype": "button" - }, - { - "rid": "85fa4928-b061-4d19-8458-c5e30d375e39", - "rtype": "button" - }, - { - "rid": "a0640313-0a01-42b9-b236-c5e0a1568ef5", - "rtype": "button" - }, - { - "rid": "50fe978e-117c-4fc5-bb17-f707c1614a11", - "rtype": "button" - }, - { - "rid": "31cffcda-efc2-401f-a152-e10db3eed232", - "rtype": "button" - }, - { - "rid": "f92aa267-1387-4f02-9950-210fb7ca1f5a", - "rtype": "button" - }, - { - "rid": "7f1ab9f6-cc2b-4b40-9011-65e2af153f75", - "rtype": "button" - }, - { - "rid": "b4edb2d6-55d0-47f4-bd43-7ae215ef1062", - "rtype": "button" - }, - { - "rid": "40a810bf-3d22-4c56-9334-4a59a00768ab", - "rtype": "button" - }, - { - "rid": "487aa265-8ea1-4280-a663-cbf93a79ccd7", - "rtype": "button" - }, - { - "rid": "f6e137cf-8e93-4f6a-be9c-2f820bf6d893", - "rtype": "button" - }, - { - "rid": "c658d3d8-a013-4b81-8ac6-78b248537e70", - "rtype": "button" - }, - { - "rid": "be1eb834-bdf5-4d26-8fba-7b1feaa83a9d", - "rtype": "button" - }, - { - "rid": "b6896534-016d-4052-8cb4-ef04454df62c", - "rtype": "motion" - }, - { - "rid": "d504e7a4-9a18-4854-90fd-c5b6ac102c40", - "rtype": "light_level" - }, - { - "rid": "66466e14-d2fa-4b96-b2a0-e10de9cd8b8b", - "rtype": "temperature" - }, - { - "rid": "0a74457c-cb8d-44c2-a5a5-dcb7b3675550", - "rtype": "grouped_light" - } - ], - "type": "bridge_home" - }, - { - "children": [ - { - "rid": "60b849cc-a8b5-4034-8881-ed1cd560fd13", - "rtype": "device" - }, - { - "rid": "fcdfab5d-8e04-4e9c-a999-7f92cb38c4fc", - "rtype": "device" - } - ], - "grouped_services": [ - { - "rid": "e937f8db-2f0e-49a0-936e-027e60e15b34", - "rtype": "grouped_light" - } - ], - "id": "6ddc9066-7e7d-4a03-a773-c73937968296", - "id_v1": "/groups/3", - "metadata": { - "archetype": "bathroom", - "name": "Test Room" - }, - "services": [ - { - "rid": "7697ac8a-25aa-4576-bb40-0036c0db15b9", - "rtype": "light" - }, - { - "rid": "3a6710fa-4474-4eba-b533-5e6e72968feb", - "rtype": "light" - }, - { - "rid": "e937f8db-2f0e-49a0-936e-027e60e15b34", - "rtype": "grouped_light" - } - ], - "type": "room" - }, - { - "channels": [ - { - "channel_id": 0, - "members": [ - { - "index": 0, - "service": { - "rid": "5d7b3979-b936-47ff-8458-554f8a2921db", - "rtype": "entertainment" - } - } - ], - "position": { - "x": -0.8399999737739563, - "y": 0.8999999761581421, - "z": -0.5 - } - }, - { - "channel_id": 1, - "members": [ - { - "index": 0, - "service": { - "rid": "d88acc42-259c-43b5-bf5d-90c16cdb8f2f", - "rtype": "entertainment" - } - } - ], - "position": { - "x": -0.9399999976158142, - "y": -0.20999999344348907, - "z": -1.0 - } - }, - { - "channel_id": 2, - "members": [ - { - "index": 0, - "service": { - "rid": "7b03eb98-4cfd-4acf-ac11-675f51613e5e", - "rtype": "entertainment" - } - } - ], - "position": { - "x": -0.4000000059604645, - "y": 0.800000011920929, - "z": -0.4000000059604645 - } - }, - { - "channel_id": 3, - "members": [ - { - "index": 1, - "service": { - "rid": "7b03eb98-4cfd-4acf-ac11-675f51613e5e", - "rtype": "entertainment" - } - } - ], - "position": { - "x": -0.4000000059604645, - "y": 0.800000011920929, - "z": 0.0 - } - }, - { - "channel_id": 4, - "members": [ - { - "index": 2, - "service": { - "rid": "7b03eb98-4cfd-4acf-ac11-675f51613e5e", - "rtype": "entertainment" - } - } - ], - "position": { - "x": -0.4000000059604645, - "y": 0.800000011920929, - "z": 0.4000000059604645 - } - }, - { - "channel_id": 5, - "members": [ - { - "index": 3, - "service": { - "rid": "7b03eb98-4cfd-4acf-ac11-675f51613e5e", - "rtype": "entertainment" - } - } - ], - "position": { - "x": 0.0, - "y": 0.800000011920929, - "z": 0.4000000059604645 - } - }, - { - "channel_id": 6, - "members": [ - { - "index": 4, - "service": { - "rid": "7b03eb98-4cfd-4acf-ac11-675f51613e5e", - "rtype": "entertainment" - } - } - ], - "position": { - "x": 0.4000000059604645, - "y": 0.800000011920929, - "z": 0.4000000059604645 - } - }, - { - "channel_id": 7, - "members": [ - { - "index": 5, - "service": { - "rid": "7b03eb98-4cfd-4acf-ac11-675f51613e5e", - "rtype": "entertainment" - } - } - ], - "position": { - "x": 0.4000000059604645, - "y": 0.800000011920929, - "z": 0.0 - } - }, - { - "channel_id": 8, - "members": [ - { - "index": 6, - "service": { - "rid": "7b03eb98-4cfd-4acf-ac11-675f51613e5e", - "rtype": "entertainment" - } - } - ], - "position": { - "x": 0.4000000059604645, - "y": 0.800000011920929, - "z": -0.4000000059604645 - } - }, - { - "channel_id": 9, - "members": [ - { - "index": 0, - "service": { - "rid": "be321947-0a48-4742-913d-073b3b540c97", - "rtype": "entertainment" - } - } - ], - "position": { - "x": 0.9100000262260437, - "y": 0.8100000023841858, - "z": -0.3799999952316284 - } - } - ], - "configuration_type": "screen", - "id": "c14cf1cf-6c7a-4984-b8bb-c5b71aeb70fc", - "id_v1": "/groups/2", - "light_services": [ - { - "rid": "02cba059-9c2c-4d45-97e4-4f79b1bfbaa1", - "rtype": "light" - }, - { - "rid": "b3fe71ef-d0ef-48de-9355-d9e604377df0", - "rtype": "light" - }, - { - "rid": "8015b17f-8336-415b-966a-b364bd082397", - "rtype": "light" - } - ], - "locations": { - "service_locations": [ + { + "rid": "e937f8db-2f0e-49a0-936e-027e60e15b34", + "rtype": "grouped_light" + } + ], + "type": "room" + }, + { + "channels": [ + { + "channel_id": 0, + "members": [ { - "position": { - "x": -0.8399999737739563, - "y": 0.8999999761581421, - "z": -0.5 - }, - "positions": [ - { - "x": -0.8399999737739563, - "y": 0.8999999761581421, - "z": -0.5 - } - ], + "index": 0, "service": { "rid": "5d7b3979-b936-47ff-8458-554f8a2921db", "rtype": "entertainment" } - }, + } + ], + "position": { + "x": -0.8399999737739563, + "y": 0.8999999761581421, + "z": -0.5 + } + }, + { + "channel_id": 1, + "members": [ { - "position": { - "x": -0.9399999976158142, - "y": -0.20999999344348907, - "z": -1.0 - }, - "positions": [ - { - "x": -0.9399999976158142, - "y": -0.20999999344348907, - "z": -1.0 - } - ], + "index": 0, "service": { "rid": "d88acc42-259c-43b5-bf5d-90c16cdb8f2f", "rtype": "entertainment" } - }, + } + ], + "position": { + "x": -0.9399999976158142, + "y": -0.20999999344348907, + "z": -1.0 + } + }, + { + "channel_id": 2, + "members": [ { - "position": { - "x": -0.4000000059604645, - "y": 0.800000011920929, - "z": -0.4000000059604645 - }, - "positions": [ - { - "x": -0.4000000059604645, - "y": 0.800000011920929, - "z": -0.4000000059604645 - }, - { - "x": 0.4000000059604645, - "y": 0.800000011920929, - "z": -0.4000000059604645 - } - ], + "index": 0, "service": { "rid": "7b03eb98-4cfd-4acf-ac11-675f51613e5e", "rtype": "entertainment" } - }, + } + ], + "position": { + "x": -0.4000000059604645, + "y": 0.800000011920929, + "z": -0.4000000059604645 + } + }, + { + "channel_id": 3, + "members": [ { - "position": { - "x": 0.9100000262260437, - "y": 0.8100000023841858, - "z": -0.3799999952316284 - }, - "positions": [ - { - "x": 0.9100000262260437, - "y": 0.8100000023841858, - "z": -0.3799999952316284 - } - ], + "index": 1, + "service": { + "rid": "7b03eb98-4cfd-4acf-ac11-675f51613e5e", + "rtype": "entertainment" + } + } + ], + "position": { + "x": -0.4000000059604645, + "y": 0.800000011920929, + "z": 0.0 + } + }, + { + "channel_id": 4, + "members": [ + { + "index": 2, + "service": { + "rid": "7b03eb98-4cfd-4acf-ac11-675f51613e5e", + "rtype": "entertainment" + } + } + ], + "position": { + "x": -0.4000000059604645, + "y": 0.800000011920929, + "z": 0.4000000059604645 + } + }, + { + "channel_id": 5, + "members": [ + { + "index": 3, + "service": { + "rid": "7b03eb98-4cfd-4acf-ac11-675f51613e5e", + "rtype": "entertainment" + } + } + ], + "position": { + "x": 0.0, + "y": 0.800000011920929, + "z": 0.4000000059604645 + } + }, + { + "channel_id": 6, + "members": [ + { + "index": 4, + "service": { + "rid": "7b03eb98-4cfd-4acf-ac11-675f51613e5e", + "rtype": "entertainment" + } + } + ], + "position": { + "x": 0.4000000059604645, + "y": 0.800000011920929, + "z": 0.4000000059604645 + } + }, + { + "channel_id": 7, + "members": [ + { + "index": 5, + "service": { + "rid": "7b03eb98-4cfd-4acf-ac11-675f51613e5e", + "rtype": "entertainment" + } + } + ], + "position": { + "x": 0.4000000059604645, + "y": 0.800000011920929, + "z": 0.0 + } + }, + { + "channel_id": 8, + "members": [ + { + "index": 6, + "service": { + "rid": "7b03eb98-4cfd-4acf-ac11-675f51613e5e", + "rtype": "entertainment" + } + } + ], + "position": { + "x": 0.4000000059604645, + "y": 0.800000011920929, + "z": -0.4000000059604645 + } + }, + { + "channel_id": 9, + "members": [ + { + "index": 0, "service": { "rid": "be321947-0a48-4742-913d-073b3b540c97", "rtype": "entertainment" } } - ] - }, - "metadata": { - "name": "Entertainmentroom 1" - }, - "name": "Entertainmentroom 1", - "status": "inactive", - "stream_proxy": { - "mode": "auto", - "node": { - "rid": "b8ab0c30-b227-4d35-9c96-7cd16131fcc5", - "rtype": "entertainment" + ], + "position": { + "x": 0.9100000262260437, + "y": 0.8100000023841858, + "z": -0.3799999952316284 } + } + ], + "configuration_type": "screen", + "id": "c14cf1cf-6c7a-4984-b8bb-c5b71aeb70fc", + "id_v1": "/groups/2", + "light_services": [ + { + "rid": "02cba059-9c2c-4d45-97e4-4f79b1bfbaa1", + "rtype": "light" }, - "type": "entertainment_configuration" - }, - { - "bridge_id": "aabbccddeeffggh", - "id": "07dd5849-abcd-efgh-b9b9-eb540408ce00", - "id_v1": "", - "owner": { - "rid": "4a507550-8742-4087-8bf5-c2334f29891c", - "rtype": "device" + { + "rid": "b3fe71ef-d0ef-48de-9355-d9e604377df0", + "rtype": "light" }, - "time_zone": { - "time_zone": "Europe/Amsterdam" - }, - "type": "bridge" - }, - { - "enabled": true, - "id": "b6896534-016d-4052-8cb4-ef04454df62c", - "id_v1": "/sensors/66", - "motion": { - "motion": false, - "motion_valid": true - }, - "owner": { - "rid": "2330b45d-6079-4c6e-bba6-1b68afb1a0d6", - "rtype": "device" - }, - "type": "motion" - }, - { - "enabled": true, - "id": "d504e7a4-9a18-4854-90fd-c5b6ac102c40", - "id_v1": "/sensors/67", - "light": { - "light_level": 18027, - "light_level_valid": true - }, - "owner": { - "rid": "2330b45d-6079-4c6e-bba6-1b68afb1a0d6", - "rtype": "device" - }, - "type": "light_level" - }, - { - "enabled": true, - "id": "66466e14-d2fa-4b96-b2a0-e10de9cd8b8b", - "id_v1": "/sensors/68", - "owner": { - "rid": "2330b45d-6079-4c6e-bba6-1b68afb1a0d6", - "rtype": "device" - }, - "temperature": { - "temperature": 18.139999389648438, - "temperature_valid": true - }, - "type": "temperature" - }, - { - "configuration": { - "end_state": "last_state", - "where": [ - { - "group": { - "rid": "c14cf1cf-6c7a-4984-b8bb-c5b71aeb70fc", - "rtype": "entertainment_configuration" - } - } - ] - }, - "dependees": [ + { + "rid": "8015b17f-8336-415b-966a-b364bd082397", + "rtype": "light" + } + ], + "locations": { + "service_locations": [ { - "level": "critical", - "target": { + "position": { + "x": -0.8399999737739563, + "y": 0.8999999761581421, + "z": -0.5 + }, + "positions": [ + { + "x": -0.8399999737739563, + "y": 0.8999999761581421, + "z": -0.5 + } + ], + "service": { + "rid": "5d7b3979-b936-47ff-8458-554f8a2921db", + "rtype": "entertainment" + } + }, + { + "position": { + "x": -0.9399999976158142, + "y": -0.20999999344348907, + "z": -1.0 + }, + "positions": [ + { + "x": -0.9399999976158142, + "y": -0.20999999344348907, + "z": -1.0 + } + ], + "service": { + "rid": "d88acc42-259c-43b5-bf5d-90c16cdb8f2f", + "rtype": "entertainment" + } + }, + { + "position": { + "x": -0.4000000059604645, + "y": 0.800000011920929, + "z": -0.4000000059604645 + }, + "positions": [ + { + "x": -0.4000000059604645, + "y": 0.800000011920929, + "z": -0.4000000059604645 + }, + { + "x": 0.4000000059604645, + "y": 0.800000011920929, + "z": -0.4000000059604645 + } + ], + "service": { + "rid": "7b03eb98-4cfd-4acf-ac11-675f51613e5e", + "rtype": "entertainment" + } + }, + { + "position": { + "x": 0.9100000262260437, + "y": 0.8100000023841858, + "z": -0.3799999952316284 + }, + "positions": [ + { + "x": 0.9100000262260437, + "y": 0.8100000023841858, + "z": -0.3799999952316284 + } + ], + "service": { + "rid": "be321947-0a48-4742-913d-073b3b540c97", + "rtype": "entertainment" + } + } + ] + }, + "metadata": { + "name": "Entertainmentroom 1" + }, + "name": "Entertainmentroom 1", + "status": "inactive", + "stream_proxy": { + "mode": "auto", + "node": { + "rid": "b8ab0c30-b227-4d35-9c96-7cd16131fcc5", + "rtype": "entertainment" + } + }, + "type": "entertainment_configuration" + }, + { + "bridge_id": "aabbccddeeffggh", + "id": "07dd5849-abcd-efgh-b9b9-eb540408ce00", + "id_v1": "", + "owner": { + "rid": "4a507550-8742-4087-8bf5-c2334f29891c", + "rtype": "device" + }, + "time_zone": { + "time_zone": "Europe/Amsterdam" + }, + "type": "bridge" + }, + { + "enabled": true, + "id": "b6896534-016d-4052-8cb4-ef04454df62c", + "id_v1": "/sensors/66", + "motion": { + "motion": false, + "motion_valid": true + }, + "owner": { + "rid": "2330b45d-6079-4c6e-bba6-1b68afb1a0d6", + "rtype": "device" + }, + "type": "motion" + }, + { + "enabled": true, + "id": "d504e7a4-9a18-4854-90fd-c5b6ac102c40", + "id_v1": "/sensors/67", + "light": { + "light_level": 18027, + "light_level_valid": true + }, + "owner": { + "rid": "2330b45d-6079-4c6e-bba6-1b68afb1a0d6", + "rtype": "device" + }, + "type": "light_level" + }, + { + "enabled": true, + "id": "66466e14-d2fa-4b96-b2a0-e10de9cd8b8b", + "id_v1": "/sensors/68", + "owner": { + "rid": "2330b45d-6079-4c6e-bba6-1b68afb1a0d6", + "rtype": "device" + }, + "temperature": { + "temperature": 18.139999389648438, + "temperature_valid": true + }, + "type": "temperature" + }, + { + "configuration": { + "end_state": "last_state", + "where": [ + { + "group": { "rid": "c14cf1cf-6c7a-4984-b8bb-c5b71aeb70fc", "rtype": "entertainment_configuration" - }, - "type": "ResourceDependee" + } } - ], - "enabled": true, - "id": "0670cfb1-2bd7-4237-a0e3-1827a44d7231", - "last_error": "", - "metadata": { - "name": "state_after_streaming" - }, - "migrated_from": "/resourcelinks/47450", - "script_id": "7719b841-6b3d-448d-a0e7-601ae9edb6a2", - "status": "running", - "type": "behavior_instance" + ] }, - { - "configuration_schema": { - "$ref": "leaving_home_config.json#" - }, - "description": "Automatically turn off your lights when you leave", - "id": "0194752a-2d53-4f92-8209-dfdc52745af3", - "metadata": { - "name": "Leaving home" - }, - "state_schema": {}, - "trigger_schema": { - "$ref": "trigger.json#" - }, - "type": "behavior_script", - "version": "0.0.1" + "dependees": [ + { + "level": "critical", + "target": { + "rid": "c14cf1cf-6c7a-4984-b8bb-c5b71aeb70fc", + "rtype": "entertainment_configuration" + }, + "type": "ResourceDependee" + } + ], + "enabled": true, + "id": "0670cfb1-2bd7-4237-a0e3-1827a44d7231", + "last_error": "", + "metadata": { + "name": "state_after_streaming" }, - { - "configuration_schema": { - "$ref": "schedule_config.json#" - }, - "description": "Schedule turning on and off lights", - "id": "7238c707-8693-4f19-9095-ccdc1444d228", - "metadata": { - "name": "Schedule" - }, - "state_schema": {}, - "trigger_schema": { - "$ref": "trigger.json#" - }, - "type": "behavior_script", - "version": "0.0.1" + "migrated_from": "/resourcelinks/47450", + "script_id": "7719b841-6b3d-448d-a0e7-601ae9edb6a2", + "status": "running", + "type": "behavior_instance" + }, + { + "configuration_schema": { + "$ref": "leaving_home_config.json#" }, - { - "configuration_schema": { - "$ref": "lights_state_after_streaming_config.json#" - }, - "description": "State of lights in the entertainment group after streaming ends", - "id": "7719b841-6b3d-448d-a0e7-601ae9edb6a2", - "metadata": { - "name": "Light state after streaming" - }, - "state_schema": {}, - "trigger_schema": {}, - "type": "behavior_script", - "version": "0.0.1" + "description": "Automatically turn off your lights when you leave", + "id": "0194752a-2d53-4f92-8209-dfdc52745af3", + "metadata": { + "name": "Leaving home" }, - { - "configuration_schema": { - "$ref": "basic_goto_sleep_config.json#" - }, - "description": "Get ready for nice sleep.", - "id": "7e571ac6-f363-42e1-809a-4cbf6523ed72", - "metadata": { - "name": "Basic go to sleep routine" - }, - "state_schema": {}, - "trigger_schema": { - "$ref": "trigger.json#" - }, - "type": "behavior_script", - "version": "0.0.1" + "state_schema": {}, + "trigger_schema": { + "$ref": "trigger.json#" }, - { - "configuration_schema": { - "$ref": "coming_home_config.json#" - }, - "description": "Automatically turn your lights to choosen light states, when you arrive at home.", - "id": "fd60fcd1-4809-4813-b510-4a18856a595c", - "metadata": { - "name": "Coming home" - }, - "state_schema": {}, - "trigger_schema": { - "$ref": "trigger.json#" - }, - "type": "behavior_script", - "version": "0.0.1" + "type": "behavior_script", + "version": "0.0.1" + }, + { + "configuration_schema": { + "$ref": "schedule_config.json#" }, - { - "configuration_schema": { - "$ref": "basic_wake_up_config.json#" - }, - "description": "Get your body in the mood to wake up by fading on the lights in the morning.", - "id": "ff8957e3-2eb9-4699-a0c8-ad2cb3ede704", - "metadata": { - "name": "Basic wake up routine" - }, - "state_schema": {}, - "trigger_schema": { - "$ref": "trigger.json#" - }, - "type": "behavior_script", - "version": "0.0.1" + "description": "Schedule turning on and off lights", + "id": "7238c707-8693-4f19-9095-ccdc1444d228", + "metadata": { + "name": "Schedule" }, - { - "configuration_schema": { - "$ref": "natural_light_config.json#" - }, - "description": "Natural light during the day", - "id": "a4260b49-0c69-4926-a29c-417f4a38a352", - "metadata": { - "name": "Natural Light" - }, - "state_schema": { - "$ref": "natural_light_state.json#" - }, - "trigger_schema": { - "$ref": "smart_scene_trigger.json#" - }, - "type": "behavior_script", - "version": "0.0.1" + "state_schema": {}, + "trigger_schema": { + "$ref": "trigger.json#" }, - { - "configuration_schema": { - "$ref": "timer_config.json#" - }, - "description": "Countdown Timer", - "id": "e73bc72d-96b1-46f8-aa57-729861f80c78", - "metadata": { - "name": "Timers" - }, - "state_schema": { - "$ref": "timer_state.json#" - }, - "trigger_schema": { - "$ref": "trigger.json#" - }, - "type": "behavior_script", - "version": "0.0.1" + "type": "behavior_script", + "version": "0.0.1" + }, + { + "configuration_schema": { + "$ref": "lights_state_after_streaming_config.json#" }, - { - "id": "c6e03a31-4c30-4cef-834f-26ffbb06a593", - "name": "Test geofence client", - "type": "geofence_client" + "description": "State of lights in the entertainment group after streaming ends", + "id": "7719b841-6b3d-448d-a0e7-601ae9edb6a2", + "metadata": { + "name": "Light state after streaming" }, - { - "id": "52612630-841e-4d39-9763-60346a0da759", - "is_configured": true, - "type": "geolocation" - } - ] - \ No newline at end of file + "state_schema": {}, + "trigger_schema": {}, + "type": "behavior_script", + "version": "0.0.1" + }, + { + "configuration_schema": { + "$ref": "basic_goto_sleep_config.json#" + }, + "description": "Get ready for nice sleep.", + "id": "7e571ac6-f363-42e1-809a-4cbf6523ed72", + "metadata": { + "name": "Basic go to sleep routine" + }, + "state_schema": {}, + "trigger_schema": { + "$ref": "trigger.json#" + }, + "type": "behavior_script", + "version": "0.0.1" + }, + { + "configuration_schema": { + "$ref": "coming_home_config.json#" + }, + "description": "Automatically turn your lights to choosen light states, when you arrive at home.", + "id": "fd60fcd1-4809-4813-b510-4a18856a595c", + "metadata": { + "name": "Coming home" + }, + "state_schema": {}, + "trigger_schema": { + "$ref": "trigger.json#" + }, + "type": "behavior_script", + "version": "0.0.1" + }, + { + "configuration_schema": { + "$ref": "basic_wake_up_config.json#" + }, + "description": "Get your body in the mood to wake up by fading on the lights in the morning.", + "id": "ff8957e3-2eb9-4699-a0c8-ad2cb3ede704", + "metadata": { + "name": "Basic wake up routine" + }, + "state_schema": {}, + "trigger_schema": { + "$ref": "trigger.json#" + }, + "type": "behavior_script", + "version": "0.0.1" + }, + { + "configuration_schema": { + "$ref": "natural_light_config.json#" + }, + "description": "Natural light during the day", + "id": "a4260b49-0c69-4926-a29c-417f4a38a352", + "metadata": { + "name": "Natural Light" + }, + "state_schema": { + "$ref": "natural_light_state.json#" + }, + "trigger_schema": { + "$ref": "smart_scene_trigger.json#" + }, + "type": "behavior_script", + "version": "0.0.1" + }, + { + "configuration_schema": { + "$ref": "timer_config.json#" + }, + "description": "Countdown Timer", + "id": "e73bc72d-96b1-46f8-aa57-729861f80c78", + "metadata": { + "name": "Timers" + }, + "state_schema": { + "$ref": "timer_state.json#" + }, + "trigger_schema": { + "$ref": "trigger.json#" + }, + "type": "behavior_script", + "version": "0.0.1" + }, + { + "id": "c6e03a31-4c30-4cef-834f-26ffbb06a593", + "name": "Test geofence client", + "type": "geofence_client" + }, + { + "id": "52612630-841e-4d39-9763-60346a0da759", + "is_configured": true, + "type": "geolocation" + } +] diff --git a/tests/components/hue/test_light_v2.py b/tests/components/hue/test_light_v2.py index f0265233e4e..203d2983fb1 100644 --- a/tests/components/hue/test_light_v2.py +++ b/tests/components/hue/test_light_v2.py @@ -14,8 +14,8 @@ async def test_lights(hass, mock_bridge_v2, v2_resources_test_data): await setup_platform(hass, mock_bridge_v2, "light") # there shouldn't have been any requests at this point assert len(mock_bridge_v2.mock_requests) == 0 - # 6 entities should be created from test data (grouped_lights are disabled by default) - assert len(hass.states.async_all()) == 6 + # 8 entities should be created from test data + assert len(hass.states.async_all()) == 8 # test light which supports color and color temperature light_1 = hass.states.get("light.hue_light_with_color_and_color_temperature_1") @@ -36,6 +36,8 @@ async def test_lights(hass, mock_bridge_v2, v2_resources_test_data): assert light_1.attributes["min_mireds"] == 153 assert light_1.attributes["max_mireds"] == 500 assert light_1.attributes["dynamics"] == "dynamic_palette" + assert light_1.attributes["effect_list"] == ["None", "candle", "fire"] + assert light_1.attributes["effect"] == "None" # test light which supports color temperature only light_2 = hass.states.get("light.hue_light_with_color_temperature_only") @@ -49,6 +51,7 @@ async def test_lights(hass, mock_bridge_v2, v2_resources_test_data): assert light_2.attributes["min_mireds"] == 153 assert light_2.attributes["max_mireds"] == 454 assert light_2.attributes["dynamics"] == "none" + assert light_2.attributes["effect_list"] == ["None", "candle", "sunrise"] # test light which supports color only light_3 = hass.states.get("light.hue_light_with_color_only") @@ -164,6 +167,39 @@ async def test_light_turn_on_service(hass, mock_bridge_v2, v2_resources_test_dat assert len(mock_bridge_v2.mock_requests) == 6 assert mock_bridge_v2.mock_requests[5]["json"]["color_temperature"]["mirek"] == 500 + # test enable effect + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": test_light_id, "effect": "candle"}, + blocking=True, + ) + assert len(mock_bridge_v2.mock_requests) == 7 + assert mock_bridge_v2.mock_requests[6]["json"]["effects"]["effect"] == "candle" + + # test disable effect + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": test_light_id, "effect": "None"}, + blocking=True, + ) + assert len(mock_bridge_v2.mock_requests) == 8 + assert mock_bridge_v2.mock_requests[7]["json"]["effects"]["effect"] == "no_effect" + + # test timed effect + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": test_light_id, "effect": "sunrise", "transition": 6}, + blocking=True, + ) + assert len(mock_bridge_v2.mock_requests) == 9 + assert ( + mock_bridge_v2.mock_requests[8]["json"]["timed_effects"]["effect"] == "sunrise" + ) + assert mock_bridge_v2.mock_requests[8]["json"]["timed_effects"]["duration"] == 6000 + async def test_light_turn_off_service(hass, mock_bridge_v2, v2_resources_test_data): """Test calling the turn off service on a light.""" @@ -293,32 +329,14 @@ async def test_grouped_lights(hass, mock_bridge_v2, v2_resources_test_data): await setup_platform(hass, mock_bridge_v2, "light") - # test if entities for hue groups are created and disabled by default + # test if entities for hue groups are created and enabled by default for entity_id in ("light.test_zone", "light.test_room"): ent_reg = er.async_get(hass) entity_entry = ent_reg.async_get(entity_id) assert entity_entry - assert entity_entry.disabled - assert entity_entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION - # entity should not have a device assigned - assert entity_entry.device_id is None - - # enable the entity - updated_entry = ent_reg.async_update_entity( - entity_entry.entity_id, **{"disabled_by": None} - ) - assert updated_entry != entity_entry - assert updated_entry.disabled is False - - # reload platform and check if entities are correctly there - await hass.config_entries.async_forward_entry_unload( - mock_bridge_v2.config_entry, "light" - ) - await hass.config_entries.async_forward_entry_setup( - mock_bridge_v2.config_entry, "light" - ) - await hass.async_block_till_done() + # scene entities should have be assigned to the room/zone device/service + assert entity_entry.device_id is not None # test light created for hue zone test_entity = hass.states.get("light.test_zone") @@ -372,31 +390,24 @@ async def test_grouped_lights(hass, mock_bridge_v2, v2_resources_test_data): blocking=True, ) - # PUT request should have been sent to ALL group lights with correct params - assert len(mock_bridge_v2.mock_requests) == 3 - for index in range(0, 3): - assert mock_bridge_v2.mock_requests[index]["json"]["on"]["on"] is True - assert ( - mock_bridge_v2.mock_requests[index]["json"]["dimming"]["brightness"] == 100 - ) - assert mock_bridge_v2.mock_requests[index]["json"]["color"]["xy"]["x"] == 0.123 - assert mock_bridge_v2.mock_requests[index]["json"]["color"]["xy"]["y"] == 0.123 - assert ( - mock_bridge_v2.mock_requests[index]["json"]["dynamics"]["duration"] == 200 - ) + # PUT request should have been sent to group_light with correct params + assert len(mock_bridge_v2.mock_requests) == 1 + assert mock_bridge_v2.mock_requests[0]["json"]["on"]["on"] is True + assert mock_bridge_v2.mock_requests[0]["json"]["dimming"]["brightness"] == 100 + assert mock_bridge_v2.mock_requests[0]["json"]["color"]["xy"]["x"] == 0.123 + assert mock_bridge_v2.mock_requests[0]["json"]["color"]["xy"]["y"] == 0.123 + assert mock_bridge_v2.mock_requests[0]["json"]["dynamics"]["duration"] == 200 # Now generate update events by emitting the json we've sent as incoming events - for index, light_id in enumerate( - [ - "02cba059-9c2c-4d45-97e4-4f79b1bfbaa1", - "b3fe71ef-d0ef-48de-9355-d9e604377df0", - "8015b17f-8336-415b-966a-b364bd082397", - ] - ): + for light_id in [ + "02cba059-9c2c-4d45-97e4-4f79b1bfbaa1", + "b3fe71ef-d0ef-48de-9355-d9e604377df0", + "8015b17f-8336-415b-966a-b364bd082397", + ]: event = { "id": light_id, "type": "light", - **mock_bridge_v2.mock_requests[index]["json"], + **mock_bridge_v2.mock_requests[0]["json"], } mock_bridge_v2.api.emit_event("update", event) await hass.async_block_till_done() @@ -452,13 +463,10 @@ async def test_grouped_lights(hass, mock_bridge_v2, v2_resources_test_data): blocking=True, ) - # PUT request should have been sent to ALL group lights with correct params - assert len(mock_bridge_v2.mock_requests) == 3 - for index in range(0, 3): - assert mock_bridge_v2.mock_requests[index]["json"]["on"]["on"] is False - assert ( - mock_bridge_v2.mock_requests[index]["json"]["dynamics"]["duration"] == 200 - ) + # PUT request should have been sent to group_light with correct params + assert len(mock_bridge_v2.mock_requests) == 1 + assert mock_bridge_v2.mock_requests[0]["json"]["on"]["on"] is False + assert mock_bridge_v2.mock_requests[0]["json"]["dynamics"]["duration"] == 200 # Test sending short flash effect to a grouped light mock_bridge_v2.mock_requests.clear() @@ -494,12 +502,9 @@ async def test_grouped_lights(hass, mock_bridge_v2, v2_resources_test_data): blocking=True, ) - # PUT request should have been sent to ALL group lights with correct params - assert len(mock_bridge_v2.mock_requests) == 3 - for index in range(0, 3): - assert ( - mock_bridge_v2.mock_requests[index]["json"]["alert"]["action"] == "breathe" - ) + # PUT request should have been sent to grouped_light with correct params + assert len(mock_bridge_v2.mock_requests) == 1 + assert mock_bridge_v2.mock_requests[0]["json"]["alert"]["action"] == "breathe" # Test sending flash effect in turn_off call mock_bridge_v2.mock_requests.clear() diff --git a/tests/components/hue/test_scene.py b/tests/components/hue/test_scene.py index 7f30fd25681..b0d9cf41c9f 100644 --- a/tests/components/hue/test_scene.py +++ b/tests/components/hue/test_scene.py @@ -21,7 +21,7 @@ async def test_scene(hass, mock_bridge_v2, v2_resources_test_data): # test (dynamic) scene for a hue zone test_entity = hass.states.get("scene.test_zone_dynamic_test_scene") assert test_entity is not None - assert test_entity.name == "Test Zone - Dynamic Test Scene" + assert test_entity.name == "Test Zone Dynamic Test Scene" assert test_entity.state == STATE_UNKNOWN assert test_entity.attributes["group_name"] == "Test Zone" assert test_entity.attributes["group_type"] == "zone" @@ -33,7 +33,7 @@ async def test_scene(hass, mock_bridge_v2, v2_resources_test_data): # test (regular) scene for a hue room test_entity = hass.states.get("scene.test_room_regular_test_scene") assert test_entity is not None - assert test_entity.name == "Test Room - Regular Test Scene" + assert test_entity.name == "Test Room Regular Test Scene" assert test_entity.state == STATE_UNKNOWN assert test_entity.attributes["group_name"] == "Test Room" assert test_entity.attributes["group_type"] == "room" @@ -42,7 +42,7 @@ async def test_scene(hass, mock_bridge_v2, v2_resources_test_data): assert test_entity.attributes["brightness"] == 100.0 assert test_entity.attributes["is_dynamic"] is False - # scene entities should not have a device assigned + # scene entities should have be assigned to the room/zone device/service ent_reg = er.async_get(hass) for entity_id in ( "scene.test_zone_dynamic_test_scene", @@ -50,7 +50,7 @@ async def test_scene(hass, mock_bridge_v2, v2_resources_test_data): ): entity_entry = ent_reg.async_get(entity_id) assert entity_entry - assert entity_entry.device_id is None + assert entity_entry.device_id is not None async def test_scene_turn_on_service(hass, mock_bridge_v2, v2_resources_test_data): @@ -144,7 +144,7 @@ async def test_scene_updates(hass, mock_bridge_v2, v2_resources_test_data): test_entity = hass.states.get(test_entity_id) assert test_entity is not None assert test_entity.state == STATE_UNKNOWN - assert test_entity.name == "Test Room - Mocked Scene" + assert test_entity.name == "Test Room Mocked Scene" assert test_entity.attributes["brightness"] == 65.0 # test update @@ -156,7 +156,7 @@ async def test_scene_updates(hass, mock_bridge_v2, v2_resources_test_data): assert test_entity is not None assert test_entity.attributes["brightness"] == 35.0 - # test entity name changes on group name change + # # test entity name changes on group name change mock_bridge_v2.api.emit_event( "update", { @@ -167,9 +167,9 @@ async def test_scene_updates(hass, mock_bridge_v2, v2_resources_test_data): ) await hass.async_block_till_done() test_entity = hass.states.get(test_entity_id) - assert test_entity.name == "Test Room 2 - Mocked Scene" + assert test_entity.name == "Test Room 2 Mocked Scene" - # test delete + # # test delete mock_bridge_v2.api.emit_event("delete", updated_resource) await hass.async_block_till_done() await hass.async_block_till_done() diff --git a/tests/components/humidifier/test_recorder.py b/tests/components/humidifier/test_recorder.py new file mode 100644 index 00000000000..c7505574b98 --- /dev/null +++ b/tests/components/humidifier/test_recorder.py @@ -0,0 +1,49 @@ +"""The tests for humidifier recorder.""" +from __future__ import annotations + +from datetime import timedelta + +from homeassistant.components import humidifier +from homeassistant.components.humidifier import ( + ATTR_AVAILABLE_MODES, + ATTR_MAX_HUMIDITY, + ATTR_MIN_HUMIDITY, +) +from homeassistant.components.recorder.models import StateAttributes, States +from homeassistant.components.recorder.util import session_scope +from homeassistant.const import ATTR_FRIENDLY_NAME +from homeassistant.core import State +from homeassistant.setup import async_setup_component +from homeassistant.util import dt as dt_util + +from tests.common import async_fire_time_changed, async_init_recorder_component +from tests.components.recorder.common import async_wait_recording_done_without_instance + + +async def test_exclude_attributes(hass): + """Test humidifier registered attributes to be excluded.""" + await async_init_recorder_component(hass) + await async_setup_component( + hass, humidifier.DOMAIN, {humidifier.DOMAIN: {"platform": "demo"}} + ) + await hass.async_block_till_done() + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5)) + await hass.async_block_till_done() + await async_wait_recording_done_without_instance(hass) + + def _fetch_states() -> list[State]: + with session_scope(hass=hass) as session: + native_states = [] + for db_state, db_state_attributes in session.query(States, StateAttributes): + state = db_state.to_native() + state.attributes = db_state_attributes.to_native() + native_states.append(state) + return native_states + + states: list[State] = await hass.async_add_executor_job(_fetch_states) + assert len(states) > 1 + for state in states: + assert ATTR_MIN_HUMIDITY not in state.attributes + assert ATTR_MAX_HUMIDITY not in state.attributes + assert ATTR_AVAILABLE_MODES not in state.attributes + assert ATTR_FRIENDLY_NAME in state.attributes diff --git a/tests/components/humidifier/test_reproduce_state.py b/tests/components/humidifier/test_reproduce_state.py index 15b797c66a8..491b2f803d9 100644 --- a/tests/components/humidifier/test_reproduce_state.py +++ b/tests/components/humidifier/test_reproduce_state.py @@ -20,6 +20,7 @@ from homeassistant.const import ( STATE_ON, ) from homeassistant.core import Context, State +from homeassistant.helpers.state import async_reproduce_state from tests.common import async_mock_service @@ -38,7 +39,8 @@ async def test_reproducing_on_off_states(hass, caplog): humidity_calls = async_mock_service(hass, DOMAIN, SERVICE_SET_HUMIDITY) # These calls should do nothing as entities already in desired state - await hass.helpers.state.async_reproduce_state( + await async_reproduce_state( + hass, [ State(ENTITY_1, "off", {ATTR_MODE: MODE_NORMAL, ATTR_HUMIDITY: 45}), State(ENTITY_2, "on", {ATTR_MODE: MODE_NORMAL, ATTR_HUMIDITY: 45}), @@ -51,7 +53,7 @@ async def test_reproducing_on_off_states(hass, caplog): assert len(humidity_calls) == 0 # Test invalid state is handled - await hass.helpers.state.async_reproduce_state([State(ENTITY_1, "not_supported")]) + await async_reproduce_state(hass, [State(ENTITY_1, "not_supported")]) assert "not_supported" in caplog.text assert len(turn_on_calls) == 0 @@ -60,13 +62,14 @@ async def test_reproducing_on_off_states(hass, caplog): assert len(humidity_calls) == 0 # Make sure correct services are called - await hass.helpers.state.async_reproduce_state( + await async_reproduce_state( + hass, [ State(ENTITY_2, "off"), State(ENTITY_1, "on", {}), # Should not raise State("humidifier.non_existing", "on"), - ] + ], ) assert len(turn_on_calls) == 1 diff --git a/tests/components/hunterdouglas_powerview/fixtures/fwversion.json b/tests/components/hunterdouglas_powerview/fixtures/fwversion.json index 96d301802ff..05fd878ddc6 100644 --- a/tests/components/hunterdouglas_powerview/fixtures/fwversion.json +++ b/tests/components/hunterdouglas_powerview/fixtures/fwversion.json @@ -1,10 +1,10 @@ { - "firmware": { - "mainProcessor": { - "name": "PowerView Hub", - "revision": 1, - "subRevision": 1, - "build": 857 - } - } - } \ No newline at end of file + "firmware": { + "mainProcessor": { + "name": "PowerView Hub", + "revision": 1, + "subRevision": 1, + "build": 857 + } + } +} diff --git a/tests/components/hunterdouglas_powerview/fixtures/userdata.json b/tests/components/hunterdouglas_powerview/fixtures/userdata.json index ca5eea73f7b..40660915fad 100644 --- a/tests/components/hunterdouglas_powerview/fixtures/userdata.json +++ b/tests/components/hunterdouglas_powerview/fixtures/userdata.json @@ -1,50 +1,50 @@ { - "userData": { - "_id": "abc", - "color": { - "green": 0, - "blue": 255, - "brightness": 5, - "red": 0 - }, - "autoBackup": false, - "ip": "192.168.1.72", - "macAddress": "aa:bb:cc:dd:ee:ff", - "mask": "255.255.255.0", - "gateway": "192.168.1.1", - "dns": "192.168.1.3", - "firmware": { - "mainProcessor": { - "name": "PV Hub2.0", - "revision": 2, - "subRevision": 0, - "build": 1024 - }, - "radio": { - "revision": 2, - "subRevision": 0, - "build": 2610 - } - }, - "serialNumber": "ABC123", - "rfIDInt": 64789, - "rfID": "0xFD15", - "rfStatus": 0, - "brand": "HD", - "wireless": false, - "hubName": "QWxleGFuZGVySEQ=", - "localTimeDataSet": true, - "enableScheduledEvents": true, - "editingEnabled": true, - "setupCompleted": false, - "staticIp": false, - "times": { - "timezone": "America/Chicago", - "localSunriseTimeInMinutes": 0, - "localSunsetTimeInMinutes": 0, - "currentOffset": -18000 - }, - "rcUp": true, - "remoteConnectEnabled": true - } + "userData": { + "_id": "abc", + "color": { + "green": 0, + "blue": 255, + "brightness": 5, + "red": 0 + }, + "autoBackup": false, + "ip": "192.168.1.72", + "macAddress": "aa:bb:cc:dd:ee:ff", + "mask": "255.255.255.0", + "gateway": "192.168.1.1", + "dns": "192.168.1.3", + "firmware": { + "mainProcessor": { + "name": "PV Hub2.0", + "revision": 2, + "subRevision": 0, + "build": 1024 + }, + "radio": { + "revision": 2, + "subRevision": 0, + "build": 2610 + } + }, + "serialNumber": "ABC123", + "rfIDInt": 64789, + "rfID": "0xFD15", + "rfStatus": 0, + "brand": "HD", + "wireless": false, + "hubName": "QWxleGFuZGVySEQ=", + "localTimeDataSet": true, + "enableScheduledEvents": true, + "editingEnabled": true, + "setupCompleted": false, + "staticIp": false, + "times": { + "timezone": "America/Chicago", + "localSunriseTimeInMinutes": 0, + "localSunsetTimeInMinutes": 0, + "currentOffset": -18000 + }, + "rcUp": true, + "remoteConnectEnabled": true + } } diff --git a/tests/components/hunterdouglas_powerview/fixtures/userdata_v1.json b/tests/components/hunterdouglas_powerview/fixtures/userdata_v1.json index d97aca162f8..643efc059e3 100644 --- a/tests/components/hunterdouglas_powerview/fixtures/userdata_v1.json +++ b/tests/components/hunterdouglas_powerview/fixtures/userdata_v1.json @@ -1,34 +1,34 @@ { - "userData" : { - "enableScheduledEvents" : true, - "staticIp" : false, - "sceneControllerCount" : 0, - "accessPointCount" : 0, - "shadeCount" : 5, - "ip" : "192.168.20.9", - "groupCount" : 9, - "scheduledEventCount" : 0, - "editingEnabled" : true, - "roomCount" : 5, - "setupCompleted" : false, - "sceneCount" : 18, - "sceneControllerMemberCount" : 0, - "mask" : "255.255.255.0", - "hubName" : "UG93ZXJWaWV3IEh1YiBHZW4gMQ==", - "rfID" : "0x8B2A", - "remoteConnectEnabled" : false, - "multiSceneMemberCount" : 0, - "rfStatus" : 0, - "serialNumber" : "REMOVED", - "undefinedShadeCount" : 0, - "sceneMemberCount" : 18, - "unassignedShadeCount" : 0, - "multiSceneCount" : 0, - "addressKind" : "newPrimary", - "gateway" : "192.168.20.1", - "localTimeDataSet" : true, - "dns" : "192.168.20.1", - "macAddress" : "00:00:00:00:00:eb", - "rfIDInt" : 35626 - } + "userData": { + "enableScheduledEvents": true, + "staticIp": false, + "sceneControllerCount": 0, + "accessPointCount": 0, + "shadeCount": 5, + "ip": "192.168.20.9", + "groupCount": 9, + "scheduledEventCount": 0, + "editingEnabled": true, + "roomCount": 5, + "setupCompleted": false, + "sceneCount": 18, + "sceneControllerMemberCount": 0, + "mask": "255.255.255.0", + "hubName": "UG93ZXJWaWV3IEh1YiBHZW4gMQ==", + "rfID": "0x8B2A", + "remoteConnectEnabled": false, + "multiSceneMemberCount": 0, + "rfStatus": 0, + "serialNumber": "REMOVED", + "undefinedShadeCount": 0, + "sceneMemberCount": 18, + "unassignedShadeCount": 0, + "multiSceneCount": 0, + "addressKind": "newPrimary", + "gateway": "192.168.20.1", + "localTimeDataSet": true, + "dns": "192.168.20.1", + "macAddress": "00:00:00:00:00:eb", + "rfIDInt": 35626 + } } diff --git a/tests/components/hvv_departures/fixtures/check_name.json b/tests/components/hvv_departures/fixtures/check_name.json index 7f1bf50d39b..29a6591672f 100644 --- a/tests/components/hvv_departures/fixtures/check_name.json +++ b/tests/components/hvv_departures/fixtures/check_name.json @@ -1,15 +1,15 @@ { - "returnCode": "OK", - "results": [ - { - "name": "Wartenau", - "city": "Hamburg", - "combinedName": "Wartenau", - "id": "Master:10901", - "type": "STATION", - "coordinate": {"x": 10.035515, "y": 53.56478}, - "serviceTypes": ["bus", "u"], - "hasStationInformation": true - } - ] -} \ No newline at end of file + "returnCode": "OK", + "results": [ + { + "name": "Wartenau", + "city": "Hamburg", + "combinedName": "Wartenau", + "id": "Master:10901", + "type": "STATION", + "coordinate": { "x": 10.035515, "y": 53.56478 }, + "serviceTypes": ["bus", "u"], + "hasStationInformation": true + } + ] +} diff --git a/tests/components/hvv_departures/fixtures/config_entry.json b/tests/components/hvv_departures/fixtures/config_entry.json index f878280953d..6d89699c8b5 100644 --- a/tests/components/hvv_departures/fixtures/config_entry.json +++ b/tests/components/hvv_departures/fixtures/config_entry.json @@ -1,16 +1,16 @@ { - "host": "api-test.geofox.de", - "username": "test-username", - "password": "test-password", - "station": { - "city": "Schmalfeld", - "combinedName": "Schmalfeld, Holstenstra\u00dfe", - "coordinate": {"x": 9.986115, "y": 53.874122}, - "hasStationInformation": false, - "id": "Master:75279", - "name": "Holstenstra\u00dfe", - "serviceTypes": ["bus"], - "type": "STATION" - }, - "stationInformation": {"returnCode": "OK"} -} \ No newline at end of file + "host": "api-test.geofox.de", + "username": "test-username", + "password": "test-password", + "station": { + "city": "Schmalfeld", + "combinedName": "Schmalfeld, Holstenstra\u00dfe", + "coordinate": { "x": 9.986115, "y": 53.874122 }, + "hasStationInformation": false, + "id": "Master:75279", + "name": "Holstenstra\u00dfe", + "serviceTypes": ["bus"], + "type": "STATION" + }, + "stationInformation": { "returnCode": "OK" } +} diff --git a/tests/components/hvv_departures/fixtures/departure_list.json b/tests/components/hvv_departures/fixtures/departure_list.json index 95099a0ab17..9cb564b0b5e 100644 --- a/tests/components/hvv_departures/fixtures/departure_list.json +++ b/tests/components/hvv_departures/fixtures/departure_list.json @@ -1,162 +1,162 @@ { - "returnCode": "OK", - "time": {"date": "26.01.2020", "time": "22:52"}, - "departures": [ - { - "line": { - "name": "U1", - "direction": "Großhansdorf", - "origin": "Norderstedt Mitte", - "type": { - "simpleType": "TRAIN", - "shortInfo": "U", - "longInfo": "U-Bahn", - "model": "DT4" - }, - "id": "HHA-U:U1_HHA-U" - }, - "timeOffset": 0, - "delay": 0, - "serviceId": 1482563187, - "station": {"combinedName": "Wartenau", "id": "Master:10901"}, - "attributes": [{"isPlanned": true, "types": ["REALTIME", "ACCURATE"]}] + "returnCode": "OK", + "time": { "date": "26.01.2020", "time": "22:52" }, + "departures": [ + { + "line": { + "name": "U1", + "direction": "Großhansdorf", + "origin": "Norderstedt Mitte", + "type": { + "simpleType": "TRAIN", + "shortInfo": "U", + "longInfo": "U-Bahn", + "model": "DT4" }, - { - "line": { - "name": "25", - "direction": "Bf. Altona", - "origin": "U Burgstraße", - "type": { - "simpleType": "BUS", - "shortInfo": "Bus", - "longInfo": "Niederflur Metrobus", - "model": "Gelenkbus" - }, - "id": "HHA-B:25_HHA-B" - }, - "timeOffset": 1, - "delay": 0, - "serviceId": 74567, - "station": {"combinedName": "U Wartenau", "id": "Master:60015"}, - "attributes": [{"isPlanned": true, "types": ["REALTIME", "ACCURATE"]}] + "id": "HHA-U:U1_HHA-U" + }, + "timeOffset": 0, + "delay": 0, + "serviceId": 1482563187, + "station": { "combinedName": "Wartenau", "id": "Master:10901" }, + "attributes": [{ "isPlanned": true, "types": ["REALTIME", "ACCURATE"] }] + }, + { + "line": { + "name": "25", + "direction": "Bf. Altona", + "origin": "U Burgstraße", + "type": { + "simpleType": "BUS", + "shortInfo": "Bus", + "longInfo": "Niederflur Metrobus", + "model": "Gelenkbus" }, - { - "line": { - "name": "25", - "direction": "U Burgstraße", - "origin": "Bf. Altona", - "type": { - "simpleType": "BUS", - "shortInfo": "Bus", - "longInfo": "Niederflur Metrobus", - "model": "Gelenkbus" - }, - "id": "HHA-B:25_HHA-B" - }, - "timeOffset": 5, - "delay": 0, - "serviceId": 74328, - "station": {"combinedName": "U Wartenau", "id": "Master:60015"}, - "attributes": [{"isPlanned": true, "types": ["REALTIME", "ACCURATE"]}] + "id": "HHA-B:25_HHA-B" + }, + "timeOffset": 1, + "delay": 0, + "serviceId": 74567, + "station": { "combinedName": "U Wartenau", "id": "Master:60015" }, + "attributes": [{ "isPlanned": true, "types": ["REALTIME", "ACCURATE"] }] + }, + { + "line": { + "name": "25", + "direction": "U Burgstraße", + "origin": "Bf. Altona", + "type": { + "simpleType": "BUS", + "shortInfo": "Bus", + "longInfo": "Niederflur Metrobus", + "model": "Gelenkbus" }, - { - "line": { - "name": "U1", - "direction": "Norderstedt Mitte", - "origin": "Großhansdorf", - "type": { - "simpleType": "TRAIN", - "shortInfo": "U", - "longInfo": "U-Bahn", - "model": "DT4" - }, - "id": "HHA-U:U1_HHA-U" - }, - "timeOffset": 8, - "delay": 0, - "station": {"combinedName": "Wartenau", "id": "Master:10901"}, - "attributes": [{"isPlanned": true, "types": ["REALTIME", "ACCURATE"]}] + "id": "HHA-B:25_HHA-B" + }, + "timeOffset": 5, + "delay": 0, + "serviceId": 74328, + "station": { "combinedName": "U Wartenau", "id": "Master:60015" }, + "attributes": [{ "isPlanned": true, "types": ["REALTIME", "ACCURATE"] }] + }, + { + "line": { + "name": "U1", + "direction": "Norderstedt Mitte", + "origin": "Großhansdorf", + "type": { + "simpleType": "TRAIN", + "shortInfo": "U", + "longInfo": "U-Bahn", + "model": "DT4" }, - { - "line": { - "name": "U1", - "direction": "Ohlstedt", - "origin": "Norderstedt Mitte", - "type": { - "simpleType": "TRAIN", - "shortInfo": "U", - "longInfo": "U-Bahn", - "model": "DT4" - }, - "id": "HHA-U:U1_HHA-U" - }, - "timeOffset": 10, - "delay": 0, - "station": {"combinedName": "Wartenau", "id": "Master:10901"}, - "attributes": [{"isPlanned": true, "types": ["REALTIME", "ACCURATE"]}] - } - ], - "filter": [ - { - "serviceID": "HHA-U:U1_HHA-U", - "stationIDs": ["Master:10902"], - "label": "Fuhlsbüttel Nord / Ochsenzoll / Norderstedt Mitte / Kellinghusenstraße / Ohlsdorf / Garstedt", - "serviceName": "U1" + "id": "HHA-U:U1_HHA-U" + }, + "timeOffset": 8, + "delay": 0, + "station": { "combinedName": "Wartenau", "id": "Master:10901" }, + "attributes": [{ "isPlanned": true, "types": ["REALTIME", "ACCURATE"] }] + }, + { + "line": { + "name": "U1", + "direction": "Ohlstedt", + "origin": "Norderstedt Mitte", + "type": { + "simpleType": "TRAIN", + "shortInfo": "U", + "longInfo": "U-Bahn", + "model": "DT4" }, - { - "serviceID": "HHA-U:U1_HHA-U", - "stationIDs": ["Master:60904"], - "label": "Volksdorf / Farmsen / Großhansdorf / Ohlstedt", - "serviceName": "U1" - }, - { - "serviceID": "HHA-B:25_HHA-B", - "stationIDs": ["Master:10047"], - "label": "Sachsenstraße / U Burgstraße", - "serviceName": "25" - }, - { - "serviceID": "HHA-B:25_HHA-B", - "stationIDs": ["Master:60029"], - "label": "Winterhuder Marktplatz / Bf. Altona", - "serviceName": "25" - }, - { - "serviceID": "HHA-B:36_HHA-B", - "stationIDs": ["Master:10049"], - "label": "S Blankenese / Rathausmarkt", - "serviceName": "36" - }, - { - "serviceID": "HHA-B:36_HHA-B", - "stationIDs": ["Master:60013"], - "label": "Berner Heerweg", - "serviceName": "36" - }, - { - "serviceID": "HHA-B:606_HHA-B", - "stationIDs": ["Master:10047"], - "label": "S Landwehr (Ramazan-Avci-Platz) - Rathausmarkt", - "serviceName": "606" - }, - { - "serviceID": "HHA-B:606_HHA-B", - "stationIDs": ["Master:60029"], - "label": "Uferstraße - Winterhuder Marktplatz / Uferstraße - S Hamburg Airport / Uferstraße - U Langenhorn Markt (Krohnstieg)", - "serviceName": "606" - }, - { - "serviceID": "HHA-B:608_HHA-B", - "stationIDs": ["Master:10048"], - "label": "Rathausmarkt / S Reeperbahn", - "serviceName": "608" - }, - { - "serviceID": "HHA-B:608_HHA-B", - "stationIDs": ["Master:60012"], - "label": "Bf. Rahlstedt (Amtsstraße) / Großlohe", - "serviceName": "608" - } - ], - "serviceTypes": ["UBAHN", "BUS", "METROBUS", "SCHNELLBUS", "NACHTBUS"] -} \ No newline at end of file + "id": "HHA-U:U1_HHA-U" + }, + "timeOffset": 10, + "delay": 0, + "station": { "combinedName": "Wartenau", "id": "Master:10901" }, + "attributes": [{ "isPlanned": true, "types": ["REALTIME", "ACCURATE"] }] + } + ], + "filter": [ + { + "serviceID": "HHA-U:U1_HHA-U", + "stationIDs": ["Master:10902"], + "label": "Fuhlsbüttel Nord / Ochsenzoll / Norderstedt Mitte / Kellinghusenstraße / Ohlsdorf / Garstedt", + "serviceName": "U1" + }, + { + "serviceID": "HHA-U:U1_HHA-U", + "stationIDs": ["Master:60904"], + "label": "Volksdorf / Farmsen / Großhansdorf / Ohlstedt", + "serviceName": "U1" + }, + { + "serviceID": "HHA-B:25_HHA-B", + "stationIDs": ["Master:10047"], + "label": "Sachsenstraße / U Burgstraße", + "serviceName": "25" + }, + { + "serviceID": "HHA-B:25_HHA-B", + "stationIDs": ["Master:60029"], + "label": "Winterhuder Marktplatz / Bf. Altona", + "serviceName": "25" + }, + { + "serviceID": "HHA-B:36_HHA-B", + "stationIDs": ["Master:10049"], + "label": "S Blankenese / Rathausmarkt", + "serviceName": "36" + }, + { + "serviceID": "HHA-B:36_HHA-B", + "stationIDs": ["Master:60013"], + "label": "Berner Heerweg", + "serviceName": "36" + }, + { + "serviceID": "HHA-B:606_HHA-B", + "stationIDs": ["Master:10047"], + "label": "S Landwehr (Ramazan-Avci-Platz) - Rathausmarkt", + "serviceName": "606" + }, + { + "serviceID": "HHA-B:606_HHA-B", + "stationIDs": ["Master:60029"], + "label": "Uferstraße - Winterhuder Marktplatz / Uferstraße - S Hamburg Airport / Uferstraße - U Langenhorn Markt (Krohnstieg)", + "serviceName": "606" + }, + { + "serviceID": "HHA-B:608_HHA-B", + "stationIDs": ["Master:10048"], + "label": "Rathausmarkt / S Reeperbahn", + "serviceName": "608" + }, + { + "serviceID": "HHA-B:608_HHA-B", + "stationIDs": ["Master:60012"], + "label": "Bf. Rahlstedt (Amtsstraße) / Großlohe", + "serviceName": "608" + } + ], + "serviceTypes": ["UBAHN", "BUS", "METROBUS", "SCHNELLBUS", "NACHTBUS"] +} diff --git a/tests/components/hvv_departures/fixtures/init.json b/tests/components/hvv_departures/fixtures/init.json index a20a96363c7..2cde98984d6 100644 --- a/tests/components/hvv_departures/fixtures/init.json +++ b/tests/components/hvv_departures/fixtures/init.json @@ -1,10 +1,10 @@ { - "returnCode": "OK", - "beginOfService": "04.06.2020", - "endOfService": "13.12.2020", - "id": "1.80.0", - "dataId": "32.55.01", - "buildDate": "04.06.2020", - "buildTime": "14:29:59", - "buildText": "Regelfahrplan 2020" -} \ No newline at end of file + "returnCode": "OK", + "beginOfService": "04.06.2020", + "endOfService": "13.12.2020", + "id": "1.80.0", + "dataId": "32.55.01", + "buildDate": "04.06.2020", + "buildTime": "14:29:59", + "buildText": "Regelfahrplan 2020" +} diff --git a/tests/components/hvv_departures/fixtures/options.json b/tests/components/hvv_departures/fixtures/options.json index f2e288d760a..1a3b0de8c10 100644 --- a/tests/components/hvv_departures/fixtures/options.json +++ b/tests/components/hvv_departures/fixtures/options.json @@ -1,12 +1,12 @@ { - "filter": [ - { - "label": "S Landwehr (Ramazan-Avci-Platz) - Rathausmarkt", - "serviceID": "HHA-B:606_HHA-B", - "serviceName": "606", - "stationIDs": ["Master:10047"] - } - ], - "offset": 10, - "realtime": true -} \ No newline at end of file + "filter": [ + { + "label": "S Landwehr (Ramazan-Avci-Platz) - Rathausmarkt", + "serviceID": "HHA-B:606_HHA-B", + "serviceName": "606", + "stationIDs": ["Master:10047"] + } + ], + "offset": 10, + "realtime": true +} diff --git a/tests/components/hvv_departures/fixtures/station_information.json b/tests/components/hvv_departures/fixtures/station_information.json index 52a2cd8da25..eb6da10910b 100644 --- a/tests/components/hvv_departures/fixtures/station_information.json +++ b/tests/components/hvv_departures/fixtures/station_information.json @@ -1,32 +1,32 @@ { - "returnCode": "OK", - "partialStations": [ + "returnCode": "OK", + "partialStations": [ + { + "stationOutline": "http://www.geofox.de/images/mobi/stationDescriptions/U_Wartenau.ZM3.jpg", + "elevators": [ { - "stationOutline": "http://www.geofox.de/images/mobi/stationDescriptions/U_Wartenau.ZM3.jpg", - "elevators": [ - { - "label": "A", - "cabinWidth": 124, - "cabinLength": 147, - "doorWidth": 110, - "description": "Zugang Landwehr <-> Schalterhalle", - "elevatorType": "Durchlader", - "buttonType": "BRAILLE", - "state": "READY" - }, - { - "lines": ["U1"], - "label": "B", - "cabinWidth": 123, - "cabinLength": 145, - "doorWidth": 90, - "description": "Schalterhalle <-> U1", - "elevatorType": "Durchlader", - "buttonType": "COMBI", - "state": "READY" - } - ] + "label": "A", + "cabinWidth": 124, + "cabinLength": 147, + "doorWidth": 110, + "description": "Zugang Landwehr <-> Schalterhalle", + "elevatorType": "Durchlader", + "buttonType": "BRAILLE", + "state": "READY" + }, + { + "lines": ["U1"], + "label": "B", + "cabinWidth": 123, + "cabinLength": 145, + "doorWidth": 90, + "description": "Schalterhalle <-> U1", + "elevatorType": "Durchlader", + "buttonType": "COMBI", + "state": "READY" } - ], - "lastUpdate": {"date": "26.01.2020", "time": "22:49"} -} \ No newline at end of file + ] + } + ], + "lastUpdate": { "date": "26.01.2020", "time": "22:49" } +} diff --git a/tests/components/icloud/conftest.py b/tests/components/icloud/conftest.py index 2230cc2ea32..c8195471878 100644 --- a/tests/components/icloud/conftest.py +++ b/tests/components/icloud/conftest.py @@ -9,3 +9,12 @@ def icloud_bypass_setup_fixture(): """Mock component setup.""" with patch("homeassistant.components.icloud.async_setup_entry", return_value=True): yield + + +@pytest.fixture(autouse=True) +def icloud_not_create_dir(): + """Mock component setup.""" + with patch( + "homeassistant.components.icloud.config_flow.os.path.exists", return_value=True + ): + yield diff --git a/tests/components/icloud/test_config_flow.py b/tests/components/icloud/test_config_flow.py index 59c5ebf24a9..cd72aae0eff 100644 --- a/tests/components/icloud/test_config_flow.py +++ b/tests/components/icloud/test_config_flow.py @@ -18,7 +18,7 @@ from homeassistant.components.icloud.const import ( DEFAULT_WITH_FAMILY, DOMAIN, ) -from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_REAUTH, SOURCE_USER +from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant @@ -197,127 +197,6 @@ async def test_user_with_cookie(hass: HomeAssistant, service_authenticated: Magi assert result["data"][CONF_GPS_ACCURACY_THRESHOLD] == DEFAULT_GPS_ACCURACY_THRESHOLD -async def test_import(hass: HomeAssistant, service: MagicMock): - """Test import step.""" - # import with required - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "trusted_device" - - # import with all - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={ - CONF_USERNAME: USERNAME_2, - CONF_PASSWORD: PASSWORD, - CONF_WITH_FAMILY: WITH_FAMILY, - CONF_MAX_INTERVAL: MAX_INTERVAL, - CONF_GPS_ACCURACY_THRESHOLD: GPS_ACCURACY_THRESHOLD, - }, - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "trusted_device" - - -async def test_import_with_cookie( - hass: HomeAssistant, service_authenticated: MagicMock -): - """Test import step with presence of a cookie.""" - # import with required - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["result"].unique_id == USERNAME - assert result["title"] == USERNAME - assert result["data"][CONF_USERNAME] == USERNAME - assert result["data"][CONF_PASSWORD] == PASSWORD - assert result["data"][CONF_WITH_FAMILY] == DEFAULT_WITH_FAMILY - assert result["data"][CONF_MAX_INTERVAL] == DEFAULT_MAX_INTERVAL - assert result["data"][CONF_GPS_ACCURACY_THRESHOLD] == DEFAULT_GPS_ACCURACY_THRESHOLD - - # import with all - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={ - CONF_USERNAME: USERNAME_2, - CONF_PASSWORD: PASSWORD, - CONF_WITH_FAMILY: WITH_FAMILY, - CONF_MAX_INTERVAL: MAX_INTERVAL, - CONF_GPS_ACCURACY_THRESHOLD: GPS_ACCURACY_THRESHOLD, - }, - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["result"].unique_id == USERNAME_2 - assert result["title"] == USERNAME_2 - assert result["data"][CONF_USERNAME] == USERNAME_2 - assert result["data"][CONF_PASSWORD] == PASSWORD - assert result["data"][CONF_WITH_FAMILY] == WITH_FAMILY - assert result["data"][CONF_MAX_INTERVAL] == MAX_INTERVAL - assert result["data"][CONF_GPS_ACCURACY_THRESHOLD] == GPS_ACCURACY_THRESHOLD - - -async def test_two_accounts_setup( - hass: HomeAssistant, service_authenticated: MagicMock -): - """Test to setup two accounts.""" - MockConfigEntry( - domain=DOMAIN, - data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, - unique_id=USERNAME, - ).add_to_hass(hass) - - # import with required - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={CONF_USERNAME: USERNAME_2, CONF_PASSWORD: PASSWORD}, - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["result"].unique_id == USERNAME_2 - assert result["title"] == USERNAME_2 - assert result["data"][CONF_USERNAME] == USERNAME_2 - assert result["data"][CONF_PASSWORD] == PASSWORD - assert result["data"][CONF_WITH_FAMILY] == DEFAULT_WITH_FAMILY - assert result["data"][CONF_MAX_INTERVAL] == DEFAULT_MAX_INTERVAL - assert result["data"][CONF_GPS_ACCURACY_THRESHOLD] == DEFAULT_GPS_ACCURACY_THRESHOLD - - -async def test_already_setup(hass: HomeAssistant): - """Test we abort if the account is already setup.""" - MockConfigEntry( - domain=DOMAIN, - data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, - unique_id=USERNAME, - ).add_to_hass(hass) - - # Should fail, same USERNAME (import) - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "already_configured" - - # Should fail, same USERNAME (flow) - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_USER}, - data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "already_configured" - - async def test_login_failed(hass: HomeAssistant): """Test when we have errors during login.""" with patch( diff --git a/tests/components/input_boolean/test_reproduce_state.py b/tests/components/input_boolean/test_reproduce_state.py index 3ee5bfb5da9..96f73284969 100644 --- a/tests/components/input_boolean/test_reproduce_state.py +++ b/tests/components/input_boolean/test_reproduce_state.py @@ -1,5 +1,6 @@ """Test reproduce state for input boolean.""" from homeassistant.core import State +from homeassistant.helpers.state import async_reproduce_state from homeassistant.setup import async_setup_component @@ -15,7 +16,8 @@ async def test_reproducing_states(hass): } }, ) - await hass.helpers.state.async_reproduce_state( + await async_reproduce_state( + hass, [ State("input_boolean.initial_on", "off"), State("input_boolean.initial_off", "on"), @@ -26,7 +28,8 @@ async def test_reproducing_states(hass): assert hass.states.get("input_boolean.initial_off").state == "on" assert hass.states.get("input_boolean.initial_on").state == "off" - await hass.helpers.state.async_reproduce_state( + await async_reproduce_state( + hass, [ # Test invalid state State("input_boolean.initial_on", "invalid_state"), diff --git a/tests/components/input_datetime/test_init.py b/tests/components/input_datetime/test_init.py index 0a968caf67f..28ca2ab02bd 100644 --- a/tests/components/input_datetime/test_init.py +++ b/tests/components/input_datetime/test_init.py @@ -38,8 +38,6 @@ INITIAL_DATE = "2020-01-10" INITIAL_TIME = "23:45:56" INITIAL_DATETIME = f"{INITIAL_DATE} {INITIAL_TIME}" -ORIG_TIMEZONE = dt_util.DEFAULT_TIME_ZONE - @pytest.fixture def storage_setup(hass, hass_storage): @@ -131,7 +129,9 @@ async def test_set_datetime(hass): entity_id = "input_datetime.test_datetime" - dt_obj = datetime.datetime(2017, 9, 7, 19, 46, 30, tzinfo=datetime.timezone.utc) + dt_obj = datetime.datetime( + 2017, 9, 7, 19, 46, 30, tzinfo=dt_util.get_time_zone(hass.config.time_zone) + ) await async_set_date_and_time(hass, entity_id, dt_obj) @@ -157,7 +157,9 @@ async def test_set_datetime_2(hass): entity_id = "input_datetime.test_datetime" - dt_obj = datetime.datetime(2017, 9, 7, 19, 46, 30, tzinfo=datetime.timezone.utc) + dt_obj = datetime.datetime( + 2017, 9, 7, 19, 46, 30, tzinfo=dt_util.get_time_zone(hass.config.time_zone) + ) await async_set_datetime(hass, entity_id, dt_obj) @@ -183,7 +185,9 @@ async def test_set_datetime_3(hass): entity_id = "input_datetime.test_datetime" - dt_obj = datetime.datetime(2017, 9, 7, 19, 46, 30, tzinfo=datetime.timezone.utc) + dt_obj = datetime.datetime( + 2017, 9, 7, 19, 46, 30, tzinfo=dt_util.get_time_zone(hass.config.time_zone) + ) await async_set_timestamp(hass, entity_id, dt_util.as_utc(dt_obj).timestamp()) @@ -649,101 +653,97 @@ async def test_setup_no_config(hass, hass_admin_user): async def test_timestamp(hass): """Test timestamp.""" - try: - dt_util.set_default_time_zone(dt_util.get_time_zone("America/Los_Angeles")) + hass.config.set_time_zone("America/Los_Angeles") - assert await async_setup_component( - hass, - DOMAIN, - { - DOMAIN: { - "test_datetime_initial_with_tz": { - "has_time": True, - "has_date": True, - "initial": "2020-12-13 10:00:00+01:00", - }, - "test_datetime_initial_without_tz": { - "has_time": True, - "has_date": True, - "initial": "2020-12-13 10:00:00", - }, - "test_time_initial": { - "has_time": True, - "has_date": False, - "initial": "10:00:00", - }, - } - }, - ) + assert await async_setup_component( + hass, + DOMAIN, + { + DOMAIN: { + "test_datetime_initial_with_tz": { + "has_time": True, + "has_date": True, + "initial": "2020-12-13 10:00:00+01:00", + }, + "test_datetime_initial_without_tz": { + "has_time": True, + "has_date": True, + "initial": "2020-12-13 10:00:00", + }, + "test_time_initial": { + "has_time": True, + "has_date": False, + "initial": "10:00:00", + }, + } + }, + ) - # initial has been converted to the set timezone - state_with_tz = hass.states.get("input_datetime.test_datetime_initial_with_tz") - assert state_with_tz is not None - # Timezone LA is UTC-8 => timestamp carries +01:00 => delta is -9 => 10:00 - 09:00 => 01:00 - assert state_with_tz.state == "2020-12-13 01:00:00" - assert ( - dt_util.as_local( - dt_util.utc_from_timestamp(state_with_tz.attributes[ATTR_TIMESTAMP]) - ).strftime(FMT_DATETIME) - == "2020-12-13 01:00:00" - ) + # initial has been converted to the set timezone + state_with_tz = hass.states.get("input_datetime.test_datetime_initial_with_tz") + assert state_with_tz is not None + # Timezone LA is UTC-8 => timestamp carries +01:00 => delta is -9 => 10:00 - 09:00 => 01:00 + assert state_with_tz.state == "2020-12-13 01:00:00" + assert ( + dt_util.as_local( + dt_util.utc_from_timestamp(state_with_tz.attributes[ATTR_TIMESTAMP]) + ).strftime(FMT_DATETIME) + == "2020-12-13 01:00:00" + ) - # initial has been interpreted as being part of set timezone - state_without_tz = hass.states.get( - "input_datetime.test_datetime_initial_without_tz" - ) - assert state_without_tz is not None - assert state_without_tz.state == "2020-12-13 10:00:00" - # Timezone LA is UTC-8 => timestamp has no zone (= assumed local) => delta to UTC is +8 => 10:00 + 08:00 => 18:00 - assert ( - dt_util.utc_from_timestamp( - state_without_tz.attributes[ATTR_TIMESTAMP] - ).strftime(FMT_DATETIME) - == "2020-12-13 18:00:00" - ) - assert ( - dt_util.as_local( - dt_util.utc_from_timestamp(state_without_tz.attributes[ATTR_TIMESTAMP]) - ).strftime(FMT_DATETIME) - == "2020-12-13 10:00:00" - ) - # Use datetime.datetime.fromtimestamp - assert ( - dt_util.as_local( - datetime.datetime.fromtimestamp( - state_without_tz.attributes[ATTR_TIMESTAMP], datetime.timezone.utc - ) - ).strftime(FMT_DATETIME) - == "2020-12-13 10:00:00" - ) + # initial has been interpreted as being part of set timezone + state_without_tz = hass.states.get( + "input_datetime.test_datetime_initial_without_tz" + ) + assert state_without_tz is not None + assert state_without_tz.state == "2020-12-13 10:00:00" + # Timezone LA is UTC-8 => timestamp has no zone (= assumed local) => delta to UTC is +8 => 10:00 + 08:00 => 18:00 + assert ( + dt_util.utc_from_timestamp( + state_without_tz.attributes[ATTR_TIMESTAMP] + ).strftime(FMT_DATETIME) + == "2020-12-13 18:00:00" + ) + assert ( + dt_util.as_local( + dt_util.utc_from_timestamp(state_without_tz.attributes[ATTR_TIMESTAMP]) + ).strftime(FMT_DATETIME) + == "2020-12-13 10:00:00" + ) + # Use datetime.datetime.fromtimestamp + assert ( + dt_util.as_local( + datetime.datetime.fromtimestamp( + state_without_tz.attributes[ATTR_TIMESTAMP], datetime.timezone.utc + ) + ).strftime(FMT_DATETIME) + == "2020-12-13 10:00:00" + ) - # Test initial time sets timestamp correctly. - state_time = hass.states.get("input_datetime.test_time_initial") - assert state_time is not None - assert state_time.state == "10:00:00" - assert state_time.attributes[ATTR_TIMESTAMP] == 10 * 60 * 60 + # Test initial time sets timestamp correctly. + state_time = hass.states.get("input_datetime.test_time_initial") + assert state_time is not None + assert state_time.state == "10:00:00" + assert state_time.attributes[ATTR_TIMESTAMP] == 10 * 60 * 60 - # Test that setting the timestamp of an entity works. - await hass.services.async_call( - DOMAIN, - "set_datetime", - { - ATTR_ENTITY_ID: "input_datetime.test_datetime_initial_with_tz", - ATTR_TIMESTAMP: state_without_tz.attributes[ATTR_TIMESTAMP], - }, - blocking=True, - ) - state_with_tz_updated = hass.states.get( - "input_datetime.test_datetime_initial_with_tz" - ) - assert state_with_tz_updated.state == "2020-12-13 10:00:00" - assert ( - state_with_tz_updated.attributes[ATTR_TIMESTAMP] - == state_without_tz.attributes[ATTR_TIMESTAMP] - ) - - finally: - dt_util.set_default_time_zone(ORIG_TIMEZONE) + # Test that setting the timestamp of an entity works. + await hass.services.async_call( + DOMAIN, + "set_datetime", + { + ATTR_ENTITY_ID: "input_datetime.test_datetime_initial_with_tz", + ATTR_TIMESTAMP: state_without_tz.attributes[ATTR_TIMESTAMP], + }, + blocking=True, + ) + state_with_tz_updated = hass.states.get( + "input_datetime.test_datetime_initial_with_tz" + ) + assert state_with_tz_updated.state == "2020-12-13 10:00:00" + assert ( + state_with_tz_updated.attributes[ATTR_TIMESTAMP] + == state_without_tz.attributes[ATTR_TIMESTAMP] + ) @pytest.mark.parametrize( diff --git a/tests/components/input_datetime/test_reproduce_state.py b/tests/components/input_datetime/test_reproduce_state.py index f2d9dd4d445..bac620b8983 100644 --- a/tests/components/input_datetime/test_reproduce_state.py +++ b/tests/components/input_datetime/test_reproduce_state.py @@ -1,5 +1,6 @@ """Test reproduce state for Input datetime.""" from homeassistant.core import State +from homeassistant.helpers.state import async_reproduce_state from tests.common import async_mock_service @@ -28,7 +29,8 @@ async def test_reproducing_states(hass, caplog): datetime_calls = async_mock_service(hass, "input_datetime", "set_datetime") # These calls should do nothing as entities already in desired state - await hass.helpers.state.async_reproduce_state( + await async_reproduce_state( + hass, [ State("input_datetime.entity_datetime", "2010-10-10 01:20:00"), State("input_datetime.entity_time", "01:20:00"), @@ -39,7 +41,8 @@ async def test_reproducing_states(hass, caplog): assert len(datetime_calls) == 0 # Test invalid state is handled - await hass.helpers.state.async_reproduce_state( + await async_reproduce_state( + hass, [ State("input_datetime.entity_datetime", "not_supported"), State("input_datetime.entity_datetime", "not-valid-date"), @@ -55,7 +58,8 @@ async def test_reproducing_states(hass, caplog): assert len(datetime_calls) == 0 # Make sure correct services are called - await hass.helpers.state.async_reproduce_state( + await async_reproduce_state( + hass, [ State("input_datetime.entity_datetime", "2011-10-10 02:20:00"), State("input_datetime.entity_time", "02:20:00"), diff --git a/tests/components/input_number/test_reproduce_state.py b/tests/components/input_number/test_reproduce_state.py index 38a777732ad..9c6f5bda708 100644 --- a/tests/components/input_number/test_reproduce_state.py +++ b/tests/components/input_number/test_reproduce_state.py @@ -1,5 +1,6 @@ """Test reproduce state for Input number.""" from homeassistant.core import State +from homeassistant.helpers.state import async_reproduce_state from homeassistant.setup import async_setup_component VALID_NUMBER1 = "19.0" @@ -20,7 +21,8 @@ async def test_reproducing_states(hass, caplog): ) # These calls should do nothing as entities already in desired state - await hass.helpers.state.async_reproduce_state( + await async_reproduce_state( + hass, [ State("input_number.test_number", VALID_NUMBER1), # Should not raise @@ -31,7 +33,8 @@ async def test_reproducing_states(hass, caplog): assert hass.states.get("input_number.test_number").state == VALID_NUMBER1 # Test reproducing with different state - await hass.helpers.state.async_reproduce_state( + await async_reproduce_state( + hass, [ State("input_number.test_number", VALID_NUMBER2), # Should not raise @@ -42,14 +45,13 @@ async def test_reproducing_states(hass, caplog): assert hass.states.get("input_number.test_number").state == VALID_NUMBER2 # Test setting state to number out of range - await hass.helpers.state.async_reproduce_state( - [State("input_number.test_number", "150")] - ) + await async_reproduce_state(hass, [State("input_number.test_number", "150")]) # The entity states should be unchanged after trying to set them to out-of-range number assert hass.states.get("input_number.test_number").state == VALID_NUMBER2 - await hass.helpers.state.async_reproduce_state( + await async_reproduce_state( + hass, [ # Test invalid state State("input_number.test_number", "invalid_state"), diff --git a/tests/components/input_select/test_reproduce_state.py b/tests/components/input_select/test_reproduce_state.py index c4cfbea268d..e095bd97dfc 100644 --- a/tests/components/input_select/test_reproduce_state.py +++ b/tests/components/input_select/test_reproduce_state.py @@ -1,5 +1,6 @@ """Test reproduce state for Input select.""" from homeassistant.core import State +from homeassistant.helpers.state import async_reproduce_state from homeassistant.setup import async_setup_component VALID_OPTION1 = "Option A" @@ -29,7 +30,8 @@ async def test_reproducing_states(hass, caplog): ) # These calls should do nothing as entities already in desired state - await hass.helpers.state.async_reproduce_state( + await async_reproduce_state( + hass, [ State(ENTITY, VALID_OPTION1), # Should not raise @@ -41,7 +43,8 @@ async def test_reproducing_states(hass, caplog): assert hass.states.get(ENTITY).state == VALID_OPTION1 # Try reproducing with different state - await hass.helpers.state.async_reproduce_state( + await async_reproduce_state( + hass, [ State(ENTITY, VALID_OPTION3), # Should not raise @@ -53,14 +56,14 @@ async def test_reproducing_states(hass, caplog): assert hass.states.get(ENTITY).state == VALID_OPTION3 # Test setting state to invalid state - await hass.helpers.state.async_reproduce_state([State(ENTITY, INVALID_OPTION)]) + await async_reproduce_state(hass, [State(ENTITY, INVALID_OPTION)]) # The entity state should be unchanged assert hass.states.get(ENTITY).state == VALID_OPTION3 # Test setting a different option set - await hass.helpers.state.async_reproduce_state( - [State(ENTITY, VALID_OPTION5, {"options": VALID_OPTION_SET2})] + await async_reproduce_state( + hass, [State(ENTITY, VALID_OPTION5, {"options": VALID_OPTION_SET2})] ) # These should fail if options weren't changed to VALID_OPTION_SET2 diff --git a/tests/components/input_text/test_reproduce_state.py b/tests/components/input_text/test_reproduce_state.py index 01117a28f53..b3235beab15 100644 --- a/tests/components/input_text/test_reproduce_state.py +++ b/tests/components/input_text/test_reproduce_state.py @@ -1,5 +1,6 @@ """Test reproduce state for Input text.""" from homeassistant.core import State +from homeassistant.helpers.state import async_reproduce_state from homeassistant.setup import async_setup_component VALID_TEXT1 = "Test text" @@ -23,7 +24,8 @@ async def test_reproducing_states(hass, caplog): ) # These calls should do nothing as entities already in desired state - await hass.helpers.state.async_reproduce_state( + await async_reproduce_state( + hass, [ State("input_text.test_text", VALID_TEXT1), # Should not raise @@ -35,7 +37,8 @@ async def test_reproducing_states(hass, caplog): assert hass.states.get("input_text.test_text").state == VALID_TEXT1 # Try reproducing with different state - await hass.helpers.state.async_reproduce_state( + await async_reproduce_state( + hass, [ State("input_text.test_text", VALID_TEXT2), # Should not raise @@ -47,17 +50,13 @@ async def test_reproducing_states(hass, caplog): assert hass.states.get("input_text.test_text").state == VALID_TEXT2 # Test setting state to invalid state (length too long) - await hass.helpers.state.async_reproduce_state( - [State("input_text.test_text", INVALID_TEXT1)] - ) + await async_reproduce_state(hass, [State("input_text.test_text", INVALID_TEXT1)]) # The entity state should be unchanged assert hass.states.get("input_text.test_text").state == VALID_TEXT2 # Test setting state to invalid state (length too short) - await hass.helpers.state.async_reproduce_state( - [State("input_text.test_text", INVALID_TEXT2)] - ) + await async_reproduce_state(hass, [State("input_text.test_text", INVALID_TEXT2)]) # The entity state should be unchanged assert hass.states.get("input_text.test_text").state == VALID_TEXT2 diff --git a/tests/components/insteon/fixtures/aldb_data.json b/tests/components/insteon/fixtures/aldb_data.json index 2cab1dd5050..59615a4a902 100644 --- a/tests/components/insteon/fixtures/aldb_data.json +++ b/tests/components/insteon/fixtures/aldb_data.json @@ -1,67 +1,67 @@ { - "4095": { - "memory": 4095, - "in_use": true, - "controller": false, - "high_water_mark": false, - "bit5": true, - "bit4": false, - "group": 0, - "target": "aaaaaa", - "data1": 0, - "data2": 0, - "data3": 0 - }, - "4087": { - "memory": 4087, - "in_use": true, - "controller": true, - "high_water_mark": false, - "bit5": true, - "bit4": false, - "group": 1, - "target": "aaaaaa", - "data1": 0, - "data2": 0, - "data3": 0 - }, - "4079": { - "memory": 4079, - "in_use": true, - "controller": false, - "high_water_mark": false, - "bit5": true, - "bit4": false, - "group": 0, - "target": "111111", - "data1": 0, - "data2": 0, - "data3": 0 - }, - "4071": { - "memory": 4071, - "in_use": true, - "controller": true, - "high_water_mark": false, - "bit5": true, - "bit4": false, - "group": 2, - "target": "222222", - "data1": 0, - "data2": 0, - "data3": 0 - }, - "4063": { - "memory": 4063, - "in_use": true, - "controller": false, - "high_water_mark": false, - "bit5": true, - "bit4": false, - "group": 3, - "target": "333333", - "data1": 0, - "data2": 0, - "data3": 0 - } -} \ No newline at end of file + "4095": { + "memory": 4095, + "in_use": true, + "controller": false, + "high_water_mark": false, + "bit5": true, + "bit4": false, + "group": 0, + "target": "aaaaaa", + "data1": 0, + "data2": 0, + "data3": 0 + }, + "4087": { + "memory": 4087, + "in_use": true, + "controller": true, + "high_water_mark": false, + "bit5": true, + "bit4": false, + "group": 1, + "target": "aaaaaa", + "data1": 0, + "data2": 0, + "data3": 0 + }, + "4079": { + "memory": 4079, + "in_use": true, + "controller": false, + "high_water_mark": false, + "bit5": true, + "bit4": false, + "group": 0, + "target": "111111", + "data1": 0, + "data2": 0, + "data3": 0 + }, + "4071": { + "memory": 4071, + "in_use": true, + "controller": true, + "high_water_mark": false, + "bit5": true, + "bit4": false, + "group": 2, + "target": "222222", + "data1": 0, + "data2": 0, + "data3": 0 + }, + "4063": { + "memory": 4063, + "in_use": true, + "controller": false, + "high_water_mark": false, + "bit5": true, + "bit4": false, + "group": 3, + "target": "333333", + "data1": 0, + "data2": 0, + "data3": 0 + } +} diff --git a/tests/components/insteon/fixtures/kpl_properties.json b/tests/components/insteon/fixtures/kpl_properties.json index 1115428a073..725432dbedc 100644 --- a/tests/components/insteon/fixtures/kpl_properties.json +++ b/tests/components/insteon/fixtures/kpl_properties.json @@ -1,66 +1,66 @@ { - "operating_flags": { - "program_lock_on": false, - "blink_on_tx_on": false, - "resume_dim_on": false, - "led_on": false, - "key_beep_on": false, - "rf_disable_on": false, - "powerline_disable_on": false, - "blink_on_error_on": false - }, - "properties": { - "led_dimming": 10, - "non_toggle_mask": 2, - "non_toggle_on_off_mask": 2, - "trigger_group_mask": 0, - "on_mask": 0, - "off_mask": 0, - "x10_house": 32, - "x10_unit": 32, - "ramp_rate": 28, - "on_level": 255, - "on_mask_2": 0, - "off_mask_2": 0, - "x10_house_2": 32, - "x10_unit_2": 32, - "ramp_rate_2": 0, - "on_level_2": 0, - "on_mask_3": 0, - "off_mask_3": 0, - "x10_house_3": 32, - "x10_unit_3": 32, - "ramp_rate_3": 0, - "on_level_3": 0, - "on_mask_4": 16, - "off_mask_4": 16, - "x10_house_4": 32, - "x10_unit_4": 32, - "ramp_rate_4": 0, - "on_level_4": 0, - "on_mask_5": 0, - "off_mask_5": 0, - "x10_house_5": 32, - "x10_unit_5": 32, - "ramp_rate_5": 0, - "on_level_5": 0, - "on_mask_6": 0, - "off_mask_6": 0, - "x10_house_6": 32, - "x10_unit_6": 32, - "ramp_rate_6": 0, - "on_level_6": 0, - "on_mask_7": 128, - "off_mask_7": 128, - "x10_house_7": 32, - "x10_unit_7": 32, - "ramp_rate_7": 0, - "on_level_7": 0, - "on_mask_8": 64, - "off_mask_8": 64, - "x10_house_8": 32, - "x10_unit_8": 2, - "ramp_rate_8": 98, - "on_level_8": 74 - } -} \ No newline at end of file + "operating_flags": { + "program_lock_on": false, + "blink_on_tx_on": false, + "resume_dim_on": false, + "led_on": false, + "key_beep_on": false, + "rf_disable_on": false, + "powerline_disable_on": false, + "blink_on_error_on": false + }, + "properties": { + "led_dimming": 10, + "non_toggle_mask": 2, + "non_toggle_on_off_mask": 2, + "trigger_group_mask": 0, + "on_mask": 0, + "off_mask": 0, + "x10_house": 32, + "x10_unit": 32, + "ramp_rate": 28, + "on_level": 255, + "on_mask_2": 0, + "off_mask_2": 0, + "x10_house_2": 32, + "x10_unit_2": 32, + "ramp_rate_2": 0, + "on_level_2": 0, + "on_mask_3": 0, + "off_mask_3": 0, + "x10_house_3": 32, + "x10_unit_3": 32, + "ramp_rate_3": 0, + "on_level_3": 0, + "on_mask_4": 16, + "off_mask_4": 16, + "x10_house_4": 32, + "x10_unit_4": 32, + "ramp_rate_4": 0, + "on_level_4": 0, + "on_mask_5": 0, + "off_mask_5": 0, + "x10_house_5": 32, + "x10_unit_5": 32, + "ramp_rate_5": 0, + "on_level_5": 0, + "on_mask_6": 0, + "off_mask_6": 0, + "x10_house_6": 32, + "x10_unit_6": 32, + "ramp_rate_6": 0, + "on_level_6": 0, + "on_mask_7": 128, + "off_mask_7": 128, + "x10_house_7": 32, + "x10_unit_7": 32, + "ramp_rate_7": 0, + "on_level_7": 0, + "on_mask_8": 64, + "off_mask_8": 64, + "x10_house_8": 32, + "x10_unit_8": 2, + "ramp_rate_8": 98, + "on_level_8": 74 + } +} diff --git a/tests/components/integration/test_config_flow.py b/tests/components/integration/test_config_flow.py new file mode 100644 index 00000000000..5992d480f80 --- /dev/null +++ b/tests/components/integration/test_config_flow.py @@ -0,0 +1,145 @@ +"""Test the Integration - Riemann sum integral config flow.""" +from unittest.mock import patch + +import pytest + +from homeassistant import config_entries +from homeassistant.components.integration.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM + +from tests.common import MockConfigEntry + + +@pytest.mark.parametrize("platform", ("sensor",)) +async def test_config_flow(hass: HomeAssistant, platform) -> None: + """Test the config flow.""" + input_sensor_entity_id = "sensor.input" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] is None + + with patch( + "homeassistant.components.integration.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "method": "left", + "name": "My integration", + "round": 1, + "source": input_sensor_entity_id, + "unit_prefix": "none", + "unit_time": "min", + }, + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "My integration" + assert result["data"] == {} + assert result["options"] == { + "method": "left", + "name": "My integration", + "round": 1.0, + "source": "sensor.input", + "unit_prefix": "none", + "unit_time": "min", + } + assert len(mock_setup_entry.mock_calls) == 1 + + config_entry = hass.config_entries.async_entries(DOMAIN)[0] + assert config_entry.data == {} + assert config_entry.options == { + "method": "left", + "name": "My integration", + "round": 1.0, + "source": "sensor.input", + "unit_prefix": "none", + "unit_time": "min", + } + assert config_entry.title == "My integration" + + +def get_suggested(schema, key): + """Get suggested value for key in voluptuous schema.""" + for k in schema.keys(): + if k == key: + if k.description is None or "suggested_value" not in k.description: + return None + return k.description["suggested_value"] + # Wanted key absent from schema + raise Exception + + +@pytest.mark.parametrize("platform", ("sensor",)) +async def test_options(hass: HomeAssistant, platform) -> None: + """Test reconfiguring.""" + # Setup the config entry + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + "method": "left", + "name": "My integration", + "round": 1.0, + "source": "sensor.input", + "unit_prefix": "k", + "unit_time": "min", + }, + title="My integration", + ) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "init" + schema = result["data_schema"].schema + assert get_suggested(schema, "round") == 1.0 + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + "round": 2.0, + }, + ) + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["data"] == { + "method": "left", + "name": "My integration", + "round": 2.0, + "source": "sensor.input", + "unit_prefix": "k", + "unit_time": "min", + } + assert config_entry.data == {} + assert config_entry.options == { + "method": "left", + "name": "My integration", + "round": 2.0, + "source": "sensor.input", + "unit_prefix": "k", + "unit_time": "min", + } + assert config_entry.title == "My integration" + + # Check config entry is reloaded with new options + await hass.async_block_till_done() + + # Check the entity was updated, no new entity was created + assert len(hass.states.async_all()) == 1 + + # Check the state of the entity has changed as expected + hass.states.async_set("sensor.input", 10, {"unit_of_measurement": "dog"}) + hass.states.async_set("sensor.input", 11, {"unit_of_measurement": "dog"}) + await hass.async_block_till_done() + + state = hass.states.get(f"{platform}.my_integration") + assert state.state != "unknown" + assert state.attributes["unit_of_measurement"] == "kdogmin" diff --git a/tests/components/integration/test_init.py b/tests/components/integration/test_init.py new file mode 100644 index 00000000000..b68e3cdb1eb --- /dev/null +++ b/tests/components/integration/test_init.py @@ -0,0 +1,61 @@ +"""Test the Integration - Riemann sum integral integration.""" +import pytest + +from homeassistant.components.integration.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from tests.common import MockConfigEntry + + +@pytest.mark.parametrize("platform", ("sensor",)) +async def test_setup_and_remove_config_entry( + hass: HomeAssistant, + platform: str, +) -> None: + """Test setting up and removing a config entry.""" + input_sensor_entity_id = "sensor.input" + registry = er.async_get(hass) + integration_entity_id = f"{platform}.my_integration" + + # Setup the config entry + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + "method": "trapezoidal", + "name": "My integration", + "round": 1.0, + "source": "sensor.input", + "unit_prefix": "k", + "unit_time": "min", + }, + title="My integration", + ) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + # Check the entity is registered in the entity registry + assert registry.async_get(integration_entity_id) is not None + + # Check the platform is setup correctly + state = hass.states.get(integration_entity_id) + assert state.state == "unknown" + assert "unit_of_measurement" not in state.attributes + assert state.attributes["source"] == "sensor.input" + + hass.states.async_set(input_sensor_entity_id, 10, {"unit_of_measurement": "cat"}) + hass.states.async_set(input_sensor_entity_id, 11, {"unit_of_measurement": "cat"}) + await hass.async_block_till_done() + state = hass.states.get(integration_entity_id) + assert state.state != "unknown" + assert state.attributes["unit_of_measurement"] == "kcatmin" + + # Remove the config entry + assert await hass.config_entries.async_remove(config_entry.entry_id) + await hass.async_block_till_done() + + # Check the state and entity registry entry are removed + assert hass.states.get(integration_entity_id) is None + assert registry.async_get(integration_entity_id) is None diff --git a/tests/components/intellifire/conftest.py b/tests/components/intellifire/conftest.py index 2bbb3318090..a9de5a260bc 100644 --- a/tests/components/intellifire/conftest.py +++ b/tests/components/intellifire/conftest.py @@ -14,6 +14,28 @@ def mock_setup_entry() -> Generator[AsyncMock, None, None]: yield mock_setup +@pytest.fixture() +def mock_fireplace_finder_none() -> Generator[None, MagicMock, None]: + """Mock fireplace finder.""" + mock_found_fireplaces = Mock() + mock_found_fireplaces.ips = [] + with patch( + "homeassistant.components.intellifire.config_flow.AsyncUDPFireplaceFinder.search_fireplace" + ): + yield mock_found_fireplaces + + +@pytest.fixture() +def mock_fireplace_finder_single() -> Generator[None, MagicMock, None]: + """Mock fireplace finder.""" + mock_found_fireplaces = Mock() + mock_found_fireplaces.ips = ["192.168.1.69"] + with patch( + "homeassistant.components.intellifire.config_flow.AsyncUDPFireplaceFinder.search_fireplace" + ): + yield mock_found_fireplaces + + @pytest.fixture def mock_intellifire_config_flow() -> Generator[None, MagicMock, None]: """Return a mocked IntelliFire client.""" diff --git a/tests/components/intellifire/test_config_flow.py b/tests/components/intellifire/test_config_flow.py index c9d08318d3c..1283c9db0b2 100644 --- a/tests/components/intellifire/test_config_flow.py +++ b/tests/components/intellifire/test_config_flow.py @@ -1,41 +1,153 @@ """Test the IntelliFire config flow.""" -from unittest.mock import AsyncMock, MagicMock +from unittest.mock import AsyncMock, MagicMock, patch from homeassistant import config_entries +from homeassistant.components import dhcp +from homeassistant.components.intellifire.config_flow import MANUAL_ENTRY_STRING from homeassistant.components.intellifire.const import DOMAIN +from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM +from tests.common import MockConfigEntry -async def test_form( + +async def test_no_discovery( hass: HomeAssistant, mock_setup_entry: AsyncMock, mock_intellifire_config_flow: MagicMock, ) -> None: - """Test we get the form.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) + """Test we should get the manual discovery form - because no discovered fireplaces.""" + with patch( + "homeassistant.components.intellifire.config_flow.AsyncUDPFireplaceFinder.search_fireplace", + return_value=[], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) assert result["type"] == RESULT_TYPE_FORM - assert result["errors"] is None + assert result["errors"] == {} + assert result["step_id"] == "manual_device_entry" result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { - "host": "1.1.1.1", + CONF_HOST: "1.1.1.1", }, ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY - assert result2["title"] == "Fireplace" - assert result2["data"] == {"host": "1.1.1.1"} - + assert result2["title"] == "Fireplace 12345" + assert result2["data"] == {CONF_HOST: "1.1.1.1"} assert len(mock_setup_entry.mock_calls) == 1 -async def test_form_cannot_connect( - hass: HomeAssistant, mock_intellifire_config_flow: MagicMock +async def test_single_discovery( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_intellifire_config_flow: MagicMock, +) -> None: + """Test single fireplace UDP discovery.""" + with patch( + "homeassistant.components.intellifire.config_flow.AsyncUDPFireplaceFinder.search_fireplace", + return_value=["192.168.1.69"], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_HOST: "192.168.1.69"} + ) + await hass.async_block_till_done() + print("Result:", result) + + assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["title"] == "Fireplace 12345" + assert result2["data"] == {CONF_HOST: "192.168.1.69"} + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_manual_entry( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_intellifire_config_flow: MagicMock, +) -> None: + """Test for multiple firepalce discovery - involing a pick_device step.""" + with patch( + "homeassistant.components.intellifire.config_flow.AsyncUDPFireplaceFinder.search_fireplace", + return_value=["192.168.1.69", "192.168.1.33", "192.168.169"], + ): + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["step_id"] == "pick_device" + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_HOST: MANUAL_ENTRY_STRING} + ) + + await hass.async_block_till_done() + assert result2["step_id"] == "manual_device_entry" + + +async def test_multi_discovery( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_intellifire_config_flow: MagicMock, +) -> None: + """Test for multiple fireplace discovery - involving a pick_device step.""" + with patch( + "homeassistant.components.intellifire.config_flow.AsyncUDPFireplaceFinder.search_fireplace", + return_value=["192.168.1.69", "192.168.1.33", "192.168.169"], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["step_id"] == "pick_device" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_HOST: "192.168.1.33"} + ) + await hass.async_block_till_done() + assert result["step_id"] == "pick_device" + + assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + + +async def test_multi_discovery_cannot_connect( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_intellifire_config_flow: MagicMock, +) -> None: + """Test for multiple fireplace discovery - involving a pick_device step.""" + with patch( + "homeassistant.components.intellifire.config_flow.AsyncUDPFireplaceFinder.search_fireplace", + return_value=["192.168.1.69", "192.168.1.33", "192.168.169"], + ): + + mock_intellifire_config_flow.poll.side_effect = ConnectionError + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "pick_device" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_HOST: "192.168.1.33"} + ) + await hass.async_block_till_done() + assert result2["type"] == RESULT_TYPE_FORM + assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_form_cannot_connect_manual_entry( + hass: HomeAssistant, + mock_intellifire_config_flow: MagicMock, + mock_fireplace_finder_single: AsyncMock, ) -> None: """Test we handle cannot connect error.""" mock_intellifire_config_flow.poll.side_effect = ConnectionError @@ -43,13 +155,102 @@ async def test_form_cannot_connect( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "manual_device_entry" result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { - "host": "1.1.1.1", + CONF_HOST: "1.1.1.1", }, ) assert result2["type"] == RESULT_TYPE_FORM assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_picker_already_discovered( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_intellifire_config_flow: MagicMock, +) -> None: + """Test single fireplace UDP discovery.""" + + entry = MockConfigEntry( + domain=DOMAIN, + data={ + "host": "192.168.1.3", + }, + title="Fireplace", + unique_id=44444, + ) + entry.add_to_hass(hass) + with patch( + "homeassistant.components.intellifire.config_flow.AsyncUDPFireplaceFinder.search_fireplace", + return_value=["192.168.1.3"], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + await hass.async_block_till_done() + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOST: "192.168.1.4", + }, + ) + assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["title"] == "Fireplace 12345" + assert result2["data"] == {CONF_HOST: "192.168.1.4"} + assert len(mock_setup_entry.mock_calls) == 2 + + +async def test_dhcp_discovery_intellifire_device( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_intellifire_config_flow: MagicMock, +) -> None: + """Test successful DHCP Discovery.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data=dhcp.DhcpServiceInfo( + ip="1.1.1.1", + macaddress="AA:BB:CC:DD:EE:FF", + hostname="zentrios-Test", + ), + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "dhcp_confirm" + result2 = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result2["type"] == RESULT_TYPE_FORM + assert result2["step_id"] == "dhcp_confirm" + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], user_input={} + ) + assert result3["title"] == "Fireplace 12345" + assert result3["data"] == {"host": "1.1.1.1"} + + +async def test_dhcp_discovery_non_intellifire_device( + hass: HomeAssistant, + mock_intellifire_config_flow: MagicMock, + mock_setup_entry: AsyncMock, +) -> None: + """Test failed DHCP Discovery.""" + + mock_intellifire_config_flow.poll.side_effect = ConnectionError + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data=dhcp.DhcpServiceInfo( + ip="1.1.1.1", + macaddress="AA:BB:CC:DD:EE:FF", + hostname="zentrios-Evil", + ), + ) + + assert result["type"] == "abort" + assert result["reason"] == "not_intellifire_device" diff --git a/tests/components/iqvia/fixtures/allergy_forecast_data.json b/tests/components/iqvia/fixtures/allergy_forecast_data.json index 2888feaa5a6..fafeec3c67a 100644 --- a/tests/components/iqvia/fixtures/allergy_forecast_data.json +++ b/tests/components/iqvia/fixtures/allergy_forecast_data.json @@ -30,4 +30,3 @@ "DisplayLocation": "Schenectady, NY" } } - diff --git a/tests/components/iqvia/fixtures/allergy_index_data.json b/tests/components/iqvia/fixtures/allergy_index_data.json index 9954c780dbb..20794cf07ad 100644 --- a/tests/components/iqvia/fixtures/allergy_index_data.json +++ b/tests/components/iqvia/fixtures/allergy_index_data.json @@ -85,4 +85,3 @@ "DisplayLocation": "Schenectady, NY" } } - diff --git a/tests/components/iqvia/fixtures/allergy_outlook_data.json b/tests/components/iqvia/fixtures/allergy_outlook_data.json index 8696b45173a..bdfdd86c854 100644 --- a/tests/components/iqvia/fixtures/allergy_outlook_data.json +++ b/tests/components/iqvia/fixtures/allergy_outlook_data.json @@ -6,4 +6,3 @@ "Outlook": "The amount of pollen in the air for Wednesday...", "Season": "Tree" } - diff --git a/tests/components/iqvia/fixtures/asthma_forecast_data.json b/tests/components/iqvia/fixtures/asthma_forecast_data.json index 89b9d2279c0..acebc3affee 100644 --- a/tests/components/iqvia/fixtures/asthma_forecast_data.json +++ b/tests/components/iqvia/fixtures/asthma_forecast_data.json @@ -35,4 +35,3 @@ "DisplayLocation": "Schenectady, NY" } } - diff --git a/tests/components/iqvia/fixtures/asthma_index_data.json b/tests/components/iqvia/fixtures/asthma_index_data.json index 3ddf54c150b..b74c05ca835 100644 --- a/tests/components/iqvia/fixtures/asthma_index_data.json +++ b/tests/components/iqvia/fixtures/asthma_index_data.json @@ -69,4 +69,3 @@ "DisplayLocation": "Schenectady, NY" } } - diff --git a/tests/components/iqvia/fixtures/disease_forecast_data.json b/tests/components/iqvia/fixtures/disease_forecast_data.json index 3760d60a549..ffbfbe4773d 100644 --- a/tests/components/iqvia/fixtures/disease_forecast_data.json +++ b/tests/components/iqvia/fixtures/disease_forecast_data.json @@ -26,4 +26,3 @@ "DisplayLocation": "Schenectady, NY" } } - diff --git a/tests/components/iqvia/fixtures/disease_index_data.json b/tests/components/iqvia/fixtures/disease_index_data.json index 3f8241bf10c..45edcf8ce9b 100644 --- a/tests/components/iqvia/fixtures/disease_index_data.json +++ b/tests/components/iqvia/fixtures/disease_index_data.json @@ -74,4 +74,3 @@ }, "Type": "cold" } - diff --git a/tests/components/jewish_calendar/__init__.py b/tests/components/jewish_calendar/__init__.py index b0279fd2748..fbcd6c5325f 100644 --- a/tests/components/jewish_calendar/__init__.py +++ b/tests/components/jewish_calendar/__init__.py @@ -13,13 +13,6 @@ HDATE_DEFAULT_ALTITUDE = 754 NYC_LATLNG = _LatLng(40.7128, -74.0060) JERUSALEM_LATLNG = _LatLng(31.778, 35.235) -ORIG_TIME_ZONE = dt_util.DEFAULT_TIME_ZONE - - -def teardown_module(): - """Reset time zone.""" - dt_util.set_default_time_zone(ORIG_TIME_ZONE) - def make_nyc_test_params(dtime, results, havdalah_offset=0): """Make test params for NYC.""" diff --git a/tests/components/jewish_calendar/test_binary_sensor.py b/tests/components/jewish_calendar/test_binary_sensor.py index b34dfdb28e4..15052243baa 100644 --- a/tests/components/jewish_calendar/test_binary_sensor.py +++ b/tests/components/jewish_calendar/test_binary_sensor.py @@ -181,7 +181,7 @@ async def test_issur_melacha_sensor( time_zone = dt_util.get_time_zone(tzname) test_time = now.replace(tzinfo=time_zone) - hass.config.time_zone = tzname + hass.config.set_time_zone(tzname) hass.config.latitude = latitude hass.config.longitude = longitude @@ -272,7 +272,7 @@ async def test_issur_melacha_sensor_update( time_zone = dt_util.get_time_zone(tzname) test_time = now.replace(tzinfo=time_zone) - hass.config.time_zone = tzname + hass.config.set_time_zone(tzname) hass.config.latitude = latitude hass.config.longitude = longitude diff --git a/tests/components/jewish_calendar/test_sensor.py b/tests/components/jewish_calendar/test_sensor.py index 879b5edb120..e2d24bcef04 100644 --- a/tests/components/jewish_calendar/test_sensor.py +++ b/tests/components/jewish_calendar/test_sensor.py @@ -165,7 +165,7 @@ async def test_jewish_calendar_sensor( time_zone = dt_util.get_time_zone(tzname) test_time = now.replace(tzinfo=time_zone) - hass.config.time_zone = tzname + hass.config.set_time_zone(tzname) hass.config.latitude = latitude hass.config.longitude = longitude @@ -510,7 +510,7 @@ async def test_shabbat_times_sensor( time_zone = dt_util.get_time_zone(tzname) test_time = now.replace(tzinfo=time_zone) - hass.config.time_zone = tzname + hass.config.set_time_zone(tzname) hass.config.latitude = latitude hass.config.longitude = longitude diff --git a/tests/components/kaleidescape/__init__.py b/tests/components/kaleidescape/__init__.py new file mode 100644 index 00000000000..8182cb73743 --- /dev/null +++ b/tests/components/kaleidescape/__init__.py @@ -0,0 +1,18 @@ +"""Tests for Kaleidescape integration.""" + +from homeassistant.components import ssdp +from homeassistant.components.ssdp import ATTR_UPNP_FRIENDLY_NAME, ATTR_UPNP_SERIAL + +MOCK_HOST = "127.0.0.1" +MOCK_SERIAL = "123456" +MOCK_NAME = "Theater" + +MOCK_SSDP_DISCOVERY_INFO = ssdp.SsdpServiceInfo( + ssdp_usn="mock_usn", + ssdp_st="mock_st", + ssdp_location=f"http://{MOCK_HOST}", + upnp={ + ATTR_UPNP_FRIENDLY_NAME: MOCK_NAME, + ATTR_UPNP_SERIAL: MOCK_SERIAL, + }, +) diff --git a/tests/components/kaleidescape/conftest.py b/tests/components/kaleidescape/conftest.py new file mode 100644 index 00000000000..c86d8f2ccd0 --- /dev/null +++ b/tests/components/kaleidescape/conftest.py @@ -0,0 +1,73 @@ +"""Fixtures for Kaleidescape integration.""" + +from collections.abc import Generator +from unittest.mock import AsyncMock, patch + +from kaleidescape import Dispatcher +from kaleidescape.device import Automation, Movie, Power, System +import pytest + +from homeassistant.components.kaleidescape.const import DOMAIN +from homeassistant.const import CONF_HOST +from homeassistant.core import HomeAssistant + +from . import MOCK_HOST, MOCK_SERIAL + +from tests.common import MockConfigEntry + + +@pytest.fixture(name="mock_device") +def fixture_mock_device() -> Generator[None, AsyncMock, None]: + """Return a mocked Kaleidescape device.""" + with patch( + "homeassistant.components.kaleidescape.KaleidescapeDevice", autospec=True + ) as mock: + host = MOCK_HOST + + device = mock.return_value + device.dispatcher = Dispatcher() + device.host = host + device.port = 10000 + device.serial_number = MOCK_SERIAL + device.is_connected = True + device.is_server_only = False + device.is_movie_player = True + device.is_music_player = False + device.system = System( + ip_address=host, + serial_number=MOCK_SERIAL, + type="Strato", + protocol=16, + kos_version="10.4.2-19218", + friendly_name=f"Device {MOCK_SERIAL}", + movie_zones=1, + music_zones=1, + ) + device.power = Power(state="standby", readiness="disabled", zone=["available"]) + device.movie = Movie() + device.automation = Automation() + + yield device + + +@pytest.fixture(name="mock_config_entry") +def fixture_mock_config_entry() -> MockConfigEntry: + """Return a mock config entry.""" + return MockConfigEntry( + domain=DOMAIN, + unique_id=MOCK_SERIAL, + version=1, + data={CONF_HOST: MOCK_HOST}, + ) + + +@pytest.fixture(name="mock_integration") +async def fixture_mock_integration( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, +) -> MockConfigEntry: + """Return a mock ConfigEntry setup for Kaleidescape integration.""" + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + return mock_config_entry diff --git a/tests/components/kaleidescape/test_config_flow.py b/tests/components/kaleidescape/test_config_flow.py new file mode 100644 index 00000000000..a2cf8091d02 --- /dev/null +++ b/tests/components/kaleidescape/test_config_flow.py @@ -0,0 +1,130 @@ +"""Tests for Kaleidescape config flow.""" + +import dataclasses +from unittest.mock import AsyncMock + +from homeassistant.components.kaleidescape.const import DOMAIN +from homeassistant.config_entries import SOURCE_SSDP, SOURCE_USER +from homeassistant.const import CONF_HOST +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import ( + RESULT_TYPE_ABORT, + RESULT_TYPE_CREATE_ENTRY, + RESULT_TYPE_FORM, +) + +from . import MOCK_HOST, MOCK_SSDP_DISCOVERY_INFO + +from tests.common import MockConfigEntry + + +async def test_user_config_flow_success( + hass: HomeAssistant, mock_device: AsyncMock +) -> None: + """Test user config flow success.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_HOST: MOCK_HOST} + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert "data" in result + assert result["data"][CONF_HOST] == MOCK_HOST + + +async def test_user_config_flow_bad_connect_errors( + hass: HomeAssistant, mock_device: AsyncMock +) -> None: + """Test errors when connection error occurs.""" + mock_device.connect.side_effect = ConnectionError + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: MOCK_HOST} + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"] == {"base": "cannot_connect"} + + +async def test_user_config_flow_unsupported_device_errors( + hass: HomeAssistant, mock_device: AsyncMock +) -> None: + """Test errors when connecting to unsupported device.""" + mock_device.is_server_only = True + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: MOCK_HOST} + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"] == {"base": "unsupported"} + + +async def test_user_config_flow_device_exists_abort( + hass: HomeAssistant, mock_device: AsyncMock, mock_integration: MockConfigEntry +) -> None: + """Test flow aborts when device already configured.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: MOCK_HOST} + ) + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_ssdp_config_flow_success( + hass: HomeAssistant, mock_device: AsyncMock +) -> None: + """Test ssdp config flow success.""" + discovery_info = dataclasses.replace(MOCK_SSDP_DISCOVERY_INFO) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_SSDP}, data=discovery_info + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "discovery_confirm" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert "data" in result + assert result["data"][CONF_HOST] == MOCK_HOST + + +async def test_ssdp_config_flow_bad_connect_aborts( + hass: HomeAssistant, mock_device: AsyncMock +) -> None: + """Test abort when connection error occurs.""" + mock_device.connect.side_effect = ConnectionError + + discovery_info = dataclasses.replace(MOCK_SSDP_DISCOVERY_INFO) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_SSDP}, data=discovery_info + ) + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "cannot_connect" + + +async def test_ssdp_config_flow_unsupported_device_aborts( + hass: HomeAssistant, mock_device: AsyncMock +) -> None: + """Test abort when connecting to unsupported device.""" + mock_device.is_server_only = True + + discovery_info = dataclasses.replace(MOCK_SSDP_DISCOVERY_INFO) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_SSDP}, data=discovery_info + ) + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "unsupported" diff --git a/tests/components/kaleidescape/test_init.py b/tests/components/kaleidescape/test_init.py new file mode 100644 index 00000000000..876c02ba5a6 --- /dev/null +++ b/tests/components/kaleidescape/test_init.py @@ -0,0 +1,58 @@ +"""Tests for Kaleidescape config entry.""" + +from unittest.mock import AsyncMock + +from homeassistant.components.kaleidescape.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant + +from . import MOCK_SERIAL + +from tests.common import MockConfigEntry + + +async def test_unload_config_entry( + hass: HomeAssistant, + mock_device: AsyncMock, + mock_integration: MockConfigEntry, +) -> None: + """Test config entry loading and unloading.""" + mock_config_entry = mock_integration + assert mock_config_entry.state is ConfigEntryState.LOADED + assert mock_device.connect.call_count == 1 + assert mock_device.disconnect.call_count == 0 + + await hass.config_entries.async_unload(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert mock_device.disconnect.call_count == 1 + assert mock_config_entry.entry_id not in hass.data[DOMAIN] + + +async def test_config_entry_not_ready( + hass: HomeAssistant, + mock_device: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test config entry not ready.""" + mock_device.connect.side_effect = ConnectionError + + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY + + +async def test_device( + hass: HomeAssistant, + mock_device: AsyncMock, + mock_integration: MockConfigEntry, +) -> None: + """Test device.""" + device_registry = await hass.helpers.device_registry.async_get_registry() + device = device_registry.async_get_device( + identifiers={("kaleidescape", MOCK_SERIAL)} + ) + assert device is not None + assert device.identifiers == {("kaleidescape", MOCK_SERIAL)} diff --git a/tests/components/kaleidescape/test_media_player.py b/tests/components/kaleidescape/test_media_player.py new file mode 100644 index 00000000000..94ba7f82fe8 --- /dev/null +++ b/tests/components/kaleidescape/test_media_player.py @@ -0,0 +1,183 @@ +"""Tests for Kaleidescape media player platform.""" + +from unittest.mock import MagicMock + +from kaleidescape import const as kaleidescape_const +from kaleidescape.device import Movie + +from homeassistant.components.media_player.const import DOMAIN as MEDIA_PLAYER_DOMAIN +from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_MEDIA_NEXT_TRACK, + SERVICE_MEDIA_PAUSE, + SERVICE_MEDIA_PLAY, + SERVICE_MEDIA_PREVIOUS_TRACK, + SERVICE_MEDIA_STOP, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_IDLE, + STATE_OFF, + STATE_PAUSED, + STATE_PLAYING, +) +from homeassistant.core import HomeAssistant + +from . import MOCK_SERIAL + +from tests.common import MockConfigEntry + +ENTITY_ID = f"media_player.kaleidescape_device_{MOCK_SERIAL}" +FRIENDLY_NAME = f"Kaleidescape Device {MOCK_SERIAL}" + + +async def test_entity( + hass: HomeAssistant, + mock_device: MagicMock, + mock_integration: MockConfigEntry, +) -> None: + """Test entity attributes.""" + entity = hass.states.get(ENTITY_ID) + assert entity is not None + assert entity.state == STATE_OFF + assert entity.attributes["friendly_name"] == FRIENDLY_NAME + + +async def test_update_state( + hass: HomeAssistant, + mock_device: MagicMock, + mock_integration: MockConfigEntry, +) -> None: + """Tests dispatched signals update player.""" + entity = hass.states.get(ENTITY_ID) + assert entity is not None + assert entity.state == STATE_OFF + + # Device turns on + mock_device.power.state = kaleidescape_const.DEVICE_POWER_STATE_ON + mock_device.dispatcher.send(kaleidescape_const.DEVICE_POWER_STATE) + await hass.async_block_till_done() + entity = hass.states.get(ENTITY_ID) + assert entity is not None + assert entity.state == STATE_IDLE + + # Devices starts playing + mock_device.movie = Movie( + handle="handle", + title="title", + cover="cover", + cover_hires="cover_hires", + rating="rating", + rating_reason="rating_reason", + year="year", + runtime="runtime", + actors=[], + director="director", + directors=[], + genre="genre", + genres=[], + synopsis="synopsis", + color="color", + country="country", + aspect_ratio="aspect_ratio", + media_type="media_type", + play_status=kaleidescape_const.PLAY_STATUS_PLAYING, + play_speed=1, + title_number=1, + title_length=1, + title_location=1, + chapter_number=1, + chapter_length=1, + chapter_location=1, + ) + mock_device.dispatcher.send(kaleidescape_const.PLAY_STATUS) + await hass.async_block_till_done() + entity = hass.states.get(ENTITY_ID) + assert entity is not None + assert entity.state == STATE_PLAYING + + # Devices pauses playing + mock_device.movie.play_status = kaleidescape_const.PLAY_STATUS_PAUSED + mock_device.dispatcher.send(kaleidescape_const.PLAY_STATUS) + await hass.async_block_till_done() + entity = hass.states.get(ENTITY_ID) + assert entity is not None + assert entity.state == STATE_PAUSED + + +async def test_services( + hass: HomeAssistant, + mock_device: MagicMock, + mock_integration: MockConfigEntry, +) -> None: + """Test service calls.""" + await hass.services.async_call( + MEDIA_PLAYER_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ENTITY_ID}, + blocking=True, + ) + assert mock_device.leave_standby.call_count == 1 + + await hass.services.async_call( + MEDIA_PLAYER_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: ENTITY_ID}, + blocking=True, + ) + assert mock_device.enter_standby.call_count == 1 + + await hass.services.async_call( + MEDIA_PLAYER_DOMAIN, + SERVICE_MEDIA_PLAY, + {ATTR_ENTITY_ID: ENTITY_ID}, + blocking=True, + ) + assert mock_device.play.call_count == 1 + + await hass.services.async_call( + MEDIA_PLAYER_DOMAIN, + SERVICE_MEDIA_PAUSE, + {ATTR_ENTITY_ID: ENTITY_ID}, + blocking=True, + ) + assert mock_device.pause.call_count == 1 + + await hass.services.async_call( + MEDIA_PLAYER_DOMAIN, + SERVICE_MEDIA_STOP, + {ATTR_ENTITY_ID: ENTITY_ID}, + blocking=True, + ) + assert mock_device.stop.call_count == 1 + + await hass.services.async_call( + MEDIA_PLAYER_DOMAIN, + SERVICE_MEDIA_NEXT_TRACK, + {ATTR_ENTITY_ID: ENTITY_ID}, + blocking=True, + ) + assert mock_device.next.call_count == 1 + + await hass.services.async_call( + MEDIA_PLAYER_DOMAIN, + SERVICE_MEDIA_PREVIOUS_TRACK, + {ATTR_ENTITY_ID: ENTITY_ID}, + blocking=True, + ) + assert mock_device.previous.call_count == 1 + + +async def test_device( + hass: HomeAssistant, + mock_device: MagicMock, + mock_integration: MockConfigEntry, +) -> None: + """Test device attributes.""" + device_registry = await hass.helpers.device_registry.async_get_registry() + device = device_registry.async_get_device( + identifiers={("kaleidescape", MOCK_SERIAL)} + ) + assert device.name == FRIENDLY_NAME + assert device.model == "Strato" + assert device.sw_version == "10.4.2-19218" + assert device.manufacturer == "Kaleidescape" diff --git a/tests/components/kaleidescape/test_remote.py b/tests/components/kaleidescape/test_remote.py new file mode 100644 index 00000000000..3573d04395d --- /dev/null +++ b/tests/components/kaleidescape/test_remote.py @@ -0,0 +1,156 @@ +"""Tests for Kaleidescape remote platform.""" + +from unittest.mock import MagicMock + +import pytest + +from homeassistant.components.remote import ( + ATTR_COMMAND, + DOMAIN as REMOTE_DOMAIN, + SERVICE_SEND_COMMAND, +) +from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError + +from . import MOCK_SERIAL + +from tests.common import MockConfigEntry + +ENTITY_ID = f"remote.kaleidescape_device_{MOCK_SERIAL}" + + +async def test_entity( + hass: HomeAssistant, + mock_device: MagicMock, + mock_integration: MockConfigEntry, +) -> None: + """Test entity attributes.""" + assert hass.states.get(ENTITY_ID) + + +async def test_commands( + hass: HomeAssistant, + mock_device: MagicMock, + mock_integration: MockConfigEntry, +) -> None: + """Test service calls.""" + await hass.services.async_call( + REMOTE_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ENTITY_ID}, + blocking=True, + ) + assert mock_device.leave_standby.call_count == 1 + + await hass.services.async_call( + REMOTE_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: ENTITY_ID}, + blocking=True, + ) + assert mock_device.enter_standby.call_count == 1 + + await hass.services.async_call( + REMOTE_DOMAIN, + SERVICE_SEND_COMMAND, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_COMMAND: ["select"]}, + blocking=True, + ) + assert mock_device.select.call_count == 1 + + await hass.services.async_call( + REMOTE_DOMAIN, + SERVICE_SEND_COMMAND, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_COMMAND: ["up"]}, + blocking=True, + ) + assert mock_device.up.call_count == 1 + + await hass.services.async_call( + REMOTE_DOMAIN, + SERVICE_SEND_COMMAND, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_COMMAND: ["down"]}, + blocking=True, + ) + assert mock_device.down.call_count == 1 + + await hass.services.async_call( + REMOTE_DOMAIN, + SERVICE_SEND_COMMAND, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_COMMAND: ["left"]}, + blocking=True, + ) + assert mock_device.left.call_count == 1 + + await hass.services.async_call( + REMOTE_DOMAIN, + SERVICE_SEND_COMMAND, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_COMMAND: ["right"]}, + blocking=True, + ) + assert mock_device.right.call_count == 1 + + await hass.services.async_call( + REMOTE_DOMAIN, + SERVICE_SEND_COMMAND, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_COMMAND: ["cancel"]}, + blocking=True, + ) + assert mock_device.cancel.call_count == 1 + + await hass.services.async_call( + REMOTE_DOMAIN, + SERVICE_SEND_COMMAND, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_COMMAND: ["replay"]}, + blocking=True, + ) + assert mock_device.replay.call_count == 1 + + await hass.services.async_call( + REMOTE_DOMAIN, + SERVICE_SEND_COMMAND, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_COMMAND: ["scan_forward"]}, + blocking=True, + ) + assert mock_device.scan_forward.call_count == 1 + + await hass.services.async_call( + REMOTE_DOMAIN, + SERVICE_SEND_COMMAND, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_COMMAND: ["scan_reverse"]}, + blocking=True, + ) + assert mock_device.scan_reverse.call_count == 1 + + await hass.services.async_call( + REMOTE_DOMAIN, + SERVICE_SEND_COMMAND, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_COMMAND: ["go_movie_covers"]}, + blocking=True, + ) + assert mock_device.go_movie_covers.call_count == 1 + + await hass.services.async_call( + REMOTE_DOMAIN, + SERVICE_SEND_COMMAND, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_COMMAND: ["menu_toggle"]}, + blocking=True, + ) + assert mock_device.menu_toggle.call_count == 1 + + +async def test_unknown_command( + hass: HomeAssistant, + mock_device: MagicMock, + mock_integration: MockConfigEntry, +) -> None: + """Test service calls.""" + with pytest.raises(HomeAssistantError) as err: + await hass.services.async_call( + REMOTE_DOMAIN, + SERVICE_SEND_COMMAND, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_COMMAND: ["bad"]}, + blocking=True, + ) + assert str(err.value) == "bad is not a known command" diff --git a/tests/components/kaleidescape/test_sensor.py b/tests/components/kaleidescape/test_sensor.py new file mode 100644 index 00000000000..0ae2dc15619 --- /dev/null +++ b/tests/components/kaleidescape/test_sensor.py @@ -0,0 +1,48 @@ +"""Tests for Kaleidescape sensor platform.""" + +from unittest.mock import MagicMock + +from kaleidescape import const as kaleidescape_const + +from homeassistant.const import ATTR_FRIENDLY_NAME +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import MOCK_SERIAL + +from tests.common import MockConfigEntry + +ENTITY_ID = f"sensor.kaleidescape_device_{MOCK_SERIAL}" +FRIENDLY_NAME = f"Kaleidescape Device {MOCK_SERIAL}" + + +async def test_sensors( + hass: HomeAssistant, + mock_device: MagicMock, + mock_integration: MockConfigEntry, +) -> None: + """Test sensors.""" + entity = hass.states.get(f"{ENTITY_ID}_media_location") + entry = er.async_get(hass).async_get(f"{ENTITY_ID}_media_location") + assert entity + assert entity.state == "none" + assert ( + entity.attributes.get(ATTR_FRIENDLY_NAME) == f"{FRIENDLY_NAME} Media Location" + ) + assert entry + assert entry.unique_id == f"{MOCK_SERIAL}-media_location" + + entity = hass.states.get(f"{ENTITY_ID}_play_status") + entry = er.async_get(hass).async_get(f"{ENTITY_ID}_play_status") + assert entity + assert entity.state == "none" + assert entity.attributes.get(ATTR_FRIENDLY_NAME) == f"{FRIENDLY_NAME} Play Status" + assert entry + assert entry.unique_id == f"{MOCK_SERIAL}-play_status" + + mock_device.movie.play_status = kaleidescape_const.PLAY_STATUS_PLAYING + mock_device.dispatcher.send(kaleidescape_const.PLAY_STATUS) + await hass.async_block_till_done() + entity = hass.states.get(f"{ENTITY_ID}_play_status") + assert entity is not None + assert entity.state == "playing" diff --git a/tests/components/knx/conftest.py b/tests/components/knx/conftest.py index 71a86f1e397..ccfd3a35085 100644 --- a/tests/components/knx/conftest.py +++ b/tests/components/knx/conftest.py @@ -13,11 +13,16 @@ from xknx.telegram import Telegram, TelegramDirection from xknx.telegram.address import GroupAddress, IndividualAddress from xknx.telegram.apci import APCI, GroupValueRead, GroupValueResponse, GroupValueWrite -from homeassistant.components.knx import ConnectionSchema from homeassistant.components.knx.const import ( CONF_KNX_AUTOMATIC, CONF_KNX_CONNECTION_TYPE, + CONF_KNX_DEFAULT_RATE_LIMIT, + CONF_KNX_DEFAULT_STATE_UPDATER, CONF_KNX_INDIVIDUAL_ADDRESS, + CONF_KNX_MCAST_GRP, + CONF_KNX_MCAST_PORT, + CONF_KNX_RATE_LIMIT, + CONF_KNX_STATE_UPDATER, DOMAIN as KNX_DOMAIN, ) from homeassistant.core import HomeAssistant @@ -208,10 +213,12 @@ def mock_config_entry() -> MockConfigEntry: title="KNX", domain=KNX_DOMAIN, data={ - CONF_KNX_INDIVIDUAL_ADDRESS: XKNX.DEFAULT_ADDRESS, - ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, - ConnectionSchema.CONF_KNX_MCAST_PORT: DEFAULT_MCAST_PORT, CONF_KNX_CONNECTION_TYPE: CONF_KNX_AUTOMATIC, + CONF_KNX_RATE_LIMIT: CONF_KNX_DEFAULT_RATE_LIMIT, + CONF_KNX_STATE_UPDATER: CONF_KNX_DEFAULT_STATE_UPDATER, + CONF_KNX_MCAST_PORT: DEFAULT_MCAST_PORT, + CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, + CONF_KNX_INDIVIDUAL_ADDRESS: XKNX.DEFAULT_ADDRESS, }, ) diff --git a/tests/components/knx/test_config_flow.py b/tests/components/knx/test_config_flow.py index 83b5a9988c7..bfdde90cb5b 100644 --- a/tests/components/knx/test_config_flow.py +++ b/tests/components/knx/test_config_flow.py @@ -2,16 +2,16 @@ from unittest.mock import patch import pytest -from xknx import XKNX +from xknx.exceptions.exception import InvalidSignature from xknx.io import DEFAULT_MCAST_GRP from xknx.io.gateway_scanner import GatewayDescriptor from homeassistant import config_entries -from homeassistant.components.knx import ConnectionSchema from homeassistant.components.knx.config_flow import ( CONF_DEFAULT_LOCAL_IP, CONF_KNX_GATEWAY, CONF_KNX_LABEL_TUNNELING_TCP, + CONF_KNX_LABEL_TUNNELING_TCP_SECURE, CONF_KNX_LABEL_TUNNELING_UDP, CONF_KNX_LABEL_TUNNELING_UDP_ROUTE_BACK, CONF_KNX_TUNNELING_TYPE, @@ -22,14 +22,30 @@ from homeassistant.components.knx.const import ( CONF_KNX_AUTOMATIC, CONF_KNX_CONNECTION_TYPE, CONF_KNX_INDIVIDUAL_ADDRESS, + CONF_KNX_KNXKEY_FILENAME, + CONF_KNX_KNXKEY_PASSWORD, + CONF_KNX_LOCAL_IP, + CONF_KNX_MCAST_GRP, + CONF_KNX_MCAST_PORT, + CONF_KNX_RATE_LIMIT, + CONF_KNX_ROUTE_BACK, CONF_KNX_ROUTING, + CONF_KNX_SECURE_DEVICE_AUTHENTICATION, + CONF_KNX_SECURE_USER_ID, + CONF_KNX_SECURE_USER_PASSWORD, + CONF_KNX_STATE_UPDATER, CONF_KNX_TUNNELING, CONF_KNX_TUNNELING_TCP, + CONF_KNX_TUNNELING_TCP_SECURE, DOMAIN, ) from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import ( + RESULT_TYPE_CREATE_ENTRY, + RESULT_TYPE_FORM, + RESULT_TYPE_MENU, +) from tests.common import MockConfigEntry @@ -89,8 +105,8 @@ async def test_routing_setup(hass: HomeAssistant) -> None: result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], { - ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, - ConnectionSchema.CONF_KNX_MCAST_PORT: 3675, + CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, + CONF_KNX_MCAST_PORT: 3675, CONF_KNX_INDIVIDUAL_ADDRESS: "1.1.110", }, ) @@ -100,9 +116,9 @@ async def test_routing_setup(hass: HomeAssistant) -> None: assert result3["data"] == { **DEFAULT_ENTRY_DATA, CONF_KNX_CONNECTION_TYPE: CONF_KNX_ROUTING, - ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, - ConnectionSchema.CONF_KNX_MCAST_PORT: 3675, - ConnectionSchema.CONF_KNX_LOCAL_IP: None, + CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, + CONF_KNX_MCAST_PORT: 3675, + CONF_KNX_LOCAL_IP: None, CONF_KNX_INDIVIDUAL_ADDRESS: "1.1.110", } @@ -141,10 +157,10 @@ async def test_routing_setup_advanced(hass: HomeAssistant) -> None: result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], { - ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, - ConnectionSchema.CONF_KNX_MCAST_PORT: 3675, + CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, + CONF_KNX_MCAST_PORT: 3675, CONF_KNX_INDIVIDUAL_ADDRESS: "1.1.110", - ConnectionSchema.CONF_KNX_LOCAL_IP: "192.168.1.112", + CONF_KNX_LOCAL_IP: "192.168.1.112", }, ) await hass.async_block_till_done() @@ -153,9 +169,9 @@ async def test_routing_setup_advanced(hass: HomeAssistant) -> None: assert result3["data"] == { **DEFAULT_ENTRY_DATA, CONF_KNX_CONNECTION_TYPE: CONF_KNX_ROUTING, - ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, - ConnectionSchema.CONF_KNX_MCAST_PORT: 3675, - ConnectionSchema.CONF_KNX_LOCAL_IP: "192.168.1.112", + CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, + CONF_KNX_MCAST_PORT: 3675, + CONF_KNX_LOCAL_IP: "192.168.1.112", CONF_KNX_INDIVIDUAL_ADDRESS: "1.1.110", } @@ -177,8 +193,8 @@ async def test_routing_setup_advanced(hass: HomeAssistant) -> None: CONF_HOST: "192.168.0.1", CONF_PORT: 3675, CONF_KNX_INDIVIDUAL_ADDRESS: "15.15.250", - ConnectionSchema.CONF_KNX_ROUTE_BACK: False, - ConnectionSchema.CONF_KNX_LOCAL_IP: None, + CONF_KNX_ROUTE_BACK: False, + CONF_KNX_LOCAL_IP: None, }, ), ( @@ -193,8 +209,8 @@ async def test_routing_setup_advanced(hass: HomeAssistant) -> None: CONF_HOST: "192.168.0.1", CONF_PORT: 3675, CONF_KNX_INDIVIDUAL_ADDRESS: "15.15.250", - ConnectionSchema.CONF_KNX_ROUTE_BACK: False, - ConnectionSchema.CONF_KNX_LOCAL_IP: None, + CONF_KNX_ROUTE_BACK: False, + CONF_KNX_LOCAL_IP: None, }, ), ( @@ -209,8 +225,8 @@ async def test_routing_setup_advanced(hass: HomeAssistant) -> None: CONF_HOST: "192.168.0.1", CONF_PORT: 3675, CONF_KNX_INDIVIDUAL_ADDRESS: "15.15.250", - ConnectionSchema.CONF_KNX_ROUTE_BACK: True, - ConnectionSchema.CONF_KNX_LOCAL_IP: None, + CONF_KNX_ROUTE_BACK: True, + CONF_KNX_LOCAL_IP: None, }, ), ], @@ -291,7 +307,7 @@ async def test_tunneling_setup_for_local_ip(hass: HomeAssistant) -> None: CONF_KNX_TUNNELING_TYPE: CONF_KNX_LABEL_TUNNELING_UDP, CONF_HOST: "192.168.0.2", CONF_PORT: 3675, - ConnectionSchema.CONF_KNX_LOCAL_IP: "192.168.1.112", + CONF_KNX_LOCAL_IP: "192.168.1.112", }, ) await hass.async_block_till_done() @@ -303,8 +319,8 @@ async def test_tunneling_setup_for_local_ip(hass: HomeAssistant) -> None: CONF_HOST: "192.168.0.2", CONF_PORT: 3675, CONF_KNX_INDIVIDUAL_ADDRESS: "15.15.250", - ConnectionSchema.CONF_KNX_ROUTE_BACK: False, - ConnectionSchema.CONF_KNX_LOCAL_IP: "192.168.1.112", + CONF_KNX_ROUTE_BACK: False, + CONF_KNX_LOCAL_IP: "192.168.1.112", } assert len(mock_setup_entry.mock_calls) == 1 @@ -361,8 +377,8 @@ async def test_tunneling_setup_for_multiple_found_gateways(hass: HomeAssistant) CONF_HOST: "192.168.0.1", CONF_PORT: 3675, CONF_KNX_INDIVIDUAL_ADDRESS: "15.15.250", - ConnectionSchema.CONF_KNX_ROUTE_BACK: False, - ConnectionSchema.CONF_KNX_LOCAL_IP: None, + CONF_KNX_ROUTE_BACK: False, + CONF_KNX_LOCAL_IP: None, } assert len(mock_setup_entry.mock_calls) == 1 @@ -422,189 +438,182 @@ async def test_form_with_automatic_connection_handling(hass: HomeAssistant) -> N assert len(mock_setup_entry.mock_calls) == 1 -## -# Import Tests -## -async def test_import_config_tunneling(hass: HomeAssistant) -> None: - """Test tunneling import from config.yaml.""" - config = { - CONF_KNX_INDIVIDUAL_ADDRESS: XKNX.DEFAULT_ADDRESS, - ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, # has a default in the original config - ConnectionSchema.CONF_KNX_MCAST_PORT: 3675, # has a default in the original config - ConnectionSchema.CONF_KNX_RATE_LIMIT: 20, # has a default in the original config - ConnectionSchema.CONF_KNX_STATE_UPDATER: True, # has a default in the original config - CONF_KNX_TUNNELING: { - CONF_HOST: "192.168.1.1", - CONF_PORT: 3675, - ConnectionSchema.CONF_KNX_LOCAL_IP: "192.168.1.112", - ConnectionSchema.CONF_KNX_ROUTE_BACK: True, - }, - } - - with patch( - "homeassistant.components.knx.async_setup_entry", - return_value=True, - ) as mock_setup_entry: +async def _get_menu_step(hass: HomeAssistant) -> None: + """Test ip secure manuel.""" + gateway = _gateway_descriptor("192.168.0.1", 3675, True) + with patch("xknx.io.gateway_scanner.GatewayScanner.scan") as gateways: + gateways.return_value = [gateway] result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=config + DOMAIN, context={"source": config_entries.SOURCE_USER} ) - await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY - assert result["title"] == "Tunneling @ 192.168.1.1" - assert result["data"] == { + assert result["type"] == RESULT_TYPE_FORM + assert not result["errors"] + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING, - CONF_HOST: "192.168.1.1", + }, + ) + await hass.async_block_till_done() + assert result2["type"] == RESULT_TYPE_FORM + assert result2["step_id"] == "manual_tunnel" + assert not result2["errors"] + + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + CONF_KNX_TUNNELING_TYPE: CONF_KNX_LABEL_TUNNELING_TCP_SECURE, + CONF_HOST: "192.168.0.1", + CONF_PORT: 3675, + }, + ) + await hass.async_block_till_done() + assert result3["type"] == RESULT_TYPE_MENU + assert result3["step_id"] == "secure_tunneling" + return result3 + + +async def test_configure_secure_manual(hass: HomeAssistant): + """Test configure secure manual.""" + menu_step = await _get_menu_step(hass) + + result = await hass.config_entries.flow.async_configure( + menu_step["flow_id"], + {"next_step_id": "secure_manual"}, + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "secure_manual" + assert not result["errors"] + + with patch( + "homeassistant.components.knx.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + secure_manual = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_KNX_SECURE_USER_ID: 2, + CONF_KNX_SECURE_USER_PASSWORD: "password", + CONF_KNX_SECURE_DEVICE_AUTHENTICATION: "device_auth", + }, + ) + await hass.async_block_till_done() + assert secure_manual["type"] == RESULT_TYPE_CREATE_ENTRY + assert secure_manual["data"] == { + **DEFAULT_ENTRY_DATA, + CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING_TCP_SECURE, + CONF_KNX_SECURE_USER_ID: 2, + CONF_KNX_SECURE_USER_PASSWORD: "password", + CONF_KNX_SECURE_DEVICE_AUTHENTICATION: "device_auth", + CONF_HOST: "192.168.0.1", CONF_PORT: 3675, CONF_KNX_INDIVIDUAL_ADDRESS: "15.15.250", - ConnectionSchema.CONF_KNX_ROUTE_BACK: True, - ConnectionSchema.CONF_KNX_STATE_UPDATER: True, - ConnectionSchema.CONF_KNX_RATE_LIMIT: 20, - ConnectionSchema.CONF_KNX_MCAST_PORT: 3675, - ConnectionSchema.CONF_KNX_LOCAL_IP: "192.168.1.112", - ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, + CONF_KNX_ROUTE_BACK: False, + CONF_KNX_LOCAL_IP: None, } assert len(mock_setup_entry.mock_calls) == 1 -async def test_import_config_routing(hass: HomeAssistant) -> None: - """Test routing import from config.yaml.""" - config = { - CONF_KNX_INDIVIDUAL_ADDRESS: XKNX.DEFAULT_ADDRESS, # has a default in the original config - ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, # has a default in the original config - ConnectionSchema.CONF_KNX_MCAST_PORT: 3675, # has a default in the original config - ConnectionSchema.CONF_KNX_RATE_LIMIT: 20, # has a default in the original config - ConnectionSchema.CONF_KNX_STATE_UPDATER: True, # has a default in the original config - CONF_KNX_ROUTING: {}, # is required when using routing - } +async def test_configure_secure_knxkeys(hass: HomeAssistant): + """Test configure secure knxkeys.""" + menu_step = await _get_menu_step(hass) - with patch( - "homeassistant.components.knx.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=config - ) - await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY - assert result["title"] == CONF_KNX_ROUTING.capitalize() - assert result["data"] == { - CONF_KNX_CONNECTION_TYPE: CONF_KNX_ROUTING, - CONF_KNX_INDIVIDUAL_ADDRESS: "15.15.250", - ConnectionSchema.CONF_KNX_MCAST_PORT: 3675, - ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, - ConnectionSchema.CONF_KNX_STATE_UPDATER: True, - ConnectionSchema.CONF_KNX_RATE_LIMIT: 20, - } - - assert len(mock_setup_entry.mock_calls) == 1 - - -async def test_import_config_automatic(hass: HomeAssistant) -> None: - """Test automatic import from config.yaml.""" - config = { - CONF_KNX_INDIVIDUAL_ADDRESS: XKNX.DEFAULT_ADDRESS, # has a default in the original config - ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, # has a default in the original config - ConnectionSchema.CONF_KNX_MCAST_PORT: 3675, # has a default in the original config - ConnectionSchema.CONF_KNX_RATE_LIMIT: 20, # has a default in the original config - ConnectionSchema.CONF_KNX_STATE_UPDATER: True, # has a default in the original config - } - - with patch( - "homeassistant.components.knx.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=config - ) - await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY - assert result["title"] == CONF_KNX_AUTOMATIC.capitalize() - assert result["data"] == { - CONF_KNX_CONNECTION_TYPE: CONF_KNX_AUTOMATIC, - CONF_KNX_INDIVIDUAL_ADDRESS: "15.15.250", - ConnectionSchema.CONF_KNX_MCAST_PORT: 3675, - ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, - ConnectionSchema.CONF_KNX_STATE_UPDATER: True, - ConnectionSchema.CONF_KNX_RATE_LIMIT: 20, - } - - assert len(mock_setup_entry.mock_calls) == 1 - - -async def test_import_rate_limit_out_of_range(hass: HomeAssistant) -> None: - """Test automatic import from config.yaml.""" - config = { - CONF_KNX_INDIVIDUAL_ADDRESS: XKNX.DEFAULT_ADDRESS, # has a default in the original config - ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, # has a default in the original config - ConnectionSchema.CONF_KNX_MCAST_PORT: 3675, # has a default in the original config - ConnectionSchema.CONF_KNX_RATE_LIMIT: 80, - ConnectionSchema.CONF_KNX_STATE_UPDATER: True, # has a default in the original config - } - - with patch( - "homeassistant.components.knx.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=config - ) - await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY - assert result["title"] == CONF_KNX_AUTOMATIC.capitalize() - assert result["data"] == { - CONF_KNX_CONNECTION_TYPE: CONF_KNX_AUTOMATIC, - CONF_KNX_INDIVIDUAL_ADDRESS: "15.15.250", - ConnectionSchema.CONF_KNX_MCAST_PORT: 3675, - ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, - ConnectionSchema.CONF_KNX_STATE_UPDATER: True, - ConnectionSchema.CONF_KNX_RATE_LIMIT: 60, - } - - assert len(mock_setup_entry.mock_calls) == 1 - - -async def test_import_options(hass: HomeAssistant) -> None: - """Test import from config.yaml with options.""" - config = { - CONF_KNX_INDIVIDUAL_ADDRESS: XKNX.DEFAULT_ADDRESS, # has a default in the original config - ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, # has a default in the original config - ConnectionSchema.CONF_KNX_MCAST_PORT: 3675, # has a default in the original config - ConnectionSchema.CONF_KNX_STATE_UPDATER: False, - ConnectionSchema.CONF_KNX_RATE_LIMIT: 30, - } - - with patch( - "homeassistant.components.knx.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=config - ) - await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY - assert result["title"] == CONF_KNX_AUTOMATIC.capitalize() - assert result["data"] == { - CONF_KNX_CONNECTION_TYPE: CONF_KNX_AUTOMATIC, - CONF_KNX_INDIVIDUAL_ADDRESS: "15.15.250", - ConnectionSchema.CONF_KNX_MCAST_PORT: 3675, - ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, - ConnectionSchema.CONF_KNX_STATE_UPDATER: False, - ConnectionSchema.CONF_KNX_RATE_LIMIT: 30, - } - - assert len(mock_setup_entry.mock_calls) == 1 - - -async def test_abort_if_entry_exists_already(hass: HomeAssistant) -> None: - """Test routing import from config.yaml.""" - MockConfigEntry(domain=DOMAIN).add_to_hass(hass) - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_IMPORT} + result = await hass.config_entries.flow.async_configure( + menu_step["flow_id"], + {"next_step_id": "secure_knxkeys"}, ) - assert result["type"] == "abort" - assert result["reason"] == "single_instance_allowed" + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "secure_knxkeys" + assert not result["errors"] + + with patch( + "homeassistant.components.knx.async_setup_entry", + return_value=True, + ) as mock_setup_entry, patch( + "homeassistant.components.knx.config_flow.load_key_ring", return_value=True + ): + secure_knxkeys = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_KNX_KNXKEY_FILENAME: "testcase.knxkeys", + CONF_KNX_KNXKEY_PASSWORD: "password", + }, + ) + await hass.async_block_till_done() + assert secure_knxkeys["type"] == RESULT_TYPE_CREATE_ENTRY + assert secure_knxkeys["data"] == { + **DEFAULT_ENTRY_DATA, + CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING_TCP_SECURE, + CONF_KNX_KNXKEY_FILENAME: "knx/testcase.knxkeys", + CONF_KNX_KNXKEY_PASSWORD: "password", + CONF_HOST: "192.168.0.1", + CONF_PORT: 3675, + CONF_KNX_INDIVIDUAL_ADDRESS: "15.15.250", + CONF_KNX_ROUTE_BACK: False, + CONF_KNX_LOCAL_IP: None, + } + + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_configure_secure_knxkeys_file_not_found(hass: HomeAssistant): + """Test configure secure knxkeys but file was not found.""" + menu_step = await _get_menu_step(hass) + + result = await hass.config_entries.flow.async_configure( + menu_step["flow_id"], + {"next_step_id": "secure_knxkeys"}, + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "secure_knxkeys" + assert not result["errors"] + + with patch( + "homeassistant.components.knx.config_flow.load_key_ring", + side_effect=FileNotFoundError(), + ): + secure_knxkeys = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_KNX_KNXKEY_FILENAME: "testcase.knxkeys", + CONF_KNX_KNXKEY_PASSWORD: "password", + }, + ) + await hass.async_block_till_done() + assert secure_knxkeys["type"] == RESULT_TYPE_FORM + assert secure_knxkeys["errors"] + assert secure_knxkeys["errors"]["base"] == "file_not_found" + + +async def test_configure_secure_knxkeys_invalid_signature(hass: HomeAssistant): + """Test configure secure knxkeys but file was not found.""" + menu_step = await _get_menu_step(hass) + + result = await hass.config_entries.flow.async_configure( + menu_step["flow_id"], + {"next_step_id": "secure_knxkeys"}, + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "secure_knxkeys" + assert not result["errors"] + + with patch( + "homeassistant.components.knx.config_flow.load_key_ring", + side_effect=InvalidSignature(), + ): + secure_knxkeys = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_KNX_KNXKEY_FILENAME: "testcase.knxkeys", + CONF_KNX_KNXKEY_PASSWORD: "password", + }, + ) + await hass.async_block_till_done() + assert secure_knxkeys["type"] == RESULT_TYPE_FORM + assert secure_knxkeys["errors"] + assert secure_knxkeys["errors"]["base"] == "invalid_signature" async def test_options_flow( @@ -629,8 +638,8 @@ async def test_options_flow( user_input={ CONF_KNX_CONNECTION_TYPE: CONF_KNX_AUTOMATIC, CONF_KNX_INDIVIDUAL_ADDRESS: "15.15.255", - ConnectionSchema.CONF_KNX_MCAST_PORT: 3675, - ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, + CONF_KNX_MCAST_PORT: 3675, + CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, }, ) @@ -642,11 +651,11 @@ async def test_options_flow( CONF_KNX_CONNECTION_TYPE: CONF_KNX_AUTOMATIC, CONF_KNX_INDIVIDUAL_ADDRESS: "15.15.255", CONF_HOST: "", - ConnectionSchema.CONF_KNX_LOCAL_IP: None, - ConnectionSchema.CONF_KNX_MCAST_PORT: 3675, - ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, - ConnectionSchema.CONF_KNX_RATE_LIMIT: 20, - ConnectionSchema.CONF_KNX_STATE_UPDATER: True, + CONF_KNX_LOCAL_IP: None, + CONF_KNX_MCAST_PORT: 3675, + CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, + CONF_KNX_RATE_LIMIT: 20, + CONF_KNX_STATE_UPDATER: True, } @@ -662,14 +671,14 @@ async def test_options_flow( { CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING, CONF_KNX_INDIVIDUAL_ADDRESS: "15.15.255", - ConnectionSchema.CONF_KNX_MCAST_PORT: 3675, - ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, - ConnectionSchema.CONF_KNX_RATE_LIMIT: 20, - ConnectionSchema.CONF_KNX_STATE_UPDATER: True, - ConnectionSchema.CONF_KNX_LOCAL_IP: None, + CONF_KNX_MCAST_PORT: 3675, + CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, + CONF_KNX_RATE_LIMIT: 20, + CONF_KNX_STATE_UPDATER: True, + CONF_KNX_LOCAL_IP: None, CONF_HOST: "192.168.1.1", CONF_PORT: 3675, - ConnectionSchema.CONF_KNX_ROUTE_BACK: True, + CONF_KNX_ROUTE_BACK: True, }, ), ( @@ -681,14 +690,14 @@ async def test_options_flow( { CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING, CONF_KNX_INDIVIDUAL_ADDRESS: "15.15.255", - ConnectionSchema.CONF_KNX_MCAST_PORT: 3675, - ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, - ConnectionSchema.CONF_KNX_RATE_LIMIT: 20, - ConnectionSchema.CONF_KNX_STATE_UPDATER: True, - ConnectionSchema.CONF_KNX_LOCAL_IP: None, + CONF_KNX_MCAST_PORT: 3675, + CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, + CONF_KNX_RATE_LIMIT: 20, + CONF_KNX_STATE_UPDATER: True, + CONF_KNX_LOCAL_IP: None, CONF_HOST: "192.168.1.1", CONF_PORT: 3675, - ConnectionSchema.CONF_KNX_ROUTE_BACK: False, + CONF_KNX_ROUTE_BACK: False, }, ), ( @@ -700,14 +709,14 @@ async def test_options_flow( { CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING_TCP, CONF_KNX_INDIVIDUAL_ADDRESS: "15.15.255", - ConnectionSchema.CONF_KNX_MCAST_PORT: 3675, - ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, - ConnectionSchema.CONF_KNX_RATE_LIMIT: 20, - ConnectionSchema.CONF_KNX_STATE_UPDATER: True, - ConnectionSchema.CONF_KNX_LOCAL_IP: None, + CONF_KNX_MCAST_PORT: 3675, + CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, + CONF_KNX_RATE_LIMIT: 20, + CONF_KNX_STATE_UPDATER: True, + CONF_KNX_LOCAL_IP: None, CONF_HOST: "192.168.1.1", CONF_PORT: 3675, - ConnectionSchema.CONF_KNX_ROUTE_BACK: False, + CONF_KNX_ROUTE_BACK: False, }, ), ], @@ -737,8 +746,8 @@ async def test_tunneling_options_flow( user_input={ CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING, CONF_KNX_INDIVIDUAL_ADDRESS: "15.15.255", - ConnectionSchema.CONF_KNX_MCAST_PORT: 3675, - ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, + CONF_KNX_MCAST_PORT: 3675, + CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, }, ) @@ -765,42 +774,42 @@ async def test_tunneling_options_flow( { CONF_KNX_CONNECTION_TYPE: CONF_KNX_AUTOMATIC, CONF_KNX_INDIVIDUAL_ADDRESS: "15.15.250", - ConnectionSchema.CONF_KNX_MCAST_PORT: 3675, - ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, - ConnectionSchema.CONF_KNX_RATE_LIMIT: 25, - ConnectionSchema.CONF_KNX_STATE_UPDATER: False, - ConnectionSchema.CONF_KNX_LOCAL_IP: "192.168.1.112", + CONF_KNX_MCAST_PORT: 3675, + CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, + CONF_KNX_RATE_LIMIT: 25, + CONF_KNX_STATE_UPDATER: False, + CONF_KNX_LOCAL_IP: "192.168.1.112", }, { CONF_KNX_CONNECTION_TYPE: CONF_KNX_AUTOMATIC, CONF_KNX_INDIVIDUAL_ADDRESS: "15.15.250", CONF_HOST: "", - ConnectionSchema.CONF_KNX_MCAST_PORT: 3675, - ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, - ConnectionSchema.CONF_KNX_RATE_LIMIT: 25, - ConnectionSchema.CONF_KNX_STATE_UPDATER: False, - ConnectionSchema.CONF_KNX_LOCAL_IP: "192.168.1.112", + CONF_KNX_MCAST_PORT: 3675, + CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, + CONF_KNX_RATE_LIMIT: 25, + CONF_KNX_STATE_UPDATER: False, + CONF_KNX_LOCAL_IP: "192.168.1.112", }, ), ( { CONF_KNX_CONNECTION_TYPE: CONF_KNX_AUTOMATIC, CONF_KNX_INDIVIDUAL_ADDRESS: "15.15.250", - ConnectionSchema.CONF_KNX_MCAST_PORT: 3675, - ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, - ConnectionSchema.CONF_KNX_RATE_LIMIT: 25, - ConnectionSchema.CONF_KNX_STATE_UPDATER: False, - ConnectionSchema.CONF_KNX_LOCAL_IP: CONF_DEFAULT_LOCAL_IP, + CONF_KNX_MCAST_PORT: 3675, + CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, + CONF_KNX_RATE_LIMIT: 25, + CONF_KNX_STATE_UPDATER: False, + CONF_KNX_LOCAL_IP: CONF_DEFAULT_LOCAL_IP, }, { CONF_KNX_CONNECTION_TYPE: CONF_KNX_AUTOMATIC, CONF_KNX_INDIVIDUAL_ADDRESS: "15.15.250", CONF_HOST: "", - ConnectionSchema.CONF_KNX_MCAST_PORT: 3675, - ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, - ConnectionSchema.CONF_KNX_RATE_LIMIT: 25, - ConnectionSchema.CONF_KNX_STATE_UPDATER: False, - ConnectionSchema.CONF_KNX_LOCAL_IP: None, + CONF_KNX_MCAST_PORT: 3675, + CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, + CONF_KNX_RATE_LIMIT: 25, + CONF_KNX_STATE_UPDATER: False, + CONF_KNX_LOCAL_IP: None, }, ), ], @@ -843,21 +852,21 @@ async def test_advanced_options( ( { CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING, - ConnectionSchema.CONF_KNX_ROUTE_BACK: False, + CONF_KNX_ROUTE_BACK: False, }, CONF_KNX_LABEL_TUNNELING_UDP, ), ( { CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING, - ConnectionSchema.CONF_KNX_ROUTE_BACK: True, + CONF_KNX_ROUTE_BACK: True, }, CONF_KNX_LABEL_TUNNELING_UDP_ROUTE_BACK, ), ( { CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING_TCP, - ConnectionSchema.CONF_KNX_ROUTE_BACK: False, + CONF_KNX_ROUTE_BACK: False, }, CONF_KNX_LABEL_TUNNELING_TCP, ), diff --git a/tests/components/knx/test_diagnostic.py b/tests/components/knx/test_diagnostic.py index 697ee45ac07..e2d93f17498 100644 --- a/tests/components/knx/test_diagnostic.py +++ b/tests/components/knx/test_diagnostic.py @@ -2,7 +2,24 @@ from unittest.mock import patch from aiohttp import ClientSession +from xknx import XKNX +from xknx.io import DEFAULT_MCAST_GRP, DEFAULT_MCAST_PORT +from homeassistant.components.knx.const import ( + CONF_KNX_AUTOMATIC, + CONF_KNX_CONNECTION_TYPE, + CONF_KNX_DEFAULT_RATE_LIMIT, + CONF_KNX_DEFAULT_STATE_UPDATER, + CONF_KNX_INDIVIDUAL_ADDRESS, + CONF_KNX_KNXKEY_PASSWORD, + CONF_KNX_MCAST_GRP, + CONF_KNX_MCAST_PORT, + CONF_KNX_RATE_LIMIT, + CONF_KNX_SECURE_DEVICE_AUTHENTICATION, + CONF_KNX_SECURE_USER_PASSWORD, + CONF_KNX_STATE_UPDATER, + DOMAIN as KNX_DOMAIN, +) from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry @@ -30,6 +47,8 @@ async def test_diagnostics( "individual_address": "15.15.250", "multicast_group": "224.0.23.12", "multicast_port": 3671, + "rate_limit": 20, + "state_updater": True, }, "configuration_error": None, "configuration_yaml": None, @@ -60,8 +79,56 @@ async def test_diagnostic_config_error( "individual_address": "15.15.250", "multicast_group": "224.0.23.12", "multicast_port": 3671, + "rate_limit": 20, + "state_updater": True, }, "configuration_error": "extra keys not allowed @ data['knx']['wrong_key']", "configuration_yaml": {"wrong_key": {}}, "xknx": {"current_address": "0.0.0", "version": "1.0.0"}, } + + +async def test_diagnostic_redact( + hass: HomeAssistant, + hass_client: ClientSession, +): + """Test diagnostics redacting data.""" + mock_config_entry: MockConfigEntry = MockConfigEntry( + title="KNX", + domain=KNX_DOMAIN, + data={ + CONF_KNX_CONNECTION_TYPE: CONF_KNX_AUTOMATIC, + CONF_KNX_RATE_LIMIT: CONF_KNX_DEFAULT_RATE_LIMIT, + CONF_KNX_STATE_UPDATER: CONF_KNX_DEFAULT_STATE_UPDATER, + CONF_KNX_MCAST_PORT: DEFAULT_MCAST_PORT, + CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, + CONF_KNX_INDIVIDUAL_ADDRESS: XKNX.DEFAULT_ADDRESS, + CONF_KNX_KNXKEY_PASSWORD: "password", + CONF_KNX_SECURE_USER_PASSWORD: "user_password", + CONF_KNX_SECURE_DEVICE_AUTHENTICATION: "device_authentication", + }, + ) + knx: KNXTestKit = KNXTestKit(hass, mock_config_entry) + await knx.setup_integration({}) + + with patch("homeassistant.config.async_hass_config_yaml", return_value={}): + # Overwrite the version for this test since we don't want to change this with every library bump + knx.xknx.version = "1.0.0" + assert await get_diagnostics_for_config_entry( + hass, hass_client, mock_config_entry + ) == { + "config_entry_data": { + "connection_type": "automatic", + "individual_address": "15.15.250", + "multicast_group": "224.0.23.12", + "multicast_port": 3671, + "rate_limit": 20, + "state_updater": True, + "knxkeys_password": "**REDACTED**", + "user_password": "**REDACTED**", + "device_authentication": "**REDACTED**", + }, + "configuration_error": None, + "configuration_yaml": None, + "xknx": {"current_address": "0.0.0", "version": "1.0.0"}, + } diff --git a/tests/components/knx/test_events.py b/tests/components/knx/test_events.py index 360a1963d2d..aff99f02b56 100644 --- a/tests/components/knx/test_events.py +++ b/tests/components/knx/test_events.py @@ -1,11 +1,6 @@ """Test KNX events.""" -from homeassistant.components.knx import ( - CONF_EVENT, - CONF_KNX_EVENT_FILTER, - CONF_TYPE, - KNX_ADDRESS, -) +from homeassistant.components.knx import CONF_EVENT, CONF_TYPE, KNX_ADDRESS from homeassistant.core import HomeAssistant from .conftest import KNXTestKit @@ -25,7 +20,6 @@ async def test_knx_event(hass: HomeAssistant, knx: KNXTestKit): test_address_c_1 = "2/6/4" test_address_c_2 = "2/6/5" test_address_d = "5/4/3" - test_address_e = "6/4/3" events = async_capture_events(hass, "knx_event") async def test_event_data(address, payload, value=None): @@ -63,8 +57,6 @@ async def test_knx_event(hass: HomeAssistant, knx: KNXTestKit): KNX_ADDRESS: [test_address_d], }, ], - # test legacy `event_filter` config - CONF_KNX_EVENT_FILTER: [test_address_e], } ) @@ -97,10 +89,6 @@ async def test_knx_event(hass: HomeAssistant, knx: KNXTestKit): await knx.receive_write(test_address_d, True) await test_event_data(test_address_d, True) - # test legacy `event_filter` config - await knx.receive_write(test_address_e, (89, 43, 34, 11)) - await test_event_data(test_address_e, (89, 43, 34, 11)) - # receive telegrams for group addresses not matching any filter await knx.receive_write("0/5/0", True) await knx.receive_write("1/7/0", True) diff --git a/tests/components/knx/test_init.py b/tests/components/knx/test_init.py index 4380b132cbd..82ddf73f1ea 100644 --- a/tests/components/knx/test_init.py +++ b/tests/components/knx/test_init.py @@ -1,17 +1,38 @@ """Test KNX init.""" import pytest from xknx import XKNX -from xknx.io import ConnectionConfig, ConnectionType +from xknx.io import ( + DEFAULT_MCAST_GRP, + DEFAULT_MCAST_PORT, + ConnectionConfig, + ConnectionType, + SecureConfig, +) from homeassistant.components.knx.const import ( CONF_KNX_AUTOMATIC, CONF_KNX_CONNECTION_TYPE, + CONF_KNX_DEFAULT_RATE_LIMIT, + CONF_KNX_DEFAULT_STATE_UPDATER, CONF_KNX_INDIVIDUAL_ADDRESS, + CONF_KNX_KNXKEY_FILENAME, + CONF_KNX_KNXKEY_PASSWORD, + CONF_KNX_LOCAL_IP, + CONF_KNX_MCAST_GRP, + CONF_KNX_MCAST_PORT, + CONF_KNX_RATE_LIMIT, + CONF_KNX_ROUTE_BACK, CONF_KNX_ROUTING, + CONF_KNX_SECURE_DEVICE_AUTHENTICATION, + CONF_KNX_SECURE_USER_ID, + CONF_KNX_SECURE_USER_PASSWORD, + CONF_KNX_STATE_UPDATER, CONF_KNX_TUNNELING, + CONF_KNX_TUNNELING_TCP, + CONF_KNX_TUNNELING_TCP_SECURE, DOMAIN as KNX_DOMAIN, + KNXConfigEntryData, ) -from homeassistant.components.knx.schema import ConnectionSchema from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.core import HomeAssistant @@ -25,18 +46,29 @@ from tests.common import MockConfigEntry [ ( { - CONF_KNX_INDIVIDUAL_ADDRESS: XKNX.DEFAULT_ADDRESS, CONF_KNX_CONNECTION_TYPE: CONF_KNX_AUTOMATIC, + CONF_KNX_RATE_LIMIT: CONF_KNX_DEFAULT_RATE_LIMIT, + CONF_KNX_STATE_UPDATER: CONF_KNX_DEFAULT_STATE_UPDATER, + CONF_KNX_MCAST_PORT: DEFAULT_MCAST_PORT, + CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, + CONF_KNX_INDIVIDUAL_ADDRESS: XKNX.DEFAULT_ADDRESS, }, - ConnectionConfig(), + ConnectionConfig(threaded=True), ), ( { CONF_KNX_CONNECTION_TYPE: CONF_KNX_ROUTING, - ConnectionSchema.CONF_KNX_LOCAL_IP: "192.168.1.1", + CONF_KNX_LOCAL_IP: "192.168.1.1", + CONF_KNX_RATE_LIMIT: CONF_KNX_DEFAULT_RATE_LIMIT, + CONF_KNX_STATE_UPDATER: CONF_KNX_DEFAULT_STATE_UPDATER, + CONF_KNX_MCAST_PORT: DEFAULT_MCAST_PORT, + CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, + CONF_KNX_INDIVIDUAL_ADDRESS: XKNX.DEFAULT_ADDRESS, }, ConnectionConfig( - connection_type=ConnectionType.ROUTING, local_ip="192.168.1.1" + connection_type=ConnectionType.ROUTING, + local_ip="192.168.1.1", + threaded=True, ), ), ( @@ -44,8 +76,13 @@ from tests.common import MockConfigEntry CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING, CONF_HOST: "192.168.0.2", CONF_PORT: 3675, - ConnectionSchema.CONF_KNX_ROUTE_BACK: False, - ConnectionSchema.CONF_KNX_LOCAL_IP: "192.168.1.112", + CONF_KNX_ROUTE_BACK: False, + CONF_KNX_LOCAL_IP: "192.168.1.112", + CONF_KNX_RATE_LIMIT: CONF_KNX_DEFAULT_RATE_LIMIT, + CONF_KNX_STATE_UPDATER: CONF_KNX_DEFAULT_STATE_UPDATER, + CONF_KNX_MCAST_PORT: DEFAULT_MCAST_PORT, + CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, + CONF_KNX_INDIVIDUAL_ADDRESS: XKNX.DEFAULT_ADDRESS, }, ConnectionConfig( connection_type=ConnectionType.TUNNELING, @@ -54,12 +91,86 @@ from tests.common import MockConfigEntry gateway_port=3675, local_ip="192.168.1.112", auto_reconnect=True, + threaded=True, + ), + ), + ( + { + CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING_TCP, + CONF_HOST: "192.168.0.2", + CONF_PORT: 3675, + CONF_KNX_RATE_LIMIT: CONF_KNX_DEFAULT_RATE_LIMIT, + CONF_KNX_STATE_UPDATER: CONF_KNX_DEFAULT_STATE_UPDATER, + CONF_KNX_MCAST_PORT: DEFAULT_MCAST_PORT, + CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, + CONF_KNX_INDIVIDUAL_ADDRESS: XKNX.DEFAULT_ADDRESS, + }, + ConnectionConfig( + connection_type=ConnectionType.TUNNELING_TCP, + gateway_ip="192.168.0.2", + gateway_port=3675, + auto_reconnect=True, + threaded=True, + ), + ), + ( + { + CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING_TCP_SECURE, + CONF_HOST: "192.168.0.2", + CONF_PORT: 3675, + CONF_KNX_RATE_LIMIT: CONF_KNX_DEFAULT_RATE_LIMIT, + CONF_KNX_STATE_UPDATER: CONF_KNX_DEFAULT_STATE_UPDATER, + CONF_KNX_MCAST_PORT: DEFAULT_MCAST_PORT, + CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, + CONF_KNX_INDIVIDUAL_ADDRESS: XKNX.DEFAULT_ADDRESS, + CONF_KNX_KNXKEY_FILENAME: "knx/testcase.knxkeys", + CONF_KNX_KNXKEY_PASSWORD: "password", + }, + ConnectionConfig( + connection_type=ConnectionType.TUNNELING_TCP_SECURE, + gateway_ip="192.168.0.2", + gateway_port=3675, + secure_config=SecureConfig( + knxkeys_file_path="testcase.knxkeys", knxkeys_password="password" + ), + auto_reconnect=True, + threaded=True, + ), + ), + ( + { + CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING_TCP_SECURE, + CONF_HOST: "192.168.0.2", + CONF_PORT: 3675, + CONF_KNX_RATE_LIMIT: CONF_KNX_DEFAULT_RATE_LIMIT, + CONF_KNX_STATE_UPDATER: CONF_KNX_DEFAULT_STATE_UPDATER, + CONF_KNX_MCAST_PORT: DEFAULT_MCAST_PORT, + CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, + CONF_KNX_INDIVIDUAL_ADDRESS: XKNX.DEFAULT_ADDRESS, + CONF_KNX_SECURE_USER_ID: 2, + CONF_KNX_SECURE_USER_PASSWORD: "password", + CONF_KNX_SECURE_DEVICE_AUTHENTICATION: "device_auth", + }, + ConnectionConfig( + connection_type=ConnectionType.TUNNELING_TCP_SECURE, + gateway_ip="192.168.0.2", + gateway_port=3675, + secure_config=SecureConfig( + device_authentication_password="device_auth", + user_password="password", + user_id=2, + ), + auto_reconnect=True, + threaded=True, ), ), ], ) async def test_init_connection_handling( - hass: HomeAssistant, knx: KNXTestKit, config_entry_data, connection_config + hass: HomeAssistant, + knx: KNXTestKit, + config_entry_data: KNXConfigEntryData, + connection_config: ConnectionConfig, ): """Test correctly generating connection config.""" @@ -73,6 +184,39 @@ async def test_init_connection_handling( assert hass.data.get(KNX_DOMAIN) is not None - assert ( - hass.data[KNX_DOMAIN].connection_config().__dict__ == connection_config.__dict__ + original_connection_config = ( + hass.data[KNX_DOMAIN].connection_config().__dict__.copy() ) + del original_connection_config["secure_config"] + + connection_config_dict = connection_config.__dict__.copy() + del connection_config_dict["secure_config"] + + assert original_connection_config == connection_config_dict + + if connection_config.secure_config is not None: + assert ( + hass.data[KNX_DOMAIN].connection_config().secure_config.knxkeys_password + == connection_config.secure_config.knxkeys_password + ) + assert ( + hass.data[KNX_DOMAIN].connection_config().secure_config.user_password + == connection_config.secure_config.user_password + ) + assert ( + hass.data[KNX_DOMAIN].connection_config().secure_config.user_id + == connection_config.secure_config.user_id + ) + assert ( + hass.data[KNX_DOMAIN] + .connection_config() + .secure_config.device_authentication_password + == connection_config.secure_config.device_authentication_password + ) + if connection_config.secure_config.knxkeys_file_path is not None: + assert ( + connection_config.secure_config.knxkeys_file_path + in hass.data[KNX_DOMAIN] + .connection_config() + .secure_config.knxkeys_file_path + ) diff --git a/tests/components/konnected/test_panel.py b/tests/components/konnected/test_panel.py index bf2da15b7a4..e7e5784ba71 100644 --- a/tests/components/konnected/test_panel.py +++ b/tests/components/konnected/test_panel.py @@ -5,6 +5,7 @@ from unittest.mock import patch import pytest from homeassistant.components.konnected import config_flow, panel +from homeassistant.helpers.entity_component import async_update_entity from homeassistant.setup import async_setup_component from homeassistant.util import utcnow @@ -654,15 +655,11 @@ async def test_connect_retry(hass, mock_panel): # confirm switch is unavailable after second attempt async_fire_time_changed(hass, utcnow() + timedelta(seconds=11)) await hass.async_block_till_done() - await hass.helpers.entity_component.async_update_entity( - "switch.konnected_445566_actuator_6" - ) + await async_update_entity(hass, "switch.konnected_445566_actuator_6") assert hass.states.get("switch.konnected_445566_actuator_6").state == "unavailable" # confirm switch is available after third attempt async_fire_time_changed(hass, utcnow() + timedelta(seconds=21)) await hass.async_block_till_done() - await hass.helpers.entity_component.async_update_entity( - "switch.konnected_445566_actuator_6" - ) + await async_update_entity(hass, "switch.konnected_445566_actuator_6") assert hass.states.get("switch.konnected_445566_actuator_6").state == "off" diff --git a/tests/components/kulersky/test_light.py b/tests/components/kulersky/test_light.py index 3315286967d..fabd263771f 100644 --- a/tests/components/kulersky/test_light.py +++ b/tests/components/kulersky/test_light.py @@ -29,6 +29,7 @@ from homeassistant.const import ( STATE_ON, STATE_UNAVAILABLE, ) +from homeassistant.helpers.entity_component import async_update_entity import homeassistant.util.dt as dt_util from tests.common import MockConfigEntry, async_fire_time_changed @@ -102,7 +103,7 @@ async def test_update_exception(hass, mock_light): """Test platform setup.""" mock_light.get_color.side_effect = pykulersky.PykulerskyException - await hass.helpers.entity_component.async_update_entity("light.bedroom") + await async_update_entity(hass, "light.bedroom") state = hass.states.get("light.bedroom") assert state is not None assert state.state == STATE_UNAVAILABLE diff --git a/tests/components/launch_library/test_config_flow.py b/tests/components/launch_library/test_config_flow.py index 1b8f2bde453..5b6cb85cfee 100644 --- a/tests/components/launch_library/test_config_flow.py +++ b/tests/components/launch_library/test_config_flow.py @@ -3,29 +3,11 @@ from unittest.mock import patch from homeassistant import data_entry_flow from homeassistant.components.launch_library.const import DOMAIN -from homeassistant.components.launch_library.sensor import DEFAULT_NEXT_LAUNCH_NAME -from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER -from homeassistant.const import CONF_NAME +from homeassistant.config_entries import SOURCE_USER from tests.common import MockConfigEntry -async def test_import(hass): - """Test entry will be imported.""" - - imported_config = {CONF_NAME: DEFAULT_NEXT_LAUNCH_NAME} - - with patch( - "homeassistant.components.launch_library.async_setup_entry", return_value=True - ): - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=imported_config - ) - assert result.get("type") == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result.get("result").data == imported_config - - async def test_create_entry(hass): """Test we can finish a config flow.""" diff --git a/tests/components/lcn/fixtures/config.json b/tests/components/lcn/fixtures/config.json index e9278d8e2cd..cc615b6083b 100644 --- a/tests/components/lcn/fixtures/config.json +++ b/tests/components/lcn/fixtures/config.json @@ -77,6 +77,19 @@ "address": "s0.g5", "output": "relay1" } + ], + "covers": [ + { + "name": "Cover_Ouputs", + "address": "s0.m7", + "motor": "outputs", + "reverse_time": "rt1200" + }, + { + "name": "Cover_Relays", + "address": "s0.m7", + "motor": "motor1" + } ] } } diff --git a/tests/components/lcn/fixtures/config_entry_pchk.json b/tests/components/lcn/fixtures/config_entry_pchk.json index 9d34add37ff..620bbb673f5 100644 --- a/tests/components/lcn/fixtures/config_entry_pchk.json +++ b/tests/components/lcn/fixtures/config_entry_pchk.json @@ -100,6 +100,26 @@ "domain_data": { "output": "RELAY1" } + }, + { + "address": [0, 7, false], + "name": "Cover_Outputs", + "resource": "outputs", + "domain": "cover", + "domain_data": { + "motor": "OUTPUTS", + "reverse_time": "RT1200" + } + }, + { + "address": [0, 7, false], + "name": "Cover_Relays", + "resource": "motor1", + "domain": "cover", + "domain_data": { + "motor": "MOTOR1", + "reverse_time": "RT1200" + } } ] } diff --git a/tests/components/lcn/test_cover.py b/tests/components/lcn/test_cover.py new file mode 100644 index 00000000000..f89cfa41071 --- /dev/null +++ b/tests/components/lcn/test_cover.py @@ -0,0 +1,390 @@ +"""Test for the LCN cover platform.""" +from unittest.mock import patch + +from pypck.inputs import ModStatusOutput, ModStatusRelays +from pypck.lcn_addr import LcnAddr +from pypck.lcn_defs import MotorReverseTime, MotorStateModifier + +from homeassistant.components.cover import DOMAIN as DOMAIN_COVER +from homeassistant.components.lcn.helpers import get_device_connection +from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_CLOSE_COVER, + SERVICE_OPEN_COVER, + SERVICE_STOP_COVER, + STATE_CLOSED, + STATE_CLOSING, + STATE_OPEN, + STATE_OPENING, + STATE_UNAVAILABLE, +) + +from .conftest import MockModuleConnection + + +async def test_setup_lcn_cover(hass, entry, lcn_connection): + """Test the setup of cover.""" + for entity_id in ( + "cover.cover_outputs", + "cover.cover_relays", + ): + state = hass.states.get(entity_id) + assert state is not None + assert state.state == STATE_OPEN + + +async def test_entity_attributes(hass, entry, lcn_connection): + """Test the attributes of an entity.""" + entity_registry = await hass.helpers.entity_registry.async_get_registry() + + entity_outputs = entity_registry.async_get("cover.cover_outputs") + + assert entity_outputs + assert entity_outputs.unique_id == f"{entry.entry_id}-m000007-outputs" + assert entity_outputs.original_name == "Cover_Outputs" + + entity_relays = entity_registry.async_get("cover.cover_relays") + + assert entity_relays + assert entity_relays.unique_id == f"{entry.entry_id}-m000007-motor1" + assert entity_relays.original_name == "Cover_Relays" + + +@patch.object(MockModuleConnection, "control_motors_outputs") +async def test_outputs_open(control_motors_outputs, hass, lcn_connection): + """Test the outputs cover opens.""" + state = hass.states.get("cover.cover_outputs") + state.state = STATE_CLOSED + + # command failed + control_motors_outputs.return_value = False + + await hass.services.async_call( + DOMAIN_COVER, + SERVICE_OPEN_COVER, + {ATTR_ENTITY_ID: "cover.cover_outputs"}, + blocking=True, + ) + await hass.async_block_till_done() + control_motors_outputs.assert_awaited_with( + MotorStateModifier.UP, MotorReverseTime.RT1200 + ) + + state = hass.states.get("cover.cover_outputs") + assert state is not None + assert state.state != STATE_OPENING + + # command success + control_motors_outputs.reset_mock(return_value=True) + control_motors_outputs.return_value = True + + await hass.services.async_call( + DOMAIN_COVER, + SERVICE_OPEN_COVER, + {ATTR_ENTITY_ID: "cover.cover_outputs"}, + blocking=True, + ) + await hass.async_block_till_done() + control_motors_outputs.assert_awaited_with( + MotorStateModifier.UP, MotorReverseTime.RT1200 + ) + + state = hass.states.get("cover.cover_outputs") + assert state is not None + assert state.state == STATE_OPENING + + +@patch.object(MockModuleConnection, "control_motors_outputs") +async def test_outputs_close(control_motors_outputs, hass, lcn_connection): + """Test the outputs cover closes.""" + state = hass.states.get("cover.cover_outputs") + state.state = STATE_OPEN + + # command failed + control_motors_outputs.return_value = False + + await hass.services.async_call( + DOMAIN_COVER, + SERVICE_CLOSE_COVER, + {ATTR_ENTITY_ID: "cover.cover_outputs"}, + blocking=True, + ) + await hass.async_block_till_done() + control_motors_outputs.assert_awaited_with( + MotorStateModifier.DOWN, MotorReverseTime.RT1200 + ) + + state = hass.states.get("cover.cover_outputs") + assert state is not None + assert state.state != STATE_CLOSING + + # command success + control_motors_outputs.reset_mock(return_value=True) + control_motors_outputs.return_value = True + + await hass.services.async_call( + DOMAIN_COVER, + SERVICE_CLOSE_COVER, + {ATTR_ENTITY_ID: "cover.cover_outputs"}, + blocking=True, + ) + await hass.async_block_till_done() + control_motors_outputs.assert_awaited_with( + MotorStateModifier.DOWN, MotorReverseTime.RT1200 + ) + + state = hass.states.get("cover.cover_outputs") + assert state is not None + assert state.state == STATE_CLOSING + + +@patch.object(MockModuleConnection, "control_motors_outputs") +async def test_outputs_stop(control_motors_outputs, hass, lcn_connection): + """Test the outputs cover stops.""" + state = hass.states.get("cover.cover_outputs") + state.state = STATE_CLOSING + + # command failed + control_motors_outputs.return_value = False + + await hass.services.async_call( + DOMAIN_COVER, + SERVICE_STOP_COVER, + {ATTR_ENTITY_ID: "cover.cover_outputs"}, + blocking=True, + ) + await hass.async_block_till_done() + control_motors_outputs.assert_awaited_with(MotorStateModifier.STOP) + + state = hass.states.get("cover.cover_outputs") + assert state is not None + assert state.state == STATE_CLOSING + + # command success + control_motors_outputs.reset_mock(return_value=True) + control_motors_outputs.return_value = True + + await hass.services.async_call( + DOMAIN_COVER, + SERVICE_STOP_COVER, + {ATTR_ENTITY_ID: "cover.cover_outputs"}, + blocking=True, + ) + await hass.async_block_till_done() + control_motors_outputs.assert_awaited_with(MotorStateModifier.STOP) + + state = hass.states.get("cover.cover_outputs") + assert state is not None + assert state.state not in (STATE_CLOSING, STATE_OPENING) + + +@patch.object(MockModuleConnection, "control_motors_relays") +async def test_relays_open(control_motors_relays, hass, lcn_connection): + """Test the relays cover opens.""" + states = [MotorStateModifier.NOCHANGE] * 4 + states[0] = MotorStateModifier.UP + + state = hass.states.get("cover.cover_relays") + state.state = STATE_CLOSED + + # command failed + control_motors_relays.return_value = False + + await hass.services.async_call( + DOMAIN_COVER, + SERVICE_OPEN_COVER, + {ATTR_ENTITY_ID: "cover.cover_relays"}, + blocking=True, + ) + await hass.async_block_till_done() + control_motors_relays.assert_awaited_with(states) + + state = hass.states.get("cover.cover_relays") + assert state is not None + assert state.state != STATE_OPENING + + # command success + control_motors_relays.reset_mock(return_value=True) + control_motors_relays.return_value = True + + await hass.services.async_call( + DOMAIN_COVER, + SERVICE_OPEN_COVER, + {ATTR_ENTITY_ID: "cover.cover_relays"}, + blocking=True, + ) + await hass.async_block_till_done() + control_motors_relays.assert_awaited_with(states) + + state = hass.states.get("cover.cover_relays") + assert state is not None + assert state.state == STATE_OPENING + + +@patch.object(MockModuleConnection, "control_motors_relays") +async def test_relays_close(control_motors_relays, hass, lcn_connection): + """Test the relays cover closes.""" + states = [MotorStateModifier.NOCHANGE] * 4 + states[0] = MotorStateModifier.DOWN + + state = hass.states.get("cover.cover_relays") + state.state = STATE_OPEN + + # command failed + control_motors_relays.return_value = False + + await hass.services.async_call( + DOMAIN_COVER, + SERVICE_CLOSE_COVER, + {ATTR_ENTITY_ID: "cover.cover_relays"}, + blocking=True, + ) + await hass.async_block_till_done() + control_motors_relays.assert_awaited_with(states) + + state = hass.states.get("cover.cover_relays") + assert state is not None + assert state.state != STATE_CLOSING + + # command success + control_motors_relays.reset_mock(return_value=True) + control_motors_relays.return_value = True + + await hass.services.async_call( + DOMAIN_COVER, + SERVICE_CLOSE_COVER, + {ATTR_ENTITY_ID: "cover.cover_relays"}, + blocking=True, + ) + await hass.async_block_till_done() + control_motors_relays.assert_awaited_with(states) + + state = hass.states.get("cover.cover_relays") + assert state is not None + assert state.state == STATE_CLOSING + + +@patch.object(MockModuleConnection, "control_motors_relays") +async def test_relays_stop(control_motors_relays, hass, lcn_connection): + """Test the relays cover stops.""" + states = [MotorStateModifier.NOCHANGE] * 4 + states[0] = MotorStateModifier.STOP + + state = hass.states.get("cover.cover_relays") + state.state = STATE_CLOSING + + # command failed + control_motors_relays.return_value = False + + await hass.services.async_call( + DOMAIN_COVER, + SERVICE_STOP_COVER, + {ATTR_ENTITY_ID: "cover.cover_relays"}, + blocking=True, + ) + await hass.async_block_till_done() + control_motors_relays.assert_awaited_with(states) + + state = hass.states.get("cover.cover_relays") + assert state is not None + assert state.state == STATE_CLOSING + + # command success + control_motors_relays.reset_mock(return_value=True) + control_motors_relays.return_value = True + + await hass.services.async_call( + DOMAIN_COVER, + SERVICE_STOP_COVER, + {ATTR_ENTITY_ID: "cover.cover_relays"}, + blocking=True, + ) + await hass.async_block_till_done() + control_motors_relays.assert_awaited_with(states) + + state = hass.states.get("cover.cover_relays") + assert state is not None + assert state.state not in (STATE_CLOSING, STATE_OPENING) + + +async def test_pushed_outputs_status_change(hass, entry, lcn_connection): + """Test the outputs cover changes its state on status received.""" + device_connection = get_device_connection(hass, (0, 7, False), entry) + address = LcnAddr(0, 7, False) + + state = hass.states.get("cover.cover_outputs") + state.state = STATE_CLOSED + + # push status "open" + input = ModStatusOutput(address, 0, 100) + await device_connection.async_process_input(input) + await hass.async_block_till_done() + + state = hass.states.get("cover.cover_outputs") + assert state is not None + assert state.state == STATE_OPENING + + # push status "stop" + input = ModStatusOutput(address, 0, 0) + await device_connection.async_process_input(input) + await hass.async_block_till_done() + + state = hass.states.get("cover.cover_outputs") + assert state is not None + assert state.state not in (STATE_OPENING, STATE_CLOSING) + + # push status "close" + input = ModStatusOutput(address, 1, 100) + await device_connection.async_process_input(input) + await hass.async_block_till_done() + + state = hass.states.get("cover.cover_outputs") + assert state is not None + assert state.state == STATE_CLOSING + + +async def test_pushed_relays_status_change(hass, entry, lcn_connection): + """Test the relays cover changes its state on status received.""" + device_connection = get_device_connection(hass, (0, 7, False), entry) + address = LcnAddr(0, 7, False) + states = [False] * 8 + + state = hass.states.get("cover.cover_relays") + state.state = STATE_CLOSED + + # push status "open" + states[0:2] = [True, False] + input = ModStatusRelays(address, states) + await device_connection.async_process_input(input) + await hass.async_block_till_done() + + state = hass.states.get("cover.cover_relays") + assert state is not None + assert state.state == STATE_OPENING + + # push status "stop" + states[0] = False + input = ModStatusRelays(address, states) + await device_connection.async_process_input(input) + await hass.async_block_till_done() + + state = hass.states.get("cover.cover_relays") + assert state is not None + assert state.state not in (STATE_OPENING, STATE_CLOSING) + + # push status "close" + states[0:2] = [True, True] + input = ModStatusRelays(address, states) + await device_connection.async_process_input(input) + await hass.async_block_till_done() + + state = hass.states.get("cover.cover_relays") + assert state is not None + assert state.state == STATE_CLOSING + + +async def test_unload_config_entry(hass, entry, lcn_connection): + """Test the cover is removed when the config entry is unloaded.""" + await hass.config_entries.async_unload(entry.entry_id) + assert hass.states.get("cover.cover_outputs").state == STATE_UNAVAILABLE + assert hass.states.get("cover.cover_relays").state == STATE_UNAVAILABLE diff --git a/tests/components/light/test_recorder.py b/tests/components/light/test_recorder.py new file mode 100644 index 00000000000..b8e35788562 --- /dev/null +++ b/tests/components/light/test_recorder.py @@ -0,0 +1,51 @@ +"""The tests for light recorder.""" +from __future__ import annotations + +from datetime import timedelta + +from homeassistant.components import light +from homeassistant.components.light import ( + ATTR_EFFECT, + ATTR_MAX_MIREDS, + ATTR_MIN_MIREDS, + ATTR_SUPPORTED_COLOR_MODES, +) +from homeassistant.components.recorder.models import StateAttributes, States +from homeassistant.components.recorder.util import session_scope +from homeassistant.const import ATTR_FRIENDLY_NAME +from homeassistant.core import State +from homeassistant.setup import async_setup_component +from homeassistant.util import dt as dt_util + +from tests.common import async_fire_time_changed, async_init_recorder_component +from tests.components.recorder.common import async_wait_recording_done_without_instance + + +async def test_exclude_attributes(hass): + """Test light registered attributes to be excluded.""" + await async_init_recorder_component(hass) + await async_setup_component( + hass, light.DOMAIN, {light.DOMAIN: {"platform": "demo"}} + ) + await hass.async_block_till_done() + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5)) + await hass.async_block_till_done() + await async_wait_recording_done_without_instance(hass) + + def _fetch_states() -> list[State]: + with session_scope(hass=hass) as session: + native_states = [] + for db_state, db_state_attributes in session.query(States, StateAttributes): + state = db_state.to_native() + state.attributes = db_state_attributes.to_native() + native_states.append(state) + return native_states + + states: list[State] = await hass.async_add_executor_job(_fetch_states) + assert len(states) > 1 + for state in states: + assert ATTR_MIN_MIREDS not in state.attributes + assert ATTR_MAX_MIREDS not in state.attributes + assert ATTR_SUPPORTED_COLOR_MODES not in state.attributes + assert ATTR_EFFECT not in state.attributes + assert ATTR_FRIENDLY_NAME in state.attributes diff --git a/tests/components/light/test_reproduce_state.py b/tests/components/light/test_reproduce_state.py index 97d969acdd9..75e917828c2 100644 --- a/tests/components/light/test_reproduce_state.py +++ b/tests/components/light/test_reproduce_state.py @@ -4,6 +4,7 @@ import pytest from homeassistant.components import light from homeassistant.components.light.reproduce_state import DEPRECATION_WARNING from homeassistant.core import State +from homeassistant.helpers.state import async_reproduce_state from tests.common import async_mock_service @@ -43,7 +44,8 @@ async def test_reproducing_states(hass, caplog): turn_off_calls = async_mock_service(hass, "light", "turn_off") # These calls should do nothing as entities already in desired state - await hass.helpers.state.async_reproduce_state( + await async_reproduce_state( + hass, [ State("light.entity_off", "off"), State("light.entity_bright", "on", VALID_BRIGHTNESS), @@ -58,23 +60,22 @@ async def test_reproducing_states(hass, caplog): State("light.entity_profile", "on", VALID_PROFILE), State("light.entity_rgb", "on", VALID_RGB_COLOR), State("light.entity_xy", "on", VALID_XY_COLOR), - ] + ], ) assert len(turn_on_calls) == 0 assert len(turn_off_calls) == 0 # Test invalid state is handled - await hass.helpers.state.async_reproduce_state( - [State("light.entity_off", "not_supported")] - ) + await async_reproduce_state(hass, [State("light.entity_off", "not_supported")]) assert "not_supported" in caplog.text assert len(turn_on_calls) == 0 assert len(turn_off_calls) == 0 # Make sure correct services are called - await hass.helpers.state.async_reproduce_state( + await async_reproduce_state( + hass, [ State("light.entity_xy", "off"), State("light.entity_off", "on", VALID_BRIGHTNESS), @@ -194,8 +195,8 @@ async def test_filter_color_modes(hass, caplog, color_mode): turn_on_calls = async_mock_service(hass, "light", "turn_on") - await hass.helpers.state.async_reproduce_state( - [State("light.entity", "on", {**all_colors, "color_mode": color_mode})] + await async_reproduce_state( + hass, [State("light.entity", "on", {**all_colors, "color_mode": color_mode})] ) expected_map = { @@ -225,8 +226,8 @@ async def test_filter_color_modes(hass, caplog, color_mode): # This should do nothing, the light is already in the desired state hass.states.async_set("light.entity", "on", {"color_mode": color_mode, **expected}) - await hass.helpers.state.async_reproduce_state( - [State("light.entity", "on", {**expected, "color_mode": color_mode})] + await async_reproduce_state( + hass, [State("light.entity", "on", {**expected, "color_mode": color_mode})] ) assert len(turn_on_calls) == 1 @@ -235,8 +236,8 @@ async def test_deprecation_warning(hass, caplog): """Test deprecation warning.""" hass.states.async_set("light.entity_off", "off", {}) turn_on_calls = async_mock_service(hass, "light", "turn_on") - await hass.helpers.state.async_reproduce_state( - [State("light.entity_off", "on", {"brightness_pct": 80})] + await async_reproduce_state( + hass, [State("light.entity_off", "on", {"brightness_pct": 80})] ) assert len(turn_on_calls) == 1 assert DEPRECATION_WARNING % ["brightness_pct"] in caplog.text diff --git a/tests/components/lock/test_reproduce_state.py b/tests/components/lock/test_reproduce_state.py index 8c08e9f6b10..bcbdc8db33b 100644 --- a/tests/components/lock/test_reproduce_state.py +++ b/tests/components/lock/test_reproduce_state.py @@ -1,5 +1,6 @@ """Test reproduce state for Lock.""" from homeassistant.core import State +from homeassistant.helpers.state import async_reproduce_state from tests.common import async_mock_service @@ -13,7 +14,8 @@ async def test_reproducing_states(hass, caplog): unlock_calls = async_mock_service(hass, "lock", "unlock") # These calls should do nothing as entities already in desired state - await hass.helpers.state.async_reproduce_state( + await async_reproduce_state( + hass, [ State("lock.entity_locked", "locked"), State("lock.entity_unlocked", "unlocked", {}), @@ -24,16 +26,15 @@ async def test_reproducing_states(hass, caplog): assert len(unlock_calls) == 0 # Test invalid state is handled - await hass.helpers.state.async_reproduce_state( - [State("lock.entity_locked", "not_supported")] - ) + await async_reproduce_state(hass, [State("lock.entity_locked", "not_supported")]) assert "not_supported" in caplog.text assert len(lock_calls) == 0 assert len(unlock_calls) == 0 # Make sure correct services are called - await hass.helpers.state.async_reproduce_state( + await async_reproduce_state( + hass, [ State("lock.entity_locked", "unlocked"), State("lock.entity_unlocked", "locked"), diff --git a/tests/components/logbook/test_init.py b/tests/components/logbook/test_init.py index fab1121542f..998efc1c167 100644 --- a/tests/components/logbook/test_init.py +++ b/tests/components/logbook/test_init.py @@ -43,7 +43,11 @@ from tests.common import ( async_init_recorder_component, mock_platform, ) -from tests.components.recorder.common import trigger_db_commit +from tests.components.recorder.common import ( + async_trigger_db_commit, + async_wait_recording_done_without_instance, + trigger_db_commit, +) EMPTY_CONFIG = logbook.CONFIG_SCHEMA({logbook.DOMAIN: {}}) @@ -56,6 +60,12 @@ async def hass_(hass): return hass +@pytest.fixture() +def set_utc(hass): + """Set timezone to UTC.""" + hass.config.set_time_zone("UTC") + + async def test_service_call_create_logbook_entry(hass_): """Test if service call create log book entry.""" calls = async_capture_events(hass_, logbook.EVENT_LOGBOOK_ENTRY) @@ -280,12 +290,14 @@ def create_state_changed_event_from_old_new( "attributes" "state_id", "old_state_id", + "shared_attrs", ], ) row.event_type = EVENT_STATE_CHANGED row.event_data = "{}" row.attributes = attributes_json + row.shared_attrs = attributes_json row.time_fired = event_time_fired row.state = new_state and new_state.get("state") row.entity_id = entity_id @@ -308,7 +320,7 @@ async def test_logbook_view(hass, hass_client): assert response.status == HTTPStatus.OK -async def test_logbook_view_period_entity(hass, hass_client): +async def test_logbook_view_period_entity(hass, hass_client, set_utc): """Test the logbook view with period and entity.""" await async_init_recorder_component(hass) await async_setup_component(hass, "logbook", {}) @@ -636,7 +648,45 @@ async def test_logbook_entity_filter_with_automations(hass, hass_client): assert json_dict[0]["entity_id"] == entity_id_second -async def test_filter_continuous_sensor_values(hass, hass_client): +async def test_logbook_entity_no_longer_in_state_machine(hass, hass_client): + """Test the logbook view with an entity that hass been removed from the state machine.""" + await async_init_recorder_component(hass) + await async_setup_component(hass, "logbook", {}) + await async_setup_component(hass, "automation", {}) + await async_setup_component(hass, "script", {}) + + await async_wait_recording_done_without_instance(hass) + + entity_id_test = "alarm_control_panel.area_001" + hass.states.async_set( + entity_id_test, STATE_OFF, {ATTR_FRIENDLY_NAME: "Alarm Control Panel"} + ) + hass.states.async_set( + entity_id_test, STATE_ON, {ATTR_FRIENDLY_NAME: "Alarm Control Panel"} + ) + + async_trigger_db_commit(hass) + await async_wait_recording_done_without_instance(hass) + + hass.states.async_remove(entity_id_test) + + client = await hass_client() + + # Today time 00:00:00 + start = dt_util.utcnow().date() + start_date = datetime(start.year, start.month, start.day) + + # Test today entries with filter by end_time + end_time = start + timedelta(hours=24) + response = await client.get( + f"/api/logbook/{start_date.isoformat()}?end_time={end_time}" + ) + assert response.status == HTTPStatus.OK + json_dict = await response.json() + assert json_dict[0]["name"] == "Alarm Control Panel" + + +async def test_filter_continuous_sensor_values(hass, hass_client, set_utc): """Test remove continuous sensor events from logbook.""" await async_init_recorder_component(hass) await async_setup_component(hass, "logbook", {}) @@ -672,7 +722,7 @@ async def test_filter_continuous_sensor_values(hass, hass_client): assert response_json[1]["entity_id"] == entity_id_third -async def test_exclude_new_entities(hass, hass_client): +async def test_exclude_new_entities(hass, hass_client, set_utc): """Test if events are excluded on first update.""" await async_init_recorder_component(hass) await async_setup_component(hass, "logbook", {}) @@ -707,7 +757,7 @@ async def test_exclude_new_entities(hass, hass_client): assert response_json[1]["message"] == "started" -async def test_exclude_removed_entities(hass, hass_client): +async def test_exclude_removed_entities(hass, hass_client, set_utc): """Test if events are excluded on last update.""" await async_init_recorder_component(hass) await async_setup_component(hass, "logbook", {}) @@ -749,7 +799,7 @@ async def test_exclude_removed_entities(hass, hass_client): assert response_json[2]["entity_id"] == entity_id2 -async def test_exclude_attribute_changes(hass, hass_client): +async def test_exclude_attribute_changes(hass, hass_client, set_utc): """Test if events of attribute changes are filtered.""" await async_init_recorder_component(hass) await async_setup_component(hass, "logbook", {}) diff --git a/tests/components/lovelace/test_cast.py b/tests/components/lovelace/test_cast.py index d5b8e43d2bb..6f6035b54b6 100644 --- a/tests/components/lovelace/test_cast.py +++ b/tests/components/lovelace/test_cast.py @@ -70,7 +70,7 @@ async def test_root_object(hass): ) assert len(root) == 1 item = root[0] - assert item.title == "Lovelace" + assert item.title == "Dashboards" assert item.media_class == lovelace_cast.MEDIA_CLASS_APP assert item.media_content_id == "" assert item.media_content_type == lovelace_cast.DOMAIN diff --git a/tests/components/mazda/fixtures/diagnostics_config_entry.json b/tests/components/mazda/fixtures/diagnostics_config_entry.json index 445ae141cb2..87f49bc29cb 100644 --- a/tests/components/mazda/fixtures/diagnostics_config_entry.json +++ b/tests/components/mazda/fixtures/diagnostics_config_entry.json @@ -1,62 +1,62 @@ { - "info": { - "email": "**REDACTED**", - "password": "**REDACTED**", - "region": "MNAO" - }, - "data": [ - { - "vin": "**REDACTED**", - "id": "**REDACTED**", - "nickname": "My Mazda3", - "carlineCode": "M3S", - "carlineName": "MAZDA3 2.5 S SE AWD", - "modelYear": "2021", - "modelCode": "M3S SE XA", - "modelName": "W/ SELECT PKG AWD SDN", - "automaticTransmission": true, - "interiorColorCode": "BY3", - "interiorColorName": "BLACK", - "exteriorColorCode": "42M", - "exteriorColorName": "DEEP CRYSTAL BLUE MICA", - "isElectric": false, - "status": { - "lastUpdatedTimestamp": "20210123143809", - "latitude": "**REDACTED**", - "longitude": "**REDACTED**", - "positionTimestamp": "20210123143808", - "fuelRemainingPercent": 87.0, - "fuelDistanceRemainingKm": 380.8, - "odometerKm": 2795.8, - "doors": { - "driverDoorOpen": false, - "passengerDoorOpen": false, - "rearLeftDoorOpen": false, - "rearRightDoorOpen": false, - "trunkOpen": false, - "hoodOpen": false, - "fuelLidOpen": false - }, - "doorLocks": { - "driverDoorUnlocked": false, - "passengerDoorUnlocked": false, - "rearLeftDoorUnlocked": false, - "rearRightDoorUnlocked": false - }, - "windows": { - "driverWindowOpen": false, - "passengerWindowOpen": false, - "rearLeftWindowOpen": false, - "rearRightWindowOpen": false - }, - "hazardLightsOn": false, - "tirePressure": { - "frontLeftTirePressurePsi": 35.0, - "frontRightTirePressurePsi": 35.0, - "rearLeftTirePressurePsi": 33.0, - "rearRightTirePressurePsi": 33.0 - } - } + "info": { + "email": "**REDACTED**", + "password": "**REDACTED**", + "region": "MNAO" + }, + "data": [ + { + "vin": "**REDACTED**", + "id": "**REDACTED**", + "nickname": "My Mazda3", + "carlineCode": "M3S", + "carlineName": "MAZDA3 2.5 S SE AWD", + "modelYear": "2021", + "modelCode": "M3S SE XA", + "modelName": "W/ SELECT PKG AWD SDN", + "automaticTransmission": true, + "interiorColorCode": "BY3", + "interiorColorName": "BLACK", + "exteriorColorCode": "42M", + "exteriorColorName": "DEEP CRYSTAL BLUE MICA", + "isElectric": false, + "status": { + "lastUpdatedTimestamp": "20210123143809", + "latitude": "**REDACTED**", + "longitude": "**REDACTED**", + "positionTimestamp": "20210123143808", + "fuelRemainingPercent": 87.0, + "fuelDistanceRemainingKm": 380.8, + "odometerKm": 2795.8, + "doors": { + "driverDoorOpen": false, + "passengerDoorOpen": true, + "rearLeftDoorOpen": false, + "rearRightDoorOpen": false, + "trunkOpen": false, + "hoodOpen": true, + "fuelLidOpen": false + }, + "doorLocks": { + "driverDoorUnlocked": false, + "passengerDoorUnlocked": false, + "rearLeftDoorUnlocked": false, + "rearRightDoorUnlocked": false + }, + "windows": { + "driverWindowOpen": false, + "passengerWindowOpen": false, + "rearLeftWindowOpen": false, + "rearRightWindowOpen": false + }, + "hazardLightsOn": false, + "tirePressure": { + "frontLeftTirePressurePsi": 35.0, + "frontRightTirePressurePsi": 35.0, + "rearLeftTirePressurePsi": 33.0, + "rearRightTirePressurePsi": 33.0 } - ] + } + } + ] } diff --git a/tests/components/mazda/fixtures/diagnostics_device.json b/tests/components/mazda/fixtures/diagnostics_device.json index 0b2fa8550ac..f2ddd658f70 100644 --- a/tests/components/mazda/fixtures/diagnostics_device.json +++ b/tests/components/mazda/fixtures/diagnostics_device.json @@ -1,60 +1,60 @@ { - "info": { - "email": "**REDACTED**", - "password": "**REDACTED**", - "region": "MNAO" - }, - "data": { - "vin": "**REDACTED**", - "id": "**REDACTED**", - "nickname": "My Mazda3", - "carlineCode": "M3S", - "carlineName": "MAZDA3 2.5 S SE AWD", - "modelYear": "2021", - "modelCode": "M3S SE XA", - "modelName": "W/ SELECT PKG AWD SDN", - "automaticTransmission": true, - "interiorColorCode": "BY3", - "interiorColorName": "BLACK", - "exteriorColorCode": "42M", - "exteriorColorName": "DEEP CRYSTAL BLUE MICA", - "isElectric": false, - "status": { - "lastUpdatedTimestamp": "20210123143809", - "latitude": "**REDACTED**", - "longitude": "**REDACTED**", - "positionTimestamp": "20210123143808", - "fuelRemainingPercent": 87.0, - "fuelDistanceRemainingKm": 380.8, - "odometerKm": 2795.8, - "doors": { - "driverDoorOpen": false, - "passengerDoorOpen": false, - "rearLeftDoorOpen": false, - "rearRightDoorOpen": false, - "trunkOpen": false, - "hoodOpen": false, - "fuelLidOpen": false - }, - "doorLocks": { - "driverDoorUnlocked": false, - "passengerDoorUnlocked": false, - "rearLeftDoorUnlocked": false, - "rearRightDoorUnlocked": false - }, - "windows": { - "driverWindowOpen": false, - "passengerWindowOpen": false, - "rearLeftWindowOpen": false, - "rearRightWindowOpen": false - }, - "hazardLightsOn": false, - "tirePressure": { - "frontLeftTirePressurePsi": 35.0, - "frontRightTirePressurePsi": 35.0, - "rearLeftTirePressurePsi": 33.0, - "rearRightTirePressurePsi": 33.0 - } - } + "info": { + "email": "**REDACTED**", + "password": "**REDACTED**", + "region": "MNAO" + }, + "data": { + "vin": "**REDACTED**", + "id": "**REDACTED**", + "nickname": "My Mazda3", + "carlineCode": "M3S", + "carlineName": "MAZDA3 2.5 S SE AWD", + "modelYear": "2021", + "modelCode": "M3S SE XA", + "modelName": "W/ SELECT PKG AWD SDN", + "automaticTransmission": true, + "interiorColorCode": "BY3", + "interiorColorName": "BLACK", + "exteriorColorCode": "42M", + "exteriorColorName": "DEEP CRYSTAL BLUE MICA", + "isElectric": false, + "status": { + "lastUpdatedTimestamp": "20210123143809", + "latitude": "**REDACTED**", + "longitude": "**REDACTED**", + "positionTimestamp": "20210123143808", + "fuelRemainingPercent": 87.0, + "fuelDistanceRemainingKm": 380.8, + "odometerKm": 2795.8, + "doors": { + "driverDoorOpen": false, + "passengerDoorOpen": true, + "rearLeftDoorOpen": false, + "rearRightDoorOpen": false, + "trunkOpen": false, + "hoodOpen": true, + "fuelLidOpen": false + }, + "doorLocks": { + "driverDoorUnlocked": false, + "passengerDoorUnlocked": false, + "rearLeftDoorUnlocked": false, + "rearRightDoorUnlocked": false + }, + "windows": { + "driverWindowOpen": false, + "passengerWindowOpen": false, + "rearLeftWindowOpen": false, + "rearRightWindowOpen": false + }, + "hazardLightsOn": false, + "tirePressure": { + "frontLeftTirePressurePsi": 35.0, + "frontRightTirePressurePsi": 35.0, + "rearLeftTirePressurePsi": 33.0, + "rearRightTirePressurePsi": 33.0 + } } + } } diff --git a/tests/components/mazda/fixtures/get_ev_vehicle_status.json b/tests/components/mazda/fixtures/get_ev_vehicle_status.json index 6aeaa1ebda0..ee9825fcbe0 100644 --- a/tests/components/mazda/fixtures/get_ev_vehicle_status.json +++ b/tests/components/mazda/fixtures/get_ev_vehicle_status.json @@ -1,19 +1,19 @@ { - "chargeInfo": { - "lastUpdatedTimestamp": "20210807083956", - "batteryLevelPercentage": 80, - "drivingRangeKm": 218, - "pluggedIn": true, - "charging": true, - "basicChargeTimeMinutes": 30, - "quickChargeTimeMinutes": 15, - "batteryHeaterAuto": true, - "batteryHeaterOn": true - }, - "hvacInfo": { - "hvacOn": true, - "frontDefroster": false, - "rearDefroster": false, - "interiorTemperatureCelsius": 15.1 - } + "chargeInfo": { + "lastUpdatedTimestamp": "20210807083956", + "batteryLevelPercentage": 80, + "drivingRangeKm": 218, + "pluggedIn": true, + "charging": true, + "basicChargeTimeMinutes": 30, + "quickChargeTimeMinutes": 15, + "batteryHeaterAuto": true, + "batteryHeaterOn": true + }, + "hvacInfo": { + "hvacOn": true, + "frontDefroster": false, + "rearDefroster": false, + "interiorTemperatureCelsius": 15.1 + } } diff --git a/tests/components/mazda/fixtures/get_vehicle_status.json b/tests/components/mazda/fixtures/get_vehicle_status.json index 1e74d7202ca..17fe86c642b 100644 --- a/tests/components/mazda/fixtures/get_vehicle_status.json +++ b/tests/components/mazda/fixtures/get_vehicle_status.json @@ -1,37 +1,37 @@ { - "lastUpdatedTimestamp": "20210123143809", - "latitude": 1.234567, - "longitude": -2.345678, - "positionTimestamp": "20210123143808", - "fuelRemainingPercent": 87.0, - "fuelDistanceRemainingKm": 380.8, - "odometerKm": 2795.8, - "doors": { - "driverDoorOpen": false, - "passengerDoorOpen": false, - "rearLeftDoorOpen": false, - "rearRightDoorOpen": false, - "trunkOpen": false, - "hoodOpen": false, - "fuelLidOpen": false - }, - "doorLocks": { - "driverDoorUnlocked": false, - "passengerDoorUnlocked": false, - "rearLeftDoorUnlocked": false, - "rearRightDoorUnlocked": false - }, - "windows": { - "driverWindowOpen": false, - "passengerWindowOpen": false, - "rearLeftWindowOpen": false, - "rearRightWindowOpen": false - }, - "hazardLightsOn": false, - "tirePressure": { - "frontLeftTirePressurePsi": 35.0, - "frontRightTirePressurePsi": 35.0, - "rearLeftTirePressurePsi": 33.0, - "rearRightTirePressurePsi": 33.0 - } + "lastUpdatedTimestamp": "20210123143809", + "latitude": 1.234567, + "longitude": -2.345678, + "positionTimestamp": "20210123143808", + "fuelRemainingPercent": 87.0, + "fuelDistanceRemainingKm": 380.8, + "odometerKm": 2795.8, + "doors": { + "driverDoorOpen": false, + "passengerDoorOpen": true, + "rearLeftDoorOpen": false, + "rearRightDoorOpen": false, + "trunkOpen": false, + "hoodOpen": true, + "fuelLidOpen": false + }, + "doorLocks": { + "driverDoorUnlocked": false, + "passengerDoorUnlocked": false, + "rearLeftDoorUnlocked": false, + "rearRightDoorUnlocked": false + }, + "windows": { + "driverWindowOpen": false, + "passengerWindowOpen": false, + "rearLeftWindowOpen": false, + "rearRightWindowOpen": false + }, + "hazardLightsOn": false, + "tirePressure": { + "frontLeftTirePressurePsi": 35.0, + "frontRightTirePressurePsi": 35.0, + "rearLeftTirePressurePsi": 33.0, + "rearRightTirePressurePsi": 33.0 + } } diff --git a/tests/components/mazda/fixtures/get_vehicles.json b/tests/components/mazda/fixtures/get_vehicles.json index 887ae1194c5..a80a09f380a 100644 --- a/tests/components/mazda/fixtures/get_vehicles.json +++ b/tests/components/mazda/fixtures/get_vehicles.json @@ -1,18 +1,18 @@ [ - { - "vin": "JM000000000000000", - "id": 12345, - "nickname": "My Mazda3", - "carlineCode": "M3S", - "carlineName": "MAZDA3 2.5 S SE AWD", - "modelYear": "2021", - "modelCode": "M3S SE XA", - "modelName": "W/ SELECT PKG AWD SDN", - "automaticTransmission": true, - "interiorColorCode": "BY3", - "interiorColorName": "BLACK", - "exteriorColorCode": "42M", - "exteriorColorName": "DEEP CRYSTAL BLUE MICA", - "isElectric": false - } + { + "vin": "JM000000000000000", + "id": 12345, + "nickname": "My Mazda3", + "carlineCode": "M3S", + "carlineName": "MAZDA3 2.5 S SE AWD", + "modelYear": "2021", + "modelCode": "M3S SE XA", + "modelName": "W/ SELECT PKG AWD SDN", + "automaticTransmission": true, + "interiorColorCode": "BY3", + "interiorColorName": "BLACK", + "exteriorColorCode": "42M", + "exteriorColorName": "DEEP CRYSTAL BLUE MICA", + "isElectric": false + } ] diff --git a/tests/components/mazda/test_binary_sensor.py b/tests/components/mazda/test_binary_sensor.py new file mode 100644 index 00000000000..f2b272109c9 --- /dev/null +++ b/tests/components/mazda/test_binary_sensor.py @@ -0,0 +1,98 @@ +"""The binary sensor tests for the Mazda Connected Services integration.""" + +from homeassistant.components.binary_sensor import BinarySensorDeviceClass +from homeassistant.const import ATTR_DEVICE_CLASS, ATTR_FRIENDLY_NAME, ATTR_ICON +from homeassistant.helpers import entity_registry as er + +from . import init_integration + + +async def test_binary_sensors(hass): + """Test creation of the binary sensors.""" + await init_integration(hass) + + entity_registry = er.async_get(hass) + + # Driver Door + state = hass.states.get("binary_sensor.my_mazda3_driver_door") + assert state + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Driver Door" + assert state.attributes.get(ATTR_ICON) == "mdi:car-door" + assert state.attributes.get(ATTR_DEVICE_CLASS) == BinarySensorDeviceClass.DOOR + assert state.state == "off" + entry = entity_registry.async_get("binary_sensor.my_mazda3_driver_door") + assert entry + assert entry.unique_id == "JM000000000000000_driver_door" + + # Passenger Door + state = hass.states.get("binary_sensor.my_mazda3_passenger_door") + assert state + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Passenger Door" + assert state.attributes.get(ATTR_ICON) == "mdi:car-door" + assert state.attributes.get(ATTR_DEVICE_CLASS) == BinarySensorDeviceClass.DOOR + assert state.state == "on" + entry = entity_registry.async_get("binary_sensor.my_mazda3_passenger_door") + assert entry + assert entry.unique_id == "JM000000000000000_passenger_door" + + # Rear Left Door + state = hass.states.get("binary_sensor.my_mazda3_rear_left_door") + assert state + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Rear Left Door" + assert state.attributes.get(ATTR_ICON) == "mdi:car-door" + assert state.attributes.get(ATTR_DEVICE_CLASS) == BinarySensorDeviceClass.DOOR + assert state.state == "off" + entry = entity_registry.async_get("binary_sensor.my_mazda3_rear_left_door") + assert entry + assert entry.unique_id == "JM000000000000000_rear_left_door" + + # Rear Right Door + state = hass.states.get("binary_sensor.my_mazda3_rear_right_door") + assert state + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Rear Right Door" + assert state.attributes.get(ATTR_ICON) == "mdi:car-door" + assert state.attributes.get(ATTR_DEVICE_CLASS) == BinarySensorDeviceClass.DOOR + assert state.state == "off" + entry = entity_registry.async_get("binary_sensor.my_mazda3_rear_right_door") + assert entry + assert entry.unique_id == "JM000000000000000_rear_right_door" + + # Trunk + state = hass.states.get("binary_sensor.my_mazda3_trunk") + assert state + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Trunk" + assert state.attributes.get(ATTR_ICON) == "mdi:car-back" + assert state.attributes.get(ATTR_DEVICE_CLASS) == BinarySensorDeviceClass.DOOR + assert state.state == "off" + entry = entity_registry.async_get("binary_sensor.my_mazda3_trunk") + assert entry + assert entry.unique_id == "JM000000000000000_trunk" + + # Hood + state = hass.states.get("binary_sensor.my_mazda3_hood") + assert state + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Hood" + assert state.attributes.get(ATTR_ICON) == "mdi:car" + assert state.attributes.get(ATTR_DEVICE_CLASS) == BinarySensorDeviceClass.DOOR + assert state.state == "on" + entry = entity_registry.async_get("binary_sensor.my_mazda3_hood") + assert entry + assert entry.unique_id == "JM000000000000000_hood" + + +async def test_electric_vehicle_binary_sensors(hass): + """Test sensors which are specific to electric vehicles.""" + + await init_integration(hass, electric_vehicle=True) + + entity_registry = er.async_get(hass) + + # Plugged In + state = hass.states.get("binary_sensor.my_mazda3_plugged_in") + assert state + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Plugged In" + assert state.attributes.get(ATTR_DEVICE_CLASS) == BinarySensorDeviceClass.PLUG + assert state.state == "on" + entry = entity_registry.async_get("binary_sensor.my_mazda3_plugged_in") + assert entry + assert entry.unique_id == "JM000000000000000_ev_plugged_in" diff --git a/tests/components/mazda/test_button.py b/tests/components/mazda/test_button.py new file mode 100644 index 00000000000..71b16434ee3 --- /dev/null +++ b/tests/components/mazda/test_button.py @@ -0,0 +1,151 @@ +"""The button tests for the Mazda Connected Services integration.""" + +from pymazda import MazdaException +import pytest + +from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS +from homeassistant.const import ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, ATTR_ICON +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import entity_registry as er + +from . import init_integration + + +async def test_button_setup_non_electric_vehicle(hass) -> None: + """Test creation of button entities.""" + await init_integration(hass) + + entity_registry = er.async_get(hass) + + entry = entity_registry.async_get("button.my_mazda3_start_engine") + assert entry + assert entry.unique_id == "JM000000000000000_start_engine" + state = hass.states.get("button.my_mazda3_start_engine") + assert state + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Start Engine" + assert state.attributes.get(ATTR_ICON) == "mdi:engine" + + entry = entity_registry.async_get("button.my_mazda3_stop_engine") + assert entry + assert entry.unique_id == "JM000000000000000_stop_engine" + state = hass.states.get("button.my_mazda3_stop_engine") + assert state + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Stop Engine" + assert state.attributes.get(ATTR_ICON) == "mdi:engine-off" + + entry = entity_registry.async_get("button.my_mazda3_turn_on_hazard_lights") + assert entry + assert entry.unique_id == "JM000000000000000_turn_on_hazard_lights" + state = hass.states.get("button.my_mazda3_turn_on_hazard_lights") + assert state + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Turn On Hazard Lights" + assert state.attributes.get(ATTR_ICON) == "mdi:hazard-lights" + + entry = entity_registry.async_get("button.my_mazda3_turn_off_hazard_lights") + assert entry + assert entry.unique_id == "JM000000000000000_turn_off_hazard_lights" + state = hass.states.get("button.my_mazda3_turn_off_hazard_lights") + assert state + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Turn Off Hazard Lights" + ) + assert state.attributes.get(ATTR_ICON) == "mdi:hazard-lights" + + # Since this is a non-electric vehicle, electric vehicle buttons should not be created + entry = entity_registry.async_get("button.my_mazda3_refresh_vehicle_status") + assert entry is None + state = hass.states.get("button.my_mazda3_refresh_vehicle_status") + assert state is None + + +async def test_button_setup_electric_vehicle(hass) -> None: + """Test creation of button entities for an electric vehicle.""" + await init_integration(hass, electric_vehicle=True) + + entity_registry = er.async_get(hass) + + entry = entity_registry.async_get("button.my_mazda3_start_engine") + assert entry + assert entry.unique_id == "JM000000000000000_start_engine" + state = hass.states.get("button.my_mazda3_start_engine") + assert state + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Start Engine" + assert state.attributes.get(ATTR_ICON) == "mdi:engine" + + entry = entity_registry.async_get("button.my_mazda3_stop_engine") + assert entry + assert entry.unique_id == "JM000000000000000_stop_engine" + state = hass.states.get("button.my_mazda3_stop_engine") + assert state + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Stop Engine" + assert state.attributes.get(ATTR_ICON) == "mdi:engine-off" + + entry = entity_registry.async_get("button.my_mazda3_turn_on_hazard_lights") + assert entry + assert entry.unique_id == "JM000000000000000_turn_on_hazard_lights" + state = hass.states.get("button.my_mazda3_turn_on_hazard_lights") + assert state + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Turn On Hazard Lights" + assert state.attributes.get(ATTR_ICON) == "mdi:hazard-lights" + + entry = entity_registry.async_get("button.my_mazda3_turn_off_hazard_lights") + assert entry + assert entry.unique_id == "JM000000000000000_turn_off_hazard_lights" + state = hass.states.get("button.my_mazda3_turn_off_hazard_lights") + assert state + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Turn Off Hazard Lights" + ) + assert state.attributes.get(ATTR_ICON) == "mdi:hazard-lights" + + entry = entity_registry.async_get("button.my_mazda3_refresh_status") + assert entry + assert entry.unique_id == "JM000000000000000_refresh_vehicle_status" + state = hass.states.get("button.my_mazda3_refresh_status") + assert state + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Refresh Status" + assert state.attributes.get(ATTR_ICON) == "mdi:refresh" + + +@pytest.mark.parametrize( + "entity_id_suffix, api_method_name", + [ + ("start_engine", "start_engine"), + ("stop_engine", "stop_engine"), + ("turn_on_hazard_lights", "turn_on_hazard_lights"), + ("turn_off_hazard_lights", "turn_off_hazard_lights"), + ("refresh_status", "refresh_vehicle_status"), + ], +) +async def test_button_press(hass, entity_id_suffix, api_method_name) -> None: + """Test pressing the button entities.""" + client_mock = await init_integration(hass, electric_vehicle=True) + + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: f"button.my_mazda3_{entity_id_suffix}"}, + blocking=True, + ) + await hass.async_block_till_done() + + api_method = getattr(client_mock, api_method_name) + api_method.assert_called_once_with(12345) + + +async def test_button_press_error(hass) -> None: + """Test the Mazda API raising an error when a button entity is pressed.""" + client_mock = await init_integration(hass) + + client_mock.start_engine.side_effect = MazdaException("Test error") + + with pytest.raises(HomeAssistantError) as err: + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: "button.my_mazda3_start_engine"}, + blocking=True, + ) + await hass.async_block_till_done() + + assert str(err.value) == "Test error" diff --git a/tests/components/mazda/test_device_tracker.py b/tests/components/mazda/test_device_tracker.py index 5e09c23ecd8..4af367c1c04 100644 --- a/tests/components/mazda/test_device_tracker.py +++ b/tests/components/mazda/test_device_tracker.py @@ -9,7 +9,7 @@ from homeassistant.const import ( ) from homeassistant.helpers import entity_registry as er -from tests.components.mazda import init_integration +from . import init_integration async def test_device_tracker(hass): diff --git a/tests/components/mazda/test_init.py b/tests/components/mazda/test_init.py index e2d4661d36f..9d221bbfe88 100644 --- a/tests/components/mazda/test_init.py +++ b/tests/components/mazda/test_init.py @@ -20,8 +20,9 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr from homeassistant.util import dt as dt_util +from . import init_integration + from tests.common import MockConfigEntry, async_fire_time_changed, load_fixture -from tests.components.mazda import init_integration FIXTURE_USER_INPUT = { CONF_EMAIL: "example@example.com", diff --git a/tests/components/mazda/test_lock.py b/tests/components/mazda/test_lock.py index 1230e624cdd..9f0959dc88b 100644 --- a/tests/components/mazda/test_lock.py +++ b/tests/components/mazda/test_lock.py @@ -9,7 +9,7 @@ from homeassistant.components.lock import ( from homeassistant.const import ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME from homeassistant.helpers import entity_registry as er -from tests.components.mazda import init_integration +from . import init_integration async def test_lock_setup(hass): diff --git a/tests/components/mazda/test_sensor.py b/tests/components/mazda/test_sensor.py index 8d4085930dd..f7fb379da51 100644 --- a/tests/components/mazda/test_sensor.py +++ b/tests/components/mazda/test_sensor.py @@ -18,7 +18,7 @@ from homeassistant.const import ( from homeassistant.helpers import entity_registry as er from homeassistant.util.unit_system import IMPERIAL_SYSTEM -from tests.components.mazda import init_integration +from . import init_integration async def test_sensors(hass): diff --git a/tests/components/mazda/test_switch.py b/tests/components/mazda/test_switch.py new file mode 100644 index 00000000000..c0fc6f15fb2 --- /dev/null +++ b/tests/components/mazda/test_switch.py @@ -0,0 +1,69 @@ +"""The switch tests for the Mazda Connected Services integration.""" + +from homeassistant.components.switch import ( + DOMAIN as SWITCH_DOMAIN, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_ON, +) +from homeassistant.const import ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, ATTR_ICON +from homeassistant.helpers import entity_registry as er + +from . import init_integration + + +async def test_switch_setup(hass): + """Test setup of the switch entity.""" + await init_integration(hass, electric_vehicle=True) + + entity_registry = er.async_get(hass) + entry = entity_registry.async_get("switch.my_mazda3_charging") + assert entry + assert entry.unique_id == "JM000000000000000" + + state = hass.states.get("switch.my_mazda3_charging") + assert state + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Charging" + assert state.attributes.get(ATTR_ICON) == "mdi:ev-station" + + assert state.state == STATE_ON + + +async def test_start_charging(hass): + """Test turning on the charging switch.""" + client_mock = await init_integration(hass, electric_vehicle=True) + + client_mock.reset_mock() + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "switch.my_mazda3_charging"}, + blocking=True, + ) + await hass.async_block_till_done() + + client_mock.start_charging.assert_called_once() + client_mock.refresh_vehicle_status.assert_called_once() + client_mock.get_vehicle_status.assert_called_once() + client_mock.get_ev_vehicle_status.assert_called_once() + + +async def test_stop_charging(hass): + """Test turning off the charging switch.""" + client_mock = await init_integration(hass, electric_vehicle=True) + + client_mock.reset_mock() + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "switch.my_mazda3_charging"}, + blocking=True, + ) + await hass.async_block_till_done() + + client_mock.stop_charging.assert_called_once() + client_mock.refresh_vehicle_status.assert_called_once() + client_mock.get_vehicle_status.assert_called_once() + client_mock.get_ev_vehicle_status.assert_called_once() diff --git a/tests/components/media_player/test_browse_media.py b/tests/components/media_player/test_browse_media.py index ba7a93fc3a3..6741432024e 100644 --- a/tests/components/media_player/test_browse_media.py +++ b/tests/components/media_player/test_browse_media.py @@ -7,10 +7,14 @@ from homeassistant.components.media_player.browse_media import ( async_process_play_media_url, ) from homeassistant.config import async_process_ha_core_config +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.network import NoURLAvailableError + +from tests.common import mock_component -@pytest.fixture -def mock_sign_path(): +@pytest.fixture(name="mock_sign_path") +def fixture_mock_sign_path(): """Mock sign path.""" with patch( "homeassistant.components.media_player.browse_media.async_sign_path", @@ -46,6 +50,11 @@ async def test_process_play_media_url(hass, mock_sign_path): async_process_play_media_url(hass, "http://192.168.123.123:8123/path") == "http://192.168.123.123:8123/path?authSig=bla" ) + with pytest.raises(HomeAssistantError), patch( + "homeassistant.components.media_player.browse_media.get_url", + side_effect=NoURLAvailableError, + ): + async_process_play_media_url(hass, "/path") # Test skip signing URLs that have a query param assert ( @@ -58,3 +67,38 @@ async def test_process_play_media_url(hass, mock_sign_path): ) == "http://192.168.123.123:8123/path?hello=world" ) + + with pytest.raises(ValueError): + async_process_play_media_url(hass, "hello") + + +async def test_process_play_media_url_for_addon(hass, mock_sign_path): + """Test it uses the hostname for an addon if available.""" + await async_process_ha_core_config( + hass, + { + "internal_url": "http://example.local:8123", + "external_url": "https://example.com", + }, + ) + + # Not hassio or hassio not loaded yet, don't use supervisor network url + hass.config.api = Mock(use_ssl=False, port=8123, local_ip="192.168.123.123") + assert ( + async_process_play_media_url(hass, "/path", for_supervisor_network=True) + != "http://homeassistant:8123/path?authSig=bla" + ) + + # Is hassio and not SSL, use an supervisor network url + mock_component(hass, "hassio") + assert ( + async_process_play_media_url(hass, "/path", for_supervisor_network=True) + == "http://homeassistant:8123/path?authSig=bla" + ) + + # Hassio loaded but using SSL, don't use an supervisor network url + hass.config.api = Mock(use_ssl=True, port=8123, local_ip="192.168.123.123") + assert ( + async_process_play_media_url(hass, "/path", for_supervisor_network=True) + != "https://homeassistant:8123/path?authSig=bla" + ) diff --git a/tests/components/media_player/test_recorder.py b/tests/components/media_player/test_recorder.py new file mode 100644 index 00000000000..df1c7cc8da0 --- /dev/null +++ b/tests/components/media_player/test_recorder.py @@ -0,0 +1,54 @@ +"""The tests for media_player recorder.""" +from __future__ import annotations + +from datetime import timedelta + +from homeassistant.components import media_player +from homeassistant.components.media_player.const import ( + ATTR_ENTITY_PICTURE_LOCAL, + ATTR_INPUT_SOURCE_LIST, + ATTR_MEDIA_POSITION, + ATTR_MEDIA_POSITION_UPDATED_AT, + ATTR_SOUND_MODE_LIST, +) +from homeassistant.components.recorder.models import StateAttributes, States +from homeassistant.components.recorder.util import session_scope +from homeassistant.const import ATTR_ENTITY_PICTURE, ATTR_FRIENDLY_NAME +from homeassistant.core import State +from homeassistant.setup import async_setup_component +from homeassistant.util import dt as dt_util + +from tests.common import async_fire_time_changed, async_init_recorder_component +from tests.components.recorder.common import async_wait_recording_done_without_instance + + +async def test_exclude_attributes(hass): + """Test media_player registered attributes to be excluded.""" + await async_init_recorder_component(hass) + await async_setup_component( + hass, media_player.DOMAIN, {media_player.DOMAIN: {"platform": "demo"}} + ) + await hass.async_block_till_done() + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5)) + await hass.async_block_till_done() + await async_wait_recording_done_without_instance(hass) + + def _fetch_states() -> list[State]: + with session_scope(hass=hass) as session: + native_states = [] + for db_state, db_state_attributes in session.query(States, StateAttributes): + state = db_state.to_native() + state.attributes = db_state_attributes.to_native() + native_states.append(state) + return native_states + + states: list[State] = await hass.async_add_executor_job(_fetch_states) + assert len(states) > 1 + for state in states: + assert ATTR_ENTITY_PICTURE not in state.attributes + assert ATTR_ENTITY_PICTURE_LOCAL not in state.attributes + assert ATTR_FRIENDLY_NAME in state.attributes + assert ATTR_INPUT_SOURCE_LIST not in state.attributes + assert ATTR_MEDIA_POSITION not in state.attributes + assert ATTR_MEDIA_POSITION_UPDATED_AT not in state.attributes + assert ATTR_SOUND_MODE_LIST not in state.attributes diff --git a/tests/components/melissa/test_climate.py b/tests/components/melissa/test_climate.py index 224a878b065..6eea6a8144b 100644 --- a/tests/components/melissa/test_climate.py +++ b/tests/components/melissa/test_climate.py @@ -3,6 +3,9 @@ import json from unittest.mock import AsyncMock, Mock, patch from homeassistant.components.climate.const import ( + FAN_HIGH, + FAN_LOW, + FAN_MEDIUM, HVAC_MODE_COOL, HVAC_MODE_DRY, HVAC_MODE_FAN_ONLY, @@ -11,7 +14,6 @@ from homeassistant.components.climate.const import ( SUPPORT_FAN_MODE, SUPPORT_TARGET_TEMPERATURE, ) -from homeassistant.components.fan import SPEED_HIGH, SPEED_LOW, SPEED_MEDIUM from homeassistant.components.melissa import DATA_MELISSA, climate as melissa from homeassistant.components.melissa.climate import MelissaClimate from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS @@ -94,7 +96,7 @@ async def test_current_fan_mode(hass): device = (await api.async_fetch_devices())[_SERIAL] thermostat = MelissaClimate(api, _SERIAL, device) await thermostat.async_update() - assert thermostat.fan_mode == SPEED_LOW + assert thermostat.fan_mode == FAN_LOW thermostat._cur_settings = None assert thermostat.fan_mode is None @@ -162,7 +164,7 @@ async def test_fan_modes(hass): api = melissa_mock() device = (await api.async_fetch_devices())[_SERIAL] thermostat = MelissaClimate(api, _SERIAL, device) - assert ["auto", SPEED_HIGH, SPEED_MEDIUM, SPEED_LOW] == thermostat.fan_modes + assert ["auto", FAN_HIGH, FAN_MEDIUM, FAN_LOW] == thermostat.fan_modes async def test_target_temperature(hass): @@ -247,9 +249,9 @@ async def test_fan_mode(hass): thermostat = MelissaClimate(api, _SERIAL, device) await thermostat.async_update() await hass.async_block_till_done() - await thermostat.async_set_fan_mode(SPEED_HIGH) + await thermostat.async_set_fan_mode(FAN_HIGH) await hass.async_block_till_done() - assert thermostat.fan_mode == SPEED_HIGH + assert thermostat.fan_mode == FAN_HIGH async def test_set_operation_mode(hass): @@ -275,12 +277,12 @@ async def test_send(hass): await hass.async_block_till_done() await thermostat.async_send({"fan": api.FAN_MEDIUM}) await hass.async_block_till_done() - assert thermostat.fan_mode == SPEED_MEDIUM + assert thermostat.fan_mode == FAN_MEDIUM api.async_send.return_value = AsyncMock(return_value=False) thermostat._cur_settings = None await thermostat.async_send({"fan": api.FAN_LOW}) await hass.async_block_till_done() - assert SPEED_LOW != thermostat.fan_mode + assert FAN_LOW != thermostat.fan_mode assert thermostat._cur_settings is None @@ -293,7 +295,7 @@ async def test_update(hass): device = (await api.async_fetch_devices())[_SERIAL] thermostat = MelissaClimate(api, _SERIAL, device) await thermostat.async_update() - assert thermostat.fan_mode == SPEED_LOW + assert thermostat.fan_mode == FAN_LOW assert thermostat.state == HVAC_MODE_HEAT api.async_status = AsyncMock(side_effect=KeyError("boom")) await thermostat.async_update() @@ -322,9 +324,9 @@ async def test_melissa_fan_to_hass(hass): device = (await api.async_fetch_devices())[_SERIAL] thermostat = MelissaClimate(api, _SERIAL, device) assert thermostat.melissa_fan_to_hass(0) == "auto" - assert thermostat.melissa_fan_to_hass(1) == SPEED_LOW - assert thermostat.melissa_fan_to_hass(2) == SPEED_MEDIUM - assert thermostat.melissa_fan_to_hass(3) == SPEED_HIGH + assert thermostat.melissa_fan_to_hass(1) == FAN_LOW + assert thermostat.melissa_fan_to_hass(2) == FAN_MEDIUM + assert thermostat.melissa_fan_to_hass(3) == FAN_HIGH assert thermostat.melissa_fan_to_hass(4) is None @@ -355,9 +357,9 @@ async def test_hass_fan_to_melissa(hass): device = (await api.async_fetch_devices())[_SERIAL] thermostat = MelissaClimate(api, _SERIAL, device) assert thermostat.hass_fan_to_melissa("auto") == 0 - assert thermostat.hass_fan_to_melissa(SPEED_LOW) == 1 - assert thermostat.hass_fan_to_melissa(SPEED_MEDIUM) == 2 - assert thermostat.hass_fan_to_melissa(SPEED_HIGH) == 3 + assert thermostat.hass_fan_to_melissa(FAN_LOW) == 1 + assert thermostat.hass_fan_to_melissa(FAN_MEDIUM) == 2 + assert thermostat.hass_fan_to_melissa(FAN_HIGH) == 3 thermostat.hass_fan_to_melissa("test") mocked_warning.assert_called_once_with( "Melissa have no setting for %s fan mode", "test" diff --git a/tests/components/met/test_config_flow.py b/tests/components/met/test_config_flow.py index ff5deb18194..358afe11cc2 100644 --- a/tests/components/met/test_config_flow.py +++ b/tests/components/met/test_config_flow.py @@ -125,21 +125,3 @@ async def test_onboarding_step_abort_no_home(hass, latitude, longitude): assert result["type"] == "abort" assert result["reason"] == "no_home" - - -async def test_import_step(hass): - """Test initializing via import step.""" - test_data = { - "name": "home", - CONF_LONGITUDE: None, - CONF_LATITUDE: None, - CONF_ELEVATION: 0, - "track_home": True, - } - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=test_data - ) - - assert result["type"] == "create_entry" - assert result["title"] == "home" - assert result["data"] == test_data diff --git a/tests/components/mfi/test_switch.py b/tests/components/mfi/test_switch.py index 8eed41f8a32..c61585898de 100644 --- a/tests/components/mfi/test_switch.py +++ b/tests/components/mfi/test_switch.py @@ -103,21 +103,3 @@ async def test_turn_off(port, switch): assert port.control.call_args == mock.call(False) # pylint: disable=protected-access assert not switch._target_state - - -async def test_current_power_w(port, switch): - """Test current power.""" - port.data = {"active_pwr": 10} - assert switch.current_power_w == 10 - - -async def test_current_power_w_no_data(port, switch): - """Test current power if there is no data.""" - port.data = {"notpower": 123} - assert switch.current_power_w == 0 - - -async def test_extra_state_attributes(port, switch): - """Test the state attributes.""" - port.data = {"v_rms": 1.25, "i_rms": 2.75} - assert switch.extra_state_attributes == {"volts": 1.2, "amps": 2.8} diff --git a/tests/components/mhz19/__init__.py b/tests/components/mhz19/__init__.py deleted file mode 100644 index a35660a3726..00000000000 --- a/tests/components/mhz19/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Tests for the mhz19 component.""" diff --git a/tests/components/mhz19/test_sensor.py b/tests/components/mhz19/test_sensor.py deleted file mode 100644 index fd494d6c099..00000000000 --- a/tests/components/mhz19/test_sensor.py +++ /dev/null @@ -1,125 +0,0 @@ -"""Tests for MH-Z19 sensor.""" -from unittest.mock import DEFAULT, Mock, patch - -from pmsensor import co2sensor -from pmsensor.co2sensor import read_mh_z19_with_temperature - -import homeassistant.components.mhz19.sensor as mhz19 -from homeassistant.components.sensor import DOMAIN -from homeassistant.const import ( - CONCENTRATION_PARTS_PER_MILLION, - TEMP_CELSIUS, - TEMP_FAHRENHEIT, -) -from homeassistant.setup import async_setup_component - -from tests.common import assert_setup_component - - -async def test_setup_missing_config(hass): - """Test setup with configuration missing required entries.""" - with assert_setup_component(0): - assert await async_setup_component( - hass, DOMAIN, {"sensor": {"platform": "mhz19"}} - ) - - -@patch("pmsensor.co2sensor.read_mh_z19", side_effect=OSError("test error")) -async def test_setup_failed_connect(mock_co2, hass): - """Test setup when connection error occurs.""" - assert not mhz19.setup_platform( - hass, - {"platform": "mhz19", mhz19.CONF_SERIAL_DEVICE: "test.serial"}, - None, - ) - - -async def test_setup_connected(hass): - """Test setup when connection succeeds.""" - with patch.multiple( - "pmsensor.co2sensor", - read_mh_z19=DEFAULT, - read_mh_z19_with_temperature=DEFAULT, - ): - read_mh_z19_with_temperature.return_value = None - mock_add = Mock() - mhz19.setup_platform( - hass, - { - "platform": "mhz19", - "name": "name", - "monitored_conditions": ["co2", "temperature"], - mhz19.CONF_SERIAL_DEVICE: "test.serial", - }, - mock_add, - ) - assert mock_add.call_count == 1 - - -@patch( - "pmsensor.co2sensor.read_mh_z19_with_temperature", - side_effect=OSError("test error"), -) -async def aiohttp_client_update_oserror(mock_function): - """Test MHZClient when library throws OSError.""" - client = mhz19.MHZClient(co2sensor, "test.serial") - client.update() - assert {} == client.data - - -@patch("pmsensor.co2sensor.read_mh_z19_with_temperature", return_value=(5001, 24)) -async def aiohttp_client_update_ppm_overflow(mock_function): - """Test MHZClient when ppm is too high.""" - client = mhz19.MHZClient(co2sensor, "test.serial") - client.update() - assert client.data.get("co2") is None - - -@patch("pmsensor.co2sensor.read_mh_z19_with_temperature", return_value=(1000, 24)) -async def aiohttp_client_update_good_read(mock_function): - """Test MHZClient when ppm is too high.""" - client = mhz19.MHZClient(co2sensor, "test.serial") - client.update() - assert {"temperature": 24, "co2": 1000} == client.data - - -@patch("pmsensor.co2sensor.read_mh_z19_with_temperature", return_value=(1000, 24)) -async def test_co2_sensor(mock_function, hass): - """Test CO2 sensor.""" - client = mhz19.MHZClient(co2sensor, "test.serial") - sensor = mhz19.MHZ19Sensor(client, "name", mhz19.SENSOR_TYPES[1]) - sensor.hass = hass - sensor.update() - - assert sensor.name == "name: CO2" - assert sensor.state == 1000 - assert sensor.native_unit_of_measurement == CONCENTRATION_PARTS_PER_MILLION - assert sensor.should_poll - assert sensor.extra_state_attributes == {"temperature": 24} - - -@patch("pmsensor.co2sensor.read_mh_z19_with_temperature", return_value=(1000, 24)) -async def test_temperature_sensor(mock_function, hass): - """Test temperature sensor.""" - client = mhz19.MHZClient(co2sensor, "test.serial") - sensor = mhz19.MHZ19Sensor(client, "name", mhz19.SENSOR_TYPES[0]) - sensor.hass = hass - sensor.update() - - assert sensor.name == "name: Temperature" - assert sensor.state == 24 - assert sensor.native_unit_of_measurement == TEMP_CELSIUS - assert sensor.should_poll - assert sensor.extra_state_attributes == {"co2_concentration": 1000} - - -@patch("pmsensor.co2sensor.read_mh_z19_with_temperature", return_value=(1000, 24)) -async def test_temperature_sensor_f(mock_function, hass): - """Test temperature sensor.""" - with patch.object(hass.config.units, "temperature_unit", TEMP_FAHRENHEIT): - client = mhz19.MHZClient(co2sensor, "test.serial") - sensor = mhz19.MHZ19Sensor(client, "name", mhz19.SENSOR_TYPES[0]) - sensor.hass = hass - sensor.update() - - assert sensor.state == 75 diff --git a/tests/components/min_max/test_config_flow.py b/tests/components/min_max/test_config_flow.py new file mode 100644 index 00000000000..8e66f1a4711 --- /dev/null +++ b/tests/components/min_max/test_config_flow.py @@ -0,0 +1,135 @@ +"""Test the Min/Max config flow.""" +from unittest.mock import patch + +import pytest + +from homeassistant import config_entries +from homeassistant.components.min_max.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM + +from tests.common import MockConfigEntry + + +@pytest.mark.parametrize("platform", ("sensor",)) +async def test_config_flow(hass: HomeAssistant, platform) -> None: + """Test the config flow.""" + input_sensors = ["sensor.input_one", "sensor.input_two"] + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] is None + + with patch( + "homeassistant.components.min_max.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"name": "My min_max", "entity_ids": input_sensors, "type": "max"}, + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "My min_max" + assert result["data"] == {} + assert result["options"] == { + "entity_ids": input_sensors, + "name": "My min_max", + "round_digits": 2.0, + "type": "max", + } + assert len(mock_setup_entry.mock_calls) == 1 + + config_entry = hass.config_entries.async_entries(DOMAIN)[0] + assert config_entry.data == {} + assert config_entry.options == { + "entity_ids": input_sensors, + "name": "My min_max", + "round_digits": 2.0, + "type": "max", + } + assert config_entry.title == "My min_max" + + +def get_suggested(schema, key): + """Get suggested value for key in voluptuous schema.""" + for k in schema.keys(): + if k == key: + if k.description is None or "suggested_value" not in k.description: + return None + return k.description["suggested_value"] + # Wanted key absent from schema + raise Exception + + +@pytest.mark.parametrize("platform", ("sensor",)) +async def test_options(hass: HomeAssistant, platform) -> None: + """Test reconfiguring.""" + hass.states.async_set("sensor.input_one", "10") + hass.states.async_set("sensor.input_two", "20") + hass.states.async_set("sensor.input_three", "33.33") + + input_sensors1 = ["sensor.input_one", "sensor.input_two"] + input_sensors2 = ["sensor.input_one", "sensor.input_two", "sensor.input_three"] + + # Setup the config entry + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + "entity_ids": input_sensors1, + "name": "My min_max", + "round_digits": 0, + "type": "min", + }, + title="My min_max", + ) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "init" + schema = result["data_schema"].schema + assert get_suggested(schema, "entity_ids") == input_sensors1 + assert get_suggested(schema, "round_digits") == 0 + assert get_suggested(schema, "type") == "min" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + "entity_ids": input_sensors2, + "round_digits": 1, + "type": "mean", + }, + ) + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["data"] == { + "entity_ids": input_sensors2, + "name": "My min_max", + "round_digits": 1, + "type": "mean", + } + assert config_entry.data == {} + assert config_entry.options == { + "entity_ids": input_sensors2, + "name": "My min_max", + "round_digits": 1, + "type": "mean", + } + assert config_entry.title == "My min_max" + + # Check config entry is reloaded with new options + await hass.async_block_till_done() + + # Check the entity was updated, no new entity was created + assert len(hass.states.async_all()) == 4 + + # Check the state of the entity has changed as expected + state = hass.states.get(f"{platform}.my_min_max") + assert state.state == "21.1" + assert state.attributes["count_sensors"] == 3 diff --git a/tests/components/min_max/test_init.py b/tests/components/min_max/test_init.py new file mode 100644 index 00000000000..4df543c2565 --- /dev/null +++ b/tests/components/min_max/test_init.py @@ -0,0 +1,55 @@ +"""Test the Min/Max integration.""" +import pytest + +from homeassistant.components.min_max.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from tests.common import MockConfigEntry + + +@pytest.mark.parametrize("platform", ("sensor",)) +async def test_setup_and_remove_config_entry( + hass: HomeAssistant, + platform: str, +) -> None: + """Test setting up and removing a config entry.""" + hass.states.async_set("sensor.input_one", "10") + hass.states.async_set("sensor.input_two", "20") + + input_sensors = ["sensor.input_one", "sensor.input_two"] + + registry = er.async_get(hass) + min_max_entity_id = f"{platform}.my_min_max" + + # Setup the config entry + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + "entity_ids": input_sensors, + "name": "My min_max", + "round_digits": 2.0, + "type": "max", + }, + title="My min_max", + ) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + # Check the entity is registered in the entity registry + assert registry.async_get(min_max_entity_id) is not None + + # Check the platform is setup correctly + state = hass.states.get(min_max_entity_id) + assert state.state == "20.0" + assert state.attributes["count_sensors"] == 2 + + # Remove the config entry + assert await hass.config_entries.async_remove(config_entry.entry_id) + await hass.async_block_till_done() + + # Check the state and entity registry entry are removed + assert hass.states.get(min_max_entity_id) is None + assert registry.async_get(min_max_entity_id) is None diff --git a/tests/components/min_max/test_sensor.py b/tests/components/min_max/test_sensor.py index 1712a9027ca..ae6a14893f8 100644 --- a/tests/components/min_max/test_sensor.py +++ b/tests/components/min_max/test_sensor.py @@ -3,7 +3,7 @@ import statistics from unittest.mock import patch from homeassistant import config as hass_config -from homeassistant.components.min_max import DOMAIN +from homeassistant.components.min_max.const import DOMAIN from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, PERCENTAGE, diff --git a/tests/components/modbus/fixtures/configuration.yaml b/tests/components/modbus/fixtures/configuration.yaml new file mode 100644 index 00000000000..0f12ac88686 --- /dev/null +++ b/tests/components/modbus/fixtures/configuration.yaml @@ -0,0 +1,5 @@ +modbus: + type: "tcp" + host: "testHost" + port: 5001 + name: "testModbus" diff --git a/tests/components/modbus/test_binary_sensor.py b/tests/components/modbus/test_binary_sensor.py index 15a03b76927..bca63321597 100644 --- a/tests/components/modbus/test_binary_sensor.py +++ b/tests/components/modbus/test_binary_sensor.py @@ -8,6 +8,7 @@ from homeassistant.components.modbus.const import ( CONF_INPUT_TYPE, CONF_LAZY_ERROR, CONF_SLAVE_COUNT, + MODBUS_DOMAIN, ) from homeassistant.const import ( CONF_ADDRESS, @@ -22,6 +23,7 @@ from homeassistant.const import ( STATE_UNKNOWN, ) from homeassistant.core import State +from homeassistant.setup import async_setup_component from .conftest import TEST_ENTITY_NAME, ReadResult, do_next_cycle @@ -257,16 +259,68 @@ async def test_config_slave_binary_sensor(hass, mock_modbus): { CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 51, - CONF_SLAVE_COUNT: 8, } ] }, ], ) @pytest.mark.parametrize( - "register_words,expected, slaves", + "config_addon,register_words,expected, slaves", [ ( + {CONF_SLAVE_COUNT: 1}, + [0x01], + STATE_ON, + [ + STATE_OFF, + ], + ), + ( + {CONF_SLAVE_COUNT: 1}, + [0x02], + STATE_OFF, + [ + STATE_ON, + ], + ), + ( + {CONF_SLAVE_COUNT: 1}, + [0x04], + STATE_OFF, + [ + STATE_OFF, + ], + ), + ( + {CONF_SLAVE_COUNT: 7}, + [0x01], + STATE_ON, + [ + STATE_OFF, + STATE_OFF, + STATE_OFF, + STATE_OFF, + STATE_OFF, + STATE_OFF, + STATE_OFF, + ], + ), + ( + {CONF_SLAVE_COUNT: 7}, + [0x82], + STATE_OFF, + [ + STATE_ON, + STATE_OFF, + STATE_OFF, + STATE_OFF, + STATE_OFF, + STATE_OFF, + STATE_ON, + ], + ), + ( + {CONF_SLAVE_COUNT: 10}, [0x01, 0x00], STATE_ON, [ @@ -278,23 +332,12 @@ async def test_config_slave_binary_sensor(hass, mock_modbus): STATE_OFF, STATE_OFF, STATE_OFF, - ], - ), - ( - [0x02, 0x00], - STATE_OFF, - [ - STATE_ON, - STATE_OFF, - STATE_OFF, - STATE_OFF, - STATE_OFF, - STATE_OFF, STATE_OFF, STATE_OFF, ], ), ( + {CONF_SLAVE_COUNT: 10}, [0x01, 0x01], STATE_ON, [ @@ -306,6 +349,25 @@ async def test_config_slave_binary_sensor(hass, mock_modbus): STATE_OFF, STATE_OFF, STATE_ON, + STATE_OFF, + STATE_OFF, + ], + ), + ( + {CONF_SLAVE_COUNT: 10}, + [0x81, 0x01], + STATE_ON, + [ + STATE_OFF, + STATE_OFF, + STATE_OFF, + STATE_OFF, + STATE_OFF, + STATE_OFF, + STATE_ON, + STATE_ON, + STATE_OFF, + STATE_OFF, ], ), ], @@ -314,6 +376,18 @@ async def test_slave_binary_sensor(hass, expected, slaves, mock_do_cycle): """Run test for given config.""" assert hass.states.get(ENTITY_ID).state == expected - for i in range(8): + for i in range(len(slaves)): entity_id = f"{SENSOR_DOMAIN}.{TEST_ENTITY_NAME}_{i+1}".replace(" ", "_") assert hass.states.get(entity_id).state == slaves[i] + + +async def test_no_discovery_info_binary_sensor(hass, caplog): + """Test setup without discovery info.""" + assert SENSOR_DOMAIN not in hass.config.components + assert await async_setup_component( + hass, + SENSOR_DOMAIN, + {SENSOR_DOMAIN: {"platform": MODBUS_DOMAIN}}, + ) + await hass.async_block_till_done() + assert SENSOR_DOMAIN in hass.config.components diff --git a/tests/components/modbus/test_climate.py b/tests/components/modbus/test_climate.py index 80c8590f7cc..a2c83a60640 100644 --- a/tests/components/modbus/test_climate.py +++ b/tests/components/modbus/test_climate.py @@ -8,19 +8,21 @@ from homeassistant.components.modbus.const import ( CONF_DATA_TYPE, CONF_LAZY_ERROR, CONF_TARGET_TEMP, + MODBUS_DOMAIN, DataType, ) from homeassistant.const import ( ATTR_TEMPERATURE, CONF_ADDRESS, - CONF_COUNT, CONF_NAME, CONF_SCAN_INTERVAL, CONF_SLAVE, + STATE_UNAVAILABLE, ) from homeassistant.core import State +from homeassistant.setup import async_setup_component -from .conftest import TEST_ENTITY_NAME, ReadResult +from .conftest import TEST_ENTITY_NAME, ReadResult, do_next_cycle ENTITY_ID = f"{CLIMATE_DOMAIN}.{TEST_ENTITY_NAME}".replace(" ", "_") @@ -46,7 +48,7 @@ ENTITY_ID = f"{CLIMATE_DOMAIN}.{TEST_ENTITY_NAME}".replace(" ", "_") CONF_ADDRESS: 117, CONF_SLAVE: 10, CONF_SCAN_INTERVAL: 20, - CONF_COUNT: 2, + CONF_DATA_TYPE: DataType.INT32, CONF_LAZY_ERROR: 10, } ], @@ -68,7 +70,7 @@ async def test_config_climate(hass, mock_modbus): CONF_SLAVE: 1, CONF_TARGET_TEMP: 117, CONF_ADDRESS: 117, - CONF_COUNT: 2, + CONF_DATA_TYPE: DataType.INT32, }, ], }, @@ -94,7 +96,6 @@ async def test_temperature_climate(hass, expected, mock_do_cycle): { CONF_CLIMATES: [ { - CONF_COUNT: 2, CONF_NAME: TEST_ENTITY_NAME, CONF_TARGET_TEMP: 117, CONF_ADDRESS: 117, @@ -224,3 +225,91 @@ async def test_restore_state_climate(hass, mock_test_state, mock_modbus): state = hass.states.get(ENTITY_ID) assert state.state == HVAC_MODE_AUTO assert state.attributes[ATTR_TEMPERATURE] == 37 + + +@pytest.mark.parametrize( + "do_config", + [ + { + CONF_CLIMATES: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_TARGET_TEMP: 117, + CONF_ADDRESS: 117, + CONF_SLAVE: 10, + CONF_LAZY_ERROR: 1, + } + ], + }, + ], +) +@pytest.mark.parametrize( + "register_words,do_exception,start_expect,end_expect", + [ + ( + [0x8000], + True, + "17", + STATE_UNAVAILABLE, + ), + ], +) +async def test_lazy_error_climate(hass, mock_do_cycle, start_expect, end_expect): + """Run test for sensor.""" + hass.states.async_set(ENTITY_ID, 17) + await hass.async_block_till_done() + now = mock_do_cycle + assert hass.states.get(ENTITY_ID).state == start_expect + now = await do_next_cycle(hass, now, 11) + assert hass.states.get(ENTITY_ID).state == start_expect + now = await do_next_cycle(hass, now, 11) + assert hass.states.get(ENTITY_ID).state == end_expect + + +@pytest.mark.parametrize( + "do_config", + [ + { + CONF_CLIMATES: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_TARGET_TEMP: 117, + CONF_ADDRESS: 117, + CONF_SLAVE: 10, + } + ], + }, + ], +) +@pytest.mark.parametrize( + "config_addon,register_words", + [ + ( + { + CONF_DATA_TYPE: DataType.INT16, + }, + [7, 9], + ), + ( + { + CONF_DATA_TYPE: DataType.INT32, + }, + [7], + ), + ], +) +async def test_wrong_unpack_climate(hass, mock_do_cycle): + """Run test for sensor.""" + assert hass.states.get(ENTITY_ID).state == STATE_UNAVAILABLE + + +async def test_no_discovery_info_climate(hass, caplog): + """Test setup without discovery info.""" + assert CLIMATE_DOMAIN not in hass.config.components + assert await async_setup_component( + hass, + CLIMATE_DOMAIN, + {CLIMATE_DOMAIN: {"platform": MODBUS_DOMAIN}}, + ) + await hass.async_block_till_done() + assert CLIMATE_DOMAIN in hass.config.components diff --git a/tests/components/modbus/test_cover.py b/tests/components/modbus/test_cover.py index 6797dc8713c..3e545f1c1ab 100644 --- a/tests/components/modbus/test_cover.py +++ b/tests/components/modbus/test_cover.py @@ -15,6 +15,7 @@ from homeassistant.components.modbus.const import ( CONF_STATE_OPENING, CONF_STATUS_REGISTER, CONF_STATUS_REGISTER_TYPE, + MODBUS_DOMAIN, ) from homeassistant.const import ( CONF_ADDRESS, @@ -29,6 +30,7 @@ from homeassistant.const import ( STATE_UNAVAILABLE, ) from homeassistant.core import State +from homeassistant.setup import async_setup_component from .conftest import TEST_ENTITY_NAME, ReadResult, do_next_cycle @@ -307,3 +309,15 @@ async def test_service_cover_move(hass, mock_modbus, mock_ha): "cover", "close_cover", {"entity_id": ENTITY_ID2}, blocking=True ) assert hass.states.get(ENTITY_ID2).state == STATE_UNAVAILABLE + + +async def test_no_discovery_info_cover(hass, caplog): + """Test setup without discovery info.""" + assert COVER_DOMAIN not in hass.config.components + assert await async_setup_component( + hass, + COVER_DOMAIN, + {COVER_DOMAIN: {"platform": MODBUS_DOMAIN}}, + ) + await hass.async_block_till_done() + assert COVER_DOMAIN in hass.config.components diff --git a/tests/components/modbus/test_fan.py b/tests/components/modbus/test_fan.py index 9b0564504d9..3baa23b2791 100644 --- a/tests/components/modbus/test_fan.py +++ b/tests/components/modbus/test_fan.py @@ -29,6 +29,7 @@ from homeassistant.const import ( STATE_UNAVAILABLE, ) from homeassistant.core import State +from homeassistant.setup import async_setup_component from .conftest import TEST_ENTITY_NAME, ReadResult @@ -309,3 +310,15 @@ async def test_service_fan_update(hass, mock_modbus, mock_ha): "homeassistant", "update_entity", {"entity_id": ENTITY_ID}, blocking=True ) assert hass.states.get(ENTITY_ID).state == STATE_ON + + +async def test_no_discovery_info_fan(hass, caplog): + """Test setup without discovery info.""" + assert FAN_DOMAIN not in hass.config.components + assert await async_setup_component( + hass, + FAN_DOMAIN, + {FAN_DOMAIN: {"platform": MODBUS_DOMAIN}}, + ) + await hass.async_block_till_done() + assert FAN_DOMAIN in hass.config.components diff --git a/tests/components/modbus/test_init.py b/tests/components/modbus/test_init.py index 9bd0a2caa6d..11ddf4bc426 100644 --- a/tests/components/modbus/test_init.py +++ b/tests/components/modbus/test_init.py @@ -21,11 +21,12 @@ from pymodbus.pdu import ExceptionResponse, IllegalFunctionRequest import pytest import voluptuous as vol +from homeassistant import config as hass_config from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.components.modbus.const import ( ATTR_ADDRESS, ATTR_HUB, - ATTR_STATE, + ATTR_SLAVE, ATTR_UNIT, ATTR_VALUE, CALL_TYPE_COIL, @@ -42,6 +43,7 @@ from homeassistant.components.modbus.const import ( CONF_INPUT_TYPE, CONF_MSG_WAIT, CONF_PARITY, + CONF_SLAVE_COUNT, CONF_STOPBITS, CONF_SWAP, CONF_SWAP_BYTE, @@ -66,6 +68,7 @@ from homeassistant.components.modbus.validators import ( ) from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.const import ( + ATTR_STATE, CONF_ADDRESS, CONF_BINARY_SENSORS, CONF_COUNT, @@ -81,6 +84,7 @@ from homeassistant.const import ( CONF_TIMEOUT, CONF_TYPE, EVENT_HOMEASSISTANT_STOP, + SERVICE_RELOAD, STATE_ON, STATE_UNAVAILABLE, STATE_UNKNOWN, @@ -97,7 +101,7 @@ from .conftest import ( ReadResult, ) -from tests.common import async_fire_time_changed +from tests.common import async_fire_time_changed, get_fixture_path @pytest.fixture(name="mock_modbus_with_pymodbus") @@ -145,13 +149,11 @@ async def test_number_validator(): }, { CONF_NAME: TEST_ENTITY_NAME, - CONF_COUNT: 2, - CONF_DATA_TYPE: DataType.INT, + CONF_DATA_TYPE: DataType.INT32, }, { CONF_NAME: TEST_ENTITY_NAME, - CONF_COUNT: 2, - CONF_DATA_TYPE: DataType.INT, + CONF_DATA_TYPE: DataType.INT32, CONF_SWAP: CONF_SWAP_BYTE, }, { @@ -177,7 +179,7 @@ async def test_ok_struct_validator(do_config): { CONF_NAME: TEST_ENTITY_NAME, CONF_COUNT: 8, - CONF_DATA_TYPE: DataType.INT, + CONF_DATA_TYPE: "int", }, { CONF_NAME: TEST_ENTITY_NAME, @@ -210,6 +212,13 @@ async def test_ok_struct_validator(do_config): CONF_STRUCTURE: ">f", CONF_SWAP: CONF_SWAP_WORD, }, + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_COUNT: 2, + CONF_DATA_TYPE: DataType.CUSTOM, + CONF_STRUCTURE: ">f", + CONF_SLAVE_COUNT: 5, + }, ], ) async def test_exception_struct_validator(do_config): @@ -484,8 +493,15 @@ SERVICE = "service" {VALUE: ModbusException("fail write_"), DATA: "Pymodbus:"}, ], ) +@pytest.mark.parametrize( + "do_unit", + [ + ATTR_UNIT, + ATTR_SLAVE, + ], +) async def test_pb_service_write( - hass, do_write, do_return, caplog, mock_modbus_with_pymodbus + hass, do_write, do_return, do_unit, caplog, mock_modbus_with_pymodbus ): """Run test for service write_register.""" @@ -498,7 +514,7 @@ async def test_pb_service_write( data = { ATTR_HUB: TEST_MODBUS_NAME, - ATTR_UNIT: 17, + do_unit: 17, ATTR_ADDRESS: 16, do_write[DATA]: do_write[VALUE], } @@ -703,34 +719,33 @@ async def test_delay(hass, mock_pymodbus): ] } mock_pymodbus.read_coils.return_value = ReadResult([0x01]) - now = dt_util.utcnow() - with mock.patch("homeassistant.helpers.event.dt_util.utcnow", return_value=now): + start_time = dt_util.utcnow() + with mock.patch( + "homeassistant.helpers.event.dt_util.utcnow", return_value=start_time + ): assert await async_setup_component(hass, DOMAIN, config) is True await hass.async_block_till_done() + assert hass.states.get(entity_id).state == STATE_UNKNOWN - # pass first scan_interval - start_time = now - now = now + timedelta(seconds=(set_scan_interval + 1)) - with mock.patch( - "homeassistant.helpers.event.dt_util.utcnow", return_value=now, autospec=True - ): - async_fire_time_changed(hass, now) - await hass.async_block_till_done() - assert hass.states.get(entity_id).state == STATE_UNAVAILABLE - - stop_time = start_time + timedelta(seconds=(set_delay + 1)) - step_timedelta = timedelta(seconds=1) - while now < stop_time: - now = now + step_timedelta - with mock.patch("homeassistant.helpers.event.dt_util.utcnow", return_value=now): + time_sensor_active = start_time + timedelta(seconds=2) + time_after_delay = start_time + timedelta(seconds=(set_delay)) + time_after_scan = start_time + timedelta(seconds=(set_delay + set_scan_interval)) + time_stop = time_after_scan + timedelta(seconds=10) + now = start_time + while now < time_stop: + now += timedelta(seconds=1) + with mock.patch( + "homeassistant.helpers.event.dt_util.utcnow", + return_value=now, + autospec=True, + ): async_fire_time_changed(hass, now) await hass.async_block_till_done() - assert hass.states.get(entity_id).state == STATE_UNAVAILABLE - now = now + step_timedelta + timedelta(seconds=2) - with mock.patch("homeassistant.helpers.event.dt_util.utcnow", return_value=now): - async_fire_time_changed(hass, now) - await hass.async_block_till_done() - assert hass.states.get(entity_id).state == STATE_ON + if now > time_sensor_active: + if now <= time_after_delay: + assert hass.states.get(entity_id).state == STATE_UNAVAILABLE + elif now > time_after_scan: + assert hass.states.get(entity_id).state == STATE_ON @pytest.mark.parametrize( @@ -811,3 +826,43 @@ async def test_stop_restart(hass, caplog, mock_modbus): assert mock_modbus.connect.called assert f"modbus {TEST_MODBUS_NAME} communication closed" in caplog.text assert f"modbus {TEST_MODBUS_NAME} communication open" in caplog.text + + +@pytest.mark.parametrize("do_config", [{}]) +async def test_write_no_client(hass, mock_modbus): + """Run test for service stop and write without client.""" + + mock_modbus.reset() + data = { + ATTR_HUB: TEST_MODBUS_NAME, + } + await hass.services.async_call(DOMAIN, SERVICE_STOP, data, blocking=True) + await hass.async_block_till_done() + assert mock_modbus.close.called + + data = { + ATTR_HUB: TEST_MODBUS_NAME, + ATTR_UNIT: 17, + ATTR_ADDRESS: 16, + ATTR_STATE: True, + } + await hass.services.async_call(DOMAIN, SERVICE_WRITE_COIL, data, blocking=True) + + +@pytest.mark.parametrize("do_config", [{}]) +async def test_integration_reload(hass, caplog, mock_modbus): + """Run test for integration reload.""" + + caplog.set_level(logging.INFO) + caplog.clear() + + yaml_path = get_fixture_path("configuration.yaml", "modbus") + now = dt_util.utcnow() + with mock.patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path): + await hass.services.async_call(DOMAIN, SERVICE_RELOAD, blocking=True) + await hass.async_block_till_done() + for i in range(4): + now = now + timedelta(seconds=1) + async_fire_time_changed(hass, now) + await hass.async_block_till_done() + assert "Modbus reloading" in caplog.text diff --git a/tests/components/modbus/test_light.py b/tests/components/modbus/test_light.py index f98f1105fa0..7ef13c0c712 100644 --- a/tests/components/modbus/test_light.py +++ b/tests/components/modbus/test_light.py @@ -29,6 +29,7 @@ from homeassistant.const import ( STATE_UNAVAILABLE, ) from homeassistant.core import State +from homeassistant.setup import async_setup_component from .conftest import TEST_ENTITY_NAME, ReadResult @@ -309,3 +310,15 @@ async def test_service_light_update(hass, mock_modbus, mock_ha): "homeassistant", "update_entity", {"entity_id": ENTITY_ID}, blocking=True ) assert hass.states.get(ENTITY_ID).state == STATE_ON + + +async def test_no_discovery_info_light(hass, caplog): + """Test setup without discovery info.""" + assert LIGHT_DOMAIN not in hass.config.components + assert await async_setup_component( + hass, + LIGHT_DOMAIN, + {LIGHT_DOMAIN: {"platform": MODBUS_DOMAIN}}, + ) + await hass.async_block_till_done() + assert LIGHT_DOMAIN in hass.config.components diff --git a/tests/components/modbus/test_sensor.py b/tests/components/modbus/test_sensor.py index 053dc46c6ba..b1432876a97 100644 --- a/tests/components/modbus/test_sensor.py +++ b/tests/components/modbus/test_sensor.py @@ -9,11 +9,13 @@ from homeassistant.components.modbus.const import ( CONF_LAZY_ERROR, CONF_PRECISION, CONF_SCALE, + CONF_SLAVE_COUNT, CONF_SWAP, CONF_SWAP_BYTE, CONF_SWAP_NONE, CONF_SWAP_WORD, CONF_SWAP_WORD_BYTE, + MODBUS_DOMAIN, DataType, ) from homeassistant.components.sensor import ( @@ -32,8 +34,10 @@ from homeassistant.const import ( CONF_SLAVE, CONF_STRUCTURE, STATE_UNAVAILABLE, + STATE_UNKNOWN, ) from homeassistant.core import State +from homeassistant.setup import async_setup_component from .conftest import TEST_ENTITY_NAME, ReadResult, do_next_cycle @@ -48,6 +52,7 @@ ENTITY_ID = f"{SENSOR_DOMAIN}.{TEST_ENTITY_NAME}".replace(" ", "_") { CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 51, + CONF_DATA_TYPE: DataType.INT16, } ] }, @@ -57,8 +62,7 @@ ENTITY_ID = f"{SENSOR_DOMAIN}.{TEST_ENTITY_NAME}".replace(" ", "_") CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 51, CONF_SLAVE: 10, - CONF_COUNT: 1, - CONF_DATA_TYPE: "int", + CONF_DATA_TYPE: DataType.INT16, CONF_PRECISION: 0, CONF_SCALE: 1, CONF_OFFSET: 0, @@ -75,8 +79,7 @@ ENTITY_ID = f"{SENSOR_DOMAIN}.{TEST_ENTITY_NAME}".replace(" ", "_") CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 51, CONF_SLAVE: 10, - CONF_COUNT: 1, - CONF_DATA_TYPE: "int", + CONF_DATA_TYPE: DataType.INT16, CONF_PRECISION: 0, CONF_SCALE: 1, CONF_OFFSET: 0, @@ -90,7 +93,7 @@ ENTITY_ID = f"{SENSOR_DOMAIN}.{TEST_ENTITY_NAME}".replace(" ", "_") { CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 51, - CONF_COUNT: 1, + CONF_DATA_TYPE: DataType.INT16, CONF_SWAP: CONF_SWAP_NONE, } ] @@ -100,7 +103,7 @@ ENTITY_ID = f"{SENSOR_DOMAIN}.{TEST_ENTITY_NAME}".replace(" ", "_") { CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 51, - CONF_COUNT: 1, + CONF_DATA_TYPE: DataType.INT16, CONF_SWAP: CONF_SWAP_BYTE, } ] @@ -110,7 +113,7 @@ ENTITY_ID = f"{SENSOR_DOMAIN}.{TEST_ENTITY_NAME}".replace(" ", "_") { CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 51, - CONF_COUNT: 2, + CONF_DATA_TYPE: DataType.INT32, CONF_SWAP: CONF_SWAP_WORD, } ] @@ -120,11 +123,21 @@ ENTITY_ID = f"{SENSOR_DOMAIN}.{TEST_ENTITY_NAME}".replace(" ", "_") { CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 51, - CONF_COUNT: 2, + CONF_DATA_TYPE: DataType.INT32, CONF_SWAP: CONF_SWAP_WORD_BYTE, } ] }, + { + CONF_SENSORS: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_ADDRESS: 51, + CONF_DATA_TYPE: DataType.INT32, + CONF_SLAVE_COUNT: 5, + } + ] + }, ], ) async def test_config_sensor(hass, mock_modbus): @@ -253,8 +266,7 @@ async def test_config_wrong_struct_sensor(hass, error_message, mock_modbus, capl [ ( { - CONF_COUNT: 1, - CONF_DATA_TYPE: DataType.INT, + CONF_DATA_TYPE: DataType.INT16, CONF_SCALE: 1, CONF_OFFSET: 0, CONF_PRECISION: 0, @@ -271,8 +283,7 @@ async def test_config_wrong_struct_sensor(hass, error_message, mock_modbus, capl ), ( { - CONF_COUNT: 1, - CONF_DATA_TYPE: DataType.INT, + CONF_DATA_TYPE: DataType.INT16, CONF_SCALE: 1, CONF_OFFSET: 13, CONF_PRECISION: 0, @@ -283,8 +294,7 @@ async def test_config_wrong_struct_sensor(hass, error_message, mock_modbus, capl ), ( { - CONF_COUNT: 1, - CONF_DATA_TYPE: DataType.INT, + CONF_DATA_TYPE: DataType.INT16, CONF_SCALE: 3, CONF_OFFSET: 13, CONF_PRECISION: 0, @@ -295,8 +305,7 @@ async def test_config_wrong_struct_sensor(hass, error_message, mock_modbus, capl ), ( { - CONF_COUNT: 1, - CONF_DATA_TYPE: DataType.UINT, + CONF_DATA_TYPE: DataType.UINT16, CONF_SCALE: 3, CONF_OFFSET: 13, CONF_PRECISION: 4, @@ -307,8 +316,7 @@ async def test_config_wrong_struct_sensor(hass, error_message, mock_modbus, capl ), ( { - CONF_COUNT: 1, - CONF_DATA_TYPE: DataType.INT, + CONF_DATA_TYPE: DataType.INT16, CONF_SCALE: 1.5, CONF_OFFSET: 0, CONF_PRECISION: 0, @@ -319,8 +327,7 @@ async def test_config_wrong_struct_sensor(hass, error_message, mock_modbus, capl ), ( { - CONF_COUNT: 1, - CONF_DATA_TYPE: DataType.INT, + CONF_DATA_TYPE: DataType.INT16, CONF_SCALE: "1.5", CONF_OFFSET: "5", CONF_PRECISION: "1", @@ -331,8 +338,7 @@ async def test_config_wrong_struct_sensor(hass, error_message, mock_modbus, capl ), ( { - CONF_COUNT: 1, - CONF_DATA_TYPE: DataType.INT, + CONF_DATA_TYPE: DataType.INT16, CONF_SCALE: 2.4, CONF_OFFSET: 0, CONF_PRECISION: 2, @@ -343,8 +349,7 @@ async def test_config_wrong_struct_sensor(hass, error_message, mock_modbus, capl ), ( { - CONF_COUNT: 1, - CONF_DATA_TYPE: DataType.INT, + CONF_DATA_TYPE: DataType.INT16, CONF_SCALE: 1, CONF_OFFSET: -10.3, CONF_PRECISION: 1, @@ -355,8 +360,7 @@ async def test_config_wrong_struct_sensor(hass, error_message, mock_modbus, capl ), ( { - CONF_COUNT: 2, - CONF_DATA_TYPE: DataType.INT, + CONF_DATA_TYPE: DataType.INT32, CONF_SCALE: 1, CONF_OFFSET: 0, CONF_PRECISION: 0, @@ -367,8 +371,7 @@ async def test_config_wrong_struct_sensor(hass, error_message, mock_modbus, capl ), ( { - CONF_COUNT: 2, - CONF_DATA_TYPE: DataType.UINT, + CONF_DATA_TYPE: DataType.UINT32, CONF_SCALE: 1, CONF_OFFSET: 0, CONF_PRECISION: 0, @@ -379,8 +382,7 @@ async def test_config_wrong_struct_sensor(hass, error_message, mock_modbus, capl ), ( { - CONF_COUNT: 4, - CONF_DATA_TYPE: DataType.UINT, + CONF_DATA_TYPE: DataType.UINT64, CONF_SCALE: 1, CONF_OFFSET: 0, CONF_PRECISION: 0, @@ -391,8 +393,7 @@ async def test_config_wrong_struct_sensor(hass, error_message, mock_modbus, capl ), ( { - CONF_COUNT: 4, - CONF_DATA_TYPE: DataType.UINT, + CONF_DATA_TYPE: DataType.UINT64, CONF_SCALE: 2, CONF_OFFSET: 3, CONF_PRECISION: 0, @@ -403,8 +404,7 @@ async def test_config_wrong_struct_sensor(hass, error_message, mock_modbus, capl ), ( { - CONF_COUNT: 4, - CONF_DATA_TYPE: DataType.UINT, + CONF_DATA_TYPE: DataType.UINT64, CONF_SCALE: 2.0, CONF_OFFSET: 3.0, CONF_PRECISION: 0, @@ -415,9 +415,8 @@ async def test_config_wrong_struct_sensor(hass, error_message, mock_modbus, capl ), ( { - CONF_COUNT: 2, CONF_INPUT_TYPE: CALL_TYPE_REGISTER_INPUT, - CONF_DATA_TYPE: DataType.UINT, + CONF_DATA_TYPE: DataType.UINT32, CONF_SCALE: 1, CONF_OFFSET: 0, CONF_PRECISION: 0, @@ -428,9 +427,8 @@ async def test_config_wrong_struct_sensor(hass, error_message, mock_modbus, capl ), ( { - CONF_COUNT: 2, CONF_INPUT_TYPE: CALL_TYPE_REGISTER_HOLDING, - CONF_DATA_TYPE: DataType.UINT, + CONF_DATA_TYPE: DataType.UINT32, CONF_SCALE: 1, CONF_OFFSET: 0, CONF_PRECISION: 0, @@ -441,9 +439,8 @@ async def test_config_wrong_struct_sensor(hass, error_message, mock_modbus, capl ), ( { - CONF_COUNT: 2, CONF_INPUT_TYPE: CALL_TYPE_REGISTER_HOLDING, - CONF_DATA_TYPE: DataType.FLOAT, + CONF_DATA_TYPE: DataType.FLOAT32, CONF_SCALE: 1, CONF_OFFSET: 0, CONF_PRECISION: 5, @@ -480,9 +477,8 @@ async def test_config_wrong_struct_sensor(hass, error_message, mock_modbus, capl ), ( { - CONF_COUNT: 2, CONF_INPUT_TYPE: CALL_TYPE_REGISTER_INPUT, - CONF_DATA_TYPE: DataType.UINT, + CONF_DATA_TYPE: DataType.UINT32, CONF_SCALE: 1, CONF_OFFSET: 0, CONF_PRECISION: 0, @@ -493,8 +489,7 @@ async def test_config_wrong_struct_sensor(hass, error_message, mock_modbus, capl ), ( { - CONF_COUNT: 1, - CONF_DATA_TYPE: DataType.INT, + CONF_DATA_TYPE: DataType.INT16, CONF_SWAP: CONF_SWAP_NONE, }, [0x0102], @@ -503,8 +498,7 @@ async def test_config_wrong_struct_sensor(hass, error_message, mock_modbus, capl ), ( { - CONF_COUNT: 1, - CONF_DATA_TYPE: DataType.INT, + CONF_DATA_TYPE: DataType.INT16, CONF_SWAP: CONF_SWAP_BYTE, }, [0x0201], @@ -513,8 +507,7 @@ async def test_config_wrong_struct_sensor(hass, error_message, mock_modbus, capl ), ( { - CONF_COUNT: 2, - CONF_DATA_TYPE: DataType.INT, + CONF_DATA_TYPE: DataType.INT32, CONF_SWAP: CONF_SWAP_BYTE, }, [0x0102, 0x0304], @@ -523,8 +516,7 @@ async def test_config_wrong_struct_sensor(hass, error_message, mock_modbus, capl ), ( { - CONF_COUNT: 2, - CONF_DATA_TYPE: DataType.INT, + CONF_DATA_TYPE: DataType.INT32, CONF_SWAP: CONF_SWAP_WORD, }, [0x0102, 0x0304], @@ -533,8 +525,7 @@ async def test_config_wrong_struct_sensor(hass, error_message, mock_modbus, capl ), ( { - CONF_COUNT: 2, - CONF_DATA_TYPE: DataType.INT, + CONF_DATA_TYPE: DataType.INT32, CONF_SWAP: CONF_SWAP_WORD_BYTE, }, [0x0102, 0x0304], @@ -543,7 +534,6 @@ async def test_config_wrong_struct_sensor(hass, error_message, mock_modbus, capl ), ( { - CONF_COUNT: 2, CONF_INPUT_TYPE: CALL_TYPE_REGISTER_INPUT, CONF_DATA_TYPE: DataType.FLOAT32, CONF_PRECISION: 2, @@ -559,6 +549,92 @@ async def test_all_sensor(hass, mock_do_cycle, expected): assert hass.states.get(ENTITY_ID).state == expected +@pytest.mark.parametrize( + "do_config", + [ + { + CONF_SENSORS: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_ADDRESS: 51, + CONF_INPUT_TYPE: CALL_TYPE_REGISTER_HOLDING, + CONF_DATA_TYPE: DataType.UINT32, + CONF_SCALE: 1, + CONF_OFFSET: 0, + CONF_PRECISION: 0, + }, + ], + }, + ], +) +@pytest.mark.parametrize( + "config_addon,register_words,do_exception,expected", + [ + ( + { + CONF_SLAVE_COUNT: 0, + }, + [0x0102, 0x0304], + False, + ["16909060"], + ), + ( + { + CONF_SLAVE_COUNT: 1, + }, + [0x0102, 0x0304, 0x0403, 0x0201], + False, + ["16909060", "67305985"], + ), + ( + { + CONF_SLAVE_COUNT: 3, + }, + [ + 0x0102, + 0x0304, + 0x0506, + 0x0708, + 0x090A, + 0x0B0C, + 0x0D0E, + 0x0F00, + ], + False, + [ + "16909060", + "84281096", + "151653132", + "219025152", + ], + ), + ( + { + CONF_SLAVE_COUNT: 1, + }, + [0x0102, 0x0304, 0x0403, 0x0201], + True, + [STATE_UNAVAILABLE, STATE_UNKNOWN], + ), + ( + { + CONF_SLAVE_COUNT: 1, + }, + [], + False, + [STATE_UNAVAILABLE, STATE_UNKNOWN], + ), + ], +) +async def test_slave_sensor(hass, mock_do_cycle, expected): + """Run test for sensor.""" + assert hass.states.get(ENTITY_ID).state == expected[0] + + for i in range(1, len(expected)): + entity_id = f"{SENSOR_DOMAIN}.{TEST_ENTITY_NAME}_{i}".replace(" ", "_") + assert hass.states.get(entity_id).state == expected[i] + + @pytest.mark.parametrize( "do_config", [ @@ -578,14 +654,12 @@ async def test_all_sensor(hass, mock_do_cycle, expected): [ ( { - CONF_COUNT: 1, CONF_DATA_TYPE: DataType.INT16, }, [7, 9], ), ( { - CONF_COUNT: 2, CONF_DATA_TYPE: DataType.INT32, }, [7], @@ -675,9 +749,8 @@ async def test_lazy_error_sensor(hass, mock_do_cycle, start_expect, end_expect): ), ( { - CONF_COUNT: 1, CONF_PRECISION: 0, - CONF_DATA_TYPE: DataType.INT, + CONF_DATA_TYPE: DataType.INT16, }, [0x0101], "257", @@ -691,7 +764,7 @@ async def test_struct_sensor(hass, mock_do_cycle, expected): @pytest.mark.parametrize( "mock_test_state", - [(State(ENTITY_ID, "117"),)], + [(State(ENTITY_ID, "117"), State(f"{ENTITY_ID}_1", "119"))], indirect=True, ) @pytest.mark.parametrize( @@ -706,6 +779,16 @@ async def test_struct_sensor(hass, mock_do_cycle, expected): } ] }, + { + CONF_SENSORS: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_ADDRESS: 51, + CONF_SCAN_INTERVAL: 0, + CONF_SLAVE_COUNT: 1, + } + ] + }, ], ) async def test_restore_state_sensor(hass, mock_test_state, mock_modbus): @@ -739,3 +822,15 @@ async def test_service_sensor_update(hass, mock_modbus, mock_ha): "homeassistant", "update_entity", {"entity_id": ENTITY_ID}, blocking=True ) assert hass.states.get(ENTITY_ID).state == "32" + + +async def test_no_discovery_info_sensor(hass, caplog): + """Test setup without discovery info.""" + assert SENSOR_DOMAIN not in hass.config.components + assert await async_setup_component( + hass, + SENSOR_DOMAIN, + {SENSOR_DOMAIN: {"platform": MODBUS_DOMAIN}}, + ) + await hass.async_block_till_done() + assert SENSOR_DOMAIN in hass.config.components diff --git a/tests/components/modbus/test_switch.py b/tests/components/modbus/test_switch.py index 006e7ee8d15..4d7a48d120f 100644 --- a/tests/components/modbus/test_switch.py +++ b/tests/components/modbus/test_switch.py @@ -34,6 +34,7 @@ from homeassistant.const import ( STATE_UNAVAILABLE, ) from homeassistant.core import State +from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util from .conftest import TEST_ENTITY_NAME, ReadResult, do_next_cycle @@ -395,3 +396,15 @@ async def test_delay_switch(hass, mock_modbus): async_fire_time_changed(hass, now) await hass.async_block_till_done() assert hass.states.get(ENTITY_ID).state == STATE_ON + + +async def test_no_discovery_info_switch(hass, caplog): + """Test setup without discovery info.""" + assert SWITCH_DOMAIN not in hass.config.components + assert await async_setup_component( + hass, + SWITCH_DOMAIN, + {SWITCH_DOMAIN: {"platform": MODBUS_DOMAIN}}, + ) + await hass.async_block_till_done() + assert SWITCH_DOMAIN in hass.config.components diff --git a/tests/components/modern_forms/fixtures/device_info.json b/tests/components/modern_forms/fixtures/device_info.json index e63f79fd468..96cc54db484 100644 --- a/tests/components/modern_forms/fixtures/device_info.json +++ b/tests/components/modern_forms/fixtures/device_info.json @@ -1,15 +1,15 @@ { - "clientId": "MF_000000000000", - "mac": "AA:BB:CC:DD:EE:FF", - "lightType": "F6IN-120V-R1-30", - "fanType": "1818-56", - "fanMotorType": "DC125X25", - "productionLotNumber": "", - "productSku": "", - "owner": "someone@somewhere.com", - "federatedIdentity": "us-east-1:f3da237b-c19c-4f61-b387-0e6dde2e470b", - "deviceName": "ModernFormsFan", - "firmwareVersion": "01.03.0025", - "mainMcuFirmwareVersion": "01.03.3008", - "firmwareUrl": "" + "clientId": "MF_000000000000", + "mac": "AA:BB:CC:DD:EE:FF", + "lightType": "F6IN-120V-R1-30", + "fanType": "1818-56", + "fanMotorType": "DC125X25", + "productionLotNumber": "", + "productSku": "", + "owner": "someone@somewhere.com", + "federatedIdentity": "us-east-1:f3da237b-c19c-4f61-b387-0e6dde2e470b", + "deviceName": "ModernFormsFan", + "firmwareVersion": "01.03.0025", + "mainMcuFirmwareVersion": "01.03.3008", + "firmwareUrl": "" } diff --git a/tests/components/modern_forms/fixtures/device_info_no_light.json b/tests/components/modern_forms/fixtures/device_info_no_light.json index 5557af57531..2aedd153708 100644 --- a/tests/components/modern_forms/fixtures/device_info_no_light.json +++ b/tests/components/modern_forms/fixtures/device_info_no_light.json @@ -1,14 +1,14 @@ { - "clientId": "MF_000000000000", - "mac": "AA:BB:CC:DD:EE:FF", - "fanType": "1818-56", - "fanMotorType": "DC125X25", - "productionLotNumber": "", - "productSku": "", - "owner": "someone@somewhere.com", - "federatedIdentity": "us-east-1:f3da237b-c19c-4f61-b387-0e6dde2e470b", - "deviceName": "ModernFormsFan", - "firmwareVersion": "01.03.0025", - "mainMcuFirmwareVersion": "01.03.3008", - "firmwareUrl": "" + "clientId": "MF_000000000000", + "mac": "AA:BB:CC:DD:EE:FF", + "fanType": "1818-56", + "fanMotorType": "DC125X25", + "productionLotNumber": "", + "productSku": "", + "owner": "someone@somewhere.com", + "federatedIdentity": "us-east-1:f3da237b-c19c-4f61-b387-0e6dde2e470b", + "deviceName": "ModernFormsFan", + "firmwareVersion": "01.03.0025", + "mainMcuFirmwareVersion": "01.03.3008", + "firmwareUrl": "" } diff --git a/tests/components/modern_forms/fixtures/device_status.json b/tests/components/modern_forms/fixtures/device_status.json index c982f884375..05c5aff30e7 100644 --- a/tests/components/modern_forms/fixtures/device_status.json +++ b/tests/components/modern_forms/fixtures/device_status.json @@ -1,17 +1,17 @@ { - "adaptiveLearning": false, - "awayModeEnabled": false, - "clientId": "MF_000000000000", - "decommission": false, - "factoryReset": false, - "fanDirection": "forward", - "fanOn": true, - "fanSleepTimer": 0, - "fanSpeed": 3, - "lightBrightness": 50, - "lightOn": true, - "lightSleepTimer": 0, - "resetRfPairList": false, - "rfPairModeActive": false, - "schedule": "" + "adaptiveLearning": false, + "awayModeEnabled": false, + "clientId": "MF_000000000000", + "decommission": false, + "factoryReset": false, + "fanDirection": "forward", + "fanOn": true, + "fanSleepTimer": 0, + "fanSpeed": 3, + "lightBrightness": 50, + "lightOn": true, + "lightSleepTimer": 0, + "resetRfPairList": false, + "rfPairModeActive": false, + "schedule": "" } diff --git a/tests/components/modern_forms/fixtures/device_status_no_light.json b/tests/components/modern_forms/fixtures/device_status_no_light.json index ca499b271fb..85f8ab363ee 100644 --- a/tests/components/modern_forms/fixtures/device_status_no_light.json +++ b/tests/components/modern_forms/fixtures/device_status_no_light.json @@ -1,14 +1,14 @@ { - "adaptiveLearning": false, - "awayModeEnabled": false, - "clientId": "MF_000000000000", - "decommission": false, - "factoryReset": false, - "fanDirection": "forward", - "fanOn": true, - "fanSleepTimer": 0, - "fanSpeed": 3, - "resetRfPairList": false, - "rfPairModeActive": false, - "schedule": "" + "adaptiveLearning": false, + "awayModeEnabled": false, + "clientId": "MF_000000000000", + "decommission": false, + "factoryReset": false, + "fanDirection": "forward", + "fanOn": true, + "fanSleepTimer": 0, + "fanSpeed": 3, + "resetRfPairList": false, + "rfPairModeActive": false, + "schedule": "" } diff --git a/tests/components/modern_forms/fixtures/device_status_timers_active.json b/tests/components/modern_forms/fixtures/device_status_timers_active.json index e788b3e5882..ab2115b1c08 100644 --- a/tests/components/modern_forms/fixtures/device_status_timers_active.json +++ b/tests/components/modern_forms/fixtures/device_status_timers_active.json @@ -1,17 +1,17 @@ { - "adaptiveLearning": false, - "awayModeEnabled": false, - "clientId": "MF_000000000000", - "decommission": false, - "factoryReset": false, - "fanDirection": "forward", - "fanOn": true, - "fanSleepTimer": 9999999999, - "fanSpeed": 3, - "lightBrightness": 50, - "lightOn": true, - "lightSleepTimer": 9999999999, - "resetRfPairList": false, - "rfPairModeActive": false, - "schedule": "" + "adaptiveLearning": false, + "awayModeEnabled": false, + "clientId": "MF_000000000000", + "decommission": false, + "factoryReset": false, + "fanDirection": "forward", + "fanOn": true, + "fanSleepTimer": 9999999999, + "fanSpeed": 3, + "lightBrightness": 50, + "lightOn": true, + "lightSleepTimer": 9999999999, + "resetRfPairList": false, + "rfPairModeActive": false, + "schedule": "" } diff --git a/tests/components/moon/conftest.py b/tests/components/moon/conftest.py new file mode 100644 index 00000000000..5c8157f257d --- /dev/null +++ b/tests/components/moon/conftest.py @@ -0,0 +1,27 @@ +"""Fixtures for Moon integration tests.""" +from __future__ import annotations + +from collections.abc import Generator +from unittest.mock import patch + +import pytest + +from homeassistant.components.moon.const import DOMAIN + +from tests.common import MockConfigEntry + + +@pytest.fixture +def mock_config_entry() -> MockConfigEntry: + """Return the default mocked config entry.""" + return MockConfigEntry( + title="Moon", + domain=DOMAIN, + ) + + +@pytest.fixture +def mock_setup_entry() -> Generator[None, None, None]: + """Mock setting up a config entry.""" + with patch("homeassistant.components.moon.async_setup_entry", return_value=True): + yield diff --git a/tests/components/moon/test_config_flow.py b/tests/components/moon/test_config_flow.py new file mode 100644 index 00000000000..4bfb61166aa --- /dev/null +++ b/tests/components/moon/test_config_flow.py @@ -0,0 +1,72 @@ +"""Tests for the Moon config flow.""" +from unittest.mock import MagicMock + +import pytest + +from homeassistant.components.moon.const import DOMAIN +from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER +from homeassistant.const import CONF_NAME +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import ( + RESULT_TYPE_ABORT, + RESULT_TYPE_CREATE_ENTRY, + RESULT_TYPE_FORM, +) + +from tests.common import MockConfigEntry + + +async def test_full_user_flow( + hass: HomeAssistant, + mock_setup_entry: MagicMock, +) -> None: + """Test the full user configuration flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result.get("type") == RESULT_TYPE_FORM + assert result.get("step_id") == SOURCE_USER + assert "flow_id" in result + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={}, + ) + + assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("title") == "Moon" + assert result2.get("data") == {} + + +@pytest.mark.parametrize("source", [SOURCE_USER, SOURCE_IMPORT]) +async def test_single_instance_allowed( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + source: str, +) -> None: + """Test we abort if already setup.""" + mock_config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": source} + ) + + assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("reason") == "single_instance_allowed" + + +async def test_import_flow( + hass: HomeAssistant, + mock_setup_entry: MagicMock, +) -> None: + """Test the import configuration flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={CONF_NAME: "My Moon"}, + ) + + assert result.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result.get("title") == "My Moon" + assert result.get("data") == {} diff --git a/tests/components/moon/test_init.py b/tests/components/moon/test_init.py new file mode 100644 index 00000000000..f0f7e593545 --- /dev/null +++ b/tests/components/moon/test_init.py @@ -0,0 +1,55 @@ +"""Tests for the Moon integration.""" +from unittest.mock import AsyncMock + +from homeassistant.components.moon.const import DOMAIN +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import CONF_NAME +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry + + +async def test_load_unload_config_entry( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, +) -> None: + """Test the Moon configuration entry loading/unloading.""" + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert mock_config_entry.state is ConfigEntryState.LOADED + + await hass.config_entries.async_unload(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert not hass.data.get(DOMAIN) + assert mock_config_entry.state is ConfigEntryState.NOT_LOADED + + +async def test_import_config( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, +) -> None: + """Test Moon being set up from config via import.""" + assert await async_setup_component( + hass, + SENSOR_DOMAIN, + { + SENSOR_DOMAIN: { + "platform": DOMAIN, + CONF_NAME: "My Moon", + } + }, + ) + await hass.async_block_till_done() + + config_entries = hass.config_entries.async_entries(DOMAIN) + assert len(config_entries) == 1 + + entry = config_entries[0] + assert entry.title == "My Moon" + assert entry.unique_id is None + assert entry.data == {} diff --git a/tests/components/moon/test_sensor.py b/tests/components/moon/test_sensor.py index 066620b1051..bb9e5dcc157 100644 --- a/tests/components/moon/test_sensor.py +++ b/tests/components/moon/test_sensor.py @@ -5,10 +5,6 @@ from unittest.mock import patch import pytest -from homeassistant.components.homeassistant import ( - DOMAIN as HA_DOMAIN, - SERVICE_UPDATE_ENTITY, -) from homeassistant.components.moon.sensor import ( MOON_ICONS, STATE_FIRST_QUARTER, @@ -20,9 +16,11 @@ from homeassistant.components.moon.sensor import ( STATE_WAXING_CRESCENT, STATE_WAXING_GIBBOUS, ) -from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.const import ATTR_ICON from homeassistant.core import HomeAssistant -from homeassistant.setup import async_setup_component +from homeassistant.helpers import entity_registry as er + +from tests.common import MockConfigEntry @pytest.mark.parametrize( @@ -39,33 +37,27 @@ from homeassistant.setup import async_setup_component ], ) async def test_moon_day( - hass: HomeAssistant, moon_value: float, native_value: str, icon: str + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + moon_value: float, + native_value: str, + icon: str, ) -> None: """Test the Moon sensor.""" - config = {"sensor": {"platform": "moon"}} - - await async_setup_component(hass, HA_DOMAIN, {}) - assert await async_setup_component(hass, "sensor", config) - await hass.async_block_till_done() - - assert hass.states.get("sensor.moon") + mock_config_entry.add_to_hass(hass) with patch( "homeassistant.components.moon.sensor.moon.phase", return_value=moon_value ): - await async_update_entity(hass, "sensor.moon") + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() state = hass.states.get("sensor.moon") + assert state assert state.state == native_value - assert state.attributes["icon"] == icon + assert state.attributes[ATTR_ICON] == icon - -async def async_update_entity(hass: HomeAssistant, entity_id: str) -> None: - """Run an update action for an entity.""" - await hass.services.async_call( - HA_DOMAIN, - SERVICE_UPDATE_ENTITY, - {ATTR_ENTITY_ID: entity_id}, - blocking=True, - ) - await hass.async_block_till_done() + entity_registry = er.async_get(hass) + entry = entity_registry.async_get("sensor.moon") + assert entry + assert entry.unique_id == mock_config_entry.entry_id diff --git a/tests/components/motion_blinds/test_config_flow.py b/tests/components/motion_blinds/test_config_flow.py index b5e2f8fb717..fce86f5f343 100644 --- a/tests/components/motion_blinds/test_config_flow.py +++ b/tests/components/motion_blinds/test_config_flow.py @@ -5,6 +5,7 @@ from unittest.mock import Mock, patch import pytest from homeassistant import config_entries, data_entry_flow +from homeassistant.components import dhcp from homeassistant.components.motion_blinds import const from homeassistant.components.motion_blinds.config_flow import DEFAULT_GATEWAY_NAME from homeassistant.const import CONF_API_KEY, CONF_HOST @@ -14,7 +15,9 @@ from tests.common import MockConfigEntry TEST_HOST = "1.2.3.4" TEST_HOST2 = "5.6.7.8" TEST_HOST_HA = "9.10.11.12" +TEST_HOST_ANY = "any" TEST_API_KEY = "12ab345c-d67e-8f" +TEST_API_KEY2 = "f8e76dc5-43ba-21" TEST_MAC = "ab:cd:ef:gh" TEST_MAC2 = "ij:kl:mn:op" TEST_DEVICE_LIST = {TEST_MAC: Mock()} @@ -76,6 +79,9 @@ def motion_blinds_connect_fixture(mock_get_source_ip): ), patch( "homeassistant.components.motion_blinds.gateway.MotionGateway.device_list", TEST_DEVICE_LIST, + ), patch( + "homeassistant.components.motion_blinds.gateway.MotionGateway.mac", + TEST_MAC, ), patch( "homeassistant.components.motion_blinds.config_flow.MotionDiscovery.discover", return_value=TEST_DISCOVERY_1, @@ -332,6 +338,36 @@ async def test_config_flow_invalid_interface(hass): assert result["errors"] == {const.CONF_INTERFACE: "invalid_interface"} +async def test_dhcp_flow(hass): + """Successful flow from DHCP discovery.""" + dhcp_data = dhcp.DhcpServiceInfo( + ip=TEST_HOST, + hostname="MOTION_abcdef", + macaddress=TEST_MAC, + ) + + result = await hass.config_entries.flow.async_init( + const.DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=dhcp_data + ) + + assert result["type"] == "form" + assert result["step_id"] == "connect" + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_API_KEY: TEST_API_KEY}, + ) + + assert result["type"] == "create_entry" + assert result["title"] == DEFAULT_GATEWAY_NAME + assert result["data"] == { + CONF_HOST: TEST_HOST, + CONF_API_KEY: TEST_API_KEY, + const.CONF_INTERFACE: TEST_HOST_HA, + } + + async def test_options_flow(hass): """Test specifying non default settings using options flow.""" config_entry = MockConfigEntry( @@ -362,3 +398,45 @@ async def test_options_flow(hass): assert config_entry.options == { const.CONF_WAIT_FOR_PUSH: False, } + + +async def test_change_connection_settings(hass): + """Test changing connection settings by issuing a second user config flow.""" + config_entry = MockConfigEntry( + domain=const.DOMAIN, + unique_id=TEST_MAC, + data={ + CONF_HOST: TEST_HOST, + CONF_API_KEY: TEST_API_KEY, + const.CONF_INTERFACE: TEST_HOST_HA, + }, + title=DEFAULT_GATEWAY_NAME, + ) + config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + const.DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: TEST_HOST2}, + ) + + assert result["type"] == "form" + assert result["step_id"] == "connect" + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_API_KEY: TEST_API_KEY2, const.CONF_INTERFACE: TEST_HOST_ANY}, + ) + + assert result["type"] == "abort" + assert config_entry.data[CONF_HOST] == TEST_HOST2 + assert config_entry.data[CONF_API_KEY] == TEST_API_KEY2 + assert config_entry.data[const.CONF_INTERFACE] == TEST_HOST_ANY diff --git a/tests/components/mqtt/test_config_flow.py b/tests/components/mqtt/test_config_flow.py index 88c6137bf94..f0e02ad8a3a 100644 --- a/tests/components/mqtt/test_config_flow.py +++ b/tests/components/mqtt/test_config_flow.py @@ -34,12 +34,32 @@ def mock_try_connection(): def mock_try_connection_success(): """Mock the try connection method with success.""" + _mid = 1 + + def get_mid(): + nonlocal _mid + _mid += 1 + return _mid + def loop_start(): """Simulate connect on loop start.""" mock_client().on_connect(mock_client, None, None, 0) + def _subscribe(topic, qos=0): + mid = get_mid() + mock_client().on_subscribe(mock_client, 0, mid) + return (0, mid) + + def _unsubscribe(topic): + mid = get_mid() + mock_client().on_unsubscribe(mock_client, 0, mid) + return (0, mid) + with patch("paho.mqtt.client.Client") as mock_client: mock_client().loop_start = loop_start + mock_client().subscribe = _subscribe + mock_client().unsubscribe = _unsubscribe + yield mock_client() diff --git a/tests/components/mqtt/test_device_tracker_discovery.py b/tests/components/mqtt/test_device_tracker_discovery.py index 3b83581b86a..f8ee94b58f9 100644 --- a/tests/components/mqtt/test_device_tracker_discovery.py +++ b/tests/components/mqtt/test_device_tracker_discovery.py @@ -3,6 +3,7 @@ import pytest from homeassistant.components import device_tracker +from homeassistant.components.mqtt.const import DOMAIN as MQTT_DOMAIN from homeassistant.components.mqtt.discovery import ALREADY_DISCOVERED from homeassistant.const import STATE_HOME, STATE_NOT_HOME, STATE_UNKNOWN from homeassistant.setup import async_setup_component @@ -187,7 +188,7 @@ async def test_device_tracker_discovery_update(hass, mqtt_mock, caplog): async def test_cleanup_device_tracker( hass, hass_ws_client, device_reg, entity_reg, mqtt_mock ): - """Test discvered device is cleaned up when removed from registry.""" + """Test discovered device is cleaned up when removed from registry.""" assert await async_setup_component(hass, "config", {}) ws_client = await hass_ws_client(hass) @@ -210,10 +211,12 @@ async def test_cleanup_device_tracker( assert state is not None # Remove MQTT from the device + mqtt_config_entry = hass.config_entries.async_entries(MQTT_DOMAIN)[0] await ws_client.send_json( { "id": 6, - "type": "mqtt/device/remove", + "type": "config/device_registry/remove_config_entry", + "config_entry_id": mqtt_config_entry.entry_id, "device_id": device_entry.id, } ) diff --git a/tests/components/mqtt/test_device_trigger.py b/tests/components/mqtt/test_device_trigger.py index 8a3719f1707..8cfac3bf993 100644 --- a/tests/components/mqtt/test_device_trigger.py +++ b/tests/components/mqtt/test_device_trigger.py @@ -692,10 +692,12 @@ async def test_not_fires_on_mqtt_message_after_remove_from_registry( assert len(calls) == 1 # Remove MQTT from the device + mqtt_config_entry = hass.config_entries.async_entries(DOMAIN)[0] await ws_client.send_json( { "id": 6, - "type": "mqtt/device/remove", + "type": "config/device_registry/remove_config_entry", + "config_entry_id": mqtt_config_entry.entry_id, "device_id": device_entry.id, } ) @@ -1005,10 +1007,12 @@ async def test_cleanup_trigger(hass, hass_ws_client, device_reg, entity_reg, mqt assert triggers[0]["type"] == "foo" # Remove MQTT from the device + mqtt_config_entry = hass.config_entries.async_entries(DOMAIN)[0] await ws_client.send_json( { "id": 6, - "type": "mqtt/device/remove", + "type": "config/device_registry/remove_config_entry", + "config_entry_id": mqtt_config_entry.entry_id, "device_id": device_entry.id, } ) diff --git a/tests/components/mqtt/test_discovery.py b/tests/components/mqtt/test_discovery.py index 463f3d03fff..a1ef6ea477a 100644 --- a/tests/components/mqtt/test_discovery.py +++ b/tests/components/mqtt/test_discovery.py @@ -550,6 +550,58 @@ async def test_rapid_rediscover_unique(hass, mqtt_mock, caplog): assert events[3].data["old_state"] is None +async def test_rapid_reconfigure(hass, mqtt_mock, caplog): + """Test immediate reconfigure of added component.""" + + events = [] + + @ha.callback + def callback(event): + """Verify event got called.""" + events.append(event) + + hass.bus.async_listen(EVENT_STATE_CHANGED, callback) + + # Discovery immediately followed by reconfig + async_fire_mqtt_message(hass, "homeassistant/binary_sensor/bla/config", "") + async_fire_mqtt_message( + hass, + "homeassistant/binary_sensor/bla/config", + '{ "name": "Beer", "state_topic": "test-topic1" }', + ) + async_fire_mqtt_message( + hass, + "homeassistant/binary_sensor/bla/config", + '{ "name": "Milk", "state_topic": "test-topic2" }', + ) + async_fire_mqtt_message( + hass, + "homeassistant/binary_sensor/bla/config", + '{ "name": "Wine", "state_topic": "test-topic3" }', + ) + await hass.async_block_till_done() + + assert len(hass.states.async_entity_ids("binary_sensor")) == 1 + state = hass.states.get("binary_sensor.beer") + assert state is not None + + assert len(events) == 3 + # Add the entity + assert events[0].data["entity_id"] == "binary_sensor.beer" + assert events[0].data["old_state"] is None + assert events[0].data["new_state"].attributes["friendly_name"] == "Beer" + # Update the entity + assert events[1].data["entity_id"] == "binary_sensor.beer" + assert events[1].data["new_state"] is not None + assert events[1].data["old_state"] is not None + assert events[1].data["new_state"].attributes["friendly_name"] == "Milk" + # Update the entity + assert events[2].data["entity_id"] == "binary_sensor.beer" + assert events[2].data["new_state"] is not None + assert events[2].data["old_state"] is not None + assert events[2].data["new_state"].attributes["friendly_name"] == "Wine" + + async def test_duplicate_removal(hass, mqtt_mock, caplog): """Test for a non duplicate component.""" async_fire_mqtt_message( @@ -592,10 +644,12 @@ async def test_cleanup_device(hass, hass_ws_client, device_reg, entity_reg, mqtt assert state is not None # Remove MQTT from the device + mqtt_config_entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0] await ws_client.send_json( { "id": 6, - "type": "mqtt/device/remove", + "type": "config/device_registry/remove_config_entry", + "config_entry_id": mqtt_config_entry.entry_id, "device_id": device_entry.id, } ) @@ -717,10 +771,12 @@ async def test_cleanup_device_multiple_config_entries( assert state is not None # Remove MQTT from the device + mqtt_config_entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0] await ws_client.send_json( { "id": 6, - "type": "mqtt/device/remove", + "type": "config/device_registry/remove_config_entry", + "config_entry_id": mqtt_config_entry.entry_id, "device_id": device_entry.id, } ) diff --git a/tests/components/mqtt/test_fan.py b/tests/components/mqtt/test_fan.py index 5418727ec0e..64b5d272af8 100644 --- a/tests/components/mqtt/test_fan.py +++ b/tests/components/mqtt/test_fan.py @@ -194,7 +194,6 @@ async def test_controlling_state_via_topic(hass, mqtt_mock, caplog): async_fire_mqtt_message(hass, "percentage-state-topic", "rEset_percentage") state = hass.states.get("fan.test") assert state.attributes.get(fan.ATTR_PERCENTAGE) is None - assert state.attributes.get(fan.ATTR_SPEED) is None async_fire_mqtt_message(hass, "state-topic", "None") state = hass.states.get("fan.test") @@ -599,9 +598,8 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock, caplog): assert state.attributes.get(fan.ATTR_PERCENTAGE) == 0 assert state.attributes.get(ATTR_ASSUMED_STATE) - await common.async_set_preset_mode(hass, "fan.test", "low") - assert "not a valid preset mode" in caplog.text - caplog.clear() + with pytest.raises(NotValidPresetModeError): + await common.async_set_preset_mode(hass, "fan.test", "low") await common.async_set_preset_mode(hass, "fan.test", "whoosh") mqtt_mock.async_publish.assert_called_once_with( @@ -799,13 +797,11 @@ async def test_sending_mqtt_commands_and_optimistic_no_legacy(hass, mqtt_mock, c assert state.attributes.get(fan.ATTR_PERCENTAGE) == 0 assert state.attributes.get(ATTR_ASSUMED_STATE) - await common.async_set_preset_mode(hass, "fan.test", "low") - assert "not a valid preset mode" in caplog.text - caplog.clear() + with pytest.raises(NotValidPresetModeError): + await common.async_set_preset_mode(hass, "fan.test", "low") - await common.async_set_preset_mode(hass, "fan.test", "auto") - assert "not a valid preset mode" in caplog.text - caplog.clear() + with pytest.raises(NotValidPresetModeError): + await common.async_set_preset_mode(hass, "fan.test", "auto") await common.async_set_preset_mode(hass, "fan.test", "whoosh") mqtt_mock.async_publish.assert_called_once_with( @@ -938,13 +934,11 @@ async def test_sending_mqtt_command_templates_(hass, mqtt_mock, caplog): assert state.attributes.get(fan.ATTR_PERCENTAGE) == 0 assert state.attributes.get(ATTR_ASSUMED_STATE) - await common.async_set_preset_mode(hass, "fan.test", "low") - assert "not a valid preset mode" in caplog.text - caplog.clear() + with pytest.raises(NotValidPresetModeError): + await common.async_set_preset_mode(hass, "fan.test", "low") - await common.async_set_preset_mode(hass, "fan.test", "medium") - assert "not a valid preset mode" in caplog.text - caplog.clear() + with pytest.raises(NotValidPresetModeError): + await common.async_set_preset_mode(hass, "fan.test", "medium") await common.async_set_preset_mode(hass, "fan.test", "whoosh") mqtt_mock.async_publish.assert_called_once_with( @@ -1035,9 +1029,8 @@ async def test_sending_mqtt_commands_and_optimistic_no_percentage_topic( assert state.state == STATE_UNKNOWN assert state.attributes.get(ATTR_ASSUMED_STATE) - await common.async_set_preset_mode(hass, "fan.test", "medium") - assert "not a valid preset mode" in caplog.text - caplog.clear() + with pytest.raises(NotValidPresetModeError): + await common.async_set_preset_mode(hass, "fan.test", "medium") await common.async_set_preset_mode(hass, "fan.test", "whoosh") mqtt_mock.async_publish.assert_called_once_with( @@ -1131,6 +1124,10 @@ async def test_sending_mqtt_commands_and_explicit_optimistic(hass, mqtt_mock, ca with pytest.raises(NotValidPresetModeError): await common.async_turn_on(hass, "fan.test", preset_mode="auto") + assert mqtt_mock.async_publish.call_count == 1 + # We can turn on, but the invalid preset mode will raise + mqtt_mock.async_publish.assert_any_call("command-topic", "ON", 0, False) + mqtt_mock.async_publish.reset_mock() await common.async_turn_on(hass, "fan.test", preset_mode="whoosh") assert mqtt_mock.async_publish.call_count == 2 @@ -1259,13 +1256,11 @@ async def test_sending_mqtt_commands_and_explicit_optimistic(hass, mqtt_mock, ca with pytest.raises(MultipleInvalid): await common.async_set_percentage(hass, "fan.test", 101) - await common.async_set_preset_mode(hass, "fan.test", "low") - assert "not a valid preset mode" in caplog.text - caplog.clear() + with pytest.raises(NotValidPresetModeError): + await common.async_set_preset_mode(hass, "fan.test", "low") - await common.async_set_preset_mode(hass, "fan.test", "medium") - assert "not a valid preset mode" in caplog.text - caplog.clear() + with pytest.raises(NotValidPresetModeError): + await common.async_set_preset_mode(hass, "fan.test", "medium") await common.async_set_preset_mode(hass, "fan.test", "whoosh") mqtt_mock.async_publish.assert_called_once_with( @@ -1285,9 +1280,8 @@ async def test_sending_mqtt_commands_and_explicit_optimistic(hass, mqtt_mock, ca assert state.state == STATE_OFF assert state.attributes.get(ATTR_ASSUMED_STATE) - await common.async_set_preset_mode(hass, "fan.test", "freaking-high") - assert "not a valid preset mode" in caplog.text - caplog.clear() + with pytest.raises(NotValidPresetModeError): + await common.async_set_preset_mode(hass, "fan.test", "freaking-high") mqtt_mock.async_publish.reset_mock() state = hass.states.get("fan.test") diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index 7296d4e8101..03874ed331e 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -12,7 +12,7 @@ import voluptuous as vol import yaml from homeassistant import config as hass_config -from homeassistant.components import mqtt, websocket_api +from homeassistant.components import mqtt from homeassistant.components.mqtt import debug_info from homeassistant.components.mqtt.mixins import MQTT_ENTITY_DEVICE_INFO_SCHEMA from homeassistant.components.mqtt.models import ReceiveMessage @@ -23,7 +23,7 @@ from homeassistant.const import ( TEMP_CELSIUS, ) import homeassistant.core as ha -from homeassistant.core import CoreState, callback +from homeassistant.core import CoreState, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr, template from homeassistant.helpers.entity import Entity @@ -1248,7 +1248,8 @@ async def test_setup_override_configuration(hass, caplog, tmp_path): await hass.async_block_till_done() assert ( - "Data in your configuration entry is going to override your configuration.yaml:" + "Deprecated configuration settings found in configuration.yaml. " + "These settings from your configuration entry will override:" in caplog.text ) @@ -1619,6 +1620,57 @@ async def test_setup_entry_with_config_override(hass, device_reg, mqtt_client_mo assert device_entry is not None +async def test_update_incomplete_entry( + hass: HomeAssistant, device_reg, mqtt_client_mock, caplog +): + """Test if the MQTT component loads when config entry data is incomplete.""" + data = ( + '{ "device":{"identifiers":["0AFFD2"]},' + ' "state_topic": "foobar/sensor",' + ' "unique_id": "unique" }' + ) + + # Config entry data is incomplete + entry = MockConfigEntry(domain=mqtt.DOMAIN, data={"port": 1234}) + entry.add_to_hass(hass) + # Mqtt present in yaml config + config = {"broker": "yaml_broker"} + await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) + await hass.async_block_till_done() + + # Config entry data should now be updated + assert entry.data == { + "port": 1234, + "broker": "yaml_broker", + } + # Warnings about broker deprecated, but not about other keys with default values + assert ( + "The 'broker' option is deprecated, please remove it from your configuration" + in caplog.text + ) + assert ( + "Deprecated configuration settings found in configuration.yaml. These settings " + "from your configuration entry will override: {'broker': 'yaml_broker'}" + in caplog.text + ) + + # Discover a device to verify the entry was setup correctly + async_fire_mqtt_message(hass, "homeassistant/sensor/bla/config", data) + await hass.async_block_till_done() + + device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}) + assert device_entry is not None + + +async def test_fail_no_broker(hass, device_reg, mqtt_client_mock, caplog): + """Test if the MQTT component loads when broker configuration is missing.""" + # Config entry data is incomplete + entry = MockConfigEntry(domain=mqtt.DOMAIN, data={}) + entry.add_to_hass(hass) + assert not await hass.config_entries.async_setup(entry.entry_id) + assert "MQTT broker is not configured, please configure it" in caplog.text + + @pytest.mark.no_fail_on_log_exception async def test_message_callback_exception_gets_logged(hass, caplog, mqtt_mock): """Test exception raised by message handler.""" @@ -1647,6 +1699,7 @@ async def test_mqtt_ws_subscription(hass, hass_ws_client, mqtt_mock): async_fire_mqtt_message(hass, "test-topic", "test1") async_fire_mqtt_message(hass, "test-topic", "test2") + async_fire_mqtt_message(hass, "test-topic", b"\xDE\xAD\xBE\xEF") response = await client.receive_json() assert response["event"]["topic"] == "test-topic" @@ -1656,6 +1709,10 @@ async def test_mqtt_ws_subscription(hass, hass_ws_client, mqtt_mock): assert response["event"]["topic"] == "test-topic" assert response["event"]["payload"] == "test2" + response = await client.receive_json() + assert response["event"]["topic"] == "test-topic" + assert response["event"]["payload"] == "b'\\xde\\xad\\xbe\\xef'" + # Unsubscribe await client.send_json({"id": 8, "type": "unsubscribe_events", "subscription": 5}) response = await client.receive_json() @@ -1698,6 +1755,8 @@ async def test_mqtt_ws_remove_discovered_device( hass, device_reg, entity_reg, hass_ws_client, mqtt_mock ): """Test MQTT websocket device removal.""" + assert await async_setup_component(hass, "config", {}) + data = ( '{ "device":{"identifiers":["0AFFD2"]},' ' "state_topic": "foobar/sensor",' @@ -1712,8 +1771,14 @@ async def test_mqtt_ws_remove_discovered_device( assert device_entry is not None client = await hass_ws_client(hass) + mqtt_config_entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0] await client.send_json( - {"id": 5, "type": "mqtt/device/remove", "device_id": device_entry.id} + { + "id": 5, + "type": "config/device_registry/remove_config_entry", + "config_entry_id": mqtt_config_entry.entry_id, + "device_id": device_entry.id, + } ) response = await client.receive_json() assert response["success"] @@ -1723,91 +1788,6 @@ async def test_mqtt_ws_remove_discovered_device( assert device_entry is None -async def test_mqtt_ws_remove_discovered_device_twice( - hass, device_reg, hass_ws_client, mqtt_mock -): - """Test MQTT websocket device removal.""" - data = ( - '{ "device":{"identifiers":["0AFFD2"]},' - ' "state_topic": "foobar/sensor",' - ' "unique_id": "unique" }' - ) - - async_fire_mqtt_message(hass, "homeassistant/sensor/bla/config", data) - await hass.async_block_till_done() - - device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}) - assert device_entry is not None - - client = await hass_ws_client(hass) - await client.send_json( - {"id": 5, "type": "mqtt/device/remove", "device_id": device_entry.id} - ) - response = await client.receive_json() - assert response["success"] - - await client.send_json( - {"id": 6, "type": "mqtt/device/remove", "device_id": device_entry.id} - ) - response = await client.receive_json() - assert not response["success"] - assert response["error"]["code"] == websocket_api.const.ERR_NOT_FOUND - - -async def test_mqtt_ws_remove_discovered_device_same_topic( - hass, device_reg, hass_ws_client, mqtt_mock -): - """Test MQTT websocket device removal.""" - data = ( - '{ "device":{"identifiers":["0AFFD2"]},' - ' "state_topic": "foobar/sensor",' - ' "availability_topic": "foobar/sensor",' - ' "unique_id": "unique" }' - ) - - async_fire_mqtt_message(hass, "homeassistant/sensor/bla/config", data) - await hass.async_block_till_done() - - device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}) - assert device_entry is not None - - client = await hass_ws_client(hass) - await client.send_json( - {"id": 5, "type": "mqtt/device/remove", "device_id": device_entry.id} - ) - response = await client.receive_json() - assert response["success"] - - await client.send_json( - {"id": 6, "type": "mqtt/device/remove", "device_id": device_entry.id} - ) - response = await client.receive_json() - assert not response["success"] - assert response["error"]["code"] == websocket_api.const.ERR_NOT_FOUND - - -async def test_mqtt_ws_remove_non_mqtt_device( - hass, device_reg, hass_ws_client, mqtt_mock -): - """Test MQTT websocket device removal of device belonging to other domain.""" - config_entry = MockConfigEntry(domain="test") - config_entry.add_to_hass(hass) - - device_entry = device_reg.async_get_or_create( - config_entry_id=config_entry.entry_id, - connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, - ) - assert device_entry is not None - - client = await hass_ws_client(hass) - await client.send_json( - {"id": 5, "type": "mqtt/device/remove", "device_id": device_entry.id} - ) - response = await client.receive_json() - assert not response["success"] - assert response["error"]["code"] == websocket_api.const.ERR_NOT_FOUND - - async def test_mqtt_ws_get_device_debug_info( hass, device_reg, hass_ws_client, mqtt_mock ): diff --git a/tests/components/mqtt/test_switch.py b/tests/components/mqtt/test_switch.py index a458ac03baa..a7dd0d8c31e 100644 --- a/tests/components/mqtt/test_switch.py +++ b/tests/components/mqtt/test_switch.py @@ -5,7 +5,6 @@ from unittest.mock import patch import pytest from homeassistant.components import switch -from homeassistant.components.mqtt.switch import MQTT_SWITCH_ATTRIBUTES_BLOCKED from homeassistant.const import ( ATTR_ASSUMED_STATE, ATTR_DEVICE_CLASS, @@ -292,7 +291,7 @@ async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): async def test_setting_blocked_attribute_via_mqtt_json_message(hass, mqtt_mock): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_blocked_attribute_via_mqtt_json_message( - hass, mqtt_mock, switch.DOMAIN, DEFAULT_CONFIG, MQTT_SWITCH_ATTRIBUTES_BLOCKED + hass, mqtt_mock, switch.DOMAIN, DEFAULT_CONFIG, {} ) diff --git a/tests/components/mqtt/test_tag.py b/tests/components/mqtt/test_tag.py index 7d3b4f2e1b2..99e2fffc085 100644 --- a/tests/components/mqtt/test_tag.py +++ b/tests/components/mqtt/test_tag.py @@ -6,6 +6,7 @@ from unittest.mock import ANY, patch import pytest from homeassistant.components.device_automation import DeviceAutomationType +from homeassistant.components.mqtt.const import DOMAIN as MQTT_DOMAIN from homeassistant.helpers import device_registry as dr from homeassistant.setup import async_setup_component @@ -378,10 +379,12 @@ async def test_not_fires_on_mqtt_message_after_remove_from_registry( tag_mock.assert_called_once_with(ANY, DEFAULT_TAG_ID, device_entry.id) # Remove MQTT from the device + mqtt_config_entry = hass.config_entries.async_entries(MQTT_DOMAIN)[0] await ws_client.send_json( { "id": 6, - "type": "mqtt/device/remove", + "type": "config/device_registry/remove_config_entry", + "config_entry_id": mqtt_config_entry.entry_id, "device_id": device_entry.id, } ) @@ -537,10 +540,12 @@ async def test_cleanup_tag(hass, hass_ws_client, device_reg, entity_reg, mqtt_mo mqtt_mock.async_publish.assert_not_called() # Remove MQTT from the device + mqtt_config_entry = hass.config_entries.async_entries(MQTT_DOMAIN)[0] await ws_client.send_json( { "id": 6, - "type": "mqtt/device/remove", + "type": "config/device_registry/remove_config_entry", + "config_entry_id": mqtt_config_entry.entry_id, "device_id": device_entry1.id, } ) diff --git a/tests/components/myq/fixtures/devices.json b/tests/components/myq/fixtures/devices.json index 1e731ffe204..0966845e3ca 100644 --- a/tests/components/myq/fixtures/devices.json +++ b/tests/components/myq/fixtures/devices.json @@ -1,163 +1,163 @@ { - "count" : 6, - "href" : "http://api.myqdevice.com/api/v5/accounts/account_id/devices", - "items" : [ - { - "device_type" : "ethernetgateway", - "created_date" : "2020-02-10T22:54:58.423", - "href" : "http://api.myqdevice.com/api/v5/accounts/account_id/devices/gateway_serial", - "device_family" : "gateway", - "name" : "Happy place", - "device_platform" : "myq", - "state" : { - "homekit_enabled" : false, - "pending_bootload_abandoned" : false, - "online" : true, - "last_status" : "2020-03-30T02:49:46.4121303Z", - "physical_devices" : [], - "firmware_version" : "1.6", - "learn_mode" : false, - "learn" : "http://api.myqdevice.com/api/v5/accounts/account_id/devices/gateway_serial/learn", - "homekit_capable" : false, - "updated_date" : "2020-03-30T02:49:46.4171299Z" - }, - "serial_number" : "gateway_serial" + "count": 6, + "href": "http://api.myqdevice.com/api/v5/accounts/account_id/devices", + "items": [ + { + "device_type": "ethernetgateway", + "created_date": "2020-02-10T22:54:58.423", + "href": "http://api.myqdevice.com/api/v5/accounts/account_id/devices/gateway_serial", + "device_family": "gateway", + "name": "Happy place", + "device_platform": "myq", + "state": { + "homekit_enabled": false, + "pending_bootload_abandoned": false, + "online": true, + "last_status": "2020-03-30T02:49:46.4121303Z", + "physical_devices": [], + "firmware_version": "1.6", + "learn_mode": false, + "learn": "http://api.myqdevice.com/api/v5/accounts/account_id/devices/gateway_serial/learn", + "homekit_capable": false, + "updated_date": "2020-03-30T02:49:46.4171299Z" }, - { - "serial_number" : "gate_serial", - "state" : { - "report_ajar" : false, - "aux_relay_delay" : "00:00:00", - "is_unattended_close_allowed" : true, - "door_ajar_interval" : "00:00:00", - "aux_relay_behavior" : "None", - "last_status" : "2020-03-30T02:47:40.2794038Z", - "online" : true, - "rex_fires_door" : false, - "close" : "http://api.myqdevice.com/api/v5/accounts/account_id/devices/gate_serial/close", - "invalid_shutout_period" : "00:00:00", - "invalid_credential_window" : "00:00:00", - "use_aux_relay" : false, - "command_channel_report_status" : false, - "last_update" : "2020-03-28T23:07:39.5611776Z", - "door_state" : "closed", - "max_invalid_attempts" : 0, - "open" : "http://api.myqdevice.com/api/v5/accounts/account_id/devices/gate_serial/open", - "passthrough_interval" : "00:00:00", - "control_from_browser" : false, - "report_forced" : false, - "is_unattended_open_allowed" : true - }, - "parent_device_id" : "gateway_serial", - "name" : "Gate", - "device_platform" : "myq", - "device_family" : "garagedoor", - "parent_device" : "http://api.myqdevice.com/api/v5/accounts/account_id/devices/gateway_serial", - "href" : "http://api.myqdevice.com/api/v5/accounts/account_id/devices/gate_serial", - "device_type" : "gate", - "created_date" : "2020-02-10T22:54:58.423" + "serial_number": "gateway_serial" + }, + { + "serial_number": "gate_serial", + "state": { + "report_ajar": false, + "aux_relay_delay": "00:00:00", + "is_unattended_close_allowed": true, + "door_ajar_interval": "00:00:00", + "aux_relay_behavior": "None", + "last_status": "2020-03-30T02:47:40.2794038Z", + "online": true, + "rex_fires_door": false, + "close": "http://api.myqdevice.com/api/v5/accounts/account_id/devices/gate_serial/close", + "invalid_shutout_period": "00:00:00", + "invalid_credential_window": "00:00:00", + "use_aux_relay": false, + "command_channel_report_status": false, + "last_update": "2020-03-28T23:07:39.5611776Z", + "door_state": "closed", + "max_invalid_attempts": 0, + "open": "http://api.myqdevice.com/api/v5/accounts/account_id/devices/gate_serial/open", + "passthrough_interval": "00:00:00", + "control_from_browser": false, + "report_forced": false, + "is_unattended_open_allowed": true }, - { - "parent_device" : "http://api.myqdevice.com/api/v5/accounts/account_id/devices/gateway_serial", - "href" : "http://api.myqdevice.com/api/v5/accounts/account_id/devices/large_garage_serial", - "device_type" : "wifigaragedooropener", - "created_date" : "2020-02-10T22:55:25.863", - "device_platform" : "myq", - "name" : "Large Garage Door", - "device_family" : "garagedoor", - "serial_number" : "large_garage_serial", - "state" : { - "report_forced" : false, - "is_unattended_open_allowed" : true, - "passthrough_interval" : "00:00:00", - "control_from_browser" : false, - "attached_work_light_error_present" : false, - "max_invalid_attempts" : 0, - "open" : "http://api.myqdevice.com/api/v5/accounts/account_id/devices/large_garage_serial/open", - "command_channel_report_status" : false, - "last_update" : "2020-03-28T23:58:55.5906643Z", - "door_state" : "closed", - "invalid_shutout_period" : "00:00:00", - "use_aux_relay" : false, - "invalid_credential_window" : "00:00:00", - "rex_fires_door" : false, - "close" : "http://api.myqdevice.com/api/v5/accounts/account_id/devices/large_garage_serial/close", - "online" : true, - "last_status" : "2020-03-30T02:49:46.4121303Z", - "aux_relay_behavior" : "None", - "door_ajar_interval" : "00:00:00", - "gdo_lock_connected" : false, - "report_ajar" : false, - "aux_relay_delay" : "00:00:00", - "is_unattended_close_allowed" : true - }, - "parent_device_id" : "gateway_serial" + "parent_device_id": "gateway_serial", + "name": "Gate", + "device_platform": "myq", + "device_family": "garagedoor", + "parent_device": "http://api.myqdevice.com/api/v5/accounts/account_id/devices/gateway_serial", + "href": "http://api.myqdevice.com/api/v5/accounts/account_id/devices/gate_serial", + "device_type": "gate", + "created_date": "2020-02-10T22:54:58.423" + }, + { + "parent_device": "http://api.myqdevice.com/api/v5/accounts/account_id/devices/gateway_serial", + "href": "http://api.myqdevice.com/api/v5/accounts/account_id/devices/large_garage_serial", + "device_type": "wifigaragedooropener", + "created_date": "2020-02-10T22:55:25.863", + "device_platform": "myq", + "name": "Large Garage Door", + "device_family": "garagedoor", + "serial_number": "large_garage_serial", + "state": { + "report_forced": false, + "is_unattended_open_allowed": true, + "passthrough_interval": "00:00:00", + "control_from_browser": false, + "attached_work_light_error_present": false, + "max_invalid_attempts": 0, + "open": "http://api.myqdevice.com/api/v5/accounts/account_id/devices/large_garage_serial/open", + "command_channel_report_status": false, + "last_update": "2020-03-28T23:58:55.5906643Z", + "door_state": "closed", + "invalid_shutout_period": "00:00:00", + "use_aux_relay": false, + "invalid_credential_window": "00:00:00", + "rex_fires_door": false, + "close": "http://api.myqdevice.com/api/v5/accounts/account_id/devices/large_garage_serial/close", + "online": true, + "last_status": "2020-03-30T02:49:46.4121303Z", + "aux_relay_behavior": "None", + "door_ajar_interval": "00:00:00", + "gdo_lock_connected": false, + "report_ajar": false, + "aux_relay_delay": "00:00:00", + "is_unattended_close_allowed": true }, - { - "serial_number" : "small_garage_serial", - "state" : { - "last_status" : "2020-03-30T02:48:45.7501595Z", - "online" : true, - "report_ajar" : false, - "aux_relay_delay" : "00:00:00", - "is_unattended_close_allowed" : true, - "gdo_lock_connected" : false, - "door_ajar_interval" : "00:00:00", - "aux_relay_behavior" : "None", - "attached_work_light_error_present" : false, - "control_from_browser" : false, - "passthrough_interval" : "00:00:00", - "is_unattended_open_allowed" : true, - "report_forced" : false, - "close" : "http://api.myqdevice.com/api/v5/accounts/account_id/devices/small_garage_serial/close", - "rex_fires_door" : false, - "invalid_credential_window" : "00:00:00", - "use_aux_relay" : false, - "invalid_shutout_period" : "00:00:00", - "door_state" : "closed", - "last_update" : "2020-03-26T15:45:31.4713796Z", - "command_channel_report_status" : false, - "open" : "http://api.myqdevice.com/api/v5/accounts/account_id/devices/small_garage_serial/open", - "max_invalid_attempts" : 0 - }, - "parent_device_id" : "gateway_serial", - "device_platform" : "myq", - "name" : "Small Garage Door", - "device_family" : "garagedoor", - "parent_device" : "http://api.myqdevice.com/api/v5/accounts/account_id/devices/gateway_serial", - "href" : "http://api.myqdevice.com/api/v5/accounts/account_id/devices/small_garage_serial", - "device_type" : "wifigaragedooropener", - "created_date" : "2020-02-10T23:11:47.487" + "parent_device_id": "gateway_serial" + }, + { + "serial_number": "small_garage_serial", + "state": { + "last_status": "2020-03-30T02:48:45.7501595Z", + "online": true, + "report_ajar": false, + "aux_relay_delay": "00:00:00", + "is_unattended_close_allowed": true, + "gdo_lock_connected": false, + "door_ajar_interval": "00:00:00", + "aux_relay_behavior": "None", + "attached_work_light_error_present": false, + "control_from_browser": false, + "passthrough_interval": "00:00:00", + "is_unattended_open_allowed": true, + "report_forced": false, + "close": "http://api.myqdevice.com/api/v5/accounts/account_id/devices/small_garage_serial/close", + "rex_fires_door": false, + "invalid_credential_window": "00:00:00", + "use_aux_relay": false, + "invalid_shutout_period": "00:00:00", + "door_state": "closed", + "last_update": "2020-03-26T15:45:31.4713796Z", + "command_channel_report_status": false, + "open": "http://api.myqdevice.com/api/v5/accounts/account_id/devices/small_garage_serial/open", + "max_invalid_attempts": 0 }, - { - "serial_number" : "garage_light_off", - "state" : { - "last_status" : "2020-03-30T02:48:45.7501595Z", - "online" : true, - "lamp_state" : "off", - "last_update" : "2020-03-26T15:45:31.4713796Z" - }, - "parent_device_id" : "gateway_serial", - "device_platform" : "myq", - "name" : "Garage Door Light Off", - "device_family" : "lamp", - "device_type" : "lamp", - "created_date" : "2020-02-10T23:11:47.487" + "parent_device_id": "gateway_serial", + "device_platform": "myq", + "name": "Small Garage Door", + "device_family": "garagedoor", + "parent_device": "http://api.myqdevice.com/api/v5/accounts/account_id/devices/gateway_serial", + "href": "http://api.myqdevice.com/api/v5/accounts/account_id/devices/small_garage_serial", + "device_type": "wifigaragedooropener", + "created_date": "2020-02-10T23:11:47.487" + }, + { + "serial_number": "garage_light_off", + "state": { + "last_status": "2020-03-30T02:48:45.7501595Z", + "online": true, + "lamp_state": "off", + "last_update": "2020-03-26T15:45:31.4713796Z" }, - { - "serial_number" : "garage_light_on", - "state" : { - "last_status" : "2020-03-30T02:48:45.7501595Z", - "online" : true, - "lamp_state" : "on", - "last_update" : "2020-03-26T15:45:31.4713796Z" - }, - "parent_device_id" : "gateway_serial", - "device_platform" : "myq", - "name" : "Garage Door Light On", - "device_family" : "lamp", - "device_type" : "lamp", - "created_date" : "2020-02-10T23:11:47.487" - } - ] + "parent_device_id": "gateway_serial", + "device_platform": "myq", + "name": "Garage Door Light Off", + "device_family": "lamp", + "device_type": "lamp", + "created_date": "2020-02-10T23:11:47.487" + }, + { + "serial_number": "garage_light_on", + "state": { + "last_status": "2020-03-30T02:48:45.7501595Z", + "online": true, + "lamp_state": "on", + "last_update": "2020-03-26T15:45:31.4713796Z" + }, + "parent_device_id": "gateway_serial", + "device_platform": "myq", + "name": "Garage Door Light On", + "device_family": "lamp", + "device_type": "lamp", + "created_date": "2020-02-10T23:11:47.487" + } + ] } diff --git a/tests/components/mysensors/conftest.py b/tests/components/mysensors/conftest.py index fe98b3f7e0a..54ae88cdccb 100644 --- a/tests/components/mysensors/conftest.py +++ b/tests/components/mysensors/conftest.py @@ -4,7 +4,7 @@ from __future__ import annotations from collections.abc import AsyncGenerator, Callable, Generator import json from typing import Any -from unittest.mock import MagicMock, patch +from unittest.mock import AsyncMock, MagicMock, patch from mysensors import BaseSyncGateway from mysensors.persistence import MySensorsJSONDecoder @@ -63,33 +63,44 @@ async def serial_transport_fixture( """Mock a serial transport.""" with patch( "mysensors.gateway_serial.AsyncTransport", autospec=True - ) as transport_class, patch("mysensors.AsyncTasks", autospec=True) as tasks_class: - tasks = tasks_class.return_value - tasks.persistence = MagicMock + ) as transport_class, patch("mysensors.task.OTAFirmware", autospec=True), patch( + "mysensors.task.load_fw", autospec=True + ), patch( + "mysensors.task.Persistence", autospec=True + ) as persistence_class: + persistence = persistence_class.return_value - mock_gateway_features(tasks, transport_class, gateway_nodes) + mock_gateway_features(persistence, transport_class, gateway_nodes) yield transport_class def mock_gateway_features( - tasks: MagicMock, transport_class: MagicMock, nodes: dict[int, Sensor] + persistence: MagicMock, transport_class: MagicMock, nodes: dict[int, Sensor] ) -> None: """Mock the gateway features.""" - async def mock_start_persistence() -> None: + async def mock_schedule_save_sensors() -> None: """Load nodes from via persistence.""" gateway = transport_class.call_args[0][0] gateway.sensors.update(nodes) - tasks.start_persistence.side_effect = mock_start_persistence + persistence.schedule_save_sensors = AsyncMock( + side_effect=mock_schedule_save_sensors + ) + # For some reason autospeccing does not recognize these methods. + persistence.safe_load_sensors = MagicMock() + persistence.save_sensors = MagicMock() - async def mock_start() -> None: + async def mock_connect() -> None: """Mock the start method.""" + transport.connect_task = MagicMock() gateway = transport_class.call_args[0][0] gateway.on_conn_made(gateway) - tasks.start.side_effect = mock_start + transport = transport_class.return_value + transport.connect_task = None + transport.connect.side_effect = mock_connect @pytest.fixture(name="transport") @@ -98,6 +109,12 @@ def transport_fixture(serial_transport: MagicMock) -> MagicMock: return serial_transport +@pytest.fixture +def transport_write(transport: MagicMock) -> MagicMock: + """Return the transport mock that accepts string messages.""" + return transport.return_value.send + + @pytest.fixture(name="serial_entry") async def serial_entry_fixture(hass: HomeAssistant) -> MockConfigEntry: """Create a config entry for a serial gateway.""" @@ -119,16 +136,28 @@ def config_entry_fixture(serial_entry: MockConfigEntry) -> MockConfigEntry: return serial_entry -@pytest.fixture -async def integration( +@pytest.fixture(name="integration") +async def integration_fixture( hass: HomeAssistant, transport: MagicMock, config_entry: MockConfigEntry -) -> AsyncGenerator[tuple[MockConfigEntry, Callable[[str], None]], None]: +) -> AsyncGenerator[MockConfigEntry, None]: """Set up the mysensors integration with a config entry.""" device = config_entry.data[CONF_DEVICE] config: dict[str, Any] = {DOMAIN: {CONF_GATEWAYS: [{CONF_DEVICE: device}]}} config_entry.add_to_hass(hass) - def receive_message(message_string: str) -> None: + with patch("homeassistant.components.mysensors.device.UPDATE_DELAY", new=0): + await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done() + yield config_entry + + +@pytest.fixture +def receive_message( + transport: MagicMock, integration: MockConfigEntry +) -> Callable[[str], None]: + """Receive a message for the gateway.""" + + def receive_message_callback(message_string: str) -> None: """Receive a message with the transport. The message_string parameter is a string in the MySensors message format. @@ -137,14 +166,13 @@ async def integration( # node_id;child_id;command;ack;type;payload\n gateway.logic(message_string) - with patch("homeassistant.components.mysensors.device.UPDATE_DELAY", new=0): - await async_setup_component(hass, DOMAIN, config) - await hass.async_block_till_done() - yield config_entry, receive_message + return receive_message_callback @pytest.fixture(name="gateway") -def gateway_fixture(transport, integration) -> BaseSyncGateway: +def gateway_fixture( + transport: MagicMock, integration: MockConfigEntry +) -> BaseSyncGateway: """Return a setup gateway.""" return transport.call_args[0][0] @@ -250,3 +278,17 @@ def temperature_sensor( nodes = update_gateway_nodes(gateway_nodes, temperature_sensor_state) node = nodes[1] return node + + +@pytest.fixture(name="text_node_state", scope="session") +def text_node_state_fixture() -> dict: + """Load the text node state.""" + return load_nodes_state("mysensors/text_node_state.json") + + +@pytest.fixture +def text_node(gateway_nodes: dict[int, Sensor], text_node_state: dict) -> Sensor: + """Load the text child node.""" + nodes = update_gateway_nodes(gateway_nodes, text_node_state) + node = nodes[1] + return node diff --git a/tests/components/mysensors/fixtures/distance_sensor_state.json b/tests/components/mysensors/fixtures/distance_sensor_state.json index ff8b246b880..82f77193c9f 100644 --- a/tests/components/mysensors/fixtures/distance_sensor_state.json +++ b/tests/components/mysensors/fixtures/distance_sensor_state.json @@ -1,22 +1,22 @@ { - "1": { - "sensor_id": 1, - "children": { - "1": { - "id": 1, - "type": 15, - "description": "", - "values": { - "13": "15", - "43": "cm" - } - } - }, - "type": 17, - "sketch_name": "Distance Sensor", - "sketch_version": "1.0", - "battery_level": 0, - "protocol_version": "2.3.2", - "heartbeat": 0 - } + "1": { + "sensor_id": 1, + "children": { + "1": { + "id": 1, + "type": 15, + "description": "", + "values": { + "13": "15", + "43": "cm" + } + } + }, + "type": 17, + "sketch_name": "Distance Sensor", + "sketch_version": "1.0", + "battery_level": 0, + "protocol_version": "2.3.2", + "heartbeat": 0 + } } diff --git a/tests/components/mysensors/fixtures/energy_sensor_state.json b/tests/components/mysensors/fixtures/energy_sensor_state.json index 063083c9c1e..ca4ecd68b3f 100644 --- a/tests/components/mysensors/fixtures/energy_sensor_state.json +++ b/tests/components/mysensors/fixtures/energy_sensor_state.json @@ -1,21 +1,21 @@ { - "1": { - "sensor_id": 1, - "children": { - "1": { - "id": 1, - "type": 13, - "description": "", - "values": { - "18": "18000" - } - } - }, - "type": 17, - "sketch_name": "Energy Sensor", - "sketch_version": "1.0", - "battery_level": 0, - "protocol_version": "2.3.2", - "heartbeat": 0 - } + "1": { + "sensor_id": 1, + "children": { + "1": { + "id": 1, + "type": 13, + "description": "", + "values": { + "18": "18000" + } + } + }, + "type": 17, + "sketch_name": "Energy Sensor", + "sketch_version": "1.0", + "battery_level": 0, + "protocol_version": "2.3.2", + "heartbeat": 0 + } } diff --git a/tests/components/mysensors/fixtures/gps_sensor_state.json b/tests/components/mysensors/fixtures/gps_sensor_state.json index 654e30e7271..826a513696b 100644 --- a/tests/components/mysensors/fixtures/gps_sensor_state.json +++ b/tests/components/mysensors/fixtures/gps_sensor_state.json @@ -1,21 +1,21 @@ { - "1": { - "sensor_id": 1, - "children": { - "1": { - "id": 1, - "type": 38, - "description": "", - "values": { - "49": "40.741894,-73.989311,12" - } - } - }, - "type": 17, - "sketch_name": "GPS Sensor", - "sketch_version": "1.0", - "battery_level": 0, - "protocol_version": "2.3.2", - "heartbeat": 0 - } + "1": { + "sensor_id": 1, + "children": { + "1": { + "id": 1, + "type": 38, + "description": "", + "values": { + "49": "40.741894,-73.989311,12" + } + } + }, + "type": 17, + "sketch_name": "GPS Sensor", + "sketch_version": "1.0", + "battery_level": 0, + "protocol_version": "2.3.2", + "heartbeat": 0 + } } diff --git a/tests/components/mysensors/fixtures/power_sensor_state.json b/tests/components/mysensors/fixtures/power_sensor_state.json index 40fcc4e4c74..c7e0f5c3fc2 100644 --- a/tests/components/mysensors/fixtures/power_sensor_state.json +++ b/tests/components/mysensors/fixtures/power_sensor_state.json @@ -1,21 +1,21 @@ { - "1": { - "sensor_id": 1, - "children": { - "1": { - "id": 1, - "type": 13, - "description": "", - "values": { - "17": "1200" - } - } - }, - "type": 17, - "sketch_name": "Power Sensor", - "sketch_version": "1.0", - "battery_level": 0, - "protocol_version": "2.3.2", - "heartbeat": 0 - } + "1": { + "sensor_id": 1, + "children": { + "1": { + "id": 1, + "type": 13, + "description": "", + "values": { + "17": "1200" + } + } + }, + "type": 17, + "sketch_name": "Power Sensor", + "sketch_version": "1.0", + "battery_level": 0, + "protocol_version": "2.3.2", + "heartbeat": 0 + } } diff --git a/tests/components/mysensors/fixtures/sound_sensor_state.json b/tests/components/mysensors/fixtures/sound_sensor_state.json index 35651243250..978d53f9761 100644 --- a/tests/components/mysensors/fixtures/sound_sensor_state.json +++ b/tests/components/mysensors/fixtures/sound_sensor_state.json @@ -1,21 +1,21 @@ { - "1": { - "sensor_id": 1, - "children": { - "1": { - "id": 1, - "type": 33, - "description": "", - "values": { - "37": "10" - } - } - }, - "type": 17, - "sketch_name": "Sound Sensor", - "sketch_version": "1.0", - "battery_level": 0, - "protocol_version": "2.3.2", - "heartbeat": 0 - } + "1": { + "sensor_id": 1, + "children": { + "1": { + "id": 1, + "type": 33, + "description": "", + "values": { + "37": "10" + } + } + }, + "type": 17, + "sketch_name": "Sound Sensor", + "sketch_version": "1.0", + "battery_level": 0, + "protocol_version": "2.3.2", + "heartbeat": 0 + } } diff --git a/tests/components/mysensors/fixtures/temperature_sensor_state.json b/tests/components/mysensors/fixtures/temperature_sensor_state.json index 4367be6a3cd..f962c01c2e4 100644 --- a/tests/components/mysensors/fixtures/temperature_sensor_state.json +++ b/tests/components/mysensors/fixtures/temperature_sensor_state.json @@ -1,21 +1,21 @@ { - "1": { - "sensor_id": 1, - "children": { - "1": { - "id": 1, - "type": 6, - "description": "", - "values": { - "0": "20.0" - } - } - }, - "type": 17, - "sketch_name": "Temperature Sensor", - "sketch_version": "1.0", - "battery_level": 0, - "protocol_version": "2.3.2", - "heartbeat": 0 - } + "1": { + "sensor_id": 1, + "children": { + "1": { + "id": 1, + "type": 6, + "description": "", + "values": { + "0": "20.0" + } + } + }, + "type": 17, + "sketch_name": "Temperature Sensor", + "sketch_version": "1.0", + "battery_level": 0, + "protocol_version": "2.3.2", + "heartbeat": 0 + } } diff --git a/tests/components/mysensors/fixtures/text_node_state.json b/tests/components/mysensors/fixtures/text_node_state.json new file mode 100644 index 00000000000..a36d78b72a8 --- /dev/null +++ b/tests/components/mysensors/fixtures/text_node_state.json @@ -0,0 +1,21 @@ +{ + "1": { + "sensor_id": 1, + "children": { + "1": { + "id": 1, + "type": 36, + "description": "", + "values": { + "47": "test" + } + } + }, + "type": 17, + "sketch_name": "Text Node", + "sketch_version": "1.0", + "battery_level": 0, + "protocol_version": "2.3.2", + "heartbeat": 0 + } +} diff --git a/tests/components/mysensors/test_init.py b/tests/components/mysensors/test_init.py index 6f97c312ec0..bb5d77dc7e3 100644 --- a/tests/components/mysensors/test_init.py +++ b/tests/components/mysensors/test_init.py @@ -1,8 +1,8 @@ """Test function in __init__.py.""" from __future__ import annotations -from collections.abc import Callable -from typing import Any, Awaitable +from collections.abc import Awaitable, Callable +from typing import Any from unittest.mock import patch from aiohttp import ClientWebSocketResponse @@ -359,14 +359,14 @@ async def test_import( async def test_remove_config_entry_device( hass: HomeAssistant, gps_sensor: Sensor, - integration: tuple[MockConfigEntry, Callable[[str], None]], + integration: MockConfigEntry, gateway: BaseSyncGateway, hass_ws_client: Callable[[HomeAssistant], Awaitable[ClientWebSocketResponse]], ) -> None: """Test that a device can be removed ok.""" entity_id = "sensor.gps_sensor_1_1" node_id = 1 - config_entry, _ = integration + config_entry = integration assert await async_setup_component(hass, "config", {}) await hass.async_block_till_done() diff --git a/tests/components/mysensors/test_notify.py b/tests/components/mysensors/test_notify.py new file mode 100644 index 00000000000..e96b463cc78 --- /dev/null +++ b/tests/components/mysensors/test_notify.py @@ -0,0 +1,95 @@ +"""Provide tests for mysensors notify platform.""" +from __future__ import annotations + +from collections.abc import Callable +from unittest.mock import MagicMock, call + +from mysensors.sensor import Sensor + +from homeassistant.components.notify import DOMAIN as NOTIFY_DOMAIN +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +async def test_text_type( + hass: HomeAssistant, + text_node: Sensor, + transport_write: MagicMock, + integration: MockConfigEntry, +) -> None: + """Test a text type child.""" + # Test without target. + await hass.services.async_call( + NOTIFY_DOMAIN, "mysensors", {"message": "Hello World"}, blocking=True + ) + + assert transport_write.call_count == 1 + assert transport_write.call_args == call("1;1;1;0;47;Hello World\n") + + # Test with target. + await hass.services.async_call( + NOTIFY_DOMAIN, + "mysensors", + {"message": "Hello", "target": "Text Node 1 1"}, + blocking=True, + ) + + assert transport_write.call_count == 2 + assert transport_write.call_args == call("1;1;1;0;47;Hello\n") + + transport_write.reset_mock() + + # Test a message longer than 25 characters. + await hass.services.async_call( + NOTIFY_DOMAIN, + "mysensors", + { + "message": "This is a long message that will be split", + "target": "Text Node 1 1", + }, + blocking=True, + ) + + assert transport_write.call_count == 2 + assert transport_write.call_args_list == [ + call("1;1;1;0;47;This is a long message th\n"), + call("1;1;1;0;47;at will be split\n"), + ] + + +async def test_text_type_discovery( + hass: HomeAssistant, + text_node: Sensor, + transport_write: MagicMock, + receive_message: Callable[[str], None], +) -> None: + """Test text type discovery.""" + receive_message("1;2;0;0;36;\n") + receive_message("1;2;1;0;47;test\n") + receive_message("1;2;1;0;47;test2\n") # Test that more than one set message works. + await hass.async_block_till_done() + + # Test targeting the discovered child. + await hass.services.async_call( + NOTIFY_DOMAIN, + "mysensors", + {"message": "Hello", "target": "Text Node 1 2"}, + blocking=True, + ) + + assert transport_write.call_count == 1 + assert transport_write.call_args == call("1;2;1;0;47;Hello\n") + + transport_write.reset_mock() + + # Test targeting all notify children. + await hass.services.async_call( + NOTIFY_DOMAIN, "mysensors", {"message": "Hello World"}, blocking=True + ) + + assert transport_write.call_count == 2 + assert transport_write.call_args_list == [ + call("1;1;1;0;47;Hello World\n"), + call("1;2;1;0;47;Hello World\n"), + ] diff --git a/tests/components/mysensors/test_sensor.py b/tests/components/mysensors/test_sensor.py index 0774d480c98..45fe98b98c7 100644 --- a/tests/components/mysensors/test_sensor.py +++ b/tests/components/mysensors/test_sensor.py @@ -29,11 +29,10 @@ from tests.common import MockConfigEntry async def test_gps_sensor( hass: HomeAssistant, gps_sensor: Sensor, - integration: tuple[MockConfigEntry, Callable[[str], None]], + receive_message: Callable[[str], None], ) -> None: """Test a gps sensor.""" entity_id = "sensor.gps_sensor_1_1" - _, receive_message = integration state = hass.states.get(entity_id) @@ -59,7 +58,7 @@ async def test_gps_sensor( async def test_power_sensor( hass: HomeAssistant, power_sensor: Sensor, - integration: tuple[MockConfigEntry, Callable[[str], None]], + integration: MockConfigEntry, ) -> None: """Test a power sensor.""" entity_id = "sensor.power_sensor_1_1" @@ -76,7 +75,7 @@ async def test_power_sensor( async def test_energy_sensor( hass: HomeAssistant, energy_sensor: Sensor, - integration: tuple[MockConfigEntry, Callable[[str], None]], + integration: MockConfigEntry, ) -> None: """Test an energy sensor.""" entity_id = "sensor.energy_sensor_1_1" @@ -93,7 +92,7 @@ async def test_energy_sensor( async def test_sound_sensor( hass: HomeAssistant, sound_sensor: Sensor, - integration: tuple[MockConfigEntry, Callable[[str], None]], + integration: MockConfigEntry, ) -> None: """Test a sound sensor.""" entity_id = "sensor.sound_sensor_1_1" @@ -109,7 +108,7 @@ async def test_sound_sensor( async def test_distance_sensor( hass: HomeAssistant, distance_sensor: Sensor, - integration: tuple[MockConfigEntry, Callable[[str], None]], + integration: MockConfigEntry, ) -> None: """Test a distance sensor.""" entity_id = "sensor.distance_sensor_1_1" @@ -129,14 +128,13 @@ async def test_distance_sensor( async def test_temperature_sensor( hass: HomeAssistant, temperature_sensor: Sensor, - integration: tuple[MockConfigEntry, Callable[[str], None]], + receive_message: Callable[[str], None], unit_system: UnitSystem, unit: str, ) -> None: """Test a temperature sensor.""" entity_id = "sensor.temperature_sensor_1_1" hass.config.units = unit_system - _, receive_message = integration temperature = "22.0" message_string = f"1;1;1;0;0;{temperature}\n" diff --git a/tests/components/nam/fixtures/diagnostics_data.json b/tests/components/nam/fixtures/diagnostics_data.json index d83e5cc9305..b90e51f34b8 100644 --- a/tests/components/nam/fixtures/diagnostics_data.json +++ b/tests/components/nam/fixtures/diagnostics_data.json @@ -1,24 +1,24 @@ { - "bme280_humidity": 45.7, - "bme280_pressure": 1011, - "bme280_temperature": 7.6, - "bmp180_pressure": 1032, - "bmp180_temperature": 7.6, - "bmp280_pressure": 1022, - "bmp280_temperature": 5.6, - "dht22_humidity": 46.2, - "dht22_temperature": 6.3, - "heca_humidity": 50.0, - "heca_temperature": 8.0, - "mhz14a_carbon_dioxide": 865, - "sds011_p1": 19, - "sds011_p2": 11, - "sht3x_humidity": 34.7, - "sht3x_temperature": 6.3, - "signal": -72, - "sps30_p0": 31, - "sps30_p1": 21, - "sps30_p2": 34, - "sps30_p4": 25, - "uptime": 456987 + "bme280_humidity": 45.7, + "bme280_pressure": 1011, + "bme280_temperature": 7.6, + "bmp180_pressure": 1032, + "bmp180_temperature": 7.6, + "bmp280_pressure": 1022, + "bmp280_temperature": 5.6, + "dht22_humidity": 46.2, + "dht22_temperature": 6.3, + "heca_humidity": 50.0, + "heca_temperature": 8.0, + "mhz14a_carbon_dioxide": 865, + "sds011_p1": 19, + "sds011_p2": 11, + "sht3x_humidity": 34.7, + "sht3x_temperature": 6.3, + "signal": -72, + "sps30_p0": 31, + "sps30_p1": 21, + "sps30_p2": 34, + "sps30_p4": 25, + "uptime": 456987 } diff --git a/tests/components/nanoleaf/test_config_flow.py b/tests/components/nanoleaf/test_config_flow.py index 305f88a2e90..0a2a262ce6f 100644 --- a/tests/components/nanoleaf/test_config_flow.py +++ b/tests/components/nanoleaf/test_config_flow.py @@ -3,7 +3,7 @@ from __future__ import annotations from unittest.mock import AsyncMock, MagicMock, patch -from aionanoleaf import InvalidToken, NanoleafException, Unauthorized, Unavailable +from aionanoleaf import InvalidToken, Unauthorized, Unavailable import pytest from homeassistant import config_entries @@ -302,55 +302,6 @@ async def test_reauth(hass: HomeAssistant) -> None: assert entry.data[CONF_TOKEN] == TEST_TOKEN -async def test_import_config(hass: HomeAssistant) -> None: - """Test configuration import.""" - with patch( - "homeassistant.components.nanoleaf.config_flow.Nanoleaf", - return_value=_mock_nanoleaf(TEST_HOST, TEST_TOKEN), - ), patch( - "homeassistant.components.nanoleaf.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={CONF_HOST: TEST_HOST, CONF_TOKEN: TEST_TOKEN}, - ) - assert result["type"] == "create_entry" - assert result["title"] == TEST_NAME - assert result["data"] == { - CONF_HOST: TEST_HOST, - CONF_TOKEN: TEST_TOKEN, - } - await hass.async_block_till_done() - assert len(mock_setup_entry.mock_calls) == 1 - - -@pytest.mark.parametrize( - "error, reason", - [ - (Unavailable, "cannot_connect"), - (InvalidToken, "invalid_token"), - (Exception, "unknown"), - ], -) -async def test_import_config_error( - hass: HomeAssistant, error: NanoleafException, reason: str -) -> None: - """Test configuration import with errors in setup_finish.""" - with patch( - "homeassistant.components.nanoleaf.config_flow.Nanoleaf.get_info", - side_effect=error, - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={CONF_HOST: TEST_HOST, CONF_TOKEN: TEST_TOKEN}, - ) - assert result["type"] == "abort" - assert result["reason"] == reason - - @pytest.mark.parametrize( "source, type_in_discovery", [ diff --git a/tests/components/nest/common.py b/tests/components/nest/common.py index e80ca84d58f..c2a9c6db157 100644 --- a/tests/components/nest/common.py +++ b/tests/components/nest/common.py @@ -22,8 +22,8 @@ from tests.common import MockConfigEntry # Typing helpers PlatformSetup = Callable[[], Awaitable[None]] -T = TypeVar("T") -YieldFixture = Generator[T, None, None] +_T = TypeVar("_T") +YieldFixture = Generator[_T, None, None] PROJECT_ID = "some-project-id" CLIENT_ID = "some-client-id" diff --git a/tests/components/nest/test_diagnostics.py b/tests/components/nest/test_diagnostics.py index cf6c9c5b20f..becb73b0b33 100644 --- a/tests/components/nest/test_diagnostics.py +++ b/tests/components/nest/test_diagnostics.py @@ -2,73 +2,103 @@ from unittest.mock import patch -from google_nest_sdm.exceptions import SubscriberException +from google_nest_sdm.exceptions import ApiException, SubscriberException import pytest +from homeassistant.components.nest.const import DOMAIN from homeassistant.config_entries import ConfigEntryState +from homeassistant.helpers import device_registry as dr from .common import TEST_CONFIG_LEGACY -from tests.components.diagnostics import get_diagnostics_for_config_entry +from tests.components.diagnostics import ( + get_diagnostics_for_config_entry, + get_diagnostics_for_device, +) + +NEST_DEVICE_ID = "enterprises/project-id/devices/device-id" + +DEVICE_API_DATA = { + "name": NEST_DEVICE_ID, + "type": "sdm.devices.types.THERMOSTAT", + "assignee": "enterprises/project-id/structures/structure-id/rooms/room-id", + "traits": { + "sdm.devices.traits.Info": { + "customName": "My Sensor", + }, + "sdm.devices.traits.Temperature": { + "ambientTemperatureCelsius": 25.1, + }, + "sdm.devices.traits.Humidity": { + "ambientHumidityPercent": 35.0, + }, + }, + "parentRelations": [ + { + "parent": "enterprises/project-id/structures/structure-id/rooms/room-id", + "displayName": "Lobby", + } + ], +} + +DEVICE_DIAGNOSTIC_DATA = { + "data": { + "assignee": "**REDACTED**", + "name": "**REDACTED**", + "parentRelations": [{"displayName": "**REDACTED**", "parent": "**REDACTED**"}], + "traits": { + "sdm.devices.traits.Info": {"customName": "**REDACTED**"}, + "sdm.devices.traits.Humidity": {"ambientHumidityPercent": 35.0}, + "sdm.devices.traits.Temperature": {"ambientTemperatureCelsius": 25.1}, + }, + "type": "sdm.devices.types.THERMOSTAT", + } +} + + +@pytest.fixture +def platforms() -> list[str]: + """Fixture to specify platforms to test.""" + return ["sensor"] async def test_entry_diagnostics( hass, hass_client, create_device, setup_platform, config_entry ): """Test config entry diagnostics.""" - create_device.create( - raw_data={ - "name": "enterprises/project-id/devices/device-id", - "type": "sdm.devices.types.THERMOSTAT", - "assignee": "enterprises/project-id/structures/structure-id/rooms/room-id", - "traits": { - "sdm.devices.traits.Info": { - "customName": "My Sensor", - }, - "sdm.devices.traits.Temperature": { - "ambientTemperatureCelsius": 25.1, - }, - "sdm.devices.traits.Humidity": { - "ambientHumidityPercent": 35.0, - }, - }, - "parentRelations": [ - { - "parent": "enterprises/project-id/structures/structure-id/rooms/room-id", - "displayName": "Lobby", - } - ], - } - ) + create_device.create(raw_data=DEVICE_API_DATA) await setup_platform() assert config_entry.state is ConfigEntryState.LOADED # Test that only non identifiable device information is returned assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == { - "devices": [ - { - "data": { - "assignee": "**REDACTED**", - "name": "**REDACTED**", - "parentRelations": [ - {"displayName": "**REDACTED**", "parent": "**REDACTED**"} - ], - "traits": { - "sdm.devices.traits.Info": {"customName": "**REDACTED**"}, - "sdm.devices.traits.Humidity": {"ambientHumidityPercent": 35.0}, - "sdm.devices.traits.Temperature": { - "ambientTemperatureCelsius": 25.1 - }, - }, - "type": "sdm.devices.types.THERMOSTAT", - } - } - ], + "devices": [DEVICE_DIAGNOSTIC_DATA] } +async def test_device_diagnostics( + hass, hass_client, create_device, setup_platform, config_entry +): + """Test config entry diagnostics.""" + create_device.create(raw_data=DEVICE_API_DATA) + await setup_platform() + assert config_entry.state is ConfigEntryState.LOADED + + device_registry = dr.async_get(hass) + device = device_registry.async_get_device({(DOMAIN, NEST_DEVICE_ID)}) + assert device is not None + + assert ( + await get_diagnostics_for_device(hass, hass_client, config_entry, device) + == DEVICE_DIAGNOSTIC_DATA + ) + + async def test_setup_susbcriber_failure( - hass, hass_client, config_entry, setup_base_platform + hass, + hass_client, + config_entry, + setup_base_platform, ): """Test configuration error.""" with patch( @@ -81,9 +111,35 @@ async def test_setup_susbcriber_failure( assert config_entry.state is ConfigEntryState.SETUP_RETRY - assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == { - "error": "No subscriber configured" - } + assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == {} + + +async def test_device_manager_failure( + hass, + hass_client, + config_entry, + setup_platform, + create_device, +): + """Test configuration error.""" + create_device.create(raw_data=DEVICE_API_DATA) + await setup_platform() + assert config_entry.state is ConfigEntryState.LOADED + + device_registry = dr.async_get(hass) + device = device_registry.async_get_device({(DOMAIN, NEST_DEVICE_ID)}) + assert device is not None + + with patch( + "homeassistant.components.nest.diagnostics._get_nest_devices", + side_effect=ApiException("Device manager failure"), + ): + assert await get_diagnostics_for_config_entry( + hass, hass_client, config_entry + ) == {"error": "Device manager failure"} + assert await get_diagnostics_for_device( + hass, hass_client, config_entry, device + ) == {"error": "Device manager failure"} @pytest.mark.parametrize("nest_test_config", [TEST_CONFIG_LEGACY]) diff --git a/tests/components/nest/test_media_source.py b/tests/components/nest/test_media_source.py index 2361049ecc1..c733d4d3cfe 100644 --- a/tests/components/nest/test_media_source.py +++ b/tests/components/nest/test_media_source.py @@ -327,6 +327,7 @@ async def test_camera_event(hass, auth, hass_client): assert browse.children[0].identifier == device.id assert browse.children[0].title == "Front: Recent Events" assert browse.children[0].can_expand + assert browse.children[0].can_play # Expanding the root does not expand the device assert len(browse.children[0].children) == 0 @@ -371,6 +372,13 @@ async def test_camera_event(hass, auth, hass_client): contents = await response.read() assert contents == IMAGE_BYTES_FROM_EVENT + # Resolving the device id points to the most recent event + media = await media_source.async_resolve_media( + hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}" + ) + assert media.url == f"/api/nest/event_media/{device.id}/{event_identifier}" + assert media.mime_type == "image/jpeg" + async def test_event_order(hass, auth): """Test multiple events are in descending timestamp order.""" @@ -787,6 +795,7 @@ async def test_camera_event_clip_preview(hass, auth, hass_client, mp4): browse.children[0].thumbnail == f"/api/nest/event_media/{device.id}/{event_identifier}/thumbnail" ) + assert browse.children[0].can_play # Browse to the device browse = await media_source.async_browse_media( hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}" @@ -805,7 +814,6 @@ async def test_camera_event_clip_preview(hass, auth, hass_client, mp4): assert not browse.children[0].can_expand assert len(browse.children[0].children) == 0 assert browse.children[0].can_play - # No thumbnail support for mp4 clips yet assert ( browse.children[0].thumbnail == f"/api/nest/event_media/{device.id}/{event_identifier}/thumbnail" @@ -973,6 +981,10 @@ async def test_multiple_devices(hass, auth, hass_client): assert device2 # Very no events have been received yet + browse = await media_source.async_browse_media(hass, f"{const.URI_SCHEME}{DOMAIN}") + assert len(browse.children) == 2 + assert not browse.children[0].can_play + assert not browse.children[1].can_play browse = await media_source.async_browse_media( hass, f"{const.URI_SCHEME}{DOMAIN}/{device1.id}" ) @@ -998,6 +1010,10 @@ async def test_multiple_devices(hass, auth, hass_client): ) await hass.async_block_till_done() + browse = await media_source.async_browse_media(hass, f"{const.URI_SCHEME}{DOMAIN}") + assert len(browse.children) == 2 + assert browse.children[0].can_play + assert not browse.children[1].can_play browse = await media_source.async_browse_media( hass, f"{const.URI_SCHEME}{DOMAIN}/{device1.id}" ) @@ -1020,6 +1036,10 @@ async def test_multiple_devices(hass, auth, hass_client): ) await hass.async_block_till_done() + browse = await media_source.async_browse_media(hass, f"{const.URI_SCHEME}{DOMAIN}") + assert len(browse.children) == 2 + assert browse.children[0].can_play + assert browse.children[1].can_play browse = await media_source.async_browse_media( hass, f"{const.URI_SCHEME}{DOMAIN}/{device1.id}" ) @@ -1427,6 +1447,7 @@ async def test_camera_image_resize(hass, auth, hass_client): browse.children[0].thumbnail == f"/api/nest/event_media/{device.id}/{event_identifier}/thumbnail" ) + assert browse.children[0].can_play # Browse to device. No thumbnail is needed for the device on the device page browse = await media_source.async_browse_media( diff --git a/tests/components/netatmo/fixtures/gethomecoachsdata.json b/tests/components/netatmo/fixtures/gethomecoachsdata.json index 3f9de74bd1a..f8756997993 100644 --- a/tests/components/netatmo/fixtures/gethomecoachsdata.json +++ b/tests/components/netatmo/fixtures/gethomecoachsdata.json @@ -1,202 +1,187 @@ { - "body": { - "devices": [ - { - "_id": "12:34:56:26:69:0c", - "cipher_id": "enc:16:1UqwQlYV5AY2pfyEi5H47dmmFOOL3mCUo+KAkchL4A2CLI5u0e45Xr5jeAswO+XO", - "date_setup": 1544560184, - "last_setup": 1544560184, - "type": "NHC", - "last_status_store": 1558268332, - "firmware": 45, - "last_upgrade": 1544560186, - "wifi_status": 58, - "reachable": false, - "co2_calibrating": false, - "station_name": "Bedroom", - "data_type": [ - "Temperature", - "CO2", - "Humidity", - "Noise", - "Pressure", - "health_idx" - ], - "place": { - "city": "Frankfurt", - "country": "DE", - "timezone": "Europe/Berlin", - "location": [ - 52.516263, - 13.377726 - ] - } - }, - { - "_id": "12:34:56:25:cf:a8", - "cipher_id": "enc:16:A+Jm0yFWBwUyKinFDutPZK7I2PuHN1fqaE9oB/KF+McbFs3oN9CKpR/dYbqL4om2", - "date_setup": 1544562192, - "last_setup": 1544562192, - "type": "NHC", - "last_status_store": 1559198922, - "firmware": 45, - "last_upgrade": 1544562194, - "wifi_status": 41, - "reachable": true, - "co2_calibrating": false, - "station_name": "Kitchen", - "data_type": [ - "Temperature", - "CO2", - "Humidity", - "Noise", - "Pressure", - "health_idx" - ], - "place": { - "city": "Frankfurt", - "country": "DE", - "timezone": "Europe/Berlin", - "location": [ - 52.516263, - 13.377726 - ] - } - }, - { - "_id": "12:34:56:26:65:14", - "cipher_id": "enc:16:7kK6ZzG4L7NgfZZ6+dMvNxw4l6vXu+88SEJkCUklNdPa4KYIHmsfa1moOilEK61i", - "date_setup": 1544564061, - "last_setup": 1544564061, - "type": "NHC", - "last_status_store": 1559067159, - "firmware": 45, - "last_upgrade": 1544564302, - "wifi_status": 66, - "reachable": true, - "co2_calibrating": false, - "station_name": "Livingroom", - "data_type": [ - "Temperature", - "CO2", - "Humidity", - "Noise", - "Pressure", - "health_idx" - ], - "place": { - "city": "Frankfurt", - "country": "DE", - "timezone": "Europe/Berlin", - "location": [ - 52.516263, - 13.377726 - ] - } - }, - { - "_id": "12:34:56:3e:c5:46", - "station_name": "Parents Bedroom", - "date_setup": 1570732241, - "last_setup": 1570732241, - "type": "NHC", - "last_status_store": 1572073818, - "module_name": "Indoor", - "firmware": 45, - "wifi_status": 67, - "reachable": true, - "co2_calibrating": false, - "data_type": [ - "Temperature", - "CO2", - "Humidity", - "Noise", - "Pressure", - "health_idx" - ], - "place": { - "city": "Frankfurt", - "country": "DE", - "timezone": "Europe/Berlin", - "location": [ - 52.516263, - 13.377726 - ] - }, - "dashboard_data": { - "time_utc": 1572073816, - "Temperature": 20.3, - "CO2": 494, - "Humidity": 63, - "Noise": 42, - "Pressure": 1014.5, - "AbsolutePressure": 1004.1, - "health_idx": 1, - "min_temp": 20.3, - "max_temp": 21.6, - "date_max_temp": 1572059333, - "date_min_temp": 1572073816 - } - }, - { - "_id": "12:34:56:26:68:92", - "station_name": "Baby Bedroom", - "date_setup": 1571342643, - "last_setup": 1571342643, - "type": "NHC", - "last_status_store": 1572073995, - "module_name": "Indoor", - "firmware": 45, - "wifi_status": 68, - "reachable": true, - "co2_calibrating": false, - "data_type": [ - "Temperature", - "CO2", - "Humidity", - "Noise", - "Pressure", - "health_idx" - ], - "place": { - "city": "Frankfurt", - "country": "DE", - "timezone": "Europe/Berlin", - "location": [ - 52.516263, - 13.377726 - ] - }, - "dashboard_data": { - "time_utc": 1572073994, - "Temperature": 21.6, - "CO2": 1053, - "Humidity": 66, - "Noise": 45, - "Pressure": 1021.4, - "AbsolutePressure": 1011, - "health_idx": 1, - "min_temp": 20.9, - "max_temp": 21.6, - "date_max_temp": 1572073690, - "date_min_temp": 1572064254 - } - } + "body": { + "devices": [ + { + "_id": "12:34:56:26:69:0c", + "cipher_id": "enc:16:1UqwQlYV5AY2pfyEi5H47dmmFOOL3mCUo+KAkchL4A2CLI5u0e45Xr5jeAswO+XO", + "date_setup": 1544560184, + "last_setup": 1544560184, + "type": "NHC", + "last_status_store": 1558268332, + "firmware": 45, + "last_upgrade": 1544560186, + "wifi_status": 58, + "reachable": false, + "co2_calibrating": false, + "station_name": "Bedroom", + "data_type": [ + "Temperature", + "CO2", + "Humidity", + "Noise", + "Pressure", + "health_idx" ], - "user": { - "mail": "john@doe.com", - "administrative": { - "lang": "de-DE", - "reg_locale": "de-DE", - "country": "DE", - "unit": 0, - "windunit": 0, - "pressureunit": 0, - "feel_like_algo": 0 - } + "place": { + "city": "Frankfurt", + "country": "DE", + "timezone": "Europe/Berlin", + "location": [52.516263, 13.377726] } - }, - "status": "ok", - "time_exec": 0.095954179763794, - "time_server": 1559463229 -} \ No newline at end of file + }, + { + "_id": "12:34:56:25:cf:a8", + "cipher_id": "enc:16:A+Jm0yFWBwUyKinFDutPZK7I2PuHN1fqaE9oB/KF+McbFs3oN9CKpR/dYbqL4om2", + "date_setup": 1544562192, + "last_setup": 1544562192, + "type": "NHC", + "last_status_store": 1559198922, + "firmware": 45, + "last_upgrade": 1544562194, + "wifi_status": 41, + "reachable": true, + "co2_calibrating": false, + "station_name": "Kitchen", + "data_type": [ + "Temperature", + "CO2", + "Humidity", + "Noise", + "Pressure", + "health_idx" + ], + "place": { + "city": "Frankfurt", + "country": "DE", + "timezone": "Europe/Berlin", + "location": [52.516263, 13.377726] + } + }, + { + "_id": "12:34:56:26:65:14", + "cipher_id": "enc:16:7kK6ZzG4L7NgfZZ6+dMvNxw4l6vXu+88SEJkCUklNdPa4KYIHmsfa1moOilEK61i", + "date_setup": 1544564061, + "last_setup": 1544564061, + "type": "NHC", + "last_status_store": 1559067159, + "firmware": 45, + "last_upgrade": 1544564302, + "wifi_status": 66, + "reachable": true, + "co2_calibrating": false, + "station_name": "Livingroom", + "data_type": [ + "Temperature", + "CO2", + "Humidity", + "Noise", + "Pressure", + "health_idx" + ], + "place": { + "city": "Frankfurt", + "country": "DE", + "timezone": "Europe/Berlin", + "location": [52.516263, 13.377726] + } + }, + { + "_id": "12:34:56:3e:c5:46", + "station_name": "Parents Bedroom", + "date_setup": 1570732241, + "last_setup": 1570732241, + "type": "NHC", + "last_status_store": 1572073818, + "module_name": "Indoor", + "firmware": 45, + "wifi_status": 67, + "reachable": true, + "co2_calibrating": false, + "data_type": [ + "Temperature", + "CO2", + "Humidity", + "Noise", + "Pressure", + "health_idx" + ], + "place": { + "city": "Frankfurt", + "country": "DE", + "timezone": "Europe/Berlin", + "location": [52.516263, 13.377726] + }, + "dashboard_data": { + "time_utc": 1572073816, + "Temperature": 20.3, + "CO2": 494, + "Humidity": 63, + "Noise": 42, + "Pressure": 1014.5, + "AbsolutePressure": 1004.1, + "health_idx": 1, + "min_temp": 20.3, + "max_temp": 21.6, + "date_max_temp": 1572059333, + "date_min_temp": 1572073816 + } + }, + { + "_id": "12:34:56:26:68:92", + "station_name": "Baby Bedroom", + "date_setup": 1571342643, + "last_setup": 1571342643, + "type": "NHC", + "last_status_store": 1572073995, + "module_name": "Indoor", + "firmware": 45, + "wifi_status": 68, + "reachable": true, + "co2_calibrating": false, + "data_type": [ + "Temperature", + "CO2", + "Humidity", + "Noise", + "Pressure", + "health_idx" + ], + "place": { + "city": "Frankfurt", + "country": "DE", + "timezone": "Europe/Berlin", + "location": [52.516263, 13.377726] + }, + "dashboard_data": { + "time_utc": 1572073994, + "Temperature": 21.6, + "CO2": 1053, + "Humidity": 66, + "Noise": 45, + "Pressure": 1021.4, + "AbsolutePressure": 1011, + "health_idx": 1, + "min_temp": 20.9, + "max_temp": 21.6, + "date_max_temp": 1572073690, + "date_min_temp": 1572064254 + } + } + ], + "user": { + "mail": "john@doe.com", + "administrative": { + "lang": "de-DE", + "reg_locale": "de-DE", + "country": "DE", + "unit": 0, + "windunit": 0, + "pressureunit": 0, + "feel_like_algo": 0 + } + } + }, + "status": "ok", + "time_exec": 0.095954179763794, + "time_server": 1559463229 +} diff --git a/tests/components/netatmo/fixtures/gethomedata.json b/tests/components/netatmo/fixtures/gethomedata.json index db7d6aa438d..7136b7e0e37 100644 --- a/tests/components/netatmo/fixtures/gethomedata.json +++ b/tests/components/netatmo/fixtures/gethomedata.json @@ -1,318 +1,318 @@ { - "body": { - "homes": [ - { - "id": "91763b24c43d3e344f424e8b", - "name": "MYHOME", - "persons": [ - { - "id": "91827374-7e04-5298-83ad-a0cb8372dff1", - "last_seen": 1557071156, - "out_of_sight": true, - "face": { - "id": "d74fad765b9100ef480720a9", - "version": 1, - "key": "a4a95c24b808a89f8d1730fb69ecdf2bb8b72039d2c69928b029d67fc40cb2d7", - "url": "https://netatmocameraimage.blob.core.windows.net/production/d74fad765b9100ef480720a9a4a95c24b808a89f8d1730fb69ecdf2bb8b72039d2c69928b029d67fc40cb2d7" - }, - "pseudo": "John Doe" - }, - { - "id": "91827375-7e04-5298-83ae-a0cb8372dff2", - "last_seen": 1560600726, - "out_of_sight": true, - "face": { - "id": "d74fad765b9100ef480720a9", - "version": 3, - "key": "a4a95c24b808a89f8d1730039d2c69928b029d67fc40cb2d7fb69ecdf2bb8b72", - "url": "https://netatmocameraimage.blob.core.windows.net/production/d74fad765b9100ef480720a9a4a95c24b808a89f8d1730039d2c69928b029d67fc40cb2d7fb69ecdf2bb8b72" - }, - "pseudo": "Jane Doe" - }, - { - "id": "91827376-7e04-5298-83af-a0cb8372dff3", - "last_seen": 1560626666, - "out_of_sight": false, - "face": { - "id": "d74fad765b9100ef480720a9", - "version": 1, - "key": "a4a95c2d1730fb69ecdf2bb8b72039d2c69928b029d67fc40cb2d74b808a89f8", - "url": "https://netatmocameraimage.blob.core.windows.net/production/d74fad765b9100ef480720a9a4a95c2d1730fb69ecdf2bb8b72039d2c69928b029d67fc40cb2d74b808a89f8" - }, - "pseudo": "Richard Doe" - }, - { - "id": "91827376-7e04-5298-83af-a0cb8372dff4", - "last_seen": 1560621666, - "out_of_sight": true, - "face": { - "id": "d0ef44fad765b980720710a9", - "version": 1, - "key": "ab029da89f84a95c2d1730fb67fc40cb2d74b80869ecdf2bb8b72039d2c69928", - "url": "https://netatmocameraimage.blob.core.windows.net/production/d0ef44fad765b980720710a9ab029da89f84a95c2d1730fb67fc40cb2d74b80869ecdf2bb8b72039d2c69928" - } - } - ], - "place": { - "city": "Frankfurt", - "country": "DE", - "timezone": "Europe/Berlin" - }, - "cameras": [ - { - "id": "12:34:56:00:f1:62", - "type": "NACamera", - "status": "on", - "vpn_url": "https://prodvpn-eu-2.netatmo.net/restricted/10.255.248.91/6d278460699e56180d47ab47169efb31/MpEylTU2MDYzNjRVD-LJxUnIndumKzLboeAwMDqTTg,,", - "is_local": true, - "sd_status": "on", - "alim_status": "on", - "name": "Hall", - "modules": [ - { - "id": "12:34:56:00:f2:f1", - "type": "NIS", - "battery_percent": 84, - "rf": 68, - "status": "no_news", - "monitoring": "on", - "alim_source": "battery", - "tamper_detection_enabled": true, - "name": "Welcome's Siren" - } - ], - "use_pin_code": false, - "last_setup": 1544828430 - }, - { - "id": "12:34:56:00:a5:a4", - "type": "NOC", - "status": "on", - "vpn_url": "https://prodvpn-eu-2.netatmo.net/restricted/10.255.248.91/6d278460699e56180d47ab47169efb31/MpEylTU2MDYzNjRVD-LJxUnIndumKzLboeAwMDqTTw,,", - "is_local": false, - "sd_status": "on", - "alim_status": "on", - "name": "Garden", - "last_setup": 1563737661, - "light_mode_status": "auto" - } - ], - "smokedetectors": [ - { - "id": "12:34:56:00:8b:a2", - "type": "NSD", - "last_setup": 1567261859, - "name": "Hall" - }, - { - "id": "12:34:56:00:8b:ac", - "type": "NSD", - "last_setup": 1567262759, - "name": "Kitchen" - } - ], - "events": [ - { - "id": "a1b2c3d4e5f6abcdef123456", - "type": "person", - "time": 1560604700, - "camera_id": "12:34:56:00:f1:62", - "device_id": "12:34:56:00:f1:62", - "person_id": "91827374-7e04-5298-83ad-a0cb8372dff1", - "video_status": "deleted", - "is_arrival": false, - "message": "John Doe gesehen" - }, - { - "id": "a1b2c3d4e5f6abcdef123457", - "type": "person_away", - "time": 1560602400, - "camera_id": "12:34:56:00:f1:62", - "device_id": "12:34:56:00:f1:62", - "person_id": "91827374-7e04-5298-83ad-a0cb8372dff1", - "message": "John Doe hat das Haus verlassen", - "sub_message": "John Doe gilt als abwesend, da das mit diesem Profil verbundene Telefon den Bereich des Hauses verlassen hat." - }, - { - "id": "a1b2c3d4e5f6abcdef123458", - "type": "person", - "time": 1560601200, - "camera_id": "12:34:56:00:f1:62", - "device_id": "12:34:56:00:f1:62", - "person_id": "91827374-7e04-5298-83ad-a0cb8372dff1", - "video_status": "deleted", - "is_arrival": false, - "message": "John Doe gesehen" - }, - { - "id": "a1b2c3d4e5f6abcdef123459", - "type": "person", - "time": 1560600100, - "camera_id": "12:34:56:00:f1:62", - "device_id": "12:34:56:00:f1:62", - "person_id": "91827375-7e04-5298-83ae-a0cb8372dff2", - "snapshot": { - "id": "d74fad765b9100ef480720a9", - "version": 1, - "key": "a4a95c24b808a89f8d1730039d2c69928b029d67fc40cb2d7fb69ecdf2bb8b72", - "url": "https://netatmocameraimage.blob.core.windows.net/production/d74fad765b9100ef480720a9a4a95c24b808a89f8d1730039d2c69928b029d67fc40cb2d7fb69ecdf2bb8b72" - }, - "video_id": "12345678-36bc-4b9a-9762-5194e707ed51", - "video_status": "available", - "is_arrival": false, - "message": "Jane Doe gesehen" - }, - { - "id": "a1b2c3d4e5f6abcdef12345a", - "type": "person", - "time": 1560603600, - "camera_id": "12:34:56:00:f1:62", - "device_id": "12:34:56:00:f1:62", - "person_id": "91827375-7e04-5298-83ae-a0cb8372dff3", - "snapshot": { - "id": "532dde8d17554c022ab071b8", - "version": 1, - "key": "9fbe490fffacf45b8416241946541b031a004a09b6747feb6c38c3ccbc456b28", - "url": "https://netatmocameraimage.blob.core.windows.net/production/532dde8d17554c022ab071b89fbe490fffacf45b8416241946541b031a004a09b6747feb6c38c3ccbc456b28" - }, - "video_id": "12345678-1234-46cb-ad8f-23d893874099", - "video_status": "available", - "is_arrival": false, - "message": "Bewegung erkannt" - }, - { - "id": "a1b2c3d4e5f6abcdef12345b", - "type": "movement", - "time": 1560506200, - "camera_id": "12:34:56:00:f1:62", - "device_id": "12:34:56:00:f1:62", - "category": "human", - "snapshot": { - "id": "532dde8d17554c022ab071b9", - "version": 1, - "key": "8fbe490fffacf45b8416241946541b031a004a09b6747feb6c38c3ccbc456b28", - "url": "https://netatmocameraimage.blob.core.windows.net/production/532dde8d17554c022ab071b98fbe490fffacf45b8416241946541b031a004a09b6747feb6c38c3ccbc456b28" - }, - "vignette": { - "id": "5dc021b5dea854bd2321707a", - "version": 1, - "key": "58c5a05bd6bd908f6bf368865ef7355231c44215f8eb7ae458c919b2c67b4944", - "url": "https://netatmocameraimage.blob.core.windows.net/production/5dc021b5dea854bd2321707a58c5a05bd6bd908f6bf368865ef7355231c44215f8eb7ae458c919b2c67b4944" - }, - "video_id": "12345678-1234-46cb-ad8f-23d89387409a", - "video_status": "available", - "message": "Bewegung erkannt" - }, - { - "id": "a1b2c3d4e5f6abcdef12345c", - "type": "sound_test", - "time": 1560506210, - "camera_id": "12:34:56:00:8b:a2", - "device_id": "12:34:56:00:8b:a2", - "sub_type": 0, - "message": "Hall: Alarmton erfolgreich getestet" - }, - { - "id": "a1b2c3d4e5f6abcdef12345d", - "type": "wifi_status", - "time": 1560506220, - "camera_id": "12:34:56:00:8b:a2", - "device_id": "12:34:56:00:8b:a2", - "sub_type": 1, - "message": "Hall:WLAN-Verbindung erfolgreich hergestellt" - }, - { - "id": "a1b2c3d4e5f6abcdef12345e", - "type": "outdoor", - "time": 1560643100, - "camera_id": "12:34:56:00:a5:a4", - "device_id": "12:34:56:00:a5:a4", - "video_id": "string", - "video_status": "available", - "event_list": [ - { - "type": "string", - "time": 1560643100, - "offset": 0, - "id": "c81bcf7b-2cfg-4ac9-8455-487ed00c0000", - "message": "Animal détecté", - "snapshot": { - "id": "5715e16849c75xxxx00000000xxxxx", - "version": 1, - "key": "7ac578d05030d0e170643a787ee0a29663dxxx00000xxxxx00000", - "url": "https://netatmocameraimage.blob.core.windows.net/production/1aa" - }, - "vignette": { - "id": "5715e16849c75xxxx00000000xxxxx", - "version": 1, - "key": "7ac578d05030d0e170643a787ee0a29663dxxx00000xxxxx00000", - "url": "https://netatmocameraimage.blob.core.windows.net/production/1aa00000" - } - }, - { - "type": "string", - "time": 1560506222, - "offset": 0, - "id": "c81bcf7b-2cfg-4ac9-8455-487ed00c0000", - "message": "Animal détecté", - "snapshot": { - "filename": "vod\/af74631d-8311-42dc-825b-82e3abeaab09\/events\/c53b-aze7a.jpg" - }, - "vignette": { - "filename": "vod\/af74631d-8311-42dc-825b-82e3abeaab09\/events\/c5.jpg" - } - } - ] - } - ] + "body": { + "homes": [ + { + "id": "91763b24c43d3e344f424e8b", + "name": "MYHOME", + "persons": [ + { + "id": "91827374-7e04-5298-83ad-a0cb8372dff1", + "last_seen": 1557071156, + "out_of_sight": true, + "face": { + "id": "d74fad765b9100ef480720a9", + "version": 1, + "key": "a4a95c24b808a89f8d1730fb69ecdf2bb8b72039d2c69928b029d67fc40cb2d7", + "url": "https://netatmocameraimage.blob.core.windows.net/production/d74fad765b9100ef480720a9a4a95c24b808a89f8d1730fb69ecdf2bb8b72039d2c69928b029d67fc40cb2d7" }, - { - "id": "91763b24c43d3e344f424e8c", - "persons": [], - "place": { - "city": "Frankfurt", - "country": "DE", - "timezone": "Europe/Berlin" - }, - "cameras": [ - { - "id": "12:34:56:00:a5:a5", - "type": "NOC", - "status": "on", - "vpn_url": "https://prodvpn-eu-2.netatmo.net/restricted/10.255.248.91/6d278460699e56180d47ab47169efb31/MpEylTU2MDYzNjRVD-LJxUnIndumKzLboeAwMDqTTz,,", - "is_local": true, - "sd_status": "on", - "alim_status": "on", - "name": "Street", - "last_setup": 1563737561, - "light_mode_status": "auto" - } - ], - "smokedetectors": [] + "pseudo": "John Doe" + }, + { + "id": "91827375-7e04-5298-83ae-a0cb8372dff2", + "last_seen": 1560600726, + "out_of_sight": true, + "face": { + "id": "d74fad765b9100ef480720a9", + "version": 3, + "key": "a4a95c24b808a89f8d1730039d2c69928b029d67fc40cb2d7fb69ecdf2bb8b72", + "url": "https://netatmocameraimage.blob.core.windows.net/production/d74fad765b9100ef480720a9a4a95c24b808a89f8d1730039d2c69928b029d67fc40cb2d7fb69ecdf2bb8b72" }, - { - "id": "91763b24c43d3e344f424e8d", - "persons": [], - "place": { - "city": "Frankfurt", - "country": "DE", - "timezone": "Europe/Berlin" - }, - "cameras": [], - "smokedetectors": [] + "pseudo": "Jane Doe" + }, + { + "id": "91827376-7e04-5298-83af-a0cb8372dff3", + "last_seen": 1560626666, + "out_of_sight": false, + "face": { + "id": "d74fad765b9100ef480720a9", + "version": 1, + "key": "a4a95c2d1730fb69ecdf2bb8b72039d2c69928b029d67fc40cb2d74b808a89f8", + "url": "https://netatmocameraimage.blob.core.windows.net/production/d74fad765b9100ef480720a9a4a95c2d1730fb69ecdf2bb8b72039d2c69928b029d67fc40cb2d74b808a89f8" + }, + "pseudo": "Richard Doe" + }, + { + "id": "91827376-7e04-5298-83af-a0cb8372dff4", + "last_seen": 1560621666, + "out_of_sight": true, + "face": { + "id": "d0ef44fad765b980720710a9", + "version": 1, + "key": "ab029da89f84a95c2d1730fb67fc40cb2d74b80869ecdf2bb8b72039d2c69928", + "url": "https://netatmocameraimage.blob.core.windows.net/production/d0ef44fad765b980720710a9ab029da89f84a95c2d1730fb67fc40cb2d74b80869ecdf2bb8b72039d2c69928" } + } ], - "user": { - "reg_locale": "de-DE", - "lang": "de-DE", - "country": "DE", - "mail": "john@doe.com" + "place": { + "city": "Frankfurt", + "country": "DE", + "timezone": "Europe/Berlin" }, - "global_info": { - "show_tags": true - } + "cameras": [ + { + "id": "12:34:56:00:f1:62", + "type": "NACamera", + "status": "on", + "vpn_url": "https://prodvpn-eu-2.netatmo.net/restricted/10.255.248.91/6d278460699e56180d47ab47169efb31/MpEylTU2MDYzNjRVD-LJxUnIndumKzLboeAwMDqTTg,,", + "is_local": true, + "sd_status": "on", + "alim_status": "on", + "name": "Hall", + "modules": [ + { + "id": "12:34:56:00:f2:f1", + "type": "NIS", + "battery_percent": 84, + "rf": 68, + "status": "no_news", + "monitoring": "on", + "alim_source": "battery", + "tamper_detection_enabled": true, + "name": "Welcome's Siren" + } + ], + "use_pin_code": false, + "last_setup": 1544828430 + }, + { + "id": "12:34:56:00:a5:a4", + "type": "NOC", + "status": "on", + "vpn_url": "https://prodvpn-eu-2.netatmo.net/restricted/10.255.248.91/6d278460699e56180d47ab47169efb31/MpEylTU2MDYzNjRVD-LJxUnIndumKzLboeAwMDqTTw,,", + "is_local": false, + "sd_status": "on", + "alim_status": "on", + "name": "Garden", + "last_setup": 1563737661, + "light_mode_status": "auto" + } + ], + "smokedetectors": [ + { + "id": "12:34:56:00:8b:a2", + "type": "NSD", + "last_setup": 1567261859, + "name": "Hall" + }, + { + "id": "12:34:56:00:8b:ac", + "type": "NSD", + "last_setup": 1567262759, + "name": "Kitchen" + } + ], + "events": [ + { + "id": "a1b2c3d4e5f6abcdef123456", + "type": "person", + "time": 1560604700, + "camera_id": "12:34:56:00:f1:62", + "device_id": "12:34:56:00:f1:62", + "person_id": "91827374-7e04-5298-83ad-a0cb8372dff1", + "video_status": "deleted", + "is_arrival": false, + "message": "John Doe gesehen" + }, + { + "id": "a1b2c3d4e5f6abcdef123457", + "type": "person_away", + "time": 1560602400, + "camera_id": "12:34:56:00:f1:62", + "device_id": "12:34:56:00:f1:62", + "person_id": "91827374-7e04-5298-83ad-a0cb8372dff1", + "message": "John Doe hat das Haus verlassen", + "sub_message": "John Doe gilt als abwesend, da das mit diesem Profil verbundene Telefon den Bereich des Hauses verlassen hat." + }, + { + "id": "a1b2c3d4e5f6abcdef123458", + "type": "person", + "time": 1560601200, + "camera_id": "12:34:56:00:f1:62", + "device_id": "12:34:56:00:f1:62", + "person_id": "91827374-7e04-5298-83ad-a0cb8372dff1", + "video_status": "deleted", + "is_arrival": false, + "message": "John Doe gesehen" + }, + { + "id": "a1b2c3d4e5f6abcdef123459", + "type": "person", + "time": 1560600100, + "camera_id": "12:34:56:00:f1:62", + "device_id": "12:34:56:00:f1:62", + "person_id": "91827375-7e04-5298-83ae-a0cb8372dff2", + "snapshot": { + "id": "d74fad765b9100ef480720a9", + "version": 1, + "key": "a4a95c24b808a89f8d1730039d2c69928b029d67fc40cb2d7fb69ecdf2bb8b72", + "url": "https://netatmocameraimage.blob.core.windows.net/production/d74fad765b9100ef480720a9a4a95c24b808a89f8d1730039d2c69928b029d67fc40cb2d7fb69ecdf2bb8b72" + }, + "video_id": "12345678-36bc-4b9a-9762-5194e707ed51", + "video_status": "available", + "is_arrival": false, + "message": "Jane Doe gesehen" + }, + { + "id": "a1b2c3d4e5f6abcdef12345a", + "type": "person", + "time": 1560603600, + "camera_id": "12:34:56:00:f1:62", + "device_id": "12:34:56:00:f1:62", + "person_id": "91827375-7e04-5298-83ae-a0cb8372dff3", + "snapshot": { + "id": "532dde8d17554c022ab071b8", + "version": 1, + "key": "9fbe490fffacf45b8416241946541b031a004a09b6747feb6c38c3ccbc456b28", + "url": "https://netatmocameraimage.blob.core.windows.net/production/532dde8d17554c022ab071b89fbe490fffacf45b8416241946541b031a004a09b6747feb6c38c3ccbc456b28" + }, + "video_id": "12345678-1234-46cb-ad8f-23d893874099", + "video_status": "available", + "is_arrival": false, + "message": "Bewegung erkannt" + }, + { + "id": "a1b2c3d4e5f6abcdef12345b", + "type": "movement", + "time": 1560506200, + "camera_id": "12:34:56:00:f1:62", + "device_id": "12:34:56:00:f1:62", + "category": "human", + "snapshot": { + "id": "532dde8d17554c022ab071b9", + "version": 1, + "key": "8fbe490fffacf45b8416241946541b031a004a09b6747feb6c38c3ccbc456b28", + "url": "https://netatmocameraimage.blob.core.windows.net/production/532dde8d17554c022ab071b98fbe490fffacf45b8416241946541b031a004a09b6747feb6c38c3ccbc456b28" + }, + "vignette": { + "id": "5dc021b5dea854bd2321707a", + "version": 1, + "key": "58c5a05bd6bd908f6bf368865ef7355231c44215f8eb7ae458c919b2c67b4944", + "url": "https://netatmocameraimage.blob.core.windows.net/production/5dc021b5dea854bd2321707a58c5a05bd6bd908f6bf368865ef7355231c44215f8eb7ae458c919b2c67b4944" + }, + "video_id": "12345678-1234-46cb-ad8f-23d89387409a", + "video_status": "available", + "message": "Bewegung erkannt" + }, + { + "id": "a1b2c3d4e5f6abcdef12345c", + "type": "sound_test", + "time": 1560506210, + "camera_id": "12:34:56:00:8b:a2", + "device_id": "12:34:56:00:8b:a2", + "sub_type": 0, + "message": "Hall: Alarmton erfolgreich getestet" + }, + { + "id": "a1b2c3d4e5f6abcdef12345d", + "type": "wifi_status", + "time": 1560506220, + "camera_id": "12:34:56:00:8b:a2", + "device_id": "12:34:56:00:8b:a2", + "sub_type": 1, + "message": "Hall:WLAN-Verbindung erfolgreich hergestellt" + }, + { + "id": "a1b2c3d4e5f6abcdef12345e", + "type": "outdoor", + "time": 1560643100, + "camera_id": "12:34:56:00:a5:a4", + "device_id": "12:34:56:00:a5:a4", + "video_id": "string", + "video_status": "available", + "event_list": [ + { + "type": "string", + "time": 1560643100, + "offset": 0, + "id": "c81bcf7b-2cfg-4ac9-8455-487ed00c0000", + "message": "Animal détecté", + "snapshot": { + "id": "5715e16849c75xxxx00000000xxxxx", + "version": 1, + "key": "7ac578d05030d0e170643a787ee0a29663dxxx00000xxxxx00000", + "url": "https://netatmocameraimage.blob.core.windows.net/production/1aa" + }, + "vignette": { + "id": "5715e16849c75xxxx00000000xxxxx", + "version": 1, + "key": "7ac578d05030d0e170643a787ee0a29663dxxx00000xxxxx00000", + "url": "https://netatmocameraimage.blob.core.windows.net/production/1aa00000" + } + }, + { + "type": "string", + "time": 1560506222, + "offset": 0, + "id": "c81bcf7b-2cfg-4ac9-8455-487ed00c0000", + "message": "Animal détecté", + "snapshot": { + "filename": "vod/af74631d-8311-42dc-825b-82e3abeaab09/events/c53b-aze7a.jpg" + }, + "vignette": { + "filename": "vod/af74631d-8311-42dc-825b-82e3abeaab09/events/c5.jpg" + } + } + ] + } + ] + }, + { + "id": "91763b24c43d3e344f424e8c", + "persons": [], + "place": { + "city": "Frankfurt", + "country": "DE", + "timezone": "Europe/Berlin" + }, + "cameras": [ + { + "id": "12:34:56:00:a5:a5", + "type": "NOC", + "status": "on", + "vpn_url": "https://prodvpn-eu-2.netatmo.net/restricted/10.255.248.91/6d278460699e56180d47ab47169efb31/MpEylTU2MDYzNjRVD-LJxUnIndumKzLboeAwMDqTTz,,", + "is_local": true, + "sd_status": "on", + "alim_status": "on", + "name": "Street", + "last_setup": 1563737561, + "light_mode_status": "auto" + } + ], + "smokedetectors": [] + }, + { + "id": "91763b24c43d3e344f424e8d", + "persons": [], + "place": { + "city": "Frankfurt", + "country": "DE", + "timezone": "Europe/Berlin" + }, + "cameras": [], + "smokedetectors": [] + } + ], + "user": { + "reg_locale": "de-DE", + "lang": "de-DE", + "country": "DE", + "mail": "john@doe.com" }, - "status": "ok", - "time_exec": 0.03621506690979, - "time_server": 1560626960 -} \ No newline at end of file + "global_info": { + "show_tags": true + } + }, + "status": "ok", + "time_exec": 0.03621506690979, + "time_server": 1560626960 +} diff --git a/tests/components/netatmo/fixtures/getpublicdata.json b/tests/components/netatmo/fixtures/getpublicdata.json index 55202713890..cf2ec3c66cb 100644 --- a/tests/components/netatmo/fixtures/getpublicdata.json +++ b/tests/components/netatmo/fixtures/getpublicdata.json @@ -1,392 +1,271 @@ { - "status": "ok", - "time_server": 1560248397, - "time_exec": 0, - "body": [ - { - "_id": "70:ee:50:36:94:7c", - "place": { - "location": [ - 8.791382999999996, - 50.2136394 - ], - "timezone": "Europe/Berlin", - "country": "DE", - "altitude": 132 - }, - "mark": 14, - "measures": { - "02:00:00:36:f2:94": { - "res": { - "1560248022": [ - 21.4, - 62 - ] - }, - "type": [ - "temperature", - "humidity" - ] - }, - "70:ee:50:36:94:7c": { - "res": { - "1560248030": [ - 1010.6 - ] - }, - "type": [ - "pressure" - ] - }, - "05:00:00:05:33:84": { - "rain_60min": 0.2, - "rain_24h": 12.322000000000001, - "rain_live": 0.5, - "rain_timeutc": 1560248022 - } - }, - "modules": [ - "05:00:00:05:33:84", - "02:00:00:36:f2:94" - ], - "module_types": { - "05:00:00:05:33:84": "NAModule3", - "02:00:00:36:f2:94": "NAModule1" - } + "status": "ok", + "time_server": 1560248397, + "time_exec": 0, + "body": [ + { + "_id": "70:ee:50:36:94:7c", + "place": { + "location": [8.791382999999996, 50.2136394], + "timezone": "Europe/Berlin", + "country": "DE", + "altitude": 132 + }, + "mark": 14, + "measures": { + "02:00:00:36:f2:94": { + "res": { + "1560248022": [21.4, 62] + }, + "type": ["temperature", "humidity"] }, - { - "_id": "70:ee:50:1f:68:9e", - "place": { - "location": [ - 8.795445200000017, - 50.2130169 - ], - "timezone": "Europe/Berlin", - "country": "DE", - "altitude": 125 - }, - "mark": 14, - "measures": { - "02:00:00:1f:82:28": { - "res": { - "1560248312": [ - 21.1, - 69 - ] - }, - "type": [ - "temperature", - "humidity" - ] - }, - "70:ee:50:1f:68:9e": { - "res": { - "1560248344": [ - 1007.3 - ] - }, - "type": [ - "pressure" - ] - }, - "05:00:00:02:bb:6e": { - "rain_60min": 0, - "rain_24h": 9.999, - "rain_live": 0, - "rain_timeutc": 1560248344 - } - }, - "modules": [ - "02:00:00:1f:82:28", - "05:00:00:02:bb:6e" - ], - "module_types": { - "02:00:00:1f:82:28": "NAModule1", - "05:00:00:02:bb:6e": "NAModule3" - } + "70:ee:50:36:94:7c": { + "res": { + "1560248030": [1010.6] + }, + "type": ["pressure"] }, - { - "_id": "70:ee:50:27:25:b0", - "place": { - "location": [ - 8.7807159, - 50.1946167 - ], - "timezone": "Europe/Berlin", - "country": "DE", - "altitude": 112 - }, - "mark": 14, - "measures": { - "02:00:00:27:19:b2": { - "res": { - "1560247889": [ - 23.2, - 60 - ] - }, - "type": [ - "temperature", - "humidity" - ] - }, - "70:ee:50:27:25:b0": { - "res": { - "1560247907": [ - 1012.8 - ] - }, - "type": [ - "pressure" - ] - }, - "05:00:00:03:5d:2e": { - "rain_60min": 0, - "rain_24h": 11.716000000000001, - "rain_live": 0, - "rain_timeutc": 1560247896 - } - }, - "modules": [ - "02:00:00:27:19:b2", - "05:00:00:03:5d:2e" - ], - "module_types": { - "02:00:00:27:19:b2": "NAModule1", - "05:00:00:03:5d:2e": "NAModule3" - } - }, - { - "_id": "70:ee:50:04:ed:7a", - "place": { - "location": [ - 8.785034, - 50.192169 - ], - "timezone": "Europe/Berlin", - "country": "DE", - "altitude": 112 - }, - "mark": 14, - "measures": { - "02:00:00:04:c2:2e": { - "res": { - "1560248137": [ - 19.8, - 76 - ] - }, - "type": [ - "temperature", - "humidity" - ] - }, - "70:ee:50:04:ed:7a": { - "res": { - "1560248152": [ - 1005.4 - ] - }, - "type": [ - "pressure" - ] - } - }, - "modules": [ - "02:00:00:04:c2:2e" - ], - "module_types": { - "02:00:00:04:c2:2e": "NAModule1" - } - }, - { - "_id": "70:ee:50:27:9f:2c", - "place": { - "location": [ - 8.785342, - 50.193573 - ], - "timezone": "Europe/Berlin", - "country": "DE", - "altitude": 116 - }, - "mark": 1, - "measures": { - "02:00:00:27:aa:70": { - "res": { - "1560247821": [ - 25.5, - 56 - ] - }, - "type": [ - "temperature", - "humidity" - ] - }, - "70:ee:50:27:9f:2c": { - "res": { - "1560247853": [ - 1010.6 - ] - }, - "type": [ - "pressure" - ] - } - }, - "modules": [ - "02:00:00:27:aa:70" - ], - "module_types": { - "02:00:00:27:aa:70": "NAModule1" - } - }, - { - "_id": "70:ee:50:01:20:fa", - "place": { - "location": [ - 8.7953, - 50.195241 - ], - "timezone": "Europe/Berlin", - "country": "DE", - "altitude": 119 - }, - "mark": 1, - "measures": { - "02:00:00:00:f7:ba": { - "res": { - "1560247831": [ - 27.4, - 58 - ] - }, - "type": [ - "temperature", - "humidity" - ] - }, - "70:ee:50:01:20:fa": { - "res": { - "1560247876": [ - 1014.4 - ] - }, - "type": [ - "pressure" - ] - } - }, - "modules": [ - "02:00:00:00:f7:ba" - ], - "module_types": { - "02:00:00:00:f7:ba": "NAModule1" - } - }, - { - "_id": "70:ee:50:3c:02:78", - "place": { - "location": [ - 8.795953681700666, - 50.19530139868166 - ], - "timezone": "Europe/Berlin", - "country": "DE", - "altitude": 119 - }, - "mark": 7, - "measures": { - "02:00:00:3c:21:f2": { - "res": { - "1560248225": [ - 23.3, - 58 - ] - }, - "type": [ - "temperature", - "humidity" - ] - }, - "70:ee:50:3c:02:78": { - "res": { - "1560248270": [ - 1011.7 - ] - }, - "type": [ - "pressure" - ] - } - }, - "modules": [ - "02:00:00:3c:21:f2" - ], - "module_types": { - "02:00:00:3c:21:f2": "NAModule1" - } - }, - { - "_id": "70:ee:50:36:a9:fc", - "place": { - "location": [ - 8.801164269110814, - 50.19596181704958 - ], - "timezone": "Europe/Berlin", - "country": "DE", - "altitude": 113 - }, - "mark": 14, - "measures": { - "02:00:00:36:a9:50": { - "res": { - "1560248145": [ - 20.1, - 67 - ] - }, - "type": [ - "temperature", - "humidity" - ] - }, - "70:ee:50:36:a9:fc": { - "res": { - "1560248191": [ - 1010 - ] - }, - "type": [ - "pressure" - ] - }, - "05:00:00:02:92:82": { - "rain_60min": 0, - "rain_24h": 11.009, - "rain_live": 0, - "rain_timeutc": 1560248184 - }, - "06:00:00:03:19:76": { - "wind_strength": 15, - "wind_angle": 17, - "gust_strength": 31, - "gust_angle": 217, - "wind_timeutc": 1560248190 - } - }, - "modules": [ - "05:00:00:02:92:82", - "02:00:00:36:a9:50", - "06:00:00:03:19:76" - ], - "module_types": { - "05:00:00:02:92:82": "NAModule3", - "02:00:00:36:a9:50": "NAModule1", - "06:00:00:03:19:76": "NAModule2" - } + "05:00:00:05:33:84": { + "rain_60min": 0.2, + "rain_24h": 12.322000000000001, + "rain_live": 0.5, + "rain_timeutc": 1560248022 } - ] -} \ No newline at end of file + }, + "modules": ["05:00:00:05:33:84", "02:00:00:36:f2:94"], + "module_types": { + "05:00:00:05:33:84": "NAModule3", + "02:00:00:36:f2:94": "NAModule1" + } + }, + { + "_id": "70:ee:50:1f:68:9e", + "place": { + "location": [8.795445200000017, 50.2130169], + "timezone": "Europe/Berlin", + "country": "DE", + "altitude": 125 + }, + "mark": 14, + "measures": { + "02:00:00:1f:82:28": { + "res": { + "1560248312": [21.1, 69] + }, + "type": ["temperature", "humidity"] + }, + "70:ee:50:1f:68:9e": { + "res": { + "1560248344": [1007.3] + }, + "type": ["pressure"] + }, + "05:00:00:02:bb:6e": { + "rain_60min": 0, + "rain_24h": 9.999, + "rain_live": 0, + "rain_timeutc": 1560248344 + } + }, + "modules": ["02:00:00:1f:82:28", "05:00:00:02:bb:6e"], + "module_types": { + "02:00:00:1f:82:28": "NAModule1", + "05:00:00:02:bb:6e": "NAModule3" + } + }, + { + "_id": "70:ee:50:27:25:b0", + "place": { + "location": [8.7807159, 50.1946167], + "timezone": "Europe/Berlin", + "country": "DE", + "altitude": 112 + }, + "mark": 14, + "measures": { + "02:00:00:27:19:b2": { + "res": { + "1560247889": [23.2, 60] + }, + "type": ["temperature", "humidity"] + }, + "70:ee:50:27:25:b0": { + "res": { + "1560247907": [1012.8] + }, + "type": ["pressure"] + }, + "05:00:00:03:5d:2e": { + "rain_60min": 0, + "rain_24h": 11.716000000000001, + "rain_live": 0, + "rain_timeutc": 1560247896 + } + }, + "modules": ["02:00:00:27:19:b2", "05:00:00:03:5d:2e"], + "module_types": { + "02:00:00:27:19:b2": "NAModule1", + "05:00:00:03:5d:2e": "NAModule3" + } + }, + { + "_id": "70:ee:50:04:ed:7a", + "place": { + "location": [8.785034, 50.192169], + "timezone": "Europe/Berlin", + "country": "DE", + "altitude": 112 + }, + "mark": 14, + "measures": { + "02:00:00:04:c2:2e": { + "res": { + "1560248137": [19.8, 76] + }, + "type": ["temperature", "humidity"] + }, + "70:ee:50:04:ed:7a": { + "res": { + "1560248152": [1005.4] + }, + "type": ["pressure"] + } + }, + "modules": ["02:00:00:04:c2:2e"], + "module_types": { + "02:00:00:04:c2:2e": "NAModule1" + } + }, + { + "_id": "70:ee:50:27:9f:2c", + "place": { + "location": [8.785342, 50.193573], + "timezone": "Europe/Berlin", + "country": "DE", + "altitude": 116 + }, + "mark": 1, + "measures": { + "02:00:00:27:aa:70": { + "res": { + "1560247821": [25.5, 56] + }, + "type": ["temperature", "humidity"] + }, + "70:ee:50:27:9f:2c": { + "res": { + "1560247853": [1010.6] + }, + "type": ["pressure"] + } + }, + "modules": ["02:00:00:27:aa:70"], + "module_types": { + "02:00:00:27:aa:70": "NAModule1" + } + }, + { + "_id": "70:ee:50:01:20:fa", + "place": { + "location": [8.7953, 50.195241], + "timezone": "Europe/Berlin", + "country": "DE", + "altitude": 119 + }, + "mark": 1, + "measures": { + "02:00:00:00:f7:ba": { + "res": { + "1560247831": [27.4, 58] + }, + "type": ["temperature", "humidity"] + }, + "70:ee:50:01:20:fa": { + "res": { + "1560247876": [1014.4] + }, + "type": ["pressure"] + } + }, + "modules": ["02:00:00:00:f7:ba"], + "module_types": { + "02:00:00:00:f7:ba": "NAModule1" + } + }, + { + "_id": "70:ee:50:3c:02:78", + "place": { + "location": [8.795953681700666, 50.19530139868166], + "timezone": "Europe/Berlin", + "country": "DE", + "altitude": 119 + }, + "mark": 7, + "measures": { + "02:00:00:3c:21:f2": { + "res": { + "1560248225": [23.3, 58] + }, + "type": ["temperature", "humidity"] + }, + "70:ee:50:3c:02:78": { + "res": { + "1560248270": [1011.7] + }, + "type": ["pressure"] + } + }, + "modules": ["02:00:00:3c:21:f2"], + "module_types": { + "02:00:00:3c:21:f2": "NAModule1" + } + }, + { + "_id": "70:ee:50:36:a9:fc", + "place": { + "location": [8.801164269110814, 50.19596181704958], + "timezone": "Europe/Berlin", + "country": "DE", + "altitude": 113 + }, + "mark": 14, + "measures": { + "02:00:00:36:a9:50": { + "res": { + "1560248145": [20.1, 67] + }, + "type": ["temperature", "humidity"] + }, + "70:ee:50:36:a9:fc": { + "res": { + "1560248191": [1010] + }, + "type": ["pressure"] + }, + "05:00:00:02:92:82": { + "rain_60min": 0, + "rain_24h": 11.009, + "rain_live": 0, + "rain_timeutc": 1560248184 + }, + "06:00:00:03:19:76": { + "wind_strength": 15, + "wind_angle": 17, + "gust_strength": 31, + "gust_angle": 217, + "wind_timeutc": 1560248190 + } + }, + "modules": [ + "05:00:00:02:92:82", + "02:00:00:36:a9:50", + "06:00:00:03:19:76" + ], + "module_types": { + "05:00:00:02:92:82": "NAModule3", + "02:00:00:36:a9:50": "NAModule1", + "06:00:00:03:19:76": "NAModule2" + } + } + ] +} diff --git a/tests/components/netatmo/fixtures/getstationsdata.json b/tests/components/netatmo/fixtures/getstationsdata.json index 2a18c7bd280..822a4c11a50 100644 --- a/tests/components/netatmo/fixtures/getstationsdata.json +++ b/tests/components/netatmo/fixtures/getstationsdata.json @@ -1,600 +1,511 @@ { - "body": { - "devices": [ - { - "_id": "12:34:56:37:11:ca", - "cipher_id": "enc:16:zjiZF/q8jTScXVdDa/kvhUAIUPGeYszaD1ClEf8byAJkRjxc5oth7cAocrMUIApX", - "date_setup": 1544558432, - "last_setup": 1544558432, - "type": "NAMain", - "last_status_store": 1559413181, - "module_name": "NetatmoIndoor", - "firmware": 137, - "last_upgrade": 1544558433, - "wifi_status": 45, - "reachable": true, - "co2_calibrating": false, - "station_name": "MyStation", - "data_type": [ - "Temperature", - "CO2", - "Humidity", - "Noise", - "Pressure" - ], - "place": { - "altitude": 664, - "city": "Frankfurt", - "country": "DE", - "timezone": "Europe/Berlin", - "location": [ - 52.516263, - 13.377726 - ] - }, - "dashboard_data": { - "time_utc": 1559413171, - "Temperature": 24.6, - "CO2": 749, - "Humidity": 36, - "Noise": 37, - "Pressure": 1017.3, - "AbsolutePressure": 939.7, - "min_temp": 23.4, - "max_temp": 25.6, - "date_min_temp": 1559371924, - "date_max_temp": 1559411964, - "temp_trend": "stable", - "pressure_trend": "down" - }, - "modules": [ - { - "_id": "12:34:56:36:fc:de", - "type": "NAModule1", - "module_name": "NetatmoOutdoor", - "data_type": [ - "Temperature", - "Humidity" - ], - "last_setup": 1544558433, - "reachable": true, - "dashboard_data": { - "time_utc": 1559413157, - "Temperature": 28.6, - "Humidity": 24, - "min_temp": 16.9, - "max_temp": 30.3, - "date_min_temp": 1559365579, - "date_max_temp": 1559404698, - "temp_trend": "down" - }, - "firmware": 46, - "last_message": 1559413177, - "last_seen": 1559413157, - "rf_status": 65, - "battery_vp": 5738, - "battery_percent": 87 - }, - { - "_id": "12:34:56:07:bb:3e", - "type": "NAModule4", - "module_name": "Kitchen", - "data_type": [ - "Temperature", - "CO2", - "Humidity" - ], - "last_setup": 1548956696, - "reachable": true, - "dashboard_data": { - "time_utc": 1559413125, - "Temperature": 28, - "CO2": 503, - "Humidity": 26, - "min_temp": 25, - "max_temp": 28, - "date_min_temp": 1559371577, - "date_max_temp": 1559412561, - "temp_trend": "up" - }, - "firmware": 44, - "last_message": 1559413177, - "last_seen": 1559413177, - "rf_status": 73, - "battery_vp": 5687, - "battery_percent": 83 - }, - { - "_id": "12:34:56:07:bb:0e", - "type": "NAModule4", - "module_name": "Livingroom", - "data_type": [ - "Temperature", - "CO2", - "Humidity" - ], - "last_setup": 1548957209, - "reachable": true, - "dashboard_data": { - "time_utc": 1559413093, - "Temperature": 26.4, - "CO2": 451, - "Humidity": 31, - "min_temp": 25.1, - "max_temp": 26.4, - "date_min_temp": 1559365290, - "date_max_temp": 1559413093, - "temp_trend": "stable" - }, - "firmware": 44, - "last_message": 1559413177, - "last_seen": 1559413093, - "rf_status": 84, - "battery_vp": 5626, - "battery_percent": 79 - }, - { - "_id": "12:34:56:03:1b:e4", - "type": "NAModule2", - "module_name": "Garden", - "data_type": [ - "Wind" - ], - "last_setup": 1549193862, - "reachable": true, - "dashboard_data": { - "time_utc": 1559413170, - "WindStrength": 4, - "WindAngle": 217, - "GustStrength": 9, - "GustAngle": 206, - "max_wind_str": 21, - "max_wind_angle": 217, - "date_max_wind_str": 1559386669 - }, - "firmware": 19, - "last_message": 1559413177, - "last_seen": 1559413177, - "rf_status": 59, - "battery_vp": 5689, - "battery_percent": 85 - }, - { - "_id": "12:34:56:05:51:20", - "type": "NAModule3", - "module_name": "Yard", - "data_type": [ - "Rain" - ], - "last_setup": 1549194580, - "reachable": true, - "dashboard_data": { - "time_utc": 1559413170, - "Rain": 0, - "sum_rain_24": 0, - "sum_rain_1": 0 - }, - "firmware": 8, - "last_message": 1559413177, - "last_seen": 1559413170, - "rf_status": 67, - "battery_vp": 5860, - "battery_percent": 93 - } - ] + "body": { + "devices": [ + { + "_id": "12:34:56:37:11:ca", + "cipher_id": "enc:16:zjiZF/q8jTScXVdDa/kvhUAIUPGeYszaD1ClEf8byAJkRjxc5oth7cAocrMUIApX", + "date_setup": 1544558432, + "last_setup": 1544558432, + "type": "NAMain", + "last_status_store": 1559413181, + "module_name": "NetatmoIndoor", + "firmware": 137, + "last_upgrade": 1544558433, + "wifi_status": 45, + "reachable": true, + "co2_calibrating": false, + "station_name": "MyStation", + "data_type": ["Temperature", "CO2", "Humidity", "Noise", "Pressure"], + "place": { + "altitude": 664, + "city": "Frankfurt", + "country": "DE", + "timezone": "Europe/Berlin", + "location": [52.516263, 13.377726] + }, + "dashboard_data": { + "time_utc": 1559413171, + "Temperature": 24.6, + "CO2": 749, + "Humidity": 36, + "Noise": 37, + "Pressure": 1017.3, + "AbsolutePressure": 939.7, + "min_temp": 23.4, + "max_temp": 25.6, + "date_min_temp": 1559371924, + "date_max_temp": 1559411964, + "temp_trend": "stable", + "pressure_trend": "down" + }, + "modules": [ + { + "_id": "12:34:56:36:fc:de", + "type": "NAModule1", + "module_name": "NetatmoOutdoor", + "data_type": ["Temperature", "Humidity"], + "last_setup": 1544558433, + "reachable": true, + "dashboard_data": { + "time_utc": 1559413157, + "Temperature": 28.6, + "Humidity": 24, + "min_temp": 16.9, + "max_temp": 30.3, + "date_min_temp": 1559365579, + "date_max_temp": 1559404698, + "temp_trend": "down" }, - { - "_id": "12 :34: 56:36:fd:3c", - "station_name": "Valley Road", - "date_setup": 1545897146, - "last_setup": 1545897146, - "type": "NAMain", - "last_status_store": 1581835369, - "firmware": 137, - "last_upgrade": 1545897125, - "wifi_status": 53, - "reachable": true, - "co2_calibrating": false, - "data_type": [ - "Temperature", - "CO2", - "Humidity", - "Noise", - "Pressure" - ], - "place": { - "altitude": 69, - "city": "Valley", - "country": "AU", - "timezone": "Australia/Hobart", - "location": [ - 148.444226, - -41.721282 - ] - }, - "read_only": true, - "dashboard_data": { - "time_utc": 1581835330, - "Temperature": 22.4, - "CO2": 471, - "Humidity": 46, - "Noise": 47, - "Pressure": 1011.5, - "AbsolutePressure": 1002.8, - "min_temp": 18.1, - "max_temp": 22.5, - "date_max_temp": 1581829891, - "date_min_temp": 1581794878, - "temp_trend": "stable", - "pressure_trend": "stable" - }, - "modules": [ - { - "_id": "12 :34: 56:36:e6:c0", - "type": "NAModule1", - "module_name": "Module", - "data_type": [ - "Temperature", - "Humidity" - ], - "last_setup": 1545897146, - "battery_percent": 22, - "reachable": false, - "firmware": 46, - "last_message": 1572497781, - "last_seen": 1572497742, - "rf_status": 88, - "battery_vp": 4118 - }, - { - "_id": "12:34:56:05:25:6e", - "type": "NAModule3", - "module_name": "Rain Gauge", - "data_type": [ - "Rain" - ], - "last_setup": 1553997427, - "battery_percent": 82, - "reachable": true, - "firmware": 8, - "last_message": 1581835362, - "last_seen": 1581835354, - "rf_status": 78, - "battery_vp": 5594, - "dashboard_data": { - "time_utc": 1581835329, - "Rain": 0, - "sum_rain_1": 0, - "sum_rain_24": 0 - } - } - ] + "firmware": 46, + "last_message": 1559413177, + "last_seen": 1559413157, + "rf_status": 65, + "battery_vp": 5738, + "battery_percent": 87 + }, + { + "_id": "12:34:56:07:bb:3e", + "type": "NAModule4", + "module_name": "Kitchen", + "data_type": ["Temperature", "CO2", "Humidity"], + "last_setup": 1548956696, + "reachable": true, + "dashboard_data": { + "time_utc": 1559413125, + "Temperature": 28, + "CO2": 503, + "Humidity": 26, + "min_temp": 25, + "max_temp": 28, + "date_min_temp": 1559371577, + "date_max_temp": 1559412561, + "temp_trend": "up" }, - { - "_id": "12:34:56:32:a7:60", - "home_name": "Ateljen", - "date_setup": 1566714693, - "last_setup": 1566714693, - "type": "NAMain", - "last_status_store": 1588481079, - "module_name": "Indoor", - "firmware": 177, - "last_upgrade": 1566714694, - "wifi_status": 50, - "reachable": true, - "co2_calibrating": false, - "data_type": [ - "Temperature", - "CO2", - "Humidity", - "Noise", - "Pressure" - ], - "place": { - "altitude": 93, - "city": "Gothenburg", - "country": "SE", - "timezone": "Europe/Stockholm", - "location": [ - 11.6136629, - 57.7006827 - ] - }, - "dashboard_data": { - "time_utc": 1588481073, - "Temperature": 18.2, - "CO2": 542, - "Humidity": 45, - "Noise": 45, - "Pressure": 1013, - "AbsolutePressure": 1001.9, - "min_temp": 18.2, - "max_temp": 19.5, - "date_max_temp": 1588456861, - "date_min_temp": 1588479561, - "temp_trend": "stable", - "pressure_trend": "up" - }, - "modules": [ - { - "_id": "12:34:56:32:db:06", - "type": "NAModule1", - "last_setup": 1587635819, - "data_type": [ - "Temperature", - "Humidity" - ], - "battery_percent": 100, - "reachable": false, - "firmware": 255, - "last_message": 0, - "last_seen": 0, - "rf_status": 255, - "battery_vp": 65535 - } - ] + "firmware": 44, + "last_message": 1559413177, + "last_seen": 1559413177, + "rf_status": 73, + "battery_vp": 5687, + "battery_percent": 83 + }, + { + "_id": "12:34:56:07:bb:0e", + "type": "NAModule4", + "module_name": "Livingroom", + "data_type": ["Temperature", "CO2", "Humidity"], + "last_setup": 1548957209, + "reachable": true, + "dashboard_data": { + "time_utc": 1559413093, + "Temperature": 26.4, + "CO2": 451, + "Humidity": 31, + "min_temp": 25.1, + "max_temp": 26.4, + "date_min_temp": 1559365290, + "date_max_temp": 1559413093, + "temp_trend": "stable" }, - { - "_id": "12:34:56:1c:68:2e", - "station_name": "Bol\u00e5s", - "date_setup": 1470935400, - "last_setup": 1470935400, - "type": "NAMain", - "last_status_store": 1588481399, - "module_name": "Inne - Nere", - "firmware": 177, - "last_upgrade": 1470935401, - "wifi_status": 13, - "reachable": true, - "co2_calibrating": false, - "data_type": [ - "Temperature", - "CO2", - "Humidity", - "Noise", - "Pressure" - ], - "place": { - "altitude": 93, - "city": "Gothenburg", - "country": "SE", - "timezone": "Europe/Stockholm", - "location": [ - 11.6136629, - 57.7006827 - ] - }, - "dashboard_data": { - "time_utc": 1588481387, - "Temperature": 20.8, - "CO2": 674, - "Humidity": 41, - "Noise": 34, - "Pressure": 1012.1, - "AbsolutePressure": 1001, - "min_temp": 20.8, - "max_temp": 22.2, - "date_max_temp": 1588456859, - "date_min_temp": 1588480176, - "temp_trend": "stable", - "pressure_trend": "up" - }, - "modules": [ - { - "_id": "12:34:56:02:b3:da", - "type": "NAModule3", - "module_name": "Regnm\u00e4tare", - "last_setup": 1470937706, - "data_type": [ - "Rain" - ], - "battery_percent": 81, - "reachable": true, - "firmware": 12, - "last_message": 1588481393, - "last_seen": 1588481386, - "rf_status": 67, - "battery_vp": 5582, - "dashboard_data": { - "time_utc": 1588481386, - "Rain": 0, - "sum_rain_1": 0, - "sum_rain_24": 0.1 - } - }, - { - "_id": "12:34:56:03:76:60", - "type": "NAModule4", - "module_name": "Inne - Uppe", - "last_setup": 1470938089, - "data_type": [ - "Temperature", - "CO2", - "Humidity" - ], - "battery_percent": 14, - "reachable": true, - "firmware": 50, - "last_message": 1588481393, - "last_seen": 1588481374, - "rf_status": 70, - "battery_vp": 4448, - "dashboard_data": { - "time_utc": 1588481374, - "Temperature": 19.6, - "CO2": 696, - "Humidity": 41, - "min_temp": 19.6, - "max_temp": 20.5, - "date_max_temp": 1588456817, - "date_min_temp": 1588481374, - "temp_trend": "stable" - } - }, - { - "_id": "12:34:56:32:db:06", - "type": "NAModule1", - "module_name": "Ute", - "last_setup": 1566326027, - "data_type": [ - "Temperature", - "Humidity" - ], - "battery_percent": 81, - "reachable": true, - "firmware": 50, - "last_message": 1588481393, - "last_seen": 1588481380, - "rf_status": 61, - "battery_vp": 5544, - "dashboard_data": { - "time_utc": 1588481380, - "Temperature": 6.4, - "Humidity": 91, - "min_temp": 3.6, - "max_temp": 6.4, - "date_max_temp": 1588481380, - "date_min_temp": 1588471383, - "temp_trend": "up" - } - } - ] + "firmware": 44, + "last_message": 1559413177, + "last_seen": 1559413093, + "rf_status": 84, + "battery_vp": 5626, + "battery_percent": 79 + }, + { + "_id": "12:34:56:03:1b:e4", + "type": "NAModule2", + "module_name": "Garden", + "data_type": ["Wind"], + "last_setup": 1549193862, + "reachable": true, + "dashboard_data": { + "time_utc": 1559413170, + "WindStrength": 4, + "WindAngle": 217, + "GustStrength": 9, + "GustAngle": 206, + "max_wind_str": 21, + "max_wind_angle": 217, + "date_max_wind_str": 1559386669 }, - { - "_id": "12:34:56:1d:68:2e", - "date_setup": 1470935500, - "last_setup": 1470935500, - "type": "NAMain", - "last_status_store": 1588481399, - "module_name": "Basisstation", - "firmware": 177, - "last_upgrade": 1470935401, - "wifi_status": 13, - "reachable": true, - "co2_calibrating": false, - "data_type": [ - "Temperature", - "CO2", - "Humidity", - "Noise", - "Pressure" - ], - "place": { - "altitude": 93, - "city": "Gothenburg", - "country": "SE", - "timezone": "Europe/Stockholm", - "location": [ - 11.6136629, - 57.7006827 - ] - }, - "dashboard_data": { - "time_utc": 1588481387, - "Temperature": 20.8, - "CO2": 674, - "Humidity": 41, - "Noise": 34, - "Pressure": 1012.1, - "AbsolutePressure": 1001, - "min_temp": 20.8, - "max_temp": 22.2, - "date_max_temp": 1588456859, - "date_min_temp": 1588480176, - "temp_trend": "stable", - "pressure_trend": "up" - }, - "modules": [] + "firmware": 19, + "last_message": 1559413177, + "last_seen": 1559413177, + "rf_status": 59, + "battery_vp": 5689, + "battery_percent": 85 + }, + { + "_id": "12:34:56:05:51:20", + "type": "NAModule3", + "module_name": "Yard", + "data_type": ["Rain"], + "last_setup": 1549194580, + "reachable": true, + "dashboard_data": { + "time_utc": 1559413170, + "Rain": 0, + "sum_rain_24": 0, + "sum_rain_1": 0 }, - { - "_id": "12:34:56:58:c8:54", - "date_setup": 1605594014, - "last_setup": 1605594014, - "type": "NAMain", - "last_status_store": 1605878352, - "firmware": 178, - "wifi_status": 47, - "reachable": true, - "co2_calibrating": false, - "data_type": [ - "Temperature", - "CO2", - "Humidity", - "Noise", - "Pressure" - ], - "place": { - "altitude": 65, - "city": "Njurunda District", - "country": "SE", - "timezone": "Europe/Stockholm", - "location": [ - 17.123456, - 62.123456 - ] - }, - "station_name": "Njurunda (Indoor)", - "home_id": "5fb36b9ec68fd10c6467ca65", - "home_name": "Njurunda", - "dashboard_data": { - "time_utc": 1605878349, - "Temperature": 19.7, - "CO2": 993, - "Humidity": 40, - "Noise": 40, - "Pressure": 1015.6, - "AbsolutePressure": 1007.8, - "min_temp": 19.7, - "max_temp": 20.4, - "date_max_temp": 1605826917, - "date_min_temp": 1605873207, - "temp_trend": "stable", - "pressure_trend": "up" - }, - "modules": [ - { - "_id": "12:34:56:58:e6:38", - "type": "NAModule1", - "last_setup": 1605594034, - "data_type": [ - "Temperature", - "Humidity" - ], - "battery_percent": 100, - "reachable": true, - "firmware": 50, - "last_message": 1605878347, - "last_seen": 1605878328, - "rf_status": 62, - "battery_vp": 6198, - "dashboard_data": { - "time_utc": 1605878328, - "Temperature": 0.6, - "Humidity": 77, - "min_temp": -2.1, - "max_temp": 1.5, - "date_max_temp": 1605865920, - "date_min_temp": 1605826904, - "temp_trend": "down" - } - } - ] + "firmware": 8, + "last_message": 1559413177, + "last_seen": 1559413170, + "rf_status": 67, + "battery_vp": 5860, + "battery_percent": 93 + } + ] + }, + { + "_id": "12 :34: 56:36:fd:3c", + "station_name": "Valley Road", + "date_setup": 1545897146, + "last_setup": 1545897146, + "type": "NAMain", + "last_status_store": 1581835369, + "firmware": 137, + "last_upgrade": 1545897125, + "wifi_status": 53, + "reachable": true, + "co2_calibrating": false, + "data_type": ["Temperature", "CO2", "Humidity", "Noise", "Pressure"], + "place": { + "altitude": 69, + "city": "Valley", + "country": "AU", + "timezone": "Australia/Hobart", + "location": [148.444226, -41.721282] + }, + "read_only": true, + "dashboard_data": { + "time_utc": 1581835330, + "Temperature": 22.4, + "CO2": 471, + "Humidity": 46, + "Noise": 47, + "Pressure": 1011.5, + "AbsolutePressure": 1002.8, + "min_temp": 18.1, + "max_temp": 22.5, + "date_max_temp": 1581829891, + "date_min_temp": 1581794878, + "temp_trend": "stable", + "pressure_trend": "stable" + }, + "modules": [ + { + "_id": "12 :34: 56:36:e6:c0", + "type": "NAModule1", + "module_name": "Module", + "data_type": ["Temperature", "Humidity"], + "last_setup": 1545897146, + "battery_percent": 22, + "reachable": false, + "firmware": 46, + "last_message": 1572497781, + "last_seen": 1572497742, + "rf_status": 88, + "battery_vp": 4118 + }, + { + "_id": "12:34:56:05:25:6e", + "type": "NAModule3", + "module_name": "Rain Gauge", + "data_type": ["Rain"], + "last_setup": 1553997427, + "battery_percent": 82, + "reachable": true, + "firmware": 8, + "last_message": 1581835362, + "last_seen": 1581835354, + "rf_status": 78, + "battery_vp": 5594, + "dashboard_data": { + "time_utc": 1581835329, + "Rain": 0, + "sum_rain_1": 0, + "sum_rain_24": 0 } - ], - "user": { - "mail": "john@doe.com", - "administrative": { - "lang": "de-DE", - "reg_locale": "de-DE", - "country": "DE", - "unit": 0, - "windunit": 0, - "pressureunit": 0, - "feel_like_algo": 0 + } + ] + }, + { + "_id": "12:34:56:32:a7:60", + "home_name": "Ateljen", + "date_setup": 1566714693, + "last_setup": 1566714693, + "type": "NAMain", + "last_status_store": 1588481079, + "module_name": "Indoor", + "firmware": 177, + "last_upgrade": 1566714694, + "wifi_status": 50, + "reachable": true, + "co2_calibrating": false, + "data_type": ["Temperature", "CO2", "Humidity", "Noise", "Pressure"], + "place": { + "altitude": 93, + "city": "Gothenburg", + "country": "SE", + "timezone": "Europe/Stockholm", + "location": [11.6136629, 57.7006827] + }, + "dashboard_data": { + "time_utc": 1588481073, + "Temperature": 18.2, + "CO2": 542, + "Humidity": 45, + "Noise": 45, + "Pressure": 1013, + "AbsolutePressure": 1001.9, + "min_temp": 18.2, + "max_temp": 19.5, + "date_max_temp": 1588456861, + "date_min_temp": 1588479561, + "temp_trend": "stable", + "pressure_trend": "up" + }, + "modules": [ + { + "_id": "12:34:56:32:db:06", + "type": "NAModule1", + "last_setup": 1587635819, + "data_type": ["Temperature", "Humidity"], + "battery_percent": 100, + "reachable": false, + "firmware": 255, + "last_message": 0, + "last_seen": 0, + "rf_status": 255, + "battery_vp": 65535 + } + ] + }, + { + "_id": "12:34:56:1c:68:2e", + "station_name": "Bol\u00e5s", + "date_setup": 1470935400, + "last_setup": 1470935400, + "type": "NAMain", + "last_status_store": 1588481399, + "module_name": "Inne - Nere", + "firmware": 177, + "last_upgrade": 1470935401, + "wifi_status": 13, + "reachable": true, + "co2_calibrating": false, + "data_type": ["Temperature", "CO2", "Humidity", "Noise", "Pressure"], + "place": { + "altitude": 93, + "city": "Gothenburg", + "country": "SE", + "timezone": "Europe/Stockholm", + "location": [11.6136629, 57.7006827] + }, + "dashboard_data": { + "time_utc": 1588481387, + "Temperature": 20.8, + "CO2": 674, + "Humidity": 41, + "Noise": 34, + "Pressure": 1012.1, + "AbsolutePressure": 1001, + "min_temp": 20.8, + "max_temp": 22.2, + "date_max_temp": 1588456859, + "date_min_temp": 1588480176, + "temp_trend": "stable", + "pressure_trend": "up" + }, + "modules": [ + { + "_id": "12:34:56:02:b3:da", + "type": "NAModule3", + "module_name": "Regnm\u00e4tare", + "last_setup": 1470937706, + "data_type": ["Rain"], + "battery_percent": 81, + "reachable": true, + "firmware": 12, + "last_message": 1588481393, + "last_seen": 1588481386, + "rf_status": 67, + "battery_vp": 5582, + "dashboard_data": { + "time_utc": 1588481386, + "Rain": 0, + "sum_rain_1": 0, + "sum_rain_24": 0.1 } - } - }, - "status": "ok", - "time_exec": 0.91107702255249, - "time_server": 1559413602 -} \ No newline at end of file + }, + { + "_id": "12:34:56:03:76:60", + "type": "NAModule4", + "module_name": "Inne - Uppe", + "last_setup": 1470938089, + "data_type": ["Temperature", "CO2", "Humidity"], + "battery_percent": 14, + "reachable": true, + "firmware": 50, + "last_message": 1588481393, + "last_seen": 1588481374, + "rf_status": 70, + "battery_vp": 4448, + "dashboard_data": { + "time_utc": 1588481374, + "Temperature": 19.6, + "CO2": 696, + "Humidity": 41, + "min_temp": 19.6, + "max_temp": 20.5, + "date_max_temp": 1588456817, + "date_min_temp": 1588481374, + "temp_trend": "stable" + } + }, + { + "_id": "12:34:56:32:db:06", + "type": "NAModule1", + "module_name": "Ute", + "last_setup": 1566326027, + "data_type": ["Temperature", "Humidity"], + "battery_percent": 81, + "reachable": true, + "firmware": 50, + "last_message": 1588481393, + "last_seen": 1588481380, + "rf_status": 61, + "battery_vp": 5544, + "dashboard_data": { + "time_utc": 1588481380, + "Temperature": 6.4, + "Humidity": 91, + "min_temp": 3.6, + "max_temp": 6.4, + "date_max_temp": 1588481380, + "date_min_temp": 1588471383, + "temp_trend": "up" + } + } + ] + }, + { + "_id": "12:34:56:1d:68:2e", + "date_setup": 1470935500, + "last_setup": 1470935500, + "type": "NAMain", + "last_status_store": 1588481399, + "module_name": "Basisstation", + "firmware": 177, + "last_upgrade": 1470935401, + "wifi_status": 13, + "reachable": true, + "co2_calibrating": false, + "data_type": ["Temperature", "CO2", "Humidity", "Noise", "Pressure"], + "place": { + "altitude": 93, + "city": "Gothenburg", + "country": "SE", + "timezone": "Europe/Stockholm", + "location": [11.6136629, 57.7006827] + }, + "dashboard_data": { + "time_utc": 1588481387, + "Temperature": 20.8, + "CO2": 674, + "Humidity": 41, + "Noise": 34, + "Pressure": 1012.1, + "AbsolutePressure": 1001, + "min_temp": 20.8, + "max_temp": 22.2, + "date_max_temp": 1588456859, + "date_min_temp": 1588480176, + "temp_trend": "stable", + "pressure_trend": "up" + }, + "modules": [] + }, + { + "_id": "12:34:56:58:c8:54", + "date_setup": 1605594014, + "last_setup": 1605594014, + "type": "NAMain", + "last_status_store": 1605878352, + "firmware": 178, + "wifi_status": 47, + "reachable": true, + "co2_calibrating": false, + "data_type": ["Temperature", "CO2", "Humidity", "Noise", "Pressure"], + "place": { + "altitude": 65, + "city": "Njurunda District", + "country": "SE", + "timezone": "Europe/Stockholm", + "location": [17.123456, 62.123456] + }, + "station_name": "Njurunda (Indoor)", + "home_id": "5fb36b9ec68fd10c6467ca65", + "home_name": "Njurunda", + "dashboard_data": { + "time_utc": 1605878349, + "Temperature": 19.7, + "CO2": 993, + "Humidity": 40, + "Noise": 40, + "Pressure": 1015.6, + "AbsolutePressure": 1007.8, + "min_temp": 19.7, + "max_temp": 20.4, + "date_max_temp": 1605826917, + "date_min_temp": 1605873207, + "temp_trend": "stable", + "pressure_trend": "up" + }, + "modules": [ + { + "_id": "12:34:56:58:e6:38", + "type": "NAModule1", + "last_setup": 1605594034, + "data_type": ["Temperature", "Humidity"], + "battery_percent": 100, + "reachable": true, + "firmware": 50, + "last_message": 1605878347, + "last_seen": 1605878328, + "rf_status": 62, + "battery_vp": 6198, + "dashboard_data": { + "time_utc": 1605878328, + "Temperature": 0.6, + "Humidity": 77, + "min_temp": -2.1, + "max_temp": 1.5, + "date_max_temp": 1605865920, + "date_min_temp": 1605826904, + "temp_trend": "down" + } + } + ] + } + ], + "user": { + "mail": "john@doe.com", + "administrative": { + "lang": "de-DE", + "reg_locale": "de-DE", + "country": "DE", + "unit": 0, + "windunit": 0, + "pressureunit": 0, + "feel_like_algo": 0 + } + } + }, + "status": "ok", + "time_exec": 0.91107702255249, + "time_server": 1559413602 +} diff --git a/tests/components/netatmo/fixtures/homesdata.json b/tests/components/netatmo/fixtures/homesdata.json index 8c6587ca973..a56ccb236b5 100644 --- a/tests/components/netatmo/fixtures/homesdata.json +++ b/tests/components/netatmo/fixtures/homesdata.json @@ -5,10 +5,7 @@ "id": "91763b24c43d3e344f424e8b", "name": "MYHOME", "altitude": 112, - "coordinates": [ - 52.516263, - 13.377726 - ], + "coordinates": [52.516263, 13.377726], "country": "DE", "timezone": "Europe/Berlin", "rooms": [ @@ -16,33 +13,25 @@ "id": "2746182631", "name": "Livingroom", "type": "livingroom", - "module_ids": [ - "12:34:56:00:01:ae" - ] + "module_ids": ["12:34:56:00:01:ae"] }, { "id": "3688132631", "name": "Hall", "type": "custom", - "module_ids": [ - "12:34:56:00:f1:62" - ] + "module_ids": ["12:34:56:00:f1:62"] }, { "id": "2833524037", "name": "Entrada", "type": "lobby", - "module_ids": [ - "12:34:56:03:a5:54" - ] + "module_ids": ["12:34:56:03:a5:54"] }, { "id": "2940411577", "name": "Cocina", "type": "kitchen", - "module_ids": [ - "12:34:56:03:a0:ac" - ] + "module_ids": ["12:34:56:03:a0:ac"] } ], "modules": [ @@ -404,10 +393,7 @@ "id": "111111111111111111111401", "name": "Home with no modules", "altitude": 9, - "coordinates": [ - 1.23456789, - 50.0987654 - ], + "coordinates": [1.23456789, 50.0987654], "country": "BE", "timezone": "Europe/Brussels", "rooms": [ @@ -494,4 +480,4 @@ "status": "ok", "time_exec": 0.056135892868042, "time_server": 1559171003 -} \ No newline at end of file +} diff --git a/tests/components/netatmo/fixtures/homestatus_111111111111111111111401.json b/tests/components/netatmo/fixtures/homestatus_111111111111111111111401.json index 2ae65dc0d21..1f7a99207b0 100644 --- a/tests/components/netatmo/fixtures/homestatus_111111111111111111111401.json +++ b/tests/components/netatmo/fixtures/homestatus_111111111111111111111401.json @@ -1,4 +1,4 @@ { - "status": "ok", - "time_server": 1638873670 -} \ No newline at end of file + "status": "ok", + "time_server": 1638873670 +} diff --git a/tests/components/netatmo/fixtures/homestatus_91763b24c43d3e344f424e8b.json b/tests/components/netatmo/fixtures/homestatus_91763b24c43d3e344f424e8b.json index 490bf999045..12c7044aaaa 100644 --- a/tests/components/netatmo/fixtures/homestatus_91763b24c43d3e344f424e8b.json +++ b/tests/components/netatmo/fixtures/homestatus_91763b24c43d3e344f424e8b.json @@ -1,110 +1,110 @@ { - "status": "ok", - "time_server": 1559292039, - "body": { - "home": { - "modules": [ - { - "id": "12:34:56:00:f1:62", - "type": "NACamera", - "monitoring": "on", - "sd_status": 4, - "alim_status": 2, - "locked": false, - "vpn_url": "https://prodvpn-eu-2.netatmo.net/restricted/10.255.123.45/609e27de5699fb18147ab47d06846631/MTRPn_BeWCav5RBq4U1OMDruTW4dkQ0NuMwNDAw11g,,", - "is_local": true - }, - { - "id": "12:34:56:00:fa:d0", - "type": "NAPlug", - "firmware_revision": 174, - "rf_strength": 107, - "wifi_strength": 42 - }, - { - "id": "12:34:56:00:01:ae", - "reachable": true, - "type": "NATherm1", - "firmware_revision": 65, - "rf_strength": 58, - "boiler_valve_comfort_boost": false, - "boiler_status": false, - "anticipating": false, - "bridge": "12:34:56:00:fa:d0", - "battery_state": "high" - }, - { - "id": "12:34:56:03:a5:54", - "reachable": true, - "type": "NRV", - "firmware_revision": 79, - "rf_strength": 51, - "bridge": "12:34:56:00:fa:d0", - "battery_state": "full" - }, - { - "id": "12:34:56:03:a0:ac", - "reachable": true, - "type": "NRV", - "firmware_revision": 79, - "rf_strength": 59, - "bridge": "12:34:56:00:fa:d0", - "battery_state": "full" - } - ], - "rooms": [ - { - "id": "2746182631", - "reachable": true, - "therm_measured_temperature": 19.8, - "therm_setpoint_temperature": 12, - "therm_setpoint_mode": "schedule", - "therm_setpoint_start_time": 1559229567, - "therm_setpoint_end_time": 0 - }, - { - "id": "2940411577", - "reachable": true, - "therm_measured_temperature": 5, - "heating_power_request": 1, - "therm_setpoint_temperature": 7, - "therm_setpoint_mode": "away", - "therm_setpoint_start_time": 0, - "therm_setpoint_end_time": 0, - "anticipating": false, - "open_window": false - }, - { - "id": "2833524037", - "reachable": true, - "therm_measured_temperature": 24.5, - "heating_power_request": 0, - "therm_setpoint_temperature": 7, - "therm_setpoint_mode": "hg", - "therm_setpoint_start_time": 0, - "therm_setpoint_end_time": 0, - "anticipating": false, - "open_window": false - } - ], - "id": "91763b24c43d3e344f424e8b", - "persons": [ - { - "id": "91827374-7e04-5298-83ad-a0cb8372dff1", - "last_seen": 1557071156, - "out_of_sight": true - }, - { - "id": "91827375-7e04-5298-83ae-a0cb8372dff2", - "last_seen": 1559282761, - "out_of_sight": false - }, - { - "id": "91827376-7e04-5298-83af-a0cb8372dff3", - "last_seen": 1559224132, - "out_of_sight": true - } - ] + "status": "ok", + "time_server": 1559292039, + "body": { + "home": { + "modules": [ + { + "id": "12:34:56:00:f1:62", + "type": "NACamera", + "monitoring": "on", + "sd_status": 4, + "alim_status": 2, + "locked": false, + "vpn_url": "https://prodvpn-eu-2.netatmo.net/restricted/10.255.123.45/609e27de5699fb18147ab47d06846631/MTRPn_BeWCav5RBq4U1OMDruTW4dkQ0NuMwNDAw11g,,", + "is_local": true + }, + { + "id": "12:34:56:00:fa:d0", + "type": "NAPlug", + "firmware_revision": 174, + "rf_strength": 107, + "wifi_strength": 42 + }, + { + "id": "12:34:56:00:01:ae", + "reachable": true, + "type": "NATherm1", + "firmware_revision": 65, + "rf_strength": 58, + "boiler_valve_comfort_boost": false, + "boiler_status": false, + "anticipating": false, + "bridge": "12:34:56:00:fa:d0", + "battery_state": "high" + }, + { + "id": "12:34:56:03:a5:54", + "reachable": true, + "type": "NRV", + "firmware_revision": 79, + "rf_strength": 51, + "bridge": "12:34:56:00:fa:d0", + "battery_state": "full" + }, + { + "id": "12:34:56:03:a0:ac", + "reachable": true, + "type": "NRV", + "firmware_revision": 79, + "rf_strength": 59, + "bridge": "12:34:56:00:fa:d0", + "battery_state": "full" } + ], + "rooms": [ + { + "id": "2746182631", + "reachable": true, + "therm_measured_temperature": 19.8, + "therm_setpoint_temperature": 12, + "therm_setpoint_mode": "schedule", + "therm_setpoint_start_time": 1559229567, + "therm_setpoint_end_time": 0 + }, + { + "id": "2940411577", + "reachable": true, + "therm_measured_temperature": 5, + "heating_power_request": 1, + "therm_setpoint_temperature": 7, + "therm_setpoint_mode": "away", + "therm_setpoint_start_time": 0, + "therm_setpoint_end_time": 0, + "anticipating": false, + "open_window": false + }, + { + "id": "2833524037", + "reachable": true, + "therm_measured_temperature": 24.5, + "heating_power_request": 0, + "therm_setpoint_temperature": 7, + "therm_setpoint_mode": "hg", + "therm_setpoint_start_time": 0, + "therm_setpoint_end_time": 0, + "anticipating": false, + "open_window": false + } + ], + "id": "91763b24c43d3e344f424e8b", + "persons": [ + { + "id": "91827374-7e04-5298-83ad-a0cb8372dff1", + "last_seen": 1557071156, + "out_of_sight": true + }, + { + "id": "91827375-7e04-5298-83ae-a0cb8372dff2", + "last_seen": 1559282761, + "out_of_sight": false + }, + { + "id": "91827376-7e04-5298-83af-a0cb8372dff3", + "last_seen": 1559224132, + "out_of_sight": true + } + ] } -} \ No newline at end of file + } +} diff --git a/tests/components/netatmo/fixtures/ping.json b/tests/components/netatmo/fixtures/ping.json index 784975de5b0..9d2185c259e 100644 --- a/tests/components/netatmo/fixtures/ping.json +++ b/tests/components/netatmo/fixtures/ping.json @@ -1,4 +1,4 @@ { - "local_url": "http://192.168.0.123/678460a0d47e5618699fb31169e2b47d", - "product_name": "Welcome Netatmo" -} \ No newline at end of file + "local_url": "http://192.168.0.123/678460a0d47e5618699fb31169e2b47d", + "product_name": "Welcome Netatmo" +} diff --git a/tests/components/network/test_init.py b/tests/components/network/test_init.py index 1103c6fa850..dc1ed064dc9 100644 --- a/tests/components/network/test_init.py +++ b/tests/components/network/test_init.py @@ -27,6 +27,21 @@ def _mock_socket(sockname): return mock_socket +def _mock_cond_socket(sockname): + class CondMockSock(MagicMock): + def connect(self, addr): + """Mock connect that stores addr.""" + self._addr = addr[0] + + def getsockname(self): + """Return addr if it matches the mock sockname.""" + if self._addr == sockname: + return [sockname] + raise AttributeError() + + return CondMockSock() + + def _mock_socket_exception(exc): mock_socket = MagicMock() mock_socket.getsockname = Mock(side_effect=exc) @@ -650,3 +665,24 @@ async def test_async_get_source_ip_cannot_be_determined_and_no_enabled_addresses await hass.async_block_till_done() with pytest.raises(HomeAssistantError): await network.async_get_source_ip(hass, MDNS_TARGET_IP) + + +async def test_async_get_source_ip_no_ip_loopback(hass, hass_storage, caplog): + """Test getting the source ip address when all adapters are disabled no target is specified.""" + hass_storage[STORAGE_KEY] = { + "version": STORAGE_VERSION, + "key": STORAGE_KEY, + "data": {ATTR_CONFIGURED_ADAPTERS: ["eth1"]}, + } + + with patch( + "homeassistant.components.network.util.ifaddr.get_adapters", + return_value=[], + ), patch( + "homeassistant.components.network.util.socket.socket", + return_value=_mock_cond_socket(_LOOPBACK_IPADDR), + ): + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + await hass.async_block_till_done() + + assert await network.async_get_source_ip(hass) == "127.0.0.1" diff --git a/tests/components/nexia/fixtures/mobile_houses_123456.json b/tests/components/nexia/fixtures/mobile_houses_123456.json index 2bf3aa123b0..45cd3bb45cb 100644 --- a/tests/components/nexia/fixtures/mobile_houses_123456.json +++ b/tests/components/nexia/fixtures/mobile_houses_123456.json @@ -1,8036 +1,9650 @@ { - "success": true, - "error": null, - "result": { - "id": 123456, - "name": "Hidden", - "third_party_integrations": [], - "latitude": 12.7633, - "longitude": -12.3633, - "dealer_opt_in": true, - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/houses/123456" - }, - "edit": [{ - "href": "https://www.mynexia.com/mobile/houses/123456/edit", - "method": "GET" - }], - "child": [{ - "href": "https://www.mynexia.com/mobile/houses/123456/devices", - "type": "application/vnd.nexia.collection+json", - "data": { - "items": [{ - "id": 2059661, - "name": "Downstairs East Wing", - "name_editable": true, - "features": [{ - "name": "advanced_info", - "items": [{ - "type": "label_value", - "label": "Model", - "value": "XL1050" - }, { - "type": "label_value", - "label": "AUID", - "value": "000000" - }, { - "type": "label_value", - "label": "Firmware Build Number", - "value": "1581321824" - }, { - "type": "label_value", - "label": "Firmware Build Date", - "value": "2020-02-10 08:03:44 UTC" - }, { - "type": "label_value", - "label": "Firmware Version", - "value": "5.9.1" - }, { - "type": "label_value", - "label": "Zoning Enabled", - "value": "yes" - }] - }, { - "name": "thermostat", - "temperature": 71, - "status": "System Idle", - "status_icon": null, - "actions": {}, - "setpoint_delta": 3, - "scale": "f", - "setpoint_increment": 1.0, - "setpoint_heat_min": 55, - "setpoint_heat_max": 90, - "setpoint_cool_min": 60, - "setpoint_cool_max": 99 - }, { - "name": "connection", - "signal_strength": "unknown", - "is_connected": true - }, { - "name": "group", - "members": [{ - "type": "xxl_zone", - "id": 83261002, - "name": "Living East", - "current_zone_mode": "AUTO", - "temperature": 71, - "setpoints": { - "heat": 63, - "cool": 79 - }, - "operating_state": "", - "heating_setpoint": 63, - "cooling_setpoint": 79, - "zone_status": "", - "settings": [{ - "type": "preset_selected", - "title": "Preset", - "current_value": 0, - "options": [{ - "value": 0, - "label": "None" - }, { - "value": 1, - "label": "Home" - }, { - "value": 2, - "label": "Away" - }, { - "value": 3, - "label": "Sleep" - }], - "labels": ["None", "Home", "Away", "Sleep"], - "values": [0, 1, 2, 3], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83261002/preset_selected" - } - } - }, { - "type": "zone_mode", - "title": "Zone Mode", - "current_value": "AUTO", - "options": [{ - "value": "AUTO", - "label": "Auto" - }, { - "value": "COOL", - "label": "Cooling" - }, { - "value": "HEAT", - "label": "Heating" - }, { - "value": "OFF", - "label": "Off" - }], - "labels": ["Auto", "Cooling", "Heating", "Off"], - "values": ["AUTO", "COOL", "HEAT", "OFF"], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83261002/zone_mode" - } - } - }, { - "type": "run_mode", - "title": "Run Mode", - "current_value": "permanent_hold", - "options": [{ - "value": "permanent_hold", - "label": "Permanent Hold" - }, { - "value": "run_schedule", - "label": "Run Schedule" - }], - "labels": ["Permanent Hold", "Run Schedule"], - "values": ["permanent_hold", "run_schedule"], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83261002/run_mode" - } - } - }, { - "type": "scheduling_enabled", - "title": "Scheduling", - "current_value": true, - "options": [{ - "value": true, - "label": "ON" - }, { - "value": false, - "label": "OFF" - }], - "labels": ["ON", "OFF"], - "values": [true, false], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83261002/scheduling_enabled" - } - } - }], - "icon": { - "name": "thermostat", - "modifiers": ["temperature-71"] - }, - "features": [{ - "name": "thermostat", - "temperature": 71, - "status": "", - "status_icon": null, - "actions": { - "set_heat_setpoint": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83261002/setpoints" - }, - "set_cool_setpoint": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83261002/setpoints" - } - }, - "setpoint_delta": 3, - "scale": "f", - "setpoint_increment": 1.0, - "setpoint_heat_min": 55, - "setpoint_heat_max": 90, - "setpoint_cool_min": 60, - "setpoint_cool_max": 99, - "setpoint_heat": 63, - "setpoint_cool": 79, - "system_status": "System Idle" - }, { - "name": "connection", - "signal_strength": "unknown", - "is_connected": true - }, { - "name": "thermostat_mode", - "label": "Zone Mode", - "value": "AUTO", - "display_value": "Auto", - "options": [{ - "id": "thermostat_mode", - "label": "Zone Mode", - "value": "thermostat_mode", - "header": true - }, { - "value": "AUTO", - "label": "Auto" - }, { - "value": "COOL", - "label": "Cooling" - }, { - "value": "HEAT", - "label": "Heating" - }, { - "value": "OFF", - "label": "Off" - }], - "actions": { - "update_thermostat_mode": { - "method": "POST", - "href": "https://www.mynexia.com/mobile/xxl_zones/83261002/zone_mode" - } - } - }, { - "name": "thermostat_run_mode", - "label": "Run Mode", - "options": [{ - "id": "thermostat_run_mode", - "label": "Run Mode", - "value": "thermostat_run_mode", - "header": true - }, { - "id": "info_text", - "label": "Follow or override the schedule.", - "value": "info_text", - "info": true - }, { - "value": "permanent_hold", - "label": "Permanent Hold" - }, { - "value": "run_schedule", - "label": "Run Schedule" - }], - "value": "permanent_hold", - "display_value": "Hold", - "actions": { - "update_thermostat_run_mode": { - "method": "POST", - "href": "https://www.mynexia.com/mobile/xxl_zones/83261002/run_mode" - } - } - }, { - "name": "schedule", - "enabled": true, - "max_period_name_length": 10, - "setpoint_increment": 1, - "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83261002\u0026house_id=123456", - "actions": { - "get_active_schedule": { - "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83261002", - "method": "POST" - }, - "set_active_schedule": { - "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83261002", - "method": "POST" - }, - "get_default_schedule": { - "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83261002", - "method": "GET" - }, - "enable_scheduling": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83261002/scheduling_enabled", - "method": "POST", - "data": { - "value": true - } - } - }, - "can_add_remove_periods": true, - "max_periods_per_day": 4 - }], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83261002" - } - } - }, { - "type": "xxl_zone", - "id": 83261005, - "name": "Kitchen", - "current_zone_mode": "AUTO", - "temperature": 77, - "setpoints": { - "heat": 63, - "cool": 79 - }, - "operating_state": "", - "heating_setpoint": 63, - "cooling_setpoint": 79, - "zone_status": "", - "settings": [{ - "type": "preset_selected", - "title": "Preset", - "current_value": 0, - "options": [{ - "value": 0, - "label": "None" - }, { - "value": 1, - "label": "Home" - }, { - "value": 2, - "label": "Away" - }, { - "value": 3, - "label": "Sleep" - }], - "labels": ["None", "Home", "Away", "Sleep"], - "values": [0, 1, 2, 3], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83261005/preset_selected" - } - } - }, { - "type": "zone_mode", - "title": "Zone Mode", - "current_value": "AUTO", - "options": [{ - "value": "AUTO", - "label": "Auto" - }, { - "value": "COOL", - "label": "Cooling" - }, { - "value": "HEAT", - "label": "Heating" - }, { - "value": "OFF", - "label": "Off" - }], - "labels": ["Auto", "Cooling", "Heating", "Off"], - "values": ["AUTO", "COOL", "HEAT", "OFF"], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83261005/zone_mode" - } - } - }, { - "type": "run_mode", - "title": "Run Mode", - "current_value": "permanent_hold", - "options": [{ - "value": "permanent_hold", - "label": "Permanent Hold" - }, { - "value": "run_schedule", - "label": "Run Schedule" - }], - "labels": ["Permanent Hold", "Run Schedule"], - "values": ["permanent_hold", "run_schedule"], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83261005/run_mode" - } - } - }, { - "type": "scheduling_enabled", - "title": "Scheduling", - "current_value": true, - "options": [{ - "value": true, - "label": "ON" - }, { - "value": false, - "label": "OFF" - }], - "labels": ["ON", "OFF"], - "values": [true, false], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83261005/scheduling_enabled" - } - } - }], - "icon": { - "name": "thermostat", - "modifiers": ["temperature-77"] - }, - "features": [{ - "name": "thermostat", - "temperature": 77, - "status": "", - "status_icon": null, - "actions": { - "set_heat_setpoint": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83261005/setpoints" - }, - "set_cool_setpoint": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83261005/setpoints" - } - }, - "setpoint_delta": 3, - "scale": "f", - "setpoint_increment": 1.0, - "setpoint_heat_min": 55, - "setpoint_heat_max": 90, - "setpoint_cool_min": 60, - "setpoint_cool_max": 99, - "setpoint_heat": 63, - "setpoint_cool": 79, - "system_status": "System Idle" - }, { - "name": "connection", - "signal_strength": "unknown", - "is_connected": true - }, { - "name": "thermostat_mode", - "label": "Zone Mode", - "value": "AUTO", - "display_value": "Auto", - "options": [{ - "id": "thermostat_mode", - "label": "Zone Mode", - "value": "thermostat_mode", - "header": true - }, { - "value": "AUTO", - "label": "Auto" - }, { - "value": "COOL", - "label": "Cooling" - }, { - "value": "HEAT", - "label": "Heating" - }, { - "value": "OFF", - "label": "Off" - }], - "actions": { - "update_thermostat_mode": { - "method": "POST", - "href": "https://www.mynexia.com/mobile/xxl_zones/83261005/zone_mode" - } - } - }, { - "name": "thermostat_run_mode", - "label": "Run Mode", - "options": [{ - "id": "thermostat_run_mode", - "label": "Run Mode", - "value": "thermostat_run_mode", - "header": true - }, { - "id": "info_text", - "label": "Follow or override the schedule.", - "value": "info_text", - "info": true - }, { - "value": "permanent_hold", - "label": "Permanent Hold" - }, { - "value": "run_schedule", - "label": "Run Schedule" - }], - "value": "permanent_hold", - "display_value": "Hold", - "actions": { - "update_thermostat_run_mode": { - "method": "POST", - "href": "https://www.mynexia.com/mobile/xxl_zones/83261005/run_mode" - } - } - }, { - "name": "schedule", - "enabled": true, - "max_period_name_length": 10, - "setpoint_increment": 1, - "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83261005\u0026house_id=123456", - "actions": { - "get_active_schedule": { - "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83261005", - "method": "POST" - }, - "set_active_schedule": { - "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83261005", - "method": "POST" - }, - "get_default_schedule": { - "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83261005", - "method": "GET" - }, - "enable_scheduling": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83261005/scheduling_enabled", - "method": "POST", - "data": { - "value": true - } - } - }, - "can_add_remove_periods": true, - "max_periods_per_day": 4 - }], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83261005" - } - } - }, { - "type": "xxl_zone", - "id": 83261008, - "name": "Down Bedroom", - "current_zone_mode": "AUTO", - "temperature": 72, - "setpoints": { - "heat": 63, - "cool": 79 - }, - "operating_state": "", - "heating_setpoint": 63, - "cooling_setpoint": 79, - "zone_status": "", - "settings": [{ - "type": "preset_selected", - "title": "Preset", - "current_value": 0, - "options": [{ - "value": 0, - "label": "None" - }, { - "value": 1, - "label": "Home" - }, { - "value": 2, - "label": "Away" - }, { - "value": 3, - "label": "Sleep" - }], - "labels": ["None", "Home", "Away", "Sleep"], - "values": [0, 1, 2, 3], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83261008/preset_selected" - } - } - }, { - "type": "zone_mode", - "title": "Zone Mode", - "current_value": "AUTO", - "options": [{ - "value": "AUTO", - "label": "Auto" - }, { - "value": "COOL", - "label": "Cooling" - }, { - "value": "HEAT", - "label": "Heating" - }, { - "value": "OFF", - "label": "Off" - }], - "labels": ["Auto", "Cooling", "Heating", "Off"], - "values": ["AUTO", "COOL", "HEAT", "OFF"], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83261008/zone_mode" - } - } - }, { - "type": "run_mode", - "title": "Run Mode", - "current_value": "permanent_hold", - "options": [{ - "value": "permanent_hold", - "label": "Permanent Hold" - }, { - "value": "run_schedule", - "label": "Run Schedule" - }], - "labels": ["Permanent Hold", "Run Schedule"], - "values": ["permanent_hold", "run_schedule"], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83261008/run_mode" - } - } - }, { - "type": "scheduling_enabled", - "title": "Scheduling", - "current_value": true, - "options": [{ - "value": true, - "label": "ON" - }, { - "value": false, - "label": "OFF" - }], - "labels": ["ON", "OFF"], - "values": [true, false], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83261008/scheduling_enabled" - } - } - }], - "icon": { - "name": "thermostat", - "modifiers": ["temperature-72"] - }, - "features": [{ - "name": "thermostat", - "temperature": 72, - "status": "", - "status_icon": null, - "actions": { - "set_heat_setpoint": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83261008/setpoints" - }, - "set_cool_setpoint": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83261008/setpoints" - } - }, - "setpoint_delta": 3, - "scale": "f", - "setpoint_increment": 1.0, - "setpoint_heat_min": 55, - "setpoint_heat_max": 90, - "setpoint_cool_min": 60, - "setpoint_cool_max": 99, - "setpoint_heat": 63, - "setpoint_cool": 79, - "system_status": "System Idle" - }, { - "name": "connection", - "signal_strength": "unknown", - "is_connected": true - }, { - "name": "thermostat_mode", - "label": "Zone Mode", - "value": "AUTO", - "display_value": "Auto", - "options": [{ - "id": "thermostat_mode", - "label": "Zone Mode", - "value": "thermostat_mode", - "header": true - }, { - "value": "AUTO", - "label": "Auto" - }, { - "value": "COOL", - "label": "Cooling" - }, { - "value": "HEAT", - "label": "Heating" - }, { - "value": "OFF", - "label": "Off" - }], - "actions": { - "update_thermostat_mode": { - "method": "POST", - "href": "https://www.mynexia.com/mobile/xxl_zones/83261008/zone_mode" - } - } - }, { - "name": "thermostat_run_mode", - "label": "Run Mode", - "options": [{ - "id": "thermostat_run_mode", - "label": "Run Mode", - "value": "thermostat_run_mode", - "header": true - }, { - "id": "info_text", - "label": "Follow or override the schedule.", - "value": "info_text", - "info": true - }, { - "value": "permanent_hold", - "label": "Permanent Hold" - }, { - "value": "run_schedule", - "label": "Run Schedule" - }], - "value": "permanent_hold", - "display_value": "Hold", - "actions": { - "update_thermostat_run_mode": { - "method": "POST", - "href": "https://www.mynexia.com/mobile/xxl_zones/83261008/run_mode" - } - } - }, { - "name": "schedule", - "enabled": true, - "max_period_name_length": 10, - "setpoint_increment": 1, - "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83261008\u0026house_id=123456", - "actions": { - "get_active_schedule": { - "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83261008", - "method": "POST" - }, - "set_active_schedule": { - "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83261008", - "method": "POST" - }, - "get_default_schedule": { - "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83261008", - "method": "GET" - }, - "enable_scheduling": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83261008/scheduling_enabled", - "method": "POST", - "data": { - "value": true - } - } - }, - "can_add_remove_periods": true, - "max_periods_per_day": 4 - }], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83261008" - } - } - }, { - "type": "xxl_zone", - "id": 83261011, - "name": "Tech Room", - "current_zone_mode": "AUTO", - "temperature": 78, - "setpoints": { - "heat": 63, - "cool": 79 - }, - "operating_state": "", - "heating_setpoint": 63, - "cooling_setpoint": 79, - "zone_status": "", - "settings": [{ - "type": "preset_selected", - "title": "Preset", - "current_value": 0, - "options": [{ - "value": 0, - "label": "None" - }, { - "value": 1, - "label": "Home" - }, { - "value": 2, - "label": "Away" - }, { - "value": 3, - "label": "Sleep" - }], - "labels": ["None", "Home", "Away", "Sleep"], - "values": [0, 1, 2, 3], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83261011/preset_selected" - } - } - }, { - "type": "zone_mode", - "title": "Zone Mode", - "current_value": "AUTO", - "options": [{ - "value": "AUTO", - "label": "Auto" - }, { - "value": "COOL", - "label": "Cooling" - }, { - "value": "HEAT", - "label": "Heating" - }, { - "value": "OFF", - "label": "Off" - }], - "labels": ["Auto", "Cooling", "Heating", "Off"], - "values": ["AUTO", "COOL", "HEAT", "OFF"], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83261011/zone_mode" - } - } - }, { - "type": "run_mode", - "title": "Run Mode", - "current_value": "permanent_hold", - "options": [{ - "value": "permanent_hold", - "label": "Permanent Hold" - }, { - "value": "run_schedule", - "label": "Run Schedule" - }], - "labels": ["Permanent Hold", "Run Schedule"], - "values": ["permanent_hold", "run_schedule"], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83261011/run_mode" - } - } - }, { - "type": "scheduling_enabled", - "title": "Scheduling", - "current_value": true, - "options": [{ - "value": true, - "label": "ON" - }, { - "value": false, - "label": "OFF" - }], - "labels": ["ON", "OFF"], - "values": [true, false], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83261011/scheduling_enabled" - } - } - }], - "icon": { - "name": "thermostat", - "modifiers": ["temperature-78"] - }, - "features": [{ - "name": "thermostat", - "temperature": 78, - "status": "", - "status_icon": null, - "actions": { - "set_heat_setpoint": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83261011/setpoints" - }, - "set_cool_setpoint": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83261011/setpoints" - } - }, - "setpoint_delta": 3, - "scale": "f", - "setpoint_increment": 1.0, - "setpoint_heat_min": 55, - "setpoint_heat_max": 90, - "setpoint_cool_min": 60, - "setpoint_cool_max": 99, - "setpoint_heat": 63, - "setpoint_cool": 79, - "system_status": "System Idle" - }, { - "name": "connection", - "signal_strength": "unknown", - "is_connected": true - }, { - "name": "thermostat_mode", - "label": "Zone Mode", - "value": "AUTO", - "display_value": "Auto", - "options": [{ - "id": "thermostat_mode", - "label": "Zone Mode", - "value": "thermostat_mode", - "header": true - }, { - "value": "AUTO", - "label": "Auto" - }, { - "value": "COOL", - "label": "Cooling" - }, { - "value": "HEAT", - "label": "Heating" - }, { - "value": "OFF", - "label": "Off" - }], - "actions": { - "update_thermostat_mode": { - "method": "POST", - "href": "https://www.mynexia.com/mobile/xxl_zones/83261011/zone_mode" - } - } - }, { - "name": "thermostat_run_mode", - "label": "Run Mode", - "options": [{ - "id": "thermostat_run_mode", - "label": "Run Mode", - "value": "thermostat_run_mode", - "header": true - }, { - "id": "info_text", - "label": "Follow or override the schedule.", - "value": "info_text", - "info": true - }, { - "value": "permanent_hold", - "label": "Permanent Hold" - }, { - "value": "run_schedule", - "label": "Run Schedule" - }], - "value": "permanent_hold", - "display_value": "Hold", - "actions": { - "update_thermostat_run_mode": { - "method": "POST", - "href": "https://www.mynexia.com/mobile/xxl_zones/83261011/run_mode" - } - } - }, { - "name": "schedule", - "enabled": true, - "max_period_name_length": 10, - "setpoint_increment": 1, - "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83261011\u0026house_id=123456", - "actions": { - "get_active_schedule": { - "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83261011", - "method": "POST" - }, - "set_active_schedule": { - "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83261011", - "method": "POST" - }, - "get_default_schedule": { - "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83261011", - "method": "GET" - }, - "enable_scheduling": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83261011/scheduling_enabled", - "method": "POST", - "data": { - "value": true - } - } - }, - "can_add_remove_periods": true, - "max_periods_per_day": 4 - }], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83261011" - } - } - }] - }, { - "name": "thermostat_fan_mode", - "label": "Fan Mode", - "options": [{ - "id": "thermostat_fan_mode", - "label": "Fan Mode", - "value": "thermostat_fan_mode", - "header": true - }, { - "value": "auto", - "label": "Auto" - }, { - "value": "on", - "label": "On" - }, { - "value": "circulate", - "label": "Circulate" - }], - "value": "auto", - "display_value": "Auto", - "status_icon": { - "name": "thermostat_fan_off", - "modifiers": [] - }, - "actions": { - "update_thermostat_fan_mode": { - "method": "POST", - "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059661/fan_mode" - } - } - }, { - "name": "thermostat_compressor_speed", - "compressor_speed": 0.0 - }, { - "name": "runtime_history", - "actions": { - "get_runtime_history": { - "method": "GET", - "href": "https://www.mynexia.com/mobile/runtime_history/2059661?report_type=daily" - }, - "get_monthly_runtime_history": { - "method": "GET", - "href": "https://www.mynexia.com/mobile/runtime_history/2059661?report_type=monthly" - } - } - }], - "icon": [{ - "name": "thermostat", - "modifiers": ["temperature-71"] - }, { - "name": "thermostat", - "modifiers": ["temperature-77"] - }, { - "name": "thermostat", - "modifiers": ["temperature-72"] - }, { - "name": "thermostat", - "modifiers": ["temperature-78"] - }], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059661" - }, - "nexia:history": { - "href": "https://www.mynexia.com/mobile/houses/123456/events?device_id=2059661" - }, - "filter_events": { - "href": "https://www.mynexia.com/mobile/houses/123456/events/collection?sys_guid=cd9a70e8-fd0d-4b58-b071-05a202fd8953" - }, - "pending_request": { - "polling_path": "https://www.mynexia.com/backstage/announcements/be6d8ede5cac02fe8be18c334b04d539c9200fa9230eef63" - } - }, - "last_updated_at": "2020-03-11T15:15:53.000-05:00", - "settings": [{ - "type": "fan_mode", - "title": "Fan Mode", - "current_value": "auto", - "options": [{ - "value": "auto", - "label": "Auto" - }, { - "value": "on", - "label": "On" - }, { - "value": "circulate", - "label": "Circulate" - }], - "labels": ["Auto", "On", "Circulate"], - "values": ["auto", "on", "circulate"], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059661/fan_mode" - } - } - }, { - "type": "fan_speed", - "title": "Fan Speed", - "current_value": 0.35, - "options": [{ - "value": 0.35, - "label": "35%" - }, { - "value": 0.4, - "label": "40%" - }, { - "value": 0.45, - "label": "45%" - }, { - "value": 0.5, - "label": "50%" - }, { - "value": 0.55, - "label": "55%" - }, { - "value": 0.6, - "label": "60%" - }, { - "value": 0.65, - "label": "65%" - }, { - "value": 0.7, - "label": "70%" - }, { - "value": 0.75, - "label": "75%" - }, { - "value": 0.8, - "label": "80%" - }, { - "value": 0.85, - "label": "85%" - }, { - "value": 0.9, - "label": "90%" - }, { - "value": 0.95, - "label": "95%" - }, { - "value": 1.0, - "label": "100%" - }], - "labels": ["35%", "40%", "45%", "50%", "55%", "60%", "65%", "70%", "75%", "80%", "85%", "90%", "95%", "100%"], - "values": [0.35, 0.4, 0.45, 0.5, 0.55, 0.6, 0.65, 0.7, 0.75, 0.8, 0.85, 0.9, 0.95, 1.0], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059661/fan_speed" - } - } - }, { - "type": "fan_circulation_time", - "title": "Fan Circulation Time", - "current_value": 30, - "options": [{ - "value": 10, - "label": "10 minutes" - }, { - "value": 15, - "label": "15 minutes" - }, { - "value": 20, - "label": "20 minutes" - }, { - "value": 25, - "label": "25 minutes" - }, { - "value": 30, - "label": "30 minutes" - }, { - "value": 35, - "label": "35 minutes" - }, { - "value": 40, - "label": "40 minutes" - }, { - "value": 45, - "label": "45 minutes" - }, { - "value": 50, - "label": "50 minutes" - }, { - "value": 55, - "label": "55 minutes" - }], - "labels": ["10 minutes", "15 minutes", "20 minutes", "25 minutes", "30 minutes", "35 minutes", "40 minutes", "45 minutes", "50 minutes", "55 minutes"], - "values": [10, 15, 20, 25, 30, 35, 40, 45, 50, 55], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059661/fan_circulation_time" - } - } - }, { - "type": "air_cleaner_mode", - "title": "Air Cleaner Mode", - "current_value": "auto", - "options": [{ - "value": "auto", - "label": "Auto" - }, { - "value": "quick", - "label": "Quick" - }, { - "value": "allergy", - "label": "Allergy" - }], - "labels": ["Auto", "Quick", "Allergy"], - "values": ["auto", "quick", "allergy"], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059661/air_cleaner_mode" - } - } - }, { - "type": "dehumidify", - "title": "Cooling Dehumidify Set Point", - "current_value": 0.5, - "options": [{ - "value": 0.35, - "label": "35%" - }, { - "value": 0.4, - "label": "40%" - }, { - "value": 0.45, - "label": "45%" - }, { - "value": 0.5, - "label": "50%" - }, { - "value": 0.55, - "label": "55%" - }, { - "value": 0.6, - "label": "60%" - }, { - "value": 0.65, - "label": "65%" - }], - "labels": ["35%", "40%", "45%", "50%", "55%", "60%", "65%"], - "values": [0.35, 0.4, 0.45, 0.5, 0.55, 0.6, 0.65], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059661/dehumidify" - } - } - }, { - "type": "scale", - "title": "Temperature Scale", - "current_value": "f", - "options": [{ - "value": "f", - "label": "F" - }, { - "value": "c", - "label": "C" - }], - "labels": ["F", "C"], - "values": ["f", "c"], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059661/scale" - } - } - }], - "status_secondary": null, - "status_tertiary": null, - "type": "xxl_thermostat", - "has_outdoor_temperature": true, - "outdoor_temperature": "88", - "has_indoor_humidity": true, - "connected": true, - "indoor_humidity": "36", - "system_status": "System Idle", - "delta": 3, - "zones": [{ - "type": "xxl_zone", - "id": 83261002, - "name": "Living East", - "current_zone_mode": "AUTO", - "temperature": 71, - "setpoints": { - "heat": 63, - "cool": 79 - }, - "operating_state": "", - "heating_setpoint": 63, - "cooling_setpoint": 79, - "zone_status": "", - "settings": [{ - "type": "preset_selected", - "title": "Preset", - "current_value": 0, - "options": [{ - "value": 0, - "label": "None" - }, { - "value": 1, - "label": "Home" - }, { - "value": 2, - "label": "Away" - }, { - "value": 3, - "label": "Sleep" - }], - "labels": ["None", "Home", "Away", "Sleep"], - "values": [0, 1, 2, 3], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83261002/preset_selected" - } - } - }, { - "type": "zone_mode", - "title": "Zone Mode", - "current_value": "AUTO", - "options": [{ - "value": "AUTO", - "label": "Auto" - }, { - "value": "COOL", - "label": "Cooling" - }, { - "value": "HEAT", - "label": "Heating" - }, { - "value": "OFF", - "label": "Off" - }], - "labels": ["Auto", "Cooling", "Heating", "Off"], - "values": ["AUTO", "COOL", "HEAT", "OFF"], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83261002/zone_mode" - } - } - }, { - "type": "run_mode", - "title": "Run Mode", - "current_value": "permanent_hold", - "options": [{ - "value": "permanent_hold", - "label": "Permanent Hold" - }, { - "value": "run_schedule", - "label": "Run Schedule" - }], - "labels": ["Permanent Hold", "Run Schedule"], - "values": ["permanent_hold", "run_schedule"], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83261002/run_mode" - } - } - }, { - "type": "scheduling_enabled", - "title": "Scheduling", - "current_value": true, - "options": [{ - "value": true, - "label": "ON" - }, { - "value": false, - "label": "OFF" - }], - "labels": ["ON", "OFF"], - "values": [true, false], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83261002/scheduling_enabled" - } - } - }], - "icon": { - "name": "thermostat", - "modifiers": ["temperature-71"] - }, - "features": [{ - "name": "thermostat", - "temperature": 71, - "status": "", - "status_icon": null, - "actions": { - "set_heat_setpoint": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83261002/setpoints" - }, - "set_cool_setpoint": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83261002/setpoints" - } - }, - "setpoint_delta": 3, - "scale": "f", - "setpoint_increment": 1.0, - "setpoint_heat_min": 55, - "setpoint_heat_max": 90, - "setpoint_cool_min": 60, - "setpoint_cool_max": 99, - "setpoint_heat": 63, - "setpoint_cool": 79, - "system_status": "System Idle" - }, { - "name": "connection", - "signal_strength": "unknown", - "is_connected": true - }, { - "name": "thermostat_mode", - "label": "Zone Mode", - "value": "AUTO", - "display_value": "Auto", - "options": [{ - "id": "thermostat_mode", - "label": "Zone Mode", - "value": "thermostat_mode", - "header": true - }, { - "value": "AUTO", - "label": "Auto" - }, { - "value": "COOL", - "label": "Cooling" - }, { - "value": "HEAT", - "label": "Heating" - }, { - "value": "OFF", - "label": "Off" - }], - "actions": { - "update_thermostat_mode": { - "method": "POST", - "href": "https://www.mynexia.com/mobile/xxl_zones/83261002/zone_mode" - } - } - }, { - "name": "thermostat_run_mode", - "label": "Run Mode", - "options": [{ - "id": "thermostat_run_mode", - "label": "Run Mode", - "value": "thermostat_run_mode", - "header": true - }, { - "id": "info_text", - "label": "Follow or override the schedule.", - "value": "info_text", - "info": true - }, { - "value": "permanent_hold", - "label": "Permanent Hold" - }, { - "value": "run_schedule", - "label": "Run Schedule" - }], - "value": "permanent_hold", - "display_value": "Hold", - "actions": { - "update_thermostat_run_mode": { - "method": "POST", - "href": "https://www.mynexia.com/mobile/xxl_zones/83261002/run_mode" - } - } - }, { - "name": "schedule", - "enabled": true, - "max_period_name_length": 10, - "setpoint_increment": 1, - "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83261002\u0026house_id=123456", - "actions": { - "get_active_schedule": { - "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83261002", - "method": "POST" - }, - "set_active_schedule": { - "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83261002", - "method": "POST" - }, - "get_default_schedule": { - "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83261002", - "method": "GET" - }, - "enable_scheduling": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83261002/scheduling_enabled", - "method": "POST", - "data": { - "value": true - } - } - }, - "can_add_remove_periods": true, - "max_periods_per_day": 4 - }], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83261002" - } - } - }, { - "type": "xxl_zone", - "id": 83261005, - "name": "Kitchen", - "current_zone_mode": "AUTO", - "temperature": 77, - "setpoints": { - "heat": 63, - "cool": 79 - }, - "operating_state": "", - "heating_setpoint": 63, - "cooling_setpoint": 79, - "zone_status": "", - "settings": [{ - "type": "preset_selected", - "title": "Preset", - "current_value": 0, - "options": [{ - "value": 0, - "label": "None" - }, { - "value": 1, - "label": "Home" - }, { - "value": 2, - "label": "Away" - }, { - "value": 3, - "label": "Sleep" - }], - "labels": ["None", "Home", "Away", "Sleep"], - "values": [0, 1, 2, 3], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83261005/preset_selected" - } - } - }, { - "type": "zone_mode", - "title": "Zone Mode", - "current_value": "AUTO", - "options": [{ - "value": "AUTO", - "label": "Auto" - }, { - "value": "COOL", - "label": "Cooling" - }, { - "value": "HEAT", - "label": "Heating" - }, { - "value": "OFF", - "label": "Off" - }], - "labels": ["Auto", "Cooling", "Heating", "Off"], - "values": ["AUTO", "COOL", "HEAT", "OFF"], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83261005/zone_mode" - } - } - }, { - "type": "run_mode", - "title": "Run Mode", - "current_value": "permanent_hold", - "options": [{ - "value": "permanent_hold", - "label": "Permanent Hold" - }, { - "value": "run_schedule", - "label": "Run Schedule" - }], - "labels": ["Permanent Hold", "Run Schedule"], - "values": ["permanent_hold", "run_schedule"], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83261005/run_mode" - } - } - }, { - "type": "scheduling_enabled", - "title": "Scheduling", - "current_value": true, - "options": [{ - "value": true, - "label": "ON" - }, { - "value": false, - "label": "OFF" - }], - "labels": ["ON", "OFF"], - "values": [true, false], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83261005/scheduling_enabled" - } - } - }], - "icon": { - "name": "thermostat", - "modifiers": ["temperature-77"] - }, - "features": [{ - "name": "thermostat", - "temperature": 77, - "status": "", - "status_icon": null, - "actions": { - "set_heat_setpoint": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83261005/setpoints" - }, - "set_cool_setpoint": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83261005/setpoints" - } - }, - "setpoint_delta": 3, - "scale": "f", - "setpoint_increment": 1.0, - "setpoint_heat_min": 55, - "setpoint_heat_max": 90, - "setpoint_cool_min": 60, - "setpoint_cool_max": 99, - "setpoint_heat": 63, - "setpoint_cool": 79, - "system_status": "System Idle" - }, { - "name": "connection", - "signal_strength": "unknown", - "is_connected": true - }, { - "name": "thermostat_mode", - "label": "Zone Mode", - "value": "AUTO", - "display_value": "Auto", - "options": [{ - "id": "thermostat_mode", - "label": "Zone Mode", - "value": "thermostat_mode", - "header": true - }, { - "value": "AUTO", - "label": "Auto" - }, { - "value": "COOL", - "label": "Cooling" - }, { - "value": "HEAT", - "label": "Heating" - }, { - "value": "OFF", - "label": "Off" - }], - "actions": { - "update_thermostat_mode": { - "method": "POST", - "href": "https://www.mynexia.com/mobile/xxl_zones/83261005/zone_mode" - } - } - }, { - "name": "thermostat_run_mode", - "label": "Run Mode", - "options": [{ - "id": "thermostat_run_mode", - "label": "Run Mode", - "value": "thermostat_run_mode", - "header": true - }, { - "id": "info_text", - "label": "Follow or override the schedule.", - "value": "info_text", - "info": true - }, { - "value": "permanent_hold", - "label": "Permanent Hold" - }, { - "value": "run_schedule", - "label": "Run Schedule" - }], - "value": "permanent_hold", - "display_value": "Hold", - "actions": { - "update_thermostat_run_mode": { - "method": "POST", - "href": "https://www.mynexia.com/mobile/xxl_zones/83261005/run_mode" - } - } - }, { - "name": "schedule", - "enabled": true, - "max_period_name_length": 10, - "setpoint_increment": 1, - "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83261005\u0026house_id=123456", - "actions": { - "get_active_schedule": { - "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83261005", - "method": "POST" - }, - "set_active_schedule": { - "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83261005", - "method": "POST" - }, - "get_default_schedule": { - "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83261005", - "method": "GET" - }, - "enable_scheduling": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83261005/scheduling_enabled", - "method": "POST", - "data": { - "value": true - } - } - }, - "can_add_remove_periods": true, - "max_periods_per_day": 4 - }], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83261005" - } - } - }, { - "type": "xxl_zone", - "id": 83261008, - "name": "Down Bedroom", - "current_zone_mode": "AUTO", - "temperature": 72, - "setpoints": { - "heat": 63, - "cool": 79 - }, - "operating_state": "", - "heating_setpoint": 63, - "cooling_setpoint": 79, - "zone_status": "", - "settings": [{ - "type": "preset_selected", - "title": "Preset", - "current_value": 0, - "options": [{ - "value": 0, - "label": "None" - }, { - "value": 1, - "label": "Home" - }, { - "value": 2, - "label": "Away" - }, { - "value": 3, - "label": "Sleep" - }], - "labels": ["None", "Home", "Away", "Sleep"], - "values": [0, 1, 2, 3], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83261008/preset_selected" - } - } - }, { - "type": "zone_mode", - "title": "Zone Mode", - "current_value": "AUTO", - "options": [{ - "value": "AUTO", - "label": "Auto" - }, { - "value": "COOL", - "label": "Cooling" - }, { - "value": "HEAT", - "label": "Heating" - }, { - "value": "OFF", - "label": "Off" - }], - "labels": ["Auto", "Cooling", "Heating", "Off"], - "values": ["AUTO", "COOL", "HEAT", "OFF"], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83261008/zone_mode" - } - } - }, { - "type": "run_mode", - "title": "Run Mode", - "current_value": "permanent_hold", - "options": [{ - "value": "permanent_hold", - "label": "Permanent Hold" - }, { - "value": "run_schedule", - "label": "Run Schedule" - }], - "labels": ["Permanent Hold", "Run Schedule"], - "values": ["permanent_hold", "run_schedule"], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83261008/run_mode" - } - } - }, { - "type": "scheduling_enabled", - "title": "Scheduling", - "current_value": true, - "options": [{ - "value": true, - "label": "ON" - }, { - "value": false, - "label": "OFF" - }], - "labels": ["ON", "OFF"], - "values": [true, false], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83261008/scheduling_enabled" - } - } - }], - "icon": { - "name": "thermostat", - "modifiers": ["temperature-72"] - }, - "features": [{ - "name": "thermostat", - "temperature": 72, - "status": "", - "status_icon": null, - "actions": { - "set_heat_setpoint": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83261008/setpoints" - }, - "set_cool_setpoint": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83261008/setpoints" - } - }, - "setpoint_delta": 3, - "scale": "f", - "setpoint_increment": 1.0, - "setpoint_heat_min": 55, - "setpoint_heat_max": 90, - "setpoint_cool_min": 60, - "setpoint_cool_max": 99, - "setpoint_heat": 63, - "setpoint_cool": 79, - "system_status": "System Idle" - }, { - "name": "connection", - "signal_strength": "unknown", - "is_connected": true - }, { - "name": "thermostat_mode", - "label": "Zone Mode", - "value": "AUTO", - "display_value": "Auto", - "options": [{ - "id": "thermostat_mode", - "label": "Zone Mode", - "value": "thermostat_mode", - "header": true - }, { - "value": "AUTO", - "label": "Auto" - }, { - "value": "COOL", - "label": "Cooling" - }, { - "value": "HEAT", - "label": "Heating" - }, { - "value": "OFF", - "label": "Off" - }], - "actions": { - "update_thermostat_mode": { - "method": "POST", - "href": "https://www.mynexia.com/mobile/xxl_zones/83261008/zone_mode" - } - } - }, { - "name": "thermostat_run_mode", - "label": "Run Mode", - "options": [{ - "id": "thermostat_run_mode", - "label": "Run Mode", - "value": "thermostat_run_mode", - "header": true - }, { - "id": "info_text", - "label": "Follow or override the schedule.", - "value": "info_text", - "info": true - }, { - "value": "permanent_hold", - "label": "Permanent Hold" - }, { - "value": "run_schedule", - "label": "Run Schedule" - }], - "value": "permanent_hold", - "display_value": "Hold", - "actions": { - "update_thermostat_run_mode": { - "method": "POST", - "href": "https://www.mynexia.com/mobile/xxl_zones/83261008/run_mode" - } - } - }, { - "name": "schedule", - "enabled": true, - "max_period_name_length": 10, - "setpoint_increment": 1, - "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83261008\u0026house_id=123456", - "actions": { - "get_active_schedule": { - "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83261008", - "method": "POST" - }, - "set_active_schedule": { - "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83261008", - "method": "POST" - }, - "get_default_schedule": { - "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83261008", - "method": "GET" - }, - "enable_scheduling": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83261008/scheduling_enabled", - "method": "POST", - "data": { - "value": true - } - } - }, - "can_add_remove_periods": true, - "max_periods_per_day": 4 - }], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83261008" - } - } - }, { - "type": "xxl_zone", - "id": 83261011, - "name": "Tech Room", - "current_zone_mode": "AUTO", - "temperature": 78, - "setpoints": { - "heat": 63, - "cool": 79 - }, - "operating_state": "", - "heating_setpoint": 63, - "cooling_setpoint": 79, - "zone_status": "", - "settings": [{ - "type": "preset_selected", - "title": "Preset", - "current_value": 0, - "options": [{ - "value": 0, - "label": "None" - }, { - "value": 1, - "label": "Home" - }, { - "value": 2, - "label": "Away" - }, { - "value": 3, - "label": "Sleep" - }], - "labels": ["None", "Home", "Away", "Sleep"], - "values": [0, 1, 2, 3], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83261011/preset_selected" - } - } - }, { - "type": "zone_mode", - "title": "Zone Mode", - "current_value": "AUTO", - "options": [{ - "value": "AUTO", - "label": "Auto" - }, { - "value": "COOL", - "label": "Cooling" - }, { - "value": "HEAT", - "label": "Heating" - }, { - "value": "OFF", - "label": "Off" - }], - "labels": ["Auto", "Cooling", "Heating", "Off"], - "values": ["AUTO", "COOL", "HEAT", "OFF"], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83261011/zone_mode" - } - } - }, { - "type": "run_mode", - "title": "Run Mode", - "current_value": "permanent_hold", - "options": [{ - "value": "permanent_hold", - "label": "Permanent Hold" - }, { - "value": "run_schedule", - "label": "Run Schedule" - }], - "labels": ["Permanent Hold", "Run Schedule"], - "values": ["permanent_hold", "run_schedule"], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83261011/run_mode" - } - } - }, { - "type": "scheduling_enabled", - "title": "Scheduling", - "current_value": true, - "options": [{ - "value": true, - "label": "ON" - }, { - "value": false, - "label": "OFF" - }], - "labels": ["ON", "OFF"], - "values": [true, false], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83261011/scheduling_enabled" - } - } - }], - "icon": { - "name": "thermostat", - "modifiers": ["temperature-78"] - }, - "features": [{ - "name": "thermostat", - "temperature": 78, - "status": "", - "status_icon": null, - "actions": { - "set_heat_setpoint": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83261011/setpoints" - }, - "set_cool_setpoint": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83261011/setpoints" - } - }, - "setpoint_delta": 3, - "scale": "f", - "setpoint_increment": 1.0, - "setpoint_heat_min": 55, - "setpoint_heat_max": 90, - "setpoint_cool_min": 60, - "setpoint_cool_max": 99, - "setpoint_heat": 63, - "setpoint_cool": 79, - "system_status": "System Idle" - }, { - "name": "connection", - "signal_strength": "unknown", - "is_connected": true - }, { - "name": "thermostat_mode", - "label": "Zone Mode", - "value": "AUTO", - "display_value": "Auto", - "options": [{ - "id": "thermostat_mode", - "label": "Zone Mode", - "value": "thermostat_mode", - "header": true - }, { - "value": "AUTO", - "label": "Auto" - }, { - "value": "COOL", - "label": "Cooling" - }, { - "value": "HEAT", - "label": "Heating" - }, { - "value": "OFF", - "label": "Off" - }], - "actions": { - "update_thermostat_mode": { - "method": "POST", - "href": "https://www.mynexia.com/mobile/xxl_zones/83261011/zone_mode" - } - } - }, { - "name": "thermostat_run_mode", - "label": "Run Mode", - "options": [{ - "id": "thermostat_run_mode", - "label": "Run Mode", - "value": "thermostat_run_mode", - "header": true - }, { - "id": "info_text", - "label": "Follow or override the schedule.", - "value": "info_text", - "info": true - }, { - "value": "permanent_hold", - "label": "Permanent Hold" - }, { - "value": "run_schedule", - "label": "Run Schedule" - }], - "value": "permanent_hold", - "display_value": "Hold", - "actions": { - "update_thermostat_run_mode": { - "method": "POST", - "href": "https://www.mynexia.com/mobile/xxl_zones/83261011/run_mode" - } - } - }, { - "name": "schedule", - "enabled": true, - "max_period_name_length": 10, - "setpoint_increment": 1, - "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83261011\u0026house_id=123456", - "actions": { - "get_active_schedule": { - "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83261011", - "method": "POST" - }, - "set_active_schedule": { - "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83261011", - "method": "POST" - }, - "get_default_schedule": { - "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83261011", - "method": "GET" - }, - "enable_scheduling": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83261011/scheduling_enabled", - "method": "POST", - "data": { - "value": true - } - } - }, - "can_add_remove_periods": true, - "max_periods_per_day": 4 - }], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83261011" - } - } - }] - }, { - "id": 2059676, - "name": "Downstairs West Wing", - "name_editable": true, - "features": [{ - "name": "advanced_info", - "items": [{ - "type": "label_value", - "label": "Model", - "value": "XL1050" - }, { - "type": "label_value", - "label": "AUID", - "value": "02853E08" - }, { - "type": "label_value", - "label": "Firmware Build Number", - "value": "1581321824" - }, { - "type": "label_value", - "label": "Firmware Build Date", - "value": "2020-02-10 08:03:44 UTC" - }, { - "type": "label_value", - "label": "Firmware Version", - "value": "5.9.1" - }, { - "type": "label_value", - "label": "Zoning Enabled", - "value": "yes" - }] - }, { - "name": "thermostat", - "temperature": 75, - "status": "System Idle", - "status_icon": null, - "actions": {}, - "setpoint_delta": 3, - "scale": "f", - "setpoint_increment": 1.0, - "setpoint_heat_min": 55, - "setpoint_heat_max": 90, - "setpoint_cool_min": 60, - "setpoint_cool_max": 99 - }, { - "name": "connection", - "signal_strength": "unknown", - "is_connected": true - }, { - "name": "group", - "members": [{ - "type": "xxl_zone", - "id": 83261015, - "name": "Living West", - "current_zone_mode": "AUTO", - "temperature": 75, - "setpoints": { - "heat": 63, - "cool": 79 - }, - "operating_state": "", - "heating_setpoint": 63, - "cooling_setpoint": 79, - "zone_status": "", - "settings": [{ - "type": "preset_selected", - "title": "Preset", - "current_value": 0, - "options": [{ - "value": 0, - "label": "None" - }, { - "value": 1, - "label": "Home" - }, { - "value": 2, - "label": "Away" - }, { - "value": 3, - "label": "Sleep" - }], - "labels": ["None", "Home", "Away", "Sleep"], - "values": [0, 1, 2, 3], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83261015/preset_selected" - } - } - }, { - "type": "zone_mode", - "title": "Zone Mode", - "current_value": "AUTO", - "options": [{ - "value": "AUTO", - "label": "Auto" - }, { - "value": "COOL", - "label": "Cooling" - }, { - "value": "HEAT", - "label": "Heating" - }, { - "value": "OFF", - "label": "Off" - }], - "labels": ["Auto", "Cooling", "Heating", "Off"], - "values": ["AUTO", "COOL", "HEAT", "OFF"], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83261015/zone_mode" - } - } - }, { - "type": "run_mode", - "title": "Run Mode", - "current_value": "permanent_hold", - "options": [{ - "value": "permanent_hold", - "label": "Permanent Hold" - }, { - "value": "run_schedule", - "label": "Run Schedule" - }], - "labels": ["Permanent Hold", "Run Schedule"], - "values": ["permanent_hold", "run_schedule"], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83261015/run_mode" - } - } - }, { - "type": "scheduling_enabled", - "title": "Scheduling", - "current_value": true, - "options": [{ - "value": true, - "label": "ON" - }, { - "value": false, - "label": "OFF" - }], - "labels": ["ON", "OFF"], - "values": [true, false], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83261015/scheduling_enabled" - } - } - }], - "icon": { - "name": "thermostat", - "modifiers": ["temperature-75"] - }, - "features": [{ - "name": "thermostat", - "temperature": 75, - "status": "", - "status_icon": null, - "actions": { - "set_heat_setpoint": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83261015/setpoints" - }, - "set_cool_setpoint": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83261015/setpoints" - } - }, - "setpoint_delta": 3, - "scale": "f", - "setpoint_increment": 1.0, - "setpoint_heat_min": 55, - "setpoint_heat_max": 90, - "setpoint_cool_min": 60, - "setpoint_cool_max": 99, - "setpoint_heat": 63, - "setpoint_cool": 79, - "system_status": "System Idle" - }, { - "name": "connection", - "signal_strength": "unknown", - "is_connected": true - }, { - "name": "thermostat_mode", - "label": "Zone Mode", - "value": "AUTO", - "display_value": "Auto", - "options": [{ - "id": "thermostat_mode", - "label": "Zone Mode", - "value": "thermostat_mode", - "header": true - }, { - "value": "AUTO", - "label": "Auto" - }, { - "value": "COOL", - "label": "Cooling" - }, { - "value": "HEAT", - "label": "Heating" - }, { - "value": "OFF", - "label": "Off" - }], - "actions": { - "update_thermostat_mode": { - "method": "POST", - "href": "https://www.mynexia.com/mobile/xxl_zones/83261015/zone_mode" - } - } - }, { - "name": "thermostat_run_mode", - "label": "Run Mode", - "options": [{ - "id": "thermostat_run_mode", - "label": "Run Mode", - "value": "thermostat_run_mode", - "header": true - }, { - "id": "info_text", - "label": "Follow or override the schedule.", - "value": "info_text", - "info": true - }, { - "value": "permanent_hold", - "label": "Permanent Hold" - }, { - "value": "run_schedule", - "label": "Run Schedule" - }], - "value": "permanent_hold", - "display_value": "Hold", - "actions": { - "update_thermostat_run_mode": { - "method": "POST", - "href": "https://www.mynexia.com/mobile/xxl_zones/83261015/run_mode" - } - } - }, { - "name": "schedule", - "enabled": true, - "max_period_name_length": 10, - "setpoint_increment": 1, - "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83261015\u0026house_id=123456", - "actions": { - "get_active_schedule": { - "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83261015", - "method": "POST" - }, - "set_active_schedule": { - "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83261015", - "method": "POST" - }, - "get_default_schedule": { - "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83261015", - "method": "GET" - }, - "enable_scheduling": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83261015/scheduling_enabled", - "method": "POST", - "data": { - "value": true - } - } - }, - "can_add_remove_periods": true, - "max_periods_per_day": 4 - }], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83261015" - } - } - }, { - "type": "xxl_zone", - "id": 83261018, - "name": "David Office", - "current_zone_mode": "AUTO", - "temperature": 75, - "setpoints": { - "heat": 63, - "cool": 79 - }, - "operating_state": "", - "heating_setpoint": 63, - "cooling_setpoint": 79, - "zone_status": "", - "settings": [{ - "type": "preset_selected", - "title": "Preset", - "current_value": 0, - "options": [{ - "value": 0, - "label": "None" - }, { - "value": 1, - "label": "Home" - }, { - "value": 2, - "label": "Away" - }, { - "value": 3, - "label": "Sleep" - }], - "labels": ["None", "Home", "Away", "Sleep"], - "values": [0, 1, 2, 3], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83261018/preset_selected" - } - } - }, { - "type": "zone_mode", - "title": "Zone Mode", - "current_value": "AUTO", - "options": [{ - "value": "AUTO", - "label": "Auto" - }, { - "value": "COOL", - "label": "Cooling" - }, { - "value": "HEAT", - "label": "Heating" - }, { - "value": "OFF", - "label": "Off" - }], - "labels": ["Auto", "Cooling", "Heating", "Off"], - "values": ["AUTO", "COOL", "HEAT", "OFF"], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83261018/zone_mode" - } - } - }, { - "type": "run_mode", - "title": "Run Mode", - "current_value": "permanent_hold", - "options": [{ - "value": "permanent_hold", - "label": "Permanent Hold" - }, { - "value": "run_schedule", - "label": "Run Schedule" - }], - "labels": ["Permanent Hold", "Run Schedule"], - "values": ["permanent_hold", "run_schedule"], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83261018/run_mode" - } - } - }, { - "type": "scheduling_enabled", - "title": "Scheduling", - "current_value": true, - "options": [{ - "value": true, - "label": "ON" - }, { - "value": false, - "label": "OFF" - }], - "labels": ["ON", "OFF"], - "values": [true, false], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83261018/scheduling_enabled" - } - } - }], - "icon": { - "name": "thermostat", - "modifiers": ["temperature-75"] - }, - "features": [{ - "name": "thermostat", - "temperature": 75, - "status": "", - "status_icon": null, - "actions": { - "set_heat_setpoint": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83261018/setpoints" - }, - "set_cool_setpoint": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83261018/setpoints" - } - }, - "setpoint_delta": 3, - "scale": "f", - "setpoint_increment": 1.0, - "setpoint_heat_min": 55, - "setpoint_heat_max": 90, - "setpoint_cool_min": 60, - "setpoint_cool_max": 99, - "setpoint_heat": 63, - "setpoint_cool": 79, - "system_status": "System Idle" - }, { - "name": "connection", - "signal_strength": "unknown", - "is_connected": true - }, { - "name": "thermostat_mode", - "label": "Zone Mode", - "value": "AUTO", - "display_value": "Auto", - "options": [{ - "id": "thermostat_mode", - "label": "Zone Mode", - "value": "thermostat_mode", - "header": true - }, { - "value": "AUTO", - "label": "Auto" - }, { - "value": "COOL", - "label": "Cooling" - }, { - "value": "HEAT", - "label": "Heating" - }, { - "value": "OFF", - "label": "Off" - }], - "actions": { - "update_thermostat_mode": { - "method": "POST", - "href": "https://www.mynexia.com/mobile/xxl_zones/83261018/zone_mode" - } - } - }, { - "name": "thermostat_run_mode", - "label": "Run Mode", - "options": [{ - "id": "thermostat_run_mode", - "label": "Run Mode", - "value": "thermostat_run_mode", - "header": true - }, { - "id": "info_text", - "label": "Follow or override the schedule.", - "value": "info_text", - "info": true - }, { - "value": "permanent_hold", - "label": "Permanent Hold" - }, { - "value": "run_schedule", - "label": "Run Schedule" - }], - "value": "permanent_hold", - "display_value": "Hold", - "actions": { - "update_thermostat_run_mode": { - "method": "POST", - "href": "https://www.mynexia.com/mobile/xxl_zones/83261018/run_mode" - } - } - }, { - "name": "schedule", - "enabled": true, - "max_period_name_length": 10, - "setpoint_increment": 1, - "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83261018\u0026house_id=123456", - "actions": { - "get_active_schedule": { - "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83261018", - "method": "POST" - }, - "set_active_schedule": { - "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83261018", - "method": "POST" - }, - "get_default_schedule": { - "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83261018", - "method": "GET" - }, - "enable_scheduling": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83261018/scheduling_enabled", - "method": "POST", - "data": { - "value": true - } - } - }, - "can_add_remove_periods": true, - "max_periods_per_day": 4 - }], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83261018" - } - } - }] - }, { - "name": "thermostat_fan_mode", - "label": "Fan Mode", - "options": [{ - "id": "thermostat_fan_mode", - "label": "Fan Mode", - "value": "thermostat_fan_mode", - "header": true - }, { - "value": "auto", - "label": "Auto" - }, { - "value": "on", - "label": "On" - }, { - "value": "circulate", - "label": "Circulate" - }], - "value": "auto", - "display_value": "Auto", - "status_icon": { - "name": "thermostat_fan_off", - "modifiers": [] - }, - "actions": { - "update_thermostat_fan_mode": { - "method": "POST", - "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059676/fan_mode" - } - } - }, { - "name": "thermostat_compressor_speed", - "compressor_speed": 0.0 - }, { - "name": "runtime_history", - "actions": { - "get_runtime_history": { - "method": "GET", - "href": "https://www.mynexia.com/mobile/runtime_history/2059676?report_type=daily" - }, - "get_monthly_runtime_history": { - "method": "GET", - "href": "https://www.mynexia.com/mobile/runtime_history/2059676?report_type=monthly" - } - } - }], - "icon": [{ - "name": "thermostat", - "modifiers": ["temperature-75"] - }, { - "name": "thermostat", - "modifiers": ["temperature-75"] - }], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059676" - }, - "nexia:history": { - "href": "https://www.mynexia.com/mobile/houses/123456/events?device_id=2059676" - }, - "filter_events": { - "href": "https://www.mynexia.com/mobile/houses/123456/events/collection?sys_guid=5aae72a6-1bd0-4d84-9bfd-673e7bc4907c" - }, - "pending_request": { - "polling_path": "https://www.mynexia.com/backstage/announcements/3412f1d96eb0c5edb5466c3c0598af60c06f8443f21e9bcb" - } - }, - "last_updated_at": "2020-03-11T15:15:53.000-05:00", - "settings": [{ - "type": "fan_mode", - "title": "Fan Mode", - "current_value": "auto", - "options": [{ - "value": "auto", - "label": "Auto" - }, { - "value": "on", - "label": "On" - }, { - "value": "circulate", - "label": "Circulate" - }], - "labels": ["Auto", "On", "Circulate"], - "values": ["auto", "on", "circulate"], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059676/fan_mode" - } - } - }, { - "type": "fan_speed", - "title": "Fan Speed", - "current_value": 0.35, - "options": [{ - "value": 0.35, - "label": "35%" - }, { - "value": 0.4, - "label": "40%" - }, { - "value": 0.45, - "label": "45%" - }, { - "value": 0.5, - "label": "50%" - }, { - "value": 0.55, - "label": "55%" - }, { - "value": 0.6, - "label": "60%" - }, { - "value": 0.65, - "label": "65%" - }, { - "value": 0.7, - "label": "70%" - }, { - "value": 0.75, - "label": "75%" - }, { - "value": 0.8, - "label": "80%" - }, { - "value": 0.85, - "label": "85%" - }, { - "value": 0.9, - "label": "90%" - }, { - "value": 0.95, - "label": "95%" - }, { - "value": 1.0, - "label": "100%" - }], - "labels": ["35%", "40%", "45%", "50%", "55%", "60%", "65%", "70%", "75%", "80%", "85%", "90%", "95%", "100%"], - "values": [0.35, 0.4, 0.45, 0.5, 0.55, 0.6, 0.65, 0.7, 0.75, 0.8, 0.85, 0.9, 0.95, 1.0], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059676/fan_speed" - } - } - }, { - "type": "fan_circulation_time", - "title": "Fan Circulation Time", - "current_value": 30, - "options": [{ - "value": 10, - "label": "10 minutes" - }, { - "value": 15, - "label": "15 minutes" - }, { - "value": 20, - "label": "20 minutes" - }, { - "value": 25, - "label": "25 minutes" - }, { - "value": 30, - "label": "30 minutes" - }, { - "value": 35, - "label": "35 minutes" - }, { - "value": 40, - "label": "40 minutes" - }, { - "value": 45, - "label": "45 minutes" - }, { - "value": 50, - "label": "50 minutes" - }, { - "value": 55, - "label": "55 minutes" - }], - "labels": ["10 minutes", "15 minutes", "20 minutes", "25 minutes", "30 minutes", "35 minutes", "40 minutes", "45 minutes", "50 minutes", "55 minutes"], - "values": [10, 15, 20, 25, 30, 35, 40, 45, 50, 55], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059676/fan_circulation_time" - } - } - }, { - "type": "air_cleaner_mode", - "title": "Air Cleaner Mode", - "current_value": "auto", - "options": [{ - "value": "auto", - "label": "Auto" - }, { - "value": "quick", - "label": "Quick" - }, { - "value": "allergy", - "label": "Allergy" - }], - "labels": ["Auto", "Quick", "Allergy"], - "values": ["auto", "quick", "allergy"], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059676/air_cleaner_mode" - } - } - }, { - "type": "dehumidify", - "title": "Cooling Dehumidify Set Point", - "current_value": 0.45, - "options": [{ - "value": 0.35, - "label": "35%" - }, { - "value": 0.4, - "label": "40%" - }, { - "value": 0.45, - "label": "45%" - }, { - "value": 0.5, - "label": "50%" - }, { - "value": 0.55, - "label": "55%" - }, { - "value": 0.6, - "label": "60%" - }, { - "value": 0.65, - "label": "65%" - }], - "labels": ["35%", "40%", "45%", "50%", "55%", "60%", "65%"], - "values": [0.35, 0.4, 0.45, 0.5, 0.55, 0.6, 0.65], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059676/dehumidify" - } - } - }, { - "type": "scale", - "title": "Temperature Scale", - "current_value": "f", - "options": [{ - "value": "f", - "label": "F" - }, { - "value": "c", - "label": "C" - }], - "labels": ["F", "C"], - "values": ["f", "c"], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059676/scale" - } - } - }], - "status_secondary": null, - "status_tertiary": null, - "type": "xxl_thermostat", - "has_outdoor_temperature": true, - "outdoor_temperature": "88", - "has_indoor_humidity": true, - "connected": true, - "indoor_humidity": "52", - "system_status": "System Idle", - "delta": 3, - "zones": [{ - "type": "xxl_zone", - "id": 83261015, - "name": "Living West", - "current_zone_mode": "AUTO", - "temperature": 75, - "setpoints": { - "heat": 63, - "cool": 79 - }, - "operating_state": "", - "heating_setpoint": 63, - "cooling_setpoint": 79, - "zone_status": "", - "settings": [{ - "type": "preset_selected", - "title": "Preset", - "current_value": 0, - "options": [{ - "value": 0, - "label": "None" - }, { - "value": 1, - "label": "Home" - }, { - "value": 2, - "label": "Away" - }, { - "value": 3, - "label": "Sleep" - }], - "labels": ["None", "Home", "Away", "Sleep"], - "values": [0, 1, 2, 3], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83261015/preset_selected" - } - } - }, { - "type": "zone_mode", - "title": "Zone Mode", - "current_value": "AUTO", - "options": [{ - "value": "AUTO", - "label": "Auto" - }, { - "value": "COOL", - "label": "Cooling" - }, { - "value": "HEAT", - "label": "Heating" - }, { - "value": "OFF", - "label": "Off" - }], - "labels": ["Auto", "Cooling", "Heating", "Off"], - "values": ["AUTO", "COOL", "HEAT", "OFF"], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83261015/zone_mode" - } - } - }, { - "type": "run_mode", - "title": "Run Mode", - "current_value": "permanent_hold", - "options": [{ - "value": "permanent_hold", - "label": "Permanent Hold" - }, { - "value": "run_schedule", - "label": "Run Schedule" - }], - "labels": ["Permanent Hold", "Run Schedule"], - "values": ["permanent_hold", "run_schedule"], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83261015/run_mode" - } - } - }, { - "type": "scheduling_enabled", - "title": "Scheduling", - "current_value": true, - "options": [{ - "value": true, - "label": "ON" - }, { - "value": false, - "label": "OFF" - }], - "labels": ["ON", "OFF"], - "values": [true, false], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83261015/scheduling_enabled" - } - } - }], - "icon": { - "name": "thermostat", - "modifiers": ["temperature-75"] - }, - "features": [{ - "name": "thermostat", - "temperature": 75, - "status": "", - "status_icon": null, - "actions": { - "set_heat_setpoint": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83261015/setpoints" - }, - "set_cool_setpoint": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83261015/setpoints" - } - }, - "setpoint_delta": 3, - "scale": "f", - "setpoint_increment": 1.0, - "setpoint_heat_min": 55, - "setpoint_heat_max": 90, - "setpoint_cool_min": 60, - "setpoint_cool_max": 99, - "setpoint_heat": 63, - "setpoint_cool": 79, - "system_status": "System Idle" - }, { - "name": "connection", - "signal_strength": "unknown", - "is_connected": true - }, { - "name": "thermostat_mode", - "label": "Zone Mode", - "value": "AUTO", - "display_value": "Auto", - "options": [{ - "id": "thermostat_mode", - "label": "Zone Mode", - "value": "thermostat_mode", - "header": true - }, { - "value": "AUTO", - "label": "Auto" - }, { - "value": "COOL", - "label": "Cooling" - }, { - "value": "HEAT", - "label": "Heating" - }, { - "value": "OFF", - "label": "Off" - }], - "actions": { - "update_thermostat_mode": { - "method": "POST", - "href": "https://www.mynexia.com/mobile/xxl_zones/83261015/zone_mode" - } - } - }, { - "name": "thermostat_run_mode", - "label": "Run Mode", - "options": [{ - "id": "thermostat_run_mode", - "label": "Run Mode", - "value": "thermostat_run_mode", - "header": true - }, { - "id": "info_text", - "label": "Follow or override the schedule.", - "value": "info_text", - "info": true - }, { - "value": "permanent_hold", - "label": "Permanent Hold" - }, { - "value": "run_schedule", - "label": "Run Schedule" - }], - "value": "permanent_hold", - "display_value": "Hold", - "actions": { - "update_thermostat_run_mode": { - "method": "POST", - "href": "https://www.mynexia.com/mobile/xxl_zones/83261015/run_mode" - } - } - }, { - "name": "schedule", - "enabled": true, - "max_period_name_length": 10, - "setpoint_increment": 1, - "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83261015\u0026house_id=123456", - "actions": { - "get_active_schedule": { - "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83261015", - "method": "POST" - }, - "set_active_schedule": { - "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83261015", - "method": "POST" - }, - "get_default_schedule": { - "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83261015", - "method": "GET" - }, - "enable_scheduling": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83261015/scheduling_enabled", - "method": "POST", - "data": { - "value": true - } - } - }, - "can_add_remove_periods": true, - "max_periods_per_day": 4 - }], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83261015" - } - } - }, { - "type": "xxl_zone", - "id": 83261018, - "name": "David Office", - "current_zone_mode": "AUTO", - "temperature": 75, - "setpoints": { - "heat": 63, - "cool": 79 - }, - "operating_state": "", - "heating_setpoint": 63, - "cooling_setpoint": 79, - "zone_status": "", - "settings": [{ - "type": "preset_selected", - "title": "Preset", - "current_value": 0, - "options": [{ - "value": 0, - "label": "None" - }, { - "value": 1, - "label": "Home" - }, { - "value": 2, - "label": "Away" - }, { - "value": 3, - "label": "Sleep" - }], - "labels": ["None", "Home", "Away", "Sleep"], - "values": [0, 1, 2, 3], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83261018/preset_selected" - } - } - }, { - "type": "zone_mode", - "title": "Zone Mode", - "current_value": "AUTO", - "options": [{ - "value": "AUTO", - "label": "Auto" - }, { - "value": "COOL", - "label": "Cooling" - }, { - "value": "HEAT", - "label": "Heating" - }, { - "value": "OFF", - "label": "Off" - }], - "labels": ["Auto", "Cooling", "Heating", "Off"], - "values": ["AUTO", "COOL", "HEAT", "OFF"], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83261018/zone_mode" - } - } - }, { - "type": "run_mode", - "title": "Run Mode", - "current_value": "permanent_hold", - "options": [{ - "value": "permanent_hold", - "label": "Permanent Hold" - }, { - "value": "run_schedule", - "label": "Run Schedule" - }], - "labels": ["Permanent Hold", "Run Schedule"], - "values": ["permanent_hold", "run_schedule"], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83261018/run_mode" - } - } - }, { - "type": "scheduling_enabled", - "title": "Scheduling", - "current_value": true, - "options": [{ - "value": true, - "label": "ON" - }, { - "value": false, - "label": "OFF" - }], - "labels": ["ON", "OFF"], - "values": [true, false], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83261018/scheduling_enabled" - } - } - }], - "icon": { - "name": "thermostat", - "modifiers": ["temperature-75"] - }, - "features": [{ - "name": "thermostat", - "temperature": 75, - "status": "", - "status_icon": null, - "actions": { - "set_heat_setpoint": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83261018/setpoints" - }, - "set_cool_setpoint": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83261018/setpoints" - } - }, - "setpoint_delta": 3, - "scale": "f", - "setpoint_increment": 1.0, - "setpoint_heat_min": 55, - "setpoint_heat_max": 90, - "setpoint_cool_min": 60, - "setpoint_cool_max": 99, - "setpoint_heat": 63, - "setpoint_cool": 79, - "system_status": "System Idle" - }, { - "name": "connection", - "signal_strength": "unknown", - "is_connected": true - }, { - "name": "thermostat_mode", - "label": "Zone Mode", - "value": "AUTO", - "display_value": "Auto", - "options": [{ - "id": "thermostat_mode", - "label": "Zone Mode", - "value": "thermostat_mode", - "header": true - }, { - "value": "AUTO", - "label": "Auto" - }, { - "value": "COOL", - "label": "Cooling" - }, { - "value": "HEAT", - "label": "Heating" - }, { - "value": "OFF", - "label": "Off" - }], - "actions": { - "update_thermostat_mode": { - "method": "POST", - "href": "https://www.mynexia.com/mobile/xxl_zones/83261018/zone_mode" - } - } - }, { - "name": "thermostat_run_mode", - "label": "Run Mode", - "options": [{ - "id": "thermostat_run_mode", - "label": "Run Mode", - "value": "thermostat_run_mode", - "header": true - }, { - "id": "info_text", - "label": "Follow or override the schedule.", - "value": "info_text", - "info": true - }, { - "value": "permanent_hold", - "label": "Permanent Hold" - }, { - "value": "run_schedule", - "label": "Run Schedule" - }], - "value": "permanent_hold", - "display_value": "Hold", - "actions": { - "update_thermostat_run_mode": { - "method": "POST", - "href": "https://www.mynexia.com/mobile/xxl_zones/83261018/run_mode" - } - } - }, { - "name": "schedule", - "enabled": true, - "max_period_name_length": 10, - "setpoint_increment": 1, - "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83261018\u0026house_id=123456", - "actions": { - "get_active_schedule": { - "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83261018", - "method": "POST" - }, - "set_active_schedule": { - "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83261018", - "method": "POST" - }, - "get_default_schedule": { - "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83261018", - "method": "GET" - }, - "enable_scheduling": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83261018/scheduling_enabled", - "method": "POST", - "data": { - "value": true - } - } - }, - "can_add_remove_periods": true, - "max_periods_per_day": 4 - }], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83261018" - } - } - }] - }, { - "id": 2293892, - "name": "Master Suite", - "name_editable": true, - "features": [{ - "name": "advanced_info", - "items": [{ - "type": "label_value", - "label": "Model", - "value": "XL1050" - }, { - "type": "label_value", - "label": "AUID", - "value": "0281B02C" - }, { - "type": "label_value", - "label": "Firmware Build Number", - "value": "1581321824" - }, { - "type": "label_value", - "label": "Firmware Build Date", - "value": "2020-02-10 08:03:44 UTC" - }, { - "type": "label_value", - "label": "Firmware Version", - "value": "5.9.1" - }, { - "type": "label_value", - "label": "Zoning Enabled", - "value": "yes" - }] - }, { - "name": "thermostat", - "temperature": 73, - "status": "Cooling", - "status_icon": { - "name": "cooling", - "modifiers": [] - }, - "actions": {}, - "setpoint_delta": 3, - "scale": "f", - "setpoint_increment": 1.0, - "setpoint_heat_min": 55, - "setpoint_heat_max": 90, - "setpoint_cool_min": 60, - "setpoint_cool_max": 99 - }, { - "name": "connection", - "signal_strength": "unknown", - "is_connected": true - }, { - "name": "group", - "members": [{ - "type": "xxl_zone", - "id": 83394133, - "name": "Bath Closet", - "current_zone_mode": "AUTO", - "temperature": 73, - "setpoints": { - "heat": 63, - "cool": 79 - }, - "operating_state": "Relieving Air", - "heating_setpoint": 63, - "cooling_setpoint": 79, - "zone_status": "Relieving Air", - "settings": [{ - "type": "preset_selected", - "title": "Preset", - "current_value": 0, - "options": [{ - "value": 0, - "label": "None" - }, { - "value": 1, - "label": "Home" - }, { - "value": 2, - "label": "Away" - }, { - "value": 3, - "label": "Sleep" - }], - "labels": ["None", "Home", "Away", "Sleep"], - "values": [0, 1, 2, 3], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/preset_selected" - } - } - }, { - "type": "zone_mode", - "title": "Zone Mode", - "current_value": "AUTO", - "options": [{ - "value": "AUTO", - "label": "Auto" - }, { - "value": "COOL", - "label": "Cooling" - }, { - "value": "HEAT", - "label": "Heating" - }, { - "value": "OFF", - "label": "Off" - }], - "labels": ["Auto", "Cooling", "Heating", "Off"], - "values": ["AUTO", "COOL", "HEAT", "OFF"], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/zone_mode" - } - } - }, { - "type": "run_mode", - "title": "Run Mode", - "current_value": "permanent_hold", - "options": [{ - "value": "permanent_hold", - "label": "Permanent Hold" - }, { - "value": "run_schedule", - "label": "Run Schedule" - }], - "labels": ["Permanent Hold", "Run Schedule"], - "values": ["permanent_hold", "run_schedule"], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/run_mode" - } - } - }, { - "type": "scheduling_enabled", - "title": "Scheduling", - "current_value": true, - "options": [{ - "value": true, - "label": "ON" - }, { - "value": false, - "label": "OFF" - }], - "labels": ["ON", "OFF"], - "values": [true, false], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/scheduling_enabled" - } - } - }], - "icon": { - "name": "thermostat", - "modifiers": ["temperature-73"] - }, - "features": [{ - "name": "thermostat", - "temperature": 73, - "status": "Relieving Air", - "status_icon": { - "name": "cooling", - "modifiers": [] - }, - "actions": { - "set_heat_setpoint": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/setpoints" - }, - "set_cool_setpoint": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/setpoints" - } - }, - "setpoint_delta": 3, - "scale": "f", - "setpoint_increment": 1.0, - "setpoint_heat_min": 55, - "setpoint_heat_max": 90, - "setpoint_cool_min": 60, - "setpoint_cool_max": 99, - "setpoint_heat": 63, - "setpoint_cool": 79, - "system_status": "Cooling" - }, { - "name": "connection", - "signal_strength": "unknown", - "is_connected": true - }, { - "name": "thermostat_mode", - "label": "Zone Mode", - "value": "AUTO", - "display_value": "Auto", - "options": [{ - "id": "thermostat_mode", - "label": "Zone Mode", - "value": "thermostat_mode", - "header": true - }, { - "value": "AUTO", - "label": "Auto" - }, { - "value": "COOL", - "label": "Cooling" - }, { - "value": "HEAT", - "label": "Heating" - }, { - "value": "OFF", - "label": "Off" - }], - "actions": { - "update_thermostat_mode": { - "method": "POST", - "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/zone_mode" - } - } - }, { - "name": "thermostat_run_mode", - "label": "Run Mode", - "options": [{ - "id": "thermostat_run_mode", - "label": "Run Mode", - "value": "thermostat_run_mode", - "header": true - }, { - "id": "info_text", - "label": "Follow or override the schedule.", - "value": "info_text", - "info": true - }, { - "value": "permanent_hold", - "label": "Permanent Hold" - }, { - "value": "run_schedule", - "label": "Run Schedule" - }], - "value": "permanent_hold", - "display_value": "Hold", - "actions": { - "update_thermostat_run_mode": { - "method": "POST", - "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/run_mode" - } - } - }, { - "name": "schedule", - "enabled": true, - "max_period_name_length": 10, - "setpoint_increment": 1, - "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83394133\u0026house_id=123456", - "actions": { - "get_active_schedule": { - "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83394133", - "method": "POST" - }, - "set_active_schedule": { - "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83394133", - "method": "POST" - }, - "get_default_schedule": { - "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83394133", - "method": "GET" - }, - "enable_scheduling": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/scheduling_enabled", - "method": "POST", - "data": { - "value": true - } - } - }, - "can_add_remove_periods": true, - "max_periods_per_day": 4 - }], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83394133" - } - } - }, { - "type": "xxl_zone", - "id": 83394130, - "name": "Master", - "current_zone_mode": "AUTO", - "temperature": 74, - "setpoints": { - "heat": 63, - "cool": 71 - }, - "operating_state": "Damper Open", - "heating_setpoint": 63, - "cooling_setpoint": 71, - "zone_status": "Damper Open", - "settings": [{ - "type": "preset_selected", - "title": "Preset", - "current_value": 0, - "options": [{ - "value": 0, - "label": "None" - }, { - "value": 1, - "label": "Home" - }, { - "value": 2, - "label": "Away" - }, { - "value": 3, - "label": "Sleep" - }], - "labels": ["None", "Home", "Away", "Sleep"], - "values": [0, 1, 2, 3], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/preset_selected" - } - } - }, { - "type": "zone_mode", - "title": "Zone Mode", - "current_value": "AUTO", - "options": [{ - "value": "AUTO", - "label": "Auto" - }, { - "value": "COOL", - "label": "Cooling" - }, { - "value": "HEAT", - "label": "Heating" - }, { - "value": "OFF", - "label": "Off" - }], - "labels": ["Auto", "Cooling", "Heating", "Off"], - "values": ["AUTO", "COOL", "HEAT", "OFF"], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/zone_mode" - } - } - }, { - "type": "run_mode", - "title": "Run Mode", - "current_value": "permanent_hold", - "options": [{ - "value": "permanent_hold", - "label": "Permanent Hold" - }, { - "value": "run_schedule", - "label": "Run Schedule" - }], - "labels": ["Permanent Hold", "Run Schedule"], - "values": ["permanent_hold", "run_schedule"], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/run_mode" - } - } - }, { - "type": "scheduling_enabled", - "title": "Scheduling", - "current_value": true, - "options": [{ - "value": true, - "label": "ON" - }, { - "value": false, - "label": "OFF" - }], - "labels": ["ON", "OFF"], - "values": [true, false], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/scheduling_enabled" - } - } - }], - "icon": { - "name": "thermostat", - "modifiers": ["temperature-74"] - }, - "features": [{ - "name": "thermostat", - "temperature": 74, - "status": "Damper Open", - "status_icon": { - "name": "cooling", - "modifiers": [] - }, - "actions": { - "set_heat_setpoint": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/setpoints" - }, - "set_cool_setpoint": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/setpoints" - } - }, - "setpoint_delta": 3, - "scale": "f", - "setpoint_increment": 1.0, - "setpoint_heat_min": 55, - "setpoint_heat_max": 90, - "setpoint_cool_min": 60, - "setpoint_cool_max": 99, - "setpoint_heat": 63, - "setpoint_cool": 71, - "system_status": "Cooling" - }, { - "name": "connection", - "signal_strength": "unknown", - "is_connected": true - }, { - "name": "thermostat_mode", - "label": "Zone Mode", - "value": "AUTO", - "display_value": "Auto", - "options": [{ - "id": "thermostat_mode", - "label": "Zone Mode", - "value": "thermostat_mode", - "header": true - }, { - "value": "AUTO", - "label": "Auto" - }, { - "value": "COOL", - "label": "Cooling" - }, { - "value": "HEAT", - "label": "Heating" - }, { - "value": "OFF", - "label": "Off" - }], - "actions": { - "update_thermostat_mode": { - "method": "POST", - "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/zone_mode" - } - } - }, { - "name": "thermostat_run_mode", - "label": "Run Mode", - "options": [{ - "id": "thermostat_run_mode", - "label": "Run Mode", - "value": "thermostat_run_mode", - "header": true - }, { - "id": "info_text", - "label": "Follow or override the schedule.", - "value": "info_text", - "info": true - }, { - "value": "permanent_hold", - "label": "Permanent Hold" - }, { - "value": "run_schedule", - "label": "Run Schedule" - }], - "value": "permanent_hold", - "display_value": "Hold", - "actions": { - "update_thermostat_run_mode": { - "method": "POST", - "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/run_mode" - } - } - }, { - "name": "schedule", - "enabled": true, - "max_period_name_length": 10, - "setpoint_increment": 1, - "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83394130\u0026house_id=123456", - "actions": { - "get_active_schedule": { - "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83394130", - "method": "POST" - }, - "set_active_schedule": { - "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83394130", - "method": "POST" - }, - "get_default_schedule": { - "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83394130", - "method": "GET" - }, - "enable_scheduling": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/scheduling_enabled", - "method": "POST", - "data": { - "value": true - } - } - }, - "can_add_remove_periods": true, - "max_periods_per_day": 4 - }], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83394130" - } - } - }, { - "type": "xxl_zone", - "id": 83394136, - "name": "Nick Office", - "current_zone_mode": "AUTO", - "temperature": 73, - "setpoints": { - "heat": 63, - "cool": 79 - }, - "operating_state": "Relieving Air", - "heating_setpoint": 63, - "cooling_setpoint": 79, - "zone_status": "Relieving Air", - "settings": [{ - "type": "preset_selected", - "title": "Preset", - "current_value": 0, - "options": [{ - "value": 0, - "label": "None" - }, { - "value": 1, - "label": "Home" - }, { - "value": 2, - "label": "Away" - }, { - "value": 3, - "label": "Sleep" - }], - "labels": ["None", "Home", "Away", "Sleep"], - "values": [0, 1, 2, 3], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/preset_selected" - } - } - }, { - "type": "zone_mode", - "title": "Zone Mode", - "current_value": "AUTO", - "options": [{ - "value": "AUTO", - "label": "Auto" - }, { - "value": "COOL", - "label": "Cooling" - }, { - "value": "HEAT", - "label": "Heating" - }, { - "value": "OFF", - "label": "Off" - }], - "labels": ["Auto", "Cooling", "Heating", "Off"], - "values": ["AUTO", "COOL", "HEAT", "OFF"], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/zone_mode" - } - } - }, { - "type": "run_mode", - "title": "Run Mode", - "current_value": "permanent_hold", - "options": [{ - "value": "permanent_hold", - "label": "Permanent Hold" - }, { - "value": "run_schedule", - "label": "Run Schedule" - }], - "labels": ["Permanent Hold", "Run Schedule"], - "values": ["permanent_hold", "run_schedule"], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/run_mode" - } - } - }, { - "type": "scheduling_enabled", - "title": "Scheduling", - "current_value": true, - "options": [{ - "value": true, - "label": "ON" - }, { - "value": false, - "label": "OFF" - }], - "labels": ["ON", "OFF"], - "values": [true, false], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/scheduling_enabled" - } - } - }], - "icon": { - "name": "thermostat", - "modifiers": ["temperature-73"] - }, - "features": [{ - "name": "thermostat", - "temperature": 73, - "status": "Relieving Air", - "status_icon": { - "name": "cooling", - "modifiers": [] - }, - "actions": { - "set_heat_setpoint": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/setpoints" - }, - "set_cool_setpoint": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/setpoints" - } - }, - "setpoint_delta": 3, - "scale": "f", - "setpoint_increment": 1.0, - "setpoint_heat_min": 55, - "setpoint_heat_max": 90, - "setpoint_cool_min": 60, - "setpoint_cool_max": 99, - "setpoint_heat": 63, - "setpoint_cool": 79, - "system_status": "Cooling" - }, { - "name": "connection", - "signal_strength": "unknown", - "is_connected": true - }, { - "name": "thermostat_mode", - "label": "Zone Mode", - "value": "AUTO", - "display_value": "Auto", - "options": [{ - "id": "thermostat_mode", - "label": "Zone Mode", - "value": "thermostat_mode", - "header": true - }, { - "value": "AUTO", - "label": "Auto" - }, { - "value": "COOL", - "label": "Cooling" - }, { - "value": "HEAT", - "label": "Heating" - }, { - "value": "OFF", - "label": "Off" - }], - "actions": { - "update_thermostat_mode": { - "method": "POST", - "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/zone_mode" - } - } - }, { - "name": "thermostat_run_mode", - "label": "Run Mode", - "options": [{ - "id": "thermostat_run_mode", - "label": "Run Mode", - "value": "thermostat_run_mode", - "header": true - }, { - "id": "info_text", - "label": "Follow or override the schedule.", - "value": "info_text", - "info": true - }, { - "value": "permanent_hold", - "label": "Permanent Hold" - }, { - "value": "run_schedule", - "label": "Run Schedule" - }], - "value": "permanent_hold", - "display_value": "Hold", - "actions": { - "update_thermostat_run_mode": { - "method": "POST", - "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/run_mode" - } - } - }, { - "name": "schedule", - "enabled": true, - "max_period_name_length": 10, - "setpoint_increment": 1, - "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83394136\u0026house_id=123456", - "actions": { - "get_active_schedule": { - "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83394136", - "method": "POST" - }, - "set_active_schedule": { - "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83394136", - "method": "POST" - }, - "get_default_schedule": { - "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83394136", - "method": "GET" - }, - "enable_scheduling": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/scheduling_enabled", - "method": "POST", - "data": { - "value": true - } - } - }, - "can_add_remove_periods": true, - "max_periods_per_day": 4 - }], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83394136" - } - } - }, { - "type": "xxl_zone", - "id": 83394127, - "name": "Snooze Room", - "current_zone_mode": "AUTO", - "temperature": 72, - "setpoints": { - "heat": 63, - "cool": 79 - }, - "operating_state": "Damper Closed", - "heating_setpoint": 63, - "cooling_setpoint": 79, - "zone_status": "Damper Closed", - "settings": [{ - "type": "preset_selected", - "title": "Preset", - "current_value": 0, - "options": [{ - "value": 0, - "label": "None" - }, { - "value": 1, - "label": "Home" - }, { - "value": 2, - "label": "Away" - }, { - "value": 3, - "label": "Sleep" - }], - "labels": ["None", "Home", "Away", "Sleep"], - "values": [0, 1, 2, 3], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/preset_selected" - } - } - }, { - "type": "zone_mode", - "title": "Zone Mode", - "current_value": "AUTO", - "options": [{ - "value": "AUTO", - "label": "Auto" - }, { - "value": "COOL", - "label": "Cooling" - }, { - "value": "HEAT", - "label": "Heating" - }, { - "value": "OFF", - "label": "Off" - }], - "labels": ["Auto", "Cooling", "Heating", "Off"], - "values": ["AUTO", "COOL", "HEAT", "OFF"], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/zone_mode" - } - } - }, { - "type": "run_mode", - "title": "Run Mode", - "current_value": "permanent_hold", - "options": [{ - "value": "permanent_hold", - "label": "Permanent Hold" - }, { - "value": "run_schedule", - "label": "Run Schedule" - }], - "labels": ["Permanent Hold", "Run Schedule"], - "values": ["permanent_hold", "run_schedule"], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/run_mode" - } - } - }, { - "type": "scheduling_enabled", - "title": "Scheduling", - "current_value": true, - "options": [{ - "value": true, - "label": "ON" - }, { - "value": false, - "label": "OFF" - }], - "labels": ["ON", "OFF"], - "values": [true, false], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/scheduling_enabled" - } - } - }], - "icon": { - "name": "thermostat", - "modifiers": ["temperature-72"] - }, - "features": [{ - "name": "thermostat", - "temperature": 72, - "status": "Damper Closed", - "status_icon": { - "name": "cooling", - "modifiers": [] - }, - "actions": { - "set_heat_setpoint": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/setpoints" - }, - "set_cool_setpoint": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/setpoints" - } - }, - "setpoint_delta": 3, - "scale": "f", - "setpoint_increment": 1.0, - "setpoint_heat_min": 55, - "setpoint_heat_max": 90, - "setpoint_cool_min": 60, - "setpoint_cool_max": 99, - "setpoint_heat": 63, - "setpoint_cool": 79, - "system_status": "Cooling" - }, { - "name": "connection", - "signal_strength": "unknown", - "is_connected": true - }, { - "name": "thermostat_mode", - "label": "Zone Mode", - "value": "AUTO", - "display_value": "Auto", - "options": [{ - "id": "thermostat_mode", - "label": "Zone Mode", - "value": "thermostat_mode", - "header": true - }, { - "value": "AUTO", - "label": "Auto" - }, { - "value": "COOL", - "label": "Cooling" - }, { - "value": "HEAT", - "label": "Heating" - }, { - "value": "OFF", - "label": "Off" - }], - "actions": { - "update_thermostat_mode": { - "method": "POST", - "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/zone_mode" - } - } - }, { - "name": "thermostat_run_mode", - "label": "Run Mode", - "options": [{ - "id": "thermostat_run_mode", - "label": "Run Mode", - "value": "thermostat_run_mode", - "header": true - }, { - "id": "info_text", - "label": "Follow or override the schedule.", - "value": "info_text", - "info": true - }, { - "value": "permanent_hold", - "label": "Permanent Hold" - }, { - "value": "run_schedule", - "label": "Run Schedule" - }], - "value": "permanent_hold", - "display_value": "Hold", - "actions": { - "update_thermostat_run_mode": { - "method": "POST", - "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/run_mode" - } - } - }, { - "name": "schedule", - "enabled": true, - "max_period_name_length": 10, - "setpoint_increment": 1, - "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83394127\u0026house_id=123456", - "actions": { - "get_active_schedule": { - "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83394127", - "method": "POST" - }, - "set_active_schedule": { - "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83394127", - "method": "POST" - }, - "get_default_schedule": { - "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83394127", - "method": "GET" - }, - "enable_scheduling": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/scheduling_enabled", - "method": "POST", - "data": { - "value": true - } - } - }, - "can_add_remove_periods": true, - "max_periods_per_day": 4 - }], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83394127" - } - } - }, { - "type": "xxl_zone", - "id": 83394139, - "name": "Safe Room", - "current_zone_mode": "AUTO", - "temperature": 74, - "setpoints": { - "heat": 63, - "cool": 79 - }, - "operating_state": "Damper Closed", - "heating_setpoint": 63, - "cooling_setpoint": 79, - "zone_status": "Damper Closed", - "settings": [{ - "type": "preset_selected", - "title": "Preset", - "current_value": 0, - "options": [{ - "value": 0, - "label": "None" - }, { - "value": 1, - "label": "Home" - }, { - "value": 2, - "label": "Away" - }, { - "value": 3, - "label": "Sleep" - }], - "labels": ["None", "Home", "Away", "Sleep"], - "values": [0, 1, 2, 3], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/preset_selected" - } - } - }, { - "type": "zone_mode", - "title": "Zone Mode", - "current_value": "AUTO", - "options": [{ - "value": "AUTO", - "label": "Auto" - }, { - "value": "COOL", - "label": "Cooling" - }, { - "value": "HEAT", - "label": "Heating" - }, { - "value": "OFF", - "label": "Off" - }], - "labels": ["Auto", "Cooling", "Heating", "Off"], - "values": ["AUTO", "COOL", "HEAT", "OFF"], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/zone_mode" - } - } - }, { - "type": "run_mode", - "title": "Run Mode", - "current_value": "permanent_hold", - "options": [{ - "value": "permanent_hold", - "label": "Permanent Hold" - }, { - "value": "run_schedule", - "label": "Run Schedule" - }], - "labels": ["Permanent Hold", "Run Schedule"], - "values": ["permanent_hold", "run_schedule"], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/run_mode" - } - } - }, { - "type": "scheduling_enabled", - "title": "Scheduling", - "current_value": true, - "options": [{ - "value": true, - "label": "ON" - }, { - "value": false, - "label": "OFF" - }], - "labels": ["ON", "OFF"], - "values": [true, false], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/scheduling_enabled" - } - } - }], - "icon": { - "name": "thermostat", - "modifiers": ["temperature-74"] - }, - "features": [{ - "name": "thermostat", - "temperature": 74, - "status": "Damper Closed", - "status_icon": { - "name": "cooling", - "modifiers": [] - }, - "actions": { - "set_heat_setpoint": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/setpoints" - }, - "set_cool_setpoint": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/setpoints" - } - }, - "setpoint_delta": 3, - "scale": "f", - "setpoint_increment": 1.0, - "setpoint_heat_min": 55, - "setpoint_heat_max": 90, - "setpoint_cool_min": 60, - "setpoint_cool_max": 99, - "setpoint_heat": 63, - "setpoint_cool": 79, - "system_status": "Cooling" - }, { - "name": "connection", - "signal_strength": "unknown", - "is_connected": true - }, { - "name": "thermostat_mode", - "label": "Zone Mode", - "value": "AUTO", - "display_value": "Auto", - "options": [{ - "id": "thermostat_mode", - "label": "Zone Mode", - "value": "thermostat_mode", - "header": true - }, { - "value": "AUTO", - "label": "Auto" - }, { - "value": "COOL", - "label": "Cooling" - }, { - "value": "HEAT", - "label": "Heating" - }, { - "value": "OFF", - "label": "Off" - }], - "actions": { - "update_thermostat_mode": { - "method": "POST", - "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/zone_mode" - } - } - }, { - "name": "thermostat_run_mode", - "label": "Run Mode", - "options": [{ - "id": "thermostat_run_mode", - "label": "Run Mode", - "value": "thermostat_run_mode", - "header": true - }, { - "id": "info_text", - "label": "Follow or override the schedule.", - "value": "info_text", - "info": true - }, { - "value": "permanent_hold", - "label": "Permanent Hold" - }, { - "value": "run_schedule", - "label": "Run Schedule" - }], - "value": "permanent_hold", - "display_value": "Hold", - "actions": { - "update_thermostat_run_mode": { - "method": "POST", - "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/run_mode" - } - } - }, { - "name": "schedule", - "enabled": true, - "max_period_name_length": 10, - "setpoint_increment": 1, - "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83394139\u0026house_id=123456", - "actions": { - "get_active_schedule": { - "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83394139", - "method": "POST" - }, - "set_active_schedule": { - "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83394139", - "method": "POST" - }, - "get_default_schedule": { - "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83394139", - "method": "GET" - }, - "enable_scheduling": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/scheduling_enabled", - "method": "POST", - "data": { - "value": true - } - } - }, - "can_add_remove_periods": true, - "max_periods_per_day": 4 - }], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83394139" - } - } - }] - }, { - "name": "thermostat_fan_mode", - "label": "Fan Mode", - "options": [{ - "id": "thermostat_fan_mode", - "label": "Fan Mode", - "value": "thermostat_fan_mode", - "header": true - }, { - "value": "auto", - "label": "Auto" - }, { - "value": "on", - "label": "On" - }, { - "value": "circulate", - "label": "Circulate" - }], - "value": "auto", - "display_value": "Auto", - "status_icon": { - "name": "thermostat_fan_on", - "modifiers": [] - }, - "actions": { - "update_thermostat_fan_mode": { - "method": "POST", - "href": "https://www.mynexia.com/mobile/xxl_thermostats/2293892/fan_mode" - } - } - }, { - "name": "thermostat_compressor_speed", - "compressor_speed": 0.69 - }, { - "name": "runtime_history", - "actions": { - "get_runtime_history": { - "method": "GET", - "href": "https://www.mynexia.com/mobile/runtime_history/2293892?report_type=daily" - }, - "get_monthly_runtime_history": { - "method": "GET", - "href": "https://www.mynexia.com/mobile/runtime_history/2293892?report_type=monthly" - } - } - }], - "icon": [{ - "name": "thermostat", - "modifiers": ["temperature-73"] - }, { - "name": "thermostat", - "modifiers": ["temperature-74"] - }, { - "name": "thermostat", - "modifiers": ["temperature-73"] - }, { - "name": "thermostat", - "modifiers": ["temperature-72"] - }, { - "name": "thermostat", - "modifiers": ["temperature-74"] - }], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_thermostats/2293892" - }, - "nexia:history": { - "href": "https://www.mynexia.com/mobile/houses/123456/events?device_id=2293892" - }, - "filter_events": { - "href": "https://www.mynexia.com/mobile/houses/123456/events/collection?sys_guid=e3fc90c7-2885-4f57-ae76-99e9ec81eef0" - }, - "pending_request": { - "polling_path": "https://www.mynexia.com/backstage/announcements/967361e8aed874aa5230930fd0e0bbd8b653261e982a6e0e" - } - }, - "last_updated_at": "2020-03-11T15:15:53.000-05:00", - "settings": [{ - "type": "fan_mode", - "title": "Fan Mode", - "current_value": "auto", - "options": [{ - "value": "auto", - "label": "Auto" - }, { - "value": "on", - "label": "On" - }, { - "value": "circulate", - "label": "Circulate" - }], - "labels": ["Auto", "On", "Circulate"], - "values": ["auto", "on", "circulate"], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_thermostats/2293892/fan_mode" - } - } - }, { - "type": "fan_speed", - "title": "Fan Speed", - "current_value": 0.35, - "options": [{ - "value": 0.35, - "label": "35%" - }, { - "value": 0.4, - "label": "40%" - }, { - "value": 0.45, - "label": "45%" - }, { - "value": 0.5, - "label": "50%" - }, { - "value": 0.55, - "label": "55%" - }, { - "value": 0.6, - "label": "60%" - }, { - "value": 0.65, - "label": "65%" - }, { - "value": 0.7, - "label": "70%" - }, { - "value": 0.75, - "label": "75%" - }, { - "value": 0.8, - "label": "80%" - }, { - "value": 0.85, - "label": "85%" - }, { - "value": 0.9, - "label": "90%" - }, { - "value": 0.95, - "label": "95%" - }, { - "value": 1.0, - "label": "100%" - }], - "labels": ["35%", "40%", "45%", "50%", "55%", "60%", "65%", "70%", "75%", "80%", "85%", "90%", "95%", "100%"], - "values": [0.35, 0.4, 0.45, 0.5, 0.55, 0.6, 0.65, 0.7, 0.75, 0.8, 0.85, 0.9, 0.95, 1.0], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_thermostats/2293892/fan_speed" - } - } - }, { - "type": "fan_circulation_time", - "title": "Fan Circulation Time", - "current_value": 30, - "options": [{ - "value": 10, - "label": "10 minutes" - }, { - "value": 15, - "label": "15 minutes" - }, { - "value": 20, - "label": "20 minutes" - }, { - "value": 25, - "label": "25 minutes" - }, { - "value": 30, - "label": "30 minutes" - }, { - "value": 35, - "label": "35 minutes" - }, { - "value": 40, - "label": "40 minutes" - }, { - "value": 45, - "label": "45 minutes" - }, { - "value": 50, - "label": "50 minutes" - }, { - "value": 55, - "label": "55 minutes" - }], - "labels": ["10 minutes", "15 minutes", "20 minutes", "25 minutes", "30 minutes", "35 minutes", "40 minutes", "45 minutes", "50 minutes", "55 minutes"], - "values": [10, 15, 20, 25, 30, 35, 40, 45, 50, 55], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_thermostats/2293892/fan_circulation_time" - } - } - }, { - "type": "air_cleaner_mode", - "title": "Air Cleaner Mode", - "current_value": "auto", - "options": [{ - "value": "auto", - "label": "Auto" - }, { - "value": "quick", - "label": "Quick" - }, { - "value": "allergy", - "label": "Allergy" - }], - "labels": ["Auto", "Quick", "Allergy"], - "values": ["auto", "quick", "allergy"], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_thermostats/2293892/air_cleaner_mode" - } - } - }, { - "type": "dehumidify", - "title": "Cooling Dehumidify Set Point", - "current_value": 0.45, - "options": [{ - "value": 0.35, - "label": "35%" - }, { - "value": 0.4, - "label": "40%" - }, { - "value": 0.45, - "label": "45%" - }, { - "value": 0.5, - "label": "50%" - }, { - "value": 0.55, - "label": "55%" - }, { - "value": 0.6, - "label": "60%" - }, { - "value": 0.65, - "label": "65%" - }], - "labels": ["35%", "40%", "45%", "50%", "55%", "60%", "65%"], - "values": [0.35, 0.4, 0.45, 0.5, 0.55, 0.6, 0.65], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_thermostats/2293892/dehumidify" - } - } - }, { - "type": "scale", - "title": "Temperature Scale", - "current_value": "f", - "options": [{ - "value": "f", - "label": "F" - }, { - "value": "c", - "label": "C" - }], - "labels": ["F", "C"], - "values": ["f", "c"], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_thermostats/2293892/scale" - } - } - }], - "status_secondary": null, - "status_tertiary": null, - "type": "xxl_thermostat", - "has_outdoor_temperature": true, - "outdoor_temperature": "87", - "has_indoor_humidity": true, - "connected": true, - "indoor_humidity": "52", - "system_status": "Cooling", - "delta": 3, - "zones": [{ - "type": "xxl_zone", - "id": 83394133, - "name": "Bath Closet", - "current_zone_mode": "AUTO", - "temperature": 73, - "setpoints": { - "heat": 63, - "cool": 79 - }, - "operating_state": "Relieving Air", - "heating_setpoint": 63, - "cooling_setpoint": 79, - "zone_status": "Relieving Air", - "settings": [{ - "type": "preset_selected", - "title": "Preset", - "current_value": 0, - "options": [{ - "value": 0, - "label": "None" - }, { - "value": 1, - "label": "Home" - }, { - "value": 2, - "label": "Away" - }, { - "value": 3, - "label": "Sleep" - }], - "labels": ["None", "Home", "Away", "Sleep"], - "values": [0, 1, 2, 3], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/preset_selected" - } - } - }, { - "type": "zone_mode", - "title": "Zone Mode", - "current_value": "AUTO", - "options": [{ - "value": "AUTO", - "label": "Auto" - }, { - "value": "COOL", - "label": "Cooling" - }, { - "value": "HEAT", - "label": "Heating" - }, { - "value": "OFF", - "label": "Off" - }], - "labels": ["Auto", "Cooling", "Heating", "Off"], - "values": ["AUTO", "COOL", "HEAT", "OFF"], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/zone_mode" - } - } - }, { - "type": "run_mode", - "title": "Run Mode", - "current_value": "permanent_hold", - "options": [{ - "value": "permanent_hold", - "label": "Permanent Hold" - }, { - "value": "run_schedule", - "label": "Run Schedule" - }], - "labels": ["Permanent Hold", "Run Schedule"], - "values": ["permanent_hold", "run_schedule"], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/run_mode" - } - } - }, { - "type": "scheduling_enabled", - "title": "Scheduling", - "current_value": true, - "options": [{ - "value": true, - "label": "ON" - }, { - "value": false, - "label": "OFF" - }], - "labels": ["ON", "OFF"], - "values": [true, false], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/scheduling_enabled" - } - } - }], - "icon": { - "name": "thermostat", - "modifiers": ["temperature-73"] - }, - "features": [{ - "name": "thermostat", - "temperature": 73, - "status": "Relieving Air", - "status_icon": { - "name": "cooling", - "modifiers": [] - }, - "actions": { - "set_heat_setpoint": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/setpoints" - }, - "set_cool_setpoint": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/setpoints" - } - }, - "setpoint_delta": 3, - "scale": "f", - "setpoint_increment": 1.0, - "setpoint_heat_min": 55, - "setpoint_heat_max": 90, - "setpoint_cool_min": 60, - "setpoint_cool_max": 99, - "setpoint_heat": 63, - "setpoint_cool": 79, - "system_status": "Cooling" - }, { - "name": "connection", - "signal_strength": "unknown", - "is_connected": true - }, { - "name": "thermostat_mode", - "label": "Zone Mode", - "value": "AUTO", - "display_value": "Auto", - "options": [{ - "id": "thermostat_mode", - "label": "Zone Mode", - "value": "thermostat_mode", - "header": true - }, { - "value": "AUTO", - "label": "Auto" - }, { - "value": "COOL", - "label": "Cooling" - }, { - "value": "HEAT", - "label": "Heating" - }, { - "value": "OFF", - "label": "Off" - }], - "actions": { - "update_thermostat_mode": { - "method": "POST", - "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/zone_mode" - } - } - }, { - "name": "thermostat_run_mode", - "label": "Run Mode", - "options": [{ - "id": "thermostat_run_mode", - "label": "Run Mode", - "value": "thermostat_run_mode", - "header": true - }, { - "id": "info_text", - "label": "Follow or override the schedule.", - "value": "info_text", - "info": true - }, { - "value": "permanent_hold", - "label": "Permanent Hold" - }, { - "value": "run_schedule", - "label": "Run Schedule" - }], - "value": "permanent_hold", - "display_value": "Hold", - "actions": { - "update_thermostat_run_mode": { - "method": "POST", - "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/run_mode" - } - } - }, { - "name": "schedule", - "enabled": true, - "max_period_name_length": 10, - "setpoint_increment": 1, - "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83394133\u0026house_id=123456", - "actions": { - "get_active_schedule": { - "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83394133", - "method": "POST" - }, - "set_active_schedule": { - "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83394133", - "method": "POST" - }, - "get_default_schedule": { - "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83394133", - "method": "GET" - }, - "enable_scheduling": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/scheduling_enabled", - "method": "POST", - "data": { - "value": true - } - } - }, - "can_add_remove_periods": true, - "max_periods_per_day": 4 - }], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83394133" - } - } - }, { - "type": "xxl_zone", - "id": 83394130, - "name": "Master", - "current_zone_mode": "AUTO", - "temperature": 74, - "setpoints": { - "heat": 63, - "cool": 71 - }, - "operating_state": "Damper Open", - "heating_setpoint": 63, - "cooling_setpoint": 71, - "zone_status": "Damper Open", - "settings": [{ - "type": "preset_selected", - "title": "Preset", - "current_value": 0, - "options": [{ - "value": 0, - "label": "None" - }, { - "value": 1, - "label": "Home" - }, { - "value": 2, - "label": "Away" - }, { - "value": 3, - "label": "Sleep" - }], - "labels": ["None", "Home", "Away", "Sleep"], - "values": [0, 1, 2, 3], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/preset_selected" - } - } - }, { - "type": "zone_mode", - "title": "Zone Mode", - "current_value": "AUTO", - "options": [{ - "value": "AUTO", - "label": "Auto" - }, { - "value": "COOL", - "label": "Cooling" - }, { - "value": "HEAT", - "label": "Heating" - }, { - "value": "OFF", - "label": "Off" - }], - "labels": ["Auto", "Cooling", "Heating", "Off"], - "values": ["AUTO", "COOL", "HEAT", "OFF"], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/zone_mode" - } - } - }, { - "type": "run_mode", - "title": "Run Mode", - "current_value": "permanent_hold", - "options": [{ - "value": "permanent_hold", - "label": "Permanent Hold" - }, { - "value": "run_schedule", - "label": "Run Schedule" - }], - "labels": ["Permanent Hold", "Run Schedule"], - "values": ["permanent_hold", "run_schedule"], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/run_mode" - } - } - }, { - "type": "scheduling_enabled", - "title": "Scheduling", - "current_value": true, - "options": [{ - "value": true, - "label": "ON" - }, { - "value": false, - "label": "OFF" - }], - "labels": ["ON", "OFF"], - "values": [true, false], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/scheduling_enabled" - } - } - }], - "icon": { - "name": "thermostat", - "modifiers": ["temperature-74"] - }, - "features": [{ - "name": "thermostat", - "temperature": 74, - "status": "Damper Open", - "status_icon": { - "name": "cooling", - "modifiers": [] - }, - "actions": { - "set_heat_setpoint": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/setpoints" - }, - "set_cool_setpoint": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/setpoints" - } - }, - "setpoint_delta": 3, - "scale": "f", - "setpoint_increment": 1.0, - "setpoint_heat_min": 55, - "setpoint_heat_max": 90, - "setpoint_cool_min": 60, - "setpoint_cool_max": 99, - "setpoint_heat": 63, - "setpoint_cool": 71, - "system_status": "Cooling" - }, { - "name": "connection", - "signal_strength": "unknown", - "is_connected": true - }, { - "name": "thermostat_mode", - "label": "Zone Mode", - "value": "AUTO", - "display_value": "Auto", - "options": [{ - "id": "thermostat_mode", - "label": "Zone Mode", - "value": "thermostat_mode", - "header": true - }, { - "value": "AUTO", - "label": "Auto" - }, { - "value": "COOL", - "label": "Cooling" - }, { - "value": "HEAT", - "label": "Heating" - }, { - "value": "OFF", - "label": "Off" - }], - "actions": { - "update_thermostat_mode": { - "method": "POST", - "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/zone_mode" - } - } - }, { - "name": "thermostat_run_mode", - "label": "Run Mode", - "options": [{ - "id": "thermostat_run_mode", - "label": "Run Mode", - "value": "thermostat_run_mode", - "header": true - }, { - "id": "info_text", - "label": "Follow or override the schedule.", - "value": "info_text", - "info": true - }, { - "value": "permanent_hold", - "label": "Permanent Hold" - }, { - "value": "run_schedule", - "label": "Run Schedule" - }], - "value": "permanent_hold", - "display_value": "Hold", - "actions": { - "update_thermostat_run_mode": { - "method": "POST", - "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/run_mode" - } - } - }, { - "name": "schedule", - "enabled": true, - "max_period_name_length": 10, - "setpoint_increment": 1, - "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83394130\u0026house_id=123456", - "actions": { - "get_active_schedule": { - "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83394130", - "method": "POST" - }, - "set_active_schedule": { - "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83394130", - "method": "POST" - }, - "get_default_schedule": { - "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83394130", - "method": "GET" - }, - "enable_scheduling": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/scheduling_enabled", - "method": "POST", - "data": { - "value": true - } - } - }, - "can_add_remove_periods": true, - "max_periods_per_day": 4 - }], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83394130" - } - } - }, { - "type": "xxl_zone", - "id": 83394136, - "name": "Nick Office", - "current_zone_mode": "AUTO", - "temperature": 73, - "setpoints": { - "heat": 63, - "cool": 79 - }, - "operating_state": "Relieving Air", - "heating_setpoint": 63, - "cooling_setpoint": 79, - "zone_status": "Relieving Air", - "settings": [{ - "type": "preset_selected", - "title": "Preset", - "current_value": 0, - "options": [{ - "value": 0, - "label": "None" - }, { - "value": 1, - "label": "Home" - }, { - "value": 2, - "label": "Away" - }, { - "value": 3, - "label": "Sleep" - }], - "labels": ["None", "Home", "Away", "Sleep"], - "values": [0, 1, 2, 3], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/preset_selected" - } - } - }, { - "type": "zone_mode", - "title": "Zone Mode", - "current_value": "AUTO", - "options": [{ - "value": "AUTO", - "label": "Auto" - }, { - "value": "COOL", - "label": "Cooling" - }, { - "value": "HEAT", - "label": "Heating" - }, { - "value": "OFF", - "label": "Off" - }], - "labels": ["Auto", "Cooling", "Heating", "Off"], - "values": ["AUTO", "COOL", "HEAT", "OFF"], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/zone_mode" - } - } - }, { - "type": "run_mode", - "title": "Run Mode", - "current_value": "permanent_hold", - "options": [{ - "value": "permanent_hold", - "label": "Permanent Hold" - }, { - "value": "run_schedule", - "label": "Run Schedule" - }], - "labels": ["Permanent Hold", "Run Schedule"], - "values": ["permanent_hold", "run_schedule"], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/run_mode" - } - } - }, { - "type": "scheduling_enabled", - "title": "Scheduling", - "current_value": true, - "options": [{ - "value": true, - "label": "ON" - }, { - "value": false, - "label": "OFF" - }], - "labels": ["ON", "OFF"], - "values": [true, false], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/scheduling_enabled" - } - } - }], - "icon": { - "name": "thermostat", - "modifiers": ["temperature-73"] - }, - "features": [{ - "name": "thermostat", - "temperature": 73, - "status": "Relieving Air", - "status_icon": { - "name": "cooling", - "modifiers": [] - }, - "actions": { - "set_heat_setpoint": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/setpoints" - }, - "set_cool_setpoint": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/setpoints" - } - }, - "setpoint_delta": 3, - "scale": "f", - "setpoint_increment": 1.0, - "setpoint_heat_min": 55, - "setpoint_heat_max": 90, - "setpoint_cool_min": 60, - "setpoint_cool_max": 99, - "setpoint_heat": 63, - "setpoint_cool": 79, - "system_status": "Cooling" - }, { - "name": "connection", - "signal_strength": "unknown", - "is_connected": true - }, { - "name": "thermostat_mode", - "label": "Zone Mode", - "value": "AUTO", - "display_value": "Auto", - "options": [{ - "id": "thermostat_mode", - "label": "Zone Mode", - "value": "thermostat_mode", - "header": true - }, { - "value": "AUTO", - "label": "Auto" - }, { - "value": "COOL", - "label": "Cooling" - }, { - "value": "HEAT", - "label": "Heating" - }, { - "value": "OFF", - "label": "Off" - }], - "actions": { - "update_thermostat_mode": { - "method": "POST", - "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/zone_mode" - } - } - }, { - "name": "thermostat_run_mode", - "label": "Run Mode", - "options": [{ - "id": "thermostat_run_mode", - "label": "Run Mode", - "value": "thermostat_run_mode", - "header": true - }, { - "id": "info_text", - "label": "Follow or override the schedule.", - "value": "info_text", - "info": true - }, { - "value": "permanent_hold", - "label": "Permanent Hold" - }, { - "value": "run_schedule", - "label": "Run Schedule" - }], - "value": "permanent_hold", - "display_value": "Hold", - "actions": { - "update_thermostat_run_mode": { - "method": "POST", - "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/run_mode" - } - } - }, { - "name": "schedule", - "enabled": true, - "max_period_name_length": 10, - "setpoint_increment": 1, - "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83394136\u0026house_id=123456", - "actions": { - "get_active_schedule": { - "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83394136", - "method": "POST" - }, - "set_active_schedule": { - "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83394136", - "method": "POST" - }, - "get_default_schedule": { - "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83394136", - "method": "GET" - }, - "enable_scheduling": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/scheduling_enabled", - "method": "POST", - "data": { - "value": true - } - } - }, - "can_add_remove_periods": true, - "max_periods_per_day": 4 - }], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83394136" - } - } - }, { - "type": "xxl_zone", - "id": 83394127, - "name": "Snooze Room", - "current_zone_mode": "AUTO", - "temperature": 72, - "setpoints": { - "heat": 63, - "cool": 79 - }, - "operating_state": "Damper Closed", - "heating_setpoint": 63, - "cooling_setpoint": 79, - "zone_status": "Damper Closed", - "settings": [{ - "type": "preset_selected", - "title": "Preset", - "current_value": 0, - "options": [{ - "value": 0, - "label": "None" - }, { - "value": 1, - "label": "Home" - }, { - "value": 2, - "label": "Away" - }, { - "value": 3, - "label": "Sleep" - }], - "labels": ["None", "Home", "Away", "Sleep"], - "values": [0, 1, 2, 3], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/preset_selected" - } - } - }, { - "type": "zone_mode", - "title": "Zone Mode", - "current_value": "AUTO", - "options": [{ - "value": "AUTO", - "label": "Auto" - }, { - "value": "COOL", - "label": "Cooling" - }, { - "value": "HEAT", - "label": "Heating" - }, { - "value": "OFF", - "label": "Off" - }], - "labels": ["Auto", "Cooling", "Heating", "Off"], - "values": ["AUTO", "COOL", "HEAT", "OFF"], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/zone_mode" - } - } - }, { - "type": "run_mode", - "title": "Run Mode", - "current_value": "permanent_hold", - "options": [{ - "value": "permanent_hold", - "label": "Permanent Hold" - }, { - "value": "run_schedule", - "label": "Run Schedule" - }], - "labels": ["Permanent Hold", "Run Schedule"], - "values": ["permanent_hold", "run_schedule"], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/run_mode" - } - } - }, { - "type": "scheduling_enabled", - "title": "Scheduling", - "current_value": true, - "options": [{ - "value": true, - "label": "ON" - }, { - "value": false, - "label": "OFF" - }], - "labels": ["ON", "OFF"], - "values": [true, false], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/scheduling_enabled" - } - } - }], - "icon": { - "name": "thermostat", - "modifiers": ["temperature-72"] - }, - "features": [{ - "name": "thermostat", - "temperature": 72, - "status": "Damper Closed", - "status_icon": { - "name": "cooling", - "modifiers": [] - }, - "actions": { - "set_heat_setpoint": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/setpoints" - }, - "set_cool_setpoint": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/setpoints" - } - }, - "setpoint_delta": 3, - "scale": "f", - "setpoint_increment": 1.0, - "setpoint_heat_min": 55, - "setpoint_heat_max": 90, - "setpoint_cool_min": 60, - "setpoint_cool_max": 99, - "setpoint_heat": 63, - "setpoint_cool": 79, - "system_status": "Cooling" - }, { - "name": "connection", - "signal_strength": "unknown", - "is_connected": true - }, { - "name": "thermostat_mode", - "label": "Zone Mode", - "value": "AUTO", - "display_value": "Auto", - "options": [{ - "id": "thermostat_mode", - "label": "Zone Mode", - "value": "thermostat_mode", - "header": true - }, { - "value": "AUTO", - "label": "Auto" - }, { - "value": "COOL", - "label": "Cooling" - }, { - "value": "HEAT", - "label": "Heating" - }, { - "value": "OFF", - "label": "Off" - }], - "actions": { - "update_thermostat_mode": { - "method": "POST", - "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/zone_mode" - } - } - }, { - "name": "thermostat_run_mode", - "label": "Run Mode", - "options": [{ - "id": "thermostat_run_mode", - "label": "Run Mode", - "value": "thermostat_run_mode", - "header": true - }, { - "id": "info_text", - "label": "Follow or override the schedule.", - "value": "info_text", - "info": true - }, { - "value": "permanent_hold", - "label": "Permanent Hold" - }, { - "value": "run_schedule", - "label": "Run Schedule" - }], - "value": "permanent_hold", - "display_value": "Hold", - "actions": { - "update_thermostat_run_mode": { - "method": "POST", - "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/run_mode" - } - } - }, { - "name": "schedule", - "enabled": true, - "max_period_name_length": 10, - "setpoint_increment": 1, - "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83394127\u0026house_id=123456", - "actions": { - "get_active_schedule": { - "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83394127", - "method": "POST" - }, - "set_active_schedule": { - "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83394127", - "method": "POST" - }, - "get_default_schedule": { - "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83394127", - "method": "GET" - }, - "enable_scheduling": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/scheduling_enabled", - "method": "POST", - "data": { - "value": true - } - } - }, - "can_add_remove_periods": true, - "max_periods_per_day": 4 - }], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83394127" - } - } - }, { - "type": "xxl_zone", - "id": 83394139, - "name": "Safe Room", - "current_zone_mode": "AUTO", - "temperature": 74, - "setpoints": { - "heat": 63, - "cool": 79 - }, - "operating_state": "Damper Closed", - "heating_setpoint": 63, - "cooling_setpoint": 79, - "zone_status": "Damper Closed", - "settings": [{ - "type": "preset_selected", - "title": "Preset", - "current_value": 0, - "options": [{ - "value": 0, - "label": "None" - }, { - "value": 1, - "label": "Home" - }, { - "value": 2, - "label": "Away" - }, { - "value": 3, - "label": "Sleep" - }], - "labels": ["None", "Home", "Away", "Sleep"], - "values": [0, 1, 2, 3], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/preset_selected" - } - } - }, { - "type": "zone_mode", - "title": "Zone Mode", - "current_value": "AUTO", - "options": [{ - "value": "AUTO", - "label": "Auto" - }, { - "value": "COOL", - "label": "Cooling" - }, { - "value": "HEAT", - "label": "Heating" - }, { - "value": "OFF", - "label": "Off" - }], - "labels": ["Auto", "Cooling", "Heating", "Off"], - "values": ["AUTO", "COOL", "HEAT", "OFF"], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/zone_mode" - } - } - }, { - "type": "run_mode", - "title": "Run Mode", - "current_value": "permanent_hold", - "options": [{ - "value": "permanent_hold", - "label": "Permanent Hold" - }, { - "value": "run_schedule", - "label": "Run Schedule" - }], - "labels": ["Permanent Hold", "Run Schedule"], - "values": ["permanent_hold", "run_schedule"], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/run_mode" - } - } - }, { - "type": "scheduling_enabled", - "title": "Scheduling", - "current_value": true, - "options": [{ - "value": true, - "label": "ON" - }, { - "value": false, - "label": "OFF" - }], - "labels": ["ON", "OFF"], - "values": [true, false], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/scheduling_enabled" - } - } - }], - "icon": { - "name": "thermostat", - "modifiers": ["temperature-74"] - }, - "features": [{ - "name": "thermostat", - "temperature": 74, - "status": "Damper Closed", - "status_icon": { - "name": "cooling", - "modifiers": [] - }, - "actions": { - "set_heat_setpoint": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/setpoints" - }, - "set_cool_setpoint": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/setpoints" - } - }, - "setpoint_delta": 3, - "scale": "f", - "setpoint_increment": 1.0, - "setpoint_heat_min": 55, - "setpoint_heat_max": 90, - "setpoint_cool_min": 60, - "setpoint_cool_max": 99, - "setpoint_heat": 63, - "setpoint_cool": 79, - "system_status": "Cooling" - }, { - "name": "connection", - "signal_strength": "unknown", - "is_connected": true - }, { - "name": "thermostat_mode", - "label": "Zone Mode", - "value": "AUTO", - "display_value": "Auto", - "options": [{ - "id": "thermostat_mode", - "label": "Zone Mode", - "value": "thermostat_mode", - "header": true - }, { - "value": "AUTO", - "label": "Auto" - }, { - "value": "COOL", - "label": "Cooling" - }, { - "value": "HEAT", - "label": "Heating" - }, { - "value": "OFF", - "label": "Off" - }], - "actions": { - "update_thermostat_mode": { - "method": "POST", - "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/zone_mode" - } - } - }, { - "name": "thermostat_run_mode", - "label": "Run Mode", - "options": [{ - "id": "thermostat_run_mode", - "label": "Run Mode", - "value": "thermostat_run_mode", - "header": true - }, { - "id": "info_text", - "label": "Follow or override the schedule.", - "value": "info_text", - "info": true - }, { - "value": "permanent_hold", - "label": "Permanent Hold" - }, { - "value": "run_schedule", - "label": "Run Schedule" - }], - "value": "permanent_hold", - "display_value": "Hold", - "actions": { - "update_thermostat_run_mode": { - "method": "POST", - "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/run_mode" - } - } - }, { - "name": "schedule", - "enabled": true, - "max_period_name_length": 10, - "setpoint_increment": 1, - "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83394139\u0026house_id=123456", - "actions": { - "get_active_schedule": { - "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83394139", - "method": "POST" - }, - "set_active_schedule": { - "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83394139", - "method": "POST" - }, - "get_default_schedule": { - "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83394139", - "method": "GET" - }, - "enable_scheduling": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/scheduling_enabled", - "method": "POST", - "data": { - "value": true - } - } - }, - "can_add_remove_periods": true, - "max_periods_per_day": 4 - }], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83394139" - } - } - }] - }, { - "id": 2059652, - "name": "Upstairs West Wing", - "name_editable": true, - "features": [{ - "name": "advanced_info", - "items": [{ - "type": "label_value", - "label": "Model", - "value": "XL1050" - }, { - "type": "label_value", - "label": "AUID", - "value": "02853DF0" - }, { - "type": "label_value", - "label": "Firmware Build Number", - "value": "1581321824" - }, { - "type": "label_value", - "label": "Firmware Build Date", - "value": "2020-02-10 08:03:44 UTC" - }, { - "type": "label_value", - "label": "Firmware Version", - "value": "5.9.1" - }, { - "type": "label_value", - "label": "Zoning Enabled", - "value": "yes" - }] - }, { - "name": "thermostat", - "temperature": 77, - "status": "System Idle", - "status_icon": null, - "actions": {}, - "setpoint_delta": 3, - "scale": "f", - "setpoint_increment": 1.0, - "setpoint_heat_min": 55, - "setpoint_heat_max": 90, - "setpoint_cool_min": 60, - "setpoint_cool_max": 99 - }, { - "name": "connection", - "signal_strength": "unknown", - "is_connected": true - }, { - "name": "group", - "members": [{ - "type": "xxl_zone", - "id": 83260991, - "name": "Hallway", - "current_zone_mode": "OFF", - "temperature": 77, - "setpoints": { - "heat": 63, - "cool": 80 - }, - "operating_state": "", - "heating_setpoint": 63, - "cooling_setpoint": 80, - "zone_status": "", - "settings": [{ - "type": "preset_selected", - "title": "Preset", - "current_value": 0, - "options": [{ - "value": 0, - "label": "None" - }, { - "value": 1, - "label": "Home" - }, { - "value": 2, - "label": "Away" - }, { - "value": 3, - "label": "Sleep" - }], - "labels": ["None", "Home", "Away", "Sleep"], - "values": [0, 1, 2, 3], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83260991/preset_selected" - } - } - }, { - "type": "zone_mode", - "title": "Zone Mode", - "current_value": "OFF", - "options": [{ - "value": "AUTO", - "label": "Auto" - }, { - "value": "COOL", - "label": "Cooling" - }, { - "value": "HEAT", - "label": "Heating" - }, { - "value": "OFF", - "label": "Off" - }], - "labels": ["Auto", "Cooling", "Heating", "Off"], - "values": ["AUTO", "COOL", "HEAT", "OFF"], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83260991/zone_mode" - } - } - }, { - "type": "run_mode", - "title": "Run Mode", - "current_value": "permanent_hold", - "options": [{ - "value": "permanent_hold", - "label": "Permanent Hold" - }, { - "value": "run_schedule", - "label": "Run Schedule" - }], - "labels": ["Permanent Hold", "Run Schedule"], - "values": ["permanent_hold", "run_schedule"], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83260991/run_mode" - } - } - }, { - "type": "scheduling_enabled", - "title": "Scheduling", - "current_value": true, - "options": [{ - "value": true, - "label": "ON" - }, { - "value": false, - "label": "OFF" - }], - "labels": ["ON", "OFF"], - "values": [true, false], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83260991/scheduling_enabled" - } - } - }], - "icon": { - "name": "thermostat", - "modifiers": ["temperature-77"] - }, - "features": [{ - "name": "thermostat", - "temperature": 77, - "status": "", - "status_icon": null, - "actions": {}, - "setpoint_delta": 3, - "scale": "f", - "setpoint_increment": 1.0, - "setpoint_heat_min": 55, - "setpoint_heat_max": 90, - "setpoint_cool_min": 60, - "setpoint_cool_max": 99, - "system_status": "System Idle" - }, { - "name": "connection", - "signal_strength": "unknown", - "is_connected": true - }, { - "name": "thermostat_mode", - "label": "Zone Mode", - "value": "OFF", - "display_value": "Off", - "options": [{ - "id": "thermostat_mode", - "label": "Zone Mode", - "value": "thermostat_mode", - "header": true - }, { - "value": "AUTO", - "label": "Auto" - }, { - "value": "COOL", - "label": "Cooling" - }, { - "value": "HEAT", - "label": "Heating" - }, { - "value": "OFF", - "label": "Off" - }], - "actions": { - "update_thermostat_mode": { - "method": "POST", - "href": "https://www.mynexia.com/mobile/xxl_zones/83260991/zone_mode" - } - } - }, { - "name": "thermostat_run_mode", - "label": "Run Mode", - "options": [{ - "id": "thermostat_run_mode", - "label": "Run Mode", - "value": "thermostat_run_mode", - "header": true - }, { - "id": "info_text", - "label": "Follow or override the schedule.", - "value": "info_text", - "info": true - }, { - "value": "permanent_hold", - "label": "Permanent Hold" - }, { - "value": "run_schedule", - "label": "Run Schedule" - }], - "value": "permanent_hold", - "display_value": "Hold", - "actions": { - "update_thermostat_run_mode": { - "method": "POST", - "href": "https://www.mynexia.com/mobile/xxl_zones/83260991/run_mode" - } - } - }, { - "name": "schedule", - "enabled": true, - "max_period_name_length": 10, - "setpoint_increment": 1, - "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83260991\u0026house_id=123456", - "actions": { - "get_active_schedule": { - "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83260991", - "method": "POST" - }, - "set_active_schedule": { - "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83260991", - "method": "POST" - }, - "get_default_schedule": { - "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83260991", - "method": "GET" - }, - "enable_scheduling": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83260991/scheduling_enabled", - "method": "POST", - "data": { - "value": true - } - } - }, - "can_add_remove_periods": true, - "max_periods_per_day": 4 - }], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83260991" - } - } - }, { - "type": "xxl_zone", - "id": 83260994, - "name": "Mid Bedroom", - "current_zone_mode": "AUTO", - "temperature": 74, - "setpoints": { - "heat": 63, - "cool": 81 - }, - "operating_state": "", - "heating_setpoint": 63, - "cooling_setpoint": 81, - "zone_status": "", - "settings": [{ - "type": "preset_selected", - "title": "Preset", - "current_value": 0, - "options": [{ - "value": 0, - "label": "None" - }, { - "value": 1, - "label": "Home" - }, { - "value": 2, - "label": "Away" - }, { - "value": 3, - "label": "Sleep" - }], - "labels": ["None", "Home", "Away", "Sleep"], - "values": [0, 1, 2, 3], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83260994/preset_selected" - } - } - }, { - "type": "zone_mode", - "title": "Zone Mode", - "current_value": "AUTO", - "options": [{ - "value": "AUTO", - "label": "Auto" - }, { - "value": "COOL", - "label": "Cooling" - }, { - "value": "HEAT", - "label": "Heating" - }, { - "value": "OFF", - "label": "Off" - }], - "labels": ["Auto", "Cooling", "Heating", "Off"], - "values": ["AUTO", "COOL", "HEAT", "OFF"], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83260994/zone_mode" - } - } - }, { - "type": "run_mode", - "title": "Run Mode", - "current_value": "permanent_hold", - "options": [{ - "value": "permanent_hold", - "label": "Permanent Hold" - }, { - "value": "run_schedule", - "label": "Run Schedule" - }], - "labels": ["Permanent Hold", "Run Schedule"], - "values": ["permanent_hold", "run_schedule"], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83260994/run_mode" - } - } - }, { - "type": "scheduling_enabled", - "title": "Scheduling", - "current_value": true, - "options": [{ - "value": true, - "label": "ON" - }, { - "value": false, - "label": "OFF" - }], - "labels": ["ON", "OFF"], - "values": [true, false], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83260994/scheduling_enabled" - } - } - }], - "icon": { - "name": "thermostat", - "modifiers": ["temperature-74"] - }, - "features": [{ - "name": "thermostat", - "temperature": 74, - "status": "", - "status_icon": null, - "actions": { - "set_heat_setpoint": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83260994/setpoints" - }, - "set_cool_setpoint": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83260994/setpoints" - } - }, - "setpoint_delta": 3, - "scale": "f", - "setpoint_increment": 1.0, - "setpoint_heat_min": 55, - "setpoint_heat_max": 90, - "setpoint_cool_min": 60, - "setpoint_cool_max": 99, - "setpoint_heat": 63, - "setpoint_cool": 81, - "system_status": "System Idle" - }, { - "name": "connection", - "signal_strength": "unknown", - "is_connected": true - }, { - "name": "thermostat_mode", - "label": "Zone Mode", - "value": "AUTO", - "display_value": "Auto", - "options": [{ - "id": "thermostat_mode", - "label": "Zone Mode", - "value": "thermostat_mode", - "header": true - }, { - "value": "AUTO", - "label": "Auto" - }, { - "value": "COOL", - "label": "Cooling" - }, { - "value": "HEAT", - "label": "Heating" - }, { - "value": "OFF", - "label": "Off" - }], - "actions": { - "update_thermostat_mode": { - "method": "POST", - "href": "https://www.mynexia.com/mobile/xxl_zones/83260994/zone_mode" - } - } - }, { - "name": "thermostat_run_mode", - "label": "Run Mode", - "options": [{ - "id": "thermostat_run_mode", - "label": "Run Mode", - "value": "thermostat_run_mode", - "header": true - }, { - "id": "info_text", - "label": "Follow or override the schedule.", - "value": "info_text", - "info": true - }, { - "value": "permanent_hold", - "label": "Permanent Hold" - }, { - "value": "run_schedule", - "label": "Run Schedule" - }], - "value": "permanent_hold", - "display_value": "Hold", - "actions": { - "update_thermostat_run_mode": { - "method": "POST", - "href": "https://www.mynexia.com/mobile/xxl_zones/83260994/run_mode" - } - } - }, { - "name": "schedule", - "enabled": true, - "max_period_name_length": 10, - "setpoint_increment": 1, - "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83260994\u0026house_id=123456", - "actions": { - "get_active_schedule": { - "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83260994", - "method": "POST" - }, - "set_active_schedule": { - "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83260994", - "method": "POST" - }, - "get_default_schedule": { - "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83260994", - "method": "GET" - }, - "enable_scheduling": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83260994/scheduling_enabled", - "method": "POST", - "data": { - "value": true - } - } - }, - "can_add_remove_periods": true, - "max_periods_per_day": 4 - }], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83260994" - } - } - }, { - "type": "xxl_zone", - "id": 83260997, - "name": "West Bedroom", - "current_zone_mode": "AUTO", - "temperature": 75, - "setpoints": { - "heat": 63, - "cool": 81 - }, - "operating_state": "", - "heating_setpoint": 63, - "cooling_setpoint": 81, - "zone_status": "", - "settings": [{ - "type": "preset_selected", - "title": "Preset", - "current_value": 0, - "options": [{ - "value": 0, - "label": "None" - }, { - "value": 1, - "label": "Home" - }, { - "value": 2, - "label": "Away" - }, { - "value": 3, - "label": "Sleep" - }], - "labels": ["None", "Home", "Away", "Sleep"], - "values": [0, 1, 2, 3], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83260997/preset_selected" - } - } - }, { - "type": "zone_mode", - "title": "Zone Mode", - "current_value": "AUTO", - "options": [{ - "value": "AUTO", - "label": "Auto" - }, { - "value": "COOL", - "label": "Cooling" - }, { - "value": "HEAT", - "label": "Heating" - }, { - "value": "OFF", - "label": "Off" - }], - "labels": ["Auto", "Cooling", "Heating", "Off"], - "values": ["AUTO", "COOL", "HEAT", "OFF"], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83260997/zone_mode" - } - } - }, { - "type": "run_mode", - "title": "Run Mode", - "current_value": "permanent_hold", - "options": [{ - "value": "permanent_hold", - "label": "Permanent Hold" - }, { - "value": "run_schedule", - "label": "Run Schedule" - }], - "labels": ["Permanent Hold", "Run Schedule"], - "values": ["permanent_hold", "run_schedule"], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83260997/run_mode" - } - } - }, { - "type": "scheduling_enabled", - "title": "Scheduling", - "current_value": true, - "options": [{ - "value": true, - "label": "ON" - }, { - "value": false, - "label": "OFF" - }], - "labels": ["ON", "OFF"], - "values": [true, false], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83260997/scheduling_enabled" - } - } - }], - "icon": { - "name": "thermostat", - "modifiers": ["temperature-75"] - }, - "features": [{ - "name": "thermostat", - "temperature": 75, - "status": "", - "status_icon": null, - "actions": { - "set_heat_setpoint": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83260997/setpoints" - }, - "set_cool_setpoint": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83260997/setpoints" - } - }, - "setpoint_delta": 3, - "scale": "f", - "setpoint_increment": 1.0, - "setpoint_heat_min": 55, - "setpoint_heat_max": 90, - "setpoint_cool_min": 60, - "setpoint_cool_max": 99, - "setpoint_heat": 63, - "setpoint_cool": 81, - "system_status": "System Idle" - }, { - "name": "connection", - "signal_strength": "unknown", - "is_connected": true - }, { - "name": "thermostat_mode", - "label": "Zone Mode", - "value": "AUTO", - "display_value": "Auto", - "options": [{ - "id": "thermostat_mode", - "label": "Zone Mode", - "value": "thermostat_mode", - "header": true - }, { - "value": "AUTO", - "label": "Auto" - }, { - "value": "COOL", - "label": "Cooling" - }, { - "value": "HEAT", - "label": "Heating" - }, { - "value": "OFF", - "label": "Off" - }], - "actions": { - "update_thermostat_mode": { - "method": "POST", - "href": "https://www.mynexia.com/mobile/xxl_zones/83260997/zone_mode" - } - } - }, { - "name": "thermostat_run_mode", - "label": "Run Mode", - "options": [{ - "id": "thermostat_run_mode", - "label": "Run Mode", - "value": "thermostat_run_mode", - "header": true - }, { - "id": "info_text", - "label": "Follow or override the schedule.", - "value": "info_text", - "info": true - }, { - "value": "permanent_hold", - "label": "Permanent Hold" - }, { - "value": "run_schedule", - "label": "Run Schedule" - }], - "value": "permanent_hold", - "display_value": "Hold", - "actions": { - "update_thermostat_run_mode": { - "method": "POST", - "href": "https://www.mynexia.com/mobile/xxl_zones/83260997/run_mode" - } - } - }, { - "name": "schedule", - "enabled": true, - "max_period_name_length": 10, - "setpoint_increment": 1, - "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83260997\u0026house_id=123456", - "actions": { - "get_active_schedule": { - "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83260997", - "method": "POST" - }, - "set_active_schedule": { - "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83260997", - "method": "POST" - }, - "get_default_schedule": { - "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83260997", - "method": "GET" - }, - "enable_scheduling": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83260997/scheduling_enabled", - "method": "POST", - "data": { - "value": true - } - } - }, - "can_add_remove_periods": true, - "max_periods_per_day": 4 - }], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83260997" - } - } - }] - }, { - "name": "thermostat_fan_mode", - "label": "Fan Mode", - "options": [{ - "id": "thermostat_fan_mode", - "label": "Fan Mode", - "value": "thermostat_fan_mode", - "header": true - }, { - "value": "auto", - "label": "Auto" - }, { - "value": "on", - "label": "On" - }, { - "value": "circulate", - "label": "Circulate" - }], - "value": "auto", - "display_value": "Auto", - "status_icon": { - "name": "thermostat_fan_off", - "modifiers": [] - }, - "actions": { - "update_thermostat_fan_mode": { - "method": "POST", - "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059652/fan_mode" - } - } - }, { - "name": "thermostat_compressor_speed", - "compressor_speed": 0.0 - }, { - "name": "runtime_history", - "actions": { - "get_runtime_history": { - "method": "GET", - "href": "https://www.mynexia.com/mobile/runtime_history/2059652?report_type=daily" - }, - "get_monthly_runtime_history": { - "method": "GET", - "href": "https://www.mynexia.com/mobile/runtime_history/2059652?report_type=monthly" - } - } - }], - "icon": [{ - "name": "thermostat", - "modifiers": ["temperature-77"] - }, { - "name": "thermostat", - "modifiers": ["temperature-74"] - }, { - "name": "thermostat", - "modifiers": ["temperature-75"] - }], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059652" - }, - "nexia:history": { - "href": "https://www.mynexia.com/mobile/houses/123456/events?device_id=2059652" - }, - "filter_events": { - "href": "https://www.mynexia.com/mobile/houses/123456/events/collection?sys_guid=3679e95b-7337-48ae-aff4-e0522e9dd0eb" - }, - "pending_request": { - "polling_path": "https://www.mynexia.com/backstage/announcements/c6627726f6339d104ee66897028d6a2ea38215675b336650" - } - }, - "last_updated_at": "2020-03-11T15:15:53.000-05:00", - "settings": [{ - "type": "fan_mode", - "title": "Fan Mode", - "current_value": "auto", - "options": [{ - "value": "auto", - "label": "Auto" - }, { - "value": "on", - "label": "On" - }, { - "value": "circulate", - "label": "Circulate" - }], - "labels": ["Auto", "On", "Circulate"], - "values": ["auto", "on", "circulate"], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059652/fan_mode" - } - } - }, { - "type": "fan_speed", - "title": "Fan Speed", - "current_value": 0.35, - "options": [{ - "value": 0.35, - "label": "35%" - }, { - "value": 0.4, - "label": "40%" - }, { - "value": 0.45, - "label": "45%" - }, { - "value": 0.5, - "label": "50%" - }, { - "value": 0.55, - "label": "55%" - }, { - "value": 0.6, - "label": "60%" - }, { - "value": 0.65, - "label": "65%" - }, { - "value": 0.7, - "label": "70%" - }, { - "value": 0.75, - "label": "75%" - }, { - "value": 0.8, - "label": "80%" - }, { - "value": 0.85, - "label": "85%" - }, { - "value": 0.9, - "label": "90%" - }, { - "value": 0.95, - "label": "95%" - }, { - "value": 1.0, - "label": "100%" - }], - "labels": ["35%", "40%", "45%", "50%", "55%", "60%", "65%", "70%", "75%", "80%", "85%", "90%", "95%", "100%"], - "values": [0.35, 0.4, 0.45, 0.5, 0.55, 0.6, 0.65, 0.7, 0.75, 0.8, 0.85, 0.9, 0.95, 1.0], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059652/fan_speed" - } - } - }, { - "type": "fan_circulation_time", - "title": "Fan Circulation Time", - "current_value": 30, - "options": [{ - "value": 10, - "label": "10 minutes" - }, { - "value": 15, - "label": "15 minutes" - }, { - "value": 20, - "label": "20 minutes" - }, { - "value": 25, - "label": "25 minutes" - }, { - "value": 30, - "label": "30 minutes" - }, { - "value": 35, - "label": "35 minutes" - }, { - "value": 40, - "label": "40 minutes" - }, { - "value": 45, - "label": "45 minutes" - }, { - "value": 50, - "label": "50 minutes" - }, { - "value": 55, - "label": "55 minutes" - }], - "labels": ["10 minutes", "15 minutes", "20 minutes", "25 minutes", "30 minutes", "35 minutes", "40 minutes", "45 minutes", "50 minutes", "55 minutes"], - "values": [10, 15, 20, 25, 30, 35, 40, 45, 50, 55], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059652/fan_circulation_time" - } - } - }, { - "type": "air_cleaner_mode", - "title": "Air Cleaner Mode", - "current_value": "auto", - "options": [{ - "value": "auto", - "label": "Auto" - }, { - "value": "quick", - "label": "Quick" - }, { - "value": "allergy", - "label": "Allergy" - }], - "labels": ["Auto", "Quick", "Allergy"], - "values": ["auto", "quick", "allergy"], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059652/air_cleaner_mode" - } - } - }, { - "type": "dehumidify", - "title": "Cooling Dehumidify Set Point", - "current_value": 0.5, - "options": [{ - "value": 0.35, - "label": "35%" - }, { - "value": 0.4, - "label": "40%" - }, { - "value": 0.45, - "label": "45%" - }, { - "value": 0.5, - "label": "50%" - }, { - "value": 0.55, - "label": "55%" - }, { - "value": 0.6, - "label": "60%" - }, { - "value": 0.65, - "label": "65%" - }], - "labels": ["35%", "40%", "45%", "50%", "55%", "60%", "65%"], - "values": [0.35, 0.4, 0.45, 0.5, 0.55, 0.6, 0.65], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059652/dehumidify" - } - } - }, { - "type": "scale", - "title": "Temperature Scale", - "current_value": "f", - "options": [{ - "value": "f", - "label": "F" - }, { - "value": "c", - "label": "C" - }], - "labels": ["F", "C"], - "values": ["f", "c"], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059652/scale" - } - } - }], - "status_secondary": null, - "status_tertiary": null, - "type": "xxl_thermostat", - "has_outdoor_temperature": true, - "outdoor_temperature": "87", - "has_indoor_humidity": true, - "connected": true, - "indoor_humidity": "37", - "system_status": "System Idle", - "delta": 3, - "zones": [{ - "type": "xxl_zone", - "id": 83260991, - "name": "Hallway", - "current_zone_mode": "OFF", - "temperature": 77, - "setpoints": { - "heat": 63, - "cool": 80 - }, - "operating_state": "", - "heating_setpoint": 63, - "cooling_setpoint": 80, - "zone_status": "", - "settings": [{ - "type": "preset_selected", - "title": "Preset", - "current_value": 0, - "options": [{ - "value": 0, - "label": "None" - }, { - "value": 1, - "label": "Home" - }, { - "value": 2, - "label": "Away" - }, { - "value": 3, - "label": "Sleep" - }], - "labels": ["None", "Home", "Away", "Sleep"], - "values": [0, 1, 2, 3], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83260991/preset_selected" - } - } - }, { - "type": "zone_mode", - "title": "Zone Mode", - "current_value": "OFF", - "options": [{ - "value": "AUTO", - "label": "Auto" - }, { - "value": "COOL", - "label": "Cooling" - }, { - "value": "HEAT", - "label": "Heating" - }, { - "value": "OFF", - "label": "Off" - }], - "labels": ["Auto", "Cooling", "Heating", "Off"], - "values": ["AUTO", "COOL", "HEAT", "OFF"], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83260991/zone_mode" - } - } - }, { - "type": "run_mode", - "title": "Run Mode", - "current_value": "permanent_hold", - "options": [{ - "value": "permanent_hold", - "label": "Permanent Hold" - }, { - "value": "run_schedule", - "label": "Run Schedule" - }], - "labels": ["Permanent Hold", "Run Schedule"], - "values": ["permanent_hold", "run_schedule"], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83260991/run_mode" - } - } - }, { - "type": "scheduling_enabled", - "title": "Scheduling", - "current_value": true, - "options": [{ - "value": true, - "label": "ON" - }, { - "value": false, - "label": "OFF" - }], - "labels": ["ON", "OFF"], - "values": [true, false], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83260991/scheduling_enabled" - } - } - }], - "icon": { - "name": "thermostat", - "modifiers": ["temperature-77"] - }, - "features": [{ - "name": "thermostat", - "temperature": 77, - "status": "", - "status_icon": null, - "actions": {}, - "setpoint_delta": 3, - "scale": "f", - "setpoint_increment": 1.0, - "setpoint_heat_min": 55, - "setpoint_heat_max": 90, - "setpoint_cool_min": 60, - "setpoint_cool_max": 99, - "system_status": "System Idle" - }, { - "name": "connection", - "signal_strength": "unknown", - "is_connected": true - }, { - "name": "thermostat_mode", - "label": "Zone Mode", - "value": "OFF", - "display_value": "Off", - "options": [{ - "id": "thermostat_mode", - "label": "Zone Mode", - "value": "thermostat_mode", - "header": true - }, { - "value": "AUTO", - "label": "Auto" - }, { - "value": "COOL", - "label": "Cooling" - }, { - "value": "HEAT", - "label": "Heating" - }, { - "value": "OFF", - "label": "Off" - }], - "actions": { - "update_thermostat_mode": { - "method": "POST", - "href": "https://www.mynexia.com/mobile/xxl_zones/83260991/zone_mode" - } - } - }, { - "name": "thermostat_run_mode", - "label": "Run Mode", - "options": [{ - "id": "thermostat_run_mode", - "label": "Run Mode", - "value": "thermostat_run_mode", - "header": true - }, { - "id": "info_text", - "label": "Follow or override the schedule.", - "value": "info_text", - "info": true - }, { - "value": "permanent_hold", - "label": "Permanent Hold" - }, { - "value": "run_schedule", - "label": "Run Schedule" - }], - "value": "permanent_hold", - "display_value": "Hold", - "actions": { - "update_thermostat_run_mode": { - "method": "POST", - "href": "https://www.mynexia.com/mobile/xxl_zones/83260991/run_mode" - } - } - }, { - "name": "schedule", - "enabled": true, - "max_period_name_length": 10, - "setpoint_increment": 1, - "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83260991\u0026house_id=123456", - "actions": { - "get_active_schedule": { - "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83260991", - "method": "POST" - }, - "set_active_schedule": { - "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83260991", - "method": "POST" - }, - "get_default_schedule": { - "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83260991", - "method": "GET" - }, - "enable_scheduling": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83260991/scheduling_enabled", - "method": "POST", - "data": { - "value": true - } - } - }, - "can_add_remove_periods": true, - "max_periods_per_day": 4 - }], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83260991" - } - } - }, { - "type": "xxl_zone", - "id": 83260994, - "name": "Mid Bedroom", - "current_zone_mode": "AUTO", - "temperature": 74, - "setpoints": { - "heat": 63, - "cool": 81 - }, - "operating_state": "", - "heating_setpoint": 63, - "cooling_setpoint": 81, - "zone_status": "", - "settings": [{ - "type": "preset_selected", - "title": "Preset", - "current_value": 0, - "options": [{ - "value": 0, - "label": "None" - }, { - "value": 1, - "label": "Home" - }, { - "value": 2, - "label": "Away" - }, { - "value": 3, - "label": "Sleep" - }], - "labels": ["None", "Home", "Away", "Sleep"], - "values": [0, 1, 2, 3], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83260994/preset_selected" - } - } - }, { - "type": "zone_mode", - "title": "Zone Mode", - "current_value": "AUTO", - "options": [{ - "value": "AUTO", - "label": "Auto" - }, { - "value": "COOL", - "label": "Cooling" - }, { - "value": "HEAT", - "label": "Heating" - }, { - "value": "OFF", - "label": "Off" - }], - "labels": ["Auto", "Cooling", "Heating", "Off"], - "values": ["AUTO", "COOL", "HEAT", "OFF"], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83260994/zone_mode" - } - } - }, { - "type": "run_mode", - "title": "Run Mode", - "current_value": "permanent_hold", - "options": [{ - "value": "permanent_hold", - "label": "Permanent Hold" - }, { - "value": "run_schedule", - "label": "Run Schedule" - }], - "labels": ["Permanent Hold", "Run Schedule"], - "values": ["permanent_hold", "run_schedule"], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83260994/run_mode" - } - } - }, { - "type": "scheduling_enabled", - "title": "Scheduling", - "current_value": true, - "options": [{ - "value": true, - "label": "ON" - }, { - "value": false, - "label": "OFF" - }], - "labels": ["ON", "OFF"], - "values": [true, false], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83260994/scheduling_enabled" - } - } - }], - "icon": { - "name": "thermostat", - "modifiers": ["temperature-74"] - }, - "features": [{ - "name": "thermostat", - "temperature": 74, - "status": "", - "status_icon": null, - "actions": { - "set_heat_setpoint": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83260994/setpoints" - }, - "set_cool_setpoint": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83260994/setpoints" - } - }, - "setpoint_delta": 3, - "scale": "f", - "setpoint_increment": 1.0, - "setpoint_heat_min": 55, - "setpoint_heat_max": 90, - "setpoint_cool_min": 60, - "setpoint_cool_max": 99, - "setpoint_heat": 63, - "setpoint_cool": 81, - "system_status": "System Idle" - }, { - "name": "connection", - "signal_strength": "unknown", - "is_connected": true - }, { - "name": "thermostat_mode", - "label": "Zone Mode", - "value": "AUTO", - "display_value": "Auto", - "options": [{ - "id": "thermostat_mode", - "label": "Zone Mode", - "value": "thermostat_mode", - "header": true - }, { - "value": "AUTO", - "label": "Auto" - }, { - "value": "COOL", - "label": "Cooling" - }, { - "value": "HEAT", - "label": "Heating" - }, { - "value": "OFF", - "label": "Off" - }], - "actions": { - "update_thermostat_mode": { - "method": "POST", - "href": "https://www.mynexia.com/mobile/xxl_zones/83260994/zone_mode" - } - } - }, { - "name": "thermostat_run_mode", - "label": "Run Mode", - "options": [{ - "id": "thermostat_run_mode", - "label": "Run Mode", - "value": "thermostat_run_mode", - "header": true - }, { - "id": "info_text", - "label": "Follow or override the schedule.", - "value": "info_text", - "info": true - }, { - "value": "permanent_hold", - "label": "Permanent Hold" - }, { - "value": "run_schedule", - "label": "Run Schedule" - }], - "value": "permanent_hold", - "display_value": "Hold", - "actions": { - "update_thermostat_run_mode": { - "method": "POST", - "href": "https://www.mynexia.com/mobile/xxl_zones/83260994/run_mode" - } - } - }, { - "name": "schedule", - "enabled": true, - "max_period_name_length": 10, - "setpoint_increment": 1, - "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83260994\u0026house_id=123456", - "actions": { - "get_active_schedule": { - "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83260994", - "method": "POST" - }, - "set_active_schedule": { - "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83260994", - "method": "POST" - }, - "get_default_schedule": { - "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83260994", - "method": "GET" - }, - "enable_scheduling": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83260994/scheduling_enabled", - "method": "POST", - "data": { - "value": true - } - } - }, - "can_add_remove_periods": true, - "max_periods_per_day": 4 - }], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83260994" - } - } - }, { - "type": "xxl_zone", - "id": 83260997, - "name": "West Bedroom", - "current_zone_mode": "AUTO", - "temperature": 75, - "setpoints": { - "heat": 63, - "cool": 81 - }, - "operating_state": "", - "heating_setpoint": 63, - "cooling_setpoint": 81, - "zone_status": "", - "settings": [{ - "type": "preset_selected", - "title": "Preset", - "current_value": 0, - "options": [{ - "value": 0, - "label": "None" - }, { - "value": 1, - "label": "Home" - }, { - "value": 2, - "label": "Away" - }, { - "value": 3, - "label": "Sleep" - }], - "labels": ["None", "Home", "Away", "Sleep"], - "values": [0, 1, 2, 3], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83260997/preset_selected" - } - } - }, { - "type": "zone_mode", - "title": "Zone Mode", - "current_value": "AUTO", - "options": [{ - "value": "AUTO", - "label": "Auto" - }, { - "value": "COOL", - "label": "Cooling" - }, { - "value": "HEAT", - "label": "Heating" - }, { - "value": "OFF", - "label": "Off" - }], - "labels": ["Auto", "Cooling", "Heating", "Off"], - "values": ["AUTO", "COOL", "HEAT", "OFF"], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83260997/zone_mode" - } - } - }, { - "type": "run_mode", - "title": "Run Mode", - "current_value": "permanent_hold", - "options": [{ - "value": "permanent_hold", - "label": "Permanent Hold" - }, { - "value": "run_schedule", - "label": "Run Schedule" - }], - "labels": ["Permanent Hold", "Run Schedule"], - "values": ["permanent_hold", "run_schedule"], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83260997/run_mode" - } - } - }, { - "type": "scheduling_enabled", - "title": "Scheduling", - "current_value": true, - "options": [{ - "value": true, - "label": "ON" - }, { - "value": false, - "label": "OFF" - }], - "labels": ["ON", "OFF"], - "values": [true, false], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83260997/scheduling_enabled" - } - } - }], - "icon": { - "name": "thermostat", - "modifiers": ["temperature-75"] - }, - "features": [{ - "name": "thermostat", - "temperature": 75, - "status": "", - "status_icon": null, - "actions": { - "set_heat_setpoint": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83260997/setpoints" - }, - "set_cool_setpoint": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83260997/setpoints" - } - }, - "setpoint_delta": 3, - "scale": "f", - "setpoint_increment": 1.0, - "setpoint_heat_min": 55, - "setpoint_heat_max": 90, - "setpoint_cool_min": 60, - "setpoint_cool_max": 99, - "setpoint_heat": 63, - "setpoint_cool": 81, - "system_status": "System Idle" - }, { - "name": "connection", - "signal_strength": "unknown", - "is_connected": true - }, { - "name": "thermostat_mode", - "label": "Zone Mode", - "value": "AUTO", - "display_value": "Auto", - "options": [{ - "id": "thermostat_mode", - "label": "Zone Mode", - "value": "thermostat_mode", - "header": true - }, { - "value": "AUTO", - "label": "Auto" - }, { - "value": "COOL", - "label": "Cooling" - }, { - "value": "HEAT", - "label": "Heating" - }, { - "value": "OFF", - "label": "Off" - }], - "actions": { - "update_thermostat_mode": { - "method": "POST", - "href": "https://www.mynexia.com/mobile/xxl_zones/83260997/zone_mode" - } - } - }, { - "name": "thermostat_run_mode", - "label": "Run Mode", - "options": [{ - "id": "thermostat_run_mode", - "label": "Run Mode", - "value": "thermostat_run_mode", - "header": true - }, { - "id": "info_text", - "label": "Follow or override the schedule.", - "value": "info_text", - "info": true - }, { - "value": "permanent_hold", - "label": "Permanent Hold" - }, { - "value": "run_schedule", - "label": "Run Schedule" - }], - "value": "permanent_hold", - "display_value": "Hold", - "actions": { - "update_thermostat_run_mode": { - "method": "POST", - "href": "https://www.mynexia.com/mobile/xxl_zones/83260997/run_mode" - } - } - }, { - "name": "schedule", - "enabled": true, - "max_period_name_length": 10, - "setpoint_increment": 1, - "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83260997\u0026house_id=123456", - "actions": { - "get_active_schedule": { - "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83260997", - "method": "POST" - }, - "set_active_schedule": { - "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83260997", - "method": "POST" - }, - "get_default_schedule": { - "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83260997", - "method": "GET" - }, - "enable_scheduling": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83260997/scheduling_enabled", - "method": "POST", - "data": { - "value": true - } - } - }, - "can_add_remove_periods": true, - "max_periods_per_day": 4 - }], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/xxl_zones/83260997" - } - } - }] - }], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/houses/123456/devices" - }, - "template": { - "data": { - "title": null, - "fields": [], - "_links": { - "child-schema": [{ - "data": { - "label": "Connect New Device", - "icon": { - "name": "new_device", - "modifiers": [] - }, - "_links": { - "next": { - "href": "https://www.mynexia.com/mobile/houses/123456/enrollables_schema" - } - } - } - }, { - "data": { - "label": "Create Group", - "icon": { - "name": "create_group", - "modifiers": [] - }, - "_links": { - "next": { - "href": "https://www.mynexia.com/mobile/houses/123456/groups/new" - } - } - } - }] - } - } - } - }, - "item_type": "application/vnd.nexia.device+json" - } - }, { - "href": "https://www.mynexia.com/mobile/houses/123456/automations", - "type": "application/vnd.nexia.collection+json", - "data": { - "items": [{ - "id": 3467876, - "name": "Away for 12 Hours", - "enabled": true, - "settings": [], - "triggers": [], - "description": "When IFTTT activates the automation Upstairs West Wing will permanently hold the heat to 62.0 and cool to 83.0 AND Downstairs East Wing will permanently hold the heat to 62.0 and cool to 83.0 AND Downstairs West Wing will permanently hold the heat to 62.0 and cool to 83.0 AND Activate the mode named 'Away 12' AND Master Suite will permanently hold the heat to 62.0 and cool to 83.0", - "icon": [{ - "name": "gears", - "modifiers": [] - }, { - "name": "climate", - "modifiers": [] - }, { - "name": "climate", - "modifiers": [] - }, { - "name": "climate", - "modifiers": [] - }, { - "name": "plane", - "modifiers": [] - }, { - "name": "climate", - "modifiers": [] - }], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/automations/3467876" - }, - "edit": { - "href": "https://www.mynexia.com/mobile/automation_edit_buffers?automation_id=3467876", - "method": "POST" - }, - "nexia:history": { - "href": "https://www.mynexia.com/mobile/houses/123456/events?automation_id=3467876" - }, - "filter_events": { - "href": "https://www.mynexia.com/mobile/houses/123456/events/collection?sys_guid=472ae0d2-5d7c-4a1c-9e47-4d9035fdace5" - } - } - }, { - "id": 3467870, - "name": "Away For 24 Hours", - "enabled": true, - "settings": [], - "triggers": [], - "description": "When IFTTT activates the automation Upstairs West Wing will permanently hold the heat to 60.0 and cool to 85.0 AND Downstairs East Wing will permanently hold the heat to 60.0 and cool to 85.0 AND Downstairs West Wing will permanently hold the heat to 60.0 and cool to 85.0 AND Activate the mode named 'Away 24' AND Master Suite will permanently hold the heat to 60.0 and cool to 85.0", - "icon": [{ - "name": "gears", - "modifiers": [] - }, { - "name": "climate", - "modifiers": [] - }, { - "name": "climate", - "modifiers": [] - }, { - "name": "climate", - "modifiers": [] - }, { - "name": "plane", - "modifiers": [] - }, { - "name": "climate", - "modifiers": [] - }], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/automations/3467870" - }, - "edit": { - "href": "https://www.mynexia.com/mobile/automation_edit_buffers?automation_id=3467870", - "method": "POST" - }, - "nexia:history": { - "href": "https://www.mynexia.com/mobile/houses/123456/events?automation_id=3467870" - }, - "filter_events": { - "href": "https://www.mynexia.com/mobile/houses/123456/events/collection?sys_guid=f63ee20c-3146-49a1-87c5-47429a063d15" - } - } - }, { - "id": 3452469, - "name": "Away Short", - "enabled": false, - "settings": [], - "triggers": [], - "description": "When IFTTT activates the automation Upstairs West Wing will permanently hold the heat to 63.0 and cool to 80.0 AND Downstairs East Wing will permanently hold the heat to 63.0 and cool to 79.0 AND Downstairs West Wing will permanently hold the heat to 63.0 and cool to 79.0 AND Upstairs West Wing will permanently hold the heat to 63.0 and cool to 81.0 AND Upstairs West Wing will change Fan Mode to Auto AND Downstairs East Wing will change Fan Mode to Auto AND Downstairs West Wing will change Fan Mode to Auto AND Activate the mode named 'Away Short' AND Master Suite will permanently hold the heat to 63.0 and cool to 79.0 AND Master Suite will change Fan Mode to Auto", - "icon": [{ - "name": "gears", - "modifiers": [] - }, { - "name": "climate", - "modifiers": [] - }, { - "name": "climate", - "modifiers": [] - }, { - "name": "climate", - "modifiers": [] - }, { - "name": "climate", - "modifiers": [] - }, { - "name": "settings", - "modifiers": [] - }, { - "name": "settings", - "modifiers": [] - }, { - "name": "settings", - "modifiers": [] - }, { - "name": "key", - "modifiers": [] - }, { - "name": "climate", - "modifiers": [] - }, { - "name": "settings", - "modifiers": [] - }], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/automations/3452469" - }, - "edit": { - "href": "https://www.mynexia.com/mobile/automation_edit_buffers?automation_id=3452469", - "method": "POST" - }, - "nexia:history": { - "href": "https://www.mynexia.com/mobile/houses/123456/events?automation_id=3452469" - }, - "filter_events": { - "href": "https://www.mynexia.com/mobile/houses/123456/events/collection?sys_guid=e5c59b93-efca-4937-9499-3f4c896ab17c" - } - } - }, { - "id": 3452472, - "name": "Home", - "enabled": true, - "settings": [], - "triggers": [], - "description": "When IFTTT activates the automation Upstairs West Wing will Run Schedule AND Downstairs East Wing will Run Schedule AND Downstairs West Wing will Run Schedule AND Activate the mode named 'Home' AND Master Suite will Run Schedule", - "icon": [{ - "name": "gears", - "modifiers": [] - }, { - "name": "settings", - "modifiers": [] - }, { - "name": "settings", - "modifiers": [] - }, { - "name": "settings", - "modifiers": [] - }, { - "name": "at_home", - "modifiers": [] - }, { - "name": "settings", - "modifiers": [] - }], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/automations/3452472" - }, - "edit": { - "href": "https://www.mynexia.com/mobile/automation_edit_buffers?automation_id=3452472", - "method": "POST" - }, - "nexia:history": { - "href": "https://www.mynexia.com/mobile/houses/123456/events?automation_id=3452472" - }, - "filter_events": { - "href": "https://www.mynexia.com/mobile/houses/123456/events/collection?sys_guid=861b9fec-d259-4492-a798-5712251666c4" - } - } - }, { - "id": 3454776, - "name": "IFTTT Power Spike", - "enabled": true, - "settings": [], - "triggers": [], - "description": "When IFTTT activates the automation Upstairs West Wing will permanently hold the heat to 60.0 and cool to 85.0 AND Downstairs East Wing will permanently hold the heat to 60.0 and cool to 85.0 AND Downstairs West Wing will permanently hold the heat to 60.0 and cool to 85.0 AND Upstairs West Wing will change Fan Mode to Auto AND Downstairs East Wing will change Fan Mode to Auto AND Downstairs West Wing will change Fan Mode to Auto AND Master Suite will permanently hold the heat to 60.0 and cool to 85.0 AND Master Suite will change Fan Mode to Auto", - "icon": [{ - "name": "gears", - "modifiers": [] - }, { - "name": "climate", - "modifiers": [] - }, { - "name": "climate", - "modifiers": [] - }, { - "name": "climate", - "modifiers": [] - }, { - "name": "settings", - "modifiers": [] - }, { - "name": "settings", - "modifiers": [] - }, { - "name": "settings", - "modifiers": [] - }, { - "name": "climate", - "modifiers": [] - }, { - "name": "settings", - "modifiers": [] - }], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/automations/3454776" - }, - "edit": { - "href": "https://www.mynexia.com/mobile/automation_edit_buffers?automation_id=3454776", - "method": "POST" - }, - "nexia:history": { - "href": "https://www.mynexia.com/mobile/houses/123456/events?automation_id=3454776" - }, - "filter_events": { - "href": "https://www.mynexia.com/mobile/houses/123456/events/collection?sys_guid=96c71d37-66aa-4cbb-84ff-a90412fd366a" - } - } - }, { - "id": 3454774, - "name": "IFTTT return to schedule", - "enabled": false, - "settings": [], - "triggers": [], - "description": "When IFTTT activates the automation Upstairs West Wing will Run Schedule AND Downstairs East Wing will Run Schedule AND Downstairs West Wing will Run Schedule AND Master Suite will Run Schedule", - "icon": [{ - "name": "gears", - "modifiers": [] - }, { - "name": "settings", - "modifiers": [] - }, { - "name": "settings", - "modifiers": [] - }, { - "name": "settings", - "modifiers": [] - }, { - "name": "settings", - "modifiers": [] - }], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/automations/3454774" - }, - "edit": { - "href": "https://www.mynexia.com/mobile/automation_edit_buffers?automation_id=3454774", - "method": "POST" - }, - "nexia:history": { - "href": "https://www.mynexia.com/mobile/houses/123456/events?automation_id=3454774" - }, - "filter_events": { - "href": "https://www.mynexia.com/mobile/houses/123456/events/collection?sys_guid=880c5287-d92c-4368-8494-e10975e92733" - } - } - }, { - "id": 3486078, - "name": "Power Outage", - "enabled": true, - "settings": [], - "triggers": [], - "description": "When IFTTT activates the automation Upstairs West Wing will permanently hold the heat to 55.0 and cool to 90.0 AND Downstairs East Wing will permanently hold the heat to 55.0 and cool to 90.0 AND Downstairs West Wing will permanently hold the heat to 55.0 and cool to 90.0 AND Activate the mode named 'Power Outage'", - "icon": [{ - "name": "gears", - "modifiers": [] - }, { - "name": "climate", - "modifiers": [] - }, { - "name": "climate", - "modifiers": [] - }, { - "name": "climate", - "modifiers": [] - }, { - "name": "bell", - "modifiers": [] - }], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/automations/3486078" - }, - "edit": { - "href": "https://www.mynexia.com/mobile/automation_edit_buffers?automation_id=3486078", - "method": "POST" - }, - "nexia:history": { - "href": "https://www.mynexia.com/mobile/houses/123456/events?automation_id=3486078" - }, - "filter_events": { - "href": "https://www.mynexia.com/mobile/houses/123456/events/collection?sys_guid=d33c013b-2357-47a9-8c66-d2c3693173b0" - } - } - }, { - "id": 3486091, - "name": "Power Restored", - "enabled": true, - "settings": [], - "triggers": [], - "description": "When IFTTT activates the automation Upstairs West Wing will Run Schedule AND Downstairs East Wing will Run Schedule AND Downstairs West Wing will Run Schedule AND Activate the mode named 'Home'", - "icon": [{ - "name": "gears", - "modifiers": [] - }, { - "name": "settings", - "modifiers": [] - }, { - "name": "settings", - "modifiers": [] - }, { - "name": "settings", - "modifiers": [] - }, { - "name": "at_home", - "modifiers": [] - }], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/automations/3486091" - }, - "edit": { - "href": "https://www.mynexia.com/mobile/automation_edit_buffers?automation_id=3486091", - "method": "POST" - }, - "nexia:history": { - "href": "https://www.mynexia.com/mobile/houses/123456/events?automation_id=3486091" - }, - "filter_events": { - "href": "https://www.mynexia.com/mobile/houses/123456/events/collection?sys_guid=b9141df8-2e5e-4524-b8ef-efcbf48d775a" - } - } - }], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/houses/123456/automations" - }, - "template": { - "href": "https://www.mynexia.com/mobile/houses/123456/automation_edit_buffers", - "method": "POST" - } - }, - "item_type": "application/vnd.nexia.automation+json" - } - }, { - "href": "https://www.mynexia.com/mobile/houses/123456/modes", - "type": "application/vnd.nexia.collection+json", - "data": { - "items": [{ - "id": 3047801, - "name": "Home", - "current_mode": false, - "icon": "home.png", - "settings": [], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/modes/3047801" - } - } - }, { - "id": 3174574, - "name": "Away Short", - "current_mode": true, - "icon": "key.png", - "settings": [], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/modes/3174574" - } - } - }, { - "id": 3174576, - "name": "Away 12", - "current_mode": false, - "icon": "picture.png", - "settings": [], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/modes/3174576" - } - } - }, { - "id": 3174577, - "name": "Away 24", - "current_mode": false, - "icon": "picture.png", - "settings": [], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/modes/3174577" - } - } - }, { - "id": 3197871, - "name": "Power Outage", - "current_mode": false, - "icon": "bell.png", - "settings": [], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/modes/3197871" - } - } - }], - "_links": { - "self": { - "href": "https://www.mynexia.com/mobile/houses/123456/modes" - } - }, - "item_type": "application/vnd.nexia.mode+json" - } - }, { - "href": "https://www.mynexia.com/mobile/houses/123456/events/collection", - "type": "application/vnd.nexia.collection+json", - "data": { - "item_type": "application/vnd.nexia.event+json" - } - }, { - "href": "https://www.mynexia.com/mobile/houses/123456/videos/collection", - "type": "application/vnd.nexia.collection+json", - "data": { - "item_type": "application/vnd.nexia.video+json" - } - }] + "success": true, + "error": null, + "result": { + "id": 123456, + "name": "Hidden", + "third_party_integrations": [], + "latitude": 12.7633, + "longitude": -12.3633, + "dealer_opt_in": true, + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/houses/123456" + }, + "edit": [ + { + "href": "https://www.mynexia.com/mobile/houses/123456/edit", + "method": "GET" } + ], + "child": [ + { + "href": "https://www.mynexia.com/mobile/houses/123456/devices", + "type": "application/vnd.nexia.collection+json", + "data": { + "items": [ + { + "id": 2059661, + "name": "Downstairs East Wing", + "name_editable": true, + "features": [ + { + "name": "advanced_info", + "items": [ + { + "type": "label_value", + "label": "Model", + "value": "XL1050" + }, + { + "type": "label_value", + "label": "AUID", + "value": "000000" + }, + { + "type": "label_value", + "label": "Firmware Build Number", + "value": "1581321824" + }, + { + "type": "label_value", + "label": "Firmware Build Date", + "value": "2020-02-10 08:03:44 UTC" + }, + { + "type": "label_value", + "label": "Firmware Version", + "value": "5.9.1" + }, + { + "type": "label_value", + "label": "Zoning Enabled", + "value": "yes" + } + ] + }, + { + "name": "thermostat", + "temperature": 71, + "status": "System Idle", + "status_icon": null, + "actions": {}, + "setpoint_delta": 3, + "scale": "f", + "setpoint_increment": 1.0, + "setpoint_heat_min": 55, + "setpoint_heat_max": 90, + "setpoint_cool_min": 60, + "setpoint_cool_max": 99 + }, + { + "name": "connection", + "signal_strength": "unknown", + "is_connected": true + }, + { + "name": "group", + "members": [ + { + "type": "xxl_zone", + "id": 83261002, + "name": "Living East", + "current_zone_mode": "AUTO", + "temperature": 71, + "setpoints": { + "heat": 63, + "cool": 79 + }, + "operating_state": "", + "heating_setpoint": 63, + "cooling_setpoint": 79, + "zone_status": "", + "settings": [ + { + "type": "preset_selected", + "title": "Preset", + "current_value": 0, + "options": [ + { + "value": 0, + "label": "None" + }, + { + "value": 1, + "label": "Home" + }, + { + "value": 2, + "label": "Away" + }, + { + "value": 3, + "label": "Sleep" + } + ], + "labels": ["None", "Home", "Away", "Sleep"], + "values": [0, 1, 2, 3], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261002/preset_selected" + } + } + }, + { + "type": "zone_mode", + "title": "Zone Mode", + "current_value": "AUTO", + "options": [ + { + "value": "AUTO", + "label": "Auto" + }, + { + "value": "COOL", + "label": "Cooling" + }, + { + "value": "HEAT", + "label": "Heating" + }, + { + "value": "OFF", + "label": "Off" + } + ], + "labels": ["Auto", "Cooling", "Heating", "Off"], + "values": ["AUTO", "COOL", "HEAT", "OFF"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261002/zone_mode" + } + } + }, + { + "type": "run_mode", + "title": "Run Mode", + "current_value": "permanent_hold", + "options": [ + { + "value": "permanent_hold", + "label": "Permanent Hold" + }, + { + "value": "run_schedule", + "label": "Run Schedule" + } + ], + "labels": ["Permanent Hold", "Run Schedule"], + "values": ["permanent_hold", "run_schedule"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261002/run_mode" + } + } + }, + { + "type": "scheduling_enabled", + "title": "Scheduling", + "current_value": true, + "options": [ + { + "value": true, + "label": "ON" + }, + { + "value": false, + "label": "OFF" + } + ], + "labels": ["ON", "OFF"], + "values": [true, false], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261002/scheduling_enabled" + } + } + } + ], + "icon": { + "name": "thermostat", + "modifiers": ["temperature-71"] + }, + "features": [ + { + "name": "thermostat", + "temperature": 71, + "status": "", + "status_icon": null, + "actions": { + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261002/setpoints" + }, + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261002/setpoints" + } + }, + "setpoint_delta": 3, + "scale": "f", + "setpoint_increment": 1.0, + "setpoint_heat_min": 55, + "setpoint_heat_max": 90, + "setpoint_cool_min": 60, + "setpoint_cool_max": 99, + "setpoint_heat": 63, + "setpoint_cool": 79, + "system_status": "System Idle" + }, + { + "name": "connection", + "signal_strength": "unknown", + "is_connected": true + }, + { + "name": "thermostat_mode", + "label": "Zone Mode", + "value": "AUTO", + "display_value": "Auto", + "options": [ + { + "id": "thermostat_mode", + "label": "Zone Mode", + "value": "thermostat_mode", + "header": true + }, + { + "value": "AUTO", + "label": "Auto" + }, + { + "value": "COOL", + "label": "Cooling" + }, + { + "value": "HEAT", + "label": "Heating" + }, + { + "value": "OFF", + "label": "Off" + } + ], + "actions": { + "update_thermostat_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83261002/zone_mode" + } + } + }, + { + "name": "thermostat_run_mode", + "label": "Run Mode", + "options": [ + { + "id": "thermostat_run_mode", + "label": "Run Mode", + "value": "thermostat_run_mode", + "header": true + }, + { + "id": "info_text", + "label": "Follow or override the schedule.", + "value": "info_text", + "info": true + }, + { + "value": "permanent_hold", + "label": "Permanent Hold" + }, + { + "value": "run_schedule", + "label": "Run Schedule" + } + ], + "value": "permanent_hold", + "display_value": "Hold", + "actions": { + "update_thermostat_run_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83261002/run_mode" + } + } + }, + { + "name": "schedule", + "enabled": true, + "max_period_name_length": 10, + "setpoint_increment": 1, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83261002\u0026house_id=123456", + "actions": { + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83261002", + "method": "POST" + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83261002", + "method": "POST" + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83261002", + "method": "GET" + }, + "enable_scheduling": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261002/scheduling_enabled", + "method": "POST", + "data": { + "value": true + } + } + }, + "can_add_remove_periods": true, + "max_periods_per_day": 4 + } + ], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261002" + } + } + }, + { + "type": "xxl_zone", + "id": 83261005, + "name": "Kitchen", + "current_zone_mode": "AUTO", + "temperature": 77, + "setpoints": { + "heat": 63, + "cool": 79 + }, + "operating_state": "", + "heating_setpoint": 63, + "cooling_setpoint": 79, + "zone_status": "", + "settings": [ + { + "type": "preset_selected", + "title": "Preset", + "current_value": 0, + "options": [ + { + "value": 0, + "label": "None" + }, + { + "value": 1, + "label": "Home" + }, + { + "value": 2, + "label": "Away" + }, + { + "value": 3, + "label": "Sleep" + } + ], + "labels": ["None", "Home", "Away", "Sleep"], + "values": [0, 1, 2, 3], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261005/preset_selected" + } + } + }, + { + "type": "zone_mode", + "title": "Zone Mode", + "current_value": "AUTO", + "options": [ + { + "value": "AUTO", + "label": "Auto" + }, + { + "value": "COOL", + "label": "Cooling" + }, + { + "value": "HEAT", + "label": "Heating" + }, + { + "value": "OFF", + "label": "Off" + } + ], + "labels": ["Auto", "Cooling", "Heating", "Off"], + "values": ["AUTO", "COOL", "HEAT", "OFF"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261005/zone_mode" + } + } + }, + { + "type": "run_mode", + "title": "Run Mode", + "current_value": "permanent_hold", + "options": [ + { + "value": "permanent_hold", + "label": "Permanent Hold" + }, + { + "value": "run_schedule", + "label": "Run Schedule" + } + ], + "labels": ["Permanent Hold", "Run Schedule"], + "values": ["permanent_hold", "run_schedule"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261005/run_mode" + } + } + }, + { + "type": "scheduling_enabled", + "title": "Scheduling", + "current_value": true, + "options": [ + { + "value": true, + "label": "ON" + }, + { + "value": false, + "label": "OFF" + } + ], + "labels": ["ON", "OFF"], + "values": [true, false], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261005/scheduling_enabled" + } + } + } + ], + "icon": { + "name": "thermostat", + "modifiers": ["temperature-77"] + }, + "features": [ + { + "name": "thermostat", + "temperature": 77, + "status": "", + "status_icon": null, + "actions": { + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261005/setpoints" + }, + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261005/setpoints" + } + }, + "setpoint_delta": 3, + "scale": "f", + "setpoint_increment": 1.0, + "setpoint_heat_min": 55, + "setpoint_heat_max": 90, + "setpoint_cool_min": 60, + "setpoint_cool_max": 99, + "setpoint_heat": 63, + "setpoint_cool": 79, + "system_status": "System Idle" + }, + { + "name": "connection", + "signal_strength": "unknown", + "is_connected": true + }, + { + "name": "thermostat_mode", + "label": "Zone Mode", + "value": "AUTO", + "display_value": "Auto", + "options": [ + { + "id": "thermostat_mode", + "label": "Zone Mode", + "value": "thermostat_mode", + "header": true + }, + { + "value": "AUTO", + "label": "Auto" + }, + { + "value": "COOL", + "label": "Cooling" + }, + { + "value": "HEAT", + "label": "Heating" + }, + { + "value": "OFF", + "label": "Off" + } + ], + "actions": { + "update_thermostat_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83261005/zone_mode" + } + } + }, + { + "name": "thermostat_run_mode", + "label": "Run Mode", + "options": [ + { + "id": "thermostat_run_mode", + "label": "Run Mode", + "value": "thermostat_run_mode", + "header": true + }, + { + "id": "info_text", + "label": "Follow or override the schedule.", + "value": "info_text", + "info": true + }, + { + "value": "permanent_hold", + "label": "Permanent Hold" + }, + { + "value": "run_schedule", + "label": "Run Schedule" + } + ], + "value": "permanent_hold", + "display_value": "Hold", + "actions": { + "update_thermostat_run_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83261005/run_mode" + } + } + }, + { + "name": "schedule", + "enabled": true, + "max_period_name_length": 10, + "setpoint_increment": 1, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83261005\u0026house_id=123456", + "actions": { + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83261005", + "method": "POST" + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83261005", + "method": "POST" + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83261005", + "method": "GET" + }, + "enable_scheduling": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261005/scheduling_enabled", + "method": "POST", + "data": { + "value": true + } + } + }, + "can_add_remove_periods": true, + "max_periods_per_day": 4 + } + ], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261005" + } + } + }, + { + "type": "xxl_zone", + "id": 83261008, + "name": "Down Bedroom", + "current_zone_mode": "AUTO", + "temperature": 72, + "setpoints": { + "heat": 63, + "cool": 79 + }, + "operating_state": "", + "heating_setpoint": 63, + "cooling_setpoint": 79, + "zone_status": "", + "settings": [ + { + "type": "preset_selected", + "title": "Preset", + "current_value": 0, + "options": [ + { + "value": 0, + "label": "None" + }, + { + "value": 1, + "label": "Home" + }, + { + "value": 2, + "label": "Away" + }, + { + "value": 3, + "label": "Sleep" + } + ], + "labels": ["None", "Home", "Away", "Sleep"], + "values": [0, 1, 2, 3], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261008/preset_selected" + } + } + }, + { + "type": "zone_mode", + "title": "Zone Mode", + "current_value": "AUTO", + "options": [ + { + "value": "AUTO", + "label": "Auto" + }, + { + "value": "COOL", + "label": "Cooling" + }, + { + "value": "HEAT", + "label": "Heating" + }, + { + "value": "OFF", + "label": "Off" + } + ], + "labels": ["Auto", "Cooling", "Heating", "Off"], + "values": ["AUTO", "COOL", "HEAT", "OFF"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261008/zone_mode" + } + } + }, + { + "type": "run_mode", + "title": "Run Mode", + "current_value": "permanent_hold", + "options": [ + { + "value": "permanent_hold", + "label": "Permanent Hold" + }, + { + "value": "run_schedule", + "label": "Run Schedule" + } + ], + "labels": ["Permanent Hold", "Run Schedule"], + "values": ["permanent_hold", "run_schedule"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261008/run_mode" + } + } + }, + { + "type": "scheduling_enabled", + "title": "Scheduling", + "current_value": true, + "options": [ + { + "value": true, + "label": "ON" + }, + { + "value": false, + "label": "OFF" + } + ], + "labels": ["ON", "OFF"], + "values": [true, false], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261008/scheduling_enabled" + } + } + } + ], + "icon": { + "name": "thermostat", + "modifiers": ["temperature-72"] + }, + "features": [ + { + "name": "thermostat", + "temperature": 72, + "status": "", + "status_icon": null, + "actions": { + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261008/setpoints" + }, + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261008/setpoints" + } + }, + "setpoint_delta": 3, + "scale": "f", + "setpoint_increment": 1.0, + "setpoint_heat_min": 55, + "setpoint_heat_max": 90, + "setpoint_cool_min": 60, + "setpoint_cool_max": 99, + "setpoint_heat": 63, + "setpoint_cool": 79, + "system_status": "System Idle" + }, + { + "name": "connection", + "signal_strength": "unknown", + "is_connected": true + }, + { + "name": "thermostat_mode", + "label": "Zone Mode", + "value": "AUTO", + "display_value": "Auto", + "options": [ + { + "id": "thermostat_mode", + "label": "Zone Mode", + "value": "thermostat_mode", + "header": true + }, + { + "value": "AUTO", + "label": "Auto" + }, + { + "value": "COOL", + "label": "Cooling" + }, + { + "value": "HEAT", + "label": "Heating" + }, + { + "value": "OFF", + "label": "Off" + } + ], + "actions": { + "update_thermostat_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83261008/zone_mode" + } + } + }, + { + "name": "thermostat_run_mode", + "label": "Run Mode", + "options": [ + { + "id": "thermostat_run_mode", + "label": "Run Mode", + "value": "thermostat_run_mode", + "header": true + }, + { + "id": "info_text", + "label": "Follow or override the schedule.", + "value": "info_text", + "info": true + }, + { + "value": "permanent_hold", + "label": "Permanent Hold" + }, + { + "value": "run_schedule", + "label": "Run Schedule" + } + ], + "value": "permanent_hold", + "display_value": "Hold", + "actions": { + "update_thermostat_run_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83261008/run_mode" + } + } + }, + { + "name": "schedule", + "enabled": true, + "max_period_name_length": 10, + "setpoint_increment": 1, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83261008\u0026house_id=123456", + "actions": { + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83261008", + "method": "POST" + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83261008", + "method": "POST" + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83261008", + "method": "GET" + }, + "enable_scheduling": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261008/scheduling_enabled", + "method": "POST", + "data": { + "value": true + } + } + }, + "can_add_remove_periods": true, + "max_periods_per_day": 4 + } + ], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261008" + } + } + }, + { + "type": "xxl_zone", + "id": 83261011, + "name": "Tech Room", + "current_zone_mode": "AUTO", + "temperature": 78, + "setpoints": { + "heat": 63, + "cool": 79 + }, + "operating_state": "", + "heating_setpoint": 63, + "cooling_setpoint": 79, + "zone_status": "", + "settings": [ + { + "type": "preset_selected", + "title": "Preset", + "current_value": 0, + "options": [ + { + "value": 0, + "label": "None" + }, + { + "value": 1, + "label": "Home" + }, + { + "value": 2, + "label": "Away" + }, + { + "value": 3, + "label": "Sleep" + } + ], + "labels": ["None", "Home", "Away", "Sleep"], + "values": [0, 1, 2, 3], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261011/preset_selected" + } + } + }, + { + "type": "zone_mode", + "title": "Zone Mode", + "current_value": "AUTO", + "options": [ + { + "value": "AUTO", + "label": "Auto" + }, + { + "value": "COOL", + "label": "Cooling" + }, + { + "value": "HEAT", + "label": "Heating" + }, + { + "value": "OFF", + "label": "Off" + } + ], + "labels": ["Auto", "Cooling", "Heating", "Off"], + "values": ["AUTO", "COOL", "HEAT", "OFF"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261011/zone_mode" + } + } + }, + { + "type": "run_mode", + "title": "Run Mode", + "current_value": "permanent_hold", + "options": [ + { + "value": "permanent_hold", + "label": "Permanent Hold" + }, + { + "value": "run_schedule", + "label": "Run Schedule" + } + ], + "labels": ["Permanent Hold", "Run Schedule"], + "values": ["permanent_hold", "run_schedule"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261011/run_mode" + } + } + }, + { + "type": "scheduling_enabled", + "title": "Scheduling", + "current_value": true, + "options": [ + { + "value": true, + "label": "ON" + }, + { + "value": false, + "label": "OFF" + } + ], + "labels": ["ON", "OFF"], + "values": [true, false], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261011/scheduling_enabled" + } + } + } + ], + "icon": { + "name": "thermostat", + "modifiers": ["temperature-78"] + }, + "features": [ + { + "name": "thermostat", + "temperature": 78, + "status": "", + "status_icon": null, + "actions": { + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261011/setpoints" + }, + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261011/setpoints" + } + }, + "setpoint_delta": 3, + "scale": "f", + "setpoint_increment": 1.0, + "setpoint_heat_min": 55, + "setpoint_heat_max": 90, + "setpoint_cool_min": 60, + "setpoint_cool_max": 99, + "setpoint_heat": 63, + "setpoint_cool": 79, + "system_status": "System Idle" + }, + { + "name": "connection", + "signal_strength": "unknown", + "is_connected": true + }, + { + "name": "thermostat_mode", + "label": "Zone Mode", + "value": "AUTO", + "display_value": "Auto", + "options": [ + { + "id": "thermostat_mode", + "label": "Zone Mode", + "value": "thermostat_mode", + "header": true + }, + { + "value": "AUTO", + "label": "Auto" + }, + { + "value": "COOL", + "label": "Cooling" + }, + { + "value": "HEAT", + "label": "Heating" + }, + { + "value": "OFF", + "label": "Off" + } + ], + "actions": { + "update_thermostat_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83261011/zone_mode" + } + } + }, + { + "name": "thermostat_run_mode", + "label": "Run Mode", + "options": [ + { + "id": "thermostat_run_mode", + "label": "Run Mode", + "value": "thermostat_run_mode", + "header": true + }, + { + "id": "info_text", + "label": "Follow or override the schedule.", + "value": "info_text", + "info": true + }, + { + "value": "permanent_hold", + "label": "Permanent Hold" + }, + { + "value": "run_schedule", + "label": "Run Schedule" + } + ], + "value": "permanent_hold", + "display_value": "Hold", + "actions": { + "update_thermostat_run_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83261011/run_mode" + } + } + }, + { + "name": "schedule", + "enabled": true, + "max_period_name_length": 10, + "setpoint_increment": 1, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83261011\u0026house_id=123456", + "actions": { + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83261011", + "method": "POST" + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83261011", + "method": "POST" + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83261011", + "method": "GET" + }, + "enable_scheduling": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261011/scheduling_enabled", + "method": "POST", + "data": { + "value": true + } + } + }, + "can_add_remove_periods": true, + "max_periods_per_day": 4 + } + ], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261011" + } + } + } + ] + }, + { + "name": "thermostat_fan_mode", + "label": "Fan Mode", + "options": [ + { + "id": "thermostat_fan_mode", + "label": "Fan Mode", + "value": "thermostat_fan_mode", + "header": true + }, + { + "value": "auto", + "label": "Auto" + }, + { + "value": "on", + "label": "On" + }, + { + "value": "circulate", + "label": "Circulate" + } + ], + "value": "auto", + "display_value": "Auto", + "status_icon": { + "name": "thermostat_fan_off", + "modifiers": [] + }, + "actions": { + "update_thermostat_fan_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059661/fan_mode" + } + } + }, + { + "name": "thermostat_compressor_speed", + "compressor_speed": 0.0 + }, + { + "name": "runtime_history", + "actions": { + "get_runtime_history": { + "method": "GET", + "href": "https://www.mynexia.com/mobile/runtime_history/2059661?report_type=daily" + }, + "get_monthly_runtime_history": { + "method": "GET", + "href": "https://www.mynexia.com/mobile/runtime_history/2059661?report_type=monthly" + } + } + } + ], + "icon": [ + { + "name": "thermostat", + "modifiers": ["temperature-71"] + }, + { + "name": "thermostat", + "modifiers": ["temperature-77"] + }, + { + "name": "thermostat", + "modifiers": ["temperature-72"] + }, + { + "name": "thermostat", + "modifiers": ["temperature-78"] + } + ], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059661" + }, + "nexia:history": { + "href": "https://www.mynexia.com/mobile/houses/123456/events?device_id=2059661" + }, + "filter_events": { + "href": "https://www.mynexia.com/mobile/houses/123456/events/collection?sys_guid=cd9a70e8-fd0d-4b58-b071-05a202fd8953" + }, + "pending_request": { + "polling_path": "https://www.mynexia.com/backstage/announcements/be6d8ede5cac02fe8be18c334b04d539c9200fa9230eef63" + } + }, + "last_updated_at": "2020-03-11T15:15:53.000-05:00", + "settings": [ + { + "type": "fan_mode", + "title": "Fan Mode", + "current_value": "auto", + "options": [ + { + "value": "auto", + "label": "Auto" + }, + { + "value": "on", + "label": "On" + }, + { + "value": "circulate", + "label": "Circulate" + } + ], + "labels": ["Auto", "On", "Circulate"], + "values": ["auto", "on", "circulate"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059661/fan_mode" + } + } + }, + { + "type": "fan_speed", + "title": "Fan Speed", + "current_value": 0.35, + "options": [ + { + "value": 0.35, + "label": "35%" + }, + { + "value": 0.4, + "label": "40%" + }, + { + "value": 0.45, + "label": "45%" + }, + { + "value": 0.5, + "label": "50%" + }, + { + "value": 0.55, + "label": "55%" + }, + { + "value": 0.6, + "label": "60%" + }, + { + "value": 0.65, + "label": "65%" + }, + { + "value": 0.7, + "label": "70%" + }, + { + "value": 0.75, + "label": "75%" + }, + { + "value": 0.8, + "label": "80%" + }, + { + "value": 0.85, + "label": "85%" + }, + { + "value": 0.9, + "label": "90%" + }, + { + "value": 0.95, + "label": "95%" + }, + { + "value": 1.0, + "label": "100%" + } + ], + "labels": [ + "35%", + "40%", + "45%", + "50%", + "55%", + "60%", + "65%", + "70%", + "75%", + "80%", + "85%", + "90%", + "95%", + "100%" + ], + "values": [ + 0.35, 0.4, 0.45, 0.5, 0.55, 0.6, 0.65, 0.7, 0.75, 0.8, + 0.85, 0.9, 0.95, 1.0 + ], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059661/fan_speed" + } + } + }, + { + "type": "fan_circulation_time", + "title": "Fan Circulation Time", + "current_value": 30, + "options": [ + { + "value": 10, + "label": "10 minutes" + }, + { + "value": 15, + "label": "15 minutes" + }, + { + "value": 20, + "label": "20 minutes" + }, + { + "value": 25, + "label": "25 minutes" + }, + { + "value": 30, + "label": "30 minutes" + }, + { + "value": 35, + "label": "35 minutes" + }, + { + "value": 40, + "label": "40 minutes" + }, + { + "value": 45, + "label": "45 minutes" + }, + { + "value": 50, + "label": "50 minutes" + }, + { + "value": 55, + "label": "55 minutes" + } + ], + "labels": [ + "10 minutes", + "15 minutes", + "20 minutes", + "25 minutes", + "30 minutes", + "35 minutes", + "40 minutes", + "45 minutes", + "50 minutes", + "55 minutes" + ], + "values": [10, 15, 20, 25, 30, 35, 40, 45, 50, 55], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059661/fan_circulation_time" + } + } + }, + { + "type": "air_cleaner_mode", + "title": "Air Cleaner Mode", + "current_value": "auto", + "options": [ + { + "value": "auto", + "label": "Auto" + }, + { + "value": "quick", + "label": "Quick" + }, + { + "value": "allergy", + "label": "Allergy" + } + ], + "labels": ["Auto", "Quick", "Allergy"], + "values": ["auto", "quick", "allergy"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059661/air_cleaner_mode" + } + } + }, + { + "type": "dehumidify", + "title": "Cooling Dehumidify Set Point", + "current_value": 0.5, + "options": [ + { + "value": 0.35, + "label": "35%" + }, + { + "value": 0.4, + "label": "40%" + }, + { + "value": 0.45, + "label": "45%" + }, + { + "value": 0.5, + "label": "50%" + }, + { + "value": 0.55, + "label": "55%" + }, + { + "value": 0.6, + "label": "60%" + }, + { + "value": 0.65, + "label": "65%" + } + ], + "labels": ["35%", "40%", "45%", "50%", "55%", "60%", "65%"], + "values": [0.35, 0.4, 0.45, 0.5, 0.55, 0.6, 0.65], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059661/dehumidify" + } + } + }, + { + "type": "scale", + "title": "Temperature Scale", + "current_value": "f", + "options": [ + { + "value": "f", + "label": "F" + }, + { + "value": "c", + "label": "C" + } + ], + "labels": ["F", "C"], + "values": ["f", "c"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059661/scale" + } + } + } + ], + "status_secondary": null, + "status_tertiary": null, + "type": "xxl_thermostat", + "has_outdoor_temperature": true, + "outdoor_temperature": "88", + "has_indoor_humidity": true, + "connected": true, + "indoor_humidity": "36", + "system_status": "System Idle", + "delta": 3, + "zones": [ + { + "type": "xxl_zone", + "id": 83261002, + "name": "Living East", + "current_zone_mode": "AUTO", + "temperature": 71, + "setpoints": { + "heat": 63, + "cool": 79 + }, + "operating_state": "", + "heating_setpoint": 63, + "cooling_setpoint": 79, + "zone_status": "", + "settings": [ + { + "type": "preset_selected", + "title": "Preset", + "current_value": 0, + "options": [ + { + "value": 0, + "label": "None" + }, + { + "value": 1, + "label": "Home" + }, + { + "value": 2, + "label": "Away" + }, + { + "value": 3, + "label": "Sleep" + } + ], + "labels": ["None", "Home", "Away", "Sleep"], + "values": [0, 1, 2, 3], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261002/preset_selected" + } + } + }, + { + "type": "zone_mode", + "title": "Zone Mode", + "current_value": "AUTO", + "options": [ + { + "value": "AUTO", + "label": "Auto" + }, + { + "value": "COOL", + "label": "Cooling" + }, + { + "value": "HEAT", + "label": "Heating" + }, + { + "value": "OFF", + "label": "Off" + } + ], + "labels": ["Auto", "Cooling", "Heating", "Off"], + "values": ["AUTO", "COOL", "HEAT", "OFF"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261002/zone_mode" + } + } + }, + { + "type": "run_mode", + "title": "Run Mode", + "current_value": "permanent_hold", + "options": [ + { + "value": "permanent_hold", + "label": "Permanent Hold" + }, + { + "value": "run_schedule", + "label": "Run Schedule" + } + ], + "labels": ["Permanent Hold", "Run Schedule"], + "values": ["permanent_hold", "run_schedule"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261002/run_mode" + } + } + }, + { + "type": "scheduling_enabled", + "title": "Scheduling", + "current_value": true, + "options": [ + { + "value": true, + "label": "ON" + }, + { + "value": false, + "label": "OFF" + } + ], + "labels": ["ON", "OFF"], + "values": [true, false], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261002/scheduling_enabled" + } + } + } + ], + "icon": { + "name": "thermostat", + "modifiers": ["temperature-71"] + }, + "features": [ + { + "name": "thermostat", + "temperature": 71, + "status": "", + "status_icon": null, + "actions": { + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261002/setpoints" + }, + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261002/setpoints" + } + }, + "setpoint_delta": 3, + "scale": "f", + "setpoint_increment": 1.0, + "setpoint_heat_min": 55, + "setpoint_heat_max": 90, + "setpoint_cool_min": 60, + "setpoint_cool_max": 99, + "setpoint_heat": 63, + "setpoint_cool": 79, + "system_status": "System Idle" + }, + { + "name": "connection", + "signal_strength": "unknown", + "is_connected": true + }, + { + "name": "thermostat_mode", + "label": "Zone Mode", + "value": "AUTO", + "display_value": "Auto", + "options": [ + { + "id": "thermostat_mode", + "label": "Zone Mode", + "value": "thermostat_mode", + "header": true + }, + { + "value": "AUTO", + "label": "Auto" + }, + { + "value": "COOL", + "label": "Cooling" + }, + { + "value": "HEAT", + "label": "Heating" + }, + { + "value": "OFF", + "label": "Off" + } + ], + "actions": { + "update_thermostat_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83261002/zone_mode" + } + } + }, + { + "name": "thermostat_run_mode", + "label": "Run Mode", + "options": [ + { + "id": "thermostat_run_mode", + "label": "Run Mode", + "value": "thermostat_run_mode", + "header": true + }, + { + "id": "info_text", + "label": "Follow or override the schedule.", + "value": "info_text", + "info": true + }, + { + "value": "permanent_hold", + "label": "Permanent Hold" + }, + { + "value": "run_schedule", + "label": "Run Schedule" + } + ], + "value": "permanent_hold", + "display_value": "Hold", + "actions": { + "update_thermostat_run_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83261002/run_mode" + } + } + }, + { + "name": "schedule", + "enabled": true, + "max_period_name_length": 10, + "setpoint_increment": 1, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83261002\u0026house_id=123456", + "actions": { + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83261002", + "method": "POST" + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83261002", + "method": "POST" + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83261002", + "method": "GET" + }, + "enable_scheduling": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261002/scheduling_enabled", + "method": "POST", + "data": { + "value": true + } + } + }, + "can_add_remove_periods": true, + "max_periods_per_day": 4 + } + ], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261002" + } + } + }, + { + "type": "xxl_zone", + "id": 83261005, + "name": "Kitchen", + "current_zone_mode": "AUTO", + "temperature": 77, + "setpoints": { + "heat": 63, + "cool": 79 + }, + "operating_state": "", + "heating_setpoint": 63, + "cooling_setpoint": 79, + "zone_status": "", + "settings": [ + { + "type": "preset_selected", + "title": "Preset", + "current_value": 0, + "options": [ + { + "value": 0, + "label": "None" + }, + { + "value": 1, + "label": "Home" + }, + { + "value": 2, + "label": "Away" + }, + { + "value": 3, + "label": "Sleep" + } + ], + "labels": ["None", "Home", "Away", "Sleep"], + "values": [0, 1, 2, 3], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261005/preset_selected" + } + } + }, + { + "type": "zone_mode", + "title": "Zone Mode", + "current_value": "AUTO", + "options": [ + { + "value": "AUTO", + "label": "Auto" + }, + { + "value": "COOL", + "label": "Cooling" + }, + { + "value": "HEAT", + "label": "Heating" + }, + { + "value": "OFF", + "label": "Off" + } + ], + "labels": ["Auto", "Cooling", "Heating", "Off"], + "values": ["AUTO", "COOL", "HEAT", "OFF"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261005/zone_mode" + } + } + }, + { + "type": "run_mode", + "title": "Run Mode", + "current_value": "permanent_hold", + "options": [ + { + "value": "permanent_hold", + "label": "Permanent Hold" + }, + { + "value": "run_schedule", + "label": "Run Schedule" + } + ], + "labels": ["Permanent Hold", "Run Schedule"], + "values": ["permanent_hold", "run_schedule"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261005/run_mode" + } + } + }, + { + "type": "scheduling_enabled", + "title": "Scheduling", + "current_value": true, + "options": [ + { + "value": true, + "label": "ON" + }, + { + "value": false, + "label": "OFF" + } + ], + "labels": ["ON", "OFF"], + "values": [true, false], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261005/scheduling_enabled" + } + } + } + ], + "icon": { + "name": "thermostat", + "modifiers": ["temperature-77"] + }, + "features": [ + { + "name": "thermostat", + "temperature": 77, + "status": "", + "status_icon": null, + "actions": { + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261005/setpoints" + }, + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261005/setpoints" + } + }, + "setpoint_delta": 3, + "scale": "f", + "setpoint_increment": 1.0, + "setpoint_heat_min": 55, + "setpoint_heat_max": 90, + "setpoint_cool_min": 60, + "setpoint_cool_max": 99, + "setpoint_heat": 63, + "setpoint_cool": 79, + "system_status": "System Idle" + }, + { + "name": "connection", + "signal_strength": "unknown", + "is_connected": true + }, + { + "name": "thermostat_mode", + "label": "Zone Mode", + "value": "AUTO", + "display_value": "Auto", + "options": [ + { + "id": "thermostat_mode", + "label": "Zone Mode", + "value": "thermostat_mode", + "header": true + }, + { + "value": "AUTO", + "label": "Auto" + }, + { + "value": "COOL", + "label": "Cooling" + }, + { + "value": "HEAT", + "label": "Heating" + }, + { + "value": "OFF", + "label": "Off" + } + ], + "actions": { + "update_thermostat_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83261005/zone_mode" + } + } + }, + { + "name": "thermostat_run_mode", + "label": "Run Mode", + "options": [ + { + "id": "thermostat_run_mode", + "label": "Run Mode", + "value": "thermostat_run_mode", + "header": true + }, + { + "id": "info_text", + "label": "Follow or override the schedule.", + "value": "info_text", + "info": true + }, + { + "value": "permanent_hold", + "label": "Permanent Hold" + }, + { + "value": "run_schedule", + "label": "Run Schedule" + } + ], + "value": "permanent_hold", + "display_value": "Hold", + "actions": { + "update_thermostat_run_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83261005/run_mode" + } + } + }, + { + "name": "schedule", + "enabled": true, + "max_period_name_length": 10, + "setpoint_increment": 1, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83261005\u0026house_id=123456", + "actions": { + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83261005", + "method": "POST" + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83261005", + "method": "POST" + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83261005", + "method": "GET" + }, + "enable_scheduling": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261005/scheduling_enabled", + "method": "POST", + "data": { + "value": true + } + } + }, + "can_add_remove_periods": true, + "max_periods_per_day": 4 + } + ], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261005" + } + } + }, + { + "type": "xxl_zone", + "id": 83261008, + "name": "Down Bedroom", + "current_zone_mode": "AUTO", + "temperature": 72, + "setpoints": { + "heat": 63, + "cool": 79 + }, + "operating_state": "", + "heating_setpoint": 63, + "cooling_setpoint": 79, + "zone_status": "", + "settings": [ + { + "type": "preset_selected", + "title": "Preset", + "current_value": 0, + "options": [ + { + "value": 0, + "label": "None" + }, + { + "value": 1, + "label": "Home" + }, + { + "value": 2, + "label": "Away" + }, + { + "value": 3, + "label": "Sleep" + } + ], + "labels": ["None", "Home", "Away", "Sleep"], + "values": [0, 1, 2, 3], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261008/preset_selected" + } + } + }, + { + "type": "zone_mode", + "title": "Zone Mode", + "current_value": "AUTO", + "options": [ + { + "value": "AUTO", + "label": "Auto" + }, + { + "value": "COOL", + "label": "Cooling" + }, + { + "value": "HEAT", + "label": "Heating" + }, + { + "value": "OFF", + "label": "Off" + } + ], + "labels": ["Auto", "Cooling", "Heating", "Off"], + "values": ["AUTO", "COOL", "HEAT", "OFF"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261008/zone_mode" + } + } + }, + { + "type": "run_mode", + "title": "Run Mode", + "current_value": "permanent_hold", + "options": [ + { + "value": "permanent_hold", + "label": "Permanent Hold" + }, + { + "value": "run_schedule", + "label": "Run Schedule" + } + ], + "labels": ["Permanent Hold", "Run Schedule"], + "values": ["permanent_hold", "run_schedule"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261008/run_mode" + } + } + }, + { + "type": "scheduling_enabled", + "title": "Scheduling", + "current_value": true, + "options": [ + { + "value": true, + "label": "ON" + }, + { + "value": false, + "label": "OFF" + } + ], + "labels": ["ON", "OFF"], + "values": [true, false], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261008/scheduling_enabled" + } + } + } + ], + "icon": { + "name": "thermostat", + "modifiers": ["temperature-72"] + }, + "features": [ + { + "name": "thermostat", + "temperature": 72, + "status": "", + "status_icon": null, + "actions": { + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261008/setpoints" + }, + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261008/setpoints" + } + }, + "setpoint_delta": 3, + "scale": "f", + "setpoint_increment": 1.0, + "setpoint_heat_min": 55, + "setpoint_heat_max": 90, + "setpoint_cool_min": 60, + "setpoint_cool_max": 99, + "setpoint_heat": 63, + "setpoint_cool": 79, + "system_status": "System Idle" + }, + { + "name": "connection", + "signal_strength": "unknown", + "is_connected": true + }, + { + "name": "thermostat_mode", + "label": "Zone Mode", + "value": "AUTO", + "display_value": "Auto", + "options": [ + { + "id": "thermostat_mode", + "label": "Zone Mode", + "value": "thermostat_mode", + "header": true + }, + { + "value": "AUTO", + "label": "Auto" + }, + { + "value": "COOL", + "label": "Cooling" + }, + { + "value": "HEAT", + "label": "Heating" + }, + { + "value": "OFF", + "label": "Off" + } + ], + "actions": { + "update_thermostat_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83261008/zone_mode" + } + } + }, + { + "name": "thermostat_run_mode", + "label": "Run Mode", + "options": [ + { + "id": "thermostat_run_mode", + "label": "Run Mode", + "value": "thermostat_run_mode", + "header": true + }, + { + "id": "info_text", + "label": "Follow or override the schedule.", + "value": "info_text", + "info": true + }, + { + "value": "permanent_hold", + "label": "Permanent Hold" + }, + { + "value": "run_schedule", + "label": "Run Schedule" + } + ], + "value": "permanent_hold", + "display_value": "Hold", + "actions": { + "update_thermostat_run_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83261008/run_mode" + } + } + }, + { + "name": "schedule", + "enabled": true, + "max_period_name_length": 10, + "setpoint_increment": 1, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83261008\u0026house_id=123456", + "actions": { + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83261008", + "method": "POST" + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83261008", + "method": "POST" + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83261008", + "method": "GET" + }, + "enable_scheduling": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261008/scheduling_enabled", + "method": "POST", + "data": { + "value": true + } + } + }, + "can_add_remove_periods": true, + "max_periods_per_day": 4 + } + ], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261008" + } + } + }, + { + "type": "xxl_zone", + "id": 83261011, + "name": "Tech Room", + "current_zone_mode": "AUTO", + "temperature": 78, + "setpoints": { + "heat": 63, + "cool": 79 + }, + "operating_state": "", + "heating_setpoint": 63, + "cooling_setpoint": 79, + "zone_status": "", + "settings": [ + { + "type": "preset_selected", + "title": "Preset", + "current_value": 0, + "options": [ + { + "value": 0, + "label": "None" + }, + { + "value": 1, + "label": "Home" + }, + { + "value": 2, + "label": "Away" + }, + { + "value": 3, + "label": "Sleep" + } + ], + "labels": ["None", "Home", "Away", "Sleep"], + "values": [0, 1, 2, 3], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261011/preset_selected" + } + } + }, + { + "type": "zone_mode", + "title": "Zone Mode", + "current_value": "AUTO", + "options": [ + { + "value": "AUTO", + "label": "Auto" + }, + { + "value": "COOL", + "label": "Cooling" + }, + { + "value": "HEAT", + "label": "Heating" + }, + { + "value": "OFF", + "label": "Off" + } + ], + "labels": ["Auto", "Cooling", "Heating", "Off"], + "values": ["AUTO", "COOL", "HEAT", "OFF"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261011/zone_mode" + } + } + }, + { + "type": "run_mode", + "title": "Run Mode", + "current_value": "permanent_hold", + "options": [ + { + "value": "permanent_hold", + "label": "Permanent Hold" + }, + { + "value": "run_schedule", + "label": "Run Schedule" + } + ], + "labels": ["Permanent Hold", "Run Schedule"], + "values": ["permanent_hold", "run_schedule"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261011/run_mode" + } + } + }, + { + "type": "scheduling_enabled", + "title": "Scheduling", + "current_value": true, + "options": [ + { + "value": true, + "label": "ON" + }, + { + "value": false, + "label": "OFF" + } + ], + "labels": ["ON", "OFF"], + "values": [true, false], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261011/scheduling_enabled" + } + } + } + ], + "icon": { + "name": "thermostat", + "modifiers": ["temperature-78"] + }, + "features": [ + { + "name": "thermostat", + "temperature": 78, + "status": "", + "status_icon": null, + "actions": { + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261011/setpoints" + }, + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261011/setpoints" + } + }, + "setpoint_delta": 3, + "scale": "f", + "setpoint_increment": 1.0, + "setpoint_heat_min": 55, + "setpoint_heat_max": 90, + "setpoint_cool_min": 60, + "setpoint_cool_max": 99, + "setpoint_heat": 63, + "setpoint_cool": 79, + "system_status": "System Idle" + }, + { + "name": "connection", + "signal_strength": "unknown", + "is_connected": true + }, + { + "name": "thermostat_mode", + "label": "Zone Mode", + "value": "AUTO", + "display_value": "Auto", + "options": [ + { + "id": "thermostat_mode", + "label": "Zone Mode", + "value": "thermostat_mode", + "header": true + }, + { + "value": "AUTO", + "label": "Auto" + }, + { + "value": "COOL", + "label": "Cooling" + }, + { + "value": "HEAT", + "label": "Heating" + }, + { + "value": "OFF", + "label": "Off" + } + ], + "actions": { + "update_thermostat_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83261011/zone_mode" + } + } + }, + { + "name": "thermostat_run_mode", + "label": "Run Mode", + "options": [ + { + "id": "thermostat_run_mode", + "label": "Run Mode", + "value": "thermostat_run_mode", + "header": true + }, + { + "id": "info_text", + "label": "Follow or override the schedule.", + "value": "info_text", + "info": true + }, + { + "value": "permanent_hold", + "label": "Permanent Hold" + }, + { + "value": "run_schedule", + "label": "Run Schedule" + } + ], + "value": "permanent_hold", + "display_value": "Hold", + "actions": { + "update_thermostat_run_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83261011/run_mode" + } + } + }, + { + "name": "schedule", + "enabled": true, + "max_period_name_length": 10, + "setpoint_increment": 1, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83261011\u0026house_id=123456", + "actions": { + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83261011", + "method": "POST" + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83261011", + "method": "POST" + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83261011", + "method": "GET" + }, + "enable_scheduling": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261011/scheduling_enabled", + "method": "POST", + "data": { + "value": true + } + } + }, + "can_add_remove_periods": true, + "max_periods_per_day": 4 + } + ], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261011" + } + } + } + ] + }, + { + "id": 2059676, + "name": "Downstairs West Wing", + "name_editable": true, + "features": [ + { + "name": "advanced_info", + "items": [ + { + "type": "label_value", + "label": "Model", + "value": "XL1050" + }, + { + "type": "label_value", + "label": "AUID", + "value": "02853E08" + }, + { + "type": "label_value", + "label": "Firmware Build Number", + "value": "1581321824" + }, + { + "type": "label_value", + "label": "Firmware Build Date", + "value": "2020-02-10 08:03:44 UTC" + }, + { + "type": "label_value", + "label": "Firmware Version", + "value": "5.9.1" + }, + { + "type": "label_value", + "label": "Zoning Enabled", + "value": "yes" + } + ] + }, + { + "name": "thermostat", + "temperature": 75, + "status": "System Idle", + "status_icon": null, + "actions": {}, + "setpoint_delta": 3, + "scale": "f", + "setpoint_increment": 1.0, + "setpoint_heat_min": 55, + "setpoint_heat_max": 90, + "setpoint_cool_min": 60, + "setpoint_cool_max": 99 + }, + { + "name": "connection", + "signal_strength": "unknown", + "is_connected": true + }, + { + "name": "group", + "members": [ + { + "type": "xxl_zone", + "id": 83261015, + "name": "Living West", + "current_zone_mode": "AUTO", + "temperature": 75, + "setpoints": { + "heat": 63, + "cool": 79 + }, + "operating_state": "", + "heating_setpoint": 63, + "cooling_setpoint": 79, + "zone_status": "", + "settings": [ + { + "type": "preset_selected", + "title": "Preset", + "current_value": 0, + "options": [ + { + "value": 0, + "label": "None" + }, + { + "value": 1, + "label": "Home" + }, + { + "value": 2, + "label": "Away" + }, + { + "value": 3, + "label": "Sleep" + } + ], + "labels": ["None", "Home", "Away", "Sleep"], + "values": [0, 1, 2, 3], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261015/preset_selected" + } + } + }, + { + "type": "zone_mode", + "title": "Zone Mode", + "current_value": "AUTO", + "options": [ + { + "value": "AUTO", + "label": "Auto" + }, + { + "value": "COOL", + "label": "Cooling" + }, + { + "value": "HEAT", + "label": "Heating" + }, + { + "value": "OFF", + "label": "Off" + } + ], + "labels": ["Auto", "Cooling", "Heating", "Off"], + "values": ["AUTO", "COOL", "HEAT", "OFF"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261015/zone_mode" + } + } + }, + { + "type": "run_mode", + "title": "Run Mode", + "current_value": "permanent_hold", + "options": [ + { + "value": "permanent_hold", + "label": "Permanent Hold" + }, + { + "value": "run_schedule", + "label": "Run Schedule" + } + ], + "labels": ["Permanent Hold", "Run Schedule"], + "values": ["permanent_hold", "run_schedule"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261015/run_mode" + } + } + }, + { + "type": "scheduling_enabled", + "title": "Scheduling", + "current_value": true, + "options": [ + { + "value": true, + "label": "ON" + }, + { + "value": false, + "label": "OFF" + } + ], + "labels": ["ON", "OFF"], + "values": [true, false], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261015/scheduling_enabled" + } + } + } + ], + "icon": { + "name": "thermostat", + "modifiers": ["temperature-75"] + }, + "features": [ + { + "name": "thermostat", + "temperature": 75, + "status": "", + "status_icon": null, + "actions": { + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261015/setpoints" + }, + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261015/setpoints" + } + }, + "setpoint_delta": 3, + "scale": "f", + "setpoint_increment": 1.0, + "setpoint_heat_min": 55, + "setpoint_heat_max": 90, + "setpoint_cool_min": 60, + "setpoint_cool_max": 99, + "setpoint_heat": 63, + "setpoint_cool": 79, + "system_status": "System Idle" + }, + { + "name": "connection", + "signal_strength": "unknown", + "is_connected": true + }, + { + "name": "thermostat_mode", + "label": "Zone Mode", + "value": "AUTO", + "display_value": "Auto", + "options": [ + { + "id": "thermostat_mode", + "label": "Zone Mode", + "value": "thermostat_mode", + "header": true + }, + { + "value": "AUTO", + "label": "Auto" + }, + { + "value": "COOL", + "label": "Cooling" + }, + { + "value": "HEAT", + "label": "Heating" + }, + { + "value": "OFF", + "label": "Off" + } + ], + "actions": { + "update_thermostat_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83261015/zone_mode" + } + } + }, + { + "name": "thermostat_run_mode", + "label": "Run Mode", + "options": [ + { + "id": "thermostat_run_mode", + "label": "Run Mode", + "value": "thermostat_run_mode", + "header": true + }, + { + "id": "info_text", + "label": "Follow or override the schedule.", + "value": "info_text", + "info": true + }, + { + "value": "permanent_hold", + "label": "Permanent Hold" + }, + { + "value": "run_schedule", + "label": "Run Schedule" + } + ], + "value": "permanent_hold", + "display_value": "Hold", + "actions": { + "update_thermostat_run_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83261015/run_mode" + } + } + }, + { + "name": "schedule", + "enabled": true, + "max_period_name_length": 10, + "setpoint_increment": 1, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83261015\u0026house_id=123456", + "actions": { + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83261015", + "method": "POST" + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83261015", + "method": "POST" + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83261015", + "method": "GET" + }, + "enable_scheduling": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261015/scheduling_enabled", + "method": "POST", + "data": { + "value": true + } + } + }, + "can_add_remove_periods": true, + "max_periods_per_day": 4 + } + ], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261015" + } + } + }, + { + "type": "xxl_zone", + "id": 83261018, + "name": "David Office", + "current_zone_mode": "AUTO", + "temperature": 75, + "setpoints": { + "heat": 63, + "cool": 79 + }, + "operating_state": "", + "heating_setpoint": 63, + "cooling_setpoint": 79, + "zone_status": "", + "settings": [ + { + "type": "preset_selected", + "title": "Preset", + "current_value": 0, + "options": [ + { + "value": 0, + "label": "None" + }, + { + "value": 1, + "label": "Home" + }, + { + "value": 2, + "label": "Away" + }, + { + "value": 3, + "label": "Sleep" + } + ], + "labels": ["None", "Home", "Away", "Sleep"], + "values": [0, 1, 2, 3], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261018/preset_selected" + } + } + }, + { + "type": "zone_mode", + "title": "Zone Mode", + "current_value": "AUTO", + "options": [ + { + "value": "AUTO", + "label": "Auto" + }, + { + "value": "COOL", + "label": "Cooling" + }, + { + "value": "HEAT", + "label": "Heating" + }, + { + "value": "OFF", + "label": "Off" + } + ], + "labels": ["Auto", "Cooling", "Heating", "Off"], + "values": ["AUTO", "COOL", "HEAT", "OFF"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261018/zone_mode" + } + } + }, + { + "type": "run_mode", + "title": "Run Mode", + "current_value": "permanent_hold", + "options": [ + { + "value": "permanent_hold", + "label": "Permanent Hold" + }, + { + "value": "run_schedule", + "label": "Run Schedule" + } + ], + "labels": ["Permanent Hold", "Run Schedule"], + "values": ["permanent_hold", "run_schedule"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261018/run_mode" + } + } + }, + { + "type": "scheduling_enabled", + "title": "Scheduling", + "current_value": true, + "options": [ + { + "value": true, + "label": "ON" + }, + { + "value": false, + "label": "OFF" + } + ], + "labels": ["ON", "OFF"], + "values": [true, false], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261018/scheduling_enabled" + } + } + } + ], + "icon": { + "name": "thermostat", + "modifiers": ["temperature-75"] + }, + "features": [ + { + "name": "thermostat", + "temperature": 75, + "status": "", + "status_icon": null, + "actions": { + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261018/setpoints" + }, + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261018/setpoints" + } + }, + "setpoint_delta": 3, + "scale": "f", + "setpoint_increment": 1.0, + "setpoint_heat_min": 55, + "setpoint_heat_max": 90, + "setpoint_cool_min": 60, + "setpoint_cool_max": 99, + "setpoint_heat": 63, + "setpoint_cool": 79, + "system_status": "System Idle" + }, + { + "name": "connection", + "signal_strength": "unknown", + "is_connected": true + }, + { + "name": "thermostat_mode", + "label": "Zone Mode", + "value": "AUTO", + "display_value": "Auto", + "options": [ + { + "id": "thermostat_mode", + "label": "Zone Mode", + "value": "thermostat_mode", + "header": true + }, + { + "value": "AUTO", + "label": "Auto" + }, + { + "value": "COOL", + "label": "Cooling" + }, + { + "value": "HEAT", + "label": "Heating" + }, + { + "value": "OFF", + "label": "Off" + } + ], + "actions": { + "update_thermostat_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83261018/zone_mode" + } + } + }, + { + "name": "thermostat_run_mode", + "label": "Run Mode", + "options": [ + { + "id": "thermostat_run_mode", + "label": "Run Mode", + "value": "thermostat_run_mode", + "header": true + }, + { + "id": "info_text", + "label": "Follow or override the schedule.", + "value": "info_text", + "info": true + }, + { + "value": "permanent_hold", + "label": "Permanent Hold" + }, + { + "value": "run_schedule", + "label": "Run Schedule" + } + ], + "value": "permanent_hold", + "display_value": "Hold", + "actions": { + "update_thermostat_run_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83261018/run_mode" + } + } + }, + { + "name": "schedule", + "enabled": true, + "max_period_name_length": 10, + "setpoint_increment": 1, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83261018\u0026house_id=123456", + "actions": { + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83261018", + "method": "POST" + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83261018", + "method": "POST" + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83261018", + "method": "GET" + }, + "enable_scheduling": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261018/scheduling_enabled", + "method": "POST", + "data": { + "value": true + } + } + }, + "can_add_remove_periods": true, + "max_periods_per_day": 4 + } + ], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261018" + } + } + } + ] + }, + { + "name": "thermostat_fan_mode", + "label": "Fan Mode", + "options": [ + { + "id": "thermostat_fan_mode", + "label": "Fan Mode", + "value": "thermostat_fan_mode", + "header": true + }, + { + "value": "auto", + "label": "Auto" + }, + { + "value": "on", + "label": "On" + }, + { + "value": "circulate", + "label": "Circulate" + } + ], + "value": "auto", + "display_value": "Auto", + "status_icon": { + "name": "thermostat_fan_off", + "modifiers": [] + }, + "actions": { + "update_thermostat_fan_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059676/fan_mode" + } + } + }, + { + "name": "thermostat_compressor_speed", + "compressor_speed": 0.0 + }, + { + "name": "runtime_history", + "actions": { + "get_runtime_history": { + "method": "GET", + "href": "https://www.mynexia.com/mobile/runtime_history/2059676?report_type=daily" + }, + "get_monthly_runtime_history": { + "method": "GET", + "href": "https://www.mynexia.com/mobile/runtime_history/2059676?report_type=monthly" + } + } + } + ], + "icon": [ + { + "name": "thermostat", + "modifiers": ["temperature-75"] + }, + { + "name": "thermostat", + "modifiers": ["temperature-75"] + } + ], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059676" + }, + "nexia:history": { + "href": "https://www.mynexia.com/mobile/houses/123456/events?device_id=2059676" + }, + "filter_events": { + "href": "https://www.mynexia.com/mobile/houses/123456/events/collection?sys_guid=5aae72a6-1bd0-4d84-9bfd-673e7bc4907c" + }, + "pending_request": { + "polling_path": "https://www.mynexia.com/backstage/announcements/3412f1d96eb0c5edb5466c3c0598af60c06f8443f21e9bcb" + } + }, + "last_updated_at": "2020-03-11T15:15:53.000-05:00", + "settings": [ + { + "type": "fan_mode", + "title": "Fan Mode", + "current_value": "auto", + "options": [ + { + "value": "auto", + "label": "Auto" + }, + { + "value": "on", + "label": "On" + }, + { + "value": "circulate", + "label": "Circulate" + } + ], + "labels": ["Auto", "On", "Circulate"], + "values": ["auto", "on", "circulate"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059676/fan_mode" + } + } + }, + { + "type": "fan_speed", + "title": "Fan Speed", + "current_value": 0.35, + "options": [ + { + "value": 0.35, + "label": "35%" + }, + { + "value": 0.4, + "label": "40%" + }, + { + "value": 0.45, + "label": "45%" + }, + { + "value": 0.5, + "label": "50%" + }, + { + "value": 0.55, + "label": "55%" + }, + { + "value": 0.6, + "label": "60%" + }, + { + "value": 0.65, + "label": "65%" + }, + { + "value": 0.7, + "label": "70%" + }, + { + "value": 0.75, + "label": "75%" + }, + { + "value": 0.8, + "label": "80%" + }, + { + "value": 0.85, + "label": "85%" + }, + { + "value": 0.9, + "label": "90%" + }, + { + "value": 0.95, + "label": "95%" + }, + { + "value": 1.0, + "label": "100%" + } + ], + "labels": [ + "35%", + "40%", + "45%", + "50%", + "55%", + "60%", + "65%", + "70%", + "75%", + "80%", + "85%", + "90%", + "95%", + "100%" + ], + "values": [ + 0.35, 0.4, 0.45, 0.5, 0.55, 0.6, 0.65, 0.7, 0.75, 0.8, + 0.85, 0.9, 0.95, 1.0 + ], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059676/fan_speed" + } + } + }, + { + "type": "fan_circulation_time", + "title": "Fan Circulation Time", + "current_value": 30, + "options": [ + { + "value": 10, + "label": "10 minutes" + }, + { + "value": 15, + "label": "15 minutes" + }, + { + "value": 20, + "label": "20 minutes" + }, + { + "value": 25, + "label": "25 minutes" + }, + { + "value": 30, + "label": "30 minutes" + }, + { + "value": 35, + "label": "35 minutes" + }, + { + "value": 40, + "label": "40 minutes" + }, + { + "value": 45, + "label": "45 minutes" + }, + { + "value": 50, + "label": "50 minutes" + }, + { + "value": 55, + "label": "55 minutes" + } + ], + "labels": [ + "10 minutes", + "15 minutes", + "20 minutes", + "25 minutes", + "30 minutes", + "35 minutes", + "40 minutes", + "45 minutes", + "50 minutes", + "55 minutes" + ], + "values": [10, 15, 20, 25, 30, 35, 40, 45, 50, 55], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059676/fan_circulation_time" + } + } + }, + { + "type": "air_cleaner_mode", + "title": "Air Cleaner Mode", + "current_value": "auto", + "options": [ + { + "value": "auto", + "label": "Auto" + }, + { + "value": "quick", + "label": "Quick" + }, + { + "value": "allergy", + "label": "Allergy" + } + ], + "labels": ["Auto", "Quick", "Allergy"], + "values": ["auto", "quick", "allergy"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059676/air_cleaner_mode" + } + } + }, + { + "type": "dehumidify", + "title": "Cooling Dehumidify Set Point", + "current_value": 0.45, + "options": [ + { + "value": 0.35, + "label": "35%" + }, + { + "value": 0.4, + "label": "40%" + }, + { + "value": 0.45, + "label": "45%" + }, + { + "value": 0.5, + "label": "50%" + }, + { + "value": 0.55, + "label": "55%" + }, + { + "value": 0.6, + "label": "60%" + }, + { + "value": 0.65, + "label": "65%" + } + ], + "labels": ["35%", "40%", "45%", "50%", "55%", "60%", "65%"], + "values": [0.35, 0.4, 0.45, 0.5, 0.55, 0.6, 0.65], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059676/dehumidify" + } + } + }, + { + "type": "scale", + "title": "Temperature Scale", + "current_value": "f", + "options": [ + { + "value": "f", + "label": "F" + }, + { + "value": "c", + "label": "C" + } + ], + "labels": ["F", "C"], + "values": ["f", "c"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059676/scale" + } + } + } + ], + "status_secondary": null, + "status_tertiary": null, + "type": "xxl_thermostat", + "has_outdoor_temperature": true, + "outdoor_temperature": "88", + "has_indoor_humidity": true, + "connected": true, + "indoor_humidity": "52", + "system_status": "System Idle", + "delta": 3, + "zones": [ + { + "type": "xxl_zone", + "id": 83261015, + "name": "Living West", + "current_zone_mode": "AUTO", + "temperature": 75, + "setpoints": { + "heat": 63, + "cool": 79 + }, + "operating_state": "", + "heating_setpoint": 63, + "cooling_setpoint": 79, + "zone_status": "", + "settings": [ + { + "type": "preset_selected", + "title": "Preset", + "current_value": 0, + "options": [ + { + "value": 0, + "label": "None" + }, + { + "value": 1, + "label": "Home" + }, + { + "value": 2, + "label": "Away" + }, + { + "value": 3, + "label": "Sleep" + } + ], + "labels": ["None", "Home", "Away", "Sleep"], + "values": [0, 1, 2, 3], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261015/preset_selected" + } + } + }, + { + "type": "zone_mode", + "title": "Zone Mode", + "current_value": "AUTO", + "options": [ + { + "value": "AUTO", + "label": "Auto" + }, + { + "value": "COOL", + "label": "Cooling" + }, + { + "value": "HEAT", + "label": "Heating" + }, + { + "value": "OFF", + "label": "Off" + } + ], + "labels": ["Auto", "Cooling", "Heating", "Off"], + "values": ["AUTO", "COOL", "HEAT", "OFF"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261015/zone_mode" + } + } + }, + { + "type": "run_mode", + "title": "Run Mode", + "current_value": "permanent_hold", + "options": [ + { + "value": "permanent_hold", + "label": "Permanent Hold" + }, + { + "value": "run_schedule", + "label": "Run Schedule" + } + ], + "labels": ["Permanent Hold", "Run Schedule"], + "values": ["permanent_hold", "run_schedule"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261015/run_mode" + } + } + }, + { + "type": "scheduling_enabled", + "title": "Scheduling", + "current_value": true, + "options": [ + { + "value": true, + "label": "ON" + }, + { + "value": false, + "label": "OFF" + } + ], + "labels": ["ON", "OFF"], + "values": [true, false], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261015/scheduling_enabled" + } + } + } + ], + "icon": { + "name": "thermostat", + "modifiers": ["temperature-75"] + }, + "features": [ + { + "name": "thermostat", + "temperature": 75, + "status": "", + "status_icon": null, + "actions": { + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261015/setpoints" + }, + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261015/setpoints" + } + }, + "setpoint_delta": 3, + "scale": "f", + "setpoint_increment": 1.0, + "setpoint_heat_min": 55, + "setpoint_heat_max": 90, + "setpoint_cool_min": 60, + "setpoint_cool_max": 99, + "setpoint_heat": 63, + "setpoint_cool": 79, + "system_status": "System Idle" + }, + { + "name": "connection", + "signal_strength": "unknown", + "is_connected": true + }, + { + "name": "thermostat_mode", + "label": "Zone Mode", + "value": "AUTO", + "display_value": "Auto", + "options": [ + { + "id": "thermostat_mode", + "label": "Zone Mode", + "value": "thermostat_mode", + "header": true + }, + { + "value": "AUTO", + "label": "Auto" + }, + { + "value": "COOL", + "label": "Cooling" + }, + { + "value": "HEAT", + "label": "Heating" + }, + { + "value": "OFF", + "label": "Off" + } + ], + "actions": { + "update_thermostat_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83261015/zone_mode" + } + } + }, + { + "name": "thermostat_run_mode", + "label": "Run Mode", + "options": [ + { + "id": "thermostat_run_mode", + "label": "Run Mode", + "value": "thermostat_run_mode", + "header": true + }, + { + "id": "info_text", + "label": "Follow or override the schedule.", + "value": "info_text", + "info": true + }, + { + "value": "permanent_hold", + "label": "Permanent Hold" + }, + { + "value": "run_schedule", + "label": "Run Schedule" + } + ], + "value": "permanent_hold", + "display_value": "Hold", + "actions": { + "update_thermostat_run_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83261015/run_mode" + } + } + }, + { + "name": "schedule", + "enabled": true, + "max_period_name_length": 10, + "setpoint_increment": 1, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83261015\u0026house_id=123456", + "actions": { + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83261015", + "method": "POST" + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83261015", + "method": "POST" + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83261015", + "method": "GET" + }, + "enable_scheduling": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261015/scheduling_enabled", + "method": "POST", + "data": { + "value": true + } + } + }, + "can_add_remove_periods": true, + "max_periods_per_day": 4 + } + ], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261015" + } + } + }, + { + "type": "xxl_zone", + "id": 83261018, + "name": "David Office", + "current_zone_mode": "AUTO", + "temperature": 75, + "setpoints": { + "heat": 63, + "cool": 79 + }, + "operating_state": "", + "heating_setpoint": 63, + "cooling_setpoint": 79, + "zone_status": "", + "settings": [ + { + "type": "preset_selected", + "title": "Preset", + "current_value": 0, + "options": [ + { + "value": 0, + "label": "None" + }, + { + "value": 1, + "label": "Home" + }, + { + "value": 2, + "label": "Away" + }, + { + "value": 3, + "label": "Sleep" + } + ], + "labels": ["None", "Home", "Away", "Sleep"], + "values": [0, 1, 2, 3], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261018/preset_selected" + } + } + }, + { + "type": "zone_mode", + "title": "Zone Mode", + "current_value": "AUTO", + "options": [ + { + "value": "AUTO", + "label": "Auto" + }, + { + "value": "COOL", + "label": "Cooling" + }, + { + "value": "HEAT", + "label": "Heating" + }, + { + "value": "OFF", + "label": "Off" + } + ], + "labels": ["Auto", "Cooling", "Heating", "Off"], + "values": ["AUTO", "COOL", "HEAT", "OFF"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261018/zone_mode" + } + } + }, + { + "type": "run_mode", + "title": "Run Mode", + "current_value": "permanent_hold", + "options": [ + { + "value": "permanent_hold", + "label": "Permanent Hold" + }, + { + "value": "run_schedule", + "label": "Run Schedule" + } + ], + "labels": ["Permanent Hold", "Run Schedule"], + "values": ["permanent_hold", "run_schedule"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261018/run_mode" + } + } + }, + { + "type": "scheduling_enabled", + "title": "Scheduling", + "current_value": true, + "options": [ + { + "value": true, + "label": "ON" + }, + { + "value": false, + "label": "OFF" + } + ], + "labels": ["ON", "OFF"], + "values": [true, false], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261018/scheduling_enabled" + } + } + } + ], + "icon": { + "name": "thermostat", + "modifiers": ["temperature-75"] + }, + "features": [ + { + "name": "thermostat", + "temperature": 75, + "status": "", + "status_icon": null, + "actions": { + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261018/setpoints" + }, + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261018/setpoints" + } + }, + "setpoint_delta": 3, + "scale": "f", + "setpoint_increment": 1.0, + "setpoint_heat_min": 55, + "setpoint_heat_max": 90, + "setpoint_cool_min": 60, + "setpoint_cool_max": 99, + "setpoint_heat": 63, + "setpoint_cool": 79, + "system_status": "System Idle" + }, + { + "name": "connection", + "signal_strength": "unknown", + "is_connected": true + }, + { + "name": "thermostat_mode", + "label": "Zone Mode", + "value": "AUTO", + "display_value": "Auto", + "options": [ + { + "id": "thermostat_mode", + "label": "Zone Mode", + "value": "thermostat_mode", + "header": true + }, + { + "value": "AUTO", + "label": "Auto" + }, + { + "value": "COOL", + "label": "Cooling" + }, + { + "value": "HEAT", + "label": "Heating" + }, + { + "value": "OFF", + "label": "Off" + } + ], + "actions": { + "update_thermostat_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83261018/zone_mode" + } + } + }, + { + "name": "thermostat_run_mode", + "label": "Run Mode", + "options": [ + { + "id": "thermostat_run_mode", + "label": "Run Mode", + "value": "thermostat_run_mode", + "header": true + }, + { + "id": "info_text", + "label": "Follow or override the schedule.", + "value": "info_text", + "info": true + }, + { + "value": "permanent_hold", + "label": "Permanent Hold" + }, + { + "value": "run_schedule", + "label": "Run Schedule" + } + ], + "value": "permanent_hold", + "display_value": "Hold", + "actions": { + "update_thermostat_run_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83261018/run_mode" + } + } + }, + { + "name": "schedule", + "enabled": true, + "max_period_name_length": 10, + "setpoint_increment": 1, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83261018\u0026house_id=123456", + "actions": { + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83261018", + "method": "POST" + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83261018", + "method": "POST" + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83261018", + "method": "GET" + }, + "enable_scheduling": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261018/scheduling_enabled", + "method": "POST", + "data": { + "value": true + } + } + }, + "can_add_remove_periods": true, + "max_periods_per_day": 4 + } + ], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261018" + } + } + } + ] + }, + { + "id": 2293892, + "name": "Master Suite", + "name_editable": true, + "features": [ + { + "name": "advanced_info", + "items": [ + { + "type": "label_value", + "label": "Model", + "value": "XL1050" + }, + { + "type": "label_value", + "label": "AUID", + "value": "0281B02C" + }, + { + "type": "label_value", + "label": "Firmware Build Number", + "value": "1581321824" + }, + { + "type": "label_value", + "label": "Firmware Build Date", + "value": "2020-02-10 08:03:44 UTC" + }, + { + "type": "label_value", + "label": "Firmware Version", + "value": "5.9.1" + }, + { + "type": "label_value", + "label": "Zoning Enabled", + "value": "yes" + } + ] + }, + { + "name": "thermostat", + "temperature": 73, + "status": "Cooling", + "status_icon": { + "name": "cooling", + "modifiers": [] + }, + "actions": {}, + "setpoint_delta": 3, + "scale": "f", + "setpoint_increment": 1.0, + "setpoint_heat_min": 55, + "setpoint_heat_max": 90, + "setpoint_cool_min": 60, + "setpoint_cool_max": 99 + }, + { + "name": "connection", + "signal_strength": "unknown", + "is_connected": true + }, + { + "name": "group", + "members": [ + { + "type": "xxl_zone", + "id": 83394133, + "name": "Bath Closet", + "current_zone_mode": "AUTO", + "temperature": 73, + "setpoints": { + "heat": 63, + "cool": 79 + }, + "operating_state": "Relieving Air", + "heating_setpoint": 63, + "cooling_setpoint": 79, + "zone_status": "Relieving Air", + "settings": [ + { + "type": "preset_selected", + "title": "Preset", + "current_value": 0, + "options": [ + { + "value": 0, + "label": "None" + }, + { + "value": 1, + "label": "Home" + }, + { + "value": 2, + "label": "Away" + }, + { + "value": 3, + "label": "Sleep" + } + ], + "labels": ["None", "Home", "Away", "Sleep"], + "values": [0, 1, 2, 3], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/preset_selected" + } + } + }, + { + "type": "zone_mode", + "title": "Zone Mode", + "current_value": "AUTO", + "options": [ + { + "value": "AUTO", + "label": "Auto" + }, + { + "value": "COOL", + "label": "Cooling" + }, + { + "value": "HEAT", + "label": "Heating" + }, + { + "value": "OFF", + "label": "Off" + } + ], + "labels": ["Auto", "Cooling", "Heating", "Off"], + "values": ["AUTO", "COOL", "HEAT", "OFF"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/zone_mode" + } + } + }, + { + "type": "run_mode", + "title": "Run Mode", + "current_value": "permanent_hold", + "options": [ + { + "value": "permanent_hold", + "label": "Permanent Hold" + }, + { + "value": "run_schedule", + "label": "Run Schedule" + } + ], + "labels": ["Permanent Hold", "Run Schedule"], + "values": ["permanent_hold", "run_schedule"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/run_mode" + } + } + }, + { + "type": "scheduling_enabled", + "title": "Scheduling", + "current_value": true, + "options": [ + { + "value": true, + "label": "ON" + }, + { + "value": false, + "label": "OFF" + } + ], + "labels": ["ON", "OFF"], + "values": [true, false], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/scheduling_enabled" + } + } + } + ], + "icon": { + "name": "thermostat", + "modifiers": ["temperature-73"] + }, + "features": [ + { + "name": "thermostat", + "temperature": 73, + "status": "Relieving Air", + "status_icon": { + "name": "cooling", + "modifiers": [] + }, + "actions": { + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/setpoints" + }, + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/setpoints" + } + }, + "setpoint_delta": 3, + "scale": "f", + "setpoint_increment": 1.0, + "setpoint_heat_min": 55, + "setpoint_heat_max": 90, + "setpoint_cool_min": 60, + "setpoint_cool_max": 99, + "setpoint_heat": 63, + "setpoint_cool": 79, + "system_status": "Cooling" + }, + { + "name": "connection", + "signal_strength": "unknown", + "is_connected": true + }, + { + "name": "thermostat_mode", + "label": "Zone Mode", + "value": "AUTO", + "display_value": "Auto", + "options": [ + { + "id": "thermostat_mode", + "label": "Zone Mode", + "value": "thermostat_mode", + "header": true + }, + { + "value": "AUTO", + "label": "Auto" + }, + { + "value": "COOL", + "label": "Cooling" + }, + { + "value": "HEAT", + "label": "Heating" + }, + { + "value": "OFF", + "label": "Off" + } + ], + "actions": { + "update_thermostat_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/zone_mode" + } + } + }, + { + "name": "thermostat_run_mode", + "label": "Run Mode", + "options": [ + { + "id": "thermostat_run_mode", + "label": "Run Mode", + "value": "thermostat_run_mode", + "header": true + }, + { + "id": "info_text", + "label": "Follow or override the schedule.", + "value": "info_text", + "info": true + }, + { + "value": "permanent_hold", + "label": "Permanent Hold" + }, + { + "value": "run_schedule", + "label": "Run Schedule" + } + ], + "value": "permanent_hold", + "display_value": "Hold", + "actions": { + "update_thermostat_run_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/run_mode" + } + } + }, + { + "name": "schedule", + "enabled": true, + "max_period_name_length": 10, + "setpoint_increment": 1, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83394133\u0026house_id=123456", + "actions": { + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83394133", + "method": "POST" + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83394133", + "method": "POST" + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83394133", + "method": "GET" + }, + "enable_scheduling": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/scheduling_enabled", + "method": "POST", + "data": { + "value": true + } + } + }, + "can_add_remove_periods": true, + "max_periods_per_day": 4 + } + ], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133" + } + } + }, + { + "type": "xxl_zone", + "id": 83394130, + "name": "Master", + "current_zone_mode": "AUTO", + "temperature": 74, + "setpoints": { + "heat": 63, + "cool": 71 + }, + "operating_state": "Damper Open", + "heating_setpoint": 63, + "cooling_setpoint": 71, + "zone_status": "Damper Open", + "settings": [ + { + "type": "preset_selected", + "title": "Preset", + "current_value": 0, + "options": [ + { + "value": 0, + "label": "None" + }, + { + "value": 1, + "label": "Home" + }, + { + "value": 2, + "label": "Away" + }, + { + "value": 3, + "label": "Sleep" + } + ], + "labels": ["None", "Home", "Away", "Sleep"], + "values": [0, 1, 2, 3], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/preset_selected" + } + } + }, + { + "type": "zone_mode", + "title": "Zone Mode", + "current_value": "AUTO", + "options": [ + { + "value": "AUTO", + "label": "Auto" + }, + { + "value": "COOL", + "label": "Cooling" + }, + { + "value": "HEAT", + "label": "Heating" + }, + { + "value": "OFF", + "label": "Off" + } + ], + "labels": ["Auto", "Cooling", "Heating", "Off"], + "values": ["AUTO", "COOL", "HEAT", "OFF"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/zone_mode" + } + } + }, + { + "type": "run_mode", + "title": "Run Mode", + "current_value": "permanent_hold", + "options": [ + { + "value": "permanent_hold", + "label": "Permanent Hold" + }, + { + "value": "run_schedule", + "label": "Run Schedule" + } + ], + "labels": ["Permanent Hold", "Run Schedule"], + "values": ["permanent_hold", "run_schedule"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/run_mode" + } + } + }, + { + "type": "scheduling_enabled", + "title": "Scheduling", + "current_value": true, + "options": [ + { + "value": true, + "label": "ON" + }, + { + "value": false, + "label": "OFF" + } + ], + "labels": ["ON", "OFF"], + "values": [true, false], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/scheduling_enabled" + } + } + } + ], + "icon": { + "name": "thermostat", + "modifiers": ["temperature-74"] + }, + "features": [ + { + "name": "thermostat", + "temperature": 74, + "status": "Damper Open", + "status_icon": { + "name": "cooling", + "modifiers": [] + }, + "actions": { + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/setpoints" + }, + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/setpoints" + } + }, + "setpoint_delta": 3, + "scale": "f", + "setpoint_increment": 1.0, + "setpoint_heat_min": 55, + "setpoint_heat_max": 90, + "setpoint_cool_min": 60, + "setpoint_cool_max": 99, + "setpoint_heat": 63, + "setpoint_cool": 71, + "system_status": "Cooling" + }, + { + "name": "connection", + "signal_strength": "unknown", + "is_connected": true + }, + { + "name": "thermostat_mode", + "label": "Zone Mode", + "value": "AUTO", + "display_value": "Auto", + "options": [ + { + "id": "thermostat_mode", + "label": "Zone Mode", + "value": "thermostat_mode", + "header": true + }, + { + "value": "AUTO", + "label": "Auto" + }, + { + "value": "COOL", + "label": "Cooling" + }, + { + "value": "HEAT", + "label": "Heating" + }, + { + "value": "OFF", + "label": "Off" + } + ], + "actions": { + "update_thermostat_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/zone_mode" + } + } + }, + { + "name": "thermostat_run_mode", + "label": "Run Mode", + "options": [ + { + "id": "thermostat_run_mode", + "label": "Run Mode", + "value": "thermostat_run_mode", + "header": true + }, + { + "id": "info_text", + "label": "Follow or override the schedule.", + "value": "info_text", + "info": true + }, + { + "value": "permanent_hold", + "label": "Permanent Hold" + }, + { + "value": "run_schedule", + "label": "Run Schedule" + } + ], + "value": "permanent_hold", + "display_value": "Hold", + "actions": { + "update_thermostat_run_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/run_mode" + } + } + }, + { + "name": "schedule", + "enabled": true, + "max_period_name_length": 10, + "setpoint_increment": 1, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83394130\u0026house_id=123456", + "actions": { + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83394130", + "method": "POST" + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83394130", + "method": "POST" + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83394130", + "method": "GET" + }, + "enable_scheduling": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/scheduling_enabled", + "method": "POST", + "data": { + "value": true + } + } + }, + "can_add_remove_periods": true, + "max_periods_per_day": 4 + } + ], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130" + } + } + }, + { + "type": "xxl_zone", + "id": 83394136, + "name": "Nick Office", + "current_zone_mode": "AUTO", + "temperature": 73, + "setpoints": { + "heat": 63, + "cool": 79 + }, + "operating_state": "Relieving Air", + "heating_setpoint": 63, + "cooling_setpoint": 79, + "zone_status": "Relieving Air", + "settings": [ + { + "type": "preset_selected", + "title": "Preset", + "current_value": 0, + "options": [ + { + "value": 0, + "label": "None" + }, + { + "value": 1, + "label": "Home" + }, + { + "value": 2, + "label": "Away" + }, + { + "value": 3, + "label": "Sleep" + } + ], + "labels": ["None", "Home", "Away", "Sleep"], + "values": [0, 1, 2, 3], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/preset_selected" + } + } + }, + { + "type": "zone_mode", + "title": "Zone Mode", + "current_value": "AUTO", + "options": [ + { + "value": "AUTO", + "label": "Auto" + }, + { + "value": "COOL", + "label": "Cooling" + }, + { + "value": "HEAT", + "label": "Heating" + }, + { + "value": "OFF", + "label": "Off" + } + ], + "labels": ["Auto", "Cooling", "Heating", "Off"], + "values": ["AUTO", "COOL", "HEAT", "OFF"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/zone_mode" + } + } + }, + { + "type": "run_mode", + "title": "Run Mode", + "current_value": "permanent_hold", + "options": [ + { + "value": "permanent_hold", + "label": "Permanent Hold" + }, + { + "value": "run_schedule", + "label": "Run Schedule" + } + ], + "labels": ["Permanent Hold", "Run Schedule"], + "values": ["permanent_hold", "run_schedule"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/run_mode" + } + } + }, + { + "type": "scheduling_enabled", + "title": "Scheduling", + "current_value": true, + "options": [ + { + "value": true, + "label": "ON" + }, + { + "value": false, + "label": "OFF" + } + ], + "labels": ["ON", "OFF"], + "values": [true, false], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/scheduling_enabled" + } + } + } + ], + "icon": { + "name": "thermostat", + "modifiers": ["temperature-73"] + }, + "features": [ + { + "name": "thermostat", + "temperature": 73, + "status": "Relieving Air", + "status_icon": { + "name": "cooling", + "modifiers": [] + }, + "actions": { + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/setpoints" + }, + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/setpoints" + } + }, + "setpoint_delta": 3, + "scale": "f", + "setpoint_increment": 1.0, + "setpoint_heat_min": 55, + "setpoint_heat_max": 90, + "setpoint_cool_min": 60, + "setpoint_cool_max": 99, + "setpoint_heat": 63, + "setpoint_cool": 79, + "system_status": "Cooling" + }, + { + "name": "connection", + "signal_strength": "unknown", + "is_connected": true + }, + { + "name": "thermostat_mode", + "label": "Zone Mode", + "value": "AUTO", + "display_value": "Auto", + "options": [ + { + "id": "thermostat_mode", + "label": "Zone Mode", + "value": "thermostat_mode", + "header": true + }, + { + "value": "AUTO", + "label": "Auto" + }, + { + "value": "COOL", + "label": "Cooling" + }, + { + "value": "HEAT", + "label": "Heating" + }, + { + "value": "OFF", + "label": "Off" + } + ], + "actions": { + "update_thermostat_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/zone_mode" + } + } + }, + { + "name": "thermostat_run_mode", + "label": "Run Mode", + "options": [ + { + "id": "thermostat_run_mode", + "label": "Run Mode", + "value": "thermostat_run_mode", + "header": true + }, + { + "id": "info_text", + "label": "Follow or override the schedule.", + "value": "info_text", + "info": true + }, + { + "value": "permanent_hold", + "label": "Permanent Hold" + }, + { + "value": "run_schedule", + "label": "Run Schedule" + } + ], + "value": "permanent_hold", + "display_value": "Hold", + "actions": { + "update_thermostat_run_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/run_mode" + } + } + }, + { + "name": "schedule", + "enabled": true, + "max_period_name_length": 10, + "setpoint_increment": 1, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83394136\u0026house_id=123456", + "actions": { + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83394136", + "method": "POST" + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83394136", + "method": "POST" + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83394136", + "method": "GET" + }, + "enable_scheduling": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/scheduling_enabled", + "method": "POST", + "data": { + "value": true + } + } + }, + "can_add_remove_periods": true, + "max_periods_per_day": 4 + } + ], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136" + } + } + }, + { + "type": "xxl_zone", + "id": 83394127, + "name": "Snooze Room", + "current_zone_mode": "AUTO", + "temperature": 72, + "setpoints": { + "heat": 63, + "cool": 79 + }, + "operating_state": "Damper Closed", + "heating_setpoint": 63, + "cooling_setpoint": 79, + "zone_status": "Damper Closed", + "settings": [ + { + "type": "preset_selected", + "title": "Preset", + "current_value": 0, + "options": [ + { + "value": 0, + "label": "None" + }, + { + "value": 1, + "label": "Home" + }, + { + "value": 2, + "label": "Away" + }, + { + "value": 3, + "label": "Sleep" + } + ], + "labels": ["None", "Home", "Away", "Sleep"], + "values": [0, 1, 2, 3], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/preset_selected" + } + } + }, + { + "type": "zone_mode", + "title": "Zone Mode", + "current_value": "AUTO", + "options": [ + { + "value": "AUTO", + "label": "Auto" + }, + { + "value": "COOL", + "label": "Cooling" + }, + { + "value": "HEAT", + "label": "Heating" + }, + { + "value": "OFF", + "label": "Off" + } + ], + "labels": ["Auto", "Cooling", "Heating", "Off"], + "values": ["AUTO", "COOL", "HEAT", "OFF"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/zone_mode" + } + } + }, + { + "type": "run_mode", + "title": "Run Mode", + "current_value": "permanent_hold", + "options": [ + { + "value": "permanent_hold", + "label": "Permanent Hold" + }, + { + "value": "run_schedule", + "label": "Run Schedule" + } + ], + "labels": ["Permanent Hold", "Run Schedule"], + "values": ["permanent_hold", "run_schedule"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/run_mode" + } + } + }, + { + "type": "scheduling_enabled", + "title": "Scheduling", + "current_value": true, + "options": [ + { + "value": true, + "label": "ON" + }, + { + "value": false, + "label": "OFF" + } + ], + "labels": ["ON", "OFF"], + "values": [true, false], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/scheduling_enabled" + } + } + } + ], + "icon": { + "name": "thermostat", + "modifiers": ["temperature-72"] + }, + "features": [ + { + "name": "thermostat", + "temperature": 72, + "status": "Damper Closed", + "status_icon": { + "name": "cooling", + "modifiers": [] + }, + "actions": { + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/setpoints" + }, + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/setpoints" + } + }, + "setpoint_delta": 3, + "scale": "f", + "setpoint_increment": 1.0, + "setpoint_heat_min": 55, + "setpoint_heat_max": 90, + "setpoint_cool_min": 60, + "setpoint_cool_max": 99, + "setpoint_heat": 63, + "setpoint_cool": 79, + "system_status": "Cooling" + }, + { + "name": "connection", + "signal_strength": "unknown", + "is_connected": true + }, + { + "name": "thermostat_mode", + "label": "Zone Mode", + "value": "AUTO", + "display_value": "Auto", + "options": [ + { + "id": "thermostat_mode", + "label": "Zone Mode", + "value": "thermostat_mode", + "header": true + }, + { + "value": "AUTO", + "label": "Auto" + }, + { + "value": "COOL", + "label": "Cooling" + }, + { + "value": "HEAT", + "label": "Heating" + }, + { + "value": "OFF", + "label": "Off" + } + ], + "actions": { + "update_thermostat_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/zone_mode" + } + } + }, + { + "name": "thermostat_run_mode", + "label": "Run Mode", + "options": [ + { + "id": "thermostat_run_mode", + "label": "Run Mode", + "value": "thermostat_run_mode", + "header": true + }, + { + "id": "info_text", + "label": "Follow or override the schedule.", + "value": "info_text", + "info": true + }, + { + "value": "permanent_hold", + "label": "Permanent Hold" + }, + { + "value": "run_schedule", + "label": "Run Schedule" + } + ], + "value": "permanent_hold", + "display_value": "Hold", + "actions": { + "update_thermostat_run_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/run_mode" + } + } + }, + { + "name": "schedule", + "enabled": true, + "max_period_name_length": 10, + "setpoint_increment": 1, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83394127\u0026house_id=123456", + "actions": { + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83394127", + "method": "POST" + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83394127", + "method": "POST" + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83394127", + "method": "GET" + }, + "enable_scheduling": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/scheduling_enabled", + "method": "POST", + "data": { + "value": true + } + } + }, + "can_add_remove_periods": true, + "max_periods_per_day": 4 + } + ], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127" + } + } + }, + { + "type": "xxl_zone", + "id": 83394139, + "name": "Safe Room", + "current_zone_mode": "AUTO", + "temperature": 74, + "setpoints": { + "heat": 63, + "cool": 79 + }, + "operating_state": "Damper Closed", + "heating_setpoint": 63, + "cooling_setpoint": 79, + "zone_status": "Damper Closed", + "settings": [ + { + "type": "preset_selected", + "title": "Preset", + "current_value": 0, + "options": [ + { + "value": 0, + "label": "None" + }, + { + "value": 1, + "label": "Home" + }, + { + "value": 2, + "label": "Away" + }, + { + "value": 3, + "label": "Sleep" + } + ], + "labels": ["None", "Home", "Away", "Sleep"], + "values": [0, 1, 2, 3], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/preset_selected" + } + } + }, + { + "type": "zone_mode", + "title": "Zone Mode", + "current_value": "AUTO", + "options": [ + { + "value": "AUTO", + "label": "Auto" + }, + { + "value": "COOL", + "label": "Cooling" + }, + { + "value": "HEAT", + "label": "Heating" + }, + { + "value": "OFF", + "label": "Off" + } + ], + "labels": ["Auto", "Cooling", "Heating", "Off"], + "values": ["AUTO", "COOL", "HEAT", "OFF"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/zone_mode" + } + } + }, + { + "type": "run_mode", + "title": "Run Mode", + "current_value": "permanent_hold", + "options": [ + { + "value": "permanent_hold", + "label": "Permanent Hold" + }, + { + "value": "run_schedule", + "label": "Run Schedule" + } + ], + "labels": ["Permanent Hold", "Run Schedule"], + "values": ["permanent_hold", "run_schedule"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/run_mode" + } + } + }, + { + "type": "scheduling_enabled", + "title": "Scheduling", + "current_value": true, + "options": [ + { + "value": true, + "label": "ON" + }, + { + "value": false, + "label": "OFF" + } + ], + "labels": ["ON", "OFF"], + "values": [true, false], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/scheduling_enabled" + } + } + } + ], + "icon": { + "name": "thermostat", + "modifiers": ["temperature-74"] + }, + "features": [ + { + "name": "thermostat", + "temperature": 74, + "status": "Damper Closed", + "status_icon": { + "name": "cooling", + "modifiers": [] + }, + "actions": { + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/setpoints" + }, + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/setpoints" + } + }, + "setpoint_delta": 3, + "scale": "f", + "setpoint_increment": 1.0, + "setpoint_heat_min": 55, + "setpoint_heat_max": 90, + "setpoint_cool_min": 60, + "setpoint_cool_max": 99, + "setpoint_heat": 63, + "setpoint_cool": 79, + "system_status": "Cooling" + }, + { + "name": "connection", + "signal_strength": "unknown", + "is_connected": true + }, + { + "name": "thermostat_mode", + "label": "Zone Mode", + "value": "AUTO", + "display_value": "Auto", + "options": [ + { + "id": "thermostat_mode", + "label": "Zone Mode", + "value": "thermostat_mode", + "header": true + }, + { + "value": "AUTO", + "label": "Auto" + }, + { + "value": "COOL", + "label": "Cooling" + }, + { + "value": "HEAT", + "label": "Heating" + }, + { + "value": "OFF", + "label": "Off" + } + ], + "actions": { + "update_thermostat_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/zone_mode" + } + } + }, + { + "name": "thermostat_run_mode", + "label": "Run Mode", + "options": [ + { + "id": "thermostat_run_mode", + "label": "Run Mode", + "value": "thermostat_run_mode", + "header": true + }, + { + "id": "info_text", + "label": "Follow or override the schedule.", + "value": "info_text", + "info": true + }, + { + "value": "permanent_hold", + "label": "Permanent Hold" + }, + { + "value": "run_schedule", + "label": "Run Schedule" + } + ], + "value": "permanent_hold", + "display_value": "Hold", + "actions": { + "update_thermostat_run_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/run_mode" + } + } + }, + { + "name": "schedule", + "enabled": true, + "max_period_name_length": 10, + "setpoint_increment": 1, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83394139\u0026house_id=123456", + "actions": { + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83394139", + "method": "POST" + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83394139", + "method": "POST" + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83394139", + "method": "GET" + }, + "enable_scheduling": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/scheduling_enabled", + "method": "POST", + "data": { + "value": true + } + } + }, + "can_add_remove_periods": true, + "max_periods_per_day": 4 + } + ], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139" + } + } + } + ] + }, + { + "name": "thermostat_fan_mode", + "label": "Fan Mode", + "options": [ + { + "id": "thermostat_fan_mode", + "label": "Fan Mode", + "value": "thermostat_fan_mode", + "header": true + }, + { + "value": "auto", + "label": "Auto" + }, + { + "value": "on", + "label": "On" + }, + { + "value": "circulate", + "label": "Circulate" + } + ], + "value": "auto", + "display_value": "Auto", + "status_icon": { + "name": "thermostat_fan_on", + "modifiers": [] + }, + "actions": { + "update_thermostat_fan_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2293892/fan_mode" + } + } + }, + { + "name": "thermostat_compressor_speed", + "compressor_speed": 0.69 + }, + { + "name": "runtime_history", + "actions": { + "get_runtime_history": { + "method": "GET", + "href": "https://www.mynexia.com/mobile/runtime_history/2293892?report_type=daily" + }, + "get_monthly_runtime_history": { + "method": "GET", + "href": "https://www.mynexia.com/mobile/runtime_history/2293892?report_type=monthly" + } + } + } + ], + "icon": [ + { + "name": "thermostat", + "modifiers": ["temperature-73"] + }, + { + "name": "thermostat", + "modifiers": ["temperature-74"] + }, + { + "name": "thermostat", + "modifiers": ["temperature-73"] + }, + { + "name": "thermostat", + "modifiers": ["temperature-72"] + }, + { + "name": "thermostat", + "modifiers": ["temperature-74"] + } + ], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2293892" + }, + "nexia:history": { + "href": "https://www.mynexia.com/mobile/houses/123456/events?device_id=2293892" + }, + "filter_events": { + "href": "https://www.mynexia.com/mobile/houses/123456/events/collection?sys_guid=e3fc90c7-2885-4f57-ae76-99e9ec81eef0" + }, + "pending_request": { + "polling_path": "https://www.mynexia.com/backstage/announcements/967361e8aed874aa5230930fd0e0bbd8b653261e982a6e0e" + } + }, + "last_updated_at": "2020-03-11T15:15:53.000-05:00", + "settings": [ + { + "type": "fan_mode", + "title": "Fan Mode", + "current_value": "auto", + "options": [ + { + "value": "auto", + "label": "Auto" + }, + { + "value": "on", + "label": "On" + }, + { + "value": "circulate", + "label": "Circulate" + } + ], + "labels": ["Auto", "On", "Circulate"], + "values": ["auto", "on", "circulate"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2293892/fan_mode" + } + } + }, + { + "type": "fan_speed", + "title": "Fan Speed", + "current_value": 0.35, + "options": [ + { + "value": 0.35, + "label": "35%" + }, + { + "value": 0.4, + "label": "40%" + }, + { + "value": 0.45, + "label": "45%" + }, + { + "value": 0.5, + "label": "50%" + }, + { + "value": 0.55, + "label": "55%" + }, + { + "value": 0.6, + "label": "60%" + }, + { + "value": 0.65, + "label": "65%" + }, + { + "value": 0.7, + "label": "70%" + }, + { + "value": 0.75, + "label": "75%" + }, + { + "value": 0.8, + "label": "80%" + }, + { + "value": 0.85, + "label": "85%" + }, + { + "value": 0.9, + "label": "90%" + }, + { + "value": 0.95, + "label": "95%" + }, + { + "value": 1.0, + "label": "100%" + } + ], + "labels": [ + "35%", + "40%", + "45%", + "50%", + "55%", + "60%", + "65%", + "70%", + "75%", + "80%", + "85%", + "90%", + "95%", + "100%" + ], + "values": [ + 0.35, 0.4, 0.45, 0.5, 0.55, 0.6, 0.65, 0.7, 0.75, 0.8, + 0.85, 0.9, 0.95, 1.0 + ], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2293892/fan_speed" + } + } + }, + { + "type": "fan_circulation_time", + "title": "Fan Circulation Time", + "current_value": 30, + "options": [ + { + "value": 10, + "label": "10 minutes" + }, + { + "value": 15, + "label": "15 minutes" + }, + { + "value": 20, + "label": "20 minutes" + }, + { + "value": 25, + "label": "25 minutes" + }, + { + "value": 30, + "label": "30 minutes" + }, + { + "value": 35, + "label": "35 minutes" + }, + { + "value": 40, + "label": "40 minutes" + }, + { + "value": 45, + "label": "45 minutes" + }, + { + "value": 50, + "label": "50 minutes" + }, + { + "value": 55, + "label": "55 minutes" + } + ], + "labels": [ + "10 minutes", + "15 minutes", + "20 minutes", + "25 minutes", + "30 minutes", + "35 minutes", + "40 minutes", + "45 minutes", + "50 minutes", + "55 minutes" + ], + "values": [10, 15, 20, 25, 30, 35, 40, 45, 50, 55], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2293892/fan_circulation_time" + } + } + }, + { + "type": "air_cleaner_mode", + "title": "Air Cleaner Mode", + "current_value": "auto", + "options": [ + { + "value": "auto", + "label": "Auto" + }, + { + "value": "quick", + "label": "Quick" + }, + { + "value": "allergy", + "label": "Allergy" + } + ], + "labels": ["Auto", "Quick", "Allergy"], + "values": ["auto", "quick", "allergy"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2293892/air_cleaner_mode" + } + } + }, + { + "type": "dehumidify", + "title": "Cooling Dehumidify Set Point", + "current_value": 0.45, + "options": [ + { + "value": 0.35, + "label": "35%" + }, + { + "value": 0.4, + "label": "40%" + }, + { + "value": 0.45, + "label": "45%" + }, + { + "value": 0.5, + "label": "50%" + }, + { + "value": 0.55, + "label": "55%" + }, + { + "value": 0.6, + "label": "60%" + }, + { + "value": 0.65, + "label": "65%" + } + ], + "labels": ["35%", "40%", "45%", "50%", "55%", "60%", "65%"], + "values": [0.35, 0.4, 0.45, 0.5, 0.55, 0.6, 0.65], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2293892/dehumidify" + } + } + }, + { + "type": "scale", + "title": "Temperature Scale", + "current_value": "f", + "options": [ + { + "value": "f", + "label": "F" + }, + { + "value": "c", + "label": "C" + } + ], + "labels": ["F", "C"], + "values": ["f", "c"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2293892/scale" + } + } + } + ], + "status_secondary": null, + "status_tertiary": null, + "type": "xxl_thermostat", + "has_outdoor_temperature": true, + "outdoor_temperature": "87", + "has_indoor_humidity": true, + "connected": true, + "indoor_humidity": "52", + "system_status": "Cooling", + "delta": 3, + "zones": [ + { + "type": "xxl_zone", + "id": 83394133, + "name": "Bath Closet", + "current_zone_mode": "AUTO", + "temperature": 73, + "setpoints": { + "heat": 63, + "cool": 79 + }, + "operating_state": "Relieving Air", + "heating_setpoint": 63, + "cooling_setpoint": 79, + "zone_status": "Relieving Air", + "settings": [ + { + "type": "preset_selected", + "title": "Preset", + "current_value": 0, + "options": [ + { + "value": 0, + "label": "None" + }, + { + "value": 1, + "label": "Home" + }, + { + "value": 2, + "label": "Away" + }, + { + "value": 3, + "label": "Sleep" + } + ], + "labels": ["None", "Home", "Away", "Sleep"], + "values": [0, 1, 2, 3], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/preset_selected" + } + } + }, + { + "type": "zone_mode", + "title": "Zone Mode", + "current_value": "AUTO", + "options": [ + { + "value": "AUTO", + "label": "Auto" + }, + { + "value": "COOL", + "label": "Cooling" + }, + { + "value": "HEAT", + "label": "Heating" + }, + { + "value": "OFF", + "label": "Off" + } + ], + "labels": ["Auto", "Cooling", "Heating", "Off"], + "values": ["AUTO", "COOL", "HEAT", "OFF"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/zone_mode" + } + } + }, + { + "type": "run_mode", + "title": "Run Mode", + "current_value": "permanent_hold", + "options": [ + { + "value": "permanent_hold", + "label": "Permanent Hold" + }, + { + "value": "run_schedule", + "label": "Run Schedule" + } + ], + "labels": ["Permanent Hold", "Run Schedule"], + "values": ["permanent_hold", "run_schedule"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/run_mode" + } + } + }, + { + "type": "scheduling_enabled", + "title": "Scheduling", + "current_value": true, + "options": [ + { + "value": true, + "label": "ON" + }, + { + "value": false, + "label": "OFF" + } + ], + "labels": ["ON", "OFF"], + "values": [true, false], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/scheduling_enabled" + } + } + } + ], + "icon": { + "name": "thermostat", + "modifiers": ["temperature-73"] + }, + "features": [ + { + "name": "thermostat", + "temperature": 73, + "status": "Relieving Air", + "status_icon": { + "name": "cooling", + "modifiers": [] + }, + "actions": { + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/setpoints" + }, + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/setpoints" + } + }, + "setpoint_delta": 3, + "scale": "f", + "setpoint_increment": 1.0, + "setpoint_heat_min": 55, + "setpoint_heat_max": 90, + "setpoint_cool_min": 60, + "setpoint_cool_max": 99, + "setpoint_heat": 63, + "setpoint_cool": 79, + "system_status": "Cooling" + }, + { + "name": "connection", + "signal_strength": "unknown", + "is_connected": true + }, + { + "name": "thermostat_mode", + "label": "Zone Mode", + "value": "AUTO", + "display_value": "Auto", + "options": [ + { + "id": "thermostat_mode", + "label": "Zone Mode", + "value": "thermostat_mode", + "header": true + }, + { + "value": "AUTO", + "label": "Auto" + }, + { + "value": "COOL", + "label": "Cooling" + }, + { + "value": "HEAT", + "label": "Heating" + }, + { + "value": "OFF", + "label": "Off" + } + ], + "actions": { + "update_thermostat_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/zone_mode" + } + } + }, + { + "name": "thermostat_run_mode", + "label": "Run Mode", + "options": [ + { + "id": "thermostat_run_mode", + "label": "Run Mode", + "value": "thermostat_run_mode", + "header": true + }, + { + "id": "info_text", + "label": "Follow or override the schedule.", + "value": "info_text", + "info": true + }, + { + "value": "permanent_hold", + "label": "Permanent Hold" + }, + { + "value": "run_schedule", + "label": "Run Schedule" + } + ], + "value": "permanent_hold", + "display_value": "Hold", + "actions": { + "update_thermostat_run_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/run_mode" + } + } + }, + { + "name": "schedule", + "enabled": true, + "max_period_name_length": 10, + "setpoint_increment": 1, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83394133\u0026house_id=123456", + "actions": { + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83394133", + "method": "POST" + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83394133", + "method": "POST" + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83394133", + "method": "GET" + }, + "enable_scheduling": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/scheduling_enabled", + "method": "POST", + "data": { + "value": true + } + } + }, + "can_add_remove_periods": true, + "max_periods_per_day": 4 + } + ], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133" + } + } + }, + { + "type": "xxl_zone", + "id": 83394130, + "name": "Master", + "current_zone_mode": "AUTO", + "temperature": 74, + "setpoints": { + "heat": 63, + "cool": 71 + }, + "operating_state": "Damper Open", + "heating_setpoint": 63, + "cooling_setpoint": 71, + "zone_status": "Damper Open", + "settings": [ + { + "type": "preset_selected", + "title": "Preset", + "current_value": 0, + "options": [ + { + "value": 0, + "label": "None" + }, + { + "value": 1, + "label": "Home" + }, + { + "value": 2, + "label": "Away" + }, + { + "value": 3, + "label": "Sleep" + } + ], + "labels": ["None", "Home", "Away", "Sleep"], + "values": [0, 1, 2, 3], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/preset_selected" + } + } + }, + { + "type": "zone_mode", + "title": "Zone Mode", + "current_value": "AUTO", + "options": [ + { + "value": "AUTO", + "label": "Auto" + }, + { + "value": "COOL", + "label": "Cooling" + }, + { + "value": "HEAT", + "label": "Heating" + }, + { + "value": "OFF", + "label": "Off" + } + ], + "labels": ["Auto", "Cooling", "Heating", "Off"], + "values": ["AUTO", "COOL", "HEAT", "OFF"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/zone_mode" + } + } + }, + { + "type": "run_mode", + "title": "Run Mode", + "current_value": "permanent_hold", + "options": [ + { + "value": "permanent_hold", + "label": "Permanent Hold" + }, + { + "value": "run_schedule", + "label": "Run Schedule" + } + ], + "labels": ["Permanent Hold", "Run Schedule"], + "values": ["permanent_hold", "run_schedule"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/run_mode" + } + } + }, + { + "type": "scheduling_enabled", + "title": "Scheduling", + "current_value": true, + "options": [ + { + "value": true, + "label": "ON" + }, + { + "value": false, + "label": "OFF" + } + ], + "labels": ["ON", "OFF"], + "values": [true, false], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/scheduling_enabled" + } + } + } + ], + "icon": { + "name": "thermostat", + "modifiers": ["temperature-74"] + }, + "features": [ + { + "name": "thermostat", + "temperature": 74, + "status": "Damper Open", + "status_icon": { + "name": "cooling", + "modifiers": [] + }, + "actions": { + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/setpoints" + }, + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/setpoints" + } + }, + "setpoint_delta": 3, + "scale": "f", + "setpoint_increment": 1.0, + "setpoint_heat_min": 55, + "setpoint_heat_max": 90, + "setpoint_cool_min": 60, + "setpoint_cool_max": 99, + "setpoint_heat": 63, + "setpoint_cool": 71, + "system_status": "Cooling" + }, + { + "name": "connection", + "signal_strength": "unknown", + "is_connected": true + }, + { + "name": "thermostat_mode", + "label": "Zone Mode", + "value": "AUTO", + "display_value": "Auto", + "options": [ + { + "id": "thermostat_mode", + "label": "Zone Mode", + "value": "thermostat_mode", + "header": true + }, + { + "value": "AUTO", + "label": "Auto" + }, + { + "value": "COOL", + "label": "Cooling" + }, + { + "value": "HEAT", + "label": "Heating" + }, + { + "value": "OFF", + "label": "Off" + } + ], + "actions": { + "update_thermostat_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/zone_mode" + } + } + }, + { + "name": "thermostat_run_mode", + "label": "Run Mode", + "options": [ + { + "id": "thermostat_run_mode", + "label": "Run Mode", + "value": "thermostat_run_mode", + "header": true + }, + { + "id": "info_text", + "label": "Follow or override the schedule.", + "value": "info_text", + "info": true + }, + { + "value": "permanent_hold", + "label": "Permanent Hold" + }, + { + "value": "run_schedule", + "label": "Run Schedule" + } + ], + "value": "permanent_hold", + "display_value": "Hold", + "actions": { + "update_thermostat_run_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/run_mode" + } + } + }, + { + "name": "schedule", + "enabled": true, + "max_period_name_length": 10, + "setpoint_increment": 1, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83394130\u0026house_id=123456", + "actions": { + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83394130", + "method": "POST" + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83394130", + "method": "POST" + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83394130", + "method": "GET" + }, + "enable_scheduling": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/scheduling_enabled", + "method": "POST", + "data": { + "value": true + } + } + }, + "can_add_remove_periods": true, + "max_periods_per_day": 4 + } + ], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130" + } + } + }, + { + "type": "xxl_zone", + "id": 83394136, + "name": "Nick Office", + "current_zone_mode": "AUTO", + "temperature": 73, + "setpoints": { + "heat": 63, + "cool": 79 + }, + "operating_state": "Relieving Air", + "heating_setpoint": 63, + "cooling_setpoint": 79, + "zone_status": "Relieving Air", + "settings": [ + { + "type": "preset_selected", + "title": "Preset", + "current_value": 0, + "options": [ + { + "value": 0, + "label": "None" + }, + { + "value": 1, + "label": "Home" + }, + { + "value": 2, + "label": "Away" + }, + { + "value": 3, + "label": "Sleep" + } + ], + "labels": ["None", "Home", "Away", "Sleep"], + "values": [0, 1, 2, 3], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/preset_selected" + } + } + }, + { + "type": "zone_mode", + "title": "Zone Mode", + "current_value": "AUTO", + "options": [ + { + "value": "AUTO", + "label": "Auto" + }, + { + "value": "COOL", + "label": "Cooling" + }, + { + "value": "HEAT", + "label": "Heating" + }, + { + "value": "OFF", + "label": "Off" + } + ], + "labels": ["Auto", "Cooling", "Heating", "Off"], + "values": ["AUTO", "COOL", "HEAT", "OFF"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/zone_mode" + } + } + }, + { + "type": "run_mode", + "title": "Run Mode", + "current_value": "permanent_hold", + "options": [ + { + "value": "permanent_hold", + "label": "Permanent Hold" + }, + { + "value": "run_schedule", + "label": "Run Schedule" + } + ], + "labels": ["Permanent Hold", "Run Schedule"], + "values": ["permanent_hold", "run_schedule"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/run_mode" + } + } + }, + { + "type": "scheduling_enabled", + "title": "Scheduling", + "current_value": true, + "options": [ + { + "value": true, + "label": "ON" + }, + { + "value": false, + "label": "OFF" + } + ], + "labels": ["ON", "OFF"], + "values": [true, false], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/scheduling_enabled" + } + } + } + ], + "icon": { + "name": "thermostat", + "modifiers": ["temperature-73"] + }, + "features": [ + { + "name": "thermostat", + "temperature": 73, + "status": "Relieving Air", + "status_icon": { + "name": "cooling", + "modifiers": [] + }, + "actions": { + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/setpoints" + }, + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/setpoints" + } + }, + "setpoint_delta": 3, + "scale": "f", + "setpoint_increment": 1.0, + "setpoint_heat_min": 55, + "setpoint_heat_max": 90, + "setpoint_cool_min": 60, + "setpoint_cool_max": 99, + "setpoint_heat": 63, + "setpoint_cool": 79, + "system_status": "Cooling" + }, + { + "name": "connection", + "signal_strength": "unknown", + "is_connected": true + }, + { + "name": "thermostat_mode", + "label": "Zone Mode", + "value": "AUTO", + "display_value": "Auto", + "options": [ + { + "id": "thermostat_mode", + "label": "Zone Mode", + "value": "thermostat_mode", + "header": true + }, + { + "value": "AUTO", + "label": "Auto" + }, + { + "value": "COOL", + "label": "Cooling" + }, + { + "value": "HEAT", + "label": "Heating" + }, + { + "value": "OFF", + "label": "Off" + } + ], + "actions": { + "update_thermostat_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/zone_mode" + } + } + }, + { + "name": "thermostat_run_mode", + "label": "Run Mode", + "options": [ + { + "id": "thermostat_run_mode", + "label": "Run Mode", + "value": "thermostat_run_mode", + "header": true + }, + { + "id": "info_text", + "label": "Follow or override the schedule.", + "value": "info_text", + "info": true + }, + { + "value": "permanent_hold", + "label": "Permanent Hold" + }, + { + "value": "run_schedule", + "label": "Run Schedule" + } + ], + "value": "permanent_hold", + "display_value": "Hold", + "actions": { + "update_thermostat_run_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/run_mode" + } + } + }, + { + "name": "schedule", + "enabled": true, + "max_period_name_length": 10, + "setpoint_increment": 1, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83394136\u0026house_id=123456", + "actions": { + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83394136", + "method": "POST" + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83394136", + "method": "POST" + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83394136", + "method": "GET" + }, + "enable_scheduling": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/scheduling_enabled", + "method": "POST", + "data": { + "value": true + } + } + }, + "can_add_remove_periods": true, + "max_periods_per_day": 4 + } + ], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136" + } + } + }, + { + "type": "xxl_zone", + "id": 83394127, + "name": "Snooze Room", + "current_zone_mode": "AUTO", + "temperature": 72, + "setpoints": { + "heat": 63, + "cool": 79 + }, + "operating_state": "Damper Closed", + "heating_setpoint": 63, + "cooling_setpoint": 79, + "zone_status": "Damper Closed", + "settings": [ + { + "type": "preset_selected", + "title": "Preset", + "current_value": 0, + "options": [ + { + "value": 0, + "label": "None" + }, + { + "value": 1, + "label": "Home" + }, + { + "value": 2, + "label": "Away" + }, + { + "value": 3, + "label": "Sleep" + } + ], + "labels": ["None", "Home", "Away", "Sleep"], + "values": [0, 1, 2, 3], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/preset_selected" + } + } + }, + { + "type": "zone_mode", + "title": "Zone Mode", + "current_value": "AUTO", + "options": [ + { + "value": "AUTO", + "label": "Auto" + }, + { + "value": "COOL", + "label": "Cooling" + }, + { + "value": "HEAT", + "label": "Heating" + }, + { + "value": "OFF", + "label": "Off" + } + ], + "labels": ["Auto", "Cooling", "Heating", "Off"], + "values": ["AUTO", "COOL", "HEAT", "OFF"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/zone_mode" + } + } + }, + { + "type": "run_mode", + "title": "Run Mode", + "current_value": "permanent_hold", + "options": [ + { + "value": "permanent_hold", + "label": "Permanent Hold" + }, + { + "value": "run_schedule", + "label": "Run Schedule" + } + ], + "labels": ["Permanent Hold", "Run Schedule"], + "values": ["permanent_hold", "run_schedule"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/run_mode" + } + } + }, + { + "type": "scheduling_enabled", + "title": "Scheduling", + "current_value": true, + "options": [ + { + "value": true, + "label": "ON" + }, + { + "value": false, + "label": "OFF" + } + ], + "labels": ["ON", "OFF"], + "values": [true, false], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/scheduling_enabled" + } + } + } + ], + "icon": { + "name": "thermostat", + "modifiers": ["temperature-72"] + }, + "features": [ + { + "name": "thermostat", + "temperature": 72, + "status": "Damper Closed", + "status_icon": { + "name": "cooling", + "modifiers": [] + }, + "actions": { + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/setpoints" + }, + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/setpoints" + } + }, + "setpoint_delta": 3, + "scale": "f", + "setpoint_increment": 1.0, + "setpoint_heat_min": 55, + "setpoint_heat_max": 90, + "setpoint_cool_min": 60, + "setpoint_cool_max": 99, + "setpoint_heat": 63, + "setpoint_cool": 79, + "system_status": "Cooling" + }, + { + "name": "connection", + "signal_strength": "unknown", + "is_connected": true + }, + { + "name": "thermostat_mode", + "label": "Zone Mode", + "value": "AUTO", + "display_value": "Auto", + "options": [ + { + "id": "thermostat_mode", + "label": "Zone Mode", + "value": "thermostat_mode", + "header": true + }, + { + "value": "AUTO", + "label": "Auto" + }, + { + "value": "COOL", + "label": "Cooling" + }, + { + "value": "HEAT", + "label": "Heating" + }, + { + "value": "OFF", + "label": "Off" + } + ], + "actions": { + "update_thermostat_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/zone_mode" + } + } + }, + { + "name": "thermostat_run_mode", + "label": "Run Mode", + "options": [ + { + "id": "thermostat_run_mode", + "label": "Run Mode", + "value": "thermostat_run_mode", + "header": true + }, + { + "id": "info_text", + "label": "Follow or override the schedule.", + "value": "info_text", + "info": true + }, + { + "value": "permanent_hold", + "label": "Permanent Hold" + }, + { + "value": "run_schedule", + "label": "Run Schedule" + } + ], + "value": "permanent_hold", + "display_value": "Hold", + "actions": { + "update_thermostat_run_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/run_mode" + } + } + }, + { + "name": "schedule", + "enabled": true, + "max_period_name_length": 10, + "setpoint_increment": 1, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83394127\u0026house_id=123456", + "actions": { + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83394127", + "method": "POST" + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83394127", + "method": "POST" + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83394127", + "method": "GET" + }, + "enable_scheduling": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/scheduling_enabled", + "method": "POST", + "data": { + "value": true + } + } + }, + "can_add_remove_periods": true, + "max_periods_per_day": 4 + } + ], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127" + } + } + }, + { + "type": "xxl_zone", + "id": 83394139, + "name": "Safe Room", + "current_zone_mode": "AUTO", + "temperature": 74, + "setpoints": { + "heat": 63, + "cool": 79 + }, + "operating_state": "Damper Closed", + "heating_setpoint": 63, + "cooling_setpoint": 79, + "zone_status": "Damper Closed", + "settings": [ + { + "type": "preset_selected", + "title": "Preset", + "current_value": 0, + "options": [ + { + "value": 0, + "label": "None" + }, + { + "value": 1, + "label": "Home" + }, + { + "value": 2, + "label": "Away" + }, + { + "value": 3, + "label": "Sleep" + } + ], + "labels": ["None", "Home", "Away", "Sleep"], + "values": [0, 1, 2, 3], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/preset_selected" + } + } + }, + { + "type": "zone_mode", + "title": "Zone Mode", + "current_value": "AUTO", + "options": [ + { + "value": "AUTO", + "label": "Auto" + }, + { + "value": "COOL", + "label": "Cooling" + }, + { + "value": "HEAT", + "label": "Heating" + }, + { + "value": "OFF", + "label": "Off" + } + ], + "labels": ["Auto", "Cooling", "Heating", "Off"], + "values": ["AUTO", "COOL", "HEAT", "OFF"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/zone_mode" + } + } + }, + { + "type": "run_mode", + "title": "Run Mode", + "current_value": "permanent_hold", + "options": [ + { + "value": "permanent_hold", + "label": "Permanent Hold" + }, + { + "value": "run_schedule", + "label": "Run Schedule" + } + ], + "labels": ["Permanent Hold", "Run Schedule"], + "values": ["permanent_hold", "run_schedule"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/run_mode" + } + } + }, + { + "type": "scheduling_enabled", + "title": "Scheduling", + "current_value": true, + "options": [ + { + "value": true, + "label": "ON" + }, + { + "value": false, + "label": "OFF" + } + ], + "labels": ["ON", "OFF"], + "values": [true, false], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/scheduling_enabled" + } + } + } + ], + "icon": { + "name": "thermostat", + "modifiers": ["temperature-74"] + }, + "features": [ + { + "name": "thermostat", + "temperature": 74, + "status": "Damper Closed", + "status_icon": { + "name": "cooling", + "modifiers": [] + }, + "actions": { + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/setpoints" + }, + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/setpoints" + } + }, + "setpoint_delta": 3, + "scale": "f", + "setpoint_increment": 1.0, + "setpoint_heat_min": 55, + "setpoint_heat_max": 90, + "setpoint_cool_min": 60, + "setpoint_cool_max": 99, + "setpoint_heat": 63, + "setpoint_cool": 79, + "system_status": "Cooling" + }, + { + "name": "connection", + "signal_strength": "unknown", + "is_connected": true + }, + { + "name": "thermostat_mode", + "label": "Zone Mode", + "value": "AUTO", + "display_value": "Auto", + "options": [ + { + "id": "thermostat_mode", + "label": "Zone Mode", + "value": "thermostat_mode", + "header": true + }, + { + "value": "AUTO", + "label": "Auto" + }, + { + "value": "COOL", + "label": "Cooling" + }, + { + "value": "HEAT", + "label": "Heating" + }, + { + "value": "OFF", + "label": "Off" + } + ], + "actions": { + "update_thermostat_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/zone_mode" + } + } + }, + { + "name": "thermostat_run_mode", + "label": "Run Mode", + "options": [ + { + "id": "thermostat_run_mode", + "label": "Run Mode", + "value": "thermostat_run_mode", + "header": true + }, + { + "id": "info_text", + "label": "Follow or override the schedule.", + "value": "info_text", + "info": true + }, + { + "value": "permanent_hold", + "label": "Permanent Hold" + }, + { + "value": "run_schedule", + "label": "Run Schedule" + } + ], + "value": "permanent_hold", + "display_value": "Hold", + "actions": { + "update_thermostat_run_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/run_mode" + } + } + }, + { + "name": "schedule", + "enabled": true, + "max_period_name_length": 10, + "setpoint_increment": 1, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83394139\u0026house_id=123456", + "actions": { + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83394139", + "method": "POST" + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83394139", + "method": "POST" + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83394139", + "method": "GET" + }, + "enable_scheduling": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/scheduling_enabled", + "method": "POST", + "data": { + "value": true + } + } + }, + "can_add_remove_periods": true, + "max_periods_per_day": 4 + } + ], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139" + } + } + } + ] + }, + { + "id": 2059652, + "name": "Upstairs West Wing", + "name_editable": true, + "features": [ + { + "name": "advanced_info", + "items": [ + { + "type": "label_value", + "label": "Model", + "value": "XL1050" + }, + { + "type": "label_value", + "label": "AUID", + "value": "02853DF0" + }, + { + "type": "label_value", + "label": "Firmware Build Number", + "value": "1581321824" + }, + { + "type": "label_value", + "label": "Firmware Build Date", + "value": "2020-02-10 08:03:44 UTC" + }, + { + "type": "label_value", + "label": "Firmware Version", + "value": "5.9.1" + }, + { + "type": "label_value", + "label": "Zoning Enabled", + "value": "yes" + } + ] + }, + { + "name": "thermostat", + "temperature": 77, + "status": "System Idle", + "status_icon": null, + "actions": {}, + "setpoint_delta": 3, + "scale": "f", + "setpoint_increment": 1.0, + "setpoint_heat_min": 55, + "setpoint_heat_max": 90, + "setpoint_cool_min": 60, + "setpoint_cool_max": 99 + }, + { + "name": "connection", + "signal_strength": "unknown", + "is_connected": true + }, + { + "name": "group", + "members": [ + { + "type": "xxl_zone", + "id": 83260991, + "name": "Hallway", + "current_zone_mode": "OFF", + "temperature": 77, + "setpoints": { + "heat": 63, + "cool": 80 + }, + "operating_state": "", + "heating_setpoint": 63, + "cooling_setpoint": 80, + "zone_status": "", + "settings": [ + { + "type": "preset_selected", + "title": "Preset", + "current_value": 0, + "options": [ + { + "value": 0, + "label": "None" + }, + { + "value": 1, + "label": "Home" + }, + { + "value": 2, + "label": "Away" + }, + { + "value": 3, + "label": "Sleep" + } + ], + "labels": ["None", "Home", "Away", "Sleep"], + "values": [0, 1, 2, 3], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260991/preset_selected" + } + } + }, + { + "type": "zone_mode", + "title": "Zone Mode", + "current_value": "OFF", + "options": [ + { + "value": "AUTO", + "label": "Auto" + }, + { + "value": "COOL", + "label": "Cooling" + }, + { + "value": "HEAT", + "label": "Heating" + }, + { + "value": "OFF", + "label": "Off" + } + ], + "labels": ["Auto", "Cooling", "Heating", "Off"], + "values": ["AUTO", "COOL", "HEAT", "OFF"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260991/zone_mode" + } + } + }, + { + "type": "run_mode", + "title": "Run Mode", + "current_value": "permanent_hold", + "options": [ + { + "value": "permanent_hold", + "label": "Permanent Hold" + }, + { + "value": "run_schedule", + "label": "Run Schedule" + } + ], + "labels": ["Permanent Hold", "Run Schedule"], + "values": ["permanent_hold", "run_schedule"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260991/run_mode" + } + } + }, + { + "type": "scheduling_enabled", + "title": "Scheduling", + "current_value": true, + "options": [ + { + "value": true, + "label": "ON" + }, + { + "value": false, + "label": "OFF" + } + ], + "labels": ["ON", "OFF"], + "values": [true, false], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260991/scheduling_enabled" + } + } + } + ], + "icon": { + "name": "thermostat", + "modifiers": ["temperature-77"] + }, + "features": [ + { + "name": "thermostat", + "temperature": 77, + "status": "", + "status_icon": null, + "actions": {}, + "setpoint_delta": 3, + "scale": "f", + "setpoint_increment": 1.0, + "setpoint_heat_min": 55, + "setpoint_heat_max": 90, + "setpoint_cool_min": 60, + "setpoint_cool_max": 99, + "system_status": "System Idle" + }, + { + "name": "connection", + "signal_strength": "unknown", + "is_connected": true + }, + { + "name": "thermostat_mode", + "label": "Zone Mode", + "value": "OFF", + "display_value": "Off", + "options": [ + { + "id": "thermostat_mode", + "label": "Zone Mode", + "value": "thermostat_mode", + "header": true + }, + { + "value": "AUTO", + "label": "Auto" + }, + { + "value": "COOL", + "label": "Cooling" + }, + { + "value": "HEAT", + "label": "Heating" + }, + { + "value": "OFF", + "label": "Off" + } + ], + "actions": { + "update_thermostat_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83260991/zone_mode" + } + } + }, + { + "name": "thermostat_run_mode", + "label": "Run Mode", + "options": [ + { + "id": "thermostat_run_mode", + "label": "Run Mode", + "value": "thermostat_run_mode", + "header": true + }, + { + "id": "info_text", + "label": "Follow or override the schedule.", + "value": "info_text", + "info": true + }, + { + "value": "permanent_hold", + "label": "Permanent Hold" + }, + { + "value": "run_schedule", + "label": "Run Schedule" + } + ], + "value": "permanent_hold", + "display_value": "Hold", + "actions": { + "update_thermostat_run_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83260991/run_mode" + } + } + }, + { + "name": "schedule", + "enabled": true, + "max_period_name_length": 10, + "setpoint_increment": 1, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83260991\u0026house_id=123456", + "actions": { + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83260991", + "method": "POST" + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83260991", + "method": "POST" + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83260991", + "method": "GET" + }, + "enable_scheduling": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260991/scheduling_enabled", + "method": "POST", + "data": { + "value": true + } + } + }, + "can_add_remove_periods": true, + "max_periods_per_day": 4 + } + ], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260991" + } + } + }, + { + "type": "xxl_zone", + "id": 83260994, + "name": "Mid Bedroom", + "current_zone_mode": "AUTO", + "temperature": 74, + "setpoints": { + "heat": 63, + "cool": 81 + }, + "operating_state": "", + "heating_setpoint": 63, + "cooling_setpoint": 81, + "zone_status": "", + "settings": [ + { + "type": "preset_selected", + "title": "Preset", + "current_value": 0, + "options": [ + { + "value": 0, + "label": "None" + }, + { + "value": 1, + "label": "Home" + }, + { + "value": 2, + "label": "Away" + }, + { + "value": 3, + "label": "Sleep" + } + ], + "labels": ["None", "Home", "Away", "Sleep"], + "values": [0, 1, 2, 3], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260994/preset_selected" + } + } + }, + { + "type": "zone_mode", + "title": "Zone Mode", + "current_value": "AUTO", + "options": [ + { + "value": "AUTO", + "label": "Auto" + }, + { + "value": "COOL", + "label": "Cooling" + }, + { + "value": "HEAT", + "label": "Heating" + }, + { + "value": "OFF", + "label": "Off" + } + ], + "labels": ["Auto", "Cooling", "Heating", "Off"], + "values": ["AUTO", "COOL", "HEAT", "OFF"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260994/zone_mode" + } + } + }, + { + "type": "run_mode", + "title": "Run Mode", + "current_value": "permanent_hold", + "options": [ + { + "value": "permanent_hold", + "label": "Permanent Hold" + }, + { + "value": "run_schedule", + "label": "Run Schedule" + } + ], + "labels": ["Permanent Hold", "Run Schedule"], + "values": ["permanent_hold", "run_schedule"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260994/run_mode" + } + } + }, + { + "type": "scheduling_enabled", + "title": "Scheduling", + "current_value": true, + "options": [ + { + "value": true, + "label": "ON" + }, + { + "value": false, + "label": "OFF" + } + ], + "labels": ["ON", "OFF"], + "values": [true, false], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260994/scheduling_enabled" + } + } + } + ], + "icon": { + "name": "thermostat", + "modifiers": ["temperature-74"] + }, + "features": [ + { + "name": "thermostat", + "temperature": 74, + "status": "", + "status_icon": null, + "actions": { + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260994/setpoints" + }, + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260994/setpoints" + } + }, + "setpoint_delta": 3, + "scale": "f", + "setpoint_increment": 1.0, + "setpoint_heat_min": 55, + "setpoint_heat_max": 90, + "setpoint_cool_min": 60, + "setpoint_cool_max": 99, + "setpoint_heat": 63, + "setpoint_cool": 81, + "system_status": "System Idle" + }, + { + "name": "connection", + "signal_strength": "unknown", + "is_connected": true + }, + { + "name": "thermostat_mode", + "label": "Zone Mode", + "value": "AUTO", + "display_value": "Auto", + "options": [ + { + "id": "thermostat_mode", + "label": "Zone Mode", + "value": "thermostat_mode", + "header": true + }, + { + "value": "AUTO", + "label": "Auto" + }, + { + "value": "COOL", + "label": "Cooling" + }, + { + "value": "HEAT", + "label": "Heating" + }, + { + "value": "OFF", + "label": "Off" + } + ], + "actions": { + "update_thermostat_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83260994/zone_mode" + } + } + }, + { + "name": "thermostat_run_mode", + "label": "Run Mode", + "options": [ + { + "id": "thermostat_run_mode", + "label": "Run Mode", + "value": "thermostat_run_mode", + "header": true + }, + { + "id": "info_text", + "label": "Follow or override the schedule.", + "value": "info_text", + "info": true + }, + { + "value": "permanent_hold", + "label": "Permanent Hold" + }, + { + "value": "run_schedule", + "label": "Run Schedule" + } + ], + "value": "permanent_hold", + "display_value": "Hold", + "actions": { + "update_thermostat_run_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83260994/run_mode" + } + } + }, + { + "name": "schedule", + "enabled": true, + "max_period_name_length": 10, + "setpoint_increment": 1, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83260994\u0026house_id=123456", + "actions": { + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83260994", + "method": "POST" + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83260994", + "method": "POST" + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83260994", + "method": "GET" + }, + "enable_scheduling": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260994/scheduling_enabled", + "method": "POST", + "data": { + "value": true + } + } + }, + "can_add_remove_periods": true, + "max_periods_per_day": 4 + } + ], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260994" + } + } + }, + { + "type": "xxl_zone", + "id": 83260997, + "name": "West Bedroom", + "current_zone_mode": "AUTO", + "temperature": 75, + "setpoints": { + "heat": 63, + "cool": 81 + }, + "operating_state": "", + "heating_setpoint": 63, + "cooling_setpoint": 81, + "zone_status": "", + "settings": [ + { + "type": "preset_selected", + "title": "Preset", + "current_value": 0, + "options": [ + { + "value": 0, + "label": "None" + }, + { + "value": 1, + "label": "Home" + }, + { + "value": 2, + "label": "Away" + }, + { + "value": 3, + "label": "Sleep" + } + ], + "labels": ["None", "Home", "Away", "Sleep"], + "values": [0, 1, 2, 3], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260997/preset_selected" + } + } + }, + { + "type": "zone_mode", + "title": "Zone Mode", + "current_value": "AUTO", + "options": [ + { + "value": "AUTO", + "label": "Auto" + }, + { + "value": "COOL", + "label": "Cooling" + }, + { + "value": "HEAT", + "label": "Heating" + }, + { + "value": "OFF", + "label": "Off" + } + ], + "labels": ["Auto", "Cooling", "Heating", "Off"], + "values": ["AUTO", "COOL", "HEAT", "OFF"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260997/zone_mode" + } + } + }, + { + "type": "run_mode", + "title": "Run Mode", + "current_value": "permanent_hold", + "options": [ + { + "value": "permanent_hold", + "label": "Permanent Hold" + }, + { + "value": "run_schedule", + "label": "Run Schedule" + } + ], + "labels": ["Permanent Hold", "Run Schedule"], + "values": ["permanent_hold", "run_schedule"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260997/run_mode" + } + } + }, + { + "type": "scheduling_enabled", + "title": "Scheduling", + "current_value": true, + "options": [ + { + "value": true, + "label": "ON" + }, + { + "value": false, + "label": "OFF" + } + ], + "labels": ["ON", "OFF"], + "values": [true, false], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260997/scheduling_enabled" + } + } + } + ], + "icon": { + "name": "thermostat", + "modifiers": ["temperature-75"] + }, + "features": [ + { + "name": "thermostat", + "temperature": 75, + "status": "", + "status_icon": null, + "actions": { + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260997/setpoints" + }, + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260997/setpoints" + } + }, + "setpoint_delta": 3, + "scale": "f", + "setpoint_increment": 1.0, + "setpoint_heat_min": 55, + "setpoint_heat_max": 90, + "setpoint_cool_min": 60, + "setpoint_cool_max": 99, + "setpoint_heat": 63, + "setpoint_cool": 81, + "system_status": "System Idle" + }, + { + "name": "connection", + "signal_strength": "unknown", + "is_connected": true + }, + { + "name": "thermostat_mode", + "label": "Zone Mode", + "value": "AUTO", + "display_value": "Auto", + "options": [ + { + "id": "thermostat_mode", + "label": "Zone Mode", + "value": "thermostat_mode", + "header": true + }, + { + "value": "AUTO", + "label": "Auto" + }, + { + "value": "COOL", + "label": "Cooling" + }, + { + "value": "HEAT", + "label": "Heating" + }, + { + "value": "OFF", + "label": "Off" + } + ], + "actions": { + "update_thermostat_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83260997/zone_mode" + } + } + }, + { + "name": "thermostat_run_mode", + "label": "Run Mode", + "options": [ + { + "id": "thermostat_run_mode", + "label": "Run Mode", + "value": "thermostat_run_mode", + "header": true + }, + { + "id": "info_text", + "label": "Follow or override the schedule.", + "value": "info_text", + "info": true + }, + { + "value": "permanent_hold", + "label": "Permanent Hold" + }, + { + "value": "run_schedule", + "label": "Run Schedule" + } + ], + "value": "permanent_hold", + "display_value": "Hold", + "actions": { + "update_thermostat_run_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83260997/run_mode" + } + } + }, + { + "name": "schedule", + "enabled": true, + "max_period_name_length": 10, + "setpoint_increment": 1, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83260997\u0026house_id=123456", + "actions": { + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83260997", + "method": "POST" + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83260997", + "method": "POST" + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83260997", + "method": "GET" + }, + "enable_scheduling": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260997/scheduling_enabled", + "method": "POST", + "data": { + "value": true + } + } + }, + "can_add_remove_periods": true, + "max_periods_per_day": 4 + } + ], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260997" + } + } + } + ] + }, + { + "name": "thermostat_fan_mode", + "label": "Fan Mode", + "options": [ + { + "id": "thermostat_fan_mode", + "label": "Fan Mode", + "value": "thermostat_fan_mode", + "header": true + }, + { + "value": "auto", + "label": "Auto" + }, + { + "value": "on", + "label": "On" + }, + { + "value": "circulate", + "label": "Circulate" + } + ], + "value": "auto", + "display_value": "Auto", + "status_icon": { + "name": "thermostat_fan_off", + "modifiers": [] + }, + "actions": { + "update_thermostat_fan_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059652/fan_mode" + } + } + }, + { + "name": "thermostat_compressor_speed", + "compressor_speed": 0.0 + }, + { + "name": "runtime_history", + "actions": { + "get_runtime_history": { + "method": "GET", + "href": "https://www.mynexia.com/mobile/runtime_history/2059652?report_type=daily" + }, + "get_monthly_runtime_history": { + "method": "GET", + "href": "https://www.mynexia.com/mobile/runtime_history/2059652?report_type=monthly" + } + } + } + ], + "icon": [ + { + "name": "thermostat", + "modifiers": ["temperature-77"] + }, + { + "name": "thermostat", + "modifiers": ["temperature-74"] + }, + { + "name": "thermostat", + "modifiers": ["temperature-75"] + } + ], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059652" + }, + "nexia:history": { + "href": "https://www.mynexia.com/mobile/houses/123456/events?device_id=2059652" + }, + "filter_events": { + "href": "https://www.mynexia.com/mobile/houses/123456/events/collection?sys_guid=3679e95b-7337-48ae-aff4-e0522e9dd0eb" + }, + "pending_request": { + "polling_path": "https://www.mynexia.com/backstage/announcements/c6627726f6339d104ee66897028d6a2ea38215675b336650" + } + }, + "last_updated_at": "2020-03-11T15:15:53.000-05:00", + "settings": [ + { + "type": "fan_mode", + "title": "Fan Mode", + "current_value": "auto", + "options": [ + { + "value": "auto", + "label": "Auto" + }, + { + "value": "on", + "label": "On" + }, + { + "value": "circulate", + "label": "Circulate" + } + ], + "labels": ["Auto", "On", "Circulate"], + "values": ["auto", "on", "circulate"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059652/fan_mode" + } + } + }, + { + "type": "fan_speed", + "title": "Fan Speed", + "current_value": 0.35, + "options": [ + { + "value": 0.35, + "label": "35%" + }, + { + "value": 0.4, + "label": "40%" + }, + { + "value": 0.45, + "label": "45%" + }, + { + "value": 0.5, + "label": "50%" + }, + { + "value": 0.55, + "label": "55%" + }, + { + "value": 0.6, + "label": "60%" + }, + { + "value": 0.65, + "label": "65%" + }, + { + "value": 0.7, + "label": "70%" + }, + { + "value": 0.75, + "label": "75%" + }, + { + "value": 0.8, + "label": "80%" + }, + { + "value": 0.85, + "label": "85%" + }, + { + "value": 0.9, + "label": "90%" + }, + { + "value": 0.95, + "label": "95%" + }, + { + "value": 1.0, + "label": "100%" + } + ], + "labels": [ + "35%", + "40%", + "45%", + "50%", + "55%", + "60%", + "65%", + "70%", + "75%", + "80%", + "85%", + "90%", + "95%", + "100%" + ], + "values": [ + 0.35, 0.4, 0.45, 0.5, 0.55, 0.6, 0.65, 0.7, 0.75, 0.8, + 0.85, 0.9, 0.95, 1.0 + ], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059652/fan_speed" + } + } + }, + { + "type": "fan_circulation_time", + "title": "Fan Circulation Time", + "current_value": 30, + "options": [ + { + "value": 10, + "label": "10 minutes" + }, + { + "value": 15, + "label": "15 minutes" + }, + { + "value": 20, + "label": "20 minutes" + }, + { + "value": 25, + "label": "25 minutes" + }, + { + "value": 30, + "label": "30 minutes" + }, + { + "value": 35, + "label": "35 minutes" + }, + { + "value": 40, + "label": "40 minutes" + }, + { + "value": 45, + "label": "45 minutes" + }, + { + "value": 50, + "label": "50 minutes" + }, + { + "value": 55, + "label": "55 minutes" + } + ], + "labels": [ + "10 minutes", + "15 minutes", + "20 minutes", + "25 minutes", + "30 minutes", + "35 minutes", + "40 minutes", + "45 minutes", + "50 minutes", + "55 minutes" + ], + "values": [10, 15, 20, 25, 30, 35, 40, 45, 50, 55], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059652/fan_circulation_time" + } + } + }, + { + "type": "air_cleaner_mode", + "title": "Air Cleaner Mode", + "current_value": "auto", + "options": [ + { + "value": "auto", + "label": "Auto" + }, + { + "value": "quick", + "label": "Quick" + }, + { + "value": "allergy", + "label": "Allergy" + } + ], + "labels": ["Auto", "Quick", "Allergy"], + "values": ["auto", "quick", "allergy"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059652/air_cleaner_mode" + } + } + }, + { + "type": "dehumidify", + "title": "Cooling Dehumidify Set Point", + "current_value": 0.5, + "options": [ + { + "value": 0.35, + "label": "35%" + }, + { + "value": 0.4, + "label": "40%" + }, + { + "value": 0.45, + "label": "45%" + }, + { + "value": 0.5, + "label": "50%" + }, + { + "value": 0.55, + "label": "55%" + }, + { + "value": 0.6, + "label": "60%" + }, + { + "value": 0.65, + "label": "65%" + } + ], + "labels": ["35%", "40%", "45%", "50%", "55%", "60%", "65%"], + "values": [0.35, 0.4, 0.45, 0.5, 0.55, 0.6, 0.65], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059652/dehumidify" + } + } + }, + { + "type": "scale", + "title": "Temperature Scale", + "current_value": "f", + "options": [ + { + "value": "f", + "label": "F" + }, + { + "value": "c", + "label": "C" + } + ], + "labels": ["F", "C"], + "values": ["f", "c"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059652/scale" + } + } + } + ], + "status_secondary": null, + "status_tertiary": null, + "type": "xxl_thermostat", + "has_outdoor_temperature": true, + "outdoor_temperature": "87", + "has_indoor_humidity": true, + "connected": true, + "indoor_humidity": "37", + "system_status": "System Idle", + "delta": 3, + "zones": [ + { + "type": "xxl_zone", + "id": 83260991, + "name": "Hallway", + "current_zone_mode": "OFF", + "temperature": 77, + "setpoints": { + "heat": 63, + "cool": 80 + }, + "operating_state": "", + "heating_setpoint": 63, + "cooling_setpoint": 80, + "zone_status": "", + "settings": [ + { + "type": "preset_selected", + "title": "Preset", + "current_value": 0, + "options": [ + { + "value": 0, + "label": "None" + }, + { + "value": 1, + "label": "Home" + }, + { + "value": 2, + "label": "Away" + }, + { + "value": 3, + "label": "Sleep" + } + ], + "labels": ["None", "Home", "Away", "Sleep"], + "values": [0, 1, 2, 3], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260991/preset_selected" + } + } + }, + { + "type": "zone_mode", + "title": "Zone Mode", + "current_value": "OFF", + "options": [ + { + "value": "AUTO", + "label": "Auto" + }, + { + "value": "COOL", + "label": "Cooling" + }, + { + "value": "HEAT", + "label": "Heating" + }, + { + "value": "OFF", + "label": "Off" + } + ], + "labels": ["Auto", "Cooling", "Heating", "Off"], + "values": ["AUTO", "COOL", "HEAT", "OFF"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260991/zone_mode" + } + } + }, + { + "type": "run_mode", + "title": "Run Mode", + "current_value": "permanent_hold", + "options": [ + { + "value": "permanent_hold", + "label": "Permanent Hold" + }, + { + "value": "run_schedule", + "label": "Run Schedule" + } + ], + "labels": ["Permanent Hold", "Run Schedule"], + "values": ["permanent_hold", "run_schedule"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260991/run_mode" + } + } + }, + { + "type": "scheduling_enabled", + "title": "Scheduling", + "current_value": true, + "options": [ + { + "value": true, + "label": "ON" + }, + { + "value": false, + "label": "OFF" + } + ], + "labels": ["ON", "OFF"], + "values": [true, false], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260991/scheduling_enabled" + } + } + } + ], + "icon": { + "name": "thermostat", + "modifiers": ["temperature-77"] + }, + "features": [ + { + "name": "thermostat", + "temperature": 77, + "status": "", + "status_icon": null, + "actions": {}, + "setpoint_delta": 3, + "scale": "f", + "setpoint_increment": 1.0, + "setpoint_heat_min": 55, + "setpoint_heat_max": 90, + "setpoint_cool_min": 60, + "setpoint_cool_max": 99, + "system_status": "System Idle" + }, + { + "name": "connection", + "signal_strength": "unknown", + "is_connected": true + }, + { + "name": "thermostat_mode", + "label": "Zone Mode", + "value": "OFF", + "display_value": "Off", + "options": [ + { + "id": "thermostat_mode", + "label": "Zone Mode", + "value": "thermostat_mode", + "header": true + }, + { + "value": "AUTO", + "label": "Auto" + }, + { + "value": "COOL", + "label": "Cooling" + }, + { + "value": "HEAT", + "label": "Heating" + }, + { + "value": "OFF", + "label": "Off" + } + ], + "actions": { + "update_thermostat_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83260991/zone_mode" + } + } + }, + { + "name": "thermostat_run_mode", + "label": "Run Mode", + "options": [ + { + "id": "thermostat_run_mode", + "label": "Run Mode", + "value": "thermostat_run_mode", + "header": true + }, + { + "id": "info_text", + "label": "Follow or override the schedule.", + "value": "info_text", + "info": true + }, + { + "value": "permanent_hold", + "label": "Permanent Hold" + }, + { + "value": "run_schedule", + "label": "Run Schedule" + } + ], + "value": "permanent_hold", + "display_value": "Hold", + "actions": { + "update_thermostat_run_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83260991/run_mode" + } + } + }, + { + "name": "schedule", + "enabled": true, + "max_period_name_length": 10, + "setpoint_increment": 1, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83260991\u0026house_id=123456", + "actions": { + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83260991", + "method": "POST" + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83260991", + "method": "POST" + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83260991", + "method": "GET" + }, + "enable_scheduling": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260991/scheduling_enabled", + "method": "POST", + "data": { + "value": true + } + } + }, + "can_add_remove_periods": true, + "max_periods_per_day": 4 + } + ], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260991" + } + } + }, + { + "type": "xxl_zone", + "id": 83260994, + "name": "Mid Bedroom", + "current_zone_mode": "AUTO", + "temperature": 74, + "setpoints": { + "heat": 63, + "cool": 81 + }, + "operating_state": "", + "heating_setpoint": 63, + "cooling_setpoint": 81, + "zone_status": "", + "settings": [ + { + "type": "preset_selected", + "title": "Preset", + "current_value": 0, + "options": [ + { + "value": 0, + "label": "None" + }, + { + "value": 1, + "label": "Home" + }, + { + "value": 2, + "label": "Away" + }, + { + "value": 3, + "label": "Sleep" + } + ], + "labels": ["None", "Home", "Away", "Sleep"], + "values": [0, 1, 2, 3], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260994/preset_selected" + } + } + }, + { + "type": "zone_mode", + "title": "Zone Mode", + "current_value": "AUTO", + "options": [ + { + "value": "AUTO", + "label": "Auto" + }, + { + "value": "COOL", + "label": "Cooling" + }, + { + "value": "HEAT", + "label": "Heating" + }, + { + "value": "OFF", + "label": "Off" + } + ], + "labels": ["Auto", "Cooling", "Heating", "Off"], + "values": ["AUTO", "COOL", "HEAT", "OFF"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260994/zone_mode" + } + } + }, + { + "type": "run_mode", + "title": "Run Mode", + "current_value": "permanent_hold", + "options": [ + { + "value": "permanent_hold", + "label": "Permanent Hold" + }, + { + "value": "run_schedule", + "label": "Run Schedule" + } + ], + "labels": ["Permanent Hold", "Run Schedule"], + "values": ["permanent_hold", "run_schedule"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260994/run_mode" + } + } + }, + { + "type": "scheduling_enabled", + "title": "Scheduling", + "current_value": true, + "options": [ + { + "value": true, + "label": "ON" + }, + { + "value": false, + "label": "OFF" + } + ], + "labels": ["ON", "OFF"], + "values": [true, false], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260994/scheduling_enabled" + } + } + } + ], + "icon": { + "name": "thermostat", + "modifiers": ["temperature-74"] + }, + "features": [ + { + "name": "thermostat", + "temperature": 74, + "status": "", + "status_icon": null, + "actions": { + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260994/setpoints" + }, + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260994/setpoints" + } + }, + "setpoint_delta": 3, + "scale": "f", + "setpoint_increment": 1.0, + "setpoint_heat_min": 55, + "setpoint_heat_max": 90, + "setpoint_cool_min": 60, + "setpoint_cool_max": 99, + "setpoint_heat": 63, + "setpoint_cool": 81, + "system_status": "System Idle" + }, + { + "name": "connection", + "signal_strength": "unknown", + "is_connected": true + }, + { + "name": "thermostat_mode", + "label": "Zone Mode", + "value": "AUTO", + "display_value": "Auto", + "options": [ + { + "id": "thermostat_mode", + "label": "Zone Mode", + "value": "thermostat_mode", + "header": true + }, + { + "value": "AUTO", + "label": "Auto" + }, + { + "value": "COOL", + "label": "Cooling" + }, + { + "value": "HEAT", + "label": "Heating" + }, + { + "value": "OFF", + "label": "Off" + } + ], + "actions": { + "update_thermostat_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83260994/zone_mode" + } + } + }, + { + "name": "thermostat_run_mode", + "label": "Run Mode", + "options": [ + { + "id": "thermostat_run_mode", + "label": "Run Mode", + "value": "thermostat_run_mode", + "header": true + }, + { + "id": "info_text", + "label": "Follow or override the schedule.", + "value": "info_text", + "info": true + }, + { + "value": "permanent_hold", + "label": "Permanent Hold" + }, + { + "value": "run_schedule", + "label": "Run Schedule" + } + ], + "value": "permanent_hold", + "display_value": "Hold", + "actions": { + "update_thermostat_run_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83260994/run_mode" + } + } + }, + { + "name": "schedule", + "enabled": true, + "max_period_name_length": 10, + "setpoint_increment": 1, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83260994\u0026house_id=123456", + "actions": { + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83260994", + "method": "POST" + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83260994", + "method": "POST" + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83260994", + "method": "GET" + }, + "enable_scheduling": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260994/scheduling_enabled", + "method": "POST", + "data": { + "value": true + } + } + }, + "can_add_remove_periods": true, + "max_periods_per_day": 4 + } + ], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260994" + } + } + }, + { + "type": "xxl_zone", + "id": 83260997, + "name": "West Bedroom", + "current_zone_mode": "AUTO", + "temperature": 75, + "setpoints": { + "heat": 63, + "cool": 81 + }, + "operating_state": "", + "heating_setpoint": 63, + "cooling_setpoint": 81, + "zone_status": "", + "settings": [ + { + "type": "preset_selected", + "title": "Preset", + "current_value": 0, + "options": [ + { + "value": 0, + "label": "None" + }, + { + "value": 1, + "label": "Home" + }, + { + "value": 2, + "label": "Away" + }, + { + "value": 3, + "label": "Sleep" + } + ], + "labels": ["None", "Home", "Away", "Sleep"], + "values": [0, 1, 2, 3], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260997/preset_selected" + } + } + }, + { + "type": "zone_mode", + "title": "Zone Mode", + "current_value": "AUTO", + "options": [ + { + "value": "AUTO", + "label": "Auto" + }, + { + "value": "COOL", + "label": "Cooling" + }, + { + "value": "HEAT", + "label": "Heating" + }, + { + "value": "OFF", + "label": "Off" + } + ], + "labels": ["Auto", "Cooling", "Heating", "Off"], + "values": ["AUTO", "COOL", "HEAT", "OFF"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260997/zone_mode" + } + } + }, + { + "type": "run_mode", + "title": "Run Mode", + "current_value": "permanent_hold", + "options": [ + { + "value": "permanent_hold", + "label": "Permanent Hold" + }, + { + "value": "run_schedule", + "label": "Run Schedule" + } + ], + "labels": ["Permanent Hold", "Run Schedule"], + "values": ["permanent_hold", "run_schedule"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260997/run_mode" + } + } + }, + { + "type": "scheduling_enabled", + "title": "Scheduling", + "current_value": true, + "options": [ + { + "value": true, + "label": "ON" + }, + { + "value": false, + "label": "OFF" + } + ], + "labels": ["ON", "OFF"], + "values": [true, false], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260997/scheduling_enabled" + } + } + } + ], + "icon": { + "name": "thermostat", + "modifiers": ["temperature-75"] + }, + "features": [ + { + "name": "thermostat", + "temperature": 75, + "status": "", + "status_icon": null, + "actions": { + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260997/setpoints" + }, + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260997/setpoints" + } + }, + "setpoint_delta": 3, + "scale": "f", + "setpoint_increment": 1.0, + "setpoint_heat_min": 55, + "setpoint_heat_max": 90, + "setpoint_cool_min": 60, + "setpoint_cool_max": 99, + "setpoint_heat": 63, + "setpoint_cool": 81, + "system_status": "System Idle" + }, + { + "name": "connection", + "signal_strength": "unknown", + "is_connected": true + }, + { + "name": "thermostat_mode", + "label": "Zone Mode", + "value": "AUTO", + "display_value": "Auto", + "options": [ + { + "id": "thermostat_mode", + "label": "Zone Mode", + "value": "thermostat_mode", + "header": true + }, + { + "value": "AUTO", + "label": "Auto" + }, + { + "value": "COOL", + "label": "Cooling" + }, + { + "value": "HEAT", + "label": "Heating" + }, + { + "value": "OFF", + "label": "Off" + } + ], + "actions": { + "update_thermostat_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83260997/zone_mode" + } + } + }, + { + "name": "thermostat_run_mode", + "label": "Run Mode", + "options": [ + { + "id": "thermostat_run_mode", + "label": "Run Mode", + "value": "thermostat_run_mode", + "header": true + }, + { + "id": "info_text", + "label": "Follow or override the schedule.", + "value": "info_text", + "info": true + }, + { + "value": "permanent_hold", + "label": "Permanent Hold" + }, + { + "value": "run_schedule", + "label": "Run Schedule" + } + ], + "value": "permanent_hold", + "display_value": "Hold", + "actions": { + "update_thermostat_run_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83260997/run_mode" + } + } + }, + { + "name": "schedule", + "enabled": true, + "max_period_name_length": 10, + "setpoint_increment": 1, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83260997\u0026house_id=123456", + "actions": { + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83260997", + "method": "POST" + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83260997", + "method": "POST" + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83260997", + "method": "GET" + }, + "enable_scheduling": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260997/scheduling_enabled", + "method": "POST", + "data": { + "value": true + } + } + }, + "can_add_remove_periods": true, + "max_periods_per_day": 4 + } + ], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260997" + } + } + } + ] + } + ], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/houses/123456/devices" + }, + "template": { + "data": { + "title": null, + "fields": [], + "_links": { + "child-schema": [ + { + "data": { + "label": "Connect New Device", + "icon": { + "name": "new_device", + "modifiers": [] + }, + "_links": { + "next": { + "href": "https://www.mynexia.com/mobile/houses/123456/enrollables_schema" + } + } + } + }, + { + "data": { + "label": "Create Group", + "icon": { + "name": "create_group", + "modifiers": [] + }, + "_links": { + "next": { + "href": "https://www.mynexia.com/mobile/houses/123456/groups/new" + } + } + } + } + ] + } + } + } + }, + "item_type": "application/vnd.nexia.device+json" + } + }, + { + "href": "https://www.mynexia.com/mobile/houses/123456/automations", + "type": "application/vnd.nexia.collection+json", + "data": { + "items": [ + { + "id": 3467876, + "name": "Away for 12 Hours", + "enabled": true, + "settings": [], + "triggers": [], + "description": "When IFTTT activates the automation Upstairs West Wing will permanently hold the heat to 62.0 and cool to 83.0 AND Downstairs East Wing will permanently hold the heat to 62.0 and cool to 83.0 AND Downstairs West Wing will permanently hold the heat to 62.0 and cool to 83.0 AND Activate the mode named 'Away 12' AND Master Suite will permanently hold the heat to 62.0 and cool to 83.0", + "icon": [ + { + "name": "gears", + "modifiers": [] + }, + { + "name": "climate", + "modifiers": [] + }, + { + "name": "climate", + "modifiers": [] + }, + { + "name": "climate", + "modifiers": [] + }, + { + "name": "plane", + "modifiers": [] + }, + { + "name": "climate", + "modifiers": [] + } + ], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/automations/3467876" + }, + "edit": { + "href": "https://www.mynexia.com/mobile/automation_edit_buffers?automation_id=3467876", + "method": "POST" + }, + "nexia:history": { + "href": "https://www.mynexia.com/mobile/houses/123456/events?automation_id=3467876" + }, + "filter_events": { + "href": "https://www.mynexia.com/mobile/houses/123456/events/collection?sys_guid=472ae0d2-5d7c-4a1c-9e47-4d9035fdace5" + } + } + }, + { + "id": 3467870, + "name": "Away For 24 Hours", + "enabled": true, + "settings": [], + "triggers": [], + "description": "When IFTTT activates the automation Upstairs West Wing will permanently hold the heat to 60.0 and cool to 85.0 AND Downstairs East Wing will permanently hold the heat to 60.0 and cool to 85.0 AND Downstairs West Wing will permanently hold the heat to 60.0 and cool to 85.0 AND Activate the mode named 'Away 24' AND Master Suite will permanently hold the heat to 60.0 and cool to 85.0", + "icon": [ + { + "name": "gears", + "modifiers": [] + }, + { + "name": "climate", + "modifiers": [] + }, + { + "name": "climate", + "modifiers": [] + }, + { + "name": "climate", + "modifiers": [] + }, + { + "name": "plane", + "modifiers": [] + }, + { + "name": "climate", + "modifiers": [] + } + ], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/automations/3467870" + }, + "edit": { + "href": "https://www.mynexia.com/mobile/automation_edit_buffers?automation_id=3467870", + "method": "POST" + }, + "nexia:history": { + "href": "https://www.mynexia.com/mobile/houses/123456/events?automation_id=3467870" + }, + "filter_events": { + "href": "https://www.mynexia.com/mobile/houses/123456/events/collection?sys_guid=f63ee20c-3146-49a1-87c5-47429a063d15" + } + } + }, + { + "id": 3452469, + "name": "Away Short", + "enabled": false, + "settings": [], + "triggers": [], + "description": "When IFTTT activates the automation Upstairs West Wing will permanently hold the heat to 63.0 and cool to 80.0 AND Downstairs East Wing will permanently hold the heat to 63.0 and cool to 79.0 AND Downstairs West Wing will permanently hold the heat to 63.0 and cool to 79.0 AND Upstairs West Wing will permanently hold the heat to 63.0 and cool to 81.0 AND Upstairs West Wing will change Fan Mode to Auto AND Downstairs East Wing will change Fan Mode to Auto AND Downstairs West Wing will change Fan Mode to Auto AND Activate the mode named 'Away Short' AND Master Suite will permanently hold the heat to 63.0 and cool to 79.0 AND Master Suite will change Fan Mode to Auto", + "icon": [ + { + "name": "gears", + "modifiers": [] + }, + { + "name": "climate", + "modifiers": [] + }, + { + "name": "climate", + "modifiers": [] + }, + { + "name": "climate", + "modifiers": [] + }, + { + "name": "climate", + "modifiers": [] + }, + { + "name": "settings", + "modifiers": [] + }, + { + "name": "settings", + "modifiers": [] + }, + { + "name": "settings", + "modifiers": [] + }, + { + "name": "key", + "modifiers": [] + }, + { + "name": "climate", + "modifiers": [] + }, + { + "name": "settings", + "modifiers": [] + } + ], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/automations/3452469" + }, + "edit": { + "href": "https://www.mynexia.com/mobile/automation_edit_buffers?automation_id=3452469", + "method": "POST" + }, + "nexia:history": { + "href": "https://www.mynexia.com/mobile/houses/123456/events?automation_id=3452469" + }, + "filter_events": { + "href": "https://www.mynexia.com/mobile/houses/123456/events/collection?sys_guid=e5c59b93-efca-4937-9499-3f4c896ab17c" + } + } + }, + { + "id": 3452472, + "name": "Home", + "enabled": true, + "settings": [], + "triggers": [], + "description": "When IFTTT activates the automation Upstairs West Wing will Run Schedule AND Downstairs East Wing will Run Schedule AND Downstairs West Wing will Run Schedule AND Activate the mode named 'Home' AND Master Suite will Run Schedule", + "icon": [ + { + "name": "gears", + "modifiers": [] + }, + { + "name": "settings", + "modifiers": [] + }, + { + "name": "settings", + "modifiers": [] + }, + { + "name": "settings", + "modifiers": [] + }, + { + "name": "at_home", + "modifiers": [] + }, + { + "name": "settings", + "modifiers": [] + } + ], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/automations/3452472" + }, + "edit": { + "href": "https://www.mynexia.com/mobile/automation_edit_buffers?automation_id=3452472", + "method": "POST" + }, + "nexia:history": { + "href": "https://www.mynexia.com/mobile/houses/123456/events?automation_id=3452472" + }, + "filter_events": { + "href": "https://www.mynexia.com/mobile/houses/123456/events/collection?sys_guid=861b9fec-d259-4492-a798-5712251666c4" + } + } + }, + { + "id": 3454776, + "name": "IFTTT Power Spike", + "enabled": true, + "settings": [], + "triggers": [], + "description": "When IFTTT activates the automation Upstairs West Wing will permanently hold the heat to 60.0 and cool to 85.0 AND Downstairs East Wing will permanently hold the heat to 60.0 and cool to 85.0 AND Downstairs West Wing will permanently hold the heat to 60.0 and cool to 85.0 AND Upstairs West Wing will change Fan Mode to Auto AND Downstairs East Wing will change Fan Mode to Auto AND Downstairs West Wing will change Fan Mode to Auto AND Master Suite will permanently hold the heat to 60.0 and cool to 85.0 AND Master Suite will change Fan Mode to Auto", + "icon": [ + { + "name": "gears", + "modifiers": [] + }, + { + "name": "climate", + "modifiers": [] + }, + { + "name": "climate", + "modifiers": [] + }, + { + "name": "climate", + "modifiers": [] + }, + { + "name": "settings", + "modifiers": [] + }, + { + "name": "settings", + "modifiers": [] + }, + { + "name": "settings", + "modifiers": [] + }, + { + "name": "climate", + "modifiers": [] + }, + { + "name": "settings", + "modifiers": [] + } + ], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/automations/3454776" + }, + "edit": { + "href": "https://www.mynexia.com/mobile/automation_edit_buffers?automation_id=3454776", + "method": "POST" + }, + "nexia:history": { + "href": "https://www.mynexia.com/mobile/houses/123456/events?automation_id=3454776" + }, + "filter_events": { + "href": "https://www.mynexia.com/mobile/houses/123456/events/collection?sys_guid=96c71d37-66aa-4cbb-84ff-a90412fd366a" + } + } + }, + { + "id": 3454774, + "name": "IFTTT return to schedule", + "enabled": false, + "settings": [], + "triggers": [], + "description": "When IFTTT activates the automation Upstairs West Wing will Run Schedule AND Downstairs East Wing will Run Schedule AND Downstairs West Wing will Run Schedule AND Master Suite will Run Schedule", + "icon": [ + { + "name": "gears", + "modifiers": [] + }, + { + "name": "settings", + "modifiers": [] + }, + { + "name": "settings", + "modifiers": [] + }, + { + "name": "settings", + "modifiers": [] + }, + { + "name": "settings", + "modifiers": [] + } + ], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/automations/3454774" + }, + "edit": { + "href": "https://www.mynexia.com/mobile/automation_edit_buffers?automation_id=3454774", + "method": "POST" + }, + "nexia:history": { + "href": "https://www.mynexia.com/mobile/houses/123456/events?automation_id=3454774" + }, + "filter_events": { + "href": "https://www.mynexia.com/mobile/houses/123456/events/collection?sys_guid=880c5287-d92c-4368-8494-e10975e92733" + } + } + }, + { + "id": 3486078, + "name": "Power Outage", + "enabled": true, + "settings": [], + "triggers": [], + "description": "When IFTTT activates the automation Upstairs West Wing will permanently hold the heat to 55.0 and cool to 90.0 AND Downstairs East Wing will permanently hold the heat to 55.0 and cool to 90.0 AND Downstairs West Wing will permanently hold the heat to 55.0 and cool to 90.0 AND Activate the mode named 'Power Outage'", + "icon": [ + { + "name": "gears", + "modifiers": [] + }, + { + "name": "climate", + "modifiers": [] + }, + { + "name": "climate", + "modifiers": [] + }, + { + "name": "climate", + "modifiers": [] + }, + { + "name": "bell", + "modifiers": [] + } + ], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/automations/3486078" + }, + "edit": { + "href": "https://www.mynexia.com/mobile/automation_edit_buffers?automation_id=3486078", + "method": "POST" + }, + "nexia:history": { + "href": "https://www.mynexia.com/mobile/houses/123456/events?automation_id=3486078" + }, + "filter_events": { + "href": "https://www.mynexia.com/mobile/houses/123456/events/collection?sys_guid=d33c013b-2357-47a9-8c66-d2c3693173b0" + } + } + }, + { + "id": 3486091, + "name": "Power Restored", + "enabled": true, + "settings": [], + "triggers": [], + "description": "When IFTTT activates the automation Upstairs West Wing will Run Schedule AND Downstairs East Wing will Run Schedule AND Downstairs West Wing will Run Schedule AND Activate the mode named 'Home'", + "icon": [ + { + "name": "gears", + "modifiers": [] + }, + { + "name": "settings", + "modifiers": [] + }, + { + "name": "settings", + "modifiers": [] + }, + { + "name": "settings", + "modifiers": [] + }, + { + "name": "at_home", + "modifiers": [] + } + ], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/automations/3486091" + }, + "edit": { + "href": "https://www.mynexia.com/mobile/automation_edit_buffers?automation_id=3486091", + "method": "POST" + }, + "nexia:history": { + "href": "https://www.mynexia.com/mobile/houses/123456/events?automation_id=3486091" + }, + "filter_events": { + "href": "https://www.mynexia.com/mobile/houses/123456/events/collection?sys_guid=b9141df8-2e5e-4524-b8ef-efcbf48d775a" + } + } + } + ], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/houses/123456/automations" + }, + "template": { + "href": "https://www.mynexia.com/mobile/houses/123456/automation_edit_buffers", + "method": "POST" + } + }, + "item_type": "application/vnd.nexia.automation+json" + } + }, + { + "href": "https://www.mynexia.com/mobile/houses/123456/modes", + "type": "application/vnd.nexia.collection+json", + "data": { + "items": [ + { + "id": 3047801, + "name": "Home", + "current_mode": false, + "icon": "home.png", + "settings": [], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/modes/3047801" + } + } + }, + { + "id": 3174574, + "name": "Away Short", + "current_mode": true, + "icon": "key.png", + "settings": [], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/modes/3174574" + } + } + }, + { + "id": 3174576, + "name": "Away 12", + "current_mode": false, + "icon": "picture.png", + "settings": [], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/modes/3174576" + } + } + }, + { + "id": 3174577, + "name": "Away 24", + "current_mode": false, + "icon": "picture.png", + "settings": [], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/modes/3174577" + } + } + }, + { + "id": 3197871, + "name": "Power Outage", + "current_mode": false, + "icon": "bell.png", + "settings": [], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/modes/3197871" + } + } + } + ], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/houses/123456/modes" + } + }, + "item_type": "application/vnd.nexia.mode+json" + } + }, + { + "href": "https://www.mynexia.com/mobile/houses/123456/events/collection", + "type": "application/vnd.nexia.collection+json", + "data": { + "item_type": "application/vnd.nexia.event+json" + } + }, + { + "href": "https://www.mynexia.com/mobile/houses/123456/videos/collection", + "type": "application/vnd.nexia.collection+json", + "data": { + "item_type": "application/vnd.nexia.video+json" + } + } + ] } -} \ No newline at end of file + } +} diff --git a/tests/components/nexia/fixtures/session_123456.json b/tests/components/nexia/fixtures/session_123456.json index 3991a7d565f..858880f66e3 100644 --- a/tests/components/nexia/fixtures/session_123456.json +++ b/tests/components/nexia/fixtures/session_123456.json @@ -1,25 +1,25 @@ { - "success" : true, - "result" : { - "is_activated_by_activation_code" : 0, - "can_receive_notifications" : true, - "can_manage_locks" : true, - "can_control_automations" : true, - "_links" : { - "child" : [ - { - "data" : { - "name" : "House", - "postal_code" : "12345", - "id" : 123456 - } - } - ], - "self" : { - "href" : "https://www.mynexia.com/mobile/session" - } - }, - "can_view_videos" : true - }, - "error" : null + "success": true, + "result": { + "is_activated_by_activation_code": 0, + "can_receive_notifications": true, + "can_manage_locks": true, + "can_control_automations": true, + "_links": { + "child": [ + { + "data": { + "name": "House", + "postal_code": "12345", + "id": 123456 + } + } + ], + "self": { + "href": "https://www.mynexia.com/mobile/session" + } + }, + "can_view_videos": true + }, + "error": null } diff --git a/tests/components/nexia/fixtures/sign_in.json b/tests/components/nexia/fixtures/sign_in.json index aac2fb1ae62..f4bd138e13d 100644 --- a/tests/components/nexia/fixtures/sign_in.json +++ b/tests/components/nexia/fixtures/sign_in.json @@ -1,10 +1,10 @@ { - "success": true, - "error": null, - "result": { - "mobile_id": 1, - "api_key": "mock", - "setup_step": "done", - "locale": "en_us" - } + "success": true, + "error": null, + "result": { + "mobile_id": 1, + "api_key": "mock", + "setup_step": "done", + "locale": "en_us" + } } diff --git a/tests/components/nexia/test_diagnostics.py b/tests/components/nexia/test_diagnostics.py new file mode 100644 index 00000000000..4aa91c2f301 --- /dev/null +++ b/tests/components/nexia/test_diagnostics.py @@ -0,0 +1,7530 @@ +"""Test august diagnostics.""" + +from .util import async_init_integration + +from tests.components.diagnostics import get_diagnostics_for_config_entry + + +async def test_diagnostics(hass, hass_client): + """Test generating diagnostics for a config entry.""" + entry = await async_init_integration(hass) + + diag = await get_diagnostics_for_config_entry(hass, hass_client, entry) + assert diag == { + "automations": [ + { + "_links": { + "edit": { + "href": "https://www.mynexia.com/mobile/automation_edit_buffers?automation_id=3467876", + "method": "POST", + }, + "filter_events": { + "href": "https://www.mynexia.com/mobile/houses/123456/events/collection?sys_guid=472ae0d2-5d7c-4a1c-9e47-4d9035fdace5" + }, + "nexia:history": { + "href": "https://www.mynexia.com/mobile/houses/123456/events?automation_id=3467876" + }, + "self": { + "href": "https://www.mynexia.com/mobile/automations/3467876" + }, + }, + "description": "When IFTTT activates the automation Upstairs " + "West Wing will permanently hold the heat to " + "62.0 and cool to 83.0 AND Downstairs East " + "Wing will permanently hold the heat to 62.0 " + "and cool to 83.0 AND Downstairs West Wing " + "will permanently hold the heat to 62.0 and " + "cool to 83.0 AND Activate the mode named " + "'Away 12' AND Master Suite will permanently " + "hold the heat to 62.0 and cool to 83.0", + "enabled": True, + "icon": [ + {"modifiers": [], "name": "gears"}, + {"modifiers": [], "name": "climate"}, + {"modifiers": [], "name": "climate"}, + {"modifiers": [], "name": "climate"}, + {"modifiers": [], "name": "plane"}, + {"modifiers": [], "name": "climate"}, + ], + "id": 3467876, + "name": "Away for 12 Hours", + "settings": [], + "triggers": [], + }, + { + "_links": { + "edit": { + "href": "https://www.mynexia.com/mobile/automation_edit_buffers?automation_id=3467870", + "method": "POST", + }, + "filter_events": { + "href": "https://www.mynexia.com/mobile/houses/123456/events/collection?sys_guid=f63ee20c-3146-49a1-87c5-47429a063d15" + }, + "nexia:history": { + "href": "https://www.mynexia.com/mobile/houses/123456/events?automation_id=3467870" + }, + "self": { + "href": "https://www.mynexia.com/mobile/automations/3467870" + }, + }, + "description": "When IFTTT activates the automation Upstairs " + "West Wing will permanently hold the heat to " + "60.0 and cool to 85.0 AND Downstairs East " + "Wing will permanently hold the heat to 60.0 " + "and cool to 85.0 AND Downstairs West Wing " + "will permanently hold the heat to 60.0 and " + "cool to 85.0 AND Activate the mode named " + "'Away 24' AND Master Suite will permanently " + "hold the heat to 60.0 and cool to 85.0", + "enabled": True, + "icon": [ + {"modifiers": [], "name": "gears"}, + {"modifiers": [], "name": "climate"}, + {"modifiers": [], "name": "climate"}, + {"modifiers": [], "name": "climate"}, + {"modifiers": [], "name": "plane"}, + {"modifiers": [], "name": "climate"}, + ], + "id": 3467870, + "name": "Away For 24 Hours", + "settings": [], + "triggers": [], + }, + { + "_links": { + "edit": { + "href": "https://www.mynexia.com/mobile/automation_edit_buffers?automation_id=3452469", + "method": "POST", + }, + "filter_events": { + "href": "https://www.mynexia.com/mobile/houses/123456/events/collection?sys_guid=e5c59b93-efca-4937-9499-3f4c896ab17c" + }, + "nexia:history": { + "href": "https://www.mynexia.com/mobile/houses/123456/events?automation_id=3452469" + }, + "self": { + "href": "https://www.mynexia.com/mobile/automations/3452469" + }, + }, + "description": "When IFTTT activates the automation Upstairs " + "West Wing will permanently hold the heat to " + "63.0 and cool to 80.0 AND Downstairs East " + "Wing will permanently hold the heat to 63.0 " + "and cool to 79.0 AND Downstairs West Wing " + "will permanently hold the heat to 63.0 and " + "cool to 79.0 AND Upstairs West Wing will " + "permanently hold the heat to 63.0 and cool " + "to 81.0 AND Upstairs West Wing will change " + "Fan Mode to Auto AND Downstairs East Wing " + "will change Fan Mode to Auto AND Downstairs " + "West Wing will change Fan Mode to Auto AND " + "Activate the mode named 'Away Short' AND " + "Master Suite will permanently hold the heat " + "to 63.0 and cool to 79.0 AND Master Suite " + "will change Fan Mode to Auto", + "enabled": False, + "icon": [ + {"modifiers": [], "name": "gears"}, + {"modifiers": [], "name": "climate"}, + {"modifiers": [], "name": "climate"}, + {"modifiers": [], "name": "climate"}, + {"modifiers": [], "name": "climate"}, + {"modifiers": [], "name": "settings"}, + {"modifiers": [], "name": "settings"}, + {"modifiers": [], "name": "settings"}, + {"modifiers": [], "name": "key"}, + {"modifiers": [], "name": "climate"}, + {"modifiers": [], "name": "settings"}, + ], + "id": 3452469, + "name": "Away Short", + "settings": [], + "triggers": [], + }, + { + "_links": { + "edit": { + "href": "https://www.mynexia.com/mobile/automation_edit_buffers?automation_id=3452472", + "method": "POST", + }, + "filter_events": { + "href": "https://www.mynexia.com/mobile/houses/123456/events/collection?sys_guid=861b9fec-d259-4492-a798-5712251666c4" + }, + "nexia:history": { + "href": "https://www.mynexia.com/mobile/houses/123456/events?automation_id=3452472" + }, + "self": { + "href": "https://www.mynexia.com/mobile/automations/3452472" + }, + }, + "description": "When IFTTT activates the automation Upstairs " + "West Wing will Run Schedule AND Downstairs " + "East Wing will Run Schedule AND Downstairs " + "West Wing will Run Schedule AND Activate the " + "mode named 'Home' AND Master Suite will Run " + "Schedule", + "enabled": True, + "icon": [ + {"modifiers": [], "name": "gears"}, + {"modifiers": [], "name": "settings"}, + {"modifiers": [], "name": "settings"}, + {"modifiers": [], "name": "settings"}, + {"modifiers": [], "name": "at_home"}, + {"modifiers": [], "name": "settings"}, + ], + "id": 3452472, + "name": "Home", + "settings": [], + "triggers": [], + }, + { + "_links": { + "edit": { + "href": "https://www.mynexia.com/mobile/automation_edit_buffers?automation_id=3454776", + "method": "POST", + }, + "filter_events": { + "href": "https://www.mynexia.com/mobile/houses/123456/events/collection?sys_guid=96c71d37-66aa-4cbb-84ff-a90412fd366a" + }, + "nexia:history": { + "href": "https://www.mynexia.com/mobile/houses/123456/events?automation_id=3454776" + }, + "self": { + "href": "https://www.mynexia.com/mobile/automations/3454776" + }, + }, + "description": "When IFTTT activates the automation Upstairs " + "West Wing will permanently hold the heat to " + "60.0 and cool to 85.0 AND Downstairs East " + "Wing will permanently hold the heat to 60.0 " + "and cool to 85.0 AND Downstairs West Wing " + "will permanently hold the heat to 60.0 and " + "cool to 85.0 AND Upstairs West Wing will " + "change Fan Mode to Auto AND Downstairs East " + "Wing will change Fan Mode to Auto AND " + "Downstairs West Wing will change Fan Mode to " + "Auto AND Master Suite will permanently hold " + "the heat to 60.0 and cool to 85.0 AND Master " + "Suite will change Fan Mode to Auto", + "enabled": True, + "icon": [ + {"modifiers": [], "name": "gears"}, + {"modifiers": [], "name": "climate"}, + {"modifiers": [], "name": "climate"}, + {"modifiers": [], "name": "climate"}, + {"modifiers": [], "name": "settings"}, + {"modifiers": [], "name": "settings"}, + {"modifiers": [], "name": "settings"}, + {"modifiers": [], "name": "climate"}, + {"modifiers": [], "name": "settings"}, + ], + "id": 3454776, + "name": "IFTTT Power Spike", + "settings": [], + "triggers": [], + }, + { + "_links": { + "edit": { + "href": "https://www.mynexia.com/mobile/automation_edit_buffers?automation_id=3454774", + "method": "POST", + }, + "filter_events": { + "href": "https://www.mynexia.com/mobile/houses/123456/events/collection?sys_guid=880c5287-d92c-4368-8494-e10975e92733" + }, + "nexia:history": { + "href": "https://www.mynexia.com/mobile/houses/123456/events?automation_id=3454774" + }, + "self": { + "href": "https://www.mynexia.com/mobile/automations/3454774" + }, + }, + "description": "When IFTTT activates the automation Upstairs " + "West Wing will Run Schedule AND Downstairs " + "East Wing will Run Schedule AND Downstairs " + "West Wing will Run Schedule AND Master Suite " + "will Run Schedule", + "enabled": False, + "icon": [ + {"modifiers": [], "name": "gears"}, + {"modifiers": [], "name": "settings"}, + {"modifiers": [], "name": "settings"}, + {"modifiers": [], "name": "settings"}, + {"modifiers": [], "name": "settings"}, + ], + "id": 3454774, + "name": "IFTTT return to schedule", + "settings": [], + "triggers": [], + }, + { + "_links": { + "edit": { + "href": "https://www.mynexia.com/mobile/automation_edit_buffers?automation_id=3486078", + "method": "POST", + }, + "filter_events": { + "href": "https://www.mynexia.com/mobile/houses/123456/events/collection?sys_guid=d33c013b-2357-47a9-8c66-d2c3693173b0" + }, + "nexia:history": { + "href": "https://www.mynexia.com/mobile/houses/123456/events?automation_id=3486078" + }, + "self": { + "href": "https://www.mynexia.com/mobile/automations/3486078" + }, + }, + "description": "When IFTTT activates the automation Upstairs " + "West Wing will permanently hold the heat to " + "55.0 and cool to 90.0 AND Downstairs East " + "Wing will permanently hold the heat to 55.0 " + "and cool to 90.0 AND Downstairs West Wing " + "will permanently hold the heat to 55.0 and " + "cool to 90.0 AND Activate the mode named " + "'Power Outage'", + "enabled": True, + "icon": [ + {"modifiers": [], "name": "gears"}, + {"modifiers": [], "name": "climate"}, + {"modifiers": [], "name": "climate"}, + {"modifiers": [], "name": "climate"}, + {"modifiers": [], "name": "bell"}, + ], + "id": 3486078, + "name": "Power Outage", + "settings": [], + "triggers": [], + }, + { + "_links": { + "edit": { + "href": "https://www.mynexia.com/mobile/automation_edit_buffers?automation_id=3486091", + "method": "POST", + }, + "filter_events": { + "href": "https://www.mynexia.com/mobile/houses/123456/events/collection?sys_guid=b9141df8-2e5e-4524-b8ef-efcbf48d775a" + }, + "nexia:history": { + "href": "https://www.mynexia.com/mobile/houses/123456/events?automation_id=3486091" + }, + "self": { + "href": "https://www.mynexia.com/mobile/automations/3486091" + }, + }, + "description": "When IFTTT activates the automation Upstairs " + "West Wing will Run Schedule AND Downstairs " + "East Wing will Run Schedule AND Downstairs " + "West Wing will Run Schedule AND Activate the " + "mode named 'Home'", + "enabled": True, + "icon": [ + {"modifiers": [], "name": "gears"}, + {"modifiers": [], "name": "settings"}, + {"modifiers": [], "name": "settings"}, + {"modifiers": [], "name": "settings"}, + {"modifiers": [], "name": "at_home"}, + ], + "id": 3486091, + "name": "Power Restored", + "settings": [], + "triggers": [], + }, + ], + "devices": [ + { + "_links": { + "filter_events": { + "href": "https://www.mynexia.com/mobile/houses/123456/events/collection?sys_guid=cd9a70e8-fd0d-4b58-b071-05a202fd8953" + }, + "nexia:history": { + "href": "https://www.mynexia.com/mobile/houses/123456/events?device_id=2059661" + }, + "pending_request": { + "polling_path": "https://www.mynexia.com/backstage/announcements/be6d8ede5cac02fe8be18c334b04d539c9200fa9230eef63" + }, + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059661" + }, + }, + "connected": True, + "delta": 3, + "features": [ + { + "items": [ + { + "label": "Model", + "type": "label_value", + "value": "XL1050", + }, + {"label": "AUID", "type": "label_value", "value": "000000"}, + { + "label": "Firmware Build Number", + "type": "label_value", + "value": "1581321824", + }, + { + "label": "Firmware Build Date", + "type": "label_value", + "value": "2020-02-10 08:03:44 UTC", + }, + { + "label": "Firmware Version", + "type": "label_value", + "value": "5.9.1", + }, + { + "label": "Zoning Enabled", + "type": "label_value", + "value": "yes", + }, + ], + "name": "advanced_info", + }, + { + "actions": {}, + "name": "thermostat", + "scale": "f", + "setpoint_cool_max": 99, + "setpoint_cool_min": 60, + "setpoint_delta": 3, + "setpoint_heat_max": 90, + "setpoint_heat_min": 55, + "setpoint_increment": 1.0, + "status": "System Idle", + "status_icon": None, + "temperature": 71, + }, + { + "is_connected": True, + "name": "connection", + "signal_strength": "unknown", + }, + { + "members": [ + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261002" + } + }, + "cooling_setpoint": 79, + "current_zone_mode": "AUTO", + "features": [ + { + "actions": { + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261002/setpoints" + }, + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261002/setpoints" + }, + }, + "name": "thermostat", + "scale": "f", + "setpoint_cool": 79, + "setpoint_cool_max": 99, + "setpoint_cool_min": 60, + "setpoint_delta": 3, + "setpoint_heat": 63, + "setpoint_heat_max": 90, + "setpoint_heat_min": 55, + "setpoint_increment": 1.0, + "status": "", + "status_icon": None, + "system_status": "System " "Idle", + "temperature": 71, + }, + { + "is_connected": True, + "name": "connection", + "signal_strength": "unknown", + }, + { + "actions": { + "update_thermostat_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261002/zone_mode", + "method": "POST", + } + }, + "display_value": "Auto", + "label": "Zone Mode", + "name": "thermostat_mode", + "options": [ + { + "header": True, + "id": "thermostat_mode", + "label": "Zone " "Mode", + "value": "thermostat_mode", + }, + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "value": "AUTO", + }, + { + "actions": { + "update_thermostat_run_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261002/run_mode", + "method": "POST", + } + }, + "display_value": "Hold", + "label": "Run Mode", + "name": "thermostat_run_mode", + "options": [ + { + "header": True, + "id": "thermostat_run_mode", + "label": "Run " "Mode", + "value": "thermostat_run_mode", + }, + { + "id": "info_text", + "info": True, + "label": "Follow " + "or " + "override " + "the " + "schedule.", + "value": "info_text", + }, + { + "label": "Permanent " "Hold", + "value": "permanent_hold", + }, + { + "label": "Run " "Schedule", + "value": "run_schedule", + }, + ], + "value": "permanent_hold", + }, + { + "actions": { + "enable_scheduling": { + "data": {"value": True}, + "href": "https://www.mynexia.com/mobile/xxl_zones/83261002/scheduling_enabled", + "method": "POST", + }, + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83261002", + "method": "POST", + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83261002", + "method": "GET", + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83261002", + "method": "POST", + }, + }, + "can_add_remove_periods": True, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83261002&house_id=123456", + "enabled": True, + "max_period_name_length": 10, + "max_periods_per_day": 4, + "name": "schedule", + "setpoint_increment": 1, + }, + ], + "heating_setpoint": 63, + "icon": { + "modifiers": ["temperature-71"], + "name": "thermostat", + }, + "id": 83261002, + "name": "Living East", + "operating_state": "", + "setpoints": {"cool": 79, "heat": 63}, + "settings": [ + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261002/preset_selected" + } + }, + "current_value": 0, + "labels": ["None", "Home", "Away", "Sleep"], + "options": [ + {"label": "None", "value": 0}, + {"label": "Home", "value": 1}, + {"label": "Away", "value": 2}, + {"label": "Sleep", "value": 3}, + ], + "title": "Preset", + "type": "preset_selected", + "values": [0, 1, 2, 3], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261002/zone_mode" + } + }, + "current_value": "AUTO", + "labels": ["Auto", "Cooling", "Heating", "Off"], + "options": [ + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "title": "Zone Mode", + "type": "zone_mode", + "values": ["AUTO", "COOL", "HEAT", "OFF"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261002/run_mode" + } + }, + "current_value": "permanent_hold", + "labels": [ + "Permanent " "Hold", + "Run " "Schedule", + ], + "options": [ + { + "label": "Permanent " "Hold", + "value": "permanent_hold", + }, + { + "label": "Run " "Schedule", + "value": "run_schedule", + }, + ], + "title": "Run Mode", + "type": "run_mode", + "values": ["permanent_hold", "run_schedule"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261002/scheduling_enabled" + } + }, + "current_value": True, + "labels": ["ON", "OFF"], + "options": [ + {"label": "ON", "value": True}, + {"label": "OFF", "value": False}, + ], + "title": "Scheduling", + "type": "scheduling_enabled", + "values": [True, False], + }, + ], + "temperature": 71, + "type": "xxl_zone", + "zone_status": "", + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261005" + } + }, + "cooling_setpoint": 79, + "current_zone_mode": "AUTO", + "features": [ + { + "actions": { + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261005/setpoints" + }, + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261005/setpoints" + }, + }, + "name": "thermostat", + "scale": "f", + "setpoint_cool": 79, + "setpoint_cool_max": 99, + "setpoint_cool_min": 60, + "setpoint_delta": 3, + "setpoint_heat": 63, + "setpoint_heat_max": 90, + "setpoint_heat_min": 55, + "setpoint_increment": 1.0, + "status": "", + "status_icon": None, + "system_status": "System " "Idle", + "temperature": 77, + }, + { + "is_connected": True, + "name": "connection", + "signal_strength": "unknown", + }, + { + "actions": { + "update_thermostat_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261005/zone_mode", + "method": "POST", + } + }, + "display_value": "Auto", + "label": "Zone Mode", + "name": "thermostat_mode", + "options": [ + { + "header": True, + "id": "thermostat_mode", + "label": "Zone " "Mode", + "value": "thermostat_mode", + }, + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "value": "AUTO", + }, + { + "actions": { + "update_thermostat_run_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261005/run_mode", + "method": "POST", + } + }, + "display_value": "Hold", + "label": "Run Mode", + "name": "thermostat_run_mode", + "options": [ + { + "header": True, + "id": "thermostat_run_mode", + "label": "Run " "Mode", + "value": "thermostat_run_mode", + }, + { + "id": "info_text", + "info": True, + "label": "Follow " + "or " + "override " + "the " + "schedule.", + "value": "info_text", + }, + { + "label": "Permanent " "Hold", + "value": "permanent_hold", + }, + { + "label": "Run " "Schedule", + "value": "run_schedule", + }, + ], + "value": "permanent_hold", + }, + { + "actions": { + "enable_scheduling": { + "data": {"value": True}, + "href": "https://www.mynexia.com/mobile/xxl_zones/83261005/scheduling_enabled", + "method": "POST", + }, + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83261005", + "method": "POST", + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83261005", + "method": "GET", + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83261005", + "method": "POST", + }, + }, + "can_add_remove_periods": True, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83261005&house_id=123456", + "enabled": True, + "max_period_name_length": 10, + "max_periods_per_day": 4, + "name": "schedule", + "setpoint_increment": 1, + }, + ], + "heating_setpoint": 63, + "icon": { + "modifiers": ["temperature-77"], + "name": "thermostat", + }, + "id": 83261005, + "name": "Kitchen", + "operating_state": "", + "setpoints": {"cool": 79, "heat": 63}, + "settings": [ + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261005/preset_selected" + } + }, + "current_value": 0, + "labels": ["None", "Home", "Away", "Sleep"], + "options": [ + {"label": "None", "value": 0}, + {"label": "Home", "value": 1}, + {"label": "Away", "value": 2}, + {"label": "Sleep", "value": 3}, + ], + "title": "Preset", + "type": "preset_selected", + "values": [0, 1, 2, 3], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261005/zone_mode" + } + }, + "current_value": "AUTO", + "labels": ["Auto", "Cooling", "Heating", "Off"], + "options": [ + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "title": "Zone Mode", + "type": "zone_mode", + "values": ["AUTO", "COOL", "HEAT", "OFF"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261005/run_mode" + } + }, + "current_value": "permanent_hold", + "labels": [ + "Permanent " "Hold", + "Run " "Schedule", + ], + "options": [ + { + "label": "Permanent " "Hold", + "value": "permanent_hold", + }, + { + "label": "Run " "Schedule", + "value": "run_schedule", + }, + ], + "title": "Run Mode", + "type": "run_mode", + "values": ["permanent_hold", "run_schedule"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261005/scheduling_enabled" + } + }, + "current_value": True, + "labels": ["ON", "OFF"], + "options": [ + {"label": "ON", "value": True}, + {"label": "OFF", "value": False}, + ], + "title": "Scheduling", + "type": "scheduling_enabled", + "values": [True, False], + }, + ], + "temperature": 77, + "type": "xxl_zone", + "zone_status": "", + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261008" + } + }, + "cooling_setpoint": 79, + "current_zone_mode": "AUTO", + "features": [ + { + "actions": { + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261008/setpoints" + }, + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261008/setpoints" + }, + }, + "name": "thermostat", + "scale": "f", + "setpoint_cool": 79, + "setpoint_cool_max": 99, + "setpoint_cool_min": 60, + "setpoint_delta": 3, + "setpoint_heat": 63, + "setpoint_heat_max": 90, + "setpoint_heat_min": 55, + "setpoint_increment": 1.0, + "status": "", + "status_icon": None, + "system_status": "System " "Idle", + "temperature": 72, + }, + { + "is_connected": True, + "name": "connection", + "signal_strength": "unknown", + }, + { + "actions": { + "update_thermostat_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261008/zone_mode", + "method": "POST", + } + }, + "display_value": "Auto", + "label": "Zone Mode", + "name": "thermostat_mode", + "options": [ + { + "header": True, + "id": "thermostat_mode", + "label": "Zone " "Mode", + "value": "thermostat_mode", + }, + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "value": "AUTO", + }, + { + "actions": { + "update_thermostat_run_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261008/run_mode", + "method": "POST", + } + }, + "display_value": "Hold", + "label": "Run Mode", + "name": "thermostat_run_mode", + "options": [ + { + "header": True, + "id": "thermostat_run_mode", + "label": "Run " "Mode", + "value": "thermostat_run_mode", + }, + { + "id": "info_text", + "info": True, + "label": "Follow " + "or " + "override " + "the " + "schedule.", + "value": "info_text", + }, + { + "label": "Permanent " "Hold", + "value": "permanent_hold", + }, + { + "label": "Run " "Schedule", + "value": "run_schedule", + }, + ], + "value": "permanent_hold", + }, + { + "actions": { + "enable_scheduling": { + "data": {"value": True}, + "href": "https://www.mynexia.com/mobile/xxl_zones/83261008/scheduling_enabled", + "method": "POST", + }, + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83261008", + "method": "POST", + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83261008", + "method": "GET", + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83261008", + "method": "POST", + }, + }, + "can_add_remove_periods": True, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83261008&house_id=123456", + "enabled": True, + "max_period_name_length": 10, + "max_periods_per_day": 4, + "name": "schedule", + "setpoint_increment": 1, + }, + ], + "heating_setpoint": 63, + "icon": { + "modifiers": ["temperature-72"], + "name": "thermostat", + }, + "id": 83261008, + "name": "Down Bedroom", + "operating_state": "", + "setpoints": {"cool": 79, "heat": 63}, + "settings": [ + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261008/preset_selected" + } + }, + "current_value": 0, + "labels": ["None", "Home", "Away", "Sleep"], + "options": [ + {"label": "None", "value": 0}, + {"label": "Home", "value": 1}, + {"label": "Away", "value": 2}, + {"label": "Sleep", "value": 3}, + ], + "title": "Preset", + "type": "preset_selected", + "values": [0, 1, 2, 3], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261008/zone_mode" + } + }, + "current_value": "AUTO", + "labels": ["Auto", "Cooling", "Heating", "Off"], + "options": [ + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "title": "Zone Mode", + "type": "zone_mode", + "values": ["AUTO", "COOL", "HEAT", "OFF"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261008/run_mode" + } + }, + "current_value": "permanent_hold", + "labels": [ + "Permanent " "Hold", + "Run " "Schedule", + ], + "options": [ + { + "label": "Permanent " "Hold", + "value": "permanent_hold", + }, + { + "label": "Run " "Schedule", + "value": "run_schedule", + }, + ], + "title": "Run Mode", + "type": "run_mode", + "values": ["permanent_hold", "run_schedule"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261008/scheduling_enabled" + } + }, + "current_value": True, + "labels": ["ON", "OFF"], + "options": [ + {"label": "ON", "value": True}, + {"label": "OFF", "value": False}, + ], + "title": "Scheduling", + "type": "scheduling_enabled", + "values": [True, False], + }, + ], + "temperature": 72, + "type": "xxl_zone", + "zone_status": "", + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261011" + } + }, + "cooling_setpoint": 79, + "current_zone_mode": "AUTO", + "features": [ + { + "actions": { + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261011/setpoints" + }, + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261011/setpoints" + }, + }, + "name": "thermostat", + "scale": "f", + "setpoint_cool": 79, + "setpoint_cool_max": 99, + "setpoint_cool_min": 60, + "setpoint_delta": 3, + "setpoint_heat": 63, + "setpoint_heat_max": 90, + "setpoint_heat_min": 55, + "setpoint_increment": 1.0, + "status": "", + "status_icon": None, + "system_status": "System " "Idle", + "temperature": 78, + }, + { + "is_connected": True, + "name": "connection", + "signal_strength": "unknown", + }, + { + "actions": { + "update_thermostat_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261011/zone_mode", + "method": "POST", + } + }, + "display_value": "Auto", + "label": "Zone Mode", + "name": "thermostat_mode", + "options": [ + { + "header": True, + "id": "thermostat_mode", + "label": "Zone " "Mode", + "value": "thermostat_mode", + }, + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "value": "AUTO", + }, + { + "actions": { + "update_thermostat_run_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261011/run_mode", + "method": "POST", + } + }, + "display_value": "Hold", + "label": "Run Mode", + "name": "thermostat_run_mode", + "options": [ + { + "header": True, + "id": "thermostat_run_mode", + "label": "Run " "Mode", + "value": "thermostat_run_mode", + }, + { + "id": "info_text", + "info": True, + "label": "Follow " + "or " + "override " + "the " + "schedule.", + "value": "info_text", + }, + { + "label": "Permanent " "Hold", + "value": "permanent_hold", + }, + { + "label": "Run " "Schedule", + "value": "run_schedule", + }, + ], + "value": "permanent_hold", + }, + { + "actions": { + "enable_scheduling": { + "data": {"value": True}, + "href": "https://www.mynexia.com/mobile/xxl_zones/83261011/scheduling_enabled", + "method": "POST", + }, + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83261011", + "method": "POST", + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83261011", + "method": "GET", + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83261011", + "method": "POST", + }, + }, + "can_add_remove_periods": True, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83261011&house_id=123456", + "enabled": True, + "max_period_name_length": 10, + "max_periods_per_day": 4, + "name": "schedule", + "setpoint_increment": 1, + }, + ], + "heating_setpoint": 63, + "icon": { + "modifiers": ["temperature-78"], + "name": "thermostat", + }, + "id": 83261011, + "name": "Tech Room", + "operating_state": "", + "setpoints": {"cool": 79, "heat": 63}, + "settings": [ + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261011/preset_selected" + } + }, + "current_value": 0, + "labels": ["None", "Home", "Away", "Sleep"], + "options": [ + {"label": "None", "value": 0}, + {"label": "Home", "value": 1}, + {"label": "Away", "value": 2}, + {"label": "Sleep", "value": 3}, + ], + "title": "Preset", + "type": "preset_selected", + "values": [0, 1, 2, 3], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261011/zone_mode" + } + }, + "current_value": "AUTO", + "labels": ["Auto", "Cooling", "Heating", "Off"], + "options": [ + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "title": "Zone Mode", + "type": "zone_mode", + "values": ["AUTO", "COOL", "HEAT", "OFF"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261011/run_mode" + } + }, + "current_value": "permanent_hold", + "labels": [ + "Permanent " "Hold", + "Run " "Schedule", + ], + "options": [ + { + "label": "Permanent " "Hold", + "value": "permanent_hold", + }, + { + "label": "Run " "Schedule", + "value": "run_schedule", + }, + ], + "title": "Run Mode", + "type": "run_mode", + "values": ["permanent_hold", "run_schedule"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261011/scheduling_enabled" + } + }, + "current_value": True, + "labels": ["ON", "OFF"], + "options": [ + {"label": "ON", "value": True}, + {"label": "OFF", "value": False}, + ], + "title": "Scheduling", + "type": "scheduling_enabled", + "values": [True, False], + }, + ], + "temperature": 78, + "type": "xxl_zone", + "zone_status": "", + }, + ], + "name": "group", + }, + { + "actions": { + "update_thermostat_fan_mode": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059661/fan_mode", + "method": "POST", + } + }, + "display_value": "Auto", + "label": "Fan Mode", + "name": "thermostat_fan_mode", + "options": [ + { + "header": True, + "id": "thermostat_fan_mode", + "label": "Fan Mode", + "value": "thermostat_fan_mode", + }, + {"label": "Auto", "value": "auto"}, + {"label": "On", "value": "on"}, + {"label": "Circulate", "value": "circulate"}, + ], + "status_icon": {"modifiers": [], "name": "thermostat_fan_off"}, + "value": "auto", + }, + {"compressor_speed": 0.0, "name": "thermostat_compressor_speed"}, + { + "actions": { + "get_monthly_runtime_history": { + "href": "https://www.mynexia.com/mobile/runtime_history/2059661?report_type=monthly", + "method": "GET", + }, + "get_runtime_history": { + "href": "https://www.mynexia.com/mobile/runtime_history/2059661?report_type=daily", + "method": "GET", + }, + }, + "name": "runtime_history", + }, + ], + "has_indoor_humidity": True, + "has_outdoor_temperature": True, + "icon": [ + {"modifiers": ["temperature-71"], "name": "thermostat"}, + {"modifiers": ["temperature-77"], "name": "thermostat"}, + {"modifiers": ["temperature-72"], "name": "thermostat"}, + {"modifiers": ["temperature-78"], "name": "thermostat"}, + ], + "id": 2059661, + "indoor_humidity": "36", + "last_updated_at": "2020-03-11T15:15:53.000-05:00", + "name": "Downstairs East Wing", + "name_editable": True, + "outdoor_temperature": "88", + "settings": [ + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059661/fan_mode" + } + }, + "current_value": "auto", + "labels": ["Auto", "On", "Circulate"], + "options": [ + {"label": "Auto", "value": "auto"}, + {"label": "On", "value": "on"}, + {"label": "Circulate", "value": "circulate"}, + ], + "title": "Fan Mode", + "type": "fan_mode", + "values": ["auto", "on", "circulate"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059661/fan_speed" + } + }, + "current_value": 0.35, + "labels": [ + "35%", + "40%", + "45%", + "50%", + "55%", + "60%", + "65%", + "70%", + "75%", + "80%", + "85%", + "90%", + "95%", + "100%", + ], + "options": [ + {"label": "35%", "value": 0.35}, + {"label": "40%", "value": 0.4}, + {"label": "45%", "value": 0.45}, + {"label": "50%", "value": 0.5}, + {"label": "55%", "value": 0.55}, + {"label": "60%", "value": 0.6}, + {"label": "65%", "value": 0.65}, + {"label": "70%", "value": 0.7}, + {"label": "75%", "value": 0.75}, + {"label": "80%", "value": 0.8}, + {"label": "85%", "value": 0.85}, + {"label": "90%", "value": 0.9}, + {"label": "95%", "value": 0.95}, + {"label": "100%", "value": 1.0}, + ], + "title": "Fan Speed", + "type": "fan_speed", + "values": [ + 0.35, + 0.4, + 0.45, + 0.5, + 0.55, + 0.6, + 0.65, + 0.7, + 0.75, + 0.8, + 0.85, + 0.9, + 0.95, + 1.0, + ], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059661/fan_circulation_time" + } + }, + "current_value": 30, + "labels": [ + "10 minutes", + "15 minutes", + "20 minutes", + "25 minutes", + "30 minutes", + "35 minutes", + "40 minutes", + "45 minutes", + "50 minutes", + "55 minutes", + ], + "options": [ + {"label": "10 minutes", "value": 10}, + {"label": "15 minutes", "value": 15}, + {"label": "20 minutes", "value": 20}, + {"label": "25 minutes", "value": 25}, + {"label": "30 minutes", "value": 30}, + {"label": "35 minutes", "value": 35}, + {"label": "40 minutes", "value": 40}, + {"label": "45 minutes", "value": 45}, + {"label": "50 minutes", "value": 50}, + {"label": "55 minutes", "value": 55}, + ], + "title": "Fan Circulation Time", + "type": "fan_circulation_time", + "values": [10, 15, 20, 25, 30, 35, 40, 45, 50, 55], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059661/air_cleaner_mode" + } + }, + "current_value": "auto", + "labels": ["Auto", "Quick", "Allergy"], + "options": [ + {"label": "Auto", "value": "auto"}, + {"label": "Quick", "value": "quick"}, + {"label": "Allergy", "value": "allergy"}, + ], + "title": "Air Cleaner Mode", + "type": "air_cleaner_mode", + "values": ["auto", "quick", "allergy"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059661/dehumidify" + } + }, + "current_value": 0.5, + "labels": ["35%", "40%", "45%", "50%", "55%", "60%", "65%"], + "options": [ + {"label": "35%", "value": 0.35}, + {"label": "40%", "value": 0.4}, + {"label": "45%", "value": 0.45}, + {"label": "50%", "value": 0.5}, + {"label": "55%", "value": 0.55}, + {"label": "60%", "value": 0.6}, + {"label": "65%", "value": 0.65}, + ], + "title": "Cooling Dehumidify Set Point", + "type": "dehumidify", + "values": [0.35, 0.4, 0.45, 0.5, 0.55, 0.6, 0.65], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059661/scale" + } + }, + "current_value": "f", + "labels": ["F", "C"], + "options": [ + {"label": "F", "value": "f"}, + {"label": "C", "value": "c"}, + ], + "title": "Temperature Scale", + "type": "scale", + "values": ["f", "c"], + }, + ], + "status_secondary": None, + "status_tertiary": None, + "system_status": "System Idle", + "type": "xxl_thermostat", + "zones": [ + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261002" + } + }, + "cooling_setpoint": 79, + "current_zone_mode": "AUTO", + "features": [ + { + "actions": { + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261002/setpoints" + }, + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261002/setpoints" + }, + }, + "name": "thermostat", + "scale": "f", + "setpoint_cool": 79, + "setpoint_cool_max": 99, + "setpoint_cool_min": 60, + "setpoint_delta": 3, + "setpoint_heat": 63, + "setpoint_heat_max": 90, + "setpoint_heat_min": 55, + "setpoint_increment": 1.0, + "status": "", + "status_icon": None, + "system_status": "System Idle", + "temperature": 71, + }, + { + "is_connected": True, + "name": "connection", + "signal_strength": "unknown", + }, + { + "actions": { + "update_thermostat_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261002/zone_mode", + "method": "POST", + } + }, + "display_value": "Auto", + "label": "Zone Mode", + "name": "thermostat_mode", + "options": [ + { + "header": True, + "id": "thermostat_mode", + "label": "Zone Mode", + "value": "thermostat_mode", + }, + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "value": "AUTO", + }, + { + "actions": { + "update_thermostat_run_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261002/run_mode", + "method": "POST", + } + }, + "display_value": "Hold", + "label": "Run Mode", + "name": "thermostat_run_mode", + "options": [ + { + "header": True, + "id": "thermostat_run_mode", + "label": "Run Mode", + "value": "thermostat_run_mode", + }, + { + "id": "info_text", + "info": True, + "label": "Follow or " + "override the " + "schedule.", + "value": "info_text", + }, + { + "label": "Permanent Hold", + "value": "permanent_hold", + }, + {"label": "Run Schedule", "value": "run_schedule"}, + ], + "value": "permanent_hold", + }, + { + "actions": { + "enable_scheduling": { + "data": {"value": True}, + "href": "https://www.mynexia.com/mobile/xxl_zones/83261002/scheduling_enabled", + "method": "POST", + }, + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83261002", + "method": "POST", + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83261002", + "method": "GET", + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83261002", + "method": "POST", + }, + }, + "can_add_remove_periods": True, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83261002&house_id=123456", + "enabled": True, + "max_period_name_length": 10, + "max_periods_per_day": 4, + "name": "schedule", + "setpoint_increment": 1, + }, + ], + "heating_setpoint": 63, + "icon": {"modifiers": ["temperature-71"], "name": "thermostat"}, + "id": 83261002, + "name": "Living East", + "operating_state": "", + "setpoints": {"cool": 79, "heat": 63}, + "settings": [ + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261002/preset_selected" + } + }, + "current_value": 0, + "labels": ["None", "Home", "Away", "Sleep"], + "options": [ + {"label": "None", "value": 0}, + {"label": "Home", "value": 1}, + {"label": "Away", "value": 2}, + {"label": "Sleep", "value": 3}, + ], + "title": "Preset", + "type": "preset_selected", + "values": [0, 1, 2, 3], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261002/zone_mode" + } + }, + "current_value": "AUTO", + "labels": ["Auto", "Cooling", "Heating", "Off"], + "options": [ + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "title": "Zone Mode", + "type": "zone_mode", + "values": ["AUTO", "COOL", "HEAT", "OFF"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261002/run_mode" + } + }, + "current_value": "permanent_hold", + "labels": ["Permanent Hold", "Run Schedule"], + "options": [ + { + "label": "Permanent Hold", + "value": "permanent_hold", + }, + {"label": "Run Schedule", "value": "run_schedule"}, + ], + "title": "Run Mode", + "type": "run_mode", + "values": ["permanent_hold", "run_schedule"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261002/scheduling_enabled" + } + }, + "current_value": True, + "labels": ["ON", "OFF"], + "options": [ + {"label": "ON", "value": True}, + {"label": "OFF", "value": False}, + ], + "title": "Scheduling", + "type": "scheduling_enabled", + "values": [True, False], + }, + ], + "temperature": 71, + "type": "xxl_zone", + "zone_status": "", + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261005" + } + }, + "cooling_setpoint": 79, + "current_zone_mode": "AUTO", + "features": [ + { + "actions": { + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261005/setpoints" + }, + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261005/setpoints" + }, + }, + "name": "thermostat", + "scale": "f", + "setpoint_cool": 79, + "setpoint_cool_max": 99, + "setpoint_cool_min": 60, + "setpoint_delta": 3, + "setpoint_heat": 63, + "setpoint_heat_max": 90, + "setpoint_heat_min": 55, + "setpoint_increment": 1.0, + "status": "", + "status_icon": None, + "system_status": "System Idle", + "temperature": 77, + }, + { + "is_connected": True, + "name": "connection", + "signal_strength": "unknown", + }, + { + "actions": { + "update_thermostat_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261005/zone_mode", + "method": "POST", + } + }, + "display_value": "Auto", + "label": "Zone Mode", + "name": "thermostat_mode", + "options": [ + { + "header": True, + "id": "thermostat_mode", + "label": "Zone Mode", + "value": "thermostat_mode", + }, + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "value": "AUTO", + }, + { + "actions": { + "update_thermostat_run_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261005/run_mode", + "method": "POST", + } + }, + "display_value": "Hold", + "label": "Run Mode", + "name": "thermostat_run_mode", + "options": [ + { + "header": True, + "id": "thermostat_run_mode", + "label": "Run Mode", + "value": "thermostat_run_mode", + }, + { + "id": "info_text", + "info": True, + "label": "Follow or " + "override the " + "schedule.", + "value": "info_text", + }, + { + "label": "Permanent Hold", + "value": "permanent_hold", + }, + {"label": "Run Schedule", "value": "run_schedule"}, + ], + "value": "permanent_hold", + }, + { + "actions": { + "enable_scheduling": { + "data": {"value": True}, + "href": "https://www.mynexia.com/mobile/xxl_zones/83261005/scheduling_enabled", + "method": "POST", + }, + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83261005", + "method": "POST", + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83261005", + "method": "GET", + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83261005", + "method": "POST", + }, + }, + "can_add_remove_periods": True, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83261005&house_id=123456", + "enabled": True, + "max_period_name_length": 10, + "max_periods_per_day": 4, + "name": "schedule", + "setpoint_increment": 1, + }, + ], + "heating_setpoint": 63, + "icon": {"modifiers": ["temperature-77"], "name": "thermostat"}, + "id": 83261005, + "name": "Kitchen", + "operating_state": "", + "setpoints": {"cool": 79, "heat": 63}, + "settings": [ + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261005/preset_selected" + } + }, + "current_value": 0, + "labels": ["None", "Home", "Away", "Sleep"], + "options": [ + {"label": "None", "value": 0}, + {"label": "Home", "value": 1}, + {"label": "Away", "value": 2}, + {"label": "Sleep", "value": 3}, + ], + "title": "Preset", + "type": "preset_selected", + "values": [0, 1, 2, 3], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261005/zone_mode" + } + }, + "current_value": "AUTO", + "labels": ["Auto", "Cooling", "Heating", "Off"], + "options": [ + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "title": "Zone Mode", + "type": "zone_mode", + "values": ["AUTO", "COOL", "HEAT", "OFF"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261005/run_mode" + } + }, + "current_value": "permanent_hold", + "labels": ["Permanent Hold", "Run Schedule"], + "options": [ + { + "label": "Permanent Hold", + "value": "permanent_hold", + }, + {"label": "Run Schedule", "value": "run_schedule"}, + ], + "title": "Run Mode", + "type": "run_mode", + "values": ["permanent_hold", "run_schedule"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261005/scheduling_enabled" + } + }, + "current_value": True, + "labels": ["ON", "OFF"], + "options": [ + {"label": "ON", "value": True}, + {"label": "OFF", "value": False}, + ], + "title": "Scheduling", + "type": "scheduling_enabled", + "values": [True, False], + }, + ], + "temperature": 77, + "type": "xxl_zone", + "zone_status": "", + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261008" + } + }, + "cooling_setpoint": 79, + "current_zone_mode": "AUTO", + "features": [ + { + "actions": { + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261008/setpoints" + }, + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261008/setpoints" + }, + }, + "name": "thermostat", + "scale": "f", + "setpoint_cool": 79, + "setpoint_cool_max": 99, + "setpoint_cool_min": 60, + "setpoint_delta": 3, + "setpoint_heat": 63, + "setpoint_heat_max": 90, + "setpoint_heat_min": 55, + "setpoint_increment": 1.0, + "status": "", + "status_icon": None, + "system_status": "System Idle", + "temperature": 72, + }, + { + "is_connected": True, + "name": "connection", + "signal_strength": "unknown", + }, + { + "actions": { + "update_thermostat_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261008/zone_mode", + "method": "POST", + } + }, + "display_value": "Auto", + "label": "Zone Mode", + "name": "thermostat_mode", + "options": [ + { + "header": True, + "id": "thermostat_mode", + "label": "Zone Mode", + "value": "thermostat_mode", + }, + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "value": "AUTO", + }, + { + "actions": { + "update_thermostat_run_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261008/run_mode", + "method": "POST", + } + }, + "display_value": "Hold", + "label": "Run Mode", + "name": "thermostat_run_mode", + "options": [ + { + "header": True, + "id": "thermostat_run_mode", + "label": "Run Mode", + "value": "thermostat_run_mode", + }, + { + "id": "info_text", + "info": True, + "label": "Follow or " + "override the " + "schedule.", + "value": "info_text", + }, + { + "label": "Permanent Hold", + "value": "permanent_hold", + }, + {"label": "Run Schedule", "value": "run_schedule"}, + ], + "value": "permanent_hold", + }, + { + "actions": { + "enable_scheduling": { + "data": {"value": True}, + "href": "https://www.mynexia.com/mobile/xxl_zones/83261008/scheduling_enabled", + "method": "POST", + }, + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83261008", + "method": "POST", + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83261008", + "method": "GET", + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83261008", + "method": "POST", + }, + }, + "can_add_remove_periods": True, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83261008&house_id=123456", + "enabled": True, + "max_period_name_length": 10, + "max_periods_per_day": 4, + "name": "schedule", + "setpoint_increment": 1, + }, + ], + "heating_setpoint": 63, + "icon": {"modifiers": ["temperature-72"], "name": "thermostat"}, + "id": 83261008, + "name": "Down Bedroom", + "operating_state": "", + "setpoints": {"cool": 79, "heat": 63}, + "settings": [ + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261008/preset_selected" + } + }, + "current_value": 0, + "labels": ["None", "Home", "Away", "Sleep"], + "options": [ + {"label": "None", "value": 0}, + {"label": "Home", "value": 1}, + {"label": "Away", "value": 2}, + {"label": "Sleep", "value": 3}, + ], + "title": "Preset", + "type": "preset_selected", + "values": [0, 1, 2, 3], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261008/zone_mode" + } + }, + "current_value": "AUTO", + "labels": ["Auto", "Cooling", "Heating", "Off"], + "options": [ + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "title": "Zone Mode", + "type": "zone_mode", + "values": ["AUTO", "COOL", "HEAT", "OFF"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261008/run_mode" + } + }, + "current_value": "permanent_hold", + "labels": ["Permanent Hold", "Run Schedule"], + "options": [ + { + "label": "Permanent Hold", + "value": "permanent_hold", + }, + {"label": "Run Schedule", "value": "run_schedule"}, + ], + "title": "Run Mode", + "type": "run_mode", + "values": ["permanent_hold", "run_schedule"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261008/scheduling_enabled" + } + }, + "current_value": True, + "labels": ["ON", "OFF"], + "options": [ + {"label": "ON", "value": True}, + {"label": "OFF", "value": False}, + ], + "title": "Scheduling", + "type": "scheduling_enabled", + "values": [True, False], + }, + ], + "temperature": 72, + "type": "xxl_zone", + "zone_status": "", + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261011" + } + }, + "cooling_setpoint": 79, + "current_zone_mode": "AUTO", + "features": [ + { + "actions": { + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261011/setpoints" + }, + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261011/setpoints" + }, + }, + "name": "thermostat", + "scale": "f", + "setpoint_cool": 79, + "setpoint_cool_max": 99, + "setpoint_cool_min": 60, + "setpoint_delta": 3, + "setpoint_heat": 63, + "setpoint_heat_max": 90, + "setpoint_heat_min": 55, + "setpoint_increment": 1.0, + "status": "", + "status_icon": None, + "system_status": "System Idle", + "temperature": 78, + }, + { + "is_connected": True, + "name": "connection", + "signal_strength": "unknown", + }, + { + "actions": { + "update_thermostat_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261011/zone_mode", + "method": "POST", + } + }, + "display_value": "Auto", + "label": "Zone Mode", + "name": "thermostat_mode", + "options": [ + { + "header": True, + "id": "thermostat_mode", + "label": "Zone Mode", + "value": "thermostat_mode", + }, + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "value": "AUTO", + }, + { + "actions": { + "update_thermostat_run_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261011/run_mode", + "method": "POST", + } + }, + "display_value": "Hold", + "label": "Run Mode", + "name": "thermostat_run_mode", + "options": [ + { + "header": True, + "id": "thermostat_run_mode", + "label": "Run Mode", + "value": "thermostat_run_mode", + }, + { + "id": "info_text", + "info": True, + "label": "Follow or " + "override the " + "schedule.", + "value": "info_text", + }, + { + "label": "Permanent Hold", + "value": "permanent_hold", + }, + {"label": "Run Schedule", "value": "run_schedule"}, + ], + "value": "permanent_hold", + }, + { + "actions": { + "enable_scheduling": { + "data": {"value": True}, + "href": "https://www.mynexia.com/mobile/xxl_zones/83261011/scheduling_enabled", + "method": "POST", + }, + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83261011", + "method": "POST", + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83261011", + "method": "GET", + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83261011", + "method": "POST", + }, + }, + "can_add_remove_periods": True, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83261011&house_id=123456", + "enabled": True, + "max_period_name_length": 10, + "max_periods_per_day": 4, + "name": "schedule", + "setpoint_increment": 1, + }, + ], + "heating_setpoint": 63, + "icon": {"modifiers": ["temperature-78"], "name": "thermostat"}, + "id": 83261011, + "name": "Tech Room", + "operating_state": "", + "setpoints": {"cool": 79, "heat": 63}, + "settings": [ + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261011/preset_selected" + } + }, + "current_value": 0, + "labels": ["None", "Home", "Away", "Sleep"], + "options": [ + {"label": "None", "value": 0}, + {"label": "Home", "value": 1}, + {"label": "Away", "value": 2}, + {"label": "Sleep", "value": 3}, + ], + "title": "Preset", + "type": "preset_selected", + "values": [0, 1, 2, 3], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261011/zone_mode" + } + }, + "current_value": "AUTO", + "labels": ["Auto", "Cooling", "Heating", "Off"], + "options": [ + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "title": "Zone Mode", + "type": "zone_mode", + "values": ["AUTO", "COOL", "HEAT", "OFF"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261011/run_mode" + } + }, + "current_value": "permanent_hold", + "labels": ["Permanent Hold", "Run Schedule"], + "options": [ + { + "label": "Permanent Hold", + "value": "permanent_hold", + }, + {"label": "Run Schedule", "value": "run_schedule"}, + ], + "title": "Run Mode", + "type": "run_mode", + "values": ["permanent_hold", "run_schedule"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261011/scheduling_enabled" + } + }, + "current_value": True, + "labels": ["ON", "OFF"], + "options": [ + {"label": "ON", "value": True}, + {"label": "OFF", "value": False}, + ], + "title": "Scheduling", + "type": "scheduling_enabled", + "values": [True, False], + }, + ], + "temperature": 78, + "type": "xxl_zone", + "zone_status": "", + }, + ], + }, + { + "_links": { + "filter_events": { + "href": "https://www.mynexia.com/mobile/houses/123456/events/collection?sys_guid=5aae72a6-1bd0-4d84-9bfd-673e7bc4907c" + }, + "nexia:history": { + "href": "https://www.mynexia.com/mobile/houses/123456/events?device_id=2059676" + }, + "pending_request": { + "polling_path": "https://www.mynexia.com/backstage/announcements/3412f1d96eb0c5edb5466c3c0598af60c06f8443f21e9bcb" + }, + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059676" + }, + }, + "connected": True, + "delta": 3, + "features": [ + { + "items": [ + { + "label": "Model", + "type": "label_value", + "value": "XL1050", + }, + { + "label": "AUID", + "type": "label_value", + "value": "02853E08", + }, + { + "label": "Firmware Build Number", + "type": "label_value", + "value": "1581321824", + }, + { + "label": "Firmware Build Date", + "type": "label_value", + "value": "2020-02-10 08:03:44 UTC", + }, + { + "label": "Firmware Version", + "type": "label_value", + "value": "5.9.1", + }, + { + "label": "Zoning Enabled", + "type": "label_value", + "value": "yes", + }, + ], + "name": "advanced_info", + }, + { + "actions": {}, + "name": "thermostat", + "scale": "f", + "setpoint_cool_max": 99, + "setpoint_cool_min": 60, + "setpoint_delta": 3, + "setpoint_heat_max": 90, + "setpoint_heat_min": 55, + "setpoint_increment": 1.0, + "status": "System Idle", + "status_icon": None, + "temperature": 75, + }, + { + "is_connected": True, + "name": "connection", + "signal_strength": "unknown", + }, + { + "members": [ + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261015" + } + }, + "cooling_setpoint": 79, + "current_zone_mode": "AUTO", + "features": [ + { + "actions": { + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261015/setpoints" + }, + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261015/setpoints" + }, + }, + "name": "thermostat", + "scale": "f", + "setpoint_cool": 79, + "setpoint_cool_max": 99, + "setpoint_cool_min": 60, + "setpoint_delta": 3, + "setpoint_heat": 63, + "setpoint_heat_max": 90, + "setpoint_heat_min": 55, + "setpoint_increment": 1.0, + "status": "", + "status_icon": None, + "system_status": "System " "Idle", + "temperature": 75, + }, + { + "is_connected": True, + "name": "connection", + "signal_strength": "unknown", + }, + { + "actions": { + "update_thermostat_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261015/zone_mode", + "method": "POST", + } + }, + "display_value": "Auto", + "label": "Zone Mode", + "name": "thermostat_mode", + "options": [ + { + "header": True, + "id": "thermostat_mode", + "label": "Zone " "Mode", + "value": "thermostat_mode", + }, + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "value": "AUTO", + }, + { + "actions": { + "update_thermostat_run_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261015/run_mode", + "method": "POST", + } + }, + "display_value": "Hold", + "label": "Run Mode", + "name": "thermostat_run_mode", + "options": [ + { + "header": True, + "id": "thermostat_run_mode", + "label": "Run " "Mode", + "value": "thermostat_run_mode", + }, + { + "id": "info_text", + "info": True, + "label": "Follow " + "or " + "override " + "the " + "schedule.", + "value": "info_text", + }, + { + "label": "Permanent " "Hold", + "value": "permanent_hold", + }, + { + "label": "Run " "Schedule", + "value": "run_schedule", + }, + ], + "value": "permanent_hold", + }, + { + "actions": { + "enable_scheduling": { + "data": {"value": True}, + "href": "https://www.mynexia.com/mobile/xxl_zones/83261015/scheduling_enabled", + "method": "POST", + }, + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83261015", + "method": "POST", + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83261015", + "method": "GET", + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83261015", + "method": "POST", + }, + }, + "can_add_remove_periods": True, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83261015&house_id=123456", + "enabled": True, + "max_period_name_length": 10, + "max_periods_per_day": 4, + "name": "schedule", + "setpoint_increment": 1, + }, + ], + "heating_setpoint": 63, + "icon": { + "modifiers": ["temperature-75"], + "name": "thermostat", + }, + "id": 83261015, + "name": "Living West", + "operating_state": "", + "setpoints": {"cool": 79, "heat": 63}, + "settings": [ + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261015/preset_selected" + } + }, + "current_value": 0, + "labels": ["None", "Home", "Away", "Sleep"], + "options": [ + {"label": "None", "value": 0}, + {"label": "Home", "value": 1}, + {"label": "Away", "value": 2}, + {"label": "Sleep", "value": 3}, + ], + "title": "Preset", + "type": "preset_selected", + "values": [0, 1, 2, 3], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261015/zone_mode" + } + }, + "current_value": "AUTO", + "labels": ["Auto", "Cooling", "Heating", "Off"], + "options": [ + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "title": "Zone Mode", + "type": "zone_mode", + "values": ["AUTO", "COOL", "HEAT", "OFF"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261015/run_mode" + } + }, + "current_value": "permanent_hold", + "labels": [ + "Permanent " "Hold", + "Run " "Schedule", + ], + "options": [ + { + "label": "Permanent " "Hold", + "value": "permanent_hold", + }, + { + "label": "Run " "Schedule", + "value": "run_schedule", + }, + ], + "title": "Run Mode", + "type": "run_mode", + "values": ["permanent_hold", "run_schedule"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261015/scheduling_enabled" + } + }, + "current_value": True, + "labels": ["ON", "OFF"], + "options": [ + {"label": "ON", "value": True}, + {"label": "OFF", "value": False}, + ], + "title": "Scheduling", + "type": "scheduling_enabled", + "values": [True, False], + }, + ], + "temperature": 75, + "type": "xxl_zone", + "zone_status": "", + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261018" + } + }, + "cooling_setpoint": 79, + "current_zone_mode": "AUTO", + "features": [ + { + "actions": { + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261018/setpoints" + }, + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261018/setpoints" + }, + }, + "name": "thermostat", + "scale": "f", + "setpoint_cool": 79, + "setpoint_cool_max": 99, + "setpoint_cool_min": 60, + "setpoint_delta": 3, + "setpoint_heat": 63, + "setpoint_heat_max": 90, + "setpoint_heat_min": 55, + "setpoint_increment": 1.0, + "status": "", + "status_icon": None, + "system_status": "System " "Idle", + "temperature": 75, + }, + { + "is_connected": True, + "name": "connection", + "signal_strength": "unknown", + }, + { + "actions": { + "update_thermostat_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261018/zone_mode", + "method": "POST", + } + }, + "display_value": "Auto", + "label": "Zone Mode", + "name": "thermostat_mode", + "options": [ + { + "header": True, + "id": "thermostat_mode", + "label": "Zone " "Mode", + "value": "thermostat_mode", + }, + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "value": "AUTO", + }, + { + "actions": { + "update_thermostat_run_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261018/run_mode", + "method": "POST", + } + }, + "display_value": "Hold", + "label": "Run Mode", + "name": "thermostat_run_mode", + "options": [ + { + "header": True, + "id": "thermostat_run_mode", + "label": "Run " "Mode", + "value": "thermostat_run_mode", + }, + { + "id": "info_text", + "info": True, + "label": "Follow " + "or " + "override " + "the " + "schedule.", + "value": "info_text", + }, + { + "label": "Permanent " "Hold", + "value": "permanent_hold", + }, + { + "label": "Run " "Schedule", + "value": "run_schedule", + }, + ], + "value": "permanent_hold", + }, + { + "actions": { + "enable_scheduling": { + "data": {"value": True}, + "href": "https://www.mynexia.com/mobile/xxl_zones/83261018/scheduling_enabled", + "method": "POST", + }, + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83261018", + "method": "POST", + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83261018", + "method": "GET", + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83261018", + "method": "POST", + }, + }, + "can_add_remove_periods": True, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83261018&house_id=123456", + "enabled": True, + "max_period_name_length": 10, + "max_periods_per_day": 4, + "name": "schedule", + "setpoint_increment": 1, + }, + ], + "heating_setpoint": 63, + "icon": { + "modifiers": ["temperature-75"], + "name": "thermostat", + }, + "id": 83261018, + "name": "David Office", + "operating_state": "", + "setpoints": {"cool": 79, "heat": 63}, + "settings": [ + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261018/preset_selected" + } + }, + "current_value": 0, + "labels": ["None", "Home", "Away", "Sleep"], + "options": [ + {"label": "None", "value": 0}, + {"label": "Home", "value": 1}, + {"label": "Away", "value": 2}, + {"label": "Sleep", "value": 3}, + ], + "title": "Preset", + "type": "preset_selected", + "values": [0, 1, 2, 3], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261018/zone_mode" + } + }, + "current_value": "AUTO", + "labels": ["Auto", "Cooling", "Heating", "Off"], + "options": [ + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "title": "Zone Mode", + "type": "zone_mode", + "values": ["AUTO", "COOL", "HEAT", "OFF"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261018/run_mode" + } + }, + "current_value": "permanent_hold", + "labels": [ + "Permanent " "Hold", + "Run " "Schedule", + ], + "options": [ + { + "label": "Permanent " "Hold", + "value": "permanent_hold", + }, + { + "label": "Run " "Schedule", + "value": "run_schedule", + }, + ], + "title": "Run Mode", + "type": "run_mode", + "values": ["permanent_hold", "run_schedule"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261018/scheduling_enabled" + } + }, + "current_value": True, + "labels": ["ON", "OFF"], + "options": [ + {"label": "ON", "value": True}, + {"label": "OFF", "value": False}, + ], + "title": "Scheduling", + "type": "scheduling_enabled", + "values": [True, False], + }, + ], + "temperature": 75, + "type": "xxl_zone", + "zone_status": "", + }, + ], + "name": "group", + }, + { + "actions": { + "update_thermostat_fan_mode": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059676/fan_mode", + "method": "POST", + } + }, + "display_value": "Auto", + "label": "Fan Mode", + "name": "thermostat_fan_mode", + "options": [ + { + "header": True, + "id": "thermostat_fan_mode", + "label": "Fan Mode", + "value": "thermostat_fan_mode", + }, + {"label": "Auto", "value": "auto"}, + {"label": "On", "value": "on"}, + {"label": "Circulate", "value": "circulate"}, + ], + "status_icon": {"modifiers": [], "name": "thermostat_fan_off"}, + "value": "auto", + }, + {"compressor_speed": 0.0, "name": "thermostat_compressor_speed"}, + { + "actions": { + "get_monthly_runtime_history": { + "href": "https://www.mynexia.com/mobile/runtime_history/2059676?report_type=monthly", + "method": "GET", + }, + "get_runtime_history": { + "href": "https://www.mynexia.com/mobile/runtime_history/2059676?report_type=daily", + "method": "GET", + }, + }, + "name": "runtime_history", + }, + ], + "has_indoor_humidity": True, + "has_outdoor_temperature": True, + "icon": [ + {"modifiers": ["temperature-75"], "name": "thermostat"}, + {"modifiers": ["temperature-75"], "name": "thermostat"}, + ], + "id": 2059676, + "indoor_humidity": "52", + "last_updated_at": "2020-03-11T15:15:53.000-05:00", + "name": "Downstairs West Wing", + "name_editable": True, + "outdoor_temperature": "88", + "settings": [ + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059676/fan_mode" + } + }, + "current_value": "auto", + "labels": ["Auto", "On", "Circulate"], + "options": [ + {"label": "Auto", "value": "auto"}, + {"label": "On", "value": "on"}, + {"label": "Circulate", "value": "circulate"}, + ], + "title": "Fan Mode", + "type": "fan_mode", + "values": ["auto", "on", "circulate"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059676/fan_speed" + } + }, + "current_value": 0.35, + "labels": [ + "35%", + "40%", + "45%", + "50%", + "55%", + "60%", + "65%", + "70%", + "75%", + "80%", + "85%", + "90%", + "95%", + "100%", + ], + "options": [ + {"label": "35%", "value": 0.35}, + {"label": "40%", "value": 0.4}, + {"label": "45%", "value": 0.45}, + {"label": "50%", "value": 0.5}, + {"label": "55%", "value": 0.55}, + {"label": "60%", "value": 0.6}, + {"label": "65%", "value": 0.65}, + {"label": "70%", "value": 0.7}, + {"label": "75%", "value": 0.75}, + {"label": "80%", "value": 0.8}, + {"label": "85%", "value": 0.85}, + {"label": "90%", "value": 0.9}, + {"label": "95%", "value": 0.95}, + {"label": "100%", "value": 1.0}, + ], + "title": "Fan Speed", + "type": "fan_speed", + "values": [ + 0.35, + 0.4, + 0.45, + 0.5, + 0.55, + 0.6, + 0.65, + 0.7, + 0.75, + 0.8, + 0.85, + 0.9, + 0.95, + 1.0, + ], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059676/fan_circulation_time" + } + }, + "current_value": 30, + "labels": [ + "10 minutes", + "15 minutes", + "20 minutes", + "25 minutes", + "30 minutes", + "35 minutes", + "40 minutes", + "45 minutes", + "50 minutes", + "55 minutes", + ], + "options": [ + {"label": "10 minutes", "value": 10}, + {"label": "15 minutes", "value": 15}, + {"label": "20 minutes", "value": 20}, + {"label": "25 minutes", "value": 25}, + {"label": "30 minutes", "value": 30}, + {"label": "35 minutes", "value": 35}, + {"label": "40 minutes", "value": 40}, + {"label": "45 minutes", "value": 45}, + {"label": "50 minutes", "value": 50}, + {"label": "55 minutes", "value": 55}, + ], + "title": "Fan Circulation Time", + "type": "fan_circulation_time", + "values": [10, 15, 20, 25, 30, 35, 40, 45, 50, 55], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059676/air_cleaner_mode" + } + }, + "current_value": "auto", + "labels": ["Auto", "Quick", "Allergy"], + "options": [ + {"label": "Auto", "value": "auto"}, + {"label": "Quick", "value": "quick"}, + {"label": "Allergy", "value": "allergy"}, + ], + "title": "Air Cleaner Mode", + "type": "air_cleaner_mode", + "values": ["auto", "quick", "allergy"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059676/dehumidify" + } + }, + "current_value": 0.45, + "labels": ["35%", "40%", "45%", "50%", "55%", "60%", "65%"], + "options": [ + {"label": "35%", "value": 0.35}, + {"label": "40%", "value": 0.4}, + {"label": "45%", "value": 0.45}, + {"label": "50%", "value": 0.5}, + {"label": "55%", "value": 0.55}, + {"label": "60%", "value": 0.6}, + {"label": "65%", "value": 0.65}, + ], + "title": "Cooling Dehumidify Set Point", + "type": "dehumidify", + "values": [0.35, 0.4, 0.45, 0.5, 0.55, 0.6, 0.65], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059676/scale" + } + }, + "current_value": "f", + "labels": ["F", "C"], + "options": [ + {"label": "F", "value": "f"}, + {"label": "C", "value": "c"}, + ], + "title": "Temperature Scale", + "type": "scale", + "values": ["f", "c"], + }, + ], + "status_secondary": None, + "status_tertiary": None, + "system_status": "System Idle", + "type": "xxl_thermostat", + "zones": [ + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261015" + } + }, + "cooling_setpoint": 79, + "current_zone_mode": "AUTO", + "features": [ + { + "actions": { + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261015/setpoints" + }, + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261015/setpoints" + }, + }, + "name": "thermostat", + "scale": "f", + "setpoint_cool": 79, + "setpoint_cool_max": 99, + "setpoint_cool_min": 60, + "setpoint_delta": 3, + "setpoint_heat": 63, + "setpoint_heat_max": 90, + "setpoint_heat_min": 55, + "setpoint_increment": 1.0, + "status": "", + "status_icon": None, + "system_status": "System Idle", + "temperature": 75, + }, + { + "is_connected": True, + "name": "connection", + "signal_strength": "unknown", + }, + { + "actions": { + "update_thermostat_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261015/zone_mode", + "method": "POST", + } + }, + "display_value": "Auto", + "label": "Zone Mode", + "name": "thermostat_mode", + "options": [ + { + "header": True, + "id": "thermostat_mode", + "label": "Zone Mode", + "value": "thermostat_mode", + }, + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "value": "AUTO", + }, + { + "actions": { + "update_thermostat_run_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261015/run_mode", + "method": "POST", + } + }, + "display_value": "Hold", + "label": "Run Mode", + "name": "thermostat_run_mode", + "options": [ + { + "header": True, + "id": "thermostat_run_mode", + "label": "Run Mode", + "value": "thermostat_run_mode", + }, + { + "id": "info_text", + "info": True, + "label": "Follow or " + "override the " + "schedule.", + "value": "info_text", + }, + { + "label": "Permanent Hold", + "value": "permanent_hold", + }, + {"label": "Run Schedule", "value": "run_schedule"}, + ], + "value": "permanent_hold", + }, + { + "actions": { + "enable_scheduling": { + "data": {"value": True}, + "href": "https://www.mynexia.com/mobile/xxl_zones/83261015/scheduling_enabled", + "method": "POST", + }, + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83261015", + "method": "POST", + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83261015", + "method": "GET", + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83261015", + "method": "POST", + }, + }, + "can_add_remove_periods": True, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83261015&house_id=123456", + "enabled": True, + "max_period_name_length": 10, + "max_periods_per_day": 4, + "name": "schedule", + "setpoint_increment": 1, + }, + ], + "heating_setpoint": 63, + "icon": {"modifiers": ["temperature-75"], "name": "thermostat"}, + "id": 83261015, + "name": "Living West", + "operating_state": "", + "setpoints": {"cool": 79, "heat": 63}, + "settings": [ + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261015/preset_selected" + } + }, + "current_value": 0, + "labels": ["None", "Home", "Away", "Sleep"], + "options": [ + {"label": "None", "value": 0}, + {"label": "Home", "value": 1}, + {"label": "Away", "value": 2}, + {"label": "Sleep", "value": 3}, + ], + "title": "Preset", + "type": "preset_selected", + "values": [0, 1, 2, 3], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261015/zone_mode" + } + }, + "current_value": "AUTO", + "labels": ["Auto", "Cooling", "Heating", "Off"], + "options": [ + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "title": "Zone Mode", + "type": "zone_mode", + "values": ["AUTO", "COOL", "HEAT", "OFF"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261015/run_mode" + } + }, + "current_value": "permanent_hold", + "labels": ["Permanent Hold", "Run Schedule"], + "options": [ + { + "label": "Permanent Hold", + "value": "permanent_hold", + }, + {"label": "Run Schedule", "value": "run_schedule"}, + ], + "title": "Run Mode", + "type": "run_mode", + "values": ["permanent_hold", "run_schedule"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261015/scheduling_enabled" + } + }, + "current_value": True, + "labels": ["ON", "OFF"], + "options": [ + {"label": "ON", "value": True}, + {"label": "OFF", "value": False}, + ], + "title": "Scheduling", + "type": "scheduling_enabled", + "values": [True, False], + }, + ], + "temperature": 75, + "type": "xxl_zone", + "zone_status": "", + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261018" + } + }, + "cooling_setpoint": 79, + "current_zone_mode": "AUTO", + "features": [ + { + "actions": { + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261018/setpoints" + }, + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261018/setpoints" + }, + }, + "name": "thermostat", + "scale": "f", + "setpoint_cool": 79, + "setpoint_cool_max": 99, + "setpoint_cool_min": 60, + "setpoint_delta": 3, + "setpoint_heat": 63, + "setpoint_heat_max": 90, + "setpoint_heat_min": 55, + "setpoint_increment": 1.0, + "status": "", + "status_icon": None, + "system_status": "System Idle", + "temperature": 75, + }, + { + "is_connected": True, + "name": "connection", + "signal_strength": "unknown", + }, + { + "actions": { + "update_thermostat_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261018/zone_mode", + "method": "POST", + } + }, + "display_value": "Auto", + "label": "Zone Mode", + "name": "thermostat_mode", + "options": [ + { + "header": True, + "id": "thermostat_mode", + "label": "Zone Mode", + "value": "thermostat_mode", + }, + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "value": "AUTO", + }, + { + "actions": { + "update_thermostat_run_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261018/run_mode", + "method": "POST", + } + }, + "display_value": "Hold", + "label": "Run Mode", + "name": "thermostat_run_mode", + "options": [ + { + "header": True, + "id": "thermostat_run_mode", + "label": "Run Mode", + "value": "thermostat_run_mode", + }, + { + "id": "info_text", + "info": True, + "label": "Follow or " + "override the " + "schedule.", + "value": "info_text", + }, + { + "label": "Permanent Hold", + "value": "permanent_hold", + }, + {"label": "Run Schedule", "value": "run_schedule"}, + ], + "value": "permanent_hold", + }, + { + "actions": { + "enable_scheduling": { + "data": {"value": True}, + "href": "https://www.mynexia.com/mobile/xxl_zones/83261018/scheduling_enabled", + "method": "POST", + }, + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83261018", + "method": "POST", + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83261018", + "method": "GET", + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83261018", + "method": "POST", + }, + }, + "can_add_remove_periods": True, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83261018&house_id=123456", + "enabled": True, + "max_period_name_length": 10, + "max_periods_per_day": 4, + "name": "schedule", + "setpoint_increment": 1, + }, + ], + "heating_setpoint": 63, + "icon": {"modifiers": ["temperature-75"], "name": "thermostat"}, + "id": 83261018, + "name": "David Office", + "operating_state": "", + "setpoints": {"cool": 79, "heat": 63}, + "settings": [ + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261018/preset_selected" + } + }, + "current_value": 0, + "labels": ["None", "Home", "Away", "Sleep"], + "options": [ + {"label": "None", "value": 0}, + {"label": "Home", "value": 1}, + {"label": "Away", "value": 2}, + {"label": "Sleep", "value": 3}, + ], + "title": "Preset", + "type": "preset_selected", + "values": [0, 1, 2, 3], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261018/zone_mode" + } + }, + "current_value": "AUTO", + "labels": ["Auto", "Cooling", "Heating", "Off"], + "options": [ + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "title": "Zone Mode", + "type": "zone_mode", + "values": ["AUTO", "COOL", "HEAT", "OFF"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261018/run_mode" + } + }, + "current_value": "permanent_hold", + "labels": ["Permanent Hold", "Run Schedule"], + "options": [ + { + "label": "Permanent Hold", + "value": "permanent_hold", + }, + {"label": "Run Schedule", "value": "run_schedule"}, + ], + "title": "Run Mode", + "type": "run_mode", + "values": ["permanent_hold", "run_schedule"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261018/scheduling_enabled" + } + }, + "current_value": True, + "labels": ["ON", "OFF"], + "options": [ + {"label": "ON", "value": True}, + {"label": "OFF", "value": False}, + ], + "title": "Scheduling", + "type": "scheduling_enabled", + "values": [True, False], + }, + ], + "temperature": 75, + "type": "xxl_zone", + "zone_status": "", + }, + ], + }, + { + "_links": { + "filter_events": { + "href": "https://www.mynexia.com/mobile/houses/123456/events/collection?sys_guid=e3fc90c7-2885-4f57-ae76-99e9ec81eef0" + }, + "nexia:history": { + "href": "https://www.mynexia.com/mobile/houses/123456/events?device_id=2293892" + }, + "pending_request": { + "polling_path": "https://www.mynexia.com/backstage/announcements/967361e8aed874aa5230930fd0e0bbd8b653261e982a6e0e" + }, + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2293892" + }, + }, + "connected": True, + "delta": 3, + "features": [ + { + "items": [ + { + "label": "Model", + "type": "label_value", + "value": "XL1050", + }, + { + "label": "AUID", + "type": "label_value", + "value": "0281B02C", + }, + { + "label": "Firmware Build Number", + "type": "label_value", + "value": "1581321824", + }, + { + "label": "Firmware Build Date", + "type": "label_value", + "value": "2020-02-10 08:03:44 UTC", + }, + { + "label": "Firmware Version", + "type": "label_value", + "value": "5.9.1", + }, + { + "label": "Zoning Enabled", + "type": "label_value", + "value": "yes", + }, + ], + "name": "advanced_info", + }, + { + "actions": {}, + "name": "thermostat", + "scale": "f", + "setpoint_cool_max": 99, + "setpoint_cool_min": 60, + "setpoint_delta": 3, + "setpoint_heat_max": 90, + "setpoint_heat_min": 55, + "setpoint_increment": 1.0, + "status": "Cooling", + "status_icon": {"modifiers": [], "name": "cooling"}, + "temperature": 73, + }, + { + "is_connected": True, + "name": "connection", + "signal_strength": "unknown", + }, + { + "members": [ + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133" + } + }, + "cooling_setpoint": 79, + "current_zone_mode": "AUTO", + "features": [ + { + "actions": { + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/setpoints" + }, + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/setpoints" + }, + }, + "name": "thermostat", + "scale": "f", + "setpoint_cool": 79, + "setpoint_cool_max": 99, + "setpoint_cool_min": 60, + "setpoint_delta": 3, + "setpoint_heat": 63, + "setpoint_heat_max": 90, + "setpoint_heat_min": 55, + "setpoint_increment": 1.0, + "status": "Relieving " "Air", + "status_icon": { + "modifiers": [], + "name": "cooling", + }, + "system_status": "Cooling", + "temperature": 73, + }, + { + "is_connected": True, + "name": "connection", + "signal_strength": "unknown", + }, + { + "actions": { + "update_thermostat_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/zone_mode", + "method": "POST", + } + }, + "display_value": "Auto", + "label": "Zone Mode", + "name": "thermostat_mode", + "options": [ + { + "header": True, + "id": "thermostat_mode", + "label": "Zone " "Mode", + "value": "thermostat_mode", + }, + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "value": "AUTO", + }, + { + "actions": { + "update_thermostat_run_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/run_mode", + "method": "POST", + } + }, + "display_value": "Hold", + "label": "Run Mode", + "name": "thermostat_run_mode", + "options": [ + { + "header": True, + "id": "thermostat_run_mode", + "label": "Run " "Mode", + "value": "thermostat_run_mode", + }, + { + "id": "info_text", + "info": True, + "label": "Follow " + "or " + "override " + "the " + "schedule.", + "value": "info_text", + }, + { + "label": "Permanent " "Hold", + "value": "permanent_hold", + }, + { + "label": "Run " "Schedule", + "value": "run_schedule", + }, + ], + "value": "permanent_hold", + }, + { + "actions": { + "enable_scheduling": { + "data": {"value": True}, + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/scheduling_enabled", + "method": "POST", + }, + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83394133", + "method": "POST", + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83394133", + "method": "GET", + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83394133", + "method": "POST", + }, + }, + "can_add_remove_periods": True, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83394133&house_id=123456", + "enabled": True, + "max_period_name_length": 10, + "max_periods_per_day": 4, + "name": "schedule", + "setpoint_increment": 1, + }, + ], + "heating_setpoint": 63, + "icon": { + "modifiers": ["temperature-73"], + "name": "thermostat", + }, + "id": 83394133, + "name": "Bath Closet", + "operating_state": "Relieving Air", + "setpoints": {"cool": 79, "heat": 63}, + "settings": [ + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/preset_selected" + } + }, + "current_value": 0, + "labels": ["None", "Home", "Away", "Sleep"], + "options": [ + {"label": "None", "value": 0}, + {"label": "Home", "value": 1}, + {"label": "Away", "value": 2}, + {"label": "Sleep", "value": 3}, + ], + "title": "Preset", + "type": "preset_selected", + "values": [0, 1, 2, 3], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/zone_mode" + } + }, + "current_value": "AUTO", + "labels": ["Auto", "Cooling", "Heating", "Off"], + "options": [ + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "title": "Zone Mode", + "type": "zone_mode", + "values": ["AUTO", "COOL", "HEAT", "OFF"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/run_mode" + } + }, + "current_value": "permanent_hold", + "labels": [ + "Permanent " "Hold", + "Run " "Schedule", + ], + "options": [ + { + "label": "Permanent " "Hold", + "value": "permanent_hold", + }, + { + "label": "Run " "Schedule", + "value": "run_schedule", + }, + ], + "title": "Run Mode", + "type": "run_mode", + "values": ["permanent_hold", "run_schedule"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/scheduling_enabled" + } + }, + "current_value": True, + "labels": ["ON", "OFF"], + "options": [ + {"label": "ON", "value": True}, + {"label": "OFF", "value": False}, + ], + "title": "Scheduling", + "type": "scheduling_enabled", + "values": [True, False], + }, + ], + "temperature": 73, + "type": "xxl_zone", + "zone_status": "Relieving Air", + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130" + } + }, + "cooling_setpoint": 71, + "current_zone_mode": "AUTO", + "features": [ + { + "actions": { + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/setpoints" + }, + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/setpoints" + }, + }, + "name": "thermostat", + "scale": "f", + "setpoint_cool": 71, + "setpoint_cool_max": 99, + "setpoint_cool_min": 60, + "setpoint_delta": 3, + "setpoint_heat": 63, + "setpoint_heat_max": 90, + "setpoint_heat_min": 55, + "setpoint_increment": 1.0, + "status": "Damper Open", + "status_icon": { + "modifiers": [], + "name": "cooling", + }, + "system_status": "Cooling", + "temperature": 74, + }, + { + "is_connected": True, + "name": "connection", + "signal_strength": "unknown", + }, + { + "actions": { + "update_thermostat_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/zone_mode", + "method": "POST", + } + }, + "display_value": "Auto", + "label": "Zone Mode", + "name": "thermostat_mode", + "options": [ + { + "header": True, + "id": "thermostat_mode", + "label": "Zone " "Mode", + "value": "thermostat_mode", + }, + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "value": "AUTO", + }, + { + "actions": { + "update_thermostat_run_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/run_mode", + "method": "POST", + } + }, + "display_value": "Hold", + "label": "Run Mode", + "name": "thermostat_run_mode", + "options": [ + { + "header": True, + "id": "thermostat_run_mode", + "label": "Run " "Mode", + "value": "thermostat_run_mode", + }, + { + "id": "info_text", + "info": True, + "label": "Follow " + "or " + "override " + "the " + "schedule.", + "value": "info_text", + }, + { + "label": "Permanent " "Hold", + "value": "permanent_hold", + }, + { + "label": "Run " "Schedule", + "value": "run_schedule", + }, + ], + "value": "permanent_hold", + }, + { + "actions": { + "enable_scheduling": { + "data": {"value": True}, + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/scheduling_enabled", + "method": "POST", + }, + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83394130", + "method": "POST", + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83394130", + "method": "GET", + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83394130", + "method": "POST", + }, + }, + "can_add_remove_periods": True, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83394130&house_id=123456", + "enabled": True, + "max_period_name_length": 10, + "max_periods_per_day": 4, + "name": "schedule", + "setpoint_increment": 1, + }, + ], + "heating_setpoint": 63, + "icon": { + "modifiers": ["temperature-74"], + "name": "thermostat", + }, + "id": 83394130, + "name": "Master", + "operating_state": "Damper Open", + "setpoints": {"cool": 71, "heat": 63}, + "settings": [ + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/preset_selected" + } + }, + "current_value": 0, + "labels": ["None", "Home", "Away", "Sleep"], + "options": [ + {"label": "None", "value": 0}, + {"label": "Home", "value": 1}, + {"label": "Away", "value": 2}, + {"label": "Sleep", "value": 3}, + ], + "title": "Preset", + "type": "preset_selected", + "values": [0, 1, 2, 3], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/zone_mode" + } + }, + "current_value": "AUTO", + "labels": ["Auto", "Cooling", "Heating", "Off"], + "options": [ + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "title": "Zone Mode", + "type": "zone_mode", + "values": ["AUTO", "COOL", "HEAT", "OFF"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/run_mode" + } + }, + "current_value": "permanent_hold", + "labels": [ + "Permanent " "Hold", + "Run " "Schedule", + ], + "options": [ + { + "label": "Permanent " "Hold", + "value": "permanent_hold", + }, + { + "label": "Run " "Schedule", + "value": "run_schedule", + }, + ], + "title": "Run Mode", + "type": "run_mode", + "values": ["permanent_hold", "run_schedule"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/scheduling_enabled" + } + }, + "current_value": True, + "labels": ["ON", "OFF"], + "options": [ + {"label": "ON", "value": True}, + {"label": "OFF", "value": False}, + ], + "title": "Scheduling", + "type": "scheduling_enabled", + "values": [True, False], + }, + ], + "temperature": 74, + "type": "xxl_zone", + "zone_status": "Damper Open", + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136" + } + }, + "cooling_setpoint": 79, + "current_zone_mode": "AUTO", + "features": [ + { + "actions": { + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/setpoints" + }, + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/setpoints" + }, + }, + "name": "thermostat", + "scale": "f", + "setpoint_cool": 79, + "setpoint_cool_max": 99, + "setpoint_cool_min": 60, + "setpoint_delta": 3, + "setpoint_heat": 63, + "setpoint_heat_max": 90, + "setpoint_heat_min": 55, + "setpoint_increment": 1.0, + "status": "Relieving " "Air", + "status_icon": { + "modifiers": [], + "name": "cooling", + }, + "system_status": "Cooling", + "temperature": 73, + }, + { + "is_connected": True, + "name": "connection", + "signal_strength": "unknown", + }, + { + "actions": { + "update_thermostat_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/zone_mode", + "method": "POST", + } + }, + "display_value": "Auto", + "label": "Zone Mode", + "name": "thermostat_mode", + "options": [ + { + "header": True, + "id": "thermostat_mode", + "label": "Zone " "Mode", + "value": "thermostat_mode", + }, + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "value": "AUTO", + }, + { + "actions": { + "update_thermostat_run_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/run_mode", + "method": "POST", + } + }, + "display_value": "Hold", + "label": "Run Mode", + "name": "thermostat_run_mode", + "options": [ + { + "header": True, + "id": "thermostat_run_mode", + "label": "Run " "Mode", + "value": "thermostat_run_mode", + }, + { + "id": "info_text", + "info": True, + "label": "Follow " + "or " + "override " + "the " + "schedule.", + "value": "info_text", + }, + { + "label": "Permanent " "Hold", + "value": "permanent_hold", + }, + { + "label": "Run " "Schedule", + "value": "run_schedule", + }, + ], + "value": "permanent_hold", + }, + { + "actions": { + "enable_scheduling": { + "data": {"value": True}, + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/scheduling_enabled", + "method": "POST", + }, + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83394136", + "method": "POST", + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83394136", + "method": "GET", + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83394136", + "method": "POST", + }, + }, + "can_add_remove_periods": True, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83394136&house_id=123456", + "enabled": True, + "max_period_name_length": 10, + "max_periods_per_day": 4, + "name": "schedule", + "setpoint_increment": 1, + }, + ], + "heating_setpoint": 63, + "icon": { + "modifiers": ["temperature-73"], + "name": "thermostat", + }, + "id": 83394136, + "name": "Nick Office", + "operating_state": "Relieving Air", + "setpoints": {"cool": 79, "heat": 63}, + "settings": [ + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/preset_selected" + } + }, + "current_value": 0, + "labels": ["None", "Home", "Away", "Sleep"], + "options": [ + {"label": "None", "value": 0}, + {"label": "Home", "value": 1}, + {"label": "Away", "value": 2}, + {"label": "Sleep", "value": 3}, + ], + "title": "Preset", + "type": "preset_selected", + "values": [0, 1, 2, 3], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/zone_mode" + } + }, + "current_value": "AUTO", + "labels": ["Auto", "Cooling", "Heating", "Off"], + "options": [ + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "title": "Zone Mode", + "type": "zone_mode", + "values": ["AUTO", "COOL", "HEAT", "OFF"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/run_mode" + } + }, + "current_value": "permanent_hold", + "labels": [ + "Permanent " "Hold", + "Run " "Schedule", + ], + "options": [ + { + "label": "Permanent " "Hold", + "value": "permanent_hold", + }, + { + "label": "Run " "Schedule", + "value": "run_schedule", + }, + ], + "title": "Run Mode", + "type": "run_mode", + "values": ["permanent_hold", "run_schedule"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/scheduling_enabled" + } + }, + "current_value": True, + "labels": ["ON", "OFF"], + "options": [ + {"label": "ON", "value": True}, + {"label": "OFF", "value": False}, + ], + "title": "Scheduling", + "type": "scheduling_enabled", + "values": [True, False], + }, + ], + "temperature": 73, + "type": "xxl_zone", + "zone_status": "Relieving Air", + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127" + } + }, + "cooling_setpoint": 79, + "current_zone_mode": "AUTO", + "features": [ + { + "actions": { + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/setpoints" + }, + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/setpoints" + }, + }, + "name": "thermostat", + "scale": "f", + "setpoint_cool": 79, + "setpoint_cool_max": 99, + "setpoint_cool_min": 60, + "setpoint_delta": 3, + "setpoint_heat": 63, + "setpoint_heat_max": 90, + "setpoint_heat_min": 55, + "setpoint_increment": 1.0, + "status": "Damper " "Closed", + "status_icon": { + "modifiers": [], + "name": "cooling", + }, + "system_status": "Cooling", + "temperature": 72, + }, + { + "is_connected": True, + "name": "connection", + "signal_strength": "unknown", + }, + { + "actions": { + "update_thermostat_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/zone_mode", + "method": "POST", + } + }, + "display_value": "Auto", + "label": "Zone Mode", + "name": "thermostat_mode", + "options": [ + { + "header": True, + "id": "thermostat_mode", + "label": "Zone " "Mode", + "value": "thermostat_mode", + }, + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "value": "AUTO", + }, + { + "actions": { + "update_thermostat_run_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/run_mode", + "method": "POST", + } + }, + "display_value": "Hold", + "label": "Run Mode", + "name": "thermostat_run_mode", + "options": [ + { + "header": True, + "id": "thermostat_run_mode", + "label": "Run " "Mode", + "value": "thermostat_run_mode", + }, + { + "id": "info_text", + "info": True, + "label": "Follow " + "or " + "override " + "the " + "schedule.", + "value": "info_text", + }, + { + "label": "Permanent " "Hold", + "value": "permanent_hold", + }, + { + "label": "Run " "Schedule", + "value": "run_schedule", + }, + ], + "value": "permanent_hold", + }, + { + "actions": { + "enable_scheduling": { + "data": {"value": True}, + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/scheduling_enabled", + "method": "POST", + }, + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83394127", + "method": "POST", + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83394127", + "method": "GET", + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83394127", + "method": "POST", + }, + }, + "can_add_remove_periods": True, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83394127&house_id=123456", + "enabled": True, + "max_period_name_length": 10, + "max_periods_per_day": 4, + "name": "schedule", + "setpoint_increment": 1, + }, + ], + "heating_setpoint": 63, + "icon": { + "modifiers": ["temperature-72"], + "name": "thermostat", + }, + "id": 83394127, + "name": "Snooze Room", + "operating_state": "Damper Closed", + "setpoints": {"cool": 79, "heat": 63}, + "settings": [ + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/preset_selected" + } + }, + "current_value": 0, + "labels": ["None", "Home", "Away", "Sleep"], + "options": [ + {"label": "None", "value": 0}, + {"label": "Home", "value": 1}, + {"label": "Away", "value": 2}, + {"label": "Sleep", "value": 3}, + ], + "title": "Preset", + "type": "preset_selected", + "values": [0, 1, 2, 3], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/zone_mode" + } + }, + "current_value": "AUTO", + "labels": ["Auto", "Cooling", "Heating", "Off"], + "options": [ + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "title": "Zone Mode", + "type": "zone_mode", + "values": ["AUTO", "COOL", "HEAT", "OFF"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/run_mode" + } + }, + "current_value": "permanent_hold", + "labels": [ + "Permanent " "Hold", + "Run " "Schedule", + ], + "options": [ + { + "label": "Permanent " "Hold", + "value": "permanent_hold", + }, + { + "label": "Run " "Schedule", + "value": "run_schedule", + }, + ], + "title": "Run Mode", + "type": "run_mode", + "values": ["permanent_hold", "run_schedule"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/scheduling_enabled" + } + }, + "current_value": True, + "labels": ["ON", "OFF"], + "options": [ + {"label": "ON", "value": True}, + {"label": "OFF", "value": False}, + ], + "title": "Scheduling", + "type": "scheduling_enabled", + "values": [True, False], + }, + ], + "temperature": 72, + "type": "xxl_zone", + "zone_status": "Damper Closed", + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139" + } + }, + "cooling_setpoint": 79, + "current_zone_mode": "AUTO", + "features": [ + { + "actions": { + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/setpoints" + }, + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/setpoints" + }, + }, + "name": "thermostat", + "scale": "f", + "setpoint_cool": 79, + "setpoint_cool_max": 99, + "setpoint_cool_min": 60, + "setpoint_delta": 3, + "setpoint_heat": 63, + "setpoint_heat_max": 90, + "setpoint_heat_min": 55, + "setpoint_increment": 1.0, + "status": "Damper " "Closed", + "status_icon": { + "modifiers": [], + "name": "cooling", + }, + "system_status": "Cooling", + "temperature": 74, + }, + { + "is_connected": True, + "name": "connection", + "signal_strength": "unknown", + }, + { + "actions": { + "update_thermostat_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/zone_mode", + "method": "POST", + } + }, + "display_value": "Auto", + "label": "Zone Mode", + "name": "thermostat_mode", + "options": [ + { + "header": True, + "id": "thermostat_mode", + "label": "Zone " "Mode", + "value": "thermostat_mode", + }, + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "value": "AUTO", + }, + { + "actions": { + "update_thermostat_run_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/run_mode", + "method": "POST", + } + }, + "display_value": "Hold", + "label": "Run Mode", + "name": "thermostat_run_mode", + "options": [ + { + "header": True, + "id": "thermostat_run_mode", + "label": "Run " "Mode", + "value": "thermostat_run_mode", + }, + { + "id": "info_text", + "info": True, + "label": "Follow " + "or " + "override " + "the " + "schedule.", + "value": "info_text", + }, + { + "label": "Permanent " "Hold", + "value": "permanent_hold", + }, + { + "label": "Run " "Schedule", + "value": "run_schedule", + }, + ], + "value": "permanent_hold", + }, + { + "actions": { + "enable_scheduling": { + "data": {"value": True}, + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/scheduling_enabled", + "method": "POST", + }, + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83394139", + "method": "POST", + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83394139", + "method": "GET", + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83394139", + "method": "POST", + }, + }, + "can_add_remove_periods": True, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83394139&house_id=123456", + "enabled": True, + "max_period_name_length": 10, + "max_periods_per_day": 4, + "name": "schedule", + "setpoint_increment": 1, + }, + ], + "heating_setpoint": 63, + "icon": { + "modifiers": ["temperature-74"], + "name": "thermostat", + }, + "id": 83394139, + "name": "Safe Room", + "operating_state": "Damper Closed", + "setpoints": {"cool": 79, "heat": 63}, + "settings": [ + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/preset_selected" + } + }, + "current_value": 0, + "labels": ["None", "Home", "Away", "Sleep"], + "options": [ + {"label": "None", "value": 0}, + {"label": "Home", "value": 1}, + {"label": "Away", "value": 2}, + {"label": "Sleep", "value": 3}, + ], + "title": "Preset", + "type": "preset_selected", + "values": [0, 1, 2, 3], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/zone_mode" + } + }, + "current_value": "AUTO", + "labels": ["Auto", "Cooling", "Heating", "Off"], + "options": [ + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "title": "Zone Mode", + "type": "zone_mode", + "values": ["AUTO", "COOL", "HEAT", "OFF"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/run_mode" + } + }, + "current_value": "permanent_hold", + "labels": [ + "Permanent " "Hold", + "Run " "Schedule", + ], + "options": [ + { + "label": "Permanent " "Hold", + "value": "permanent_hold", + }, + { + "label": "Run " "Schedule", + "value": "run_schedule", + }, + ], + "title": "Run Mode", + "type": "run_mode", + "values": ["permanent_hold", "run_schedule"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/scheduling_enabled" + } + }, + "current_value": True, + "labels": ["ON", "OFF"], + "options": [ + {"label": "ON", "value": True}, + {"label": "OFF", "value": False}, + ], + "title": "Scheduling", + "type": "scheduling_enabled", + "values": [True, False], + }, + ], + "temperature": 74, + "type": "xxl_zone", + "zone_status": "Damper Closed", + }, + ], + "name": "group", + }, + { + "actions": { + "update_thermostat_fan_mode": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2293892/fan_mode", + "method": "POST", + } + }, + "display_value": "Auto", + "label": "Fan Mode", + "name": "thermostat_fan_mode", + "options": [ + { + "header": True, + "id": "thermostat_fan_mode", + "label": "Fan Mode", + "value": "thermostat_fan_mode", + }, + {"label": "Auto", "value": "auto"}, + {"label": "On", "value": "on"}, + {"label": "Circulate", "value": "circulate"}, + ], + "status_icon": {"modifiers": [], "name": "thermostat_fan_on"}, + "value": "auto", + }, + {"compressor_speed": 0.69, "name": "thermostat_compressor_speed"}, + { + "actions": { + "get_monthly_runtime_history": { + "href": "https://www.mynexia.com/mobile/runtime_history/2293892?report_type=monthly", + "method": "GET", + }, + "get_runtime_history": { + "href": "https://www.mynexia.com/mobile/runtime_history/2293892?report_type=daily", + "method": "GET", + }, + }, + "name": "runtime_history", + }, + ], + "has_indoor_humidity": True, + "has_outdoor_temperature": True, + "icon": [ + {"modifiers": ["temperature-73"], "name": "thermostat"}, + {"modifiers": ["temperature-74"], "name": "thermostat"}, + {"modifiers": ["temperature-73"], "name": "thermostat"}, + {"modifiers": ["temperature-72"], "name": "thermostat"}, + {"modifiers": ["temperature-74"], "name": "thermostat"}, + ], + "id": 2293892, + "indoor_humidity": "52", + "last_updated_at": "2020-03-11T15:15:53.000-05:00", + "name": "Master Suite", + "name_editable": True, + "outdoor_temperature": "87", + "settings": [ + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2293892/fan_mode" + } + }, + "current_value": "auto", + "labels": ["Auto", "On", "Circulate"], + "options": [ + {"label": "Auto", "value": "auto"}, + {"label": "On", "value": "on"}, + {"label": "Circulate", "value": "circulate"}, + ], + "title": "Fan Mode", + "type": "fan_mode", + "values": ["auto", "on", "circulate"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2293892/fan_speed" + } + }, + "current_value": 0.35, + "labels": [ + "35%", + "40%", + "45%", + "50%", + "55%", + "60%", + "65%", + "70%", + "75%", + "80%", + "85%", + "90%", + "95%", + "100%", + ], + "options": [ + {"label": "35%", "value": 0.35}, + {"label": "40%", "value": 0.4}, + {"label": "45%", "value": 0.45}, + {"label": "50%", "value": 0.5}, + {"label": "55%", "value": 0.55}, + {"label": "60%", "value": 0.6}, + {"label": "65%", "value": 0.65}, + {"label": "70%", "value": 0.7}, + {"label": "75%", "value": 0.75}, + {"label": "80%", "value": 0.8}, + {"label": "85%", "value": 0.85}, + {"label": "90%", "value": 0.9}, + {"label": "95%", "value": 0.95}, + {"label": "100%", "value": 1.0}, + ], + "title": "Fan Speed", + "type": "fan_speed", + "values": [ + 0.35, + 0.4, + 0.45, + 0.5, + 0.55, + 0.6, + 0.65, + 0.7, + 0.75, + 0.8, + 0.85, + 0.9, + 0.95, + 1.0, + ], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2293892/fan_circulation_time" + } + }, + "current_value": 30, + "labels": [ + "10 minutes", + "15 minutes", + "20 minutes", + "25 minutes", + "30 minutes", + "35 minutes", + "40 minutes", + "45 minutes", + "50 minutes", + "55 minutes", + ], + "options": [ + {"label": "10 minutes", "value": 10}, + {"label": "15 minutes", "value": 15}, + {"label": "20 minutes", "value": 20}, + {"label": "25 minutes", "value": 25}, + {"label": "30 minutes", "value": 30}, + {"label": "35 minutes", "value": 35}, + {"label": "40 minutes", "value": 40}, + {"label": "45 minutes", "value": 45}, + {"label": "50 minutes", "value": 50}, + {"label": "55 minutes", "value": 55}, + ], + "title": "Fan Circulation Time", + "type": "fan_circulation_time", + "values": [10, 15, 20, 25, 30, 35, 40, 45, 50, 55], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2293892/air_cleaner_mode" + } + }, + "current_value": "auto", + "labels": ["Auto", "Quick", "Allergy"], + "options": [ + {"label": "Auto", "value": "auto"}, + {"label": "Quick", "value": "quick"}, + {"label": "Allergy", "value": "allergy"}, + ], + "title": "Air Cleaner Mode", + "type": "air_cleaner_mode", + "values": ["auto", "quick", "allergy"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2293892/dehumidify" + } + }, + "current_value": 0.45, + "labels": ["35%", "40%", "45%", "50%", "55%", "60%", "65%"], + "options": [ + {"label": "35%", "value": 0.35}, + {"label": "40%", "value": 0.4}, + {"label": "45%", "value": 0.45}, + {"label": "50%", "value": 0.5}, + {"label": "55%", "value": 0.55}, + {"label": "60%", "value": 0.6}, + {"label": "65%", "value": 0.65}, + ], + "title": "Cooling Dehumidify Set Point", + "type": "dehumidify", + "values": [0.35, 0.4, 0.45, 0.5, 0.55, 0.6, 0.65], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2293892/scale" + } + }, + "current_value": "f", + "labels": ["F", "C"], + "options": [ + {"label": "F", "value": "f"}, + {"label": "C", "value": "c"}, + ], + "title": "Temperature Scale", + "type": "scale", + "values": ["f", "c"], + }, + ], + "status_secondary": None, + "status_tertiary": None, + "system_status": "Cooling", + "type": "xxl_thermostat", + "zones": [ + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133" + } + }, + "cooling_setpoint": 79, + "current_zone_mode": "AUTO", + "features": [ + { + "actions": { + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/setpoints" + }, + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/setpoints" + }, + }, + "name": "thermostat", + "scale": "f", + "setpoint_cool": 79, + "setpoint_cool_max": 99, + "setpoint_cool_min": 60, + "setpoint_delta": 3, + "setpoint_heat": 63, + "setpoint_heat_max": 90, + "setpoint_heat_min": 55, + "setpoint_increment": 1.0, + "status": "Relieving Air", + "status_icon": {"modifiers": [], "name": "cooling"}, + "system_status": "Cooling", + "temperature": 73, + }, + { + "is_connected": True, + "name": "connection", + "signal_strength": "unknown", + }, + { + "actions": { + "update_thermostat_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/zone_mode", + "method": "POST", + } + }, + "display_value": "Auto", + "label": "Zone Mode", + "name": "thermostat_mode", + "options": [ + { + "header": True, + "id": "thermostat_mode", + "label": "Zone Mode", + "value": "thermostat_mode", + }, + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "value": "AUTO", + }, + { + "actions": { + "update_thermostat_run_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/run_mode", + "method": "POST", + } + }, + "display_value": "Hold", + "label": "Run Mode", + "name": "thermostat_run_mode", + "options": [ + { + "header": True, + "id": "thermostat_run_mode", + "label": "Run Mode", + "value": "thermostat_run_mode", + }, + { + "id": "info_text", + "info": True, + "label": "Follow or " + "override the " + "schedule.", + "value": "info_text", + }, + { + "label": "Permanent Hold", + "value": "permanent_hold", + }, + {"label": "Run Schedule", "value": "run_schedule"}, + ], + "value": "permanent_hold", + }, + { + "actions": { + "enable_scheduling": { + "data": {"value": True}, + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/scheduling_enabled", + "method": "POST", + }, + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83394133", + "method": "POST", + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83394133", + "method": "GET", + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83394133", + "method": "POST", + }, + }, + "can_add_remove_periods": True, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83394133&house_id=123456", + "enabled": True, + "max_period_name_length": 10, + "max_periods_per_day": 4, + "name": "schedule", + "setpoint_increment": 1, + }, + ], + "heating_setpoint": 63, + "icon": {"modifiers": ["temperature-73"], "name": "thermostat"}, + "id": 83394133, + "name": "Bath Closet", + "operating_state": "Relieving Air", + "setpoints": {"cool": 79, "heat": 63}, + "settings": [ + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/preset_selected" + } + }, + "current_value": 0, + "labels": ["None", "Home", "Away", "Sleep"], + "options": [ + {"label": "None", "value": 0}, + {"label": "Home", "value": 1}, + {"label": "Away", "value": 2}, + {"label": "Sleep", "value": 3}, + ], + "title": "Preset", + "type": "preset_selected", + "values": [0, 1, 2, 3], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/zone_mode" + } + }, + "current_value": "AUTO", + "labels": ["Auto", "Cooling", "Heating", "Off"], + "options": [ + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "title": "Zone Mode", + "type": "zone_mode", + "values": ["AUTO", "COOL", "HEAT", "OFF"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/run_mode" + } + }, + "current_value": "permanent_hold", + "labels": ["Permanent Hold", "Run Schedule"], + "options": [ + { + "label": "Permanent Hold", + "value": "permanent_hold", + }, + {"label": "Run Schedule", "value": "run_schedule"}, + ], + "title": "Run Mode", + "type": "run_mode", + "values": ["permanent_hold", "run_schedule"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/scheduling_enabled" + } + }, + "current_value": True, + "labels": ["ON", "OFF"], + "options": [ + {"label": "ON", "value": True}, + {"label": "OFF", "value": False}, + ], + "title": "Scheduling", + "type": "scheduling_enabled", + "values": [True, False], + }, + ], + "temperature": 73, + "type": "xxl_zone", + "zone_status": "Relieving Air", + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130" + } + }, + "cooling_setpoint": 71, + "current_zone_mode": "AUTO", + "features": [ + { + "actions": { + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/setpoints" + }, + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/setpoints" + }, + }, + "name": "thermostat", + "scale": "f", + "setpoint_cool": 71, + "setpoint_cool_max": 99, + "setpoint_cool_min": 60, + "setpoint_delta": 3, + "setpoint_heat": 63, + "setpoint_heat_max": 90, + "setpoint_heat_min": 55, + "setpoint_increment": 1.0, + "status": "Damper Open", + "status_icon": {"modifiers": [], "name": "cooling"}, + "system_status": "Cooling", + "temperature": 74, + }, + { + "is_connected": True, + "name": "connection", + "signal_strength": "unknown", + }, + { + "actions": { + "update_thermostat_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/zone_mode", + "method": "POST", + } + }, + "display_value": "Auto", + "label": "Zone Mode", + "name": "thermostat_mode", + "options": [ + { + "header": True, + "id": "thermostat_mode", + "label": "Zone Mode", + "value": "thermostat_mode", + }, + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "value": "AUTO", + }, + { + "actions": { + "update_thermostat_run_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/run_mode", + "method": "POST", + } + }, + "display_value": "Hold", + "label": "Run Mode", + "name": "thermostat_run_mode", + "options": [ + { + "header": True, + "id": "thermostat_run_mode", + "label": "Run Mode", + "value": "thermostat_run_mode", + }, + { + "id": "info_text", + "info": True, + "label": "Follow or " + "override the " + "schedule.", + "value": "info_text", + }, + { + "label": "Permanent Hold", + "value": "permanent_hold", + }, + {"label": "Run Schedule", "value": "run_schedule"}, + ], + "value": "permanent_hold", + }, + { + "actions": { + "enable_scheduling": { + "data": {"value": True}, + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/scheduling_enabled", + "method": "POST", + }, + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83394130", + "method": "POST", + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83394130", + "method": "GET", + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83394130", + "method": "POST", + }, + }, + "can_add_remove_periods": True, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83394130&house_id=123456", + "enabled": True, + "max_period_name_length": 10, + "max_periods_per_day": 4, + "name": "schedule", + "setpoint_increment": 1, + }, + ], + "heating_setpoint": 63, + "icon": {"modifiers": ["temperature-74"], "name": "thermostat"}, + "id": 83394130, + "name": "Master", + "operating_state": "Damper Open", + "setpoints": {"cool": 71, "heat": 63}, + "settings": [ + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/preset_selected" + } + }, + "current_value": 0, + "labels": ["None", "Home", "Away", "Sleep"], + "options": [ + {"label": "None", "value": 0}, + {"label": "Home", "value": 1}, + {"label": "Away", "value": 2}, + {"label": "Sleep", "value": 3}, + ], + "title": "Preset", + "type": "preset_selected", + "values": [0, 1, 2, 3], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/zone_mode" + } + }, + "current_value": "AUTO", + "labels": ["Auto", "Cooling", "Heating", "Off"], + "options": [ + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "title": "Zone Mode", + "type": "zone_mode", + "values": ["AUTO", "COOL", "HEAT", "OFF"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/run_mode" + } + }, + "current_value": "permanent_hold", + "labels": ["Permanent Hold", "Run Schedule"], + "options": [ + { + "label": "Permanent Hold", + "value": "permanent_hold", + }, + {"label": "Run Schedule", "value": "run_schedule"}, + ], + "title": "Run Mode", + "type": "run_mode", + "values": ["permanent_hold", "run_schedule"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/scheduling_enabled" + } + }, + "current_value": True, + "labels": ["ON", "OFF"], + "options": [ + {"label": "ON", "value": True}, + {"label": "OFF", "value": False}, + ], + "title": "Scheduling", + "type": "scheduling_enabled", + "values": [True, False], + }, + ], + "temperature": 74, + "type": "xxl_zone", + "zone_status": "Damper Open", + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136" + } + }, + "cooling_setpoint": 79, + "current_zone_mode": "AUTO", + "features": [ + { + "actions": { + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/setpoints" + }, + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/setpoints" + }, + }, + "name": "thermostat", + "scale": "f", + "setpoint_cool": 79, + "setpoint_cool_max": 99, + "setpoint_cool_min": 60, + "setpoint_delta": 3, + "setpoint_heat": 63, + "setpoint_heat_max": 90, + "setpoint_heat_min": 55, + "setpoint_increment": 1.0, + "status": "Relieving Air", + "status_icon": {"modifiers": [], "name": "cooling"}, + "system_status": "Cooling", + "temperature": 73, + }, + { + "is_connected": True, + "name": "connection", + "signal_strength": "unknown", + }, + { + "actions": { + "update_thermostat_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/zone_mode", + "method": "POST", + } + }, + "display_value": "Auto", + "label": "Zone Mode", + "name": "thermostat_mode", + "options": [ + { + "header": True, + "id": "thermostat_mode", + "label": "Zone Mode", + "value": "thermostat_mode", + }, + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "value": "AUTO", + }, + { + "actions": { + "update_thermostat_run_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/run_mode", + "method": "POST", + } + }, + "display_value": "Hold", + "label": "Run Mode", + "name": "thermostat_run_mode", + "options": [ + { + "header": True, + "id": "thermostat_run_mode", + "label": "Run Mode", + "value": "thermostat_run_mode", + }, + { + "id": "info_text", + "info": True, + "label": "Follow or " + "override the " + "schedule.", + "value": "info_text", + }, + { + "label": "Permanent Hold", + "value": "permanent_hold", + }, + {"label": "Run Schedule", "value": "run_schedule"}, + ], + "value": "permanent_hold", + }, + { + "actions": { + "enable_scheduling": { + "data": {"value": True}, + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/scheduling_enabled", + "method": "POST", + }, + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83394136", + "method": "POST", + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83394136", + "method": "GET", + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83394136", + "method": "POST", + }, + }, + "can_add_remove_periods": True, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83394136&house_id=123456", + "enabled": True, + "max_period_name_length": 10, + "max_periods_per_day": 4, + "name": "schedule", + "setpoint_increment": 1, + }, + ], + "heating_setpoint": 63, + "icon": {"modifiers": ["temperature-73"], "name": "thermostat"}, + "id": 83394136, + "name": "Nick Office", + "operating_state": "Relieving Air", + "setpoints": {"cool": 79, "heat": 63}, + "settings": [ + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/preset_selected" + } + }, + "current_value": 0, + "labels": ["None", "Home", "Away", "Sleep"], + "options": [ + {"label": "None", "value": 0}, + {"label": "Home", "value": 1}, + {"label": "Away", "value": 2}, + {"label": "Sleep", "value": 3}, + ], + "title": "Preset", + "type": "preset_selected", + "values": [0, 1, 2, 3], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/zone_mode" + } + }, + "current_value": "AUTO", + "labels": ["Auto", "Cooling", "Heating", "Off"], + "options": [ + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "title": "Zone Mode", + "type": "zone_mode", + "values": ["AUTO", "COOL", "HEAT", "OFF"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/run_mode" + } + }, + "current_value": "permanent_hold", + "labels": ["Permanent Hold", "Run Schedule"], + "options": [ + { + "label": "Permanent Hold", + "value": "permanent_hold", + }, + {"label": "Run Schedule", "value": "run_schedule"}, + ], + "title": "Run Mode", + "type": "run_mode", + "values": ["permanent_hold", "run_schedule"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/scheduling_enabled" + } + }, + "current_value": True, + "labels": ["ON", "OFF"], + "options": [ + {"label": "ON", "value": True}, + {"label": "OFF", "value": False}, + ], + "title": "Scheduling", + "type": "scheduling_enabled", + "values": [True, False], + }, + ], + "temperature": 73, + "type": "xxl_zone", + "zone_status": "Relieving Air", + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127" + } + }, + "cooling_setpoint": 79, + "current_zone_mode": "AUTO", + "features": [ + { + "actions": { + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/setpoints" + }, + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/setpoints" + }, + }, + "name": "thermostat", + "scale": "f", + "setpoint_cool": 79, + "setpoint_cool_max": 99, + "setpoint_cool_min": 60, + "setpoint_delta": 3, + "setpoint_heat": 63, + "setpoint_heat_max": 90, + "setpoint_heat_min": 55, + "setpoint_increment": 1.0, + "status": "Damper Closed", + "status_icon": {"modifiers": [], "name": "cooling"}, + "system_status": "Cooling", + "temperature": 72, + }, + { + "is_connected": True, + "name": "connection", + "signal_strength": "unknown", + }, + { + "actions": { + "update_thermostat_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/zone_mode", + "method": "POST", + } + }, + "display_value": "Auto", + "label": "Zone Mode", + "name": "thermostat_mode", + "options": [ + { + "header": True, + "id": "thermostat_mode", + "label": "Zone Mode", + "value": "thermostat_mode", + }, + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "value": "AUTO", + }, + { + "actions": { + "update_thermostat_run_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/run_mode", + "method": "POST", + } + }, + "display_value": "Hold", + "label": "Run Mode", + "name": "thermostat_run_mode", + "options": [ + { + "header": True, + "id": "thermostat_run_mode", + "label": "Run Mode", + "value": "thermostat_run_mode", + }, + { + "id": "info_text", + "info": True, + "label": "Follow or " + "override the " + "schedule.", + "value": "info_text", + }, + { + "label": "Permanent Hold", + "value": "permanent_hold", + }, + {"label": "Run Schedule", "value": "run_schedule"}, + ], + "value": "permanent_hold", + }, + { + "actions": { + "enable_scheduling": { + "data": {"value": True}, + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/scheduling_enabled", + "method": "POST", + }, + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83394127", + "method": "POST", + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83394127", + "method": "GET", + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83394127", + "method": "POST", + }, + }, + "can_add_remove_periods": True, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83394127&house_id=123456", + "enabled": True, + "max_period_name_length": 10, + "max_periods_per_day": 4, + "name": "schedule", + "setpoint_increment": 1, + }, + ], + "heating_setpoint": 63, + "icon": {"modifiers": ["temperature-72"], "name": "thermostat"}, + "id": 83394127, + "name": "Snooze Room", + "operating_state": "Damper Closed", + "setpoints": {"cool": 79, "heat": 63}, + "settings": [ + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/preset_selected" + } + }, + "current_value": 0, + "labels": ["None", "Home", "Away", "Sleep"], + "options": [ + {"label": "None", "value": 0}, + {"label": "Home", "value": 1}, + {"label": "Away", "value": 2}, + {"label": "Sleep", "value": 3}, + ], + "title": "Preset", + "type": "preset_selected", + "values": [0, 1, 2, 3], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/zone_mode" + } + }, + "current_value": "AUTO", + "labels": ["Auto", "Cooling", "Heating", "Off"], + "options": [ + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "title": "Zone Mode", + "type": "zone_mode", + "values": ["AUTO", "COOL", "HEAT", "OFF"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/run_mode" + } + }, + "current_value": "permanent_hold", + "labels": ["Permanent Hold", "Run Schedule"], + "options": [ + { + "label": "Permanent Hold", + "value": "permanent_hold", + }, + {"label": "Run Schedule", "value": "run_schedule"}, + ], + "title": "Run Mode", + "type": "run_mode", + "values": ["permanent_hold", "run_schedule"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/scheduling_enabled" + } + }, + "current_value": True, + "labels": ["ON", "OFF"], + "options": [ + {"label": "ON", "value": True}, + {"label": "OFF", "value": False}, + ], + "title": "Scheduling", + "type": "scheduling_enabled", + "values": [True, False], + }, + ], + "temperature": 72, + "type": "xxl_zone", + "zone_status": "Damper Closed", + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139" + } + }, + "cooling_setpoint": 79, + "current_zone_mode": "AUTO", + "features": [ + { + "actions": { + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/setpoints" + }, + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/setpoints" + }, + }, + "name": "thermostat", + "scale": "f", + "setpoint_cool": 79, + "setpoint_cool_max": 99, + "setpoint_cool_min": 60, + "setpoint_delta": 3, + "setpoint_heat": 63, + "setpoint_heat_max": 90, + "setpoint_heat_min": 55, + "setpoint_increment": 1.0, + "status": "Damper Closed", + "status_icon": {"modifiers": [], "name": "cooling"}, + "system_status": "Cooling", + "temperature": 74, + }, + { + "is_connected": True, + "name": "connection", + "signal_strength": "unknown", + }, + { + "actions": { + "update_thermostat_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/zone_mode", + "method": "POST", + } + }, + "display_value": "Auto", + "label": "Zone Mode", + "name": "thermostat_mode", + "options": [ + { + "header": True, + "id": "thermostat_mode", + "label": "Zone Mode", + "value": "thermostat_mode", + }, + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "value": "AUTO", + }, + { + "actions": { + "update_thermostat_run_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/run_mode", + "method": "POST", + } + }, + "display_value": "Hold", + "label": "Run Mode", + "name": "thermostat_run_mode", + "options": [ + { + "header": True, + "id": "thermostat_run_mode", + "label": "Run Mode", + "value": "thermostat_run_mode", + }, + { + "id": "info_text", + "info": True, + "label": "Follow or " + "override the " + "schedule.", + "value": "info_text", + }, + { + "label": "Permanent Hold", + "value": "permanent_hold", + }, + {"label": "Run Schedule", "value": "run_schedule"}, + ], + "value": "permanent_hold", + }, + { + "actions": { + "enable_scheduling": { + "data": {"value": True}, + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/scheduling_enabled", + "method": "POST", + }, + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83394139", + "method": "POST", + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83394139", + "method": "GET", + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83394139", + "method": "POST", + }, + }, + "can_add_remove_periods": True, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83394139&house_id=123456", + "enabled": True, + "max_period_name_length": 10, + "max_periods_per_day": 4, + "name": "schedule", + "setpoint_increment": 1, + }, + ], + "heating_setpoint": 63, + "icon": {"modifiers": ["temperature-74"], "name": "thermostat"}, + "id": 83394139, + "name": "Safe Room", + "operating_state": "Damper Closed", + "setpoints": {"cool": 79, "heat": 63}, + "settings": [ + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/preset_selected" + } + }, + "current_value": 0, + "labels": ["None", "Home", "Away", "Sleep"], + "options": [ + {"label": "None", "value": 0}, + {"label": "Home", "value": 1}, + {"label": "Away", "value": 2}, + {"label": "Sleep", "value": 3}, + ], + "title": "Preset", + "type": "preset_selected", + "values": [0, 1, 2, 3], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/zone_mode" + } + }, + "current_value": "AUTO", + "labels": ["Auto", "Cooling", "Heating", "Off"], + "options": [ + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "title": "Zone Mode", + "type": "zone_mode", + "values": ["AUTO", "COOL", "HEAT", "OFF"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/run_mode" + } + }, + "current_value": "permanent_hold", + "labels": ["Permanent Hold", "Run Schedule"], + "options": [ + { + "label": "Permanent Hold", + "value": "permanent_hold", + }, + {"label": "Run Schedule", "value": "run_schedule"}, + ], + "title": "Run Mode", + "type": "run_mode", + "values": ["permanent_hold", "run_schedule"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/scheduling_enabled" + } + }, + "current_value": True, + "labels": ["ON", "OFF"], + "options": [ + {"label": "ON", "value": True}, + {"label": "OFF", "value": False}, + ], + "title": "Scheduling", + "type": "scheduling_enabled", + "values": [True, False], + }, + ], + "temperature": 74, + "type": "xxl_zone", + "zone_status": "Damper Closed", + }, + ], + }, + { + "_links": { + "filter_events": { + "href": "https://www.mynexia.com/mobile/houses/123456/events/collection?sys_guid=3679e95b-7337-48ae-aff4-e0522e9dd0eb" + }, + "nexia:history": { + "href": "https://www.mynexia.com/mobile/houses/123456/events?device_id=2059652" + }, + "pending_request": { + "polling_path": "https://www.mynexia.com/backstage/announcements/c6627726f6339d104ee66897028d6a2ea38215675b336650" + }, + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059652" + }, + }, + "connected": True, + "delta": 3, + "features": [ + { + "items": [ + { + "label": "Model", + "type": "label_value", + "value": "XL1050", + }, + { + "label": "AUID", + "type": "label_value", + "value": "02853DF0", + }, + { + "label": "Firmware Build Number", + "type": "label_value", + "value": "1581321824", + }, + { + "label": "Firmware Build Date", + "type": "label_value", + "value": "2020-02-10 08:03:44 UTC", + }, + { + "label": "Firmware Version", + "type": "label_value", + "value": "5.9.1", + }, + { + "label": "Zoning Enabled", + "type": "label_value", + "value": "yes", + }, + ], + "name": "advanced_info", + }, + { + "actions": {}, + "name": "thermostat", + "scale": "f", + "setpoint_cool_max": 99, + "setpoint_cool_min": 60, + "setpoint_delta": 3, + "setpoint_heat_max": 90, + "setpoint_heat_min": 55, + "setpoint_increment": 1.0, + "status": "System Idle", + "status_icon": None, + "temperature": 77, + }, + { + "is_connected": True, + "name": "connection", + "signal_strength": "unknown", + }, + { + "members": [ + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260991" + } + }, + "cooling_setpoint": 80, + "current_zone_mode": "OFF", + "features": [ + { + "actions": {}, + "name": "thermostat", + "scale": "f", + "setpoint_cool_max": 99, + "setpoint_cool_min": 60, + "setpoint_delta": 3, + "setpoint_heat_max": 90, + "setpoint_heat_min": 55, + "setpoint_increment": 1.0, + "status": "", + "status_icon": None, + "system_status": "System " "Idle", + "temperature": 77, + }, + { + "is_connected": True, + "name": "connection", + "signal_strength": "unknown", + }, + { + "actions": { + "update_thermostat_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260991/zone_mode", + "method": "POST", + } + }, + "display_value": "Off", + "label": "Zone Mode", + "name": "thermostat_mode", + "options": [ + { + "header": True, + "id": "thermostat_mode", + "label": "Zone " "Mode", + "value": "thermostat_mode", + }, + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "value": "OFF", + }, + { + "actions": { + "update_thermostat_run_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260991/run_mode", + "method": "POST", + } + }, + "display_value": "Hold", + "label": "Run Mode", + "name": "thermostat_run_mode", + "options": [ + { + "header": True, + "id": "thermostat_run_mode", + "label": "Run " "Mode", + "value": "thermostat_run_mode", + }, + { + "id": "info_text", + "info": True, + "label": "Follow " + "or " + "override " + "the " + "schedule.", + "value": "info_text", + }, + { + "label": "Permanent " "Hold", + "value": "permanent_hold", + }, + { + "label": "Run " "Schedule", + "value": "run_schedule", + }, + ], + "value": "permanent_hold", + }, + { + "actions": { + "enable_scheduling": { + "data": {"value": True}, + "href": "https://www.mynexia.com/mobile/xxl_zones/83260991/scheduling_enabled", + "method": "POST", + }, + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83260991", + "method": "POST", + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83260991", + "method": "GET", + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83260991", + "method": "POST", + }, + }, + "can_add_remove_periods": True, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83260991&house_id=123456", + "enabled": True, + "max_period_name_length": 10, + "max_periods_per_day": 4, + "name": "schedule", + "setpoint_increment": 1, + }, + ], + "heating_setpoint": 63, + "icon": { + "modifiers": ["temperature-77"], + "name": "thermostat", + }, + "id": 83260991, + "name": "Hallway", + "operating_state": "", + "setpoints": {"cool": 80, "heat": 63}, + "settings": [ + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260991/preset_selected" + } + }, + "current_value": 0, + "labels": ["None", "Home", "Away", "Sleep"], + "options": [ + {"label": "None", "value": 0}, + {"label": "Home", "value": 1}, + {"label": "Away", "value": 2}, + {"label": "Sleep", "value": 3}, + ], + "title": "Preset", + "type": "preset_selected", + "values": [0, 1, 2, 3], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260991/zone_mode" + } + }, + "current_value": "OFF", + "labels": ["Auto", "Cooling", "Heating", "Off"], + "options": [ + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "title": "Zone Mode", + "type": "zone_mode", + "values": ["AUTO", "COOL", "HEAT", "OFF"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260991/run_mode" + } + }, + "current_value": "permanent_hold", + "labels": [ + "Permanent " "Hold", + "Run " "Schedule", + ], + "options": [ + { + "label": "Permanent " "Hold", + "value": "permanent_hold", + }, + { + "label": "Run " "Schedule", + "value": "run_schedule", + }, + ], + "title": "Run Mode", + "type": "run_mode", + "values": ["permanent_hold", "run_schedule"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260991/scheduling_enabled" + } + }, + "current_value": True, + "labels": ["ON", "OFF"], + "options": [ + {"label": "ON", "value": True}, + {"label": "OFF", "value": False}, + ], + "title": "Scheduling", + "type": "scheduling_enabled", + "values": [True, False], + }, + ], + "temperature": 77, + "type": "xxl_zone", + "zone_status": "", + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260994" + } + }, + "cooling_setpoint": 81, + "current_zone_mode": "AUTO", + "features": [ + { + "actions": { + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260994/setpoints" + }, + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260994/setpoints" + }, + }, + "name": "thermostat", + "scale": "f", + "setpoint_cool": 81, + "setpoint_cool_max": 99, + "setpoint_cool_min": 60, + "setpoint_delta": 3, + "setpoint_heat": 63, + "setpoint_heat_max": 90, + "setpoint_heat_min": 55, + "setpoint_increment": 1.0, + "status": "", + "status_icon": None, + "system_status": "System " "Idle", + "temperature": 74, + }, + { + "is_connected": True, + "name": "connection", + "signal_strength": "unknown", + }, + { + "actions": { + "update_thermostat_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260994/zone_mode", + "method": "POST", + } + }, + "display_value": "Auto", + "label": "Zone Mode", + "name": "thermostat_mode", + "options": [ + { + "header": True, + "id": "thermostat_mode", + "label": "Zone " "Mode", + "value": "thermostat_mode", + }, + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "value": "AUTO", + }, + { + "actions": { + "update_thermostat_run_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260994/run_mode", + "method": "POST", + } + }, + "display_value": "Hold", + "label": "Run Mode", + "name": "thermostat_run_mode", + "options": [ + { + "header": True, + "id": "thermostat_run_mode", + "label": "Run " "Mode", + "value": "thermostat_run_mode", + }, + { + "id": "info_text", + "info": True, + "label": "Follow " + "or " + "override " + "the " + "schedule.", + "value": "info_text", + }, + { + "label": "Permanent " "Hold", + "value": "permanent_hold", + }, + { + "label": "Run " "Schedule", + "value": "run_schedule", + }, + ], + "value": "permanent_hold", + }, + { + "actions": { + "enable_scheduling": { + "data": {"value": True}, + "href": "https://www.mynexia.com/mobile/xxl_zones/83260994/scheduling_enabled", + "method": "POST", + }, + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83260994", + "method": "POST", + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83260994", + "method": "GET", + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83260994", + "method": "POST", + }, + }, + "can_add_remove_periods": True, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83260994&house_id=123456", + "enabled": True, + "max_period_name_length": 10, + "max_periods_per_day": 4, + "name": "schedule", + "setpoint_increment": 1, + }, + ], + "heating_setpoint": 63, + "icon": { + "modifiers": ["temperature-74"], + "name": "thermostat", + }, + "id": 83260994, + "name": "Mid Bedroom", + "operating_state": "", + "setpoints": {"cool": 81, "heat": 63}, + "settings": [ + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260994/preset_selected" + } + }, + "current_value": 0, + "labels": ["None", "Home", "Away", "Sleep"], + "options": [ + {"label": "None", "value": 0}, + {"label": "Home", "value": 1}, + {"label": "Away", "value": 2}, + {"label": "Sleep", "value": 3}, + ], + "title": "Preset", + "type": "preset_selected", + "values": [0, 1, 2, 3], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260994/zone_mode" + } + }, + "current_value": "AUTO", + "labels": ["Auto", "Cooling", "Heating", "Off"], + "options": [ + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "title": "Zone Mode", + "type": "zone_mode", + "values": ["AUTO", "COOL", "HEAT", "OFF"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260994/run_mode" + } + }, + "current_value": "permanent_hold", + "labels": [ + "Permanent " "Hold", + "Run " "Schedule", + ], + "options": [ + { + "label": "Permanent " "Hold", + "value": "permanent_hold", + }, + { + "label": "Run " "Schedule", + "value": "run_schedule", + }, + ], + "title": "Run Mode", + "type": "run_mode", + "values": ["permanent_hold", "run_schedule"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260994/scheduling_enabled" + } + }, + "current_value": True, + "labels": ["ON", "OFF"], + "options": [ + {"label": "ON", "value": True}, + {"label": "OFF", "value": False}, + ], + "title": "Scheduling", + "type": "scheduling_enabled", + "values": [True, False], + }, + ], + "temperature": 74, + "type": "xxl_zone", + "zone_status": "", + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260997" + } + }, + "cooling_setpoint": 81, + "current_zone_mode": "AUTO", + "features": [ + { + "actions": { + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260997/setpoints" + }, + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260997/setpoints" + }, + }, + "name": "thermostat", + "scale": "f", + "setpoint_cool": 81, + "setpoint_cool_max": 99, + "setpoint_cool_min": 60, + "setpoint_delta": 3, + "setpoint_heat": 63, + "setpoint_heat_max": 90, + "setpoint_heat_min": 55, + "setpoint_increment": 1.0, + "status": "", + "status_icon": None, + "system_status": "System " "Idle", + "temperature": 75, + }, + { + "is_connected": True, + "name": "connection", + "signal_strength": "unknown", + }, + { + "actions": { + "update_thermostat_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260997/zone_mode", + "method": "POST", + } + }, + "display_value": "Auto", + "label": "Zone Mode", + "name": "thermostat_mode", + "options": [ + { + "header": True, + "id": "thermostat_mode", + "label": "Zone " "Mode", + "value": "thermostat_mode", + }, + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "value": "AUTO", + }, + { + "actions": { + "update_thermostat_run_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260997/run_mode", + "method": "POST", + } + }, + "display_value": "Hold", + "label": "Run Mode", + "name": "thermostat_run_mode", + "options": [ + { + "header": True, + "id": "thermostat_run_mode", + "label": "Run " "Mode", + "value": "thermostat_run_mode", + }, + { + "id": "info_text", + "info": True, + "label": "Follow " + "or " + "override " + "the " + "schedule.", + "value": "info_text", + }, + { + "label": "Permanent " "Hold", + "value": "permanent_hold", + }, + { + "label": "Run " "Schedule", + "value": "run_schedule", + }, + ], + "value": "permanent_hold", + }, + { + "actions": { + "enable_scheduling": { + "data": {"value": True}, + "href": "https://www.mynexia.com/mobile/xxl_zones/83260997/scheduling_enabled", + "method": "POST", + }, + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83260997", + "method": "POST", + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83260997", + "method": "GET", + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83260997", + "method": "POST", + }, + }, + "can_add_remove_periods": True, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83260997&house_id=123456", + "enabled": True, + "max_period_name_length": 10, + "max_periods_per_day": 4, + "name": "schedule", + "setpoint_increment": 1, + }, + ], + "heating_setpoint": 63, + "icon": { + "modifiers": ["temperature-75"], + "name": "thermostat", + }, + "id": 83260997, + "name": "West Bedroom", + "operating_state": "", + "setpoints": {"cool": 81, "heat": 63}, + "settings": [ + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260997/preset_selected" + } + }, + "current_value": 0, + "labels": ["None", "Home", "Away", "Sleep"], + "options": [ + {"label": "None", "value": 0}, + {"label": "Home", "value": 1}, + {"label": "Away", "value": 2}, + {"label": "Sleep", "value": 3}, + ], + "title": "Preset", + "type": "preset_selected", + "values": [0, 1, 2, 3], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260997/zone_mode" + } + }, + "current_value": "AUTO", + "labels": ["Auto", "Cooling", "Heating", "Off"], + "options": [ + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "title": "Zone Mode", + "type": "zone_mode", + "values": ["AUTO", "COOL", "HEAT", "OFF"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260997/run_mode" + } + }, + "current_value": "permanent_hold", + "labels": [ + "Permanent " "Hold", + "Run " "Schedule", + ], + "options": [ + { + "label": "Permanent " "Hold", + "value": "permanent_hold", + }, + { + "label": "Run " "Schedule", + "value": "run_schedule", + }, + ], + "title": "Run Mode", + "type": "run_mode", + "values": ["permanent_hold", "run_schedule"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260997/scheduling_enabled" + } + }, + "current_value": True, + "labels": ["ON", "OFF"], + "options": [ + {"label": "ON", "value": True}, + {"label": "OFF", "value": False}, + ], + "title": "Scheduling", + "type": "scheduling_enabled", + "values": [True, False], + }, + ], + "temperature": 75, + "type": "xxl_zone", + "zone_status": "", + }, + ], + "name": "group", + }, + { + "actions": { + "update_thermostat_fan_mode": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059652/fan_mode", + "method": "POST", + } + }, + "display_value": "Auto", + "label": "Fan Mode", + "name": "thermostat_fan_mode", + "options": [ + { + "header": True, + "id": "thermostat_fan_mode", + "label": "Fan Mode", + "value": "thermostat_fan_mode", + }, + {"label": "Auto", "value": "auto"}, + {"label": "On", "value": "on"}, + {"label": "Circulate", "value": "circulate"}, + ], + "status_icon": {"modifiers": [], "name": "thermostat_fan_off"}, + "value": "auto", + }, + {"compressor_speed": 0.0, "name": "thermostat_compressor_speed"}, + { + "actions": { + "get_monthly_runtime_history": { + "href": "https://www.mynexia.com/mobile/runtime_history/2059652?report_type=monthly", + "method": "GET", + }, + "get_runtime_history": { + "href": "https://www.mynexia.com/mobile/runtime_history/2059652?report_type=daily", + "method": "GET", + }, + }, + "name": "runtime_history", + }, + ], + "has_indoor_humidity": True, + "has_outdoor_temperature": True, + "icon": [ + {"modifiers": ["temperature-77"], "name": "thermostat"}, + {"modifiers": ["temperature-74"], "name": "thermostat"}, + {"modifiers": ["temperature-75"], "name": "thermostat"}, + ], + "id": 2059652, + "indoor_humidity": "37", + "last_updated_at": "2020-03-11T15:15:53.000-05:00", + "name": "Upstairs West Wing", + "name_editable": True, + "outdoor_temperature": "87", + "settings": [ + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059652/fan_mode" + } + }, + "current_value": "auto", + "labels": ["Auto", "On", "Circulate"], + "options": [ + {"label": "Auto", "value": "auto"}, + {"label": "On", "value": "on"}, + {"label": "Circulate", "value": "circulate"}, + ], + "title": "Fan Mode", + "type": "fan_mode", + "values": ["auto", "on", "circulate"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059652/fan_speed" + } + }, + "current_value": 0.35, + "labels": [ + "35%", + "40%", + "45%", + "50%", + "55%", + "60%", + "65%", + "70%", + "75%", + "80%", + "85%", + "90%", + "95%", + "100%", + ], + "options": [ + {"label": "35%", "value": 0.35}, + {"label": "40%", "value": 0.4}, + {"label": "45%", "value": 0.45}, + {"label": "50%", "value": 0.5}, + {"label": "55%", "value": 0.55}, + {"label": "60%", "value": 0.6}, + {"label": "65%", "value": 0.65}, + {"label": "70%", "value": 0.7}, + {"label": "75%", "value": 0.75}, + {"label": "80%", "value": 0.8}, + {"label": "85%", "value": 0.85}, + {"label": "90%", "value": 0.9}, + {"label": "95%", "value": 0.95}, + {"label": "100%", "value": 1.0}, + ], + "title": "Fan Speed", + "type": "fan_speed", + "values": [ + 0.35, + 0.4, + 0.45, + 0.5, + 0.55, + 0.6, + 0.65, + 0.7, + 0.75, + 0.8, + 0.85, + 0.9, + 0.95, + 1.0, + ], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059652/fan_circulation_time" + } + }, + "current_value": 30, + "labels": [ + "10 minutes", + "15 minutes", + "20 minutes", + "25 minutes", + "30 minutes", + "35 minutes", + "40 minutes", + "45 minutes", + "50 minutes", + "55 minutes", + ], + "options": [ + {"label": "10 minutes", "value": 10}, + {"label": "15 minutes", "value": 15}, + {"label": "20 minutes", "value": 20}, + {"label": "25 minutes", "value": 25}, + {"label": "30 minutes", "value": 30}, + {"label": "35 minutes", "value": 35}, + {"label": "40 minutes", "value": 40}, + {"label": "45 minutes", "value": 45}, + {"label": "50 minutes", "value": 50}, + {"label": "55 minutes", "value": 55}, + ], + "title": "Fan Circulation Time", + "type": "fan_circulation_time", + "values": [10, 15, 20, 25, 30, 35, 40, 45, 50, 55], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059652/air_cleaner_mode" + } + }, + "current_value": "auto", + "labels": ["Auto", "Quick", "Allergy"], + "options": [ + {"label": "Auto", "value": "auto"}, + {"label": "Quick", "value": "quick"}, + {"label": "Allergy", "value": "allergy"}, + ], + "title": "Air Cleaner Mode", + "type": "air_cleaner_mode", + "values": ["auto", "quick", "allergy"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059652/dehumidify" + } + }, + "current_value": 0.5, + "labels": ["35%", "40%", "45%", "50%", "55%", "60%", "65%"], + "options": [ + {"label": "35%", "value": 0.35}, + {"label": "40%", "value": 0.4}, + {"label": "45%", "value": 0.45}, + {"label": "50%", "value": 0.5}, + {"label": "55%", "value": 0.55}, + {"label": "60%", "value": 0.6}, + {"label": "65%", "value": 0.65}, + ], + "title": "Cooling Dehumidify Set Point", + "type": "dehumidify", + "values": [0.35, 0.4, 0.45, 0.5, 0.55, 0.6, 0.65], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059652/scale" + } + }, + "current_value": "f", + "labels": ["F", "C"], + "options": [ + {"label": "F", "value": "f"}, + {"label": "C", "value": "c"}, + ], + "title": "Temperature Scale", + "type": "scale", + "values": ["f", "c"], + }, + ], + "status_secondary": None, + "status_tertiary": None, + "system_status": "System Idle", + "type": "xxl_thermostat", + "zones": [ + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260991" + } + }, + "cooling_setpoint": 80, + "current_zone_mode": "OFF", + "features": [ + { + "actions": {}, + "name": "thermostat", + "scale": "f", + "setpoint_cool_max": 99, + "setpoint_cool_min": 60, + "setpoint_delta": 3, + "setpoint_heat_max": 90, + "setpoint_heat_min": 55, + "setpoint_increment": 1.0, + "status": "", + "status_icon": None, + "system_status": "System Idle", + "temperature": 77, + }, + { + "is_connected": True, + "name": "connection", + "signal_strength": "unknown", + }, + { + "actions": { + "update_thermostat_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260991/zone_mode", + "method": "POST", + } + }, + "display_value": "Off", + "label": "Zone Mode", + "name": "thermostat_mode", + "options": [ + { + "header": True, + "id": "thermostat_mode", + "label": "Zone Mode", + "value": "thermostat_mode", + }, + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "value": "OFF", + }, + { + "actions": { + "update_thermostat_run_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260991/run_mode", + "method": "POST", + } + }, + "display_value": "Hold", + "label": "Run Mode", + "name": "thermostat_run_mode", + "options": [ + { + "header": True, + "id": "thermostat_run_mode", + "label": "Run Mode", + "value": "thermostat_run_mode", + }, + { + "id": "info_text", + "info": True, + "label": "Follow or " + "override the " + "schedule.", + "value": "info_text", + }, + { + "label": "Permanent Hold", + "value": "permanent_hold", + }, + {"label": "Run Schedule", "value": "run_schedule"}, + ], + "value": "permanent_hold", + }, + { + "actions": { + "enable_scheduling": { + "data": {"value": True}, + "href": "https://www.mynexia.com/mobile/xxl_zones/83260991/scheduling_enabled", + "method": "POST", + }, + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83260991", + "method": "POST", + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83260991", + "method": "GET", + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83260991", + "method": "POST", + }, + }, + "can_add_remove_periods": True, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83260991&house_id=123456", + "enabled": True, + "max_period_name_length": 10, + "max_periods_per_day": 4, + "name": "schedule", + "setpoint_increment": 1, + }, + ], + "heating_setpoint": 63, + "icon": {"modifiers": ["temperature-77"], "name": "thermostat"}, + "id": 83260991, + "name": "Hallway", + "operating_state": "", + "setpoints": {"cool": 80, "heat": 63}, + "settings": [ + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260991/preset_selected" + } + }, + "current_value": 0, + "labels": ["None", "Home", "Away", "Sleep"], + "options": [ + {"label": "None", "value": 0}, + {"label": "Home", "value": 1}, + {"label": "Away", "value": 2}, + {"label": "Sleep", "value": 3}, + ], + "title": "Preset", + "type": "preset_selected", + "values": [0, 1, 2, 3], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260991/zone_mode" + } + }, + "current_value": "OFF", + "labels": ["Auto", "Cooling", "Heating", "Off"], + "options": [ + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "title": "Zone Mode", + "type": "zone_mode", + "values": ["AUTO", "COOL", "HEAT", "OFF"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260991/run_mode" + } + }, + "current_value": "permanent_hold", + "labels": ["Permanent Hold", "Run Schedule"], + "options": [ + { + "label": "Permanent Hold", + "value": "permanent_hold", + }, + {"label": "Run Schedule", "value": "run_schedule"}, + ], + "title": "Run Mode", + "type": "run_mode", + "values": ["permanent_hold", "run_schedule"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260991/scheduling_enabled" + } + }, + "current_value": True, + "labels": ["ON", "OFF"], + "options": [ + {"label": "ON", "value": True}, + {"label": "OFF", "value": False}, + ], + "title": "Scheduling", + "type": "scheduling_enabled", + "values": [True, False], + }, + ], + "temperature": 77, + "type": "xxl_zone", + "zone_status": "", + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260994" + } + }, + "cooling_setpoint": 81, + "current_zone_mode": "AUTO", + "features": [ + { + "actions": { + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260994/setpoints" + }, + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260994/setpoints" + }, + }, + "name": "thermostat", + "scale": "f", + "setpoint_cool": 81, + "setpoint_cool_max": 99, + "setpoint_cool_min": 60, + "setpoint_delta": 3, + "setpoint_heat": 63, + "setpoint_heat_max": 90, + "setpoint_heat_min": 55, + "setpoint_increment": 1.0, + "status": "", + "status_icon": None, + "system_status": "System Idle", + "temperature": 74, + }, + { + "is_connected": True, + "name": "connection", + "signal_strength": "unknown", + }, + { + "actions": { + "update_thermostat_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260994/zone_mode", + "method": "POST", + } + }, + "display_value": "Auto", + "label": "Zone Mode", + "name": "thermostat_mode", + "options": [ + { + "header": True, + "id": "thermostat_mode", + "label": "Zone Mode", + "value": "thermostat_mode", + }, + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "value": "AUTO", + }, + { + "actions": { + "update_thermostat_run_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260994/run_mode", + "method": "POST", + } + }, + "display_value": "Hold", + "label": "Run Mode", + "name": "thermostat_run_mode", + "options": [ + { + "header": True, + "id": "thermostat_run_mode", + "label": "Run Mode", + "value": "thermostat_run_mode", + }, + { + "id": "info_text", + "info": True, + "label": "Follow or " + "override the " + "schedule.", + "value": "info_text", + }, + { + "label": "Permanent Hold", + "value": "permanent_hold", + }, + {"label": "Run Schedule", "value": "run_schedule"}, + ], + "value": "permanent_hold", + }, + { + "actions": { + "enable_scheduling": { + "data": {"value": True}, + "href": "https://www.mynexia.com/mobile/xxl_zones/83260994/scheduling_enabled", + "method": "POST", + }, + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83260994", + "method": "POST", + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83260994", + "method": "GET", + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83260994", + "method": "POST", + }, + }, + "can_add_remove_periods": True, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83260994&house_id=123456", + "enabled": True, + "max_period_name_length": 10, + "max_periods_per_day": 4, + "name": "schedule", + "setpoint_increment": 1, + }, + ], + "heating_setpoint": 63, + "icon": {"modifiers": ["temperature-74"], "name": "thermostat"}, + "id": 83260994, + "name": "Mid Bedroom", + "operating_state": "", + "setpoints": {"cool": 81, "heat": 63}, + "settings": [ + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260994/preset_selected" + } + }, + "current_value": 0, + "labels": ["None", "Home", "Away", "Sleep"], + "options": [ + {"label": "None", "value": 0}, + {"label": "Home", "value": 1}, + {"label": "Away", "value": 2}, + {"label": "Sleep", "value": 3}, + ], + "title": "Preset", + "type": "preset_selected", + "values": [0, 1, 2, 3], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260994/zone_mode" + } + }, + "current_value": "AUTO", + "labels": ["Auto", "Cooling", "Heating", "Off"], + "options": [ + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "title": "Zone Mode", + "type": "zone_mode", + "values": ["AUTO", "COOL", "HEAT", "OFF"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260994/run_mode" + } + }, + "current_value": "permanent_hold", + "labels": ["Permanent Hold", "Run Schedule"], + "options": [ + { + "label": "Permanent Hold", + "value": "permanent_hold", + }, + {"label": "Run Schedule", "value": "run_schedule"}, + ], + "title": "Run Mode", + "type": "run_mode", + "values": ["permanent_hold", "run_schedule"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260994/scheduling_enabled" + } + }, + "current_value": True, + "labels": ["ON", "OFF"], + "options": [ + {"label": "ON", "value": True}, + {"label": "OFF", "value": False}, + ], + "title": "Scheduling", + "type": "scheduling_enabled", + "values": [True, False], + }, + ], + "temperature": 74, + "type": "xxl_zone", + "zone_status": "", + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260997" + } + }, + "cooling_setpoint": 81, + "current_zone_mode": "AUTO", + "features": [ + { + "actions": { + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260997/setpoints" + }, + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260997/setpoints" + }, + }, + "name": "thermostat", + "scale": "f", + "setpoint_cool": 81, + "setpoint_cool_max": 99, + "setpoint_cool_min": 60, + "setpoint_delta": 3, + "setpoint_heat": 63, + "setpoint_heat_max": 90, + "setpoint_heat_min": 55, + "setpoint_increment": 1.0, + "status": "", + "status_icon": None, + "system_status": "System Idle", + "temperature": 75, + }, + { + "is_connected": True, + "name": "connection", + "signal_strength": "unknown", + }, + { + "actions": { + "update_thermostat_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260997/zone_mode", + "method": "POST", + } + }, + "display_value": "Auto", + "label": "Zone Mode", + "name": "thermostat_mode", + "options": [ + { + "header": True, + "id": "thermostat_mode", + "label": "Zone Mode", + "value": "thermostat_mode", + }, + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "value": "AUTO", + }, + { + "actions": { + "update_thermostat_run_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260997/run_mode", + "method": "POST", + } + }, + "display_value": "Hold", + "label": "Run Mode", + "name": "thermostat_run_mode", + "options": [ + { + "header": True, + "id": "thermostat_run_mode", + "label": "Run Mode", + "value": "thermostat_run_mode", + }, + { + "id": "info_text", + "info": True, + "label": "Follow or " + "override the " + "schedule.", + "value": "info_text", + }, + { + "label": "Permanent Hold", + "value": "permanent_hold", + }, + {"label": "Run Schedule", "value": "run_schedule"}, + ], + "value": "permanent_hold", + }, + { + "actions": { + "enable_scheduling": { + "data": {"value": True}, + "href": "https://www.mynexia.com/mobile/xxl_zones/83260997/scheduling_enabled", + "method": "POST", + }, + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83260997", + "method": "POST", + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83260997", + "method": "GET", + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83260997", + "method": "POST", + }, + }, + "can_add_remove_periods": True, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83260997&house_id=123456", + "enabled": True, + "max_period_name_length": 10, + "max_periods_per_day": 4, + "name": "schedule", + "setpoint_increment": 1, + }, + ], + "heating_setpoint": 63, + "icon": {"modifiers": ["temperature-75"], "name": "thermostat"}, + "id": 83260997, + "name": "West Bedroom", + "operating_state": "", + "setpoints": {"cool": 81, "heat": 63}, + "settings": [ + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260997/preset_selected" + } + }, + "current_value": 0, + "labels": ["None", "Home", "Away", "Sleep"], + "options": [ + {"label": "None", "value": 0}, + {"label": "Home", "value": 1}, + {"label": "Away", "value": 2}, + {"label": "Sleep", "value": 3}, + ], + "title": "Preset", + "type": "preset_selected", + "values": [0, 1, 2, 3], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260997/zone_mode" + } + }, + "current_value": "AUTO", + "labels": ["Auto", "Cooling", "Heating", "Off"], + "options": [ + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "title": "Zone Mode", + "type": "zone_mode", + "values": ["AUTO", "COOL", "HEAT", "OFF"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260997/run_mode" + } + }, + "current_value": "permanent_hold", + "labels": ["Permanent Hold", "Run Schedule"], + "options": [ + { + "label": "Permanent Hold", + "value": "permanent_hold", + }, + {"label": "Run Schedule", "value": "run_schedule"}, + ], + "title": "Run Mode", + "type": "run_mode", + "values": ["permanent_hold", "run_schedule"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260997/scheduling_enabled" + } + }, + "current_value": True, + "labels": ["ON", "OFF"], + "options": [ + {"label": "ON", "value": True}, + {"label": "OFF", "value": False}, + ], + "title": "Scheduling", + "type": "scheduling_enabled", + "values": [True, False], + }, + ], + "temperature": 75, + "type": "xxl_zone", + "zone_status": "", + }, + ], + }, + ], + "entry": {"brand": None, "title": "Mock Title"}, + } diff --git a/tests/components/nina/fixtures/sample_regions.json b/tests/components/nina/fixtures/sample_regions.json index b25106e8138..4fbc0638604 100644 --- a/tests/components/nina/fixtures/sample_regions.json +++ b/tests/components/nina/fixtures/sample_regions.json @@ -1 +1,11507 @@ -{"metadaten":{"kennung":"urn:de:bund:destatis:bevoelkerungsstatistik:schluessel:rs_2021-07-31","kennungInhalt":"urn:de:bund:destatis:bevoelkerungsstatistik:schluessel:rs","version":"2021-07-31","nameKurz":"Regionalschlüssel","nameLang":"Gemeinden, dargestellt durch den Amtlichen Regionalschlüssel (ARS) des Statistischen Bundesamtes","nameTechnisch":"Regionalschluessel","herausgebernameLang":"Statistisches Bundesamt, Wiesbaden","herausgebernameKurz":"Destatis","beschreibung":"Diese Codeliste stellt alle Gemeinden Deutschlands durch den Amtlichen Regionalschlüssel (ARS) dar, wie im Gemeindeverzeichnis des Statistischen Bundesamtes enthalten. Darüber hinaus enthält die Codeliste für die Stadtstaaten Hamburg, Bremen und Berlin Einträge für Stadt-/Ortsteile bzw. Stadtbezirke. Diese Einträge sind mit einem entsprechenden Hinweis versehen.","versionBeschreibung":null,"aenderungZurVorversion":"Mehrere Aenderungen","handbuchVersion":"1.0","xoevHandbuch":false,"gueltigAb":1627682400000,"bezugsorte":[]},"spalten":[{"spaltennameLang":"SCHLUESSEL","spaltennameTechnisch":"SCHLUESSEL","datentyp":"string","codeSpalte":true,"verwendung":{"code":"REQUIRED"},"empfohleneCodeSpalte":true},{"spaltennameLang":"Bezeichnung","spaltennameTechnisch":"Bezeichnung","datentyp":"string","codeSpalte":false,"verwendung":{"code":"REQUIRED"},"empfohleneCodeSpalte":false},{"spaltennameLang":"Hinweis","spaltennameTechnisch":"Hinweis","datentyp":"string","codeSpalte":false,"verwendung":{"code":"OPTIONAL"},"empfohleneCodeSpalte":false}],"daten":[["010010000000","Flensburg, Stadt",null],["010020000000","Kiel, Landeshauptstadt",null],["010030000000","Lübeck, Hansestadt",null],["010040000000","Neumünster, Stadt",null],["010510011011","Brunsbüttel, Stadt",null],["010510044044","Heide, Stadt",null],["010515163003","Averlak",null],["010515163010","Brickeln",null],["010515163012","Buchholz",null],["010515163016","Burg (Dithmarschen)",null],["010515163022","Dingen",null],["010515163024","Eddelak",null],["010515163026","Eggstedt",null],["010515163032","Frestedt",null],["010515163037","Großenrade",null],["010515163051","Hochdonn",null],["010515163064","Kuden",null],["010515163089","Quickborn",null],["010515163097","Sankt Michaelisdonn",null],["010515163110","Süderhastedt",null],["010515166021","Diekhusen-Fahrstedt",null],["010515166034","Friedrichskoog",null],["010515166046","Helse",null],["010515166057","Kaiser-Wilhelm-Koog",null],["010515166062","Kronprinzenkoog",null],["010515166072","Marne, Stadt",null],["010515166073","Marnerdeich",null],["010515166076","Neufeld",null],["010515166077","Neufelderkoog",null],["010515166090","Ramhusen",null],["010515166103","Schmedeswurth",null],["010515166118","Trennewurth",null],["010515166119","Volsemenhusen",null],["010515169005","Barkenholm",null],["010515169008","Bergewöhrden",null],["010515169019","Dellstedt",null],["010515169020","Delve",null],["010515169023","Dörpling",null],["010515169030","Fedderingen",null],["010515169035","Gaushorn",null],["010515169036","Glüsing",null],["010515169038","Groven",null],["010515169047","Hemme",null],["010515169049","Hennstedt",null],["010515169052","Hövede",null],["010515169053","Hollingstedt",null],["010515169058","Karolinenkoog",null],["010515169060","Kleve",null],["010515169061","Krempel",null],["010515169065","Lehe",null],["010515169068","Linden",null],["010515169071","Lunden",null],["010515169080","Norderheistedt",null],["010515169088","Pahlen",null],["010515169092","Rehm-Flehde-Bargen",null],["010515169096","Sankt Annen",null],["010515169100","Schalkholz",null],["010515169102","Schlichting",null],["010515169114","Tellingstedt",null],["010515169117","Tielenhemme",null],["010515169120","Wallen",null],["010515169125","Welmbüttel",null],["010515169131","Westerborstel",null],["010515169133","Wiemerstedt",null],["010515169136","Wrohm",null],["010515169139","Süderdorf",null],["010515169141","Süderheistedt",null],["010515172048","Hemmingstedt",null],["010515172067","Lieth",null],["010515172069","Lohe-Rickelshof",null],["010515172075","Neuenkirchen",null],["010515172081","Norderwöhrden",null],["010515172082","Nordhastedt",null],["010515172087","Ostrohe",null],["010515172107","Stelle-Wittenwurth",null],["010515172113","Wöhrden",null],["010515172122","Weddingstedt",null],["010515172130","Wesseln",null],["010515175001","Albersdorf",null],["010515175002","Arkebek",null],["010515175004","Bargenstedt",null],["010515175006","Barlt",null],["010515175015","Bunsoh",null],["010515175017","Busenwurth",null],["010515175027","Elpersbüttel",null],["010515175028","Epenwöhrden",null],["010515175039","Gudendorf",null],["010515175054","Immenstedt",null],["010515175063","Krumstedt",null],["010515175074","Meldorf, Stadt",null],["010515175078","Nindorf",null],["010515175083","Odderade",null],["010515175085","Offenbüttel",null],["010515175086","Osterrade",null],["010515175098","Sarzbüttel",null],["010515175099","Schafstedt",null],["010515175104","Schrum",null],["010515175126","Wennbüttel",null],["010515175134","Windbergen",null],["010515175135","Wolmersdorf",null],["010515175137","Nordermeldorf",null],["010515175138","Tensbüttel-Röst",null],["010515178013","Büsum",null],["010515178014","Büsumer Deichhausen",null],["010515178033","Friedrichsgabekoog",null],["010515178043","Hedwigenkoog",null],["010515178045","Hellschen-Heringsand-Unterschaar",null],["010515178050","Hillgroven",null],["010515178079","Norddeich",null],["010515178084","Oesterdeichstrich",null],["010515178093","Reinsbüttel",null],["010515178105","Schülp",null],["010515178108","Strübbel",null],["010515178109","Süderdeich",null],["010515178121","Warwerort",null],["010515178127","Wesselburen, Stadt",null],["010515178128","Wesselburener Deichhausen",null],["010515178129","Wesselburenerkoog",null],["010515178132","Westerdeichstrich",null],["010515178140","Oesterwurth",null],["010530032032","Geesthacht, Stadt",null],["010530083083","Lauenburg/ Elbe, Stadt",null],["010530090090","Mölln, Stadt",null],["010530100100","Ratzeburg, Stadt",null],["010530116116","Schwarzenbek, Stadt",null],["010530129129","Wentorf bei Hamburg",null],["010535308008","Behlendorf",null],["010535308009","Berkenthin",null],["010535308011","Bliestorf",null],["010535308024","Düchelsdorf",null],["010535308034","Göldenitz",null],["010535308061","Kastorf",null],["010535308067","Klempau",null],["010535308075","Krummesse",null],["010535308094","Niendorf bei Berkenthin",null],["010535308103","Rondeshagen",null],["010535308120","Sierksrade",null],["010535313002","Alt-Mölln",null],["010535313005","Bälau",null],["010535313013","Borstorf",null],["010535313014","Breitenfelde",null],["010535313037","Grambek",null],["010535313056","Hornbek",null],["010535313084","Lehmrade",null],["010535313095","Niendorf/ Stecknitz",null],["010535313113","Schretstaken",null],["010535313125","Talkau",null],["010535313134","Woltersdorf",null],["010535318010","Besenthal",null],["010535318015","Bröthen",null],["010535318020","Büchen",null],["010535318029","Fitzen",null],["010535318035","Göttin",null],["010535318046","Gudow",null],["010535318048","Güster",null],["010535318064","Klein Pampau",null],["010535318080","Langenlehsten",null],["010535318092","Müssen",null],["010535318104","Roseburg",null],["010535318115","Schulendorf",null],["010535318119","Siebeneichen",null],["010535318126","Tramm",null],["010535318132","Witzeeze",null],["010535323003","Aumühle",null],["010535323012","Börnsen",null],["010535323023","Dassendorf",null],["010535323028","Escheburg",null],["010535323050","Hamwarde",null],["010535323053","Hohenhorn",null],["010535323072","Kröppelshagen-Fahrendorf",null],["010535323131","Wiershop",null],["010535323133","Wohltorf",null],["010535323135","Worth",null],["010535343006","Basedow",null],["010535343019","Buchhorst",null],["010535343022","Dalldorf",null],["010535343058","Juliusburg",null],["010535343073","Krüzen",null],["010535343074","Krukow",null],["010535343082","Lanze",null],["010535343087","Lütau",null],["010535343111","Schnakenbek",null],["010535343128","Wangelau",null],["010535358001","Albsfelde",null],["010535358004","Bäk",null],["010535358016","Brunsmark",null],["010535358018","Buchholz",null],["010535358026","Einhaus",null],["010535358030","Fredeburg",null],["010535358033","Giesensdorf",null],["010535358040","Groß Disnack",null],["010535358041","Groß Grönau",null],["010535358043","Groß Sarau",null],["010535358051","Harmsdorf",null],["010535358054","Hollenbek",null],["010535358057","Horst",null],["010535358062","Kittlitz",null],["010535358066","Klein Zecher",null],["010535358078","Kulpin",null],["010535358088","Mechow",null],["010535358093","Mustin",null],["010535358098","Pogeez",null],["010535358102","Römnitz",null],["010535358107","Salem",null],["010535358110","Schmilau",null],["010535358117","Seedorf",null],["010535358123","Sterley",null],["010535358136","Ziethen",null],["010535373007","Basthorst",null],["010535373017","Brunstorf",null],["010535373021","Dahmker",null],["010535373027","Elmenhorst",null],["010535373031","Fuhlenhagen",null],["010535373036","Grabau",null],["010535373042","Groß Pampau",null],["010535373045","Grove",null],["010535373047","Gülzow",null],["010535373049","Hamfelde",null],["010535373052","Havekost",null],["010535373059","Kankelau",null],["010535373060","Kasseburg",null],["010535373070","Köthel",null],["010535373071","Kollow",null],["010535373076","Kuddewörde",null],["010535373089","Möhnsen",null],["010535373091","Mühlenrade",null],["010535373106","Sahms",null],["010535391025","Duvensee",null],["010535391038","Grinau",null],["010535391039","Groß Boden",null],["010535391044","Groß Schenkenberg",null],["010535391068","Klinkrade",null],["010535391069","Koberg",null],["010535391077","Kühsen",null],["010535391079","Labenz",null],["010535391081","Lankau",null],["010535391085","Linau",null],["010535391086","Lüchow",null],["010535391096","Nusse",null],["010535391097","Panten",null],["010535391099","Poggensee",null],["010535391101","Ritzerau",null],["010535391108","Sandesneben",null],["010535391109","Schiphorst",null],["010535391112","Schönberg",null],["010535391114","Schürensöhlen",null],["010535391118","Siebenbäumen",null],["010535391121","Sirksfelde",null],["010535391122","Steinhorst",null],["010535391124","Stubben",null],["010535391127","Walksfelde",null],["010535391130","Wentorf (Amt Sandesneben)",null],["010539105105","Sachsenwald (Forstgutsbez.),gemfr.Geb.",null],["010540033033","Friedrichstadt, Stadt",null],["010540056056","Husum, Stadt",null],["010540108108","Reußenköge",null],["010540138138","Tönning, Stadt",null],["010540168168","Sylt",null],["010545417035","Garding, Kirchspiel",null],["010545417036","Garding, Stadt",null],["010545417040","Grothusenkoog",null],["010545417063","Katharinenheerd",null],["010545417072","Kotzenbüll",null],["010545417090","Norderfriedrichskoog",null],["010545417095","Oldenswort",null],["010545417100","Osterhever",null],["010545417104","Poppenbüll",null],["010545417113","Sankt Peter-Ording",null],["010545417134","Tating",null],["010545417135","Tetenbüll",null],["010545417140","Tümlauer Koog",null],["010545417145","Vollerwiek",null],["010545417148","Welt",null],["010545417150","Westerhever",null],["010545439046","Hörnum (Sylt)",null],["010545439061","Kampen (Sylt)",null],["010545439078","List auf Sylt",null],["010545439149","Wenningstedt-Braderup (Sylt)",null],["010545453003","Ahrenviöl",null],["010545453004","Ahrenviölfeld",null],["010545453011","Behrendorf",null],["010545453013","Bondelum",null],["010545453041","Haselund",null],["010545453057","Immenstedt",null],["010545453079","Löwenstedt",null],["010545453092","Norstedt",null],["010545453101","Oster-Ohrstedt",null],["010545453118","Schwesing",null],["010545453123","Sollwitt",null],["010545453144","Viöl",null],["010545453152","Wester-Ohrstedt",null],["010545459039","Gröde",null],["010545459050","Hallig Hooge",null],["010545459074","Langeneß",null],["010545459103","Pellworm",null],["010545488005","Alkersum",null],["010545488015","Borgsum",null],["010545488025","Dunsum",null],["010545488083","Midlum",null],["010545488085","Nebel",null],["010545488087","Nieblum",null],["010545488089","Norddorf auf Amrum",null],["010545488094","Oevenum",null],["010545488098","Oldsum",null],["010545488129","Süderende",null],["010545488143","Utersum",null],["010545488158","Witsum",null],["010545488160","Wittdün auf Amrum",null],["010545488163","Wrixum",null],["010545488164","Wyk auf Föhr, Stadt",null],["010545489001","Achtrup",null],["010545489009","Aventoft",null],["010545489016","Bosbüll",null],["010545489017","Braderup",null],["010545489018","Bramstedtlund",null],["010545489022","Dagebüll",null],["010545489027","Ellhöft",null],["010545489034","Friedrich-Wilhelm-Lübke-Koog",null],["010545489048","Holm",null],["010545489055","Humptrup",null],["010545489062","Karlum",null],["010545489065","Klanxbüll",null],["010545489068","Klixbüll",null],["010545489073","Ladelund",null],["010545489076","Leck",null],["010545489077","Lexgaard",null],["010545489086","Neukirchen",null],["010545489088","Niebüll, Stadt",null],["010545489109","Risum-Lindholm",null],["010545489110","Rodenäs",null],["010545489124","Sprakebüll",null],["010545489125","Stadum",null],["010545489126","Stedesand",null],["010545489131","Süderlügum",null],["010545489136","Tinningstedt",null],["010545489142","Uphusum",null],["010545489154","Westre",null],["010545489165","Galmsbüll",null],["010545489166","Emmelsbüll-Horsbüll",null],["010545489167","Enge-Sande",null],["010545492007","Arlewatt",null],["010545492023","Drage",null],["010545492026","Elisabeth-Sophien-Koog",null],["010545492032","Fresendelf",null],["010545492042","Hattstedt",null],["010545492043","Hattstedtermarsch",null],["010545492052","Horstedt",null],["010545492054","Hude",null],["010545492070","Koldenbüttel",null],["010545492084","Mildstedt",null],["010545492091","Nordstrand",null],["010545492096","Oldersbek",null],["010545492097","Olderup",null],["010545492099","Ostenfeld (Husum)",null],["010545492105","Ramstedt",null],["010545492106","Rantrum",null],["010545492116","Schwabstedt",null],["010545492119","Seeth",null],["010545492120","Simonsberg",null],["010545492130","Süderhöft",null],["010545492132","Südermarsch",null],["010545492141","Uelvesbüll",null],["010545492156","Winnert",null],["010545492157","Wisch",null],["010545492159","Wittbek",null],["010545492161","Witzwort",null],["010545492162","Wobbenbüll",null],["010545494002","Ahrenshöft",null],["010545494006","Almdorf",null],["010545494010","Bargum",null],["010545494012","Bohmstedt",null],["010545494014","Bordelum",null],["010545494019","Bredstedt, Stadt",null],["010545494020","Breklum",null],["010545494024","Drelsdorf",null],["010545494037","Goldebek",null],["010545494038","Goldelund",null],["010545494045","Högel",null],["010545494059","Joldelund",null],["010545494071","Kolkerheide",null],["010545494075","Langenhorn",null],["010545494080","Lütjenholm",null],["010545494093","Ockholm",null],["010545494121","Sönnebüll",null],["010545494128","Struckum",null],["010545494146","Vollstedt",null],["010550001001","Ahrensbök",null],["010550004004","Bad Schwartau, Stadt",null],["010550007007","Bosau",null],["010550010010","Dahme",null],["010550012012","Eutin, Stadt",null],["010550016016","Grömitz",null],["010550018018","Grube",null],["010550021021","Heiligenhafen, Stadt",null],["010550025025","Kellenhusen (Ostsee)",null],["010550028028","Malente",null],["010550032032","Neustadt in Holstein, Stadt",null],["010550033033","Oldenburg in Holstein, Stadt",null],["010550035035","Ratekau",null],["010550040040","Stockelsdorf",null],["010550041041","Süsel",null],["010550042042","Timmendorfer Strand",null],["010550044044","Scharbeutz",null],["010550046046","Fehmarn, Stadt",null],["010555543014","Göhl",null],["010555543015","Gremersdorf",null],["010555543017","Großenbrode",null],["010555543022","Heringsdorf",null],["010555543031","Neukirchen",null],["010555543043","Wangels",null],["010555546006","Beschendorf",null],["010555546011","Damlos",null],["010555546020","Harmsdorf",null],["010555546023","Kabelhorst",null],["010555546027","Lensahn",null],["010555546029","Manhagen",null],["010555546036","Riepsdorf",null],["010555591002","Altenkrempe",null],["010555591024","Kasseedorf",null],["010555591037","Schashagen",null],["010555591038","Schönwalde am Bungsberg",null],["010555591039","Sierksdorf",null],["010560002002","Barmstedt, Stadt",null],["010560005005","Bönningstedt",null],["010560015015","Elmshorn, Stadt",null],["010560018018","Halstenbek",null],["010560021021","Hasloh",null],["010560025025","Helgoland",null],["010560039039","Pinneberg, Stadt",null],["010560041041","Quickborn, Stadt",null],["010560043043","Rellingen",null],["010560044044","Schenefeld, Stadt",null],["010560048048","Tornesch, Stadt",null],["010560049049","Uetersen, Stadt",null],["010560050050","Wedel, Stadt",null],["010565616029","Klein Nordende",null],["010565616030","Klein Offenseth-Sparrieshoop",null],["010565616031","Kölln-Reisiek",null],["010565616033","Seester",null],["010565616042","Raa-Besenbek",null],["010565616045","Seestermühe",null],["010565616046","Seeth-Ekholt",null],["010565636006","Bokel",null],["010565636010","Brande-Hörnerkirchen",null],["010565636038","Osterhorn",null],["010565636051","Westerhorn",null],["010565660003","Bevern",null],["010565660004","Bilsen",null],["010565660008","Bokholt-Hanredder",null],["010565660011","Bullenkuhlen",null],["010565660014","Ellerhoop",null],["010565660017","Groß Offenseth-Aspern",null],["010565660022","Heede",null],["010565660026","Hemdingen",null],["010565660034","Langeln",null],["010565660035","Lutzhorn",null],["010565687009","Borstel-Hohenraden",null],["010565687013","Ellerbek",null],["010565687032","Kummerfeld",null],["010565687040","Prisdorf",null],["010565687047","Tangstedt",null],["010565690001","Appen",null],["010565690016","Groß Nordende",null],["010565690019","Haselau",null],["010565690020","Haseldorf",null],["010565690023","Heidgraben",null],["010565690024","Heist",null],["010565690027","Hetlingen",null],["010565690028","Holm",null],["010565690036","Moorrege",null],["010565690037","Neuendeich",null],["010570001001","Ascheberg (Holstein)",null],["010570008008","Bönebüttel",null],["010570009009","Bösdorf",null],["010570057057","Plön, Stadt",null],["010570062062","Preetz, Stadt",null],["010570091091","Schwentinental, Stadt",null],["010575727004","Behrensdorf (Ostsee)",null],["010575727007","Blekendorf",null],["010575727013","Dannau",null],["010575727021","Giekau",null],["010575727026","Helmstorf",null],["010575727027","Högsdorf",null],["010575727029","Hohenfelde",null],["010575727030","Hohwacht (Ostsee)",null],["010575727034","Kirchnüchel",null],["010575727035","Klamp",null],["010575727038","Kletkamp",null],["010575727048","Lütjenburg, Stadt",null],["010575727055","Panker",null],["010575727076","Schwartbuck",null],["010575727082","Tröndel",null],["010575739015","Dersau",null],["010575739017","Dörnick",null],["010575739022","Grebin",null],["010575739032","Kalübbe",null],["010575739045","Lebrade",null],["010575739053","Nehmten",null],["010575739065","Rantzau",null],["010575739067","Rathjensdorf",null],["010575739089","Wittmoldt",null],["010575747002","Barmissen",null],["010575747010","Boksee",null],["010575747011","Bothkamp",null],["010575747023","Großbarkau",null],["010575747031","Honigsee",null],["010575747033","Kirchbarkau",null],["010575747037","Klein Barkau",null],["010575747042","Kühren",null],["010575747046","Lehmkuhlen",null],["010575747047","Löptin",null],["010575747054","Nettelsee",null],["010575747058","Pohnsdorf",null],["010575747059","Postfeld",null],["010575747066","Rastorf",null],["010575747070","Schellhorn",null],["010575747084","Wahlstorf",null],["010575747086","Warnau",null],["010575755003","Barsbek",null],["010575755006","Bendfeld",null],["010575755012","Brodersdorf",null],["010575755018","Fahren",null],["010575755020","Fiefbergen",null],["010575755028","Höhndorf",null],["010575755039","Köhn",null],["010575755040","Krokau",null],["010575755041","Krummbek",null],["010575755043","Laboe",null],["010575755049","Lutterbek",null],["010575755056","Passade",null],["010575755060","Prasdorf",null],["010575755063","Probsteierhagen",null],["010575755073","Schönberg (Holstein)",null],["010575755078","Stakendorf",null],["010575755079","Stein",null],["010575755081","Stoltenberg",null],["010575755087","Wendtorf",null],["010575755088","Wisch",null],["010575775016","Dobersdorf",null],["010575775044","Lammershagen",null],["010575775050","Martensrade",null],["010575775052","Mucheln",null],["010575775072","Schlesen",null],["010575775077","Selent",null],["010575775090","Fargau-Pratjau",null],["010575782025","Heikendorf",null],["010575782051","Mönkeberg",null],["010575782074","Schönkirchen",null],["010575785005","Belau",null],["010575785024","Großharrie",null],["010575785068","Rendswühren",null],["010575785069","Ruhwinkel",null],["010575785071","Schillsdorf",null],["010575785080","Stolpe",null],["010575785083","Tasdorf",null],["010575785085","Wankendorf",null],["010580005005","Altenholz",null],["010580034034","Büdelsdorf, Stadt",null],["010580043043","Eckernförde, Stadt",null],["010580092092","Kronshagen",null],["010580135135","Rendsburg, Stadt",null],["010580169169","Wasbek",null],["010585803001","Achterwehr",null],["010585803028","Bredenbek",null],["010585803050","Felde",null],["010585803093","Krummwisch",null],["010585803104","Melsdorf",null],["010585803126","Ottendorf",null],["010585803130","Quarnbek",null],["010585803171","Westensee",null],["010585822037","Dänischenhagen",null],["010585822116","Noer",null],["010585822150","Schwedeneck",null],["010585822157","Strande",null],["010585824051","Felm",null],["010585824058","Gettorf",null],["010585824096","Lindau",null],["010585824110","Neudorf-Bornstein",null],["010585824112","Neuwittenbek",null],["010585824121","Osdorf",null],["010585824142","Schinkel",null],["010585824165","Tüttendorf",null],["010585830019","Böhnhusen",null],["010585830053","Flintbek",null],["010585830145","Schönhorst",null],["010585830160","Techelsdorf",null],["010585833003","Alt Duvenstedt",null],["010585833054","Fockbek",null],["010585833118","Nübbel",null],["010585833136","Rickert",null],["010585847010","Bargstall",null],["010585847029","Breiholz",null],["010585847036","Christiansholm",null],["010585847047","Elsdorf-Westermühlen",null],["010585847055","Friedrichsgraben",null],["010585847056","Friedrichsholm",null],["010585847070","Hamdorf",null],["010585847078","Hohn",null],["010585847089","Königshügel",null],["010585847097","Lohe-Föhrden",null],["010585847129","Prinzenmoor",null],["010585847154","Sophienhamm",null],["010585853031","Brinjahe",null],["010585853048","Embühren",null],["010585853068","Haale",null],["010585853071","Hamweddel",null],["010585853075","Hörsten",null],["010585853086","Jevenstedt",null],["010585853101","Luhnstedt",null],["010585853148","Schülp b. Rendsburg",null],["010585853155","Stafstedt",null],["010585853172","Westerrönfeld",null],["010585859018","Blumenthal",null],["010585859105","Mielkendorf",null],["010585859107","Molfsee",null],["010585859138","Rodenbek",null],["010585859139","Rumohr",null],["010585859141","Schierensee",null],["010585864011","Bargstedt",null],["010585864021","Bokel",null],["010585864023","Borgdorf-Seedorf",null],["010585864027","Brammer",null],["010585864038","Dätgen",null],["010585864045","Eisendorf",null],["010585864046","Ellerdorf",null],["010585864049","Emkendorf",null],["010585864059","Gnutz",null],["010585864065","Groß Vollstedt",null],["010585864091","Krogaspe",null],["010585864094","Langwedel",null],["010585864117","Nortorf, Stadt",null],["010585864120","Oldenhütten",null],["010585864147","Schülp b. Nortorf",null],["010585864163","Timmaspe",null],["010585864168","Warder",null],["010585888026","Bovenau",null],["010585888073","Haßmoor",null],["010585888122","Ostenfeld (Rendsburg)",null],["010585888124","Osterrönfeld",null],["010585888132","Rade b. Rendsburg",null],["010585888140","Schacht-Audorf",null],["010585888146","Schülldorf",null],["010585889016","Bissee",null],["010585889022","Bordesholm",null],["010585889033","Brügge",null],["010585889063","Grevenkrug",null],["010585889064","Groß Buchwald",null],["010585889076","Hoffeld",null],["010585889098","Loop",null],["010585889108","Mühbrook",null],["010585889109","Negenharrie",null],["010585889133","Reesdorf",null],["010585889143","Schmalstede",null],["010585889144","Schönbek",null],["010585889153","Sören",null],["010585889170","Wattenbek",null],["010585890008","Ascheffel",null],["010585890024","Borgstedt",null],["010585890030","Brekendorf",null],["010585890035","Bünsdorf",null],["010585890039","Damendorf",null],["010585890066","Groß Wittensee",null],["010585890069","Haby",null],["010585890080","Holtsee",null],["010585890081","Holzbunge",null],["010585890083","Hütten",null],["010585890088","Klein Wittensee",null],["010585890111","Neu Duvenstedt",null],["010585890123","Osterby",null],["010585890127","Owschlag",null],["010585890152","Sehestedt",null],["010585890175","Ahlefeld-Bistensee",null],["010585893004","Altenhof",null],["010585893012","Barkelsby",null],["010585893032","Brodersby",null],["010585893040","Damp",null],["010585893042","Dörphof",null],["010585893052","Fleckeby",null],["010585893057","Gammelby",null],["010585893067","Güby",null],["010585893082","Holzdorf",null],["010585893084","Hummelfeld",null],["010585893087","Karby",null],["010585893090","Kosel",null],["010585893099","Loose",null],["010585893102","Goosefeld",null],["010585893137","Rieseby",null],["010585893162","Thumby",null],["010585893166","Waabs",null],["010585893173","Windeby",null],["010585893174","Winnemark",null],["010585895007","Arpsdorf",null],["010585895009","Aukrug",null],["010585895013","Beldorf",null],["010585895014","Bendorf",null],["010585895015","Beringstedt",null],["010585895025","Bornholt",null],["010585895044","Ehndorf",null],["010585895061","Gokels",null],["010585895062","Grauel",null],["010585895072","Hanerau-Hademarschen",null],["010585895074","Heinkenborstel",null],["010585895077","Hohenwestedt",null],["010585895085","Jahrsdorf",null],["010585895100","Lütjenwestedt",null],["010585895103","Meezen",null],["010585895106","Mörel",null],["010585895113","Nienborstel",null],["010585895115","Nindorf",null],["010585895119","Oldenbüttel",null],["010585895125","Osterstedt",null],["010585895128","Padenstedt",null],["010585895131","Rade b. Hohenwestedt",null],["010585895134","Remmels",null],["010585895151","Seefeld",null],["010585895156","Steenfeld",null],["010585895158","Tackesdorf",null],["010585895159","Tappendorf",null],["010585895161","Thaden",null],["010585895164","Todenbüttel",null],["010585895167","Wapelfeld",null],["010590045045","Kappeln, Stadt",null],["010590075075","Schleswig, Stadt",null],["010590113113","Glücksburg (Ostsee), Stadt",null],["010590120120","Harrislee",null],["010590183183","Handewitt",null],["010595912107","Eggebek",null],["010595912128","Janneby",null],["010595912131","Jerrishoe",null],["010595912132","Jörl",null],["010595912138","Langstedt",null],["010595912162","Sollerup",null],["010595912169","Süderhackstedt",null],["010595912174","Wanderup",null],["010595915012","Borgwedel",null],["010595915018","Busdorf",null],["010595915019","Dannewerk",null],["010595915026","Fahrdorf",null],["010595915032","Geltorf",null],["010595915043","Jagel",null],["010595915056","Lottorf",null],["010595915078","Selk",null],["010595919101","Tastrup",null],["010595919103","Ausacker",null],["010595919116","Großsolt",null],["010595919126","Hürup",null],["010595919127","Husby",null],["010595919141","Maasbüll",null],["010595919182","Freienwill",null],["010595920002","Arnis, Stadt",null],["010595920034","Grödersby",null],["010595920067","Oersberg",null],["010595920068","Rabenkirchen-Faulück",null],["010595937106","Dollerup",null],["010595937118","Grundhof",null],["010595937137","Langballig",null],["010595937145","Munkbrarup",null],["010595937157","Ringsberg",null],["010595937176","Wees",null],["010595937178","Westerholz",null],["010595940159","Sieverstedt",null],["010595940171","Tarp",null],["010595940184","Oeversee",null],["010595949076","Schnarup-Thumby",null],["010595949161","Sörup",null],["010595949185","Mittelangeln",null],["010595952105","Böxlund",null],["010595952115","Großenwiehe",null],["010595952123","Hörup",null],["010595952124","Holt",null],["010595952129","Jardelund",null],["010595952143","Medelby",null],["010595952144","Meyn",null],["010595952149","Nordhackstedt",null],["010595952151","Osterby",null],["010595952158","Schafflund",null],["010595952173","Wallsbüll",null],["010595952177","Weesby",null],["010595952179","Lindewitt",null],["010595974006","Böel",null],["010595974055","Loit",null],["010595974060","Mohrkirch",null],["010595974063","Norderbrarup",null],["010595974065","Nottfeld",null],["010595974070","Rügge",null],["010595974072","Saustrup",null],["010595974074","Scheggerott",null],["010595974080","Steinfeld",null],["010595974083","Süderbrarup",null],["010595974094","Ulsnis",null],["010595974095","Wagersrott",null],["010595974187","Boren",null],["010595987008","Böklund",null],["010595987037","Havetoft",null],["010595987042","Idstedt",null],["010595987049","Klappholz",null],["010595987062","Neuberend",null],["010595987073","Schaalby",null],["010595987081","Stolk",null],["010595987082","Struxdorf",null],["010595987084","Süderfahrenstedt",null],["010595987086","Taarstedt",null],["010595987090","Tolk",null],["010595987093","Uelsby",null],["010595987097","Twedt",null],["010595987098","Nübel",null],["010595987189","Brodersby-Goltoft",null],["010595990102","Ahneby",null],["010595990109","Esgrus",null],["010595990112","Gelting",null],["010595990121","Hasselberg",null],["010595990136","Kronsgaard",null],["010595990142","Maasholm",null],["010595990147","Nieby",null],["010595990148","Niesgrau",null],["010595990152","Pommerby",null],["010595990154","Rabel",null],["010595990155","Rabenholz",null],["010595990163","Stangheck",null],["010595990164","Steinberg",null],["010595990167","Sterup",null],["010595990168","Stoltebüll",null],["010595990186","Steinbergkirche",null],["010595993010","Bollingstedt",null],["010595993023","Ellingstedt",null],["010595993039","Hollingstedt",null],["010595993041","Hüsby",null],["010595993044","Jübek",null],["010595993057","Lürschau",null],["010595993077","Schuby",null],["010595993079","Silberstedt",null],["010595993092","Treia",null],["010595996001","Alt Bennebek",null],["010595996005","Bergenhusen",null],["010595996009","Börm",null],["010595996020","Dörpstedt",null],["010595996024","Erfde",null],["010595996035","Groß Rheide",null],["010595996050","Klein Bennebek",null],["010595996051","Klein Rheide",null],["010595996053","Kropp",null],["010595996058","Meggerdorf",null],["010595996087","Tetenhusen",null],["010595996088","Tielen",null],["010595996096","Wohlde",null],["010595996188","Stapel",null],["010600004004","Bad Bramstedt, Stadt",null],["010600005005","Bad Segeberg, Stadt",null],["010600019019","Ellerau",null],["010600039039","Henstedt-Ulzburg",null],["010600044044","Kaltenkirchen, Stadt",null],["010600063063","Norderstedt, Stadt",null],["010600092092","Wahlstedt, Stadt",null],["010605005003","Armstedt",null],["010605005009","Bimöhlen",null],["010605005013","Borstel",null],["010605005021","Föhrden-Barl",null],["010605005023","Fuhlendorf",null],["010605005027","Großenaspe",null],["010605005031","Hagen",null],["010605005033","Hardebek",null],["010605005035","Hasenkrug",null],["010605005037","Heidmoor",null],["010605005040","Hitzhusen",null],["010605005056","Mönkloh",null],["010605005095","Weddelbrook",null],["010605005099","Wiemersdorf",null],["010605024012","Bornhöved",null],["010605024017","Damsdorf",null],["010605024026","Gönnebek",null],["010605024072","Schmalensee",null],["010605024080","Stocksee",null],["010605024086","Tarbek",null],["010605024087","Tensfeld",null],["010605024089","Trappenkamp",null],["010605034043","Itzstedt",null],["010605034046","Kayhude",null],["010605034058","Nahe",null],["010605034065","Oering",null],["010605034076","Seth",null],["010605034085","Sülfeld",null],["010605043002","Alveslohe",null],["010605043034","Hartenholm",null],["010605043036","Hasenmoor",null],["010605043054","Lentföhrden",null],["010605043064","Nützen",null],["010605043073","Schmalfeld",null],["010605048042","Hüttblek",null],["010605048045","Kattendorf",null],["010605048047","Kisdorf",null],["010605048066","Oersdorf",null],["010605048077","Sievershütten",null],["010605048082","Struvenhütten",null],["010605048084","Stuvenborn",null],["010605048094","Wakendorf II",null],["010605048100","Winsen",null],["010605053007","Bark",null],["010605053008","Bebensee",null],["010605053022","Fredesdorf",null],["010605053029","Groß Niendorf",null],["010605053041","Högersdorf",null],["010605053051","Kükels",null],["010605053053","Leezen",null],["010605053057","Mözen",null],["010605053062","Neversdorf",null],["010605053074","Schwissel",null],["010605053088","Todesfelde",null],["010605053101","Wittenborn",null],["010605063011","Boostedt",null],["010605063016","Daldorf",null],["010605063028","Groß Kummerfeld",null],["010605063038","Heidmühlen",null],["010605063052","Latendorf",null],["010605063068","Rickling",null],["010605086006","Bahrenhof",null],["010605086010","Blunk",null],["010605086015","Bühnsdorf",null],["010605086018","Dreggers",null],["010605086020","Fahrenkrug",null],["010605086024","Geschendorf",null],["010605086025","Glasau",null],["010605086030","Groß Rönnau",null],["010605086048","Klein Gladebrügge",null],["010605086049","Klein Rönnau",null],["010605086050","Krems II",null],["010605086059","Negernbötel",null],["010605086060","Nehms",null],["010605086061","Neuengörs",null],["010605086067","Pronstorf",null],["010605086069","Rohlstorf",null],["010605086070","Schackendorf",null],["010605086071","Schieren",null],["010605086075","Seedorf",null],["010605086079","Stipsdorf",null],["010605086081","Strukdorf",null],["010605086090","Travenhorst",null],["010605086091","Traventhal",null],["010605086093","Wakendorf I",null],["010605086096","Weede",null],["010605086097","Wensin",null],["010605086098","Westerrade",null],["010609014014","Buchholz (Forstgutsbez.),gemfr. Gebiet",null],["010610029029","Glückstadt, Stadt",null],["010610046046","Itzehoe, Stadt",null],["010610113113","Wilster, Stadt",null],["010615104005","Auufer",null],["010615104016","Breitenberg",null],["010615104017","Breitenburg",null],["010615104053","Kollmoor",null],["010615104058","Kronsmoor",null],["010615104061","Lägerdorf",null],["010615104068","Moordiek",null],["010615104072","Münsterdorf",null],["010615104079","Oelixdorf",null],["010615104109","Westermoor",null],["010615104115","Wittenbergen",null],["010615134004","Altenmoor",null],["010615134012","Blomesche Wildnis",null],["010615134015","Borsfleth",null],["010615134027","Engelbrechtsche Wildnis",null],["010615134037","Herzhorn",null],["010615134041","Hohenfelde",null],["010615134044","Horst (Holstein)",null],["010615134050","Kiebitzreihe",null],["010615134054","Krempdorf",null],["010615134074","Neuendorf b. Elmshorn",null],["010615134101","Sommerland",null],["010615134118","Kollmar",null],["010615138008","Bekdorf",null],["010615138010","Bekmünde",null],["010615138024","Drage",null],["010615138034","Heiligenstedten",null],["010615138035","Heiligenstedtenerkamp",null],["010615138039","Hodorf",null],["010615138040","Hohenaspe",null],["010615138045","Huje",null],["010615138047","Kaaks",null],["010615138052","Kleve",null],["010615138059","Krummendiek",null],["010615138065","Lohbarbek",null],["010615138067","Mehlbek",null],["010615138070","Moorhusen",null],["010615138082","Oldendorf",null],["010615138083","Ottenbüttel",null],["010615138084","Peissen",null],["010615138098","Schlotfeld",null],["010615138100","Silzen",null],["010615138114","Winseldorf",null],["010615153006","Bahrenfleth",null],["010615153022","Dägeling",null],["010615153026","Elskop",null],["010615153030","Grevenkop",null],["010615153055","Krempe, Stadt",null],["010615153056","Kremperheide",null],["010615153057","Krempermoor",null],["010615153073","Neuenbrook",null],["010615153092","Rethwisch",null],["010615153104","Süderau",null],["010615168001","Aasbüttel",null],["010615168003","Agethorst",null],["010615168011","Besdorf",null],["010615168013","Bokelrehm",null],["010615168014","Bokhorst",null],["010615168021","Christinenthal",null],["010615168031","Gribbohm",null],["010615168033","Hadenfeld",null],["010615168043","Holstenniendorf",null],["010615168048","Kaisborstel",null],["010615168066","Looft",null],["010615168076","Nienbüttel",null],["010615168078","Nutteln",null],["010615168081","Oldenborstel",null],["010615168085","Pöschendorf",null],["010615168087","Puls",null],["010615168091","Reher",null],["010615168097","Schenefeld",null],["010615168105","Vaale",null],["010615168106","Vaalermoor",null],["010615168107","Wacken",null],["010615168108","Warringholz",null],["010615179002","Aebtissinwisch",null],["010615179007","Beidenfleth",null],["010615179018","Brokdorf",null],["010615179020","Büttel",null],["010615179023","Dammfleth",null],["010615179025","Ecklak",null],["010615179060","Kudensee",null],["010615179062","Landrecht",null],["010615179063","Landscheide",null],["010615179077","Nortorf",null],["010615179095","Sankt Margarethen",null],["010615179102","Stördorf",null],["010615179110","Wewelsfleth",null],["010615179119","Neuendorf-Sachsenbande",null],["010615189019","Brokstedt",null],["010615189028","Fitzbek",null],["010615189036","Hennstedt",null],["010615189038","Hingstheide",null],["010615189042","Hohenlockstedt",null],["010615189049","Kellinghusen, Stadt",null],["010615189064","Lockstedt",null],["010615189071","Mühlenbarbek",null],["010615189080","Oeschebüttel",null],["010615189086","Poyenberg",null],["010615189088","Quarnstedt",null],["010615189089","Rade",null],["010615189093","Rosdorf",null],["010615189096","Sarlhusen",null],["010615189103","Störkathen",null],["010615189111","Wiedenborstel",null],["010615189112","Willenscharen",null],["010615189116","Wrist",null],["010615189117","Wulfsmoor",null],["010620001001","Ahrensburg, Stadt",null],["010620004004","Bad Oldesloe, Stadt",null],["010620006006","Bargteheide, Stadt",null],["010620009009","Barsbüttel",null],["010620018018","Glinde, Stadt",null],["010620023023","Großhansdorf",null],["010620053053","Oststeinbek",null],["010620060060","Reinbek, Stadt",null],["010620061061","Reinfeld (Holstein), Stadt",null],["010620076076","Tangstedt",null],["010620090090","Ammersbek",null],["010625207019","Grabau",null],["010625207046","Meddewade",null],["010625207050","Neritz",null],["010625207056","Pölitz",null],["010625207062","Rethwisch",null],["010625207065","Rümpel",null],["010625207089","Lasbek",null],["010625207091","Steinburg",null],["010625207092","Travenbrück",null],["010625218005","Bargfeld-Stegen",null],["010625218014","Delingsdorf",null],["010625218016","Elmenhorst",null],["010625218027","Hammoor",null],["010625218036","Jersbek",null],["010625218051","Nienwohld",null],["010625218078","Todendorf",null],["010625218081","Tremsbüttel",null],["010625244003","Badendorf",null],["010625244008","Barnitz",null],["010625244025","Hamberge",null],["010625244031","Heidekamp",null],["010625244032","Heilshoop",null],["010625244039","Klein Wesenberg",null],["010625244048","Mönkhagen",null],["010625244059","Rehhorst",null],["010625244083","Westerau",null],["010625244087","Zarpen",null],["010625244093","Feldhorst",null],["010625244094","Wesenberg",null],["010625262011","Braak",null],["010625262035","Hoisdorf",null],["010625262069","Siek",null],["010625262071","Stapelfeld",null],["010625262088","Brunsbek",null],["010625270020","Grande",null],["010625270021","Grönwohld",null],["010625270022","Großensee",null],["010625270026","Hamfelde",null],["010625270033","Hohenfelde",null],["010625270040","Köthel",null],["010625270045","Lütjensee",null],["010625270058","Rausdorf",null],["010625270082","Trittau",null],["010625270086","Witzhave",null],["020000000000","Hamburg, Freie und Hansestadt",null],["021010101101","Hamburg-Altstadt, OT 101","Stadt-/Ortsteil bzw. Stadtbezirk"],["021010102102","Hamburg-Altstadt, OT 102","Stadt-/Ortsteil bzw. Stadtbezirk"],["021020103103","HafenCity, OT 103","Stadt-/Ortsteil bzw. Stadtbezirk"],["021020104104","HafenCity, OT 104","Stadt-/Ortsteil bzw. Stadtbezirk"],["021030105105","Neustadt, OT 105","Stadt-/Ortsteil bzw. Stadtbezirk"],["021030106106","Neustadt, OT 106","Stadt-/Ortsteil bzw. Stadtbezirk"],["021030107107","Neustadt, OT 107","Stadt-/Ortsteil bzw. Stadtbezirk"],["021030108108","Neustadt, OT 108","Stadt-/Ortsteil bzw. Stadtbezirk"],["021040109109","St. Pauli, OT 109","Stadt-/Ortsteil bzw. Stadtbezirk"],["021040110110","St. Pauli, OT 110","Stadt-/Ortsteil bzw. Stadtbezirk"],["021040111111","St. Pauli, OT 111","Stadt-/Ortsteil bzw. Stadtbezirk"],["021040112112","St. Pauli, OT 112","Stadt-/Ortsteil bzw. Stadtbezirk"],["021050113113","St. Georg, OT 113","Stadt-/Ortsteil bzw. Stadtbezirk"],["021050114114","St. Georg, OT 114","Stadt-/Ortsteil bzw. Stadtbezirk"],["021060115115","Hammerbrook, OT 115","Stadt-/Ortsteil bzw. Stadtbezirk"],["021060116116","Hammerbrook, OT 116","Stadt-/Ortsteil bzw. Stadtbezirk"],["021060117117","Hammerbrook, OT 117","Stadt-/Ortsteil bzw. Stadtbezirk"],["021060118118","Hammerbrook, OT 118","Stadt-/Ortsteil bzw. Stadtbezirk"],["021070119119","Borgfelde, OT 119","Stadt-/Ortsteil bzw. Stadtbezirk"],["021070120120","Borgfelde, OT 120","Stadt-/Ortsteil bzw. Stadtbezirk"],["021080121121","Hamm, OT 121","Stadt-/Ortsteil bzw. Stadtbezirk"],["021080122122","Hamm, OT 122","Stadt-/Ortsteil bzw. Stadtbezirk"],["021080123123","Hamm, OT 123","Stadt-/Ortsteil bzw. Stadtbezirk"],["021080124124","Hamm, OT 124","Stadt-/Ortsteil bzw. Stadtbezirk"],["021080125125","Hamm, OT 125","Stadt-/Ortsteil bzw. Stadtbezirk"],["021080126126","Hamm, OT 126","Stadt-/Ortsteil bzw. Stadtbezirk"],["021080127127","Hamm, OT 127","Stadt-/Ortsteil bzw. Stadtbezirk"],["021110128128","Horn, OT 128","Stadt-/Ortsteil bzw. Stadtbezirk"],["021110129129","Horn, OT 129","Stadt-/Ortsteil bzw. Stadtbezirk"],["021120130130","Billstedt, OT 130","Stadt-/Ortsteil bzw. Stadtbezirk"],["021130131131","Billbrook, OT 131","Stadt-/Ortsteil bzw. Stadtbezirk"],["021140132132","Rothenburgsort, OT 132","Stadt-/Ortsteil bzw. Stadtbezirk"],["021140133133","Rothenburgsort, OT 133","Stadt-/Ortsteil bzw. Stadtbezirk"],["021150134134","Veddel, OT 134","Stadt-/Ortsteil bzw. Stadtbezirk"],["021160135135","Wilhelmsburg, OT 135","Stadt-/Ortsteil bzw. Stadtbezirk"],["021160136136","Wilhelmsburg, OT 136","Stadt-/Ortsteil bzw. Stadtbezirk"],["021160137137","Wilhelmsburg, OT 137","Stadt-/Ortsteil bzw. Stadtbezirk"],["021170138138","Kleiner Grasbrook, OT 138","Stadt-/Ortsteil bzw. Stadtbezirk"],["021180139139","Steinwerder, OT 139","Stadt-/Ortsteil bzw. Stadtbezirk"],["021190140140","Waltershof, OT 140","Stadt-/Ortsteil bzw. Stadtbezirk"],["021200141141","Finkenwerder, OT 141","Stadt-/Ortsteil bzw. Stadtbezirk"],["021210142142","Neuwerk, OT 142","Stadt-/Ortsteil bzw. Stadtbezirk"],["021220150150","Seeleute/Binnenschiffer, OT 150","Stadt-/Ortsteil bzw. Stadtbezirk"],["022010201201","Altona-Altstadt, OT 201","Stadt-/Ortsteil bzw. Stadtbezirk"],["022010202202","Altona-Altstadt, OT 202","Stadt-/Ortsteil bzw. Stadtbezirk"],["022010203203","Altona-Altstadt, OT 203","Stadt-/Ortsteil bzw. Stadtbezirk"],["022010204204","Altona-Altstadt, OT 204","Stadt-/Ortsteil bzw. Stadtbezirk"],["022010205205","Altona-Altstadt, OT 205","Stadt-/Ortsteil bzw. Stadtbezirk"],["022010206206","Altona-Altstadt, OT 206","Stadt-/Ortsteil bzw. Stadtbezirk"],["022020207207","Sternschanze, OT 207","Stadt-/Ortsteil bzw. Stadtbezirk"],["022030208208","Altona-Nord, OT 208","Stadt-/Ortsteil bzw. Stadtbezirk"],["022030209209","Altona-Nord, OT 209","Stadt-/Ortsteil bzw. Stadtbezirk"],["022030210210","Altona-Nord, OT 210","Stadt-/Ortsteil bzw. Stadtbezirk"],["022040211211","Ottensen, OT 211","Stadt-/Ortsteil bzw. Stadtbezirk"],["022040212212","Ottensen, OT 212","Stadt-/Ortsteil bzw. Stadtbezirk"],["022040213213","Ottensen, OT 213","Stadt-/Ortsteil bzw. Stadtbezirk"],["022040214214","Ottensen, OT 214","Stadt-/Ortsteil bzw. Stadtbezirk"],["022050215215","Bahrenfeld, OT 215","Stadt-/Ortsteil bzw. Stadtbezirk"],["022050216216","Bahrenfeld, OT 216","Stadt-/Ortsteil bzw. Stadtbezirk"],["022050217217","Bahrenfeld, OT 217","Stadt-/Ortsteil bzw. Stadtbezirk"],["022060218218","Groß Flottbek, OT 218","Stadt-/Ortsteil bzw. Stadtbezirk"],["022070219219","Othmarschen, OT 219","Stadt-/Ortsteil bzw. Stadtbezirk"],["022080220220","Lurup, OT 220","Stadt-/Ortsteil bzw. Stadtbezirk"],["022090221221","Osdorf, OT 221","Stadt-/Ortsteil bzw. Stadtbezirk"],["022100222222","Nienstedten, OT 222","Stadt-/Ortsteil bzw. Stadtbezirk"],["022110223223","Blankenese, OT 223","Stadt-/Ortsteil bzw. Stadtbezirk"],["022110224224","Blankenese, OT 224","Stadt-/Ortsteil bzw. Stadtbezirk"],["022120225225","Iserbrook, OT 225","Stadt-/Ortsteil bzw. Stadtbezirk"],["022130226226","Sülldorf, OT 226","Stadt-/Ortsteil bzw. Stadtbezirk"],["022140227227","Rissen, OT 227","Stadt-/Ortsteil bzw. Stadtbezirk"],["023010301301","Eimsbüttel, OT 301","Stadt-/Ortsteil bzw. Stadtbezirk"],["023010302302","Eimsbüttel, OT 302","Stadt-/Ortsteil bzw. Stadtbezirk"],["023010303303","Eimsbüttel, OT 303","Stadt-/Ortsteil bzw. Stadtbezirk"],["023010304304","Eimsbüttel, OT 304","Stadt-/Ortsteil bzw. Stadtbezirk"],["023010305305","Eimsbüttel, OT 305","Stadt-/Ortsteil bzw. Stadtbezirk"],["023010306306","Eimsbüttel, OT 306","Stadt-/Ortsteil bzw. Stadtbezirk"],["023010307307","Eimsbüttel, OT 307","Stadt-/Ortsteil bzw. Stadtbezirk"],["023010308308","Eimsbüttel, OT 308","Stadt-/Ortsteil bzw. Stadtbezirk"],["023010309309","Eimsbüttel, OT 309","Stadt-/Ortsteil bzw. Stadtbezirk"],["023010310310","Eimsbüttel, OT 310","Stadt-/Ortsteil bzw. Stadtbezirk"],["023020311311","Rotherbaum, OT 311","Stadt-/Ortsteil bzw. Stadtbezirk"],["023020312312","Rotherbaum, OT 312","Stadt-/Ortsteil bzw. Stadtbezirk"],["023030313313","Harvestehude, OT 313","Stadt-/Ortsteil bzw. Stadtbezirk"],["023030314314","Harvestehude, OT 314","Stadt-/Ortsteil bzw. Stadtbezirk"],["023040315315","Hoheluft-West, OT 315","Stadt-/Ortsteil bzw. Stadtbezirk"],["023040316316","Hoheluft-West, OT 316","Stadt-/Ortsteil bzw. Stadtbezirk"],["023050317317","Lokstedt, OT 317","Stadt-/Ortsteil bzw. Stadtbezirk"],["023060318318","Niendorf, OT 318","Stadt-/Ortsteil bzw. Stadtbezirk"],["023070319319","Schnelsen, OT 319","Stadt-/Ortsteil bzw. Stadtbezirk"],["023080320320","Eidelstedt, OT 320","Stadt-/Ortsteil bzw. Stadtbezirk"],["023090321321","Stellingen, OT 321","Stadt-/Ortsteil bzw. Stadtbezirk"],["024010401401","Hoheluft-Ost, OT 401","Stadt-/Ortsteil bzw. Stadtbezirk"],["024010402402","Hoheluft-Ost, OT 402","Stadt-/Ortsteil bzw. Stadtbezirk"],["024020403403","Eppendorf, OT 403","Stadt-/Ortsteil bzw. Stadtbezirk"],["024020404404","Eppendorf, OT 404","Stadt-/Ortsteil bzw. Stadtbezirk"],["024020405405","Eppendorf, OT 405","Stadt-/Ortsteil bzw. Stadtbezirk"],["024030406406","Gross Borstel, OT 406","Stadt-/Ortsteil bzw. Stadtbezirk"],["024040407407","Alsterdorf, OT 407","Stadt-/Ortsteil bzw. Stadtbezirk"],["024050408408","Winterhude, OT 408","Stadt-/Ortsteil bzw. Stadtbezirk"],["024050409409","Winterhude, OT 409","Stadt-/Ortsteil bzw. Stadtbezirk"],["024050410410","Winterhude, OT 410","Stadt-/Ortsteil bzw. Stadtbezirk"],["024050411411","Winterhude, OT 411","Stadt-/Ortsteil bzw. Stadtbezirk"],["024050412412","Winterhude, OT 412","Stadt-/Ortsteil bzw. Stadtbezirk"],["024050413413","Winterhude, OT 413","Stadt-/Ortsteil bzw. Stadtbezirk"],["024060414414","Uhlenhorst, OT 414","Stadt-/Ortsteil bzw. Stadtbezirk"],["024060415415","Uhlenhorst, OT 415","Stadt-/Ortsteil bzw. Stadtbezirk"],["024070416416","Hohenfelde, OT 416","Stadt-/Ortsteil bzw. Stadtbezirk"],["024070417417","Hohenfelde, OT 417","Stadt-/Ortsteil bzw. Stadtbezirk"],["024080418418","Barmbek-Süd, OT 418","Stadt-/Ortsteil bzw. Stadtbezirk"],["024080419419","Barmbek-Süd, OT 419","Stadt-/Ortsteil bzw. Stadtbezirk"],["024080420420","Barmbek-Süd, OT 420","Stadt-/Ortsteil bzw. Stadtbezirk"],["024080421421","Barmbek-Süd, OT 421","Stadt-/Ortsteil bzw. Stadtbezirk"],["024080422422","Barmbek-Süd, OT 422","Stadt-/Ortsteil bzw. Stadtbezirk"],["024080423423","Barmbek-Süd, OT 423","Stadt-/Ortsteil bzw. Stadtbezirk"],["024090424424","Dulsberg, OT 424","Stadt-/Ortsteil bzw. Stadtbezirk"],["024090425425","Dulsberg, OT 425","Stadt-/Ortsteil bzw. Stadtbezirk"],["024100426426","Barmbek-Nord, OT 426","Stadt-/Ortsteil bzw. Stadtbezirk"],["024100427427","Barmbek-Nord, OT 427","Stadt-/Ortsteil bzw. Stadtbezirk"],["024100428428","Barmbek-Nord, OT 428","Stadt-/Ortsteil bzw. Stadtbezirk"],["024100429429","Barmbek-Nord, OT 429","Stadt-/Ortsteil bzw. Stadtbezirk"],["024110430430","Ohlsdorf, OT 430","Stadt-/Ortsteil bzw. Stadtbezirk"],["024120431431","Fuhlsbüttel, OT 431","Stadt-/Ortsteil bzw. Stadtbezirk"],["024130432432","Langenhorn, OT 432","Stadt-/Ortsteil bzw. Stadtbezirk"],["025010501501","Eilbek, OT 501","Stadt-/Ortsteil bzw. Stadtbezirk"],["025010502502","Eilbek, OT 502","Stadt-/Ortsteil bzw. Stadtbezirk"],["025010503503","Eilbek, OT 503","Stadt-/Ortsteil bzw. Stadtbezirk"],["025010504504","Eilbek, OT 504","Stadt-/Ortsteil bzw. Stadtbezirk"],["025020505505","Wandsbek, OT 505","Stadt-/Ortsteil bzw. Stadtbezirk"],["025020506506","Wandsbek, OT 506","Stadt-/Ortsteil bzw. Stadtbezirk"],["025020507507","Wandsbek, OT 507","Stadt-/Ortsteil bzw. Stadtbezirk"],["025020508508","Wandsbek, OT 508","Stadt-/Ortsteil bzw. Stadtbezirk"],["025020509509","Wandsbek, OT 509","Stadt-/Ortsteil bzw. Stadtbezirk"],["025030510510","Marienthal, OT 510","Stadt-/Ortsteil bzw. Stadtbezirk"],["025030511511","Marienthal, OT 511","Stadt-/Ortsteil bzw. Stadtbezirk"],["025040512512","Jenfeld, OT 512","Stadt-/Ortsteil bzw. Stadtbezirk"],["025050513513","Tonndorf, OT 513","Stadt-/Ortsteil bzw. Stadtbezirk"],["025060514514","Farmsen-Berne, OT 514","Stadt-/Ortsteil bzw. Stadtbezirk"],["025070515515","Bramfeld, OT 515","Stadt-/Ortsteil bzw. Stadtbezirk"],["025080516516","Steilshoop, OT 516","Stadt-/Ortsteil bzw. Stadtbezirk"],["025090517517","Wellingsbüttel, OT 517","Stadt-/Ortsteil bzw. Stadtbezirk"],["025100518518","Sasel, OT 518","Stadt-/Ortsteil bzw. Stadtbezirk"],["025110519519","Poppenbüttel, OT 519","Stadt-/Ortsteil bzw. Stadtbezirk"],["025120520520","Hummelsbüttel, OT 520","Stadt-/Ortsteil bzw. Stadtbezirk"],["025130521521","Lemsahl-Mellingstedt, OT 521","Stadt-/Ortsteil bzw. Stadtbezirk"],["025140522522","Duvenstedt, OT 522","Stadt-/Ortsteil bzw. Stadtbezirk"],["025150523523","Wohldorf-Ohlstedt, OT 523","Stadt-/Ortsteil bzw. Stadtbezirk"],["025160524524","Bergstedt, OT 524","Stadt-/Ortsteil bzw. Stadtbezirk"],["025170525525","Volksdorf, OT 525","Stadt-/Ortsteil bzw. Stadtbezirk"],["025180526526","Rahlstedt, OT 526","Stadt-/Ortsteil bzw. Stadtbezirk"],["026010601601","Lohbrügge, OT 601","Stadt-/Ortsteil bzw. Stadtbezirk"],["026020602602","Bergedorf, OT 602","Stadt-/Ortsteil bzw. Stadtbezirk"],["026020603603","Bergedorf, OT 603","Stadt-/Ortsteil bzw. Stadtbezirk"],["026030604604","Curslack, OT 604","Stadt-/Ortsteil bzw. Stadtbezirk"],["026040605605","Altengamme, OT 605","Stadt-/Ortsteil bzw. Stadtbezirk"],["026050606606","Neuengamme, OT 606","Stadt-/Ortsteil bzw. Stadtbezirk"],["026060607607","Kirchwerder, OT 607","Stadt-/Ortsteil bzw. Stadtbezirk"],["026070608608","Ochsenwerder, OT 608","Stadt-/Ortsteil bzw. Stadtbezirk"],["026080609609","Reitbrook, OT 609","Stadt-/Ortsteil bzw. Stadtbezirk"],["026090610610","Allermöhe, OT 610","Stadt-/Ortsteil bzw. Stadtbezirk"],["026100611611","Billwerder, OT 611","Stadt-/Ortsteil bzw. Stadtbezirk"],["026110612612","Moorfleet, OT 612","Stadt-/Ortsteil bzw. Stadtbezirk"],["026120613613","Tatenberg, OT 613","Stadt-/Ortsteil bzw. Stadtbezirk"],["026130614614","Spadenland, OT 614","Stadt-/Ortsteil bzw. Stadtbezirk"],["026140615615","Neuallermöhe, OT 615","Stadt-/Ortsteil bzw. Stadtbezirk"],["027010701701","Harburg, OT 701","Stadt-/Ortsteil bzw. Stadtbezirk"],["027010702702","Harburg, OT 702","Stadt-/Ortsteil bzw. Stadtbezirk"],["027020703703","Neuland, OT 703","Stadt-/Ortsteil bzw. Stadtbezirk"],["027030704704","Gut Moor, OT 704","Stadt-/Ortsteil bzw. Stadtbezirk"],["027040705705","Wilstorf, OT 705","Stadt-/Ortsteil bzw. Stadtbezirk"],["027050706706","Rönneburg, OT 706","Stadt-/Ortsteil bzw. Stadtbezirk"],["027060707707","Langenbek, OT 707","Stadt-/Ortsteil bzw. Stadtbezirk"],["027070708708","Sinstorf, OT 708","Stadt-/Ortsteil bzw. Stadtbezirk"],["027080709709","Marmstorf, OT 709","Stadt-/Ortsteil bzw. Stadtbezirk"],["027090710710","Eissendorf, OT 710","Stadt-/Ortsteil bzw. Stadtbezirk"],["027100711711","Heimfeld, OT 711","Stadt-/Ortsteil bzw. Stadtbezirk"],["027110712712","Moorburg, OT 712","Stadt-/Ortsteil bzw. Stadtbezirk"],["027120713713","Altenwerder, OT 713","Stadt-/Ortsteil bzw. Stadtbezirk"],["027130714714","Hausbruch, OT 714","Stadt-/Ortsteil bzw. Stadtbezirk"],["027140715715","Neugraben-Fischbek, OT 715","Stadt-/Ortsteil bzw. Stadtbezirk"],["027150716716","Francop, OT 716","Stadt-/Ortsteil bzw. Stadtbezirk"],["027160717717","Neuenfelde, OT 717","Stadt-/Ortsteil bzw. Stadtbezirk"],["027170718718","Cranz, OT 718","Stadt-/Ortsteil bzw. Stadtbezirk"],["031010000000","Braunschweig, Stadt",null],["031020000000","Salzgitter, Stadt",null],["031030000000","Wolfsburg, Stadt",null],["031510009009","Gifhorn, Stadt",null],["031510025025","Sassenburg",null],["031510040040","Wittingen, Stadt",null],["031515401002","Barwedel",null],["031515401004","Bokensdorf",null],["031515401014","Jembke",null],["031515401020","Osloß",null],["031515401030","Tappenbeck",null],["031515401039","Weyhausen",null],["031515402003","Bergfeld",null],["031515402005","Brome, Flecken",null],["031515402008","Ehra-Lessien",null],["031515402021","Parsau",null],["031515402024","Rühen",null],["031515402031","Tiddische",null],["031515402032","Tülau",null],["031515403007","Dedelstorf",null],["031515403011","Hankensbüttel",null],["031515403019","Obernholz",null],["031515403028","Sprakensehl",null],["031515403029","Steinhorst",null],["031515404006","Calberlah",null],["031515404013","Isenbüttel",null],["031515404022","Ribbesbüttel",null],["031515404037","Wasbüttel",null],["031515405012","Hillerse",null],["031515405015","Leiferde",null],["031515405017","Meinersen",null],["031515405018","Müden (Aller)",null],["031515406001","Adenbüttel",null],["031515406016","Meine",null],["031515406023","Rötgesbüttel",null],["031515406027","Schwülper",null],["031515406034","Vordorf",null],["031515406041","Didderse",null],["031515407010","Groß Oesingen",null],["031515407026","Schönewörde",null],["031515407033","Ummern",null],["031515407035","Wagenhoff",null],["031515407036","Wahrenholz",null],["031515407038","Wesendorf",null],["031519501501","Giebel, gemfr. Gebiet",null],["031530002002","Bad Harzburg, Stadt",null],["031530007007","Langelsheim, Stadt",null],["031530008008","Liebenburg",null],["031530012012","Seesen, Stadt",null],["031530016016","Braunlage, Stadt",null],["031530017017","Goslar, Stadt",null],["031530018018","Clausthal-Zellerfeld, Berg- und Universitätsstadt",null],["031535401006","Hahausen",null],["031535401009","Lutter am Barenberge, Flecken",null],["031535401014","Wallmoden",null],["031539504504","Harz (Landkreis Goslar), gemfr. Gebiet",null],["031540013013","Königslutter am Elm, Stadt",null],["031540014014","Lehre",null],["031540019019","Schöningen, Stadt",null],["031540028028","Helmstedt, Stadt",null],["031545401008","Grasleben",null],["031545401015","Mariental",null],["031545401016","Querenhorst",null],["031545401018","Rennau",null],["031545402002","Beierstedt",null],["031545402006","Gevensleben",null],["031545402012","Jerxheim",null],["031545402027","Söllingen",null],["031545403005","Frellstedt",null],["031545403017","Räbke",null],["031545403021","Süpplingen",null],["031545403022","Süpplingenburg",null],["031545403025","Warberg",null],["031545403026","Wolsdorf",null],["031545404001","Bahrdorf",null],["031545404004","Danndorf",null],["031545404007","Grafhorst",null],["031545404009","Groß Twülpstedt",null],["031545404024","Velpke",null],["031549501501","Brunsleberfeld, gemfr. Gebiet",null],["031549502502","Helmstedt, gemfr. Gebiet",null],["031549503503","Königslutter, gemfr. Gebiet",null],["031549504504","Mariental, gemfr. Gebiet",null],["031549506506","Schöningen, gemfr. Gebiet",null],["031550001001","Bad Gandersheim, Stadt",null],["031550002002","Bodenfelde, Flecken",null],["031550003003","Dassel, Stadt",null],["031550005005","Hardegsen, Stadt",null],["031550006006","Kalefeld",null],["031550007007","Katlenburg-Lindau",null],["031550009009","Moringen, Stadt",null],["031550010010","Nörten-Hardenberg, Flecken",null],["031550011011","Northeim, Stadt",null],["031550012012","Uslar, Stadt",null],["031550013013","Einbeck, Stadt",null],["031559501501","Solling (Landkreis Northeim), gemfr. Geb.",null],["031570001001","Edemissen",null],["031570002002","Hohenhameln",null],["031570005005","Lengede",null],["031570006006","Peine, Stadt",null],["031570007007","Vechelde",null],["031570008008","Wendeburg",null],["031570009009","Ilsede",null],["031580006006","Cremlingen",null],["031580037037","Wolfenbüttel, Stadt",null],["031580039039","Schladen-Werla",null],["031585402002","Baddeckenstedt",null],["031585402004","Burgdorf",null],["031585402011","Elbe",null],["031585402016","Haverlah",null],["031585402018","Heere",null],["031585402028","Sehlde",null],["031585403005","Cramme",null],["031585403010","Dorstadt",null],["031585403014","Flöthe",null],["031585403019","Heiningen",null],["031585403023","Ohrum",null],["031585403038","Börßum",null],["031585406009","Dettum",null],["031585406012","Erkerode",null],["031585406013","Evessen",null],["031585406030","Sickte",null],["031585406033","Veltheim (Ohe)",null],["031585407007","Dahlum",null],["031585407008","Denkte",null],["031585407017","Hedeper",null],["031585407021","Kissenbrück",null],["031585407022","Kneitlingen",null],["031585407025","Roklum",null],["031585407027","Schöppenstedt, Stadt",null],["031585407031","Uehrde",null],["031585407032","Vahlberg",null],["031585407035","Winnigstedt",null],["031585407036","Wittmar",null],["031585407040","Remlingen-Semmenstedt",null],["031589501501","Am Großen Rhode, gemfr. Gebiet",null],["031589502502","Barnstorf-Warle, gemfr. Gebiet",null],["031589503503","Voigtsdahlum, gemfr. Gebiet",null],["031590001001","Adelebsen, Flecken",null],["031590002002","Bad Grund (Harz)",null],["031590003003","Bad Lauterberg im Harz, Stadt",null],["031590004004","Bad Sachsa, Stadt",null],["031590007007","Bovenden, Flecken",null],["031590010010","Duderstadt, Stadt",null],["031590013013","Friedland",null],["031590015015","Gleichen",null],["031590016016","Göttingen, Stadt",null],["031590017017","Hann. Münden, Stadt",null],["031590019019","Herzberg am Harz, Stadt",null],["031590026026","Osterode am Harz, Stadt",null],["031590029029","Rosdorf",null],["031590034034","Staufenberg",null],["031590036036","Walkenried",null],["031595401008","Bühren",null],["031595401009","Dransfeld, Stadt",null],["031595401021","Jühnde",null],["031595401024","Niemetal",null],["031595401031","Scheden",null],["031595402005","Bilshausen",null],["031595402006","Bodensee",null],["031595402014","Gieboldehausen, Flecken",null],["031595402022","Krebeck",null],["031595402025","Obernfeld",null],["031595402027","Rhumspringe",null],["031595402028","Rollshausen",null],["031595402030","Rüdershausen",null],["031595402037","Wollbrandshausen",null],["031595402038","Wollershausen",null],["031595403012","Elbingerode",null],["031595403018","Hattorf am Harz",null],["031595403020","Hörden am Harz",null],["031595403039","Wulften am Harz",null],["031595404011","Ebergötzen",null],["031595404023","Landolfshausen",null],["031595404032","Seeburg",null],["031595404033","Seulingen",null],["031595404035","Waake",null],["031599501501","Harz (Landkreis Göttingen), gemfr. Geb.",null],["032410001001","Hannover, Landeshauptstadt",null],["032410002002","Barsinghausen, Stadt",null],["032410003003","Burgdorf, Stadt",null],["032410004004","Burgwedel, Stadt",null],["032410005005","Garbsen, Stadt",null],["032410006006","Gehrden, Stadt",null],["032410007007","Hemmingen, Stadt",null],["032410008008","Isernhagen",null],["032410009009","Laatzen, Stadt",null],["032410010010","Langenhagen, Stadt",null],["032410011011","Lehrte, Stadt",null],["032410012012","Neustadt am Rübenberge, Stadt",null],["032410013013","Pattensen, Stadt",null],["032410014014","Ronnenberg, Stadt",null],["032410015015","Seelze, Stadt",null],["032410016016","Sehnde, Stadt",null],["032410017017","Springe, Stadt",null],["032410018018","Uetze",null],["032410019019","Wedemark",null],["032410020020","Wennigsen (Deister)",null],["032410021021","Wunstorf, Stadt",null],["032510007007","Bassum, Stadt",null],["032510012012","Diepholz, Stadt",null],["032510037037","Stuhr",null],["032510040040","Sulingen, Stadt",null],["032510041041","Syke, Stadt",null],["032510042042","Twistringen, Stadt",null],["032510044044","Wagenfeld",null],["032510047047","Weyhe",null],["032515401009","Brockum",null],["032515401020","Hüde",null],["032515401022","Lembruch",null],["032515401023","Lemförde, Flecken",null],["032515401025","Marl",null],["032515401029","Quernheim",null],["032515401036","Stemshorn",null],["032515402005","Barnstorf, Flecken",null],["032515402013","Drebber",null],["032515402014","Drentwede",null],["032515402017","Eydelstedt",null],["032515403002","Asendorf",null],["032515403026","Martfeld",null],["032515403033","Schwarme",null],["032515403049","Bruchhausen-Vilsen, Flecken",null],["032515404003","Bahrenborstel",null],["032515404004","Barenburg, Flecken",null],["032515404018","Freistatt",null],["032515404021","Kirchdorf",null],["032515404043","Varrel",null],["032515404045","Wehrbleck",null],["032515405006","Barver",null],["032515405011","Dickel",null],["032515405019","Hemsloh",null],["032515405030","Rehden",null],["032515405046","Wetschen",null],["032515406001","Affinghausen",null],["032515406015","Ehrenburg",null],["032515406028","Neuenkirchen",null],["032515406031","Scholen",null],["032515406032","Schwaförden",null],["032515406038","Sudwalde",null],["032515407008","Borstel",null],["032515407024","Maasen",null],["032515407027","Mellinghausen",null],["032515407034","Siedenburg, Flecken",null],["032515407035","Staffhorst",null],["032520001001","Aerzen, Flecken",null],["032520002002","Bad Münder am Deister, Stadt",null],["032520003003","Bad Pyrmont, Stadt",null],["032520004004","Coppenbrügge, Flecken",null],["032520005005","Emmerthal",null],["032520006006","Hameln, Stadt",null],["032520007007","Hessisch Oldendorf, Stadt",null],["032520008008","Salzhemmendorf, Flecken",null],["032540002002","Alfeld (Leine), Stadt",null],["032540003003","Algermissen",null],["032540005005","Bad Salzdetfurth, Stadt",null],["032540008008","Bockenem, Stadt",null],["032540011011","Diekholzen",null],["032540014014","Elze, Stadt",null],["032540017017","Giesen",null],["032540020020","Harsum",null],["032540021021","Hildesheim, Stadt",null],["032540022022","Holle",null],["032540026026","Nordstemmen",null],["032540028028","Sarstedt, Stadt",null],["032540029029","Schellerten",null],["032540032032","Söhlde",null],["032540042042","Freden (Leine)",null],["032540044044","Lamspringe",null],["032540045045","Sibbesse",null],["032545406013","Eime, Flecken",null],["032545406041","Duingen, Flecken",null],["032545406043","Gronau (Leine), Stadt",null],["032550008008","Delligsen, Flecken",null],["032550023023","Holzminden, Stadt",null],["032555401002","Bevern, Flecken",null],["032555401015","Golmbach",null],["032555401021","Holenberg",null],["032555401030","Negenborn",null],["032555403004","Boffzen",null],["032555403009","Derental",null],["032555403014","Fürstenberg",null],["032555403026","Lauenförde, Flecken",null],["032555408003","Bodenwerder, Münchhausenstadt",null],["032555408005","Brevörde",null],["032555408016","Halle",null],["032555408017","Hehlen",null],["032555408019","Heinsen",null],["032555408020","Heyen",null],["032555408025","Kirchbrak",null],["032555408031","Ottenstein, Flecken",null],["032555408032","Pegestorf",null],["032555408033","Polle, Flecken",null],["032555408035","Vahlbruch",null],["032555409001","Arholzen",null],["032555409007","Deensen",null],["032555409010","Dielmissen",null],["032555409012","Eimen",null],["032555409013","Eschershausen, Stadt",null],["032555409018","Heinade",null],["032555409022","Holzen",null],["032555409027","Lenne",null],["032555409028","Lüerdissen",null],["032555409034","Stadtoldendorf, Stadt",null],["032555409036","Wangelnstedt",null],["032559501501","Boffzen, gemfr. Gebiet",null],["032559502502","Eimen, gemfr. Gebiet",null],["032559503503","Eschershausen, gemfr. Gebiet",null],["032559504504","Grünenplan, gemfr. Gebiet",null],["032559505505","Holzminden, gemfr. Gebiet",null],["032559506506","Merxhausen, gemfr. Gebiet",null],["032559508508","Wenzen, gemfr. Gebiet",null],["032560022022","Nienburg (Weser), Stadt",null],["032560025025","Rehburg-Loccum, Stadt",null],["032560030030","Steyerberg, Flecken",null],["032565402005","Drakenburg, Flecken",null],["032565402011","Haßbergen",null],["032565402012","Heemsen",null],["032565402027","Rohrsen",null],["032565405002","Binnen",null],["032565405019","Liebenau, Flecken",null],["032565405023","Pennigsehl",null],["032565406001","Balge",null],["032565406021","Marklohe",null],["032565406036","Wietzen",null],["032565407020","Linsburg",null],["032565407026","Rodewald",null],["032565407029","Steimbke",null],["032565407031","Stöckse",null],["032565408004","Diepenau, Flecken",null],["032565408024","Raddestorf",null],["032565408033","Uchte, Flecken",null],["032565408034","Warmsen",null],["032565409003","Bücken, Flecken",null],["032565409007","Eystrup",null],["032565409008","Gandesbergen",null],["032565409009","Hämelhausen",null],["032565409010","Hassel (Weser)",null],["032565409013","Hilgermissen",null],["032565409014","Hoya, Stadt",null],["032565409015","Hoyerhagen",null],["032565409028","Schweringen",null],["032565409035","Warpe",null],["032565410006","Estorf",null],["032565410016","Husum",null],["032565410017","Landesbergen",null],["032565410018","Leese",null],["032565410032","Stolzenau",null],["032570003003","Auetal",null],["032570009009","Bückeburg, Stadt",null],["032570028028","Obernkirchen, Stadt",null],["032570031031","Rinteln, Stadt",null],["032570035035","Stadthagen, Stadt",null],["032575401001","Ahnsen",null],["032575401005","Bad Eilsen",null],["032575401008","Buchholz",null],["032575401012","Heeßen",null],["032575401022","Luhden",null],["032575402007","Beckedorf",null],["032575402015","Heuerßen",null],["032575402020","Lindhorst",null],["032575402021","Lüdersfeld",null],["032575403006","Bad Nenndorf, Stadt",null],["032575403011","Haste",null],["032575403016","Hohnhorst",null],["032575403036","Suthfeld",null],["032575404019","Lauenhagen",null],["032575404023","Meerbeck",null],["032575404025","Niedernwöhren",null],["032575404027","Nordsehl",null],["032575404030","Pollhagen",null],["032575404037","Wiedensahl, Flecken",null],["032575405013","Helpsen",null],["032575405014","Hespe",null],["032575405026","Nienstädt",null],["032575405034","Seggebruch",null],["032575406002","Apelern",null],["032575406017","Hülsede",null],["032575406018","Lauenau, Flecken",null],["032575406024","Messenkamp",null],["032575406029","Pohle",null],["032575406032","Rodenberg, Stadt",null],["032575407004","Auhagen",null],["032575407010","Hagenburg, Flecken",null],["032575407033","Sachsenhagen, Stadt",null],["032575407038","Wölpinghausen",null],["033510004004","Bergen, Stadt",null],["033510006006","Celle, Stadt",null],["033510010010","Faßberg",null],["033510012012","Hambühren",null],["033510023023","Wietze",null],["033510024024","Winsen (Aller)",null],["033510025025","Eschede",null],["033510026026","Südheide",null],["033515402005","Bröckel",null],["033515402007","Eicklingen",null],["033515402017","Langlingen",null],["033515402022","Wienhausen, Klostergemeinde",null],["033515403002","Ahnsbeck",null],["033515403003","Beedenbostel",null],["033515403008","Eldingen",null],["033515403015","Hohne",null],["033515403016","Lachendorf",null],["033515404001","Adelheidsdorf",null],["033515404018","Nienhagen",null],["033515404021","Wathlingen",null],["033519501501","Lohheide, gemfr. Bezirk",null],["033520011011","Cuxhaven, Stadt",null],["033520032032","Loxstedt",null],["033520050050","Schiffdorf",null],["033520059059","Beverstedt",null],["033520060060","Hagen im Bremischen",null],["033520061061","Wurster Nordseeküste",null],["033520062062","Geestland, Stadt",null],["033525404002","Armstorf",null],["033525404024","Hollnseth",null],["033525404029","Lamstedt",null],["033525404036","Mittelstenahe",null],["033525404052","Stinstedt",null],["033525407020","Hechthausen",null],["033525407022","Hemmoor, Stadt",null],["033525407044","Osten",null],["033525411004","Belum",null],["033525411008","Bülkau",null],["033525411025","Ihlienworth",null],["033525411038","Neuenkirchen",null],["033525411039","Neuhaus (Oste), Flecken",null],["033525411041","Nordleda",null],["033525411042","Oberndorf",null],["033525411043","Odisheim",null],["033525411045","Osterbruch",null],["033525411046","Otterndorf, Stadt",null],["033525411051","Steinau",null],["033525411055","Wanna",null],["033525411056","Wingst",null],["033525411063","Cadenberge",null],["033530005005","Buchholz in der Nordheide, Stadt",null],["033530026026","Neu Wulmstorf",null],["033530029029","Rosengarten",null],["033530031031","Seevetal",null],["033530032032","Stelle",null],["033530040040","Winsen (Luhe), Stadt",null],["033535401007","Drage",null],["033535401023","Marschacht",null],["033535401033","Tespe",null],["033535402002","Asendorf",null],["033535402004","Brackel",null],["033535402009","Egestorf",null],["033535402016","Hanstedt",null],["033535402024","Marxen",null],["033535402036","Undeloh",null],["033535403001","Appel",null],["033535403008","Drestedt",null],["033535403014","Halvesbostel",null],["033535403019","Hollenstedt",null],["033535403025","Moisburg",null],["033535403028","Regesbostel",null],["033535403039","Wenzendorf",null],["033535404003","Bendestorf",null],["033535404017","Harmstorf",null],["033535404020","Jesteburg",null],["033535405010","Eyendorf",null],["033535405011","Garlstorf",null],["033535405012","Garstedt",null],["033535405013","Gödenstorf",null],["033535405030","Salzhausen",null],["033535405034","Toppenstedt",null],["033535405037","Vierhöfen",null],["033535405042","Wulfsen",null],["033535406006","Dohren",null],["033535406015","Handeloh",null],["033535406018","Heidenau",null],["033535406021","Kakenstorf",null],["033535406022","Königsmoor",null],["033535406027","Otter",null],["033535406035","Tostedt",null],["033535406038","Welle",null],["033535406041","Wistedt",null],["033545403005","Gartow, Flecken",null],["033545403007","Gorleben",null],["033545403010","Höhbeck",null],["033545403020","Prezelle",null],["033545403021","Schnackenburg, Stadt",null],["033545406003","Damnatz",null],["033545406004","Dannenberg (Elbe), Stadt",null],["033545406006","Göhrde",null],["033545406008","Gusborn",null],["033545406009","Hitzacker (Elbe), Stadt",null],["033545406011","Jameln",null],["033545406012","Karwitz",null],["033545406014","Langendorf",null],["033545406019","Neu Darchau",null],["033545406027","Zernien",null],["033545407001","Bergen an der Dumme, Flecken",null],["033545407002","Clenze, Flecken",null],["033545407013","Küsten",null],["033545407015","Lemgow",null],["033545407016","Luckau (Wendland)",null],["033545407017","Lübbow",null],["033545407018","Lüchow (Wendland), Stadt",null],["033545407022","Schnega",null],["033545407023","Trebel",null],["033545407024","Waddeweitz",null],["033545407025","Woltersdorf",null],["033545407026","Wustrow (Wendland), Stadt",null],["033549501501","Gartow, gemfr. Gebiet",null],["033549502502","Göhrde, gemfr. Gebiet",null],["033550001001","Adendorf",null],["033550009009","Bleckede, Stadt",null],["033550022022","Lüneburg, Hansestadt",null],["033550049049","Amt Neuhaus",null],["033555401002","Amelinghausen",null],["033555401008","Betzendorf",null],["033555401027","Oldendorf (Luhe)",null],["033555401029","Rehlingen",null],["033555401034","Soderstorf",null],["033555402004","Bardowick, Flecken",null],["033555402007","Barum",null],["033555402017","Handorf",null],["033555402023","Mechtersen",null],["033555402028","Radbruch",null],["033555402039","Vögelsen",null],["033555402042","Wittorf",null],["033555403010","Boitze",null],["033555403012","Dahlem",null],["033555403013","Dahlenburg, Flecken",null],["033555403025","Nahrendorf",null],["033555403037","Tosterglope",null],["033555404020","Kirchgellersen",null],["033555404031","Reppenstedt",null],["033555404035","Südergellersen",null],["033555404041","Westergellersen",null],["033555405006","Barnstedt",null],["033555405014","Deutsch Evern",null],["033555405016","Embsen",null],["033555405024","Melbeck",null],["033555406005","Barendorf",null],["033555406026","Neetze",null],["033555406030","Reinstorf",null],["033555406036","Thomasburg",null],["033555406038","Vastorf",null],["033555406040","Wendisch Evern",null],["033555407003","Artlenburg, Flecken",null],["033555407011","Brietlingen",null],["033555407015","Echem",null],["033555407018","Hittbergen",null],["033555407019","Hohnstorf (Elbe)",null],["033555407021","Lüdersburg",null],["033555407032","Rullstorf",null],["033555407033","Scharnebeck",null],["033560002002","Grasberg",null],["033560005005","Lilienthal",null],["033560007007","Osterholz-Scharmbeck, Stadt",null],["033560008008","Ritterhude",null],["033560009009","Schwanewede",null],["033560011011","Worpswede",null],["033565401001","Axstedt",null],["033565401003","Hambergen",null],["033565401004","Holste",null],["033565401006","Lübberstedt",null],["033565401010","Vollersode",null],["033570008008","Bremervörde, Stadt",null],["033570016016","Gnarrenburg",null],["033570039039","Rotenburg (Wümme), Stadt",null],["033570041041","Scheeßel",null],["033570051051","Visselhövede, Stadt",null],["033575401006","Bothel",null],["033575401009","Brockel",null],["033575401024","Hemsbünde",null],["033575401025","Hemslingen",null],["033575401031","Kirchwalsede",null],["033575401054","Westerwalsede",null],["033575402015","Fintel",null],["033575402023","Helvesiek",null],["033575402033","Lauenbrück",null],["033575402046","Stemmen",null],["033575402049","Vahlde",null],["033575403002","Alfstedt",null],["033575403004","Basdahl",null],["033575403012","Ebersdorf",null],["033575403027","Hipstedt",null],["033575403035","Oerel",null],["033575404003","Anderlingen",null],["033575404011","Deinstedt",null],["033575404014","Farven",null],["033575404036","Ostereistedt",null],["033575404038","Rhade",null],["033575404040","Sandbostel",null],["033575404042","Seedorf",null],["033575404043","Selsingen",null],["033575405017","Groß Meckelsen",null],["033575405019","Hamersen",null],["033575405029","Kalbe",null],["033575405032","Klein Meckelsen",null],["033575405034","Lengenbostel",null],["033575405044","Sittensen",null],["033575405048","Tiste",null],["033575405050","Vierden",null],["033575405056","Wohnste",null],["033575406001","Ahausen",null],["033575406005","Bötersen",null],["033575406020","Hassendorf",null],["033575406022","Hellwege",null],["033575406028","Horstedt",null],["033575406037","Reeßum",null],["033575406045","Sottrum",null],["033575407007","Breddorf",null],["033575407010","Bülstedt",null],["033575407026","Hepstedt",null],["033575407030","Kirchtimke",null],["033575407047","Tarmstedt",null],["033575407052","Vorwerk",null],["033575407053","Westertimke",null],["033575407055","Wilstedt",null],["033575408013","Elsdorf",null],["033575408018","Gyhum",null],["033575408021","Heeslingen",null],["033575408057","Zeven, Stadt",null],["033580002002","Bispingen",null],["033580008008","Bad Fallingbostel, Stadt",null],["033580016016","Munster, Stadt",null],["033580017017","Neuenkirchen",null],["033580019019","Schneverdingen, Stadt",null],["033580021021","Soltau, Stadt",null],["033580023023","Wietzendorf",null],["033580024024","Walsrode, Stadt",null],["033585401001","Ahlden (Aller), Flecken",null],["033585401006","Eickeloh",null],["033585401011","Grethem",null],["033585401012","Hademstorf",null],["033585401014","Hodenhagen",null],["033585402003","Böhme",null],["033585402009","Frankenfeld",null],["033585402013","Häuslingen",null],["033585402018","Rethem (Aller), Stadt",null],["033585403005","Buchholz (Aller)",null],["033585403007","Essel",null],["033585403010","Gilten",null],["033585403015","Lindwedel",null],["033585403020","Schwarmstedt",null],["033589501501","Osterheide, gemfr. Bezirk",null],["033590010010","Buxtehude, Hansestadt",null],["033590013013","Drochtersen",null],["033590028028","Jork",null],["033590038038","Stade, Hansestadt",null],["033595401003","Apensen",null],["033595401006","Beckdorf",null],["033595401037","Sauensiek",null],["033595402011","Deinste",null],["033595402017","Fredenbeck",null],["033595402031","Kutenholz",null],["033595403002","Ahlerstedt",null],["033595403005","Bargstedt",null],["033595403008","Brest",null],["033595403023","Harsefeld, Flecken",null],["033595405001","Agathenburg",null],["033595405007","Bliedersdorf",null],["033595405012","Dollern",null],["033595405027","Horneburg, Flecken",null],["033595405034","Nottensdorf",null],["033595406020","Grünendeich",null],["033595406021","Guderhandviertel",null],["033595406026","Hollern-Twielenfleth",null],["033595406032","Mittelnkirchen",null],["033595406033","Neuenkirchen",null],["033595406039","Steinkirchen",null],["033595407004","Balje",null],["033595407018","Freiburg (Elbe), Flecken",null],["033595407030","Krummendeich",null],["033595407035","Oederquart",null],["033595407040","Wischhafen",null],["033595409009","Burweg",null],["033595409014","Düdenbüttel",null],["033595409015","Engelschoff",null],["033595409016","Estorf",null],["033595409019","Großenwörden",null],["033595409022","Hammah",null],["033595409024","Heinbockel",null],["033595409025","Himmelpforten",null],["033595409029","Kranenburg",null],["033595409036","Oldendorf",null],["033600004004","Bienenbüttel",null],["033600025025","Uelzen, Hansestadt",null],["033605404015","Oetzen",null],["033605404016","Rätzlingen",null],["033605404018","Rosche",null],["033605404022","Stoetze",null],["033605404024","Suhlendorf",null],["033605405007","Eimke",null],["033605405009","Gerdau",null],["033605405023","Suderburg",null],["033605407001","Altenmedingen",null],["033605407002","Bad Bevensen, Stadt",null],["033605407003","Barum",null],["033605407006","Ebstorf,Klosterflecken",null],["033605407008","Emmendorf",null],["033605407010","Hanstedt",null],["033605407011","Himbergen",null],["033605407012","Jelmstorf",null],["033605407014","Natendorf",null],["033605407017","Römstedt",null],["033605407019","Schwienau",null],["033605407026","Weste",null],["033605407029","Wriedel",null],["033605408005","Bad Bodenteich, Flecken",null],["033605408013","Lüder",null],["033605408020","Soltendieck",null],["033605408030","Wrestedt",null],["033610001001","Achim, Stadt",null],["033610003003","Dörverden",null],["033610005005","Kirchlinteln",null],["033610006006","Langwedel, Flecken",null],["033610008008","Ottersberg, Flecken",null],["033610009009","Oyten",null],["033610012012","Verden (Aller), Stadt",null],["033615401002","Blender",null],["033615401004","Emtinghausen",null],["033615401010","Riede",null],["033615401013","Thedinghausen",null],["034010000000","Delmenhorst, Stadt",null],["034020000000","Emden, Stadt",null],["034030000000","Oldenburg (Oldenburg), Stadt",null],["034040000000","Osnabrück, Stadt",null],["034050000000","Wilhelmshaven, Stadt",null],["034510001001","Apen",null],["034510002002","Bad Zwischenahn",null],["034510004004","Edewecht",null],["034510005005","Rastede",null],["034510007007","Westerstede, Stadt",null],["034510008008","Wiefelstede",null],["034520001001","Aurich, Stadt",null],["034520002002","Baltrum",null],["034520006006","Großefehn",null],["034520007007","Großheide",null],["034520011011","Hinte",null],["034520012012","Ihlow",null],["034520013013","Juist, Inselgemeinde",null],["034520014014","Krummhörn",null],["034520019019","Norden, Stadt",null],["034520020020","Norderney, Stadt",null],["034520023023","Südbrookmerland",null],["034520025025","Wiesmoor, Stadt",null],["034520027027","Dornum",null],["034525401015","Leezdorf",null],["034525401017","Marienhafe, Flecken",null],["034525401021","Osteel",null],["034525401022","Rechtsupweg",null],["034525401024","Upgant-Schott",null],["034525401026","Wirdum",null],["034525403003","Berumbur",null],["034525403008","Hage, Flecken",null],["034525403009","Hagermarsch",null],["034525403010","Halbemond",null],["034525403016","Lütetsburg",null],["034529501501","Nordseeinsel Memmert, gemfr. Gebiet",null],["034530001001","Barßel",null],["034530002002","Bösel",null],["034530003003","Cappeln (Oldenburg)",null],["034530004004","Cloppenburg, Stadt",null],["034530005005","Emstek",null],["034530006006","Essen (Oldenburg)",null],["034530007007","Friesoythe, Stadt",null],["034530008008","Garrel",null],["034530009009","Lastrup",null],["034530010010","Lindern (Oldenburg)",null],["034530011011","Löningen, Stadt",null],["034530012012","Molbergen",null],["034530013013","Saterland",null],["034540010010","Emsbüren",null],["034540014014","Geeste",null],["034540018018","Haren (Ems), Stadt",null],["034540019019","Haselünne, Stadt",null],["034540032032","Lingen (Ems), Stadt",null],["034540035035","Meppen, Stadt",null],["034540041041","Papenburg, Stadt",null],["034540044044","Rhede (Ems)",null],["034540045045","Salzbergen",null],["034540054054","Twist",null],["034545401007","Dersum",null],["034545401008","Dörpen",null],["034545401020","Heede",null],["034545401025","Kluse",null],["034545401030","Lehe",null],["034545401037","Neubörger",null],["034545401038","Neulehe",null],["034545401056","Walchum",null],["034545401060","Wippingen",null],["034545402001","Andervenne",null],["034545402003","Beesten",null],["034545402012","Freren, Stadt",null],["034545402036","Messingen",null],["034545402053","Thuine",null],["034545403009","Dohren",null],["034545403021","Herzlake",null],["034545403026","Lähden",null],["034545404013","Fresenburg",null],["034545404029","Lathen",null],["034545404039","Niederlangen",null],["034545404040","Oberlangen",null],["034545404043","Renkenberge",null],["034545404052","Sustrum",null],["034545405002","Bawinkel",null],["034545405015","Gersten",null],["034545405017","Handrup",null],["034545405028","Langen",null],["034545405031","Lengerich",null],["034545405059","Wettrup",null],["034545406004","Bockhorst",null],["034545406006","Breddenberg",null],["034545406011","Esterwegen",null],["034545406022","Hilkenbrook",null],["034545406051","Surwold",null],["034545407005","Börger",null],["034545407016","Groß Berßen",null],["034545407023","Hüven",null],["034545407024","Klein Berßen",null],["034545407047","Sögel",null],["034545407048","Spahnharrenstätte",null],["034545407050","Stavern",null],["034545407058","Werpeloh",null],["034545408034","Lünne",null],["034545408046","Schapen",null],["034545408049","Spelle",null],["034545409027","Lahn",null],["034545409033","Lorup",null],["034545409042","Rastdorf",null],["034545409055","Vrees",null],["034545409057","Werlte, Stadt",null],["034550007007","Jever, Stadt",null],["034550014014","Sande",null],["034550015015","Schortens, Stadt",null],["034550020020","Wangerland",null],["034550021021","Wangerooge, Nordseebad",null],["034550025025","Bockhorn",null],["034550026026","Varel, Stadt",null],["034550027027","Zetel",null],["034560001001","Bad Bentheim, Stadt",null],["034560015015","Nordhorn, Stadt",null],["034560025025","Wietmarschen",null],["034565401002","Emlichheim",null],["034565401009","Hoogstede",null],["034565401012","Laar",null],["034565401019","Ringe",null],["034565402004","Esche",null],["034565402005","Georgsdorf",null],["034565402013","Lage",null],["034565402014","Neuenhaus, Stadt",null],["034565402017","Osterwald",null],["034565403003","Engden",null],["034565403010","Isterberg",null],["034565403016","Ohne",null],["034565403018","Quendorf",null],["034565403020","Samern",null],["034565403027","Schüttorf, Stadt",null],["034565404006","Getelo",null],["034565404007","Gölenkamp",null],["034565404008","Halle",null],["034565404011","Itterbeck",null],["034565404023","Uelsen",null],["034565404024","Wielen",null],["034565404026","Wilsum",null],["034570002002","Borkum, Stadt",null],["034570012012","Jemgum",null],["034570013013","Leer (Ostfriesland), Stadt",null],["034570014014","Moormerland",null],["034570017017","Ostrhauderfehn",null],["034570018018","Rhauderfehn",null],["034570020020","Uplengen",null],["034570021021","Weener, Stadt",null],["034570022022","Westoverledingen",null],["034570024024","Bunde",null],["034575402003","Brinkum",null],["034575402009","Firrel",null],["034575402010","Hesel",null],["034575402011","Holtland",null],["034575402015","Neukamperfehn",null],["034575402019","Schwerinsdorf",null],["034575403006","Detern, Flecken",null],["034575403008","Filsum",null],["034575403016","Nortmoor",null],["034579501501","Insel Lütje Hörn, gemfr. Gebiet",null],["034580003003","Dötlingen",null],["034580005005","Ganderkesee",null],["034580007007","Großenkneten",null],["034580009009","Hatten",null],["034580010010","Hude (Oldb)",null],["034580013013","Wardenburg",null],["034580014014","Wildeshausen, Stadt",null],["034585401001","Beckeln",null],["034585401002","Colnrade",null],["034585401004","Dünsen",null],["034585401006","Groß Ippener",null],["034585401008","Harpstedt, Flecken",null],["034585401011","Kirchseelte",null],["034585401012","Prinzhöfte",null],["034585401015","Winkelsett",null],["034590003003","Bad Essen",null],["034590004004","Bad Iburg, Stadt",null],["034590005005","Bad Laer",null],["034590006006","Bad Rothenfelde",null],["034590008008","Belm",null],["034590012012","Bissendorf",null],["034590013013","Bohmte",null],["034590014014","Bramsche, Stadt",null],["034590015015","Dissen am Teutoburger Wald, Stadt",null],["034590019019","Georgsmarienhütte, Stadt",null],["034590020020","Hagen am Teutoburger Wald",null],["034590021021","Hasbergen",null],["034590022022","Hilter am Teutoburger Wald",null],["034590024024","Melle, Stadt",null],["034590029029","Ostercappeln",null],["034590033033","Wallenhorst",null],["034590034034","Glandorf",null],["034595401007","Badbergen",null],["034595401025","Menslage",null],["034595401028","Nortrup",null],["034595401030","Quakenbrück, Stadt",null],["034595402001","Alfhausen",null],["034595402002","Ankum",null],["034595402010","Bersenbrück, Stadt",null],["034595402016","Eggermühlen",null],["034595402018","Gehrde",null],["034595402023","Kettenkamp",null],["034595402031","Rieste",null],["034595403009","Berge",null],["034595403011","Bippen",null],["034595403017","Fürstenau, Stadt",null],["034595404026","Merzen",null],["034595404027","Neuenkirchen",null],["034595404032","Voltlage",null],["034600001001","Bakum",null],["034600002002","Damme, Stadt",null],["034600003003","Dinklage, Stadt",null],["034600004004","Goldenstedt",null],["034600005005","Holdorf",null],["034600006006","Lohne (Oldenburg), Stadt",null],["034600007007","Neuenkirchen-Vörden",null],["034600008008","Steinfeld (Oldenburg)",null],["034600009009","Vechta, Stadt",null],["034600010010","Visbek",null],["034610001001","Berne",null],["034610002002","Brake (Unterweser), Stadt",null],["034610003003","Butjadingen",null],["034610004004","Elsfleth, Stadt",null],["034610005005","Jade",null],["034610006006","Lemwerder",null],["034610007007","Nordenham, Stadt",null],["034610008008","Ovelgönne",null],["034610009009","Stadland",null],["034620005005","Friedeburg",null],["034620007007","Langeoog",null],["034620014014","Spiekeroog",null],["034620019019","Wittmund, Stadt",null],["034625401002","Dunum",null],["034625401003","Esens, Stadt",null],["034625401006","Holtgast",null],["034625401008","Moorweg",null],["034625401010","Neuharlingersiel",null],["034625401015","Stedesdorf",null],["034625401017","Werdum",null],["034625402001","Blomberg",null],["034625402004","Eversmeer",null],["034625402009","Nenndorf",null],["034625402011","Neuschoo",null],["034625402012","Ochtersum",null],["034625402013","Schweindorf",null],["034625402016","Utarp",null],["034625402018","Westerholt",null],["039019999999","Nds-Küstengewässer(Gemarkung Nordsee)",null],["040110000000","Bremen, Stadt",null],["040110111111","Altstadt","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110112112","Bahnhofsvorstadt","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110113113","Ostertor","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110122122","Industriehäfen","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110123123","Stadtbremisches Überseehafengebiet Bremerhaven","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110124124","Neustädter Hafen","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110125125","Hohentorshafen","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110211211","Alte Neustadt","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110212212","Hohentor","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110213213","Neustadt","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110214214","Südervorstadt","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110215215","Gartenstadt Süd","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110216216","Buntentor","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110217217","Neuenland","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110218218","Huckelriede","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110231231","Habenhausen","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110232232","Arsten","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110233233","Kattenturm","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110234234","Kattenesch","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110241241","Mittelshuchting","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110242242","Sodenmatt","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110243243","Kirchhuchting","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110244244","Grolland","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110251251","Woltmershausen","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110252252","Rablinghausen","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110261261","Seehausen","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110271271","Strom","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110311311","Steintor","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110312312","Fesenfeld","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110313313","Peterswerder","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110314314","Hulsberg","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110321321","Neu-Schwachhausen","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110322322","Bürgerpark","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110323323","Barkhof","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110324324","Riensberg","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110325325","Radio Bremen","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110326326","Schwachhausen","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110327327","Gete","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110331331","Gartenstadt Vahr","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110332332","Neue Vahr Nord","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110334334","Neue Vahr Südwest","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110335335","Neue Vahr Südost","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110341341","Horn","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110342342","Lehe","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110343343","Lehesterdeich","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110351351","Borgfeld","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110361361","Oberneuland","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110371371","Ellener Feld","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110372372","Ellenerbrok-Schevemoor","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110373373","Tenever","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110374374","Osterholz","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110375375","Blockdiek","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110381381","Sebaldsbrück","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110382382","Hastedt","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110383383","Hemelingen","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110384384","Arbergen","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110385385","Mahndorf","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110411411","Blockland","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110421421","Regensburger Straße","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110422422","Findorff-Bürgerweide","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110423423","Weidedamm","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110424424","In den Hufen","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110431431","Utbremen","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110432432","Steffensweg","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110433433","Westend","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110434434","Walle","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110435435","Osterfeuerberg","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110436436","Hohweg","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110437437","Überseestadt","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110441441","Lindenhof","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110442442","Gröpelingen","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110443443","Ohlenhof","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110444444","In den Wischen","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110445445","Oslebshausen","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110511511","Burg-Grambke","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110512512","Werderland","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110513513","Burgdamm","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110514514","Lesum","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110515515","St. Magnus","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110521521","Vegesack","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110522522","Grohn","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110523523","Schönebeck","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110524524","Aumund-Hammersbeck","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110525525","Fähr-Lobbendorf","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110531531","Blumenthal","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110532532","Rönnebeck","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110533533","Lüssum-Bockhorn","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110534534","Farge","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110535535","Rekum","Stadt-/Ortsteil bzw. Stadtbezirk"],["040120000000","Bremerhaven, Stadt",null],["051110000000","Düsseldorf, Stadt",null],["051120000000","Duisburg, Stadt",null],["051130000000","Essen, Stadt",null],["051140000000","Krefeld, Stadt",null],["051160000000","Mönchengladbach, Stadt",null],["051170000000","Mülheim an der Ruhr, Stadt",null],["051190000000","Oberhausen, Stadt",null],["051200000000","Remscheid, Stadt",null],["051220000000","Solingen, Klingenstadt",null],["051240000000","Wuppertal, Stadt",null],["051540004004","Bedburg-Hau",null],["051540008008","Emmerich am Rhein, Stadt",null],["051540012012","Geldern, Stadt",null],["051540016016","Goch, Stadt",null],["051540020020","Issum",null],["051540024024","Kalkar, Stadt",null],["051540028028","Kerken",null],["051540032032","Kevelaer, Stadt",null],["051540036036","Kleve, Stadt",null],["051540040040","Kranenburg",null],["051540044044","Rees, Stadt",null],["051540048048","Rheurdt",null],["051540052052","Straelen, Stadt",null],["051540056056","Uedem",null],["051540060060","Wachtendonk",null],["051540064064","Weeze",null],["051580004004","Erkrath, Fundort des Neanderthalers, Stadt",null],["051580008008","Haan, Stadt",null],["051580012012","Heiligenhaus, Stadt",null],["051580016016","Hilden, Stadt",null],["051580020020","Langenfeld (Rheinland), Stadt",null],["051580024024","Mettmann, Stadt",null],["051580026026","Monheim am Rhein, Stadt",null],["051580028028","Ratingen, Stadt",null],["051580032032","Velbert, Stadt",null],["051580036036","Wülfrath, Stadt",null],["051620004004","Dormagen, Stadt",null],["051620008008","Grevenbroich, Stadt",null],["051620012012","Jüchen, Stadt",null],["051620016016","Kaarst, Stadt",null],["051620020020","Korschenbroich, Stadt",null],["051620022022","Meerbusch, Stadt",null],["051620024024","Neuss, Stadt",null],["051620028028","Rommerskirchen",null],["051660004004","Brüggen, Burggemeinde",null],["051660008008","Grefrath, Sport- und Freizeitgemeinde",null],["051660012012","Kempen, Stadt",null],["051660016016","Nettetal, Stadt",null],["051660020020","Niederkrüchten",null],["051660024024","Schwalmtal",null],["051660028028","Tönisvorst, Stadt",null],["051660032032","Viersen, Stadt",null],["051660036036","Willich, Stadt",null],["051700004004","Alpen",null],["051700008008","Dinslaken, Stadt",null],["051700012012","Hamminkeln, Stadt",null],["051700016016","Hünxe",null],["051700020020","Kamp-Lintfort, Stadt",null],["051700024024","Moers, Stadt",null],["051700028028","Neukirchen-Vluyn, Stadt",null],["051700032032","Rheinberg, Stadt",null],["051700036036","Schermbeck",null],["051700040040","Sonsbeck",null],["051700044044","Voerde (Niederrhein), Stadt",null],["051700048048","Wesel, Stadt",null],["051700052052","Xanten, Stadt",null],["053140000000","Bonn, Stadt",null],["053150000000","Köln, Stadt",null],["053160000000","Leverkusen, Stadt",null],["053340002002","Aachen, Stadt",null],["053340004004","Alsdorf, Stadt",null],["053340008008","Baesweiler, Stadt",null],["053340012012","Eschweiler, Stadt",null],["053340016016","Herzogenrath, Stadt",null],["053340020020","Monschau, Stadt",null],["053340024024","Roetgen, Tor zur Eifel",null],["053340028028","Simmerath",null],["053340032032","Stolberg (Rhld.), Kupferstadt",null],["053340036036","Würselen, Stadt",null],["053580004004","Aldenhoven",null],["053580008008","Düren, Stadt",null],["053580012012","Heimbach, Stadt",null],["053580016016","Hürtgenwald",null],["053580020020","Inden",null],["053580024024","Jülich, Stadt",null],["053580028028","Kreuzau",null],["053580032032","Langerwehe",null],["053580036036","Linnich, Stadt",null],["053580040040","Merzenich",null],["053580044044","Nideggen, Stadt",null],["053580048048","Niederzier",null],["053580052052","Nörvenich",null],["053580056056","Titz",null],["053580060060","Vettweiß",null],["053620004004","Bedburg, Stadt",null],["053620008008","Bergheim, Stadt",null],["053620012012","Brühl, Stadt",null],["053620016016","Elsdorf, Stadt",null],["053620020020","Erftstadt, Stadt",null],["053620024024","Frechen, Stadt",null],["053620028028","Hürth, Stadt",null],["053620032032","Kerpen, Kolpingstadt",null],["053620036036","Pulheim, Stadt",null],["053620040040","Wesseling, Stadt",null],["053660004004","Bad Münstereifel, Stadt",null],["053660008008","Blankenheim",null],["053660012012","Dahlem",null],["053660016016","Euskirchen, Stadt",null],["053660020020","Hellenthal",null],["053660024024","Kall",null],["053660028028","Mechernich, Stadt",null],["053660032032","Nettersheim",null],["053660036036","Schleiden, Stadt",null],["053660040040","Weilerswist",null],["053660044044","Zülpich, Stadt",null],["053700004004","Erkelenz, Stadt",null],["053700008008","Gangelt",null],["053700012012","Geilenkirchen, Stadt",null],["053700016016","Heinsberg, Stadt",null],["053700020020","Hückelhoven, Stadt",null],["053700024024","Selfkant",null],["053700028028","Übach-Palenberg, Stadt",null],["053700032032","Waldfeucht",null],["053700036036","Wassenberg, Stadt",null],["053700040040","Wegberg, Stadt",null],["053740004004","Bergneustadt, Stadt",null],["053740008008","Engelskirchen",null],["053740012012","Gummersbach, Stadt",null],["053740016016","Hückeswagen, Schloss-Stadt",null],["053740020020","Lindlar",null],["053740024024","Marienheide",null],["053740028028","Morsbach",null],["053740032032","Nümbrecht",null],["053740036036","Radevormwald, Stadt auf der Höhe",null],["053740040040","Reichshof",null],["053740044044","Waldbröl, Stadt",null],["053740048048","Wiehl, Stadt",null],["053740052052","Wipperfürth, Hansestadt",null],["053780004004","Bergisch Gladbach, Stadt",null],["053780008008","Burscheid, Stadt",null],["053780012012","Kürten",null],["053780016016","Leichlingen (Rheinland), Blütenstadt",null],["053780020020","Odenthal",null],["053780024024","Overath, Stadt",null],["053780028028","Rösrath, Stadt",null],["053780032032","Wermelskirchen, Stadt",null],["053820004004","Alfter",null],["053820008008","Bad Honnef, Stadt",null],["053820012012","Bornheim, Stadt",null],["053820016016","Eitorf",null],["053820020020","Hennef (Sieg), Stadt",null],["053820024024","Königswinter, Stadt",null],["053820028028","Lohmar, Stadt",null],["053820032032","Meckenheim, Stadt",null],["053820036036","Much",null],["053820040040","Neunkirchen-Seelscheid",null],["053820044044","Niederkassel, Stadt",null],["053820048048","Rheinbach, Stadt",null],["053820052052","Ruppichteroth",null],["053820056056","Sankt Augustin, Stadt",null],["053820060060","Siegburg, Stadt",null],["053820064064","Swisttal",null],["053820068068","Troisdorf, Stadt",null],["053820072072","Wachtberg",null],["053820076076","Windeck",null],["055120000000","Bottrop, Stadt",null],["055130000000","Gelsenkirchen, Stadt",null],["055150000000","Münster, Stadt",null],["055540004004","Ahaus, Stadt",null],["055540008008","Bocholt, Stadt",null],["055540012012","Borken, Stadt",null],["055540016016","Gescher, Glockenstadt",null],["055540020020","Gronau (Westf.), Stadt",null],["055540024024","Heek",null],["055540028028","Heiden",null],["055540032032","Isselburg, Stadt",null],["055540036036","Legden",null],["055540040040","Raesfeld",null],["055540044044","Reken",null],["055540048048","Rhede, Stadt",null],["055540052052","Schöppingen",null],["055540056056","Stadtlohn, Stadt",null],["055540060060","Südlohn",null],["055540064064","Velen, Stadt",null],["055540068068","Vreden, Stadt",null],["055580004004","Ascheberg",null],["055580008008","Billerbeck, Stadt",null],["055580012012","Coesfeld, Stadt",null],["055580016016","Dülmen, Stadt",null],["055580020020","Havixbeck",null],["055580024024","Lüdinghausen, Stadt",null],["055580028028","Nordkirchen",null],["055580032032","Nottuln",null],["055580036036","Olfen, Stadt",null],["055580040040","Rosendahl",null],["055580044044","Senden",null],["055620004004","Castrop-Rauxel, Stadt",null],["055620008008","Datteln, Stadt",null],["055620012012","Dorsten, Stadt",null],["055620014014","Gladbeck, Stadt",null],["055620016016","Haltern am See, Stadt",null],["055620020020","Herten, Stadt",null],["055620024024","Marl, Stadt",null],["055620028028","Oer-Erkenschwick, Stadt",null],["055620032032","Recklinghausen, Stadt",null],["055620036036","Waltrop, Stadt",null],["055660004004","Altenberge",null],["055660008008","Emsdetten, Stadt",null],["055660012012","Greven, Stadt",null],["055660016016","Hörstel, Stadt",null],["055660020020","Hopsten",null],["055660024024","Horstmar, Stadt der Burgmannshöfe",null],["055660028028","Ibbenbüren, Stadt",null],["055660032032","Ladbergen",null],["055660036036","Laer",null],["055660040040","Lengerich, Stadt",null],["055660044044","Lienen",null],["055660048048","Lotte",null],["055660052052","Metelen",null],["055660056056","Mettingen",null],["055660060060","Neuenkirchen",null],["055660064064","Nordwalde",null],["055660068068","Ochtrup, Stadt",null],["055660072072","Recke",null],["055660076076","Rheine, Stadt",null],["055660080080","Saerbeck, NRW-Klimakommune",null],["055660084084","Steinfurt, Stadt",null],["055660088088","Tecklenburg, Stadt",null],["055660092092","Westerkappeln",null],["055660096096","Wettringen",null],["055700004004","Ahlen, Stadt",null],["055700008008","Beckum, Stadt",null],["055700012012","Beelen",null],["055700016016","Drensteinfurt, Stadt",null],["055700020020","Ennigerloh, Stadt",null],["055700024024","Everswinkel",null],["055700028028","Oelde, Stadt",null],["055700032032","Ostbevern",null],["055700036036","Sassenberg, Stadt",null],["055700040040","Sendenhorst, Stadt",null],["055700044044","Telgte, Stadt",null],["055700048048","Wadersloh",null],["055700052052","Warendorf, Stadt",null],["057110000000","Bielefeld, Stadt",null],["057540004004","Borgholzhausen, Stadt",null],["057540008008","Gütersloh, Stadt",null],["057540012012","Halle (Westf.), Stadt",null],["057540016016","Harsewinkel, Die Mähdrescherstadt",null],["057540020020","Herzebrock-Clarholz",null],["057540024024","Langenberg",null],["057540028028","Rheda-Wiedenbrück, Stadt",null],["057540032032","Rietberg, Stadt",null],["057540036036","Schloß Holte-Stukenbrock, Stadt",null],["057540040040","Steinhagen",null],["057540044044","Verl, Stadt",null],["057540048048","Versmold, Stadt",null],["057540052052","Werther (Westf.), Stadt",null],["057580004004","Bünde, Stadt",null],["057580008008","Enger, Widukindstadt",null],["057580012012","Herford, Hansestadt",null],["057580016016","Hiddenhausen",null],["057580020020","Kirchlengern",null],["057580024024","Löhne, Stadt",null],["057580028028","Rödinghausen",null],["057580032032","Spenge, Stadt",null],["057580036036","Vlotho, Stadt",null],["057620004004","Bad Driburg, Stadt",null],["057620008008","Beverungen, Stadt",null],["057620012012","Borgentreich, Orgelstadt",null],["057620016016","Brakel, Stadt",null],["057620020020","Höxter, Stadt",null],["057620024024","Marienmünster, Stadt",null],["057620028028","Nieheim, Stadt",null],["057620032032","Steinheim, Stadt",null],["057620036036","Warburg, Hansestadt",null],["057620040040","Willebadessen, Stadt",null],["057660004004","Augustdorf",null],["057660008008","Bad Salzuflen, Stadt",null],["057660012012","Barntrup, Stadt",null],["057660016016","Blomberg, Stadt",null],["057660020020","Detmold, Stadt",null],["057660024024","Dörentrup",null],["057660028028","Extertal",null],["057660032032","Horn-Bad Meinberg, Stadt",null],["057660036036","Kalletal",null],["057660040040","Lage, Stadt",null],["057660044044","Lemgo, Stadt",null],["057660048048","Leopoldshöhe",null],["057660052052","Lügde, Stadt der Osterräder",null],["057660056056","Oerlinghausen, Stadt",null],["057660060060","Schieder-Schwalenberg, Stadt",null],["057660064064","Schlangen",null],["057700004004","Bad Oeynhausen, Stadt",null],["057700008008","Espelkamp, Stadt",null],["057700012012","Hille",null],["057700016016","Hüllhorst",null],["057700020020","Lübbecke, Stadt",null],["057700024024","Minden, Stadt",null],["057700028028","Petershagen, Stadt",null],["057700032032","Porta Westfalica, Stadt",null],["057700036036","Preußisch Oldendorf, Stadt",null],["057700040040","Rahden, Stadt",null],["057700044044","Stemwede",null],["057740004004","Altenbeken",null],["057740008008","Bad Lippspringe, Stadt",null],["057740012012","Borchen",null],["057740016016","Büren, Stadt",null],["057740020020","Delbrück, Stadt",null],["057740024024","Hövelhof, Sennegemeinde",null],["057740028028","Lichtenau, Stadt",null],["057740032032","Paderborn, Stadt",null],["057740036036","Salzkotten, Stadt",null],["057740040040","Bad Wünnenberg, Stadt",null],["059110000000","Bochum, Stadt",null],["059130000000","Dortmund, Stadt",null],["059140000000","Hagen, Stadt der FernUniversität",null],["059150000000","Hamm, Stadt",null],["059160000000","Herne, Stadt",null],["059540004004","Breckerfeld, Hansestadt",null],["059540008008","Ennepetal, Stadt der Kluterthöhle",null],["059540012012","Gevelsberg, Stadt",null],["059540016016","Hattingen, Stadt",null],["059540020020","Herdecke, Stadt",null],["059540024024","Schwelm, Stadt",null],["059540028028","Sprockhövel, Stadt",null],["059540032032","Wetter (Ruhr), Stadt",null],["059540036036","Witten, Stadt",null],["059580004004","Arnsberg, Stadt",null],["059580008008","Bestwig",null],["059580012012","Brilon, Stadt",null],["059580016016","Eslohe (Sauerland)",null],["059580020020","Hallenberg, Stadt",null],["059580024024","Marsberg, Stadt",null],["059580028028","Medebach, Hansestadt",null],["059580032032","Meschede, Kreis- und Hochschulstadt",null],["059580036036","Olsberg, Stadt",null],["059580040040","Schmallenberg, Stadt",null],["059580044044","Sundern (Sauerland), Stadt",null],["059580048048","Winterberg, Stadt",null],["059620004004","Altena, Stadt",null],["059620008008","Balve, Stadt",null],["059620012012","Halver, Stadt",null],["059620016016","Hemer, Stadt",null],["059620020020","Herscheid",null],["059620024024","Iserlohn, Stadt",null],["059620028028","Kierspe, Stadt",null],["059620032032","Lüdenscheid, Stadt",null],["059620036036","Meinerzhagen, Stadt",null],["059620040040","Menden (Sauerland), Stadt",null],["059620044044","Nachrodt-Wiblingwerde",null],["059620048048","Neuenrade, Stadt",null],["059620052052","Plettenberg, Stadt",null],["059620056056","Schalksmühle",null],["059620060060","Werdohl, Stadt",null],["059660004004","Attendorn, Hansestadt",null],["059660008008","Drolshagen, Stadt",null],["059660012012","Finnentrop",null],["059660016016","Kirchhundem",null],["059660020020","Lennestadt, Stadt",null],["059660024024","Olpe, Stadt",null],["059660028028","Wenden",null],["059700004004","Bad Berleburg, Stadt",null],["059700008008","Burbach",null],["059700012012","Erndtebrück",null],["059700016016","Freudenberg, Stadt",null],["059700020020","Hilchenbach, Stadt",null],["059700024024","Kreuztal, Stadt",null],["059700028028","Bad Laasphe, Stadt",null],["059700032032","Netphen, Stadt",null],["059700036036","Neunkirchen",null],["059700040040","Siegen, Universitätsstadt",null],["059700044044","Wilnsdorf",null],["059740004004","Anröchte",null],["059740008008","Bad Sassendorf",null],["059740012012","Ense",null],["059740016016","Erwitte, Stadt",null],["059740020020","Geseke, Stadt",null],["059740024024","Lippetal",null],["059740028028","Lippstadt, Stadt",null],["059740032032","Möhnesee",null],["059740036036","Rüthen, Stadt",null],["059740040040","Soest, Stadt",null],["059740044044","Warstein, Stadt",null],["059740048048","Welver",null],["059740052052","Werl, Stadt",null],["059740056056","Wickede (Ruhr)",null],["059780004004","Bergkamen, Stadt",null],["059780008008","Bönen",null],["059780012012","Fröndenberg/Ruhr, Stadt",null],["059780016016","Holzwickede",null],["059780020020","Kamen, Stadt",null],["059780024024","Lünen, Stadt",null],["059780028028","Schwerte, Hansestadt an der Ruhr",null],["059780032032","Selm, Stadt",null],["059780036036","Unna, Stadt",null],["059780040040","Werne, Stadt",null],["064110000000","Darmstadt, Wissenschaftsstadt",null],["064120000000","Frankfurt am Main, Stadt",null],["064130000000","Offenbach am Main, Stadt",null],["064140000000","Wiesbaden, Landeshauptstadt",null],["064310001001","Abtsteinach",null],["064310002002","Bensheim, Stadt",null],["064310003003","Biblis",null],["064310004004","Birkenau",null],["064310005005","Bürstadt, Stadt",null],["064310006006","Einhausen",null],["064310007007","Fürth",null],["064310008008","Gorxheimertal",null],["064310009009","Grasellenbach",null],["064310010010","Groß-Rohrheim",null],["064310011011","Heppenheim (Bergstraße), Kreisstadt",null],["064310012012","Hirschhorn (Neckar), Stadt",null],["064310013013","Lampertheim, Stadt",null],["064310014014","Lautertal (Odenwald)",null],["064310015015","Lindenfels, Stadt",null],["064310016016","Lorsch, Karolingerstadt",null],["064310017017","Mörlenbach",null],["064310018018","Neckarsteinach, Stadt",null],["064310019019","Rimbach",null],["064310020020","Viernheim, Stadt",null],["064310021021","Wald-Michelbach",null],["064310022022","Zwingenberg, Stadt",null],["064319200200","Michelbuch, gemfr. Gebiet",null],["064320001001","Alsbach-Hähnlein",null],["064320002002","Babenhausen, Stadt",null],["064320003003","Bickenbach",null],["064320004004","Dieburg, Stadt",null],["064320005005","Eppertshausen",null],["064320006006","Erzhausen",null],["064320007007","Fischbachtal",null],["064320008008","Griesheim, Stadt",null],["064320009009","Groß-Bieberau, Stadt",null],["064320010010","Groß-Umstadt, Stadt",null],["064320011011","Groß-Zimmern",null],["064320012012","Messel",null],["064320013013","Modautal",null],["064320014014","Mühltal",null],["064320015015","Münster (Hessen)",null],["064320016016","Ober-Ramstadt, Stadt",null],["064320017017","Otzberg",null],["064320018018","Pfungstadt, Stadt",null],["064320019019","Reinheim, Stadt",null],["064320020020","Roßdorf",null],["064320021021","Schaafheim",null],["064320022022","Seeheim-Jugenheim",null],["064320023023","Weiterstadt, Stadt",null],["064330001001","Biebesheim am Rhein",null],["064330002002","Bischofsheim",null],["064330003003","Büttelborn",null],["064330004004","Gernsheim, Schöfferstadt",null],["064330005005","Ginsheim-Gustavsburg, Stadt",null],["064330006006","Groß-Gerau, Stadt",null],["064330007007","Kelsterbach, Stadt",null],["064330008008","Mörfelden-Walldorf, Stadt",null],["064330009009","Nauheim",null],["064330010010","Raunheim, Stadt",null],["064330011011","Riedstadt, Büchnerstadt",null],["064330012012","Rüsselsheim am Main, Stadt",null],["064330013013","Stockstadt am Rhein",null],["064330014014","Trebur",null],["064340001001","Bad Homburg v. d. Höhe, Stadt",null],["064340002002","Friedrichsdorf, Stadt",null],["064340003003","Glashütten",null],["064340004004","Grävenwiesbach",null],["064340005005","Königstein im Taunus, Stadt",null],["064340006006","Kronberg im Taunus, Stadt",null],["064340007007","Neu-Anspach, Stadt",null],["064340008008","Oberursel (Taunus), Stadt",null],["064340009009","Schmitten",null],["064340010010","Steinbach (Taunus), Stadt",null],["064340011011","Usingen, Stadt",null],["064340012012","Wehrheim",null],["064340013013","Weilrod",null],["064350001001","Bad Orb, Stadt",null],["064350002002","Bad Soden-Salmünster, Stadt",null],["064350003003","Biebergemünd",null],["064350004004","Birstein",null],["064350005005","Brachttal",null],["064350006006","Bruchköbel, Stadt",null],["064350007007","Erlensee, Stadt",null],["064350008008","Flörsbachtal",null],["064350009009","Freigericht",null],["064350010010","Gelnhausen, Barbarossast., Krst.",null],["064350011011","Großkrotzenburg",null],["064350012012","Gründau",null],["064350013013","Hammersbach",null],["064350014014","Hanau, Brüder-Grimm-Stadt",null],["064350015015","Hasselroth",null],["064350016016","Jossgrund",null],["064350017017","Langenselbold, Stadt",null],["064350018018","Linsengericht",null],["064350019019","Maintal, Stadt",null],["064350020020","Neuberg",null],["064350021021","Nidderau, Stadt",null],["064350022022","Niederdorfelden",null],["064350023023","Rodenbach",null],["064350024024","Ronneburg",null],["064350025025","Schlüchtern, Stadt",null],["064350026026","Schöneck",null],["064350027027","Sinntal",null],["064350028028","Steinau an der Straße, Brüder-Grimm-Stadt",null],["064350029029","Wächtersbach, Stadt",null],["064359200200","Gutsbezirk Spessart, gemfr. Gebiet",null],["064360001001","Bad Soden am Taunus, Stadt",null],["064360002002","Eppstein, Stadt",null],["064360003003","Eschborn, Stadt",null],["064360004004","Flörsheim am Main, Stadt",null],["064360005005","Hattersheim am Main, Stadt",null],["064360006006","Hochheim am Main, Stadt",null],["064360007007","Hofheim am Taunus, Kreisstadt",null],["064360008008","Kelkheim (Taunus), Stadt",null],["064360009009","Kriftel",null],["064360010010","Liederbach am Taunus",null],["064360011011","Schwalbach am Taunus, Stadt",null],["064360012012","Sulzbach (Taunus)",null],["064370001001","Bad König, Stadt",null],["064370003003","Brensbach",null],["064370004004","Breuberg, Stadt",null],["064370005005","Brombachtal",null],["064370006006","Erbach, Kreisstadt",null],["064370007007","Fränkisch-Crumbach",null],["064370009009","Höchst i. Odw.",null],["064370010010","Lützelbach",null],["064370011011","Michelstadt, Stadt",null],["064370012012","Mossautal",null],["064370013013","Reichelsheim (Odenwald)",null],["064370016016","Oberzent, Stadt",null],["064380001001","Dietzenbach, Kreisstadt",null],["064380002002","Dreieich, Stadt",null],["064380003003","Egelsbach",null],["064380004004","Hainburg",null],["064380005005","Heusenstamm, Stadt",null],["064380006006","Langen (Hessen), Stadt",null],["064380007007","Mainhausen",null],["064380008008","Mühlheim am Main, Stadt",null],["064380009009","Neu-Isenburg, Stadt",null],["064380010010","Obertshausen, Stadt",null],["064380011011","Rodgau, Stadt",null],["064380012012","Rödermark, Stadt",null],["064380013013","Seligenstadt, Einhardstadt",null],["064390001001","Aarbergen",null],["064390002002","Bad Schwalbach, Kreisstadt",null],["064390003003","Eltville am Rhein, Stadt",null],["064390004004","Geisenheim, Hochschulstadt",null],["064390005005","Heidenrod",null],["064390006006","Hohenstein",null],["064390007007","Hünstetten",null],["064390008008","Idstein, Hochschulstadt",null],["064390009009","Kiedrich",null],["064390010010","Lorch, Stadt",null],["064390011011","Niedernhausen",null],["064390012012","Oestrich-Winkel, Stadt",null],["064390013013","Rüdesheim am Rhein, Stadt",null],["064390014014","Schlangenbad",null],["064390015015","Taunusstein, Stadt",null],["064390016016","Waldems",null],["064390017017","Walluf",null],["064400001001","Altenstadt",null],["064400002002","Bad Nauheim, Stadt",null],["064400003003","Bad Vilbel, Stadt",null],["064400004004","Büdingen, Stadt",null],["064400005005","Butzbach, Friedrich-Ludwig-Weidig-Stadt",null],["064400006006","Echzell",null],["064400007007","Florstadt, Stadt",null],["064400008008","Friedberg (Hessen), Kreisstadt",null],["064400009009","Gedern, Stadt",null],["064400010010","Glauburg",null],["064400011011","Hirzenhain",null],["064400012012","Karben, Stadt",null],["064400013013","Kefenrod",null],["064400014014","Limeshain",null],["064400015015","Münzenberg, Stadt",null],["064400016016","Nidda, Stadt",null],["064400017017","Niddatal, Stadt",null],["064400018018","Ober-Mörlen",null],["064400019019","Ortenberg, Stadt",null],["064400020020","Ranstadt",null],["064400021021","Reichelsheim (Wetterau), Stadt",null],["064400022022","Rockenberg",null],["064400023023","Rosbach v. d. Höhe, Stadt",null],["064400024024","Wölfersheim",null],["064400025025","Wöllstadt",null],["065310001001","Allendorf (Lumda), Stadt",null],["065310002002","Biebertal",null],["065310003003","Buseck",null],["065310004004","Fernwald",null],["065310005005","Gießen, Universitätsstadt",null],["065310006006","Grünberg, Stadt",null],["065310007007","Heuchelheim a. d. Lahn",null],["065310008008","Hungen, Stadt",null],["065310009009","Langgöns",null],["065310010010","Laubach, Stadt",null],["065310011011","Lich, Stadt",null],["065310012012","Linden, Stadt",null],["065310013013","Lollar, Stadt",null],["065310014014","Pohlheim, Stadt",null],["065310015015","Rabenau",null],["065310016016","Reiskirchen",null],["065310017017","Staufenberg, Stadt",null],["065310018018","Wettenberg",null],["065320001001","Aßlar, Stadt",null],["065320002002","Bischoffen",null],["065320003003","Braunfels, Stadt",null],["065320004004","Breitscheid",null],["065320005005","Dietzhölztal",null],["065320006006","Dillenburg, Oranienstadt",null],["065320007007","Driedorf",null],["065320008008","Ehringshausen",null],["065320009009","Eschenburg",null],["065320010010","Greifenstein",null],["065320011011","Haiger, Stadt",null],["065320012012","Herborn, Stadt",null],["065320013013","Hohenahr",null],["065320014014","Hüttenberg",null],["065320015015","Lahnau",null],["065320016016","Leun, Stadt",null],["065320017017","Mittenaar",null],["065320018018","Schöffengrund",null],["065320019019","Siegbach",null],["065320020020","Sinn",null],["065320021021","Solms, Stadt",null],["065320022022","Waldsolms",null],["065320023023","Wetzlar, Stadt",null],["065330001001","Beselich",null],["065330002002","Brechen",null],["065330003003","Bad Camberg, Stadt",null],["065330004004","Dornburg",null],["065330005005","Elbtal",null],["065330006006","Elz",null],["065330007007","Hadamar, Stadt",null],["065330008008","Hünfelden",null],["065330009009","Limburg a. d. Lahn, Kreisstadt",null],["065330010010","Löhnberg",null],["065330011011","Mengerskirchen, Marktflecken",null],["065330012012","Merenberg, Marktflecken",null],["065330013013","Runkel, Stadt",null],["065330014014","Selters (Taunus)",null],["065330015015","Villmar, Marktflecken",null],["065330016016","Waldbrunn (Westerwald)",null],["065330017017","Weilburg, Stadt",null],["065330018018","Weilmünster, Marktflecken",null],["065330019019","Weinbach",null],["065340001001","Amöneburg, Stadt",null],["065340002002","Angelburg",null],["065340003003","Bad Endbach",null],["065340004004","Biedenkopf, Stadt",null],["065340005005","Breidenbach",null],["065340006006","Cölbe",null],["065340007007","Dautphetal",null],["065340008008","Ebsdorfergrund",null],["065340009009","Fronhausen",null],["065340010010","Gladenbach, Stadt",null],["065340011011","Kirchhain, Stadt",null],["065340012012","Lahntal",null],["065340013013","Lohra",null],["065340014014","Marburg, Universitätsstadt",null],["065340015015","Münchhausen",null],["065340016016","Neustadt (Hessen), Stadt",null],["065340017017","Rauschenberg, Stadt",null],["065340018018","Stadtallendorf, Stadt",null],["065340019019","Steffenberg",null],["065340020020","Weimar (Lahn)",null],["065340021021","Wetter (Hessen), Stadt",null],["065340022022","Wohratal",null],["065350001001","Alsfeld, Stadt",null],["065350002002","Antrifttal",null],["065350003003","Feldatal",null],["065350004004","Freiensteinau",null],["065350005005","Gemünden (Felda)",null],["065350006006","Grebenau, Stadt",null],["065350007007","Grebenhain",null],["065350008008","Herbstein, Stadt",null],["065350009009","Homberg (Ohm), Stadt",null],["065350010010","Kirtorf, Stadt",null],["065350011011","Lauterbach (Hessen), Kreisstadt",null],["065350012012","Lautertal (Vogelsberg)",null],["065350013013","Mücke",null],["065350014014","Romrod, Stadt",null],["065350015015","Schlitz, Stadt",null],["065350016016","Schotten, Stadt",null],["065350017017","Schwalmtal",null],["065350018018","Ulrichstein, Stadt",null],["065350019019","Wartenberg",null],["066110000000","Kassel, documenta-Stadt",null],["066310001001","Bad Salzschlirf",null],["066310002002","Burghaun, Marktgemeinde",null],["066310003003","Dipperz",null],["066310004004","Ebersburg",null],["066310005005","Ehrenberg (Rhön)",null],["066310006006","Eichenzell",null],["066310007007","Eiterfeld, Marktgemeinde",null],["066310008008","Flieden",null],["066310009009","Fulda, Stadt",null],["066310010010","Gersfeld (Rhön), Stadt",null],["066310011011","Großenlüder",null],["066310012012","Hilders, Marktgemeinde",null],["066310013013","Hofbieber",null],["066310014014","Hosenfeld",null],["066310015015","Hünfeld, Konrad-Zuse-Stadt",null],["066310016016","Kalbach",null],["066310017017","Künzell",null],["066310018018","Neuhof",null],["066310019019","Nüsttal",null],["066310020020","Petersberg",null],["066310021021","Poppenhausen (Wasserkuppe)",null],["066310022022","Rasdorf, Point-Alpha-Gemeinde",null],["066310023023","Tann (Rhön), Stadt",null],["066320001001","Alheim",null],["066320002002","Bad Hersfeld, Kreisstadt",null],["066320003003","Bebra, Stadt",null],["066320004004","Breitenbach a. Herzberg",null],["066320005005","Cornberg",null],["066320006006","Friedewald",null],["066320007007","Hauneck",null],["066320008008","Haunetal",null],["066320009009","Heringen (Werra), Stadt",null],["066320010010","Hohenroda",null],["066320011011","Kirchheim",null],["066320012012","Ludwigsau",null],["066320013013","Nentershausen",null],["066320014014","Neuenstein",null],["066320015015","Niederaula, Marktgemeinde",null],["066320016016","Philippsthal (Werra), Marktgemeinde",null],["066320017017","Ronshausen",null],["066320018018","Rotenburg a. d. Fulda, Stadt",null],["066320019019","Schenklengsfeld",null],["066320020020","Wildeck",null],["066330001001","Ahnatal",null],["066330002002","Bad Karlshafen, Stadt",null],["066330003003","Baunatal, Stadt",null],["066330004004","Breuna",null],["066330005005","Calden",null],["066330006006","Bad Emstal",null],["066330007007","Espenau",null],["066330008008","Fuldabrück",null],["066330009009","Fuldatal",null],["066330010010","Grebenstein, Stadt",null],["066330011011","Habichtswald",null],["066330012012","Helsa",null],["066330013013","Hofgeismar, Stadt",null],["066330014014","Immenhausen, Stadt",null],["066330015015","Kaufungen",null],["066330016016","Liebenau, Stadt",null],["066330017017","Lohfelden",null],["066330018018","Naumburg, Stadt",null],["066330019019","Nieste",null],["066330020020","Niestetal",null],["066330022022","Reinhardshagen",null],["066330023023","Schauenburg",null],["066330024024","Söhrewald",null],["066330025025","Trendelburg, Stadt",null],["066330026026","Vellmar, Stadt",null],["066330028028","Wolfhagen, Hans-Staden-Stadt",null],["066330029029","Zierenberg, Stadt",null],["066330030030","Wesertal",null],["066339200200","Gutsbezirk Reinhardswald, gemfr. Gebiet",null],["066340001001","Borken (Hessen), Stadt",null],["066340002002","Edermünde",null],["066340003003","Felsberg, Stadt",null],["066340004004","Frielendorf, Marktflecken",null],["066340005005","Fritzlar, Dom- und Kaiserstadt",null],["066340006006","Gilserberg",null],["066340007007","Gudensberg, Stadt",null],["066340008008","Guxhagen",null],["066340009009","Homberg (Efze), Reformationsstadt, Kreisstadt",null],["066340010010","Jesberg",null],["066340011011","Knüllwald",null],["066340012012","Körle",null],["066340013013","Malsfeld",null],["066340014014","Melsungen, Stadt",null],["066340015015","Morschen",null],["066340016016","Neuental",null],["066340017017","Neukirchen, Stadt",null],["066340018018","Niedenstein, Stadt",null],["066340019019","Oberaula",null],["066340020020","Ottrau",null],["066340021021","Schrecksbach",null],["066340022022","Schwalmstadt, Konfirmationsstadt",null],["066340023023","Schwarzenborn, Stadt",null],["066340024024","Spangenberg, Liebenbachstadt",null],["066340025025","Wabern",null],["066340026026","Willingshausen",null],["066340027027","Bad Zwesten",null],["066350001001","Allendorf (Eder)",null],["066350002002","Bad Arolsen, Stadt",null],["066350003003","Bad Wildungen, Stadt",null],["066350004004","Battenberg (Eder), Stadt",null],["066350005005","Bromskirchen",null],["066350006006","Burgwald",null],["066350007007","Diemelsee",null],["066350008008","Diemelstadt, Stadt",null],["066350009009","Edertal, Nationalparkgemeinde",null],["066350010010","Frankenau, Nationalparkstadt",null],["066350011011","Frankenberg (Eder), Philipp-Soldan-Stadt",null],["066350012012","Gemünden (Wohra), Stadt",null],["066350013013","Haina (Kloster)",null],["066350014014","Hatzfeld (Eder), Stadt",null],["066350015015","Korbach, Hansestadt, Kreisstadt",null],["066350016016","Lichtenfels, Stadt",null],["066350017017","Rosenthal, Stadt",null],["066350018018","Twistetal",null],["066350019019","Vöhl, Nationalparkgemeinde",null],["066350020020","Volkmarsen, Stadt",null],["066350021021","Waldeck, Stadt",null],["066350022022","Willingen (Upland)",null],["066360001001","Bad Sooden-Allendorf, Stadt",null],["066360002002","Berkatal",null],["066360003003","Eschwege, Kreisstadt",null],["066360004004","Großalmerode, Stadt",null],["066360005005","Herleshausen",null],["066360006006","Hessisch Lichtenau, Stadt",null],["066360007007","Meinhard",null],["066360008008","Meißner",null],["066360009009","Neu-Eichenberg",null],["066360010010","Ringgau",null],["066360011011","Sontra, Stadt",null],["066360012012","Waldkappel, Stadt",null],["066360013013","Wanfried, Stadt",null],["066360014014","Wehretal",null],["066360015015","Weißenborn",null],["066360016016","Witzenhausen, Stadt",null],["066369200200","Gutsbezirk Kaufunger Wald, gemfr. Gebiet",null],["070009999999","Gemeinsames deutsch-luxemburgisches Hoheitsgebiet",null],["071110000000","Koblenz, Stadt",null],["071310007007","Bad Neuenahr-Ahrweiler, Stadt",null],["071310070070","Remagen, Stadt",null],["071310077077","Sinzig, Stadt",null],["071310090090","Grafschaft",null],["071315001001","Adenau, Stadt",null],["071315001004","Antweiler",null],["071315001005","Aremberg",null],["071315001008","Barweiler",null],["071315001009","Bauler",null],["071315001015","Dankerath",null],["071315001018","Dorsel",null],["071315001021","Eichenbach",null],["071315001022","Fuchshofen",null],["071315001026","Harscheid",null],["071315001028","Herschbroich",null],["071315001030","Hoffeld",null],["071315001032","Honerath",null],["071315001033","Hümmel",null],["071315001034","Insul",null],["071315001037","Kaltenborn",null],["071315001042","Kottenborn",null],["071315001044","Leimbach",null],["071315001050","Meuspath",null],["071315001051","Müllenbach",null],["071315001052","Müsch",null],["071315001058","Nürburg",null],["071315001062","Ohlenhard",null],["071315001065","Pomster",null],["071315001066","Quiddelbach",null],["071315001069","Reifferscheid",null],["071315001072","Rodder",null],["071315001074","Schuld",null],["071315001075","Senscheid",null],["071315001076","Sierscheid",null],["071315001079","Trierscheid",null],["071315001082","Wershofen",null],["071315001083","Wiesemscheid",null],["071315001084","Wimbach",null],["071315001085","Winnerath",null],["071315001086","Wirft",null],["071315001501","Dümpelfeld",null],["071315002002","Ahrbrück",null],["071315002003","Altenahr",null],["071315002011","Berg",null],["071315002017","Dernau",null],["071315002027","Heckenbach",null],["071315002029","Hönningen",null],["071315002036","Kalenborn",null],["071315002039","Kesseling",null],["071315002040","Kirchsahr",null],["071315002047","Lind",null],["071315002049","Mayschoß",null],["071315002068","Rech",null],["071315003006","Bad Breisig, Stadt",null],["071315003014","Brohl-Lützing",null],["071315003025","Gönnersdorf",null],["071315003081","Waldorf",null],["071315004016","Dedenbach",null],["071315004041","Königsfeld",null],["071315004054","Niederdürenbach",null],["071315004055","Niederzissen",null],["071315004059","Oberdürenbach",null],["071315004060","Oberzissen",null],["071315004073","Schalkenbach",null],["071315004201","Brenk",null],["071315004202","Burgbrohl",null],["071315004204","Galenberg",null],["071315004205","Glees",null],["071315004206","Hohenleimbach",null],["071315004208","Spessart",null],["071315004209","Wassenach",null],["071315004210","Wehr",null],["071315004211","Weibern",null],["071315004502","Kempenich",null],["071325003018","Daaden, Stadt",null],["071325003019","Derschen",null],["071325003026","Emmerzhausen",null],["071325003036","Friedewald",null],["071325003050","Herdorf, Stadt",null],["071325003068","Mauden",null],["071325003075","Niederdreisbach",null],["071325003079","Nisterberg",null],["071325003101","Schutzbach",null],["071325003113","Weitefeld",null],["071325006007","Birkenbeul",null],["071325006010","Bitzen",null],["071325006013","Breitscheidt",null],["071325006014","Bruchertseifen",null],["071325006028","Etzbach",null],["071325006034","Forst",null],["071325006038","Fürthen",null],["071325006044","Hamm (Sieg)",null],["071325006077","Niederirsen",null],["071325006091","Pracht",null],["071325006096","Roth",null],["071325006102","Seelbach bei Hamm (Sieg)",null],["071325007012","Brachbach",null],["071325007037","Friesenhagen",null],["071325007045","Harbach",null],["071325007063","Kirchen (Sieg), Stadt",null],["071325007072","Mudersbach",null],["071325007076","Niederfischbach",null],["071325008008","Birken-Honigsessen",null],["071325008011","Mittelhof",null],["071325008054","Hövels",null],["071325008080","Katzwinkel (Sieg)",null],["071325008105","Selbach (Sieg)",null],["071325008117","Wissen, Stadt",null],["071325009002","Alsdorf",null],["071325009006","Betzdorf, Stadt",null],["071325009020","Dickendorf",null],["071325009024","Elben",null],["071325009025","Elkenroth",null],["071325009030","Fensdorf",null],["071325009039","Gebhardshain",null],["071325009042","Grünebach",null],["071325009059","Kausen",null],["071325009066","Malberg",null],["071325009071","Molzhain",null],["071325009073","Nauroth",null],["071325009095","Rosenheim (Landkreis Altenkirchen)",null],["071325009098","Scheuerfeld",null],["071325009107","Steinebach/ Sieg",null],["071325009108","Steineroth",null],["071325009111","Wallmenroth",null],["071325010001","Almersbach",null],["071325010004","Bachenberg",null],["071325010005","Berzhausen",null],["071325010009","Birnbach",null],["071325010015","Bürdenbach",null],["071325010016","Burglahr",null],["071325010017","Busenhausen",null],["071325010022","Eichelhardt",null],["071325010023","Eichen",null],["071325010027","Ersfeld",null],["071325010029","Eulenberg",null],["071325010031","Fiersbach",null],["071325010032","Flammersfeld",null],["071325010033","Fluterschen",null],["071325010035","Forstmehren",null],["071325010040","Gieleroth",null],["071325010041","Giershausen",null],["071325010043","Güllesheim",null],["071325010046","Hasselbach",null],["071325010047","Helmenzen",null],["071325010048","Helmeroth",null],["071325010049","Hemmelzen",null],["071325010051","Heupelzen",null],["071325010052","Hilgenroth",null],["071325010053","Hirz-Maulsbach",null],["071325010055","Horhausen (Westerwald)",null],["071325010056","Idelberg",null],["071325010057","Ingelbach",null],["071325010058","Isert",null],["071325010060","Kescheid",null],["071325010061","Kettenhausen",null],["071325010062","Kircheib",null],["071325010064","Kraam",null],["071325010065","Krunkel",null],["071325010067","Mammelzen",null],["071325010069","Mehren",null],["071325010070","Michelbach (Westerwald)",null],["071325010078","Niedersteinebach",null],["071325010081","Obererbach (Westerwald)",null],["071325010082","Oberirsen",null],["071325010083","Oberlahr",null],["071325010085","Obersteinebach",null],["071325010086","Oberwambach",null],["071325010087","Ölsen",null],["071325010088","Orfgen",null],["071325010089","Peterslahr",null],["071325010090","Pleckhausen",null],["071325010092","Racksen",null],["071325010093","Reiferscheid",null],["071325010094","Rettersen",null],["071325010097","Rott",null],["071325010099","Schöneberg",null],["071325010100","Schürdt",null],["071325010103","Seelbach (Westerwald)",null],["071325010104","Seifen",null],["071325010106","Sörth",null],["071325010109","Stürzelbach",null],["071325010110","Volkerzen",null],["071325010112","Walterschen",null],["071325010114","Werkhausen",null],["071325010115","Weyerbusch",null],["071325010116","Willroth",null],["071325010118","Wölmersen",null],["071325010119","Ziegenhain",null],["071325010201","Berod bei Hachenburg",null],["071325010501","Altenkirchen (Westerwald), Stadt",null],["071325010502","Neitersen",null],["071330006006","Bad Kreuznach, Stadt",null],["071335001003","Altenbamberg",null],["071335001012","Biebelsheim",null],["071335001030","Feilbingert",null],["071335001031","Frei-Laubersheim",null],["071335001032","Fürfeld",null],["071335001037","Hackenheim",null],["071335001039","Hallgarten",null],["071335001045","Hochstätten",null],["071335001069","Neu-Bamberg",null],["071335001078","Pfaffen-Schwabenheim",null],["071335001080","Pleitersheim",null],["071335001104","Tiefenthal",null],["071335001106","Volxheim",null],["071335006002","Allenfeld",null],["071335006004","Argenschwang",null],["071335006013","Bockenau",null],["071335006014","Boos",null],["071335006015","Braunweiler",null],["071335006019","Burgsponheim",null],["071335006021","Dalberg",null],["071335006027","Duchroth",null],["071335006033","Gebroth",null],["071335006036","Gutenberg",null],["071335006040","Hargesheim",null],["071335006044","Hergenfeld",null],["071335006048","Hüffelsheim",null],["071335006061","Mandel",null],["071335006068","Münchwald",null],["071335006070","Niederhausen",null],["071335006071","Norheim",null],["071335006074","Oberhausen an der Nahe",null],["071335006075","Oberstreit",null],["071335006086","Roxheim",null],["071335006088","Sankt Katharinen",null],["071335006089","Schloßböckelheim",null],["071335006098","Sommerloch",null],["071335006099","Spabrücken",null],["071335006100","Spall",null],["071335006101","Sponheim",null],["071335006105","Traisen",null],["071335006107","Waldböckelheim",null],["071335006109","Wallhausen",null],["071335006112","Weinsheim",null],["071335006115","Winterbach",null],["071335006117","Rüdesheim",null],["071335009008","Bärenbach",null],["071335009010","Becherbach bei Kirn",null],["071335009016","Brauweiler",null],["071335009038","Hahnenbach",null],["071335009041","Heimweiler",null],["071335009042","Heinzenberg",null],["071335009043","Hennweiler",null],["071335009046","Hochstetten-Dhaun",null],["071335009047","Horbach",null],["071335009052","Kirn, Stadt",null],["071335009059","Limbach",null],["071335009063","Meckenbach",null],["071335009073","Oberhausen bei Kirn",null],["071335009077","Otzweiler",null],["071335009096","Simmertal",null],["071335009113","Weitersborn",null],["071335009201","Bruschied",null],["071335009202","Kellenbach",null],["071335009203","Königsau",null],["071335009204","Schneppenbach",null],["071335009205","Schwarzerden",null],["071335010001","Abtweiler",null],["071335010005","Auen",null],["071335010009","Bärweiler",null],["071335010011","Becherbach",null],["071335010017","Breitenheim",null],["071335010020","Callbach",null],["071335010022","Daubach",null],["071335010024","Desloch",null],["071335010049","Hundsbach",null],["071335010050","Ippenschied",null],["071335010051","Jeckenbach",null],["071335010053","Kirschroth",null],["071335010055","Langenthal",null],["071335010057","Lauschied",null],["071335010058","Lettweiler",null],["071335010060","Löllbach",null],["071335010062","Martinstein",null],["071335010064","Meddersheim",null],["071335010065","Meisenheim, Stadt",null],["071335010066","Merxheim",null],["071335010067","Monzingen",null],["071335010072","Nußbaum",null],["071335010076","Odernheim am Glan",null],["071335010081","Raumbach",null],["071335010082","Rehbach",null],["071335010083","Rehborn",null],["071335010084","Reiffelbach",null],["071335010090","Schmittweiler",null],["071335010092","Schweinschied",null],["071335010094","Seesbach",null],["071335010102","Staudernheim",null],["071335010111","Weiler bei Monzingen",null],["071335010116","Winterburg",null],["071335010501","Bad Sobernheim, Stadt",null],["071335011018","Bretzenheim",null],["071335011023","Daxweiler",null],["071335011025","Dörrebach",null],["071335011026","Dorsheim",null],["071335011028","Eckenroth",null],["071335011035","Guldental",null],["071335011054","Langenlonsheim",null],["071335011056","Laubenheim",null],["071335011085","Roth",null],["071335011087","Rümmelsheim",null],["071335011091","Schöneberg",null],["071335011093","Schweppenhausen",null],["071335011095","Seibersbach",null],["071335011103","Stromberg, Stadt",null],["071335011108","Waldlaubersheim",null],["071335011110","Warmsroth",null],["071335011114","Windesheim",null],["071340045045","Idar-Oberstein, Stadt",null],["071345001005","Baumholder, Stadt",null],["071345001007","Berglangenbach",null],["071345001008","Berschweiler bei Baumholder",null],["071345001021","Eckersweiler",null],["071345001026","Fohren-Linden",null],["071345001027","Frauenberg",null],["071345001033","Hahnweiler",null],["071345001036","Heimbach",null],["071345001051","Leitzweiler",null],["071345001054","Mettweiler",null],["071345001068","Reichenbach",null],["071345001073","Rohrbach",null],["071345001074","Rückweiler",null],["071345001075","Ruschberg",null],["071345002001","Abentheuer",null],["071345002002","Achtelsbach",null],["071345002010","Birkenfeld, Stadt",null],["071345002011","Börfink",null],["071345002015","Brücken",null],["071345002016","Buhlenberg",null],["071345002018","Dambach",null],["071345002020","Dienstweiler",null],["071345002022","Elchweiler",null],["071345002023","Ellenberg",null],["071345002024","Ellweiler",null],["071345002029","Gimbweiler",null],["071345002031","Gollenberg",null],["071345002034","Hattgenstein",null],["071345002042","Hoppstädten-Weiersbach",null],["071345002048","Kronweiler",null],["071345002050","Leisel",null],["071345002053","Meckenbach",null],["071345002057","Niederbrombach",null],["071345002058","Niederhambach",null],["071345002061","Nohen",null],["071345002062","Oberbrombach",null],["071345002063","Oberhambach",null],["071345002070","Rimsberg",null],["071345002071","Rinzenberg",null],["071345002072","Rötsweiler-Nockenthal",null],["071345002078","Schmißberg",null],["071345002080","Schwollen",null],["071345002084","Siesbach",null],["071345002085","Sonnenberg-Winnenberg",null],["071345002094","Wilzenberg-Hußweiler",null],["071345005003","Allenbach",null],["071345005004","Asbach",null],["071345005006","Bergen",null],["071345005009","Berschweiler bei Kirn",null],["071345005012","Bollenbach",null],["071345005013","Breitenthal",null],["071345005014","Bruchweiler",null],["071345005017","Bundenbach",null],["071345005019","Dickesbach",null],["071345005025","Fischbach",null],["071345005028","Gerach",null],["071345005030","Gösenroth",null],["071345005032","Griebelschied",null],["071345005035","Hausen",null],["071345005037","Hellertshausen",null],["071345005038","Herborn",null],["071345005039","Herrstein",null],["071345005040","Hettenrodt",null],["071345005041","Hintertiefenbach",null],["071345005043","Horbruch",null],["071345005044","Hottenbach",null],["071345005046","Kempfeld",null],["071345005047","Kirschweiler",null],["071345005049","Krummenau",null],["071345005052","Mackenrodt",null],["071345005055","Mittelreidenbach",null],["071345005056","Mörschied",null],["071345005059","Niederhosenbach",null],["071345005060","Niederwörresbach",null],["071345005064","Oberhosenbach",null],["071345005065","Oberkirn",null],["071345005066","Oberreidenbach",null],["071345005067","Oberwörresbach",null],["071345005069","Rhaunen",null],["071345005076","Schauren",null],["071345005077","Schmidthachenbach",null],["071345005079","Schwerbach",null],["071345005081","Sensweiler",null],["071345005082","Sien",null],["071345005083","Sienhachenbach",null],["071345005086","Sonnschied",null],["071345005087","Stipshausen",null],["071345005088","Sulzbach",null],["071345005089","Veitsrodt",null],["071345005090","Vollmersbach",null],["071345005091","Weiden",null],["071345005092","Weitersbach",null],["071345005093","Wickenrodt",null],["071345005095","Wirschweiler",null],["071345005502","Langweiler",null],["071355001007","Beilstein",null],["071355001012","Bremm",null],["071355001015","Briedern",null],["071355001017","Bruttig-Fankel",null],["071355001020","Cochem, Stadt",null],["071355001021","Dohr",null],["071355001024","Ediger-Eller",null],["071355001025","Ellenz-Poltersdorf",null],["071355001027","Ernst",null],["071355001029","Faid",null],["071355001036","Greimersburg",null],["071355001049","Klotten",null],["071355001053","Lieg",null],["071355001056","Lütz",null],["071355001060","Mesenich",null],["071355001065","Moselkern",null],["071355001066","Müden (Mosel)",null],["071355001069","Nehren",null],["071355001072","Pommern",null],["071355001079","Senheim",null],["071355001082","Treis-Karden",null],["071355001086","Valwig",null],["071355001090","Wirfus",null],["071355002009","Binningen",null],["071355002011","Brachtendorf",null],["071355002014","Brieden",null],["071355002016","Brohl",null],["071355002022","Dünfus",null],["071355002023","Düngenheim",null],["071355002026","Eppenberg",null],["071355002028","Eulgem",null],["071355002031","Forst (Eifel)",null],["071355002033","Gamlen",null],["071355002038","Hambuch",null],["071355002040","Hauroth",null],["071355002042","Illerich",null],["071355002043","Kaifenheim",null],["071355002044","Kail",null],["071355002045","Kaisersesch, Stadt",null],["071355002046","Kalenborn",null],["071355002051","Landkern",null],["071355002052","Laubach",null],["071355002058","Masburg",null],["071355002062","Möntenich",null],["071355002067","Müllenbach",null],["071355002075","Roes",null],["071355002084","Urmersbach",null],["071355002093","Zettingen",null],["071355002502","Leienkaul",null],["071355003002","Alflen",null],["071355003005","Auderath",null],["071355003008","Beuren",null],["071355003018","Büchel",null],["071355003030","Filz",null],["071355003034","Gevenich",null],["071355003035","Gillenbeuren",null],["071355003048","Kliding",null],["071355003057","Lutzerath",null],["071355003078","Schmitt",null],["071355003083","Ulmen, Stadt",null],["071355003085","Urschmitt",null],["071355003087","Wagenhausen",null],["071355003089","Weiler",null],["071355003091","Wollmerath",null],["071355003501","Bad Bertrich",null],["071355005001","Alf",null],["071355005003","Altlay",null],["071355005004","Altstrimmig",null],["071355005010","Blankenrath",null],["071355005013","Briedel",null],["071355005019","Bullay",null],["071355005032","Forst (Hunsrück)",null],["071355005037","Grenderich",null],["071355005039","Haserich",null],["071355005041","Hesweiler",null],["071355005054","Liesenich",null],["071355005061","Mittelstrimmig",null],["071355005064","Moritzheim",null],["071355005068","Neef",null],["071355005070","Panzweiler",null],["071355005071","Peterswald-Löffelscheid",null],["071355005073","Pünderich",null],["071355005074","Reidenhausen",null],["071355005076","Sankt Aldegund",null],["071355005077","Schauren",null],["071355005080","Sosberg",null],["071355005081","Tellig",null],["071355005088","Walhausen",null],["071355005092","Zell (Mosel), Stadt",null],["071370003003","Andernach, Stadt",null],["071370068068","Mayen, Stadt",null],["071370203203","Bendorf, Stadt",null],["071375001056","Kretz",null],["071375001057","Kruft",null],["071375001081","Nickenich",null],["071375001088","Plaidt",null],["071375001096","Saffig",null],["071375002023","Einig",null],["071375002027","Gappenach",null],["071375002029","Gering",null],["071375002030","Gierschnach",null],["071375002041","Kalt",null],["071375002048","Kerben",null],["071375002053","Kollig",null],["071375002065","Lonnig",null],["071375002070","Mertloch",null],["071375002080","Naunheim",null],["071375002086","Ochtendung",null],["071375002087","Pillig",null],["071375002089","Polch, Stadt",null],["071375002095","Rüber",null],["071375002102","Trimbs",null],["071375002112","Welling",null],["071375002114","Wierschem",null],["071375002501","Münstermaifeld, Stadt",null],["071375003001","Acht",null],["071375003004","Anschau",null],["071375003006","Arft",null],["071375003007","Baar",null],["071375003011","Bermel",null],["071375003014","Boos",null],["071375003019","Ditscheid",null],["071375003025","Ettringen",null],["071375003034","Hausten",null],["071375003035","Herresbach",null],["071375003036","Hirten",null],["071375003043","Kehrig",null],["071375003049","Kirchwald",null],["071375003055","Kottenheim",null],["071375003060","Langenfeld",null],["071375003061","Langscheid",null],["071375003063","Lind",null],["071375003066","Luxem",null],["071375003074","Monreal",null],["071375003077","Münk",null],["071375003079","Nachtsheim",null],["071375003092","Reudelsterz",null],["071375003097","Sankt Johann",null],["071375003099","Siebenbach",null],["071375003105","Virneburg",null],["071375003110","Weiler",null],["071375003113","Welschenbach",null],["071375004008","Bell",null],["071375004069","Mendig, Stadt",null],["071375004093","Rieden",null],["071375004101","Thür",null],["071375004106","Volkesfeld",null],["071375007218","Niederwerth",null],["071375007224","Urbar",null],["071375007226","Vallendar, Stadt",null],["071375007229","Weitersburg",null],["071375008202","Bassenheim",null],["071375008209","Kaltenengers",null],["071375008211","Kettig",null],["071375008216","Mülheim-Kärlich, Stadt",null],["071375008222","Sankt Sebastian",null],["071375008225","Urmitz",null],["071375008228","Weißenthurm, Stadt",null],["071375009201","Alken",null],["071375009204","Brey",null],["071375009205","Brodenbach",null],["071375009206","Burgen",null],["071375009207","Dieblich",null],["071375009208","Hatzenport",null],["071375009212","Kobern-Gondorf",null],["071375009214","Löf",null],["071375009215","Macken",null],["071375009217","Niederfell",null],["071375009219","Nörtershausen",null],["071375009220","Oberfell",null],["071375009221","Rhens, Stadt",null],["071375009223","Spay",null],["071375009227","Waldesch",null],["071375009230","Winningen",null],["071375009231","Wolken",null],["071375009504","Lehmen",null],["071380045045","Neuwied, Stadt",null],["071385001003","Asbach",null],["071385001044","Neustadt (Wied)",null],["071385001077","Windhagen",null],["071385001080","Buchholz (Westerwald)",null],["071385002004","Bad Hönningen, Stadt",null],["071385002024","Hammerstein",null],["071385002038","Leutesdorf",null],["071385002063","Rheinbrohl",null],["071385003012","Dierdorf, Stadt",null],["071385003023","Großmaischeid",null],["071385003031","Isenburg",null],["071385003034","Kleinmaischeid",null],["071385003069","Stebach",null],["071385003201","Marienhausen",null],["071385004009","Dattenberg",null],["071385004037","Leubsdorf",null],["071385004041","Linz am Rhein, Stadt",null],["071385004055","Ockenfels",null],["071385004068","Sankt Katharinen (Landkreis Neuwied)",null],["071385004075","Vettelschoß",null],["071385004501","Kasbach-Ohlenberg",null],["071385005011","Dernbach",null],["071385005013","Döttesfeld",null],["071385005014","Dürrholz",null],["071385005025","Hanroth",null],["071385005027","Harschbach",null],["071385005040","Linkenbach",null],["071385005048","Niederhofen",null],["071385005050","Niederwambach",null],["071385005052","Oberdreis",null],["071385005057","Puderbach",null],["071385005058","Ratzert",null],["071385005059","Raubach",null],["071385005064","Rodenbach bei Puderbach",null],["071385005070","Steimel",null],["071385005074","Urbach",null],["071385005078","Woldert",null],["071385007008","Bruchhausen",null],["071385007019","Erpel",null],["071385007062","Rheinbreitbach",null],["071385007073","Unkel, Stadt",null],["071385009002","Anhausen",null],["071385009005","Bonefeld",null],["071385009006","Breitscheid",null],["071385009007","Hausen (Wied)",null],["071385009010","Datzeroth",null],["071385009015","Ehlscheid",null],["071385009026","Hardert",null],["071385009030","Hümmerich",null],["071385009036","Kurtscheid",null],["071385009042","Meinborn",null],["071385009043","Melsbach",null],["071385009047","Niederbreitbach",null],["071385009053","Oberhonnefeld-Gierend",null],["071385009054","Oberraden",null],["071385009061","Rengsdorf",null],["071385009065","Roßbach",null],["071385009066","Rüscheid",null],["071385009071","Straßenhaus",null],["071385009072","Thalhausen",null],["071385009076","Waldbreitbach",null],["071400501501","Boppard, Stadt",null],["071405003001","Alterkülz",null],["071405003009","Bell (Hunsrück)",null],["071405003010","Beltheim",null],["071405003018","Braunshorn",null],["071405003021","Buch",null],["071405003042","Gödenroth",null],["071405003046","Hasselbach",null],["071405003055","Hollnich",null],["071405003064","Kastellaun, Stadt",null],["071405003073","Korweiler",null],["071405003095","Michelbach",null],["071405003131","Roth",null],["071405003147","Spesenroth",null],["071405003153","Uhler",null],["071405003202","Dommershausen",null],["071405003204","Mastershausen",null],["071405003502","Lahr",null],["071405003503","Mörsdorf",null],["071405003504","Zilshausen",null],["071405004006","Bärenbach",null],["071405004007","Belg",null],["071405004024","Büchenbeuren",null],["071405004028","Dickenschied",null],["071405004029","Dill",null],["071405004030","Dillendorf",null],["071405004040","Gehlweiler",null],["071405004041","Gemünden",null],["071405004044","Hahn",null],["071405004048","Hecken",null],["071405004049","Heinzenbach",null],["071405004050","Henau",null],["071405004053","Hirschfeld (Hunsrück)",null],["071405004062","Kappel",null],["071405004067","Kirchberg (Hunsrück), Stadt",null],["071405004071","Kludenbach",null],["071405004081","Laufersweiler",null],["071405004082","Lautzenhausen",null],["071405004086","Lindenschied",null],["071405004090","Maitzborn",null],["071405004094","Metzenhausen",null],["071405004105","Nieder Kostenz",null],["071405004107","Niedersohren",null],["071405004109","Niederweiler",null],["071405004111","Ober Kostenz",null],["071405004120","Raversbeuren",null],["071405004122","Reckershausen",null],["071405004128","Rödelhausen",null],["071405004129","Rödern",null],["071405004130","Rohrbach",null],["071405004135","Schlierschied",null],["071405004141","Schwarzen",null],["071405004145","Sohren",null],["071405004146","Sohrschied",null],["071405004151","Todenroth",null],["071405004154","Unzenberg",null],["071405004159","Wahlenau",null],["071405004163","Womrath",null],["071405004164","Woppenroth",null],["071405004165","Würrich",null],["071405008002","Altweidelbach",null],["071405008003","Argenthal",null],["071405008008","Belgweiler",null],["071405008011","Benzweiler",null],["071405008012","Bergenhausen",null],["071405008015","Biebern",null],["071405008020","Bubach",null],["071405008023","Budenbach",null],["071405008027","Dichtelbach",null],["071405008035","Ellern (Hunsrück)",null],["071405008037","Erbach",null],["071405008039","Fronhofen",null],["071405008056","Holzbach",null],["071405008058","Horn",null],["071405008065","Keidelheim",null],["071405008068","Kisselbach",null],["071405008070","Klosterkumbd",null],["071405008076","Külz (Hunsrück)",null],["071405008077","Kümbdchen",null],["071405008079","Laubach",null],["071405008085","Liebshausen",null],["071405008092","Mengerschied",null],["071405008096","Mörschbach",null],["071405008099","Mutterschied",null],["071405008100","Nannhausen",null],["071405008101","Neuerkirch",null],["071405008106","Niederkumbd",null],["071405008113","Ohlweiler",null],["071405008115","Oppertshausen",null],["071405008118","Pleizenhausen",null],["071405008119","Ravengiersburg",null],["071405008121","Rayerschied",null],["071405008123","Reich",null],["071405008125","Rheinböllen, Stadt",null],["071405008126","Riegenroth",null],["071405008127","Riesweiler",null],["071405008134","Sargenroth",null],["071405008138","Schnorbach",null],["071405008139","Schönborn",null],["071405008144","Simmern/ Hunsrück, Stadt",null],["071405008148","Steinbach",null],["071405008150","Tiefenbach",null],["071405008158","Wahlbach",null],["071405008166","Wüschheim",null],["071405009005","Badenhard",null],["071405009014","Bickenbach",null],["071405009016","Birkheim",null],["071405009025","Damscheid",null],["071405009031","Dörth",null],["071405009036","Emmelshausen, Stadt",null],["071405009043","Gondershausen",null],["071405009045","Halsenbach",null],["071405009047","Hausbay",null],["071405009060","Hungenroth",null],["071405009063","Karbach",null],["071405009075","Kratzenburg",null],["071405009080","Laudert",null],["071405009084","Leiningen",null],["071405009087","Lingerhahn",null],["071405009089","Maisborn",null],["071405009093","Mermuth",null],["071405009098","Mühlpfad",null],["071405009102","Ney",null],["071405009104","Niederburg",null],["071405009108","Niedert",null],["071405009110","Norath",null],["071405009112","Oberwesel, Stadt",null],["071405009116","Perscheid",null],["071405009117","Pfalzfeld",null],["071405009133","Sankt Goar, Stadt",null],["071405009140","Schwall",null],["071405009149","Thörlingen",null],["071405009155","Urbar",null],["071405009156","Utzenhain",null],["071405009161","Wiebelsheim",null],["071405009201","Beulich",null],["071405009205","Morshausen",null],["071410075075","Lahnstein, Stadt",null],["071415003002","Altendiez",null],["071415003005","Aull",null],["071415003014","Birlenbach",null],["071415003021","Charlottenberg",null],["071415003022","Cramberg",null],["071415003029","Diez, Stadt",null],["071415003030","Dörnberg",null],["071415003038","Eppenrod",null],["071415003045","Geilnau",null],["071415003049","Gückingen",null],["071415003052","Hambach",null],["071415003053","Heistenbach",null],["071415003057","Hirschberg",null],["071415003059","Holzappel",null],["071415003061","Holzheim",null],["071415003062","Horhausen",null],["071415003064","Isselbach",null],["071415003076","Langenscheid",null],["071415003077","Laurenburg",null],["071415003124","Scheidt",null],["071415003130","Steinsberg",null],["071415003133","Wasenbach",null],["071415003503","Balduinstein",null],["071415007009","Berg",null],["071415007012","Bettendorf",null],["071415007015","Bogel",null],["071415007019","Buch",null],["071415007035","Ehr",null],["071415007037","Endlichhofen",null],["071415007040","Eschbach",null],["071415007047","Gemmerich",null],["071415007055","Himmighofen",null],["071415007060","Holzhausen an der Haide",null],["071415007063","Hunzel",null],["071415007067","Kasdorf",null],["071415007070","Kehlbach",null],["071415007078","Lautert",null],["071415007080","Lipporn",null],["071415007084","Marienfels",null],["071415007085","Miehlen",null],["071415007092","Nastätten, Stadt",null],["071415007094","Niederbachheim",null],["071415007097","Niederwallmenach",null],["071415007100","Oberbachheim",null],["071415007104","Obertiefenbach",null],["071415007105","Oberwallmenach",null],["071415007107","Oelsberg",null],["071415007110","Hainau",null],["071415007116","Rettershain",null],["071415007120","Ruppertshofen",null],["071415007131","Strüth",null],["071415007134","Weidenbach",null],["071415007137","Welterod",null],["071415007140","Winterwerb",null],["071415007502","Diethardt",null],["071415009004","Auel",null],["071415009016","Bornich",null],["071415009023","Dachsenhausen",null],["071415009024","Dahlheim",null],["071415009031","Dörscheid",null],["071415009042","Filsen",null],["071415009066","Kamp-Bornhofen",null],["071415009069","Kaub, Stadt",null],["071415009072","Kestert",null],["071415009079","Lierschied",null],["071415009083","Lykershausen",null],["071415009099","Nochern",null],["071415009108","Osterspai",null],["071415009109","Patersberg",null],["071415009112","Prath",null],["071415009114","Reichenberg",null],["071415009115","Reitzenhain",null],["071415009121","Sankt Goarshausen, Loreleystadt, Stadt",null],["071415009122","Sauerthal",null],["071415009136","Weisel",null],["071415009138","Weyer",null],["071415009501","Braubach, Stadt",null],["071415010003","Attenhausen",null],["071415010006","Bad Ems, Stadt",null],["071415010008","Becheln",null],["071415010025","Dausenau",null],["071415010026","Dessighofen",null],["071415010027","Dienethal",null],["071415010033","Dornholzhausen",null],["071415010041","Fachbach",null],["071415010044","Frücht",null],["071415010046","Geisig",null],["071415010058","Hömberg",null],["071415010071","Kemmenau",null],["071415010082","Lollschied",null],["071415010086","Miellen",null],["071415010087","Misselberg",null],["071415010091","Nassau, Stadt",null],["071415010098","Nievern",null],["071415010103","Obernhof",null],["071415010106","Oberwies",null],["071415010111","Pohl",null],["071415010127","Schweighausen",null],["071415010128","Seelbach",null],["071415010129","Singhofen",null],["071415010132","Sulzbach",null],["071415010135","Weinähr",null],["071415010139","Winden",null],["071415010141","Zimmerschied",null],["071415010201","Arzbach",null],["071415011001","Allendorf",null],["071415011010","Berghausen",null],["071415011011","Berndroth",null],["071415011013","Biebrich",null],["071415011018","Bremberg",null],["071415011020","Burgschwalbach",null],["071415011032","Dörsdorf",null],["071415011034","Ebertshausen",null],["071415011036","Eisighofen",null],["071415011039","Ergeshausen",null],["071415011043","Flacht",null],["071415011050","Gutenacker",null],["071415011051","Hahnstätten",null],["071415011054","Herold",null],["071415011065","Kaltenholzhausen",null],["071415011068","Katzenelnbogen, Stadt",null],["071415011073","Klingelbach",null],["071415011074","Kördorf",null],["071415011081","Lohrheim",null],["071415011088","Mittelfischbach",null],["071415011089","Mudershausen",null],["071415011093","Netzbach",null],["071415011095","Niederneisen",null],["071415011096","Niedertiefenbach",null],["071415011101","Oberfischbach",null],["071415011102","Oberneisen",null],["071415011113","Reckenroth",null],["071415011117","Rettert",null],["071415011118","Roth",null],["071415011125","Schiesheim",null],["071415011126","Schönborn",null],["071435001206","Bad Marienberg (Westerwald), Stadt",null],["071435001211","Bölsberg",null],["071435001216","Dreisbach",null],["071435001222","Fehl-Ritzhausen",null],["071435001227","Großseifen",null],["071435001231","Hahn bei Marienberg",null],["071435001234","Hardt",null],["071435001243","Hof",null],["071435001248","Kirburg",null],["071435001253","Langenbach bei Kirburg",null],["071435001255","Lautzenbrücken",null],["071435001264","Mörlen",null],["071435001270","Neunkhausen",null],["071435001277","Nisterau",null],["071435001279","Nistertal",null],["071435001280","Norken",null],["071435001297","Stockhausen-Illfurth",null],["071435001300","Unnau",null],["071435002202","Alpenrod",null],["071435002204","Astert",null],["071435002205","Atzelgift",null],["071435002212","Borod",null],["071435002215","Dreifelden",null],["071435002223","Gehlert",null],["071435002225","Giesenhausen",null],["071435002229","Hachenburg, Stadt",null],["071435002235","Hattert",null],["071435002236","Heimborn",null],["071435002240","Heuzert",null],["071435002241","Höchstenbach",null],["071435002250","Kroppach",null],["071435002252","Kundert",null],["071435002257","Limbach",null],["071435002258","Linden",null],["071435002259","Lochum",null],["071435002260","Luckenbach",null],["071435002261","Marzhausen",null],["071435002262","Merkelbach",null],["071435002265","Mörsbach",null],["071435002267","Mudenbach",null],["071435002268","Mündersbach",null],["071435002269","Müschenbach",null],["071435002276","Nister",null],["071435002287","Roßbach",null],["071435002294","Steinebach an der Wied",null],["071435002296","Stein-Wingert",null],["071435002299","Streithausen",null],["071435002301","Wahlrod",null],["071435002306","Welkenbach",null],["071435002310","Wied",null],["071435002313","Winkelbach",null],["071435003030","Hilgert",null],["071435003031","Hillscheid",null],["071435003032","Höhr-Grenzhausen, Stadt",null],["071435003040","Kammerforst",null],["071435004005","Boden",null],["071435004008","Daubach",null],["071435004013","Eitelborn",null],["071435004020","Gackenbach",null],["071435004021","Girod",null],["071435004023","Görgeshausen",null],["071435004024","Großholbach",null],["071435004026","Heilberscheid",null],["071435004027","Heiligenroth",null],["071435004033","Holler",null],["071435004034","Horbach",null],["071435004036","Hübingen",null],["071435004039","Kadenbach",null],["071435004048","Montabaur, Stadt",null],["071435004051","Nentershausen",null],["071435004052","Neuhäusel",null],["071435004053","Niederelbert",null],["071435004054","Niedererbach",null],["071435004055","Nomborn",null],["071435004057","Oberelbert",null],["071435004065","Ruppach-Goldhausen",null],["071435004071","Simmern",null],["071435004072","Stahlhofen",null],["071435004077","Untershausen",null],["071435004079","Welschneudorf",null],["071435005001","Alsbach",null],["071435005006","Breitenau",null],["071435005007","Caan",null],["071435005009","Deesen",null],["071435005038","Hundsdorf",null],["071435005050","Nauort",null],["071435005059","Oberhaid",null],["071435005062","Ransbach-Baumbach, Stadt",null],["071435005068","Sessenbach",null],["071435005082","Wirscheid",null],["071435005084","Wittgert",null],["071435006214","Bretthausen",null],["071435006218","Elsoff (Westerwald)",null],["071435006237","Hellenhahn-Schellenberg",null],["071435006244","Homberg",null],["071435006245","Hüblingen",null],["071435006246","Irmtraut",null],["071435006256","Liebenscheid",null],["071435006271","Neunkirchen",null],["071435006272","Neustadt/ Westerwald",null],["071435006274","Niederroßbach",null],["071435006278","Nister-Möhrendorf",null],["071435006282","Oberrod",null],["071435006283","Oberroßbach",null],["071435006285","Rehe",null],["071435006286","Rennerod, Stadt",null],["071435006291","Salzburg",null],["071435006292","Seck",null],["071435006295","Stein-Neukirch",null],["071435006302","Waigandshain",null],["071435006303","Waldmühlen",null],["071435006309","Westernohe",null],["071435006311","Willingen",null],["071435006315","Zehnhausen bei Rennerod",null],["071435007015","Ellenhausen",null],["071435007018","Freilingen",null],["071435007019","Freirachdorf",null],["071435007022","Goddert",null],["071435007025","Hartenfels",null],["071435007029","Herschbach",null],["071435007041","Krümmel",null],["071435007044","Marienrachdorf",null],["071435007045","Maroth",null],["071435007046","Maxsain",null],["071435007056","Nordhofen",null],["071435007061","Quirnbach",null],["071435007064","Rückeroth",null],["071435007066","Schenkelberg",null],["071435007067","Selters (Westerwald), Stadt",null],["071435007069","Sessenhausen",null],["071435007075","Steinen",null],["071435007078","Vielbach",null],["071435007085","Wölferlingen",null],["071435007221","Ewighausen",null],["071435007305","Weidenhahn",null],["071435008011","Dreikirchen",null],["071435008037","Hundsangen",null],["071435008058","Obererbach",null],["071435008074","Steinefrenz",null],["071435008080","Weroth",null],["071435008203","Arnshöfen",null],["071435008208","Berod bei Wallmerod",null],["071435008210","Bilkheim",null],["071435008220","Ettinghausen",null],["071435008232","Hahn am See",null],["071435008239","Herschbach (Oberwesterwald)",null],["071435008251","Kuhnhöfen",null],["071435008263","Meudt",null],["071435008266","Molsberg",null],["071435008273","Niederahr",null],["071435008281","Oberahr",null],["071435008290","Salz",null],["071435008304","Wallmerod",null],["071435008316","Zehnhausen bei Wallmerod",null],["071435008501","Elbingen",null],["071435008502","Mähren",null],["071435009200","Ailertchen",null],["071435009207","Bellingen",null],["071435009209","Berzhahn",null],["071435009213","Brandscheid",null],["071435009219","Enspel",null],["071435009224","Gemünden",null],["071435009226","Girkenroth",null],["071435009228","Guckheim",null],["071435009230","Härtlingen",null],["071435009233","Halbs",null],["071435009238","Hergenroth",null],["071435009242","Höhn",null],["071435009247","Kaden",null],["071435009249","Kölbingen",null],["071435009254","Langenhahn",null],["071435009284","Pottum",null],["071435009288","Rotenhain",null],["071435009289","Rothenbach",null],["071435009293","Stahlhofen am Wiesensee",null],["071435009298","Stockum-Püschen",null],["071435009307","Weltersburg",null],["071435009308","Westerburg, Stadt",null],["071435009312","Willmenrod",null],["071435009314","Winnen",null],["071435010003","Bannberscheid",null],["071435010010","Dernbach (Westerwald)",null],["071435010012","Ebernhahn",null],["071435010028","Helferskirchen",null],["071435010042","Leuterod",null],["071435010047","Mogendorf",null],["071435010049","Moschheim",null],["071435010060","Ötzingen",null],["071435010070","Siershahn",null],["071435010073","Staudt",null],["071435010081","Wirges, Stadt",null],["071435010275","Niedersayn",null],["072110000000","Trier, Stadt",null],["072310134134","Wittlich, Stadt",null],["072310502502","Morbach",null],["072315001008","Bernkastel-Kues, Stadt",null],["072315001012","Brauneberg",null],["072315001016","Burgen",null],["072315001030","Erden",null],["072315001040","Gornhausen",null],["072315001041","Graach an der Mosel",null],["072315001056","Hochscheid",null],["072315001066","Kesten",null],["072315001070","Kleinich",null],["072315001071","Kommen",null],["072315001075","Lieser",null],["072315001076","Lösnich",null],["072315001077","Longkamp",null],["072315001081","Maring-Noviand",null],["072315001086","Minheim",null],["072315001087","Monzelfeld",null],["072315001090","Mülheim an der Mosel",null],["072315001092","Neumagen-Dhron",null],["072315001105","Piesport",null],["072315001125","Ürzig",null],["072315001126","Veldenz",null],["072315001133","Wintrich",null],["072315001136","Zeltingen-Rachtig",null],["072315006006","Berglicht",null],["072315006017","Burtscheid",null],["072315006018","Deuselbach",null],["072315006019","Dhronecken",null],["072315006032","Etgert",null],["072315006035","Gielert",null],["072315006042","Gräfendhron",null],["072315006054","Hilscheid",null],["072315006058","Horath",null],["072315006064","Immert",null],["072315006078","Lückenburg",null],["072315006079","Malborn",null],["072315006083","Merschbach",null],["072315006093","Neunkirchen",null],["072315006112","Rorodt",null],["072315006115","Schönberg",null],["072315006122","Talling",null],["072315006123","Thalfang",null],["072315006202","Breit",null],["072315006203","Büdlich",null],["072315006204","Heidenburg",null],["072315008001","Altrich",null],["072315008003","Arenrath",null],["072315008007","Bergweiler",null],["072315008009","Bettenfeld",null],["072315008010","Binsfeld",null],["072315008013","Bruch",null],["072315008021","Dierfeld",null],["072315008022","Dierscheid",null],["072315008023","Dodenburg",null],["072315008024","Dreis",null],["072315008025","Eckfeld",null],["072315008026","Eisenschmitt",null],["072315008031","Esch",null],["072315008036","Gipperath",null],["072315008037","Gladbach",null],["072315008044","Greimerath",null],["072315008046","Großlittgen",null],["072315008049","Hasborn",null],["072315008050","Heckenmünster",null],["072315008051","Heidweiler",null],["072315008053","Hetzerath",null],["072315008062","Hupperath",null],["072315008065","Karl",null],["072315008069","Klausen",null],["072315008074","Laufeld",null],["072315008080","Manderscheid, Stadt",null],["072315008082","Meerfeld",null],["072315008085","Minderlittgen",null],["072315008091","Musweiler",null],["072315008095","Niederöfflingen",null],["072315008096","Niederscheidweiler",null],["072315008100","Oberöfflingen",null],["072315008101","Oberscheidweiler",null],["072315008103","Osann-Monzel",null],["072315008104","Pantenburg",null],["072315008107","Platten",null],["072315008108","Plein",null],["072315008111","Rivenich",null],["072315008113","Salmtal",null],["072315008114","Schladt",null],["072315008116","Schwarzenborn",null],["072315008117","Sehlem",null],["072315008127","Wallscheid",null],["072315008503","Landscheid",null],["072315008504","Niersbach",null],["072315009004","Bausendorf",null],["072315009005","Bengel",null],["072315009014","Burg (Mosel)",null],["072315009020","Diefenbach",null],["072315009029","Enkirch",null],["072315009033","Flußbach",null],["072315009057","Hontheim",null],["072315009067","Kinderbeuern",null],["072315009068","Kinheim",null],["072315009072","Kröv",null],["072315009110","Reil",null],["072315009120","Starkenburg",null],["072315009124","Traben-Trarbach, Stadt",null],["072315009132","Willwerscheid",null],["072315009206","Lötzbeuren",null],["072315009501","Irmenach",null],["072320018018","Bitburg, Stadt",null],["072325001201","Arzfeld",null],["072325001211","Dackscheid",null],["072325001212","Dahnen",null],["072325001213","Daleiden",null],["072325001214","Dasburg",null],["072325001217","Eilscheid",null],["072325001220","Eschfeld",null],["072325001221","Euscheid",null],["072325001229","Großkampenberg",null],["072325001233","Hargarten",null],["072325001234","Harspelt",null],["072325001240","Herzfeld",null],["072325001245","Irrhausen",null],["072325001246","Jucken",null],["072325001247","Kesfeld",null],["072325001248","Kickeshausen",null],["072325001249","Kinzenburg",null],["072325001253","Krautscheid",null],["072325001254","Lambertsberg",null],["072325001255","Lascheid",null],["072325001258","Lauperath",null],["072325001259","Leidenborn",null],["072325001260","Lichtenborn",null],["072325001261","Lierfeld",null],["072325001262","Lünebach",null],["072325001263","Lützkampen",null],["072325001264","Manderscheid",null],["072325001267","Mauel",null],["072325001270","Merlscheid",null],["072325001277","Niederpierscheid",null],["072325001285","Oberpierscheid",null],["072325001287","Olmscheid",null],["072325001291","Pintesfeld",null],["072325001293","Plütscheid",null],["072325001294","Preischeid",null],["072325001297","Reiff",null],["072325001298","Reipeldingen",null],["072325001301","Roscheid",null],["072325001309","Sengerich",null],["072325001310","Sevenig (Our)",null],["072325001315","Strickscheid",null],["072325001322","Waxweiler",null],["072325001333","Üttfeld",null],["072325005001","Affler",null],["072325005002","Alsdorf",null],["072325005003","Altscheid",null],["072325005004","Ammeldingen an der Our",null],["072325005005","Ammeldingen bei Neuerburg",null],["072325005008","Bauler",null],["072325005011","Berkoth",null],["072325005012","Berscheid",null],["072325005016","Biesdorf",null],["072325005019","Bollendorf",null],["072325005022","Burg",null],["072325005025","Dauwelshausen",null],["072325005028","Echternacherbrück",null],["072325005031","Emmelbaum",null],["072325005033","Ernzen",null],["072325005037","Ferschweiler",null],["072325005038","Fischbach-Oberraden",null],["072325005040","Geichlingen",null],["072325005041","Gemünd",null],["072325005042","Gentingen",null],["072325005047","Heilbach",null],["072325005049","Herbstmühle",null],["072325005053","Holsthum",null],["072325005054","Hommerdingen",null],["072325005056","Hütten",null],["072325005059","Hüttingen bei Lahr",null],["072325005063","Irrel",null],["072325005064","Karlshausen",null],["072325005065","Kaschenbach",null],["072325005066","Keppeshausen",null],["072325005067","Körperich",null],["072325005068","Koxhausen",null],["072325005069","Kruchten",null],["072325005072","Lahr",null],["072325005073","Leimbach",null],["072325005078","Menningen",null],["072325005080","Mettendorf",null],["072325005082","Minden",null],["072325005084","Muxerath",null],["072325005085","Nasingen",null],["072325005088","Neuerburg, Stadt",null],["072325005089","Niedergeckler",null],["072325005090","Niederraden",null],["072325005093","Niederweis",null],["072325005094","Niehl",null],["072325005095","Nusbaum",null],["072325005096","Obergeckler",null],["072325005102","Utscheid",null],["072325005103","Peffingen",null],["072325005106","Plascheid",null],["072325005108","Prümzurlay",null],["072325005110","Rodershausen",null],["072325005112","Roth an der Our",null],["072325005114","Schankweiler",null],["072325005116","Scheitenkorb",null],["072325005117","Scheuern",null],["072325005121","Sevenig bei Neuerburg",null],["072325005122","Sinspelt",null],["072325005127","Übereisenbach",null],["072325005128","Uppershausen",null],["072325005130","Waldhof-Falkenstein",null],["072325005131","Wallendorf",null],["072325005132","Weidingen",null],["072325005138","Zweifelscheid",null],["072325005218","Eisenach",null],["072325005225","Gilzem",null],["072325006202","Auw bei Prüm",null],["072325006206","Bleialf",null],["072325006207","Brandscheid",null],["072325006208","Buchet",null],["072325006209","Büdesheim",null],["072325006216","Dingdorf",null],["072325006222","Feuerscheid",null],["072325006223","Fleringen",null],["072325006224","Giesdorf",null],["072325006226","Weinsheim",null],["072325006227","Gondenbrett",null],["072325006230","Großlangenfeld",null],["072325006231","Habscheid",null],["072325006236","Heckhuscheid",null],["072325006238","Heisdorf",null],["072325006250","Kleinlangenfeld",null],["072325006256","Lasel",null],["072325006265","Masthorn",null],["072325006266","Matzerath",null],["072325006271","Mützenich",null],["072325006272","Neuendorf",null],["072325006276","Niederlauch",null],["072325006279","Nimshuscheid",null],["072325006280","Nimsreuland",null],["072325006283","Oberlascheid",null],["072325006284","Oberlauch",null],["072325006288","Olzheim",null],["072325006290","Orlenbach",null],["072325006292","Pittenbach",null],["072325006295","Pronsfeld",null],["072325006296","Prüm, Stadt",null],["072325006300","Rommersheim",null],["072325006302","Roth bei Prüm",null],["072325006304","Schönecken",null],["072325006305","Schwirzheim",null],["072325006307","Seiwerath",null],["072325006308","Sellerich",null],["072325006318","Wallersheim",null],["072325006320","Watzerath",null],["072325006321","Wawern",null],["072325006327","Winringen",null],["072325006328","Winterscheid",null],["072325006329","Winterspelt",null],["072325006332","Hersdorf",null],["072325007006","Auw an der Kyll",null],["072325007010","Beilingen",null],["072325007050","Herforst",null],["072325007055","Hosten",null],["072325007104","Philippsheim",null],["072325007107","Preist",null],["072325007123","Speicher, Stadt",null],["072325007289","Orenhofen",null],["072325007311","Spangdahlem",null],["072325008007","Badem",null],["072325008009","Baustert",null],["072325008013","Bettingen",null],["072325008014","Bickendorf",null],["072325008015","Biersdorf am See",null],["072325008017","Birtlingen",null],["072325008020","Brecht",null],["072325008024","Dahlem",null],["072325008026","Dockendorf",null],["072325008027","Dudeldorf",null],["072325008029","Echtershausen",null],["072325008030","Ehlenz",null],["072325008032","Enzen",null],["072325008034","Eßlingen",null],["072325008035","Etteldorf",null],["072325008036","Feilsdorf",null],["072325008039","Fließem",null],["072325008043","Gindorf",null],["072325008044","Gondorf",null],["072325008045","Halsdorf",null],["072325008046","Hamm",null],["072325008048","Heilenbach",null],["072325008057","Hütterscheid",null],["072325008058","Hüttingen an der Kyll",null],["072325008060","Idenheim",null],["072325008061","Idesheim",null],["072325008062","Ingendorf",null],["072325008070","Kyllburg, Stadt",null],["072325008071","Kyllburgweiler",null],["072325008074","Ließem",null],["072325008075","Malberg",null],["072325008076","Malbergweich",null],["072325008077","Meckel",null],["072325008079","Messerich",null],["072325008081","Metterich",null],["072325008083","Mülbach",null],["072325008086","Nattenheim",null],["072325008087","Neidenbach",null],["072325008091","Niederstedem",null],["072325008092","Niederweiler",null],["072325008097","Oberstedem",null],["072325008098","Oberweiler",null],["072325008099","Oberweis",null],["072325008100","Olsdorf",null],["072325008101","Orsfeld",null],["072325008105","Pickließem",null],["072325008109","Rittersdorf",null],["072325008111","Röhl",null],["072325008113","Sankt Thomas",null],["072325008115","Scharfbillig",null],["072325008118","Schleid",null],["072325008119","Seffern",null],["072325008120","Sefferweich",null],["072325008124","Stockem",null],["072325008125","Sülm",null],["072325008126","Trimport",null],["072325008129","Usch",null],["072325008133","Wettlingen",null],["072325008134","Wiersdorf",null],["072325008135","Wilsecker",null],["072325008137","Wolsfeld",null],["072325008203","Balesfeld",null],["072325008210","Burbach",null],["072325008228","Gransdorf",null],["072325008273","Neuheilenbach",null],["072325008282","Oberkail",null],["072325008306","Seinsfeld",null],["072325008313","Steinborn",null],["072325008331","Zendscheid",null],["072325008501","Wißmannsdorf",null],["072325008502","Brimingen",null],["072335001006","Betteldorf",null],["072335001008","Bleckhausen",null],["072335001011","Brockscheid",null],["072335001014","Darscheid",null],["072335001016","Demerath",null],["072335001017","Deudesfeld",null],["072335001018","Dockweiler",null],["072335001020","Dreis-Brück",null],["072335001021","Ellscheid",null],["072335001025","Gefell",null],["072335001027","Gillenfeld",null],["072335001030","Hinterweiler",null],["072335001031","Hörscheid",null],["072335001034","Immerath",null],["072335001039","Kirchweiler",null],["072335001040","Kradenbach",null],["072335001042","Mehren",null],["072335001043","Meisburg",null],["072335001046","Mückeln",null],["072335001049","Nerdlen",null],["072335001052","Niederstadtfeld",null],["072335001055","Oberstadtfeld",null],["072335001061","Sarmersbach",null],["072335001062","Saxler",null],["072335001063","Schalkenmehren",null],["072335001064","Schönbach",null],["072335001065","Schutz",null],["072335001067","Steineberg",null],["072335001068","Steiningen",null],["072335001070","Strohn",null],["072335001071","Strotzbüsch",null],["072335001074","Udler",null],["072335001075","Üdersdorf",null],["072335001077","Utzerath",null],["072335001079","Wallenborn",null],["072335001081","Weidenbach",null],["072335001084","Winkel (Eifel)",null],["072335001501","Daun, Stadt",null],["072335004003","Beinhausen",null],["072335004010","Boxberg",null],["072335004032","Hörschhausen",null],["072335004037","Katzwinkel",null],["072335004048","Neichen",null],["072335004201","Arbach",null],["072335004202","Bereborn",null],["072335004203","Berenbach",null],["072335004205","Bodenbach",null],["072335004206","Bongard",null],["072335004207","Borler",null],["072335004208","Brücktal",null],["072335004210","Drees",null],["072335004212","Gelenberg",null],["072335004213","Gunderath",null],["072335004215","Höchstberg",null],["072335004216","Horperath",null],["072335004217","Kaperich",null],["072335004218","Kelberg",null],["072335004220","Kirsbach",null],["072335004221","Kötterichen",null],["072335004222","Kolverath",null],["072335004224","Lirstal",null],["072335004225","Mannebach",null],["072335004226","Mosbruch",null],["072335004228","Nitz",null],["072335004230","Oberelz",null],["072335004233","Reimerath",null],["072335004234","Retterath",null],["072335004236","Sassen",null],["072335004242","Uersfeld",null],["072335004243","Ueß",null],["072335004244","Welcherath",null],["072335006002","Basberg",null],["072335006004","Berlingen",null],["072335006005","Berndorf",null],["072335006007","Birgel",null],["072335006019","Dohm-Lammersdorf",null],["072335006022","Esch",null],["072335006023","Feusdorf",null],["072335006026","Gerolstein, Stadt",null],["072335006028","Gönnersdorf",null],["072335006029","Hillesheim, Stadt",null],["072335006033","Hohenfels-Essingen",null],["072335006035","Jünkerath",null],["072335006036","Kalenborn-Scheuern",null],["072335006038","Kerpen (Eifel)",null],["072335006041","Lissendorf",null],["072335006050","Neroth",null],["072335006053","Oberbettingen",null],["072335006054","Oberehe-Stroheich",null],["072335006056","Pelm",null],["072335006058","Rockeskyll",null],["072335006060","Salm",null],["072335006076","Üxheim",null],["072335006080","Walsdorf",null],["072335006083","Wiesbaum",null],["072335006204","Birresborn",null],["072335006209","Densborn",null],["072335006211","Duppach",null],["072335006214","Hallschlag",null],["072335006219","Kerschenbach",null],["072335006223","Kopp",null],["072335006227","Mürlenbach",null],["072335006229","Nohn",null],["072335006232","Ormont",null],["072335006235","Reuth",null],["072335006237","Scheid",null],["072335006239","Schüller",null],["072335006240","Stadtkyll",null],["072335006241","Steffeln",null],["072355001005","Bescheid",null],["072355001008","Beuren (Hochwald)",null],["072355001014","Damflos",null],["072355001030","Geisfeld",null],["072355001035","Grimburg",null],["072355001036","Gusenburg",null],["072355001045","Hermeskeil, Stadt",null],["072355001047","Hinzert-Pölert",null],["072355001092","Naurath (Wald)",null],["072355001093","Neuhütten",null],["072355001112","Rascheid",null],["072355001114","Reinsfeld",null],["072355001153","Züsch",null],["072355003055","Kanzem",null],["072355003068","Konz, Stadt",null],["072355003095","Nittel",null],["072355003096","Oberbillig",null],["072355003101","Onsdorf",null],["072355003106","Pellingen",null],["072355003132","Tawern",null],["072355003133","Temmels",null],["072355003143","Wasserliesch",null],["072355003144","Wawern",null],["072355003146","Wellen",null],["072355003148","Wiltingen",null],["072355004010","Bonerath",null],["072355004021","Farschweiler",null],["072355004037","Gusterath",null],["072355004038","Gutweiler",null],["072355004044","Herl",null],["072355004046","Hinzenburg",null],["072355004050","Holzerath",null],["072355004056","Kasel",null],["072355004070","Korlingen",null],["072355004080","Lorscheid",null],["072355004085","Mertesdorf",null],["072355004090","Morscheid",null],["072355004100","Ollmuth",null],["072355004103","Osburg",null],["072355004107","Pluwig",null],["072355004116","Riveris",null],["072355004124","Schöndorf",null],["072355004129","Sommerau",null],["072355004135","Thomm",null],["072355004141","Waldrach",null],["072355006004","Bekond",null],["072355006015","Detzem",null],["072355006019","Ensch",null],["072355006022","Fell",null],["072355006026","Föhren",null],["072355006060","Kenn",null],["072355006063","Klüsserath",null],["072355006067","Köwerich",null],["072355006074","Leiwen",null],["072355006077","Longen",null],["072355006078","Longuich",null],["072355006083","Mehring",null],["072355006091","Naurath (Eifel)",null],["072355006108","Pölich",null],["072355006115","Riol",null],["072355006120","Schleich",null],["072355006125","Schweich, Stadt",null],["072355006134","Thörnich",null],["072355006207","Trittenheim",null],["072355007001","Aach",null],["072355007027","Franzenheim",null],["072355007048","Hockweiler",null],["072355007051","Igel",null],["072355007069","Kordel",null],["072355007073","Langsur",null],["072355007094","Newel",null],["072355007111","Ralingen",null],["072355007137","Trierweiler",null],["072355007151","Zemmer",null],["072355007501","Welschbillig",null],["072355008002","Ayl",null],["072355008003","Baldringen",null],["072355008025","Fisch",null],["072355008028","Freudenburg",null],["072355008033","Greimerath",null],["072355008040","Heddert",null],["072355008043","Hentern",null],["072355008052","Irsch",null],["072355008057","Kastel-Staadt",null],["072355008058","Kell am See",null],["072355008062","Kirf",null],["072355008072","Lampaden",null],["072355008081","Mandern",null],["072355008082","Mannebach",null],["072355008098","Ockfen",null],["072355008104","Palzem",null],["072355008105","Paschel",null],["072355008118","Saarburg, Stadt",null],["072355008119","Schillingen",null],["072355008122","Schoden",null],["072355008123","Schömerich",null],["072355008126","Serrig",null],["072355008131","Taben-Rodt",null],["072355008136","Trassem",null],["072355008140","Vierherrenborn",null],["072355008142","Waldweiler",null],["072355008149","Wincheringen",null],["072355008152","Zerf",null],["072355008154","Merzkirchen",null],["073110000000","Frankenthal (Pfalz), Stadt",null],["073120000000","Kaiserslautern, Stadt",null],["073130000000","Landau in der Pfalz, Stadt",null],["073140000000","Ludwigshafen am Rhein, Stadt",null],["073150000000","Mainz, Stadt",null],["073160000000","Neustadt an der Weinstraße, Stadt",null],["073170000000","Pirmasens, Stadt",null],["073180000000","Speyer, Stadt",null],["073190000000","Worms, Stadt",null],["073200000000","Zweibrücken, Stadt",null],["073310003003","Alzey, Stadt",null],["073315001001","Albig",null],["073315001005","Bechenheim",null],["073315001007","Bechtolsheim",null],["073315001008","Bermersheim vor der Höhe",null],["073315001010","Biebelnheim",null],["073315001012","Bornheim",null],["073315001014","Dintesheim",null],["073315001020","Eppelsheim",null],["073315001021","Erbes-Büdesheim",null],["073315001022","Esselborn",null],["073315001024","Flomborn",null],["073315001025","Flonheim",null],["073315001026","Framersheim",null],["073315001027","Freimersheim",null],["073315001031","Gau-Heppenheim",null],["073315001032","Gau-Odernheim",null],["073315001042","Kettenheim",null],["073315001043","Lonsheim",null],["073315001044","Mauchenheim",null],["073315001050","Nack",null],["073315001051","Nieder-Wiesen",null],["073315001052","Ober-Flörsheim",null],["073315001053","Offenheim",null],["073315001067","Wahlheim",null],["073315002002","Alsheim",null],["073315002018","Eich",null],["073315002034","Gimbsheim",null],["073315002038","Hamm am Rhein",null],["073315002045","Mettenheim",null],["073315003023","Flörsheim-Dalsheim",null],["073315003041","Hohen-Sülzen",null],["073315003046","Mölsheim",null],["073315003047","Mörstadt",null],["073315003048","Monsheim",null],["073315003054","Offstein",null],["073315003066","Wachenheim",null],["073315005017","Eckelsheim",null],["073315005030","Gau-Bickelheim",null],["073315005035","Gumbsheim",null],["073315005060","Siefersheim",null],["073315005062","Stein-Bockenheim",null],["073315005070","Wendelsheim",null],["073315005072","Wöllstein",null],["073315005075","Wonsheim",null],["073315006004","Armsheim",null],["073315006019","Ensheim",null],["073315006029","Gabsheim",null],["073315006033","Gau-Weinheim",null],["073315006056","Partenheim",null],["073315006058","Saulheim",null],["073315006059","Schornsheim",null],["073315006061","Spiesheim",null],["073315006063","Sulzheim",null],["073315006064","Udenheim",null],["073315006065","Vendersheim",null],["073315006068","Wallertheim",null],["073315006073","Wörrstadt, Stadt",null],["073315007006","Bechtheim",null],["073315007009","Bermersheim",null],["073315007011","Hochborn",null],["073315007015","Dittelsheim-Heßloch",null],["073315007028","Frettenheim",null],["073315007036","Gundersheim",null],["073315007037","Gundheim",null],["073315007039","Hangen-Weisheim",null],["073315007049","Monzernheim",null],["073315007055","Osthofen, Stadt",null],["073315007071","Westhofen",null],["073320002002","Bad Dürkheim, Stadt",null],["073320024024","Grünstadt, Stadt",null],["073320025025","Haßloch",null],["073325001009","Deidesheim, Stadt",null],["073325001017","Forst an der Weinstraße",null],["073325001035","Meckenheim",null],["073325001039","Niederkirchen bei Deidesheim",null],["073325001043","Ruppertsberg",null],["073325002005","Bobenheim am Berg",null],["073325002008","Dackenheim",null],["073325002015","Erpolzheim",null],["073325002019","Freinsheim, Stadt",null],["073325002026","Herxheim am Berg",null],["073325002028","Kallstadt",null],["073325002049","Weisenheim am Berg",null],["073325002050","Weisenheim am Sand",null],["073325005014","Elmstein",null],["073325005016","Esthal",null],["073325005018","Frankeneck",null],["073325005032","Lambrecht (Pfalz), Stadt",null],["073325005034","Lindenberg",null],["073325005037","Neidenfels",null],["073325005048","Weidenthal",null],["073325006013","Ellerstadt",null],["073325006020","Friedelsheim",null],["073325006022","Gönnheim",null],["073325006046","Wachenheim an der Weinstraße, Stadt",null],["073325007001","Altleiningen",null],["073325007003","Battenberg (Pfalz)",null],["073325007004","Bissersheim",null],["073325007006","Bockenheim an der Weinstraße",null],["073325007007","Carlsberg",null],["073325007010","Dirmstein",null],["073325007012","Ebertsheim",null],["073325007021","Gerolsheim",null],["073325007023","Großkarlbach",null],["073325007027","Hettenleidelheim",null],["073325007029","Kindenheim",null],["073325007030","Kirchheim an der Weinstraße",null],["073325007031","Kleinkarlbach",null],["073325007033","Laumersheim",null],["073325007036","Mertesheim",null],["073325007038","Neuleiningen",null],["073325007040","Obersülzen",null],["073325007041","Obrigheim (Pfalz)",null],["073325007042","Quirnheim",null],["073325007044","Tiefenthal",null],["073325007047","Wattenheim",null],["073335002019","Eisenberg (Pfalz), Stadt",null],["073335002038","Kerzenheim",null],["073335002060","Ramsen",null],["073335003001","Albisheim (Pfrimm)",null],["073335003006","Biedesheim",null],["073335003012","Bubenheim",null],["073335003017","Dreisen",null],["073335003018","Einselthum",null],["073335003026","Göllheim",null],["073335003032","Immesheim",null],["073335003041","Lautersheim",null],["073335003058","Ottersheim",null],["073335003064","Rüssingen",null],["073335003074","Standenbühl",null],["073335003081","Weitersweiler",null],["073335003501","Zellertal",null],["073335004005","Bennhausen",null],["073335004007","Bischheim",null],["073335004010","Bolanden",null],["073335004013","Dannenfels",null],["073335004022","Gauersheim",null],["073335004031","Ilbesheim",null],["073335004035","Jakobsweiler",null],["073335004039","Kirchheimbolanden, Stadt",null],["073335004040","Kriegsfeld",null],["073335004045","Marnheim",null],["073335004046","Mörsfeld",null],["073335004047","Morschheim",null],["073335004056","Oberwiesen",null],["073335004057","Orbis",null],["073335004062","Rittersheim",null],["073335004076","Stetten",null],["073335006009","Börrstadt",null],["073335006011","Breunigweiler",null],["073335006020","Falkenstein",null],["073335006027","Gonbach",null],["073335006030","Höringen",null],["073335006033","Imsbach",null],["073335006042","Lohnsfeld",null],["073335006048","Münchweiler an der Alsenz",null],["073335006069","Schweisweiler",null],["073335006071","Sippersfeld",null],["073335006075","Steinbach am Donnersberg",null],["073335006080","Wartenberg-Rohrbach",null],["073335006503","Winnweiler",null],["073335007003","Alsenz",null],["073335007004","Bayerfeld-Steckweiler",null],["073335007008","Bisterschied",null],["073335007014","Dielkirchen",null],["073335007016","Dörrmoschel",null],["073335007021","Finkenbach-Gersweiler",null],["073335007023","Gaugrehweiler",null],["073335007024","Gehrweiler",null],["073335007025","Gerbach",null],["073335007028","Gundersweiler",null],["073335007034","Imsweiler",null],["073335007036","Kalkofen",null],["073335007037","Katzenbach",null],["073335007043","Mannweiler-Cölln",null],["073335007049","Münsterappel",null],["073335007050","Niederhausen an der Appel",null],["073335007051","Niedermoschel",null],["073335007053","Oberhausen an der Appel",null],["073335007054","Obermoschel, Stadt",null],["073335007055","Oberndorf",null],["073335007061","Ransweiler",null],["073335007065","Ruppertsecken",null],["073335007066","Sankt Alban",null],["073335007067","Schiersfeld",null],["073335007068","Schönborn",null],["073335007072","Sitters",null],["073335007073","Stahlberg",null],["073335007077","Teschenmoschel",null],["073335007078","Unkenbach",null],["073335007079","Waldgrehweiler",null],["073335007083","Winterborn",null],["073335007084","Würzweiler",null],["073335007201","Rathskirchen",null],["073335007202","Reichsthal",null],["073335007203","Seelen",null],["073335007502","Rockenhausen, Stadt",null],["073340007007","Germersheim, Stadt",null],["073340501501","Wörth am Rhein, Stadt",null],["073345001001","Bellheim",null],["073345001014","Knittelsheim",null],["073345001023","Ottersheim bei Landau",null],["073345001036","Zeiskam",null],["073345002002","Berg (Pfalz)",null],["073345002008","Hagenbach, Stadt",null],["073345002021","Neuburg am Rhein",null],["073345002027","Scheibenhardt",null],["073345003009","Hatzenbühl",null],["073345003012","Jockgrim",null],["073345003022","Neupotz",null],["073345003024","Rheinzabern",null],["073345004004","Erlenbach bei Kandel",null],["073345004005","Freckenfeld",null],["073345004013","Kandel, Stadt",null],["073345004020","Minfeld",null],["073345004030","Steinweiler",null],["073345004031","Vollmersweiler",null],["073345004034","Winden",null],["073345005006","Freisbach",null],["073345005017","Lingenfeld",null],["073345005018","Lustadt",null],["073345005028","Schwegenheim",null],["073345005032","Weingarten (Pfalz)",null],["073345005033","Westheim (Pfalz)",null],["073345006011","Hördt",null],["073345006015","Kuhardt",null],["073345006016","Leimersheim",null],["073345006025","Rülzheim",null],["073355001003","Bruchmühlbach-Miesau",null],["073355001011","Gerhardsbrunn",null],["073355001201","Lambsborn",null],["073355001202","Langwieden",null],["073355001203","Martinshöhe",null],["073355002004","Enkenbach-Alsenborn",null],["073355002007","Fischbach",null],["073355002010","Frankenstein",null],["073355002015","Hochspeyer",null],["073355002026","Mehlingen",null],["073355002028","Neuhemsbach",null],["073355002048","Waldleiningen",null],["073355002205","Sembach",null],["073355008016","Hütschenhausen",null],["073355008020","Kottweiler-Schwanden",null],["073355008030","Niedermohr",null],["073355008038","Ramstein-Miesenbach, Stadt",null],["073355008044","Steinwenden",null],["073355009005","Erzenhausen",null],["073355009006","Eulenbis",null],["073355009019","Kollweiler",null],["073355009024","Mackenbach",null],["073355009040","Rodenbach",null],["073355009043","Schwedelbach",null],["073355009049","Weilerbach",null],["073355009501","Reichenbach-Steegen",null],["073355010009","Frankelbach",null],["073355010013","Heiligenmoschel",null],["073355010014","Hirschhorn/ Pfalz",null],["073355010017","Katzweiler",null],["073355010025","Mehlbach",null],["073355010029","Niederkirchen",null],["073355010033","Olsbrücken",null],["073355010034","Otterbach",null],["073355010035","Otterberg, Stadt",null],["073355010041","Schallodenbach",null],["073355010042","Schneckenhausen",null],["073355010046","Sulzbachtal",null],["073355011002","Bann",null],["073355011012","Hauptstuhl",null],["073355011018","Kindsbach",null],["073355011021","Krickenbach",null],["073355011022","Landstuhl, Sickingenstadt, Stadt",null],["073355011023","Linden",null],["073355011027","Mittelbrunn",null],["073355011031","Oberarnbach",null],["073355011037","Queidersbach",null],["073355011045","Stelzenberg",null],["073355011047","Trippstadt",null],["073355011204","Schopp",null],["073365008001","Adenbach",null],["073365008005","Aschbach",null],["073365008012","Buborn",null],["073365008013","Cronenberg",null],["073365008014","Deimberg",null],["073365008019","Einöllen",null],["073365008023","Eßweiler",null],["073365008029","Ginsweiler",null],["073365008030","Glanbrücken",null],["073365008033","Grumbach",null],["073365008035","Hausweiler",null],["073365008036","Hefersweiler",null],["073365008038","Heinzenhausen",null],["073365008040","Herren-Sulzbach",null],["073365008042","Hinzweiler",null],["073365008043","Hohenöllen",null],["073365008044","Homberg",null],["073365008045","Hoppstädten",null],["073365008048","Jettenbach",null],["073365008049","Kappeln",null],["073365008050","Kirrweiler",null],["073365008053","Kreimbach-Kaulbach",null],["073365008057","Langweiler",null],["073365008058","Lauterecken, Stadt",null],["073365008060","Lohnweiler",null],["073365008061","Medard",null],["073365008062","Merzweiler",null],["073365008065","Nerzweiler",null],["073365008069","Nußbach",null],["073365008072","Oberweiler im Tal",null],["073365008073","Oberweiler-Tiefenbach",null],["073365008074","Odenbach",null],["073365008075","Offenbach-Hundheim",null],["073365008085","Reipoltskirchen",null],["073365008086","Relsberg",null],["073365008087","Rothselberg",null],["073365008090","Rutsweiler an der Lauter",null],["073365008095","Sankt Julian",null],["073365008100","Unterjeckenbach",null],["073365008104","Wiesweiler",null],["073365008105","Wolfstein, Stadt",null],["073365009004","Altenkirchen",null],["073365009008","Börsborn",null],["073365009010","Breitenbach",null],["073365009011","Brücken (Pfalz)",null],["073365009016","Dittweiler",null],["073365009017","Dunzweiler",null],["073365009027","Frohnhofen",null],["073365009031","Glan-Münchweiler",null],["073365009032","Gries",null],["073365009037","Henschtal",null],["073365009041","Herschweiler-Pettersheim",null],["073365009047","Hüffler",null],["073365009054","Krottelbach",null],["073365009056","Langenbach",null],["073365009064","Nanzdietschweiler",null],["073365009076","Ohmbach",null],["073365009082","Rehweiler",null],["073365009092","Schönenberg-Kübelberg",null],["073365009096","Steinbach am Glan",null],["073365009101","Wahnwegen",null],["073365009102","Waldmohr, Stadt",null],["073365009107","Matzenbach",null],["073365009501","Quirnbach/ Pfalz",null],["073365010002","Albessen",null],["073365010003","Altenglan",null],["073365010006","Blaubach",null],["073365010009","Bosenbach",null],["073365010015","Dennweiler-Frohnbach",null],["073365010018","Ehweiler",null],["073365010021","Elzweiler",null],["073365010022","Erdesbach",null],["073365010024","Etschberg",null],["073365010025","Föckelberg",null],["073365010034","Haschbach am Remigiusberg",null],["073365010039","Herchweiler",null],["073365010046","Horschbach",null],["073365010051","Körborn",null],["073365010052","Konken",null],["073365010055","Kusel, Stadt",null],["073365010066","Neunkirchen am Potzberg",null],["073365010067","Niederalben",null],["073365010068","Niederstaufenbach",null],["073365010070","Oberalben",null],["073365010071","Oberstaufenbach",null],["073365010077","Pfeffelbach",null],["073365010079","Rammelsbach",null],["073365010081","Rathsweiler",null],["073365010084","Reichweiler",null],["073365010088","Ruthweiler",null],["073365010089","Rutsweiler am Glan",null],["073365010091","Schellweiler",null],["073365010094","Selchenbach",null],["073365010097","Thallichtenberg",null],["073365010098","Theisbergstegen",null],["073365010099","Ulmet",null],["073365010103","Welchweiler",null],["073365010106","Bedesbach",null],["073375001001","Albersweiler",null],["073375001017","Dernbach",null],["073375001024","Eußerthal",null],["073375001033","Gossersweiler-Stein",null],["073375001054","Münchweiler am Klingbach",null],["073375001064","Ramberg",null],["073375001067","Rinnthal",null],["073375001074","Silz",null],["073375001078","Völkersweiler",null],["073375001080","Waldhambach",null],["073375001081","Waldrohrbach",null],["073375001083","Wernersberg",null],["073375001501","Annweiler am Trifels, Stadt",null],["073375002005","Bad Bergzabern, Stadt",null],["073375002006","Barbelroth",null],["073375002008","Birkenhördt",null],["073375002013","Böllenborn",null],["073375002018","Dierbach",null],["073375002019","Dörrenbach",null],["073375002029","Gleiszellen-Gleishorbach",null],["073375002037","Hergersweiler",null],["073375002045","Kapellen-Drusweiler",null],["073375002046","Kapsweyer",null],["073375002049","Klingenmünster",null],["073375002055","Niederhorbach",null],["073375002056","Niederotterbach",null],["073375002058","Oberhausen",null],["073375002059","Oberotterbach",null],["073375002060","Oberschlettenbach",null],["073375002062","Pleisweiler-Oberhofen",null],["073375002071","Schweigen-Rechtenbach",null],["073375002072","Schweighofen",null],["073375002076","Steinfeld",null],["073375002079","Vorderweidenthal",null],["073375003002","Altdorf",null],["073375003011","Böbingen",null],["073375003015","Burrweiler",null],["073375003020","Edenkoben, Stadt",null],["073375003021","Edesheim",null],["073375003025","Flemlingen",null],["073375003027","Freimersheim (Pfalz)",null],["073375003028","Gleisweiler",null],["073375003032","Gommersheim",null],["073375003035","Großfischlingen",null],["073375003036","Hainfeld",null],["073375003048","Kleinfischlingen",null],["073375003066","Rhodt unter Rietburg",null],["073375003069","Roschbach",null],["073375003077","Venningen",null],["073375003084","Weyher in der Pfalz",null],["073375004038","Herxheim bei Landau/ Pfalz",null],["073375004039","Herxheimweyher",null],["073375004044","Insheim",null],["073375004068","Rohrbach",null],["073375005007","Billigheim-Ingenheim",null],["073375005009","Birkweiler",null],["073375005012","Böchingen",null],["073375005022","Eschbach",null],["073375005026","Frankweiler",null],["073375005031","Göcklingen",null],["073375005040","Heuchelheim-Klingen",null],["073375005042","Ilbesheim bei Landau in der Pfalz",null],["073375005043","Impflingen",null],["073375005050","Knöringen",null],["073375005051","Leinsweiler",null],["073375005065","Ranschbach",null],["073375005073","Siebeldingen",null],["073375005082","Walsheim",null],["073375006047","Kirrweiler (Pfalz)",null],["073375006052","Maikammer",null],["073375006070","Sankt Martin",null],["073375007014","Bornheim",null],["073375007023","Essingen",null],["073375007041","Hochstadt (Pfalz)",null],["073375007061","Offenbach an der Queich",null],["073380004004","Bobenheim-Roxheim",null],["073380005005","Böhl-Iggelheim",null],["073380017017","Limburgerhof",null],["073380019019","Mutterstadt",null],["073380025025","Schifferstadt, Stadt",null],["073385001006","Dannstadt-Schauernheim",null],["073385001014","Hochdorf-Assenheim",null],["073385001022","Rödersheim-Gronau",null],["073385004003","Birkenheide",null],["073385004008","Fußgönheim",null],["073385004018","Maxdorf",null],["073385006002","Beindersheim",null],["073385006009","Großniedesheim",null],["073385006012","Heßheim",null],["073385006013","Heuchelheim bei Frankenthal",null],["073385006015","Kleinniedesheim",null],["073385006016","Lambsheim",null],["073385007007","Dudenhofen",null],["073385007010","Hanhofen",null],["073385007011","Harthausen",null],["073385007023","Römerberg",null],["073385008001","Altrip",null],["073385008020","Neuhofen",null],["073385008021","Otterstadt",null],["073385008026","Waldsee",null],["073390005005","Bingen am Rhein, Stadt",null],["073390009009","Budenheim",null],["073390030030","Ingelheim am Rhein, Stadt",null],["073395001003","Bacharach, Stadt",null],["073395001007","Breitscheid",null],["073395001036","Manubach",null],["073395001038","Münster-Sarmsheim",null],["073395001040","Niederheimbach",null],["073395001044","Oberdiebach",null],["073395001045","Oberheimbach",null],["073395001058","Trechtingshausen",null],["073395001062","Waldalgesheim",null],["073395001063","Weiler bei Bingen",null],["073395002006","Bodenheim",null],["073395002020","Gau-Bischofsheim",null],["073395002026","Harxheim",null],["073395002034","Lörzweiler",null],["073395002039","Nackenheim",null],["073395003001","Appenheim",null],["073395003008","Bubenheim",null],["073395003016","Engelstadt",null],["073395003019","Gau-Algesheim, Stadt",null],["073395003041","Nieder-Hilbersheim",null],["073395003046","Ober-Hilbersheim",null],["073395003048","Ockenheim",null],["073395003051","Schwabenheim an der Selz",null],["073395006017","Essenheim",null],["073395006031","Jugenheim in Rheinhessen",null],["073395006032","Klein-Winternheim",null],["073395006042","Nieder-Olm, Stadt",null],["073395006047","Ober-Olm",null],["073395006054","Sörgenloch",null],["073395006057","Stadecken-Elsheim",null],["073395006067","Zornheim",null],["073395007010","Dalheim",null],["073395007011","Dexheim",null],["073395007012","Dienheim",null],["073395007013","Dolgesheim",null],["073395007015","Eimsheim",null],["073395007018","Friesenheim",null],["073395007024","Guntersblum",null],["073395007025","Hahnheim",null],["073395007028","Hillesheim",null],["073395007033","Köngernheim",null],["073395007035","Ludwigshöhe",null],["073395007037","Mommenheim",null],["073395007043","Nierstein, Stadt",null],["073395007049","Oppenheim, Stadt",null],["073395007053","Selzen",null],["073395007059","Uelversheim",null],["073395007060","Undenheim",null],["073395007064","Weinolsheim",null],["073395007066","Wintersheim",null],["073395007201","Dorn-Dürkheim",null],["073395008002","Aspisheim",null],["073395008004","Badenheim",null],["073395008021","Gensingen",null],["073395008022","Grolsheim",null],["073395008029","Horrweiler",null],["073395008050","Sankt Johann",null],["073395008056","Sprendlingen",null],["073395008065","Welgesheim",null],["073395008068","Zotzenheim",null],["073395008202","Wolfsheim",null],["073405001001","Bobenthal",null],["073405001002","Busenberg",null],["073405001004","Dahn, Stadt",null],["073405001009","Erfweiler",null],["073405001010","Erlenbach bei Dahn",null],["073405001011","Fischbach bei Dahn",null],["073405001021","Hirschthal",null],["073405001029","Ludwigswinkel",null],["073405001033","Niederschlettenbach",null],["073405001034","Nothweiler",null],["073405001039","Rumbach",null],["073405001043","Schindhard",null],["073405001045","Schönau (Pfalz)",null],["073405001501","Bruchweiler-Bärenbach",null],["073405001502","Bundenthal",null],["073405002005","Darstein",null],["073405002006","Dimbach",null],["073405002014","Hauenstein",null],["073405002020","Hinterweidenthal",null],["073405002030","Lug",null],["073405002047","Schwanheim",null],["073405002049","Spirkelbach",null],["073405002057","Wilgartswiesen",null],["073405003008","Eppenbrunn",null],["073405003019","Hilst",null],["073405003026","Kröppen",null],["073405003028","Lemberg",null],["073405003036","Obersimten",null],["073405003040","Ruppertsweiler",null],["073405003048","Schweix",null],["073405003052","Trulben",null],["073405003053","Vinningen",null],["073405003205","Bottenbach",null],["073405004003","Clausen",null],["073405004007","Donsieders",null],["073405004027","Leimen",null],["073405004031","Merzalben",null],["073405004032","Münchweiler an der Rodalb",null],["073405004038","Rodalben, Stadt",null],["073405006012","Geiselberg",null],["073405006015","Heltersberg",null],["073405006016","Hermersberg",null],["073405006022","Höheinöd",null],["073405006025","Horbach",null],["073405006044","Schmalenberg",null],["073405006050","Steinalben",null],["073405006054","Waldfischbach-Burgalben",null],["073405008201","Althornbach",null],["073405008202","Battweiler",null],["073405008203","Bechhofen",null],["073405008206","Contwig",null],["073405008207","Dellfeld",null],["073405008208","Dietrichingen",null],["073405008209","Großbundenbach",null],["073405008210","Großsteinhausen",null],["073405008211","Hornbach, Stadt",null],["073405008212","Käshofen",null],["073405008213","Kleinbundenbach",null],["073405008214","Kleinsteinhausen",null],["073405008218","Mauschbach",null],["073405008221","Riedelberg",null],["073405008223","Rosenkopf",null],["073405008226","Walshausen",null],["073405008227","Wiesbach",null],["073405009017","Herschberg",null],["073405009018","Hettenhausen",null],["073405009023","Höheischweiler",null],["073405009024","Höhfröschen",null],["073405009035","Nünschweiler",null],["073405009037","Petersberg",null],["073405009041","Saalstadt",null],["073405009042","Schauerberg",null],["073405009051","Thaleischweiler-Fröschen",null],["073405009055","Weselberg",null],["073405009204","Biedershausen",null],["073405009215","Knopp-Labach",null],["073405009216","Krähenberg",null],["073405009217","Maßweiler",null],["073405009219","Obernheim-Kirchenarnbach",null],["073405009220","Reifenberg",null],["073405009222","Rieschweiler-Mühlbach",null],["073405009224","Schmitshausen",null],["073405009225","Wallhalben",null],["073405009228","Winterbach (Pfalz)",null],["081110000000","Stuttgart, Landeshauptstadt",null],["081150003003","Böblingen, Stadt",null],["081150028028","Leonberg, Stadt",null],["081150029029","Magstadt",null],["081150041041","Renningen, Stadt",null],["081150042042","Rutesheim, Stadt",null],["081150044044","Schönaich",null],["081150045045","Sindelfingen, Stadt",null],["081150050050","Weil der Stadt, Stadt",null],["081150051051","Weil im Schönbuch",null],["081150052052","Weissach",null],["081155001001","Aidlingen",null],["081155001054","Grafenau",null],["081155002013","Ehningen",null],["081155002015","Gärtringen",null],["081155003010","Deckenpfronn",null],["081155003021","Herrenberg, Stadt",null],["081155003037","Nufringen",null],["081155004002","Altdorf",null],["081155004022","Hildrizhausen",null],["081155004024","Holzgerlingen, Stadt",null],["081155005004","Bondorf",null],["081155005016","Gäufelden",null],["081155005034","Mötzingen",null],["081155005053","Jettingen",null],["081155006046","Steinenbronn",null],["081155006048","Waldenbuch, Stadt",null],["081160015015","Denkendorf",null],["081160019019","Esslingen am Neckar, Stadt",null],["081160047047","Neuhausen auf den Fildern",null],["081160072072","Wernau (Neckar), Stadt",null],["081160076076","Aichwald",null],["081160077077","Filderstadt, Stadt",null],["081160078078","Leinfelden-Echterdingen, Stadt",null],["081160080080","Ostfildern, Stadt",null],["081160081081","Aichtal, Stadt",null],["081165001016","Dettingen unter Teck",null],["081165001033","Kirchheim unter Teck, Stadt",null],["081165001048","Notzingen",null],["081165002018","Erkenbrechtsweiler",null],["081165002054","Owen, Stadt",null],["081165002079","Lenningen",null],["081165003005","Altdorf",null],["081165003006","Altenriet",null],["081165003008","Bempflingen",null],["081165003041","Neckartailfingen",null],["081165003042","Neckartenzlingen",null],["081165003063","Schlaitdorf",null],["081165004011","Beuren",null],["081165004036","Kohlberg",null],["081165004046","Neuffen, Stadt",null],["081165005020","Frickenhausen",null],["081165005022","Großbettlingen",null],["081165005049","Nürtingen, Stadt",null],["081165005050","Oberboihingen",null],["081165005068","Unterensingen",null],["081165005073","Wolfschlugen",null],["081165006004","Altbach",null],["081165006014","Deizisau",null],["081165006056","Plochingen, Stadt",null],["081165007007","Baltmannsweiler",null],["081165007027","Hochdorf",null],["081165007037","Lichtenwald",null],["081165007058","Reichenbach an der Fils",null],["081165008012","Bissingen an der Teck",null],["081165008029","Holzmaden",null],["081165008043","Neidlingen",null],["081165008053","Ohmden",null],["081165008070","Weilheim an der Teck, Stadt",null],["081165009035","Köngen",null],["081165009071","Wendlingen am Neckar, Stadt",null],["081170010010","Böhmenkirch",null],["081175001006","Bad Ditzenbach",null],["081175001014","Deggingen",null],["081175002018","Ebersbach an der Fils, Stadt",null],["081175002044","Schlierbach",null],["081175003019","Eislingen/Fils, Stadt",null],["081175003037","Ottenbach",null],["081175003042","Salach",null],["081175004007","Bad Überkingen",null],["081175004024","Geislingen an der Steige, Stadt",null],["081175004033","Kuchen",null],["081175005026","Göppingen, Stadt",null],["081175005043","Schlat",null],["081175005053","Wäschenbeuren",null],["081175005055","Wangen",null],["081175006015","Donzdorf, Stadt",null],["081175006025","Gingen an der Fils",null],["081175006049","Süßen, Stadt",null],["081175006061","Lauterstein, Stadt",null],["081175007016","Drackenstein",null],["081175007028","Gruibingen",null],["081175007031","Hohenstadt",null],["081175007035","Mühlhausen im Täle",null],["081175007058","Wiesensteig, Stadt",null],["081175008001","Adelberg",null],["081175008009","Birenbach",null],["081175008011","Börtlingen",null],["081175008038","Rechberghausen",null],["081175009002","Aichelberg",null],["081175009012","Bad Boll",null],["081175009017","Dürnau",null],["081175009023","Gammelshausen",null],["081175009029","Hattenhofen",null],["081175009060","Zell unter Aichelberg",null],["081175010003","Albershausen",null],["081175010051","Uhingen, Stadt",null],["081175011020","Eschenbach",null],["081175011030","Heiningen",null],["081180003003","Asperg, Stadt",null],["081180011011","Ditzingen, Stadt",null],["081180019019","Gerlingen, Stadt",null],["081180021021","Großbottwar, Stadt",null],["081180046046","Kornwestheim, Stadt",null],["081180048048","Ludwigsburg, Stadt",null],["081180050050","Markgröningen, Stadt",null],["081180051051","Möglingen",null],["081180060060","Oberstenfeld",null],["081180076076","Sachsenheim, Stadt",null],["081180080080","Korntal-Münchingen, Stadt",null],["081180081081","Remseck am Neckar, Stadt",null],["081185001007","Besigheim, Stadt",null],["081185001016","Freudental",null],["081185001018","Gemmrigheim",null],["081185001028","Hessigheim",null],["081185001047","Löchgau",null],["081185001053","Mundelsheim",null],["081185001074","Walheim",null],["081185002071","Tamm",null],["081185002077","Ingersheim",null],["081185002079","Bietigheim-Bissingen, Stadt",null],["081185003010","Bönnigheim, Stadt",null],["081185003015","Erligheim",null],["081185003040","Kirchheim am Neckar",null],["081185004063","Pleidelsheim",null],["081185004078","Freiberg am Neckar, Stadt",null],["081185005001","Affalterbach",null],["081185005006","Benningen am Neckar",null],["081185005014","Erdmannhausen",null],["081185005049","Marbach am Neckar, Stadt",null],["081185006027","Hemmingen",null],["081185006067","Schwieberdingen",null],["081185007054","Murr",null],["081185007070","Steinheim an der Murr, Stadt",null],["081185008012","Eberdingen",null],["081185008059","Oberriexingen, Stadt",null],["081185008068","Sersheim",null],["081185008073","Vaihingen an der Enz, Stadt",null],["081190001001","Alfdorf",null],["081190020020","Fellbach, Stadt",null],["081190041041","Korb",null],["081190044044","Murrhardt, Stadt",null],["081190061061","Rudersberg",null],["081190079079","Waiblingen, Stadt",null],["081190089089","Berglen",null],["081190090090","Remshalden",null],["081190091091","Weinstadt, Stadt",null],["081190093093","Kernen im Remstal",null],["081195001003","Allmersbach im Tal",null],["081195001004","Althütte",null],["081195001006","Auenwald",null],["081195001008","Backnang, Stadt",null],["081195001018","Burgstetten",null],["081195001038","Kirchberg an der Murr",null],["081195001053","Oppenweiler",null],["081195001083","Weissach im Tal",null],["081195001087","Aspach",null],["081195002055","Plüderhausen",null],["081195002076","Urbach",null],["081195003067","Schorndorf, Stadt",null],["081195003086","Winterbach",null],["081195004024","Großerlach",null],["081195004069","Spiegelberg",null],["081195004075","Sulzbach an der Murr",null],["081195005037","Kaisersbach",null],["081195005084","Welzheim, Stadt",null],["081195006042","Leutenbach",null],["081195006068","Schwaikheim",null],["081195006085","Winnenden, Stadt",null],["081210000000","Heilbronn, Universitätsstadt",null],["081250007007","Bad Wimpfen, Stadt",null],["081250039039","Gundelsheim, Stadt",null],["081250058058","Leingarten, Stadt",null],["081250068068","Neudenau, Stadt",null],["081250107107","Wüstenrot",null],["081255001005","Bad Friedrichshall, Stadt",null],["081255001078","Oedheim",null],["081255001079","Offenau",null],["081255002006","Bad Rappenau, Stadt",null],["081255002049","Kirchardt",null],["081255002087","Siegelsbach",null],["081255003013","Brackenheim, Stadt",null],["081255003017","Cleebronn",null],["081255004026","Eppingen, Stadt",null],["081255004034","Gemmingen",null],["081255004047","Ittlingen",null],["081255005030","Flein",null],["081255005094","Talheim",null],["081255006056","Lauffen am Neckar, Stadt",null],["081255006066","Neckarwestheim",null],["081255006074","Nordheim",null],["081255007048","Jagsthausen",null],["081255007063","Möckmühl, Stadt",null],["081255007084","Roigheim",null],["081255007103","Widdern, Stadt",null],["081255008027","Erlenbach",null],["081255008065","Neckarsulm, Stadt",null],["081255008096","Untereisesheim",null],["081255009069","Neuenstadt am Kocher, Stadt",null],["081255009111","Hardthausen am Kocher",null],["081255009113","Langenbrettach",null],["081255010038","Güglingen, Stadt",null],["081255010081","Pfaffenhofen",null],["081255010108","Zaberfeld",null],["081255011059","Löwenstein, Stadt",null],["081255011110","Obersulm",null],["081255012001","Abstatt",null],["081255012008","Beilstein, Stadt",null],["081255012046","Ilsfeld",null],["081255012098","Untergruppenbach",null],["081255013061","Massenbachhausen",null],["081255013086","Schwaigern, Stadt",null],["081255014021","Eberstadt",null],["081255014024","Ellhofen",null],["081255014057","Lehrensteinsfeld",null],["081255014102","Weinsberg, Stadt",null],["081260011011","Bretzfeld",null],["081260072072","Schöntal",null],["081265001047","Kupferzell",null],["081265001058","Neuenstein, Stadt",null],["081265001085","Waldenburg, Stadt",null],["081265002020","Dörzbach",null],["081265002045","Krautheim, Stadt",null],["081265002056","Mulfingen",null],["081265003039","Ingelfingen, Stadt",null],["081265003046","Künzelsau, Stadt",null],["081265004028","Forchtenberg, Stadt",null],["081265004060","Niedernhall, Stadt",null],["081265004086","Weißbach",null],["081265005066","Öhringen, Stadt",null],["081265005069","Pfedelbach",null],["081265005094","Zweiflingen",null],["081270008008","Blaufelden",null],["081270052052","Mainhardt",null],["081270075075","Schrozberg, Stadt",null],["081275001009","Braunsbach",null],["081275001086","Untermünkheim",null],["081275002014","Crailsheim, Stadt",null],["081275002073","Satteldorf",null],["081275002103","Frankenhardt",null],["081275002104","Stimpfach",null],["081275003101","Kreßberg",null],["081275003102","Fichtenau",null],["081275004032","Gerabronn, Stadt",null],["081275004047","Langenburg, Stadt",null],["081275005043","Ilshofen, Stadt",null],["081275005089","Vellberg, Stadt",null],["081275005099","Wolpertshausen",null],["081275006023","Fichtenberg",null],["081275006025","Gaildorf, Stadt",null],["081275006062","Oberrot",null],["081275006079","Sulzbach-Laufen",null],["081275007012","Bühlertann",null],["081275007013","Bühlerzell",null],["081275007063","Obersontheim",null],["081275008046","Kirchberg an der Jagst, Stadt",null],["081275008071","Rot am See",null],["081275008091","Wallhausen",null],["081275009056","Michelbach an der Bilz",null],["081275009059","Michelfeld",null],["081275009076","Schwäbisch Hall, Stadt",null],["081275009100","Rosengarten",null],["081280020020","Creglingen, Stadt",null],["081280039039","Freudenberg, Stadt",null],["081280064064","Külsheim, Stadt",null],["081280082082","Niederstetten, Stadt",null],["081280126126","Weikersheim, Stadt",null],["081280131131","Wertheim, Stadt",null],["081280139139","Lauda-Königshofen, Stadt",null],["081285001006","Assamstadt",null],["081285001007","Bad Mergentheim, Stadt",null],["081285001058","Igersheim",null],["081285002014","Boxberg, Stadt",null],["081285002138","Ahorn",null],["081285003047","Grünsfeld, Stadt",null],["081285003137","Wittighausen",null],["081285004045","Großrinderfeld",null],["081285004061","Königheim",null],["081285004115","Tauberbischofsheim, Stadt",null],["081285004128","Werbach",null],["081350010010","Dischingen",null],["081350015015","Gerstetten",null],["081350020020","Herbrechtingen, Stadt",null],["081350025025","Königsbronn",null],["081350032032","Steinheim am Albuch",null],["081355001016","Giengen an der Brenz, Stadt",null],["081355001021","Hermaringen",null],["081355002019","Heidenheim an der Brenz, Stadt",null],["081355002026","Nattheim",null],["081355003027","Niederstotzingen, Stadt",null],["081355003031","Sontheim an der Brenz",null],["081360002002","Abtsgmünd",null],["081360027027","Gschwend",null],["081360042042","Lorch, Stadt",null],["081360045045","Neresheim, Stadt",null],["081360050050","Oberkochen, Stadt",null],["081365001021","Essingen",null],["081365001033","Hüttlingen",null],["081365001088","Aalen, Stadt",null],["081365002010","Bopfingen, Stadt",null],["081365002037","Kirchheim am Ries",null],["081365002087","Riesbürg",null],["081365003003","Adelmannsfelden",null],["081365003018","Ellenberg",null],["081365003019","Ellwangen (Jagst), Stadt",null],["081365003035","Jagstzell",null],["081365003046","Neuler",null],["081365003060","Rosenberg",null],["081365003084","Wört",null],["081365003089","Rainau",null],["081365004038","Lauchheim, Stadt",null],["081365004082","Westhausen",null],["081365005020","Eschach",null],["081365005024","Göggingen",null],["081365005034","Iggingen",null],["081365005040","Leinzell",null],["081365005049","Obergröningen",null],["081365005062","Schechingen",null],["081365006007","Bartholomä",null],["081365006009","Böbingen an der Rems",null],["081365006028","Heubach, Stadt",null],["081365006029","Heuchlingen",null],["081365006043","Mögglingen",null],["081365007065","Schwäbisch Gmünd, Stadt",null],["081365007079","Waldstetten",null],["081365008015","Durlangen",null],["081365008044","Mutlangen",null],["081365008061","Ruppertshofen",null],["081365008066","Spraitbach",null],["081365008070","Täferrot",null],["081365009068","Stödtlen",null],["081365009071","Tannhausen",null],["081365009075","Unterschneidheim",null],["082110000000","Baden-Baden, Stadt",null],["082120000000","Karlsruhe, Stadt",null],["082150017017","Ettlingen, Stadt",null],["082150046046","Malsch",null],["082150047047","Marxzell",null],["082150064064","Östringen, Stadt",null],["082150084084","Ubstadt-Weiher",null],["082150089089","Walzbachtal",null],["082150090090","Weingarten (Baden)",null],["082150096096","Karlsbad",null],["082150097097","Kraichtal, Stadt",null],["082150101101","Pfinztal",null],["082150102102","Eggenstein-Leopoldshafen",null],["082150105105","Linkenheim-Hochstetten",null],["082150106106","Waghäusel, Stadt",null],["082150108108","Rheinstetten, Stadt",null],["082150109109","Stutensee, Stadt",null],["082150110110","Waldbronn",null],["082155001039","Kronau",null],["082155001100","Bad Schönborn",null],["082155002007","Bretten, Stadt",null],["082155002025","Gondelsheim",null],["082155003009","Bruchsal, Stadt",null],["082155003021","Forst",null],["082155003029","Hambrücken",null],["082155003103","Karlsdorf-Neuthard",null],["082155004099","Graben-Neudorf",null],["082155004111","Dettenheim",null],["082155005040","Kürnbach",null],["082155005059","Oberderdingen",null],["082155006066","Philippsburg, Stadt",null],["082155006107","Oberhausen-Rheinhausen",null],["082155007082","Sulzfeld",null],["082155007094","Zaisenhausen",null],["082160008008","Bühlertal",null],["082160013013","Forbach",null],["082160015015","Gaggenau, Stadt",null],["082165001006","Bischweier",null],["082165001024","Kuppenheim, Stadt",null],["082165002007","Bühl, Stadt",null],["082165002041","Ottersweier",null],["082165003002","Au am Rhein",null],["082165003005","Bietigheim",null],["082165003009","Durmersheim",null],["082165003012","Elchesheim-Illingen",null],["082165004017","Gernsbach, Stadt",null],["082165004029","Loffenau",null],["082165004059","Weisenbach",null],["082165005023","Iffezheim",null],["082165005033","Muggensturm",null],["082165005039","Ötigheim",null],["082165005043","Rastatt, Stadt",null],["082165005052","Steinmauern",null],["082165006028","Lichtenau, Stadt",null],["082165006063","Rheinmünster",null],["082165007022","Hügelsheim",null],["082165007049","Sinzheim",null],["082210000000","Heidelberg, Stadt",null],["082220000000","Mannheim, Universitätsstadt",null],["082250014014","Buchen (Odenwald), Stadt",null],["082250060060","Mudau",null],["082255001032","Hardheim",null],["082255001039","Höpfingen",null],["082255001109","Walldürn, Stadt",null],["082255002033","Haßmersheim",null],["082255002042","Hüffenhardt",null],["082255003002","Aglasterhausen",null],["082255003068","Neunkirchen",null],["082255003116","Schwarzach",null],["082255004024","Fahrenbach",null],["082255004052","Limbach",null],["082255005058","Mosbach, Stadt",null],["082255005067","Neckarzimmern",null],["082255005074","Obrigheim",null],["082255005117","Elztal",null],["082255006010","Binau",null],["082255006064","Neckargerach",null],["082255006113","Zwingenberg",null],["082255006118","Waldbrunn",null],["082255007075","Osterburken, Stadt",null],["082255007082","Rosenberg",null],["082255007114","Ravenstein, Stadt",null],["082255008009","Billigheim",null],["082255008115","Schefflenz",null],["082255009001","Adelsheim, Stadt",null],["082255009091","Seckach",null],["082260009009","Brühl",null],["082260012012","Dossenheim",null],["082260018018","Eppelheim, Stadt",null],["082260028028","Heddesheim",null],["082260036036","Ilvesheim",null],["082260037037","Ketsch",null],["082260038038","Ladenburg, Stadt",null],["082260041041","Leimen, Stadt",null],["082260060060","Nußloch",null],["082260062062","Oftersheim",null],["082260063063","Plankstadt",null],["082260076076","Sandhausen",null],["082260082082","Schriesheim, Stadt",null],["082260084084","Schwetzingen, Stadt",null],["082260095095","Walldorf, Stadt",null],["082260096096","Weinheim, Stadt",null],["082260103103","St. Leon-Rot",null],["082260105105","Edingen-Neckarhausen",null],["082260107107","Hirschberg an der Bergstraße",null],["082265001013","Eberbach, Stadt",null],["082265001081","Schönbrunn",null],["082265002020","Eschelbronn",null],["082265002048","Mauer",null],["082265002049","Meckesheim",null],["082265002086","Spechbach",null],["082265002104","Lobbach",null],["082265003031","Hemsbach, Stadt",null],["082265003040","Laudenbach",null],["082265004003","Altlußheim",null],["082265004032","Hockenheim, Stadt",null],["082265004059","Neulußheim",null],["082265004068","Reilingen",null],["082265005006","Bammental",null],["082265005022","Gaiberg",null],["082265005056","Neckargemünd, Stadt",null],["082265005097","Wiesenbach",null],["082265006046","Malsch",null],["082265006054","Mühlhausen",null],["082265006065","Rauenberg, Stadt",null],["082265007027","Heddesbach",null],["082265007029","Heiligkreuzsteinach",null],["082265007080","Schönau, Stadt",null],["082265007099","Wilhelmsfeld",null],["082265008085","Sinsheim, Stadt",null],["082265008101","Zuzenhausen",null],["082265008102","Angelbachtal",null],["082265009017","Epfenbach",null],["082265009055","Neckarbischofsheim, Stadt",null],["082265009058","Neidenstein",null],["082265009066","Reichartshausen",null],["082265009091","Waibstadt, Stadt",null],["082265009106","Helmstadt-Bargen",null],["082265010010","Dielheim",null],["082265010098","Wiesloch, Stadt",null],["082310000000","Pforzheim, Stadt",null],["082350065065","Schömberg",null],["082350080080","Wildberg, Stadt",null],["082355001006","Altensteig, Stadt",null],["082355001022","Egenhausen",null],["082355001066","Simmersfeld",null],["082355002007","Althengstett",null],["082355002029","Gechingen",null],["082355002057","Ostelsheim",null],["082355002067","Simmozheim",null],["082355003018","Dobel",null],["082355003033","Bad Herrenalb, Stadt",null],["082355004008","Bad Liebenzell, Stadt",null],["082355004073","Unterreichenbach",null],["082355005047","Neubulach, Stadt",null],["082355005050","Neuweiler",null],["082355005084","Bad Teinach-Zavelstein, Stadt",null],["082355006055","Oberreichenbach",null],["082355006085","Calw, Stadt",null],["082355007020","Ebhausen",null],["082355007032","Haiterbach, Stadt",null],["082355007046","Nagold, Stadt",null],["082355007060","Rohrdorf",null],["082355008025","Enzklösterle",null],["082355008035","Höfen an der Enz",null],["082355008079","Bad Wildbad, Stadt",null],["082360004004","Birkenfeld",null],["082360028028","Illingen",null],["082360030030","Ispringen",null],["082360033033","Knittlingen, Stadt",null],["082360046046","Niefern-Öschelbronn",null],["082360070070","Keltern",null],["082360071071","Remchingen",null],["082360072072","Straubenhardt",null],["082365001019","Friolzheim",null],["082365001025","Heimsheim, Stadt",null],["082365001039","Mönsheim",null],["082365001065","Wiernsheim",null],["082365001067","Wimsheim",null],["082365001068","Wurmberg",null],["082365002011","Eisingen",null],["082365002074","Kämpfelbach",null],["082365002076","Königsbach-Stein",null],["082365003038","Maulbronn, Stadt",null],["082365003061","Sternenfels",null],["082365004040","Mühlacker, Stadt",null],["082365004050","Ötisheim",null],["082365005013","Engelsbrand",null],["082365005043","Neuenbürg, Stadt",null],["082365006031","Kieselbronn",null],["082365006073","Neulingen",null],["082365006075","Ölbronn-Dürrn",null],["082365007044","Neuhausen",null],["082365007062","Tiefenbronn",null],["082370002002","Alpirsbach, Stadt",null],["082370004004","Baiersbronn",null],["082370045045","Loßburg",null],["082375001019","Dornstetten, Stadt",null],["082375001030","Glatten",null],["082375001061","Schopfloch",null],["082375001074","Waldachtal",null],["082375002028","Freudenstadt, Stadt",null],["082375002073","Seewald",null],["082375002075","Bad Rippoldsau-Schapbach",null],["082375003024","Empfingen",null],["082375003027","Eutingen im Gäu",null],["082375003040","Horb am Neckar, Stadt",null],["082375005032","Grömbach",null],["082375005054","Pfalzgrafenweiler",null],["082375005072","Wörnersberg",null],["083110000000","Freiburg im Breisgau, Stadt",null],["083150068068","Lenzkirch",null],["083150076076","Neuenburg am Rhein, Stadt",null],["083150133133","Vogtsburg im Kaiserstuhl, Stadt",null],["083155001006","Bad Krozingen, Stadt",null],["083155001048","Hartheim am Rhein",null],["083155002015","Breisach am Rhein, Stadt",null],["083155002059","Ihringen",null],["083155002072","Merdingen",null],["083155003020","Buchenbach",null],["083155003064","Kirchzarten",null],["083155003084","Oberried",null],["083155003109","Stegen",null],["083155004014","Bollschweil",null],["083155004131","Ehrenkirchen",null],["083155005047","Gundelfingen",null],["083155005051","Heuweiler",null],["083155006008","Ballrechten-Dottingen",null],["083155006033","Eschbach",null],["083155006050","Heitersheim, Stadt",null],["083155007003","Au",null],["083155007056","Horben",null],["083155007073","Merzhausen",null],["083155007107","Sölden",null],["083155007125","Wittnau",null],["083155008016","Breitnau",null],["083155008052","Hinterzarten",null],["083155009013","Bötzingen",null],["083155009030","Eichstetten am Kaiserstuhl",null],["083155009043","Gottenheim",null],["083155010039","Friedenweiler",null],["083155010070","Löffingen, Stadt",null],["083155011115","Umkirch",null],["083155011132","March",null],["083155012004","Auggen",null],["083155012007","Badenweiler",null],["083155012022","Buggingen",null],["083155012074","Müllheim, Stadt",null],["083155012111","Sulzburg, Stadt",null],["083155013041","Glottertal",null],["083155013094","St. Märgen",null],["083155013095","St. Peter",null],["083155014028","Ebringen",null],["083155014089","Pfaffenweiler",null],["083155014098","Schallstadt",null],["083155015037","Feldberg (Schwarzwald)",null],["083155015102","Schluchsee",null],["083155016108","Staufen im Breisgau, Stadt",null],["083155016130","Münstertal/Schwarzwald",null],["083155017031","Eisenbach (Hochschwarzwald)",null],["083155017113","Titisee-Neustadt, Stadt",null],["083165001009","Denzlingen",null],["083165001036","Reute",null],["083165001045","Vörstetten",null],["083165002003","Biederbach",null],["083165002010","Elzach, Stadt",null],["083165002055","Winden im Elztal",null],["083165003011","Emmendingen, Stadt",null],["083165003024","Malterdingen",null],["083165003039","Sexau",null],["083165003043","Teningen",null],["083165003054","Freiamt",null],["083165004017","Herbolzheim, Stadt",null],["083165004020","Kenzingen, Stadt",null],["083165004049","Weisweil",null],["083165004053","Rheinhausen",null],["083165005002","Bahlingen am Kaiserstuhl",null],["083165005012","Endingen am Kaiserstuhl, Stadt",null],["083165005013","Forchheim",null],["083165005037","Riegel am Kaiserstuhl",null],["083165005038","Sasbach am Kaiserstuhl",null],["083165005051","Wyhl am Kaiserstuhl",null],["083165006014","Gutach im Breisgau",null],["083165006042","Simonswald",null],["083165006056","Waldkirch, Stadt",null],["083170005005","Appenweier",null],["083170031031","Friesenheim",null],["083170051051","Hornberg, Stadt",null],["083170057057","Kehl, Stadt",null],["083170141141","Willstätt",null],["083170151151","Neuried",null],["083170153153","Rheinau, Stadt",null],["083175001001","Achern, Stadt",null],["083175001068","Lauf",null],["083175001116","Sasbach",null],["083175001118","Sasbachwalden",null],["083175002026","Ettenheim, Stadt",null],["083175002073","Mahlberg, Stadt",null],["083175002113","Ringsheim",null],["083175002114","Rust",null],["083175002152","Kappel-Grafenhausen",null],["083175003009","Berghaupten",null],["083175003034","Gengenbach, Stadt",null],["083175003097","Ohlsbach",null],["083175004029","Fischerbach",null],["083175004040","Haslach im Kinzigtal, Stadt",null],["083175004046","Hofstetten",null],["083175004078","Mühlenbach",null],["083175004129","Steinach",null],["083175005039","Gutach (Schwarzwaldbahn)",null],["083175005041","Hausach, Stadt",null],["083175006056","Kappelrodeck",null],["083175006102","Ottenhöfen im Schwarzwald",null],["083175006126","Seebach",null],["083175007059","Kippenheim",null],["083175007065","Lahr/Schwarzwald, Stadt",null],["083175008008","Bad Peterstal-Griesbach",null],["083175008098","Oppenau, Stadt",null],["083175009067","Lautenbach",null],["083175009089","Oberkirch, Stadt",null],["083175009110","Renchen, Stadt",null],["083175010021","Durbach",null],["083175010047","Hohberg",null],["083175010096","Offenburg, Stadt",null],["083175010100","Ortenberg",null],["083175010122","Schutterwald",null],["083175011121","Schuttertal",null],["083175011127","Seelbach",null],["083175012075","Meißenheim",null],["083175012150","Schwanau",null],["083175013093","Oberwolfach",null],["083175013145","Wolfach, Stadt",null],["083175014011","Biberach",null],["083175014085","Nordrach",null],["083175014088","Oberharmersbach",null],["083175014146","Zell am Harmersbach, Stadt",null],["083179971971","Rheinau, gemeindefreies Gebiet",null],["083250012012","Dornhan, Stadt",null],["083255001014","Dunningen",null],["083255001071","Eschbronn",null],["083255002015","Epfendorf",null],["083255002045","Oberndorf am Neckar, Stadt",null],["083255002070","Fluorn-Winzeln",null],["083255003011","Dietingen",null],["083255003049","Rottweil, Stadt",null],["083255003064","Wellendingen",null],["083255003069","Zimmern ob Rottweil",null],["083255003072","Deißlingen",null],["083255004050","Schenkenzell",null],["083255004051","Schiltach, Stadt",null],["083255005001","Aichhalden",null],["083255005024","Hardt",null],["083255005036","Lauterbach",null],["083255005053","Schramberg, Stadt",null],["083255006057","Sulz am Neckar, Stadt",null],["083255006061","Vöhringen",null],["083255007009","Bösingen",null],["083255007060","Villingendorf",null],["083260003003","Bad Dürrheim, Stadt",null],["083260005005","Blumberg, Stadt",null],["083260031031","Königsfeld im Schwarzwald",null],["083260052052","St. Georgen im Schwarzwald, Stadt",null],["083260068068","Vöhrenbach, Stadt",null],["083265001006","Bräunlingen, Stadt",null],["083265001012","Donaueschingen, Stadt",null],["083265001027","Hüfingen, Stadt",null],["083265002017","Furtwangen im Schwarzwald, Stadt",null],["083265002020","Gütenbach",null],["083265003054","Schönwald im Schwarzwald",null],["083265003055","Schonach im Schwarzwald",null],["083265003060","Triberg im Schwarzwald, Stadt",null],["083265004010","Dauchingen",null],["083265004037","Mönchweiler",null],["083265004041","Niedereschach",null],["083265004061","Tuningen",null],["083265004065","Unterkirnach",null],["083265004074","Villingen-Schwenningen, Stadt",null],["083265004075","Brigachtal",null],["083275001004","Bärenthal",null],["083275001008","Buchheim",null],["083275001016","Fridingen an der Donau, Stadt",null],["083275001027","Irndorf",null],["083275001030","Kolbingen",null],["083275001036","Mühlheim an der Donau, Stadt",null],["083275001041","Renquishausen",null],["083275002007","Bubsheim",null],["083275002009","Deilingen",null],["083275002013","Egesheim",null],["083275002019","Gosheim",null],["083275002029","Königsheim",null],["083275002040","Reichenbach am Heuberg",null],["083275002051","Wehingen",null],["083275003018","Geisingen, Stadt",null],["083275003025","Immendingen",null],["083275004002","Aldingen",null],["083275004005","Balgheim",null],["083275004006","Böttingen",null],["083275004010","Denkingen",null],["083275004011","Dürbheim",null],["083275004017","Frittlingen",null],["083275004023","Hausen ob Verena",null],["083275004033","Mahlstetten",null],["083275004046","Spaichingen, Stadt",null],["083275005012","Durchhausen",null],["083275005020","Gunningen",null],["083275005048","Talheim",null],["083275005049","Trossingen, Stadt",null],["083275006038","Neuhausen ob Eck",null],["083275006050","Tuttlingen, Stadt",null],["083275006054","Wurmlingen",null],["083275006055","Seitingen-Oberflacht",null],["083275006056","Rietheim-Weilheim",null],["083275006057","Emmingen-Liptingen",null],["083350035035","Hilzingen",null],["083350063063","Radolfzell am Bodensee, Stadt",null],["083350080080","Tengen, Stadt",null],["083355001001","Aach, Stadt",null],["083355001022","Engen, Stadt",null],["083355001097","Mühlhausen-Ehingen",null],["083355002015","Büsingen am Hochrhein",null],["083355002026","Gailingen am Hochrhein",null],["083355002028","Gottmadingen",null],["083355003025","Gaienhofen",null],["083355003055","Moos",null],["083355003061","Öhningen",null],["083355004002","Allensbach",null],["083355004043","Konstanz, Universitätsstadt",null],["083355004066","Reichenau",null],["083355005075","Singen (Hohentwiel), Stadt",null],["083355005077","Steißlingen",null],["083355005081","Volkertshausen",null],["083355005100","Rielasingen-Worblingen",null],["083355006021","Eigeltingen",null],["083355006057","Mühlingen",null],["083355006079","Stockach, Stadt",null],["083355006096","Hohenfels",null],["083355006098","Bodman-Ludwigshafen",null],["083355006099","Orsingen-Nenzingen",null],["083360014014","Efringen-Kirchen",null],["083360084084","Steinen",null],["083360087087","Todtnau, Stadt",null],["083360091091","Weil am Rhein, Stadt",null],["083360105105","Grenzach-Wyhlen",null],["083360107107","Kleines Wiesental",null],["083365001045","Kandern, Stadt",null],["083365001104","Malsburg-Marzell",null],["083365003043","Inzlingen",null],["083365003050","Lörrach, Stadt",null],["083365004069","Rheinfelden (Baden), Stadt",null],["083365004082","Schwörstadt",null],["083365005006","Bad Bellingen",null],["083365005078","Schliengen",null],["083365006004","Aitern",null],["083365006010","Böllen",null],["083365006025","Fröhnd",null],["083365006079","Schönau im Schwarzwald, Stadt",null],["083365006080","Schönenberg",null],["083365006089","Tunau",null],["083365006090","Utzenfeld",null],["083365006094","Wembach",null],["083365006096","Wieden",null],["083365007034","Hasel",null],["083365007036","Hausen im Wiesental",null],["083365007057","Maulburg",null],["083365007081","Schopfheim, Stadt",null],["083365008008","Binzen",null],["083365008019","Eimeldingen",null],["083365008024","Fischingen",null],["083365008073","Rümmingen",null],["083365008075","Schallbach",null],["083365008100","Wittlingen",null],["083365009103","Zell im Wiesental, Stadt",null],["083365009106","Häg-Ehrsberg",null],["083370002002","Albbruck",null],["083370038038","Görwihl",null],["083370062062","Klettgau",null],["083370066066","Laufenburg (Baden), Stadt",null],["083370106106","Stühlingen, Stadt",null],["083370116116","Wehr, Stadt",null],["083375001022","Bonndorf im Schwarzwald, Stadt",null],["083375001127","Wutach",null],["083375002030","Dettighofen",null],["083375002060","Jestetten",null],["083375002070","Lottstetten",null],["083375003053","Hohentengen am Hochrhein",null],["083375003125","Küssaberg",null],["083375004039","Grafenhausen",null],["083375004128","Ühlingen-Birkendorf",null],["083375005049","Herrischried",null],["083375005076","Murg",null],["083375005090","Rickenbach",null],["083375005096","Bad Säckingen, Stadt",null],["083375006013","Bernau im Schwarzwald",null],["083375006027","Dachsberg (Südschwarzwald)",null],["083375006045","Häusern",null],["083375006051","Höchenschwand",null],["083375006059","Ibach",null],["083375006097","St. Blasien, Stadt",null],["083375006108","Todtmoos",null],["083375007032","Dogern",null],["083375007065","Lauchringen",null],["083375007118","Weilheim",null],["083375007126","Waldshut-Tiengen, Stadt",null],["083375008123","Wutöschingen",null],["083375008124","Eggingen",null],["084150014014","Dettingen an der Erms",null],["084150019019","Eningen unter Achalm",null],["084150059059","Pfullingen, Stadt",null],["084150061061","Reutlingen, Stadt",null],["084150073073","Trochtelfingen, Stadt",null],["084150080080","Wannweil",null],["084150091091","Sonnenbühl",null],["084150092092","Lichtenstein",null],["084150093093","St. Johann",null],["084155001089","Engstingen",null],["084155001090","Hohenstein",null],["084155002029","Grafenberg",null],["084155002050","Metzingen, Stadt",null],["084155002062","Riederich",null],["084155003027","Gomadingen",null],["084155003048","Mehrstetten",null],["084155003053","Münsingen, Stadt",null],["084155004060","Pliezhausen",null],["084155004087","Walddorfhäslach",null],["084155005028","Grabenstetten",null],["084155005039","Hülben",null],["084155005078","Bad Urach, Stadt",null],["084155005088","Römerstein",null],["084155006034","Hayingen, Stadt",null],["084155006058","Pfronstetten",null],["084155006085","Zwiefalten",null],["084159971971","Gutsbezirk Münsingen, gemeindefreies Gebiet",null],["084160009009","Dettenhausen",null],["084160022022","Kirchentellinsfurt",null],["084160023023","Kusterdingen",null],["084160041041","Tübingen, Universitätsstadt",null],["084160048048","Ammerbuch",null],["084165001011","Dußlingen",null],["084165001015","Gomaringen",null],["084165001026","Nehren",null],["084165002006","Bodelshausen",null],["084165002025","Mössingen, Stadt",null],["084165002031","Ofterdingen",null],["084165003018","Hirrlingen",null],["084165003036","Rottenburg am Neckar, Stadt",null],["084165003049","Neustetten",null],["084165003050","Starzach",null],["084170013013","Burladingen, Stadt",null],["084170025025","Haigerloch, Stadt",null],["084170054054","Rosenfeld, Stadt",null],["084175001010","Bitz",null],["084175001079","Albstadt, Stadt",null],["084175002002","Balingen, Stadt",null],["084175002022","Geislingen, Stadt",null],["084175003008","Bisingen",null],["084175003023","Grosselfingen",null],["084175004031","Hechingen, Stadt",null],["084175004036","Jungingen",null],["084175004051","Rangendingen",null],["084175005044","Meßstetten, Stadt",null],["084175005045","Nusplingen",null],["084175005047","Obernheim",null],["084175006014","Dautmergen",null],["084175006015","Dormettingen",null],["084175006016","Dotternhausen",null],["084175006029","Hausen am Tann",null],["084175006052","Ratshausen",null],["084175006057","Schömberg, Stadt",null],["084175006071","Weilen unter den Rinnen",null],["084175006078","Zimmern unter der Burg",null],["084175007063","Straßberg",null],["084175007075","Winterlingen",null],["084210000000","Ulm, Universitätsstadt",null],["084250039039","Erbach, Stadt",null],["084250108108","Schelklingen, Stadt",null],["084250141141","Blaustein, Stadt",null],["084255001002","Allmendingen",null],["084255001004","Altheim",null],["084255002017","Berghülen",null],["084255002020","Blaubeuren, Stadt",null],["084255003028","Dietenheim, Stadt",null],["084255003066","Illerrieden",null],["084255003140","Balzheim",null],["084255004014","Beimerstetten",null],["084255004031","Dornstadt",null],["084255004135","Westerstetten",null],["084255005033","Ehingen (Donau), Stadt",null],["084255005050","Griesingen",null],["084255005088","Oberdischingen",null],["084255005093","Öpfingen",null],["084255006064","Hüttisheim",null],["084255006110","Schnürpflingen",null],["084255006137","Illerkirchberg",null],["084255006138","Staig",null],["084255007071","Laichingen, Stadt",null],["084255007079","Merklingen",null],["084255007084","Nellingen",null],["084255007134","Westerheim",null],["084255007139","Heroldstatt",null],["084255008005","Altheim (Alb)",null],["084255008011","Asselfingen",null],["084255008013","Ballendorf",null],["084255008019","Bernstadt",null],["084255008022","Börslingen",null],["084255008024","Breitingen",null],["084255008062","Holzkirch",null],["084255008072","Langenau, Stadt",null],["084255008083","Neenstetten",null],["084255008085","Nerenstetten",null],["084255008092","Öllingen",null],["084255008097","Rammingen",null],["084255008112","Setzingen",null],["084255008130","Weidenstetten",null],["084255009008","Amstetten",null],["084255009075","Lonsee",null],["084255010035","Emeringen",null],["084255010036","Emerkingen",null],["084255010052","Grundsheim",null],["084255010055","Hausen am Bussen",null],["084255010073","Lauterach",null],["084255010081","Munderkingen, Stadt",null],["084255010090","Obermarchtal",null],["084255010091","Oberstadion",null],["084255010098","Rechtenstein",null],["084255010104","Rottenacker",null],["084255010123","Untermarchtal",null],["084255010124","Unterstadion",null],["084255010125","Unterwachingen",null],["084260134134","Schemmerhofen",null],["084265001005","Alleshausen",null],["084265001006","Allmannsweiler",null],["084265001013","Bad Buchau, Stadt",null],["084265001020","Betzenweiler",null],["084265001036","Dürnau",null],["084265001064","Kanzach",null],["084265001078","Moosburg",null],["084265001090","Oggelshausen",null],["084265001109","Seekirch",null],["084265001118","Tiefenbach",null],["084265002014","Bad Schussenried, Stadt",null],["084265002062","Ingoldingen",null],["084265003011","Attenweiler",null],["084265003021","Biberach an der Riß, Stadt",null],["084265003038","Eberhardzell",null],["084265003058","Hochdorf",null],["084265003071","Maselheim",null],["084265003074","Mittelbiberach",null],["084265003120","Ummendorf",null],["084265003128","Warthausen",null],["084265004019","Berkheim",null],["084265004031","Dettingen an der Iller",null],["084265004044","Erolzheim",null],["084265004065","Kirchberg an der Iller",null],["084265004066","Kirchdorf an der Iller",null],["084265005001","Achstetten",null],["084265005028","Burgrieden",null],["084265005070","Laupheim, Stadt",null],["084265005073","Mietingen",null],["084265006043","Erlenmoos",null],["084265006087","Ochsenhausen, Stadt",null],["084265006113","Steinhausen an der Rottum",null],["084265006135","Gutenzell-Hürbel",null],["084265007008","Altheim",null],["084265007035","Dürmentingen",null],["084265007045","Ertingen",null],["084265007067","Langenenslingen",null],["084265007097","Riedlingen, Stadt",null],["084265007121","Unlingen",null],["084265007124","Uttenweiler",null],["084265008100","Rot an der Rot",null],["084265008117","Tannheim",null],["084265009108","Schwendi",null],["084265009125","Wain",null],["084350035035","Meckenbeuren",null],["084355001013","Eriskirch",null],["084355001029","Kressbronn am Bodensee",null],["084355001030","Langenargen",null],["084355002016","Friedrichshafen, Stadt",null],["084355002024","Immenstaad am Bodensee",null],["084355003005","Bermatingen",null],["084355003034","Markdorf, Stadt",null],["084355003045","Oberteuringen",null],["084355003067","Deggenhausertal",null],["084355004010","Daisendorf",null],["084355004018","Hagnau am Bodensee",null],["084355004036","Meersburg, Stadt",null],["084355004054","Stetten",null],["084355004066","Uhldingen-Mühlhofen",null],["084355005015","Frickingen",null],["084355005020","Heiligenberg",null],["084355005052","Salem",null],["084355006042","Neukirch",null],["084355006057","Tettnang, Stadt",null],["084355007047","Owingen",null],["084355007053","Sipplingen",null],["084355007059","Überlingen, Stadt",null],["084360008008","Aulendorf, Stadt",null],["084360010010","Bad Wurzach, Stadt",null],["084360049049","Isny im Allgäu, Stadt",null],["084360052052","Kißlegg",null],["084360094094","Argenbühl",null],["084365001005","Altshausen",null],["084365001019","Boms",null],["084365001024","Ebenweiler",null],["084365001027","Eichstegen",null],["084365001032","Fleischwangen",null],["084365001040","Guggenhausen",null],["084365001047","Hoßkirch",null],["084365001053","Königseggwald",null],["084365001067","Riedhausen",null],["084365001077","Unterwaldhausen",null],["084365001093","Ebersbach-Musbach",null],["084365002009","Bad Waldsee, Stadt",null],["084365002014","Bergatreute",null],["084365003018","Bodnegg",null],["084365003039","Grünkraut",null],["084365003069","Schlier",null],["084365003079","Waldburg",null],["084365004003","Aichstetten",null],["084365004004","Aitrach",null],["084365004055","Leutkirch im Allgäu, Stadt",null],["084365005011","Baienfurt",null],["084365005012","Baindt",null],["084365005013","Berg",null],["084365005064","Ravensburg, Stadt",null],["084365005082","Weingarten, Stadt",null],["084365006078","Vogt",null],["084365006085","Wolfegg",null],["084365007001","Achberg",null],["084365007006","Amtzell",null],["084365007081","Wangen im Allgäu, Stadt",null],["084365008083","Wilhelmsdorf",null],["084365008095","Horgenzell",null],["084365009087","Wolpertswende",null],["084365009096","Fronreute",null],["084370086086","Ostrach",null],["084375001031","Gammertingen, Stadt",null],["084375001047","Hettingen, Stadt",null],["084375001082","Neufra",null],["084375001114","Veringenstadt, Stadt",null],["084375002053","Hohentengen",null],["084375002076","Mengen, Stadt",null],["084375002101","Scheer, Stadt",null],["084375003072","Leibertingen",null],["084375003078","Meßkirch, Stadt",null],["084375003123","Sauldorf",null],["084375004056","Illmensee",null],["084375004088","Pfullendorf, Stadt",null],["084375004118","Wald",null],["084375004124","Herdwangen-Schönach",null],["084375005044","Herbertingen",null],["084375005100","Bad Saulgau, Stadt",null],["084375006005","Beuron",null],["084375006008","Bingen",null],["084375006059","Inzigkofen",null],["084375006065","Krauchenwies",null],["084375006104","Sigmaringen, Stadt",null],["084375006105","Sigmaringendorf",null],["084375007102","Schwenningen",null],["084375007107","Stetten am kalten Markt",null],["091610000000","Ingolstadt",null],["091620000000","München, Landeshauptstadt",null],["091630000000","Rosenheim",null],["091710111111","Altötting, St",null],["091710112112","Burghausen, St",null],["091710113113","Burgkirchen a.d.Alz",null],["091710117117","Garching a.d.Alz",null],["091710118118","Haiming",null],["091710125125","Neuötting, St",null],["091710127127","Pleiskirchen",null],["091710131131","Teising",null],["091710132132","Töging a.Inn, St",null],["091710133133","Tüßling, M",null],["091710137137","Winhöring",null],["091715101114","Emmerting",null],["091715101124","Mehring",null],["091715102116","Feichten a.d.Alz",null],["091715102119","Halsbach",null],["091715102122","Kirchweidach",null],["091715102134","Tyrlaching",null],["091715103123","Marktl, M",null],["091715103130","Stammham",null],["091715104115","Erlbach",null],["091715104126","Perach",null],["091715104129","Reischach",null],["091715106121","Kastl",null],["091715106135","Unterneukirchen",null],["091720111111","Ainring",null],["091720112112","Anger",null],["091720114114","Bad Reichenhall, GKSt",null],["091720115115","Bayerisch Gmain",null],["091720116116","Berchtesgaden, M",null],["091720117117","Bischofswiesen",null],["091720118118","Freilassing, St",null],["091720122122","Laufen, St",null],["091720124124","Marktschellenberg, M",null],["091720128128","Piding",null],["091720129129","Ramsau b.Berchtesgaden",null],["091720130130","Saaldorf-Surheim",null],["091720131131","Schneizlreuth",null],["091720132132","Schönau a.Königssee",null],["091720134134","Teisendorf, M",null],["091729452452","Eck",null],["091729454454","Schellenberger Forst",null],["091730111111","Bad Heilbrunn",null],["091730112112","Bad Tölz, St",null],["091730118118","Dietramszell",null],["091730120120","Egling",null],["091730123123","Eurasburg",null],["091730124124","Gaißach",null],["091730126126","Geretsried, St",null],["091730130130","Icking",null],["091730131131","Jachenau",null],["091730134134","Königsdorf",null],["091730135135","Lenggries",null],["091730137137","Münsing",null],["091730145145","Wackersberg",null],["091730147147","Wolfratshausen, St",null],["091735107113","Benediktbeuern",null],["091735107115","Bichl",null],["091735108133","Kochel a.See",null],["091735108142","Schlehdorf",null],["091735109127","Greiling",null],["091735109140","Reichersbeuern",null],["091735109141","Sachsenkam",null],["091739451451","Pupplinger Au",null],["091739452452","Wolfratshauser Forst",null],["091740111111","Altomünster, M",null],["091740113113","Bergkirchen",null],["091740115115","Dachau, GKSt",null],["091740118118","Erdweg",null],["091740121121","Haimhausen",null],["091740122122","Hebertshausen",null],["091740126126","Karlsfeld",null],["091740131131","Markt Indersdorf, M",null],["091740135135","Odelzhausen",null],["091740136136","Petershausen",null],["091740137137","Pfaffenhofen a.d.Glonn",null],["091740141141","Röhrmoos",null],["091740143143","Schwabhausen",null],["091740146146","Sulzemoos",null],["091740147147","Hilgertshausen-Tandern",null],["091740150150","Vierkirchen",null],["091740151151","Weichs",null],["091750111111","Anzing",null],["091750115115","Ebersberg, St",null],["091750118118","Forstinning",null],["091750122122","Grafing b.München, St",null],["091750123123","Hohenlinden",null],["091750124124","Kirchseeon, M",null],["091750127127","Markt Schwaben, M",null],["091750132132","Vaterstetten",null],["091750133133","Pliening",null],["091750135135","Poing",null],["091750137137","Steinhöring",null],["091750139139","Zorneding",null],["091755112112","Aßling",null],["091755112119","Frauenneuharting",null],["091755112136","Emmering",null],["091755114113","Baiern",null],["091755114114","Bruck",null],["091755114116","Egmating",null],["091755114121","Glonn, M",null],["091755114128","Moosach",null],["091755114131","Oberpframmern",null],["091759451451","Anzinger Forst",null],["091759452452","Ebersberger Forst",null],["091759453453","Eglhartinger Forst",null],["091760112112","Altmannstein, M",null],["091760114114","Beilngries, St",null],["091760118118","Buxheim",null],["091760120120","Denkendorf",null],["091760121121","Dollnstein, M",null],["091760123123","Eichstätt, GKSt",null],["091760126126","Gaimersheim, M",null],["091760129129","Großmehring",null],["091760131131","Hepberg",null],["091760132132","Hitzhofen",null],["091760137137","Kinding, M",null],["091760138138","Kipfenberg, M",null],["091760139139","Kösching, M",null],["091760143143","Lenting",null],["091760148148","Mörnsheim, M",null],["091760161161","Stammham",null],["091760164164","Titting, M",null],["091760166166","Wellheim, M",null],["091760167167","Wettstetten",null],["091765115155","Pollenfeld",null],["091765115160","Schernfeld",null],["091765115165","Walting",null],["091765116116","Böhmfeld",null],["091765116124","Eitensheim",null],["091765118111","Adelschlag",null],["091765118122","Egweil",null],["091765118149","Nassenfels, M",null],["091765119147","Mindelstetten",null],["091765119150","Oberdolling",null],["091765119153","Pförring, M",null],["091769451451","Haunstetter Forst",null],["091770113113","Bockhorn",null],["091770115115","Dorfen, St",null],["091770117117","Erding, GKSt",null],["091770118118","Finsing",null],["091770119119","Forstern",null],["091770120120","Fraunberg",null],["091770123123","Isen, M",null],["091770127127","Lengdorf",null],["091770130130","Moosinning",null],["091770137137","Sankt Wolfgang",null],["091770139139","Taufkirchen (Vils)",null],["091775120114","Buch a.Buchrain",null],["091775120135","Pastetten",null],["091775121142","Walpertskirchen",null],["091775121144","Wörth",null],["091775123116","Eitting",null],["091775123133","Oberding",null],["091775124131","Neuching",null],["091775124134","Ottenhofen",null],["091775125121","Hohenpolding",null],["091775125122","Inning a.Holz",null],["091775125124","Kirchberg",null],["091775125138","Steinkirchen",null],["091775126112","Berglern",null],["091775126126","Langenpreising",null],["091775126143","Wartenberg, M",null],["091780116116","Au i.d.Hallertau, M",null],["091780120120","Eching",null],["091780122122","Rudelzhausen",null],["091780123123","Fahrenzhausen",null],["091780124124","Freising, GKSt",null],["091780130130","Hallbergmoos",null],["091780133133","Hohenkammer",null],["091780136136","Kirchdorf a.d.Amper",null],["091780137137","Kranzberg",null],["091780138138","Langenbach",null],["091780140140","Marzling",null],["091780143143","Moosburg a.d.Isar, St",null],["091780144144","Nandlstadt, M",null],["091780145145","Neufahrn b.Freising",null],["091785127113","Allershausen",null],["091785127150","Paunzhausen",null],["091785129125","Gammelsdorf",null],["091785129132","Hörgertshausen",null],["091785129142","Mauern",null],["091785129155","Wang",null],["091785130115","Attenkirchen",null],["091785130129","Haag a.d.Amper",null],["091785130156","Wolfersdorf",null],["091785130157","Zolling",null],["091790113113","Alling",null],["091790117117","Egenhofen",null],["091790118118","Eichenau",null],["091790119119","Emmering",null],["091790121121","Fürstenfeldbruck, GKSt",null],["091790123123","Germering, GKSt",null],["091790126126","Gröbenzell",null],["091790134134","Maisach",null],["091790138138","Moorenweis",null],["091790142142","Olching, St",null],["091790145145","Puchheim, St",null],["091790149149","Türkenfeld",null],["091795131111","Adelshofen",null],["091795131114","Althegnenberg",null],["091795131128","Hattenhofen",null],["091795131130","Jesenwang",null],["091795131132","Landsberied",null],["091795131136","Mammendorf",null],["091795131137","Mittelstetten",null],["091795131140","Oberschweinbach",null],["091795132125","Grafrath",null],["091795132131","Kottgeisering",null],["091795132147","Schöngeising",null],["091800112112","Bad Kohlgrub",null],["091800116116","Farchant",null],["091800117117","Garmisch-Partenkirchen, M",null],["091800118118","Grainau",null],["091800122122","Krün",null],["091800123123","Mittenwald, M",null],["091800124124","Murnau a.Staffelsee, M",null],["091800125125","Oberammergau",null],["091800126126","Oberau",null],["091800134134","Uffing a.Staffelsee",null],["091800136136","Wallgau",null],["091805133113","Bad Bayersoien",null],["091805133129","Saulgrub",null],["091805135115","Ettal",null],["091805135135","Unterammergau",null],["091805136114","Eschenlohe",null],["091805136119","Großweil",null],["091805136127","Ohlstadt",null],["091805136131","Schwaigen",null],["091805137128","Riegsee",null],["091805137132","Seehausen a.Staffelsee",null],["091805137133","Spatzenhausen",null],["091809451451","Ettaler Forst",null],["091810113113","Denklingen",null],["091810114114","Dießen am Ammersee, M",null],["091810116116","Egling a.d.Paar",null],["091810122122","Geltendorf",null],["091810128128","Kaufering, M",null],["091810130130","Landsberg am Lech, GKSt",null],["091810132132","Penzing",null],["091810144144","Utting am Ammersee",null],["091810145145","Weil",null],["091815138121","Fuchstal",null],["091815138143","Unterdießen",null],["091815139126","Hurlach",null],["091815139127","Igling",null],["091815139131","Obermeitingen",null],["091815140134","Prittriching",null],["091815140138","Scheuring",null],["091815141124","Hofstetten",null],["091815141140","Schwifting",null],["091815141141","Pürgen",null],["091815142111","Apfeldorf",null],["091815142129","Kinsau",null],["091815142133","Vilgertshofen",null],["091815142135","Reichling",null],["091815142137","Rott",null],["091815142142","Thaining",null],["091815143115","Eching am Ammersee",null],["091815143123","Greifenberg",null],["091815143139","Schondorf am Ammersee",null],["091815144118","Eresing",null],["091815144120","Finning",null],["091815144146","Windach",null],["091819451451","Ammersee",null],["091820111111","Bad Wiessee",null],["091820112112","Bayrischzell",null],["091820114114","Fischbachau",null],["091820116116","Gmund a.Tegernsee",null],["091820119119","Hausham",null],["091820120120","Holzkirchen, M",null],["091820123123","Irschenberg",null],["091820124124","Kreuth",null],["091820125125","Miesbach, St",null],["091820127127","Otterfing",null],["091820129129","Rottach-Egern",null],["091820131131","Schliersee, M",null],["091820132132","Tegernsee, St",null],["091820133133","Valley",null],["091820134134","Waakirchen",null],["091820136136","Warngau",null],["091820137137","Weyarn",null],["091830112112","Ampfing",null],["091830113113","Aschau a.Inn",null],["091830114114","Buchbach, M",null],["091830119119","Haag i.OB, M",null],["091830127127","Mettenheim",null],["091830128128","Mühldorf a.Inn, St",null],["091830135135","Obertaufkirchen",null],["091830144144","Schwindegg",null],["091830148148","Waldkraiburg, St",null],["091835145120","Heldenstein",null],["091835145138","Rattenkirchen",null],["091835146118","Gars a.Inn, M",null],["091835146147","Unterreit",null],["091835147123","Kirchdorf",null],["091835147140","Reichertsheim",null],["091835148122","Jettenbach",null],["091835148124","Kraiburg a.Inn, M",null],["091835148145","Taufkirchen",null],["091835149115","Egglkofen",null],["091835149129","Neumarkt-Sankt Veit, St",null],["091835150125","Lohkirchen",null],["091835150132","Oberbergkirchen",null],["091835150143","Schönberg",null],["091835150151","Zangberg",null],["091835151134","Oberneukirchen",null],["091835151136","Polling",null],["091835152116","Erharting",null],["091835152130","Niederbergkirchen",null],["091835152131","Niedertaufkirchen",null],["091835183126","Maitenbeth",null],["091835183139","Rechtmehring",null],["091839451451","Mühldorfer Hart",null],["091840112112","Aschheim",null],["091840113113","Baierbrunn",null],["091840114114","Brunnthal",null],["091840118118","Feldkirchen",null],["091840119119","Garching b.München, St",null],["091840120120","Gräfelfing",null],["091840121121","Grasbrunn",null],["091840122122","Grünwald",null],["091840123123","Haar",null],["091840127127","Höhenkirchen-Siegertsbrunn",null],["091840129129","Hohenbrunn",null],["091840130130","Ismaning",null],["091840131131","Kirchheim b.München",null],["091840132132","Neuried",null],["091840134134","Oberhaching",null],["091840135135","Oberschleißheim",null],["091840136136","Ottobrunn",null],["091840137137","Aying",null],["091840138138","Planegg",null],["091840139139","Pullach i.Isartal",null],["091840140140","Putzbrunn",null],["091840141141","Sauerlach",null],["091840142142","Schäftlarn",null],["091840144144","Straßlach-Dingharting",null],["091840145145","Taufkirchen",null],["091840146146","Neubiberg",null],["091840147147","Unterföhring",null],["091840148148","Unterhaching",null],["091840149149","Unterschleißheim, St",null],["091849452452","Forstenrieder Park",null],["091849454454","Grünwalder Forst",null],["091849457457","Perlacher Forst",null],["091850113113","Aresing",null],["091850125125","Burgheim, M",null],["091850127127","Ehekirchen",null],["091850139139","Karlshuld",null],["091850140140","Karlskron",null],["091850149149","Neuburg a.d.Donau, GKSt",null],["091850150150","Oberhausen",null],["091850153153","Rennertshofen, M",null],["091850158158","Schrobenhausen, St",null],["091850163163","Königsmoos",null],["091850168168","Weichering",null],["091855154118","Bergheim",null],["091855154157","Rohrenfels",null],["091855155116","Berg im Gau",null],["091855155123","Brunnen",null],["091855155131","Gachenbach",null],["091855155143","Langenmosen",null],["091855155166","Waidhofen",null],["091860113113","Baar-Ebenhausen",null],["091860125125","Gerolsbach",null],["091860128128","Hohenwart, M",null],["091860132132","Jetzendorf",null],["091860137137","Manching, M",null],["091860139139","Münchsmünster",null],["091860143143","Pfaffenhofen a.d.Ilm, St",null],["091860146146","Reichertshausen",null],["091860149149","Rohrbach",null],["091860151151","Scheyern",null],["091860152152","Schweitenkirchen",null],["091860158158","Vohburg a.d.Donau, St",null],["091860162162","Wolnzach, M",null],["091865156116","Ernsgaden",null],["091865156122","Geisenfeld, St",null],["091865157126","Hettenshausen",null],["091865157130","Ilmmünster",null],["091865158144","Pörnbach",null],["091865158147","Reichertshofen, M",null],["091870113113","Amerang",null],["091870114114","Aschau i.Chiemgau",null],["091870116116","Babensham",null],["091870117117","Bad Aibling, St",null],["091870118118","Bernau a.Chiemsee",null],["091870120120","Brannenburg",null],["091870122122","Bruckmühl, M",null],["091870124124","Edling",null],["091870125125","Eggstätt",null],["091870126126","Eiselfing",null],["091870128128","Bad Endorf, M",null],["091870129129","Bad Feilnbach",null],["091870130130","Feldkirchen-Westerham",null],["091870131131","Flintsbach a.Inn",null],["091870132132","Frasdorf",null],["091870134134","Griesstätt",null],["091870137137","Großkarolinenfeld",null],["091870142142","Schechen",null],["091870148148","Kiefersfelden",null],["091870150150","Kolbermoor, St",null],["091870154154","Neubeuern, M",null],["091870156156","Nußdorf a.Inn",null],["091870157157","Oberaudorf",null],["091870162162","Prien a.Chiemsee, M",null],["091870163163","Prutting",null],["091870165165","Raubling",null],["091870167167","Riedering",null],["091870168168","Rimsting",null],["091870169169","Rohrdorf",null],["091870172172","Samerberg",null],["091870174174","Söchtenau",null],["091870176176","Soyen",null],["091870177177","Stephanskirchen",null],["091870179179","Tuntenhausen",null],["091870181181","Vogtareuth",null],["091870182182","Wasserburg a.Inn, St",null],["091875160121","Breitbrunn a.Chiemsee",null],["091875160123","Chiemsee",null],["091875160138","Gstadt a.Chiemsee",null],["091875162139","Halfing",null],["091875162145","Höslwang",null],["091875162173","Schonstett",null],["091875165164","Ramerberg",null],["091875165170","Rott a.Inn",null],["091875184159","Pfaffing",null],["091875184186","Albaching",null],["091879451451","Rotter Forst-Nord",null],["091879452452","Rotter Forst-Süd",null],["091880113113","Berg",null],["091880117117","Andechs",null],["091880118118","Feldafing",null],["091880120120","Gauting",null],["091880121121","Gilching",null],["091880124124","Herrsching a.Ammersee",null],["091880126126","Inning a.Ammersee",null],["091880127127","Krailling",null],["091880132132","Seefeld",null],["091880137137","Pöcking",null],["091880139139","Starnberg, St",null],["091880141141","Tutzing",null],["091880144144","Weßling",null],["091880145145","Wörthsee",null],["091889451451","Starnberger See",null],["091890111111","Altenmarkt a.d.Alz",null],["091890114114","Chieming",null],["091890115115","Engelsberg",null],["091890118118","Fridolfing",null],["091890119119","Grabenstätt",null],["091890120120","Grassau, M",null],["091890124124","Inzell",null],["091890127127","Kirchanschöring",null],["091890130130","Nußdorf",null],["091890134134","Palling",null],["091890135135","Petting",null],["091890139139","Reit im Winkl",null],["091890140140","Ruhpolding",null],["091890141141","Schleching",null],["091890142142","Schnaitsee",null],["091890143143","Seeon-Seebruck",null],["091890145145","Siegsdorf",null],["091890148148","Surberg",null],["091890149149","Tacherting",null],["091890152152","Tittmoning, St",null],["091890154154","Traunreut, St",null],["091890155155","Traunstein, GKSt",null],["091890157157","Trostberg, St",null],["091890159159","Übersee",null],["091890160160","Unterwössen",null],["091895166113","Bergen",null],["091895166161","Vachendorf",null],["091895169129","Marquartstein",null],["091895169146","Staudach-Egerndach",null],["091895170126","Kienberg",null],["091895170133","Obing",null],["091895170137","Pittenhart",null],["091895173150","Taching a.See",null],["091895173162","Waging a.See, M",null],["091895173165","Wonneberg",null],["091899451451","Chiemsee (See)",null],["091899452452","Waginger See",null],["091900115115","Bernried am Starnberger See",null],["091900130130","Hohenpeißenberg",null],["091900138138","Pähl",null],["091900139139","Peißenberg, M",null],["091900140140","Peiting, M",null],["091900141141","Penzberg, St",null],["091900142142","Polling",null],["091900144144","Raisting",null],["091900148148","Schongau, St",null],["091900157157","Weilheim i.OB, St",null],["091900158158","Wessobrunn",null],["091900159159","Wielenbach",null],["091905174111","Altenstadt",null],["091905174129","Hohenfurch",null],["091905174133","Ingenried",null],["091905174149","Schwabbruck",null],["091905174151","Schwabsoien",null],["091905175114","Bernbeuren",null],["091905175118","Burggen",null],["091905176113","Antdorf",null],["091905176126","Habach",null],["091905176136","Obersöchering",null],["091905176153","Sindelsdorf",null],["091905177120","Eberfing",null],["091905177121","Eglfing",null],["091905177131","Huglfing",null],["091905177135","Oberhausen",null],["091905178117","Böbing",null],["091905178145","Rottenbuch",null],["091905179132","Iffeldorf",null],["091905179152","Seeshaupt",null],["091905180143","Prem",null],["091905180154","Steingaden",null],["091905180160","Wildsteig",null],["092610000000","Landshut",null],["092620000000","Passau",null],["092630000000","Straubing",null],["092710111111","Aholming",null],["092710113113","Auerbach",null],["092710116116","Bernried",null],["092710119119","Deggendorf, GKSt",null],["092710122122","Grafling",null],["092710125125","Hengersberg, M",null],["092710127127","Iggensbach",null],["092710128128","Künzing",null],["092710132132","Metten, M",null],["092710138138","Niederalteich",null],["092710140140","Offenberg",null],["092710141141","Osterhofen, St",null],["092710146146","Plattling, St",null],["092710151151","Stephansposching",null],["092710153153","Winzer, M",null],["092715202123","Grattersdorf",null],["092715202126","Hunding",null],["092715202130","Lalling",null],["092715202148","Schaufling",null],["092715204139","Oberpöring",null],["092715204143","Otzing",null],["092715204152","Wallerfing",null],["092715205118","Buchhofen",null],["092715205135","Moos",null],["092715206114","Außernzell",null],["092715206149","Schöllnach, M",null],["092720118118","Freyung, St",null],["092720120120","Grafenau, St",null],["092720121121","Grainet",null],["092720122122","Haidmühle",null],["092720127127","Hohenau",null],["092720129129","Jandelsbrunn",null],["092720134134","Mauth",null],["092720136136","Neureichenau",null],["092720140140","Ringelai",null],["092720141141","Röhrnbach, M",null],["092720142142","Saldenburg",null],["092720143143","Sankt Oswald-Riedlhütte",null],["092720146146","Neuschönau",null],["092720149149","Spiegelau",null],["092720151151","Waldkirchen, St",null],["092725211116","Eppenschlag",null],["092725211128","Innernzell",null],["092725211145","Schöfweg",null],["092725211147","Schönberg, M",null],["092725212126","Hinterschmiding",null],["092725212139","Philippsreut",null],["092725213150","Thurmansbang",null],["092725213152","Zenting",null],["092725214119","Fürsteneck",null],["092725214138","Perlesreut, M",null],["092729451451","Annathaler Wald",null],["092729452452","Frauenberger u. Duschlberger Wald",null],["092729453453","Graineter Wald",null],["092729455455","Leopoldsreuter Wald",null],["092729456456","Mauther Forst",null],["092729457457","Philippsreuter Wald",null],["092729458458","Pleckensteiner Wald",null],["092729459459","Sankt Oswald",null],["092729460460","Schlichtenberger Wald",null],["092729461461","Schönbrunner Wald",null],["092729463463","Waldhäuserwald",null],["092730111111","Abensberg, St",null],["092730116116","Bad Abbach, M",null],["092730137137","Kelheim, St",null],["092730147147","Mainburg, St",null],["092730152152","Neustadt a.d.Donau, St",null],["092730159159","Painten, M",null],["092730164164","Riedenburg, St",null],["092730165165","Rohr i.NB, M",null],["092735215121","Essing, M",null],["092735215133","Ihrlerstein",null],["092735216166","Saal a.d.Donau",null],["092735216175","Teugn",null],["092735217125","Hausen",null],["092735217127","Herrngiersdorf",null],["092735217141","Langquaid, M",null],["092735218119","Biburg",null],["092735218139","Kirchdorf",null],["092735218172","Siegenburg, M",null],["092735218177","Train",null],["092735218181","Wildenberg",null],["092735219113","Aiglsbach",null],["092735219115","Attenhofen",null],["092735219163","Elsendorf",null],["092735219178","Volkenschwand",null],["092739451451","Dürnbucher Forst",null],["092739452452","Frauenforst",null],["092739453453","Hacklberg",null],["092739454454","Hienheimer Forst",null],["092740111111","Adlkofen",null],["092740113113","Altdorf, M",null],["092740120120","Bodenkirchen",null],["092740121121","Buch a.Erlbach",null],["092740124124","Eching",null],["092740126126","Ergolding, M",null],["092740128128","Essenbach, M",null],["092740134134","Geisenhausen, M",null],["092740141141","Hohenthann",null],["092740146146","Kumhausen",null],["092740153153","Neufahrn i.NB",null],["092740156156","Niederaichbach",null],["092740172172","Pfeffenhausen, M",null],["092740176176","Rottenburg a.d.Laaber, St",null],["092740182182","Tiefenbach",null],["092740184184","Vilsbiburg, St",null],["092740185185","Vilsheim",null],["092740194194","Bruckberg",null],["092745220119","Bayerbach b.Ergoldsbach",null],["092745220127","Ergoldsbach, M",null],["092745221132","Furth",null],["092745221165","Obersüßbach",null],["092745221187","Weihmichl",null],["092745222174","Postau",null],["092745222188","Weng",null],["092745222191","Wörth a.d.Isar",null],["092745223112","Aham",null],["092745223135","Gerzen",null],["092745223145","Kröning",null],["092745223179","Schalkham",null],["092745226114","Altfraunhofen",null],["092745226118","Baierbach",null],["092745227154","Neufraunhofen",null],["092745227183","Velden, M",null],["092745227193","Wurmsham",null],["092750111111","Aicha vorm Wald",null],["092750114114","Aldersbach",null],["092750116116","Bad Füssing",null],["092750118118","Breitenberg",null],["092750119119","Büchlberg",null],["092750120120","Eging a.See, M",null],["092750121121","Fürstenstein",null],["092750122122","Fürstenzell, M",null],["092750124124","Bad Griesbach i.Rottal, St",null],["092750125125","Haarbach",null],["092750126126","Hauzenberg, St",null],["092750127127","Hofkirchen, M",null],["092750128128","Hutthurm, M",null],["092750130130","Kirchham",null],["092750131131","Kößlarn, M",null],["092750133133","Neuburg a.Inn",null],["092750134134","Neuhaus a.Inn",null],["092750135135","Neukirchen vorm Wald",null],["092750137137","Obernzell, M",null],["092750138138","Ortenburg, M",null],["092750141141","Pocking, St",null],["092750144144","Ruderting",null],["092750145145","Ruhstorf a.d.Rott, M",null],["092750146146","Salzweg",null],["092750148148","Sonnen",null],["092750149149","Tettenweis",null],["092750150150","Thyrnau",null],["092750151151","Tiefenbach",null],["092750153153","Untergriesbach, M",null],["092750154154","Vilshofen an der Donau, St",null],["092750156156","Wegscheid, M",null],["092750159159","Windorf, M",null],["092755229152","Tittling, M",null],["092755229160","Witzmannsberg",null],["092755232112","Aidenbach, M",null],["092755232117","Beutelsbach",null],["092755234132","Malching",null],["092755234143","Rotthalmünster, M",null],["092760113113","Arnbruck",null],["092760115115","Bayerisch Eisenstein",null],["092760116116","Bischofsmais",null],["092760117117","Bodenmais, M",null],["092760118118","Böbrach",null],["092760120120","Drachselsried",null],["092760121121","Frauenau",null],["092760122122","Geiersthal",null],["092760126126","Kirchberg i.Wald",null],["092760127127","Kirchdorf i.Wald",null],["092760128128","Kollnburg",null],["092760129129","Langdorf",null],["092760130130","Lindberg",null],["092760134134","Patersdorf",null],["092760135135","Prackenbach",null],["092760138138","Regen, St",null],["092760139139","Rinchnach",null],["092760143143","Teisnach, M",null],["092760144144","Viechtach, St",null],["092760148148","Zwiesel, St",null],["092765238111","Achslach",null],["092765238123","Gotteszell",null],["092765238142","Ruhmannsfelden, M",null],["092765238146","Zachenberg",null],["092770111111","Arnstorf, M",null],["092770114114","Dietersburg",null],["092770116116","Eggenfelden, St",null],["092770117117","Egglham",null],["092770121121","Gangkofen, M",null],["092770124124","Hebertsfelden",null],["092770126126","Johanniskirchen",null],["092770127127","Julbach",null],["092770128128","Kirchdorf a.Inn",null],["092770134134","Mitterskirchen",null],["092770138138","Pfarrkirchen, St",null],["092770139139","Postmünster",null],["092770142142","Roßbach",null],["092770144144","Schönau",null],["092770145145","Simbach a.Inn, St",null],["092770149149","Triftern, M",null],["092770151151","Unterdietfurt",null],["092770152152","Wittibreut",null],["092770153153","Wurmannsquick, M",null],["092770154154","Zeilarn",null],["092775239119","Falkenberg",null],["092775239131","Malgersdorf",null],["092775239141","Rimbach",null],["092775240122","Geratskirchen",null],["092775240133","Massing, M",null],["092775241112","Bayerbach",null],["092775241113","Bad Birnbach, M",null],["092775243140","Reut",null],["092775243148","Tann, M",null],["092775244118","Ering",null],["092775244147","Stubenberg",null],["092780118118","Bogen, St",null],["092780121121","Feldkirchen",null],["092780123123","Geiselhöring, St",null],["092780129129","Haibach",null],["092780141141","Kirchroth",null],["092780143143","Konzell",null],["092780144144","Laberweinting",null],["092780146146","Leiblfing",null],["092780148148","Mallersdorf-Pfaffenberg, M",null],["092780167167","Oberschneiding",null],["092780170170","Parkstetten",null],["092780178178","Rattenberg",null],["092780184184","Sankt Englmar",null],["092780190190","Steinach",null],["092780197197","Wiesenfelden",null],["092785246147","Loitzendorf",null],["092785246179","Rattiszell",null],["092785246189","Stallwang",null],["092785248116","Ascha",null],["092785248120","Falkenfels",null],["092785248134","Haselbach",null],["092785248151","Mitterfels, M",null],["092785249139","Hunderdorf",null],["092785249154","Neukirchen",null],["092785249198","Windberg",null],["092785250112","Aholfing",null],["092785250117","Atting",null],["092785250172","Perkam",null],["092785250177","Rain",null],["092785252149","Mariaposching",null],["092785252159","Niederwinkling",null],["092785252171","Perasdorf",null],["092785252187","Schwarzach, M",null],["092785256113","Aiterhofen",null],["092785256182","Salching",null],["092785257140","Irlbach",null],["092785257192","Straßkirchen",null],["092790112112","Dingolfing, St",null],["092790113113","Eichendorf, M",null],["092790115115","Frontenhausen, M",null],["092790122122","Landau a.d.Isar, St",null],["092790124124","Loiching",null],["092790126126","Marklkofen",null],["092790127127","Mengkofen",null],["092790128128","Moosthenning",null],["092790130130","Niederviehbach",null],["092790132132","Pilsting, M",null],["092790134134","Reisbach, M",null],["092790135135","Simbach, M",null],["092790137137","Wallersdorf, M",null],["092795208116","Gottfrieding",null],["092795208125","Mamming",null],["093610000000","Amberg",null],["093620000000","Regensburg",null],["093630000000","Weiden i.d.OPf.",null],["093710111111","Ammerthal",null],["093710113113","Auerbach i.d.OPf., St",null],["093710118118","Ebermannsdorf",null],["093710119119","Edelsfeld",null],["093710120120","Ensdorf",null],["093710121121","Freihung, M",null],["093710122122","Freudenberg",null],["093710127127","Hirschau, St",null],["093710129129","Hohenburg, M",null],["093710132132","Kastl, M",null],["093710136136","Kümmersbruck",null],["093710144144","Poppenricht",null],["093710146146","Rieden, M",null],["093710148148","Schmidmühlen, M",null],["093710150150","Schnaittenbach, St",null],["093710151151","Sulzbach-Rosenberg, St",null],["093710154154","Ursensollen",null],["093710156156","Vilseck, St",null],["093715301123","Gebenbach",null],["093715301126","Hahnbach, M",null],["093715302128","Hirschbach",null],["093715302135","Königstein, M",null],["093715303140","Etzelwang",null],["093715303141","Neukirchen b.Sulzbach-Rosenberg",null],["093715303157","Weigendorf",null],["093715304116","Birgland",null],["093715304131","Illschwang",null],["093719452452","Eichen",null],["093720112112","Arnschwang",null],["093720113113","Arrach",null],["093720115115","Blaibach",null],["093720116116","Cham, St",null],["093720117117","Chamerau",null],["093720124124","Eschlkam, M",null],["093720126126","Furth im Wald, St",null],["093720130130","Grafenwiesen",null],["093720135135","Hohenwarth",null],["093720137137","Bad Kötzting, St",null],["093720138138","Lam, M",null],["093720143143","Miltach",null],["093720144144","Neukirchen b.Hl.Blut, M",null],["093720146146","Pemfling",null],["093720151151","Rimbach",null],["093720153153","Roding, St",null],["093720154154","Rötz, St",null],["093720155155","Runding",null],["093720157157","Schönthal",null],["093720158158","Schorndorf",null],["093720164164","Traitsching",null],["093720168168","Waffenbrunn",null],["093720171171","Waldmünchen, St",null],["093720175175","Willmering",null],["093720177177","Zandt",null],["093720178178","Lohberg",null],["093725308163","Tiefenbach",null],["093725308165","Treffelstein",null],["093725310147","Pösing",null],["093725310161","Stamsried, M",null],["093725312128","Gleißenberg",null],["093725312174","Weiding",null],["093725313149","Reichenbach",null],["093725313170","Walderbach",null],["093725317167","Zell",null],["093725317169","Wald",null],["093725318125","Falkenstein, M",null],["093725318142","Michelsneukirchen",null],["093725318150","Rettenbach",null],["093730112112","Berching, St",null],["093730113113","Berg b.Neumarkt i.d.OPf.",null],["093730115115","Breitenbrunn, M",null],["093730119119","Deining",null],["093730121121","Dietfurt a.d.Altmühl, St",null],["093730126126","Freystadt, St",null],["093730134134","Hohenfels, M",null],["093730140140","Lauterhofen, M",null],["093730143143","Lupburg, M",null],["093730146146","Mühlhausen",null],["093730147147","Neumarkt i.d.OPf., GKSt",null],["093730151151","Parsberg, St",null],["093730155155","Postbauer-Heng, M",null],["093730156156","Pyrbaum, M",null],["093730160160","Seubersdorf i.d.OPf.",null],["093730167167","Velburg, St",null],["093735321114","Berngau",null],["093735321153","Pilsach",null],["093735321159","Sengenthal",null],["093740111111","Altenstadt a.d.Waldnaab",null],["093740118118","Eslarn, M",null],["093740121121","Floß, M",null],["093740122122","Flossenbürg",null],["093740124124","Grafenwöhr, St",null],["093740133133","Luhe-Wildenau, M",null],["093740134134","Mantel, M",null],["093740137137","Moosbach, M",null],["093740139139","Neustadt a.d.Waldnaab, St",null],["093740162162","Vohenstrauß, St",null],["093740164164","Waidhaus, M",null],["093740165165","Waldthurn, M",null],["093740168168","Windischeschenbach, St",null],["093745323128","Kirchendemenreuth",null],["093745323144","Parkstein, M",null],["093745323150","Püchersreuth",null],["093745323158","Störnstein",null],["093745323160","Theisseil",null],["093745324148","Trabitz",null],["093745324149","Pressath, St",null],["093745324156","Schwarzenbach",null],["093745325119","Etzenricht",null],["093745325131","Kohlberg, M",null],["093745325166","Weiherhammer",null],["093745326129","Kirchenthumbach, M",null],["093745326155","Schlammersdorf",null],["093745326163","Vorbach",null],["093745327117","Eschenbach i.d.OPf., St",null],["093745327140","Neustadt am Kulm, St",null],["093745327157","Speinshart",null],["093745329127","Irchenrieth",null],["093745329146","Pirk",null],["093745329154","Schirmitz",null],["093745329170","Bechtsrieth",null],["093745330132","Leuchtenberg, M",null],["093745330159","Tännesberg, M",null],["093745331123","Georgenberg",null],["093745331147","Pleystein, St",null],["093749451451","Heinersreuther Forst",null],["093749452452","Manteler Forst",null],["093749458458","Speinsharter Forst",null],["093750117117","Barbing",null],["093750118118","Beratzhausen, M",null],["093750119119","Bernhardswald",null],["093750143143","Hagelstadt",null],["093750148148","Hemau, St",null],["093750161161","Köfering",null],["093750165165","Lappersdorf, M",null],["093750170170","Mintraching",null],["093750174174","Neutraubling, St",null],["093750175175","Nittendorf, M",null],["093750179179","Obertraubling",null],["093750180180","Pentling",null],["093750181181","Pettendorf",null],["093750183183","Pfatter",null],["093750190190","Regenstauf, M",null],["093750196196","Schierling, M",null],["093750199199","Sinzing",null],["093750204204","Tegernheim",null],["093750205205","Thalmassing",null],["093750208208","Wenzenbach",null],["093750209209","Wiesent",null],["093750213213","Zeitlarn",null],["093755332131","Duggendorf",null],["093755332153","Holzheim a.Forst",null],["093755332156","Kallmünz, M",null],["093755333122","Brunn",null],["093755333127","Deuerling",null],["093755333162","Laaber, M",null],["093755334184","Pielenhofen",null],["093755334211","Wolfsegg",null],["093755335114","Altenthann",null],["093755335116","Bach a.d.Donau",null],["093755335130","Donaustauf, M",null],["093755336120","Brennberg",null],["093755336210","Wörth a.d.Donau, St",null],["093755337113","Alteglofsheim",null],["093755337182","Pfakofen",null],["093755338115","Aufhausen",null],["093755338171","Mötzing",null],["093755338191","Riekofen",null],["093755338201","Sünching",null],["093759451451","Forstmühler Forst",null],["093759452452","Kreuther Forst",null],["093760116116","Bodenwöhr",null],["093760117117","Bruck i.d.OPf., M",null],["093760119119","Burglengenfeld, St",null],["093760125125","Fensterbach",null],["093760141141","Maxhütte-Haidhof, St",null],["093760147147","Neunburg vorm Wald, St",null],["093760149149","Nittenau, St",null],["093760150150","Wernberg-Köblitz, M",null],["093760151151","Oberviechtach, St",null],["093760159159","Schmidgaden",null],["093760161161","Schwandorf, GKSt",null],["093760170170","Teublitz, St",null],["093765339131","Gleiritsch",null],["093765339148","Niedermurach",null],["093765339171","Teunz",null],["093765339178","Winklarn, M",null],["093765341112","Altendorf",null],["093765341133","Guteneck",null],["093765341144","Nabburg, St",null],["093765342162","Schwarzach b.Nabburg",null],["093765342163","Schwarzenfeld, M",null],["093765342169","Stulln",null],["093765343153","Pfreimd, St",null],["093765343173","Trausnitz",null],["093765344160","Schönsee, St",null],["093765344167","Stadlern",null],["093765344176","Weiding",null],["093765345122","Dieterskirchen",null],["093765345146","Neukirchen-Balbini, M",null],["093765345164","Schwarzhofen, M",null],["093765345172","Thanstein",null],["093765346168","Steinberg am See",null],["093765346175","Wackersdorf",null],["093769455455","Wolferlohe",null],["093770112112","Bärnau, St",null],["093770116116","Erbendorf, St",null],["093770118118","Friedenfels",null],["093770119119","Fuchsmühl, M",null],["093770127127","Immenreuth",null],["093770131131","Konnersreuth, M",null],["093770133133","Kulmain",null],["093770139139","Mähring, M",null],["093770142142","Bad Neualbenreuth, M",null],["093770146146","Plößberg, M",null],["093770154154","Tirschenreuth, St",null],["093770157157","Waldershof, St",null],["093770158158","Waldsassen, St",null],["093775347137","Leonberg",null],["093775347141","Mitterteich, St",null],["093775347145","Pechbrunn",null],["093775348128","Kastl",null],["093775348129","Kemnath, St",null],["093775349113","Brand",null],["093775349115","Ebnath",null],["093775349143","Neusorg",null],["093775349148","Pullenreuth",null],["093775350132","Krummennaab",null],["093775350149","Reuth b.Erbendorf",null],["093775351117","Falkenberg, M",null],["093775351159","Wiesau, M",null],["094610000000","Bamberg",null],["094620000000","Bayreuth",null],["094630000000","Coburg",null],["094640000000","Hof",null],["094710111111","Altendorf",null],["094710117117","Bischberg",null],["094710119119","Breitengüßbach",null],["094710123123","Buttenheim, M",null],["094710131131","Frensdorf",null],["094710137137","Gundelsheim",null],["094710140140","Hallstadt, St",null],["094710142142","Heiligenstadt i.OFr., M",null],["094710145145","Hirschaid, M",null],["094710150150","Kemmern",null],["094710155155","Litzendorf",null],["094710159159","Memmelsdorf",null],["094710165165","Oberhaid",null],["094710169169","Pettstadt",null],["094710172172","Pommersfelden",null],["094710174174","Rattelsdorf, M",null],["094710185185","Scheßlitz, St",null],["094710191191","Stegaurach",null],["094710195195","Strullendorf",null],["094710207207","Viereth-Trunstadt",null],["094710208208","Walsdorf",null],["094710214214","Zapfendorf, M",null],["094710220220","Schlüsselfeld, St",null],["094715401115","Baunach, St",null],["094715401133","Gerach",null],["094715401152","Lauter",null],["094715401175","Reckendorf",null],["094715403151","Königsfeld",null],["094715403189","Stadelhofen",null],["094715403209","Wattendorf",null],["094715407122","Burgwindheim, M",null],["094715407128","Ebrach, M",null],["094715408120","Burgebrach, M",null],["094715408186","Schönbrunn i.Steigerwald",null],["094715445154","Lisberg",null],["094715445173","Priesendorf",null],["094719452452","Ebracher Forst",null],["094719453453","Eichwald",null],["094719454454","Geisberger Forst",null],["094719455455","Hauptsmoor",null],["094719456456","Koppenwinder Forst",null],["094719457457","Lindach",null],["094719459459","Semberg",null],["094719460460","Steinachsrangen",null],["094719461461","Winkelhofer Forst",null],["094719462462","Zückshuter Forst",null],["094720111111","Ahorntal",null],["094720116116","Bad Berneck i.Fichtelgebirge, St",null],["094720119119","Bindlach",null],["094720121121","Bischofsgrün",null],["094720131131","Eckersdorf",null],["094720138138","Fichtelberg",null],["094720139139","Gefrees, St",null],["094720143143","Goldkronach, St",null],["094720150150","Heinersreuth",null],["094720164164","Mehlmeisel",null],["094720175175","Pegnitz, St",null],["094720179179","Pottenstein, St",null],["094720190190","Speichersdorf",null],["094720197197","Waischenfeld, St",null],["094720198198","Warmensteinach",null],["094725412115","Aufseß",null],["094725412154","Hollfeld, St",null],["094725412176","Plankenfels",null],["094725413141","Glashütten",null],["094725413167","Mistelgau",null],["094725414140","Gesees",null],["094725414155","Hummeltal",null],["094725414166","Mistelbach",null],["094725415133","Emtmannsberg",null],["094725415156","Kirchenpingarten",null],["094725415188","Seybothenreuth",null],["094725415199","Weidenberg, M",null],["094725416127","Creußen, St",null],["094725416146","Haag",null],["094725416180","Prebitz",null],["094725416184","Schnabelwaid, M",null],["094725417118","Betzenstein, St",null],["094725417177","Plech, M",null],["094729451451","Bischofsgrüner Forst",null],["094729453453","Fichtelberg",null],["094729454454","Forst Neustädtlein a.Forst",null],["094729456456","Glashüttener Forst",null],["094729458458","Heinersreuther Forst",null],["094729463463","Neubauer Forst-Nord",null],["094729464464","Prüll",null],["094729468468","Veldensteinerforst",null],["094729469469","Waidacher Forst",null],["094729470470","Warmensteinacher Forst-Nord",null],["094730112112","Ahorn",null],["094730120120","Dörfles-Esbach",null],["094730121121","Ebersdorf b.Coburg",null],["094730132132","Großheirath",null],["094730138138","Itzgrund",null],["094730141141","Lautertal",null],["094730144144","Meeder",null],["094730151151","Neustadt b.Coburg, GKSt",null],["094730158158","Bad Rodach, St",null],["094730159159","Rödental, St",null],["094730165165","Seßlach, St",null],["094730166166","Sonnefeld",null],["094730170170","Untersiemau",null],["094730174174","Weidhausen b.Coburg",null],["094730175175","Weitramsdorf",null],["094735418134","Grub a.Forst",null],["094735418153","Niederfüllbach",null],["094739452452","Callenberger Forst-West",null],["094739453453","Gellnhausen",null],["094739454454","Köllnholz",null],["094740123123","Eggolsheim, M",null],["094740124124","Egloffstein, M",null],["094740126126","Forchheim, GKSt",null],["094740129129","Gößweinstein, M",null],["094740133133","Hallerndorf",null],["094740134134","Hausen",null],["094740135135","Heroldsbach",null],["094740140140","Igensdorf, M",null],["094740146146","Langensendelbach",null],["094740154154","Neunkirchen a.Brand, M",null],["094740156156","Obertrubach",null],["094740161161","Pretzfeld, M",null],["094740176176","Wiesenttal, M",null],["094745420121","Ebermannstadt, St",null],["094745420168","Unterleinleiter",null],["094745422145","Kunreuth",null],["094745422158","Pinzberg",null],["094745422175","Wiesenthau",null],["094745423143","Kirchehrenbach",null],["094745423147","Leutenbach",null],["094745423171","Weilersbach",null],["094745425122","Effeltrich",null],["094745425160","Poxdorf",null],["094745426119","Dormitz",null],["094745426137","Hetzles",null],["094745426144","Kleinsendelbach",null],["094745427132","Gräfenberg, St",null],["094745427138","Hiltpoltstein, M",null],["094745427173","Weißenohe",null],["094750112112","Bad Steben, M",null],["094750113113","Berg",null],["094750120120","Döhlau",null],["094750128128","Geroldsgrün",null],["094750136136","Helmbrechts, St",null],["094750141141","Köditz",null],["094750142142","Konradsreuth",null],["094750154154","Münchberg, St",null],["094750156156","Naila, St",null],["094750158158","Oberkotzau, M",null],["094750161161","Regnitzlosau",null],["094750162162","Rehau, St",null],["094750168168","Schwarzenbach a.d.Saale, St",null],["094750169169","Schwarzenbach a.Wald, St",null],["094750171171","Selbitz, St",null],["094750175175","Stammbach, M",null],["094750189189","Zell im Fichtelgebirge, M",null],["094755428137","Issigau",null],["094755428146","Lichtenberg, St",null],["094755430123","Feilitzsch",null],["094755430127","Gattendorf",null],["094755430181","Töpen",null],["094755430182","Trogen",null],["094755431145","Leupoldsgrün",null],["094755431165","Schauenstein, St",null],["094755432174","Sparneck, M",null],["094755432184","Weißdorf",null],["094759451451","Forst Schwarzenbach a.Wald",null],["094759452452","Gerlaser Forst",null],["094759453453","Geroldsgrüner Forst",null],["094759454454","Martinlamitzer Forst-Nord",null],["094760145145","Kronach, St",null],["094760146146","Küps, M",null],["094760152152","Ludwigsstadt, St",null],["094760159159","Nordhalben, M",null],["094760164164","Pressig, M",null],["094760175175","Steinbach a.Wald",null],["094760177177","Steinwiesen, M",null],["094760178178","Stockheim",null],["094760179179","Tettau, M",null],["094760183183","Marktrodach, M",null],["094760184184","Wallenfels, St",null],["094760185185","Weißenbrunn",null],["094760189189","Wilhelmsthal",null],["094765433166","Reichenbach",null],["094765433180","Teuschnitz, St",null],["094765433182","Tschirn",null],["094765434154","Mitwitz, M",null],["094765434171","Schneckenlohe",null],["094769451451","Birnbaum",null],["094769453453","Langenbacher Forst",null],["094770121121","Himmelkron",null],["094770128128","Kulmbach, GKSt",null],["094770136136","Mainleus, M",null],["094770139139","Marktschorgast, M",null],["094770142142","Neudrossenfeld",null],["094770143143","Neuenmarkt",null],["094770148148","Presseck, M",null],["094770157157","Thurnau, M",null],["094770163163","Wirsberg, M",null],["094775435151","Rugendorf",null],["094775435156","Stadtsteinach, St",null],["094775436117","Grafengehaig, M",null],["094775436138","Marktleugast, M",null],["094775437118","Guttenberg",null],["094775437129","Kupferberg, St",null],["094775437135","Ludwigschorgast, M",null],["094775437159","Untersteinach",null],["094775438124","Kasendorf, M",null],["094775438164","Wonsees, M",null],["094775439119","Harsdorf",null],["094775439127","Ködnitz",null],["094775439158","Trebgast",null],["094780111111","Altenkunstadt",null],["094780116116","Burgkunstadt, St",null],["094780120120","Ebensfeld, M",null],["094780139139","Lichtenfels, St",null],["094780145145","Michelau i.OFr.",null],["094780165165","Bad Staffelstein, St",null],["094780176176","Weismain, St",null],["094785441143","Marktgraitz, M",null],["094785441155","Redwitz a.d.Rodach",null],["094785446127","Hochstadt a.Main",null],["094785446144","Marktzeuln, M",null],["094789451451","Breitengüßbacher Forst",null],["094789453453","Neuensorger Forst",null],["094790112112","Arzberg, St",null],["094790129129","Kirchenlamitz, St",null],["094790135135","Marktleuthen, St",null],["094790136136","Marktredwitz, GKSt",null],["094790145145","Röslau",null],["094790150150","Schönwald, St",null],["094790152152","Selb, GKSt",null],["094790166166","Weißenstadt, St",null],["094790169169","Wunsiedel, St",null],["094795442126","Höchstädt i.Fichtelgebirge",null],["094795442158","Thiersheim, M",null],["094795442159","Thierstein, M",null],["094795443127","Hohenberg a.d.Eger, St",null],["094795443147","Schirnding, M",null],["094795444111","Bad Alexandersbad",null],["094795444138","Nagel",null],["094795444161","Tröstau",null],["094799453453","Kaiserhammer Forst-Ost",null],["094799455455","Martinlamitzer Forst-Süd",null],["094799456456","Meierhöfer Seite",null],["094799457457","Neubauer Forst-Süd",null],["094799459459","Tröstauer Forst-Ost",null],["094799460460","Tröstauer Forst-West",null],["094799461461","Vordorfer Forst",null],["094799462462","Weißenstadter Forst-Nord",null],["094799463463","Weißenstadter Forst-Süd",null],["095610000000","Ansbach",null],["095620000000","Erlangen",null],["095630000000","Fürth",null],["095640000000","Nürnberg",null],["095650000000","Schwabach",null],["095710113113","Arberg, M",null],["095710114114","Aurach",null],["095710115115","Bechhofen, M",null],["095710127127","Burgoberbach",null],["095710130130","Colmberg, M",null],["095710135135","Dietenhofen, M",null],["095710136136","Dinkelsbühl, GKSt",null],["095710139139","Dürrwangen, M",null],["095710145145","Feuchtwangen, St",null],["095710146146","Flachslanden, M",null],["095710165165","Heilsbronn, St",null],["095710166166","Herrieden, St",null],["095710170170","Langfurth",null],["095710171171","Lehrberg, M",null],["095710174174","Leutershausen, St",null],["095710175175","Lichtenau, M",null],["095710177177","Merkendorf, St",null],["095710180180","Neuendettelsau",null],["095710183183","Oberdachstetten",null],["095710190190","Petersaurach",null],["095710193193","Rothenburg ob der Tauber, GKSt",null],["095710196196","Sachsen b.Ansbach",null],["095710199199","Schnelldorf",null],["095710200200","Schopfloch, M",null],["095710214214","Wassertrüdingen, St",null],["095710226226","Windsbach, St",null],["095715501111","Adelshofen",null],["095715501152","Gebsattel",null],["095715501155","Geslau",null],["095715501169","Insingen",null],["095715501181","Neusitz",null],["095715501188","Ohrenbach",null],["095715501205","Steinsfeld",null],["095715501225","Windelsbach",null],["095715502125","Buch a.Wald",null],["095715502134","Diebach",null],["095715502137","Dombühl, M",null],["095715502198","Schillingsfürst, St",null],["095715502222","Wettringen",null],["095715502228","Wörnitz",null],["095715504122","Bruckberg",null],["095715504194","Rügland",null],["095715504217","Weihenzell",null],["095715506189","Ornbau, St",null],["095715506216","Weidenbach, M",null],["095715507128","Burk",null],["095715507132","Dentlein a.Forst, M",null],["095715507223","Wieseth",null],["095715508179","Mönchsroth",null],["095715508218","Weiltingen, M",null],["095715508224","Wilburgstetten",null],["095715509141","Ehingen",null],["095715509154","Gerolfingen",null],["095715509192","Röckingen",null],["095715509208","Unterschwaningen",null],["095715509227","Wittelshofen",null],["095715538178","Mitteleschenbach",null],["095715538229","Wolframs-Eschenbach, St",null],["095719451451","Unterer Wald",null],["095720111111","Adelsdorf",null],["095720115115","Baiersdorf, St",null],["095720119119","Bubenreuth",null],["095720121121","Eckental, M",null],["095720130130","Hemhofen",null],["095720131131","Heroldsberg, M",null],["095720132132","Herzogenaurach, St",null],["095720135135","Höchstadt a.d.Aisch, St",null],["095720137137","Kalchreuth",null],["095720142142","Möhrendorf",null],["095720149149","Röttenbach",null],["095720160160","Wachenroth, M",null],["095720164164","Weisendorf, M",null],["095725510126","Gremsdorf",null],["095725510139","Lonnerstadt, M",null],["095725510143","Mühlhausen, M",null],["095725510159","Vestenbergsgreuth, M",null],["095725512114","Aurachtal",null],["095725512147","Oberreichenbach",null],["095725514120","Buckenhof",null],["095725514141","Marloffstein",null],["095725514154","Spardorf",null],["095725514158","Uttenreuth",null],["095725539127","Großenseebach",null],["095725539133","Heßdorf",null],["095729451451","Birkach",null],["095729452452","Buckenhofer Forst",null],["095729453453","Dormitzer Forst",null],["095729454454","Erlenstegener Forst",null],["095729455455","Forst Tennenlohe",null],["095729456456","Geschaidt",null],["095729457457","Kalchreuther Forst",null],["095729458458","Kraftshofer Forst",null],["095729459459","Mark",null],["095729460460","Neunhofer Forst",null],["095730111111","Ammerndorf, M",null],["095730114114","Cadolzburg, M",null],["095730115115","Großhabersdorf",null],["095730120120","Langenzenn, St",null],["095730122122","Oberasbach, St",null],["095730124124","Puschendorf",null],["095730125125","Roßtal, M",null],["095730127127","Stein, St",null],["095730133133","Wilhermsdorf, M",null],["095730134134","Zirndorf, St",null],["095735517126","Seukendorf",null],["095735517130","Veitsbronn",null],["095735540123","Obermichelbach",null],["095735540129","Tuchenbach",null],["095740112112","Altdorf b.Nürnberg, St",null],["095740117117","Burgthann",null],["095740123123","Feucht, M",null],["095740132132","Hersbruck, St",null],["095740135135","Kirchensittenbach",null],["095740138138","Lauf a.d.Pegnitz, St",null],["095740139139","Leinburg",null],["095740140140","Neuhaus a.d.Pegnitz, M",null],["095740141141","Neunkirchen a.Sand",null],["095740146146","Ottensoos",null],["095740147147","Pommelsbrunn",null],["095740150150","Reichenschwand",null],["095740152152","Röthenbach a.d.Pegnitz, St",null],["095740154154","Rückersdorf",null],["095740155155","Schnaittach, M",null],["095740156156","Schwaig b.Nürnberg",null],["095740157157","Schwarzenbruck",null],["095740158158","Simmelsdorf",null],["095740164164","Winkelhaid",null],["095745527129","Hartenstein",null],["095745527160","Velden, St",null],["095745527161","Vorra",null],["095745528111","Alfeld",null],["095745528128","Happurg",null],["095745529120","Engelthal",null],["095745529131","Henfenfeld",null],["095745529145","Offenhausen",null],["095749451451","Behringersdorfer Forst",null],["095749452452","Brunn",null],["095749453453","Engelthaler Forst",null],["095749454454","Feuchter Forst",null],["095749455455","Fischbach",null],["095749456456","Forsthof",null],["095749457457","Günthersbühler Forst",null],["095749458458","Haimendorfer Forst",null],["095749460460","Laufamholzer Forst",null],["095749461461","Leinburg",null],["095749462462","Rückersdorfer Forst",null],["095749463463","Schönberg",null],["095749464464","Winkelhaid",null],["095749465465","Zerzabelshofer Forst",null],["095750112112","Bad Windsheim, St",null],["095750116116","Burghaslach, M",null],["095750119119","Dietersheim",null],["095750121121","Emskirchen, M",null],["095750135135","Ipsheim, M",null],["095750145145","Markt Erlbach, M",null],["095750153153","Neustadt a.d.Aisch, St",null],["095750156156","Obernzenn, M",null],["095755518138","Langenfeld",null],["095755518144","Markt Bibart, M",null],["095755518147","Markt Taschendorf, M",null],["095755518157","Oberscheinfeld, M",null],["095755518161","Scheinfeld, St",null],["095755518165","Sugenheim, M",null],["095755519122","Ergersheim",null],["095755519127","Gollhofen",null],["095755519130","Hemmersheim",null],["095755519134","Ippesheim, M",null],["095755519146","Markt Nordheim, M",null],["095755519155","Oberickelsheim",null],["095755519163","Simmershofen",null],["095755519168","Uffenheim, St",null],["095755519179","Weigenheim",null],["095755520129","Hagenbüchach",null],["095755520181","Wilhelmsdorf",null],["095755521113","Baudenbach, M",null],["095755521118","Diespeck",null],["095755521128","Gutenstetten",null],["095755521150","Münchsteinach",null],["095755522117","Dachsbach, M",null],["095755522125","Gerhardshofen",null],["095755522167","Uehlfeld, M",null],["095755524115","Burgbernheim, St",null],["095755524124","Gallmersgarten",null],["095755524133","Illesheim",null],["095755524143","Marktbergel, M",null],["095755525152","Neuhof a.d.Zenn, M",null],["095755525166","Trautskirchen",null],["095759451451","Osing",null],["095760111111","Abenberg, St",null],["095760113113","Allersberg, M",null],["095760117117","Büchenbach",null],["095760121121","Georgensgmünd",null],["095760122122","Greding, St",null],["095760126126","Heideck, St",null],["095760127127","Hilpoltstein, St",null],["095760128128","Kammerstein",null],["095760132132","Schwanstetten, M",null],["095760137137","Rednitzhembach",null],["095760141141","Röttenbach",null],["095760142142","Rohr",null],["095760143143","Roth, St",null],["095760147147","Spalt, St",null],["095760148148","Thalmässing, M",null],["095760151151","Wendelstein, M",null],["095769451451","Abenberger Wald",null],["095769452452","Dechenwald",null],["095769453453","Forst Kleinschwarzenlohe",null],["095769454454","Heidenberg",null],["095769455455","Soos",null],["095770114114","Muhr a.See",null],["095770136136","Gunzenhausen, St",null],["095770148148","Langenaltheim",null],["095770158158","Pappenheim, St",null],["095770161161","Pleinfeld, M",null],["095770162162","Polsingen",null],["095770168168","Solnhofen",null],["095770173173","Treuchtlingen, St",null],["095770177177","Weißenburg i.Bay., GKSt",null],["095775532111","Absberg, M",null],["095775532138","Haundorf",null],["095775532159","Pfofeld",null],["095775532172","Theilenhofen",null],["095775533113","Alesheim",null],["095775533122","Dittenheim",null],["095775533149","Markt Berolzheim, M",null],["095775533150","Meinheim",null],["095775534125","Ellingen, St",null],["095775534127","Ettenstatt",null],["095775534141","Höttingen",null],["095775535115","Bergen",null],["095775535120","Burgsalach",null],["095775535151","Nennslingen, M",null],["095775535163","Raitenbuch",null],["095775536133","Gnotzheim, M",null],["095775536140","Heidenheim, M",null],["095775536179","Westheim",null],["096610000000","Aschaffenburg",null],["096620000000","Schweinfurt",null],["096630000000","Würzburg",null],["096710111111","Alzenau, St",null],["096710112112","Bessenbach",null],["096710114114","Karlstein a.Main",null],["096710119119","Geiselbach",null],["096710120120","Glattbach",null],["096710121121","Goldbach, M",null],["096710122122","Großostheim, M",null],["096710124124","Haibach",null],["096710130130","Hösbach, M",null],["096710133133","Johannesberg",null],["096710134134","Kahl a.Main",null],["096710136136","Kleinostheim",null],["096710139139","Laufach",null],["096710140140","Mainaschaff",null],["096710143143","Mömbris, M",null],["096710148148","Rothenbuch",null],["096710150150","Sailauf",null],["096710155155","Stockstadt a.Main, M",null],["096710156156","Waldaschaff",null],["096710157157","Weibersbrunn",null],["096715602126","Heigenbrücken",null],["096715602128","Heinrichsthal",null],["096715603127","Heimbuchenthal",null],["096715603141","Mespelbrunn",null],["096715603160","Dammbach",null],["096715604113","Blankenbach",null],["096715604135","Kleinkahl",null],["096715604138","Krombach",null],["096715604152","Schöllkrippen, M",null],["096715604153","Sommerkahl",null],["096715604159","Westerngrund",null],["096715604162","Wiesen",null],["096719451451","Forst Hain i.Spessart",null],["096719453453","Heinrichsthaler Forst",null],["096719456456","Rohrbrunner Forst",null],["096719457457","Rothenbucher Forst",null],["096719458458","Sailaufer Forst",null],["096719459459","Schöllkrippener Forst",null],["096719460460","Waldaschaffer Forst",null],["096719461461","Wiesener Forst",null],["096720112112","Bad Bocklet, M",null],["096720113113","Bad Brückenau, St",null],["096720114114","Bad Kissingen, GKSt",null],["096720117117","Burkardroth, M",null],["096720127127","Hammelburg, St",null],["096720134134","Motten",null],["096720135135","Münnerstadt, St",null],["096720136136","Nüdlingen",null],["096720139139","Oberthulba, M",null],["096720140140","Oerlenbach",null],["096720161161","Wartmannsroth",null],["096720163163","Wildflecken, M",null],["096720166166","Zeitlofs, M",null],["096725606126","Geroda, M",null],["096725606138","Oberleichtersbach",null],["096725606145","Riedenberg",null],["096725606149","Schondra, M",null],["096725607121","Elfershausen, M",null],["096725607124","Fuchsstadt",null],["096725608111","Aura a.d.Saale",null],["096725608122","Euerdorf, M",null],["096725608142","Ramsthal",null],["096725608155","Sulzthal, M",null],["096725609131","Maßbach, M",null],["096725609143","Rannungen",null],["096725609157","Thundorf i.UFr.",null],["096729451451","Dreistelzer Forst",null],["096729454454","Forst Detter-Süd",null],["096729455455","Geiersnest-Ost",null],["096729456456","Geiersnest-West",null],["096729457457","Großer Auersberg",null],["096729458458","Kälberberg",null],["096729461461","Mottener Forst-Süd",null],["096729462462","Neuwirtshauser Forst",null],["096729463463","Omerz u. Roter Berg",null],["096729464464","Römershager Forst-Nord",null],["096729465465","Römershager Forst-Ost",null],["096729466466","Roßbacher Forst",null],["096729468468","Waldfensterer Forst",null],["096730114114","Bad Neustadt a.d.Saale, St",null],["096730116116","Bastheim",null],["096730117117","Bischofsheim i.d.Rhön, St",null],["096730141141","Bad Königshofen i.Grabfeld, St",null],["096730149149","Oberelsbach, M",null],["096730162162","Sandberg",null],["096735633130","Hendungen",null],["096735633142","Mellrichstadt, St",null],["096735633151","Oberstreu",null],["096735633170","Stockheim",null],["096735634113","Aubstadt",null],["096735634126","Großbardorf",null],["096735634131","Herbstadt",null],["096735634134","Höchheim",null],["096735634172","Sulzdorf a.d.Lederhecke",null],["096735634173","Sulzfeld",null],["096735634174","Trappstadt, M",null],["096735635135","Hohenroth",null],["096735635146","Niederlauer",null],["096735635156","Rödelmaier",null],["096735635161","Salz",null],["096735635163","Schönau a.d.Brend",null],["096735635171","Strahlungen",null],["096735635186","Burglauer",null],["096735637123","Fladungen, St",null],["096735637129","Hausen",null],["096735637147","Nordheim v.d.Rhön",null],["096735638133","Heustreu",null],["096735638136","Hollstadt",null],["096735638175","Unsleben",null],["096735638183","Wollbach",null],["096735639153","Ostheim v.d.Rhön, St",null],["096735639167","Sondheim v.d.Rhön",null],["096735639182","Willmars",null],["096735640127","Großeibstadt",null],["096735640160","Saal a.d.Saale, M",null],["096735640184","Wülfershausen a.d.Saale",null],["096739451451","Bundorfer Forst",null],["096739452452","Burgwallbacher Forst",null],["096739453453","Forst Schmalwasser-Nord",null],["096739454454","Forst Schmalwasser-Süd",null],["096739455455","Mellrichstadter Forst",null],["096739456456","Steinacher Forst r.d.Saale",null],["096739457457","Sulzfelder Forst",null],["096739458458","Weigler",null],["096740133133","Eltmann, St",null],["096740147147","Haßfurt, St",null],["096740159159","Oberaurach",null],["096740163163","Knetzgau",null],["096740164164","Königsberg i.Bay., St",null],["096740171171","Maroldsweisach, M",null],["096740187187","Rauhenebrach",null],["096740195195","Sand a.Main",null],["096740210210","Untermerzbach",null],["096740221221","Zeil a.Main, St",null],["096745610118","Breitbrunn",null],["096745610129","Ebelsbach",null],["096745610160","Kirchlauter",null],["096745610201","Stettfeld",null],["096745611130","Ebern, St",null],["096745611184","Pfarrweisach",null],["096745611190","Rentweinsdorf, M",null],["096745612111","Aidhausen",null],["096745612120","Bundorf",null],["096745612121","Burgpreppach, M",null],["096745612149","Hofheim i.UFr., St",null],["096745612153","Riedbach",null],["096745612223","Ermershausen",null],["096745613139","Gädheim",null],["096745613180","Theres",null],["096745613219","Wonfurt",null],["096750117117","Dettelbach, St",null],["096750127127","Geiselwind, M",null],["096750141141","Kitzingen, GKSt",null],["096750144144","Mainbernheim, St",null],["096750158158","Prichsenstadt, St",null],["096750165165","Schwarzach a.Main, M",null],["096755614111","Abtswind, M",null],["096755614116","Castell",null],["096755614162","Rüdenhausen, M",null],["096755614178","Wiesentheid, M",null],["096755615131","Großlangheim, M",null],["096755615142","Kleinlangheim, M",null],["096755615177","Wiesenbronn",null],["096755616139","Iphofen, St",null],["096755616148","Markt Einersheim, M",null],["096755616161","Rödelsee",null],["096755616179","Willanzheim, M",null],["096755617112","Albertshofen",null],["096755617113","Biebelried",null],["096755617114","Buchbrunn",null],["096755617146","Mainstockheim",null],["096755617170","Sulzfeld a.Main",null],["096755618147","Marktbreit, St",null],["096755618149","Marktsteft, St",null],["096755618150","Martinsheim",null],["096755618156","Obernbreit, M",null],["096755618166","Segnitz",null],["096755618167","Seinsheim, M",null],["096755619155","Nordheim a.Main",null],["096755619169","Sommerach",null],["096755619174","Volkach, St",null],["096760112112","Amorbach, St",null],["096760117117","Collenberg",null],["096760118118","Dorfprozelten",null],["096760119119","Eichenbühl",null],["096760121121","Elsenfeld, M",null],["096760122122","Erlenbach a.Main, St",null],["096760123123","Eschau, M",null],["096760124124","Faulbach",null],["096760125125","Großheubach, M",null],["096760126126","Großwallstadt",null],["096760131131","Kirchzell, M",null],["096760134134","Klingenberg a.Main, St",null],["096760136136","Leidersbach",null],["096760139139","Miltenberg, St",null],["096760140140","Mömlingen",null],["096760144144","Niedernberg",null],["096760145145","Obernburg a.Main, St",null],["096760156156","Schneeberg, M",null],["096760160160","Sulzbach a.Main, M",null],["096760165165","Weilbach, M",null],["096760169169","Wörth a.Main, St",null],["096765626116","Bürgstadt, M",null],["096765626143","Neunkirchen",null],["096765627132","Kleinheubach, M",null],["096765627135","Laudenbach",null],["096765627153","Rüdenau",null],["096765630128","Hausen",null],["096765630133","Kleinwallstadt, M",null],["096765631141","Mönchberg, M",null],["096765631151","Röllbach",null],["096765632111","Altenbuch",null],["096765632158","Stadtprozelten, St",null],["096769452452","Forstwald",null],["096769455455","Hohe Wart",null],["096770114114","Arnstein, St",null],["096770127127","Eußenheim",null],["096770129129","Frammersbach, M",null],["096770131131","Gemünden a.Main, St",null],["096770148148","Karlstadt, St",null],["096770154154","Triefenstein, M",null],["096770155155","Lohr a.Main, St",null],["096770157157","Marktheidenfeld, St",null],["096770177177","Rieneck, St",null],["096775620137","Hasloch",null],["096775620151","Kreuzwertheim, M",null],["096775620182","Schollbrunn",null],["096775621119","Birkenfeld",null],["096775621120","Bischbrunn",null],["096775621125","Erlenbach b.Marktheidenfeld",null],["096775621126","Esselbach",null],["096775621135","Hafenlohr",null],["096775621146","Karbach, M",null],["096775621178","Roden",null],["096775621181","Rothenfels, St",null],["096775621193","Urspringen",null],["096775622116","Aura i.Sinngrund",null],["096775622122","Burgsinn, M",null],["096775622128","Fellen",null],["096775622159","Mittelsinn",null],["096775622169","Obersinn, M",null],["096775623132","Gössenheim",null],["096775623133","Gräfendorf",null],["096775623149","Karsbach",null],["096775624164","Neuendorf",null],["096775624166","Neustadt a.Main",null],["096775624172","Rechtenbach",null],["096775624186","Steinfeld",null],["096775625142","Himmelstadt",null],["096775625175","Retzstadt",null],["096775625189","Thüngen, M",null],["096775625203","Zellingen, M",null],["096775656165","Neuhütten",null],["096775656170","Partenstein",null],["096775656200","Wiesthal",null],["096779452452","Burgjoß",null],["096779453453","Forst Aura",null],["096779454454","Forst Lohrerstraße",null],["096779455455","Frammersbacher Forst",null],["096779456456","Fürstl. Löwenstein'scher Park",null],["096779457457","Haurain",null],["096779458458","Herrnwald",null],["096779459459","Langenprozeltener Forst",null],["096779461461","Partensteiner Forst",null],["096779463463","Ruppertshüttener Forst",null],["096780115115","Bergrheinfeld",null],["096780123123","Dittelbrunn",null],["096780128128","Euerbach",null],["096780132132","Geldersheim",null],["096780135135","Gochsheim",null],["096780136136","Grafenrheinfeld",null],["096780138138","Grettstadt",null],["096780150150","Kolitzheim",null],["096780160160","Niederwerrn",null],["096780168168","Poppenhausen",null],["096780170170","Röthlein",null],["096780174174","Schonungen",null],["096780176176","Schwebheim",null],["096780178178","Sennfeld",null],["096780181181","Stadtlauringen, M",null],["096780186186","Üchtelhausen",null],["096780190190","Waigolshausen",null],["096780192192","Wasserlosen",null],["096780193193","Werneck, M",null],["096785642122","Dingolshausen",null],["096785642124","Donnersdorf",null],["096785642130","Frankenwinheim",null],["096785642134","Gerolzhofen, St",null],["096785642153","Lülsfeld",null],["096785642157","Michelau i.Steigerwald",null],["096785642164","Oberschwarzach, M",null],["096785642183","Sulzheim",null],["096785643175","Schwanfeld",null],["096785643196","Wipfeld",null],["096789451451","Bürgerwald",null],["096789452452","Geiersberg",null],["096789453453","Hundelshausen",null],["096789454454","Nonnenkloster",null],["096789455455","Stollbergerforst",null],["096789456456","Vollburg",null],["096789457457","Wustvieler Forst",null],["096790126126","Eisingen",null],["096790134134","Gaukönigshofen",null],["096790136136","Gerbrunn",null],["096790142142","Güntersleben",null],["096790143143","Hausen b.Würzburg",null],["096790147147","Höchberg, M",null],["096790155155","Kleinrinderfeld",null],["096790156156","Kürnach",null],["096790164164","Neubrunn, M",null],["096790170170","Ochsenfurt, St",null],["096790175175","Randersacker, M",null],["096790176176","Reichenberg, M",null],["096790180180","Rimpar, M",null],["096790185185","Rottendorf",null],["096790193193","Theilheim",null],["096790194194","Thüngersheim",null],["096790200200","Leinach",null],["096790201201","Unterpleichfeld",null],["096790202202","Veitshöchheim",null],["096790204204","Waldbrunn",null],["096790205205","Waldbüttelbrunn",null],["096790209209","Zell a.Main, M",null],["096795644114","Aub, St",null],["096795644135","Gelchsheim, M",null],["096795644188","Sonderhofen",null],["096795645117","Bergtheim",null],["096795645169","Oberpleichfeld",null],["096795646124","Eibelstadt, St",null],["096795646131","Frickenhausen a.Main, M",null],["096795646187","Sommerhausen, M",null],["096795646206","Winterhausen, M",null],["096795647130","Estenfeld",null],["096795647167","Eisenheim, M",null],["096795647174","Prosselsheim",null],["096795648122","Bütthard, M",null],["096795648138","Giebelstadt, M",null],["096795649144","Helmstadt, M",null],["096795649149","Holzkirchen",null],["096795649177","Remlingen, M",null],["096795649196","Uettingen",null],["096795650137","Geroldshausen",null],["096795650153","Kirchheim",null],["096795651154","Kist",null],["096795651165","Altertheim",null],["096795652128","Erlabrunn",null],["096795652161","Margetshöchheim",null],["096795654118","Bieberehren",null],["096795654179","Riedenheim",null],["096795654182","Röttingen, St",null],["096795654192","Tauberrettersheim",null],["096795655141","Greußenheim",null],["096795655146","Hettstadt",null],["096799451451","Gramschatzer Wald",null],["096799452452","Guttenberger Wald",null],["096799453453","Irtenberger Wald",null],["097610000000","Augsburg",null],["097620000000","Kaufbeuren",null],["097630000000","Kempten (Allgäu)",null],["097640000000","Memmingen",null],["097710112112","Affing",null],["097710113113","Aichach, St",null],["097710130130","Friedberg, St",null],["097710140140","Hollenbach",null],["097710141141","Inchenhofen, M",null],["097710142142","Kissing",null],["097710145145","Merching",null],["097710158158","Rehling",null],["097710160160","Ried",null],["097715701114","Aindling, M",null],["097715701155","Petersdorf",null],["097715701169","Todtenweis",null],["097715703144","Kühbach, M",null],["097715703162","Schiltberg",null],["097715704111","Adelzhausen",null],["097715704122","Dasing",null],["097715704129","Eurasburg",null],["097715704149","Obergriesbach",null],["097715704165","Sielenbach",null],["097715705146","Mering, M",null],["097715705163","Schmiechen",null],["097715705168","Steindorf",null],["097715771156","Pöttmes, M",null],["097715771176","Baar (Schwaben)",null],["097720111111","Adelsried",null],["097720115115","Altenmünster",null],["097720117117","Aystetten",null],["097720121121","Biberbach, M",null],["097720125125","Bobingen, St",null],["097720130130","Diedorf, M",null],["097720131131","Dinkelscherben, M",null],["097720141141","Fischach, M",null],["097720145145","Gablingen",null],["097720147147","Gersthofen, St",null],["097720149149","Graben",null],["097720159159","Horgau",null],["097720163163","Königsbrunn, St",null],["097720167167","Kutzenhausen",null],["097720171171","Langweid a.Lech",null],["097720177177","Meitingen, M",null],["097720184184","Neusäß, St",null],["097720200200","Schwabmünchen, St",null],["097720202202","Stadtbergen, St",null],["097720207207","Thierhaupten, M",null],["097720215215","Wehringen",null],["097720223223","Zusmarshausen, M",null],["097725706114","Allmannshofen",null],["097725706134","Ehingen",null],["097725706136","Ellgau",null],["097725706166","Kühlenthal",null],["097725706185","Nordendorf",null],["097725706217","Westendorf",null],["097725707126","Bonstetten",null],["097725707137","Emersacker",null],["097725707156","Heretsried",null],["097725707216","Welden, M",null],["097725708148","Gessertshausen",null],["097725708211","Ustersbach",null],["097725709168","Langenneufnach",null],["097725709178","Mickhausen",null],["097725709179","Mittelneufnach",null],["097725709197","Scherstetten",null],["097725709214","Walkertshofen",null],["097725710151","Großaitingen",null],["097725710160","Kleinaitingen",null],["097725710186","Oberottmarshausen",null],["097725711162","Klosterlechfeld",null],["097725711209","Untermeitingen",null],["097725712157","Hiltenfingen",null],["097725712170","Langerringen",null],["097729451451","Schmellerforst",null],["097730117117","Bissingen, M",null],["097730122122","Buttenwiesen",null],["097730125125","Dillingen a.d.Donau, GKSt",null],["097730144144","Lauingen (Donau), St",null],["097735713113","Bächingen a.d.Brenz",null],["097735713136","Gundelfingen a.d.Donau, St",null],["097735713137","Haunsheim",null],["097735713153","Medlingen",null],["097735714112","Bachhagel",null],["097735714170","Syrgenstein",null],["097735714187","Zöschingen",null],["097735715147","Mödingen",null],["097735715183","Wittislingen, M",null],["097735715186","Ziertheim",null],["097735716119","Blindheim",null],["097735716139","Höchstädt a.d.Donau, St",null],["097735716146","Lutzingen",null],["097735716150","Finningen",null],["097735716164","Schwenningen",null],["097735718116","Binswangen",null],["097735718143","Laugna",null],["097735718179","Villenbach",null],["097735718182","Wertingen, St",null],["097735718188","Zusamaltheim",null],["097735719111","Aislingen, M",null],["097735719133","Glött",null],["097735719140","Holzheim",null],["097740116116","Ursberg",null],["097740119119","Bibertal",null],["097740121121","Burgau, St",null],["097740122122","Burtenbach, M",null],["097740135135","Günzburg, GKSt",null],["097740144144","Jettingen-Scheppach, M",null],["097740145145","Kammeltal",null],["097740150150","Krumbach (Schwaben), St",null],["097740155155","Leipheim, St",null],["097740162162","Neuburg a.d.Kammel, M",null],["097745727136","Gundremmingen",null],["097745727171","Offingen, M",null],["097745727174","Rettenbach",null],["097745728127","Dürrlauingen",null],["097745728140","Haldenwang",null],["097745728151","Landensberg",null],["097745728178","Röfingen",null],["097745728196","Winterbach",null],["097745729118","Bubesheim",null],["097745729148","Kötz",null],["097745730133","Ellzee",null],["097745730143","Ichenhausen, St",null],["097745730191","Waldstetten, M",null],["097745731111","Aletshausen",null],["097745731117","Breitenthal",null],["097745731124","Deisenhausen",null],["097745731129","Ebershausen",null],["097745731189","Wiesenbach",null],["097745731192","Waltenhausen",null],["097745732115","Balzhausen",null],["097745732160","Münsterhausen, M",null],["097745732185","Thannhausen, St",null],["097745733166","Aichen",null],["097745733198","Ziemetshausen, M",null],["097749451451","Ebershauser-Nattenhauser Wald",null],["097749452452","Winzerwald",null],["097750115115","Bellenberg",null],["097750129129","Illertissen, St",null],["097750134134","Nersingen",null],["097750135135","Neu-Ulm, GKSt",null],["097750139139","Elchingen",null],["097750149149","Roggenburg",null],["097750152152","Senden, St",null],["097750162162","Vöhringen, St",null],["097750164164","Weißenhorn, St",null],["097755739126","Holzheim",null],["097755739143","Pfaffenhofen a.d.Roth, M",null],["097755740111","Altenstadt, M",null],["097755740132","Kellmünz a.d.Iller, M",null],["097755740142","Osterberg",null],["097755741118","Buch, M",null],["097755741141","Oberroth",null],["097755741161","Unterroth",null],["097759451451","Auwald",null],["097759452452","Oberroggenburger Wald",null],["097759454454","Stoffenrieder Forst",null],["097759455455","Unterroggenburger Wald",null],["097760111111","Bodolz",null],["097760114114","Heimenkirch, M",null],["097760116116","Lindau (Bodensee), GKSt",null],["097760117117","Lindenberg i.Allgäu, St",null],["097760120120","Nonnenhorn",null],["097760122122","Opfenbach",null],["097760125125","Scheidegg, M",null],["097760128128","Wasserburg (Bodensee)",null],["097760129129","Weiler-Simmerberg, M",null],["097760131131","Hergatz",null],["097765735115","Hergensweiler",null],["097765735126","Sigmarszell",null],["097765735130","Weißensberg",null],["097765737112","Gestratz",null],["097765737113","Grünenbach",null],["097765737118","Maierhöfen",null],["097765737124","Röthenbach (Allgäu)",null],["097765738121","Oberreute",null],["097765738127","Stiefenhofen",null],["097770129129","Füssen, St",null],["097770130130","Germaringen",null],["097770147147","Lechbruck am See",null],["097770151151","Marktoberdorf, St",null],["097770152152","Mauerstetten",null],["097770153153","Nesselwang, M",null],["097770159159","Pfronten",null],["097770165165","Ronsberg, M",null],["097770169169","Schwangau",null],["097770173173","Halblech",null],["097775748121","Buchloe, St",null],["097775748140","Jengen",null],["097775748145","Lamerdingen",null],["097775748177","Waal, M",null],["097775749139","Irsee, M",null],["097775749158","Pforzen",null],["097775749164","Rieden",null],["097775751141","Kaltental, M",null],["097775751155","Oberostendorf",null],["097775751157","Osterzell",null],["097775751172","Stöttwang",null],["097775751182","Westendorf",null],["097775752111","Aitrang",null],["097775752112","Biessenhofen",null],["097775752118","Bidingen",null],["097775752167","Ruderatshofen",null],["097775753114","Baisweil",null],["097775753124","Eggenthal",null],["097775753128","Friesenried",null],["097775754138","Günzach",null],["097775754154","Obergünzburg, M",null],["097775754176","Untrasried",null],["097775755131","Görisried",null],["097775755144","Kraftisried",null],["097775755175","Unterthingau, M",null],["097775756125","Eisenberg",null],["097775756135","Hopferau",null],["097775756149","Lengenwang",null],["097775756168","Rückholz",null],["097775756170","Seeg",null],["097775756179","Wald",null],["097775770163","Rieden am Forggensee",null],["097775770166","Roßhaupten",null],["097775772171","Stötten a.Auerberg",null],["097775772183","Rettenbach a.Auerberg",null],["097780116116","Bad Wörishofen, St",null],["097780123123","Buxheim",null],["097780137137","Ettringen",null],["097780168168","Markt Rettenbach, M",null],["097780169169","Markt Wald, M",null],["097780173173","Mindelheim, St",null],["097780196196","Sontheim",null],["097780204204","Tussenhausen, M",null],["097785757119","Böhen",null],["097785757149","Hawangen",null],["097785757186","Ottobeuren, M",null],["097785758115","Babenhausen, M",null],["097785758130","Egg a.d.Günz",null],["097785758157","Kirchhaslach",null],["097785758184","Oberschönegg",null],["097785758217","Winterrieden",null],["097785758221","Kettershausen",null],["097785759121","Breitenbrunn",null],["097785759183","Oberrieden",null],["097785759187","Pfaffenhausen, M",null],["097785759190","Salgen",null],["097785760134","Eppishausen",null],["097785760158","Kirchheim i.Schw., M",null],["097785761120","Boos",null],["097785761139","Fellheim",null],["097785761150","Heimertingen",null],["097785761177","Niederrieden",null],["097785761188","Pleß",null],["097785762136","Erkheim, M",null],["097785762163","Lauben",null],["097785762180","Kammlach",null],["097785762214","Westerheim",null],["097785764111","Amberg",null],["097785764203","Türkheim, M",null],["097785764209","Rammingen",null],["097785764216","Wiedergeltingen",null],["097785765118","Benningen",null],["097785765151","Holzgünz",null],["097785765162","Lachen",null],["097785765171","Memmingerberg",null],["097785765202","Trunkelsberg",null],["097785765205","Ungerhausen",null],["097785766113","Apfeltrach",null],["097785766127","Dirlewang, M",null],["097785766199","Stetten",null],["097785766207","Unteregg",null],["097785767161","Kronburg",null],["097785767164","Lautrach",null],["097785767165","Legau, M",null],["097785768144","Bad Grönenbach, M",null],["097785768218","Wolfertschwenden",null],["097785768219","Woringen",null],["097789451451","Ungerhauser Wald",null],["097790115115","Asbach-Bäumenheim",null],["097790131131","Donauwörth, GKSt",null],["097790147147","Fremdingen",null],["097790155155","Harburg (Schwaben), St",null],["097790169169","Kaisheim, M",null],["097790178178","Marxheim",null],["097790181181","Mertingen",null],["097790185185","Möttingen",null],["097790194194","Nördlingen, GKSt",null],["097790196196","Oberndorf a.Lech",null],["097790218218","Tapfheim",null],["097795720176","Maihingen",null],["097795720177","Marktoffingen",null],["097795720224","Wallerstein, M",null],["097795721117","Auhausen",null],["097795721138","Ehingen a.Ries",null],["097795721154","Hainsfarth",null],["097795721180","Megesheim",null],["097795721188","Munningen",null],["097795721197","Oettingen i.Bay., St",null],["097795722111","Alerheim",null],["097795722112","Amerdingen",null],["097795722130","Deiningen",null],["097795722136","Ederheim",null],["097795722146","Forheim",null],["097795722162","Hohenaltheim",null],["097795722184","Mönchsdeggingen",null],["097795722203","Reimlingen",null],["097795722226","Wechingen",null],["097795723148","Fünfstetten",null],["097795723167","Huisheim",null],["097795723198","Otting",null],["097795723228","Wemding, St",null],["097795723231","Wolferstadt",null],["097795724126","Buchdorf",null],["097795724129","Daiting",null],["097795724186","Monheim, St",null],["097795724206","Rögling",null],["097795724217","Tagmersheim",null],["097795725149","Genderkingen",null],["097795725163","Holzheim",null],["097795725187","Münster",null],["097795725192","Niederschönenfeld",null],["097795725201","Rain, St",null],["097799452452","Dornstadt-Linkersbaindt",null],["097799453453","Esterholz",null],["097800112112","Altusried, M",null],["097800114114","Betzigau",null],["097800115115","Blaichach",null],["097800117117","Buchenberg, M",null],["097800118118","Burgberg i.Allgäu",null],["097800119119","Dietmannsried, M",null],["097800120120","Durach",null],["097800122122","Haldenwang",null],["097800123123","Bad Hindelang, M",null],["097800124124","Immenstadt i.Allgäu, St",null],["097800125125","Lauben",null],["097800128128","Oy-Mittelberg",null],["097800132132","Oberstaufen, M",null],["097800133133","Oberstdorf, M",null],["097800137137","Rettenberg",null],["097800139139","Sonthofen, St",null],["097800140140","Sulzberg, M",null],["097800143143","Waltenhofen",null],["097800145145","Wertach, M",null],["097800146146","Wiggensbach, M",null],["097800147147","Wildpoldsried",null],["097805742113","Balderschwang",null],["097805742116","Bolsterlang",null],["097805742121","Fischen i.Allgäu",null],["097805742131","Obermaiselstein",null],["097805742134","Ofterschwang",null],["097805745127","Missen-Wilhams",null],["097805745144","Weitnau, M",null],["097809451451","Kempter Wald",null],["100410100100","Saarbrücken, Landeshauptstadt",null],["100410511511","Friedrichsthal, Stadt",null],["100410512512","Großrosseln",null],["100410513513","Heusweiler",null],["100410514514","Kleinblittersdorf",null],["100410515515","Püttlingen, Stadt",null],["100410516516","Quierschied",null],["100410517517","Riegelsberg",null],["100410518518","Sulzbach/ Saar, Stadt",null],["100410519519","Völklingen, Stadt",null],["100420111111","Beckingen",null],["100420112112","Losheim am See",null],["100420113113","Merzig, Kreisstadt",null],["100420114114","Mettlach",null],["100420115115","Perl",null],["100420116116","Wadern, Stadt",null],["100420117117","Weiskirchen",null],["100429999999","Deutsch-luxemburgisches Hoheitsgebiet",null],["100430111111","Eppelborn",null],["100430112112","Illingen",null],["100430113113","Merchweiler",null],["100430114114","Neunkirchen, Kreisstadt",null],["100430115115","Ottweiler, Stadt",null],["100430116116","Schiffweiler",null],["100430117117","Spiesen-Elversberg",null],["100440111111","Dillingen/ Saar, Stadt",null],["100440112112","Lebach, Stadt",null],["100440113113","Nalbach",null],["100440114114","Rehlingen-Siersburg",null],["100440115115","Saarlouis, Kreisstadt",null],["100440116116","Saarwellingen",null],["100440117117","Schmelz",null],["100440118118","Schwalbach",null],["100440119119","Überherrn",null],["100440120120","Wadgassen",null],["100440121121","Wallerfangen",null],["100440122122","Bous",null],["100440123123","Ensdorf",null],["100450111111","Bexbach, Stadt",null],["100450112112","Blieskastel, Stadt",null],["100450113113","Gersheim",null],["100450114114","Homburg, Kreisstadt",null],["100450115115","Kirkel",null],["100450116116","Mandelbachtal",null],["100450117117","St. Ingbert, Stadt",null],["100460111111","Freisen",null],["100460112112","Marpingen",null],["100460113113","Namborn",null],["100460114114","Nohfelden",null],["100460115115","Nonnweiler",null],["100460116116","Oberthal",null],["100460117117","St. Wendel, Kreisstadt",null],["100460118118","Tholey",null],["110000000000","Berlin, Stadt",null],["110010001001","Mitte","Stadt-/Ortsteil bzw. Stadtbezirk"],["110020002002","Friedrichshain-Kreuzberg","Stadt-/Ortsteil bzw. Stadtbezirk"],["110030003003","Pankow","Stadt-/Ortsteil bzw. Stadtbezirk"],["110040004004","Charlottenburg-Wilmersdorf","Stadt-/Ortsteil bzw. Stadtbezirk"],["110050005005","Spandau","Stadt-/Ortsteil bzw. Stadtbezirk"],["110060006006","Steglitz-Zehlendorf","Stadt-/Ortsteil bzw. Stadtbezirk"],["110070007007","Tempelhof-Schöneberg","Stadt-/Ortsteil bzw. Stadtbezirk"],["110080008008","Neukölln","Stadt-/Ortsteil bzw. Stadtbezirk"],["110090009009","Treptow-Köpenick","Stadt-/Ortsteil bzw. Stadtbezirk"],["110100010010","Marzahn-Hellersdorf","Stadt-/Ortsteil bzw. Stadtbezirk"],["110110011011","Lichtenberg","Stadt-/Ortsteil bzw. Stadtbezirk"],["110120012012","Reinickendorf","Stadt-/Ortsteil bzw. Stadtbezirk"],["120510000000","Brandenburg an der Havel, Stadt",null],["120520000000","Cottbus/Chóśebuz, Stadt",null],["120530000000","Frankfurt (Oder), Stadt",null],["120540000000","Potsdam, Stadt",null],["120600005005","Ahrensfelde",null],["120600020020","Bernau bei Berlin, Stadt",null],["120600052052","Eberswalde, Stadt",null],["120600181181","Panketal",null],["120600198198","Schorfheide",null],["120600269269","Wandlitz",null],["120600280280","Werneuchen, Stadt",null],["120605003024","Biesenthal, Stadt",null],["120605003034","Breydin",null],["120605003154","Marienwerder",null],["120605003161","Melchow",null],["120605003192","Rüdnitz",null],["120605003250","Sydower Fließ",null],["120605006012","Althüttendorf",null],["120605006068","Friedrichswalde",null],["120605006100","Joachimsthal, Stadt",null],["120605006296","Ziethen",null],["120605011036","Britz",null],["120605011045","Chorin",null],["120605011092","Hohenfinow",null],["120605011128","Liepe",null],["120605011149","Lunow-Stolzenhagen",null],["120605011172","Niederfinow",null],["120605011176","Oderberg, Stadt",null],["120605011185","Parsteinsee",null],["120610020020","Bestensee",null],["120610112112","Eichwalde",null],["120610217217","Heidesee",null],["120610219219","Heideblick",null],["120610260260","Königs Wusterhausen, Stadt",null],["120610316316","Lübben (Spreewald) / Lubin (Błota), Stadt",null],["120610320320","Luckau, Stadt",null],["120610329329","Märkische Heide/Markojska Góla",null],["120610332332","Mittenwalde, Stadt",null],["120610433433","Schönefeld",null],["120610444444","Schulzendorf",null],["120610540540","Wildau, Stadt",null],["120610572572","Zeuthen",null],["120615108192","Groß Köris",null],["120615108216","Halbe",null],["120615108328","Märkisch Buchholz, Stadt",null],["120615108344","Münchehofe",null],["120615108448","Schwerin",null],["120615108492","Teupitz, Stadt",null],["120615113005","Alt Zauche-Wußwerk/Stara Niwa-Wózwjerch",null],["120615113061","Byhleguhre-Byhlen/Beła Góra-Bělin",null],["120615113224","Jamlitz",null],["120615113308","Lieberose, Stadt",null],["120615113352","Neu Zauche/Nowa Niwa",null],["120615113450","Schwielochsee/Gójacki Jazor",null],["120615113470","Spreewaldheide/Błośańska Góla",null],["120615113476","Straupitz (Spreewald)/Tšupc (Błota)",null],["120615114017","Bersteland",null],["120615114097","Drahnsdorf",null],["120615114164","Golßen, Stadt",null],["120615114244","Kasel-Golzig",null],["120615114265","Krausnick-Groß Wasserburg",null],["120615114405","Rietzneuendorf-Staakow",null],["120615114428","Schlepzig/Słopišća",null],["120615114435","Schönwald",null],["120615114471","Steinreich",null],["120615114510","Unterspreewald",null],["120620092092","Doberlug-Kirchhain, Stadt",null],["120620124124","Elsterwerda, Stadt",null],["120620140140","Finsterwalde, Stadt",null],["120620224224","Herzberg (Elster), Stadt",null],["120620410410","Röderland",null],["120620461461","Schönewalde, Stadt",null],["120620469469","Sonnewalde, Stadt",null],["120625031024","Bad Liebenwerda, Stadt",null],["120625031128","Falkenberg/Elster, Stadt",null],["120625031341","Mühlberg/Elbe, Stadt",null],["120625031500","Uebigau-Wahrenbrück, Stadt",null],["120625202219","Heideland",null],["120625202417","Rückersdorf",null],["120625202440","Schilda",null],["120625202453","Schönborn",null],["120625202492","Tröbitz",null],["120625205088","Crinitz",null],["120625205293","Lichterfeld-Schacksdorf",null],["120625205333","Massen-Niederlausitz",null],["120625205425","Sallgast",null],["120625207177","Gorden-Staupitz",null],["120625207240","Hohenleipisch",null],["120625207372","Plessa",null],["120625207464","Schraden",null],["120625209134","Fichtwald",null],["120625209237","Hohenbucko",null],["120625209282","Kremitzaue",null],["120625209289","Lebusa",null],["120625209445","Schlieben, Stadt",null],["120625211196","Gröden",null],["120625211208","Großthiemig",null],["120625211232","Hirschfeld",null],["120625211336","Merzdorf",null],["120630036036","Brieselang",null],["120630056056","Dallgow-Döberitz",null],["120630080080","Falkensee, Stadt",null],["120630148148","Ketzin/Havel, Stadt",null],["120630189189","Milower Land",null],["120630208208","Nauen, Stadt",null],["120630244244","Premnitz, Stadt",null],["120630252252","Rathenow, Stadt",null],["120630273273","Schönwalde-Glien",null],["120630357357","Wustermark",null],["120635302088","Friesack, Stadt",null],["120635302142","Wiesenaue",null],["120635302202","Mühlenberge",null],["120635302228","Paulinenaue",null],["120635302240","Pessin",null],["120635302256","Retzow",null],["120635306165","Kotzen",null],["120635306186","Märkisch Luch",null],["120635306212","Nennhausen",null],["120635306293","Stechow-Ferchesar",null],["120635309094","Gollenberg",null],["120635309112","Großderschau",null],["120635309134","Havelaue",null],["120635309161","Kleßen-Görne",null],["120635309260","Rhinow, Stadt",null],["120635309274","Seeblick",null],["120640029029","Altlandsberg, Stadt",null],["120640044044","Bad Freienwalde (Oder), Stadt",null],["120640136136","Fredersdorf-Vogelsdorf",null],["120640227227","Hoppegarten",null],["120640274274","Letschin",null],["120640317317","Müncheberg, Stadt",null],["120640336336","Neuenhagen bei Berlin",null],["120640380380","Petershagen/Eggersdorf",null],["120640428428","Rüdersdorf bei Berlin",null],["120640448448","Seelow, Stadt",null],["120640472472","Strausberg, Stadt",null],["120640512512","Wriezen, Stadt",null],["120645403053","Beiersdorf-Freudenberg",null],["120645403125","Falkenberg",null],["120645403205","Heckelberg-Brunow",null],["120645403222","Höhenland",null],["120645404009","Alt Tucheband",null],["120645404057","Bleyen-Genschmar",null],["120645404172","Golzow",null],["120645404266","Küstriner Vorland",null],["120645404538","Zechin",null],["120645406268","Lebus, Stadt",null],["120645406388","Podelzig",null],["120645406420","Reitwein",null],["120645406480","Treplin",null],["120645406539","Zeschdorf",null],["120645408084","Buckow (Märkische Schweiz), Stadt",null],["120645408153","Garzau-Garzin",null],["120645408370","Oberbarnim",null],["120645408408","Rehfelde",null],["120645408484","Waldsieversdorf",null],["120645410190","Gusow-Platkow",null],["120645410303","Märkische Höhe",null],["120645410340","Neuhardenberg",null],["120645412128","Falkenhagen (Mark)",null],["120645412130","Fichtenhöhe",null],["120645412288","Lietzen",null],["120645412290","Lindendorf",null],["120645412482","Vierlinden",null],["120645414061","Bliesdorf",null],["120645414349","Neulewin",null],["120645414365","Neutrebbin",null],["120645414371","Oderaue",null],["120645414393","Prötzel",null],["120645414417","Reichenow-Möglin",null],["120650036036","Birkenwerder",null],["120650084084","Fürstenberg/Havel, Stadt",null],["120650096096","Glienicke/Nordbahn",null],["120650136136","Hennigsdorf, Stadt",null],["120650144144","Hohen Neuendorf, Stadt",null],["120650165165","Kremmen, Stadt",null],["120650180180","Leegebruch",null],["120650193193","Liebenwalde, Stadt",null],["120650198198","Löwenberger Land",null],["120650225225","Mühlenbecker Land",null],["120650251251","Oberkrämer",null],["120650256256","Oranienburg, Stadt",null],["120650332332","Velten, Stadt",null],["120650356356","Zehdenick, Stadt",null],["120655502100","Gransee, Stadt",null],["120655502117","Großwoltersdorf",null],["120655502276","Schönermark",null],["120655502301","Sonnenberg",null],["120655502310","Stechlin",null],["120660052052","Calau/Kalawa, Stadt",null],["120660112112","Großräschen/Rań, Stadt",null],["120660176176","Lauchhammer, Stadt",null],["120660196196","Lübbenau/Spreewald / Lubnjow/Błota, Stadt",null],["120660285285","Schipkau",null],["120660296296","Schwarzheide, Stadt",null],["120660304304","Senftenberg/Zły Komorow, Stadt",null],["120660320320","Vetschau/Spreewald / Wětošow/Błota, Stadt",null],["120665601008","Altdöbern",null],["120665601041","Bronkow",null],["120665601202","Luckaitztal",null],["120665601226","Neu-Seeland/Nowa Jazorina",null],["120665601228","Neupetershain/Nowe Wiki",null],["120665606064","Frauendorf",null],["120665606104","Großkmehlen",null],["120665606168","Kroppen",null],["120665606188","Lindenau",null],["120665606240","Ortrand, Stadt",null],["120665606316","Tettau",null],["120665607116","Grünewald",null],["120665607120","Guteborn",null],["120665607124","Hermsdorf",null],["120665607132","Hohenbocka",null],["120665607272","Ruhland, Stadt",null],["120665607292","Schwarzbach",null],["120670036036","Beeskow, Stadt",null],["120670120120","Eisenhüttenstadt, Stadt",null],["120670124124","Erkner, Stadt",null],["120670137137","Friedland, Stadt",null],["120670144144","Fürstenwalde/Spree, Stadt",null],["120670201201","Grünheide (Mark)",null],["120670426426","Rietz-Neuendorf",null],["120670440440","Schöneiche bei Berlin",null],["120670481481","Storkow (Mark), Stadt",null],["120670493493","Tauche",null],["120670544544","Woltersdorf",null],["120675701076","Brieskow-Finkenheerd",null],["120675701180","Groß Lindow",null],["120675701508","Vogelsang",null],["120675701528","Wiesenau",null],["120675701552","Ziltendorf",null],["120675705292","Lawitz",null],["120675705338","Neißemünde",null],["120675705357","Neuzelle",null],["120675706040","Berkenbrück",null],["120675706072","Briesen (Mark)",null],["120675706237","Jacobsdorf",null],["120675706473","Steinhöfel",null],["120675707024","Bad Saarow",null],["120675707112","Diensdorf-Radlow",null],["120675707288","Langewahl",null],["120675707413","Reichenwalde",null],["120675707520","Wendisch Rietz",null],["120675708205","Grunow-Dammendorf",null],["120675708324","Mixdorf",null],["120675708336","Müllrose, Stadt",null],["120675708397","Ragow-Merz",null],["120675708438","Schlaubetal",null],["120675708458","Siehdichum",null],["120675709173","Gosen-Neu Zittau",null],["120675709408","Rauen",null],["120675709469","Spreenhagen",null],["120680117117","Fehrbellin",null],["120680181181","Heiligengrabe",null],["120680264264","Kyritz, Stadt",null],["120680320320","Neuruppin, Stadt",null],["120680353353","Rheinsberg, Stadt",null],["120680468468","Wittstock/Dosse, Stadt",null],["120680477477","Wusterhausen/Dosse",null],["120685804188","Herzberg (Mark)",null],["120685804280","Lindow (Mark), Stadt",null],["120685804372","Rüthnick",null],["120685804437","Vielitzsee",null],["120685805052","Breddin",null],["120685805109","Dreetz",null],["120685805324","Neustadt (Dosse), Stadt",null],["120685805409","Sieversdorf-Hohenofen",null],["120685805417","Stüdenitz-Schönermark",null],["120685805501","Zernitz-Lohm",null],["120685807072","Dabergotz",null],["120685807306","Märkisch Linden",null],["120685807413","Storbeck-Frankendorf",null],["120685807425","Temnitzquell",null],["120685807426","Temnitztal",null],["120685807452","Walsleben",null],["120690017017","Beelitz, Stadt",null],["120690020020","Bad Belzig, Stadt",null],["120690249249","Groß Kreutz (Havel)",null],["120690304304","Kleinmachnow",null],["120690306306","Kloster Lehnin",null],["120690397397","Michendorf",null],["120690454454","Nuthetal",null],["120690590590","Schwielowsee",null],["120690596596","Seddiner See",null],["120690604604","Stahnsdorf",null],["120690616616","Teltow, Stadt",null],["120690632632","Treuenbrietzen, Stadt",null],["120690656656","Werder (Havel), Stadt",null],["120690665665","Wiesenburg/Mark",null],["120695902018","Beetzsee",null],["120695902019","Beetzseeheide",null],["120695902270","Havelsee, Stadt",null],["120695902460","Päwesin",null],["120695902541","Roskow",null],["120695904052","Borkheide",null],["120695904056","Borkwalde",null],["120695904076","Brück, Stadt",null],["120695904216","Golzow",null],["120695904345","Linthe",null],["120695904470","Planebruch",null],["120695910402","Mühlenfließ",null],["120695910448","Niemegk, Stadt",null],["120695910474","Planetal",null],["120695910485","Rabenstein/Fläming",null],["120695917028","Bensdorf",null],["120695917537","Rosenau",null],["120695917688","Wusterwitz",null],["120695918089","Buckautal",null],["120695918224","Görzke",null],["120695918232","Gräben",null],["120695918648","Wenzlow",null],["120695918680","Wollin",null],["120695918696","Ziesar, Stadt",null],["120700125125","Groß Pankow (Prignitz)",null],["120700149149","Gumtow",null],["120700173173","Karstädt",null],["120700296296","Perleberg, Stadt",null],["120700302302","Plattenburg",null],["120700316316","Pritzwalk, Stadt",null],["120700424424","Wittenberge, Stadt",null],["120705001008","Bad Wilsnack, Stadt",null],["120705001052","Breese",null],["120705001241","Legde/Quitzöbel",null],["120705001348","Rühstädt",null],["120705001416","Weisen",null],["120705005060","Cumlosen",null],["120705005236","Lanz",null],["120705005244","Lenzen (Elbe), Stadt",null],["120705005246","Lenzerwische",null],["120705006096","Gerdshagen",null],["120705006153","Halenbeck-Rohlsdorf",null],["120705006222","Kümmernitztal",null],["120705006266","Marienfließ",null],["120705006280","Meyenburg, Stadt",null],["120705009028","Berge",null],["120705009145","Gülitz-Reetz",null],["120705009300","Pirow",null],["120705009325","Putlitz, Stadt",null],["120705009393","Triglitz",null],["120710057057","Drebkau/Drjowk, Stadt",null],["120710076076","Forst (Lausitz)/Baršć (Łužyca), Stadt",null],["120710160160","Guben, Stadt",null],["120710244244","Kolkwitz/Gołkojce",null],["120710301301","Neuhausen/Spree / Kopańce/Sprjewja",null],["120710337337","Schenkendöbern/Derbno",null],["120710372372","Spremberg/Grodk, Stadt",null],["120710408408","Welzow/Wjelcej, Stadt",null],["120715101028","Briesen/Brjazyna",null],["120715101032","Burg (Spreewald)/Bórkowy (Błota)",null],["120715101041","Dissen-Striesow/Dešno-Strjažow",null],["120715101164","Guhrow/Góry",null],["120715101341","Schmogrow-Fehrow/Smogorjow-Prjawoz",null],["120715101412","Werben/Wjerbno",null],["120715102044","Döbern/Derbno, Stadt",null],["120715102074","Felixsee/Feliksowy Jazor",null],["120715102153","Groß Schacksdorf-Simmersdorf",null],["120715102189","Jämlitz-Klein Düben",null],["120715102294","Neiße-Malxetal/Dolina Nysa-Małksa",null],["120715102392","Tschernitz/Cersk",null],["120715102414","Wiesengrund/Łukojce",null],["120715107052","Drachhausen/Hochoza",null],["120715107060","Drehnow/Drjenow",null],["120715107176","Heinersbrück/Móst",null],["120715107193","Jänschwalde/Janšojce",null],["120715107304","Peitz/Picnjo, Stadt",null],["120715107384","Tauer/Turjej",null],["120715107386","Teichland/Gatojce",null],["120715107401","Turnow-Preilack/Turnow-Pśiłuk",null],["120720002002","Am Mellensee",null],["120720014014","Baruth/Mark, Stadt",null],["120720017017","Blankenfelde-Mahlow",null],["120720120120","Großbeeren",null],["120720169169","Jüterbog, Stadt",null],["120720232232","Luckenwalde, Stadt",null],["120720240240","Ludwigsfelde, Stadt",null],["120720297297","Niedergörsdorf",null],["120720312312","Nuthe-Urstromtal",null],["120720340340","Rangsdorf",null],["120720426426","Trebbin, Stadt",null],["120720477477","Zossen, Stadt",null],["120725204053","Dahme/Mark, Stadt",null],["120725204055","Dahmetal",null],["120725204157","Ihlow",null],["120725204298","Niederer Fläming",null],["120730008008","Angermünde, Stadt",null],["120730069069","Boitzenburger Land",null],["120730384384","Lychen, Stadt",null],["120730429429","Nordwestuckermark",null],["120730452452","Prenzlau, Stadt",null],["120730532532","Schwedt/Oder, Stadt",null],["120730572572","Templin, Stadt",null],["120730579579","Uckerland",null],["120735303085","Brüssow, Stadt",null],["120735303093","Carmzow-Wallmow",null],["120735303216","Göritz",null],["120735303490","Schenkenberg",null],["120735303520","Schönfeld",null],["120735304097","Casekow",null],["120735304189","Gartz (Oder), Stadt",null],["120735304309","Hohenselchow-Groß Pinnow",null],["120735304393","Mescherin",null],["120735304565","Tantow",null],["120735305157","Flieth-Stegelitz",null],["120735305201","Gerswalde",null],["120735305396","Milmersdorf",null],["120735305404","Mittenwalde",null],["120735305569","Temmen-Ringenwalde",null],["120735306225","Gramzow",null],["120735306261","Grünow",null],["120735306430","Oberuckersee",null],["120735306458","Randowtal",null],["120735306578","Uckerfelde",null],["120735306645","Zichow",null],["120735310032","Berkholz-Meyenburg",null],["120735310386","Mark Landin",null],["120735310440","Pinnow",null],["120735310603","Passow",null],["130009999999","Küstengewässer einschl. Anteil am Festlandsockel",null],["130030000000","Rostock, Hanse- und Universitätsstadt",null],["130040000000","Schwerin, Landeshauptstadt",null],["130710027027","Dargun, Stadt",null],["130710029029","Demmin, Hansestadt",null],["130710033033","Feldberger Seenlandschaft",null],["130710107107","Neubrandenburg, Vier-Tore-Stadt",null],["130710110110","Neustrelitz, Residenzstadt",null],["130710156156","Waren (Müritz), Stadt",null],["130715151008","Beggerow",null],["130715151014","Borrentin",null],["130715151064","Hohenbollentin",null],["130715151065","Hohenmocker",null],["130715151072","Kentzlin",null],["130715151076","Kletzin",null],["130715151089","Lindenberg",null],["130715151096","Meesiger",null],["130715151112","Nossendorf",null],["130715151128","Sarow",null],["130715151131","Schönfeld",null],["130715151136","Siedenbrünzow",null],["130715151139","Sommersdorf",null],["130715151148","Utzedel",null],["130715151150","Verchen",null],["130715151157","Warrenzin",null],["130715152028","Datzetal",null],["130715152035","Friedland, Stadt",null],["130715152037","Galenbeck",null],["130715153007","Basedow",null],["130715153032","Faulenrost",null],["130715153039","Gielow",null],["130715153084","Kummerow",null],["130715153092","Malchin, Stadt",null],["130715153109","Neukalen, Peenestadt",null],["130715154001","Alt Schwerin",null],["130715154036","Fünfseen",null],["130715154043","Göhren-Lebbin",null],["130715154093","Malchow, Inselstadt",null],["130715154113","Nossentiner Hütte",null],["130715154114","Penkow",null],["130715154138","Silz",null],["130715154155","Walow",null],["130715154171","Zislow",null],["130715155099","Mirow, Stadt",null],["130715155119","Priepert",null],["130715155159","Wesenberg, Stadt",null],["130715155167","Wustrow",null],["130715156011","Blankensee",null],["130715156012","Blumenholz",null],["130715156025","Carpin",null],["130715156042","Godendorf",null],["130715156058","Grünow",null],["130715156066","Hohenzieritz",null],["130715156075","Klein Vielen",null],["130715156080","Kratzeburg",null],["130715156100","Möllenbeck",null],["130715156147","Userin",null],["130715156162","Wokuhl-Dabelow",null],["130715157009","Beseritz",null],["130715157010","Blankenhof",null],["130715157019","Brunn",null],["130715157104","Neddemin",null],["130715157108","Neuenkirchen",null],["130715157111","Neverin",null],["130715157140","Sponholz",null],["130715157141","Staven",null],["130715157145","Trollenhagen",null],["130715157161","Woggersin",null],["130715157166","Wulkenzin",null],["130715157170","Zirzow",null],["130715158005","Ankershagen, Schliemanngemeinde",null],["130715158101","Möllenhagen",null],["130715158115","Penzlin, Stadt",null],["130715158173","Kuckssee",null],["130715159003","Altenhof",null],["130715159013","Bollewick",null],["130715159020","Buchholz",null],["130715159023","Bütow",null],["130715159034","Fincken",null],["130715159045","Gotthun",null],["130715159053","Groß Kelle",null],["130715159073","Kieve",null],["130715159087","Lärz",null],["130715159088","Leizen",null],["130715159097","Melz",null],["130715159118","Priborn",null],["130715159122","Rechlin",null],["130715159124","Röbel/Müritz, Stadt",null],["130715159133","Schwarz",null],["130715159137","Sietow",null],["130715159143","Stuer",null],["130715159175","Eldetal",null],["130715159176","Südmüritz",null],["130715160047","Grabowhöfe",null],["130715160056","Groß Plasten",null],["130715160063","Hohen Wangelin",null],["130715160069","Jabel",null],["130715160071","Kargow",null],["130715160077","Klink",null],["130715160078","Klocksin",null],["130715160103","Moltzow",null],["130715160144","Torgelow am See",null],["130715160154","Vollrathsruhe",null],["130715160172","Peenehagen",null],["130715160174","Schloen-Dratow",null],["130715161021","Burg Stargard, Stadt",null],["130715161026","Cölpin",null],["130715161055","Groß Nemerow",null],["130715161067","Holldorf",null],["130715161090","Lindetal",null],["130715161117","Pragsdorf",null],["130715162015","Bredenfelde",null],["130715162018","Briggow",null],["130715162048","Grammentin",null],["130715162060","Gülzow",null],["130715162068","Ivenack",null],["130715162070","Jürgenstorf",null],["130715162074","Kittendorf",null],["130715162079","Knorrendorf",null],["130715162102","Mölln",null],["130715162123","Ritzerow",null],["130715162127","Rosenow",null],["130715162142","Stavenhagen, Reuterstadt, Stadt",null],["130715162169","Zettemin",null],["130715163002","Altenhagen",null],["130715163004","Altentreptow, Stadt",null],["130715163006","Bartow",null],["130715163016","Breesen",null],["130715163017","Breest",null],["130715163022","Burow",null],["130715163041","Gnevkow",null],["130715163044","Golchen",null],["130715163049","Grapzow",null],["130715163050","Grischow",null],["130715163057","Groß Teetzleben",null],["130715163059","Gültz",null],["130715163081","Kriesow",null],["130715163120","Pripsleben",null],["130715163125","Röckwitz",null],["130715163135","Siedenbollentin",null],["130715163146","Tützpatz",null],["130715163158","Werder",null],["130715163160","Wildberg",null],["130715163163","Wolde",null],["130715164054","Groß Miltzow",null],["130715164083","Kublank",null],["130715164105","Neetzka",null],["130715164130","Schönbeck",null],["130715164132","Schönhausen",null],["130715164153","Voigtsdorf",null],["130715164164","Woldegk, Windmühlenstadt",null],["130720006006","Bad Doberan, Stadt",null],["130720029029","Dummerstorf",null],["130720036036","Graal-Müritz, Ostseeheilbad",null],["130720043043","Güstrow, Barlachstadt",null],["130720058058","Kröpelin, Stadt",null],["130720060060","Kühlungsborn, Ostseebad, Stadt",null],["130720074074","Neubukow, Stadt",null],["130720091091","Sanitz",null],["130720093093","Satow",null],["130720106106","Teterow, Bergringstadt",null],["130725251001","Admannshagen-Bargeshagen",null],["130725251007","Bartenshagen-Parkentin",null],["130725251017","Börgerende-Rethwisch",null],["130725251047","Hohenfelde",null],["130725251075","Nienhagen, Ostseebad",null],["130725251083","Reddelich",null],["130725251086","Retschow",null],["130725251099","Steffenshagen",null],["130725251117","Wittenbeck",null],["130725252009","Baumgarten",null],["130725252013","Bernitt",null],["130725252020","Bützow, Stadt",null],["130725252028","Dreetz",null],["130725252050","Jürgenshagen",null],["130725252053","Klein Belitz",null],["130725252078","Penzin",null],["130725252089","Rühn",null],["130725252101","Steinhagen",null],["130725252104","Tarnow",null],["130725252114","Warnow",null],["130725252120","Zepelin",null],["130725253019","Broderstorf",null],["130725253081","Poppendorf",null],["130725253087","Roggentin",null],["130725253108","Thulendorf",null],["130725254004","Altkalen",null],["130725254010","Behren-Lübchin",null],["130725254031","Finkenthal",null],["130725254035","Gnoien, Warbelstadt",null],["130725254111","Walkendorf",null],["130725255033","Glasewitz",null],["130725255039","Groß Schwiesow",null],["130725255042","Gülzow-Prüzen",null],["130725255044","Gutow",null],["130725255055","Klein Upahl",null],["130725255061","Kuhs",null],["130725255067","Lohmen",null],["130725255069","Lüssow",null],["130725255071","Mistorf",null],["130725255073","Mühl Rosin",null],["130725255079","Plaaz",null],["130725255084","Reimershagen",null],["130725255092","Sarmstorf",null],["130725255119","Zehna",null],["130725256026","Dobbin-Linstow",null],["130725256048","Hoppenrade",null],["130725256056","Krakow am See, Stadt",null],["130725256059","Kuchelmiß",null],["130725256063","Lalendorf",null],["130725257027","Dolgen am See",null],["130725257046","Hohen Sprenz",null],["130725257062","Laage, Stadt",null],["130725257112","Wardow",null],["130725258003","Alt Sührkow",null],["130725258023","Dahmen",null],["130725258024","Dalkendorf",null],["130725258038","Groß Roge",null],["130725258040","Groß Wokern",null],["130725258041","Groß Wüstenfelde",null],["130725258045","Hohen Demzin",null],["130725258049","Jördenstorf",null],["130725258066","Lelkendorf",null],["130725258082","Prebberede",null],["130725258094","Schorssow",null],["130725258096","Schwasdorf",null],["130725258103","Sukow-Levitzow",null],["130725258109","Thürkow",null],["130725258113","Warnkenhagen",null],["130725259002","Alt Bukow",null],["130725259005","Am Salzhaff",null],["130725259008","Bastorf",null],["130725259014","Biendorf",null],["130725259022","Carinerland",null],["130725259085","Rerik, Ostseebad, Stadt",null],["130725260012","Bentwisch",null],["130725260015","Blankenhagen",null],["130725260032","Gelbensande",null],["130725260072","Mönchhagen",null],["130725260088","Rövershagen",null],["130725261011","Benitz",null],["130725261018","Bröbberow",null],["130725261051","Kassow",null],["130725261090","Rukieten",null],["130725261095","Schwaan, Stadt",null],["130725261110","Vorbeck",null],["130725261116","Wiendorf",null],["130725262021","Cammin",null],["130725262034","Gnewitz",null],["130725262037","Grammow",null],["130725262076","Nustrow",null],["130725262097","Selpin",null],["130725262102","Stubbendorf",null],["130725262105","Tessin, Stadt",null],["130725262107","Thelkow",null],["130725262118","Zarnewanz",null],["130725263030","Elmenhorst/Lichtenhagen",null],["130725263057","Kritzmow",null],["130725263064","Lambrechtshagen",null],["130725263077","Papendorf",null],["130725263080","Pölchow",null],["130725263098","Stäbelow",null],["130725263121","Ziesendorf",null],["130730011011","Binz, Ostseebad",null],["130730035035","Grimmen, Stadt",null],["130730055055","Marlow, Stadt",null],["130730070070","Putbus, Stadt",null],["130730080080","Sassnitz, Stadt",null],["130730088088","Stralsund, Hansestadt",null],["130730089089","Süderholz",null],["130730105105","Zingst, Ostseeheilbad",null],["130735351005","Altenpleen",null],["130735351037","Groß Mohrdorf",null],["130735351044","Klausdorf",null],["130735351046","Kramerhof",null],["130735351066","Preetz",null],["130735351068","Prohn",null],["130735352009","Barth, Stadt",null],["130735352018","Divitz-Spoldershagen",null],["130735352025","Fuhlendorf",null],["130735352042","Karnin",null],["130735352043","Kenz-Küstrow",null],["130735352051","Löbnitz",null],["130735352053","Lüdershagen",null],["130735352069","Pruchten",null],["130735352077","Saal",null],["130735352094","Trinwillershagen",null],["130735353010","Bergen auf Rügen, Stadt",null],["130735353014","Buschvitz",null],["130735353027","Garz/Rügen, Stadt",null],["130735353038","Gustow",null],["130735353049","Lietzow",null],["130735353063","Parchtitz",null],["130735353064","Patzig",null],["130735353065","Poseritz",null],["130735353072","Ralswiek",null],["130735353074","Rappin",null],["130735353083","Sehlen",null],["130735354002","Ahrenshoop, Ostseebad",null],["130735354012","Born a. Darß",null],["130735354017","Dierhagen, Ostseebad",null],["130735354067","Prerow, Ostseebad",null],["130735354100","Wieck a. Darß",null],["130735354103","Wustrow, Ostseebad",null],["130735355024","Franzburg, Stadt",null],["130735355029","Glewitz",null],["130735355034","Gremersdorf-Buchholz",null],["130735355057","Millienhagen-Oebelitz",null],["130735355062","Papenhagen",null],["130735355076","Richtenberg, Stadt",null],["130735355086","Splietsdorf",null],["130735355096","Velgast",null],["130735355097","Weitenhagen",null],["130735355098","Wendisch Baggendorf",null],["130735356023","Elmenhorst",null],["130735356090","Sundhagen",null],["130735356102","Wittenhagen",null],["130735357006","Baabe, Ostseebad",null],["130735357031","Göhren, Ostseebad",null],["130735357048","Lancken-Granitz",null],["130735357084","Sellin, Ostseebad",null],["130735357106","Zirkow",null],["130735357107","Mönchgut, Ostseebad",null],["130735358036","Groß Kordshagen",null],["130735358041","Jakobsdorf",null],["130735358054","Lüssow",null],["130735358060","Niepars",null],["130735358061","Pantelitz",null],["130735358087","Steinhagen",null],["130735358099","Wendorf",null],["130735358104","Zarrendorf",null],["130735359004","Altenkirchen",null],["130735359013","Breege",null],["130735359019","Dranske",null],["130735359030","Glowe",null],["130735359052","Lohme",null],["130735359071","Putgarten",null],["130735359078","Sagard",null],["130735359101","Wiek",null],["130735360007","Bad Sülze, Stadt",null],["130735360015","Dettmannsdorf",null],["130735360016","Deyelsdorf",null],["130735360020","Drechow",null],["130735360022","Eixen",null],["130735360032","Grammendorf",null],["130735360033","Gransebieth",null],["130735360039","Hugoldsdorf",null],["130735360050","Lindholz",null],["130735360093","Tribsees, Stadt",null],["130735361001","Ahrenshagen-Daskow",null],["130735361075","Ribnitz-Damgarten, Bernsteinstadt",null],["130735361082","Schlemmin",null],["130735361085","Semlow",null],["130735362003","Altefähr",null],["130735362021","Dreschvitz",null],["130735362028","Gingst",null],["130735362040","Insel Hiddensee, Seebad",null],["130735362045","Kluis",null],["130735362059","Neuenkirchen",null],["130735362073","Rambin",null],["130735362079","Samtens",null],["130735362081","Schaprode",null],["130735362092","Trent",null],["130735362095","Ummanz",null],["130740026026","Grevesmühlen, Stadt",null],["130740035035","Insel Poel, Ostseebad",null],["130740087087","Wismar, Hansestadt",null],["130745451002","Bad Kleinen",null],["130745451003","Barnekow",null],["130745451008","Bobitz",null],["130745451019","Dorf Mecklenburg",null],["130745451030","Groß Stieten",null],["130745451031","Hohen Viecheln",null],["130745451047","Lübow",null],["130745451053","Metelsdorf",null],["130745451082","Ventschow",null],["130745452020","Dragun",null],["130745452021","Gadebusch, Stadt",null],["130745452040","Kneese",null],["130745452043","Krembz",null],["130745452054","Mühlen Eichsen",null],["130745452068","Roggendorf",null],["130745452070","Rögnitz",null],["130745452081","Veelböken",null],["130745453005","Bernstorf",null],["130745453022","Gägelow",null],["130745453069","Roggenstorf",null],["130745453071","Rüting",null],["130745453077","Testorf-Steinfort",null],["130745453079","Upahl",null],["130745453085","Warnow",null],["130745453093","Stepenitztal",null],["130745454010","Boltenhagen, Ostseebad",null],["130745454016","Damshagen",null],["130745454032","Hohenkirchen",null],["130745454037","Kalkhorst",null],["130745454039","Klütz, Stadt",null],["130745454089","Zierow",null],["130745455001","Alt Meteln",null],["130745455012","Brüsewitz",null],["130745455014","Cramonshagen",null],["130745455015","Dalberg-Wendelstorf",null],["130745455024","Gottesgabe",null],["130745455025","Grambow",null],["130745455038","Klein Trebbow",null],["130745455048","Lübstorf",null],["130745455050","Lützow",null],["130745455061","Perlin",null],["130745455062","Pingelshagen",null],["130745455064","Pokrent",null],["130745455072","Schildetal",null],["130745455075","Seehof",null],["130745455088","Zickhusen",null],["130745456004","Benz",null],["130745456007","Blowatz",null],["130745456009","Boiensdorf",null],["130745456034","Hornstorf",null],["130745456044","Krusenhagen",null],["130745456056","Neuburg",null],["130745457006","Bibow",null],["130745457023","Glasin",null],["130745457036","Jesendorf",null],["130745457046","Lübberstorf",null],["130745457057","Neukloster, Stadt",null],["130745457060","Passee",null],["130745457084","Warin, Stadt",null],["130745457090","Zurow",null],["130745457091","Züsow",null],["130745458013","Carlow",null],["130745458018","Dechow",null],["130745458028","Groß Molzahn",null],["130745458033","Holdorf",null],["130745458042","Königsfeld",null],["130745458065","Rehna, Stadt",null],["130745458066","Rieps",null],["130745458073","Schlagsdorf",null],["130745458078","Thandorf",null],["130745458080","Utecht",null],["130745458092","Wedendorfersee",null],["130745459017","Dassow, Stadt",null],["130745459027","Grieben",null],["130745459049","Lüdersdorf",null],["130745459052","Menzendorf",null],["130745459067","Roduchelstorf",null],["130745459074","Schönberg, Stadt",null],["130745459076","Selmsdorf",null],["130745459094","Siemz-Niendorf",null],["130750005005","Anklam, Hansestadt",null],["130750039039","Greifswald, Universitäts- und Hansestadt",null],["130750049049","Heringsdorf, Ostseebad",null],["130750105105","Pasewalk, Stadt",null],["130750130130","Strasburg (Uckermark), Stadt",null],["130750136136","Ueckermünde, Seebad , Stadt",null],["130755551021","Buggenhagen",null],["130755551072","Krummin",null],["130755551074","Lassan, Stadt",null],["130755551087","Lütow",null],["130755551124","Sauzin",null],["130755551144","Wolgast, Stadt",null],["130755551147","Zemitz",null],["130755552001","Ahlbeck",null],["130755552003","Altwarp",null],["130755552031","Eggesin, Stadt",null],["130755552037","Grambin",null],["130755552051","Hintersee",null],["130755552075","Leopoldshagen",null],["130755552078","Liepgarten",null],["130755552084","Lübs",null],["130755552085","Luckow",null],["130755552089","Meiersberg",null],["130755552093","Mönkebude",null],["130755552139","Vogelsang-Warsin",null],["130755553007","Bargischow",null],["130755553013","Blesewitz",null],["130755553015","Boldekow",null],["130755553020","Bugewitz",null],["130755553022","Butzow",null],["130755553029","Ducherow",null],["130755553053","Iven",null],["130755553068","Krien",null],["130755553073","Krusenfelde",null],["130755553088","Medow",null],["130755553098","Neu Kosenow",null],["130755553101","Neuenkirchen",null],["130755553110","Postlow",null],["130755553116","Rossin",null],["130755553122","Sarnow",null],["130755553127","Spantekow",null],["130755553128","Stolpe an der Peene",null],["130755553155","Neetzow-Liepen",null],["130755554002","Alt Tellin",null],["130755554009","Bentzin",null],["130755554023","Daberkow",null],["130755554054","Jarmen, Stadt",null],["130755554070","Kruckow",null],["130755554134","Tutow",null],["130755554140","Völschow",null],["130755555008","Behrenhoff",null],["130755555025","Dargelin",null],["130755555027","Dersekow",null],["130755555050","Hinrichshagen",null],["130755555076","Levenhagen",null],["130755555091","Mesekenhagen",null],["130755555102","Neuenkirchen",null],["130755555141","Wackerow",null],["130755555142","Weitenhagen",null],["130755556011","Bergholz",null],["130755556012","Blankensee",null],["130755556016","Boock",null],["130755556035","Glasow",null],["130755556038","Grambow",null],["130755556067","Krackow",null],["130755556079","Löcknitz",null],["130755556095","Nadrensee",null],["130755556107","Penkun, Stadt",null],["130755556108","Plöwen",null],["130755556113","Ramin",null],["130755556117","Rossow",null],["130755556119","Rothenklempenow",null],["130755557018","Brünzow",null],["130755557046","Hanshagen",null],["130755557059","Katzow",null],["130755557060","Kemnitz",null],["130755557069","Kröslin",null],["130755557081","Loissin",null],["130755557083","Lubmin, Seebad",null],["130755557097","Neu Boltenhagen",null],["130755557120","Rubenow",null],["130755557146","Wusterhusen",null],["130755558036","Görmin",null],["130755558082","Loitz, Stadt",null],["130755558123","Sassen-Trantow",null],["130755559004","Altwigshagen",null],["130755559033","Ferdinandshof",null],["130755559045","Hammer a.d. Uecker",null],["130755559048","Heinrichswalde",null],["130755559118","Rothemühl",null],["130755559131","Torgelow, Stadt",null],["130755559143","Wilhelmsburg",null],["130755560017","Brietzig",null],["130755560032","Fahrenwalde",null],["130755560042","Groß Luckow",null],["130755560055","Jatznick",null],["130755560063","Koblentz",null],["130755560071","Krugsdorf",null],["130755560103","Nieden",null],["130755560104","Papendorf",null],["130755560109","Polzow",null],["130755560115","Rollwitz",null],["130755560126","Schönwalde",null],["130755560138","Viereck",null],["130755560149","Zerrenthin",null],["130755561058","Karlshagen, Ostseebad",null],["130755561092","Mölschow",null],["130755561106","Peenemünde",null],["130755561133","Trassenheide, Ostseebad",null],["130755561151","Zinnowitz, Ostseebad",null],["130755562010","Benz",null],["130755562026","Dargen",null],["130755562034","Garz",null],["130755562056","Kamminke",null],["130755562065","Korswandt",null],["130755562066","Koserow, Ostseebad",null],["130755562080","Loddin, Seebad",null],["130755562090","Mellenthin",null],["130755562111","Pudagla",null],["130755562114","Rankwitz",null],["130755562129","Stolpe auf Usedom",null],["130755562135","Ückeritz, Seebad",null],["130755562137","Usedom, Stadt",null],["130755562148","Zempin, Seebad",null],["130755562152","Zirchow",null],["130755563006","Bandelin",null],["130755563040","Gribow",null],["130755563041","Groß Kiesow",null],["130755563043","Groß Polzin",null],["130755563044","Gützkow, Stadt",null],["130755563061","Klein Bünzow",null],["130755563094","Murchin",null],["130755563121","Rubkow",null],["130755563125","Schmatzin",null],["130755563145","Wrangelsburg",null],["130755563150","Ziethen",null],["130755563154","Züssow",null],["130755563156","Karlsburg",null],["130760014014","Boizenburg/ Elbe, Stadt",null],["130760060060","Hagenow, Stadt",null],["130760088088","Lübtheen, Stadt",null],["130760090090","Ludwigslust, Stadt",null],["130760108108","Parchim, Stadt",null],["130765652009","Bengerstorf",null],["130765652010","Besitz",null],["130765652016","Brahlstorf",null],["130765652030","Dersenow",null],["130765652054","Gresse",null],["130765652055","Greven",null],["130765652102","Neu Gülze",null],["130765652106","Nostorf",null],["130765652122","Schwanheide",null],["130765652136","Teldau",null],["130765652138","Tessin b. Boizenburg",null],["130765654034","Dömitz, Stadt",null],["130765654053","Grebs-Niendorf",null],["130765654067","Karenz",null],["130765654093","Malk Göhren",null],["130765654094","Malliß",null],["130765654103","Neu Kaliß",null],["130765654143","Vielank",null],["130765655040","Gallin-Kuppentin",null],["130765655051","Granzin",null],["130765655075","Kreien",null],["130765655077","Kritzow",null],["130765655089","Lübz, Stadt",null],["130765655109","Passow",null],["130765655125","Siggelkow",null],["130765655151","Werder",null],["130765655165","Gehlsbach",null],["130765655168","Ruhner Berge",null],["130765656032","Dobbertin",null],["130765656048","Goldberg, Stadt",null],["130765656096","Mestlin",null],["130765656104","Neu Poserin",null],["130765656135","Techentin",null],["130765657003","Balow",null],["130765657021","Brunow",null],["130765657027","Dambeck",null],["130765657037","Eldena",null],["130765657049","Gorlosen",null],["130765657050","Grabow, Stadt",null],["130765657069","Karstädt",null],["130765657076","Kremmin",null],["130765657097","Milow",null],["130765657098","Möllenbeck",null],["130765657100","Muchow",null],["130765657115","Prislich",null],["130765657161","Zierzow",null],["130765658002","Alt Zachun",null],["130765658004","Bandenitz",null],["130765658008","Belsch",null],["130765658013","Bobzin",null],["130765658019","Bresegard bei Picher",null],["130765658041","Gammelin",null],["130765658057","Groß Krams",null],["130765658064","Hoort",null],["130765658065","Hülseburg",null],["130765658070","Kirch Jesar",null],["130765658079","Kuhstorf",null],["130765658099","Moraas",null],["130765658110","Pätow-Steegen",null],["130765658111","Picher",null],["130765658116","Pritzier",null],["130765658119","Redefin",null],["130765658131","Strohkirchen",null],["130765658169","Toddin",null],["130765658145","Warlitz",null],["130765659001","Alt Krenzlin",null],["130765659018","Bresegard bei Eldena",null],["130765659046","Göhlen",null],["130765659058","Groß Laasch",null],["130765659086","Lübesse",null],["130765659087","Lüblow",null],["130765659118","Rastow",null],["130765659134","Sülstorf",null],["130765659141","Uelitz",null],["130765659146","Warlow",null],["130765659156","Wöbbelin",null],["130765660012","Blievenstorf",null],["130765660017","Brenz",null],["130765660105","Neustadt-Glewe, Stadt",null],["130765662035","Domsühl",null],["130765662056","Groß Godems",null],["130765662068","Karrenzin",null],["130765662085","Lewitzrand",null],["130765662120","Rom",null],["130765662126","Spornitz",null],["130765662129","Stolpe",null],["130765662160","Ziegendorf",null],["130765662162","Zölkow",null],["130765662164","Obere Warnow",null],["130765663006","Barkhagen",null],["130765663114","Plau am See, Stadt",null],["130765663166","Ganzlin",null],["130765664011","Blankenberg",null],["130765664015","Borkow",null],["130765664020","Brüel, Stadt",null],["130765664026","Dabel",null],["130765664062","Hohen Pritz",null],["130765664072","Kobrow",null],["130765664078","Kuhlen-Wendorf",null],["130765664101","Mustin",null],["130765664128","Sternberg, Stadt",null],["130765664148","Weitendorf",null],["130765664155","Witzin",null],["130765664167","Kloster Tempzin",null],["130765665036","Dümmer",null],["130765665063","Holthusen",null],["130765665071","Klein Rogahn",null],["130765665107","Pampow",null],["130765665121","Schossin",null],["130765665130","Stralendorf",null],["130765665147","Warsow",null],["130765665154","Wittenförden",null],["130765665163","Zülow",null],["130765666152","Wittenburg, Stadt",null],["130765666153","Wittendörp",null],["130765667039","Gallin",null],["130765667073","Kogel",null],["130765667092","Lüttow-Valluhn",null],["130765667142","Vellahn",null],["130765667159","Zarrentin am Schaalsee, Stadt",null],["130765668005","Banzkow",null],["130765668007","Barnin",null],["130765668023","Bülow",null],["130765668024","Cambs",null],["130765668025","Crivitz, Stadt",null],["130765668029","Demen",null],["130765668033","Dobin am See",null],["130765668038","Friedrichsruhe",null],["130765668044","Gneven",null],["130765668080","Langen Brütz",null],["130765668082","Leezen",null],["130765668112","Pinnow",null],["130765668113","Plate",null],["130765668117","Raben Steinfeld",null],["130765668133","Sukow",null],["130765668140","Tramm",null],["130765668158","Zapel",null],["145110000000","Chemnitz, Stadt",null],["145210010010","Amtsberg",null],["145210020020","Annaberg-Buchholz, Stadt",null],["145210035035","Aue-Bad Schlema, Stadt",null],["145210110110","Breitenbrunn/Erzgeb.",null],["145210130130","Crottendorf",null],["145210150150","Drebach",null],["145210160160","Ehrenfriedersdorf, Stadt",null],["145210170170","Eibenstock, Stadt",null],["145210200200","Gelenau/Erzgeb.",null],["145210240240","Großolbersdorf",null],["145210250250","Großrückerswalde",null],["145210260260","Grünhain-Beierfeld, Stadt",null],["145210290290","Hohndorf",null],["145210310310","Jahnsdorf/Erzgeb.",null],["145210320320","Johanngeorgenstadt, Stadt",null],["145210330330","Jöhstadt, Stadt",null],["145210355355","Lauter-Bernsbach, Stadt",null],["145210370370","Lößnitz, Stadt",null],["145210390390","Marienberg, Stadt",null],["145210400400","Mildenau",null],["145210410410","Neukirchen/Erzgeb.",null],["145210440440","Oberwiesenthal, Kurort, Stadt",null],["145210450450","Oelsnitz/Erzgeb., Stadt",null],["145210460460","Olbernhau, Stadt",null],["145210495495","Pockau-Lengefeld, Stadt",null],["145210500500","Raschau-Markersbach",null],["145210530530","Schneeberg, Stadt",null],["145210540540","Schönheide",null],["145210550550","Schwarzenberg/Erzgeb., Stadt",null],["145210560560","Sehmatal",null],["145210600600","Stützengrün",null],["145210620620","Thalheim/Erzgeb., Stadt",null],["145210630630","Thermalbad Wiesenbad",null],["145210640640","Thum, Stadt",null],["145210670670","Wolkenstein, Stadt",null],["145215101060","Bärenstein",null],["145215101340","Königswalde",null],["145215103040","Auerbach",null],["145215103120","Burkhardtsdorf",null],["145215103230","Gornsdorf",null],["145215110210","Geyer, Stadt",null],["145215110610","Tannenberg",null],["145215115380","Lugau/Erzgeb., Stadt",null],["145215115430","Niederwürschnitz",null],["145215130510","Scheibenberg, Stadt",null],["145215130520","Schlettau, Stadt",null],["145215132140","Deutschneudorf",null],["145215132280","Heidersdorf",null],["145215132570","Seiffen/Erzgeb., Kurort",null],["145215133420","Niederdorf",null],["145215133590","Stollberg/Erzgeb., Stadt",null],["145215138220","Gornau/Erzgeb.",null],["145215138690","Zschopau, Stadt",null],["145215139080","Bockau",null],["145215139700","Zschorlau",null],["145215140180","Elterlein, Stadt",null],["145215140710","Zwönitz, Stadt",null],["145215405090","Börnichen/Erzgeb.",null],["145215405270","Grünhainichen",null],["145220020020","Augustusburg, Stadt",null],["145220035035","Bobritzsch-Hilbersdorf",null],["145220050050","Brand-Erbisdorf, Stadt",null],["145220070070","Claußnitz",null],["145220080080","Döbeln, Stadt",null],["145220110110","Eppendorf",null],["145220120120","Erlau",null],["145220140140","Flöha, Stadt",null],["145220150150","Frankenberg/Sa., Stadt",null],["145220170170","Frauenstein, Stadt",null],["145220180180","Freiberg, Stadt, Universitätsstadt",null],["145220190190","Geringswalde, Stadt",null],["145220200200","Großhartmannsdorf",null],["145220210210","Großschirma, Stadt",null],["145220220220","Großweitzschen",null],["145220230230","Hainichen, Stadt",null],["145220240240","Halsbrücke",null],["145220250250","Hartha, Stadt",null],["145220260260","Hartmannsdorf",null],["145220290290","Königshain-Wiederau",null],["145220300300","Kriebstein",null],["145220310310","Leisnig, Stadt",null],["145220320320","Leubsdorf",null],["145220330330","Lichtenau",null],["145220350350","Lunzenau, Stadt",null],["145220390390","Mulda/Sa.",null],["145220400400","Neuhausen/Erzgeb.",null],["145220420420","Niederwiesa",null],["145220430430","Oberschöna",null],["145220440440","Oederan, Stadt",null],["145220460460","Penig, Stadt",null],["145220470470","Rechenberg-Bienenmühle",null],["145220480480","Reinsberg",null],["145220500500","Rossau",null],["145220510510","Roßwein, Stadt",null],["145220540540","Striegistal",null],["145220570570","Waldheim, Stadt",null],["145220580580","Wechselburg",null],["145225102060","Burgstädt, Stadt",null],["145225102380","Mühlau",null],["145225102550","Taura",null],["145225113340","Lichtenberg/Erzgeb.",null],["145225113590","Weißenborn/Erzgeb.",null],["145225119010","Altmittweida",null],["145225119360","Mittweida, Stadt, Hochschulstadt",null],["145225123450","Ostrau",null],["145225123620","Zschaitz-Ottewig",null],["145225126280","Königsfeld",null],["145225126490","Rochlitz, Stadt",null],["145225126530","Seelitz",null],["145225126600","Zettlitz",null],["145225129090","Dorfchemnitz",null],["145225129520","Sayda, Stadt",null],["145230010010","Adorf/Vogtl., Stadt",null],["145230020020","Auerbach/Vogtl., Stadt",null],["145230030030","Bad Brambach",null],["145230040040","Bad Elster, Stadt",null],["145230090090","Ellefeld",null],["145230100100","Elsterberg, Stadt",null],["145230160160","Klingenthal, Stadt",null],["145230170170","Lengenfeld, Stadt",null],["145230200200","Markneukirchen, Stadt",null],["145230245245","Muldenhammer",null],["145230280280","Neumark",null],["145230310310","Pausa-Mühltroff, Stadt",null],["145230320320","Plauen, Stadt",null],["145230330330","Pöhl",null],["145230360360","Rodewisch, Stadt",null],["145230365365","Rosenbach/Vogtl.",null],["145230380380","Steinberg",null],["145230450450","Weischlitz",null],["145235107120","Falkenstein/Vogtl., Stadt",null],["145235107130","Grünbach",null],["145235107290","Neustadt/Vogtl.",null],["145235120190","Limbach",null],["145235120260","Netzschkau, Stadt",null],["145235122060","Bösenbrunn",null],["145235122080","Eichigt",null],["145235122300","Oelsnitz/Vogtl., Stadt",null],["145235122440","Triebel/Vogtl.",null],["145235125150","Heinsdorfergrund",null],["145235125340","Reichenbach im Vogtland, Stadt",null],["145235131230","Mühlental",null],["145235131370","Schöneck/Vogtl., Stadt",null],["145235134270","Neuensalz",null],["145235134430","Treuen, Stadt",null],["145235402050","Bergen",null],["145235402410","Theuma",null],["145235402420","Tirpersdorf",null],["145235402460","Werda",null],["145240020020","Callenberg",null],["145240060060","Fraureuth",null],["145240070070","Gersdorf",null],["145240080080","Glauchau, Stadt",null],["145240090090","Hartenstein, Stadt",null],["145240120120","Hohenstein-Ernstthal, Stadt",null],["145240140140","Langenbernsdorf",null],["145240150150","Langenweißbach",null],["145240170170","Lichtentanne",null],["145240200200","Mülsen",null],["145240210210","Neukirchen/Pleiße",null],["145240230230","Oberlungwitz, Stadt",null],["145240250250","Reinsdorf",null],["145240300300","Werdau, Stadt",null],["145240310310","Wildenfels, Stadt",null],["145240320320","Wilkau-Haßlau, Stadt",null],["145240330330","Zwickau, Stadt",null],["145245104030","Crimmitschau, Stadt",null],["145245104050","Dennheritz",null],["145245111040","Crinitzberg",null],["145245111100","Hartmannsdorf b. Kirchberg",null],["145245111110","Hirschfeld",null],["145245111130","Kirchberg, Stadt",null],["145245114180","Limbach-Oberfrohna, Stadt",null],["145245114220","Niederfrohna",null],["145245118190","Meerane, Stadt",null],["145245118270","Schönberg",null],["145245128010","Bernsdorf",null],["145245128160","Lichtenstein/Sa., Stadt",null],["145245128280","St. Egidien",null],["145245135240","Oberwiera",null],["145245135260","Remse",null],["145245135290","Waldenburg, Stadt",null],["146120000000","Dresden, Stadt",null],["146250010010","Arnsdorf",null],["146250020020","Bautzen / Budyšin, Stadt",null],["146250030030","Bernsdorf, Stadt",null],["146250060060","Burkau",null],["146250090090","Cunewalde",null],["146250100100","Demitz-Thumitz",null],["146250110110","Doberschau-Gaußig / Dobruša-Huska",null],["146250120120","Elsterheide / Halštrowska Hola",null],["146250130130","Elstra, Stadt",null],["146250150150","Göda / Hodźij",null],["146250160160","Großdubrau / Wulka Dubrawa",null],["146250200200","Großröhrsdorf, Stadt",null],["146250220220","Haselbachtal",null],["146250230230","Hochkirch / Bukecy",null],["146250240240","Hoyerswerda / Wojerecy, Stadt",null],["146250250250","Kamenz / Kamjenc, Stadt",null],["146250280280","Königswartha / Rakecy",null],["146250290290","Kubschütz / Kubšicy",null],["146250310310","Lauta, Stadt",null],["146250330330","Lohsa / Łaz",null],["146250340340","Malschwitz / Malešecy",null],["146250380380","Neukirch/Lausitz",null],["146250420420","Oßling",null],["146250430430","Ottendorf-Okrilla",null],["146250480480","Radeberg, Stadt",null],["146250490490","Radibor / Radwor",null],["146250525525","Schirgiswalde-Kirschau, Stadt",null],["146250530530","Schmölln-Putzkau",null],["146250550550","Schwepnitz",null],["146250560560","Sohland a. d. Spree",null],["146250570570","Spreetal / Sprjewiny Doł",null],["146250590590","Steinigtwolmsdorf",null],["146250600600","Wachau",null],["146250610610","Weißenberg / Wóspork, Stadt",null],["146250630630","Wilthen, Stadt",null],["146250640640","Wittichenau / Kulow, Stadt",null],["146255207040","Bischofswerda, Stadt",null],["146255207510","Rammenau",null],["146255211140","Frankenthal",null],["146255211170","Großharthau",null],["146255212190","Großpostwitz/O.L. / Budestecy",null],["146255212390","Obergurig / Hornja Hórka",null],["146255218270","Königsbrück, Stadt",null],["146255218300","Laußnitz",null],["146255218370","Neukirch",null],["146255223360","Neschwitz / Njeswačidło",null],["146255223460","Puschwitz / Bóšicy",null],["146255231180","Großnaundorf",null],["146255231320","Lichtenberg",null],["146255231410","Ohorn",null],["146255231450","Pulsnitz, Stadt",null],["146255231580","Steina",null],["146255501080","Crostwitz / Chrósćicy",null],["146255501350","Nebelschütz / Njebjelčicy",null],["146255501440","Panschwitz-Kuckau / Pančicy-Kukow",null],["146255501470","Räckelwitz / Worklecy",null],["146255501500","Ralbitz-Rosenthal / Ralbicy-Róžant",null],["146260060060","Boxberg/O.L. / Hamor",null],["146260085085","Ebersbach-Neugersdorf, Stadt",null],["146260110110","Görlitz, Stadt",null],["146260180180","Herrnhut, Stadt",null],["146260245245","Kottmar",null],["146260250250","Krauschwitz i.d. O.L. / Krušwica",null],["146260280280","Leutersdorf",null],["146260300300","Markersdorf",null],["146260310310","Mittelherwigsdorf",null],["146260370370","Niesky, Stadt",null],["146260390390","Oderwitz",null],["146260420420","Ostritz, Stadt",null],["146260530530","Seifhennersdorf, Stadt",null],["146260610610","Zittau, Stadt",null],["146265203010","Bad Muskau / Mužakow, Stadt",null],["146265203100","Gablenz / Jabłońc",null],["146265206030","Bernstadt a. d. Eigen, Stadt",null],["146265206500","Schönau-Berzdorf a. d. Eigen",null],["146265214140","Großschönau",null],["146265214170","Hainewalde",null],["146265220150","Großschweidnitz",null],["146265220270","Lawalde",null],["146265220290","Löbau, Stadt",null],["146265220470","Rosenbach",null],["146265224070","Dürrhennersdorf",null],["146265224350","Neusalza-Spremberg, Stadt",null],["146265224510","Schönbach",null],["146265227050","Bertsdorf-Hörnitz",null],["146265227210","Jonsdorf, Kurort",null],["146265227400","Olbersdorf",null],["146265227430","Oybin",null],["146265228020","Beiersdorf",null],["146265228410","Oppach",null],["146265232240","Königshain",null],["146265232450","Reichenbach/O.L., Stadt",null],["146265232570","Vierkirchen",null],["146265233260","Kreba-Neudorf / Chrjebja-Nowa Wjes",null],["146265233460","Rietschen / Rěčicy",null],["146265235160","Hähnichen",null],["146265235480","Rothenburg/O.L., Stadt",null],["146265237120","Groß Düben / Dźěwin",null],["146265237490","Schleife / Slepo",null],["146265237560","Trebendorf / Trjebin",null],["146265242590","Weißkeißel / Wuskidź",null],["146265242600","Weißwasser/O.L., Stadt / Běła Woda",null],["146265502190","Hohendubrau / Wysoka Dubrawa",null],["146265502320","Mücka / Mikow",null],["146265502440","Quitzdorf am See",null],["146265502580","Waldhufen",null],["146265503200","Horka",null],["146265503230","Kodersdorf",null],["146265503330","Neißeaue",null],["146265503520","Schöpstal",null],["146270010010","Coswig, Stadt",null],["146270020020","Diera-Zehren",null],["146270030030","Ebersbach",null],["146270050050","Gröditz, Stadt",null],["146270060060","Großenhain, Stadt",null],["146270070070","Hirschstein",null],["146270080080","Käbschütztal",null],["146270100100","Klipphausen",null],["146270130130","Lommatzsch, Stadt",null],["146270140140","Meißen, Stadt",null],["146270150150","Moritzburg",null],["146270170170","Niederau",null],["146270180180","Nossen, Stadt",null],["146270200200","Priestewitz",null],["146270210210","Radebeul, Stadt",null],["146270220220","Radeburg, Stadt",null],["146270230230","Riesa, Stadt",null],["146270260260","Stauchitz",null],["146270270270","Strehla, Stadt",null],["146270290290","Thiendorf",null],["146270310310","Weinböhla",null],["146270360360","Zeithain",null],["146275225040","Glaubitz",null],["146275225190","Nünchritz",null],["146275234240","Röderaue",null],["146275234340","Wülknitz",null],["146275238110","Lampertswalde",null],["146275238250","Schönfeld",null],["146280050050","Bannewitz",null],["146280060060","Dippoldiswalde, Stadt",null],["146280100100","Dürrröhrsdorf-Dittersbach",null],["146280110110","Freital, Stadt",null],["146280130130","Glashütte, Stadt",null],["146280160160","Heidenau, Stadt",null],["146280190190","Hohnstein, Stadt",null],["146280220220","Kreischa",null],["146280260260","Neustadt in Sachsen, Stadt",null],["146280300300","Rabenau, Stadt",null],["146280360360","Sebnitz, Stadt",null],["146280380380","Stolpen, Stadt",null],["146280410410","Wilsdruff, Stadt",null],["146285201010","Altenberg, Stadt",null],["146285201170","Hermsdorf/Erzgeb.",null],["146285202020","Bad Gottleuba-Berggießhübel, Stadt",null],["146285202040","Bahretal",null],["146285202230","Liebstadt, Stadt",null],["146285204030","Bad Schandau, Stadt",null],["146285204320","Rathmannsdorf",null],["146285204330","Reinhardtsdorf-Schöna",null],["146285209080","Dohna, Stadt",null],["146285209250","Müglitztal",null],["146285219140","Gohrisch",null],["146285219210","Königstein/Sächs. Schw., Stadt",null],["146285219310","Rathen, Kurort",null],["146285219340","Rosenthal-Bielatal",null],["146285219390","Struppen",null],["146285221240","Lohmen",null],["146285221370","Stadt Wehlen, Stadt",null],["146285229070","Dohma",null],["146285229270","Pirna, Stadt",null],["146285230150","Hartmannsdorf-Reichenau",null],["146285230205","Klingenberg",null],["146285240090","Dorfhain",null],["146285240400","Tharandt, Stadt",null],["147130000000","Leipzig, Stadt",null],["147290030030","Bennewitz",null],["147290040040","Böhlen, Stadt",null],["147290050050","Borna, Stadt",null],["147290060060","Borsdorf",null],["147290070070","Brandis, Stadt",null],["147290080080","Colditz, Stadt",null],["147290140140","Frohburg, Stadt",null],["147290150150","Geithain, Stadt",null],["147290160160","Grimma, Stadt",null],["147290170170","Groitzsch, Stadt",null],["147290190190","Großpösna",null],["147290220220","Kitzscher, Stadt",null],["147290245245","Lossatal",null],["147290250250","Machern",null],["147290260260","Markkleeberg, Stadt",null],["147290270270","Markranstädt, Stadt",null],["147290320320","Neukieritzsch",null],["147290360360","Regis-Breitingen, Stadt",null],["147290370370","Rötha, Stadt",null],["147290380380","Thallwitz",null],["147290400400","Trebsen/Mulde, Stadt",null],["147290410410","Wurzen, Stadt",null],["147290430430","Zwenkau, Stadt",null],["147295301010","Bad Lausick, Stadt",null],["147295301330","Otterwisch",null],["147295307020","Belgershain",null],["147295307300","Naunhof, Stadt",null],["147295307340","Parthenstein",null],["147295308100","Elstertrebnitz",null],["147295308350","Pegau, Stadt",null],["147300020020","Bad Düben, Stadt",null],["147300045045","Belgern-Schildau, Stadt",null],["147300050050","Cavertitz",null],["147300060060","Dahlen, Stadt",null],["147300070070","Delitzsch, Stadt",null],["147300080080","Doberschütz",null],["147300110110","Eilenburg, Stadt",null],["147300160160","Laußig",null],["147300170170","Liebschützberg",null],["147300180180","Löbnitz",null],["147300190190","Mockrehna",null],["147300200200","Mügeln, Stadt",null],["147300210210","Naundorf",null],["147300230230","Oschatz, Stadt",null],["147300250250","Rackwitz",null],["147300270270","Schkeuditz, Stadt",null],["147300300300","Taucha, Stadt",null],["147300330330","Wermsdorf",null],["147300340340","Wiedemar",null],["147305302010","Arzberg",null],["147305302030","Beilrode",null],["147305303090","Dommitzsch, Stadt",null],["147305303120","Elsnig",null],["147305303320","Trossin",null],["147305306150","Krostitz",null],["147305306280","Schönwölkau",null],["147305311100","Dreiheide",null],["147305311310","Torgau, Stadt",null],["147305601140","Jesewitz",null],["147305601360","Zschepplin",null],["150010000000","Dessau-Roßlau, Stadt",null],["150020000000","Halle (Saale), Stadt",null],["150030000000","Magdeburg, Landeshauptstadt",null],["150810030030","Arendsee (Altmark), Stadt",null],["150810135135","Gardelegen, Hansestadt",null],["150810240240","Kalbe (Milde), Stadt",null],["150810280280","Klötze, Stadt",null],["150810455455","Salzwedel, Hansestadt",null],["150815051026","Apenburg-Winterfeld, Flecken",null],["150815051045","Beetzendorf",null],["150815051095","Dähre",null],["150815051105","Diesdorf, Flecken",null],["150815051225","Jübar",null],["150815051290","Kuhfelde",null],["150815051440","Rohrberg",null],["150815051545","Wallstawe",null],["150820005005","Aken (Elbe), Stadt",null],["150820015015","Bitterfeld-Wolfen, Stadt",null],["150820180180","Köthen (Anhalt), Stadt",null],["150820241241","Muldestausee",null],["150820256256","Osternienburger Land",null],["150820301301","Raguhn-Jeßnitz, Stadt",null],["150820340340","Sandersdorf-Brehna, Stadt",null],["150820377377","Südliches Anhalt, Stadt",null],["150820430430","Zerbst/Anhalt, Stadt",null],["150820440440","Zörbig, Stadt",null],["150830040040","Barleben",null],["150830270270","Haldensleben, Stadt",null],["150830298298","Hohe Börde",null],["150830390390","Niedere Börde",null],["150830411411","Oebisfelde-Weferlingen, Stadt",null],["150830415415","Oschersleben (Bode), Stadt",null],["150830490490","Sülzetal",null],["150830531531","Wanzleben-Börde, Stadt",null],["150830565565","Wolmirstedt, Stadt",null],["150835051030","Angern",null],["150835051120","Burgstall",null],["150835051130","Colbitz",null],["150835051361","Loitsche-Heinrichsberg",null],["150835051440","Rogätz",null],["150835051557","Westheide",null],["150835051580","Zielitz",null],["150835052020","Altenhausen",null],["150835052060","Beendorf",null],["150835052115","Bülstringen",null],["150835052125","Calvörde",null],["150835052205","Erxleben",null],["150835052230","Flechtingen",null],["150835052323","Ingersleben",null],["150835053190","Eilsleben",null],["150835053275","Harbke",null],["150835053320","Hötensleben",null],["150835053485","Sommersdorf",null],["150835053505","Ummendorf",null],["150835053515","Völpke",null],["150835053535","Wefensleben",null],["150835054025","Am Großen Bruch",null],["150835054035","Ausleben",null],["150835054245","Gröningen, Stadt",null],["150835054355","Kroppenstedt, Stadt",null],["150840130130","Elsteraue",null],["150840235235","Hohenmölsen, Stadt",null],["150840315315","Lützen, Stadt",null],["150840355355","Naumburg (Saale), Stadt",null],["150840490490","Teuchern, Stadt",null],["150840550550","Weißenfels, Stadt",null],["150840590590","Zeitz, Stadt",null],["150845051012","An der Poststraße",null],["150845051015","Bad Bibra, Stadt",null],["150845051125","Eckartsberga, Stadt",null],["150845051132","Finne",null],["150845051133","Finneland",null],["150845051246","Kaiserpfalz",null],["150845051282","Lanitz-Hassel-Tal",null],["150845052115","Droyßig",null],["150845052207","Gutenborn",null],["150845052275","Kretzschau",null],["150845052442","Schnaudertal",null],["150845052565","Wetterzeube",null],["150845053025","Balgstädt",null],["150845053135","Freyburg (Unstrut), Stadt",null],["150845053150","Gleina",null],["150845053170","Goseck",null],["150845053250","Karsdorf",null],["150845053285","Laucha an der Unstrut, Stadt",null],["150845053360","Nebra (Unstrut), Stadt",null],["150845054013","Meineweh",null],["150845054335","Mertendorf",null],["150845054341","Molauer Land",null],["150845054375","Osterfeld, Stadt",null],["150845054445","Schönburg",null],["150845054470","Stößen, Stadt",null],["150845054560","Wethau",null],["150850040040","Ballenstedt, Stadt",null],["150850055055","Blankenburg (Harz), Stadt",null],["150850110110","Falkenstein/Harz, Stadt",null],["150850135135","Halberstadt, Stadt",null],["150850145145","Harzgerode, Stadt",null],["150850185185","Huy",null],["150850190190","Ilsenburg (Harz), Stadt",null],["150850227227","Nordharz",null],["150850228228","Oberharz am Brocken, Stadt",null],["150850230230","Osterwieck, Stadt",null],["150850235235","Quedlinburg, Welterbestadt",null],["150850330330","Thale, Stadt",null],["150850370370","Wernigerode, Stadt",null],["150855051090","Ditfurt",null],["150855051125","Groß Quenstedt",null],["150855051140","Harsleben",null],["150855051160","Hedersleben",null],["150855051285","Schwanebeck, Stadt",null],["150855051287","Selke-Aue",null],["150855051365","Wegeleben, Stadt",null],["150860005005","Biederitz",null],["150860015015","Burg, Stadt",null],["150860035035","Elbe-Parey",null],["150860040040","Genthin, Stadt",null],["150860055055","Gommern, Stadt",null],["150860080080","Jerichow, Stadt",null],["150860140140","Möckern, Stadt",null],["150860145145","Möser",null],["150870015015","Allstedt, Stadt",null],["150870031031","Arnstein, Stadt",null],["150870130130","Eisleben, Lutherstadt",null],["150870165165","Gerbstedt, Stadt",null],["150870220220","Hettstedt, Stadt",null],["150870275275","Mansfeld, Stadt",null],["150870370370","Sangerhausen, Stadt",null],["150870386386","Seegebiet Mansfelder Land",null],["150870412412","Südharz",null],["150875051055","Berga",null],["150875051101","Brücken-Hackpfüffel",null],["150875051125","Edersleben",null],["150875051250","Kelbra (Kyffhäuser), Stadt",null],["150875051440","Wallhausen",null],["150875052010","Ahlsdorf",null],["150875052045","Benndorf",null],["150875052070","Blankenheim",null],["150875052075","Bornstedt",null],["150875052205","Helbra",null],["150875052210","Hergisdorf",null],["150875052260","Klostermansfeld",null],["150875052470","Wimmelburg",null],["150880020020","Bad Dürrenberg, Solestadt",null],["150880025025","Bad Lauchstädt, Goethestadt",null],["150880065065","Braunsbedra, Stadt",null],["150880150150","Kabelsketal",null],["150880195195","Landsberg, Stadt",null],["150880205205","Leuna, Stadt",null],["150880216216","Wettin-Löbejün, Stadt",null],["150880220220","Merseburg, Stadt",null],["150880235235","Mücheln (Geiseltal), Stadt",null],["150880295295","Petersberg",null],["150880305305","Querfurt, Stadt",null],["150880319319","Salzatal",null],["150880330330","Schkopau",null],["150880365365","Teutschenthal",null],["150885051030","Barnstädt",null],["150885051100","Farnstädt",null],["150885051250","Nemsdorf-Göhrendorf",null],["150885051265","Obhausen",null],["150885051340","Schraplau, Stadt",null],["150885051355","Steigra",null],["150890015015","Aschersleben, Stadt",null],["150890026026","Barby, Stadt",null],["150890030030","Bernburg (Saale), Stadt",null],["150890042042","Bördeland",null],["150890055055","Calbe (Saale), Stadt",null],["150890175175","Hecklingen, Stadt",null],["150890195195","Könnern, Stadt",null],["150890235235","Nienburg (Saale), Stadt",null],["150890305305","Schönebeck (Elbe), Stadt",null],["150890307307","Seeland, Stadt",null],["150890310310","Staßfurt, Stadt",null],["150895051041","Bördeaue",null],["150895051043","Börde-Hakel",null],["150895051045","Borne",null],["150895051075","Egeln, Stadt",null],["150895051365","Wolmirsleben",null],["150895052005","Alsleben (Saale), Stadt",null],["150895052130","Giersleben",null],["150895052165","Güsten, Stadt",null],["150895052185","Ilberstedt",null],["150895052245","Plötzkau",null],["150900070070","Bismark (Altmark), Stadt",null],["150900225225","Havelberg, Hansestadt",null],["150900415415","Osterburg (Altmark), Hansestadt",null],["150900535535","Stendal, Hansestadt",null],["150900546546","Tangerhütte, Stadt",null],["150900550550","Tangermünde, Stadt",null],["150905051010","Arneburg, Stadt",null],["150905051135","Eichstedt (Altmark)",null],["150905051180","Goldbeck",null],["150905051220","Hassel",null],["150905051245","Hohenberg-Krusemark",null],["150905051270","Iden",null],["150905051435","Rochau",null],["150905051610","Werben (Elbe), Hansestadt",null],["150905052285","Kamern",null],["150905052310","Klietz",null],["150905052445","Sandau (Elbe), Stadt",null],["150905052485","Schollene",null],["150905052500","Schönhausen (Elbe)",null],["150905052631","Wust-Fischbeck",null],["150905053003","Aland",null],["150905053007","Altmärkische Höhe",null],["150905053008","Altmärkische Wische",null],["150905053520","Seehausen (Altmark), Hansestadt",null],["150905053635","Zehrental",null],["150910010010","Annaburg, Stadt",null],["150910020020","Bad Schmiedeberg, Stadt",null],["150910060060","Coswig (Anhalt), Stadt",null],["150910110110","Gräfenhainichen, Stadt",null],["150910145145","Jessen (Elster), Stadt",null],["150910160160","Kemberg, Stadt",null],["150910241241","Oranienbaum-Wörlitz, Stadt",null],["150910375375","Wittenberg, Lutherstadt",null],["150910391391","Zahna-Elster, Stadt",null],["160510000000","Erfurt, Stadt",null],["160520000000","Gera, Stadt",null],["160530000000","Jena, Stadt",null],["160540000000","Suhl, Stadt",null],["160550000000","Weimar, Stadt",null],["160610045045","Heilbad Heiligenstadt, Stadt",null],["160610074074","Niederorschel",null],["160610115115","Leinefelde-Worbis, Stadt",null],["160610116116","Am Ohmberg",null],["160610117117","Sonnenstein",null],["160610118118","Dingelstädt, Stadt",null],["160615001003","Berlingerode",null],["160615001015","Brehme",null],["160615001026","Ecklingerode",null],["160615001031","Ferna",null],["160615001094","Tastungen",null],["160615001103","Wehnde",null],["160615001114","Teistungen",null],["160615006017","Breitenworbis",null],["160615006019","Buhla",null],["160615006037","Gernrode",null],["160615006044","Haynrode",null],["160615006058","Kirchworbis",null],["160615008001","Arenshausen",null],["160615008014","Bornhagen",null],["160615008021","Burgwalde",null],["160615008032","Freienhagen",null],["160615008033","Fretterode",null],["160615008036","Gerbershausen",null],["160615008048","Hohengandern",null],["160615008057","Kirchgandern",null],["160615008066","Lindewerra",null],["160615008069","Marth",null],["160615008078","Rohrberg",null],["160615008082","Rustenfelde",null],["160615008083","Schachtebich",null],["160615008102","Wahlhausen",null],["160615009012","Bodenrode-Westhausen",null],["160615009034","Geisleden",null],["160615009039","Glasehausen",null],["160615009047","Heuthen",null],["160615009049","Hohes Kreuz",null],["160615009076","Reinholterode",null],["160615009089","Steinbach",null],["160615009107","Wingerode",null],["160615012002","Asbach-Sickenberg",null],["160615012007","Birkenfelde",null],["160615012024","Dietzenrode/Vatterode",null],["160615012028","Eichstruth",null],["160615012065","Lenterode",null],["160615012067","Lutter",null],["160615012068","Mackenrode",null],["160615012077","Röhrig",null],["160615012084","Schönhagen",null],["160615012091","Steinheuterode",null],["160615012096","Thalwenden",null],["160615012097","Uder",null],["160615012111","Wüstheuterode",null],["160615013018","Büttstedt",null],["160615013027","Effelder",null],["160615013041","Großbartloff",null],["160615013063","Küllstedt",null],["160615013101","Wachstedt",null],["160615014023","Dieterode",null],["160615014035","Geismar",null],["160615014056","Kella",null],["160615014062","Krombach",null],["160615014075","Pfaffschwende",null],["160615014085","Schwobfeld",null],["160615014086","Sickerode",null],["160615014098","Volkerode",null],["160615014105","Wiesenfeld",null],["160615014113","Schimberg",null],["160620005005","Ellrich, Stadt",null],["160620041041","Nordhausen, Stadt",null],["160620049049","Sollstedt",null],["160620062062","Hohenstein",null],["160620063063","Werther",null],["160620065065","Harztor",null],["160625053008","Görsbach",null],["160625053054","Urbach",null],["160625053064","Heringen/Helme, Stadt",null],["160625054009","Großlohra",null],["160625054024","Kehmstedt",null],["160625054026","Kleinfurra",null],["160625054033","Lipprechterode",null],["160625054037","Niedergebra",null],["160625054066","Bleicherode, Stadt",null],["160630004004","Barchfeld-Immelborn",null],["160630076076","Treffurt, Stadt",null],["160630078078","Unterbreizbach",null],["160630082082","Vacha, Stadt",null],["160630092092","Wutha-Farnroda",null],["160630097097","Gerstungen",null],["160630098098","Hörselberg-Hainich",null],["160630099099","Bad Liebenstein, Stadt",null],["160630101101","Krayenberggemeinde",null],["160630103103","Werra-Suhl-Tal, Stadt",null],["160630105105","Eisenach, Stadt",null],["160635006006","Berka v. d. Hainich",null],["160635006008","Bischofroda",null],["160635006028","Frankenroda",null],["160635006037","Hallungen",null],["160635006046","Krauthausen",null],["160635006049","Lauterbach",null],["160635006058","Nazza",null],["160635006104","Amt Creuzburg, Stadt",null],["160635051003","Bad Salzungen, Stadt",null],["160635051051","Leimbach",null],["160635056011","Buttlar",null],["160635056032","Geisa, Stadt",null],["160635056033","Gerstengrund",null],["160635056068","Schleid",null],["160635057066","Ruhla, Stadt",null],["160635057071","Seebach",null],["160635059015","Dermbach",null],["160635059023","Empfertshausen",null],["160635059062","Oechsen",null],["160635059084","Weilar",null],["160635059086","Wiesenthal",null],["160640003003","Bad Langensalza, Stadt",null],["160640014014","Dünwald",null],["160640046046","Mühlhausen/Thüringen, Stadt",null],["160640071071","Unstruttal",null],["160640072072","Menteroda",null],["160640073073","Anrode",null],["160645001004","Bad Tennstedt, Stadt",null],["160645001005","Ballhausen",null],["160645001007","Blankenburg",null],["160645001009","Bruchstedt",null],["160645001021","Haussömmern",null],["160645001027","Hornsömmern",null],["160645001033","Kirchheilingen",null],["160645001038","Kutzleben",null],["160645001045","Mittelsömmern",null],["160645001061","Sundhausen",null],["160645001062","Tottleben",null],["160645001064","Urleben",null],["160645051019","Großvargula",null],["160645051022","Herbsleben",null],["160645052055","Rodeberg",null],["160645052074","Südeichsfeld",null],["160645053032","Kammerforst",null],["160645053053","Oppershausen",null],["160645053075","Vogtei",null],["160645054058","Schönstedt",null],["160645054076","Unstrut-Hainich",null],["160645055037","Körner",null],["160645055043","Marolterode",null],["160645055077","Nottertal-Heilinger Höhen, Stadt",null],["160650003003","Bad Frankenhausen/Kyffhäuser, Stadt",null],["160650032032","Helbedündorf",null],["160650067067","Sondershausen, Stadt",null],["160650085085","Kyffhäuserland",null],["160650087087","Roßleben-Wiehe, Stadt",null],["160650089089","Greußen, Stadt",null],["160655002012","Clingen, Stadt",null],["160655002048","Niederbösa",null],["160655002051","Oberbösa",null],["160655002074","Topfstedt",null],["160655002075","Trebra",null],["160655002077","Wasserthaleben",null],["160655002079","Westgreußen",null],["160655052001","Abtsbessingen",null],["160655052005","Bellstedt",null],["160655052014","Ebeleben, Stadt",null],["160655052018","Freienbessingen",null],["160655052038","Holzsußra",null],["160655052058","Rockstedt",null],["160655055008","Borxleben",null],["160655055019","Gehofen",null],["160655055042","Kalbsrieth",null],["160655055046","Mönchpfiffel-Nikolausrieth",null],["160655055056","Reinsdorf",null],["160655055086","Artern, Stadt",null],["160655056016","Etzleben",null],["160655056052","Oberheldrungen",null],["160655056088","An der Schmücke, Stadt",null],["160660023023","Floh-Seligenthal",null],["160660047047","Oberhof, Stadt",null],["160660063063","Schmalkalden, Kurort, Stadt",null],["160660069069","Steinbach-Hallenberg, Kurort, Stadt",null],["160660074074","Brotterode-Trusetal, Stadt",null],["160660092092","Zella-Mehlis, Stadt",null],["160660093093","Rhönblick",null],["160660094094","Grabfeld",null],["160665005012","Birx",null],["160665005019","Erbenhausen",null],["160665005024","Frankenheim/Rhön",null],["160665005052","Oberweid",null],["160665005095","Kaltennordheim, Stadt",null],["160665013025","Friedelshausen",null],["160665013041","Mehmels",null],["160665013064","Schwallungen",null],["160665013086","Wasungen, Stadt",null],["160665014005","Belrieth",null],["160665014015","Christes",null],["160665014016","Dillstädt",null],["160665014017","Einhausen",null],["160665014018","Ellingshausen",null],["160665014038","Kühndorf",null],["160665014039","Leutersdorf",null],["160665014045","Neubrunn",null],["160665014049","Obermaßfeld-Grimmenthal",null],["160665014057","Ritschenhausen",null],["160665014058","Rohr",null],["160665014065","Schwarza",null],["160665014079","Utendorf",null],["160665014081","Vachdorf",null],["160665050042","Meiningen, Stadt",null],["160665050056","Rippershausen",null],["160665050073","Sülzfeld",null],["160665050076","Untermaßfeld",null],["160665051013","Breitungen/Werra",null],["160665051022","Fambach",null],["160665051059","Rosa",null],["160665051061","Roßdorf",null],["160670019019","Friedrichroda, Stadt",null],["160670029029","Gotha, Stadt",null],["160670064064","Bad Tabarz",null],["160670065065","Tambach-Dietharz/Thür. Wald, Stadt",null],["160670072072","Waltershausen, Stadt",null],["160670087087","Nesse-Apfelstädt",null],["160670088088","Hörsel",null],["160675007004","Bienstädt",null],["160675007016","Eschenbergen",null],["160675007022","Friemar",null],["160675007047","Molschleben",null],["160675007052","Nottleben",null],["160675007055","Pferdingsleben",null],["160675007068","Tröchtelborn",null],["160675007071","Tüttleben",null],["160675007082","Zimmernsupra",null],["160675012009","Dachwig",null],["160675012011","Döllstädt",null],["160675012026","Gierstädt",null],["160675012033","Großfahner",null],["160675012067","Tonna",null],["160675050044","Luisenthal",null],["160675050053","Ohrdruf, Stadt",null],["160675052059","Schwabhausen",null],["160675052089","Drei Gleichen",null],["160675053063","Sonneborn",null],["160675053091","Nessetal",null],["160675054013","Emleben",null],["160675054036","Herrenhof",null],["160675054092","Georgenthal",null],["160680034034","Kölleda, Stadt",null],["160680051051","Sömmerda, Stadt",null],["160680058058","Weißensee, Stadt",null],["160680063063","Buttstädt",null],["160685002002","Andisleben",null],["160685002014","Gebesee, Stadt",null],["160685002045","Ringleben",null],["160685002057","Walschleben",null],["160685005005","Büchel",null],["160685005015","Griefstedt",null],["160685005022","Günstedt",null],["160685005043","Riethgen",null],["160685005064","Kindelbrück",null],["160685006019","Großneuhausen",null],["160685006033","Kleinneuhausen",null],["160685006041","Ostramondra",null],["160685006042","Rastenberg, Stadt",null],["160685009013","Gangloffsömmern",null],["160685009025","Haßleben",null],["160685009044","Riethnordhausen",null],["160685009049","Schwerstedt",null],["160685009053","Straußfurt",null],["160685009059","Werningshausen",null],["160685009062","Wundersleben",null],["160685012001","Alperstedt",null],["160685012007","Eckstedt",null],["160685012017","Großmölsen",null],["160685012021","Großrudestedt",null],["160685012032","Kleinmölsen",null],["160685012036","Markvippach",null],["160685012037","Nöda",null],["160685012039","Ollendorf",null],["160685012048","Schloßvippach",null],["160685012052","Sprötau",null],["160685012055","Udestedt",null],["160685012056","Vogelsberg",null],["160685050009","Elxleben",null],["160685050061","Witterda",null],["160690012012","Eisfeld, Stadt",null],["160690024024","Hildburghausen, Stadt",null],["160690042042","Schleusegrund",null],["160690043043","Schleusingen, Stadt",null],["160690053053","Veilsdorf",null],["160690061061","Masserberg",null],["160690062062","Römhild, Stadt",null],["160695002001","Ahlstädt",null],["160695002003","Beinerstadt",null],["160695002004","Bischofrod",null],["160695002008","Dingsleben",null],["160695002009","Ehrenberg",null],["160695002011","Eichenberg",null],["160695002016","Grimmelshausen",null],["160695002017","Grub",null],["160695002021","Henfstädt",null],["160695002025","Kloster Veßra",null],["160695002026","Lengfeld",null],["160695002028","Marisfeld",null],["160695002035","Oberstadt",null],["160695002037","Reurieth",null],["160695002044","Schmeheim",null],["160695002047","St.Bernhard",null],["160695002051","Themar, Stadt",null],["160695004041","Schlechtsart",null],["160695004046","Schweickershausen",null],["160695004049","Straufhain",null],["160695004052","Ummerstadt, Stadt",null],["160695004056","Westhausen",null],["160695004063","Heldburg, Stadt",null],["160695051006","Brünn/Thür.",null],["160695051058","Auengrund",null],["160700004004","Arnstadt, Stadt",null],["160700028028","Amt Wachsenburg",null],["160700029029","Ilmenau, Stadt",null],["160700048048","Stadtilm, Stadt",null],["160700057057","Geratal",null],["160700058058","Großbreitenbach, Stadt",null],["160705002011","Elgersburg",null],["160705002034","Martinroda",null],["160705002043","Plaue, Stadt",null],["160705009001","Alkersleben",null],["160705009006","Bösleben-Wüllersleben",null],["160705009008","Dornheim",null],["160705009012","Elleben",null],["160705009013","Elxleben",null],["160705009041","Osthausen-Wülfershausen",null],["160705009054","Witzleben",null],["160710001001","Apolda, Stadt",null],["160710003003","Bad Berka, Stadt",null],["160710008008","Blankenhain, Stadt",null],["160710101101","Ilmtal-Weinstraße",null],["160710103103","Grammetal",null],["160715007032","Hohenfelden",null],["160715007043","Klettbach",null],["160715007046","Kranichfeld, Stadt",null],["160715007059","Nauendorf",null],["160715007079","Rittersdorf",null],["160715007087","Tonndorf",null],["160715008009","Buchfart",null],["160715008013","Döbritschen",null],["160715008019","Frankendorf",null],["160715008025","Großschwabhausen",null],["160715008027","Hammerstedt",null],["160715008031","Hetschburg",null],["160715008037","Kapellendorf",null],["160715008038","Kiliansroda",null],["160715008042","Kleinschwabhausen",null],["160715008049","Lehnstedt",null],["160715008053","Magdala, Stadt",null],["160715008055","Mechelroda",null],["160715008056","Mellingen",null],["160715008071","Oettern",null],["160715008089","Umpferstedt",null],["160715008093","Vollersroda",null],["160715008095","Wiegendorf",null],["160715051004","Bad Sulza, Stadt",null],["160715051015","Eberstedt",null],["160715051022","Großheringen",null],["160715051064","Niedertrebra",null],["160715051069","Obertrebra",null],["160715051077","Rannstedt",null],["160715051083","Schmiedehausen",null],["160715053005","Ballstedt",null],["160715053017","Ettersburg",null],["160715053061","Neumark, Stadt",null],["160715053102","Am Ettersberg",null],["160720011011","Lauscha, Stadt",null],["160720015015","Schalkau, Stadt",null],["160720018018","Sonneberg, Stadt",null],["160720019019","Steinach, Stadt",null],["160720023023","Frankenblick",null],["160720024024","Föritztal",null],["160725051006","Goldisthal",null],["160725051013","Neuhaus am Rennweg, Stadt",null],["160730005005","Bad Blankenburg, Stadt",null],["160730076076","Rudolstadt, Stadt",null],["160730077077","Saalfeld/Saale, Stadt",null],["160730106106","Leutenberg, Stadt",null],["160730109109","Uhlstädt-Kirchhasel",null],["160730111111","Unterwellenborn",null],["160735005028","Gräfenthal, Stadt",null],["160735005046","Lehesten, Stadt",null],["160735005067","Probstzella",null],["160735012013","Cursdorf",null],["160735012014","Deesbach",null],["160735012017","Döschnitz",null],["160735012037","Katzhütte",null],["160735012055","Meura",null],["160735012074","Rohrbach",null],["160735012082","Schwarzburg",null],["160735012084","Sitzendorf",null],["160735012094","Unterweißbach",null],["160735012113","Schwarzatal, Stadt",null],["160735051002","Altenbeuthen",null],["160735051035","Hohenwarte",null],["160735051038","Kaulsdorf",null],["160735051107","Drognitz",null],["160735054001","Allendorf",null],["160735054006","Bechstedt",null],["160735054112","Königsee, Stadt",null],["160740044044","Kahla, Stadt",null],["160745005012","Crossen an der Elster",null],["160745005038","Hartmannsdorf",null],["160745005039","Heideland",null],["160745005072","Rauda",null],["160745005092","Silbitz",null],["160745005106","Walpernhain",null],["160745005116","Schkölen, Stadt",null],["160745007007","Bremsnitz",null],["160745007017","Eineborn",null],["160745007022","Geisenhain",null],["160745007024","Gneus",null],["160745007029","Großbockedra",null],["160745007045","Karlsdorf",null],["160745007046","Kleinbockedra",null],["160745007047","Kleinebersdorf",null],["160745007053","Lippersdorf-Erdmannsdorf",null],["160745007056","Meusebach",null],["160745007064","Oberbodnitz",null],["160745007066","Ottendorf",null],["160745007071","Rattelsdorf",null],["160745007074","Rausdorf",null],["160745007077","Renthendorf",null],["160745007097","Tautendorf",null],["160745007101","Tissa",null],["160745007102","Trockenborn-Wolfersdorf",null],["160745007103","Tröbnitz",null],["160745007104","Unterbodnitz",null],["160745007107","Waltersdorf",null],["160745007108","Weißbach",null],["160745011002","Altenberga",null],["160745011004","Bibra",null],["160745011008","Bucha",null],["160745011016","Eichenberg",null],["160745011021","Freienorla",null],["160745011031","Großeutersdorf",null],["160745011033","Großpürschütz",null],["160745011034","Gumperda",null],["160745011042","Hummelshain",null],["160745011048","Kleineutersdorf",null],["160745011049","Laasdorf",null],["160745011052","Lindig",null],["160745011057","Milda",null],["160745011065","Orlamünde, Stadt",null],["160745011076","Reinstädt",null],["160745011079","Rothenstein",null],["160745011087","Schöps",null],["160745011089","Seitenroda",null],["160745011095","Sulza",null],["160745011114","Zöllnitz",null],["160745014041","Hermsdorf, Stadt",null],["160745014059","Mörsdorf",null],["160745014075","Reichenbach",null],["160745014084","Schleifreisen",null],["160745014093","St.Gangloff",null],["160745015011","Dornburg-Camburg, Stadt",null],["160745015019","Frauenprießnitz",null],["160745015026","Golmsdorf",null],["160745015032","Großlöbichau",null],["160745015036","Hainichen",null],["160745015043","Jenalöbnitz",null],["160745015051","Lehesten",null],["160745015054","Löberschütz",null],["160745015063","Neuengönna",null],["160745015096","Tautenburg",null],["160745015099","Thierschneck",null],["160745015112","Wichmar",null],["160745015113","Zimmern",null],["160745050058","Möckern",null],["160745050081","Ruttersdorf-Lotschen",null],["160745050094","Stadtroda, Stadt",null],["160745051009","Bürgel, Stadt",null],["160745051028","Graitschen b. Bürgel",null],["160745051061","Nausnitz",null],["160745051068","Poxdorf",null],["160745052018","Eisenberg, Stadt",null],["160745052025","Gösen",null],["160745052037","Hainspitz",null],["160745052055","Mertendorf",null],["160745052067","Petersberg",null],["160745052073","Rauschwitz",null],["160745053001","Albersdorf",null],["160745053003","Bad Klosterlausnitz",null],["160745053005","Bobeck",null],["160745053082","Scheiditz",null],["160745053085","Schlöben",null],["160745053086","Schöngleina",null],["160745053091","Serba",null],["160745053098","Tautenhain",null],["160745053105","Waldeck",null],["160745053109","Weißenborn",null],["160750046046","Hirschberg, Stadt",null],["160750062062","Bad Lobenstein, Stadt",null],["160750085085","Pößneck, Stadt",null],["160750098098","Schleiz, Stadt",null],["160750131131","Gefell, Stadt",null],["160750132132","Tanna, Stadt",null],["160750133133","Wurzbach, Stadt",null],["160750134134","Remptendorf",null],["160750135135","Saalburg-Ebersdorf, Stadt",null],["160750136136","Rosenthal am Rennsteig",null],["160755004014","Dittersdorf",null],["160755004033","Görkwitz",null],["160755004034","Göschitz",null],["160755004048","Kirschkau",null],["160755004063","Löhma",null],["160755004068","Moßbach",null],["160755004072","Neundorf (bei Schleiz)",null],["160755004076","Oettersdorf",null],["160755004083","Plothen",null],["160755004084","Pörmitz",null],["160755004109","Tegau",null],["160755004119","Volkmannsdorf",null],["160755005006","Bodelwitz",null],["160755005016","Döbritz",null],["160755005031","Gertewitz",null],["160755005039","Grobengereuth",null],["160755005054","Langenorla",null],["160755005056","Lausnitz b. Neustadt an der Orla",null],["160755005074","Nimritz",null],["160755005075","Oberoppurg",null],["160755005077","Oppurg",null],["160755005087","Quaschwitz",null],["160755005105","Solkwitz",null],["160755005121","Weira",null],["160755005124","Wernburg",null],["160755011019","Dreitzsch",null],["160755011029","Geroda",null],["160755011057","Lemnitz",null],["160755011065","Miesitz",null],["160755011066","Mittelpöllnitz",null],["160755011093","Rosendorf",null],["160755011099","Schmieritz",null],["160755011114","Tömmelsdorf",null],["160755011116","Triptis, Stadt",null],["160755013023","Eßbach",null],["160755013035","Gössitz",null],["160755013047","Keila",null],["160755013069","Moxa",null],["160755013079","Paska",null],["160755013081","Peuschen",null],["160755013088","Ranis, Stadt",null],["160755013101","Schmorda",null],["160755013102","Schöndorf",null],["160755013103","Seisla",null],["160755013125","Wilhelmsdorf",null],["160755013127","Ziegenrück, Stadt",null],["160755013129","Krölpa",null],["160755050051","Kospoda",null],["160755050073","Neustadt an der Orla, Stadt",null],["160760004004","Berga/Elster, Stadt",null],["160760022022","Greiz, Stadt",null],["160760061061","Ronneburg, Stadt",null],["160760088088","Harth-Pöllnitz",null],["160760089089","Kraftsdorf",null],["160760092092","Auma-Weidatal, Stadt",null],["160760093093","Mohlsdorf-Teichwolframsdorf",null],["160765004009","Braunichswalde",null],["160765004017","Endschütz",null],["160765004019","Gauern",null],["160765004027","Hilbersdorf",null],["160765004034","Kauern",null],["160765004043","Linda b. Weida",null],["160765004055","Paitzdorf",null],["160765004062","Rückersdorf",null],["160765004069","Seelingstädt",null],["160765004074","Teichwitz",null],["160765004084","Wünschendorf/Elster",null],["160765006007","Bocka",null],["160765006033","Hundhaupten",null],["160765006042","Lederhose",null],["160765006044","Lindenkreuz",null],["160765006049","Münchenbernsdorf, Stadt",null],["160765006064","Saara",null],["160765006068","Schwarzbach",null],["160765006086","Zedlitz",null],["160765008006","Bethenhausen",null],["160765008008","Brahmenau",null],["160765008023","Großenstein",null],["160765008028","Hirschfeld",null],["160765008036","Korbußen",null],["160765008058","Pölzig",null],["160765008059","Reichstädt",null],["160765008067","Schwaara",null],["160765051003","Bad Köstritz, Stadt",null],["160765051012","Caaschwitz",null],["160765051026","Hartmannsdorf",null],["160765053014","Crimla",null],["160765053079","Weida, Stadt",null],["160765054041","Langenwolschendorf",null],["160765054081","Weißendorf",null],["160765054087","Zeulenroda-Triebes, Stadt",null],["160765056029","Hohenleuben, Stadt",null],["160765056038","Kühdorf",null],["160765056039","Langenwetzendorf",null],["160770001001","Altenburg, Stadt",null],["160770028028","Lucka, Stadt",null],["160770032032","Meuselwitz, Stadt",null],["160775004005","Fockendorf",null],["160775004007","Gerstenberg",null],["160775004015","Haselbach",null],["160775004048","Treben",null],["160775004052","Windischleuba",null],["160775005008","Göhren",null],["160775005009","Göllnitz",null],["160775005022","Kriebitzsch",null],["160775005027","Lödla",null],["160775005031","Mehna",null],["160775005034","Monstab",null],["160775005042","Rositz",null],["160775005044","Starkenberg",null],["160775009016","Heukewalde",null],["160775009018","Jonaswalde",null],["160775009026","Löbichau",null],["160775009041","Posterstein",null],["160775009047","Thonhausen",null],["160775009049","Vollmershain",null],["160775050012","Gößnitz, Stadt",null],["160775050017","Heyersdorf",null],["160775050039","Ponitz",null],["160775051011","Göpfersdorf",null],["160775051023","Langenleuba-Niederhain",null],["160775051036","Nobitz",null],["160775052003","Dobitschen",null],["160775052043","Schmölln, Stadt",null]]} \ No newline at end of file +{ + "metadaten": { + "kennung": "urn:de:bund:destatis:bevoelkerungsstatistik:schluessel:rs_2021-07-31", + "kennungInhalt": "urn:de:bund:destatis:bevoelkerungsstatistik:schluessel:rs", + "version": "2021-07-31", + "nameKurz": "Regionalschlüssel", + "nameLang": "Gemeinden, dargestellt durch den Amtlichen Regionalschlüssel (ARS) des Statistischen Bundesamtes", + "nameTechnisch": "Regionalschluessel", + "herausgebernameLang": "Statistisches Bundesamt, Wiesbaden", + "herausgebernameKurz": "Destatis", + "beschreibung": "Diese Codeliste stellt alle Gemeinden Deutschlands durch den Amtlichen Regionalschlüssel (ARS) dar, wie im Gemeindeverzeichnis des Statistischen Bundesamtes enthalten. Darüber hinaus enthält die Codeliste für die Stadtstaaten Hamburg, Bremen und Berlin Einträge für Stadt-/Ortsteile bzw. Stadtbezirke. Diese Einträge sind mit einem entsprechenden Hinweis versehen.", + "versionBeschreibung": null, + "aenderungZurVorversion": "Mehrere Aenderungen", + "handbuchVersion": "1.0", + "xoevHandbuch": false, + "gueltigAb": 1627682400000, + "bezugsorte": [] + }, + "spalten": [ + { + "spaltennameLang": "SCHLUESSEL", + "spaltennameTechnisch": "SCHLUESSEL", + "datentyp": "string", + "codeSpalte": true, + "verwendung": { "code": "REQUIRED" }, + "empfohleneCodeSpalte": true + }, + { + "spaltennameLang": "Bezeichnung", + "spaltennameTechnisch": "Bezeichnung", + "datentyp": "string", + "codeSpalte": false, + "verwendung": { "code": "REQUIRED" }, + "empfohleneCodeSpalte": false + }, + { + "spaltennameLang": "Hinweis", + "spaltennameTechnisch": "Hinweis", + "datentyp": "string", + "codeSpalte": false, + "verwendung": { "code": "OPTIONAL" }, + "empfohleneCodeSpalte": false + } + ], + "daten": [ + ["010010000000", "Flensburg, Stadt", null], + ["010020000000", "Kiel, Landeshauptstadt", null], + ["010030000000", "Lübeck, Hansestadt", null], + ["010040000000", "Neumünster, Stadt", null], + ["010510011011", "Brunsbüttel, Stadt", null], + ["010510044044", "Heide, Stadt", null], + ["010515163003", "Averlak", null], + ["010515163010", "Brickeln", null], + ["010515163012", "Buchholz", null], + ["010515163016", "Burg (Dithmarschen)", null], + ["010515163022", "Dingen", null], + ["010515163024", "Eddelak", null], + ["010515163026", "Eggstedt", null], + ["010515163032", "Frestedt", null], + ["010515163037", "Großenrade", null], + ["010515163051", "Hochdonn", null], + ["010515163064", "Kuden", null], + ["010515163089", "Quickborn", null], + ["010515163097", "Sankt Michaelisdonn", null], + ["010515163110", "Süderhastedt", null], + ["010515166021", "Diekhusen-Fahrstedt", null], + ["010515166034", "Friedrichskoog", null], + ["010515166046", "Helse", null], + ["010515166057", "Kaiser-Wilhelm-Koog", null], + ["010515166062", "Kronprinzenkoog", null], + ["010515166072", "Marne, Stadt", null], + ["010515166073", "Marnerdeich", null], + ["010515166076", "Neufeld", null], + ["010515166077", "Neufelderkoog", null], + ["010515166090", "Ramhusen", null], + ["010515166103", "Schmedeswurth", null], + ["010515166118", "Trennewurth", null], + ["010515166119", "Volsemenhusen", null], + ["010515169005", "Barkenholm", null], + ["010515169008", "Bergewöhrden", null], + ["010515169019", "Dellstedt", null], + ["010515169020", "Delve", null], + ["010515169023", "Dörpling", null], + ["010515169030", "Fedderingen", null], + ["010515169035", "Gaushorn", null], + ["010515169036", "Glüsing", null], + ["010515169038", "Groven", null], + ["010515169047", "Hemme", null], + ["010515169049", "Hennstedt", null], + ["010515169052", "Hövede", null], + ["010515169053", "Hollingstedt", null], + ["010515169058", "Karolinenkoog", null], + ["010515169060", "Kleve", null], + ["010515169061", "Krempel", null], + ["010515169065", "Lehe", null], + ["010515169068", "Linden", null], + ["010515169071", "Lunden", null], + ["010515169080", "Norderheistedt", null], + ["010515169088", "Pahlen", null], + ["010515169092", "Rehm-Flehde-Bargen", null], + ["010515169096", "Sankt Annen", null], + ["010515169100", "Schalkholz", null], + ["010515169102", "Schlichting", null], + ["010515169114", "Tellingstedt", null], + ["010515169117", "Tielenhemme", null], + ["010515169120", "Wallen", null], + ["010515169125", "Welmbüttel", null], + ["010515169131", "Westerborstel", null], + ["010515169133", "Wiemerstedt", null], + ["010515169136", "Wrohm", null], + ["010515169139", "Süderdorf", null], + ["010515169141", "Süderheistedt", null], + ["010515172048", "Hemmingstedt", null], + ["010515172067", "Lieth", null], + ["010515172069", "Lohe-Rickelshof", null], + ["010515172075", "Neuenkirchen", null], + ["010515172081", "Norderwöhrden", null], + ["010515172082", "Nordhastedt", null], + ["010515172087", "Ostrohe", null], + ["010515172107", "Stelle-Wittenwurth", null], + ["010515172113", "Wöhrden", null], + ["010515172122", "Weddingstedt", null], + ["010515172130", "Wesseln", null], + ["010515175001", "Albersdorf", null], + ["010515175002", "Arkebek", null], + ["010515175004", "Bargenstedt", null], + ["010515175006", "Barlt", null], + ["010515175015", "Bunsoh", null], + ["010515175017", "Busenwurth", null], + ["010515175027", "Elpersbüttel", null], + ["010515175028", "Epenwöhrden", null], + ["010515175039", "Gudendorf", null], + ["010515175054", "Immenstedt", null], + ["010515175063", "Krumstedt", null], + ["010515175074", "Meldorf, Stadt", null], + ["010515175078", "Nindorf", null], + ["010515175083", "Odderade", null], + ["010515175085", "Offenbüttel", null], + ["010515175086", "Osterrade", null], + ["010515175098", "Sarzbüttel", null], + ["010515175099", "Schafstedt", null], + ["010515175104", "Schrum", null], + ["010515175126", "Wennbüttel", null], + ["010515175134", "Windbergen", null], + ["010515175135", "Wolmersdorf", null], + ["010515175137", "Nordermeldorf", null], + ["010515175138", "Tensbüttel-Röst", null], + ["010515178013", "Büsum", null], + ["010515178014", "Büsumer Deichhausen", null], + ["010515178033", "Friedrichsgabekoog", null], + ["010515178043", "Hedwigenkoog", null], + ["010515178045", "Hellschen-Heringsand-Unterschaar", null], + ["010515178050", "Hillgroven", null], + ["010515178079", "Norddeich", null], + ["010515178084", "Oesterdeichstrich", null], + ["010515178093", "Reinsbüttel", null], + ["010515178105", "Schülp", null], + ["010515178108", "Strübbel", null], + ["010515178109", "Süderdeich", null], + ["010515178121", "Warwerort", null], + ["010515178127", "Wesselburen, Stadt", null], + ["010515178128", "Wesselburener Deichhausen", null], + ["010515178129", "Wesselburenerkoog", null], + ["010515178132", "Westerdeichstrich", null], + ["010515178140", "Oesterwurth", null], + ["010530032032", "Geesthacht, Stadt", null], + ["010530083083", "Lauenburg/ Elbe, Stadt", null], + ["010530090090", "Mölln, Stadt", null], + ["010530100100", "Ratzeburg, Stadt", null], + ["010530116116", "Schwarzenbek, Stadt", null], + ["010530129129", "Wentorf bei Hamburg", null], + ["010535308008", "Behlendorf", null], + ["010535308009", "Berkenthin", null], + ["010535308011", "Bliestorf", null], + ["010535308024", "Düchelsdorf", null], + ["010535308034", "Göldenitz", null], + ["010535308061", "Kastorf", null], + ["010535308067", "Klempau", null], + ["010535308075", "Krummesse", null], + ["010535308094", "Niendorf bei Berkenthin", null], + ["010535308103", "Rondeshagen", null], + ["010535308120", "Sierksrade", null], + ["010535313002", "Alt-Mölln", null], + ["010535313005", "Bälau", null], + ["010535313013", "Borstorf", null], + ["010535313014", "Breitenfelde", null], + ["010535313037", "Grambek", null], + ["010535313056", "Hornbek", null], + ["010535313084", "Lehmrade", null], + ["010535313095", "Niendorf/ Stecknitz", null], + ["010535313113", "Schretstaken", null], + ["010535313125", "Talkau", null], + ["010535313134", "Woltersdorf", null], + ["010535318010", "Besenthal", null], + ["010535318015", "Bröthen", null], + ["010535318020", "Büchen", null], + ["010535318029", "Fitzen", null], + ["010535318035", "Göttin", null], + ["010535318046", "Gudow", null], + ["010535318048", "Güster", null], + ["010535318064", "Klein Pampau", null], + ["010535318080", "Langenlehsten", null], + ["010535318092", "Müssen", null], + ["010535318104", "Roseburg", null], + ["010535318115", "Schulendorf", null], + ["010535318119", "Siebeneichen", null], + ["010535318126", "Tramm", null], + ["010535318132", "Witzeeze", null], + ["010535323003", "Aumühle", null], + ["010535323012", "Börnsen", null], + ["010535323023", "Dassendorf", null], + ["010535323028", "Escheburg", null], + ["010535323050", "Hamwarde", null], + ["010535323053", "Hohenhorn", null], + ["010535323072", "Kröppelshagen-Fahrendorf", null], + ["010535323131", "Wiershop", null], + ["010535323133", "Wohltorf", null], + ["010535323135", "Worth", null], + ["010535343006", "Basedow", null], + ["010535343019", "Buchhorst", null], + ["010535343022", "Dalldorf", null], + ["010535343058", "Juliusburg", null], + ["010535343073", "Krüzen", null], + ["010535343074", "Krukow", null], + ["010535343082", "Lanze", null], + ["010535343087", "Lütau", null], + ["010535343111", "Schnakenbek", null], + ["010535343128", "Wangelau", null], + ["010535358001", "Albsfelde", null], + ["010535358004", "Bäk", null], + ["010535358016", "Brunsmark", null], + ["010535358018", "Buchholz", null], + ["010535358026", "Einhaus", null], + ["010535358030", "Fredeburg", null], + ["010535358033", "Giesensdorf", null], + ["010535358040", "Groß Disnack", null], + ["010535358041", "Groß Grönau", null], + ["010535358043", "Groß Sarau", null], + ["010535358051", "Harmsdorf", null], + ["010535358054", "Hollenbek", null], + ["010535358057", "Horst", null], + ["010535358062", "Kittlitz", null], + ["010535358066", "Klein Zecher", null], + ["010535358078", "Kulpin", null], + ["010535358088", "Mechow", null], + ["010535358093", "Mustin", null], + ["010535358098", "Pogeez", null], + ["010535358102", "Römnitz", null], + ["010535358107", "Salem", null], + ["010535358110", "Schmilau", null], + ["010535358117", "Seedorf", null], + ["010535358123", "Sterley", null], + ["010535358136", "Ziethen", null], + ["010535373007", "Basthorst", null], + ["010535373017", "Brunstorf", null], + ["010535373021", "Dahmker", null], + ["010535373027", "Elmenhorst", null], + ["010535373031", "Fuhlenhagen", null], + ["010535373036", "Grabau", null], + ["010535373042", "Groß Pampau", null], + ["010535373045", "Grove", null], + ["010535373047", "Gülzow", null], + ["010535373049", "Hamfelde", null], + ["010535373052", "Havekost", null], + ["010535373059", "Kankelau", null], + ["010535373060", "Kasseburg", null], + ["010535373070", "Köthel", null], + ["010535373071", "Kollow", null], + ["010535373076", "Kuddewörde", null], + ["010535373089", "Möhnsen", null], + ["010535373091", "Mühlenrade", null], + ["010535373106", "Sahms", null], + ["010535391025", "Duvensee", null], + ["010535391038", "Grinau", null], + ["010535391039", "Groß Boden", null], + ["010535391044", "Groß Schenkenberg", null], + ["010535391068", "Klinkrade", null], + ["010535391069", "Koberg", null], + ["010535391077", "Kühsen", null], + ["010535391079", "Labenz", null], + ["010535391081", "Lankau", null], + ["010535391085", "Linau", null], + ["010535391086", "Lüchow", null], + ["010535391096", "Nusse", null], + ["010535391097", "Panten", null], + ["010535391099", "Poggensee", null], + ["010535391101", "Ritzerau", null], + ["010535391108", "Sandesneben", null], + ["010535391109", "Schiphorst", null], + ["010535391112", "Schönberg", null], + ["010535391114", "Schürensöhlen", null], + ["010535391118", "Siebenbäumen", null], + ["010535391121", "Sirksfelde", null], + ["010535391122", "Steinhorst", null], + ["010535391124", "Stubben", null], + ["010535391127", "Walksfelde", null], + ["010535391130", "Wentorf (Amt Sandesneben)", null], + ["010539105105", "Sachsenwald (Forstgutsbez.),gemfr.Geb.", null], + ["010540033033", "Friedrichstadt, Stadt", null], + ["010540056056", "Husum, Stadt", null], + ["010540108108", "Reußenköge", null], + ["010540138138", "Tönning, Stadt", null], + ["010540168168", "Sylt", null], + ["010545417035", "Garding, Kirchspiel", null], + ["010545417036", "Garding, Stadt", null], + ["010545417040", "Grothusenkoog", null], + ["010545417063", "Katharinenheerd", null], + ["010545417072", "Kotzenbüll", null], + ["010545417090", "Norderfriedrichskoog", null], + ["010545417095", "Oldenswort", null], + ["010545417100", "Osterhever", null], + ["010545417104", "Poppenbüll", null], + ["010545417113", "Sankt Peter-Ording", null], + ["010545417134", "Tating", null], + ["010545417135", "Tetenbüll", null], + ["010545417140", "Tümlauer Koog", null], + ["010545417145", "Vollerwiek", null], + ["010545417148", "Welt", null], + ["010545417150", "Westerhever", null], + ["010545439046", "Hörnum (Sylt)", null], + ["010545439061", "Kampen (Sylt)", null], + ["010545439078", "List auf Sylt", null], + ["010545439149", "Wenningstedt-Braderup (Sylt)", null], + ["010545453003", "Ahrenviöl", null], + ["010545453004", "Ahrenviölfeld", null], + ["010545453011", "Behrendorf", null], + ["010545453013", "Bondelum", null], + ["010545453041", "Haselund", null], + ["010545453057", "Immenstedt", null], + ["010545453079", "Löwenstedt", null], + ["010545453092", "Norstedt", null], + ["010545453101", "Oster-Ohrstedt", null], + ["010545453118", "Schwesing", null], + ["010545453123", "Sollwitt", null], + ["010545453144", "Viöl", null], + ["010545453152", "Wester-Ohrstedt", null], + ["010545459039", "Gröde", null], + ["010545459050", "Hallig Hooge", null], + ["010545459074", "Langeneß", null], + ["010545459103", "Pellworm", null], + ["010545488005", "Alkersum", null], + ["010545488015", "Borgsum", null], + ["010545488025", "Dunsum", null], + ["010545488083", "Midlum", null], + ["010545488085", "Nebel", null], + ["010545488087", "Nieblum", null], + ["010545488089", "Norddorf auf Amrum", null], + ["010545488094", "Oevenum", null], + ["010545488098", "Oldsum", null], + ["010545488129", "Süderende", null], + ["010545488143", "Utersum", null], + ["010545488158", "Witsum", null], + ["010545488160", "Wittdün auf Amrum", null], + ["010545488163", "Wrixum", null], + ["010545488164", "Wyk auf Föhr, Stadt", null], + ["010545489001", "Achtrup", null], + ["010545489009", "Aventoft", null], + ["010545489016", "Bosbüll", null], + ["010545489017", "Braderup", null], + ["010545489018", "Bramstedtlund", null], + ["010545489022", "Dagebüll", null], + ["010545489027", "Ellhöft", null], + ["010545489034", "Friedrich-Wilhelm-Lübke-Koog", null], + ["010545489048", "Holm", null], + ["010545489055", "Humptrup", null], + ["010545489062", "Karlum", null], + ["010545489065", "Klanxbüll", null], + ["010545489068", "Klixbüll", null], + ["010545489073", "Ladelund", null], + ["010545489076", "Leck", null], + ["010545489077", "Lexgaard", null], + ["010545489086", "Neukirchen", null], + ["010545489088", "Niebüll, Stadt", null], + ["010545489109", "Risum-Lindholm", null], + ["010545489110", "Rodenäs", null], + ["010545489124", "Sprakebüll", null], + ["010545489125", "Stadum", null], + ["010545489126", "Stedesand", null], + ["010545489131", "Süderlügum", null], + ["010545489136", "Tinningstedt", null], + ["010545489142", "Uphusum", null], + ["010545489154", "Westre", null], + ["010545489165", "Galmsbüll", null], + ["010545489166", "Emmelsbüll-Horsbüll", null], + ["010545489167", "Enge-Sande", null], + ["010545492007", "Arlewatt", null], + ["010545492023", "Drage", null], + ["010545492026", "Elisabeth-Sophien-Koog", null], + ["010545492032", "Fresendelf", null], + ["010545492042", "Hattstedt", null], + ["010545492043", "Hattstedtermarsch", null], + ["010545492052", "Horstedt", null], + ["010545492054", "Hude", null], + ["010545492070", "Koldenbüttel", null], + ["010545492084", "Mildstedt", null], + ["010545492091", "Nordstrand", null], + ["010545492096", "Oldersbek", null], + ["010545492097", "Olderup", null], + ["010545492099", "Ostenfeld (Husum)", null], + ["010545492105", "Ramstedt", null], + ["010545492106", "Rantrum", null], + ["010545492116", "Schwabstedt", null], + ["010545492119", "Seeth", null], + ["010545492120", "Simonsberg", null], + ["010545492130", "Süderhöft", null], + ["010545492132", "Südermarsch", null], + ["010545492141", "Uelvesbüll", null], + ["010545492156", "Winnert", null], + ["010545492157", "Wisch", null], + ["010545492159", "Wittbek", null], + ["010545492161", "Witzwort", null], + ["010545492162", "Wobbenbüll", null], + ["010545494002", "Ahrenshöft", null], + ["010545494006", "Almdorf", null], + ["010545494010", "Bargum", null], + ["010545494012", "Bohmstedt", null], + ["010545494014", "Bordelum", null], + ["010545494019", "Bredstedt, Stadt", null], + ["010545494020", "Breklum", null], + ["010545494024", "Drelsdorf", null], + ["010545494037", "Goldebek", null], + ["010545494038", "Goldelund", null], + ["010545494045", "Högel", null], + ["010545494059", "Joldelund", null], + ["010545494071", "Kolkerheide", null], + ["010545494075", "Langenhorn", null], + ["010545494080", "Lütjenholm", null], + ["010545494093", "Ockholm", null], + ["010545494121", "Sönnebüll", null], + ["010545494128", "Struckum", null], + ["010545494146", "Vollstedt", null], + ["010550001001", "Ahrensbök", null], + ["010550004004", "Bad Schwartau, Stadt", null], + ["010550007007", "Bosau", null], + ["010550010010", "Dahme", null], + ["010550012012", "Eutin, Stadt", null], + ["010550016016", "Grömitz", null], + ["010550018018", "Grube", null], + ["010550021021", "Heiligenhafen, Stadt", null], + ["010550025025", "Kellenhusen (Ostsee)", null], + ["010550028028", "Malente", null], + ["010550032032", "Neustadt in Holstein, Stadt", null], + ["010550033033", "Oldenburg in Holstein, Stadt", null], + ["010550035035", "Ratekau", null], + ["010550040040", "Stockelsdorf", null], + ["010550041041", "Süsel", null], + ["010550042042", "Timmendorfer Strand", null], + ["010550044044", "Scharbeutz", null], + ["010550046046", "Fehmarn, Stadt", null], + ["010555543014", "Göhl", null], + ["010555543015", "Gremersdorf", null], + ["010555543017", "Großenbrode", null], + ["010555543022", "Heringsdorf", null], + ["010555543031", "Neukirchen", null], + ["010555543043", "Wangels", null], + ["010555546006", "Beschendorf", null], + ["010555546011", "Damlos", null], + ["010555546020", "Harmsdorf", null], + ["010555546023", "Kabelhorst", null], + ["010555546027", "Lensahn", null], + ["010555546029", "Manhagen", null], + ["010555546036", "Riepsdorf", null], + ["010555591002", "Altenkrempe", null], + ["010555591024", "Kasseedorf", null], + ["010555591037", "Schashagen", null], + ["010555591038", "Schönwalde am Bungsberg", null], + ["010555591039", "Sierksdorf", null], + ["010560002002", "Barmstedt, Stadt", null], + ["010560005005", "Bönningstedt", null], + ["010560015015", "Elmshorn, Stadt", null], + ["010560018018", "Halstenbek", null], + ["010560021021", "Hasloh", null], + ["010560025025", "Helgoland", null], + ["010560039039", "Pinneberg, Stadt", null], + ["010560041041", "Quickborn, Stadt", null], + ["010560043043", "Rellingen", null], + ["010560044044", "Schenefeld, Stadt", null], + ["010560048048", "Tornesch, Stadt", null], + ["010560049049", "Uetersen, Stadt", null], + ["010560050050", "Wedel, Stadt", null], + ["010565616029", "Klein Nordende", null], + ["010565616030", "Klein Offenseth-Sparrieshoop", null], + ["010565616031", "Kölln-Reisiek", null], + ["010565616033", "Seester", null], + ["010565616042", "Raa-Besenbek", null], + ["010565616045", "Seestermühe", null], + ["010565616046", "Seeth-Ekholt", null], + ["010565636006", "Bokel", null], + ["010565636010", "Brande-Hörnerkirchen", null], + ["010565636038", "Osterhorn", null], + ["010565636051", "Westerhorn", null], + ["010565660003", "Bevern", null], + ["010565660004", "Bilsen", null], + ["010565660008", "Bokholt-Hanredder", null], + ["010565660011", "Bullenkuhlen", null], + ["010565660014", "Ellerhoop", null], + ["010565660017", "Groß Offenseth-Aspern", null], + ["010565660022", "Heede", null], + ["010565660026", "Hemdingen", null], + ["010565660034", "Langeln", null], + ["010565660035", "Lutzhorn", null], + ["010565687009", "Borstel-Hohenraden", null], + ["010565687013", "Ellerbek", null], + ["010565687032", "Kummerfeld", null], + ["010565687040", "Prisdorf", null], + ["010565687047", "Tangstedt", null], + ["010565690001", "Appen", null], + ["010565690016", "Groß Nordende", null], + ["010565690019", "Haselau", null], + ["010565690020", "Haseldorf", null], + ["010565690023", "Heidgraben", null], + ["010565690024", "Heist", null], + ["010565690027", "Hetlingen", null], + ["010565690028", "Holm", null], + ["010565690036", "Moorrege", null], + ["010565690037", "Neuendeich", null], + ["010570001001", "Ascheberg (Holstein)", null], + ["010570008008", "Bönebüttel", null], + ["010570009009", "Bösdorf", null], + ["010570057057", "Plön, Stadt", null], + ["010570062062", "Preetz, Stadt", null], + ["010570091091", "Schwentinental, Stadt", null], + ["010575727004", "Behrensdorf (Ostsee)", null], + ["010575727007", "Blekendorf", null], + ["010575727013", "Dannau", null], + ["010575727021", "Giekau", null], + ["010575727026", "Helmstorf", null], + ["010575727027", "Högsdorf", null], + ["010575727029", "Hohenfelde", null], + ["010575727030", "Hohwacht (Ostsee)", null], + ["010575727034", "Kirchnüchel", null], + ["010575727035", "Klamp", null], + ["010575727038", "Kletkamp", null], + ["010575727048", "Lütjenburg, Stadt", null], + ["010575727055", "Panker", null], + ["010575727076", "Schwartbuck", null], + ["010575727082", "Tröndel", null], + ["010575739015", "Dersau", null], + ["010575739017", "Dörnick", null], + ["010575739022", "Grebin", null], + ["010575739032", "Kalübbe", null], + ["010575739045", "Lebrade", null], + ["010575739053", "Nehmten", null], + ["010575739065", "Rantzau", null], + ["010575739067", "Rathjensdorf", null], + ["010575739089", "Wittmoldt", null], + ["010575747002", "Barmissen", null], + ["010575747010", "Boksee", null], + ["010575747011", "Bothkamp", null], + ["010575747023", "Großbarkau", null], + ["010575747031", "Honigsee", null], + ["010575747033", "Kirchbarkau", null], + ["010575747037", "Klein Barkau", null], + ["010575747042", "Kühren", null], + ["010575747046", "Lehmkuhlen", null], + ["010575747047", "Löptin", null], + ["010575747054", "Nettelsee", null], + ["010575747058", "Pohnsdorf", null], + ["010575747059", "Postfeld", null], + ["010575747066", "Rastorf", null], + ["010575747070", "Schellhorn", null], + ["010575747084", "Wahlstorf", null], + ["010575747086", "Warnau", null], + ["010575755003", "Barsbek", null], + ["010575755006", "Bendfeld", null], + ["010575755012", "Brodersdorf", null], + ["010575755018", "Fahren", null], + ["010575755020", "Fiefbergen", null], + ["010575755028", "Höhndorf", null], + ["010575755039", "Köhn", null], + ["010575755040", "Krokau", null], + ["010575755041", "Krummbek", null], + ["010575755043", "Laboe", null], + ["010575755049", "Lutterbek", null], + ["010575755056", "Passade", null], + ["010575755060", "Prasdorf", null], + ["010575755063", "Probsteierhagen", null], + ["010575755073", "Schönberg (Holstein)", null], + ["010575755078", "Stakendorf", null], + ["010575755079", "Stein", null], + ["010575755081", "Stoltenberg", null], + ["010575755087", "Wendtorf", null], + ["010575755088", "Wisch", null], + ["010575775016", "Dobersdorf", null], + ["010575775044", "Lammershagen", null], + ["010575775050", "Martensrade", null], + ["010575775052", "Mucheln", null], + ["010575775072", "Schlesen", null], + ["010575775077", "Selent", null], + ["010575775090", "Fargau-Pratjau", null], + ["010575782025", "Heikendorf", null], + ["010575782051", "Mönkeberg", null], + ["010575782074", "Schönkirchen", null], + ["010575785005", "Belau", null], + ["010575785024", "Großharrie", null], + ["010575785068", "Rendswühren", null], + ["010575785069", "Ruhwinkel", null], + ["010575785071", "Schillsdorf", null], + ["010575785080", "Stolpe", null], + ["010575785083", "Tasdorf", null], + ["010575785085", "Wankendorf", null], + ["010580005005", "Altenholz", null], + ["010580034034", "Büdelsdorf, Stadt", null], + ["010580043043", "Eckernförde, Stadt", null], + ["010580092092", "Kronshagen", null], + ["010580135135", "Rendsburg, Stadt", null], + ["010580169169", "Wasbek", null], + ["010585803001", "Achterwehr", null], + ["010585803028", "Bredenbek", null], + ["010585803050", "Felde", null], + ["010585803093", "Krummwisch", null], + ["010585803104", "Melsdorf", null], + ["010585803126", "Ottendorf", null], + ["010585803130", "Quarnbek", null], + ["010585803171", "Westensee", null], + ["010585822037", "Dänischenhagen", null], + ["010585822116", "Noer", null], + ["010585822150", "Schwedeneck", null], + ["010585822157", "Strande", null], + ["010585824051", "Felm", null], + ["010585824058", "Gettorf", null], + ["010585824096", "Lindau", null], + ["010585824110", "Neudorf-Bornstein", null], + ["010585824112", "Neuwittenbek", null], + ["010585824121", "Osdorf", null], + ["010585824142", "Schinkel", null], + ["010585824165", "Tüttendorf", null], + ["010585830019", "Böhnhusen", null], + ["010585830053", "Flintbek", null], + ["010585830145", "Schönhorst", null], + ["010585830160", "Techelsdorf", null], + ["010585833003", "Alt Duvenstedt", null], + ["010585833054", "Fockbek", null], + ["010585833118", "Nübbel", null], + ["010585833136", "Rickert", null], + ["010585847010", "Bargstall", null], + ["010585847029", "Breiholz", null], + ["010585847036", "Christiansholm", null], + ["010585847047", "Elsdorf-Westermühlen", null], + ["010585847055", "Friedrichsgraben", null], + ["010585847056", "Friedrichsholm", null], + ["010585847070", "Hamdorf", null], + ["010585847078", "Hohn", null], + ["010585847089", "Königshügel", null], + ["010585847097", "Lohe-Föhrden", null], + ["010585847129", "Prinzenmoor", null], + ["010585847154", "Sophienhamm", null], + ["010585853031", "Brinjahe", null], + ["010585853048", "Embühren", null], + ["010585853068", "Haale", null], + ["010585853071", "Hamweddel", null], + ["010585853075", "Hörsten", null], + ["010585853086", "Jevenstedt", null], + ["010585853101", "Luhnstedt", null], + ["010585853148", "Schülp b. Rendsburg", null], + ["010585853155", "Stafstedt", null], + ["010585853172", "Westerrönfeld", null], + ["010585859018", "Blumenthal", null], + ["010585859105", "Mielkendorf", null], + ["010585859107", "Molfsee", null], + ["010585859138", "Rodenbek", null], + ["010585859139", "Rumohr", null], + ["010585859141", "Schierensee", null], + ["010585864011", "Bargstedt", null], + ["010585864021", "Bokel", null], + ["010585864023", "Borgdorf-Seedorf", null], + ["010585864027", "Brammer", null], + ["010585864038", "Dätgen", null], + ["010585864045", "Eisendorf", null], + ["010585864046", "Ellerdorf", null], + ["010585864049", "Emkendorf", null], + ["010585864059", "Gnutz", null], + ["010585864065", "Groß Vollstedt", null], + ["010585864091", "Krogaspe", null], + ["010585864094", "Langwedel", null], + ["010585864117", "Nortorf, Stadt", null], + ["010585864120", "Oldenhütten", null], + ["010585864147", "Schülp b. Nortorf", null], + ["010585864163", "Timmaspe", null], + ["010585864168", "Warder", null], + ["010585888026", "Bovenau", null], + ["010585888073", "Haßmoor", null], + ["010585888122", "Ostenfeld (Rendsburg)", null], + ["010585888124", "Osterrönfeld", null], + ["010585888132", "Rade b. Rendsburg", null], + ["010585888140", "Schacht-Audorf", null], + ["010585888146", "Schülldorf", null], + ["010585889016", "Bissee", null], + ["010585889022", "Bordesholm", null], + ["010585889033", "Brügge", null], + ["010585889063", "Grevenkrug", null], + ["010585889064", "Groß Buchwald", null], + ["010585889076", "Hoffeld", null], + ["010585889098", "Loop", null], + ["010585889108", "Mühbrook", null], + ["010585889109", "Negenharrie", null], + ["010585889133", "Reesdorf", null], + ["010585889143", "Schmalstede", null], + ["010585889144", "Schönbek", null], + ["010585889153", "Sören", null], + ["010585889170", "Wattenbek", null], + ["010585890008", "Ascheffel", null], + ["010585890024", "Borgstedt", null], + ["010585890030", "Brekendorf", null], + ["010585890035", "Bünsdorf", null], + ["010585890039", "Damendorf", null], + ["010585890066", "Groß Wittensee", null], + ["010585890069", "Haby", null], + ["010585890080", "Holtsee", null], + ["010585890081", "Holzbunge", null], + ["010585890083", "Hütten", null], + ["010585890088", "Klein Wittensee", null], + ["010585890111", "Neu Duvenstedt", null], + ["010585890123", "Osterby", null], + ["010585890127", "Owschlag", null], + ["010585890152", "Sehestedt", null], + ["010585890175", "Ahlefeld-Bistensee", null], + ["010585893004", "Altenhof", null], + ["010585893012", "Barkelsby", null], + ["010585893032", "Brodersby", null], + ["010585893040", "Damp", null], + ["010585893042", "Dörphof", null], + ["010585893052", "Fleckeby", null], + ["010585893057", "Gammelby", null], + ["010585893067", "Güby", null], + ["010585893082", "Holzdorf", null], + ["010585893084", "Hummelfeld", null], + ["010585893087", "Karby", null], + ["010585893090", "Kosel", null], + ["010585893099", "Loose", null], + ["010585893102", "Goosefeld", null], + ["010585893137", "Rieseby", null], + ["010585893162", "Thumby", null], + ["010585893166", "Waabs", null], + ["010585893173", "Windeby", null], + ["010585893174", "Winnemark", null], + ["010585895007", "Arpsdorf", null], + ["010585895009", "Aukrug", null], + ["010585895013", "Beldorf", null], + ["010585895014", "Bendorf", null], + ["010585895015", "Beringstedt", null], + ["010585895025", "Bornholt", null], + ["010585895044", "Ehndorf", null], + ["010585895061", "Gokels", null], + ["010585895062", "Grauel", null], + ["010585895072", "Hanerau-Hademarschen", null], + ["010585895074", "Heinkenborstel", null], + ["010585895077", "Hohenwestedt", null], + ["010585895085", "Jahrsdorf", null], + ["010585895100", "Lütjenwestedt", null], + ["010585895103", "Meezen", null], + ["010585895106", "Mörel", null], + ["010585895113", "Nienborstel", null], + ["010585895115", "Nindorf", null], + ["010585895119", "Oldenbüttel", null], + ["010585895125", "Osterstedt", null], + ["010585895128", "Padenstedt", null], + ["010585895131", "Rade b. Hohenwestedt", null], + ["010585895134", "Remmels", null], + ["010585895151", "Seefeld", null], + ["010585895156", "Steenfeld", null], + ["010585895158", "Tackesdorf", null], + ["010585895159", "Tappendorf", null], + ["010585895161", "Thaden", null], + ["010585895164", "Todenbüttel", null], + ["010585895167", "Wapelfeld", null], + ["010590045045", "Kappeln, Stadt", null], + ["010590075075", "Schleswig, Stadt", null], + ["010590113113", "Glücksburg (Ostsee), Stadt", null], + ["010590120120", "Harrislee", null], + ["010590183183", "Handewitt", null], + ["010595912107", "Eggebek", null], + ["010595912128", "Janneby", null], + ["010595912131", "Jerrishoe", null], + ["010595912132", "Jörl", null], + ["010595912138", "Langstedt", null], + ["010595912162", "Sollerup", null], + ["010595912169", "Süderhackstedt", null], + ["010595912174", "Wanderup", null], + ["010595915012", "Borgwedel", null], + ["010595915018", "Busdorf", null], + ["010595915019", "Dannewerk", null], + ["010595915026", "Fahrdorf", null], + ["010595915032", "Geltorf", null], + ["010595915043", "Jagel", null], + ["010595915056", "Lottorf", null], + ["010595915078", "Selk", null], + ["010595919101", "Tastrup", null], + ["010595919103", "Ausacker", null], + ["010595919116", "Großsolt", null], + ["010595919126", "Hürup", null], + ["010595919127", "Husby", null], + ["010595919141", "Maasbüll", null], + ["010595919182", "Freienwill", null], + ["010595920002", "Arnis, Stadt", null], + ["010595920034", "Grödersby", null], + ["010595920067", "Oersberg", null], + ["010595920068", "Rabenkirchen-Faulück", null], + ["010595937106", "Dollerup", null], + ["010595937118", "Grundhof", null], + ["010595937137", "Langballig", null], + ["010595937145", "Munkbrarup", null], + ["010595937157", "Ringsberg", null], + ["010595937176", "Wees", null], + ["010595937178", "Westerholz", null], + ["010595940159", "Sieverstedt", null], + ["010595940171", "Tarp", null], + ["010595940184", "Oeversee", null], + ["010595949076", "Schnarup-Thumby", null], + ["010595949161", "Sörup", null], + ["010595949185", "Mittelangeln", null], + ["010595952105", "Böxlund", null], + ["010595952115", "Großenwiehe", null], + ["010595952123", "Hörup", null], + ["010595952124", "Holt", null], + ["010595952129", "Jardelund", null], + ["010595952143", "Medelby", null], + ["010595952144", "Meyn", null], + ["010595952149", "Nordhackstedt", null], + ["010595952151", "Osterby", null], + ["010595952158", "Schafflund", null], + ["010595952173", "Wallsbüll", null], + ["010595952177", "Weesby", null], + ["010595952179", "Lindewitt", null], + ["010595974006", "Böel", null], + ["010595974055", "Loit", null], + ["010595974060", "Mohrkirch", null], + ["010595974063", "Norderbrarup", null], + ["010595974065", "Nottfeld", null], + ["010595974070", "Rügge", null], + ["010595974072", "Saustrup", null], + ["010595974074", "Scheggerott", null], + ["010595974080", "Steinfeld", null], + ["010595974083", "Süderbrarup", null], + ["010595974094", "Ulsnis", null], + ["010595974095", "Wagersrott", null], + ["010595974187", "Boren", null], + ["010595987008", "Böklund", null], + ["010595987037", "Havetoft", null], + ["010595987042", "Idstedt", null], + ["010595987049", "Klappholz", null], + ["010595987062", "Neuberend", null], + ["010595987073", "Schaalby", null], + ["010595987081", "Stolk", null], + ["010595987082", "Struxdorf", null], + ["010595987084", "Süderfahrenstedt", null], + ["010595987086", "Taarstedt", null], + ["010595987090", "Tolk", null], + ["010595987093", "Uelsby", null], + ["010595987097", "Twedt", null], + ["010595987098", "Nübel", null], + ["010595987189", "Brodersby-Goltoft", null], + ["010595990102", "Ahneby", null], + ["010595990109", "Esgrus", null], + ["010595990112", "Gelting", null], + ["010595990121", "Hasselberg", null], + ["010595990136", "Kronsgaard", null], + ["010595990142", "Maasholm", null], + ["010595990147", "Nieby", null], + ["010595990148", "Niesgrau", null], + ["010595990152", "Pommerby", null], + ["010595990154", "Rabel", null], + ["010595990155", "Rabenholz", null], + ["010595990163", "Stangheck", null], + ["010595990164", "Steinberg", null], + ["010595990167", "Sterup", null], + ["010595990168", "Stoltebüll", null], + ["010595990186", "Steinbergkirche", null], + ["010595993010", "Bollingstedt", null], + ["010595993023", "Ellingstedt", null], + ["010595993039", "Hollingstedt", null], + ["010595993041", "Hüsby", null], + ["010595993044", "Jübek", null], + ["010595993057", "Lürschau", null], + ["010595993077", "Schuby", null], + ["010595993079", "Silberstedt", null], + ["010595993092", "Treia", null], + ["010595996001", "Alt Bennebek", null], + ["010595996005", "Bergenhusen", null], + ["010595996009", "Börm", null], + ["010595996020", "Dörpstedt", null], + ["010595996024", "Erfde", null], + ["010595996035", "Groß Rheide", null], + ["010595996050", "Klein Bennebek", null], + ["010595996051", "Klein Rheide", null], + ["010595996053", "Kropp", null], + ["010595996058", "Meggerdorf", null], + ["010595996087", "Tetenhusen", null], + ["010595996088", "Tielen", null], + ["010595996096", "Wohlde", null], + ["010595996188", "Stapel", null], + ["010600004004", "Bad Bramstedt, Stadt", null], + ["010600005005", "Bad Segeberg, Stadt", null], + ["010600019019", "Ellerau", null], + ["010600039039", "Henstedt-Ulzburg", null], + ["010600044044", "Kaltenkirchen, Stadt", null], + ["010600063063", "Norderstedt, Stadt", null], + ["010600092092", "Wahlstedt, Stadt", null], + ["010605005003", "Armstedt", null], + ["010605005009", "Bimöhlen", null], + ["010605005013", "Borstel", null], + ["010605005021", "Föhrden-Barl", null], + ["010605005023", "Fuhlendorf", null], + ["010605005027", "Großenaspe", null], + ["010605005031", "Hagen", null], + ["010605005033", "Hardebek", null], + ["010605005035", "Hasenkrug", null], + ["010605005037", "Heidmoor", null], + ["010605005040", "Hitzhusen", null], + ["010605005056", "Mönkloh", null], + ["010605005095", "Weddelbrook", null], + ["010605005099", "Wiemersdorf", null], + ["010605024012", "Bornhöved", null], + ["010605024017", "Damsdorf", null], + ["010605024026", "Gönnebek", null], + ["010605024072", "Schmalensee", null], + ["010605024080", "Stocksee", null], + ["010605024086", "Tarbek", null], + ["010605024087", "Tensfeld", null], + ["010605024089", "Trappenkamp", null], + ["010605034043", "Itzstedt", null], + ["010605034046", "Kayhude", null], + ["010605034058", "Nahe", null], + ["010605034065", "Oering", null], + ["010605034076", "Seth", null], + ["010605034085", "Sülfeld", null], + ["010605043002", "Alveslohe", null], + ["010605043034", "Hartenholm", null], + ["010605043036", "Hasenmoor", null], + ["010605043054", "Lentföhrden", null], + ["010605043064", "Nützen", null], + ["010605043073", "Schmalfeld", null], + ["010605048042", "Hüttblek", null], + ["010605048045", "Kattendorf", null], + ["010605048047", "Kisdorf", null], + ["010605048066", "Oersdorf", null], + ["010605048077", "Sievershütten", null], + ["010605048082", "Struvenhütten", null], + ["010605048084", "Stuvenborn", null], + ["010605048094", "Wakendorf II", null], + ["010605048100", "Winsen", null], + ["010605053007", "Bark", null], + ["010605053008", "Bebensee", null], + ["010605053022", "Fredesdorf", null], + ["010605053029", "Groß Niendorf", null], + ["010605053041", "Högersdorf", null], + ["010605053051", "Kükels", null], + ["010605053053", "Leezen", null], + ["010605053057", "Mözen", null], + ["010605053062", "Neversdorf", null], + ["010605053074", "Schwissel", null], + ["010605053088", "Todesfelde", null], + ["010605053101", "Wittenborn", null], + ["010605063011", "Boostedt", null], + ["010605063016", "Daldorf", null], + ["010605063028", "Groß Kummerfeld", null], + ["010605063038", "Heidmühlen", null], + ["010605063052", "Latendorf", null], + ["010605063068", "Rickling", null], + ["010605086006", "Bahrenhof", null], + ["010605086010", "Blunk", null], + ["010605086015", "Bühnsdorf", null], + ["010605086018", "Dreggers", null], + ["010605086020", "Fahrenkrug", null], + ["010605086024", "Geschendorf", null], + ["010605086025", "Glasau", null], + ["010605086030", "Groß Rönnau", null], + ["010605086048", "Klein Gladebrügge", null], + ["010605086049", "Klein Rönnau", null], + ["010605086050", "Krems II", null], + ["010605086059", "Negernbötel", null], + ["010605086060", "Nehms", null], + ["010605086061", "Neuengörs", null], + ["010605086067", "Pronstorf", null], + ["010605086069", "Rohlstorf", null], + ["010605086070", "Schackendorf", null], + ["010605086071", "Schieren", null], + ["010605086075", "Seedorf", null], + ["010605086079", "Stipsdorf", null], + ["010605086081", "Strukdorf", null], + ["010605086090", "Travenhorst", null], + ["010605086091", "Traventhal", null], + ["010605086093", "Wakendorf I", null], + ["010605086096", "Weede", null], + ["010605086097", "Wensin", null], + ["010605086098", "Westerrade", null], + ["010609014014", "Buchholz (Forstgutsbez.),gemfr. Gebiet", null], + ["010610029029", "Glückstadt, Stadt", null], + ["010610046046", "Itzehoe, Stadt", null], + ["010610113113", "Wilster, Stadt", null], + ["010615104005", "Auufer", null], + ["010615104016", "Breitenberg", null], + ["010615104017", "Breitenburg", null], + ["010615104053", "Kollmoor", null], + ["010615104058", "Kronsmoor", null], + ["010615104061", "Lägerdorf", null], + ["010615104068", "Moordiek", null], + ["010615104072", "Münsterdorf", null], + ["010615104079", "Oelixdorf", null], + ["010615104109", "Westermoor", null], + ["010615104115", "Wittenbergen", null], + ["010615134004", "Altenmoor", null], + ["010615134012", "Blomesche Wildnis", null], + ["010615134015", "Borsfleth", null], + ["010615134027", "Engelbrechtsche Wildnis", null], + ["010615134037", "Herzhorn", null], + ["010615134041", "Hohenfelde", null], + ["010615134044", "Horst (Holstein)", null], + ["010615134050", "Kiebitzreihe", null], + ["010615134054", "Krempdorf", null], + ["010615134074", "Neuendorf b. Elmshorn", null], + ["010615134101", "Sommerland", null], + ["010615134118", "Kollmar", null], + ["010615138008", "Bekdorf", null], + ["010615138010", "Bekmünde", null], + ["010615138024", "Drage", null], + ["010615138034", "Heiligenstedten", null], + ["010615138035", "Heiligenstedtenerkamp", null], + ["010615138039", "Hodorf", null], + ["010615138040", "Hohenaspe", null], + ["010615138045", "Huje", null], + ["010615138047", "Kaaks", null], + ["010615138052", "Kleve", null], + ["010615138059", "Krummendiek", null], + ["010615138065", "Lohbarbek", null], + ["010615138067", "Mehlbek", null], + ["010615138070", "Moorhusen", null], + ["010615138082", "Oldendorf", null], + ["010615138083", "Ottenbüttel", null], + ["010615138084", "Peissen", null], + ["010615138098", "Schlotfeld", null], + ["010615138100", "Silzen", null], + ["010615138114", "Winseldorf", null], + ["010615153006", "Bahrenfleth", null], + ["010615153022", "Dägeling", null], + ["010615153026", "Elskop", null], + ["010615153030", "Grevenkop", null], + ["010615153055", "Krempe, Stadt", null], + ["010615153056", "Kremperheide", null], + ["010615153057", "Krempermoor", null], + ["010615153073", "Neuenbrook", null], + ["010615153092", "Rethwisch", null], + ["010615153104", "Süderau", null], + ["010615168001", "Aasbüttel", null], + ["010615168003", "Agethorst", null], + ["010615168011", "Besdorf", null], + ["010615168013", "Bokelrehm", null], + ["010615168014", "Bokhorst", null], + ["010615168021", "Christinenthal", null], + ["010615168031", "Gribbohm", null], + ["010615168033", "Hadenfeld", null], + ["010615168043", "Holstenniendorf", null], + ["010615168048", "Kaisborstel", null], + ["010615168066", "Looft", null], + ["010615168076", "Nienbüttel", null], + ["010615168078", "Nutteln", null], + ["010615168081", "Oldenborstel", null], + ["010615168085", "Pöschendorf", null], + ["010615168087", "Puls", null], + ["010615168091", "Reher", null], + ["010615168097", "Schenefeld", null], + ["010615168105", "Vaale", null], + ["010615168106", "Vaalermoor", null], + ["010615168107", "Wacken", null], + ["010615168108", "Warringholz", null], + ["010615179002", "Aebtissinwisch", null], + ["010615179007", "Beidenfleth", null], + ["010615179018", "Brokdorf", null], + ["010615179020", "Büttel", null], + ["010615179023", "Dammfleth", null], + ["010615179025", "Ecklak", null], + ["010615179060", "Kudensee", null], + ["010615179062", "Landrecht", null], + ["010615179063", "Landscheide", null], + ["010615179077", "Nortorf", null], + ["010615179095", "Sankt Margarethen", null], + ["010615179102", "Stördorf", null], + ["010615179110", "Wewelsfleth", null], + ["010615179119", "Neuendorf-Sachsenbande", null], + ["010615189019", "Brokstedt", null], + ["010615189028", "Fitzbek", null], + ["010615189036", "Hennstedt", null], + ["010615189038", "Hingstheide", null], + ["010615189042", "Hohenlockstedt", null], + ["010615189049", "Kellinghusen, Stadt", null], + ["010615189064", "Lockstedt", null], + ["010615189071", "Mühlenbarbek", null], + ["010615189080", "Oeschebüttel", null], + ["010615189086", "Poyenberg", null], + ["010615189088", "Quarnstedt", null], + ["010615189089", "Rade", null], + ["010615189093", "Rosdorf", null], + ["010615189096", "Sarlhusen", null], + ["010615189103", "Störkathen", null], + ["010615189111", "Wiedenborstel", null], + ["010615189112", "Willenscharen", null], + ["010615189116", "Wrist", null], + ["010615189117", "Wulfsmoor", null], + ["010620001001", "Ahrensburg, Stadt", null], + ["010620004004", "Bad Oldesloe, Stadt", null], + ["010620006006", "Bargteheide, Stadt", null], + ["010620009009", "Barsbüttel", null], + ["010620018018", "Glinde, Stadt", null], + ["010620023023", "Großhansdorf", null], + ["010620053053", "Oststeinbek", null], + ["010620060060", "Reinbek, Stadt", null], + ["010620061061", "Reinfeld (Holstein), Stadt", null], + ["010620076076", "Tangstedt", null], + ["010620090090", "Ammersbek", null], + ["010625207019", "Grabau", null], + ["010625207046", "Meddewade", null], + ["010625207050", "Neritz", null], + ["010625207056", "Pölitz", null], + ["010625207062", "Rethwisch", null], + ["010625207065", "Rümpel", null], + ["010625207089", "Lasbek", null], + ["010625207091", "Steinburg", null], + ["010625207092", "Travenbrück", null], + ["010625218005", "Bargfeld-Stegen", null], + ["010625218014", "Delingsdorf", null], + ["010625218016", "Elmenhorst", null], + ["010625218027", "Hammoor", null], + ["010625218036", "Jersbek", null], + ["010625218051", "Nienwohld", null], + ["010625218078", "Todendorf", null], + ["010625218081", "Tremsbüttel", null], + ["010625244003", "Badendorf", null], + ["010625244008", "Barnitz", null], + ["010625244025", "Hamberge", null], + ["010625244031", "Heidekamp", null], + ["010625244032", "Heilshoop", null], + ["010625244039", "Klein Wesenberg", null], + ["010625244048", "Mönkhagen", null], + ["010625244059", "Rehhorst", null], + ["010625244083", "Westerau", null], + ["010625244087", "Zarpen", null], + ["010625244093", "Feldhorst", null], + ["010625244094", "Wesenberg", null], + ["010625262011", "Braak", null], + ["010625262035", "Hoisdorf", null], + ["010625262069", "Siek", null], + ["010625262071", "Stapelfeld", null], + ["010625262088", "Brunsbek", null], + ["010625270020", "Grande", null], + ["010625270021", "Grönwohld", null], + ["010625270022", "Großensee", null], + ["010625270026", "Hamfelde", null], + ["010625270033", "Hohenfelde", null], + ["010625270040", "Köthel", null], + ["010625270045", "Lütjensee", null], + ["010625270058", "Rausdorf", null], + ["010625270082", "Trittau", null], + ["010625270086", "Witzhave", null], + ["020000000000", "Hamburg, Freie und Hansestadt", null], + [ + "021010101101", + "Hamburg-Altstadt, OT 101", + "Stadt-/Ortsteil bzw. Stadtbezirk" + ], + [ + "021010102102", + "Hamburg-Altstadt, OT 102", + "Stadt-/Ortsteil bzw. Stadtbezirk" + ], + ["021020103103", "HafenCity, OT 103", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["021020104104", "HafenCity, OT 104", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["021030105105", "Neustadt, OT 105", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["021030106106", "Neustadt, OT 106", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["021030107107", "Neustadt, OT 107", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["021030108108", "Neustadt, OT 108", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["021040109109", "St. Pauli, OT 109", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["021040110110", "St. Pauli, OT 110", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["021040111111", "St. Pauli, OT 111", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["021040112112", "St. Pauli, OT 112", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["021050113113", "St. Georg, OT 113", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["021050114114", "St. Georg, OT 114", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["021060115115", "Hammerbrook, OT 115", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["021060116116", "Hammerbrook, OT 116", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["021060117117", "Hammerbrook, OT 117", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["021060118118", "Hammerbrook, OT 118", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["021070119119", "Borgfelde, OT 119", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["021070120120", "Borgfelde, OT 120", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["021080121121", "Hamm, OT 121", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["021080122122", "Hamm, OT 122", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["021080123123", "Hamm, OT 123", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["021080124124", "Hamm, OT 124", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["021080125125", "Hamm, OT 125", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["021080126126", "Hamm, OT 126", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["021080127127", "Hamm, OT 127", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["021110128128", "Horn, OT 128", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["021110129129", "Horn, OT 129", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["021120130130", "Billstedt, OT 130", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["021130131131", "Billbrook, OT 131", "Stadt-/Ortsteil bzw. Stadtbezirk"], + [ + "021140132132", + "Rothenburgsort, OT 132", + "Stadt-/Ortsteil bzw. Stadtbezirk" + ], + [ + "021140133133", + "Rothenburgsort, OT 133", + "Stadt-/Ortsteil bzw. Stadtbezirk" + ], + ["021150134134", "Veddel, OT 134", "Stadt-/Ortsteil bzw. Stadtbezirk"], + [ + "021160135135", + "Wilhelmsburg, OT 135", + "Stadt-/Ortsteil bzw. Stadtbezirk" + ], + [ + "021160136136", + "Wilhelmsburg, OT 136", + "Stadt-/Ortsteil bzw. Stadtbezirk" + ], + [ + "021160137137", + "Wilhelmsburg, OT 137", + "Stadt-/Ortsteil bzw. Stadtbezirk" + ], + [ + "021170138138", + "Kleiner Grasbrook, OT 138", + "Stadt-/Ortsteil bzw. Stadtbezirk" + ], + ["021180139139", "Steinwerder, OT 139", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["021190140140", "Waltershof, OT 140", "Stadt-/Ortsteil bzw. Stadtbezirk"], + [ + "021200141141", + "Finkenwerder, OT 141", + "Stadt-/Ortsteil bzw. Stadtbezirk" + ], + ["021210142142", "Neuwerk, OT 142", "Stadt-/Ortsteil bzw. Stadtbezirk"], + [ + "021220150150", + "Seeleute/Binnenschiffer, OT 150", + "Stadt-/Ortsteil bzw. Stadtbezirk" + ], + [ + "022010201201", + "Altona-Altstadt, OT 201", + "Stadt-/Ortsteil bzw. Stadtbezirk" + ], + [ + "022010202202", + "Altona-Altstadt, OT 202", + "Stadt-/Ortsteil bzw. Stadtbezirk" + ], + [ + "022010203203", + "Altona-Altstadt, OT 203", + "Stadt-/Ortsteil bzw. Stadtbezirk" + ], + [ + "022010204204", + "Altona-Altstadt, OT 204", + "Stadt-/Ortsteil bzw. Stadtbezirk" + ], + [ + "022010205205", + "Altona-Altstadt, OT 205", + "Stadt-/Ortsteil bzw. Stadtbezirk" + ], + [ + "022010206206", + "Altona-Altstadt, OT 206", + "Stadt-/Ortsteil bzw. Stadtbezirk" + ], + [ + "022020207207", + "Sternschanze, OT 207", + "Stadt-/Ortsteil bzw. Stadtbezirk" + ], + ["022030208208", "Altona-Nord, OT 208", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["022030209209", "Altona-Nord, OT 209", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["022030210210", "Altona-Nord, OT 210", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["022040211211", "Ottensen, OT 211", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["022040212212", "Ottensen, OT 212", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["022040213213", "Ottensen, OT 213", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["022040214214", "Ottensen, OT 214", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["022050215215", "Bahrenfeld, OT 215", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["022050216216", "Bahrenfeld, OT 216", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["022050217217", "Bahrenfeld, OT 217", "Stadt-/Ortsteil bzw. Stadtbezirk"], + [ + "022060218218", + "Groß Flottbek, OT 218", + "Stadt-/Ortsteil bzw. Stadtbezirk" + ], + ["022070219219", "Othmarschen, OT 219", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["022080220220", "Lurup, OT 220", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["022090221221", "Osdorf, OT 221", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["022100222222", "Nienstedten, OT 222", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["022110223223", "Blankenese, OT 223", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["022110224224", "Blankenese, OT 224", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["022120225225", "Iserbrook, OT 225", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["022130226226", "Sülldorf, OT 226", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["022140227227", "Rissen, OT 227", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["023010301301", "Eimsbüttel, OT 301", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["023010302302", "Eimsbüttel, OT 302", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["023010303303", "Eimsbüttel, OT 303", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["023010304304", "Eimsbüttel, OT 304", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["023010305305", "Eimsbüttel, OT 305", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["023010306306", "Eimsbüttel, OT 306", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["023010307307", "Eimsbüttel, OT 307", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["023010308308", "Eimsbüttel, OT 308", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["023010309309", "Eimsbüttel, OT 309", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["023010310310", "Eimsbüttel, OT 310", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["023020311311", "Rotherbaum, OT 311", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["023020312312", "Rotherbaum, OT 312", "Stadt-/Ortsteil bzw. Stadtbezirk"], + [ + "023030313313", + "Harvestehude, OT 313", + "Stadt-/Ortsteil bzw. Stadtbezirk" + ], + [ + "023030314314", + "Harvestehude, OT 314", + "Stadt-/Ortsteil bzw. Stadtbezirk" + ], + [ + "023040315315", + "Hoheluft-West, OT 315", + "Stadt-/Ortsteil bzw. Stadtbezirk" + ], + [ + "023040316316", + "Hoheluft-West, OT 316", + "Stadt-/Ortsteil bzw. Stadtbezirk" + ], + ["023050317317", "Lokstedt, OT 317", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["023060318318", "Niendorf, OT 318", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["023070319319", "Schnelsen, OT 319", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["023080320320", "Eidelstedt, OT 320", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["023090321321", "Stellingen, OT 321", "Stadt-/Ortsteil bzw. Stadtbezirk"], + [ + "024010401401", + "Hoheluft-Ost, OT 401", + "Stadt-/Ortsteil bzw. Stadtbezirk" + ], + [ + "024010402402", + "Hoheluft-Ost, OT 402", + "Stadt-/Ortsteil bzw. Stadtbezirk" + ], + ["024020403403", "Eppendorf, OT 403", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["024020404404", "Eppendorf, OT 404", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["024020405405", "Eppendorf, OT 405", "Stadt-/Ortsteil bzw. Stadtbezirk"], + [ + "024030406406", + "Gross Borstel, OT 406", + "Stadt-/Ortsteil bzw. Stadtbezirk" + ], + ["024040407407", "Alsterdorf, OT 407", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["024050408408", "Winterhude, OT 408", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["024050409409", "Winterhude, OT 409", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["024050410410", "Winterhude, OT 410", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["024050411411", "Winterhude, OT 411", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["024050412412", "Winterhude, OT 412", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["024050413413", "Winterhude, OT 413", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["024060414414", "Uhlenhorst, OT 414", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["024060415415", "Uhlenhorst, OT 415", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["024070416416", "Hohenfelde, OT 416", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["024070417417", "Hohenfelde, OT 417", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["024080418418", "Barmbek-Süd, OT 418", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["024080419419", "Barmbek-Süd, OT 419", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["024080420420", "Barmbek-Süd, OT 420", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["024080421421", "Barmbek-Süd, OT 421", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["024080422422", "Barmbek-Süd, OT 422", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["024080423423", "Barmbek-Süd, OT 423", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["024090424424", "Dulsberg, OT 424", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["024090425425", "Dulsberg, OT 425", "Stadt-/Ortsteil bzw. Stadtbezirk"], + [ + "024100426426", + "Barmbek-Nord, OT 426", + "Stadt-/Ortsteil bzw. Stadtbezirk" + ], + [ + "024100427427", + "Barmbek-Nord, OT 427", + "Stadt-/Ortsteil bzw. Stadtbezirk" + ], + [ + "024100428428", + "Barmbek-Nord, OT 428", + "Stadt-/Ortsteil bzw. Stadtbezirk" + ], + [ + "024100429429", + "Barmbek-Nord, OT 429", + "Stadt-/Ortsteil bzw. Stadtbezirk" + ], + ["024110430430", "Ohlsdorf, OT 430", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["024120431431", "Fuhlsbüttel, OT 431", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["024130432432", "Langenhorn, OT 432", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["025010501501", "Eilbek, OT 501", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["025010502502", "Eilbek, OT 502", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["025010503503", "Eilbek, OT 503", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["025010504504", "Eilbek, OT 504", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["025020505505", "Wandsbek, OT 505", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["025020506506", "Wandsbek, OT 506", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["025020507507", "Wandsbek, OT 507", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["025020508508", "Wandsbek, OT 508", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["025020509509", "Wandsbek, OT 509", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["025030510510", "Marienthal, OT 510", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["025030511511", "Marienthal, OT 511", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["025040512512", "Jenfeld, OT 512", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["025050513513", "Tonndorf, OT 513", "Stadt-/Ortsteil bzw. Stadtbezirk"], + [ + "025060514514", + "Farmsen-Berne, OT 514", + "Stadt-/Ortsteil bzw. Stadtbezirk" + ], + ["025070515515", "Bramfeld, OT 515", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["025080516516", "Steilshoop, OT 516", "Stadt-/Ortsteil bzw. Stadtbezirk"], + [ + "025090517517", + "Wellingsbüttel, OT 517", + "Stadt-/Ortsteil bzw. Stadtbezirk" + ], + ["025100518518", "Sasel, OT 518", "Stadt-/Ortsteil bzw. Stadtbezirk"], + [ + "025110519519", + "Poppenbüttel, OT 519", + "Stadt-/Ortsteil bzw. Stadtbezirk" + ], + [ + "025120520520", + "Hummelsbüttel, OT 520", + "Stadt-/Ortsteil bzw. Stadtbezirk" + ], + [ + "025130521521", + "Lemsahl-Mellingstedt, OT 521", + "Stadt-/Ortsteil bzw. Stadtbezirk" + ], + ["025140522522", "Duvenstedt, OT 522", "Stadt-/Ortsteil bzw. Stadtbezirk"], + [ + "025150523523", + "Wohldorf-Ohlstedt, OT 523", + "Stadt-/Ortsteil bzw. Stadtbezirk" + ], + ["025160524524", "Bergstedt, OT 524", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["025170525525", "Volksdorf, OT 525", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["025180526526", "Rahlstedt, OT 526", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["026010601601", "Lohbrügge, OT 601", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["026020602602", "Bergedorf, OT 602", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["026020603603", "Bergedorf, OT 603", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["026030604604", "Curslack, OT 604", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["026040605605", "Altengamme, OT 605", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["026050606606", "Neuengamme, OT 606", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["026060607607", "Kirchwerder, OT 607", "Stadt-/Ortsteil bzw. Stadtbezirk"], + [ + "026070608608", + "Ochsenwerder, OT 608", + "Stadt-/Ortsteil bzw. Stadtbezirk" + ], + ["026080609609", "Reitbrook, OT 609", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["026090610610", "Allermöhe, OT 610", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["026100611611", "Billwerder, OT 611", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["026110612612", "Moorfleet, OT 612", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["026120613613", "Tatenberg, OT 613", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["026130614614", "Spadenland, OT 614", "Stadt-/Ortsteil bzw. Stadtbezirk"], + [ + "026140615615", + "Neuallermöhe, OT 615", + "Stadt-/Ortsteil bzw. Stadtbezirk" + ], + ["027010701701", "Harburg, OT 701", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["027010702702", "Harburg, OT 702", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["027020703703", "Neuland, OT 703", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["027030704704", "Gut Moor, OT 704", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["027040705705", "Wilstorf, OT 705", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["027050706706", "Rönneburg, OT 706", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["027060707707", "Langenbek, OT 707", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["027070708708", "Sinstorf, OT 708", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["027080709709", "Marmstorf, OT 709", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["027090710710", "Eissendorf, OT 710", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["027100711711", "Heimfeld, OT 711", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["027110712712", "Moorburg, OT 712", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["027120713713", "Altenwerder, OT 713", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["027130714714", "Hausbruch, OT 714", "Stadt-/Ortsteil bzw. Stadtbezirk"], + [ + "027140715715", + "Neugraben-Fischbek, OT 715", + "Stadt-/Ortsteil bzw. Stadtbezirk" + ], + ["027150716716", "Francop, OT 716", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["027160717717", "Neuenfelde, OT 717", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["027170718718", "Cranz, OT 718", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["031010000000", "Braunschweig, Stadt", null], + ["031020000000", "Salzgitter, Stadt", null], + ["031030000000", "Wolfsburg, Stadt", null], + ["031510009009", "Gifhorn, Stadt", null], + ["031510025025", "Sassenburg", null], + ["031510040040", "Wittingen, Stadt", null], + ["031515401002", "Barwedel", null], + ["031515401004", "Bokensdorf", null], + ["031515401014", "Jembke", null], + ["031515401020", "Osloß", null], + ["031515401030", "Tappenbeck", null], + ["031515401039", "Weyhausen", null], + ["031515402003", "Bergfeld", null], + ["031515402005", "Brome, Flecken", null], + ["031515402008", "Ehra-Lessien", null], + ["031515402021", "Parsau", null], + ["031515402024", "Rühen", null], + ["031515402031", "Tiddische", null], + ["031515402032", "Tülau", null], + ["031515403007", "Dedelstorf", null], + ["031515403011", "Hankensbüttel", null], + ["031515403019", "Obernholz", null], + ["031515403028", "Sprakensehl", null], + ["031515403029", "Steinhorst", null], + ["031515404006", "Calberlah", null], + ["031515404013", "Isenbüttel", null], + ["031515404022", "Ribbesbüttel", null], + ["031515404037", "Wasbüttel", null], + ["031515405012", "Hillerse", null], + ["031515405015", "Leiferde", null], + ["031515405017", "Meinersen", null], + ["031515405018", "Müden (Aller)", null], + ["031515406001", "Adenbüttel", null], + ["031515406016", "Meine", null], + ["031515406023", "Rötgesbüttel", null], + ["031515406027", "Schwülper", null], + ["031515406034", "Vordorf", null], + ["031515406041", "Didderse", null], + ["031515407010", "Groß Oesingen", null], + ["031515407026", "Schönewörde", null], + ["031515407033", "Ummern", null], + ["031515407035", "Wagenhoff", null], + ["031515407036", "Wahrenholz", null], + ["031515407038", "Wesendorf", null], + ["031519501501", "Giebel, gemfr. Gebiet", null], + ["031530002002", "Bad Harzburg, Stadt", null], + ["031530007007", "Langelsheim, Stadt", null], + ["031530008008", "Liebenburg", null], + ["031530012012", "Seesen, Stadt", null], + ["031530016016", "Braunlage, Stadt", null], + ["031530017017", "Goslar, Stadt", null], + ["031530018018", "Clausthal-Zellerfeld, Berg- und Universitätsstadt", null], + ["031535401006", "Hahausen", null], + ["031535401009", "Lutter am Barenberge, Flecken", null], + ["031535401014", "Wallmoden", null], + ["031539504504", "Harz (Landkreis Goslar), gemfr. Gebiet", null], + ["031540013013", "Königslutter am Elm, Stadt", null], + ["031540014014", "Lehre", null], + ["031540019019", "Schöningen, Stadt", null], + ["031540028028", "Helmstedt, Stadt", null], + ["031545401008", "Grasleben", null], + ["031545401015", "Mariental", null], + ["031545401016", "Querenhorst", null], + ["031545401018", "Rennau", null], + ["031545402002", "Beierstedt", null], + ["031545402006", "Gevensleben", null], + ["031545402012", "Jerxheim", null], + ["031545402027", "Söllingen", null], + ["031545403005", "Frellstedt", null], + ["031545403017", "Räbke", null], + ["031545403021", "Süpplingen", null], + ["031545403022", "Süpplingenburg", null], + ["031545403025", "Warberg", null], + ["031545403026", "Wolsdorf", null], + ["031545404001", "Bahrdorf", null], + ["031545404004", "Danndorf", null], + ["031545404007", "Grafhorst", null], + ["031545404009", "Groß Twülpstedt", null], + ["031545404024", "Velpke", null], + ["031549501501", "Brunsleberfeld, gemfr. Gebiet", null], + ["031549502502", "Helmstedt, gemfr. Gebiet", null], + ["031549503503", "Königslutter, gemfr. Gebiet", null], + ["031549504504", "Mariental, gemfr. Gebiet", null], + ["031549506506", "Schöningen, gemfr. Gebiet", null], + ["031550001001", "Bad Gandersheim, Stadt", null], + ["031550002002", "Bodenfelde, Flecken", null], + ["031550003003", "Dassel, Stadt", null], + ["031550005005", "Hardegsen, Stadt", null], + ["031550006006", "Kalefeld", null], + ["031550007007", "Katlenburg-Lindau", null], + ["031550009009", "Moringen, Stadt", null], + ["031550010010", "Nörten-Hardenberg, Flecken", null], + ["031550011011", "Northeim, Stadt", null], + ["031550012012", "Uslar, Stadt", null], + ["031550013013", "Einbeck, Stadt", null], + ["031559501501", "Solling (Landkreis Northeim), gemfr. Geb.", null], + ["031570001001", "Edemissen", null], + ["031570002002", "Hohenhameln", null], + ["031570005005", "Lengede", null], + ["031570006006", "Peine, Stadt", null], + ["031570007007", "Vechelde", null], + ["031570008008", "Wendeburg", null], + ["031570009009", "Ilsede", null], + ["031580006006", "Cremlingen", null], + ["031580037037", "Wolfenbüttel, Stadt", null], + ["031580039039", "Schladen-Werla", null], + ["031585402002", "Baddeckenstedt", null], + ["031585402004", "Burgdorf", null], + ["031585402011", "Elbe", null], + ["031585402016", "Haverlah", null], + ["031585402018", "Heere", null], + ["031585402028", "Sehlde", null], + ["031585403005", "Cramme", null], + ["031585403010", "Dorstadt", null], + ["031585403014", "Flöthe", null], + ["031585403019", "Heiningen", null], + ["031585403023", "Ohrum", null], + ["031585403038", "Börßum", null], + ["031585406009", "Dettum", null], + ["031585406012", "Erkerode", null], + ["031585406013", "Evessen", null], + ["031585406030", "Sickte", null], + ["031585406033", "Veltheim (Ohe)", null], + ["031585407007", "Dahlum", null], + ["031585407008", "Denkte", null], + ["031585407017", "Hedeper", null], + ["031585407021", "Kissenbrück", null], + ["031585407022", "Kneitlingen", null], + ["031585407025", "Roklum", null], + ["031585407027", "Schöppenstedt, Stadt", null], + ["031585407031", "Uehrde", null], + ["031585407032", "Vahlberg", null], + ["031585407035", "Winnigstedt", null], + ["031585407036", "Wittmar", null], + ["031585407040", "Remlingen-Semmenstedt", null], + ["031589501501", "Am Großen Rhode, gemfr. Gebiet", null], + ["031589502502", "Barnstorf-Warle, gemfr. Gebiet", null], + ["031589503503", "Voigtsdahlum, gemfr. Gebiet", null], + ["031590001001", "Adelebsen, Flecken", null], + ["031590002002", "Bad Grund (Harz)", null], + ["031590003003", "Bad Lauterberg im Harz, Stadt", null], + ["031590004004", "Bad Sachsa, Stadt", null], + ["031590007007", "Bovenden, Flecken", null], + ["031590010010", "Duderstadt, Stadt", null], + ["031590013013", "Friedland", null], + ["031590015015", "Gleichen", null], + ["031590016016", "Göttingen, Stadt", null], + ["031590017017", "Hann. Münden, Stadt", null], + ["031590019019", "Herzberg am Harz, Stadt", null], + ["031590026026", "Osterode am Harz, Stadt", null], + ["031590029029", "Rosdorf", null], + ["031590034034", "Staufenberg", null], + ["031590036036", "Walkenried", null], + ["031595401008", "Bühren", null], + ["031595401009", "Dransfeld, Stadt", null], + ["031595401021", "Jühnde", null], + ["031595401024", "Niemetal", null], + ["031595401031", "Scheden", null], + ["031595402005", "Bilshausen", null], + ["031595402006", "Bodensee", null], + ["031595402014", "Gieboldehausen, Flecken", null], + ["031595402022", "Krebeck", null], + ["031595402025", "Obernfeld", null], + ["031595402027", "Rhumspringe", null], + ["031595402028", "Rollshausen", null], + ["031595402030", "Rüdershausen", null], + ["031595402037", "Wollbrandshausen", null], + ["031595402038", "Wollershausen", null], + ["031595403012", "Elbingerode", null], + ["031595403018", "Hattorf am Harz", null], + ["031595403020", "Hörden am Harz", null], + ["031595403039", "Wulften am Harz", null], + ["031595404011", "Ebergötzen", null], + ["031595404023", "Landolfshausen", null], + ["031595404032", "Seeburg", null], + ["031595404033", "Seulingen", null], + ["031595404035", "Waake", null], + ["031599501501", "Harz (Landkreis Göttingen), gemfr. Geb.", null], + ["032410001001", "Hannover, Landeshauptstadt", null], + ["032410002002", "Barsinghausen, Stadt", null], + ["032410003003", "Burgdorf, Stadt", null], + ["032410004004", "Burgwedel, Stadt", null], + ["032410005005", "Garbsen, Stadt", null], + ["032410006006", "Gehrden, Stadt", null], + ["032410007007", "Hemmingen, Stadt", null], + ["032410008008", "Isernhagen", null], + ["032410009009", "Laatzen, Stadt", null], + ["032410010010", "Langenhagen, Stadt", null], + ["032410011011", "Lehrte, Stadt", null], + ["032410012012", "Neustadt am Rübenberge, Stadt", null], + ["032410013013", "Pattensen, Stadt", null], + ["032410014014", "Ronnenberg, Stadt", null], + ["032410015015", "Seelze, Stadt", null], + ["032410016016", "Sehnde, Stadt", null], + ["032410017017", "Springe, Stadt", null], + ["032410018018", "Uetze", null], + ["032410019019", "Wedemark", null], + ["032410020020", "Wennigsen (Deister)", null], + ["032410021021", "Wunstorf, Stadt", null], + ["032510007007", "Bassum, Stadt", null], + ["032510012012", "Diepholz, Stadt", null], + ["032510037037", "Stuhr", null], + ["032510040040", "Sulingen, Stadt", null], + ["032510041041", "Syke, Stadt", null], + ["032510042042", "Twistringen, Stadt", null], + ["032510044044", "Wagenfeld", null], + ["032510047047", "Weyhe", null], + ["032515401009", "Brockum", null], + ["032515401020", "Hüde", null], + ["032515401022", "Lembruch", null], + ["032515401023", "Lemförde, Flecken", null], + ["032515401025", "Marl", null], + ["032515401029", "Quernheim", null], + ["032515401036", "Stemshorn", null], + ["032515402005", "Barnstorf, Flecken", null], + ["032515402013", "Drebber", null], + ["032515402014", "Drentwede", null], + ["032515402017", "Eydelstedt", null], + ["032515403002", "Asendorf", null], + ["032515403026", "Martfeld", null], + ["032515403033", "Schwarme", null], + ["032515403049", "Bruchhausen-Vilsen, Flecken", null], + ["032515404003", "Bahrenborstel", null], + ["032515404004", "Barenburg, Flecken", null], + ["032515404018", "Freistatt", null], + ["032515404021", "Kirchdorf", null], + ["032515404043", "Varrel", null], + ["032515404045", "Wehrbleck", null], + ["032515405006", "Barver", null], + ["032515405011", "Dickel", null], + ["032515405019", "Hemsloh", null], + ["032515405030", "Rehden", null], + ["032515405046", "Wetschen", null], + ["032515406001", "Affinghausen", null], + ["032515406015", "Ehrenburg", null], + ["032515406028", "Neuenkirchen", null], + ["032515406031", "Scholen", null], + ["032515406032", "Schwaförden", null], + ["032515406038", "Sudwalde", null], + ["032515407008", "Borstel", null], + ["032515407024", "Maasen", null], + ["032515407027", "Mellinghausen", null], + ["032515407034", "Siedenburg, Flecken", null], + ["032515407035", "Staffhorst", null], + ["032520001001", "Aerzen, Flecken", null], + ["032520002002", "Bad Münder am Deister, Stadt", null], + ["032520003003", "Bad Pyrmont, Stadt", null], + ["032520004004", "Coppenbrügge, Flecken", null], + ["032520005005", "Emmerthal", null], + ["032520006006", "Hameln, Stadt", null], + ["032520007007", "Hessisch Oldendorf, Stadt", null], + ["032520008008", "Salzhemmendorf, Flecken", null], + ["032540002002", "Alfeld (Leine), Stadt", null], + ["032540003003", "Algermissen", null], + ["032540005005", "Bad Salzdetfurth, Stadt", null], + ["032540008008", "Bockenem, Stadt", null], + ["032540011011", "Diekholzen", null], + ["032540014014", "Elze, Stadt", null], + ["032540017017", "Giesen", null], + ["032540020020", "Harsum", null], + ["032540021021", "Hildesheim, Stadt", null], + ["032540022022", "Holle", null], + ["032540026026", "Nordstemmen", null], + ["032540028028", "Sarstedt, Stadt", null], + ["032540029029", "Schellerten", null], + ["032540032032", "Söhlde", null], + ["032540042042", "Freden (Leine)", null], + ["032540044044", "Lamspringe", null], + ["032540045045", "Sibbesse", null], + ["032545406013", "Eime, Flecken", null], + ["032545406041", "Duingen, Flecken", null], + ["032545406043", "Gronau (Leine), Stadt", null], + ["032550008008", "Delligsen, Flecken", null], + ["032550023023", "Holzminden, Stadt", null], + ["032555401002", "Bevern, Flecken", null], + ["032555401015", "Golmbach", null], + ["032555401021", "Holenberg", null], + ["032555401030", "Negenborn", null], + ["032555403004", "Boffzen", null], + ["032555403009", "Derental", null], + ["032555403014", "Fürstenberg", null], + ["032555403026", "Lauenförde, Flecken", null], + ["032555408003", "Bodenwerder, Münchhausenstadt", null], + ["032555408005", "Brevörde", null], + ["032555408016", "Halle", null], + ["032555408017", "Hehlen", null], + ["032555408019", "Heinsen", null], + ["032555408020", "Heyen", null], + ["032555408025", "Kirchbrak", null], + ["032555408031", "Ottenstein, Flecken", null], + ["032555408032", "Pegestorf", null], + ["032555408033", "Polle, Flecken", null], + ["032555408035", "Vahlbruch", null], + ["032555409001", "Arholzen", null], + ["032555409007", "Deensen", null], + ["032555409010", "Dielmissen", null], + ["032555409012", "Eimen", null], + ["032555409013", "Eschershausen, Stadt", null], + ["032555409018", "Heinade", null], + ["032555409022", "Holzen", null], + ["032555409027", "Lenne", null], + ["032555409028", "Lüerdissen", null], + ["032555409034", "Stadtoldendorf, Stadt", null], + ["032555409036", "Wangelnstedt", null], + ["032559501501", "Boffzen, gemfr. Gebiet", null], + ["032559502502", "Eimen, gemfr. Gebiet", null], + ["032559503503", "Eschershausen, gemfr. Gebiet", null], + ["032559504504", "Grünenplan, gemfr. Gebiet", null], + ["032559505505", "Holzminden, gemfr. Gebiet", null], + ["032559506506", "Merxhausen, gemfr. Gebiet", null], + ["032559508508", "Wenzen, gemfr. Gebiet", null], + ["032560022022", "Nienburg (Weser), Stadt", null], + ["032560025025", "Rehburg-Loccum, Stadt", null], + ["032560030030", "Steyerberg, Flecken", null], + ["032565402005", "Drakenburg, Flecken", null], + ["032565402011", "Haßbergen", null], + ["032565402012", "Heemsen", null], + ["032565402027", "Rohrsen", null], + ["032565405002", "Binnen", null], + ["032565405019", "Liebenau, Flecken", null], + ["032565405023", "Pennigsehl", null], + ["032565406001", "Balge", null], + ["032565406021", "Marklohe", null], + ["032565406036", "Wietzen", null], + ["032565407020", "Linsburg", null], + ["032565407026", "Rodewald", null], + ["032565407029", "Steimbke", null], + ["032565407031", "Stöckse", null], + ["032565408004", "Diepenau, Flecken", null], + ["032565408024", "Raddestorf", null], + ["032565408033", "Uchte, Flecken", null], + ["032565408034", "Warmsen", null], + ["032565409003", "Bücken, Flecken", null], + ["032565409007", "Eystrup", null], + ["032565409008", "Gandesbergen", null], + ["032565409009", "Hämelhausen", null], + ["032565409010", "Hassel (Weser)", null], + ["032565409013", "Hilgermissen", null], + ["032565409014", "Hoya, Stadt", null], + ["032565409015", "Hoyerhagen", null], + ["032565409028", "Schweringen", null], + ["032565409035", "Warpe", null], + ["032565410006", "Estorf", null], + ["032565410016", "Husum", null], + ["032565410017", "Landesbergen", null], + ["032565410018", "Leese", null], + ["032565410032", "Stolzenau", null], + ["032570003003", "Auetal", null], + ["032570009009", "Bückeburg, Stadt", null], + ["032570028028", "Obernkirchen, Stadt", null], + ["032570031031", "Rinteln, Stadt", null], + ["032570035035", "Stadthagen, Stadt", null], + ["032575401001", "Ahnsen", null], + ["032575401005", "Bad Eilsen", null], + ["032575401008", "Buchholz", null], + ["032575401012", "Heeßen", null], + ["032575401022", "Luhden", null], + ["032575402007", "Beckedorf", null], + ["032575402015", "Heuerßen", null], + ["032575402020", "Lindhorst", null], + ["032575402021", "Lüdersfeld", null], + ["032575403006", "Bad Nenndorf, Stadt", null], + ["032575403011", "Haste", null], + ["032575403016", "Hohnhorst", null], + ["032575403036", "Suthfeld", null], + ["032575404019", "Lauenhagen", null], + ["032575404023", "Meerbeck", null], + ["032575404025", "Niedernwöhren", null], + ["032575404027", "Nordsehl", null], + ["032575404030", "Pollhagen", null], + ["032575404037", "Wiedensahl, Flecken", null], + ["032575405013", "Helpsen", null], + ["032575405014", "Hespe", null], + ["032575405026", "Nienstädt", null], + ["032575405034", "Seggebruch", null], + ["032575406002", "Apelern", null], + ["032575406017", "Hülsede", null], + ["032575406018", "Lauenau, Flecken", null], + ["032575406024", "Messenkamp", null], + ["032575406029", "Pohle", null], + ["032575406032", "Rodenberg, Stadt", null], + ["032575407004", "Auhagen", null], + ["032575407010", "Hagenburg, Flecken", null], + ["032575407033", "Sachsenhagen, Stadt", null], + ["032575407038", "Wölpinghausen", null], + ["033510004004", "Bergen, Stadt", null], + ["033510006006", "Celle, Stadt", null], + ["033510010010", "Faßberg", null], + ["033510012012", "Hambühren", null], + ["033510023023", "Wietze", null], + ["033510024024", "Winsen (Aller)", null], + ["033510025025", "Eschede", null], + ["033510026026", "Südheide", null], + ["033515402005", "Bröckel", null], + ["033515402007", "Eicklingen", null], + ["033515402017", "Langlingen", null], + ["033515402022", "Wienhausen, Klostergemeinde", null], + ["033515403002", "Ahnsbeck", null], + ["033515403003", "Beedenbostel", null], + ["033515403008", "Eldingen", null], + ["033515403015", "Hohne", null], + ["033515403016", "Lachendorf", null], + ["033515404001", "Adelheidsdorf", null], + ["033515404018", "Nienhagen", null], + ["033515404021", "Wathlingen", null], + ["033519501501", "Lohheide, gemfr. Bezirk", null], + ["033520011011", "Cuxhaven, Stadt", null], + ["033520032032", "Loxstedt", null], + ["033520050050", "Schiffdorf", null], + ["033520059059", "Beverstedt", null], + ["033520060060", "Hagen im Bremischen", null], + ["033520061061", "Wurster Nordseeküste", null], + ["033520062062", "Geestland, Stadt", null], + ["033525404002", "Armstorf", null], + ["033525404024", "Hollnseth", null], + ["033525404029", "Lamstedt", null], + ["033525404036", "Mittelstenahe", null], + ["033525404052", "Stinstedt", null], + ["033525407020", "Hechthausen", null], + ["033525407022", "Hemmoor, Stadt", null], + ["033525407044", "Osten", null], + ["033525411004", "Belum", null], + ["033525411008", "Bülkau", null], + ["033525411025", "Ihlienworth", null], + ["033525411038", "Neuenkirchen", null], + ["033525411039", "Neuhaus (Oste), Flecken", null], + ["033525411041", "Nordleda", null], + ["033525411042", "Oberndorf", null], + ["033525411043", "Odisheim", null], + ["033525411045", "Osterbruch", null], + ["033525411046", "Otterndorf, Stadt", null], + ["033525411051", "Steinau", null], + ["033525411055", "Wanna", null], + ["033525411056", "Wingst", null], + ["033525411063", "Cadenberge", null], + ["033530005005", "Buchholz in der Nordheide, Stadt", null], + ["033530026026", "Neu Wulmstorf", null], + ["033530029029", "Rosengarten", null], + ["033530031031", "Seevetal", null], + ["033530032032", "Stelle", null], + ["033530040040", "Winsen (Luhe), Stadt", null], + ["033535401007", "Drage", null], + ["033535401023", "Marschacht", null], + ["033535401033", "Tespe", null], + ["033535402002", "Asendorf", null], + ["033535402004", "Brackel", null], + ["033535402009", "Egestorf", null], + ["033535402016", "Hanstedt", null], + ["033535402024", "Marxen", null], + ["033535402036", "Undeloh", null], + ["033535403001", "Appel", null], + ["033535403008", "Drestedt", null], + ["033535403014", "Halvesbostel", null], + ["033535403019", "Hollenstedt", null], + ["033535403025", "Moisburg", null], + ["033535403028", "Regesbostel", null], + ["033535403039", "Wenzendorf", null], + ["033535404003", "Bendestorf", null], + ["033535404017", "Harmstorf", null], + ["033535404020", "Jesteburg", null], + ["033535405010", "Eyendorf", null], + ["033535405011", "Garlstorf", null], + ["033535405012", "Garstedt", null], + ["033535405013", "Gödenstorf", null], + ["033535405030", "Salzhausen", null], + ["033535405034", "Toppenstedt", null], + ["033535405037", "Vierhöfen", null], + ["033535405042", "Wulfsen", null], + ["033535406006", "Dohren", null], + ["033535406015", "Handeloh", null], + ["033535406018", "Heidenau", null], + ["033535406021", "Kakenstorf", null], + ["033535406022", "Königsmoor", null], + ["033535406027", "Otter", null], + ["033535406035", "Tostedt", null], + ["033535406038", "Welle", null], + ["033535406041", "Wistedt", null], + ["033545403005", "Gartow, Flecken", null], + ["033545403007", "Gorleben", null], + ["033545403010", "Höhbeck", null], + ["033545403020", "Prezelle", null], + ["033545403021", "Schnackenburg, Stadt", null], + ["033545406003", "Damnatz", null], + ["033545406004", "Dannenberg (Elbe), Stadt", null], + ["033545406006", "Göhrde", null], + ["033545406008", "Gusborn", null], + ["033545406009", "Hitzacker (Elbe), Stadt", null], + ["033545406011", "Jameln", null], + ["033545406012", "Karwitz", null], + ["033545406014", "Langendorf", null], + ["033545406019", "Neu Darchau", null], + ["033545406027", "Zernien", null], + ["033545407001", "Bergen an der Dumme, Flecken", null], + ["033545407002", "Clenze, Flecken", null], + ["033545407013", "Küsten", null], + ["033545407015", "Lemgow", null], + ["033545407016", "Luckau (Wendland)", null], + ["033545407017", "Lübbow", null], + ["033545407018", "Lüchow (Wendland), Stadt", null], + ["033545407022", "Schnega", null], + ["033545407023", "Trebel", null], + ["033545407024", "Waddeweitz", null], + ["033545407025", "Woltersdorf", null], + ["033545407026", "Wustrow (Wendland), Stadt", null], + ["033549501501", "Gartow, gemfr. Gebiet", null], + ["033549502502", "Göhrde, gemfr. Gebiet", null], + ["033550001001", "Adendorf", null], + ["033550009009", "Bleckede, Stadt", null], + ["033550022022", "Lüneburg, Hansestadt", null], + ["033550049049", "Amt Neuhaus", null], + ["033555401002", "Amelinghausen", null], + ["033555401008", "Betzendorf", null], + ["033555401027", "Oldendorf (Luhe)", null], + ["033555401029", "Rehlingen", null], + ["033555401034", "Soderstorf", null], + ["033555402004", "Bardowick, Flecken", null], + ["033555402007", "Barum", null], + ["033555402017", "Handorf", null], + ["033555402023", "Mechtersen", null], + ["033555402028", "Radbruch", null], + ["033555402039", "Vögelsen", null], + ["033555402042", "Wittorf", null], + ["033555403010", "Boitze", null], + ["033555403012", "Dahlem", null], + ["033555403013", "Dahlenburg, Flecken", null], + ["033555403025", "Nahrendorf", null], + ["033555403037", "Tosterglope", null], + ["033555404020", "Kirchgellersen", null], + ["033555404031", "Reppenstedt", null], + ["033555404035", "Südergellersen", null], + ["033555404041", "Westergellersen", null], + ["033555405006", "Barnstedt", null], + ["033555405014", "Deutsch Evern", null], + ["033555405016", "Embsen", null], + ["033555405024", "Melbeck", null], + ["033555406005", "Barendorf", null], + ["033555406026", "Neetze", null], + ["033555406030", "Reinstorf", null], + ["033555406036", "Thomasburg", null], + ["033555406038", "Vastorf", null], + ["033555406040", "Wendisch Evern", null], + ["033555407003", "Artlenburg, Flecken", null], + ["033555407011", "Brietlingen", null], + ["033555407015", "Echem", null], + ["033555407018", "Hittbergen", null], + ["033555407019", "Hohnstorf (Elbe)", null], + ["033555407021", "Lüdersburg", null], + ["033555407032", "Rullstorf", null], + ["033555407033", "Scharnebeck", null], + ["033560002002", "Grasberg", null], + ["033560005005", "Lilienthal", null], + ["033560007007", "Osterholz-Scharmbeck, Stadt", null], + ["033560008008", "Ritterhude", null], + ["033560009009", "Schwanewede", null], + ["033560011011", "Worpswede", null], + ["033565401001", "Axstedt", null], + ["033565401003", "Hambergen", null], + ["033565401004", "Holste", null], + ["033565401006", "Lübberstedt", null], + ["033565401010", "Vollersode", null], + ["033570008008", "Bremervörde, Stadt", null], + ["033570016016", "Gnarrenburg", null], + ["033570039039", "Rotenburg (Wümme), Stadt", null], + ["033570041041", "Scheeßel", null], + ["033570051051", "Visselhövede, Stadt", null], + ["033575401006", "Bothel", null], + ["033575401009", "Brockel", null], + ["033575401024", "Hemsbünde", null], + ["033575401025", "Hemslingen", null], + ["033575401031", "Kirchwalsede", null], + ["033575401054", "Westerwalsede", null], + ["033575402015", "Fintel", null], + ["033575402023", "Helvesiek", null], + ["033575402033", "Lauenbrück", null], + ["033575402046", "Stemmen", null], + ["033575402049", "Vahlde", null], + ["033575403002", "Alfstedt", null], + ["033575403004", "Basdahl", null], + ["033575403012", "Ebersdorf", null], + ["033575403027", "Hipstedt", null], + ["033575403035", "Oerel", null], + ["033575404003", "Anderlingen", null], + ["033575404011", "Deinstedt", null], + ["033575404014", "Farven", null], + ["033575404036", "Ostereistedt", null], + ["033575404038", "Rhade", null], + ["033575404040", "Sandbostel", null], + ["033575404042", "Seedorf", null], + ["033575404043", "Selsingen", null], + ["033575405017", "Groß Meckelsen", null], + ["033575405019", "Hamersen", null], + ["033575405029", "Kalbe", null], + ["033575405032", "Klein Meckelsen", null], + ["033575405034", "Lengenbostel", null], + ["033575405044", "Sittensen", null], + ["033575405048", "Tiste", null], + ["033575405050", "Vierden", null], + ["033575405056", "Wohnste", null], + ["033575406001", "Ahausen", null], + ["033575406005", "Bötersen", null], + ["033575406020", "Hassendorf", null], + ["033575406022", "Hellwege", null], + ["033575406028", "Horstedt", null], + ["033575406037", "Reeßum", null], + ["033575406045", "Sottrum", null], + ["033575407007", "Breddorf", null], + ["033575407010", "Bülstedt", null], + ["033575407026", "Hepstedt", null], + ["033575407030", "Kirchtimke", null], + ["033575407047", "Tarmstedt", null], + ["033575407052", "Vorwerk", null], + ["033575407053", "Westertimke", null], + ["033575407055", "Wilstedt", null], + ["033575408013", "Elsdorf", null], + ["033575408018", "Gyhum", null], + ["033575408021", "Heeslingen", null], + ["033575408057", "Zeven, Stadt", null], + ["033580002002", "Bispingen", null], + ["033580008008", "Bad Fallingbostel, Stadt", null], + ["033580016016", "Munster, Stadt", null], + ["033580017017", "Neuenkirchen", null], + ["033580019019", "Schneverdingen, Stadt", null], + ["033580021021", "Soltau, Stadt", null], + ["033580023023", "Wietzendorf", null], + ["033580024024", "Walsrode, Stadt", null], + ["033585401001", "Ahlden (Aller), Flecken", null], + ["033585401006", "Eickeloh", null], + ["033585401011", "Grethem", null], + ["033585401012", "Hademstorf", null], + ["033585401014", "Hodenhagen", null], + ["033585402003", "Böhme", null], + ["033585402009", "Frankenfeld", null], + ["033585402013", "Häuslingen", null], + ["033585402018", "Rethem (Aller), Stadt", null], + ["033585403005", "Buchholz (Aller)", null], + ["033585403007", "Essel", null], + ["033585403010", "Gilten", null], + ["033585403015", "Lindwedel", null], + ["033585403020", "Schwarmstedt", null], + ["033589501501", "Osterheide, gemfr. Bezirk", null], + ["033590010010", "Buxtehude, Hansestadt", null], + ["033590013013", "Drochtersen", null], + ["033590028028", "Jork", null], + ["033590038038", "Stade, Hansestadt", null], + ["033595401003", "Apensen", null], + ["033595401006", "Beckdorf", null], + ["033595401037", "Sauensiek", null], + ["033595402011", "Deinste", null], + ["033595402017", "Fredenbeck", null], + ["033595402031", "Kutenholz", null], + ["033595403002", "Ahlerstedt", null], + ["033595403005", "Bargstedt", null], + ["033595403008", "Brest", null], + ["033595403023", "Harsefeld, Flecken", null], + ["033595405001", "Agathenburg", null], + ["033595405007", "Bliedersdorf", null], + ["033595405012", "Dollern", null], + ["033595405027", "Horneburg, Flecken", null], + ["033595405034", "Nottensdorf", null], + ["033595406020", "Grünendeich", null], + ["033595406021", "Guderhandviertel", null], + ["033595406026", "Hollern-Twielenfleth", null], + ["033595406032", "Mittelnkirchen", null], + ["033595406033", "Neuenkirchen", null], + ["033595406039", "Steinkirchen", null], + ["033595407004", "Balje", null], + ["033595407018", "Freiburg (Elbe), Flecken", null], + ["033595407030", "Krummendeich", null], + ["033595407035", "Oederquart", null], + ["033595407040", "Wischhafen", null], + ["033595409009", "Burweg", null], + ["033595409014", "Düdenbüttel", null], + ["033595409015", "Engelschoff", null], + ["033595409016", "Estorf", null], + ["033595409019", "Großenwörden", null], + ["033595409022", "Hammah", null], + ["033595409024", "Heinbockel", null], + ["033595409025", "Himmelpforten", null], + ["033595409029", "Kranenburg", null], + ["033595409036", "Oldendorf", null], + ["033600004004", "Bienenbüttel", null], + ["033600025025", "Uelzen, Hansestadt", null], + ["033605404015", "Oetzen", null], + ["033605404016", "Rätzlingen", null], + ["033605404018", "Rosche", null], + ["033605404022", "Stoetze", null], + ["033605404024", "Suhlendorf", null], + ["033605405007", "Eimke", null], + ["033605405009", "Gerdau", null], + ["033605405023", "Suderburg", null], + ["033605407001", "Altenmedingen", null], + ["033605407002", "Bad Bevensen, Stadt", null], + ["033605407003", "Barum", null], + ["033605407006", "Ebstorf,Klosterflecken", null], + ["033605407008", "Emmendorf", null], + ["033605407010", "Hanstedt", null], + ["033605407011", "Himbergen", null], + ["033605407012", "Jelmstorf", null], + ["033605407014", "Natendorf", null], + ["033605407017", "Römstedt", null], + ["033605407019", "Schwienau", null], + ["033605407026", "Weste", null], + ["033605407029", "Wriedel", null], + ["033605408005", "Bad Bodenteich, Flecken", null], + ["033605408013", "Lüder", null], + ["033605408020", "Soltendieck", null], + ["033605408030", "Wrestedt", null], + ["033610001001", "Achim, Stadt", null], + ["033610003003", "Dörverden", null], + ["033610005005", "Kirchlinteln", null], + ["033610006006", "Langwedel, Flecken", null], + ["033610008008", "Ottersberg, Flecken", null], + ["033610009009", "Oyten", null], + ["033610012012", "Verden (Aller), Stadt", null], + ["033615401002", "Blender", null], + ["033615401004", "Emtinghausen", null], + ["033615401010", "Riede", null], + ["033615401013", "Thedinghausen", null], + ["034010000000", "Delmenhorst, Stadt", null], + ["034020000000", "Emden, Stadt", null], + ["034030000000", "Oldenburg (Oldenburg), Stadt", null], + ["034040000000", "Osnabrück, Stadt", null], + ["034050000000", "Wilhelmshaven, Stadt", null], + ["034510001001", "Apen", null], + ["034510002002", "Bad Zwischenahn", null], + ["034510004004", "Edewecht", null], + ["034510005005", "Rastede", null], + ["034510007007", "Westerstede, Stadt", null], + ["034510008008", "Wiefelstede", null], + ["034520001001", "Aurich, Stadt", null], + ["034520002002", "Baltrum", null], + ["034520006006", "Großefehn", null], + ["034520007007", "Großheide", null], + ["034520011011", "Hinte", null], + ["034520012012", "Ihlow", null], + ["034520013013", "Juist, Inselgemeinde", null], + ["034520014014", "Krummhörn", null], + ["034520019019", "Norden, Stadt", null], + ["034520020020", "Norderney, Stadt", null], + ["034520023023", "Südbrookmerland", null], + ["034520025025", "Wiesmoor, Stadt", null], + ["034520027027", "Dornum", null], + ["034525401015", "Leezdorf", null], + ["034525401017", "Marienhafe, Flecken", null], + ["034525401021", "Osteel", null], + ["034525401022", "Rechtsupweg", null], + ["034525401024", "Upgant-Schott", null], + ["034525401026", "Wirdum", null], + ["034525403003", "Berumbur", null], + ["034525403008", "Hage, Flecken", null], + ["034525403009", "Hagermarsch", null], + ["034525403010", "Halbemond", null], + ["034525403016", "Lütetsburg", null], + ["034529501501", "Nordseeinsel Memmert, gemfr. Gebiet", null], + ["034530001001", "Barßel", null], + ["034530002002", "Bösel", null], + ["034530003003", "Cappeln (Oldenburg)", null], + ["034530004004", "Cloppenburg, Stadt", null], + ["034530005005", "Emstek", null], + ["034530006006", "Essen (Oldenburg)", null], + ["034530007007", "Friesoythe, Stadt", null], + ["034530008008", "Garrel", null], + ["034530009009", "Lastrup", null], + ["034530010010", "Lindern (Oldenburg)", null], + ["034530011011", "Löningen, Stadt", null], + ["034530012012", "Molbergen", null], + ["034530013013", "Saterland", null], + ["034540010010", "Emsbüren", null], + ["034540014014", "Geeste", null], + ["034540018018", "Haren (Ems), Stadt", null], + ["034540019019", "Haselünne, Stadt", null], + ["034540032032", "Lingen (Ems), Stadt", null], + ["034540035035", "Meppen, Stadt", null], + ["034540041041", "Papenburg, Stadt", null], + ["034540044044", "Rhede (Ems)", null], + ["034540045045", "Salzbergen", null], + ["034540054054", "Twist", null], + ["034545401007", "Dersum", null], + ["034545401008", "Dörpen", null], + ["034545401020", "Heede", null], + ["034545401025", "Kluse", null], + ["034545401030", "Lehe", null], + ["034545401037", "Neubörger", null], + ["034545401038", "Neulehe", null], + ["034545401056", "Walchum", null], + ["034545401060", "Wippingen", null], + ["034545402001", "Andervenne", null], + ["034545402003", "Beesten", null], + ["034545402012", "Freren, Stadt", null], + ["034545402036", "Messingen", null], + ["034545402053", "Thuine", null], + ["034545403009", "Dohren", null], + ["034545403021", "Herzlake", null], + ["034545403026", "Lähden", null], + ["034545404013", "Fresenburg", null], + ["034545404029", "Lathen", null], + ["034545404039", "Niederlangen", null], + ["034545404040", "Oberlangen", null], + ["034545404043", "Renkenberge", null], + ["034545404052", "Sustrum", null], + ["034545405002", "Bawinkel", null], + ["034545405015", "Gersten", null], + ["034545405017", "Handrup", null], + ["034545405028", "Langen", null], + ["034545405031", "Lengerich", null], + ["034545405059", "Wettrup", null], + ["034545406004", "Bockhorst", null], + ["034545406006", "Breddenberg", null], + ["034545406011", "Esterwegen", null], + ["034545406022", "Hilkenbrook", null], + ["034545406051", "Surwold", null], + ["034545407005", "Börger", null], + ["034545407016", "Groß Berßen", null], + ["034545407023", "Hüven", null], + ["034545407024", "Klein Berßen", null], + ["034545407047", "Sögel", null], + ["034545407048", "Spahnharrenstätte", null], + ["034545407050", "Stavern", null], + ["034545407058", "Werpeloh", null], + ["034545408034", "Lünne", null], + ["034545408046", "Schapen", null], + ["034545408049", "Spelle", null], + ["034545409027", "Lahn", null], + ["034545409033", "Lorup", null], + ["034545409042", "Rastdorf", null], + ["034545409055", "Vrees", null], + ["034545409057", "Werlte, Stadt", null], + ["034550007007", "Jever, Stadt", null], + ["034550014014", "Sande", null], + ["034550015015", "Schortens, Stadt", null], + ["034550020020", "Wangerland", null], + ["034550021021", "Wangerooge, Nordseebad", null], + ["034550025025", "Bockhorn", null], + ["034550026026", "Varel, Stadt", null], + ["034550027027", "Zetel", null], + ["034560001001", "Bad Bentheim, Stadt", null], + ["034560015015", "Nordhorn, Stadt", null], + ["034560025025", "Wietmarschen", null], + ["034565401002", "Emlichheim", null], + ["034565401009", "Hoogstede", null], + ["034565401012", "Laar", null], + ["034565401019", "Ringe", null], + ["034565402004", "Esche", null], + ["034565402005", "Georgsdorf", null], + ["034565402013", "Lage", null], + ["034565402014", "Neuenhaus, Stadt", null], + ["034565402017", "Osterwald", null], + ["034565403003", "Engden", null], + ["034565403010", "Isterberg", null], + ["034565403016", "Ohne", null], + ["034565403018", "Quendorf", null], + ["034565403020", "Samern", null], + ["034565403027", "Schüttorf, Stadt", null], + ["034565404006", "Getelo", null], + ["034565404007", "Gölenkamp", null], + ["034565404008", "Halle", null], + ["034565404011", "Itterbeck", null], + ["034565404023", "Uelsen", null], + ["034565404024", "Wielen", null], + ["034565404026", "Wilsum", null], + ["034570002002", "Borkum, Stadt", null], + ["034570012012", "Jemgum", null], + ["034570013013", "Leer (Ostfriesland), Stadt", null], + ["034570014014", "Moormerland", null], + ["034570017017", "Ostrhauderfehn", null], + ["034570018018", "Rhauderfehn", null], + ["034570020020", "Uplengen", null], + ["034570021021", "Weener, Stadt", null], + ["034570022022", "Westoverledingen", null], + ["034570024024", "Bunde", null], + ["034575402003", "Brinkum", null], + ["034575402009", "Firrel", null], + ["034575402010", "Hesel", null], + ["034575402011", "Holtland", null], + ["034575402015", "Neukamperfehn", null], + ["034575402019", "Schwerinsdorf", null], + ["034575403006", "Detern, Flecken", null], + ["034575403008", "Filsum", null], + ["034575403016", "Nortmoor", null], + ["034579501501", "Insel Lütje Hörn, gemfr. Gebiet", null], + ["034580003003", "Dötlingen", null], + ["034580005005", "Ganderkesee", null], + ["034580007007", "Großenkneten", null], + ["034580009009", "Hatten", null], + ["034580010010", "Hude (Oldb)", null], + ["034580013013", "Wardenburg", null], + ["034580014014", "Wildeshausen, Stadt", null], + ["034585401001", "Beckeln", null], + ["034585401002", "Colnrade", null], + ["034585401004", "Dünsen", null], + ["034585401006", "Groß Ippener", null], + ["034585401008", "Harpstedt, Flecken", null], + ["034585401011", "Kirchseelte", null], + ["034585401012", "Prinzhöfte", null], + ["034585401015", "Winkelsett", null], + ["034590003003", "Bad Essen", null], + ["034590004004", "Bad Iburg, Stadt", null], + ["034590005005", "Bad Laer", null], + ["034590006006", "Bad Rothenfelde", null], + ["034590008008", "Belm", null], + ["034590012012", "Bissendorf", null], + ["034590013013", "Bohmte", null], + ["034590014014", "Bramsche, Stadt", null], + ["034590015015", "Dissen am Teutoburger Wald, Stadt", null], + ["034590019019", "Georgsmarienhütte, Stadt", null], + ["034590020020", "Hagen am Teutoburger Wald", null], + ["034590021021", "Hasbergen", null], + ["034590022022", "Hilter am Teutoburger Wald", null], + ["034590024024", "Melle, Stadt", null], + ["034590029029", "Ostercappeln", null], + ["034590033033", "Wallenhorst", null], + ["034590034034", "Glandorf", null], + ["034595401007", "Badbergen", null], + ["034595401025", "Menslage", null], + ["034595401028", "Nortrup", null], + ["034595401030", "Quakenbrück, Stadt", null], + ["034595402001", "Alfhausen", null], + ["034595402002", "Ankum", null], + ["034595402010", "Bersenbrück, Stadt", null], + ["034595402016", "Eggermühlen", null], + ["034595402018", "Gehrde", null], + ["034595402023", "Kettenkamp", null], + ["034595402031", "Rieste", null], + ["034595403009", "Berge", null], + ["034595403011", "Bippen", null], + ["034595403017", "Fürstenau, Stadt", null], + ["034595404026", "Merzen", null], + ["034595404027", "Neuenkirchen", null], + ["034595404032", "Voltlage", null], + ["034600001001", "Bakum", null], + ["034600002002", "Damme, Stadt", null], + ["034600003003", "Dinklage, Stadt", null], + ["034600004004", "Goldenstedt", null], + ["034600005005", "Holdorf", null], + ["034600006006", "Lohne (Oldenburg), Stadt", null], + ["034600007007", "Neuenkirchen-Vörden", null], + ["034600008008", "Steinfeld (Oldenburg)", null], + ["034600009009", "Vechta, Stadt", null], + ["034600010010", "Visbek", null], + ["034610001001", "Berne", null], + ["034610002002", "Brake (Unterweser), Stadt", null], + ["034610003003", "Butjadingen", null], + ["034610004004", "Elsfleth, Stadt", null], + ["034610005005", "Jade", null], + ["034610006006", "Lemwerder", null], + ["034610007007", "Nordenham, Stadt", null], + ["034610008008", "Ovelgönne", null], + ["034610009009", "Stadland", null], + ["034620005005", "Friedeburg", null], + ["034620007007", "Langeoog", null], + ["034620014014", "Spiekeroog", null], + ["034620019019", "Wittmund, Stadt", null], + ["034625401002", "Dunum", null], + ["034625401003", "Esens, Stadt", null], + ["034625401006", "Holtgast", null], + ["034625401008", "Moorweg", null], + ["034625401010", "Neuharlingersiel", null], + ["034625401015", "Stedesdorf", null], + ["034625401017", "Werdum", null], + ["034625402001", "Blomberg", null], + ["034625402004", "Eversmeer", null], + ["034625402009", "Nenndorf", null], + ["034625402011", "Neuschoo", null], + ["034625402012", "Ochtersum", null], + ["034625402013", "Schweindorf", null], + ["034625402016", "Utarp", null], + ["034625402018", "Westerholt", null], + ["039019999999", "Nds-Küstengewässer(Gemarkung Nordsee)", null], + ["040110000000", "Bremen, Stadt", null], + ["040110111111", "Altstadt", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["040110112112", "Bahnhofsvorstadt", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["040110113113", "Ostertor", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["040110122122", "Industriehäfen", "Stadt-/Ortsteil bzw. Stadtbezirk"], + [ + "040110123123", + "Stadtbremisches Überseehafengebiet Bremerhaven", + "Stadt-/Ortsteil bzw. Stadtbezirk" + ], + ["040110124124", "Neustädter Hafen", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["040110125125", "Hohentorshafen", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["040110211211", "Alte Neustadt", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["040110212212", "Hohentor", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["040110213213", "Neustadt", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["040110214214", "Südervorstadt", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["040110215215", "Gartenstadt Süd", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["040110216216", "Buntentor", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["040110217217", "Neuenland", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["040110218218", "Huckelriede", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["040110231231", "Habenhausen", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["040110232232", "Arsten", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["040110233233", "Kattenturm", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["040110234234", "Kattenesch", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["040110241241", "Mittelshuchting", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["040110242242", "Sodenmatt", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["040110243243", "Kirchhuchting", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["040110244244", "Grolland", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["040110251251", "Woltmershausen", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["040110252252", "Rablinghausen", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["040110261261", "Seehausen", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["040110271271", "Strom", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["040110311311", "Steintor", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["040110312312", "Fesenfeld", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["040110313313", "Peterswerder", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["040110314314", "Hulsberg", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["040110321321", "Neu-Schwachhausen", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["040110322322", "Bürgerpark", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["040110323323", "Barkhof", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["040110324324", "Riensberg", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["040110325325", "Radio Bremen", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["040110326326", "Schwachhausen", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["040110327327", "Gete", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["040110331331", "Gartenstadt Vahr", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["040110332332", "Neue Vahr Nord", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["040110334334", "Neue Vahr Südwest", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["040110335335", "Neue Vahr Südost", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["040110341341", "Horn", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["040110342342", "Lehe", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["040110343343", "Lehesterdeich", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["040110351351", "Borgfeld", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["040110361361", "Oberneuland", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["040110371371", "Ellener Feld", "Stadt-/Ortsteil bzw. Stadtbezirk"], + [ + "040110372372", + "Ellenerbrok-Schevemoor", + "Stadt-/Ortsteil bzw. Stadtbezirk" + ], + ["040110373373", "Tenever", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["040110374374", "Osterholz", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["040110375375", "Blockdiek", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["040110381381", "Sebaldsbrück", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["040110382382", "Hastedt", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["040110383383", "Hemelingen", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["040110384384", "Arbergen", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["040110385385", "Mahndorf", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["040110411411", "Blockland", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["040110421421", "Regensburger Straße", "Stadt-/Ortsteil bzw. Stadtbezirk"], + [ + "040110422422", + "Findorff-Bürgerweide", + "Stadt-/Ortsteil bzw. Stadtbezirk" + ], + ["040110423423", "Weidedamm", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["040110424424", "In den Hufen", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["040110431431", "Utbremen", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["040110432432", "Steffensweg", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["040110433433", "Westend", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["040110434434", "Walle", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["040110435435", "Osterfeuerberg", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["040110436436", "Hohweg", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["040110437437", "Überseestadt", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["040110441441", "Lindenhof", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["040110442442", "Gröpelingen", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["040110443443", "Ohlenhof", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["040110444444", "In den Wischen", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["040110445445", "Oslebshausen", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["040110511511", "Burg-Grambke", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["040110512512", "Werderland", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["040110513513", "Burgdamm", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["040110514514", "Lesum", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["040110515515", "St. Magnus", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["040110521521", "Vegesack", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["040110522522", "Grohn", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["040110523523", "Schönebeck", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["040110524524", "Aumund-Hammersbeck", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["040110525525", "Fähr-Lobbendorf", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["040110531531", "Blumenthal", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["040110532532", "Rönnebeck", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["040110533533", "Lüssum-Bockhorn", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["040110534534", "Farge", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["040110535535", "Rekum", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["040120000000", "Bremerhaven, Stadt", null], + ["051110000000", "Düsseldorf, Stadt", null], + ["051120000000", "Duisburg, Stadt", null], + ["051130000000", "Essen, Stadt", null], + ["051140000000", "Krefeld, Stadt", null], + ["051160000000", "Mönchengladbach, Stadt", null], + ["051170000000", "Mülheim an der Ruhr, Stadt", null], + ["051190000000", "Oberhausen, Stadt", null], + ["051200000000", "Remscheid, Stadt", null], + ["051220000000", "Solingen, Klingenstadt", null], + ["051240000000", "Wuppertal, Stadt", null], + ["051540004004", "Bedburg-Hau", null], + ["051540008008", "Emmerich am Rhein, Stadt", null], + ["051540012012", "Geldern, Stadt", null], + ["051540016016", "Goch, Stadt", null], + ["051540020020", "Issum", null], + ["051540024024", "Kalkar, Stadt", null], + ["051540028028", "Kerken", null], + ["051540032032", "Kevelaer, Stadt", null], + ["051540036036", "Kleve, Stadt", null], + ["051540040040", "Kranenburg", null], + ["051540044044", "Rees, Stadt", null], + ["051540048048", "Rheurdt", null], + ["051540052052", "Straelen, Stadt", null], + ["051540056056", "Uedem", null], + ["051540060060", "Wachtendonk", null], + ["051540064064", "Weeze", null], + ["051580004004", "Erkrath, Fundort des Neanderthalers, Stadt", null], + ["051580008008", "Haan, Stadt", null], + ["051580012012", "Heiligenhaus, Stadt", null], + ["051580016016", "Hilden, Stadt", null], + ["051580020020", "Langenfeld (Rheinland), Stadt", null], + ["051580024024", "Mettmann, Stadt", null], + ["051580026026", "Monheim am Rhein, Stadt", null], + ["051580028028", "Ratingen, Stadt", null], + ["051580032032", "Velbert, Stadt", null], + ["051580036036", "Wülfrath, Stadt", null], + ["051620004004", "Dormagen, Stadt", null], + ["051620008008", "Grevenbroich, Stadt", null], + ["051620012012", "Jüchen, Stadt", null], + ["051620016016", "Kaarst, Stadt", null], + ["051620020020", "Korschenbroich, Stadt", null], + ["051620022022", "Meerbusch, Stadt", null], + ["051620024024", "Neuss, Stadt", null], + ["051620028028", "Rommerskirchen", null], + ["051660004004", "Brüggen, Burggemeinde", null], + ["051660008008", "Grefrath, Sport- und Freizeitgemeinde", null], + ["051660012012", "Kempen, Stadt", null], + ["051660016016", "Nettetal, Stadt", null], + ["051660020020", "Niederkrüchten", null], + ["051660024024", "Schwalmtal", null], + ["051660028028", "Tönisvorst, Stadt", null], + ["051660032032", "Viersen, Stadt", null], + ["051660036036", "Willich, Stadt", null], + ["051700004004", "Alpen", null], + ["051700008008", "Dinslaken, Stadt", null], + ["051700012012", "Hamminkeln, Stadt", null], + ["051700016016", "Hünxe", null], + ["051700020020", "Kamp-Lintfort, Stadt", null], + ["051700024024", "Moers, Stadt", null], + ["051700028028", "Neukirchen-Vluyn, Stadt", null], + ["051700032032", "Rheinberg, Stadt", null], + ["051700036036", "Schermbeck", null], + ["051700040040", "Sonsbeck", null], + ["051700044044", "Voerde (Niederrhein), Stadt", null], + ["051700048048", "Wesel, Stadt", null], + ["051700052052", "Xanten, Stadt", null], + ["053140000000", "Bonn, Stadt", null], + ["053150000000", "Köln, Stadt", null], + ["053160000000", "Leverkusen, Stadt", null], + ["053340002002", "Aachen, Stadt", null], + ["053340004004", "Alsdorf, Stadt", null], + ["053340008008", "Baesweiler, Stadt", null], + ["053340012012", "Eschweiler, Stadt", null], + ["053340016016", "Herzogenrath, Stadt", null], + ["053340020020", "Monschau, Stadt", null], + ["053340024024", "Roetgen, Tor zur Eifel", null], + ["053340028028", "Simmerath", null], + ["053340032032", "Stolberg (Rhld.), Kupferstadt", null], + ["053340036036", "Würselen, Stadt", null], + ["053580004004", "Aldenhoven", null], + ["053580008008", "Düren, Stadt", null], + ["053580012012", "Heimbach, Stadt", null], + ["053580016016", "Hürtgenwald", null], + ["053580020020", "Inden", null], + ["053580024024", "Jülich, Stadt", null], + ["053580028028", "Kreuzau", null], + ["053580032032", "Langerwehe", null], + ["053580036036", "Linnich, Stadt", null], + ["053580040040", "Merzenich", null], + ["053580044044", "Nideggen, Stadt", null], + ["053580048048", "Niederzier", null], + ["053580052052", "Nörvenich", null], + ["053580056056", "Titz", null], + ["053580060060", "Vettweiß", null], + ["053620004004", "Bedburg, Stadt", null], + ["053620008008", "Bergheim, Stadt", null], + ["053620012012", "Brühl, Stadt", null], + ["053620016016", "Elsdorf, Stadt", null], + ["053620020020", "Erftstadt, Stadt", null], + ["053620024024", "Frechen, Stadt", null], + ["053620028028", "Hürth, Stadt", null], + ["053620032032", "Kerpen, Kolpingstadt", null], + ["053620036036", "Pulheim, Stadt", null], + ["053620040040", "Wesseling, Stadt", null], + ["053660004004", "Bad Münstereifel, Stadt", null], + ["053660008008", "Blankenheim", null], + ["053660012012", "Dahlem", null], + ["053660016016", "Euskirchen, Stadt", null], + ["053660020020", "Hellenthal", null], + ["053660024024", "Kall", null], + ["053660028028", "Mechernich, Stadt", null], + ["053660032032", "Nettersheim", null], + ["053660036036", "Schleiden, Stadt", null], + ["053660040040", "Weilerswist", null], + ["053660044044", "Zülpich, Stadt", null], + ["053700004004", "Erkelenz, Stadt", null], + ["053700008008", "Gangelt", null], + ["053700012012", "Geilenkirchen, Stadt", null], + ["053700016016", "Heinsberg, Stadt", null], + ["053700020020", "Hückelhoven, Stadt", null], + ["053700024024", "Selfkant", null], + ["053700028028", "Übach-Palenberg, Stadt", null], + ["053700032032", "Waldfeucht", null], + ["053700036036", "Wassenberg, Stadt", null], + ["053700040040", "Wegberg, Stadt", null], + ["053740004004", "Bergneustadt, Stadt", null], + ["053740008008", "Engelskirchen", null], + ["053740012012", "Gummersbach, Stadt", null], + ["053740016016", "Hückeswagen, Schloss-Stadt", null], + ["053740020020", "Lindlar", null], + ["053740024024", "Marienheide", null], + ["053740028028", "Morsbach", null], + ["053740032032", "Nümbrecht", null], + ["053740036036", "Radevormwald, Stadt auf der Höhe", null], + ["053740040040", "Reichshof", null], + ["053740044044", "Waldbröl, Stadt", null], + ["053740048048", "Wiehl, Stadt", null], + ["053740052052", "Wipperfürth, Hansestadt", null], + ["053780004004", "Bergisch Gladbach, Stadt", null], + ["053780008008", "Burscheid, Stadt", null], + ["053780012012", "Kürten", null], + ["053780016016", "Leichlingen (Rheinland), Blütenstadt", null], + ["053780020020", "Odenthal", null], + ["053780024024", "Overath, Stadt", null], + ["053780028028", "Rösrath, Stadt", null], + ["053780032032", "Wermelskirchen, Stadt", null], + ["053820004004", "Alfter", null], + ["053820008008", "Bad Honnef, Stadt", null], + ["053820012012", "Bornheim, Stadt", null], + ["053820016016", "Eitorf", null], + ["053820020020", "Hennef (Sieg), Stadt", null], + ["053820024024", "Königswinter, Stadt", null], + ["053820028028", "Lohmar, Stadt", null], + ["053820032032", "Meckenheim, Stadt", null], + ["053820036036", "Much", null], + ["053820040040", "Neunkirchen-Seelscheid", null], + ["053820044044", "Niederkassel, Stadt", null], + ["053820048048", "Rheinbach, Stadt", null], + ["053820052052", "Ruppichteroth", null], + ["053820056056", "Sankt Augustin, Stadt", null], + ["053820060060", "Siegburg, Stadt", null], + ["053820064064", "Swisttal", null], + ["053820068068", "Troisdorf, Stadt", null], + ["053820072072", "Wachtberg", null], + ["053820076076", "Windeck", null], + ["055120000000", "Bottrop, Stadt", null], + ["055130000000", "Gelsenkirchen, Stadt", null], + ["055150000000", "Münster, Stadt", null], + ["055540004004", "Ahaus, Stadt", null], + ["055540008008", "Bocholt, Stadt", null], + ["055540012012", "Borken, Stadt", null], + ["055540016016", "Gescher, Glockenstadt", null], + ["055540020020", "Gronau (Westf.), Stadt", null], + ["055540024024", "Heek", null], + ["055540028028", "Heiden", null], + ["055540032032", "Isselburg, Stadt", null], + ["055540036036", "Legden", null], + ["055540040040", "Raesfeld", null], + ["055540044044", "Reken", null], + ["055540048048", "Rhede, Stadt", null], + ["055540052052", "Schöppingen", null], + ["055540056056", "Stadtlohn, Stadt", null], + ["055540060060", "Südlohn", null], + ["055540064064", "Velen, Stadt", null], + ["055540068068", "Vreden, Stadt", null], + ["055580004004", "Ascheberg", null], + ["055580008008", "Billerbeck, Stadt", null], + ["055580012012", "Coesfeld, Stadt", null], + ["055580016016", "Dülmen, Stadt", null], + ["055580020020", "Havixbeck", null], + ["055580024024", "Lüdinghausen, Stadt", null], + ["055580028028", "Nordkirchen", null], + ["055580032032", "Nottuln", null], + ["055580036036", "Olfen, Stadt", null], + ["055580040040", "Rosendahl", null], + ["055580044044", "Senden", null], + ["055620004004", "Castrop-Rauxel, Stadt", null], + ["055620008008", "Datteln, Stadt", null], + ["055620012012", "Dorsten, Stadt", null], + ["055620014014", "Gladbeck, Stadt", null], + ["055620016016", "Haltern am See, Stadt", null], + ["055620020020", "Herten, Stadt", null], + ["055620024024", "Marl, Stadt", null], + ["055620028028", "Oer-Erkenschwick, Stadt", null], + ["055620032032", "Recklinghausen, Stadt", null], + ["055620036036", "Waltrop, Stadt", null], + ["055660004004", "Altenberge", null], + ["055660008008", "Emsdetten, Stadt", null], + ["055660012012", "Greven, Stadt", null], + ["055660016016", "Hörstel, Stadt", null], + ["055660020020", "Hopsten", null], + ["055660024024", "Horstmar, Stadt der Burgmannshöfe", null], + ["055660028028", "Ibbenbüren, Stadt", null], + ["055660032032", "Ladbergen", null], + ["055660036036", "Laer", null], + ["055660040040", "Lengerich, Stadt", null], + ["055660044044", "Lienen", null], + ["055660048048", "Lotte", null], + ["055660052052", "Metelen", null], + ["055660056056", "Mettingen", null], + ["055660060060", "Neuenkirchen", null], + ["055660064064", "Nordwalde", null], + ["055660068068", "Ochtrup, Stadt", null], + ["055660072072", "Recke", null], + ["055660076076", "Rheine, Stadt", null], + ["055660080080", "Saerbeck, NRW-Klimakommune", null], + ["055660084084", "Steinfurt, Stadt", null], + ["055660088088", "Tecklenburg, Stadt", null], + ["055660092092", "Westerkappeln", null], + ["055660096096", "Wettringen", null], + ["055700004004", "Ahlen, Stadt", null], + ["055700008008", "Beckum, Stadt", null], + ["055700012012", "Beelen", null], + ["055700016016", "Drensteinfurt, Stadt", null], + ["055700020020", "Ennigerloh, Stadt", null], + ["055700024024", "Everswinkel", null], + ["055700028028", "Oelde, Stadt", null], + ["055700032032", "Ostbevern", null], + ["055700036036", "Sassenberg, Stadt", null], + ["055700040040", "Sendenhorst, Stadt", null], + ["055700044044", "Telgte, Stadt", null], + ["055700048048", "Wadersloh", null], + ["055700052052", "Warendorf, Stadt", null], + ["057110000000", "Bielefeld, Stadt", null], + ["057540004004", "Borgholzhausen, Stadt", null], + ["057540008008", "Gütersloh, Stadt", null], + ["057540012012", "Halle (Westf.), Stadt", null], + ["057540016016", "Harsewinkel, Die Mähdrescherstadt", null], + ["057540020020", "Herzebrock-Clarholz", null], + ["057540024024", "Langenberg", null], + ["057540028028", "Rheda-Wiedenbrück, Stadt", null], + ["057540032032", "Rietberg, Stadt", null], + ["057540036036", "Schloß Holte-Stukenbrock, Stadt", null], + ["057540040040", "Steinhagen", null], + ["057540044044", "Verl, Stadt", null], + ["057540048048", "Versmold, Stadt", null], + ["057540052052", "Werther (Westf.), Stadt", null], + ["057580004004", "Bünde, Stadt", null], + ["057580008008", "Enger, Widukindstadt", null], + ["057580012012", "Herford, Hansestadt", null], + ["057580016016", "Hiddenhausen", null], + ["057580020020", "Kirchlengern", null], + ["057580024024", "Löhne, Stadt", null], + ["057580028028", "Rödinghausen", null], + ["057580032032", "Spenge, Stadt", null], + ["057580036036", "Vlotho, Stadt", null], + ["057620004004", "Bad Driburg, Stadt", null], + ["057620008008", "Beverungen, Stadt", null], + ["057620012012", "Borgentreich, Orgelstadt", null], + ["057620016016", "Brakel, Stadt", null], + ["057620020020", "Höxter, Stadt", null], + ["057620024024", "Marienmünster, Stadt", null], + ["057620028028", "Nieheim, Stadt", null], + ["057620032032", "Steinheim, Stadt", null], + ["057620036036", "Warburg, Hansestadt", null], + ["057620040040", "Willebadessen, Stadt", null], + ["057660004004", "Augustdorf", null], + ["057660008008", "Bad Salzuflen, Stadt", null], + ["057660012012", "Barntrup, Stadt", null], + ["057660016016", "Blomberg, Stadt", null], + ["057660020020", "Detmold, Stadt", null], + ["057660024024", "Dörentrup", null], + ["057660028028", "Extertal", null], + ["057660032032", "Horn-Bad Meinberg, Stadt", null], + ["057660036036", "Kalletal", null], + ["057660040040", "Lage, Stadt", null], + ["057660044044", "Lemgo, Stadt", null], + ["057660048048", "Leopoldshöhe", null], + ["057660052052", "Lügde, Stadt der Osterräder", null], + ["057660056056", "Oerlinghausen, Stadt", null], + ["057660060060", "Schieder-Schwalenberg, Stadt", null], + ["057660064064", "Schlangen", null], + ["057700004004", "Bad Oeynhausen, Stadt", null], + ["057700008008", "Espelkamp, Stadt", null], + ["057700012012", "Hille", null], + ["057700016016", "Hüllhorst", null], + ["057700020020", "Lübbecke, Stadt", null], + ["057700024024", "Minden, Stadt", null], + ["057700028028", "Petershagen, Stadt", null], + ["057700032032", "Porta Westfalica, Stadt", null], + ["057700036036", "Preußisch Oldendorf, Stadt", null], + ["057700040040", "Rahden, Stadt", null], + ["057700044044", "Stemwede", null], + ["057740004004", "Altenbeken", null], + ["057740008008", "Bad Lippspringe, Stadt", null], + ["057740012012", "Borchen", null], + ["057740016016", "Büren, Stadt", null], + ["057740020020", "Delbrück, Stadt", null], + ["057740024024", "Hövelhof, Sennegemeinde", null], + ["057740028028", "Lichtenau, Stadt", null], + ["057740032032", "Paderborn, Stadt", null], + ["057740036036", "Salzkotten, Stadt", null], + ["057740040040", "Bad Wünnenberg, Stadt", null], + ["059110000000", "Bochum, Stadt", null], + ["059130000000", "Dortmund, Stadt", null], + ["059140000000", "Hagen, Stadt der FernUniversität", null], + ["059150000000", "Hamm, Stadt", null], + ["059160000000", "Herne, Stadt", null], + ["059540004004", "Breckerfeld, Hansestadt", null], + ["059540008008", "Ennepetal, Stadt der Kluterthöhle", null], + ["059540012012", "Gevelsberg, Stadt", null], + ["059540016016", "Hattingen, Stadt", null], + ["059540020020", "Herdecke, Stadt", null], + ["059540024024", "Schwelm, Stadt", null], + ["059540028028", "Sprockhövel, Stadt", null], + ["059540032032", "Wetter (Ruhr), Stadt", null], + ["059540036036", "Witten, Stadt", null], + ["059580004004", "Arnsberg, Stadt", null], + ["059580008008", "Bestwig", null], + ["059580012012", "Brilon, Stadt", null], + ["059580016016", "Eslohe (Sauerland)", null], + ["059580020020", "Hallenberg, Stadt", null], + ["059580024024", "Marsberg, Stadt", null], + ["059580028028", "Medebach, Hansestadt", null], + ["059580032032", "Meschede, Kreis- und Hochschulstadt", null], + ["059580036036", "Olsberg, Stadt", null], + ["059580040040", "Schmallenberg, Stadt", null], + ["059580044044", "Sundern (Sauerland), Stadt", null], + ["059580048048", "Winterberg, Stadt", null], + ["059620004004", "Altena, Stadt", null], + ["059620008008", "Balve, Stadt", null], + ["059620012012", "Halver, Stadt", null], + ["059620016016", "Hemer, Stadt", null], + ["059620020020", "Herscheid", null], + ["059620024024", "Iserlohn, Stadt", null], + ["059620028028", "Kierspe, Stadt", null], + ["059620032032", "Lüdenscheid, Stadt", null], + ["059620036036", "Meinerzhagen, Stadt", null], + ["059620040040", "Menden (Sauerland), Stadt", null], + ["059620044044", "Nachrodt-Wiblingwerde", null], + ["059620048048", "Neuenrade, Stadt", null], + ["059620052052", "Plettenberg, Stadt", null], + ["059620056056", "Schalksmühle", null], + ["059620060060", "Werdohl, Stadt", null], + ["059660004004", "Attendorn, Hansestadt", null], + ["059660008008", "Drolshagen, Stadt", null], + ["059660012012", "Finnentrop", null], + ["059660016016", "Kirchhundem", null], + ["059660020020", "Lennestadt, Stadt", null], + ["059660024024", "Olpe, Stadt", null], + ["059660028028", "Wenden", null], + ["059700004004", "Bad Berleburg, Stadt", null], + ["059700008008", "Burbach", null], + ["059700012012", "Erndtebrück", null], + ["059700016016", "Freudenberg, Stadt", null], + ["059700020020", "Hilchenbach, Stadt", null], + ["059700024024", "Kreuztal, Stadt", null], + ["059700028028", "Bad Laasphe, Stadt", null], + ["059700032032", "Netphen, Stadt", null], + ["059700036036", "Neunkirchen", null], + ["059700040040", "Siegen, Universitätsstadt", null], + ["059700044044", "Wilnsdorf", null], + ["059740004004", "Anröchte", null], + ["059740008008", "Bad Sassendorf", null], + ["059740012012", "Ense", null], + ["059740016016", "Erwitte, Stadt", null], + ["059740020020", "Geseke, Stadt", null], + ["059740024024", "Lippetal", null], + ["059740028028", "Lippstadt, Stadt", null], + ["059740032032", "Möhnesee", null], + ["059740036036", "Rüthen, Stadt", null], + ["059740040040", "Soest, Stadt", null], + ["059740044044", "Warstein, Stadt", null], + ["059740048048", "Welver", null], + ["059740052052", "Werl, Stadt", null], + ["059740056056", "Wickede (Ruhr)", null], + ["059780004004", "Bergkamen, Stadt", null], + ["059780008008", "Bönen", null], + ["059780012012", "Fröndenberg/Ruhr, Stadt", null], + ["059780016016", "Holzwickede", null], + ["059780020020", "Kamen, Stadt", null], + ["059780024024", "Lünen, Stadt", null], + ["059780028028", "Schwerte, Hansestadt an der Ruhr", null], + ["059780032032", "Selm, Stadt", null], + ["059780036036", "Unna, Stadt", null], + ["059780040040", "Werne, Stadt", null], + ["064110000000", "Darmstadt, Wissenschaftsstadt", null], + ["064120000000", "Frankfurt am Main, Stadt", null], + ["064130000000", "Offenbach am Main, Stadt", null], + ["064140000000", "Wiesbaden, Landeshauptstadt", null], + ["064310001001", "Abtsteinach", null], + ["064310002002", "Bensheim, Stadt", null], + ["064310003003", "Biblis", null], + ["064310004004", "Birkenau", null], + ["064310005005", "Bürstadt, Stadt", null], + ["064310006006", "Einhausen", null], + ["064310007007", "Fürth", null], + ["064310008008", "Gorxheimertal", null], + ["064310009009", "Grasellenbach", null], + ["064310010010", "Groß-Rohrheim", null], + ["064310011011", "Heppenheim (Bergstraße), Kreisstadt", null], + ["064310012012", "Hirschhorn (Neckar), Stadt", null], + ["064310013013", "Lampertheim, Stadt", null], + ["064310014014", "Lautertal (Odenwald)", null], + ["064310015015", "Lindenfels, Stadt", null], + ["064310016016", "Lorsch, Karolingerstadt", null], + ["064310017017", "Mörlenbach", null], + ["064310018018", "Neckarsteinach, Stadt", null], + ["064310019019", "Rimbach", null], + ["064310020020", "Viernheim, Stadt", null], + ["064310021021", "Wald-Michelbach", null], + ["064310022022", "Zwingenberg, Stadt", null], + ["064319200200", "Michelbuch, gemfr. Gebiet", null], + ["064320001001", "Alsbach-Hähnlein", null], + ["064320002002", "Babenhausen, Stadt", null], + ["064320003003", "Bickenbach", null], + ["064320004004", "Dieburg, Stadt", null], + ["064320005005", "Eppertshausen", null], + ["064320006006", "Erzhausen", null], + ["064320007007", "Fischbachtal", null], + ["064320008008", "Griesheim, Stadt", null], + ["064320009009", "Groß-Bieberau, Stadt", null], + ["064320010010", "Groß-Umstadt, Stadt", null], + ["064320011011", "Groß-Zimmern", null], + ["064320012012", "Messel", null], + ["064320013013", "Modautal", null], + ["064320014014", "Mühltal", null], + ["064320015015", "Münster (Hessen)", null], + ["064320016016", "Ober-Ramstadt, Stadt", null], + ["064320017017", "Otzberg", null], + ["064320018018", "Pfungstadt, Stadt", null], + ["064320019019", "Reinheim, Stadt", null], + ["064320020020", "Roßdorf", null], + ["064320021021", "Schaafheim", null], + ["064320022022", "Seeheim-Jugenheim", null], + ["064320023023", "Weiterstadt, Stadt", null], + ["064330001001", "Biebesheim am Rhein", null], + ["064330002002", "Bischofsheim", null], + ["064330003003", "Büttelborn", null], + ["064330004004", "Gernsheim, Schöfferstadt", null], + ["064330005005", "Ginsheim-Gustavsburg, Stadt", null], + ["064330006006", "Groß-Gerau, Stadt", null], + ["064330007007", "Kelsterbach, Stadt", null], + ["064330008008", "Mörfelden-Walldorf, Stadt", null], + ["064330009009", "Nauheim", null], + ["064330010010", "Raunheim, Stadt", null], + ["064330011011", "Riedstadt, Büchnerstadt", null], + ["064330012012", "Rüsselsheim am Main, Stadt", null], + ["064330013013", "Stockstadt am Rhein", null], + ["064330014014", "Trebur", null], + ["064340001001", "Bad Homburg v. d. Höhe, Stadt", null], + ["064340002002", "Friedrichsdorf, Stadt", null], + ["064340003003", "Glashütten", null], + ["064340004004", "Grävenwiesbach", null], + ["064340005005", "Königstein im Taunus, Stadt", null], + ["064340006006", "Kronberg im Taunus, Stadt", null], + ["064340007007", "Neu-Anspach, Stadt", null], + ["064340008008", "Oberursel (Taunus), Stadt", null], + ["064340009009", "Schmitten", null], + ["064340010010", "Steinbach (Taunus), Stadt", null], + ["064340011011", "Usingen, Stadt", null], + ["064340012012", "Wehrheim", null], + ["064340013013", "Weilrod", null], + ["064350001001", "Bad Orb, Stadt", null], + ["064350002002", "Bad Soden-Salmünster, Stadt", null], + ["064350003003", "Biebergemünd", null], + ["064350004004", "Birstein", null], + ["064350005005", "Brachttal", null], + ["064350006006", "Bruchköbel, Stadt", null], + ["064350007007", "Erlensee, Stadt", null], + ["064350008008", "Flörsbachtal", null], + ["064350009009", "Freigericht", null], + ["064350010010", "Gelnhausen, Barbarossast., Krst.", null], + ["064350011011", "Großkrotzenburg", null], + ["064350012012", "Gründau", null], + ["064350013013", "Hammersbach", null], + ["064350014014", "Hanau, Brüder-Grimm-Stadt", null], + ["064350015015", "Hasselroth", null], + ["064350016016", "Jossgrund", null], + ["064350017017", "Langenselbold, Stadt", null], + ["064350018018", "Linsengericht", null], + ["064350019019", "Maintal, Stadt", null], + ["064350020020", "Neuberg", null], + ["064350021021", "Nidderau, Stadt", null], + ["064350022022", "Niederdorfelden", null], + ["064350023023", "Rodenbach", null], + ["064350024024", "Ronneburg", null], + ["064350025025", "Schlüchtern, Stadt", null], + ["064350026026", "Schöneck", null], + ["064350027027", "Sinntal", null], + ["064350028028", "Steinau an der Straße, Brüder-Grimm-Stadt", null], + ["064350029029", "Wächtersbach, Stadt", null], + ["064359200200", "Gutsbezirk Spessart, gemfr. Gebiet", null], + ["064360001001", "Bad Soden am Taunus, Stadt", null], + ["064360002002", "Eppstein, Stadt", null], + ["064360003003", "Eschborn, Stadt", null], + ["064360004004", "Flörsheim am Main, Stadt", null], + ["064360005005", "Hattersheim am Main, Stadt", null], + ["064360006006", "Hochheim am Main, Stadt", null], + ["064360007007", "Hofheim am Taunus, Kreisstadt", null], + ["064360008008", "Kelkheim (Taunus), Stadt", null], + ["064360009009", "Kriftel", null], + ["064360010010", "Liederbach am Taunus", null], + ["064360011011", "Schwalbach am Taunus, Stadt", null], + ["064360012012", "Sulzbach (Taunus)", null], + ["064370001001", "Bad König, Stadt", null], + ["064370003003", "Brensbach", null], + ["064370004004", "Breuberg, Stadt", null], + ["064370005005", "Brombachtal", null], + ["064370006006", "Erbach, Kreisstadt", null], + ["064370007007", "Fränkisch-Crumbach", null], + ["064370009009", "Höchst i. Odw.", null], + ["064370010010", "Lützelbach", null], + ["064370011011", "Michelstadt, Stadt", null], + ["064370012012", "Mossautal", null], + ["064370013013", "Reichelsheim (Odenwald)", null], + ["064370016016", "Oberzent, Stadt", null], + ["064380001001", "Dietzenbach, Kreisstadt", null], + ["064380002002", "Dreieich, Stadt", null], + ["064380003003", "Egelsbach", null], + ["064380004004", "Hainburg", null], + ["064380005005", "Heusenstamm, Stadt", null], + ["064380006006", "Langen (Hessen), Stadt", null], + ["064380007007", "Mainhausen", null], + ["064380008008", "Mühlheim am Main, Stadt", null], + ["064380009009", "Neu-Isenburg, Stadt", null], + ["064380010010", "Obertshausen, Stadt", null], + ["064380011011", "Rodgau, Stadt", null], + ["064380012012", "Rödermark, Stadt", null], + ["064380013013", "Seligenstadt, Einhardstadt", null], + ["064390001001", "Aarbergen", null], + ["064390002002", "Bad Schwalbach, Kreisstadt", null], + ["064390003003", "Eltville am Rhein, Stadt", null], + ["064390004004", "Geisenheim, Hochschulstadt", null], + ["064390005005", "Heidenrod", null], + ["064390006006", "Hohenstein", null], + ["064390007007", "Hünstetten", null], + ["064390008008", "Idstein, Hochschulstadt", null], + ["064390009009", "Kiedrich", null], + ["064390010010", "Lorch, Stadt", null], + ["064390011011", "Niedernhausen", null], + ["064390012012", "Oestrich-Winkel, Stadt", null], + ["064390013013", "Rüdesheim am Rhein, Stadt", null], + ["064390014014", "Schlangenbad", null], + ["064390015015", "Taunusstein, Stadt", null], + ["064390016016", "Waldems", null], + ["064390017017", "Walluf", null], + ["064400001001", "Altenstadt", null], + ["064400002002", "Bad Nauheim, Stadt", null], + ["064400003003", "Bad Vilbel, Stadt", null], + ["064400004004", "Büdingen, Stadt", null], + ["064400005005", "Butzbach, Friedrich-Ludwig-Weidig-Stadt", null], + ["064400006006", "Echzell", null], + ["064400007007", "Florstadt, Stadt", null], + ["064400008008", "Friedberg (Hessen), Kreisstadt", null], + ["064400009009", "Gedern, Stadt", null], + ["064400010010", "Glauburg", null], + ["064400011011", "Hirzenhain", null], + ["064400012012", "Karben, Stadt", null], + ["064400013013", "Kefenrod", null], + ["064400014014", "Limeshain", null], + ["064400015015", "Münzenberg, Stadt", null], + ["064400016016", "Nidda, Stadt", null], + ["064400017017", "Niddatal, Stadt", null], + ["064400018018", "Ober-Mörlen", null], + ["064400019019", "Ortenberg, Stadt", null], + ["064400020020", "Ranstadt", null], + ["064400021021", "Reichelsheim (Wetterau), Stadt", null], + ["064400022022", "Rockenberg", null], + ["064400023023", "Rosbach v. d. Höhe, Stadt", null], + ["064400024024", "Wölfersheim", null], + ["064400025025", "Wöllstadt", null], + ["065310001001", "Allendorf (Lumda), Stadt", null], + ["065310002002", "Biebertal", null], + ["065310003003", "Buseck", null], + ["065310004004", "Fernwald", null], + ["065310005005", "Gießen, Universitätsstadt", null], + ["065310006006", "Grünberg, Stadt", null], + ["065310007007", "Heuchelheim a. d. Lahn", null], + ["065310008008", "Hungen, Stadt", null], + ["065310009009", "Langgöns", null], + ["065310010010", "Laubach, Stadt", null], + ["065310011011", "Lich, Stadt", null], + ["065310012012", "Linden, Stadt", null], + ["065310013013", "Lollar, Stadt", null], + ["065310014014", "Pohlheim, Stadt", null], + ["065310015015", "Rabenau", null], + ["065310016016", "Reiskirchen", null], + ["065310017017", "Staufenberg, Stadt", null], + ["065310018018", "Wettenberg", null], + ["065320001001", "Aßlar, Stadt", null], + ["065320002002", "Bischoffen", null], + ["065320003003", "Braunfels, Stadt", null], + ["065320004004", "Breitscheid", null], + ["065320005005", "Dietzhölztal", null], + ["065320006006", "Dillenburg, Oranienstadt", null], + ["065320007007", "Driedorf", null], + ["065320008008", "Ehringshausen", null], + ["065320009009", "Eschenburg", null], + ["065320010010", "Greifenstein", null], + ["065320011011", "Haiger, Stadt", null], + ["065320012012", "Herborn, Stadt", null], + ["065320013013", "Hohenahr", null], + ["065320014014", "Hüttenberg", null], + ["065320015015", "Lahnau", null], + ["065320016016", "Leun, Stadt", null], + ["065320017017", "Mittenaar", null], + ["065320018018", "Schöffengrund", null], + ["065320019019", "Siegbach", null], + ["065320020020", "Sinn", null], + ["065320021021", "Solms, Stadt", null], + ["065320022022", "Waldsolms", null], + ["065320023023", "Wetzlar, Stadt", null], + ["065330001001", "Beselich", null], + ["065330002002", "Brechen", null], + ["065330003003", "Bad Camberg, Stadt", null], + ["065330004004", "Dornburg", null], + ["065330005005", "Elbtal", null], + ["065330006006", "Elz", null], + ["065330007007", "Hadamar, Stadt", null], + ["065330008008", "Hünfelden", null], + ["065330009009", "Limburg a. d. Lahn, Kreisstadt", null], + ["065330010010", "Löhnberg", null], + ["065330011011", "Mengerskirchen, Marktflecken", null], + ["065330012012", "Merenberg, Marktflecken", null], + ["065330013013", "Runkel, Stadt", null], + ["065330014014", "Selters (Taunus)", null], + ["065330015015", "Villmar, Marktflecken", null], + ["065330016016", "Waldbrunn (Westerwald)", null], + ["065330017017", "Weilburg, Stadt", null], + ["065330018018", "Weilmünster, Marktflecken", null], + ["065330019019", "Weinbach", null], + ["065340001001", "Amöneburg, Stadt", null], + ["065340002002", "Angelburg", null], + ["065340003003", "Bad Endbach", null], + ["065340004004", "Biedenkopf, Stadt", null], + ["065340005005", "Breidenbach", null], + ["065340006006", "Cölbe", null], + ["065340007007", "Dautphetal", null], + ["065340008008", "Ebsdorfergrund", null], + ["065340009009", "Fronhausen", null], + ["065340010010", "Gladenbach, Stadt", null], + ["065340011011", "Kirchhain, Stadt", null], + ["065340012012", "Lahntal", null], + ["065340013013", "Lohra", null], + ["065340014014", "Marburg, Universitätsstadt", null], + ["065340015015", "Münchhausen", null], + ["065340016016", "Neustadt (Hessen), Stadt", null], + ["065340017017", "Rauschenberg, Stadt", null], + ["065340018018", "Stadtallendorf, Stadt", null], + ["065340019019", "Steffenberg", null], + ["065340020020", "Weimar (Lahn)", null], + ["065340021021", "Wetter (Hessen), Stadt", null], + ["065340022022", "Wohratal", null], + ["065350001001", "Alsfeld, Stadt", null], + ["065350002002", "Antrifttal", null], + ["065350003003", "Feldatal", null], + ["065350004004", "Freiensteinau", null], + ["065350005005", "Gemünden (Felda)", null], + ["065350006006", "Grebenau, Stadt", null], + ["065350007007", "Grebenhain", null], + ["065350008008", "Herbstein, Stadt", null], + ["065350009009", "Homberg (Ohm), Stadt", null], + ["065350010010", "Kirtorf, Stadt", null], + ["065350011011", "Lauterbach (Hessen), Kreisstadt", null], + ["065350012012", "Lautertal (Vogelsberg)", null], + ["065350013013", "Mücke", null], + ["065350014014", "Romrod, Stadt", null], + ["065350015015", "Schlitz, Stadt", null], + ["065350016016", "Schotten, Stadt", null], + ["065350017017", "Schwalmtal", null], + ["065350018018", "Ulrichstein, Stadt", null], + ["065350019019", "Wartenberg", null], + ["066110000000", "Kassel, documenta-Stadt", null], + ["066310001001", "Bad Salzschlirf", null], + ["066310002002", "Burghaun, Marktgemeinde", null], + ["066310003003", "Dipperz", null], + ["066310004004", "Ebersburg", null], + ["066310005005", "Ehrenberg (Rhön)", null], + ["066310006006", "Eichenzell", null], + ["066310007007", "Eiterfeld, Marktgemeinde", null], + ["066310008008", "Flieden", null], + ["066310009009", "Fulda, Stadt", null], + ["066310010010", "Gersfeld (Rhön), Stadt", null], + ["066310011011", "Großenlüder", null], + ["066310012012", "Hilders, Marktgemeinde", null], + ["066310013013", "Hofbieber", null], + ["066310014014", "Hosenfeld", null], + ["066310015015", "Hünfeld, Konrad-Zuse-Stadt", null], + ["066310016016", "Kalbach", null], + ["066310017017", "Künzell", null], + ["066310018018", "Neuhof", null], + ["066310019019", "Nüsttal", null], + ["066310020020", "Petersberg", null], + ["066310021021", "Poppenhausen (Wasserkuppe)", null], + ["066310022022", "Rasdorf, Point-Alpha-Gemeinde", null], + ["066310023023", "Tann (Rhön), Stadt", null], + ["066320001001", "Alheim", null], + ["066320002002", "Bad Hersfeld, Kreisstadt", null], + ["066320003003", "Bebra, Stadt", null], + ["066320004004", "Breitenbach a. Herzberg", null], + ["066320005005", "Cornberg", null], + ["066320006006", "Friedewald", null], + ["066320007007", "Hauneck", null], + ["066320008008", "Haunetal", null], + ["066320009009", "Heringen (Werra), Stadt", null], + ["066320010010", "Hohenroda", null], + ["066320011011", "Kirchheim", null], + ["066320012012", "Ludwigsau", null], + ["066320013013", "Nentershausen", null], + ["066320014014", "Neuenstein", null], + ["066320015015", "Niederaula, Marktgemeinde", null], + ["066320016016", "Philippsthal (Werra), Marktgemeinde", null], + ["066320017017", "Ronshausen", null], + ["066320018018", "Rotenburg a. d. Fulda, Stadt", null], + ["066320019019", "Schenklengsfeld", null], + ["066320020020", "Wildeck", null], + ["066330001001", "Ahnatal", null], + ["066330002002", "Bad Karlshafen, Stadt", null], + ["066330003003", "Baunatal, Stadt", null], + ["066330004004", "Breuna", null], + ["066330005005", "Calden", null], + ["066330006006", "Bad Emstal", null], + ["066330007007", "Espenau", null], + ["066330008008", "Fuldabrück", null], + ["066330009009", "Fuldatal", null], + ["066330010010", "Grebenstein, Stadt", null], + ["066330011011", "Habichtswald", null], + ["066330012012", "Helsa", null], + ["066330013013", "Hofgeismar, Stadt", null], + ["066330014014", "Immenhausen, Stadt", null], + ["066330015015", "Kaufungen", null], + ["066330016016", "Liebenau, Stadt", null], + ["066330017017", "Lohfelden", null], + ["066330018018", "Naumburg, Stadt", null], + ["066330019019", "Nieste", null], + ["066330020020", "Niestetal", null], + ["066330022022", "Reinhardshagen", null], + ["066330023023", "Schauenburg", null], + ["066330024024", "Söhrewald", null], + ["066330025025", "Trendelburg, Stadt", null], + ["066330026026", "Vellmar, Stadt", null], + ["066330028028", "Wolfhagen, Hans-Staden-Stadt", null], + ["066330029029", "Zierenberg, Stadt", null], + ["066330030030", "Wesertal", null], + ["066339200200", "Gutsbezirk Reinhardswald, gemfr. Gebiet", null], + ["066340001001", "Borken (Hessen), Stadt", null], + ["066340002002", "Edermünde", null], + ["066340003003", "Felsberg, Stadt", null], + ["066340004004", "Frielendorf, Marktflecken", null], + ["066340005005", "Fritzlar, Dom- und Kaiserstadt", null], + ["066340006006", "Gilserberg", null], + ["066340007007", "Gudensberg, Stadt", null], + ["066340008008", "Guxhagen", null], + ["066340009009", "Homberg (Efze), Reformationsstadt, Kreisstadt", null], + ["066340010010", "Jesberg", null], + ["066340011011", "Knüllwald", null], + ["066340012012", "Körle", null], + ["066340013013", "Malsfeld", null], + ["066340014014", "Melsungen, Stadt", null], + ["066340015015", "Morschen", null], + ["066340016016", "Neuental", null], + ["066340017017", "Neukirchen, Stadt", null], + ["066340018018", "Niedenstein, Stadt", null], + ["066340019019", "Oberaula", null], + ["066340020020", "Ottrau", null], + ["066340021021", "Schrecksbach", null], + ["066340022022", "Schwalmstadt, Konfirmationsstadt", null], + ["066340023023", "Schwarzenborn, Stadt", null], + ["066340024024", "Spangenberg, Liebenbachstadt", null], + ["066340025025", "Wabern", null], + ["066340026026", "Willingshausen", null], + ["066340027027", "Bad Zwesten", null], + ["066350001001", "Allendorf (Eder)", null], + ["066350002002", "Bad Arolsen, Stadt", null], + ["066350003003", "Bad Wildungen, Stadt", null], + ["066350004004", "Battenberg (Eder), Stadt", null], + ["066350005005", "Bromskirchen", null], + ["066350006006", "Burgwald", null], + ["066350007007", "Diemelsee", null], + ["066350008008", "Diemelstadt, Stadt", null], + ["066350009009", "Edertal, Nationalparkgemeinde", null], + ["066350010010", "Frankenau, Nationalparkstadt", null], + ["066350011011", "Frankenberg (Eder), Philipp-Soldan-Stadt", null], + ["066350012012", "Gemünden (Wohra), Stadt", null], + ["066350013013", "Haina (Kloster)", null], + ["066350014014", "Hatzfeld (Eder), Stadt", null], + ["066350015015", "Korbach, Hansestadt, Kreisstadt", null], + ["066350016016", "Lichtenfels, Stadt", null], + ["066350017017", "Rosenthal, Stadt", null], + ["066350018018", "Twistetal", null], + ["066350019019", "Vöhl, Nationalparkgemeinde", null], + ["066350020020", "Volkmarsen, Stadt", null], + ["066350021021", "Waldeck, Stadt", null], + ["066350022022", "Willingen (Upland)", null], + ["066360001001", "Bad Sooden-Allendorf, Stadt", null], + ["066360002002", "Berkatal", null], + ["066360003003", "Eschwege, Kreisstadt", null], + ["066360004004", "Großalmerode, Stadt", null], + ["066360005005", "Herleshausen", null], + ["066360006006", "Hessisch Lichtenau, Stadt", null], + ["066360007007", "Meinhard", null], + ["066360008008", "Meißner", null], + ["066360009009", "Neu-Eichenberg", null], + ["066360010010", "Ringgau", null], + ["066360011011", "Sontra, Stadt", null], + ["066360012012", "Waldkappel, Stadt", null], + ["066360013013", "Wanfried, Stadt", null], + ["066360014014", "Wehretal", null], + ["066360015015", "Weißenborn", null], + ["066360016016", "Witzenhausen, Stadt", null], + ["066369200200", "Gutsbezirk Kaufunger Wald, gemfr. Gebiet", null], + ["070009999999", "Gemeinsames deutsch-luxemburgisches Hoheitsgebiet", null], + ["071110000000", "Koblenz, Stadt", null], + ["071310007007", "Bad Neuenahr-Ahrweiler, Stadt", null], + ["071310070070", "Remagen, Stadt", null], + ["071310077077", "Sinzig, Stadt", null], + ["071310090090", "Grafschaft", null], + ["071315001001", "Adenau, Stadt", null], + ["071315001004", "Antweiler", null], + ["071315001005", "Aremberg", null], + ["071315001008", "Barweiler", null], + ["071315001009", "Bauler", null], + ["071315001015", "Dankerath", null], + ["071315001018", "Dorsel", null], + ["071315001021", "Eichenbach", null], + ["071315001022", "Fuchshofen", null], + ["071315001026", "Harscheid", null], + ["071315001028", "Herschbroich", null], + ["071315001030", "Hoffeld", null], + ["071315001032", "Honerath", null], + ["071315001033", "Hümmel", null], + ["071315001034", "Insul", null], + ["071315001037", "Kaltenborn", null], + ["071315001042", "Kottenborn", null], + ["071315001044", "Leimbach", null], + ["071315001050", "Meuspath", null], + ["071315001051", "Müllenbach", null], + ["071315001052", "Müsch", null], + ["071315001058", "Nürburg", null], + ["071315001062", "Ohlenhard", null], + ["071315001065", "Pomster", null], + ["071315001066", "Quiddelbach", null], + ["071315001069", "Reifferscheid", null], + ["071315001072", "Rodder", null], + ["071315001074", "Schuld", null], + ["071315001075", "Senscheid", null], + ["071315001076", "Sierscheid", null], + ["071315001079", "Trierscheid", null], + ["071315001082", "Wershofen", null], + ["071315001083", "Wiesemscheid", null], + ["071315001084", "Wimbach", null], + ["071315001085", "Winnerath", null], + ["071315001086", "Wirft", null], + ["071315001501", "Dümpelfeld", null], + ["071315002002", "Ahrbrück", null], + ["071315002003", "Altenahr", null], + ["071315002011", "Berg", null], + ["071315002017", "Dernau", null], + ["071315002027", "Heckenbach", null], + ["071315002029", "Hönningen", null], + ["071315002036", "Kalenborn", null], + ["071315002039", "Kesseling", null], + ["071315002040", "Kirchsahr", null], + ["071315002047", "Lind", null], + ["071315002049", "Mayschoß", null], + ["071315002068", "Rech", null], + ["071315003006", "Bad Breisig, Stadt", null], + ["071315003014", "Brohl-Lützing", null], + ["071315003025", "Gönnersdorf", null], + ["071315003081", "Waldorf", null], + ["071315004016", "Dedenbach", null], + ["071315004041", "Königsfeld", null], + ["071315004054", "Niederdürenbach", null], + ["071315004055", "Niederzissen", null], + ["071315004059", "Oberdürenbach", null], + ["071315004060", "Oberzissen", null], + ["071315004073", "Schalkenbach", null], + ["071315004201", "Brenk", null], + ["071315004202", "Burgbrohl", null], + ["071315004204", "Galenberg", null], + ["071315004205", "Glees", null], + ["071315004206", "Hohenleimbach", null], + ["071315004208", "Spessart", null], + ["071315004209", "Wassenach", null], + ["071315004210", "Wehr", null], + ["071315004211", "Weibern", null], + ["071315004502", "Kempenich", null], + ["071325003018", "Daaden, Stadt", null], + ["071325003019", "Derschen", null], + ["071325003026", "Emmerzhausen", null], + ["071325003036", "Friedewald", null], + ["071325003050", "Herdorf, Stadt", null], + ["071325003068", "Mauden", null], + ["071325003075", "Niederdreisbach", null], + ["071325003079", "Nisterberg", null], + ["071325003101", "Schutzbach", null], + ["071325003113", "Weitefeld", null], + ["071325006007", "Birkenbeul", null], + ["071325006010", "Bitzen", null], + ["071325006013", "Breitscheidt", null], + ["071325006014", "Bruchertseifen", null], + ["071325006028", "Etzbach", null], + ["071325006034", "Forst", null], + ["071325006038", "Fürthen", null], + ["071325006044", "Hamm (Sieg)", null], + ["071325006077", "Niederirsen", null], + ["071325006091", "Pracht", null], + ["071325006096", "Roth", null], + ["071325006102", "Seelbach bei Hamm (Sieg)", null], + ["071325007012", "Brachbach", null], + ["071325007037", "Friesenhagen", null], + ["071325007045", "Harbach", null], + ["071325007063", "Kirchen (Sieg), Stadt", null], + ["071325007072", "Mudersbach", null], + ["071325007076", "Niederfischbach", null], + ["071325008008", "Birken-Honigsessen", null], + ["071325008011", "Mittelhof", null], + ["071325008054", "Hövels", null], + ["071325008080", "Katzwinkel (Sieg)", null], + ["071325008105", "Selbach (Sieg)", null], + ["071325008117", "Wissen, Stadt", null], + ["071325009002", "Alsdorf", null], + ["071325009006", "Betzdorf, Stadt", null], + ["071325009020", "Dickendorf", null], + ["071325009024", "Elben", null], + ["071325009025", "Elkenroth", null], + ["071325009030", "Fensdorf", null], + ["071325009039", "Gebhardshain", null], + ["071325009042", "Grünebach", null], + ["071325009059", "Kausen", null], + ["071325009066", "Malberg", null], + ["071325009071", "Molzhain", null], + ["071325009073", "Nauroth", null], + ["071325009095", "Rosenheim (Landkreis Altenkirchen)", null], + ["071325009098", "Scheuerfeld", null], + ["071325009107", "Steinebach/ Sieg", null], + ["071325009108", "Steineroth", null], + ["071325009111", "Wallmenroth", null], + ["071325010001", "Almersbach", null], + ["071325010004", "Bachenberg", null], + ["071325010005", "Berzhausen", null], + ["071325010009", "Birnbach", null], + ["071325010015", "Bürdenbach", null], + ["071325010016", "Burglahr", null], + ["071325010017", "Busenhausen", null], + ["071325010022", "Eichelhardt", null], + ["071325010023", "Eichen", null], + ["071325010027", "Ersfeld", null], + ["071325010029", "Eulenberg", null], + ["071325010031", "Fiersbach", null], + ["071325010032", "Flammersfeld", null], + ["071325010033", "Fluterschen", null], + ["071325010035", "Forstmehren", null], + ["071325010040", "Gieleroth", null], + ["071325010041", "Giershausen", null], + ["071325010043", "Güllesheim", null], + ["071325010046", "Hasselbach", null], + ["071325010047", "Helmenzen", null], + ["071325010048", "Helmeroth", null], + ["071325010049", "Hemmelzen", null], + ["071325010051", "Heupelzen", null], + ["071325010052", "Hilgenroth", null], + ["071325010053", "Hirz-Maulsbach", null], + ["071325010055", "Horhausen (Westerwald)", null], + ["071325010056", "Idelberg", null], + ["071325010057", "Ingelbach", null], + ["071325010058", "Isert", null], + ["071325010060", "Kescheid", null], + ["071325010061", "Kettenhausen", null], + ["071325010062", "Kircheib", null], + ["071325010064", "Kraam", null], + ["071325010065", "Krunkel", null], + ["071325010067", "Mammelzen", null], + ["071325010069", "Mehren", null], + ["071325010070", "Michelbach (Westerwald)", null], + ["071325010078", "Niedersteinebach", null], + ["071325010081", "Obererbach (Westerwald)", null], + ["071325010082", "Oberirsen", null], + ["071325010083", "Oberlahr", null], + ["071325010085", "Obersteinebach", null], + ["071325010086", "Oberwambach", null], + ["071325010087", "Ölsen", null], + ["071325010088", "Orfgen", null], + ["071325010089", "Peterslahr", null], + ["071325010090", "Pleckhausen", null], + ["071325010092", "Racksen", null], + ["071325010093", "Reiferscheid", null], + ["071325010094", "Rettersen", null], + ["071325010097", "Rott", null], + ["071325010099", "Schöneberg", null], + ["071325010100", "Schürdt", null], + ["071325010103", "Seelbach (Westerwald)", null], + ["071325010104", "Seifen", null], + ["071325010106", "Sörth", null], + ["071325010109", "Stürzelbach", null], + ["071325010110", "Volkerzen", null], + ["071325010112", "Walterschen", null], + ["071325010114", "Werkhausen", null], + ["071325010115", "Weyerbusch", null], + ["071325010116", "Willroth", null], + ["071325010118", "Wölmersen", null], + ["071325010119", "Ziegenhain", null], + ["071325010201", "Berod bei Hachenburg", null], + ["071325010501", "Altenkirchen (Westerwald), Stadt", null], + ["071325010502", "Neitersen", null], + ["071330006006", "Bad Kreuznach, Stadt", null], + ["071335001003", "Altenbamberg", null], + ["071335001012", "Biebelsheim", null], + ["071335001030", "Feilbingert", null], + ["071335001031", "Frei-Laubersheim", null], + ["071335001032", "Fürfeld", null], + ["071335001037", "Hackenheim", null], + ["071335001039", "Hallgarten", null], + ["071335001045", "Hochstätten", null], + ["071335001069", "Neu-Bamberg", null], + ["071335001078", "Pfaffen-Schwabenheim", null], + ["071335001080", "Pleitersheim", null], + ["071335001104", "Tiefenthal", null], + ["071335001106", "Volxheim", null], + ["071335006002", "Allenfeld", null], + ["071335006004", "Argenschwang", null], + ["071335006013", "Bockenau", null], + ["071335006014", "Boos", null], + ["071335006015", "Braunweiler", null], + ["071335006019", "Burgsponheim", null], + ["071335006021", "Dalberg", null], + ["071335006027", "Duchroth", null], + ["071335006033", "Gebroth", null], + ["071335006036", "Gutenberg", null], + ["071335006040", "Hargesheim", null], + ["071335006044", "Hergenfeld", null], + ["071335006048", "Hüffelsheim", null], + ["071335006061", "Mandel", null], + ["071335006068", "Münchwald", null], + ["071335006070", "Niederhausen", null], + ["071335006071", "Norheim", null], + ["071335006074", "Oberhausen an der Nahe", null], + ["071335006075", "Oberstreit", null], + ["071335006086", "Roxheim", null], + ["071335006088", "Sankt Katharinen", null], + ["071335006089", "Schloßböckelheim", null], + ["071335006098", "Sommerloch", null], + ["071335006099", "Spabrücken", null], + ["071335006100", "Spall", null], + ["071335006101", "Sponheim", null], + ["071335006105", "Traisen", null], + ["071335006107", "Waldböckelheim", null], + ["071335006109", "Wallhausen", null], + ["071335006112", "Weinsheim", null], + ["071335006115", "Winterbach", null], + ["071335006117", "Rüdesheim", null], + ["071335009008", "Bärenbach", null], + ["071335009010", "Becherbach bei Kirn", null], + ["071335009016", "Brauweiler", null], + ["071335009038", "Hahnenbach", null], + ["071335009041", "Heimweiler", null], + ["071335009042", "Heinzenberg", null], + ["071335009043", "Hennweiler", null], + ["071335009046", "Hochstetten-Dhaun", null], + ["071335009047", "Horbach", null], + ["071335009052", "Kirn, Stadt", null], + ["071335009059", "Limbach", null], + ["071335009063", "Meckenbach", null], + ["071335009073", "Oberhausen bei Kirn", null], + ["071335009077", "Otzweiler", null], + ["071335009096", "Simmertal", null], + ["071335009113", "Weitersborn", null], + ["071335009201", "Bruschied", null], + ["071335009202", "Kellenbach", null], + ["071335009203", "Königsau", null], + ["071335009204", "Schneppenbach", null], + ["071335009205", "Schwarzerden", null], + ["071335010001", "Abtweiler", null], + ["071335010005", "Auen", null], + ["071335010009", "Bärweiler", null], + ["071335010011", "Becherbach", null], + ["071335010017", "Breitenheim", null], + ["071335010020", "Callbach", null], + ["071335010022", "Daubach", null], + ["071335010024", "Desloch", null], + ["071335010049", "Hundsbach", null], + ["071335010050", "Ippenschied", null], + ["071335010051", "Jeckenbach", null], + ["071335010053", "Kirschroth", null], + ["071335010055", "Langenthal", null], + ["071335010057", "Lauschied", null], + ["071335010058", "Lettweiler", null], + ["071335010060", "Löllbach", null], + ["071335010062", "Martinstein", null], + ["071335010064", "Meddersheim", null], + ["071335010065", "Meisenheim, Stadt", null], + ["071335010066", "Merxheim", null], + ["071335010067", "Monzingen", null], + ["071335010072", "Nußbaum", null], + ["071335010076", "Odernheim am Glan", null], + ["071335010081", "Raumbach", null], + ["071335010082", "Rehbach", null], + ["071335010083", "Rehborn", null], + ["071335010084", "Reiffelbach", null], + ["071335010090", "Schmittweiler", null], + ["071335010092", "Schweinschied", null], + ["071335010094", "Seesbach", null], + ["071335010102", "Staudernheim", null], + ["071335010111", "Weiler bei Monzingen", null], + ["071335010116", "Winterburg", null], + ["071335010501", "Bad Sobernheim, Stadt", null], + ["071335011018", "Bretzenheim", null], + ["071335011023", "Daxweiler", null], + ["071335011025", "Dörrebach", null], + ["071335011026", "Dorsheim", null], + ["071335011028", "Eckenroth", null], + ["071335011035", "Guldental", null], + ["071335011054", "Langenlonsheim", null], + ["071335011056", "Laubenheim", null], + ["071335011085", "Roth", null], + ["071335011087", "Rümmelsheim", null], + ["071335011091", "Schöneberg", null], + ["071335011093", "Schweppenhausen", null], + ["071335011095", "Seibersbach", null], + ["071335011103", "Stromberg, Stadt", null], + ["071335011108", "Waldlaubersheim", null], + ["071335011110", "Warmsroth", null], + ["071335011114", "Windesheim", null], + ["071340045045", "Idar-Oberstein, Stadt", null], + ["071345001005", "Baumholder, Stadt", null], + ["071345001007", "Berglangenbach", null], + ["071345001008", "Berschweiler bei Baumholder", null], + ["071345001021", "Eckersweiler", null], + ["071345001026", "Fohren-Linden", null], + ["071345001027", "Frauenberg", null], + ["071345001033", "Hahnweiler", null], + ["071345001036", "Heimbach", null], + ["071345001051", "Leitzweiler", null], + ["071345001054", "Mettweiler", null], + ["071345001068", "Reichenbach", null], + ["071345001073", "Rohrbach", null], + ["071345001074", "Rückweiler", null], + ["071345001075", "Ruschberg", null], + ["071345002001", "Abentheuer", null], + ["071345002002", "Achtelsbach", null], + ["071345002010", "Birkenfeld, Stadt", null], + ["071345002011", "Börfink", null], + ["071345002015", "Brücken", null], + ["071345002016", "Buhlenberg", null], + ["071345002018", "Dambach", null], + ["071345002020", "Dienstweiler", null], + ["071345002022", "Elchweiler", null], + ["071345002023", "Ellenberg", null], + ["071345002024", "Ellweiler", null], + ["071345002029", "Gimbweiler", null], + ["071345002031", "Gollenberg", null], + ["071345002034", "Hattgenstein", null], + ["071345002042", "Hoppstädten-Weiersbach", null], + ["071345002048", "Kronweiler", null], + ["071345002050", "Leisel", null], + ["071345002053", "Meckenbach", null], + ["071345002057", "Niederbrombach", null], + ["071345002058", "Niederhambach", null], + ["071345002061", "Nohen", null], + ["071345002062", "Oberbrombach", null], + ["071345002063", "Oberhambach", null], + ["071345002070", "Rimsberg", null], + ["071345002071", "Rinzenberg", null], + ["071345002072", "Rötsweiler-Nockenthal", null], + ["071345002078", "Schmißberg", null], + ["071345002080", "Schwollen", null], + ["071345002084", "Siesbach", null], + ["071345002085", "Sonnenberg-Winnenberg", null], + ["071345002094", "Wilzenberg-Hußweiler", null], + ["071345005003", "Allenbach", null], + ["071345005004", "Asbach", null], + ["071345005006", "Bergen", null], + ["071345005009", "Berschweiler bei Kirn", null], + ["071345005012", "Bollenbach", null], + ["071345005013", "Breitenthal", null], + ["071345005014", "Bruchweiler", null], + ["071345005017", "Bundenbach", null], + ["071345005019", "Dickesbach", null], + ["071345005025", "Fischbach", null], + ["071345005028", "Gerach", null], + ["071345005030", "Gösenroth", null], + ["071345005032", "Griebelschied", null], + ["071345005035", "Hausen", null], + ["071345005037", "Hellertshausen", null], + ["071345005038", "Herborn", null], + ["071345005039", "Herrstein", null], + ["071345005040", "Hettenrodt", null], + ["071345005041", "Hintertiefenbach", null], + ["071345005043", "Horbruch", null], + ["071345005044", "Hottenbach", null], + ["071345005046", "Kempfeld", null], + ["071345005047", "Kirschweiler", null], + ["071345005049", "Krummenau", null], + ["071345005052", "Mackenrodt", null], + ["071345005055", "Mittelreidenbach", null], + ["071345005056", "Mörschied", null], + ["071345005059", "Niederhosenbach", null], + ["071345005060", "Niederwörresbach", null], + ["071345005064", "Oberhosenbach", null], + ["071345005065", "Oberkirn", null], + ["071345005066", "Oberreidenbach", null], + ["071345005067", "Oberwörresbach", null], + ["071345005069", "Rhaunen", null], + ["071345005076", "Schauren", null], + ["071345005077", "Schmidthachenbach", null], + ["071345005079", "Schwerbach", null], + ["071345005081", "Sensweiler", null], + ["071345005082", "Sien", null], + ["071345005083", "Sienhachenbach", null], + ["071345005086", "Sonnschied", null], + ["071345005087", "Stipshausen", null], + ["071345005088", "Sulzbach", null], + ["071345005089", "Veitsrodt", null], + ["071345005090", "Vollmersbach", null], + ["071345005091", "Weiden", null], + ["071345005092", "Weitersbach", null], + ["071345005093", "Wickenrodt", null], + ["071345005095", "Wirschweiler", null], + ["071345005502", "Langweiler", null], + ["071355001007", "Beilstein", null], + ["071355001012", "Bremm", null], + ["071355001015", "Briedern", null], + ["071355001017", "Bruttig-Fankel", null], + ["071355001020", "Cochem, Stadt", null], + ["071355001021", "Dohr", null], + ["071355001024", "Ediger-Eller", null], + ["071355001025", "Ellenz-Poltersdorf", null], + ["071355001027", "Ernst", null], + ["071355001029", "Faid", null], + ["071355001036", "Greimersburg", null], + ["071355001049", "Klotten", null], + ["071355001053", "Lieg", null], + ["071355001056", "Lütz", null], + ["071355001060", "Mesenich", null], + ["071355001065", "Moselkern", null], + ["071355001066", "Müden (Mosel)", null], + ["071355001069", "Nehren", null], + ["071355001072", "Pommern", null], + ["071355001079", "Senheim", null], + ["071355001082", "Treis-Karden", null], + ["071355001086", "Valwig", null], + ["071355001090", "Wirfus", null], + ["071355002009", "Binningen", null], + ["071355002011", "Brachtendorf", null], + ["071355002014", "Brieden", null], + ["071355002016", "Brohl", null], + ["071355002022", "Dünfus", null], + ["071355002023", "Düngenheim", null], + ["071355002026", "Eppenberg", null], + ["071355002028", "Eulgem", null], + ["071355002031", "Forst (Eifel)", null], + ["071355002033", "Gamlen", null], + ["071355002038", "Hambuch", null], + ["071355002040", "Hauroth", null], + ["071355002042", "Illerich", null], + ["071355002043", "Kaifenheim", null], + ["071355002044", "Kail", null], + ["071355002045", "Kaisersesch, Stadt", null], + ["071355002046", "Kalenborn", null], + ["071355002051", "Landkern", null], + ["071355002052", "Laubach", null], + ["071355002058", "Masburg", null], + ["071355002062", "Möntenich", null], + ["071355002067", "Müllenbach", null], + ["071355002075", "Roes", null], + ["071355002084", "Urmersbach", null], + ["071355002093", "Zettingen", null], + ["071355002502", "Leienkaul", null], + ["071355003002", "Alflen", null], + ["071355003005", "Auderath", null], + ["071355003008", "Beuren", null], + ["071355003018", "Büchel", null], + ["071355003030", "Filz", null], + ["071355003034", "Gevenich", null], + ["071355003035", "Gillenbeuren", null], + ["071355003048", "Kliding", null], + ["071355003057", "Lutzerath", null], + ["071355003078", "Schmitt", null], + ["071355003083", "Ulmen, Stadt", null], + ["071355003085", "Urschmitt", null], + ["071355003087", "Wagenhausen", null], + ["071355003089", "Weiler", null], + ["071355003091", "Wollmerath", null], + ["071355003501", "Bad Bertrich", null], + ["071355005001", "Alf", null], + ["071355005003", "Altlay", null], + ["071355005004", "Altstrimmig", null], + ["071355005010", "Blankenrath", null], + ["071355005013", "Briedel", null], + ["071355005019", "Bullay", null], + ["071355005032", "Forst (Hunsrück)", null], + ["071355005037", "Grenderich", null], + ["071355005039", "Haserich", null], + ["071355005041", "Hesweiler", null], + ["071355005054", "Liesenich", null], + ["071355005061", "Mittelstrimmig", null], + ["071355005064", "Moritzheim", null], + ["071355005068", "Neef", null], + ["071355005070", "Panzweiler", null], + ["071355005071", "Peterswald-Löffelscheid", null], + ["071355005073", "Pünderich", null], + ["071355005074", "Reidenhausen", null], + ["071355005076", "Sankt Aldegund", null], + ["071355005077", "Schauren", null], + ["071355005080", "Sosberg", null], + ["071355005081", "Tellig", null], + ["071355005088", "Walhausen", null], + ["071355005092", "Zell (Mosel), Stadt", null], + ["071370003003", "Andernach, Stadt", null], + ["071370068068", "Mayen, Stadt", null], + ["071370203203", "Bendorf, Stadt", null], + ["071375001056", "Kretz", null], + ["071375001057", "Kruft", null], + ["071375001081", "Nickenich", null], + ["071375001088", "Plaidt", null], + ["071375001096", "Saffig", null], + ["071375002023", "Einig", null], + ["071375002027", "Gappenach", null], + ["071375002029", "Gering", null], + ["071375002030", "Gierschnach", null], + ["071375002041", "Kalt", null], + ["071375002048", "Kerben", null], + ["071375002053", "Kollig", null], + ["071375002065", "Lonnig", null], + ["071375002070", "Mertloch", null], + ["071375002080", "Naunheim", null], + ["071375002086", "Ochtendung", null], + ["071375002087", "Pillig", null], + ["071375002089", "Polch, Stadt", null], + ["071375002095", "Rüber", null], + ["071375002102", "Trimbs", null], + ["071375002112", "Welling", null], + ["071375002114", "Wierschem", null], + ["071375002501", "Münstermaifeld, Stadt", null], + ["071375003001", "Acht", null], + ["071375003004", "Anschau", null], + ["071375003006", "Arft", null], + ["071375003007", "Baar", null], + ["071375003011", "Bermel", null], + ["071375003014", "Boos", null], + ["071375003019", "Ditscheid", null], + ["071375003025", "Ettringen", null], + ["071375003034", "Hausten", null], + ["071375003035", "Herresbach", null], + ["071375003036", "Hirten", null], + ["071375003043", "Kehrig", null], + ["071375003049", "Kirchwald", null], + ["071375003055", "Kottenheim", null], + ["071375003060", "Langenfeld", null], + ["071375003061", "Langscheid", null], + ["071375003063", "Lind", null], + ["071375003066", "Luxem", null], + ["071375003074", "Monreal", null], + ["071375003077", "Münk", null], + ["071375003079", "Nachtsheim", null], + ["071375003092", "Reudelsterz", null], + ["071375003097", "Sankt Johann", null], + ["071375003099", "Siebenbach", null], + ["071375003105", "Virneburg", null], + ["071375003110", "Weiler", null], + ["071375003113", "Welschenbach", null], + ["071375004008", "Bell", null], + ["071375004069", "Mendig, Stadt", null], + ["071375004093", "Rieden", null], + ["071375004101", "Thür", null], + ["071375004106", "Volkesfeld", null], + ["071375007218", "Niederwerth", null], + ["071375007224", "Urbar", null], + ["071375007226", "Vallendar, Stadt", null], + ["071375007229", "Weitersburg", null], + ["071375008202", "Bassenheim", null], + ["071375008209", "Kaltenengers", null], + ["071375008211", "Kettig", null], + ["071375008216", "Mülheim-Kärlich, Stadt", null], + ["071375008222", "Sankt Sebastian", null], + ["071375008225", "Urmitz", null], + ["071375008228", "Weißenthurm, Stadt", null], + ["071375009201", "Alken", null], + ["071375009204", "Brey", null], + ["071375009205", "Brodenbach", null], + ["071375009206", "Burgen", null], + ["071375009207", "Dieblich", null], + ["071375009208", "Hatzenport", null], + ["071375009212", "Kobern-Gondorf", null], + ["071375009214", "Löf", null], + ["071375009215", "Macken", null], + ["071375009217", "Niederfell", null], + ["071375009219", "Nörtershausen", null], + ["071375009220", "Oberfell", null], + ["071375009221", "Rhens, Stadt", null], + ["071375009223", "Spay", null], + ["071375009227", "Waldesch", null], + ["071375009230", "Winningen", null], + ["071375009231", "Wolken", null], + ["071375009504", "Lehmen", null], + ["071380045045", "Neuwied, Stadt", null], + ["071385001003", "Asbach", null], + ["071385001044", "Neustadt (Wied)", null], + ["071385001077", "Windhagen", null], + ["071385001080", "Buchholz (Westerwald)", null], + ["071385002004", "Bad Hönningen, Stadt", null], + ["071385002024", "Hammerstein", null], + ["071385002038", "Leutesdorf", null], + ["071385002063", "Rheinbrohl", null], + ["071385003012", "Dierdorf, Stadt", null], + ["071385003023", "Großmaischeid", null], + ["071385003031", "Isenburg", null], + ["071385003034", "Kleinmaischeid", null], + ["071385003069", "Stebach", null], + ["071385003201", "Marienhausen", null], + ["071385004009", "Dattenberg", null], + ["071385004037", "Leubsdorf", null], + ["071385004041", "Linz am Rhein, Stadt", null], + ["071385004055", "Ockenfels", null], + ["071385004068", "Sankt Katharinen (Landkreis Neuwied)", null], + ["071385004075", "Vettelschoß", null], + ["071385004501", "Kasbach-Ohlenberg", null], + ["071385005011", "Dernbach", null], + ["071385005013", "Döttesfeld", null], + ["071385005014", "Dürrholz", null], + ["071385005025", "Hanroth", null], + ["071385005027", "Harschbach", null], + ["071385005040", "Linkenbach", null], + ["071385005048", "Niederhofen", null], + ["071385005050", "Niederwambach", null], + ["071385005052", "Oberdreis", null], + ["071385005057", "Puderbach", null], + ["071385005058", "Ratzert", null], + ["071385005059", "Raubach", null], + ["071385005064", "Rodenbach bei Puderbach", null], + ["071385005070", "Steimel", null], + ["071385005074", "Urbach", null], + ["071385005078", "Woldert", null], + ["071385007008", "Bruchhausen", null], + ["071385007019", "Erpel", null], + ["071385007062", "Rheinbreitbach", null], + ["071385007073", "Unkel, Stadt", null], + ["071385009002", "Anhausen", null], + ["071385009005", "Bonefeld", null], + ["071385009006", "Breitscheid", null], + ["071385009007", "Hausen (Wied)", null], + ["071385009010", "Datzeroth", null], + ["071385009015", "Ehlscheid", null], + ["071385009026", "Hardert", null], + ["071385009030", "Hümmerich", null], + ["071385009036", "Kurtscheid", null], + ["071385009042", "Meinborn", null], + ["071385009043", "Melsbach", null], + ["071385009047", "Niederbreitbach", null], + ["071385009053", "Oberhonnefeld-Gierend", null], + ["071385009054", "Oberraden", null], + ["071385009061", "Rengsdorf", null], + ["071385009065", "Roßbach", null], + ["071385009066", "Rüscheid", null], + ["071385009071", "Straßenhaus", null], + ["071385009072", "Thalhausen", null], + ["071385009076", "Waldbreitbach", null], + ["071400501501", "Boppard, Stadt", null], + ["071405003001", "Alterkülz", null], + ["071405003009", "Bell (Hunsrück)", null], + ["071405003010", "Beltheim", null], + ["071405003018", "Braunshorn", null], + ["071405003021", "Buch", null], + ["071405003042", "Gödenroth", null], + ["071405003046", "Hasselbach", null], + ["071405003055", "Hollnich", null], + ["071405003064", "Kastellaun, Stadt", null], + ["071405003073", "Korweiler", null], + ["071405003095", "Michelbach", null], + ["071405003131", "Roth", null], + ["071405003147", "Spesenroth", null], + ["071405003153", "Uhler", null], + ["071405003202", "Dommershausen", null], + ["071405003204", "Mastershausen", null], + ["071405003502", "Lahr", null], + ["071405003503", "Mörsdorf", null], + ["071405003504", "Zilshausen", null], + ["071405004006", "Bärenbach", null], + ["071405004007", "Belg", null], + ["071405004024", "Büchenbeuren", null], + ["071405004028", "Dickenschied", null], + ["071405004029", "Dill", null], + ["071405004030", "Dillendorf", null], + ["071405004040", "Gehlweiler", null], + ["071405004041", "Gemünden", null], + ["071405004044", "Hahn", null], + ["071405004048", "Hecken", null], + ["071405004049", "Heinzenbach", null], + ["071405004050", "Henau", null], + ["071405004053", "Hirschfeld (Hunsrück)", null], + ["071405004062", "Kappel", null], + ["071405004067", "Kirchberg (Hunsrück), Stadt", null], + ["071405004071", "Kludenbach", null], + ["071405004081", "Laufersweiler", null], + ["071405004082", "Lautzenhausen", null], + ["071405004086", "Lindenschied", null], + ["071405004090", "Maitzborn", null], + ["071405004094", "Metzenhausen", null], + ["071405004105", "Nieder Kostenz", null], + ["071405004107", "Niedersohren", null], + ["071405004109", "Niederweiler", null], + ["071405004111", "Ober Kostenz", null], + ["071405004120", "Raversbeuren", null], + ["071405004122", "Reckershausen", null], + ["071405004128", "Rödelhausen", null], + ["071405004129", "Rödern", null], + ["071405004130", "Rohrbach", null], + ["071405004135", "Schlierschied", null], + ["071405004141", "Schwarzen", null], + ["071405004145", "Sohren", null], + ["071405004146", "Sohrschied", null], + ["071405004151", "Todenroth", null], + ["071405004154", "Unzenberg", null], + ["071405004159", "Wahlenau", null], + ["071405004163", "Womrath", null], + ["071405004164", "Woppenroth", null], + ["071405004165", "Würrich", null], + ["071405008002", "Altweidelbach", null], + ["071405008003", "Argenthal", null], + ["071405008008", "Belgweiler", null], + ["071405008011", "Benzweiler", null], + ["071405008012", "Bergenhausen", null], + ["071405008015", "Biebern", null], + ["071405008020", "Bubach", null], + ["071405008023", "Budenbach", null], + ["071405008027", "Dichtelbach", null], + ["071405008035", "Ellern (Hunsrück)", null], + ["071405008037", "Erbach", null], + ["071405008039", "Fronhofen", null], + ["071405008056", "Holzbach", null], + ["071405008058", "Horn", null], + ["071405008065", "Keidelheim", null], + ["071405008068", "Kisselbach", null], + ["071405008070", "Klosterkumbd", null], + ["071405008076", "Külz (Hunsrück)", null], + ["071405008077", "Kümbdchen", null], + ["071405008079", "Laubach", null], + ["071405008085", "Liebshausen", null], + ["071405008092", "Mengerschied", null], + ["071405008096", "Mörschbach", null], + ["071405008099", "Mutterschied", null], + ["071405008100", "Nannhausen", null], + ["071405008101", "Neuerkirch", null], + ["071405008106", "Niederkumbd", null], + ["071405008113", "Ohlweiler", null], + ["071405008115", "Oppertshausen", null], + ["071405008118", "Pleizenhausen", null], + ["071405008119", "Ravengiersburg", null], + ["071405008121", "Rayerschied", null], + ["071405008123", "Reich", null], + ["071405008125", "Rheinböllen, Stadt", null], + ["071405008126", "Riegenroth", null], + ["071405008127", "Riesweiler", null], + ["071405008134", "Sargenroth", null], + ["071405008138", "Schnorbach", null], + ["071405008139", "Schönborn", null], + ["071405008144", "Simmern/ Hunsrück, Stadt", null], + ["071405008148", "Steinbach", null], + ["071405008150", "Tiefenbach", null], + ["071405008158", "Wahlbach", null], + ["071405008166", "Wüschheim", null], + ["071405009005", "Badenhard", null], + ["071405009014", "Bickenbach", null], + ["071405009016", "Birkheim", null], + ["071405009025", "Damscheid", null], + ["071405009031", "Dörth", null], + ["071405009036", "Emmelshausen, Stadt", null], + ["071405009043", "Gondershausen", null], + ["071405009045", "Halsenbach", null], + ["071405009047", "Hausbay", null], + ["071405009060", "Hungenroth", null], + ["071405009063", "Karbach", null], + ["071405009075", "Kratzenburg", null], + ["071405009080", "Laudert", null], + ["071405009084", "Leiningen", null], + ["071405009087", "Lingerhahn", null], + ["071405009089", "Maisborn", null], + ["071405009093", "Mermuth", null], + ["071405009098", "Mühlpfad", null], + ["071405009102", "Ney", null], + ["071405009104", "Niederburg", null], + ["071405009108", "Niedert", null], + ["071405009110", "Norath", null], + ["071405009112", "Oberwesel, Stadt", null], + ["071405009116", "Perscheid", null], + ["071405009117", "Pfalzfeld", null], + ["071405009133", "Sankt Goar, Stadt", null], + ["071405009140", "Schwall", null], + ["071405009149", "Thörlingen", null], + ["071405009155", "Urbar", null], + ["071405009156", "Utzenhain", null], + ["071405009161", "Wiebelsheim", null], + ["071405009201", "Beulich", null], + ["071405009205", "Morshausen", null], + ["071410075075", "Lahnstein, Stadt", null], + ["071415003002", "Altendiez", null], + ["071415003005", "Aull", null], + ["071415003014", "Birlenbach", null], + ["071415003021", "Charlottenberg", null], + ["071415003022", "Cramberg", null], + ["071415003029", "Diez, Stadt", null], + ["071415003030", "Dörnberg", null], + ["071415003038", "Eppenrod", null], + ["071415003045", "Geilnau", null], + ["071415003049", "Gückingen", null], + ["071415003052", "Hambach", null], + ["071415003053", "Heistenbach", null], + ["071415003057", "Hirschberg", null], + ["071415003059", "Holzappel", null], + ["071415003061", "Holzheim", null], + ["071415003062", "Horhausen", null], + ["071415003064", "Isselbach", null], + ["071415003076", "Langenscheid", null], + ["071415003077", "Laurenburg", null], + ["071415003124", "Scheidt", null], + ["071415003130", "Steinsberg", null], + ["071415003133", "Wasenbach", null], + ["071415003503", "Balduinstein", null], + ["071415007009", "Berg", null], + ["071415007012", "Bettendorf", null], + ["071415007015", "Bogel", null], + ["071415007019", "Buch", null], + ["071415007035", "Ehr", null], + ["071415007037", "Endlichhofen", null], + ["071415007040", "Eschbach", null], + ["071415007047", "Gemmerich", null], + ["071415007055", "Himmighofen", null], + ["071415007060", "Holzhausen an der Haide", null], + ["071415007063", "Hunzel", null], + ["071415007067", "Kasdorf", null], + ["071415007070", "Kehlbach", null], + ["071415007078", "Lautert", null], + ["071415007080", "Lipporn", null], + ["071415007084", "Marienfels", null], + ["071415007085", "Miehlen", null], + ["071415007092", "Nastätten, Stadt", null], + ["071415007094", "Niederbachheim", null], + ["071415007097", "Niederwallmenach", null], + ["071415007100", "Oberbachheim", null], + ["071415007104", "Obertiefenbach", null], + ["071415007105", "Oberwallmenach", null], + ["071415007107", "Oelsberg", null], + ["071415007110", "Hainau", null], + ["071415007116", "Rettershain", null], + ["071415007120", "Ruppertshofen", null], + ["071415007131", "Strüth", null], + ["071415007134", "Weidenbach", null], + ["071415007137", "Welterod", null], + ["071415007140", "Winterwerb", null], + ["071415007502", "Diethardt", null], + ["071415009004", "Auel", null], + ["071415009016", "Bornich", null], + ["071415009023", "Dachsenhausen", null], + ["071415009024", "Dahlheim", null], + ["071415009031", "Dörscheid", null], + ["071415009042", "Filsen", null], + ["071415009066", "Kamp-Bornhofen", null], + ["071415009069", "Kaub, Stadt", null], + ["071415009072", "Kestert", null], + ["071415009079", "Lierschied", null], + ["071415009083", "Lykershausen", null], + ["071415009099", "Nochern", null], + ["071415009108", "Osterspai", null], + ["071415009109", "Patersberg", null], + ["071415009112", "Prath", null], + ["071415009114", "Reichenberg", null], + ["071415009115", "Reitzenhain", null], + ["071415009121", "Sankt Goarshausen, Loreleystadt, Stadt", null], + ["071415009122", "Sauerthal", null], + ["071415009136", "Weisel", null], + ["071415009138", "Weyer", null], + ["071415009501", "Braubach, Stadt", null], + ["071415010003", "Attenhausen", null], + ["071415010006", "Bad Ems, Stadt", null], + ["071415010008", "Becheln", null], + ["071415010025", "Dausenau", null], + ["071415010026", "Dessighofen", null], + ["071415010027", "Dienethal", null], + ["071415010033", "Dornholzhausen", null], + ["071415010041", "Fachbach", null], + ["071415010044", "Frücht", null], + ["071415010046", "Geisig", null], + ["071415010058", "Hömberg", null], + ["071415010071", "Kemmenau", null], + ["071415010082", "Lollschied", null], + ["071415010086", "Miellen", null], + ["071415010087", "Misselberg", null], + ["071415010091", "Nassau, Stadt", null], + ["071415010098", "Nievern", null], + ["071415010103", "Obernhof", null], + ["071415010106", "Oberwies", null], + ["071415010111", "Pohl", null], + ["071415010127", "Schweighausen", null], + ["071415010128", "Seelbach", null], + ["071415010129", "Singhofen", null], + ["071415010132", "Sulzbach", null], + ["071415010135", "Weinähr", null], + ["071415010139", "Winden", null], + ["071415010141", "Zimmerschied", null], + ["071415010201", "Arzbach", null], + ["071415011001", "Allendorf", null], + ["071415011010", "Berghausen", null], + ["071415011011", "Berndroth", null], + ["071415011013", "Biebrich", null], + ["071415011018", "Bremberg", null], + ["071415011020", "Burgschwalbach", null], + ["071415011032", "Dörsdorf", null], + ["071415011034", "Ebertshausen", null], + ["071415011036", "Eisighofen", null], + ["071415011039", "Ergeshausen", null], + ["071415011043", "Flacht", null], + ["071415011050", "Gutenacker", null], + ["071415011051", "Hahnstätten", null], + ["071415011054", "Herold", null], + ["071415011065", "Kaltenholzhausen", null], + ["071415011068", "Katzenelnbogen, Stadt", null], + ["071415011073", "Klingelbach", null], + ["071415011074", "Kördorf", null], + ["071415011081", "Lohrheim", null], + ["071415011088", "Mittelfischbach", null], + ["071415011089", "Mudershausen", null], + ["071415011093", "Netzbach", null], + ["071415011095", "Niederneisen", null], + ["071415011096", "Niedertiefenbach", null], + ["071415011101", "Oberfischbach", null], + ["071415011102", "Oberneisen", null], + ["071415011113", "Reckenroth", null], + ["071415011117", "Rettert", null], + ["071415011118", "Roth", null], + ["071415011125", "Schiesheim", null], + ["071415011126", "Schönborn", null], + ["071435001206", "Bad Marienberg (Westerwald), Stadt", null], + ["071435001211", "Bölsberg", null], + ["071435001216", "Dreisbach", null], + ["071435001222", "Fehl-Ritzhausen", null], + ["071435001227", "Großseifen", null], + ["071435001231", "Hahn bei Marienberg", null], + ["071435001234", "Hardt", null], + ["071435001243", "Hof", null], + ["071435001248", "Kirburg", null], + ["071435001253", "Langenbach bei Kirburg", null], + ["071435001255", "Lautzenbrücken", null], + ["071435001264", "Mörlen", null], + ["071435001270", "Neunkhausen", null], + ["071435001277", "Nisterau", null], + ["071435001279", "Nistertal", null], + ["071435001280", "Norken", null], + ["071435001297", "Stockhausen-Illfurth", null], + ["071435001300", "Unnau", null], + ["071435002202", "Alpenrod", null], + ["071435002204", "Astert", null], + ["071435002205", "Atzelgift", null], + ["071435002212", "Borod", null], + ["071435002215", "Dreifelden", null], + ["071435002223", "Gehlert", null], + ["071435002225", "Giesenhausen", null], + ["071435002229", "Hachenburg, Stadt", null], + ["071435002235", "Hattert", null], + ["071435002236", "Heimborn", null], + ["071435002240", "Heuzert", null], + ["071435002241", "Höchstenbach", null], + ["071435002250", "Kroppach", null], + ["071435002252", "Kundert", null], + ["071435002257", "Limbach", null], + ["071435002258", "Linden", null], + ["071435002259", "Lochum", null], + ["071435002260", "Luckenbach", null], + ["071435002261", "Marzhausen", null], + ["071435002262", "Merkelbach", null], + ["071435002265", "Mörsbach", null], + ["071435002267", "Mudenbach", null], + ["071435002268", "Mündersbach", null], + ["071435002269", "Müschenbach", null], + ["071435002276", "Nister", null], + ["071435002287", "Roßbach", null], + ["071435002294", "Steinebach an der Wied", null], + ["071435002296", "Stein-Wingert", null], + ["071435002299", "Streithausen", null], + ["071435002301", "Wahlrod", null], + ["071435002306", "Welkenbach", null], + ["071435002310", "Wied", null], + ["071435002313", "Winkelbach", null], + ["071435003030", "Hilgert", null], + ["071435003031", "Hillscheid", null], + ["071435003032", "Höhr-Grenzhausen, Stadt", null], + ["071435003040", "Kammerforst", null], + ["071435004005", "Boden", null], + ["071435004008", "Daubach", null], + ["071435004013", "Eitelborn", null], + ["071435004020", "Gackenbach", null], + ["071435004021", "Girod", null], + ["071435004023", "Görgeshausen", null], + ["071435004024", "Großholbach", null], + ["071435004026", "Heilberscheid", null], + ["071435004027", "Heiligenroth", null], + ["071435004033", "Holler", null], + ["071435004034", "Horbach", null], + ["071435004036", "Hübingen", null], + ["071435004039", "Kadenbach", null], + ["071435004048", "Montabaur, Stadt", null], + ["071435004051", "Nentershausen", null], + ["071435004052", "Neuhäusel", null], + ["071435004053", "Niederelbert", null], + ["071435004054", "Niedererbach", null], + ["071435004055", "Nomborn", null], + ["071435004057", "Oberelbert", null], + ["071435004065", "Ruppach-Goldhausen", null], + ["071435004071", "Simmern", null], + ["071435004072", "Stahlhofen", null], + ["071435004077", "Untershausen", null], + ["071435004079", "Welschneudorf", null], + ["071435005001", "Alsbach", null], + ["071435005006", "Breitenau", null], + ["071435005007", "Caan", null], + ["071435005009", "Deesen", null], + ["071435005038", "Hundsdorf", null], + ["071435005050", "Nauort", null], + ["071435005059", "Oberhaid", null], + ["071435005062", "Ransbach-Baumbach, Stadt", null], + ["071435005068", "Sessenbach", null], + ["071435005082", "Wirscheid", null], + ["071435005084", "Wittgert", null], + ["071435006214", "Bretthausen", null], + ["071435006218", "Elsoff (Westerwald)", null], + ["071435006237", "Hellenhahn-Schellenberg", null], + ["071435006244", "Homberg", null], + ["071435006245", "Hüblingen", null], + ["071435006246", "Irmtraut", null], + ["071435006256", "Liebenscheid", null], + ["071435006271", "Neunkirchen", null], + ["071435006272", "Neustadt/ Westerwald", null], + ["071435006274", "Niederroßbach", null], + ["071435006278", "Nister-Möhrendorf", null], + ["071435006282", "Oberrod", null], + ["071435006283", "Oberroßbach", null], + ["071435006285", "Rehe", null], + ["071435006286", "Rennerod, Stadt", null], + ["071435006291", "Salzburg", null], + ["071435006292", "Seck", null], + ["071435006295", "Stein-Neukirch", null], + ["071435006302", "Waigandshain", null], + ["071435006303", "Waldmühlen", null], + ["071435006309", "Westernohe", null], + ["071435006311", "Willingen", null], + ["071435006315", "Zehnhausen bei Rennerod", null], + ["071435007015", "Ellenhausen", null], + ["071435007018", "Freilingen", null], + ["071435007019", "Freirachdorf", null], + ["071435007022", "Goddert", null], + ["071435007025", "Hartenfels", null], + ["071435007029", "Herschbach", null], + ["071435007041", "Krümmel", null], + ["071435007044", "Marienrachdorf", null], + ["071435007045", "Maroth", null], + ["071435007046", "Maxsain", null], + ["071435007056", "Nordhofen", null], + ["071435007061", "Quirnbach", null], + ["071435007064", "Rückeroth", null], + ["071435007066", "Schenkelberg", null], + ["071435007067", "Selters (Westerwald), Stadt", null], + ["071435007069", "Sessenhausen", null], + ["071435007075", "Steinen", null], + ["071435007078", "Vielbach", null], + ["071435007085", "Wölferlingen", null], + ["071435007221", "Ewighausen", null], + ["071435007305", "Weidenhahn", null], + ["071435008011", "Dreikirchen", null], + ["071435008037", "Hundsangen", null], + ["071435008058", "Obererbach", null], + ["071435008074", "Steinefrenz", null], + ["071435008080", "Weroth", null], + ["071435008203", "Arnshöfen", null], + ["071435008208", "Berod bei Wallmerod", null], + ["071435008210", "Bilkheim", null], + ["071435008220", "Ettinghausen", null], + ["071435008232", "Hahn am See", null], + ["071435008239", "Herschbach (Oberwesterwald)", null], + ["071435008251", "Kuhnhöfen", null], + ["071435008263", "Meudt", null], + ["071435008266", "Molsberg", null], + ["071435008273", "Niederahr", null], + ["071435008281", "Oberahr", null], + ["071435008290", "Salz", null], + ["071435008304", "Wallmerod", null], + ["071435008316", "Zehnhausen bei Wallmerod", null], + ["071435008501", "Elbingen", null], + ["071435008502", "Mähren", null], + ["071435009200", "Ailertchen", null], + ["071435009207", "Bellingen", null], + ["071435009209", "Berzhahn", null], + ["071435009213", "Brandscheid", null], + ["071435009219", "Enspel", null], + ["071435009224", "Gemünden", null], + ["071435009226", "Girkenroth", null], + ["071435009228", "Guckheim", null], + ["071435009230", "Härtlingen", null], + ["071435009233", "Halbs", null], + ["071435009238", "Hergenroth", null], + ["071435009242", "Höhn", null], + ["071435009247", "Kaden", null], + ["071435009249", "Kölbingen", null], + ["071435009254", "Langenhahn", null], + ["071435009284", "Pottum", null], + ["071435009288", "Rotenhain", null], + ["071435009289", "Rothenbach", null], + ["071435009293", "Stahlhofen am Wiesensee", null], + ["071435009298", "Stockum-Püschen", null], + ["071435009307", "Weltersburg", null], + ["071435009308", "Westerburg, Stadt", null], + ["071435009312", "Willmenrod", null], + ["071435009314", "Winnen", null], + ["071435010003", "Bannberscheid", null], + ["071435010010", "Dernbach (Westerwald)", null], + ["071435010012", "Ebernhahn", null], + ["071435010028", "Helferskirchen", null], + ["071435010042", "Leuterod", null], + ["071435010047", "Mogendorf", null], + ["071435010049", "Moschheim", null], + ["071435010060", "Ötzingen", null], + ["071435010070", "Siershahn", null], + ["071435010073", "Staudt", null], + ["071435010081", "Wirges, Stadt", null], + ["071435010275", "Niedersayn", null], + ["072110000000", "Trier, Stadt", null], + ["072310134134", "Wittlich, Stadt", null], + ["072310502502", "Morbach", null], + ["072315001008", "Bernkastel-Kues, Stadt", null], + ["072315001012", "Brauneberg", null], + ["072315001016", "Burgen", null], + ["072315001030", "Erden", null], + ["072315001040", "Gornhausen", null], + ["072315001041", "Graach an der Mosel", null], + ["072315001056", "Hochscheid", null], + ["072315001066", "Kesten", null], + ["072315001070", "Kleinich", null], + ["072315001071", "Kommen", null], + ["072315001075", "Lieser", null], + ["072315001076", "Lösnich", null], + ["072315001077", "Longkamp", null], + ["072315001081", "Maring-Noviand", null], + ["072315001086", "Minheim", null], + ["072315001087", "Monzelfeld", null], + ["072315001090", "Mülheim an der Mosel", null], + ["072315001092", "Neumagen-Dhron", null], + ["072315001105", "Piesport", null], + ["072315001125", "Ürzig", null], + ["072315001126", "Veldenz", null], + ["072315001133", "Wintrich", null], + ["072315001136", "Zeltingen-Rachtig", null], + ["072315006006", "Berglicht", null], + ["072315006017", "Burtscheid", null], + ["072315006018", "Deuselbach", null], + ["072315006019", "Dhronecken", null], + ["072315006032", "Etgert", null], + ["072315006035", "Gielert", null], + ["072315006042", "Gräfendhron", null], + ["072315006054", "Hilscheid", null], + ["072315006058", "Horath", null], + ["072315006064", "Immert", null], + ["072315006078", "Lückenburg", null], + ["072315006079", "Malborn", null], + ["072315006083", "Merschbach", null], + ["072315006093", "Neunkirchen", null], + ["072315006112", "Rorodt", null], + ["072315006115", "Schönberg", null], + ["072315006122", "Talling", null], + ["072315006123", "Thalfang", null], + ["072315006202", "Breit", null], + ["072315006203", "Büdlich", null], + ["072315006204", "Heidenburg", null], + ["072315008001", "Altrich", null], + ["072315008003", "Arenrath", null], + ["072315008007", "Bergweiler", null], + ["072315008009", "Bettenfeld", null], + ["072315008010", "Binsfeld", null], + ["072315008013", "Bruch", null], + ["072315008021", "Dierfeld", null], + ["072315008022", "Dierscheid", null], + ["072315008023", "Dodenburg", null], + ["072315008024", "Dreis", null], + ["072315008025", "Eckfeld", null], + ["072315008026", "Eisenschmitt", null], + ["072315008031", "Esch", null], + ["072315008036", "Gipperath", null], + ["072315008037", "Gladbach", null], + ["072315008044", "Greimerath", null], + ["072315008046", "Großlittgen", null], + ["072315008049", "Hasborn", null], + ["072315008050", "Heckenmünster", null], + ["072315008051", "Heidweiler", null], + ["072315008053", "Hetzerath", null], + ["072315008062", "Hupperath", null], + ["072315008065", "Karl", null], + ["072315008069", "Klausen", null], + ["072315008074", "Laufeld", null], + ["072315008080", "Manderscheid, Stadt", null], + ["072315008082", "Meerfeld", null], + ["072315008085", "Minderlittgen", null], + ["072315008091", "Musweiler", null], + ["072315008095", "Niederöfflingen", null], + ["072315008096", "Niederscheidweiler", null], + ["072315008100", "Oberöfflingen", null], + ["072315008101", "Oberscheidweiler", null], + ["072315008103", "Osann-Monzel", null], + ["072315008104", "Pantenburg", null], + ["072315008107", "Platten", null], + ["072315008108", "Plein", null], + ["072315008111", "Rivenich", null], + ["072315008113", "Salmtal", null], + ["072315008114", "Schladt", null], + ["072315008116", "Schwarzenborn", null], + ["072315008117", "Sehlem", null], + ["072315008127", "Wallscheid", null], + ["072315008503", "Landscheid", null], + ["072315008504", "Niersbach", null], + ["072315009004", "Bausendorf", null], + ["072315009005", "Bengel", null], + ["072315009014", "Burg (Mosel)", null], + ["072315009020", "Diefenbach", null], + ["072315009029", "Enkirch", null], + ["072315009033", "Flußbach", null], + ["072315009057", "Hontheim", null], + ["072315009067", "Kinderbeuern", null], + ["072315009068", "Kinheim", null], + ["072315009072", "Kröv", null], + ["072315009110", "Reil", null], + ["072315009120", "Starkenburg", null], + ["072315009124", "Traben-Trarbach, Stadt", null], + ["072315009132", "Willwerscheid", null], + ["072315009206", "Lötzbeuren", null], + ["072315009501", "Irmenach", null], + ["072320018018", "Bitburg, Stadt", null], + ["072325001201", "Arzfeld", null], + ["072325001211", "Dackscheid", null], + ["072325001212", "Dahnen", null], + ["072325001213", "Daleiden", null], + ["072325001214", "Dasburg", null], + ["072325001217", "Eilscheid", null], + ["072325001220", "Eschfeld", null], + ["072325001221", "Euscheid", null], + ["072325001229", "Großkampenberg", null], + ["072325001233", "Hargarten", null], + ["072325001234", "Harspelt", null], + ["072325001240", "Herzfeld", null], + ["072325001245", "Irrhausen", null], + ["072325001246", "Jucken", null], + ["072325001247", "Kesfeld", null], + ["072325001248", "Kickeshausen", null], + ["072325001249", "Kinzenburg", null], + ["072325001253", "Krautscheid", null], + ["072325001254", "Lambertsberg", null], + ["072325001255", "Lascheid", null], + ["072325001258", "Lauperath", null], + ["072325001259", "Leidenborn", null], + ["072325001260", "Lichtenborn", null], + ["072325001261", "Lierfeld", null], + ["072325001262", "Lünebach", null], + ["072325001263", "Lützkampen", null], + ["072325001264", "Manderscheid", null], + ["072325001267", "Mauel", null], + ["072325001270", "Merlscheid", null], + ["072325001277", "Niederpierscheid", null], + ["072325001285", "Oberpierscheid", null], + ["072325001287", "Olmscheid", null], + ["072325001291", "Pintesfeld", null], + ["072325001293", "Plütscheid", null], + ["072325001294", "Preischeid", null], + ["072325001297", "Reiff", null], + ["072325001298", "Reipeldingen", null], + ["072325001301", "Roscheid", null], + ["072325001309", "Sengerich", null], + ["072325001310", "Sevenig (Our)", null], + ["072325001315", "Strickscheid", null], + ["072325001322", "Waxweiler", null], + ["072325001333", "Üttfeld", null], + ["072325005001", "Affler", null], + ["072325005002", "Alsdorf", null], + ["072325005003", "Altscheid", null], + ["072325005004", "Ammeldingen an der Our", null], + ["072325005005", "Ammeldingen bei Neuerburg", null], + ["072325005008", "Bauler", null], + ["072325005011", "Berkoth", null], + ["072325005012", "Berscheid", null], + ["072325005016", "Biesdorf", null], + ["072325005019", "Bollendorf", null], + ["072325005022", "Burg", null], + ["072325005025", "Dauwelshausen", null], + ["072325005028", "Echternacherbrück", null], + ["072325005031", "Emmelbaum", null], + ["072325005033", "Ernzen", null], + ["072325005037", "Ferschweiler", null], + ["072325005038", "Fischbach-Oberraden", null], + ["072325005040", "Geichlingen", null], + ["072325005041", "Gemünd", null], + ["072325005042", "Gentingen", null], + ["072325005047", "Heilbach", null], + ["072325005049", "Herbstmühle", null], + ["072325005053", "Holsthum", null], + ["072325005054", "Hommerdingen", null], + ["072325005056", "Hütten", null], + ["072325005059", "Hüttingen bei Lahr", null], + ["072325005063", "Irrel", null], + ["072325005064", "Karlshausen", null], + ["072325005065", "Kaschenbach", null], + ["072325005066", "Keppeshausen", null], + ["072325005067", "Körperich", null], + ["072325005068", "Koxhausen", null], + ["072325005069", "Kruchten", null], + ["072325005072", "Lahr", null], + ["072325005073", "Leimbach", null], + ["072325005078", "Menningen", null], + ["072325005080", "Mettendorf", null], + ["072325005082", "Minden", null], + ["072325005084", "Muxerath", null], + ["072325005085", "Nasingen", null], + ["072325005088", "Neuerburg, Stadt", null], + ["072325005089", "Niedergeckler", null], + ["072325005090", "Niederraden", null], + ["072325005093", "Niederweis", null], + ["072325005094", "Niehl", null], + ["072325005095", "Nusbaum", null], + ["072325005096", "Obergeckler", null], + ["072325005102", "Utscheid", null], + ["072325005103", "Peffingen", null], + ["072325005106", "Plascheid", null], + ["072325005108", "Prümzurlay", null], + ["072325005110", "Rodershausen", null], + ["072325005112", "Roth an der Our", null], + ["072325005114", "Schankweiler", null], + ["072325005116", "Scheitenkorb", null], + ["072325005117", "Scheuern", null], + ["072325005121", "Sevenig bei Neuerburg", null], + ["072325005122", "Sinspelt", null], + ["072325005127", "Übereisenbach", null], + ["072325005128", "Uppershausen", null], + ["072325005130", "Waldhof-Falkenstein", null], + ["072325005131", "Wallendorf", null], + ["072325005132", "Weidingen", null], + ["072325005138", "Zweifelscheid", null], + ["072325005218", "Eisenach", null], + ["072325005225", "Gilzem", null], + ["072325006202", "Auw bei Prüm", null], + ["072325006206", "Bleialf", null], + ["072325006207", "Brandscheid", null], + ["072325006208", "Buchet", null], + ["072325006209", "Büdesheim", null], + ["072325006216", "Dingdorf", null], + ["072325006222", "Feuerscheid", null], + ["072325006223", "Fleringen", null], + ["072325006224", "Giesdorf", null], + ["072325006226", "Weinsheim", null], + ["072325006227", "Gondenbrett", null], + ["072325006230", "Großlangenfeld", null], + ["072325006231", "Habscheid", null], + ["072325006236", "Heckhuscheid", null], + ["072325006238", "Heisdorf", null], + ["072325006250", "Kleinlangenfeld", null], + ["072325006256", "Lasel", null], + ["072325006265", "Masthorn", null], + ["072325006266", "Matzerath", null], + ["072325006271", "Mützenich", null], + ["072325006272", "Neuendorf", null], + ["072325006276", "Niederlauch", null], + ["072325006279", "Nimshuscheid", null], + ["072325006280", "Nimsreuland", null], + ["072325006283", "Oberlascheid", null], + ["072325006284", "Oberlauch", null], + ["072325006288", "Olzheim", null], + ["072325006290", "Orlenbach", null], + ["072325006292", "Pittenbach", null], + ["072325006295", "Pronsfeld", null], + ["072325006296", "Prüm, Stadt", null], + ["072325006300", "Rommersheim", null], + ["072325006302", "Roth bei Prüm", null], + ["072325006304", "Schönecken", null], + ["072325006305", "Schwirzheim", null], + ["072325006307", "Seiwerath", null], + ["072325006308", "Sellerich", null], + ["072325006318", "Wallersheim", null], + ["072325006320", "Watzerath", null], + ["072325006321", "Wawern", null], + ["072325006327", "Winringen", null], + ["072325006328", "Winterscheid", null], + ["072325006329", "Winterspelt", null], + ["072325006332", "Hersdorf", null], + ["072325007006", "Auw an der Kyll", null], + ["072325007010", "Beilingen", null], + ["072325007050", "Herforst", null], + ["072325007055", "Hosten", null], + ["072325007104", "Philippsheim", null], + ["072325007107", "Preist", null], + ["072325007123", "Speicher, Stadt", null], + ["072325007289", "Orenhofen", null], + ["072325007311", "Spangdahlem", null], + ["072325008007", "Badem", null], + ["072325008009", "Baustert", null], + ["072325008013", "Bettingen", null], + ["072325008014", "Bickendorf", null], + ["072325008015", "Biersdorf am See", null], + ["072325008017", "Birtlingen", null], + ["072325008020", "Brecht", null], + ["072325008024", "Dahlem", null], + ["072325008026", "Dockendorf", null], + ["072325008027", "Dudeldorf", null], + ["072325008029", "Echtershausen", null], + ["072325008030", "Ehlenz", null], + ["072325008032", "Enzen", null], + ["072325008034", "Eßlingen", null], + ["072325008035", "Etteldorf", null], + ["072325008036", "Feilsdorf", null], + ["072325008039", "Fließem", null], + ["072325008043", "Gindorf", null], + ["072325008044", "Gondorf", null], + ["072325008045", "Halsdorf", null], + ["072325008046", "Hamm", null], + ["072325008048", "Heilenbach", null], + ["072325008057", "Hütterscheid", null], + ["072325008058", "Hüttingen an der Kyll", null], + ["072325008060", "Idenheim", null], + ["072325008061", "Idesheim", null], + ["072325008062", "Ingendorf", null], + ["072325008070", "Kyllburg, Stadt", null], + ["072325008071", "Kyllburgweiler", null], + ["072325008074", "Ließem", null], + ["072325008075", "Malberg", null], + ["072325008076", "Malbergweich", null], + ["072325008077", "Meckel", null], + ["072325008079", "Messerich", null], + ["072325008081", "Metterich", null], + ["072325008083", "Mülbach", null], + ["072325008086", "Nattenheim", null], + ["072325008087", "Neidenbach", null], + ["072325008091", "Niederstedem", null], + ["072325008092", "Niederweiler", null], + ["072325008097", "Oberstedem", null], + ["072325008098", "Oberweiler", null], + ["072325008099", "Oberweis", null], + ["072325008100", "Olsdorf", null], + ["072325008101", "Orsfeld", null], + ["072325008105", "Pickließem", null], + ["072325008109", "Rittersdorf", null], + ["072325008111", "Röhl", null], + ["072325008113", "Sankt Thomas", null], + ["072325008115", "Scharfbillig", null], + ["072325008118", "Schleid", null], + ["072325008119", "Seffern", null], + ["072325008120", "Sefferweich", null], + ["072325008124", "Stockem", null], + ["072325008125", "Sülm", null], + ["072325008126", "Trimport", null], + ["072325008129", "Usch", null], + ["072325008133", "Wettlingen", null], + ["072325008134", "Wiersdorf", null], + ["072325008135", "Wilsecker", null], + ["072325008137", "Wolsfeld", null], + ["072325008203", "Balesfeld", null], + ["072325008210", "Burbach", null], + ["072325008228", "Gransdorf", null], + ["072325008273", "Neuheilenbach", null], + ["072325008282", "Oberkail", null], + ["072325008306", "Seinsfeld", null], + ["072325008313", "Steinborn", null], + ["072325008331", "Zendscheid", null], + ["072325008501", "Wißmannsdorf", null], + ["072325008502", "Brimingen", null], + ["072335001006", "Betteldorf", null], + ["072335001008", "Bleckhausen", null], + ["072335001011", "Brockscheid", null], + ["072335001014", "Darscheid", null], + ["072335001016", "Demerath", null], + ["072335001017", "Deudesfeld", null], + ["072335001018", "Dockweiler", null], + ["072335001020", "Dreis-Brück", null], + ["072335001021", "Ellscheid", null], + ["072335001025", "Gefell", null], + ["072335001027", "Gillenfeld", null], + ["072335001030", "Hinterweiler", null], + ["072335001031", "Hörscheid", null], + ["072335001034", "Immerath", null], + ["072335001039", "Kirchweiler", null], + ["072335001040", "Kradenbach", null], + ["072335001042", "Mehren", null], + ["072335001043", "Meisburg", null], + ["072335001046", "Mückeln", null], + ["072335001049", "Nerdlen", null], + ["072335001052", "Niederstadtfeld", null], + ["072335001055", "Oberstadtfeld", null], + ["072335001061", "Sarmersbach", null], + ["072335001062", "Saxler", null], + ["072335001063", "Schalkenmehren", null], + ["072335001064", "Schönbach", null], + ["072335001065", "Schutz", null], + ["072335001067", "Steineberg", null], + ["072335001068", "Steiningen", null], + ["072335001070", "Strohn", null], + ["072335001071", "Strotzbüsch", null], + ["072335001074", "Udler", null], + ["072335001075", "Üdersdorf", null], + ["072335001077", "Utzerath", null], + ["072335001079", "Wallenborn", null], + ["072335001081", "Weidenbach", null], + ["072335001084", "Winkel (Eifel)", null], + ["072335001501", "Daun, Stadt", null], + ["072335004003", "Beinhausen", null], + ["072335004010", "Boxberg", null], + ["072335004032", "Hörschhausen", null], + ["072335004037", "Katzwinkel", null], + ["072335004048", "Neichen", null], + ["072335004201", "Arbach", null], + ["072335004202", "Bereborn", null], + ["072335004203", "Berenbach", null], + ["072335004205", "Bodenbach", null], + ["072335004206", "Bongard", null], + ["072335004207", "Borler", null], + ["072335004208", "Brücktal", null], + ["072335004210", "Drees", null], + ["072335004212", "Gelenberg", null], + ["072335004213", "Gunderath", null], + ["072335004215", "Höchstberg", null], + ["072335004216", "Horperath", null], + ["072335004217", "Kaperich", null], + ["072335004218", "Kelberg", null], + ["072335004220", "Kirsbach", null], + ["072335004221", "Kötterichen", null], + ["072335004222", "Kolverath", null], + ["072335004224", "Lirstal", null], + ["072335004225", "Mannebach", null], + ["072335004226", "Mosbruch", null], + ["072335004228", "Nitz", null], + ["072335004230", "Oberelz", null], + ["072335004233", "Reimerath", null], + ["072335004234", "Retterath", null], + ["072335004236", "Sassen", null], + ["072335004242", "Uersfeld", null], + ["072335004243", "Ueß", null], + ["072335004244", "Welcherath", null], + ["072335006002", "Basberg", null], + ["072335006004", "Berlingen", null], + ["072335006005", "Berndorf", null], + ["072335006007", "Birgel", null], + ["072335006019", "Dohm-Lammersdorf", null], + ["072335006022", "Esch", null], + ["072335006023", "Feusdorf", null], + ["072335006026", "Gerolstein, Stadt", null], + ["072335006028", "Gönnersdorf", null], + ["072335006029", "Hillesheim, Stadt", null], + ["072335006033", "Hohenfels-Essingen", null], + ["072335006035", "Jünkerath", null], + ["072335006036", "Kalenborn-Scheuern", null], + ["072335006038", "Kerpen (Eifel)", null], + ["072335006041", "Lissendorf", null], + ["072335006050", "Neroth", null], + ["072335006053", "Oberbettingen", null], + ["072335006054", "Oberehe-Stroheich", null], + ["072335006056", "Pelm", null], + ["072335006058", "Rockeskyll", null], + ["072335006060", "Salm", null], + ["072335006076", "Üxheim", null], + ["072335006080", "Walsdorf", null], + ["072335006083", "Wiesbaum", null], + ["072335006204", "Birresborn", null], + ["072335006209", "Densborn", null], + ["072335006211", "Duppach", null], + ["072335006214", "Hallschlag", null], + ["072335006219", "Kerschenbach", null], + ["072335006223", "Kopp", null], + ["072335006227", "Mürlenbach", null], + ["072335006229", "Nohn", null], + ["072335006232", "Ormont", null], + ["072335006235", "Reuth", null], + ["072335006237", "Scheid", null], + ["072335006239", "Schüller", null], + ["072335006240", "Stadtkyll", null], + ["072335006241", "Steffeln", null], + ["072355001005", "Bescheid", null], + ["072355001008", "Beuren (Hochwald)", null], + ["072355001014", "Damflos", null], + ["072355001030", "Geisfeld", null], + ["072355001035", "Grimburg", null], + ["072355001036", "Gusenburg", null], + ["072355001045", "Hermeskeil, Stadt", null], + ["072355001047", "Hinzert-Pölert", null], + ["072355001092", "Naurath (Wald)", null], + ["072355001093", "Neuhütten", null], + ["072355001112", "Rascheid", null], + ["072355001114", "Reinsfeld", null], + ["072355001153", "Züsch", null], + ["072355003055", "Kanzem", null], + ["072355003068", "Konz, Stadt", null], + ["072355003095", "Nittel", null], + ["072355003096", "Oberbillig", null], + ["072355003101", "Onsdorf", null], + ["072355003106", "Pellingen", null], + ["072355003132", "Tawern", null], + ["072355003133", "Temmels", null], + ["072355003143", "Wasserliesch", null], + ["072355003144", "Wawern", null], + ["072355003146", "Wellen", null], + ["072355003148", "Wiltingen", null], + ["072355004010", "Bonerath", null], + ["072355004021", "Farschweiler", null], + ["072355004037", "Gusterath", null], + ["072355004038", "Gutweiler", null], + ["072355004044", "Herl", null], + ["072355004046", "Hinzenburg", null], + ["072355004050", "Holzerath", null], + ["072355004056", "Kasel", null], + ["072355004070", "Korlingen", null], + ["072355004080", "Lorscheid", null], + ["072355004085", "Mertesdorf", null], + ["072355004090", "Morscheid", null], + ["072355004100", "Ollmuth", null], + ["072355004103", "Osburg", null], + ["072355004107", "Pluwig", null], + ["072355004116", "Riveris", null], + ["072355004124", "Schöndorf", null], + ["072355004129", "Sommerau", null], + ["072355004135", "Thomm", null], + ["072355004141", "Waldrach", null], + ["072355006004", "Bekond", null], + ["072355006015", "Detzem", null], + ["072355006019", "Ensch", null], + ["072355006022", "Fell", null], + ["072355006026", "Föhren", null], + ["072355006060", "Kenn", null], + ["072355006063", "Klüsserath", null], + ["072355006067", "Köwerich", null], + ["072355006074", "Leiwen", null], + ["072355006077", "Longen", null], + ["072355006078", "Longuich", null], + ["072355006083", "Mehring", null], + ["072355006091", "Naurath (Eifel)", null], + ["072355006108", "Pölich", null], + ["072355006115", "Riol", null], + ["072355006120", "Schleich", null], + ["072355006125", "Schweich, Stadt", null], + ["072355006134", "Thörnich", null], + ["072355006207", "Trittenheim", null], + ["072355007001", "Aach", null], + ["072355007027", "Franzenheim", null], + ["072355007048", "Hockweiler", null], + ["072355007051", "Igel", null], + ["072355007069", "Kordel", null], + ["072355007073", "Langsur", null], + ["072355007094", "Newel", null], + ["072355007111", "Ralingen", null], + ["072355007137", "Trierweiler", null], + ["072355007151", "Zemmer", null], + ["072355007501", "Welschbillig", null], + ["072355008002", "Ayl", null], + ["072355008003", "Baldringen", null], + ["072355008025", "Fisch", null], + ["072355008028", "Freudenburg", null], + ["072355008033", "Greimerath", null], + ["072355008040", "Heddert", null], + ["072355008043", "Hentern", null], + ["072355008052", "Irsch", null], + ["072355008057", "Kastel-Staadt", null], + ["072355008058", "Kell am See", null], + ["072355008062", "Kirf", null], + ["072355008072", "Lampaden", null], + ["072355008081", "Mandern", null], + ["072355008082", "Mannebach", null], + ["072355008098", "Ockfen", null], + ["072355008104", "Palzem", null], + ["072355008105", "Paschel", null], + ["072355008118", "Saarburg, Stadt", null], + ["072355008119", "Schillingen", null], + ["072355008122", "Schoden", null], + ["072355008123", "Schömerich", null], + ["072355008126", "Serrig", null], + ["072355008131", "Taben-Rodt", null], + ["072355008136", "Trassem", null], + ["072355008140", "Vierherrenborn", null], + ["072355008142", "Waldweiler", null], + ["072355008149", "Wincheringen", null], + ["072355008152", "Zerf", null], + ["072355008154", "Merzkirchen", null], + ["073110000000", "Frankenthal (Pfalz), Stadt", null], + ["073120000000", "Kaiserslautern, Stadt", null], + ["073130000000", "Landau in der Pfalz, Stadt", null], + ["073140000000", "Ludwigshafen am Rhein, Stadt", null], + ["073150000000", "Mainz, Stadt", null], + ["073160000000", "Neustadt an der Weinstraße, Stadt", null], + ["073170000000", "Pirmasens, Stadt", null], + ["073180000000", "Speyer, Stadt", null], + ["073190000000", "Worms, Stadt", null], + ["073200000000", "Zweibrücken, Stadt", null], + ["073310003003", "Alzey, Stadt", null], + ["073315001001", "Albig", null], + ["073315001005", "Bechenheim", null], + ["073315001007", "Bechtolsheim", null], + ["073315001008", "Bermersheim vor der Höhe", null], + ["073315001010", "Biebelnheim", null], + ["073315001012", "Bornheim", null], + ["073315001014", "Dintesheim", null], + ["073315001020", "Eppelsheim", null], + ["073315001021", "Erbes-Büdesheim", null], + ["073315001022", "Esselborn", null], + ["073315001024", "Flomborn", null], + ["073315001025", "Flonheim", null], + ["073315001026", "Framersheim", null], + ["073315001027", "Freimersheim", null], + ["073315001031", "Gau-Heppenheim", null], + ["073315001032", "Gau-Odernheim", null], + ["073315001042", "Kettenheim", null], + ["073315001043", "Lonsheim", null], + ["073315001044", "Mauchenheim", null], + ["073315001050", "Nack", null], + ["073315001051", "Nieder-Wiesen", null], + ["073315001052", "Ober-Flörsheim", null], + ["073315001053", "Offenheim", null], + ["073315001067", "Wahlheim", null], + ["073315002002", "Alsheim", null], + ["073315002018", "Eich", null], + ["073315002034", "Gimbsheim", null], + ["073315002038", "Hamm am Rhein", null], + ["073315002045", "Mettenheim", null], + ["073315003023", "Flörsheim-Dalsheim", null], + ["073315003041", "Hohen-Sülzen", null], + ["073315003046", "Mölsheim", null], + ["073315003047", "Mörstadt", null], + ["073315003048", "Monsheim", null], + ["073315003054", "Offstein", null], + ["073315003066", "Wachenheim", null], + ["073315005017", "Eckelsheim", null], + ["073315005030", "Gau-Bickelheim", null], + ["073315005035", "Gumbsheim", null], + ["073315005060", "Siefersheim", null], + ["073315005062", "Stein-Bockenheim", null], + ["073315005070", "Wendelsheim", null], + ["073315005072", "Wöllstein", null], + ["073315005075", "Wonsheim", null], + ["073315006004", "Armsheim", null], + ["073315006019", "Ensheim", null], + ["073315006029", "Gabsheim", null], + ["073315006033", "Gau-Weinheim", null], + ["073315006056", "Partenheim", null], + ["073315006058", "Saulheim", null], + ["073315006059", "Schornsheim", null], + ["073315006061", "Spiesheim", null], + ["073315006063", "Sulzheim", null], + ["073315006064", "Udenheim", null], + ["073315006065", "Vendersheim", null], + ["073315006068", "Wallertheim", null], + ["073315006073", "Wörrstadt, Stadt", null], + ["073315007006", "Bechtheim", null], + ["073315007009", "Bermersheim", null], + ["073315007011", "Hochborn", null], + ["073315007015", "Dittelsheim-Heßloch", null], + ["073315007028", "Frettenheim", null], + ["073315007036", "Gundersheim", null], + ["073315007037", "Gundheim", null], + ["073315007039", "Hangen-Weisheim", null], + ["073315007049", "Monzernheim", null], + ["073315007055", "Osthofen, Stadt", null], + ["073315007071", "Westhofen", null], + ["073320002002", "Bad Dürkheim, Stadt", null], + ["073320024024", "Grünstadt, Stadt", null], + ["073320025025", "Haßloch", null], + ["073325001009", "Deidesheim, Stadt", null], + ["073325001017", "Forst an der Weinstraße", null], + ["073325001035", "Meckenheim", null], + ["073325001039", "Niederkirchen bei Deidesheim", null], + ["073325001043", "Ruppertsberg", null], + ["073325002005", "Bobenheim am Berg", null], + ["073325002008", "Dackenheim", null], + ["073325002015", "Erpolzheim", null], + ["073325002019", "Freinsheim, Stadt", null], + ["073325002026", "Herxheim am Berg", null], + ["073325002028", "Kallstadt", null], + ["073325002049", "Weisenheim am Berg", null], + ["073325002050", "Weisenheim am Sand", null], + ["073325005014", "Elmstein", null], + ["073325005016", "Esthal", null], + ["073325005018", "Frankeneck", null], + ["073325005032", "Lambrecht (Pfalz), Stadt", null], + ["073325005034", "Lindenberg", null], + ["073325005037", "Neidenfels", null], + ["073325005048", "Weidenthal", null], + ["073325006013", "Ellerstadt", null], + ["073325006020", "Friedelsheim", null], + ["073325006022", "Gönnheim", null], + ["073325006046", "Wachenheim an der Weinstraße, Stadt", null], + ["073325007001", "Altleiningen", null], + ["073325007003", "Battenberg (Pfalz)", null], + ["073325007004", "Bissersheim", null], + ["073325007006", "Bockenheim an der Weinstraße", null], + ["073325007007", "Carlsberg", null], + ["073325007010", "Dirmstein", null], + ["073325007012", "Ebertsheim", null], + ["073325007021", "Gerolsheim", null], + ["073325007023", "Großkarlbach", null], + ["073325007027", "Hettenleidelheim", null], + ["073325007029", "Kindenheim", null], + ["073325007030", "Kirchheim an der Weinstraße", null], + ["073325007031", "Kleinkarlbach", null], + ["073325007033", "Laumersheim", null], + ["073325007036", "Mertesheim", null], + ["073325007038", "Neuleiningen", null], + ["073325007040", "Obersülzen", null], + ["073325007041", "Obrigheim (Pfalz)", null], + ["073325007042", "Quirnheim", null], + ["073325007044", "Tiefenthal", null], + ["073325007047", "Wattenheim", null], + ["073335002019", "Eisenberg (Pfalz), Stadt", null], + ["073335002038", "Kerzenheim", null], + ["073335002060", "Ramsen", null], + ["073335003001", "Albisheim (Pfrimm)", null], + ["073335003006", "Biedesheim", null], + ["073335003012", "Bubenheim", null], + ["073335003017", "Dreisen", null], + ["073335003018", "Einselthum", null], + ["073335003026", "Göllheim", null], + ["073335003032", "Immesheim", null], + ["073335003041", "Lautersheim", null], + ["073335003058", "Ottersheim", null], + ["073335003064", "Rüssingen", null], + ["073335003074", "Standenbühl", null], + ["073335003081", "Weitersweiler", null], + ["073335003501", "Zellertal", null], + ["073335004005", "Bennhausen", null], + ["073335004007", "Bischheim", null], + ["073335004010", "Bolanden", null], + ["073335004013", "Dannenfels", null], + ["073335004022", "Gauersheim", null], + ["073335004031", "Ilbesheim", null], + ["073335004035", "Jakobsweiler", null], + ["073335004039", "Kirchheimbolanden, Stadt", null], + ["073335004040", "Kriegsfeld", null], + ["073335004045", "Marnheim", null], + ["073335004046", "Mörsfeld", null], + ["073335004047", "Morschheim", null], + ["073335004056", "Oberwiesen", null], + ["073335004057", "Orbis", null], + ["073335004062", "Rittersheim", null], + ["073335004076", "Stetten", null], + ["073335006009", "Börrstadt", null], + ["073335006011", "Breunigweiler", null], + ["073335006020", "Falkenstein", null], + ["073335006027", "Gonbach", null], + ["073335006030", "Höringen", null], + ["073335006033", "Imsbach", null], + ["073335006042", "Lohnsfeld", null], + ["073335006048", "Münchweiler an der Alsenz", null], + ["073335006069", "Schweisweiler", null], + ["073335006071", "Sippersfeld", null], + ["073335006075", "Steinbach am Donnersberg", null], + ["073335006080", "Wartenberg-Rohrbach", null], + ["073335006503", "Winnweiler", null], + ["073335007003", "Alsenz", null], + ["073335007004", "Bayerfeld-Steckweiler", null], + ["073335007008", "Bisterschied", null], + ["073335007014", "Dielkirchen", null], + ["073335007016", "Dörrmoschel", null], + ["073335007021", "Finkenbach-Gersweiler", null], + ["073335007023", "Gaugrehweiler", null], + ["073335007024", "Gehrweiler", null], + ["073335007025", "Gerbach", null], + ["073335007028", "Gundersweiler", null], + ["073335007034", "Imsweiler", null], + ["073335007036", "Kalkofen", null], + ["073335007037", "Katzenbach", null], + ["073335007043", "Mannweiler-Cölln", null], + ["073335007049", "Münsterappel", null], + ["073335007050", "Niederhausen an der Appel", null], + ["073335007051", "Niedermoschel", null], + ["073335007053", "Oberhausen an der Appel", null], + ["073335007054", "Obermoschel, Stadt", null], + ["073335007055", "Oberndorf", null], + ["073335007061", "Ransweiler", null], + ["073335007065", "Ruppertsecken", null], + ["073335007066", "Sankt Alban", null], + ["073335007067", "Schiersfeld", null], + ["073335007068", "Schönborn", null], + ["073335007072", "Sitters", null], + ["073335007073", "Stahlberg", null], + ["073335007077", "Teschenmoschel", null], + ["073335007078", "Unkenbach", null], + ["073335007079", "Waldgrehweiler", null], + ["073335007083", "Winterborn", null], + ["073335007084", "Würzweiler", null], + ["073335007201", "Rathskirchen", null], + ["073335007202", "Reichsthal", null], + ["073335007203", "Seelen", null], + ["073335007502", "Rockenhausen, Stadt", null], + ["073340007007", "Germersheim, Stadt", null], + ["073340501501", "Wörth am Rhein, Stadt", null], + ["073345001001", "Bellheim", null], + ["073345001014", "Knittelsheim", null], + ["073345001023", "Ottersheim bei Landau", null], + ["073345001036", "Zeiskam", null], + ["073345002002", "Berg (Pfalz)", null], + ["073345002008", "Hagenbach, Stadt", null], + ["073345002021", "Neuburg am Rhein", null], + ["073345002027", "Scheibenhardt", null], + ["073345003009", "Hatzenbühl", null], + ["073345003012", "Jockgrim", null], + ["073345003022", "Neupotz", null], + ["073345003024", "Rheinzabern", null], + ["073345004004", "Erlenbach bei Kandel", null], + ["073345004005", "Freckenfeld", null], + ["073345004013", "Kandel, Stadt", null], + ["073345004020", "Minfeld", null], + ["073345004030", "Steinweiler", null], + ["073345004031", "Vollmersweiler", null], + ["073345004034", "Winden", null], + ["073345005006", "Freisbach", null], + ["073345005017", "Lingenfeld", null], + ["073345005018", "Lustadt", null], + ["073345005028", "Schwegenheim", null], + ["073345005032", "Weingarten (Pfalz)", null], + ["073345005033", "Westheim (Pfalz)", null], + ["073345006011", "Hördt", null], + ["073345006015", "Kuhardt", null], + ["073345006016", "Leimersheim", null], + ["073345006025", "Rülzheim", null], + ["073355001003", "Bruchmühlbach-Miesau", null], + ["073355001011", "Gerhardsbrunn", null], + ["073355001201", "Lambsborn", null], + ["073355001202", "Langwieden", null], + ["073355001203", "Martinshöhe", null], + ["073355002004", "Enkenbach-Alsenborn", null], + ["073355002007", "Fischbach", null], + ["073355002010", "Frankenstein", null], + ["073355002015", "Hochspeyer", null], + ["073355002026", "Mehlingen", null], + ["073355002028", "Neuhemsbach", null], + ["073355002048", "Waldleiningen", null], + ["073355002205", "Sembach", null], + ["073355008016", "Hütschenhausen", null], + ["073355008020", "Kottweiler-Schwanden", null], + ["073355008030", "Niedermohr", null], + ["073355008038", "Ramstein-Miesenbach, Stadt", null], + ["073355008044", "Steinwenden", null], + ["073355009005", "Erzenhausen", null], + ["073355009006", "Eulenbis", null], + ["073355009019", "Kollweiler", null], + ["073355009024", "Mackenbach", null], + ["073355009040", "Rodenbach", null], + ["073355009043", "Schwedelbach", null], + ["073355009049", "Weilerbach", null], + ["073355009501", "Reichenbach-Steegen", null], + ["073355010009", "Frankelbach", null], + ["073355010013", "Heiligenmoschel", null], + ["073355010014", "Hirschhorn/ Pfalz", null], + ["073355010017", "Katzweiler", null], + ["073355010025", "Mehlbach", null], + ["073355010029", "Niederkirchen", null], + ["073355010033", "Olsbrücken", null], + ["073355010034", "Otterbach", null], + ["073355010035", "Otterberg, Stadt", null], + ["073355010041", "Schallodenbach", null], + ["073355010042", "Schneckenhausen", null], + ["073355010046", "Sulzbachtal", null], + ["073355011002", "Bann", null], + ["073355011012", "Hauptstuhl", null], + ["073355011018", "Kindsbach", null], + ["073355011021", "Krickenbach", null], + ["073355011022", "Landstuhl, Sickingenstadt, Stadt", null], + ["073355011023", "Linden", null], + ["073355011027", "Mittelbrunn", null], + ["073355011031", "Oberarnbach", null], + ["073355011037", "Queidersbach", null], + ["073355011045", "Stelzenberg", null], + ["073355011047", "Trippstadt", null], + ["073355011204", "Schopp", null], + ["073365008001", "Adenbach", null], + ["073365008005", "Aschbach", null], + ["073365008012", "Buborn", null], + ["073365008013", "Cronenberg", null], + ["073365008014", "Deimberg", null], + ["073365008019", "Einöllen", null], + ["073365008023", "Eßweiler", null], + ["073365008029", "Ginsweiler", null], + ["073365008030", "Glanbrücken", null], + ["073365008033", "Grumbach", null], + ["073365008035", "Hausweiler", null], + ["073365008036", "Hefersweiler", null], + ["073365008038", "Heinzenhausen", null], + ["073365008040", "Herren-Sulzbach", null], + ["073365008042", "Hinzweiler", null], + ["073365008043", "Hohenöllen", null], + ["073365008044", "Homberg", null], + ["073365008045", "Hoppstädten", null], + ["073365008048", "Jettenbach", null], + ["073365008049", "Kappeln", null], + ["073365008050", "Kirrweiler", null], + ["073365008053", "Kreimbach-Kaulbach", null], + ["073365008057", "Langweiler", null], + ["073365008058", "Lauterecken, Stadt", null], + ["073365008060", "Lohnweiler", null], + ["073365008061", "Medard", null], + ["073365008062", "Merzweiler", null], + ["073365008065", "Nerzweiler", null], + ["073365008069", "Nußbach", null], + ["073365008072", "Oberweiler im Tal", null], + ["073365008073", "Oberweiler-Tiefenbach", null], + ["073365008074", "Odenbach", null], + ["073365008075", "Offenbach-Hundheim", null], + ["073365008085", "Reipoltskirchen", null], + ["073365008086", "Relsberg", null], + ["073365008087", "Rothselberg", null], + ["073365008090", "Rutsweiler an der Lauter", null], + ["073365008095", "Sankt Julian", null], + ["073365008100", "Unterjeckenbach", null], + ["073365008104", "Wiesweiler", null], + ["073365008105", "Wolfstein, Stadt", null], + ["073365009004", "Altenkirchen", null], + ["073365009008", "Börsborn", null], + ["073365009010", "Breitenbach", null], + ["073365009011", "Brücken (Pfalz)", null], + ["073365009016", "Dittweiler", null], + ["073365009017", "Dunzweiler", null], + ["073365009027", "Frohnhofen", null], + ["073365009031", "Glan-Münchweiler", null], + ["073365009032", "Gries", null], + ["073365009037", "Henschtal", null], + ["073365009041", "Herschweiler-Pettersheim", null], + ["073365009047", "Hüffler", null], + ["073365009054", "Krottelbach", null], + ["073365009056", "Langenbach", null], + ["073365009064", "Nanzdietschweiler", null], + ["073365009076", "Ohmbach", null], + ["073365009082", "Rehweiler", null], + ["073365009092", "Schönenberg-Kübelberg", null], + ["073365009096", "Steinbach am Glan", null], + ["073365009101", "Wahnwegen", null], + ["073365009102", "Waldmohr, Stadt", null], + ["073365009107", "Matzenbach", null], + ["073365009501", "Quirnbach/ Pfalz", null], + ["073365010002", "Albessen", null], + ["073365010003", "Altenglan", null], + ["073365010006", "Blaubach", null], + ["073365010009", "Bosenbach", null], + ["073365010015", "Dennweiler-Frohnbach", null], + ["073365010018", "Ehweiler", null], + ["073365010021", "Elzweiler", null], + ["073365010022", "Erdesbach", null], + ["073365010024", "Etschberg", null], + ["073365010025", "Föckelberg", null], + ["073365010034", "Haschbach am Remigiusberg", null], + ["073365010039", "Herchweiler", null], + ["073365010046", "Horschbach", null], + ["073365010051", "Körborn", null], + ["073365010052", "Konken", null], + ["073365010055", "Kusel, Stadt", null], + ["073365010066", "Neunkirchen am Potzberg", null], + ["073365010067", "Niederalben", null], + ["073365010068", "Niederstaufenbach", null], + ["073365010070", "Oberalben", null], + ["073365010071", "Oberstaufenbach", null], + ["073365010077", "Pfeffelbach", null], + ["073365010079", "Rammelsbach", null], + ["073365010081", "Rathsweiler", null], + ["073365010084", "Reichweiler", null], + ["073365010088", "Ruthweiler", null], + ["073365010089", "Rutsweiler am Glan", null], + ["073365010091", "Schellweiler", null], + ["073365010094", "Selchenbach", null], + ["073365010097", "Thallichtenberg", null], + ["073365010098", "Theisbergstegen", null], + ["073365010099", "Ulmet", null], + ["073365010103", "Welchweiler", null], + ["073365010106", "Bedesbach", null], + ["073375001001", "Albersweiler", null], + ["073375001017", "Dernbach", null], + ["073375001024", "Eußerthal", null], + ["073375001033", "Gossersweiler-Stein", null], + ["073375001054", "Münchweiler am Klingbach", null], + ["073375001064", "Ramberg", null], + ["073375001067", "Rinnthal", null], + ["073375001074", "Silz", null], + ["073375001078", "Völkersweiler", null], + ["073375001080", "Waldhambach", null], + ["073375001081", "Waldrohrbach", null], + ["073375001083", "Wernersberg", null], + ["073375001501", "Annweiler am Trifels, Stadt", null], + ["073375002005", "Bad Bergzabern, Stadt", null], + ["073375002006", "Barbelroth", null], + ["073375002008", "Birkenhördt", null], + ["073375002013", "Böllenborn", null], + ["073375002018", "Dierbach", null], + ["073375002019", "Dörrenbach", null], + ["073375002029", "Gleiszellen-Gleishorbach", null], + ["073375002037", "Hergersweiler", null], + ["073375002045", "Kapellen-Drusweiler", null], + ["073375002046", "Kapsweyer", null], + ["073375002049", "Klingenmünster", null], + ["073375002055", "Niederhorbach", null], + ["073375002056", "Niederotterbach", null], + ["073375002058", "Oberhausen", null], + ["073375002059", "Oberotterbach", null], + ["073375002060", "Oberschlettenbach", null], + ["073375002062", "Pleisweiler-Oberhofen", null], + ["073375002071", "Schweigen-Rechtenbach", null], + ["073375002072", "Schweighofen", null], + ["073375002076", "Steinfeld", null], + ["073375002079", "Vorderweidenthal", null], + ["073375003002", "Altdorf", null], + ["073375003011", "Böbingen", null], + ["073375003015", "Burrweiler", null], + ["073375003020", "Edenkoben, Stadt", null], + ["073375003021", "Edesheim", null], + ["073375003025", "Flemlingen", null], + ["073375003027", "Freimersheim (Pfalz)", null], + ["073375003028", "Gleisweiler", null], + ["073375003032", "Gommersheim", null], + ["073375003035", "Großfischlingen", null], + ["073375003036", "Hainfeld", null], + ["073375003048", "Kleinfischlingen", null], + ["073375003066", "Rhodt unter Rietburg", null], + ["073375003069", "Roschbach", null], + ["073375003077", "Venningen", null], + ["073375003084", "Weyher in der Pfalz", null], + ["073375004038", "Herxheim bei Landau/ Pfalz", null], + ["073375004039", "Herxheimweyher", null], + ["073375004044", "Insheim", null], + ["073375004068", "Rohrbach", null], + ["073375005007", "Billigheim-Ingenheim", null], + ["073375005009", "Birkweiler", null], + ["073375005012", "Böchingen", null], + ["073375005022", "Eschbach", null], + ["073375005026", "Frankweiler", null], + ["073375005031", "Göcklingen", null], + ["073375005040", "Heuchelheim-Klingen", null], + ["073375005042", "Ilbesheim bei Landau in der Pfalz", null], + ["073375005043", "Impflingen", null], + ["073375005050", "Knöringen", null], + ["073375005051", "Leinsweiler", null], + ["073375005065", "Ranschbach", null], + ["073375005073", "Siebeldingen", null], + ["073375005082", "Walsheim", null], + ["073375006047", "Kirrweiler (Pfalz)", null], + ["073375006052", "Maikammer", null], + ["073375006070", "Sankt Martin", null], + ["073375007014", "Bornheim", null], + ["073375007023", "Essingen", null], + ["073375007041", "Hochstadt (Pfalz)", null], + ["073375007061", "Offenbach an der Queich", null], + ["073380004004", "Bobenheim-Roxheim", null], + ["073380005005", "Böhl-Iggelheim", null], + ["073380017017", "Limburgerhof", null], + ["073380019019", "Mutterstadt", null], + ["073380025025", "Schifferstadt, Stadt", null], + ["073385001006", "Dannstadt-Schauernheim", null], + ["073385001014", "Hochdorf-Assenheim", null], + ["073385001022", "Rödersheim-Gronau", null], + ["073385004003", "Birkenheide", null], + ["073385004008", "Fußgönheim", null], + ["073385004018", "Maxdorf", null], + ["073385006002", "Beindersheim", null], + ["073385006009", "Großniedesheim", null], + ["073385006012", "Heßheim", null], + ["073385006013", "Heuchelheim bei Frankenthal", null], + ["073385006015", "Kleinniedesheim", null], + ["073385006016", "Lambsheim", null], + ["073385007007", "Dudenhofen", null], + ["073385007010", "Hanhofen", null], + ["073385007011", "Harthausen", null], + ["073385007023", "Römerberg", null], + ["073385008001", "Altrip", null], + ["073385008020", "Neuhofen", null], + ["073385008021", "Otterstadt", null], + ["073385008026", "Waldsee", null], + ["073390005005", "Bingen am Rhein, Stadt", null], + ["073390009009", "Budenheim", null], + ["073390030030", "Ingelheim am Rhein, Stadt", null], + ["073395001003", "Bacharach, Stadt", null], + ["073395001007", "Breitscheid", null], + ["073395001036", "Manubach", null], + ["073395001038", "Münster-Sarmsheim", null], + ["073395001040", "Niederheimbach", null], + ["073395001044", "Oberdiebach", null], + ["073395001045", "Oberheimbach", null], + ["073395001058", "Trechtingshausen", null], + ["073395001062", "Waldalgesheim", null], + ["073395001063", "Weiler bei Bingen", null], + ["073395002006", "Bodenheim", null], + ["073395002020", "Gau-Bischofsheim", null], + ["073395002026", "Harxheim", null], + ["073395002034", "Lörzweiler", null], + ["073395002039", "Nackenheim", null], + ["073395003001", "Appenheim", null], + ["073395003008", "Bubenheim", null], + ["073395003016", "Engelstadt", null], + ["073395003019", "Gau-Algesheim, Stadt", null], + ["073395003041", "Nieder-Hilbersheim", null], + ["073395003046", "Ober-Hilbersheim", null], + ["073395003048", "Ockenheim", null], + ["073395003051", "Schwabenheim an der Selz", null], + ["073395006017", "Essenheim", null], + ["073395006031", "Jugenheim in Rheinhessen", null], + ["073395006032", "Klein-Winternheim", null], + ["073395006042", "Nieder-Olm, Stadt", null], + ["073395006047", "Ober-Olm", null], + ["073395006054", "Sörgenloch", null], + ["073395006057", "Stadecken-Elsheim", null], + ["073395006067", "Zornheim", null], + ["073395007010", "Dalheim", null], + ["073395007011", "Dexheim", null], + ["073395007012", "Dienheim", null], + ["073395007013", "Dolgesheim", null], + ["073395007015", "Eimsheim", null], + ["073395007018", "Friesenheim", null], + ["073395007024", "Guntersblum", null], + ["073395007025", "Hahnheim", null], + ["073395007028", "Hillesheim", null], + ["073395007033", "Köngernheim", null], + ["073395007035", "Ludwigshöhe", null], + ["073395007037", "Mommenheim", null], + ["073395007043", "Nierstein, Stadt", null], + ["073395007049", "Oppenheim, Stadt", null], + ["073395007053", "Selzen", null], + ["073395007059", "Uelversheim", null], + ["073395007060", "Undenheim", null], + ["073395007064", "Weinolsheim", null], + ["073395007066", "Wintersheim", null], + ["073395007201", "Dorn-Dürkheim", null], + ["073395008002", "Aspisheim", null], + ["073395008004", "Badenheim", null], + ["073395008021", "Gensingen", null], + ["073395008022", "Grolsheim", null], + ["073395008029", "Horrweiler", null], + ["073395008050", "Sankt Johann", null], + ["073395008056", "Sprendlingen", null], + ["073395008065", "Welgesheim", null], + ["073395008068", "Zotzenheim", null], + ["073395008202", "Wolfsheim", null], + ["073405001001", "Bobenthal", null], + ["073405001002", "Busenberg", null], + ["073405001004", "Dahn, Stadt", null], + ["073405001009", "Erfweiler", null], + ["073405001010", "Erlenbach bei Dahn", null], + ["073405001011", "Fischbach bei Dahn", null], + ["073405001021", "Hirschthal", null], + ["073405001029", "Ludwigswinkel", null], + ["073405001033", "Niederschlettenbach", null], + ["073405001034", "Nothweiler", null], + ["073405001039", "Rumbach", null], + ["073405001043", "Schindhard", null], + ["073405001045", "Schönau (Pfalz)", null], + ["073405001501", "Bruchweiler-Bärenbach", null], + ["073405001502", "Bundenthal", null], + ["073405002005", "Darstein", null], + ["073405002006", "Dimbach", null], + ["073405002014", "Hauenstein", null], + ["073405002020", "Hinterweidenthal", null], + ["073405002030", "Lug", null], + ["073405002047", "Schwanheim", null], + ["073405002049", "Spirkelbach", null], + ["073405002057", "Wilgartswiesen", null], + ["073405003008", "Eppenbrunn", null], + ["073405003019", "Hilst", null], + ["073405003026", "Kröppen", null], + ["073405003028", "Lemberg", null], + ["073405003036", "Obersimten", null], + ["073405003040", "Ruppertsweiler", null], + ["073405003048", "Schweix", null], + ["073405003052", "Trulben", null], + ["073405003053", "Vinningen", null], + ["073405003205", "Bottenbach", null], + ["073405004003", "Clausen", null], + ["073405004007", "Donsieders", null], + ["073405004027", "Leimen", null], + ["073405004031", "Merzalben", null], + ["073405004032", "Münchweiler an der Rodalb", null], + ["073405004038", "Rodalben, Stadt", null], + ["073405006012", "Geiselberg", null], + ["073405006015", "Heltersberg", null], + ["073405006016", "Hermersberg", null], + ["073405006022", "Höheinöd", null], + ["073405006025", "Horbach", null], + ["073405006044", "Schmalenberg", null], + ["073405006050", "Steinalben", null], + ["073405006054", "Waldfischbach-Burgalben", null], + ["073405008201", "Althornbach", null], + ["073405008202", "Battweiler", null], + ["073405008203", "Bechhofen", null], + ["073405008206", "Contwig", null], + ["073405008207", "Dellfeld", null], + ["073405008208", "Dietrichingen", null], + ["073405008209", "Großbundenbach", null], + ["073405008210", "Großsteinhausen", null], + ["073405008211", "Hornbach, Stadt", null], + ["073405008212", "Käshofen", null], + ["073405008213", "Kleinbundenbach", null], + ["073405008214", "Kleinsteinhausen", null], + ["073405008218", "Mauschbach", null], + ["073405008221", "Riedelberg", null], + ["073405008223", "Rosenkopf", null], + ["073405008226", "Walshausen", null], + ["073405008227", "Wiesbach", null], + ["073405009017", "Herschberg", null], + ["073405009018", "Hettenhausen", null], + ["073405009023", "Höheischweiler", null], + ["073405009024", "Höhfröschen", null], + ["073405009035", "Nünschweiler", null], + ["073405009037", "Petersberg", null], + ["073405009041", "Saalstadt", null], + ["073405009042", "Schauerberg", null], + ["073405009051", "Thaleischweiler-Fröschen", null], + ["073405009055", "Weselberg", null], + ["073405009204", "Biedershausen", null], + ["073405009215", "Knopp-Labach", null], + ["073405009216", "Krähenberg", null], + ["073405009217", "Maßweiler", null], + ["073405009219", "Obernheim-Kirchenarnbach", null], + ["073405009220", "Reifenberg", null], + ["073405009222", "Rieschweiler-Mühlbach", null], + ["073405009224", "Schmitshausen", null], + ["073405009225", "Wallhalben", null], + ["073405009228", "Winterbach (Pfalz)", null], + ["081110000000", "Stuttgart, Landeshauptstadt", null], + ["081150003003", "Böblingen, Stadt", null], + ["081150028028", "Leonberg, Stadt", null], + ["081150029029", "Magstadt", null], + ["081150041041", "Renningen, Stadt", null], + ["081150042042", "Rutesheim, Stadt", null], + ["081150044044", "Schönaich", null], + ["081150045045", "Sindelfingen, Stadt", null], + ["081150050050", "Weil der Stadt, Stadt", null], + ["081150051051", "Weil im Schönbuch", null], + ["081150052052", "Weissach", null], + ["081155001001", "Aidlingen", null], + ["081155001054", "Grafenau", null], + ["081155002013", "Ehningen", null], + ["081155002015", "Gärtringen", null], + ["081155003010", "Deckenpfronn", null], + ["081155003021", "Herrenberg, Stadt", null], + ["081155003037", "Nufringen", null], + ["081155004002", "Altdorf", null], + ["081155004022", "Hildrizhausen", null], + ["081155004024", "Holzgerlingen, Stadt", null], + ["081155005004", "Bondorf", null], + ["081155005016", "Gäufelden", null], + ["081155005034", "Mötzingen", null], + ["081155005053", "Jettingen", null], + ["081155006046", "Steinenbronn", null], + ["081155006048", "Waldenbuch, Stadt", null], + ["081160015015", "Denkendorf", null], + ["081160019019", "Esslingen am Neckar, Stadt", null], + ["081160047047", "Neuhausen auf den Fildern", null], + ["081160072072", "Wernau (Neckar), Stadt", null], + ["081160076076", "Aichwald", null], + ["081160077077", "Filderstadt, Stadt", null], + ["081160078078", "Leinfelden-Echterdingen, Stadt", null], + ["081160080080", "Ostfildern, Stadt", null], + ["081160081081", "Aichtal, Stadt", null], + ["081165001016", "Dettingen unter Teck", null], + ["081165001033", "Kirchheim unter Teck, Stadt", null], + ["081165001048", "Notzingen", null], + ["081165002018", "Erkenbrechtsweiler", null], + ["081165002054", "Owen, Stadt", null], + ["081165002079", "Lenningen", null], + ["081165003005", "Altdorf", null], + ["081165003006", "Altenriet", null], + ["081165003008", "Bempflingen", null], + ["081165003041", "Neckartailfingen", null], + ["081165003042", "Neckartenzlingen", null], + ["081165003063", "Schlaitdorf", null], + ["081165004011", "Beuren", null], + ["081165004036", "Kohlberg", null], + ["081165004046", "Neuffen, Stadt", null], + ["081165005020", "Frickenhausen", null], + ["081165005022", "Großbettlingen", null], + ["081165005049", "Nürtingen, Stadt", null], + ["081165005050", "Oberboihingen", null], + ["081165005068", "Unterensingen", null], + ["081165005073", "Wolfschlugen", null], + ["081165006004", "Altbach", null], + ["081165006014", "Deizisau", null], + ["081165006056", "Plochingen, Stadt", null], + ["081165007007", "Baltmannsweiler", null], + ["081165007027", "Hochdorf", null], + ["081165007037", "Lichtenwald", null], + ["081165007058", "Reichenbach an der Fils", null], + ["081165008012", "Bissingen an der Teck", null], + ["081165008029", "Holzmaden", null], + ["081165008043", "Neidlingen", null], + ["081165008053", "Ohmden", null], + ["081165008070", "Weilheim an der Teck, Stadt", null], + ["081165009035", "Köngen", null], + ["081165009071", "Wendlingen am Neckar, Stadt", null], + ["081170010010", "Böhmenkirch", null], + ["081175001006", "Bad Ditzenbach", null], + ["081175001014", "Deggingen", null], + ["081175002018", "Ebersbach an der Fils, Stadt", null], + ["081175002044", "Schlierbach", null], + ["081175003019", "Eislingen/Fils, Stadt", null], + ["081175003037", "Ottenbach", null], + ["081175003042", "Salach", null], + ["081175004007", "Bad Überkingen", null], + ["081175004024", "Geislingen an der Steige, Stadt", null], + ["081175004033", "Kuchen", null], + ["081175005026", "Göppingen, Stadt", null], + ["081175005043", "Schlat", null], + ["081175005053", "Wäschenbeuren", null], + ["081175005055", "Wangen", null], + ["081175006015", "Donzdorf, Stadt", null], + ["081175006025", "Gingen an der Fils", null], + ["081175006049", "Süßen, Stadt", null], + ["081175006061", "Lauterstein, Stadt", null], + ["081175007016", "Drackenstein", null], + ["081175007028", "Gruibingen", null], + ["081175007031", "Hohenstadt", null], + ["081175007035", "Mühlhausen im Täle", null], + ["081175007058", "Wiesensteig, Stadt", null], + ["081175008001", "Adelberg", null], + ["081175008009", "Birenbach", null], + ["081175008011", "Börtlingen", null], + ["081175008038", "Rechberghausen", null], + ["081175009002", "Aichelberg", null], + ["081175009012", "Bad Boll", null], + ["081175009017", "Dürnau", null], + ["081175009023", "Gammelshausen", null], + ["081175009029", "Hattenhofen", null], + ["081175009060", "Zell unter Aichelberg", null], + ["081175010003", "Albershausen", null], + ["081175010051", "Uhingen, Stadt", null], + ["081175011020", "Eschenbach", null], + ["081175011030", "Heiningen", null], + ["081180003003", "Asperg, Stadt", null], + ["081180011011", "Ditzingen, Stadt", null], + ["081180019019", "Gerlingen, Stadt", null], + ["081180021021", "Großbottwar, Stadt", null], + ["081180046046", "Kornwestheim, Stadt", null], + ["081180048048", "Ludwigsburg, Stadt", null], + ["081180050050", "Markgröningen, Stadt", null], + ["081180051051", "Möglingen", null], + ["081180060060", "Oberstenfeld", null], + ["081180076076", "Sachsenheim, Stadt", null], + ["081180080080", "Korntal-Münchingen, Stadt", null], + ["081180081081", "Remseck am Neckar, Stadt", null], + ["081185001007", "Besigheim, Stadt", null], + ["081185001016", "Freudental", null], + ["081185001018", "Gemmrigheim", null], + ["081185001028", "Hessigheim", null], + ["081185001047", "Löchgau", null], + ["081185001053", "Mundelsheim", null], + ["081185001074", "Walheim", null], + ["081185002071", "Tamm", null], + ["081185002077", "Ingersheim", null], + ["081185002079", "Bietigheim-Bissingen, Stadt", null], + ["081185003010", "Bönnigheim, Stadt", null], + ["081185003015", "Erligheim", null], + ["081185003040", "Kirchheim am Neckar", null], + ["081185004063", "Pleidelsheim", null], + ["081185004078", "Freiberg am Neckar, Stadt", null], + ["081185005001", "Affalterbach", null], + ["081185005006", "Benningen am Neckar", null], + ["081185005014", "Erdmannhausen", null], + ["081185005049", "Marbach am Neckar, Stadt", null], + ["081185006027", "Hemmingen", null], + ["081185006067", "Schwieberdingen", null], + ["081185007054", "Murr", null], + ["081185007070", "Steinheim an der Murr, Stadt", null], + ["081185008012", "Eberdingen", null], + ["081185008059", "Oberriexingen, Stadt", null], + ["081185008068", "Sersheim", null], + ["081185008073", "Vaihingen an der Enz, Stadt", null], + ["081190001001", "Alfdorf", null], + ["081190020020", "Fellbach, Stadt", null], + ["081190041041", "Korb", null], + ["081190044044", "Murrhardt, Stadt", null], + ["081190061061", "Rudersberg", null], + ["081190079079", "Waiblingen, Stadt", null], + ["081190089089", "Berglen", null], + ["081190090090", "Remshalden", null], + ["081190091091", "Weinstadt, Stadt", null], + ["081190093093", "Kernen im Remstal", null], + ["081195001003", "Allmersbach im Tal", null], + ["081195001004", "Althütte", null], + ["081195001006", "Auenwald", null], + ["081195001008", "Backnang, Stadt", null], + ["081195001018", "Burgstetten", null], + ["081195001038", "Kirchberg an der Murr", null], + ["081195001053", "Oppenweiler", null], + ["081195001083", "Weissach im Tal", null], + ["081195001087", "Aspach", null], + ["081195002055", "Plüderhausen", null], + ["081195002076", "Urbach", null], + ["081195003067", "Schorndorf, Stadt", null], + ["081195003086", "Winterbach", null], + ["081195004024", "Großerlach", null], + ["081195004069", "Spiegelberg", null], + ["081195004075", "Sulzbach an der Murr", null], + ["081195005037", "Kaisersbach", null], + ["081195005084", "Welzheim, Stadt", null], + ["081195006042", "Leutenbach", null], + ["081195006068", "Schwaikheim", null], + ["081195006085", "Winnenden, Stadt", null], + ["081210000000", "Heilbronn, Universitätsstadt", null], + ["081250007007", "Bad Wimpfen, Stadt", null], + ["081250039039", "Gundelsheim, Stadt", null], + ["081250058058", "Leingarten, Stadt", null], + ["081250068068", "Neudenau, Stadt", null], + ["081250107107", "Wüstenrot", null], + ["081255001005", "Bad Friedrichshall, Stadt", null], + ["081255001078", "Oedheim", null], + ["081255001079", "Offenau", null], + ["081255002006", "Bad Rappenau, Stadt", null], + ["081255002049", "Kirchardt", null], + ["081255002087", "Siegelsbach", null], + ["081255003013", "Brackenheim, Stadt", null], + ["081255003017", "Cleebronn", null], + ["081255004026", "Eppingen, Stadt", null], + ["081255004034", "Gemmingen", null], + ["081255004047", "Ittlingen", null], + ["081255005030", "Flein", null], + ["081255005094", "Talheim", null], + ["081255006056", "Lauffen am Neckar, Stadt", null], + ["081255006066", "Neckarwestheim", null], + ["081255006074", "Nordheim", null], + ["081255007048", "Jagsthausen", null], + ["081255007063", "Möckmühl, Stadt", null], + ["081255007084", "Roigheim", null], + ["081255007103", "Widdern, Stadt", null], + ["081255008027", "Erlenbach", null], + ["081255008065", "Neckarsulm, Stadt", null], + ["081255008096", "Untereisesheim", null], + ["081255009069", "Neuenstadt am Kocher, Stadt", null], + ["081255009111", "Hardthausen am Kocher", null], + ["081255009113", "Langenbrettach", null], + ["081255010038", "Güglingen, Stadt", null], + ["081255010081", "Pfaffenhofen", null], + ["081255010108", "Zaberfeld", null], + ["081255011059", "Löwenstein, Stadt", null], + ["081255011110", "Obersulm", null], + ["081255012001", "Abstatt", null], + ["081255012008", "Beilstein, Stadt", null], + ["081255012046", "Ilsfeld", null], + ["081255012098", "Untergruppenbach", null], + ["081255013061", "Massenbachhausen", null], + ["081255013086", "Schwaigern, Stadt", null], + ["081255014021", "Eberstadt", null], + ["081255014024", "Ellhofen", null], + ["081255014057", "Lehrensteinsfeld", null], + ["081255014102", "Weinsberg, Stadt", null], + ["081260011011", "Bretzfeld", null], + ["081260072072", "Schöntal", null], + ["081265001047", "Kupferzell", null], + ["081265001058", "Neuenstein, Stadt", null], + ["081265001085", "Waldenburg, Stadt", null], + ["081265002020", "Dörzbach", null], + ["081265002045", "Krautheim, Stadt", null], + ["081265002056", "Mulfingen", null], + ["081265003039", "Ingelfingen, Stadt", null], + ["081265003046", "Künzelsau, Stadt", null], + ["081265004028", "Forchtenberg, Stadt", null], + ["081265004060", "Niedernhall, Stadt", null], + ["081265004086", "Weißbach", null], + ["081265005066", "Öhringen, Stadt", null], + ["081265005069", "Pfedelbach", null], + ["081265005094", "Zweiflingen", null], + ["081270008008", "Blaufelden", null], + ["081270052052", "Mainhardt", null], + ["081270075075", "Schrozberg, Stadt", null], + ["081275001009", "Braunsbach", null], + ["081275001086", "Untermünkheim", null], + ["081275002014", "Crailsheim, Stadt", null], + ["081275002073", "Satteldorf", null], + ["081275002103", "Frankenhardt", null], + ["081275002104", "Stimpfach", null], + ["081275003101", "Kreßberg", null], + ["081275003102", "Fichtenau", null], + ["081275004032", "Gerabronn, Stadt", null], + ["081275004047", "Langenburg, Stadt", null], + ["081275005043", "Ilshofen, Stadt", null], + ["081275005089", "Vellberg, Stadt", null], + ["081275005099", "Wolpertshausen", null], + ["081275006023", "Fichtenberg", null], + ["081275006025", "Gaildorf, Stadt", null], + ["081275006062", "Oberrot", null], + ["081275006079", "Sulzbach-Laufen", null], + ["081275007012", "Bühlertann", null], + ["081275007013", "Bühlerzell", null], + ["081275007063", "Obersontheim", null], + ["081275008046", "Kirchberg an der Jagst, Stadt", null], + ["081275008071", "Rot am See", null], + ["081275008091", "Wallhausen", null], + ["081275009056", "Michelbach an der Bilz", null], + ["081275009059", "Michelfeld", null], + ["081275009076", "Schwäbisch Hall, Stadt", null], + ["081275009100", "Rosengarten", null], + ["081280020020", "Creglingen, Stadt", null], + ["081280039039", "Freudenberg, Stadt", null], + ["081280064064", "Külsheim, Stadt", null], + ["081280082082", "Niederstetten, Stadt", null], + ["081280126126", "Weikersheim, Stadt", null], + ["081280131131", "Wertheim, Stadt", null], + ["081280139139", "Lauda-Königshofen, Stadt", null], + ["081285001006", "Assamstadt", null], + ["081285001007", "Bad Mergentheim, Stadt", null], + ["081285001058", "Igersheim", null], + ["081285002014", "Boxberg, Stadt", null], + ["081285002138", "Ahorn", null], + ["081285003047", "Grünsfeld, Stadt", null], + ["081285003137", "Wittighausen", null], + ["081285004045", "Großrinderfeld", null], + ["081285004061", "Königheim", null], + ["081285004115", "Tauberbischofsheim, Stadt", null], + ["081285004128", "Werbach", null], + ["081350010010", "Dischingen", null], + ["081350015015", "Gerstetten", null], + ["081350020020", "Herbrechtingen, Stadt", null], + ["081350025025", "Königsbronn", null], + ["081350032032", "Steinheim am Albuch", null], + ["081355001016", "Giengen an der Brenz, Stadt", null], + ["081355001021", "Hermaringen", null], + ["081355002019", "Heidenheim an der Brenz, Stadt", null], + ["081355002026", "Nattheim", null], + ["081355003027", "Niederstotzingen, Stadt", null], + ["081355003031", "Sontheim an der Brenz", null], + ["081360002002", "Abtsgmünd", null], + ["081360027027", "Gschwend", null], + ["081360042042", "Lorch, Stadt", null], + ["081360045045", "Neresheim, Stadt", null], + ["081360050050", "Oberkochen, Stadt", null], + ["081365001021", "Essingen", null], + ["081365001033", "Hüttlingen", null], + ["081365001088", "Aalen, Stadt", null], + ["081365002010", "Bopfingen, Stadt", null], + ["081365002037", "Kirchheim am Ries", null], + ["081365002087", "Riesbürg", null], + ["081365003003", "Adelmannsfelden", null], + ["081365003018", "Ellenberg", null], + ["081365003019", "Ellwangen (Jagst), Stadt", null], + ["081365003035", "Jagstzell", null], + ["081365003046", "Neuler", null], + ["081365003060", "Rosenberg", null], + ["081365003084", "Wört", null], + ["081365003089", "Rainau", null], + ["081365004038", "Lauchheim, Stadt", null], + ["081365004082", "Westhausen", null], + ["081365005020", "Eschach", null], + ["081365005024", "Göggingen", null], + ["081365005034", "Iggingen", null], + ["081365005040", "Leinzell", null], + ["081365005049", "Obergröningen", null], + ["081365005062", "Schechingen", null], + ["081365006007", "Bartholomä", null], + ["081365006009", "Böbingen an der Rems", null], + ["081365006028", "Heubach, Stadt", null], + ["081365006029", "Heuchlingen", null], + ["081365006043", "Mögglingen", null], + ["081365007065", "Schwäbisch Gmünd, Stadt", null], + ["081365007079", "Waldstetten", null], + ["081365008015", "Durlangen", null], + ["081365008044", "Mutlangen", null], + ["081365008061", "Ruppertshofen", null], + ["081365008066", "Spraitbach", null], + ["081365008070", "Täferrot", null], + ["081365009068", "Stödtlen", null], + ["081365009071", "Tannhausen", null], + ["081365009075", "Unterschneidheim", null], + ["082110000000", "Baden-Baden, Stadt", null], + ["082120000000", "Karlsruhe, Stadt", null], + ["082150017017", "Ettlingen, Stadt", null], + ["082150046046", "Malsch", null], + ["082150047047", "Marxzell", null], + ["082150064064", "Östringen, Stadt", null], + ["082150084084", "Ubstadt-Weiher", null], + ["082150089089", "Walzbachtal", null], + ["082150090090", "Weingarten (Baden)", null], + ["082150096096", "Karlsbad", null], + ["082150097097", "Kraichtal, Stadt", null], + ["082150101101", "Pfinztal", null], + ["082150102102", "Eggenstein-Leopoldshafen", null], + ["082150105105", "Linkenheim-Hochstetten", null], + ["082150106106", "Waghäusel, Stadt", null], + ["082150108108", "Rheinstetten, Stadt", null], + ["082150109109", "Stutensee, Stadt", null], + ["082150110110", "Waldbronn", null], + ["082155001039", "Kronau", null], + ["082155001100", "Bad Schönborn", null], + ["082155002007", "Bretten, Stadt", null], + ["082155002025", "Gondelsheim", null], + ["082155003009", "Bruchsal, Stadt", null], + ["082155003021", "Forst", null], + ["082155003029", "Hambrücken", null], + ["082155003103", "Karlsdorf-Neuthard", null], + ["082155004099", "Graben-Neudorf", null], + ["082155004111", "Dettenheim", null], + ["082155005040", "Kürnbach", null], + ["082155005059", "Oberderdingen", null], + ["082155006066", "Philippsburg, Stadt", null], + ["082155006107", "Oberhausen-Rheinhausen", null], + ["082155007082", "Sulzfeld", null], + ["082155007094", "Zaisenhausen", null], + ["082160008008", "Bühlertal", null], + ["082160013013", "Forbach", null], + ["082160015015", "Gaggenau, Stadt", null], + ["082165001006", "Bischweier", null], + ["082165001024", "Kuppenheim, Stadt", null], + ["082165002007", "Bühl, Stadt", null], + ["082165002041", "Ottersweier", null], + ["082165003002", "Au am Rhein", null], + ["082165003005", "Bietigheim", null], + ["082165003009", "Durmersheim", null], + ["082165003012", "Elchesheim-Illingen", null], + ["082165004017", "Gernsbach, Stadt", null], + ["082165004029", "Loffenau", null], + ["082165004059", "Weisenbach", null], + ["082165005023", "Iffezheim", null], + ["082165005033", "Muggensturm", null], + ["082165005039", "Ötigheim", null], + ["082165005043", "Rastatt, Stadt", null], + ["082165005052", "Steinmauern", null], + ["082165006028", "Lichtenau, Stadt", null], + ["082165006063", "Rheinmünster", null], + ["082165007022", "Hügelsheim", null], + ["082165007049", "Sinzheim", null], + ["082210000000", "Heidelberg, Stadt", null], + ["082220000000", "Mannheim, Universitätsstadt", null], + ["082250014014", "Buchen (Odenwald), Stadt", null], + ["082250060060", "Mudau", null], + ["082255001032", "Hardheim", null], + ["082255001039", "Höpfingen", null], + ["082255001109", "Walldürn, Stadt", null], + ["082255002033", "Haßmersheim", null], + ["082255002042", "Hüffenhardt", null], + ["082255003002", "Aglasterhausen", null], + ["082255003068", "Neunkirchen", null], + ["082255003116", "Schwarzach", null], + ["082255004024", "Fahrenbach", null], + ["082255004052", "Limbach", null], + ["082255005058", "Mosbach, Stadt", null], + ["082255005067", "Neckarzimmern", null], + ["082255005074", "Obrigheim", null], + ["082255005117", "Elztal", null], + ["082255006010", "Binau", null], + ["082255006064", "Neckargerach", null], + ["082255006113", "Zwingenberg", null], + ["082255006118", "Waldbrunn", null], + ["082255007075", "Osterburken, Stadt", null], + ["082255007082", "Rosenberg", null], + ["082255007114", "Ravenstein, Stadt", null], + ["082255008009", "Billigheim", null], + ["082255008115", "Schefflenz", null], + ["082255009001", "Adelsheim, Stadt", null], + ["082255009091", "Seckach", null], + ["082260009009", "Brühl", null], + ["082260012012", "Dossenheim", null], + ["082260018018", "Eppelheim, Stadt", null], + ["082260028028", "Heddesheim", null], + ["082260036036", "Ilvesheim", null], + ["082260037037", "Ketsch", null], + ["082260038038", "Ladenburg, Stadt", null], + ["082260041041", "Leimen, Stadt", null], + ["082260060060", "Nußloch", null], + ["082260062062", "Oftersheim", null], + ["082260063063", "Plankstadt", null], + ["082260076076", "Sandhausen", null], + ["082260082082", "Schriesheim, Stadt", null], + ["082260084084", "Schwetzingen, Stadt", null], + ["082260095095", "Walldorf, Stadt", null], + ["082260096096", "Weinheim, Stadt", null], + ["082260103103", "St. Leon-Rot", null], + ["082260105105", "Edingen-Neckarhausen", null], + ["082260107107", "Hirschberg an der Bergstraße", null], + ["082265001013", "Eberbach, Stadt", null], + ["082265001081", "Schönbrunn", null], + ["082265002020", "Eschelbronn", null], + ["082265002048", "Mauer", null], + ["082265002049", "Meckesheim", null], + ["082265002086", "Spechbach", null], + ["082265002104", "Lobbach", null], + ["082265003031", "Hemsbach, Stadt", null], + ["082265003040", "Laudenbach", null], + ["082265004003", "Altlußheim", null], + ["082265004032", "Hockenheim, Stadt", null], + ["082265004059", "Neulußheim", null], + ["082265004068", "Reilingen", null], + ["082265005006", "Bammental", null], + ["082265005022", "Gaiberg", null], + ["082265005056", "Neckargemünd, Stadt", null], + ["082265005097", "Wiesenbach", null], + ["082265006046", "Malsch", null], + ["082265006054", "Mühlhausen", null], + ["082265006065", "Rauenberg, Stadt", null], + ["082265007027", "Heddesbach", null], + ["082265007029", "Heiligkreuzsteinach", null], + ["082265007080", "Schönau, Stadt", null], + ["082265007099", "Wilhelmsfeld", null], + ["082265008085", "Sinsheim, Stadt", null], + ["082265008101", "Zuzenhausen", null], + ["082265008102", "Angelbachtal", null], + ["082265009017", "Epfenbach", null], + ["082265009055", "Neckarbischofsheim, Stadt", null], + ["082265009058", "Neidenstein", null], + ["082265009066", "Reichartshausen", null], + ["082265009091", "Waibstadt, Stadt", null], + ["082265009106", "Helmstadt-Bargen", null], + ["082265010010", "Dielheim", null], + ["082265010098", "Wiesloch, Stadt", null], + ["082310000000", "Pforzheim, Stadt", null], + ["082350065065", "Schömberg", null], + ["082350080080", "Wildberg, Stadt", null], + ["082355001006", "Altensteig, Stadt", null], + ["082355001022", "Egenhausen", null], + ["082355001066", "Simmersfeld", null], + ["082355002007", "Althengstett", null], + ["082355002029", "Gechingen", null], + ["082355002057", "Ostelsheim", null], + ["082355002067", "Simmozheim", null], + ["082355003018", "Dobel", null], + ["082355003033", "Bad Herrenalb, Stadt", null], + ["082355004008", "Bad Liebenzell, Stadt", null], + ["082355004073", "Unterreichenbach", null], + ["082355005047", "Neubulach, Stadt", null], + ["082355005050", "Neuweiler", null], + ["082355005084", "Bad Teinach-Zavelstein, Stadt", null], + ["082355006055", "Oberreichenbach", null], + ["082355006085", "Calw, Stadt", null], + ["082355007020", "Ebhausen", null], + ["082355007032", "Haiterbach, Stadt", null], + ["082355007046", "Nagold, Stadt", null], + ["082355007060", "Rohrdorf", null], + ["082355008025", "Enzklösterle", null], + ["082355008035", "Höfen an der Enz", null], + ["082355008079", "Bad Wildbad, Stadt", null], + ["082360004004", "Birkenfeld", null], + ["082360028028", "Illingen", null], + ["082360030030", "Ispringen", null], + ["082360033033", "Knittlingen, Stadt", null], + ["082360046046", "Niefern-Öschelbronn", null], + ["082360070070", "Keltern", null], + ["082360071071", "Remchingen", null], + ["082360072072", "Straubenhardt", null], + ["082365001019", "Friolzheim", null], + ["082365001025", "Heimsheim, Stadt", null], + ["082365001039", "Mönsheim", null], + ["082365001065", "Wiernsheim", null], + ["082365001067", "Wimsheim", null], + ["082365001068", "Wurmberg", null], + ["082365002011", "Eisingen", null], + ["082365002074", "Kämpfelbach", null], + ["082365002076", "Königsbach-Stein", null], + ["082365003038", "Maulbronn, Stadt", null], + ["082365003061", "Sternenfels", null], + ["082365004040", "Mühlacker, Stadt", null], + ["082365004050", "Ötisheim", null], + ["082365005013", "Engelsbrand", null], + ["082365005043", "Neuenbürg, Stadt", null], + ["082365006031", "Kieselbronn", null], + ["082365006073", "Neulingen", null], + ["082365006075", "Ölbronn-Dürrn", null], + ["082365007044", "Neuhausen", null], + ["082365007062", "Tiefenbronn", null], + ["082370002002", "Alpirsbach, Stadt", null], + ["082370004004", "Baiersbronn", null], + ["082370045045", "Loßburg", null], + ["082375001019", "Dornstetten, Stadt", null], + ["082375001030", "Glatten", null], + ["082375001061", "Schopfloch", null], + ["082375001074", "Waldachtal", null], + ["082375002028", "Freudenstadt, Stadt", null], + ["082375002073", "Seewald", null], + ["082375002075", "Bad Rippoldsau-Schapbach", null], + ["082375003024", "Empfingen", null], + ["082375003027", "Eutingen im Gäu", null], + ["082375003040", "Horb am Neckar, Stadt", null], + ["082375005032", "Grömbach", null], + ["082375005054", "Pfalzgrafenweiler", null], + ["082375005072", "Wörnersberg", null], + ["083110000000", "Freiburg im Breisgau, Stadt", null], + ["083150068068", "Lenzkirch", null], + ["083150076076", "Neuenburg am Rhein, Stadt", null], + ["083150133133", "Vogtsburg im Kaiserstuhl, Stadt", null], + ["083155001006", "Bad Krozingen, Stadt", null], + ["083155001048", "Hartheim am Rhein", null], + ["083155002015", "Breisach am Rhein, Stadt", null], + ["083155002059", "Ihringen", null], + ["083155002072", "Merdingen", null], + ["083155003020", "Buchenbach", null], + ["083155003064", "Kirchzarten", null], + ["083155003084", "Oberried", null], + ["083155003109", "Stegen", null], + ["083155004014", "Bollschweil", null], + ["083155004131", "Ehrenkirchen", null], + ["083155005047", "Gundelfingen", null], + ["083155005051", "Heuweiler", null], + ["083155006008", "Ballrechten-Dottingen", null], + ["083155006033", "Eschbach", null], + ["083155006050", "Heitersheim, Stadt", null], + ["083155007003", "Au", null], + ["083155007056", "Horben", null], + ["083155007073", "Merzhausen", null], + ["083155007107", "Sölden", null], + ["083155007125", "Wittnau", null], + ["083155008016", "Breitnau", null], + ["083155008052", "Hinterzarten", null], + ["083155009013", "Bötzingen", null], + ["083155009030", "Eichstetten am Kaiserstuhl", null], + ["083155009043", "Gottenheim", null], + ["083155010039", "Friedenweiler", null], + ["083155010070", "Löffingen, Stadt", null], + ["083155011115", "Umkirch", null], + ["083155011132", "March", null], + ["083155012004", "Auggen", null], + ["083155012007", "Badenweiler", null], + ["083155012022", "Buggingen", null], + ["083155012074", "Müllheim, Stadt", null], + ["083155012111", "Sulzburg, Stadt", null], + ["083155013041", "Glottertal", null], + ["083155013094", "St. Märgen", null], + ["083155013095", "St. Peter", null], + ["083155014028", "Ebringen", null], + ["083155014089", "Pfaffenweiler", null], + ["083155014098", "Schallstadt", null], + ["083155015037", "Feldberg (Schwarzwald)", null], + ["083155015102", "Schluchsee", null], + ["083155016108", "Staufen im Breisgau, Stadt", null], + ["083155016130", "Münstertal/Schwarzwald", null], + ["083155017031", "Eisenbach (Hochschwarzwald)", null], + ["083155017113", "Titisee-Neustadt, Stadt", null], + ["083165001009", "Denzlingen", null], + ["083165001036", "Reute", null], + ["083165001045", "Vörstetten", null], + ["083165002003", "Biederbach", null], + ["083165002010", "Elzach, Stadt", null], + ["083165002055", "Winden im Elztal", null], + ["083165003011", "Emmendingen, Stadt", null], + ["083165003024", "Malterdingen", null], + ["083165003039", "Sexau", null], + ["083165003043", "Teningen", null], + ["083165003054", "Freiamt", null], + ["083165004017", "Herbolzheim, Stadt", null], + ["083165004020", "Kenzingen, Stadt", null], + ["083165004049", "Weisweil", null], + ["083165004053", "Rheinhausen", null], + ["083165005002", "Bahlingen am Kaiserstuhl", null], + ["083165005012", "Endingen am Kaiserstuhl, Stadt", null], + ["083165005013", "Forchheim", null], + ["083165005037", "Riegel am Kaiserstuhl", null], + ["083165005038", "Sasbach am Kaiserstuhl", null], + ["083165005051", "Wyhl am Kaiserstuhl", null], + ["083165006014", "Gutach im Breisgau", null], + ["083165006042", "Simonswald", null], + ["083165006056", "Waldkirch, Stadt", null], + ["083170005005", "Appenweier", null], + ["083170031031", "Friesenheim", null], + ["083170051051", "Hornberg, Stadt", null], + ["083170057057", "Kehl, Stadt", null], + ["083170141141", "Willstätt", null], + ["083170151151", "Neuried", null], + ["083170153153", "Rheinau, Stadt", null], + ["083175001001", "Achern, Stadt", null], + ["083175001068", "Lauf", null], + ["083175001116", "Sasbach", null], + ["083175001118", "Sasbachwalden", null], + ["083175002026", "Ettenheim, Stadt", null], + ["083175002073", "Mahlberg, Stadt", null], + ["083175002113", "Ringsheim", null], + ["083175002114", "Rust", null], + ["083175002152", "Kappel-Grafenhausen", null], + ["083175003009", "Berghaupten", null], + ["083175003034", "Gengenbach, Stadt", null], + ["083175003097", "Ohlsbach", null], + ["083175004029", "Fischerbach", null], + ["083175004040", "Haslach im Kinzigtal, Stadt", null], + ["083175004046", "Hofstetten", null], + ["083175004078", "Mühlenbach", null], + ["083175004129", "Steinach", null], + ["083175005039", "Gutach (Schwarzwaldbahn)", null], + ["083175005041", "Hausach, Stadt", null], + ["083175006056", "Kappelrodeck", null], + ["083175006102", "Ottenhöfen im Schwarzwald", null], + ["083175006126", "Seebach", null], + ["083175007059", "Kippenheim", null], + ["083175007065", "Lahr/Schwarzwald, Stadt", null], + ["083175008008", "Bad Peterstal-Griesbach", null], + ["083175008098", "Oppenau, Stadt", null], + ["083175009067", "Lautenbach", null], + ["083175009089", "Oberkirch, Stadt", null], + ["083175009110", "Renchen, Stadt", null], + ["083175010021", "Durbach", null], + ["083175010047", "Hohberg", null], + ["083175010096", "Offenburg, Stadt", null], + ["083175010100", "Ortenberg", null], + ["083175010122", "Schutterwald", null], + ["083175011121", "Schuttertal", null], + ["083175011127", "Seelbach", null], + ["083175012075", "Meißenheim", null], + ["083175012150", "Schwanau", null], + ["083175013093", "Oberwolfach", null], + ["083175013145", "Wolfach, Stadt", null], + ["083175014011", "Biberach", null], + ["083175014085", "Nordrach", null], + ["083175014088", "Oberharmersbach", null], + ["083175014146", "Zell am Harmersbach, Stadt", null], + ["083179971971", "Rheinau, gemeindefreies Gebiet", null], + ["083250012012", "Dornhan, Stadt", null], + ["083255001014", "Dunningen", null], + ["083255001071", "Eschbronn", null], + ["083255002015", "Epfendorf", null], + ["083255002045", "Oberndorf am Neckar, Stadt", null], + ["083255002070", "Fluorn-Winzeln", null], + ["083255003011", "Dietingen", null], + ["083255003049", "Rottweil, Stadt", null], + ["083255003064", "Wellendingen", null], + ["083255003069", "Zimmern ob Rottweil", null], + ["083255003072", "Deißlingen", null], + ["083255004050", "Schenkenzell", null], + ["083255004051", "Schiltach, Stadt", null], + ["083255005001", "Aichhalden", null], + ["083255005024", "Hardt", null], + ["083255005036", "Lauterbach", null], + ["083255005053", "Schramberg, Stadt", null], + ["083255006057", "Sulz am Neckar, Stadt", null], + ["083255006061", "Vöhringen", null], + ["083255007009", "Bösingen", null], + ["083255007060", "Villingendorf", null], + ["083260003003", "Bad Dürrheim, Stadt", null], + ["083260005005", "Blumberg, Stadt", null], + ["083260031031", "Königsfeld im Schwarzwald", null], + ["083260052052", "St. Georgen im Schwarzwald, Stadt", null], + ["083260068068", "Vöhrenbach, Stadt", null], + ["083265001006", "Bräunlingen, Stadt", null], + ["083265001012", "Donaueschingen, Stadt", null], + ["083265001027", "Hüfingen, Stadt", null], + ["083265002017", "Furtwangen im Schwarzwald, Stadt", null], + ["083265002020", "Gütenbach", null], + ["083265003054", "Schönwald im Schwarzwald", null], + ["083265003055", "Schonach im Schwarzwald", null], + ["083265003060", "Triberg im Schwarzwald, Stadt", null], + ["083265004010", "Dauchingen", null], + ["083265004037", "Mönchweiler", null], + ["083265004041", "Niedereschach", null], + ["083265004061", "Tuningen", null], + ["083265004065", "Unterkirnach", null], + ["083265004074", "Villingen-Schwenningen, Stadt", null], + ["083265004075", "Brigachtal", null], + ["083275001004", "Bärenthal", null], + ["083275001008", "Buchheim", null], + ["083275001016", "Fridingen an der Donau, Stadt", null], + ["083275001027", "Irndorf", null], + ["083275001030", "Kolbingen", null], + ["083275001036", "Mühlheim an der Donau, Stadt", null], + ["083275001041", "Renquishausen", null], + ["083275002007", "Bubsheim", null], + ["083275002009", "Deilingen", null], + ["083275002013", "Egesheim", null], + ["083275002019", "Gosheim", null], + ["083275002029", "Königsheim", null], + ["083275002040", "Reichenbach am Heuberg", null], + ["083275002051", "Wehingen", null], + ["083275003018", "Geisingen, Stadt", null], + ["083275003025", "Immendingen", null], + ["083275004002", "Aldingen", null], + ["083275004005", "Balgheim", null], + ["083275004006", "Böttingen", null], + ["083275004010", "Denkingen", null], + ["083275004011", "Dürbheim", null], + ["083275004017", "Frittlingen", null], + ["083275004023", "Hausen ob Verena", null], + ["083275004033", "Mahlstetten", null], + ["083275004046", "Spaichingen, Stadt", null], + ["083275005012", "Durchhausen", null], + ["083275005020", "Gunningen", null], + ["083275005048", "Talheim", null], + ["083275005049", "Trossingen, Stadt", null], + ["083275006038", "Neuhausen ob Eck", null], + ["083275006050", "Tuttlingen, Stadt", null], + ["083275006054", "Wurmlingen", null], + ["083275006055", "Seitingen-Oberflacht", null], + ["083275006056", "Rietheim-Weilheim", null], + ["083275006057", "Emmingen-Liptingen", null], + ["083350035035", "Hilzingen", null], + ["083350063063", "Radolfzell am Bodensee, Stadt", null], + ["083350080080", "Tengen, Stadt", null], + ["083355001001", "Aach, Stadt", null], + ["083355001022", "Engen, Stadt", null], + ["083355001097", "Mühlhausen-Ehingen", null], + ["083355002015", "Büsingen am Hochrhein", null], + ["083355002026", "Gailingen am Hochrhein", null], + ["083355002028", "Gottmadingen", null], + ["083355003025", "Gaienhofen", null], + ["083355003055", "Moos", null], + ["083355003061", "Öhningen", null], + ["083355004002", "Allensbach", null], + ["083355004043", "Konstanz, Universitätsstadt", null], + ["083355004066", "Reichenau", null], + ["083355005075", "Singen (Hohentwiel), Stadt", null], + ["083355005077", "Steißlingen", null], + ["083355005081", "Volkertshausen", null], + ["083355005100", "Rielasingen-Worblingen", null], + ["083355006021", "Eigeltingen", null], + ["083355006057", "Mühlingen", null], + ["083355006079", "Stockach, Stadt", null], + ["083355006096", "Hohenfels", null], + ["083355006098", "Bodman-Ludwigshafen", null], + ["083355006099", "Orsingen-Nenzingen", null], + ["083360014014", "Efringen-Kirchen", null], + ["083360084084", "Steinen", null], + ["083360087087", "Todtnau, Stadt", null], + ["083360091091", "Weil am Rhein, Stadt", null], + ["083360105105", "Grenzach-Wyhlen", null], + ["083360107107", "Kleines Wiesental", null], + ["083365001045", "Kandern, Stadt", null], + ["083365001104", "Malsburg-Marzell", null], + ["083365003043", "Inzlingen", null], + ["083365003050", "Lörrach, Stadt", null], + ["083365004069", "Rheinfelden (Baden), Stadt", null], + ["083365004082", "Schwörstadt", null], + ["083365005006", "Bad Bellingen", null], + ["083365005078", "Schliengen", null], + ["083365006004", "Aitern", null], + ["083365006010", "Böllen", null], + ["083365006025", "Fröhnd", null], + ["083365006079", "Schönau im Schwarzwald, Stadt", null], + ["083365006080", "Schönenberg", null], + ["083365006089", "Tunau", null], + ["083365006090", "Utzenfeld", null], + ["083365006094", "Wembach", null], + ["083365006096", "Wieden", null], + ["083365007034", "Hasel", null], + ["083365007036", "Hausen im Wiesental", null], + ["083365007057", "Maulburg", null], + ["083365007081", "Schopfheim, Stadt", null], + ["083365008008", "Binzen", null], + ["083365008019", "Eimeldingen", null], + ["083365008024", "Fischingen", null], + ["083365008073", "Rümmingen", null], + ["083365008075", "Schallbach", null], + ["083365008100", "Wittlingen", null], + ["083365009103", "Zell im Wiesental, Stadt", null], + ["083365009106", "Häg-Ehrsberg", null], + ["083370002002", "Albbruck", null], + ["083370038038", "Görwihl", null], + ["083370062062", "Klettgau", null], + ["083370066066", "Laufenburg (Baden), Stadt", null], + ["083370106106", "Stühlingen, Stadt", null], + ["083370116116", "Wehr, Stadt", null], + ["083375001022", "Bonndorf im Schwarzwald, Stadt", null], + ["083375001127", "Wutach", null], + ["083375002030", "Dettighofen", null], + ["083375002060", "Jestetten", null], + ["083375002070", "Lottstetten", null], + ["083375003053", "Hohentengen am Hochrhein", null], + ["083375003125", "Küssaberg", null], + ["083375004039", "Grafenhausen", null], + ["083375004128", "Ühlingen-Birkendorf", null], + ["083375005049", "Herrischried", null], + ["083375005076", "Murg", null], + ["083375005090", "Rickenbach", null], + ["083375005096", "Bad Säckingen, Stadt", null], + ["083375006013", "Bernau im Schwarzwald", null], + ["083375006027", "Dachsberg (Südschwarzwald)", null], + ["083375006045", "Häusern", null], + ["083375006051", "Höchenschwand", null], + ["083375006059", "Ibach", null], + ["083375006097", "St. Blasien, Stadt", null], + ["083375006108", "Todtmoos", null], + ["083375007032", "Dogern", null], + ["083375007065", "Lauchringen", null], + ["083375007118", "Weilheim", null], + ["083375007126", "Waldshut-Tiengen, Stadt", null], + ["083375008123", "Wutöschingen", null], + ["083375008124", "Eggingen", null], + ["084150014014", "Dettingen an der Erms", null], + ["084150019019", "Eningen unter Achalm", null], + ["084150059059", "Pfullingen, Stadt", null], + ["084150061061", "Reutlingen, Stadt", null], + ["084150073073", "Trochtelfingen, Stadt", null], + ["084150080080", "Wannweil", null], + ["084150091091", "Sonnenbühl", null], + ["084150092092", "Lichtenstein", null], + ["084150093093", "St. Johann", null], + ["084155001089", "Engstingen", null], + ["084155001090", "Hohenstein", null], + ["084155002029", "Grafenberg", null], + ["084155002050", "Metzingen, Stadt", null], + ["084155002062", "Riederich", null], + ["084155003027", "Gomadingen", null], + ["084155003048", "Mehrstetten", null], + ["084155003053", "Münsingen, Stadt", null], + ["084155004060", "Pliezhausen", null], + ["084155004087", "Walddorfhäslach", null], + ["084155005028", "Grabenstetten", null], + ["084155005039", "Hülben", null], + ["084155005078", "Bad Urach, Stadt", null], + ["084155005088", "Römerstein", null], + ["084155006034", "Hayingen, Stadt", null], + ["084155006058", "Pfronstetten", null], + ["084155006085", "Zwiefalten", null], + ["084159971971", "Gutsbezirk Münsingen, gemeindefreies Gebiet", null], + ["084160009009", "Dettenhausen", null], + ["084160022022", "Kirchentellinsfurt", null], + ["084160023023", "Kusterdingen", null], + ["084160041041", "Tübingen, Universitätsstadt", null], + ["084160048048", "Ammerbuch", null], + ["084165001011", "Dußlingen", null], + ["084165001015", "Gomaringen", null], + ["084165001026", "Nehren", null], + ["084165002006", "Bodelshausen", null], + ["084165002025", "Mössingen, Stadt", null], + ["084165002031", "Ofterdingen", null], + ["084165003018", "Hirrlingen", null], + ["084165003036", "Rottenburg am Neckar, Stadt", null], + ["084165003049", "Neustetten", null], + ["084165003050", "Starzach", null], + ["084170013013", "Burladingen, Stadt", null], + ["084170025025", "Haigerloch, Stadt", null], + ["084170054054", "Rosenfeld, Stadt", null], + ["084175001010", "Bitz", null], + ["084175001079", "Albstadt, Stadt", null], + ["084175002002", "Balingen, Stadt", null], + ["084175002022", "Geislingen, Stadt", null], + ["084175003008", "Bisingen", null], + ["084175003023", "Grosselfingen", null], + ["084175004031", "Hechingen, Stadt", null], + ["084175004036", "Jungingen", null], + ["084175004051", "Rangendingen", null], + ["084175005044", "Meßstetten, Stadt", null], + ["084175005045", "Nusplingen", null], + ["084175005047", "Obernheim", null], + ["084175006014", "Dautmergen", null], + ["084175006015", "Dormettingen", null], + ["084175006016", "Dotternhausen", null], + ["084175006029", "Hausen am Tann", null], + ["084175006052", "Ratshausen", null], + ["084175006057", "Schömberg, Stadt", null], + ["084175006071", "Weilen unter den Rinnen", null], + ["084175006078", "Zimmern unter der Burg", null], + ["084175007063", "Straßberg", null], + ["084175007075", "Winterlingen", null], + ["084210000000", "Ulm, Universitätsstadt", null], + ["084250039039", "Erbach, Stadt", null], + ["084250108108", "Schelklingen, Stadt", null], + ["084250141141", "Blaustein, Stadt", null], + ["084255001002", "Allmendingen", null], + ["084255001004", "Altheim", null], + ["084255002017", "Berghülen", null], + ["084255002020", "Blaubeuren, Stadt", null], + ["084255003028", "Dietenheim, Stadt", null], + ["084255003066", "Illerrieden", null], + ["084255003140", "Balzheim", null], + ["084255004014", "Beimerstetten", null], + ["084255004031", "Dornstadt", null], + ["084255004135", "Westerstetten", null], + ["084255005033", "Ehingen (Donau), Stadt", null], + ["084255005050", "Griesingen", null], + ["084255005088", "Oberdischingen", null], + ["084255005093", "Öpfingen", null], + ["084255006064", "Hüttisheim", null], + ["084255006110", "Schnürpflingen", null], + ["084255006137", "Illerkirchberg", null], + ["084255006138", "Staig", null], + ["084255007071", "Laichingen, Stadt", null], + ["084255007079", "Merklingen", null], + ["084255007084", "Nellingen", null], + ["084255007134", "Westerheim", null], + ["084255007139", "Heroldstatt", null], + ["084255008005", "Altheim (Alb)", null], + ["084255008011", "Asselfingen", null], + ["084255008013", "Ballendorf", null], + ["084255008019", "Bernstadt", null], + ["084255008022", "Börslingen", null], + ["084255008024", "Breitingen", null], + ["084255008062", "Holzkirch", null], + ["084255008072", "Langenau, Stadt", null], + ["084255008083", "Neenstetten", null], + ["084255008085", "Nerenstetten", null], + ["084255008092", "Öllingen", null], + ["084255008097", "Rammingen", null], + ["084255008112", "Setzingen", null], + ["084255008130", "Weidenstetten", null], + ["084255009008", "Amstetten", null], + ["084255009075", "Lonsee", null], + ["084255010035", "Emeringen", null], + ["084255010036", "Emerkingen", null], + ["084255010052", "Grundsheim", null], + ["084255010055", "Hausen am Bussen", null], + ["084255010073", "Lauterach", null], + ["084255010081", "Munderkingen, Stadt", null], + ["084255010090", "Obermarchtal", null], + ["084255010091", "Oberstadion", null], + ["084255010098", "Rechtenstein", null], + ["084255010104", "Rottenacker", null], + ["084255010123", "Untermarchtal", null], + ["084255010124", "Unterstadion", null], + ["084255010125", "Unterwachingen", null], + ["084260134134", "Schemmerhofen", null], + ["084265001005", "Alleshausen", null], + ["084265001006", "Allmannsweiler", null], + ["084265001013", "Bad Buchau, Stadt", null], + ["084265001020", "Betzenweiler", null], + ["084265001036", "Dürnau", null], + ["084265001064", "Kanzach", null], + ["084265001078", "Moosburg", null], + ["084265001090", "Oggelshausen", null], + ["084265001109", "Seekirch", null], + ["084265001118", "Tiefenbach", null], + ["084265002014", "Bad Schussenried, Stadt", null], + ["084265002062", "Ingoldingen", null], + ["084265003011", "Attenweiler", null], + ["084265003021", "Biberach an der Riß, Stadt", null], + ["084265003038", "Eberhardzell", null], + ["084265003058", "Hochdorf", null], + ["084265003071", "Maselheim", null], + ["084265003074", "Mittelbiberach", null], + ["084265003120", "Ummendorf", null], + ["084265003128", "Warthausen", null], + ["084265004019", "Berkheim", null], + ["084265004031", "Dettingen an der Iller", null], + ["084265004044", "Erolzheim", null], + ["084265004065", "Kirchberg an der Iller", null], + ["084265004066", "Kirchdorf an der Iller", null], + ["084265005001", "Achstetten", null], + ["084265005028", "Burgrieden", null], + ["084265005070", "Laupheim, Stadt", null], + ["084265005073", "Mietingen", null], + ["084265006043", "Erlenmoos", null], + ["084265006087", "Ochsenhausen, Stadt", null], + ["084265006113", "Steinhausen an der Rottum", null], + ["084265006135", "Gutenzell-Hürbel", null], + ["084265007008", "Altheim", null], + ["084265007035", "Dürmentingen", null], + ["084265007045", "Ertingen", null], + ["084265007067", "Langenenslingen", null], + ["084265007097", "Riedlingen, Stadt", null], + ["084265007121", "Unlingen", null], + ["084265007124", "Uttenweiler", null], + ["084265008100", "Rot an der Rot", null], + ["084265008117", "Tannheim", null], + ["084265009108", "Schwendi", null], + ["084265009125", "Wain", null], + ["084350035035", "Meckenbeuren", null], + ["084355001013", "Eriskirch", null], + ["084355001029", "Kressbronn am Bodensee", null], + ["084355001030", "Langenargen", null], + ["084355002016", "Friedrichshafen, Stadt", null], + ["084355002024", "Immenstaad am Bodensee", null], + ["084355003005", "Bermatingen", null], + ["084355003034", "Markdorf, Stadt", null], + ["084355003045", "Oberteuringen", null], + ["084355003067", "Deggenhausertal", null], + ["084355004010", "Daisendorf", null], + ["084355004018", "Hagnau am Bodensee", null], + ["084355004036", "Meersburg, Stadt", null], + ["084355004054", "Stetten", null], + ["084355004066", "Uhldingen-Mühlhofen", null], + ["084355005015", "Frickingen", null], + ["084355005020", "Heiligenberg", null], + ["084355005052", "Salem", null], + ["084355006042", "Neukirch", null], + ["084355006057", "Tettnang, Stadt", null], + ["084355007047", "Owingen", null], + ["084355007053", "Sipplingen", null], + ["084355007059", "Überlingen, Stadt", null], + ["084360008008", "Aulendorf, Stadt", null], + ["084360010010", "Bad Wurzach, Stadt", null], + ["084360049049", "Isny im Allgäu, Stadt", null], + ["084360052052", "Kißlegg", null], + ["084360094094", "Argenbühl", null], + ["084365001005", "Altshausen", null], + ["084365001019", "Boms", null], + ["084365001024", "Ebenweiler", null], + ["084365001027", "Eichstegen", null], + ["084365001032", "Fleischwangen", null], + ["084365001040", "Guggenhausen", null], + ["084365001047", "Hoßkirch", null], + ["084365001053", "Königseggwald", null], + ["084365001067", "Riedhausen", null], + ["084365001077", "Unterwaldhausen", null], + ["084365001093", "Ebersbach-Musbach", null], + ["084365002009", "Bad Waldsee, Stadt", null], + ["084365002014", "Bergatreute", null], + ["084365003018", "Bodnegg", null], + ["084365003039", "Grünkraut", null], + ["084365003069", "Schlier", null], + ["084365003079", "Waldburg", null], + ["084365004003", "Aichstetten", null], + ["084365004004", "Aitrach", null], + ["084365004055", "Leutkirch im Allgäu, Stadt", null], + ["084365005011", "Baienfurt", null], + ["084365005012", "Baindt", null], + ["084365005013", "Berg", null], + ["084365005064", "Ravensburg, Stadt", null], + ["084365005082", "Weingarten, Stadt", null], + ["084365006078", "Vogt", null], + ["084365006085", "Wolfegg", null], + ["084365007001", "Achberg", null], + ["084365007006", "Amtzell", null], + ["084365007081", "Wangen im Allgäu, Stadt", null], + ["084365008083", "Wilhelmsdorf", null], + ["084365008095", "Horgenzell", null], + ["084365009087", "Wolpertswende", null], + ["084365009096", "Fronreute", null], + ["084370086086", "Ostrach", null], + ["084375001031", "Gammertingen, Stadt", null], + ["084375001047", "Hettingen, Stadt", null], + ["084375001082", "Neufra", null], + ["084375001114", "Veringenstadt, Stadt", null], + ["084375002053", "Hohentengen", null], + ["084375002076", "Mengen, Stadt", null], + ["084375002101", "Scheer, Stadt", null], + ["084375003072", "Leibertingen", null], + ["084375003078", "Meßkirch, Stadt", null], + ["084375003123", "Sauldorf", null], + ["084375004056", "Illmensee", null], + ["084375004088", "Pfullendorf, Stadt", null], + ["084375004118", "Wald", null], + ["084375004124", "Herdwangen-Schönach", null], + ["084375005044", "Herbertingen", null], + ["084375005100", "Bad Saulgau, Stadt", null], + ["084375006005", "Beuron", null], + ["084375006008", "Bingen", null], + ["084375006059", "Inzigkofen", null], + ["084375006065", "Krauchenwies", null], + ["084375006104", "Sigmaringen, Stadt", null], + ["084375006105", "Sigmaringendorf", null], + ["084375007102", "Schwenningen", null], + ["084375007107", "Stetten am kalten Markt", null], + ["091610000000", "Ingolstadt", null], + ["091620000000", "München, Landeshauptstadt", null], + ["091630000000", "Rosenheim", null], + ["091710111111", "Altötting, St", null], + ["091710112112", "Burghausen, St", null], + ["091710113113", "Burgkirchen a.d.Alz", null], + ["091710117117", "Garching a.d.Alz", null], + ["091710118118", "Haiming", null], + ["091710125125", "Neuötting, St", null], + ["091710127127", "Pleiskirchen", null], + ["091710131131", "Teising", null], + ["091710132132", "Töging a.Inn, St", null], + ["091710133133", "Tüßling, M", null], + ["091710137137", "Winhöring", null], + ["091715101114", "Emmerting", null], + ["091715101124", "Mehring", null], + ["091715102116", "Feichten a.d.Alz", null], + ["091715102119", "Halsbach", null], + ["091715102122", "Kirchweidach", null], + ["091715102134", "Tyrlaching", null], + ["091715103123", "Marktl, M", null], + ["091715103130", "Stammham", null], + ["091715104115", "Erlbach", null], + ["091715104126", "Perach", null], + ["091715104129", "Reischach", null], + ["091715106121", "Kastl", null], + ["091715106135", "Unterneukirchen", null], + ["091720111111", "Ainring", null], + ["091720112112", "Anger", null], + ["091720114114", "Bad Reichenhall, GKSt", null], + ["091720115115", "Bayerisch Gmain", null], + ["091720116116", "Berchtesgaden, M", null], + ["091720117117", "Bischofswiesen", null], + ["091720118118", "Freilassing, St", null], + ["091720122122", "Laufen, St", null], + ["091720124124", "Marktschellenberg, M", null], + ["091720128128", "Piding", null], + ["091720129129", "Ramsau b.Berchtesgaden", null], + ["091720130130", "Saaldorf-Surheim", null], + ["091720131131", "Schneizlreuth", null], + ["091720132132", "Schönau a.Königssee", null], + ["091720134134", "Teisendorf, M", null], + ["091729452452", "Eck", null], + ["091729454454", "Schellenberger Forst", null], + ["091730111111", "Bad Heilbrunn", null], + ["091730112112", "Bad Tölz, St", null], + ["091730118118", "Dietramszell", null], + ["091730120120", "Egling", null], + ["091730123123", "Eurasburg", null], + ["091730124124", "Gaißach", null], + ["091730126126", "Geretsried, St", null], + ["091730130130", "Icking", null], + ["091730131131", "Jachenau", null], + ["091730134134", "Königsdorf", null], + ["091730135135", "Lenggries", null], + ["091730137137", "Münsing", null], + ["091730145145", "Wackersberg", null], + ["091730147147", "Wolfratshausen, St", null], + ["091735107113", "Benediktbeuern", null], + ["091735107115", "Bichl", null], + ["091735108133", "Kochel a.See", null], + ["091735108142", "Schlehdorf", null], + ["091735109127", "Greiling", null], + ["091735109140", "Reichersbeuern", null], + ["091735109141", "Sachsenkam", null], + ["091739451451", "Pupplinger Au", null], + ["091739452452", "Wolfratshauser Forst", null], + ["091740111111", "Altomünster, M", null], + ["091740113113", "Bergkirchen", null], + ["091740115115", "Dachau, GKSt", null], + ["091740118118", "Erdweg", null], + ["091740121121", "Haimhausen", null], + ["091740122122", "Hebertshausen", null], + ["091740126126", "Karlsfeld", null], + ["091740131131", "Markt Indersdorf, M", null], + ["091740135135", "Odelzhausen", null], + ["091740136136", "Petershausen", null], + ["091740137137", "Pfaffenhofen a.d.Glonn", null], + ["091740141141", "Röhrmoos", null], + ["091740143143", "Schwabhausen", null], + ["091740146146", "Sulzemoos", null], + ["091740147147", "Hilgertshausen-Tandern", null], + ["091740150150", "Vierkirchen", null], + ["091740151151", "Weichs", null], + ["091750111111", "Anzing", null], + ["091750115115", "Ebersberg, St", null], + ["091750118118", "Forstinning", null], + ["091750122122", "Grafing b.München, St", null], + ["091750123123", "Hohenlinden", null], + ["091750124124", "Kirchseeon, M", null], + ["091750127127", "Markt Schwaben, M", null], + ["091750132132", "Vaterstetten", null], + ["091750133133", "Pliening", null], + ["091750135135", "Poing", null], + ["091750137137", "Steinhöring", null], + ["091750139139", "Zorneding", null], + ["091755112112", "Aßling", null], + ["091755112119", "Frauenneuharting", null], + ["091755112136", "Emmering", null], + ["091755114113", "Baiern", null], + ["091755114114", "Bruck", null], + ["091755114116", "Egmating", null], + ["091755114121", "Glonn, M", null], + ["091755114128", "Moosach", null], + ["091755114131", "Oberpframmern", null], + ["091759451451", "Anzinger Forst", null], + ["091759452452", "Ebersberger Forst", null], + ["091759453453", "Eglhartinger Forst", null], + ["091760112112", "Altmannstein, M", null], + ["091760114114", "Beilngries, St", null], + ["091760118118", "Buxheim", null], + ["091760120120", "Denkendorf", null], + ["091760121121", "Dollnstein, M", null], + ["091760123123", "Eichstätt, GKSt", null], + ["091760126126", "Gaimersheim, M", null], + ["091760129129", "Großmehring", null], + ["091760131131", "Hepberg", null], + ["091760132132", "Hitzhofen", null], + ["091760137137", "Kinding, M", null], + ["091760138138", "Kipfenberg, M", null], + ["091760139139", "Kösching, M", null], + ["091760143143", "Lenting", null], + ["091760148148", "Mörnsheim, M", null], + ["091760161161", "Stammham", null], + ["091760164164", "Titting, M", null], + ["091760166166", "Wellheim, M", null], + ["091760167167", "Wettstetten", null], + ["091765115155", "Pollenfeld", null], + ["091765115160", "Schernfeld", null], + ["091765115165", "Walting", null], + ["091765116116", "Böhmfeld", null], + ["091765116124", "Eitensheim", null], + ["091765118111", "Adelschlag", null], + ["091765118122", "Egweil", null], + ["091765118149", "Nassenfels, M", null], + ["091765119147", "Mindelstetten", null], + ["091765119150", "Oberdolling", null], + ["091765119153", "Pförring, M", null], + ["091769451451", "Haunstetter Forst", null], + ["091770113113", "Bockhorn", null], + ["091770115115", "Dorfen, St", null], + ["091770117117", "Erding, GKSt", null], + ["091770118118", "Finsing", null], + ["091770119119", "Forstern", null], + ["091770120120", "Fraunberg", null], + ["091770123123", "Isen, M", null], + ["091770127127", "Lengdorf", null], + ["091770130130", "Moosinning", null], + ["091770137137", "Sankt Wolfgang", null], + ["091770139139", "Taufkirchen (Vils)", null], + ["091775120114", "Buch a.Buchrain", null], + ["091775120135", "Pastetten", null], + ["091775121142", "Walpertskirchen", null], + ["091775121144", "Wörth", null], + ["091775123116", "Eitting", null], + ["091775123133", "Oberding", null], + ["091775124131", "Neuching", null], + ["091775124134", "Ottenhofen", null], + ["091775125121", "Hohenpolding", null], + ["091775125122", "Inning a.Holz", null], + ["091775125124", "Kirchberg", null], + ["091775125138", "Steinkirchen", null], + ["091775126112", "Berglern", null], + ["091775126126", "Langenpreising", null], + ["091775126143", "Wartenberg, M", null], + ["091780116116", "Au i.d.Hallertau, M", null], + ["091780120120", "Eching", null], + ["091780122122", "Rudelzhausen", null], + ["091780123123", "Fahrenzhausen", null], + ["091780124124", "Freising, GKSt", null], + ["091780130130", "Hallbergmoos", null], + ["091780133133", "Hohenkammer", null], + ["091780136136", "Kirchdorf a.d.Amper", null], + ["091780137137", "Kranzberg", null], + ["091780138138", "Langenbach", null], + ["091780140140", "Marzling", null], + ["091780143143", "Moosburg a.d.Isar, St", null], + ["091780144144", "Nandlstadt, M", null], + ["091780145145", "Neufahrn b.Freising", null], + ["091785127113", "Allershausen", null], + ["091785127150", "Paunzhausen", null], + ["091785129125", "Gammelsdorf", null], + ["091785129132", "Hörgertshausen", null], + ["091785129142", "Mauern", null], + ["091785129155", "Wang", null], + ["091785130115", "Attenkirchen", null], + ["091785130129", "Haag a.d.Amper", null], + ["091785130156", "Wolfersdorf", null], + ["091785130157", "Zolling", null], + ["091790113113", "Alling", null], + ["091790117117", "Egenhofen", null], + ["091790118118", "Eichenau", null], + ["091790119119", "Emmering", null], + ["091790121121", "Fürstenfeldbruck, GKSt", null], + ["091790123123", "Germering, GKSt", null], + ["091790126126", "Gröbenzell", null], + ["091790134134", "Maisach", null], + ["091790138138", "Moorenweis", null], + ["091790142142", "Olching, St", null], + ["091790145145", "Puchheim, St", null], + ["091790149149", "Türkenfeld", null], + ["091795131111", "Adelshofen", null], + ["091795131114", "Althegnenberg", null], + ["091795131128", "Hattenhofen", null], + ["091795131130", "Jesenwang", null], + ["091795131132", "Landsberied", null], + ["091795131136", "Mammendorf", null], + ["091795131137", "Mittelstetten", null], + ["091795131140", "Oberschweinbach", null], + ["091795132125", "Grafrath", null], + ["091795132131", "Kottgeisering", null], + ["091795132147", "Schöngeising", null], + ["091800112112", "Bad Kohlgrub", null], + ["091800116116", "Farchant", null], + ["091800117117", "Garmisch-Partenkirchen, M", null], + ["091800118118", "Grainau", null], + ["091800122122", "Krün", null], + ["091800123123", "Mittenwald, M", null], + ["091800124124", "Murnau a.Staffelsee, M", null], + ["091800125125", "Oberammergau", null], + ["091800126126", "Oberau", null], + ["091800134134", "Uffing a.Staffelsee", null], + ["091800136136", "Wallgau", null], + ["091805133113", "Bad Bayersoien", null], + ["091805133129", "Saulgrub", null], + ["091805135115", "Ettal", null], + ["091805135135", "Unterammergau", null], + ["091805136114", "Eschenlohe", null], + ["091805136119", "Großweil", null], + ["091805136127", "Ohlstadt", null], + ["091805136131", "Schwaigen", null], + ["091805137128", "Riegsee", null], + ["091805137132", "Seehausen a.Staffelsee", null], + ["091805137133", "Spatzenhausen", null], + ["091809451451", "Ettaler Forst", null], + ["091810113113", "Denklingen", null], + ["091810114114", "Dießen am Ammersee, M", null], + ["091810116116", "Egling a.d.Paar", null], + ["091810122122", "Geltendorf", null], + ["091810128128", "Kaufering, M", null], + ["091810130130", "Landsberg am Lech, GKSt", null], + ["091810132132", "Penzing", null], + ["091810144144", "Utting am Ammersee", null], + ["091810145145", "Weil", null], + ["091815138121", "Fuchstal", null], + ["091815138143", "Unterdießen", null], + ["091815139126", "Hurlach", null], + ["091815139127", "Igling", null], + ["091815139131", "Obermeitingen", null], + ["091815140134", "Prittriching", null], + ["091815140138", "Scheuring", null], + ["091815141124", "Hofstetten", null], + ["091815141140", "Schwifting", null], + ["091815141141", "Pürgen", null], + ["091815142111", "Apfeldorf", null], + ["091815142129", "Kinsau", null], + ["091815142133", "Vilgertshofen", null], + ["091815142135", "Reichling", null], + ["091815142137", "Rott", null], + ["091815142142", "Thaining", null], + ["091815143115", "Eching am Ammersee", null], + ["091815143123", "Greifenberg", null], + ["091815143139", "Schondorf am Ammersee", null], + ["091815144118", "Eresing", null], + ["091815144120", "Finning", null], + ["091815144146", "Windach", null], + ["091819451451", "Ammersee", null], + ["091820111111", "Bad Wiessee", null], + ["091820112112", "Bayrischzell", null], + ["091820114114", "Fischbachau", null], + ["091820116116", "Gmund a.Tegernsee", null], + ["091820119119", "Hausham", null], + ["091820120120", "Holzkirchen, M", null], + ["091820123123", "Irschenberg", null], + ["091820124124", "Kreuth", null], + ["091820125125", "Miesbach, St", null], + ["091820127127", "Otterfing", null], + ["091820129129", "Rottach-Egern", null], + ["091820131131", "Schliersee, M", null], + ["091820132132", "Tegernsee, St", null], + ["091820133133", "Valley", null], + ["091820134134", "Waakirchen", null], + ["091820136136", "Warngau", null], + ["091820137137", "Weyarn", null], + ["091830112112", "Ampfing", null], + ["091830113113", "Aschau a.Inn", null], + ["091830114114", "Buchbach, M", null], + ["091830119119", "Haag i.OB, M", null], + ["091830127127", "Mettenheim", null], + ["091830128128", "Mühldorf a.Inn, St", null], + ["091830135135", "Obertaufkirchen", null], + ["091830144144", "Schwindegg", null], + ["091830148148", "Waldkraiburg, St", null], + ["091835145120", "Heldenstein", null], + ["091835145138", "Rattenkirchen", null], + ["091835146118", "Gars a.Inn, M", null], + ["091835146147", "Unterreit", null], + ["091835147123", "Kirchdorf", null], + ["091835147140", "Reichertsheim", null], + ["091835148122", "Jettenbach", null], + ["091835148124", "Kraiburg a.Inn, M", null], + ["091835148145", "Taufkirchen", null], + ["091835149115", "Egglkofen", null], + ["091835149129", "Neumarkt-Sankt Veit, St", null], + ["091835150125", "Lohkirchen", null], + ["091835150132", "Oberbergkirchen", null], + ["091835150143", "Schönberg", null], + ["091835150151", "Zangberg", null], + ["091835151134", "Oberneukirchen", null], + ["091835151136", "Polling", null], + ["091835152116", "Erharting", null], + ["091835152130", "Niederbergkirchen", null], + ["091835152131", "Niedertaufkirchen", null], + ["091835183126", "Maitenbeth", null], + ["091835183139", "Rechtmehring", null], + ["091839451451", "Mühldorfer Hart", null], + ["091840112112", "Aschheim", null], + ["091840113113", "Baierbrunn", null], + ["091840114114", "Brunnthal", null], + ["091840118118", "Feldkirchen", null], + ["091840119119", "Garching b.München, St", null], + ["091840120120", "Gräfelfing", null], + ["091840121121", "Grasbrunn", null], + ["091840122122", "Grünwald", null], + ["091840123123", "Haar", null], + ["091840127127", "Höhenkirchen-Siegertsbrunn", null], + ["091840129129", "Hohenbrunn", null], + ["091840130130", "Ismaning", null], + ["091840131131", "Kirchheim b.München", null], + ["091840132132", "Neuried", null], + ["091840134134", "Oberhaching", null], + ["091840135135", "Oberschleißheim", null], + ["091840136136", "Ottobrunn", null], + ["091840137137", "Aying", null], + ["091840138138", "Planegg", null], + ["091840139139", "Pullach i.Isartal", null], + ["091840140140", "Putzbrunn", null], + ["091840141141", "Sauerlach", null], + ["091840142142", "Schäftlarn", null], + ["091840144144", "Straßlach-Dingharting", null], + ["091840145145", "Taufkirchen", null], + ["091840146146", "Neubiberg", null], + ["091840147147", "Unterföhring", null], + ["091840148148", "Unterhaching", null], + ["091840149149", "Unterschleißheim, St", null], + ["091849452452", "Forstenrieder Park", null], + ["091849454454", "Grünwalder Forst", null], + ["091849457457", "Perlacher Forst", null], + ["091850113113", "Aresing", null], + ["091850125125", "Burgheim, M", null], + ["091850127127", "Ehekirchen", null], + ["091850139139", "Karlshuld", null], + ["091850140140", "Karlskron", null], + ["091850149149", "Neuburg a.d.Donau, GKSt", null], + ["091850150150", "Oberhausen", null], + ["091850153153", "Rennertshofen, M", null], + ["091850158158", "Schrobenhausen, St", null], + ["091850163163", "Königsmoos", null], + ["091850168168", "Weichering", null], + ["091855154118", "Bergheim", null], + ["091855154157", "Rohrenfels", null], + ["091855155116", "Berg im Gau", null], + ["091855155123", "Brunnen", null], + ["091855155131", "Gachenbach", null], + ["091855155143", "Langenmosen", null], + ["091855155166", "Waidhofen", null], + ["091860113113", "Baar-Ebenhausen", null], + ["091860125125", "Gerolsbach", null], + ["091860128128", "Hohenwart, M", null], + ["091860132132", "Jetzendorf", null], + ["091860137137", "Manching, M", null], + ["091860139139", "Münchsmünster", null], + ["091860143143", "Pfaffenhofen a.d.Ilm, St", null], + ["091860146146", "Reichertshausen", null], + ["091860149149", "Rohrbach", null], + ["091860151151", "Scheyern", null], + ["091860152152", "Schweitenkirchen", null], + ["091860158158", "Vohburg a.d.Donau, St", null], + ["091860162162", "Wolnzach, M", null], + ["091865156116", "Ernsgaden", null], + ["091865156122", "Geisenfeld, St", null], + ["091865157126", "Hettenshausen", null], + ["091865157130", "Ilmmünster", null], + ["091865158144", "Pörnbach", null], + ["091865158147", "Reichertshofen, M", null], + ["091870113113", "Amerang", null], + ["091870114114", "Aschau i.Chiemgau", null], + ["091870116116", "Babensham", null], + ["091870117117", "Bad Aibling, St", null], + ["091870118118", "Bernau a.Chiemsee", null], + ["091870120120", "Brannenburg", null], + ["091870122122", "Bruckmühl, M", null], + ["091870124124", "Edling", null], + ["091870125125", "Eggstätt", null], + ["091870126126", "Eiselfing", null], + ["091870128128", "Bad Endorf, M", null], + ["091870129129", "Bad Feilnbach", null], + ["091870130130", "Feldkirchen-Westerham", null], + ["091870131131", "Flintsbach a.Inn", null], + ["091870132132", "Frasdorf", null], + ["091870134134", "Griesstätt", null], + ["091870137137", "Großkarolinenfeld", null], + ["091870142142", "Schechen", null], + ["091870148148", "Kiefersfelden", null], + ["091870150150", "Kolbermoor, St", null], + ["091870154154", "Neubeuern, M", null], + ["091870156156", "Nußdorf a.Inn", null], + ["091870157157", "Oberaudorf", null], + ["091870162162", "Prien a.Chiemsee, M", null], + ["091870163163", "Prutting", null], + ["091870165165", "Raubling", null], + ["091870167167", "Riedering", null], + ["091870168168", "Rimsting", null], + ["091870169169", "Rohrdorf", null], + ["091870172172", "Samerberg", null], + ["091870174174", "Söchtenau", null], + ["091870176176", "Soyen", null], + ["091870177177", "Stephanskirchen", null], + ["091870179179", "Tuntenhausen", null], + ["091870181181", "Vogtareuth", null], + ["091870182182", "Wasserburg a.Inn, St", null], + ["091875160121", "Breitbrunn a.Chiemsee", null], + ["091875160123", "Chiemsee", null], + ["091875160138", "Gstadt a.Chiemsee", null], + ["091875162139", "Halfing", null], + ["091875162145", "Höslwang", null], + ["091875162173", "Schonstett", null], + ["091875165164", "Ramerberg", null], + ["091875165170", "Rott a.Inn", null], + ["091875184159", "Pfaffing", null], + ["091875184186", "Albaching", null], + ["091879451451", "Rotter Forst-Nord", null], + ["091879452452", "Rotter Forst-Süd", null], + ["091880113113", "Berg", null], + ["091880117117", "Andechs", null], + ["091880118118", "Feldafing", null], + ["091880120120", "Gauting", null], + ["091880121121", "Gilching", null], + ["091880124124", "Herrsching a.Ammersee", null], + ["091880126126", "Inning a.Ammersee", null], + ["091880127127", "Krailling", null], + ["091880132132", "Seefeld", null], + ["091880137137", "Pöcking", null], + ["091880139139", "Starnberg, St", null], + ["091880141141", "Tutzing", null], + ["091880144144", "Weßling", null], + ["091880145145", "Wörthsee", null], + ["091889451451", "Starnberger See", null], + ["091890111111", "Altenmarkt a.d.Alz", null], + ["091890114114", "Chieming", null], + ["091890115115", "Engelsberg", null], + ["091890118118", "Fridolfing", null], + ["091890119119", "Grabenstätt", null], + ["091890120120", "Grassau, M", null], + ["091890124124", "Inzell", null], + ["091890127127", "Kirchanschöring", null], + ["091890130130", "Nußdorf", null], + ["091890134134", "Palling", null], + ["091890135135", "Petting", null], + ["091890139139", "Reit im Winkl", null], + ["091890140140", "Ruhpolding", null], + ["091890141141", "Schleching", null], + ["091890142142", "Schnaitsee", null], + ["091890143143", "Seeon-Seebruck", null], + ["091890145145", "Siegsdorf", null], + ["091890148148", "Surberg", null], + ["091890149149", "Tacherting", null], + ["091890152152", "Tittmoning, St", null], + ["091890154154", "Traunreut, St", null], + ["091890155155", "Traunstein, GKSt", null], + ["091890157157", "Trostberg, St", null], + ["091890159159", "Übersee", null], + ["091890160160", "Unterwössen", null], + ["091895166113", "Bergen", null], + ["091895166161", "Vachendorf", null], + ["091895169129", "Marquartstein", null], + ["091895169146", "Staudach-Egerndach", null], + ["091895170126", "Kienberg", null], + ["091895170133", "Obing", null], + ["091895170137", "Pittenhart", null], + ["091895173150", "Taching a.See", null], + ["091895173162", "Waging a.See, M", null], + ["091895173165", "Wonneberg", null], + ["091899451451", "Chiemsee (See)", null], + ["091899452452", "Waginger See", null], + ["091900115115", "Bernried am Starnberger See", null], + ["091900130130", "Hohenpeißenberg", null], + ["091900138138", "Pähl", null], + ["091900139139", "Peißenberg, M", null], + ["091900140140", "Peiting, M", null], + ["091900141141", "Penzberg, St", null], + ["091900142142", "Polling", null], + ["091900144144", "Raisting", null], + ["091900148148", "Schongau, St", null], + ["091900157157", "Weilheim i.OB, St", null], + ["091900158158", "Wessobrunn", null], + ["091900159159", "Wielenbach", null], + ["091905174111", "Altenstadt", null], + ["091905174129", "Hohenfurch", null], + ["091905174133", "Ingenried", null], + ["091905174149", "Schwabbruck", null], + ["091905174151", "Schwabsoien", null], + ["091905175114", "Bernbeuren", null], + ["091905175118", "Burggen", null], + ["091905176113", "Antdorf", null], + ["091905176126", "Habach", null], + ["091905176136", "Obersöchering", null], + ["091905176153", "Sindelsdorf", null], + ["091905177120", "Eberfing", null], + ["091905177121", "Eglfing", null], + ["091905177131", "Huglfing", null], + ["091905177135", "Oberhausen", null], + ["091905178117", "Böbing", null], + ["091905178145", "Rottenbuch", null], + ["091905179132", "Iffeldorf", null], + ["091905179152", "Seeshaupt", null], + ["091905180143", "Prem", null], + ["091905180154", "Steingaden", null], + ["091905180160", "Wildsteig", null], + ["092610000000", "Landshut", null], + ["092620000000", "Passau", null], + ["092630000000", "Straubing", null], + ["092710111111", "Aholming", null], + ["092710113113", "Auerbach", null], + ["092710116116", "Bernried", null], + ["092710119119", "Deggendorf, GKSt", null], + ["092710122122", "Grafling", null], + ["092710125125", "Hengersberg, M", null], + ["092710127127", "Iggensbach", null], + ["092710128128", "Künzing", null], + ["092710132132", "Metten, M", null], + ["092710138138", "Niederalteich", null], + ["092710140140", "Offenberg", null], + ["092710141141", "Osterhofen, St", null], + ["092710146146", "Plattling, St", null], + ["092710151151", "Stephansposching", null], + ["092710153153", "Winzer, M", null], + ["092715202123", "Grattersdorf", null], + ["092715202126", "Hunding", null], + ["092715202130", "Lalling", null], + ["092715202148", "Schaufling", null], + ["092715204139", "Oberpöring", null], + ["092715204143", "Otzing", null], + ["092715204152", "Wallerfing", null], + ["092715205118", "Buchhofen", null], + ["092715205135", "Moos", null], + ["092715206114", "Außernzell", null], + ["092715206149", "Schöllnach, M", null], + ["092720118118", "Freyung, St", null], + ["092720120120", "Grafenau, St", null], + ["092720121121", "Grainet", null], + ["092720122122", "Haidmühle", null], + ["092720127127", "Hohenau", null], + ["092720129129", "Jandelsbrunn", null], + ["092720134134", "Mauth", null], + ["092720136136", "Neureichenau", null], + ["092720140140", "Ringelai", null], + ["092720141141", "Röhrnbach, M", null], + ["092720142142", "Saldenburg", null], + ["092720143143", "Sankt Oswald-Riedlhütte", null], + ["092720146146", "Neuschönau", null], + ["092720149149", "Spiegelau", null], + ["092720151151", "Waldkirchen, St", null], + ["092725211116", "Eppenschlag", null], + ["092725211128", "Innernzell", null], + ["092725211145", "Schöfweg", null], + ["092725211147", "Schönberg, M", null], + ["092725212126", "Hinterschmiding", null], + ["092725212139", "Philippsreut", null], + ["092725213150", "Thurmansbang", null], + ["092725213152", "Zenting", null], + ["092725214119", "Fürsteneck", null], + ["092725214138", "Perlesreut, M", null], + ["092729451451", "Annathaler Wald", null], + ["092729452452", "Frauenberger u. Duschlberger Wald", null], + ["092729453453", "Graineter Wald", null], + ["092729455455", "Leopoldsreuter Wald", null], + ["092729456456", "Mauther Forst", null], + ["092729457457", "Philippsreuter Wald", null], + ["092729458458", "Pleckensteiner Wald", null], + ["092729459459", "Sankt Oswald", null], + ["092729460460", "Schlichtenberger Wald", null], + ["092729461461", "Schönbrunner Wald", null], + ["092729463463", "Waldhäuserwald", null], + ["092730111111", "Abensberg, St", null], + ["092730116116", "Bad Abbach, M", null], + ["092730137137", "Kelheim, St", null], + ["092730147147", "Mainburg, St", null], + ["092730152152", "Neustadt a.d.Donau, St", null], + ["092730159159", "Painten, M", null], + ["092730164164", "Riedenburg, St", null], + ["092730165165", "Rohr i.NB, M", null], + ["092735215121", "Essing, M", null], + ["092735215133", "Ihrlerstein", null], + ["092735216166", "Saal a.d.Donau", null], + ["092735216175", "Teugn", null], + ["092735217125", "Hausen", null], + ["092735217127", "Herrngiersdorf", null], + ["092735217141", "Langquaid, M", null], + ["092735218119", "Biburg", null], + ["092735218139", "Kirchdorf", null], + ["092735218172", "Siegenburg, M", null], + ["092735218177", "Train", null], + ["092735218181", "Wildenberg", null], + ["092735219113", "Aiglsbach", null], + ["092735219115", "Attenhofen", null], + ["092735219163", "Elsendorf", null], + ["092735219178", "Volkenschwand", null], + ["092739451451", "Dürnbucher Forst", null], + ["092739452452", "Frauenforst", null], + ["092739453453", "Hacklberg", null], + ["092739454454", "Hienheimer Forst", null], + ["092740111111", "Adlkofen", null], + ["092740113113", "Altdorf, M", null], + ["092740120120", "Bodenkirchen", null], + ["092740121121", "Buch a.Erlbach", null], + ["092740124124", "Eching", null], + ["092740126126", "Ergolding, M", null], + ["092740128128", "Essenbach, M", null], + ["092740134134", "Geisenhausen, M", null], + ["092740141141", "Hohenthann", null], + ["092740146146", "Kumhausen", null], + ["092740153153", "Neufahrn i.NB", null], + ["092740156156", "Niederaichbach", null], + ["092740172172", "Pfeffenhausen, M", null], + ["092740176176", "Rottenburg a.d.Laaber, St", null], + ["092740182182", "Tiefenbach", null], + ["092740184184", "Vilsbiburg, St", null], + ["092740185185", "Vilsheim", null], + ["092740194194", "Bruckberg", null], + ["092745220119", "Bayerbach b.Ergoldsbach", null], + ["092745220127", "Ergoldsbach, M", null], + ["092745221132", "Furth", null], + ["092745221165", "Obersüßbach", null], + ["092745221187", "Weihmichl", null], + ["092745222174", "Postau", null], + ["092745222188", "Weng", null], + ["092745222191", "Wörth a.d.Isar", null], + ["092745223112", "Aham", null], + ["092745223135", "Gerzen", null], + ["092745223145", "Kröning", null], + ["092745223179", "Schalkham", null], + ["092745226114", "Altfraunhofen", null], + ["092745226118", "Baierbach", null], + ["092745227154", "Neufraunhofen", null], + ["092745227183", "Velden, M", null], + ["092745227193", "Wurmsham", null], + ["092750111111", "Aicha vorm Wald", null], + ["092750114114", "Aldersbach", null], + ["092750116116", "Bad Füssing", null], + ["092750118118", "Breitenberg", null], + ["092750119119", "Büchlberg", null], + ["092750120120", "Eging a.See, M", null], + ["092750121121", "Fürstenstein", null], + ["092750122122", "Fürstenzell, M", null], + ["092750124124", "Bad Griesbach i.Rottal, St", null], + ["092750125125", "Haarbach", null], + ["092750126126", "Hauzenberg, St", null], + ["092750127127", "Hofkirchen, M", null], + ["092750128128", "Hutthurm, M", null], + ["092750130130", "Kirchham", null], + ["092750131131", "Kößlarn, M", null], + ["092750133133", "Neuburg a.Inn", null], + ["092750134134", "Neuhaus a.Inn", null], + ["092750135135", "Neukirchen vorm Wald", null], + ["092750137137", "Obernzell, M", null], + ["092750138138", "Ortenburg, M", null], + ["092750141141", "Pocking, St", null], + ["092750144144", "Ruderting", null], + ["092750145145", "Ruhstorf a.d.Rott, M", null], + ["092750146146", "Salzweg", null], + ["092750148148", "Sonnen", null], + ["092750149149", "Tettenweis", null], + ["092750150150", "Thyrnau", null], + ["092750151151", "Tiefenbach", null], + ["092750153153", "Untergriesbach, M", null], + ["092750154154", "Vilshofen an der Donau, St", null], + ["092750156156", "Wegscheid, M", null], + ["092750159159", "Windorf, M", null], + ["092755229152", "Tittling, M", null], + ["092755229160", "Witzmannsberg", null], + ["092755232112", "Aidenbach, M", null], + ["092755232117", "Beutelsbach", null], + ["092755234132", "Malching", null], + ["092755234143", "Rotthalmünster, M", null], + ["092760113113", "Arnbruck", null], + ["092760115115", "Bayerisch Eisenstein", null], + ["092760116116", "Bischofsmais", null], + ["092760117117", "Bodenmais, M", null], + ["092760118118", "Böbrach", null], + ["092760120120", "Drachselsried", null], + ["092760121121", "Frauenau", null], + ["092760122122", "Geiersthal", null], + ["092760126126", "Kirchberg i.Wald", null], + ["092760127127", "Kirchdorf i.Wald", null], + ["092760128128", "Kollnburg", null], + ["092760129129", "Langdorf", null], + ["092760130130", "Lindberg", null], + ["092760134134", "Patersdorf", null], + ["092760135135", "Prackenbach", null], + ["092760138138", "Regen, St", null], + ["092760139139", "Rinchnach", null], + ["092760143143", "Teisnach, M", null], + ["092760144144", "Viechtach, St", null], + ["092760148148", "Zwiesel, St", null], + ["092765238111", "Achslach", null], + ["092765238123", "Gotteszell", null], + ["092765238142", "Ruhmannsfelden, M", null], + ["092765238146", "Zachenberg", null], + ["092770111111", "Arnstorf, M", null], + ["092770114114", "Dietersburg", null], + ["092770116116", "Eggenfelden, St", null], + ["092770117117", "Egglham", null], + ["092770121121", "Gangkofen, M", null], + ["092770124124", "Hebertsfelden", null], + ["092770126126", "Johanniskirchen", null], + ["092770127127", "Julbach", null], + ["092770128128", "Kirchdorf a.Inn", null], + ["092770134134", "Mitterskirchen", null], + ["092770138138", "Pfarrkirchen, St", null], + ["092770139139", "Postmünster", null], + ["092770142142", "Roßbach", null], + ["092770144144", "Schönau", null], + ["092770145145", "Simbach a.Inn, St", null], + ["092770149149", "Triftern, M", null], + ["092770151151", "Unterdietfurt", null], + ["092770152152", "Wittibreut", null], + ["092770153153", "Wurmannsquick, M", null], + ["092770154154", "Zeilarn", null], + ["092775239119", "Falkenberg", null], + ["092775239131", "Malgersdorf", null], + ["092775239141", "Rimbach", null], + ["092775240122", "Geratskirchen", null], + ["092775240133", "Massing, M", null], + ["092775241112", "Bayerbach", null], + ["092775241113", "Bad Birnbach, M", null], + ["092775243140", "Reut", null], + ["092775243148", "Tann, M", null], + ["092775244118", "Ering", null], + ["092775244147", "Stubenberg", null], + ["092780118118", "Bogen, St", null], + ["092780121121", "Feldkirchen", null], + ["092780123123", "Geiselhöring, St", null], + ["092780129129", "Haibach", null], + ["092780141141", "Kirchroth", null], + ["092780143143", "Konzell", null], + ["092780144144", "Laberweinting", null], + ["092780146146", "Leiblfing", null], + ["092780148148", "Mallersdorf-Pfaffenberg, M", null], + ["092780167167", "Oberschneiding", null], + ["092780170170", "Parkstetten", null], + ["092780178178", "Rattenberg", null], + ["092780184184", "Sankt Englmar", null], + ["092780190190", "Steinach", null], + ["092780197197", "Wiesenfelden", null], + ["092785246147", "Loitzendorf", null], + ["092785246179", "Rattiszell", null], + ["092785246189", "Stallwang", null], + ["092785248116", "Ascha", null], + ["092785248120", "Falkenfels", null], + ["092785248134", "Haselbach", null], + ["092785248151", "Mitterfels, M", null], + ["092785249139", "Hunderdorf", null], + ["092785249154", "Neukirchen", null], + ["092785249198", "Windberg", null], + ["092785250112", "Aholfing", null], + ["092785250117", "Atting", null], + ["092785250172", "Perkam", null], + ["092785250177", "Rain", null], + ["092785252149", "Mariaposching", null], + ["092785252159", "Niederwinkling", null], + ["092785252171", "Perasdorf", null], + ["092785252187", "Schwarzach, M", null], + ["092785256113", "Aiterhofen", null], + ["092785256182", "Salching", null], + ["092785257140", "Irlbach", null], + ["092785257192", "Straßkirchen", null], + ["092790112112", "Dingolfing, St", null], + ["092790113113", "Eichendorf, M", null], + ["092790115115", "Frontenhausen, M", null], + ["092790122122", "Landau a.d.Isar, St", null], + ["092790124124", "Loiching", null], + ["092790126126", "Marklkofen", null], + ["092790127127", "Mengkofen", null], + ["092790128128", "Moosthenning", null], + ["092790130130", "Niederviehbach", null], + ["092790132132", "Pilsting, M", null], + ["092790134134", "Reisbach, M", null], + ["092790135135", "Simbach, M", null], + ["092790137137", "Wallersdorf, M", null], + ["092795208116", "Gottfrieding", null], + ["092795208125", "Mamming", null], + ["093610000000", "Amberg", null], + ["093620000000", "Regensburg", null], + ["093630000000", "Weiden i.d.OPf.", null], + ["093710111111", "Ammerthal", null], + ["093710113113", "Auerbach i.d.OPf., St", null], + ["093710118118", "Ebermannsdorf", null], + ["093710119119", "Edelsfeld", null], + ["093710120120", "Ensdorf", null], + ["093710121121", "Freihung, M", null], + ["093710122122", "Freudenberg", null], + ["093710127127", "Hirschau, St", null], + ["093710129129", "Hohenburg, M", null], + ["093710132132", "Kastl, M", null], + ["093710136136", "Kümmersbruck", null], + ["093710144144", "Poppenricht", null], + ["093710146146", "Rieden, M", null], + ["093710148148", "Schmidmühlen, M", null], + ["093710150150", "Schnaittenbach, St", null], + ["093710151151", "Sulzbach-Rosenberg, St", null], + ["093710154154", "Ursensollen", null], + ["093710156156", "Vilseck, St", null], + ["093715301123", "Gebenbach", null], + ["093715301126", "Hahnbach, M", null], + ["093715302128", "Hirschbach", null], + ["093715302135", "Königstein, M", null], + ["093715303140", "Etzelwang", null], + ["093715303141", "Neukirchen b.Sulzbach-Rosenberg", null], + ["093715303157", "Weigendorf", null], + ["093715304116", "Birgland", null], + ["093715304131", "Illschwang", null], + ["093719452452", "Eichen", null], + ["093720112112", "Arnschwang", null], + ["093720113113", "Arrach", null], + ["093720115115", "Blaibach", null], + ["093720116116", "Cham, St", null], + ["093720117117", "Chamerau", null], + ["093720124124", "Eschlkam, M", null], + ["093720126126", "Furth im Wald, St", null], + ["093720130130", "Grafenwiesen", null], + ["093720135135", "Hohenwarth", null], + ["093720137137", "Bad Kötzting, St", null], + ["093720138138", "Lam, M", null], + ["093720143143", "Miltach", null], + ["093720144144", "Neukirchen b.Hl.Blut, M", null], + ["093720146146", "Pemfling", null], + ["093720151151", "Rimbach", null], + ["093720153153", "Roding, St", null], + ["093720154154", "Rötz, St", null], + ["093720155155", "Runding", null], + ["093720157157", "Schönthal", null], + ["093720158158", "Schorndorf", null], + ["093720164164", "Traitsching", null], + ["093720168168", "Waffenbrunn", null], + ["093720171171", "Waldmünchen, St", null], + ["093720175175", "Willmering", null], + ["093720177177", "Zandt", null], + ["093720178178", "Lohberg", null], + ["093725308163", "Tiefenbach", null], + ["093725308165", "Treffelstein", null], + ["093725310147", "Pösing", null], + ["093725310161", "Stamsried, M", null], + ["093725312128", "Gleißenberg", null], + ["093725312174", "Weiding", null], + ["093725313149", "Reichenbach", null], + ["093725313170", "Walderbach", null], + ["093725317167", "Zell", null], + ["093725317169", "Wald", null], + ["093725318125", "Falkenstein, M", null], + ["093725318142", "Michelsneukirchen", null], + ["093725318150", "Rettenbach", null], + ["093730112112", "Berching, St", null], + ["093730113113", "Berg b.Neumarkt i.d.OPf.", null], + ["093730115115", "Breitenbrunn, M", null], + ["093730119119", "Deining", null], + ["093730121121", "Dietfurt a.d.Altmühl, St", null], + ["093730126126", "Freystadt, St", null], + ["093730134134", "Hohenfels, M", null], + ["093730140140", "Lauterhofen, M", null], + ["093730143143", "Lupburg, M", null], + ["093730146146", "Mühlhausen", null], + ["093730147147", "Neumarkt i.d.OPf., GKSt", null], + ["093730151151", "Parsberg, St", null], + ["093730155155", "Postbauer-Heng, M", null], + ["093730156156", "Pyrbaum, M", null], + ["093730160160", "Seubersdorf i.d.OPf.", null], + ["093730167167", "Velburg, St", null], + ["093735321114", "Berngau", null], + ["093735321153", "Pilsach", null], + ["093735321159", "Sengenthal", null], + ["093740111111", "Altenstadt a.d.Waldnaab", null], + ["093740118118", "Eslarn, M", null], + ["093740121121", "Floß, M", null], + ["093740122122", "Flossenbürg", null], + ["093740124124", "Grafenwöhr, St", null], + ["093740133133", "Luhe-Wildenau, M", null], + ["093740134134", "Mantel, M", null], + ["093740137137", "Moosbach, M", null], + ["093740139139", "Neustadt a.d.Waldnaab, St", null], + ["093740162162", "Vohenstrauß, St", null], + ["093740164164", "Waidhaus, M", null], + ["093740165165", "Waldthurn, M", null], + ["093740168168", "Windischeschenbach, St", null], + ["093745323128", "Kirchendemenreuth", null], + ["093745323144", "Parkstein, M", null], + ["093745323150", "Püchersreuth", null], + ["093745323158", "Störnstein", null], + ["093745323160", "Theisseil", null], + ["093745324148", "Trabitz", null], + ["093745324149", "Pressath, St", null], + ["093745324156", "Schwarzenbach", null], + ["093745325119", "Etzenricht", null], + ["093745325131", "Kohlberg, M", null], + ["093745325166", "Weiherhammer", null], + ["093745326129", "Kirchenthumbach, M", null], + ["093745326155", "Schlammersdorf", null], + ["093745326163", "Vorbach", null], + ["093745327117", "Eschenbach i.d.OPf., St", null], + ["093745327140", "Neustadt am Kulm, St", null], + ["093745327157", "Speinshart", null], + ["093745329127", "Irchenrieth", null], + ["093745329146", "Pirk", null], + ["093745329154", "Schirmitz", null], + ["093745329170", "Bechtsrieth", null], + ["093745330132", "Leuchtenberg, M", null], + ["093745330159", "Tännesberg, M", null], + ["093745331123", "Georgenberg", null], + ["093745331147", "Pleystein, St", null], + ["093749451451", "Heinersreuther Forst", null], + ["093749452452", "Manteler Forst", null], + ["093749458458", "Speinsharter Forst", null], + ["093750117117", "Barbing", null], + ["093750118118", "Beratzhausen, M", null], + ["093750119119", "Bernhardswald", null], + ["093750143143", "Hagelstadt", null], + ["093750148148", "Hemau, St", null], + ["093750161161", "Köfering", null], + ["093750165165", "Lappersdorf, M", null], + ["093750170170", "Mintraching", null], + ["093750174174", "Neutraubling, St", null], + ["093750175175", "Nittendorf, M", null], + ["093750179179", "Obertraubling", null], + ["093750180180", "Pentling", null], + ["093750181181", "Pettendorf", null], + ["093750183183", "Pfatter", null], + ["093750190190", "Regenstauf, M", null], + ["093750196196", "Schierling, M", null], + ["093750199199", "Sinzing", null], + ["093750204204", "Tegernheim", null], + ["093750205205", "Thalmassing", null], + ["093750208208", "Wenzenbach", null], + ["093750209209", "Wiesent", null], + ["093750213213", "Zeitlarn", null], + ["093755332131", "Duggendorf", null], + ["093755332153", "Holzheim a.Forst", null], + ["093755332156", "Kallmünz, M", null], + ["093755333122", "Brunn", null], + ["093755333127", "Deuerling", null], + ["093755333162", "Laaber, M", null], + ["093755334184", "Pielenhofen", null], + ["093755334211", "Wolfsegg", null], + ["093755335114", "Altenthann", null], + ["093755335116", "Bach a.d.Donau", null], + ["093755335130", "Donaustauf, M", null], + ["093755336120", "Brennberg", null], + ["093755336210", "Wörth a.d.Donau, St", null], + ["093755337113", "Alteglofsheim", null], + ["093755337182", "Pfakofen", null], + ["093755338115", "Aufhausen", null], + ["093755338171", "Mötzing", null], + ["093755338191", "Riekofen", null], + ["093755338201", "Sünching", null], + ["093759451451", "Forstmühler Forst", null], + ["093759452452", "Kreuther Forst", null], + ["093760116116", "Bodenwöhr", null], + ["093760117117", "Bruck i.d.OPf., M", null], + ["093760119119", "Burglengenfeld, St", null], + ["093760125125", "Fensterbach", null], + ["093760141141", "Maxhütte-Haidhof, St", null], + ["093760147147", "Neunburg vorm Wald, St", null], + ["093760149149", "Nittenau, St", null], + ["093760150150", "Wernberg-Köblitz, M", null], + ["093760151151", "Oberviechtach, St", null], + ["093760159159", "Schmidgaden", null], + ["093760161161", "Schwandorf, GKSt", null], + ["093760170170", "Teublitz, St", null], + ["093765339131", "Gleiritsch", null], + ["093765339148", "Niedermurach", null], + ["093765339171", "Teunz", null], + ["093765339178", "Winklarn, M", null], + ["093765341112", "Altendorf", null], + ["093765341133", "Guteneck", null], + ["093765341144", "Nabburg, St", null], + ["093765342162", "Schwarzach b.Nabburg", null], + ["093765342163", "Schwarzenfeld, M", null], + ["093765342169", "Stulln", null], + ["093765343153", "Pfreimd, St", null], + ["093765343173", "Trausnitz", null], + ["093765344160", "Schönsee, St", null], + ["093765344167", "Stadlern", null], + ["093765344176", "Weiding", null], + ["093765345122", "Dieterskirchen", null], + ["093765345146", "Neukirchen-Balbini, M", null], + ["093765345164", "Schwarzhofen, M", null], + ["093765345172", "Thanstein", null], + ["093765346168", "Steinberg am See", null], + ["093765346175", "Wackersdorf", null], + ["093769455455", "Wolferlohe", null], + ["093770112112", "Bärnau, St", null], + ["093770116116", "Erbendorf, St", null], + ["093770118118", "Friedenfels", null], + ["093770119119", "Fuchsmühl, M", null], + ["093770127127", "Immenreuth", null], + ["093770131131", "Konnersreuth, M", null], + ["093770133133", "Kulmain", null], + ["093770139139", "Mähring, M", null], + ["093770142142", "Bad Neualbenreuth, M", null], + ["093770146146", "Plößberg, M", null], + ["093770154154", "Tirschenreuth, St", null], + ["093770157157", "Waldershof, St", null], + ["093770158158", "Waldsassen, St", null], + ["093775347137", "Leonberg", null], + ["093775347141", "Mitterteich, St", null], + ["093775347145", "Pechbrunn", null], + ["093775348128", "Kastl", null], + ["093775348129", "Kemnath, St", null], + ["093775349113", "Brand", null], + ["093775349115", "Ebnath", null], + ["093775349143", "Neusorg", null], + ["093775349148", "Pullenreuth", null], + ["093775350132", "Krummennaab", null], + ["093775350149", "Reuth b.Erbendorf", null], + ["093775351117", "Falkenberg, M", null], + ["093775351159", "Wiesau, M", null], + ["094610000000", "Bamberg", null], + ["094620000000", "Bayreuth", null], + ["094630000000", "Coburg", null], + ["094640000000", "Hof", null], + ["094710111111", "Altendorf", null], + ["094710117117", "Bischberg", null], + ["094710119119", "Breitengüßbach", null], + ["094710123123", "Buttenheim, M", null], + ["094710131131", "Frensdorf", null], + ["094710137137", "Gundelsheim", null], + ["094710140140", "Hallstadt, St", null], + ["094710142142", "Heiligenstadt i.OFr., M", null], + ["094710145145", "Hirschaid, M", null], + ["094710150150", "Kemmern", null], + ["094710155155", "Litzendorf", null], + ["094710159159", "Memmelsdorf", null], + ["094710165165", "Oberhaid", null], + ["094710169169", "Pettstadt", null], + ["094710172172", "Pommersfelden", null], + ["094710174174", "Rattelsdorf, M", null], + ["094710185185", "Scheßlitz, St", null], + ["094710191191", "Stegaurach", null], + ["094710195195", "Strullendorf", null], + ["094710207207", "Viereth-Trunstadt", null], + ["094710208208", "Walsdorf", null], + ["094710214214", "Zapfendorf, M", null], + ["094710220220", "Schlüsselfeld, St", null], + ["094715401115", "Baunach, St", null], + ["094715401133", "Gerach", null], + ["094715401152", "Lauter", null], + ["094715401175", "Reckendorf", null], + ["094715403151", "Königsfeld", null], + ["094715403189", "Stadelhofen", null], + ["094715403209", "Wattendorf", null], + ["094715407122", "Burgwindheim, M", null], + ["094715407128", "Ebrach, M", null], + ["094715408120", "Burgebrach, M", null], + ["094715408186", "Schönbrunn i.Steigerwald", null], + ["094715445154", "Lisberg", null], + ["094715445173", "Priesendorf", null], + ["094719452452", "Ebracher Forst", null], + ["094719453453", "Eichwald", null], + ["094719454454", "Geisberger Forst", null], + ["094719455455", "Hauptsmoor", null], + ["094719456456", "Koppenwinder Forst", null], + ["094719457457", "Lindach", null], + ["094719459459", "Semberg", null], + ["094719460460", "Steinachsrangen", null], + ["094719461461", "Winkelhofer Forst", null], + ["094719462462", "Zückshuter Forst", null], + ["094720111111", "Ahorntal", null], + ["094720116116", "Bad Berneck i.Fichtelgebirge, St", null], + ["094720119119", "Bindlach", null], + ["094720121121", "Bischofsgrün", null], + ["094720131131", "Eckersdorf", null], + ["094720138138", "Fichtelberg", null], + ["094720139139", "Gefrees, St", null], + ["094720143143", "Goldkronach, St", null], + ["094720150150", "Heinersreuth", null], + ["094720164164", "Mehlmeisel", null], + ["094720175175", "Pegnitz, St", null], + ["094720179179", "Pottenstein, St", null], + ["094720190190", "Speichersdorf", null], + ["094720197197", "Waischenfeld, St", null], + ["094720198198", "Warmensteinach", null], + ["094725412115", "Aufseß", null], + ["094725412154", "Hollfeld, St", null], + ["094725412176", "Plankenfels", null], + ["094725413141", "Glashütten", null], + ["094725413167", "Mistelgau", null], + ["094725414140", "Gesees", null], + ["094725414155", "Hummeltal", null], + ["094725414166", "Mistelbach", null], + ["094725415133", "Emtmannsberg", null], + ["094725415156", "Kirchenpingarten", null], + ["094725415188", "Seybothenreuth", null], + ["094725415199", "Weidenberg, M", null], + ["094725416127", "Creußen, St", null], + ["094725416146", "Haag", null], + ["094725416180", "Prebitz", null], + ["094725416184", "Schnabelwaid, M", null], + ["094725417118", "Betzenstein, St", null], + ["094725417177", "Plech, M", null], + ["094729451451", "Bischofsgrüner Forst", null], + ["094729453453", "Fichtelberg", null], + ["094729454454", "Forst Neustädtlein a.Forst", null], + ["094729456456", "Glashüttener Forst", null], + ["094729458458", "Heinersreuther Forst", null], + ["094729463463", "Neubauer Forst-Nord", null], + ["094729464464", "Prüll", null], + ["094729468468", "Veldensteinerforst", null], + ["094729469469", "Waidacher Forst", null], + ["094729470470", "Warmensteinacher Forst-Nord", null], + ["094730112112", "Ahorn", null], + ["094730120120", "Dörfles-Esbach", null], + ["094730121121", "Ebersdorf b.Coburg", null], + ["094730132132", "Großheirath", null], + ["094730138138", "Itzgrund", null], + ["094730141141", "Lautertal", null], + ["094730144144", "Meeder", null], + ["094730151151", "Neustadt b.Coburg, GKSt", null], + ["094730158158", "Bad Rodach, St", null], + ["094730159159", "Rödental, St", null], + ["094730165165", "Seßlach, St", null], + ["094730166166", "Sonnefeld", null], + ["094730170170", "Untersiemau", null], + ["094730174174", "Weidhausen b.Coburg", null], + ["094730175175", "Weitramsdorf", null], + ["094735418134", "Grub a.Forst", null], + ["094735418153", "Niederfüllbach", null], + ["094739452452", "Callenberger Forst-West", null], + ["094739453453", "Gellnhausen", null], + ["094739454454", "Köllnholz", null], + ["094740123123", "Eggolsheim, M", null], + ["094740124124", "Egloffstein, M", null], + ["094740126126", "Forchheim, GKSt", null], + ["094740129129", "Gößweinstein, M", null], + ["094740133133", "Hallerndorf", null], + ["094740134134", "Hausen", null], + ["094740135135", "Heroldsbach", null], + ["094740140140", "Igensdorf, M", null], + ["094740146146", "Langensendelbach", null], + ["094740154154", "Neunkirchen a.Brand, M", null], + ["094740156156", "Obertrubach", null], + ["094740161161", "Pretzfeld, M", null], + ["094740176176", "Wiesenttal, M", null], + ["094745420121", "Ebermannstadt, St", null], + ["094745420168", "Unterleinleiter", null], + ["094745422145", "Kunreuth", null], + ["094745422158", "Pinzberg", null], + ["094745422175", "Wiesenthau", null], + ["094745423143", "Kirchehrenbach", null], + ["094745423147", "Leutenbach", null], + ["094745423171", "Weilersbach", null], + ["094745425122", "Effeltrich", null], + ["094745425160", "Poxdorf", null], + ["094745426119", "Dormitz", null], + ["094745426137", "Hetzles", null], + ["094745426144", "Kleinsendelbach", null], + ["094745427132", "Gräfenberg, St", null], + ["094745427138", "Hiltpoltstein, M", null], + ["094745427173", "Weißenohe", null], + ["094750112112", "Bad Steben, M", null], + ["094750113113", "Berg", null], + ["094750120120", "Döhlau", null], + ["094750128128", "Geroldsgrün", null], + ["094750136136", "Helmbrechts, St", null], + ["094750141141", "Köditz", null], + ["094750142142", "Konradsreuth", null], + ["094750154154", "Münchberg, St", null], + ["094750156156", "Naila, St", null], + ["094750158158", "Oberkotzau, M", null], + ["094750161161", "Regnitzlosau", null], + ["094750162162", "Rehau, St", null], + ["094750168168", "Schwarzenbach a.d.Saale, St", null], + ["094750169169", "Schwarzenbach a.Wald, St", null], + ["094750171171", "Selbitz, St", null], + ["094750175175", "Stammbach, M", null], + ["094750189189", "Zell im Fichtelgebirge, M", null], + ["094755428137", "Issigau", null], + ["094755428146", "Lichtenberg, St", null], + ["094755430123", "Feilitzsch", null], + ["094755430127", "Gattendorf", null], + ["094755430181", "Töpen", null], + ["094755430182", "Trogen", null], + ["094755431145", "Leupoldsgrün", null], + ["094755431165", "Schauenstein, St", null], + ["094755432174", "Sparneck, M", null], + ["094755432184", "Weißdorf", null], + ["094759451451", "Forst Schwarzenbach a.Wald", null], + ["094759452452", "Gerlaser Forst", null], + ["094759453453", "Geroldsgrüner Forst", null], + ["094759454454", "Martinlamitzer Forst-Nord", null], + ["094760145145", "Kronach, St", null], + ["094760146146", "Küps, M", null], + ["094760152152", "Ludwigsstadt, St", null], + ["094760159159", "Nordhalben, M", null], + ["094760164164", "Pressig, M", null], + ["094760175175", "Steinbach a.Wald", null], + ["094760177177", "Steinwiesen, M", null], + ["094760178178", "Stockheim", null], + ["094760179179", "Tettau, M", null], + ["094760183183", "Marktrodach, M", null], + ["094760184184", "Wallenfels, St", null], + ["094760185185", "Weißenbrunn", null], + ["094760189189", "Wilhelmsthal", null], + ["094765433166", "Reichenbach", null], + ["094765433180", "Teuschnitz, St", null], + ["094765433182", "Tschirn", null], + ["094765434154", "Mitwitz, M", null], + ["094765434171", "Schneckenlohe", null], + ["094769451451", "Birnbaum", null], + ["094769453453", "Langenbacher Forst", null], + ["094770121121", "Himmelkron", null], + ["094770128128", "Kulmbach, GKSt", null], + ["094770136136", "Mainleus, M", null], + ["094770139139", "Marktschorgast, M", null], + ["094770142142", "Neudrossenfeld", null], + ["094770143143", "Neuenmarkt", null], + ["094770148148", "Presseck, M", null], + ["094770157157", "Thurnau, M", null], + ["094770163163", "Wirsberg, M", null], + ["094775435151", "Rugendorf", null], + ["094775435156", "Stadtsteinach, St", null], + ["094775436117", "Grafengehaig, M", null], + ["094775436138", "Marktleugast, M", null], + ["094775437118", "Guttenberg", null], + ["094775437129", "Kupferberg, St", null], + ["094775437135", "Ludwigschorgast, M", null], + ["094775437159", "Untersteinach", null], + ["094775438124", "Kasendorf, M", null], + ["094775438164", "Wonsees, M", null], + ["094775439119", "Harsdorf", null], + ["094775439127", "Ködnitz", null], + ["094775439158", "Trebgast", null], + ["094780111111", "Altenkunstadt", null], + ["094780116116", "Burgkunstadt, St", null], + ["094780120120", "Ebensfeld, M", null], + ["094780139139", "Lichtenfels, St", null], + ["094780145145", "Michelau i.OFr.", null], + ["094780165165", "Bad Staffelstein, St", null], + ["094780176176", "Weismain, St", null], + ["094785441143", "Marktgraitz, M", null], + ["094785441155", "Redwitz a.d.Rodach", null], + ["094785446127", "Hochstadt a.Main", null], + ["094785446144", "Marktzeuln, M", null], + ["094789451451", "Breitengüßbacher Forst", null], + ["094789453453", "Neuensorger Forst", null], + ["094790112112", "Arzberg, St", null], + ["094790129129", "Kirchenlamitz, St", null], + ["094790135135", "Marktleuthen, St", null], + ["094790136136", "Marktredwitz, GKSt", null], + ["094790145145", "Röslau", null], + ["094790150150", "Schönwald, St", null], + ["094790152152", "Selb, GKSt", null], + ["094790166166", "Weißenstadt, St", null], + ["094790169169", "Wunsiedel, St", null], + ["094795442126", "Höchstädt i.Fichtelgebirge", null], + ["094795442158", "Thiersheim, M", null], + ["094795442159", "Thierstein, M", null], + ["094795443127", "Hohenberg a.d.Eger, St", null], + ["094795443147", "Schirnding, M", null], + ["094795444111", "Bad Alexandersbad", null], + ["094795444138", "Nagel", null], + ["094795444161", "Tröstau", null], + ["094799453453", "Kaiserhammer Forst-Ost", null], + ["094799455455", "Martinlamitzer Forst-Süd", null], + ["094799456456", "Meierhöfer Seite", null], + ["094799457457", "Neubauer Forst-Süd", null], + ["094799459459", "Tröstauer Forst-Ost", null], + ["094799460460", "Tröstauer Forst-West", null], + ["094799461461", "Vordorfer Forst", null], + ["094799462462", "Weißenstadter Forst-Nord", null], + ["094799463463", "Weißenstadter Forst-Süd", null], + ["095610000000", "Ansbach", null], + ["095620000000", "Erlangen", null], + ["095630000000", "Fürth", null], + ["095640000000", "Nürnberg", null], + ["095650000000", "Schwabach", null], + ["095710113113", "Arberg, M", null], + ["095710114114", "Aurach", null], + ["095710115115", "Bechhofen, M", null], + ["095710127127", "Burgoberbach", null], + ["095710130130", "Colmberg, M", null], + ["095710135135", "Dietenhofen, M", null], + ["095710136136", "Dinkelsbühl, GKSt", null], + ["095710139139", "Dürrwangen, M", null], + ["095710145145", "Feuchtwangen, St", null], + ["095710146146", "Flachslanden, M", null], + ["095710165165", "Heilsbronn, St", null], + ["095710166166", "Herrieden, St", null], + ["095710170170", "Langfurth", null], + ["095710171171", "Lehrberg, M", null], + ["095710174174", "Leutershausen, St", null], + ["095710175175", "Lichtenau, M", null], + ["095710177177", "Merkendorf, St", null], + ["095710180180", "Neuendettelsau", null], + ["095710183183", "Oberdachstetten", null], + ["095710190190", "Petersaurach", null], + ["095710193193", "Rothenburg ob der Tauber, GKSt", null], + ["095710196196", "Sachsen b.Ansbach", null], + ["095710199199", "Schnelldorf", null], + ["095710200200", "Schopfloch, M", null], + ["095710214214", "Wassertrüdingen, St", null], + ["095710226226", "Windsbach, St", null], + ["095715501111", "Adelshofen", null], + ["095715501152", "Gebsattel", null], + ["095715501155", "Geslau", null], + ["095715501169", "Insingen", null], + ["095715501181", "Neusitz", null], + ["095715501188", "Ohrenbach", null], + ["095715501205", "Steinsfeld", null], + ["095715501225", "Windelsbach", null], + ["095715502125", "Buch a.Wald", null], + ["095715502134", "Diebach", null], + ["095715502137", "Dombühl, M", null], + ["095715502198", "Schillingsfürst, St", null], + ["095715502222", "Wettringen", null], + ["095715502228", "Wörnitz", null], + ["095715504122", "Bruckberg", null], + ["095715504194", "Rügland", null], + ["095715504217", "Weihenzell", null], + ["095715506189", "Ornbau, St", null], + ["095715506216", "Weidenbach, M", null], + ["095715507128", "Burk", null], + ["095715507132", "Dentlein a.Forst, M", null], + ["095715507223", "Wieseth", null], + ["095715508179", "Mönchsroth", null], + ["095715508218", "Weiltingen, M", null], + ["095715508224", "Wilburgstetten", null], + ["095715509141", "Ehingen", null], + ["095715509154", "Gerolfingen", null], + ["095715509192", "Röckingen", null], + ["095715509208", "Unterschwaningen", null], + ["095715509227", "Wittelshofen", null], + ["095715538178", "Mitteleschenbach", null], + ["095715538229", "Wolframs-Eschenbach, St", null], + ["095719451451", "Unterer Wald", null], + ["095720111111", "Adelsdorf", null], + ["095720115115", "Baiersdorf, St", null], + ["095720119119", "Bubenreuth", null], + ["095720121121", "Eckental, M", null], + ["095720130130", "Hemhofen", null], + ["095720131131", "Heroldsberg, M", null], + ["095720132132", "Herzogenaurach, St", null], + ["095720135135", "Höchstadt a.d.Aisch, St", null], + ["095720137137", "Kalchreuth", null], + ["095720142142", "Möhrendorf", null], + ["095720149149", "Röttenbach", null], + ["095720160160", "Wachenroth, M", null], + ["095720164164", "Weisendorf, M", null], + ["095725510126", "Gremsdorf", null], + ["095725510139", "Lonnerstadt, M", null], + ["095725510143", "Mühlhausen, M", null], + ["095725510159", "Vestenbergsgreuth, M", null], + ["095725512114", "Aurachtal", null], + ["095725512147", "Oberreichenbach", null], + ["095725514120", "Buckenhof", null], + ["095725514141", "Marloffstein", null], + ["095725514154", "Spardorf", null], + ["095725514158", "Uttenreuth", null], + ["095725539127", "Großenseebach", null], + ["095725539133", "Heßdorf", null], + ["095729451451", "Birkach", null], + ["095729452452", "Buckenhofer Forst", null], + ["095729453453", "Dormitzer Forst", null], + ["095729454454", "Erlenstegener Forst", null], + ["095729455455", "Forst Tennenlohe", null], + ["095729456456", "Geschaidt", null], + ["095729457457", "Kalchreuther Forst", null], + ["095729458458", "Kraftshofer Forst", null], + ["095729459459", "Mark", null], + ["095729460460", "Neunhofer Forst", null], + ["095730111111", "Ammerndorf, M", null], + ["095730114114", "Cadolzburg, M", null], + ["095730115115", "Großhabersdorf", null], + ["095730120120", "Langenzenn, St", null], + ["095730122122", "Oberasbach, St", null], + ["095730124124", "Puschendorf", null], + ["095730125125", "Roßtal, M", null], + ["095730127127", "Stein, St", null], + ["095730133133", "Wilhermsdorf, M", null], + ["095730134134", "Zirndorf, St", null], + ["095735517126", "Seukendorf", null], + ["095735517130", "Veitsbronn", null], + ["095735540123", "Obermichelbach", null], + ["095735540129", "Tuchenbach", null], + ["095740112112", "Altdorf b.Nürnberg, St", null], + ["095740117117", "Burgthann", null], + ["095740123123", "Feucht, M", null], + ["095740132132", "Hersbruck, St", null], + ["095740135135", "Kirchensittenbach", null], + ["095740138138", "Lauf a.d.Pegnitz, St", null], + ["095740139139", "Leinburg", null], + ["095740140140", "Neuhaus a.d.Pegnitz, M", null], + ["095740141141", "Neunkirchen a.Sand", null], + ["095740146146", "Ottensoos", null], + ["095740147147", "Pommelsbrunn", null], + ["095740150150", "Reichenschwand", null], + ["095740152152", "Röthenbach a.d.Pegnitz, St", null], + ["095740154154", "Rückersdorf", null], + ["095740155155", "Schnaittach, M", null], + ["095740156156", "Schwaig b.Nürnberg", null], + ["095740157157", "Schwarzenbruck", null], + ["095740158158", "Simmelsdorf", null], + ["095740164164", "Winkelhaid", null], + ["095745527129", "Hartenstein", null], + ["095745527160", "Velden, St", null], + ["095745527161", "Vorra", null], + ["095745528111", "Alfeld", null], + ["095745528128", "Happurg", null], + ["095745529120", "Engelthal", null], + ["095745529131", "Henfenfeld", null], + ["095745529145", "Offenhausen", null], + ["095749451451", "Behringersdorfer Forst", null], + ["095749452452", "Brunn", null], + ["095749453453", "Engelthaler Forst", null], + ["095749454454", "Feuchter Forst", null], + ["095749455455", "Fischbach", null], + ["095749456456", "Forsthof", null], + ["095749457457", "Günthersbühler Forst", null], + ["095749458458", "Haimendorfer Forst", null], + ["095749460460", "Laufamholzer Forst", null], + ["095749461461", "Leinburg", null], + ["095749462462", "Rückersdorfer Forst", null], + ["095749463463", "Schönberg", null], + ["095749464464", "Winkelhaid", null], + ["095749465465", "Zerzabelshofer Forst", null], + ["095750112112", "Bad Windsheim, St", null], + ["095750116116", "Burghaslach, M", null], + ["095750119119", "Dietersheim", null], + ["095750121121", "Emskirchen, M", null], + ["095750135135", "Ipsheim, M", null], + ["095750145145", "Markt Erlbach, M", null], + ["095750153153", "Neustadt a.d.Aisch, St", null], + ["095750156156", "Obernzenn, M", null], + ["095755518138", "Langenfeld", null], + ["095755518144", "Markt Bibart, M", null], + ["095755518147", "Markt Taschendorf, M", null], + ["095755518157", "Oberscheinfeld, M", null], + ["095755518161", "Scheinfeld, St", null], + ["095755518165", "Sugenheim, M", null], + ["095755519122", "Ergersheim", null], + ["095755519127", "Gollhofen", null], + ["095755519130", "Hemmersheim", null], + ["095755519134", "Ippesheim, M", null], + ["095755519146", "Markt Nordheim, M", null], + ["095755519155", "Oberickelsheim", null], + ["095755519163", "Simmershofen", null], + ["095755519168", "Uffenheim, St", null], + ["095755519179", "Weigenheim", null], + ["095755520129", "Hagenbüchach", null], + ["095755520181", "Wilhelmsdorf", null], + ["095755521113", "Baudenbach, M", null], + ["095755521118", "Diespeck", null], + ["095755521128", "Gutenstetten", null], + ["095755521150", "Münchsteinach", null], + ["095755522117", "Dachsbach, M", null], + ["095755522125", "Gerhardshofen", null], + ["095755522167", "Uehlfeld, M", null], + ["095755524115", "Burgbernheim, St", null], + ["095755524124", "Gallmersgarten", null], + ["095755524133", "Illesheim", null], + ["095755524143", "Marktbergel, M", null], + ["095755525152", "Neuhof a.d.Zenn, M", null], + ["095755525166", "Trautskirchen", null], + ["095759451451", "Osing", null], + ["095760111111", "Abenberg, St", null], + ["095760113113", "Allersberg, M", null], + ["095760117117", "Büchenbach", null], + ["095760121121", "Georgensgmünd", null], + ["095760122122", "Greding, St", null], + ["095760126126", "Heideck, St", null], + ["095760127127", "Hilpoltstein, St", null], + ["095760128128", "Kammerstein", null], + ["095760132132", "Schwanstetten, M", null], + ["095760137137", "Rednitzhembach", null], + ["095760141141", "Röttenbach", null], + ["095760142142", "Rohr", null], + ["095760143143", "Roth, St", null], + ["095760147147", "Spalt, St", null], + ["095760148148", "Thalmässing, M", null], + ["095760151151", "Wendelstein, M", null], + ["095769451451", "Abenberger Wald", null], + ["095769452452", "Dechenwald", null], + ["095769453453", "Forst Kleinschwarzenlohe", null], + ["095769454454", "Heidenberg", null], + ["095769455455", "Soos", null], + ["095770114114", "Muhr a.See", null], + ["095770136136", "Gunzenhausen, St", null], + ["095770148148", "Langenaltheim", null], + ["095770158158", "Pappenheim, St", null], + ["095770161161", "Pleinfeld, M", null], + ["095770162162", "Polsingen", null], + ["095770168168", "Solnhofen", null], + ["095770173173", "Treuchtlingen, St", null], + ["095770177177", "Weißenburg i.Bay., GKSt", null], + ["095775532111", "Absberg, M", null], + ["095775532138", "Haundorf", null], + ["095775532159", "Pfofeld", null], + ["095775532172", "Theilenhofen", null], + ["095775533113", "Alesheim", null], + ["095775533122", "Dittenheim", null], + ["095775533149", "Markt Berolzheim, M", null], + ["095775533150", "Meinheim", null], + ["095775534125", "Ellingen, St", null], + ["095775534127", "Ettenstatt", null], + ["095775534141", "Höttingen", null], + ["095775535115", "Bergen", null], + ["095775535120", "Burgsalach", null], + ["095775535151", "Nennslingen, M", null], + ["095775535163", "Raitenbuch", null], + ["095775536133", "Gnotzheim, M", null], + ["095775536140", "Heidenheim, M", null], + ["095775536179", "Westheim", null], + ["096610000000", "Aschaffenburg", null], + ["096620000000", "Schweinfurt", null], + ["096630000000", "Würzburg", null], + ["096710111111", "Alzenau, St", null], + ["096710112112", "Bessenbach", null], + ["096710114114", "Karlstein a.Main", null], + ["096710119119", "Geiselbach", null], + ["096710120120", "Glattbach", null], + ["096710121121", "Goldbach, M", null], + ["096710122122", "Großostheim, M", null], + ["096710124124", "Haibach", null], + ["096710130130", "Hösbach, M", null], + ["096710133133", "Johannesberg", null], + ["096710134134", "Kahl a.Main", null], + ["096710136136", "Kleinostheim", null], + ["096710139139", "Laufach", null], + ["096710140140", "Mainaschaff", null], + ["096710143143", "Mömbris, M", null], + ["096710148148", "Rothenbuch", null], + ["096710150150", "Sailauf", null], + ["096710155155", "Stockstadt a.Main, M", null], + ["096710156156", "Waldaschaff", null], + ["096710157157", "Weibersbrunn", null], + ["096715602126", "Heigenbrücken", null], + ["096715602128", "Heinrichsthal", null], + ["096715603127", "Heimbuchenthal", null], + ["096715603141", "Mespelbrunn", null], + ["096715603160", "Dammbach", null], + ["096715604113", "Blankenbach", null], + ["096715604135", "Kleinkahl", null], + ["096715604138", "Krombach", null], + ["096715604152", "Schöllkrippen, M", null], + ["096715604153", "Sommerkahl", null], + ["096715604159", "Westerngrund", null], + ["096715604162", "Wiesen", null], + ["096719451451", "Forst Hain i.Spessart", null], + ["096719453453", "Heinrichsthaler Forst", null], + ["096719456456", "Rohrbrunner Forst", null], + ["096719457457", "Rothenbucher Forst", null], + ["096719458458", "Sailaufer Forst", null], + ["096719459459", "Schöllkrippener Forst", null], + ["096719460460", "Waldaschaffer Forst", null], + ["096719461461", "Wiesener Forst", null], + ["096720112112", "Bad Bocklet, M", null], + ["096720113113", "Bad Brückenau, St", null], + ["096720114114", "Bad Kissingen, GKSt", null], + ["096720117117", "Burkardroth, M", null], + ["096720127127", "Hammelburg, St", null], + ["096720134134", "Motten", null], + ["096720135135", "Münnerstadt, St", null], + ["096720136136", "Nüdlingen", null], + ["096720139139", "Oberthulba, M", null], + ["096720140140", "Oerlenbach", null], + ["096720161161", "Wartmannsroth", null], + ["096720163163", "Wildflecken, M", null], + ["096720166166", "Zeitlofs, M", null], + ["096725606126", "Geroda, M", null], + ["096725606138", "Oberleichtersbach", null], + ["096725606145", "Riedenberg", null], + ["096725606149", "Schondra, M", null], + ["096725607121", "Elfershausen, M", null], + ["096725607124", "Fuchsstadt", null], + ["096725608111", "Aura a.d.Saale", null], + ["096725608122", "Euerdorf, M", null], + ["096725608142", "Ramsthal", null], + ["096725608155", "Sulzthal, M", null], + ["096725609131", "Maßbach, M", null], + ["096725609143", "Rannungen", null], + ["096725609157", "Thundorf i.UFr.", null], + ["096729451451", "Dreistelzer Forst", null], + ["096729454454", "Forst Detter-Süd", null], + ["096729455455", "Geiersnest-Ost", null], + ["096729456456", "Geiersnest-West", null], + ["096729457457", "Großer Auersberg", null], + ["096729458458", "Kälberberg", null], + ["096729461461", "Mottener Forst-Süd", null], + ["096729462462", "Neuwirtshauser Forst", null], + ["096729463463", "Omerz u. Roter Berg", null], + ["096729464464", "Römershager Forst-Nord", null], + ["096729465465", "Römershager Forst-Ost", null], + ["096729466466", "Roßbacher Forst", null], + ["096729468468", "Waldfensterer Forst", null], + ["096730114114", "Bad Neustadt a.d.Saale, St", null], + ["096730116116", "Bastheim", null], + ["096730117117", "Bischofsheim i.d.Rhön, St", null], + ["096730141141", "Bad Königshofen i.Grabfeld, St", null], + ["096730149149", "Oberelsbach, M", null], + ["096730162162", "Sandberg", null], + ["096735633130", "Hendungen", null], + ["096735633142", "Mellrichstadt, St", null], + ["096735633151", "Oberstreu", null], + ["096735633170", "Stockheim", null], + ["096735634113", "Aubstadt", null], + ["096735634126", "Großbardorf", null], + ["096735634131", "Herbstadt", null], + ["096735634134", "Höchheim", null], + ["096735634172", "Sulzdorf a.d.Lederhecke", null], + ["096735634173", "Sulzfeld", null], + ["096735634174", "Trappstadt, M", null], + ["096735635135", "Hohenroth", null], + ["096735635146", "Niederlauer", null], + ["096735635156", "Rödelmaier", null], + ["096735635161", "Salz", null], + ["096735635163", "Schönau a.d.Brend", null], + ["096735635171", "Strahlungen", null], + ["096735635186", "Burglauer", null], + ["096735637123", "Fladungen, St", null], + ["096735637129", "Hausen", null], + ["096735637147", "Nordheim v.d.Rhön", null], + ["096735638133", "Heustreu", null], + ["096735638136", "Hollstadt", null], + ["096735638175", "Unsleben", null], + ["096735638183", "Wollbach", null], + ["096735639153", "Ostheim v.d.Rhön, St", null], + ["096735639167", "Sondheim v.d.Rhön", null], + ["096735639182", "Willmars", null], + ["096735640127", "Großeibstadt", null], + ["096735640160", "Saal a.d.Saale, M", null], + ["096735640184", "Wülfershausen a.d.Saale", null], + ["096739451451", "Bundorfer Forst", null], + ["096739452452", "Burgwallbacher Forst", null], + ["096739453453", "Forst Schmalwasser-Nord", null], + ["096739454454", "Forst Schmalwasser-Süd", null], + ["096739455455", "Mellrichstadter Forst", null], + ["096739456456", "Steinacher Forst r.d.Saale", null], + ["096739457457", "Sulzfelder Forst", null], + ["096739458458", "Weigler", null], + ["096740133133", "Eltmann, St", null], + ["096740147147", "Haßfurt, St", null], + ["096740159159", "Oberaurach", null], + ["096740163163", "Knetzgau", null], + ["096740164164", "Königsberg i.Bay., St", null], + ["096740171171", "Maroldsweisach, M", null], + ["096740187187", "Rauhenebrach", null], + ["096740195195", "Sand a.Main", null], + ["096740210210", "Untermerzbach", null], + ["096740221221", "Zeil a.Main, St", null], + ["096745610118", "Breitbrunn", null], + ["096745610129", "Ebelsbach", null], + ["096745610160", "Kirchlauter", null], + ["096745610201", "Stettfeld", null], + ["096745611130", "Ebern, St", null], + ["096745611184", "Pfarrweisach", null], + ["096745611190", "Rentweinsdorf, M", null], + ["096745612111", "Aidhausen", null], + ["096745612120", "Bundorf", null], + ["096745612121", "Burgpreppach, M", null], + ["096745612149", "Hofheim i.UFr., St", null], + ["096745612153", "Riedbach", null], + ["096745612223", "Ermershausen", null], + ["096745613139", "Gädheim", null], + ["096745613180", "Theres", null], + ["096745613219", "Wonfurt", null], + ["096750117117", "Dettelbach, St", null], + ["096750127127", "Geiselwind, M", null], + ["096750141141", "Kitzingen, GKSt", null], + ["096750144144", "Mainbernheim, St", null], + ["096750158158", "Prichsenstadt, St", null], + ["096750165165", "Schwarzach a.Main, M", null], + ["096755614111", "Abtswind, M", null], + ["096755614116", "Castell", null], + ["096755614162", "Rüdenhausen, M", null], + ["096755614178", "Wiesentheid, M", null], + ["096755615131", "Großlangheim, M", null], + ["096755615142", "Kleinlangheim, M", null], + ["096755615177", "Wiesenbronn", null], + ["096755616139", "Iphofen, St", null], + ["096755616148", "Markt Einersheim, M", null], + ["096755616161", "Rödelsee", null], + ["096755616179", "Willanzheim, M", null], + ["096755617112", "Albertshofen", null], + ["096755617113", "Biebelried", null], + ["096755617114", "Buchbrunn", null], + ["096755617146", "Mainstockheim", null], + ["096755617170", "Sulzfeld a.Main", null], + ["096755618147", "Marktbreit, St", null], + ["096755618149", "Marktsteft, St", null], + ["096755618150", "Martinsheim", null], + ["096755618156", "Obernbreit, M", null], + ["096755618166", "Segnitz", null], + ["096755618167", "Seinsheim, M", null], + ["096755619155", "Nordheim a.Main", null], + ["096755619169", "Sommerach", null], + ["096755619174", "Volkach, St", null], + ["096760112112", "Amorbach, St", null], + ["096760117117", "Collenberg", null], + ["096760118118", "Dorfprozelten", null], + ["096760119119", "Eichenbühl", null], + ["096760121121", "Elsenfeld, M", null], + ["096760122122", "Erlenbach a.Main, St", null], + ["096760123123", "Eschau, M", null], + ["096760124124", "Faulbach", null], + ["096760125125", "Großheubach, M", null], + ["096760126126", "Großwallstadt", null], + ["096760131131", "Kirchzell, M", null], + ["096760134134", "Klingenberg a.Main, St", null], + ["096760136136", "Leidersbach", null], + ["096760139139", "Miltenberg, St", null], + ["096760140140", "Mömlingen", null], + ["096760144144", "Niedernberg", null], + ["096760145145", "Obernburg a.Main, St", null], + ["096760156156", "Schneeberg, M", null], + ["096760160160", "Sulzbach a.Main, M", null], + ["096760165165", "Weilbach, M", null], + ["096760169169", "Wörth a.Main, St", null], + ["096765626116", "Bürgstadt, M", null], + ["096765626143", "Neunkirchen", null], + ["096765627132", "Kleinheubach, M", null], + ["096765627135", "Laudenbach", null], + ["096765627153", "Rüdenau", null], + ["096765630128", "Hausen", null], + ["096765630133", "Kleinwallstadt, M", null], + ["096765631141", "Mönchberg, M", null], + ["096765631151", "Röllbach", null], + ["096765632111", "Altenbuch", null], + ["096765632158", "Stadtprozelten, St", null], + ["096769452452", "Forstwald", null], + ["096769455455", "Hohe Wart", null], + ["096770114114", "Arnstein, St", null], + ["096770127127", "Eußenheim", null], + ["096770129129", "Frammersbach, M", null], + ["096770131131", "Gemünden a.Main, St", null], + ["096770148148", "Karlstadt, St", null], + ["096770154154", "Triefenstein, M", null], + ["096770155155", "Lohr a.Main, St", null], + ["096770157157", "Marktheidenfeld, St", null], + ["096770177177", "Rieneck, St", null], + ["096775620137", "Hasloch", null], + ["096775620151", "Kreuzwertheim, M", null], + ["096775620182", "Schollbrunn", null], + ["096775621119", "Birkenfeld", null], + ["096775621120", "Bischbrunn", null], + ["096775621125", "Erlenbach b.Marktheidenfeld", null], + ["096775621126", "Esselbach", null], + ["096775621135", "Hafenlohr", null], + ["096775621146", "Karbach, M", null], + ["096775621178", "Roden", null], + ["096775621181", "Rothenfels, St", null], + ["096775621193", "Urspringen", null], + ["096775622116", "Aura i.Sinngrund", null], + ["096775622122", "Burgsinn, M", null], + ["096775622128", "Fellen", null], + ["096775622159", "Mittelsinn", null], + ["096775622169", "Obersinn, M", null], + ["096775623132", "Gössenheim", null], + ["096775623133", "Gräfendorf", null], + ["096775623149", "Karsbach", null], + ["096775624164", "Neuendorf", null], + ["096775624166", "Neustadt a.Main", null], + ["096775624172", "Rechtenbach", null], + ["096775624186", "Steinfeld", null], + ["096775625142", "Himmelstadt", null], + ["096775625175", "Retzstadt", null], + ["096775625189", "Thüngen, M", null], + ["096775625203", "Zellingen, M", null], + ["096775656165", "Neuhütten", null], + ["096775656170", "Partenstein", null], + ["096775656200", "Wiesthal", null], + ["096779452452", "Burgjoß", null], + ["096779453453", "Forst Aura", null], + ["096779454454", "Forst Lohrerstraße", null], + ["096779455455", "Frammersbacher Forst", null], + ["096779456456", "Fürstl. Löwenstein'scher Park", null], + ["096779457457", "Haurain", null], + ["096779458458", "Herrnwald", null], + ["096779459459", "Langenprozeltener Forst", null], + ["096779461461", "Partensteiner Forst", null], + ["096779463463", "Ruppertshüttener Forst", null], + ["096780115115", "Bergrheinfeld", null], + ["096780123123", "Dittelbrunn", null], + ["096780128128", "Euerbach", null], + ["096780132132", "Geldersheim", null], + ["096780135135", "Gochsheim", null], + ["096780136136", "Grafenrheinfeld", null], + ["096780138138", "Grettstadt", null], + ["096780150150", "Kolitzheim", null], + ["096780160160", "Niederwerrn", null], + ["096780168168", "Poppenhausen", null], + ["096780170170", "Röthlein", null], + ["096780174174", "Schonungen", null], + ["096780176176", "Schwebheim", null], + ["096780178178", "Sennfeld", null], + ["096780181181", "Stadtlauringen, M", null], + ["096780186186", "Üchtelhausen", null], + ["096780190190", "Waigolshausen", null], + ["096780192192", "Wasserlosen", null], + ["096780193193", "Werneck, M", null], + ["096785642122", "Dingolshausen", null], + ["096785642124", "Donnersdorf", null], + ["096785642130", "Frankenwinheim", null], + ["096785642134", "Gerolzhofen, St", null], + ["096785642153", "Lülsfeld", null], + ["096785642157", "Michelau i.Steigerwald", null], + ["096785642164", "Oberschwarzach, M", null], + ["096785642183", "Sulzheim", null], + ["096785643175", "Schwanfeld", null], + ["096785643196", "Wipfeld", null], + ["096789451451", "Bürgerwald", null], + ["096789452452", "Geiersberg", null], + ["096789453453", "Hundelshausen", null], + ["096789454454", "Nonnenkloster", null], + ["096789455455", "Stollbergerforst", null], + ["096789456456", "Vollburg", null], + ["096789457457", "Wustvieler Forst", null], + ["096790126126", "Eisingen", null], + ["096790134134", "Gaukönigshofen", null], + ["096790136136", "Gerbrunn", null], + ["096790142142", "Güntersleben", null], + ["096790143143", "Hausen b.Würzburg", null], + ["096790147147", "Höchberg, M", null], + ["096790155155", "Kleinrinderfeld", null], + ["096790156156", "Kürnach", null], + ["096790164164", "Neubrunn, M", null], + ["096790170170", "Ochsenfurt, St", null], + ["096790175175", "Randersacker, M", null], + ["096790176176", "Reichenberg, M", null], + ["096790180180", "Rimpar, M", null], + ["096790185185", "Rottendorf", null], + ["096790193193", "Theilheim", null], + ["096790194194", "Thüngersheim", null], + ["096790200200", "Leinach", null], + ["096790201201", "Unterpleichfeld", null], + ["096790202202", "Veitshöchheim", null], + ["096790204204", "Waldbrunn", null], + ["096790205205", "Waldbüttelbrunn", null], + ["096790209209", "Zell a.Main, M", null], + ["096795644114", "Aub, St", null], + ["096795644135", "Gelchsheim, M", null], + ["096795644188", "Sonderhofen", null], + ["096795645117", "Bergtheim", null], + ["096795645169", "Oberpleichfeld", null], + ["096795646124", "Eibelstadt, St", null], + ["096795646131", "Frickenhausen a.Main, M", null], + ["096795646187", "Sommerhausen, M", null], + ["096795646206", "Winterhausen, M", null], + ["096795647130", "Estenfeld", null], + ["096795647167", "Eisenheim, M", null], + ["096795647174", "Prosselsheim", null], + ["096795648122", "Bütthard, M", null], + ["096795648138", "Giebelstadt, M", null], + ["096795649144", "Helmstadt, M", null], + ["096795649149", "Holzkirchen", null], + ["096795649177", "Remlingen, M", null], + ["096795649196", "Uettingen", null], + ["096795650137", "Geroldshausen", null], + ["096795650153", "Kirchheim", null], + ["096795651154", "Kist", null], + ["096795651165", "Altertheim", null], + ["096795652128", "Erlabrunn", null], + ["096795652161", "Margetshöchheim", null], + ["096795654118", "Bieberehren", null], + ["096795654179", "Riedenheim", null], + ["096795654182", "Röttingen, St", null], + ["096795654192", "Tauberrettersheim", null], + ["096795655141", "Greußenheim", null], + ["096795655146", "Hettstadt", null], + ["096799451451", "Gramschatzer Wald", null], + ["096799452452", "Guttenberger Wald", null], + ["096799453453", "Irtenberger Wald", null], + ["097610000000", "Augsburg", null], + ["097620000000", "Kaufbeuren", null], + ["097630000000", "Kempten (Allgäu)", null], + ["097640000000", "Memmingen", null], + ["097710112112", "Affing", null], + ["097710113113", "Aichach, St", null], + ["097710130130", "Friedberg, St", null], + ["097710140140", "Hollenbach", null], + ["097710141141", "Inchenhofen, M", null], + ["097710142142", "Kissing", null], + ["097710145145", "Merching", null], + ["097710158158", "Rehling", null], + ["097710160160", "Ried", null], + ["097715701114", "Aindling, M", null], + ["097715701155", "Petersdorf", null], + ["097715701169", "Todtenweis", null], + ["097715703144", "Kühbach, M", null], + ["097715703162", "Schiltberg", null], + ["097715704111", "Adelzhausen", null], + ["097715704122", "Dasing", null], + ["097715704129", "Eurasburg", null], + ["097715704149", "Obergriesbach", null], + ["097715704165", "Sielenbach", null], + ["097715705146", "Mering, M", null], + ["097715705163", "Schmiechen", null], + ["097715705168", "Steindorf", null], + ["097715771156", "Pöttmes, M", null], + ["097715771176", "Baar (Schwaben)", null], + ["097720111111", "Adelsried", null], + ["097720115115", "Altenmünster", null], + ["097720117117", "Aystetten", null], + ["097720121121", "Biberbach, M", null], + ["097720125125", "Bobingen, St", null], + ["097720130130", "Diedorf, M", null], + ["097720131131", "Dinkelscherben, M", null], + ["097720141141", "Fischach, M", null], + ["097720145145", "Gablingen", null], + ["097720147147", "Gersthofen, St", null], + ["097720149149", "Graben", null], + ["097720159159", "Horgau", null], + ["097720163163", "Königsbrunn, St", null], + ["097720167167", "Kutzenhausen", null], + ["097720171171", "Langweid a.Lech", null], + ["097720177177", "Meitingen, M", null], + ["097720184184", "Neusäß, St", null], + ["097720200200", "Schwabmünchen, St", null], + ["097720202202", "Stadtbergen, St", null], + ["097720207207", "Thierhaupten, M", null], + ["097720215215", "Wehringen", null], + ["097720223223", "Zusmarshausen, M", null], + ["097725706114", "Allmannshofen", null], + ["097725706134", "Ehingen", null], + ["097725706136", "Ellgau", null], + ["097725706166", "Kühlenthal", null], + ["097725706185", "Nordendorf", null], + ["097725706217", "Westendorf", null], + ["097725707126", "Bonstetten", null], + ["097725707137", "Emersacker", null], + ["097725707156", "Heretsried", null], + ["097725707216", "Welden, M", null], + ["097725708148", "Gessertshausen", null], + ["097725708211", "Ustersbach", null], + ["097725709168", "Langenneufnach", null], + ["097725709178", "Mickhausen", null], + ["097725709179", "Mittelneufnach", null], + ["097725709197", "Scherstetten", null], + ["097725709214", "Walkertshofen", null], + ["097725710151", "Großaitingen", null], + ["097725710160", "Kleinaitingen", null], + ["097725710186", "Oberottmarshausen", null], + ["097725711162", "Klosterlechfeld", null], + ["097725711209", "Untermeitingen", null], + ["097725712157", "Hiltenfingen", null], + ["097725712170", "Langerringen", null], + ["097729451451", "Schmellerforst", null], + ["097730117117", "Bissingen, M", null], + ["097730122122", "Buttenwiesen", null], + ["097730125125", "Dillingen a.d.Donau, GKSt", null], + ["097730144144", "Lauingen (Donau), St", null], + ["097735713113", "Bächingen a.d.Brenz", null], + ["097735713136", "Gundelfingen a.d.Donau, St", null], + ["097735713137", "Haunsheim", null], + ["097735713153", "Medlingen", null], + ["097735714112", "Bachhagel", null], + ["097735714170", "Syrgenstein", null], + ["097735714187", "Zöschingen", null], + ["097735715147", "Mödingen", null], + ["097735715183", "Wittislingen, M", null], + ["097735715186", "Ziertheim", null], + ["097735716119", "Blindheim", null], + ["097735716139", "Höchstädt a.d.Donau, St", null], + ["097735716146", "Lutzingen", null], + ["097735716150", "Finningen", null], + ["097735716164", "Schwenningen", null], + ["097735718116", "Binswangen", null], + ["097735718143", "Laugna", null], + ["097735718179", "Villenbach", null], + ["097735718182", "Wertingen, St", null], + ["097735718188", "Zusamaltheim", null], + ["097735719111", "Aislingen, M", null], + ["097735719133", "Glött", null], + ["097735719140", "Holzheim", null], + ["097740116116", "Ursberg", null], + ["097740119119", "Bibertal", null], + ["097740121121", "Burgau, St", null], + ["097740122122", "Burtenbach, M", null], + ["097740135135", "Günzburg, GKSt", null], + ["097740144144", "Jettingen-Scheppach, M", null], + ["097740145145", "Kammeltal", null], + ["097740150150", "Krumbach (Schwaben), St", null], + ["097740155155", "Leipheim, St", null], + ["097740162162", "Neuburg a.d.Kammel, M", null], + ["097745727136", "Gundremmingen", null], + ["097745727171", "Offingen, M", null], + ["097745727174", "Rettenbach", null], + ["097745728127", "Dürrlauingen", null], + ["097745728140", "Haldenwang", null], + ["097745728151", "Landensberg", null], + ["097745728178", "Röfingen", null], + ["097745728196", "Winterbach", null], + ["097745729118", "Bubesheim", null], + ["097745729148", "Kötz", null], + ["097745730133", "Ellzee", null], + ["097745730143", "Ichenhausen, St", null], + ["097745730191", "Waldstetten, M", null], + ["097745731111", "Aletshausen", null], + ["097745731117", "Breitenthal", null], + ["097745731124", "Deisenhausen", null], + ["097745731129", "Ebershausen", null], + ["097745731189", "Wiesenbach", null], + ["097745731192", "Waltenhausen", null], + ["097745732115", "Balzhausen", null], + ["097745732160", "Münsterhausen, M", null], + ["097745732185", "Thannhausen, St", null], + ["097745733166", "Aichen", null], + ["097745733198", "Ziemetshausen, M", null], + ["097749451451", "Ebershauser-Nattenhauser Wald", null], + ["097749452452", "Winzerwald", null], + ["097750115115", "Bellenberg", null], + ["097750129129", "Illertissen, St", null], + ["097750134134", "Nersingen", null], + ["097750135135", "Neu-Ulm, GKSt", null], + ["097750139139", "Elchingen", null], + ["097750149149", "Roggenburg", null], + ["097750152152", "Senden, St", null], + ["097750162162", "Vöhringen, St", null], + ["097750164164", "Weißenhorn, St", null], + ["097755739126", "Holzheim", null], + ["097755739143", "Pfaffenhofen a.d.Roth, M", null], + ["097755740111", "Altenstadt, M", null], + ["097755740132", "Kellmünz a.d.Iller, M", null], + ["097755740142", "Osterberg", null], + ["097755741118", "Buch, M", null], + ["097755741141", "Oberroth", null], + ["097755741161", "Unterroth", null], + ["097759451451", "Auwald", null], + ["097759452452", "Oberroggenburger Wald", null], + ["097759454454", "Stoffenrieder Forst", null], + ["097759455455", "Unterroggenburger Wald", null], + ["097760111111", "Bodolz", null], + ["097760114114", "Heimenkirch, M", null], + ["097760116116", "Lindau (Bodensee), GKSt", null], + ["097760117117", "Lindenberg i.Allgäu, St", null], + ["097760120120", "Nonnenhorn", null], + ["097760122122", "Opfenbach", null], + ["097760125125", "Scheidegg, M", null], + ["097760128128", "Wasserburg (Bodensee)", null], + ["097760129129", "Weiler-Simmerberg, M", null], + ["097760131131", "Hergatz", null], + ["097765735115", "Hergensweiler", null], + ["097765735126", "Sigmarszell", null], + ["097765735130", "Weißensberg", null], + ["097765737112", "Gestratz", null], + ["097765737113", "Grünenbach", null], + ["097765737118", "Maierhöfen", null], + ["097765737124", "Röthenbach (Allgäu)", null], + ["097765738121", "Oberreute", null], + ["097765738127", "Stiefenhofen", null], + ["097770129129", "Füssen, St", null], + ["097770130130", "Germaringen", null], + ["097770147147", "Lechbruck am See", null], + ["097770151151", "Marktoberdorf, St", null], + ["097770152152", "Mauerstetten", null], + ["097770153153", "Nesselwang, M", null], + ["097770159159", "Pfronten", null], + ["097770165165", "Ronsberg, M", null], + ["097770169169", "Schwangau", null], + ["097770173173", "Halblech", null], + ["097775748121", "Buchloe, St", null], + ["097775748140", "Jengen", null], + ["097775748145", "Lamerdingen", null], + ["097775748177", "Waal, M", null], + ["097775749139", "Irsee, M", null], + ["097775749158", "Pforzen", null], + ["097775749164", "Rieden", null], + ["097775751141", "Kaltental, M", null], + ["097775751155", "Oberostendorf", null], + ["097775751157", "Osterzell", null], + ["097775751172", "Stöttwang", null], + ["097775751182", "Westendorf", null], + ["097775752111", "Aitrang", null], + ["097775752112", "Biessenhofen", null], + ["097775752118", "Bidingen", null], + ["097775752167", "Ruderatshofen", null], + ["097775753114", "Baisweil", null], + ["097775753124", "Eggenthal", null], + ["097775753128", "Friesenried", null], + ["097775754138", "Günzach", null], + ["097775754154", "Obergünzburg, M", null], + ["097775754176", "Untrasried", null], + ["097775755131", "Görisried", null], + ["097775755144", "Kraftisried", null], + ["097775755175", "Unterthingau, M", null], + ["097775756125", "Eisenberg", null], + ["097775756135", "Hopferau", null], + ["097775756149", "Lengenwang", null], + ["097775756168", "Rückholz", null], + ["097775756170", "Seeg", null], + ["097775756179", "Wald", null], + ["097775770163", "Rieden am Forggensee", null], + ["097775770166", "Roßhaupten", null], + ["097775772171", "Stötten a.Auerberg", null], + ["097775772183", "Rettenbach a.Auerberg", null], + ["097780116116", "Bad Wörishofen, St", null], + ["097780123123", "Buxheim", null], + ["097780137137", "Ettringen", null], + ["097780168168", "Markt Rettenbach, M", null], + ["097780169169", "Markt Wald, M", null], + ["097780173173", "Mindelheim, St", null], + ["097780196196", "Sontheim", null], + ["097780204204", "Tussenhausen, M", null], + ["097785757119", "Böhen", null], + ["097785757149", "Hawangen", null], + ["097785757186", "Ottobeuren, M", null], + ["097785758115", "Babenhausen, M", null], + ["097785758130", "Egg a.d.Günz", null], + ["097785758157", "Kirchhaslach", null], + ["097785758184", "Oberschönegg", null], + ["097785758217", "Winterrieden", null], + ["097785758221", "Kettershausen", null], + ["097785759121", "Breitenbrunn", null], + ["097785759183", "Oberrieden", null], + ["097785759187", "Pfaffenhausen, M", null], + ["097785759190", "Salgen", null], + ["097785760134", "Eppishausen", null], + ["097785760158", "Kirchheim i.Schw., M", null], + ["097785761120", "Boos", null], + ["097785761139", "Fellheim", null], + ["097785761150", "Heimertingen", null], + ["097785761177", "Niederrieden", null], + ["097785761188", "Pleß", null], + ["097785762136", "Erkheim, M", null], + ["097785762163", "Lauben", null], + ["097785762180", "Kammlach", null], + ["097785762214", "Westerheim", null], + ["097785764111", "Amberg", null], + ["097785764203", "Türkheim, M", null], + ["097785764209", "Rammingen", null], + ["097785764216", "Wiedergeltingen", null], + ["097785765118", "Benningen", null], + ["097785765151", "Holzgünz", null], + ["097785765162", "Lachen", null], + ["097785765171", "Memmingerberg", null], + ["097785765202", "Trunkelsberg", null], + ["097785765205", "Ungerhausen", null], + ["097785766113", "Apfeltrach", null], + ["097785766127", "Dirlewang, M", null], + ["097785766199", "Stetten", null], + ["097785766207", "Unteregg", null], + ["097785767161", "Kronburg", null], + ["097785767164", "Lautrach", null], + ["097785767165", "Legau, M", null], + ["097785768144", "Bad Grönenbach, M", null], + ["097785768218", "Wolfertschwenden", null], + ["097785768219", "Woringen", null], + ["097789451451", "Ungerhauser Wald", null], + ["097790115115", "Asbach-Bäumenheim", null], + ["097790131131", "Donauwörth, GKSt", null], + ["097790147147", "Fremdingen", null], + ["097790155155", "Harburg (Schwaben), St", null], + ["097790169169", "Kaisheim, M", null], + ["097790178178", "Marxheim", null], + ["097790181181", "Mertingen", null], + ["097790185185", "Möttingen", null], + ["097790194194", "Nördlingen, GKSt", null], + ["097790196196", "Oberndorf a.Lech", null], + ["097790218218", "Tapfheim", null], + ["097795720176", "Maihingen", null], + ["097795720177", "Marktoffingen", null], + ["097795720224", "Wallerstein, M", null], + ["097795721117", "Auhausen", null], + ["097795721138", "Ehingen a.Ries", null], + ["097795721154", "Hainsfarth", null], + ["097795721180", "Megesheim", null], + ["097795721188", "Munningen", null], + ["097795721197", "Oettingen i.Bay., St", null], + ["097795722111", "Alerheim", null], + ["097795722112", "Amerdingen", null], + ["097795722130", "Deiningen", null], + ["097795722136", "Ederheim", null], + ["097795722146", "Forheim", null], + ["097795722162", "Hohenaltheim", null], + ["097795722184", "Mönchsdeggingen", null], + ["097795722203", "Reimlingen", null], + ["097795722226", "Wechingen", null], + ["097795723148", "Fünfstetten", null], + ["097795723167", "Huisheim", null], + ["097795723198", "Otting", null], + ["097795723228", "Wemding, St", null], + ["097795723231", "Wolferstadt", null], + ["097795724126", "Buchdorf", null], + ["097795724129", "Daiting", null], + ["097795724186", "Monheim, St", null], + ["097795724206", "Rögling", null], + ["097795724217", "Tagmersheim", null], + ["097795725149", "Genderkingen", null], + ["097795725163", "Holzheim", null], + ["097795725187", "Münster", null], + ["097795725192", "Niederschönenfeld", null], + ["097795725201", "Rain, St", null], + ["097799452452", "Dornstadt-Linkersbaindt", null], + ["097799453453", "Esterholz", null], + ["097800112112", "Altusried, M", null], + ["097800114114", "Betzigau", null], + ["097800115115", "Blaichach", null], + ["097800117117", "Buchenberg, M", null], + ["097800118118", "Burgberg i.Allgäu", null], + ["097800119119", "Dietmannsried, M", null], + ["097800120120", "Durach", null], + ["097800122122", "Haldenwang", null], + ["097800123123", "Bad Hindelang, M", null], + ["097800124124", "Immenstadt i.Allgäu, St", null], + ["097800125125", "Lauben", null], + ["097800128128", "Oy-Mittelberg", null], + ["097800132132", "Oberstaufen, M", null], + ["097800133133", "Oberstdorf, M", null], + ["097800137137", "Rettenberg", null], + ["097800139139", "Sonthofen, St", null], + ["097800140140", "Sulzberg, M", null], + ["097800143143", "Waltenhofen", null], + ["097800145145", "Wertach, M", null], + ["097800146146", "Wiggensbach, M", null], + ["097800147147", "Wildpoldsried", null], + ["097805742113", "Balderschwang", null], + ["097805742116", "Bolsterlang", null], + ["097805742121", "Fischen i.Allgäu", null], + ["097805742131", "Obermaiselstein", null], + ["097805742134", "Ofterschwang", null], + ["097805745127", "Missen-Wilhams", null], + ["097805745144", "Weitnau, M", null], + ["097809451451", "Kempter Wald", null], + ["100410100100", "Saarbrücken, Landeshauptstadt", null], + ["100410511511", "Friedrichsthal, Stadt", null], + ["100410512512", "Großrosseln", null], + ["100410513513", "Heusweiler", null], + ["100410514514", "Kleinblittersdorf", null], + ["100410515515", "Püttlingen, Stadt", null], + ["100410516516", "Quierschied", null], + ["100410517517", "Riegelsberg", null], + ["100410518518", "Sulzbach/ Saar, Stadt", null], + ["100410519519", "Völklingen, Stadt", null], + ["100420111111", "Beckingen", null], + ["100420112112", "Losheim am See", null], + ["100420113113", "Merzig, Kreisstadt", null], + ["100420114114", "Mettlach", null], + ["100420115115", "Perl", null], + ["100420116116", "Wadern, Stadt", null], + ["100420117117", "Weiskirchen", null], + ["100429999999", "Deutsch-luxemburgisches Hoheitsgebiet", null], + ["100430111111", "Eppelborn", null], + ["100430112112", "Illingen", null], + ["100430113113", "Merchweiler", null], + ["100430114114", "Neunkirchen, Kreisstadt", null], + ["100430115115", "Ottweiler, Stadt", null], + ["100430116116", "Schiffweiler", null], + ["100430117117", "Spiesen-Elversberg", null], + ["100440111111", "Dillingen/ Saar, Stadt", null], + ["100440112112", "Lebach, Stadt", null], + ["100440113113", "Nalbach", null], + ["100440114114", "Rehlingen-Siersburg", null], + ["100440115115", "Saarlouis, Kreisstadt", null], + ["100440116116", "Saarwellingen", null], + ["100440117117", "Schmelz", null], + ["100440118118", "Schwalbach", null], + ["100440119119", "Überherrn", null], + ["100440120120", "Wadgassen", null], + ["100440121121", "Wallerfangen", null], + ["100440122122", "Bous", null], + ["100440123123", "Ensdorf", null], + ["100450111111", "Bexbach, Stadt", null], + ["100450112112", "Blieskastel, Stadt", null], + ["100450113113", "Gersheim", null], + ["100450114114", "Homburg, Kreisstadt", null], + ["100450115115", "Kirkel", null], + ["100450116116", "Mandelbachtal", null], + ["100450117117", "St. Ingbert, Stadt", null], + ["100460111111", "Freisen", null], + ["100460112112", "Marpingen", null], + ["100460113113", "Namborn", null], + ["100460114114", "Nohfelden", null], + ["100460115115", "Nonnweiler", null], + ["100460116116", "Oberthal", null], + ["100460117117", "St. Wendel, Kreisstadt", null], + ["100460118118", "Tholey", null], + ["110000000000", "Berlin, Stadt", null], + ["110010001001", "Mitte", "Stadt-/Ortsteil bzw. Stadtbezirk"], + [ + "110020002002", + "Friedrichshain-Kreuzberg", + "Stadt-/Ortsteil bzw. Stadtbezirk" + ], + ["110030003003", "Pankow", "Stadt-/Ortsteil bzw. Stadtbezirk"], + [ + "110040004004", + "Charlottenburg-Wilmersdorf", + "Stadt-/Ortsteil bzw. Stadtbezirk" + ], + ["110050005005", "Spandau", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["110060006006", "Steglitz-Zehlendorf", "Stadt-/Ortsteil bzw. Stadtbezirk"], + [ + "110070007007", + "Tempelhof-Schöneberg", + "Stadt-/Ortsteil bzw. Stadtbezirk" + ], + ["110080008008", "Neukölln", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["110090009009", "Treptow-Köpenick", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["110100010010", "Marzahn-Hellersdorf", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["110110011011", "Lichtenberg", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["110120012012", "Reinickendorf", "Stadt-/Ortsteil bzw. Stadtbezirk"], + ["120510000000", "Brandenburg an der Havel, Stadt", null], + ["120520000000", "Cottbus/Chóśebuz, Stadt", null], + ["120530000000", "Frankfurt (Oder), Stadt", null], + ["120540000000", "Potsdam, Stadt", null], + ["120600005005", "Ahrensfelde", null], + ["120600020020", "Bernau bei Berlin, Stadt", null], + ["120600052052", "Eberswalde, Stadt", null], + ["120600181181", "Panketal", null], + ["120600198198", "Schorfheide", null], + ["120600269269", "Wandlitz", null], + ["120600280280", "Werneuchen, Stadt", null], + ["120605003024", "Biesenthal, Stadt", null], + ["120605003034", "Breydin", null], + ["120605003154", "Marienwerder", null], + ["120605003161", "Melchow", null], + ["120605003192", "Rüdnitz", null], + ["120605003250", "Sydower Fließ", null], + ["120605006012", "Althüttendorf", null], + ["120605006068", "Friedrichswalde", null], + ["120605006100", "Joachimsthal, Stadt", null], + ["120605006296", "Ziethen", null], + ["120605011036", "Britz", null], + ["120605011045", "Chorin", null], + ["120605011092", "Hohenfinow", null], + ["120605011128", "Liepe", null], + ["120605011149", "Lunow-Stolzenhagen", null], + ["120605011172", "Niederfinow", null], + ["120605011176", "Oderberg, Stadt", null], + ["120605011185", "Parsteinsee", null], + ["120610020020", "Bestensee", null], + ["120610112112", "Eichwalde", null], + ["120610217217", "Heidesee", null], + ["120610219219", "Heideblick", null], + ["120610260260", "Königs Wusterhausen, Stadt", null], + ["120610316316", "Lübben (Spreewald) / Lubin (Błota), Stadt", null], + ["120610320320", "Luckau, Stadt", null], + ["120610329329", "Märkische Heide/Markojska Góla", null], + ["120610332332", "Mittenwalde, Stadt", null], + ["120610433433", "Schönefeld", null], + ["120610444444", "Schulzendorf", null], + ["120610540540", "Wildau, Stadt", null], + ["120610572572", "Zeuthen", null], + ["120615108192", "Groß Köris", null], + ["120615108216", "Halbe", null], + ["120615108328", "Märkisch Buchholz, Stadt", null], + ["120615108344", "Münchehofe", null], + ["120615108448", "Schwerin", null], + ["120615108492", "Teupitz, Stadt", null], + ["120615113005", "Alt Zauche-Wußwerk/Stara Niwa-Wózwjerch", null], + ["120615113061", "Byhleguhre-Byhlen/Beła Góra-Bělin", null], + ["120615113224", "Jamlitz", null], + ["120615113308", "Lieberose, Stadt", null], + ["120615113352", "Neu Zauche/Nowa Niwa", null], + ["120615113450", "Schwielochsee/Gójacki Jazor", null], + ["120615113470", "Spreewaldheide/Błośańska Góla", null], + ["120615113476", "Straupitz (Spreewald)/Tšupc (Błota)", null], + ["120615114017", "Bersteland", null], + ["120615114097", "Drahnsdorf", null], + ["120615114164", "Golßen, Stadt", null], + ["120615114244", "Kasel-Golzig", null], + ["120615114265", "Krausnick-Groß Wasserburg", null], + ["120615114405", "Rietzneuendorf-Staakow", null], + ["120615114428", "Schlepzig/Słopišća", null], + ["120615114435", "Schönwald", null], + ["120615114471", "Steinreich", null], + ["120615114510", "Unterspreewald", null], + ["120620092092", "Doberlug-Kirchhain, Stadt", null], + ["120620124124", "Elsterwerda, Stadt", null], + ["120620140140", "Finsterwalde, Stadt", null], + ["120620224224", "Herzberg (Elster), Stadt", null], + ["120620410410", "Röderland", null], + ["120620461461", "Schönewalde, Stadt", null], + ["120620469469", "Sonnewalde, Stadt", null], + ["120625031024", "Bad Liebenwerda, Stadt", null], + ["120625031128", "Falkenberg/Elster, Stadt", null], + ["120625031341", "Mühlberg/Elbe, Stadt", null], + ["120625031500", "Uebigau-Wahrenbrück, Stadt", null], + ["120625202219", "Heideland", null], + ["120625202417", "Rückersdorf", null], + ["120625202440", "Schilda", null], + ["120625202453", "Schönborn", null], + ["120625202492", "Tröbitz", null], + ["120625205088", "Crinitz", null], + ["120625205293", "Lichterfeld-Schacksdorf", null], + ["120625205333", "Massen-Niederlausitz", null], + ["120625205425", "Sallgast", null], + ["120625207177", "Gorden-Staupitz", null], + ["120625207240", "Hohenleipisch", null], + ["120625207372", "Plessa", null], + ["120625207464", "Schraden", null], + ["120625209134", "Fichtwald", null], + ["120625209237", "Hohenbucko", null], + ["120625209282", "Kremitzaue", null], + ["120625209289", "Lebusa", null], + ["120625209445", "Schlieben, Stadt", null], + ["120625211196", "Gröden", null], + ["120625211208", "Großthiemig", null], + ["120625211232", "Hirschfeld", null], + ["120625211336", "Merzdorf", null], + ["120630036036", "Brieselang", null], + ["120630056056", "Dallgow-Döberitz", null], + ["120630080080", "Falkensee, Stadt", null], + ["120630148148", "Ketzin/Havel, Stadt", null], + ["120630189189", "Milower Land", null], + ["120630208208", "Nauen, Stadt", null], + ["120630244244", "Premnitz, Stadt", null], + ["120630252252", "Rathenow, Stadt", null], + ["120630273273", "Schönwalde-Glien", null], + ["120630357357", "Wustermark", null], + ["120635302088", "Friesack, Stadt", null], + ["120635302142", "Wiesenaue", null], + ["120635302202", "Mühlenberge", null], + ["120635302228", "Paulinenaue", null], + ["120635302240", "Pessin", null], + ["120635302256", "Retzow", null], + ["120635306165", "Kotzen", null], + ["120635306186", "Märkisch Luch", null], + ["120635306212", "Nennhausen", null], + ["120635306293", "Stechow-Ferchesar", null], + ["120635309094", "Gollenberg", null], + ["120635309112", "Großderschau", null], + ["120635309134", "Havelaue", null], + ["120635309161", "Kleßen-Görne", null], + ["120635309260", "Rhinow, Stadt", null], + ["120635309274", "Seeblick", null], + ["120640029029", "Altlandsberg, Stadt", null], + ["120640044044", "Bad Freienwalde (Oder), Stadt", null], + ["120640136136", "Fredersdorf-Vogelsdorf", null], + ["120640227227", "Hoppegarten", null], + ["120640274274", "Letschin", null], + ["120640317317", "Müncheberg, Stadt", null], + ["120640336336", "Neuenhagen bei Berlin", null], + ["120640380380", "Petershagen/Eggersdorf", null], + ["120640428428", "Rüdersdorf bei Berlin", null], + ["120640448448", "Seelow, Stadt", null], + ["120640472472", "Strausberg, Stadt", null], + ["120640512512", "Wriezen, Stadt", null], + ["120645403053", "Beiersdorf-Freudenberg", null], + ["120645403125", "Falkenberg", null], + ["120645403205", "Heckelberg-Brunow", null], + ["120645403222", "Höhenland", null], + ["120645404009", "Alt Tucheband", null], + ["120645404057", "Bleyen-Genschmar", null], + ["120645404172", "Golzow", null], + ["120645404266", "Küstriner Vorland", null], + ["120645404538", "Zechin", null], + ["120645406268", "Lebus, Stadt", null], + ["120645406388", "Podelzig", null], + ["120645406420", "Reitwein", null], + ["120645406480", "Treplin", null], + ["120645406539", "Zeschdorf", null], + ["120645408084", "Buckow (Märkische Schweiz), Stadt", null], + ["120645408153", "Garzau-Garzin", null], + ["120645408370", "Oberbarnim", null], + ["120645408408", "Rehfelde", null], + ["120645408484", "Waldsieversdorf", null], + ["120645410190", "Gusow-Platkow", null], + ["120645410303", "Märkische Höhe", null], + ["120645410340", "Neuhardenberg", null], + ["120645412128", "Falkenhagen (Mark)", null], + ["120645412130", "Fichtenhöhe", null], + ["120645412288", "Lietzen", null], + ["120645412290", "Lindendorf", null], + ["120645412482", "Vierlinden", null], + ["120645414061", "Bliesdorf", null], + ["120645414349", "Neulewin", null], + ["120645414365", "Neutrebbin", null], + ["120645414371", "Oderaue", null], + ["120645414393", "Prötzel", null], + ["120645414417", "Reichenow-Möglin", null], + ["120650036036", "Birkenwerder", null], + ["120650084084", "Fürstenberg/Havel, Stadt", null], + ["120650096096", "Glienicke/Nordbahn", null], + ["120650136136", "Hennigsdorf, Stadt", null], + ["120650144144", "Hohen Neuendorf, Stadt", null], + ["120650165165", "Kremmen, Stadt", null], + ["120650180180", "Leegebruch", null], + ["120650193193", "Liebenwalde, Stadt", null], + ["120650198198", "Löwenberger Land", null], + ["120650225225", "Mühlenbecker Land", null], + ["120650251251", "Oberkrämer", null], + ["120650256256", "Oranienburg, Stadt", null], + ["120650332332", "Velten, Stadt", null], + ["120650356356", "Zehdenick, Stadt", null], + ["120655502100", "Gransee, Stadt", null], + ["120655502117", "Großwoltersdorf", null], + ["120655502276", "Schönermark", null], + ["120655502301", "Sonnenberg", null], + ["120655502310", "Stechlin", null], + ["120660052052", "Calau/Kalawa, Stadt", null], + ["120660112112", "Großräschen/Rań, Stadt", null], + ["120660176176", "Lauchhammer, Stadt", null], + ["120660196196", "Lübbenau/Spreewald / Lubnjow/Błota, Stadt", null], + ["120660285285", "Schipkau", null], + ["120660296296", "Schwarzheide, Stadt", null], + ["120660304304", "Senftenberg/Zły Komorow, Stadt", null], + ["120660320320", "Vetschau/Spreewald / Wětošow/Błota, Stadt", null], + ["120665601008", "Altdöbern", null], + ["120665601041", "Bronkow", null], + ["120665601202", "Luckaitztal", null], + ["120665601226", "Neu-Seeland/Nowa Jazorina", null], + ["120665601228", "Neupetershain/Nowe Wiki", null], + ["120665606064", "Frauendorf", null], + ["120665606104", "Großkmehlen", null], + ["120665606168", "Kroppen", null], + ["120665606188", "Lindenau", null], + ["120665606240", "Ortrand, Stadt", null], + ["120665606316", "Tettau", null], + ["120665607116", "Grünewald", null], + ["120665607120", "Guteborn", null], + ["120665607124", "Hermsdorf", null], + ["120665607132", "Hohenbocka", null], + ["120665607272", "Ruhland, Stadt", null], + ["120665607292", "Schwarzbach", null], + ["120670036036", "Beeskow, Stadt", null], + ["120670120120", "Eisenhüttenstadt, Stadt", null], + ["120670124124", "Erkner, Stadt", null], + ["120670137137", "Friedland, Stadt", null], + ["120670144144", "Fürstenwalde/Spree, Stadt", null], + ["120670201201", "Grünheide (Mark)", null], + ["120670426426", "Rietz-Neuendorf", null], + ["120670440440", "Schöneiche bei Berlin", null], + ["120670481481", "Storkow (Mark), Stadt", null], + ["120670493493", "Tauche", null], + ["120670544544", "Woltersdorf", null], + ["120675701076", "Brieskow-Finkenheerd", null], + ["120675701180", "Groß Lindow", null], + ["120675701508", "Vogelsang", null], + ["120675701528", "Wiesenau", null], + ["120675701552", "Ziltendorf", null], + ["120675705292", "Lawitz", null], + ["120675705338", "Neißemünde", null], + ["120675705357", "Neuzelle", null], + ["120675706040", "Berkenbrück", null], + ["120675706072", "Briesen (Mark)", null], + ["120675706237", "Jacobsdorf", null], + ["120675706473", "Steinhöfel", null], + ["120675707024", "Bad Saarow", null], + ["120675707112", "Diensdorf-Radlow", null], + ["120675707288", "Langewahl", null], + ["120675707413", "Reichenwalde", null], + ["120675707520", "Wendisch Rietz", null], + ["120675708205", "Grunow-Dammendorf", null], + ["120675708324", "Mixdorf", null], + ["120675708336", "Müllrose, Stadt", null], + ["120675708397", "Ragow-Merz", null], + ["120675708438", "Schlaubetal", null], + ["120675708458", "Siehdichum", null], + ["120675709173", "Gosen-Neu Zittau", null], + ["120675709408", "Rauen", null], + ["120675709469", "Spreenhagen", null], + ["120680117117", "Fehrbellin", null], + ["120680181181", "Heiligengrabe", null], + ["120680264264", "Kyritz, Stadt", null], + ["120680320320", "Neuruppin, Stadt", null], + ["120680353353", "Rheinsberg, Stadt", null], + ["120680468468", "Wittstock/Dosse, Stadt", null], + ["120680477477", "Wusterhausen/Dosse", null], + ["120685804188", "Herzberg (Mark)", null], + ["120685804280", "Lindow (Mark), Stadt", null], + ["120685804372", "Rüthnick", null], + ["120685804437", "Vielitzsee", null], + ["120685805052", "Breddin", null], + ["120685805109", "Dreetz", null], + ["120685805324", "Neustadt (Dosse), Stadt", null], + ["120685805409", "Sieversdorf-Hohenofen", null], + ["120685805417", "Stüdenitz-Schönermark", null], + ["120685805501", "Zernitz-Lohm", null], + ["120685807072", "Dabergotz", null], + ["120685807306", "Märkisch Linden", null], + ["120685807413", "Storbeck-Frankendorf", null], + ["120685807425", "Temnitzquell", null], + ["120685807426", "Temnitztal", null], + ["120685807452", "Walsleben", null], + ["120690017017", "Beelitz, Stadt", null], + ["120690020020", "Bad Belzig, Stadt", null], + ["120690249249", "Groß Kreutz (Havel)", null], + ["120690304304", "Kleinmachnow", null], + ["120690306306", "Kloster Lehnin", null], + ["120690397397", "Michendorf", null], + ["120690454454", "Nuthetal", null], + ["120690590590", "Schwielowsee", null], + ["120690596596", "Seddiner See", null], + ["120690604604", "Stahnsdorf", null], + ["120690616616", "Teltow, Stadt", null], + ["120690632632", "Treuenbrietzen, Stadt", null], + ["120690656656", "Werder (Havel), Stadt", null], + ["120690665665", "Wiesenburg/Mark", null], + ["120695902018", "Beetzsee", null], + ["120695902019", "Beetzseeheide", null], + ["120695902270", "Havelsee, Stadt", null], + ["120695902460", "Päwesin", null], + ["120695902541", "Roskow", null], + ["120695904052", "Borkheide", null], + ["120695904056", "Borkwalde", null], + ["120695904076", "Brück, Stadt", null], + ["120695904216", "Golzow", null], + ["120695904345", "Linthe", null], + ["120695904470", "Planebruch", null], + ["120695910402", "Mühlenfließ", null], + ["120695910448", "Niemegk, Stadt", null], + ["120695910474", "Planetal", null], + ["120695910485", "Rabenstein/Fläming", null], + ["120695917028", "Bensdorf", null], + ["120695917537", "Rosenau", null], + ["120695917688", "Wusterwitz", null], + ["120695918089", "Buckautal", null], + ["120695918224", "Görzke", null], + ["120695918232", "Gräben", null], + ["120695918648", "Wenzlow", null], + ["120695918680", "Wollin", null], + ["120695918696", "Ziesar, Stadt", null], + ["120700125125", "Groß Pankow (Prignitz)", null], + ["120700149149", "Gumtow", null], + ["120700173173", "Karstädt", null], + ["120700296296", "Perleberg, Stadt", null], + ["120700302302", "Plattenburg", null], + ["120700316316", "Pritzwalk, Stadt", null], + ["120700424424", "Wittenberge, Stadt", null], + ["120705001008", "Bad Wilsnack, Stadt", null], + ["120705001052", "Breese", null], + ["120705001241", "Legde/Quitzöbel", null], + ["120705001348", "Rühstädt", null], + ["120705001416", "Weisen", null], + ["120705005060", "Cumlosen", null], + ["120705005236", "Lanz", null], + ["120705005244", "Lenzen (Elbe), Stadt", null], + ["120705005246", "Lenzerwische", null], + ["120705006096", "Gerdshagen", null], + ["120705006153", "Halenbeck-Rohlsdorf", null], + ["120705006222", "Kümmernitztal", null], + ["120705006266", "Marienfließ", null], + ["120705006280", "Meyenburg, Stadt", null], + ["120705009028", "Berge", null], + ["120705009145", "Gülitz-Reetz", null], + ["120705009300", "Pirow", null], + ["120705009325", "Putlitz, Stadt", null], + ["120705009393", "Triglitz", null], + ["120710057057", "Drebkau/Drjowk, Stadt", null], + ["120710076076", "Forst (Lausitz)/Baršć (Łužyca), Stadt", null], + ["120710160160", "Guben, Stadt", null], + ["120710244244", "Kolkwitz/Gołkojce", null], + ["120710301301", "Neuhausen/Spree / Kopańce/Sprjewja", null], + ["120710337337", "Schenkendöbern/Derbno", null], + ["120710372372", "Spremberg/Grodk, Stadt", null], + ["120710408408", "Welzow/Wjelcej, Stadt", null], + ["120715101028", "Briesen/Brjazyna", null], + ["120715101032", "Burg (Spreewald)/Bórkowy (Błota)", null], + ["120715101041", "Dissen-Striesow/Dešno-Strjažow", null], + ["120715101164", "Guhrow/Góry", null], + ["120715101341", "Schmogrow-Fehrow/Smogorjow-Prjawoz", null], + ["120715101412", "Werben/Wjerbno", null], + ["120715102044", "Döbern/Derbno, Stadt", null], + ["120715102074", "Felixsee/Feliksowy Jazor", null], + ["120715102153", "Groß Schacksdorf-Simmersdorf", null], + ["120715102189", "Jämlitz-Klein Düben", null], + ["120715102294", "Neiße-Malxetal/Dolina Nysa-Małksa", null], + ["120715102392", "Tschernitz/Cersk", null], + ["120715102414", "Wiesengrund/Łukojce", null], + ["120715107052", "Drachhausen/Hochoza", null], + ["120715107060", "Drehnow/Drjenow", null], + ["120715107176", "Heinersbrück/Móst", null], + ["120715107193", "Jänschwalde/Janšojce", null], + ["120715107304", "Peitz/Picnjo, Stadt", null], + ["120715107384", "Tauer/Turjej", null], + ["120715107386", "Teichland/Gatojce", null], + ["120715107401", "Turnow-Preilack/Turnow-Pśiłuk", null], + ["120720002002", "Am Mellensee", null], + ["120720014014", "Baruth/Mark, Stadt", null], + ["120720017017", "Blankenfelde-Mahlow", null], + ["120720120120", "Großbeeren", null], + ["120720169169", "Jüterbog, Stadt", null], + ["120720232232", "Luckenwalde, Stadt", null], + ["120720240240", "Ludwigsfelde, Stadt", null], + ["120720297297", "Niedergörsdorf", null], + ["120720312312", "Nuthe-Urstromtal", null], + ["120720340340", "Rangsdorf", null], + ["120720426426", "Trebbin, Stadt", null], + ["120720477477", "Zossen, Stadt", null], + ["120725204053", "Dahme/Mark, Stadt", null], + ["120725204055", "Dahmetal", null], + ["120725204157", "Ihlow", null], + ["120725204298", "Niederer Fläming", null], + ["120730008008", "Angermünde, Stadt", null], + ["120730069069", "Boitzenburger Land", null], + ["120730384384", "Lychen, Stadt", null], + ["120730429429", "Nordwestuckermark", null], + ["120730452452", "Prenzlau, Stadt", null], + ["120730532532", "Schwedt/Oder, Stadt", null], + ["120730572572", "Templin, Stadt", null], + ["120730579579", "Uckerland", null], + ["120735303085", "Brüssow, Stadt", null], + ["120735303093", "Carmzow-Wallmow", null], + ["120735303216", "Göritz", null], + ["120735303490", "Schenkenberg", null], + ["120735303520", "Schönfeld", null], + ["120735304097", "Casekow", null], + ["120735304189", "Gartz (Oder), Stadt", null], + ["120735304309", "Hohenselchow-Groß Pinnow", null], + ["120735304393", "Mescherin", null], + ["120735304565", "Tantow", null], + ["120735305157", "Flieth-Stegelitz", null], + ["120735305201", "Gerswalde", null], + ["120735305396", "Milmersdorf", null], + ["120735305404", "Mittenwalde", null], + ["120735305569", "Temmen-Ringenwalde", null], + ["120735306225", "Gramzow", null], + ["120735306261", "Grünow", null], + ["120735306430", "Oberuckersee", null], + ["120735306458", "Randowtal", null], + ["120735306578", "Uckerfelde", null], + ["120735306645", "Zichow", null], + ["120735310032", "Berkholz-Meyenburg", null], + ["120735310386", "Mark Landin", null], + ["120735310440", "Pinnow", null], + ["120735310603", "Passow", null], + ["130009999999", "Küstengewässer einschl. Anteil am Festlandsockel", null], + ["130030000000", "Rostock, Hanse- und Universitätsstadt", null], + ["130040000000", "Schwerin, Landeshauptstadt", null], + ["130710027027", "Dargun, Stadt", null], + ["130710029029", "Demmin, Hansestadt", null], + ["130710033033", "Feldberger Seenlandschaft", null], + ["130710107107", "Neubrandenburg, Vier-Tore-Stadt", null], + ["130710110110", "Neustrelitz, Residenzstadt", null], + ["130710156156", "Waren (Müritz), Stadt", null], + ["130715151008", "Beggerow", null], + ["130715151014", "Borrentin", null], + ["130715151064", "Hohenbollentin", null], + ["130715151065", "Hohenmocker", null], + ["130715151072", "Kentzlin", null], + ["130715151076", "Kletzin", null], + ["130715151089", "Lindenberg", null], + ["130715151096", "Meesiger", null], + ["130715151112", "Nossendorf", null], + ["130715151128", "Sarow", null], + ["130715151131", "Schönfeld", null], + ["130715151136", "Siedenbrünzow", null], + ["130715151139", "Sommersdorf", null], + ["130715151148", "Utzedel", null], + ["130715151150", "Verchen", null], + ["130715151157", "Warrenzin", null], + ["130715152028", "Datzetal", null], + ["130715152035", "Friedland, Stadt", null], + ["130715152037", "Galenbeck", null], + ["130715153007", "Basedow", null], + ["130715153032", "Faulenrost", null], + ["130715153039", "Gielow", null], + ["130715153084", "Kummerow", null], + ["130715153092", "Malchin, Stadt", null], + ["130715153109", "Neukalen, Peenestadt", null], + ["130715154001", "Alt Schwerin", null], + ["130715154036", "Fünfseen", null], + ["130715154043", "Göhren-Lebbin", null], + ["130715154093", "Malchow, Inselstadt", null], + ["130715154113", "Nossentiner Hütte", null], + ["130715154114", "Penkow", null], + ["130715154138", "Silz", null], + ["130715154155", "Walow", null], + ["130715154171", "Zislow", null], + ["130715155099", "Mirow, Stadt", null], + ["130715155119", "Priepert", null], + ["130715155159", "Wesenberg, Stadt", null], + ["130715155167", "Wustrow", null], + ["130715156011", "Blankensee", null], + ["130715156012", "Blumenholz", null], + ["130715156025", "Carpin", null], + ["130715156042", "Godendorf", null], + ["130715156058", "Grünow", null], + ["130715156066", "Hohenzieritz", null], + ["130715156075", "Klein Vielen", null], + ["130715156080", "Kratzeburg", null], + ["130715156100", "Möllenbeck", null], + ["130715156147", "Userin", null], + ["130715156162", "Wokuhl-Dabelow", null], + ["130715157009", "Beseritz", null], + ["130715157010", "Blankenhof", null], + ["130715157019", "Brunn", null], + ["130715157104", "Neddemin", null], + ["130715157108", "Neuenkirchen", null], + ["130715157111", "Neverin", null], + ["130715157140", "Sponholz", null], + ["130715157141", "Staven", null], + ["130715157145", "Trollenhagen", null], + ["130715157161", "Woggersin", null], + ["130715157166", "Wulkenzin", null], + ["130715157170", "Zirzow", null], + ["130715158005", "Ankershagen, Schliemanngemeinde", null], + ["130715158101", "Möllenhagen", null], + ["130715158115", "Penzlin, Stadt", null], + ["130715158173", "Kuckssee", null], + ["130715159003", "Altenhof", null], + ["130715159013", "Bollewick", null], + ["130715159020", "Buchholz", null], + ["130715159023", "Bütow", null], + ["130715159034", "Fincken", null], + ["130715159045", "Gotthun", null], + ["130715159053", "Groß Kelle", null], + ["130715159073", "Kieve", null], + ["130715159087", "Lärz", null], + ["130715159088", "Leizen", null], + ["130715159097", "Melz", null], + ["130715159118", "Priborn", null], + ["130715159122", "Rechlin", null], + ["130715159124", "Röbel/Müritz, Stadt", null], + ["130715159133", "Schwarz", null], + ["130715159137", "Sietow", null], + ["130715159143", "Stuer", null], + ["130715159175", "Eldetal", null], + ["130715159176", "Südmüritz", null], + ["130715160047", "Grabowhöfe", null], + ["130715160056", "Groß Plasten", null], + ["130715160063", "Hohen Wangelin", null], + ["130715160069", "Jabel", null], + ["130715160071", "Kargow", null], + ["130715160077", "Klink", null], + ["130715160078", "Klocksin", null], + ["130715160103", "Moltzow", null], + ["130715160144", "Torgelow am See", null], + ["130715160154", "Vollrathsruhe", null], + ["130715160172", "Peenehagen", null], + ["130715160174", "Schloen-Dratow", null], + ["130715161021", "Burg Stargard, Stadt", null], + ["130715161026", "Cölpin", null], + ["130715161055", "Groß Nemerow", null], + ["130715161067", "Holldorf", null], + ["130715161090", "Lindetal", null], + ["130715161117", "Pragsdorf", null], + ["130715162015", "Bredenfelde", null], + ["130715162018", "Briggow", null], + ["130715162048", "Grammentin", null], + ["130715162060", "Gülzow", null], + ["130715162068", "Ivenack", null], + ["130715162070", "Jürgenstorf", null], + ["130715162074", "Kittendorf", null], + ["130715162079", "Knorrendorf", null], + ["130715162102", "Mölln", null], + ["130715162123", "Ritzerow", null], + ["130715162127", "Rosenow", null], + ["130715162142", "Stavenhagen, Reuterstadt, Stadt", null], + ["130715162169", "Zettemin", null], + ["130715163002", "Altenhagen", null], + ["130715163004", "Altentreptow, Stadt", null], + ["130715163006", "Bartow", null], + ["130715163016", "Breesen", null], + ["130715163017", "Breest", null], + ["130715163022", "Burow", null], + ["130715163041", "Gnevkow", null], + ["130715163044", "Golchen", null], + ["130715163049", "Grapzow", null], + ["130715163050", "Grischow", null], + ["130715163057", "Groß Teetzleben", null], + ["130715163059", "Gültz", null], + ["130715163081", "Kriesow", null], + ["130715163120", "Pripsleben", null], + ["130715163125", "Röckwitz", null], + ["130715163135", "Siedenbollentin", null], + ["130715163146", "Tützpatz", null], + ["130715163158", "Werder", null], + ["130715163160", "Wildberg", null], + ["130715163163", "Wolde", null], + ["130715164054", "Groß Miltzow", null], + ["130715164083", "Kublank", null], + ["130715164105", "Neetzka", null], + ["130715164130", "Schönbeck", null], + ["130715164132", "Schönhausen", null], + ["130715164153", "Voigtsdorf", null], + ["130715164164", "Woldegk, Windmühlenstadt", null], + ["130720006006", "Bad Doberan, Stadt", null], + ["130720029029", "Dummerstorf", null], + ["130720036036", "Graal-Müritz, Ostseeheilbad", null], + ["130720043043", "Güstrow, Barlachstadt", null], + ["130720058058", "Kröpelin, Stadt", null], + ["130720060060", "Kühlungsborn, Ostseebad, Stadt", null], + ["130720074074", "Neubukow, Stadt", null], + ["130720091091", "Sanitz", null], + ["130720093093", "Satow", null], + ["130720106106", "Teterow, Bergringstadt", null], + ["130725251001", "Admannshagen-Bargeshagen", null], + ["130725251007", "Bartenshagen-Parkentin", null], + ["130725251017", "Börgerende-Rethwisch", null], + ["130725251047", "Hohenfelde", null], + ["130725251075", "Nienhagen, Ostseebad", null], + ["130725251083", "Reddelich", null], + ["130725251086", "Retschow", null], + ["130725251099", "Steffenshagen", null], + ["130725251117", "Wittenbeck", null], + ["130725252009", "Baumgarten", null], + ["130725252013", "Bernitt", null], + ["130725252020", "Bützow, Stadt", null], + ["130725252028", "Dreetz", null], + ["130725252050", "Jürgenshagen", null], + ["130725252053", "Klein Belitz", null], + ["130725252078", "Penzin", null], + ["130725252089", "Rühn", null], + ["130725252101", "Steinhagen", null], + ["130725252104", "Tarnow", null], + ["130725252114", "Warnow", null], + ["130725252120", "Zepelin", null], + ["130725253019", "Broderstorf", null], + ["130725253081", "Poppendorf", null], + ["130725253087", "Roggentin", null], + ["130725253108", "Thulendorf", null], + ["130725254004", "Altkalen", null], + ["130725254010", "Behren-Lübchin", null], + ["130725254031", "Finkenthal", null], + ["130725254035", "Gnoien, Warbelstadt", null], + ["130725254111", "Walkendorf", null], + ["130725255033", "Glasewitz", null], + ["130725255039", "Groß Schwiesow", null], + ["130725255042", "Gülzow-Prüzen", null], + ["130725255044", "Gutow", null], + ["130725255055", "Klein Upahl", null], + ["130725255061", "Kuhs", null], + ["130725255067", "Lohmen", null], + ["130725255069", "Lüssow", null], + ["130725255071", "Mistorf", null], + ["130725255073", "Mühl Rosin", null], + ["130725255079", "Plaaz", null], + ["130725255084", "Reimershagen", null], + ["130725255092", "Sarmstorf", null], + ["130725255119", "Zehna", null], + ["130725256026", "Dobbin-Linstow", null], + ["130725256048", "Hoppenrade", null], + ["130725256056", "Krakow am See, Stadt", null], + ["130725256059", "Kuchelmiß", null], + ["130725256063", "Lalendorf", null], + ["130725257027", "Dolgen am See", null], + ["130725257046", "Hohen Sprenz", null], + ["130725257062", "Laage, Stadt", null], + ["130725257112", "Wardow", null], + ["130725258003", "Alt Sührkow", null], + ["130725258023", "Dahmen", null], + ["130725258024", "Dalkendorf", null], + ["130725258038", "Groß Roge", null], + ["130725258040", "Groß Wokern", null], + ["130725258041", "Groß Wüstenfelde", null], + ["130725258045", "Hohen Demzin", null], + ["130725258049", "Jördenstorf", null], + ["130725258066", "Lelkendorf", null], + ["130725258082", "Prebberede", null], + ["130725258094", "Schorssow", null], + ["130725258096", "Schwasdorf", null], + ["130725258103", "Sukow-Levitzow", null], + ["130725258109", "Thürkow", null], + ["130725258113", "Warnkenhagen", null], + ["130725259002", "Alt Bukow", null], + ["130725259005", "Am Salzhaff", null], + ["130725259008", "Bastorf", null], + ["130725259014", "Biendorf", null], + ["130725259022", "Carinerland", null], + ["130725259085", "Rerik, Ostseebad, Stadt", null], + ["130725260012", "Bentwisch", null], + ["130725260015", "Blankenhagen", null], + ["130725260032", "Gelbensande", null], + ["130725260072", "Mönchhagen", null], + ["130725260088", "Rövershagen", null], + ["130725261011", "Benitz", null], + ["130725261018", "Bröbberow", null], + ["130725261051", "Kassow", null], + ["130725261090", "Rukieten", null], + ["130725261095", "Schwaan, Stadt", null], + ["130725261110", "Vorbeck", null], + ["130725261116", "Wiendorf", null], + ["130725262021", "Cammin", null], + ["130725262034", "Gnewitz", null], + ["130725262037", "Grammow", null], + ["130725262076", "Nustrow", null], + ["130725262097", "Selpin", null], + ["130725262102", "Stubbendorf", null], + ["130725262105", "Tessin, Stadt", null], + ["130725262107", "Thelkow", null], + ["130725262118", "Zarnewanz", null], + ["130725263030", "Elmenhorst/Lichtenhagen", null], + ["130725263057", "Kritzmow", null], + ["130725263064", "Lambrechtshagen", null], + ["130725263077", "Papendorf", null], + ["130725263080", "Pölchow", null], + ["130725263098", "Stäbelow", null], + ["130725263121", "Ziesendorf", null], + ["130730011011", "Binz, Ostseebad", null], + ["130730035035", "Grimmen, Stadt", null], + ["130730055055", "Marlow, Stadt", null], + ["130730070070", "Putbus, Stadt", null], + ["130730080080", "Sassnitz, Stadt", null], + ["130730088088", "Stralsund, Hansestadt", null], + ["130730089089", "Süderholz", null], + ["130730105105", "Zingst, Ostseeheilbad", null], + ["130735351005", "Altenpleen", null], + ["130735351037", "Groß Mohrdorf", null], + ["130735351044", "Klausdorf", null], + ["130735351046", "Kramerhof", null], + ["130735351066", "Preetz", null], + ["130735351068", "Prohn", null], + ["130735352009", "Barth, Stadt", null], + ["130735352018", "Divitz-Spoldershagen", null], + ["130735352025", "Fuhlendorf", null], + ["130735352042", "Karnin", null], + ["130735352043", "Kenz-Küstrow", null], + ["130735352051", "Löbnitz", null], + ["130735352053", "Lüdershagen", null], + ["130735352069", "Pruchten", null], + ["130735352077", "Saal", null], + ["130735352094", "Trinwillershagen", null], + ["130735353010", "Bergen auf Rügen, Stadt", null], + ["130735353014", "Buschvitz", null], + ["130735353027", "Garz/Rügen, Stadt", null], + ["130735353038", "Gustow", null], + ["130735353049", "Lietzow", null], + ["130735353063", "Parchtitz", null], + ["130735353064", "Patzig", null], + ["130735353065", "Poseritz", null], + ["130735353072", "Ralswiek", null], + ["130735353074", "Rappin", null], + ["130735353083", "Sehlen", null], + ["130735354002", "Ahrenshoop, Ostseebad", null], + ["130735354012", "Born a. Darß", null], + ["130735354017", "Dierhagen, Ostseebad", null], + ["130735354067", "Prerow, Ostseebad", null], + ["130735354100", "Wieck a. Darß", null], + ["130735354103", "Wustrow, Ostseebad", null], + ["130735355024", "Franzburg, Stadt", null], + ["130735355029", "Glewitz", null], + ["130735355034", "Gremersdorf-Buchholz", null], + ["130735355057", "Millienhagen-Oebelitz", null], + ["130735355062", "Papenhagen", null], + ["130735355076", "Richtenberg, Stadt", null], + ["130735355086", "Splietsdorf", null], + ["130735355096", "Velgast", null], + ["130735355097", "Weitenhagen", null], + ["130735355098", "Wendisch Baggendorf", null], + ["130735356023", "Elmenhorst", null], + ["130735356090", "Sundhagen", null], + ["130735356102", "Wittenhagen", null], + ["130735357006", "Baabe, Ostseebad", null], + ["130735357031", "Göhren, Ostseebad", null], + ["130735357048", "Lancken-Granitz", null], + ["130735357084", "Sellin, Ostseebad", null], + ["130735357106", "Zirkow", null], + ["130735357107", "Mönchgut, Ostseebad", null], + ["130735358036", "Groß Kordshagen", null], + ["130735358041", "Jakobsdorf", null], + ["130735358054", "Lüssow", null], + ["130735358060", "Niepars", null], + ["130735358061", "Pantelitz", null], + ["130735358087", "Steinhagen", null], + ["130735358099", "Wendorf", null], + ["130735358104", "Zarrendorf", null], + ["130735359004", "Altenkirchen", null], + ["130735359013", "Breege", null], + ["130735359019", "Dranske", null], + ["130735359030", "Glowe", null], + ["130735359052", "Lohme", null], + ["130735359071", "Putgarten", null], + ["130735359078", "Sagard", null], + ["130735359101", "Wiek", null], + ["130735360007", "Bad Sülze, Stadt", null], + ["130735360015", "Dettmannsdorf", null], + ["130735360016", "Deyelsdorf", null], + ["130735360020", "Drechow", null], + ["130735360022", "Eixen", null], + ["130735360032", "Grammendorf", null], + ["130735360033", "Gransebieth", null], + ["130735360039", "Hugoldsdorf", null], + ["130735360050", "Lindholz", null], + ["130735360093", "Tribsees, Stadt", null], + ["130735361001", "Ahrenshagen-Daskow", null], + ["130735361075", "Ribnitz-Damgarten, Bernsteinstadt", null], + ["130735361082", "Schlemmin", null], + ["130735361085", "Semlow", null], + ["130735362003", "Altefähr", null], + ["130735362021", "Dreschvitz", null], + ["130735362028", "Gingst", null], + ["130735362040", "Insel Hiddensee, Seebad", null], + ["130735362045", "Kluis", null], + ["130735362059", "Neuenkirchen", null], + ["130735362073", "Rambin", null], + ["130735362079", "Samtens", null], + ["130735362081", "Schaprode", null], + ["130735362092", "Trent", null], + ["130735362095", "Ummanz", null], + ["130740026026", "Grevesmühlen, Stadt", null], + ["130740035035", "Insel Poel, Ostseebad", null], + ["130740087087", "Wismar, Hansestadt", null], + ["130745451002", "Bad Kleinen", null], + ["130745451003", "Barnekow", null], + ["130745451008", "Bobitz", null], + ["130745451019", "Dorf Mecklenburg", null], + ["130745451030", "Groß Stieten", null], + ["130745451031", "Hohen Viecheln", null], + ["130745451047", "Lübow", null], + ["130745451053", "Metelsdorf", null], + ["130745451082", "Ventschow", null], + ["130745452020", "Dragun", null], + ["130745452021", "Gadebusch, Stadt", null], + ["130745452040", "Kneese", null], + ["130745452043", "Krembz", null], + ["130745452054", "Mühlen Eichsen", null], + ["130745452068", "Roggendorf", null], + ["130745452070", "Rögnitz", null], + ["130745452081", "Veelböken", null], + ["130745453005", "Bernstorf", null], + ["130745453022", "Gägelow", null], + ["130745453069", "Roggenstorf", null], + ["130745453071", "Rüting", null], + ["130745453077", "Testorf-Steinfort", null], + ["130745453079", "Upahl", null], + ["130745453085", "Warnow", null], + ["130745453093", "Stepenitztal", null], + ["130745454010", "Boltenhagen, Ostseebad", null], + ["130745454016", "Damshagen", null], + ["130745454032", "Hohenkirchen", null], + ["130745454037", "Kalkhorst", null], + ["130745454039", "Klütz, Stadt", null], + ["130745454089", "Zierow", null], + ["130745455001", "Alt Meteln", null], + ["130745455012", "Brüsewitz", null], + ["130745455014", "Cramonshagen", null], + ["130745455015", "Dalberg-Wendelstorf", null], + ["130745455024", "Gottesgabe", null], + ["130745455025", "Grambow", null], + ["130745455038", "Klein Trebbow", null], + ["130745455048", "Lübstorf", null], + ["130745455050", "Lützow", null], + ["130745455061", "Perlin", null], + ["130745455062", "Pingelshagen", null], + ["130745455064", "Pokrent", null], + ["130745455072", "Schildetal", null], + ["130745455075", "Seehof", null], + ["130745455088", "Zickhusen", null], + ["130745456004", "Benz", null], + ["130745456007", "Blowatz", null], + ["130745456009", "Boiensdorf", null], + ["130745456034", "Hornstorf", null], + ["130745456044", "Krusenhagen", null], + ["130745456056", "Neuburg", null], + ["130745457006", "Bibow", null], + ["130745457023", "Glasin", null], + ["130745457036", "Jesendorf", null], + ["130745457046", "Lübberstorf", null], + ["130745457057", "Neukloster, Stadt", null], + ["130745457060", "Passee", null], + ["130745457084", "Warin, Stadt", null], + ["130745457090", "Zurow", null], + ["130745457091", "Züsow", null], + ["130745458013", "Carlow", null], + ["130745458018", "Dechow", null], + ["130745458028", "Groß Molzahn", null], + ["130745458033", "Holdorf", null], + ["130745458042", "Königsfeld", null], + ["130745458065", "Rehna, Stadt", null], + ["130745458066", "Rieps", null], + ["130745458073", "Schlagsdorf", null], + ["130745458078", "Thandorf", null], + ["130745458080", "Utecht", null], + ["130745458092", "Wedendorfersee", null], + ["130745459017", "Dassow, Stadt", null], + ["130745459027", "Grieben", null], + ["130745459049", "Lüdersdorf", null], + ["130745459052", "Menzendorf", null], + ["130745459067", "Roduchelstorf", null], + ["130745459074", "Schönberg, Stadt", null], + ["130745459076", "Selmsdorf", null], + ["130745459094", "Siemz-Niendorf", null], + ["130750005005", "Anklam, Hansestadt", null], + ["130750039039", "Greifswald, Universitäts- und Hansestadt", null], + ["130750049049", "Heringsdorf, Ostseebad", null], + ["130750105105", "Pasewalk, Stadt", null], + ["130750130130", "Strasburg (Uckermark), Stadt", null], + ["130750136136", "Ueckermünde, Seebad , Stadt", null], + ["130755551021", "Buggenhagen", null], + ["130755551072", "Krummin", null], + ["130755551074", "Lassan, Stadt", null], + ["130755551087", "Lütow", null], + ["130755551124", "Sauzin", null], + ["130755551144", "Wolgast, Stadt", null], + ["130755551147", "Zemitz", null], + ["130755552001", "Ahlbeck", null], + ["130755552003", "Altwarp", null], + ["130755552031", "Eggesin, Stadt", null], + ["130755552037", "Grambin", null], + ["130755552051", "Hintersee", null], + ["130755552075", "Leopoldshagen", null], + ["130755552078", "Liepgarten", null], + ["130755552084", "Lübs", null], + ["130755552085", "Luckow", null], + ["130755552089", "Meiersberg", null], + ["130755552093", "Mönkebude", null], + ["130755552139", "Vogelsang-Warsin", null], + ["130755553007", "Bargischow", null], + ["130755553013", "Blesewitz", null], + ["130755553015", "Boldekow", null], + ["130755553020", "Bugewitz", null], + ["130755553022", "Butzow", null], + ["130755553029", "Ducherow", null], + ["130755553053", "Iven", null], + ["130755553068", "Krien", null], + ["130755553073", "Krusenfelde", null], + ["130755553088", "Medow", null], + ["130755553098", "Neu Kosenow", null], + ["130755553101", "Neuenkirchen", null], + ["130755553110", "Postlow", null], + ["130755553116", "Rossin", null], + ["130755553122", "Sarnow", null], + ["130755553127", "Spantekow", null], + ["130755553128", "Stolpe an der Peene", null], + ["130755553155", "Neetzow-Liepen", null], + ["130755554002", "Alt Tellin", null], + ["130755554009", "Bentzin", null], + ["130755554023", "Daberkow", null], + ["130755554054", "Jarmen, Stadt", null], + ["130755554070", "Kruckow", null], + ["130755554134", "Tutow", null], + ["130755554140", "Völschow", null], + ["130755555008", "Behrenhoff", null], + ["130755555025", "Dargelin", null], + ["130755555027", "Dersekow", null], + ["130755555050", "Hinrichshagen", null], + ["130755555076", "Levenhagen", null], + ["130755555091", "Mesekenhagen", null], + ["130755555102", "Neuenkirchen", null], + ["130755555141", "Wackerow", null], + ["130755555142", "Weitenhagen", null], + ["130755556011", "Bergholz", null], + ["130755556012", "Blankensee", null], + ["130755556016", "Boock", null], + ["130755556035", "Glasow", null], + ["130755556038", "Grambow", null], + ["130755556067", "Krackow", null], + ["130755556079", "Löcknitz", null], + ["130755556095", "Nadrensee", null], + ["130755556107", "Penkun, Stadt", null], + ["130755556108", "Plöwen", null], + ["130755556113", "Ramin", null], + ["130755556117", "Rossow", null], + ["130755556119", "Rothenklempenow", null], + ["130755557018", "Brünzow", null], + ["130755557046", "Hanshagen", null], + ["130755557059", "Katzow", null], + ["130755557060", "Kemnitz", null], + ["130755557069", "Kröslin", null], + ["130755557081", "Loissin", null], + ["130755557083", "Lubmin, Seebad", null], + ["130755557097", "Neu Boltenhagen", null], + ["130755557120", "Rubenow", null], + ["130755557146", "Wusterhusen", null], + ["130755558036", "Görmin", null], + ["130755558082", "Loitz, Stadt", null], + ["130755558123", "Sassen-Trantow", null], + ["130755559004", "Altwigshagen", null], + ["130755559033", "Ferdinandshof", null], + ["130755559045", "Hammer a.d. Uecker", null], + ["130755559048", "Heinrichswalde", null], + ["130755559118", "Rothemühl", null], + ["130755559131", "Torgelow, Stadt", null], + ["130755559143", "Wilhelmsburg", null], + ["130755560017", "Brietzig", null], + ["130755560032", "Fahrenwalde", null], + ["130755560042", "Groß Luckow", null], + ["130755560055", "Jatznick", null], + ["130755560063", "Koblentz", null], + ["130755560071", "Krugsdorf", null], + ["130755560103", "Nieden", null], + ["130755560104", "Papendorf", null], + ["130755560109", "Polzow", null], + ["130755560115", "Rollwitz", null], + ["130755560126", "Schönwalde", null], + ["130755560138", "Viereck", null], + ["130755560149", "Zerrenthin", null], + ["130755561058", "Karlshagen, Ostseebad", null], + ["130755561092", "Mölschow", null], + ["130755561106", "Peenemünde", null], + ["130755561133", "Trassenheide, Ostseebad", null], + ["130755561151", "Zinnowitz, Ostseebad", null], + ["130755562010", "Benz", null], + ["130755562026", "Dargen", null], + ["130755562034", "Garz", null], + ["130755562056", "Kamminke", null], + ["130755562065", "Korswandt", null], + ["130755562066", "Koserow, Ostseebad", null], + ["130755562080", "Loddin, Seebad", null], + ["130755562090", "Mellenthin", null], + ["130755562111", "Pudagla", null], + ["130755562114", "Rankwitz", null], + ["130755562129", "Stolpe auf Usedom", null], + ["130755562135", "Ückeritz, Seebad", null], + ["130755562137", "Usedom, Stadt", null], + ["130755562148", "Zempin, Seebad", null], + ["130755562152", "Zirchow", null], + ["130755563006", "Bandelin", null], + ["130755563040", "Gribow", null], + ["130755563041", "Groß Kiesow", null], + ["130755563043", "Groß Polzin", null], + ["130755563044", "Gützkow, Stadt", null], + ["130755563061", "Klein Bünzow", null], + ["130755563094", "Murchin", null], + ["130755563121", "Rubkow", null], + ["130755563125", "Schmatzin", null], + ["130755563145", "Wrangelsburg", null], + ["130755563150", "Ziethen", null], + ["130755563154", "Züssow", null], + ["130755563156", "Karlsburg", null], + ["130760014014", "Boizenburg/ Elbe, Stadt", null], + ["130760060060", "Hagenow, Stadt", null], + ["130760088088", "Lübtheen, Stadt", null], + ["130760090090", "Ludwigslust, Stadt", null], + ["130760108108", "Parchim, Stadt", null], + ["130765652009", "Bengerstorf", null], + ["130765652010", "Besitz", null], + ["130765652016", "Brahlstorf", null], + ["130765652030", "Dersenow", null], + ["130765652054", "Gresse", null], + ["130765652055", "Greven", null], + ["130765652102", "Neu Gülze", null], + ["130765652106", "Nostorf", null], + ["130765652122", "Schwanheide", null], + ["130765652136", "Teldau", null], + ["130765652138", "Tessin b. Boizenburg", null], + ["130765654034", "Dömitz, Stadt", null], + ["130765654053", "Grebs-Niendorf", null], + ["130765654067", "Karenz", null], + ["130765654093", "Malk Göhren", null], + ["130765654094", "Malliß", null], + ["130765654103", "Neu Kaliß", null], + ["130765654143", "Vielank", null], + ["130765655040", "Gallin-Kuppentin", null], + ["130765655051", "Granzin", null], + ["130765655075", "Kreien", null], + ["130765655077", "Kritzow", null], + ["130765655089", "Lübz, Stadt", null], + ["130765655109", "Passow", null], + ["130765655125", "Siggelkow", null], + ["130765655151", "Werder", null], + ["130765655165", "Gehlsbach", null], + ["130765655168", "Ruhner Berge", null], + ["130765656032", "Dobbertin", null], + ["130765656048", "Goldberg, Stadt", null], + ["130765656096", "Mestlin", null], + ["130765656104", "Neu Poserin", null], + ["130765656135", "Techentin", null], + ["130765657003", "Balow", null], + ["130765657021", "Brunow", null], + ["130765657027", "Dambeck", null], + ["130765657037", "Eldena", null], + ["130765657049", "Gorlosen", null], + ["130765657050", "Grabow, Stadt", null], + ["130765657069", "Karstädt", null], + ["130765657076", "Kremmin", null], + ["130765657097", "Milow", null], + ["130765657098", "Möllenbeck", null], + ["130765657100", "Muchow", null], + ["130765657115", "Prislich", null], + ["130765657161", "Zierzow", null], + ["130765658002", "Alt Zachun", null], + ["130765658004", "Bandenitz", null], + ["130765658008", "Belsch", null], + ["130765658013", "Bobzin", null], + ["130765658019", "Bresegard bei Picher", null], + ["130765658041", "Gammelin", null], + ["130765658057", "Groß Krams", null], + ["130765658064", "Hoort", null], + ["130765658065", "Hülseburg", null], + ["130765658070", "Kirch Jesar", null], + ["130765658079", "Kuhstorf", null], + ["130765658099", "Moraas", null], + ["130765658110", "Pätow-Steegen", null], + ["130765658111", "Picher", null], + ["130765658116", "Pritzier", null], + ["130765658119", "Redefin", null], + ["130765658131", "Strohkirchen", null], + ["130765658169", "Toddin", null], + ["130765658145", "Warlitz", null], + ["130765659001", "Alt Krenzlin", null], + ["130765659018", "Bresegard bei Eldena", null], + ["130765659046", "Göhlen", null], + ["130765659058", "Groß Laasch", null], + ["130765659086", "Lübesse", null], + ["130765659087", "Lüblow", null], + ["130765659118", "Rastow", null], + ["130765659134", "Sülstorf", null], + ["130765659141", "Uelitz", null], + ["130765659146", "Warlow", null], + ["130765659156", "Wöbbelin", null], + ["130765660012", "Blievenstorf", null], + ["130765660017", "Brenz", null], + ["130765660105", "Neustadt-Glewe, Stadt", null], + ["130765662035", "Domsühl", null], + ["130765662056", "Groß Godems", null], + ["130765662068", "Karrenzin", null], + ["130765662085", "Lewitzrand", null], + ["130765662120", "Rom", null], + ["130765662126", "Spornitz", null], + ["130765662129", "Stolpe", null], + ["130765662160", "Ziegendorf", null], + ["130765662162", "Zölkow", null], + ["130765662164", "Obere Warnow", null], + ["130765663006", "Barkhagen", null], + ["130765663114", "Plau am See, Stadt", null], + ["130765663166", "Ganzlin", null], + ["130765664011", "Blankenberg", null], + ["130765664015", "Borkow", null], + ["130765664020", "Brüel, Stadt", null], + ["130765664026", "Dabel", null], + ["130765664062", "Hohen Pritz", null], + ["130765664072", "Kobrow", null], + ["130765664078", "Kuhlen-Wendorf", null], + ["130765664101", "Mustin", null], + ["130765664128", "Sternberg, Stadt", null], + ["130765664148", "Weitendorf", null], + ["130765664155", "Witzin", null], + ["130765664167", "Kloster Tempzin", null], + ["130765665036", "Dümmer", null], + ["130765665063", "Holthusen", null], + ["130765665071", "Klein Rogahn", null], + ["130765665107", "Pampow", null], + ["130765665121", "Schossin", null], + ["130765665130", "Stralendorf", null], + ["130765665147", "Warsow", null], + ["130765665154", "Wittenförden", null], + ["130765665163", "Zülow", null], + ["130765666152", "Wittenburg, Stadt", null], + ["130765666153", "Wittendörp", null], + ["130765667039", "Gallin", null], + ["130765667073", "Kogel", null], + ["130765667092", "Lüttow-Valluhn", null], + ["130765667142", "Vellahn", null], + ["130765667159", "Zarrentin am Schaalsee, Stadt", null], + ["130765668005", "Banzkow", null], + ["130765668007", "Barnin", null], + ["130765668023", "Bülow", null], + ["130765668024", "Cambs", null], + ["130765668025", "Crivitz, Stadt", null], + ["130765668029", "Demen", null], + ["130765668033", "Dobin am See", null], + ["130765668038", "Friedrichsruhe", null], + ["130765668044", "Gneven", null], + ["130765668080", "Langen Brütz", null], + ["130765668082", "Leezen", null], + ["130765668112", "Pinnow", null], + ["130765668113", "Plate", null], + ["130765668117", "Raben Steinfeld", null], + ["130765668133", "Sukow", null], + ["130765668140", "Tramm", null], + ["130765668158", "Zapel", null], + ["145110000000", "Chemnitz, Stadt", null], + ["145210010010", "Amtsberg", null], + ["145210020020", "Annaberg-Buchholz, Stadt", null], + ["145210035035", "Aue-Bad Schlema, Stadt", null], + ["145210110110", "Breitenbrunn/Erzgeb.", null], + ["145210130130", "Crottendorf", null], + ["145210150150", "Drebach", null], + ["145210160160", "Ehrenfriedersdorf, Stadt", null], + ["145210170170", "Eibenstock, Stadt", null], + ["145210200200", "Gelenau/Erzgeb.", null], + ["145210240240", "Großolbersdorf", null], + ["145210250250", "Großrückerswalde", null], + ["145210260260", "Grünhain-Beierfeld, Stadt", null], + ["145210290290", "Hohndorf", null], + ["145210310310", "Jahnsdorf/Erzgeb.", null], + ["145210320320", "Johanngeorgenstadt, Stadt", null], + ["145210330330", "Jöhstadt, Stadt", null], + ["145210355355", "Lauter-Bernsbach, Stadt", null], + ["145210370370", "Lößnitz, Stadt", null], + ["145210390390", "Marienberg, Stadt", null], + ["145210400400", "Mildenau", null], + ["145210410410", "Neukirchen/Erzgeb.", null], + ["145210440440", "Oberwiesenthal, Kurort, Stadt", null], + ["145210450450", "Oelsnitz/Erzgeb., Stadt", null], + ["145210460460", "Olbernhau, Stadt", null], + ["145210495495", "Pockau-Lengefeld, Stadt", null], + ["145210500500", "Raschau-Markersbach", null], + ["145210530530", "Schneeberg, Stadt", null], + ["145210540540", "Schönheide", null], + ["145210550550", "Schwarzenberg/Erzgeb., Stadt", null], + ["145210560560", "Sehmatal", null], + ["145210600600", "Stützengrün", null], + ["145210620620", "Thalheim/Erzgeb., Stadt", null], + ["145210630630", "Thermalbad Wiesenbad", null], + ["145210640640", "Thum, Stadt", null], + ["145210670670", "Wolkenstein, Stadt", null], + ["145215101060", "Bärenstein", null], + ["145215101340", "Königswalde", null], + ["145215103040", "Auerbach", null], + ["145215103120", "Burkhardtsdorf", null], + ["145215103230", "Gornsdorf", null], + ["145215110210", "Geyer, Stadt", null], + ["145215110610", "Tannenberg", null], + ["145215115380", "Lugau/Erzgeb., Stadt", null], + ["145215115430", "Niederwürschnitz", null], + ["145215130510", "Scheibenberg, Stadt", null], + ["145215130520", "Schlettau, Stadt", null], + ["145215132140", "Deutschneudorf", null], + ["145215132280", "Heidersdorf", null], + ["145215132570", "Seiffen/Erzgeb., Kurort", null], + ["145215133420", "Niederdorf", null], + ["145215133590", "Stollberg/Erzgeb., Stadt", null], + ["145215138220", "Gornau/Erzgeb.", null], + ["145215138690", "Zschopau, Stadt", null], + ["145215139080", "Bockau", null], + ["145215139700", "Zschorlau", null], + ["145215140180", "Elterlein, Stadt", null], + ["145215140710", "Zwönitz, Stadt", null], + ["145215405090", "Börnichen/Erzgeb.", null], + ["145215405270", "Grünhainichen", null], + ["145220020020", "Augustusburg, Stadt", null], + ["145220035035", "Bobritzsch-Hilbersdorf", null], + ["145220050050", "Brand-Erbisdorf, Stadt", null], + ["145220070070", "Claußnitz", null], + ["145220080080", "Döbeln, Stadt", null], + ["145220110110", "Eppendorf", null], + ["145220120120", "Erlau", null], + ["145220140140", "Flöha, Stadt", null], + ["145220150150", "Frankenberg/Sa., Stadt", null], + ["145220170170", "Frauenstein, Stadt", null], + ["145220180180", "Freiberg, Stadt, Universitätsstadt", null], + ["145220190190", "Geringswalde, Stadt", null], + ["145220200200", "Großhartmannsdorf", null], + ["145220210210", "Großschirma, Stadt", null], + ["145220220220", "Großweitzschen", null], + ["145220230230", "Hainichen, Stadt", null], + ["145220240240", "Halsbrücke", null], + ["145220250250", "Hartha, Stadt", null], + ["145220260260", "Hartmannsdorf", null], + ["145220290290", "Königshain-Wiederau", null], + ["145220300300", "Kriebstein", null], + ["145220310310", "Leisnig, Stadt", null], + ["145220320320", "Leubsdorf", null], + ["145220330330", "Lichtenau", null], + ["145220350350", "Lunzenau, Stadt", null], + ["145220390390", "Mulda/Sa.", null], + ["145220400400", "Neuhausen/Erzgeb.", null], + ["145220420420", "Niederwiesa", null], + ["145220430430", "Oberschöna", null], + ["145220440440", "Oederan, Stadt", null], + ["145220460460", "Penig, Stadt", null], + ["145220470470", "Rechenberg-Bienenmühle", null], + ["145220480480", "Reinsberg", null], + ["145220500500", "Rossau", null], + ["145220510510", "Roßwein, Stadt", null], + ["145220540540", "Striegistal", null], + ["145220570570", "Waldheim, Stadt", null], + ["145220580580", "Wechselburg", null], + ["145225102060", "Burgstädt, Stadt", null], + ["145225102380", "Mühlau", null], + ["145225102550", "Taura", null], + ["145225113340", "Lichtenberg/Erzgeb.", null], + ["145225113590", "Weißenborn/Erzgeb.", null], + ["145225119010", "Altmittweida", null], + ["145225119360", "Mittweida, Stadt, Hochschulstadt", null], + ["145225123450", "Ostrau", null], + ["145225123620", "Zschaitz-Ottewig", null], + ["145225126280", "Königsfeld", null], + ["145225126490", "Rochlitz, Stadt", null], + ["145225126530", "Seelitz", null], + ["145225126600", "Zettlitz", null], + ["145225129090", "Dorfchemnitz", null], + ["145225129520", "Sayda, Stadt", null], + ["145230010010", "Adorf/Vogtl., Stadt", null], + ["145230020020", "Auerbach/Vogtl., Stadt", null], + ["145230030030", "Bad Brambach", null], + ["145230040040", "Bad Elster, Stadt", null], + ["145230090090", "Ellefeld", null], + ["145230100100", "Elsterberg, Stadt", null], + ["145230160160", "Klingenthal, Stadt", null], + ["145230170170", "Lengenfeld, Stadt", null], + ["145230200200", "Markneukirchen, Stadt", null], + ["145230245245", "Muldenhammer", null], + ["145230280280", "Neumark", null], + ["145230310310", "Pausa-Mühltroff, Stadt", null], + ["145230320320", "Plauen, Stadt", null], + ["145230330330", "Pöhl", null], + ["145230360360", "Rodewisch, Stadt", null], + ["145230365365", "Rosenbach/Vogtl.", null], + ["145230380380", "Steinberg", null], + ["145230450450", "Weischlitz", null], + ["145235107120", "Falkenstein/Vogtl., Stadt", null], + ["145235107130", "Grünbach", null], + ["145235107290", "Neustadt/Vogtl.", null], + ["145235120190", "Limbach", null], + ["145235120260", "Netzschkau, Stadt", null], + ["145235122060", "Bösenbrunn", null], + ["145235122080", "Eichigt", null], + ["145235122300", "Oelsnitz/Vogtl., Stadt", null], + ["145235122440", "Triebel/Vogtl.", null], + ["145235125150", "Heinsdorfergrund", null], + ["145235125340", "Reichenbach im Vogtland, Stadt", null], + ["145235131230", "Mühlental", null], + ["145235131370", "Schöneck/Vogtl., Stadt", null], + ["145235134270", "Neuensalz", null], + ["145235134430", "Treuen, Stadt", null], + ["145235402050", "Bergen", null], + ["145235402410", "Theuma", null], + ["145235402420", "Tirpersdorf", null], + ["145235402460", "Werda", null], + ["145240020020", "Callenberg", null], + ["145240060060", "Fraureuth", null], + ["145240070070", "Gersdorf", null], + ["145240080080", "Glauchau, Stadt", null], + ["145240090090", "Hartenstein, Stadt", null], + ["145240120120", "Hohenstein-Ernstthal, Stadt", null], + ["145240140140", "Langenbernsdorf", null], + ["145240150150", "Langenweißbach", null], + ["145240170170", "Lichtentanne", null], + ["145240200200", "Mülsen", null], + ["145240210210", "Neukirchen/Pleiße", null], + ["145240230230", "Oberlungwitz, Stadt", null], + ["145240250250", "Reinsdorf", null], + ["145240300300", "Werdau, Stadt", null], + ["145240310310", "Wildenfels, Stadt", null], + ["145240320320", "Wilkau-Haßlau, Stadt", null], + ["145240330330", "Zwickau, Stadt", null], + ["145245104030", "Crimmitschau, Stadt", null], + ["145245104050", "Dennheritz", null], + ["145245111040", "Crinitzberg", null], + ["145245111100", "Hartmannsdorf b. Kirchberg", null], + ["145245111110", "Hirschfeld", null], + ["145245111130", "Kirchberg, Stadt", null], + ["145245114180", "Limbach-Oberfrohna, Stadt", null], + ["145245114220", "Niederfrohna", null], + ["145245118190", "Meerane, Stadt", null], + ["145245118270", "Schönberg", null], + ["145245128010", "Bernsdorf", null], + ["145245128160", "Lichtenstein/Sa., Stadt", null], + ["145245128280", "St. Egidien", null], + ["145245135240", "Oberwiera", null], + ["145245135260", "Remse", null], + ["145245135290", "Waldenburg, Stadt", null], + ["146120000000", "Dresden, Stadt", null], + ["146250010010", "Arnsdorf", null], + ["146250020020", "Bautzen / Budyšin, Stadt", null], + ["146250030030", "Bernsdorf, Stadt", null], + ["146250060060", "Burkau", null], + ["146250090090", "Cunewalde", null], + ["146250100100", "Demitz-Thumitz", null], + ["146250110110", "Doberschau-Gaußig / Dobruša-Huska", null], + ["146250120120", "Elsterheide / Halštrowska Hola", null], + ["146250130130", "Elstra, Stadt", null], + ["146250150150", "Göda / Hodźij", null], + ["146250160160", "Großdubrau / Wulka Dubrawa", null], + ["146250200200", "Großröhrsdorf, Stadt", null], + ["146250220220", "Haselbachtal", null], + ["146250230230", "Hochkirch / Bukecy", null], + ["146250240240", "Hoyerswerda / Wojerecy, Stadt", null], + ["146250250250", "Kamenz / Kamjenc, Stadt", null], + ["146250280280", "Königswartha / Rakecy", null], + ["146250290290", "Kubschütz / Kubšicy", null], + ["146250310310", "Lauta, Stadt", null], + ["146250330330", "Lohsa / Łaz", null], + ["146250340340", "Malschwitz / Malešecy", null], + ["146250380380", "Neukirch/Lausitz", null], + ["146250420420", "Oßling", null], + ["146250430430", "Ottendorf-Okrilla", null], + ["146250480480", "Radeberg, Stadt", null], + ["146250490490", "Radibor / Radwor", null], + ["146250525525", "Schirgiswalde-Kirschau, Stadt", null], + ["146250530530", "Schmölln-Putzkau", null], + ["146250550550", "Schwepnitz", null], + ["146250560560", "Sohland a. d. Spree", null], + ["146250570570", "Spreetal / Sprjewiny Doł", null], + ["146250590590", "Steinigtwolmsdorf", null], + ["146250600600", "Wachau", null], + ["146250610610", "Weißenberg / Wóspork, Stadt", null], + ["146250630630", "Wilthen, Stadt", null], + ["146250640640", "Wittichenau / Kulow, Stadt", null], + ["146255207040", "Bischofswerda, Stadt", null], + ["146255207510", "Rammenau", null], + ["146255211140", "Frankenthal", null], + ["146255211170", "Großharthau", null], + ["146255212190", "Großpostwitz/O.L. / Budestecy", null], + ["146255212390", "Obergurig / Hornja Hórka", null], + ["146255218270", "Königsbrück, Stadt", null], + ["146255218300", "Laußnitz", null], + ["146255218370", "Neukirch", null], + ["146255223360", "Neschwitz / Njeswačidło", null], + ["146255223460", "Puschwitz / Bóšicy", null], + ["146255231180", "Großnaundorf", null], + ["146255231320", "Lichtenberg", null], + ["146255231410", "Ohorn", null], + ["146255231450", "Pulsnitz, Stadt", null], + ["146255231580", "Steina", null], + ["146255501080", "Crostwitz / Chrósćicy", null], + ["146255501350", "Nebelschütz / Njebjelčicy", null], + ["146255501440", "Panschwitz-Kuckau / Pančicy-Kukow", null], + ["146255501470", "Räckelwitz / Worklecy", null], + ["146255501500", "Ralbitz-Rosenthal / Ralbicy-Róžant", null], + ["146260060060", "Boxberg/O.L. / Hamor", null], + ["146260085085", "Ebersbach-Neugersdorf, Stadt", null], + ["146260110110", "Görlitz, Stadt", null], + ["146260180180", "Herrnhut, Stadt", null], + ["146260245245", "Kottmar", null], + ["146260250250", "Krauschwitz i.d. O.L. / Krušwica", null], + ["146260280280", "Leutersdorf", null], + ["146260300300", "Markersdorf", null], + ["146260310310", "Mittelherwigsdorf", null], + ["146260370370", "Niesky, Stadt", null], + ["146260390390", "Oderwitz", null], + ["146260420420", "Ostritz, Stadt", null], + ["146260530530", "Seifhennersdorf, Stadt", null], + ["146260610610", "Zittau, Stadt", null], + ["146265203010", "Bad Muskau / Mužakow, Stadt", null], + ["146265203100", "Gablenz / Jabłońc", null], + ["146265206030", "Bernstadt a. d. Eigen, Stadt", null], + ["146265206500", "Schönau-Berzdorf a. d. Eigen", null], + ["146265214140", "Großschönau", null], + ["146265214170", "Hainewalde", null], + ["146265220150", "Großschweidnitz", null], + ["146265220270", "Lawalde", null], + ["146265220290", "Löbau, Stadt", null], + ["146265220470", "Rosenbach", null], + ["146265224070", "Dürrhennersdorf", null], + ["146265224350", "Neusalza-Spremberg, Stadt", null], + ["146265224510", "Schönbach", null], + ["146265227050", "Bertsdorf-Hörnitz", null], + ["146265227210", "Jonsdorf, Kurort", null], + ["146265227400", "Olbersdorf", null], + ["146265227430", "Oybin", null], + ["146265228020", "Beiersdorf", null], + ["146265228410", "Oppach", null], + ["146265232240", "Königshain", null], + ["146265232450", "Reichenbach/O.L., Stadt", null], + ["146265232570", "Vierkirchen", null], + ["146265233260", "Kreba-Neudorf / Chrjebja-Nowa Wjes", null], + ["146265233460", "Rietschen / Rěčicy", null], + ["146265235160", "Hähnichen", null], + ["146265235480", "Rothenburg/O.L., Stadt", null], + ["146265237120", "Groß Düben / Dźěwin", null], + ["146265237490", "Schleife / Slepo", null], + ["146265237560", "Trebendorf / Trjebin", null], + ["146265242590", "Weißkeißel / Wuskidź", null], + ["146265242600", "Weißwasser/O.L., Stadt / Běła Woda", null], + ["146265502190", "Hohendubrau / Wysoka Dubrawa", null], + ["146265502320", "Mücka / Mikow", null], + ["146265502440", "Quitzdorf am See", null], + ["146265502580", "Waldhufen", null], + ["146265503200", "Horka", null], + ["146265503230", "Kodersdorf", null], + ["146265503330", "Neißeaue", null], + ["146265503520", "Schöpstal", null], + ["146270010010", "Coswig, Stadt", null], + ["146270020020", "Diera-Zehren", null], + ["146270030030", "Ebersbach", null], + ["146270050050", "Gröditz, Stadt", null], + ["146270060060", "Großenhain, Stadt", null], + ["146270070070", "Hirschstein", null], + ["146270080080", "Käbschütztal", null], + ["146270100100", "Klipphausen", null], + ["146270130130", "Lommatzsch, Stadt", null], + ["146270140140", "Meißen, Stadt", null], + ["146270150150", "Moritzburg", null], + ["146270170170", "Niederau", null], + ["146270180180", "Nossen, Stadt", null], + ["146270200200", "Priestewitz", null], + ["146270210210", "Radebeul, Stadt", null], + ["146270220220", "Radeburg, Stadt", null], + ["146270230230", "Riesa, Stadt", null], + ["146270260260", "Stauchitz", null], + ["146270270270", "Strehla, Stadt", null], + ["146270290290", "Thiendorf", null], + ["146270310310", "Weinböhla", null], + ["146270360360", "Zeithain", null], + ["146275225040", "Glaubitz", null], + ["146275225190", "Nünchritz", null], + ["146275234240", "Röderaue", null], + ["146275234340", "Wülknitz", null], + ["146275238110", "Lampertswalde", null], + ["146275238250", "Schönfeld", null], + ["146280050050", "Bannewitz", null], + ["146280060060", "Dippoldiswalde, Stadt", null], + ["146280100100", "Dürrröhrsdorf-Dittersbach", null], + ["146280110110", "Freital, Stadt", null], + ["146280130130", "Glashütte, Stadt", null], + ["146280160160", "Heidenau, Stadt", null], + ["146280190190", "Hohnstein, Stadt", null], + ["146280220220", "Kreischa", null], + ["146280260260", "Neustadt in Sachsen, Stadt", null], + ["146280300300", "Rabenau, Stadt", null], + ["146280360360", "Sebnitz, Stadt", null], + ["146280380380", "Stolpen, Stadt", null], + ["146280410410", "Wilsdruff, Stadt", null], + ["146285201010", "Altenberg, Stadt", null], + ["146285201170", "Hermsdorf/Erzgeb.", null], + ["146285202020", "Bad Gottleuba-Berggießhübel, Stadt", null], + ["146285202040", "Bahretal", null], + ["146285202230", "Liebstadt, Stadt", null], + ["146285204030", "Bad Schandau, Stadt", null], + ["146285204320", "Rathmannsdorf", null], + ["146285204330", "Reinhardtsdorf-Schöna", null], + ["146285209080", "Dohna, Stadt", null], + ["146285209250", "Müglitztal", null], + ["146285219140", "Gohrisch", null], + ["146285219210", "Königstein/Sächs. Schw., Stadt", null], + ["146285219310", "Rathen, Kurort", null], + ["146285219340", "Rosenthal-Bielatal", null], + ["146285219390", "Struppen", null], + ["146285221240", "Lohmen", null], + ["146285221370", "Stadt Wehlen, Stadt", null], + ["146285229070", "Dohma", null], + ["146285229270", "Pirna, Stadt", null], + ["146285230150", "Hartmannsdorf-Reichenau", null], + ["146285230205", "Klingenberg", null], + ["146285240090", "Dorfhain", null], + ["146285240400", "Tharandt, Stadt", null], + ["147130000000", "Leipzig, Stadt", null], + ["147290030030", "Bennewitz", null], + ["147290040040", "Böhlen, Stadt", null], + ["147290050050", "Borna, Stadt", null], + ["147290060060", "Borsdorf", null], + ["147290070070", "Brandis, Stadt", null], + ["147290080080", "Colditz, Stadt", null], + ["147290140140", "Frohburg, Stadt", null], + ["147290150150", "Geithain, Stadt", null], + ["147290160160", "Grimma, Stadt", null], + ["147290170170", "Groitzsch, Stadt", null], + ["147290190190", "Großpösna", null], + ["147290220220", "Kitzscher, Stadt", null], + ["147290245245", "Lossatal", null], + ["147290250250", "Machern", null], + ["147290260260", "Markkleeberg, Stadt", null], + ["147290270270", "Markranstädt, Stadt", null], + ["147290320320", "Neukieritzsch", null], + ["147290360360", "Regis-Breitingen, Stadt", null], + ["147290370370", "Rötha, Stadt", null], + ["147290380380", "Thallwitz", null], + ["147290400400", "Trebsen/Mulde, Stadt", null], + ["147290410410", "Wurzen, Stadt", null], + ["147290430430", "Zwenkau, Stadt", null], + ["147295301010", "Bad Lausick, Stadt", null], + ["147295301330", "Otterwisch", null], + ["147295307020", "Belgershain", null], + ["147295307300", "Naunhof, Stadt", null], + ["147295307340", "Parthenstein", null], + ["147295308100", "Elstertrebnitz", null], + ["147295308350", "Pegau, Stadt", null], + ["147300020020", "Bad Düben, Stadt", null], + ["147300045045", "Belgern-Schildau, Stadt", null], + ["147300050050", "Cavertitz", null], + ["147300060060", "Dahlen, Stadt", null], + ["147300070070", "Delitzsch, Stadt", null], + ["147300080080", "Doberschütz", null], + ["147300110110", "Eilenburg, Stadt", null], + ["147300160160", "Laußig", null], + ["147300170170", "Liebschützberg", null], + ["147300180180", "Löbnitz", null], + ["147300190190", "Mockrehna", null], + ["147300200200", "Mügeln, Stadt", null], + ["147300210210", "Naundorf", null], + ["147300230230", "Oschatz, Stadt", null], + ["147300250250", "Rackwitz", null], + ["147300270270", "Schkeuditz, Stadt", null], + ["147300300300", "Taucha, Stadt", null], + ["147300330330", "Wermsdorf", null], + ["147300340340", "Wiedemar", null], + ["147305302010", "Arzberg", null], + ["147305302030", "Beilrode", null], + ["147305303090", "Dommitzsch, Stadt", null], + ["147305303120", "Elsnig", null], + ["147305303320", "Trossin", null], + ["147305306150", "Krostitz", null], + ["147305306280", "Schönwölkau", null], + ["147305311100", "Dreiheide", null], + ["147305311310", "Torgau, Stadt", null], + ["147305601140", "Jesewitz", null], + ["147305601360", "Zschepplin", null], + ["150010000000", "Dessau-Roßlau, Stadt", null], + ["150020000000", "Halle (Saale), Stadt", null], + ["150030000000", "Magdeburg, Landeshauptstadt", null], + ["150810030030", "Arendsee (Altmark), Stadt", null], + ["150810135135", "Gardelegen, Hansestadt", null], + ["150810240240", "Kalbe (Milde), Stadt", null], + ["150810280280", "Klötze, Stadt", null], + ["150810455455", "Salzwedel, Hansestadt", null], + ["150815051026", "Apenburg-Winterfeld, Flecken", null], + ["150815051045", "Beetzendorf", null], + ["150815051095", "Dähre", null], + ["150815051105", "Diesdorf, Flecken", null], + ["150815051225", "Jübar", null], + ["150815051290", "Kuhfelde", null], + ["150815051440", "Rohrberg", null], + ["150815051545", "Wallstawe", null], + ["150820005005", "Aken (Elbe), Stadt", null], + ["150820015015", "Bitterfeld-Wolfen, Stadt", null], + ["150820180180", "Köthen (Anhalt), Stadt", null], + ["150820241241", "Muldestausee", null], + ["150820256256", "Osternienburger Land", null], + ["150820301301", "Raguhn-Jeßnitz, Stadt", null], + ["150820340340", "Sandersdorf-Brehna, Stadt", null], + ["150820377377", "Südliches Anhalt, Stadt", null], + ["150820430430", "Zerbst/Anhalt, Stadt", null], + ["150820440440", "Zörbig, Stadt", null], + ["150830040040", "Barleben", null], + ["150830270270", "Haldensleben, Stadt", null], + ["150830298298", "Hohe Börde", null], + ["150830390390", "Niedere Börde", null], + ["150830411411", "Oebisfelde-Weferlingen, Stadt", null], + ["150830415415", "Oschersleben (Bode), Stadt", null], + ["150830490490", "Sülzetal", null], + ["150830531531", "Wanzleben-Börde, Stadt", null], + ["150830565565", "Wolmirstedt, Stadt", null], + ["150835051030", "Angern", null], + ["150835051120", "Burgstall", null], + ["150835051130", "Colbitz", null], + ["150835051361", "Loitsche-Heinrichsberg", null], + ["150835051440", "Rogätz", null], + ["150835051557", "Westheide", null], + ["150835051580", "Zielitz", null], + ["150835052020", "Altenhausen", null], + ["150835052060", "Beendorf", null], + ["150835052115", "Bülstringen", null], + ["150835052125", "Calvörde", null], + ["150835052205", "Erxleben", null], + ["150835052230", "Flechtingen", null], + ["150835052323", "Ingersleben", null], + ["150835053190", "Eilsleben", null], + ["150835053275", "Harbke", null], + ["150835053320", "Hötensleben", null], + ["150835053485", "Sommersdorf", null], + ["150835053505", "Ummendorf", null], + ["150835053515", "Völpke", null], + ["150835053535", "Wefensleben", null], + ["150835054025", "Am Großen Bruch", null], + ["150835054035", "Ausleben", null], + ["150835054245", "Gröningen, Stadt", null], + ["150835054355", "Kroppenstedt, Stadt", null], + ["150840130130", "Elsteraue", null], + ["150840235235", "Hohenmölsen, Stadt", null], + ["150840315315", "Lützen, Stadt", null], + ["150840355355", "Naumburg (Saale), Stadt", null], + ["150840490490", "Teuchern, Stadt", null], + ["150840550550", "Weißenfels, Stadt", null], + ["150840590590", "Zeitz, Stadt", null], + ["150845051012", "An der Poststraße", null], + ["150845051015", "Bad Bibra, Stadt", null], + ["150845051125", "Eckartsberga, Stadt", null], + ["150845051132", "Finne", null], + ["150845051133", "Finneland", null], + ["150845051246", "Kaiserpfalz", null], + ["150845051282", "Lanitz-Hassel-Tal", null], + ["150845052115", "Droyßig", null], + ["150845052207", "Gutenborn", null], + ["150845052275", "Kretzschau", null], + ["150845052442", "Schnaudertal", null], + ["150845052565", "Wetterzeube", null], + ["150845053025", "Balgstädt", null], + ["150845053135", "Freyburg (Unstrut), Stadt", null], + ["150845053150", "Gleina", null], + ["150845053170", "Goseck", null], + ["150845053250", "Karsdorf", null], + ["150845053285", "Laucha an der Unstrut, Stadt", null], + ["150845053360", "Nebra (Unstrut), Stadt", null], + ["150845054013", "Meineweh", null], + ["150845054335", "Mertendorf", null], + ["150845054341", "Molauer Land", null], + ["150845054375", "Osterfeld, Stadt", null], + ["150845054445", "Schönburg", null], + ["150845054470", "Stößen, Stadt", null], + ["150845054560", "Wethau", null], + ["150850040040", "Ballenstedt, Stadt", null], + ["150850055055", "Blankenburg (Harz), Stadt", null], + ["150850110110", "Falkenstein/Harz, Stadt", null], + ["150850135135", "Halberstadt, Stadt", null], + ["150850145145", "Harzgerode, Stadt", null], + ["150850185185", "Huy", null], + ["150850190190", "Ilsenburg (Harz), Stadt", null], + ["150850227227", "Nordharz", null], + ["150850228228", "Oberharz am Brocken, Stadt", null], + ["150850230230", "Osterwieck, Stadt", null], + ["150850235235", "Quedlinburg, Welterbestadt", null], + ["150850330330", "Thale, Stadt", null], + ["150850370370", "Wernigerode, Stadt", null], + ["150855051090", "Ditfurt", null], + ["150855051125", "Groß Quenstedt", null], + ["150855051140", "Harsleben", null], + ["150855051160", "Hedersleben", null], + ["150855051285", "Schwanebeck, Stadt", null], + ["150855051287", "Selke-Aue", null], + ["150855051365", "Wegeleben, Stadt", null], + ["150860005005", "Biederitz", null], + ["150860015015", "Burg, Stadt", null], + ["150860035035", "Elbe-Parey", null], + ["150860040040", "Genthin, Stadt", null], + ["150860055055", "Gommern, Stadt", null], + ["150860080080", "Jerichow, Stadt", null], + ["150860140140", "Möckern, Stadt", null], + ["150860145145", "Möser", null], + ["150870015015", "Allstedt, Stadt", null], + ["150870031031", "Arnstein, Stadt", null], + ["150870130130", "Eisleben, Lutherstadt", null], + ["150870165165", "Gerbstedt, Stadt", null], + ["150870220220", "Hettstedt, Stadt", null], + ["150870275275", "Mansfeld, Stadt", null], + ["150870370370", "Sangerhausen, Stadt", null], + ["150870386386", "Seegebiet Mansfelder Land", null], + ["150870412412", "Südharz", null], + ["150875051055", "Berga", null], + ["150875051101", "Brücken-Hackpfüffel", null], + ["150875051125", "Edersleben", null], + ["150875051250", "Kelbra (Kyffhäuser), Stadt", null], + ["150875051440", "Wallhausen", null], + ["150875052010", "Ahlsdorf", null], + ["150875052045", "Benndorf", null], + ["150875052070", "Blankenheim", null], + ["150875052075", "Bornstedt", null], + ["150875052205", "Helbra", null], + ["150875052210", "Hergisdorf", null], + ["150875052260", "Klostermansfeld", null], + ["150875052470", "Wimmelburg", null], + ["150880020020", "Bad Dürrenberg, Solestadt", null], + ["150880025025", "Bad Lauchstädt, Goethestadt", null], + ["150880065065", "Braunsbedra, Stadt", null], + ["150880150150", "Kabelsketal", null], + ["150880195195", "Landsberg, Stadt", null], + ["150880205205", "Leuna, Stadt", null], + ["150880216216", "Wettin-Löbejün, Stadt", null], + ["150880220220", "Merseburg, Stadt", null], + ["150880235235", "Mücheln (Geiseltal), Stadt", null], + ["150880295295", "Petersberg", null], + ["150880305305", "Querfurt, Stadt", null], + ["150880319319", "Salzatal", null], + ["150880330330", "Schkopau", null], + ["150880365365", "Teutschenthal", null], + ["150885051030", "Barnstädt", null], + ["150885051100", "Farnstädt", null], + ["150885051250", "Nemsdorf-Göhrendorf", null], + ["150885051265", "Obhausen", null], + ["150885051340", "Schraplau, Stadt", null], + ["150885051355", "Steigra", null], + ["150890015015", "Aschersleben, Stadt", null], + ["150890026026", "Barby, Stadt", null], + ["150890030030", "Bernburg (Saale), Stadt", null], + ["150890042042", "Bördeland", null], + ["150890055055", "Calbe (Saale), Stadt", null], + ["150890175175", "Hecklingen, Stadt", null], + ["150890195195", "Könnern, Stadt", null], + ["150890235235", "Nienburg (Saale), Stadt", null], + ["150890305305", "Schönebeck (Elbe), Stadt", null], + ["150890307307", "Seeland, Stadt", null], + ["150890310310", "Staßfurt, Stadt", null], + ["150895051041", "Bördeaue", null], + ["150895051043", "Börde-Hakel", null], + ["150895051045", "Borne", null], + ["150895051075", "Egeln, Stadt", null], + ["150895051365", "Wolmirsleben", null], + ["150895052005", "Alsleben (Saale), Stadt", null], + ["150895052130", "Giersleben", null], + ["150895052165", "Güsten, Stadt", null], + ["150895052185", "Ilberstedt", null], + ["150895052245", "Plötzkau", null], + ["150900070070", "Bismark (Altmark), Stadt", null], + ["150900225225", "Havelberg, Hansestadt", null], + ["150900415415", "Osterburg (Altmark), Hansestadt", null], + ["150900535535", "Stendal, Hansestadt", null], + ["150900546546", "Tangerhütte, Stadt", null], + ["150900550550", "Tangermünde, Stadt", null], + ["150905051010", "Arneburg, Stadt", null], + ["150905051135", "Eichstedt (Altmark)", null], + ["150905051180", "Goldbeck", null], + ["150905051220", "Hassel", null], + ["150905051245", "Hohenberg-Krusemark", null], + ["150905051270", "Iden", null], + ["150905051435", "Rochau", null], + ["150905051610", "Werben (Elbe), Hansestadt", null], + ["150905052285", "Kamern", null], + ["150905052310", "Klietz", null], + ["150905052445", "Sandau (Elbe), Stadt", null], + ["150905052485", "Schollene", null], + ["150905052500", "Schönhausen (Elbe)", null], + ["150905052631", "Wust-Fischbeck", null], + ["150905053003", "Aland", null], + ["150905053007", "Altmärkische Höhe", null], + ["150905053008", "Altmärkische Wische", null], + ["150905053520", "Seehausen (Altmark), Hansestadt", null], + ["150905053635", "Zehrental", null], + ["150910010010", "Annaburg, Stadt", null], + ["150910020020", "Bad Schmiedeberg, Stadt", null], + ["150910060060", "Coswig (Anhalt), Stadt", null], + ["150910110110", "Gräfenhainichen, Stadt", null], + ["150910145145", "Jessen (Elster), Stadt", null], + ["150910160160", "Kemberg, Stadt", null], + ["150910241241", "Oranienbaum-Wörlitz, Stadt", null], + ["150910375375", "Wittenberg, Lutherstadt", null], + ["150910391391", "Zahna-Elster, Stadt", null], + ["160510000000", "Erfurt, Stadt", null], + ["160520000000", "Gera, Stadt", null], + ["160530000000", "Jena, Stadt", null], + ["160540000000", "Suhl, Stadt", null], + ["160550000000", "Weimar, Stadt", null], + ["160610045045", "Heilbad Heiligenstadt, Stadt", null], + ["160610074074", "Niederorschel", null], + ["160610115115", "Leinefelde-Worbis, Stadt", null], + ["160610116116", "Am Ohmberg", null], + ["160610117117", "Sonnenstein", null], + ["160610118118", "Dingelstädt, Stadt", null], + ["160615001003", "Berlingerode", null], + ["160615001015", "Brehme", null], + ["160615001026", "Ecklingerode", null], + ["160615001031", "Ferna", null], + ["160615001094", "Tastungen", null], + ["160615001103", "Wehnde", null], + ["160615001114", "Teistungen", null], + ["160615006017", "Breitenworbis", null], + ["160615006019", "Buhla", null], + ["160615006037", "Gernrode", null], + ["160615006044", "Haynrode", null], + ["160615006058", "Kirchworbis", null], + ["160615008001", "Arenshausen", null], + ["160615008014", "Bornhagen", null], + ["160615008021", "Burgwalde", null], + ["160615008032", "Freienhagen", null], + ["160615008033", "Fretterode", null], + ["160615008036", "Gerbershausen", null], + ["160615008048", "Hohengandern", null], + ["160615008057", "Kirchgandern", null], + ["160615008066", "Lindewerra", null], + ["160615008069", "Marth", null], + ["160615008078", "Rohrberg", null], + ["160615008082", "Rustenfelde", null], + ["160615008083", "Schachtebich", null], + ["160615008102", "Wahlhausen", null], + ["160615009012", "Bodenrode-Westhausen", null], + ["160615009034", "Geisleden", null], + ["160615009039", "Glasehausen", null], + ["160615009047", "Heuthen", null], + ["160615009049", "Hohes Kreuz", null], + ["160615009076", "Reinholterode", null], + ["160615009089", "Steinbach", null], + ["160615009107", "Wingerode", null], + ["160615012002", "Asbach-Sickenberg", null], + ["160615012007", "Birkenfelde", null], + ["160615012024", "Dietzenrode/Vatterode", null], + ["160615012028", "Eichstruth", null], + ["160615012065", "Lenterode", null], + ["160615012067", "Lutter", null], + ["160615012068", "Mackenrode", null], + ["160615012077", "Röhrig", null], + ["160615012084", "Schönhagen", null], + ["160615012091", "Steinheuterode", null], + ["160615012096", "Thalwenden", null], + ["160615012097", "Uder", null], + ["160615012111", "Wüstheuterode", null], + ["160615013018", "Büttstedt", null], + ["160615013027", "Effelder", null], + ["160615013041", "Großbartloff", null], + ["160615013063", "Küllstedt", null], + ["160615013101", "Wachstedt", null], + ["160615014023", "Dieterode", null], + ["160615014035", "Geismar", null], + ["160615014056", "Kella", null], + ["160615014062", "Krombach", null], + ["160615014075", "Pfaffschwende", null], + ["160615014085", "Schwobfeld", null], + ["160615014086", "Sickerode", null], + ["160615014098", "Volkerode", null], + ["160615014105", "Wiesenfeld", null], + ["160615014113", "Schimberg", null], + ["160620005005", "Ellrich, Stadt", null], + ["160620041041", "Nordhausen, Stadt", null], + ["160620049049", "Sollstedt", null], + ["160620062062", "Hohenstein", null], + ["160620063063", "Werther", null], + ["160620065065", "Harztor", null], + ["160625053008", "Görsbach", null], + ["160625053054", "Urbach", null], + ["160625053064", "Heringen/Helme, Stadt", null], + ["160625054009", "Großlohra", null], + ["160625054024", "Kehmstedt", null], + ["160625054026", "Kleinfurra", null], + ["160625054033", "Lipprechterode", null], + ["160625054037", "Niedergebra", null], + ["160625054066", "Bleicherode, Stadt", null], + ["160630004004", "Barchfeld-Immelborn", null], + ["160630076076", "Treffurt, Stadt", null], + ["160630078078", "Unterbreizbach", null], + ["160630082082", "Vacha, Stadt", null], + ["160630092092", "Wutha-Farnroda", null], + ["160630097097", "Gerstungen", null], + ["160630098098", "Hörselberg-Hainich", null], + ["160630099099", "Bad Liebenstein, Stadt", null], + ["160630101101", "Krayenberggemeinde", null], + ["160630103103", "Werra-Suhl-Tal, Stadt", null], + ["160630105105", "Eisenach, Stadt", null], + ["160635006006", "Berka v. d. Hainich", null], + ["160635006008", "Bischofroda", null], + ["160635006028", "Frankenroda", null], + ["160635006037", "Hallungen", null], + ["160635006046", "Krauthausen", null], + ["160635006049", "Lauterbach", null], + ["160635006058", "Nazza", null], + ["160635006104", "Amt Creuzburg, Stadt", null], + ["160635051003", "Bad Salzungen, Stadt", null], + ["160635051051", "Leimbach", null], + ["160635056011", "Buttlar", null], + ["160635056032", "Geisa, Stadt", null], + ["160635056033", "Gerstengrund", null], + ["160635056068", "Schleid", null], + ["160635057066", "Ruhla, Stadt", null], + ["160635057071", "Seebach", null], + ["160635059015", "Dermbach", null], + ["160635059023", "Empfertshausen", null], + ["160635059062", "Oechsen", null], + ["160635059084", "Weilar", null], + ["160635059086", "Wiesenthal", null], + ["160640003003", "Bad Langensalza, Stadt", null], + ["160640014014", "Dünwald", null], + ["160640046046", "Mühlhausen/Thüringen, Stadt", null], + ["160640071071", "Unstruttal", null], + ["160640072072", "Menteroda", null], + ["160640073073", "Anrode", null], + ["160645001004", "Bad Tennstedt, Stadt", null], + ["160645001005", "Ballhausen", null], + ["160645001007", "Blankenburg", null], + ["160645001009", "Bruchstedt", null], + ["160645001021", "Haussömmern", null], + ["160645001027", "Hornsömmern", null], + ["160645001033", "Kirchheilingen", null], + ["160645001038", "Kutzleben", null], + ["160645001045", "Mittelsömmern", null], + ["160645001061", "Sundhausen", null], + ["160645001062", "Tottleben", null], + ["160645001064", "Urleben", null], + ["160645051019", "Großvargula", null], + ["160645051022", "Herbsleben", null], + ["160645052055", "Rodeberg", null], + ["160645052074", "Südeichsfeld", null], + ["160645053032", "Kammerforst", null], + ["160645053053", "Oppershausen", null], + ["160645053075", "Vogtei", null], + ["160645054058", "Schönstedt", null], + ["160645054076", "Unstrut-Hainich", null], + ["160645055037", "Körner", null], + ["160645055043", "Marolterode", null], + ["160645055077", "Nottertal-Heilinger Höhen, Stadt", null], + ["160650003003", "Bad Frankenhausen/Kyffhäuser, Stadt", null], + ["160650032032", "Helbedündorf", null], + ["160650067067", "Sondershausen, Stadt", null], + ["160650085085", "Kyffhäuserland", null], + ["160650087087", "Roßleben-Wiehe, Stadt", null], + ["160650089089", "Greußen, Stadt", null], + ["160655002012", "Clingen, Stadt", null], + ["160655002048", "Niederbösa", null], + ["160655002051", "Oberbösa", null], + ["160655002074", "Topfstedt", null], + ["160655002075", "Trebra", null], + ["160655002077", "Wasserthaleben", null], + ["160655002079", "Westgreußen", null], + ["160655052001", "Abtsbessingen", null], + ["160655052005", "Bellstedt", null], + ["160655052014", "Ebeleben, Stadt", null], + ["160655052018", "Freienbessingen", null], + ["160655052038", "Holzsußra", null], + ["160655052058", "Rockstedt", null], + ["160655055008", "Borxleben", null], + ["160655055019", "Gehofen", null], + ["160655055042", "Kalbsrieth", null], + ["160655055046", "Mönchpfiffel-Nikolausrieth", null], + ["160655055056", "Reinsdorf", null], + ["160655055086", "Artern, Stadt", null], + ["160655056016", "Etzleben", null], + ["160655056052", "Oberheldrungen", null], + ["160655056088", "An der Schmücke, Stadt", null], + ["160660023023", "Floh-Seligenthal", null], + ["160660047047", "Oberhof, Stadt", null], + ["160660063063", "Schmalkalden, Kurort, Stadt", null], + ["160660069069", "Steinbach-Hallenberg, Kurort, Stadt", null], + ["160660074074", "Brotterode-Trusetal, Stadt", null], + ["160660092092", "Zella-Mehlis, Stadt", null], + ["160660093093", "Rhönblick", null], + ["160660094094", "Grabfeld", null], + ["160665005012", "Birx", null], + ["160665005019", "Erbenhausen", null], + ["160665005024", "Frankenheim/Rhön", null], + ["160665005052", "Oberweid", null], + ["160665005095", "Kaltennordheim, Stadt", null], + ["160665013025", "Friedelshausen", null], + ["160665013041", "Mehmels", null], + ["160665013064", "Schwallungen", null], + ["160665013086", "Wasungen, Stadt", null], + ["160665014005", "Belrieth", null], + ["160665014015", "Christes", null], + ["160665014016", "Dillstädt", null], + ["160665014017", "Einhausen", null], + ["160665014018", "Ellingshausen", null], + ["160665014038", "Kühndorf", null], + ["160665014039", "Leutersdorf", null], + ["160665014045", "Neubrunn", null], + ["160665014049", "Obermaßfeld-Grimmenthal", null], + ["160665014057", "Ritschenhausen", null], + ["160665014058", "Rohr", null], + ["160665014065", "Schwarza", null], + ["160665014079", "Utendorf", null], + ["160665014081", "Vachdorf", null], + ["160665050042", "Meiningen, Stadt", null], + ["160665050056", "Rippershausen", null], + ["160665050073", "Sülzfeld", null], + ["160665050076", "Untermaßfeld", null], + ["160665051013", "Breitungen/Werra", null], + ["160665051022", "Fambach", null], + ["160665051059", "Rosa", null], + ["160665051061", "Roßdorf", null], + ["160670019019", "Friedrichroda, Stadt", null], + ["160670029029", "Gotha, Stadt", null], + ["160670064064", "Bad Tabarz", null], + ["160670065065", "Tambach-Dietharz/Thür. Wald, Stadt", null], + ["160670072072", "Waltershausen, Stadt", null], + ["160670087087", "Nesse-Apfelstädt", null], + ["160670088088", "Hörsel", null], + ["160675007004", "Bienstädt", null], + ["160675007016", "Eschenbergen", null], + ["160675007022", "Friemar", null], + ["160675007047", "Molschleben", null], + ["160675007052", "Nottleben", null], + ["160675007055", "Pferdingsleben", null], + ["160675007068", "Tröchtelborn", null], + ["160675007071", "Tüttleben", null], + ["160675007082", "Zimmernsupra", null], + ["160675012009", "Dachwig", null], + ["160675012011", "Döllstädt", null], + ["160675012026", "Gierstädt", null], + ["160675012033", "Großfahner", null], + ["160675012067", "Tonna", null], + ["160675050044", "Luisenthal", null], + ["160675050053", "Ohrdruf, Stadt", null], + ["160675052059", "Schwabhausen", null], + ["160675052089", "Drei Gleichen", null], + ["160675053063", "Sonneborn", null], + ["160675053091", "Nessetal", null], + ["160675054013", "Emleben", null], + ["160675054036", "Herrenhof", null], + ["160675054092", "Georgenthal", null], + ["160680034034", "Kölleda, Stadt", null], + ["160680051051", "Sömmerda, Stadt", null], + ["160680058058", "Weißensee, Stadt", null], + ["160680063063", "Buttstädt", null], + ["160685002002", "Andisleben", null], + ["160685002014", "Gebesee, Stadt", null], + ["160685002045", "Ringleben", null], + ["160685002057", "Walschleben", null], + ["160685005005", "Büchel", null], + ["160685005015", "Griefstedt", null], + ["160685005022", "Günstedt", null], + ["160685005043", "Riethgen", null], + ["160685005064", "Kindelbrück", null], + ["160685006019", "Großneuhausen", null], + ["160685006033", "Kleinneuhausen", null], + ["160685006041", "Ostramondra", null], + ["160685006042", "Rastenberg, Stadt", null], + ["160685009013", "Gangloffsömmern", null], + ["160685009025", "Haßleben", null], + ["160685009044", "Riethnordhausen", null], + ["160685009049", "Schwerstedt", null], + ["160685009053", "Straußfurt", null], + ["160685009059", "Werningshausen", null], + ["160685009062", "Wundersleben", null], + ["160685012001", "Alperstedt", null], + ["160685012007", "Eckstedt", null], + ["160685012017", "Großmölsen", null], + ["160685012021", "Großrudestedt", null], + ["160685012032", "Kleinmölsen", null], + ["160685012036", "Markvippach", null], + ["160685012037", "Nöda", null], + ["160685012039", "Ollendorf", null], + ["160685012048", "Schloßvippach", null], + ["160685012052", "Sprötau", null], + ["160685012055", "Udestedt", null], + ["160685012056", "Vogelsberg", null], + ["160685050009", "Elxleben", null], + ["160685050061", "Witterda", null], + ["160690012012", "Eisfeld, Stadt", null], + ["160690024024", "Hildburghausen, Stadt", null], + ["160690042042", "Schleusegrund", null], + ["160690043043", "Schleusingen, Stadt", null], + ["160690053053", "Veilsdorf", null], + ["160690061061", "Masserberg", null], + ["160690062062", "Römhild, Stadt", null], + ["160695002001", "Ahlstädt", null], + ["160695002003", "Beinerstadt", null], + ["160695002004", "Bischofrod", null], + ["160695002008", "Dingsleben", null], + ["160695002009", "Ehrenberg", null], + ["160695002011", "Eichenberg", null], + ["160695002016", "Grimmelshausen", null], + ["160695002017", "Grub", null], + ["160695002021", "Henfstädt", null], + ["160695002025", "Kloster Veßra", null], + ["160695002026", "Lengfeld", null], + ["160695002028", "Marisfeld", null], + ["160695002035", "Oberstadt", null], + ["160695002037", "Reurieth", null], + ["160695002044", "Schmeheim", null], + ["160695002047", "St.Bernhard", null], + ["160695002051", "Themar, Stadt", null], + ["160695004041", "Schlechtsart", null], + ["160695004046", "Schweickershausen", null], + ["160695004049", "Straufhain", null], + ["160695004052", "Ummerstadt, Stadt", null], + ["160695004056", "Westhausen", null], + ["160695004063", "Heldburg, Stadt", null], + ["160695051006", "Brünn/Thür.", null], + ["160695051058", "Auengrund", null], + ["160700004004", "Arnstadt, Stadt", null], + ["160700028028", "Amt Wachsenburg", null], + ["160700029029", "Ilmenau, Stadt", null], + ["160700048048", "Stadtilm, Stadt", null], + ["160700057057", "Geratal", null], + ["160700058058", "Großbreitenbach, Stadt", null], + ["160705002011", "Elgersburg", null], + ["160705002034", "Martinroda", null], + ["160705002043", "Plaue, Stadt", null], + ["160705009001", "Alkersleben", null], + ["160705009006", "Bösleben-Wüllersleben", null], + ["160705009008", "Dornheim", null], + ["160705009012", "Elleben", null], + ["160705009013", "Elxleben", null], + ["160705009041", "Osthausen-Wülfershausen", null], + ["160705009054", "Witzleben", null], + ["160710001001", "Apolda, Stadt", null], + ["160710003003", "Bad Berka, Stadt", null], + ["160710008008", "Blankenhain, Stadt", null], + ["160710101101", "Ilmtal-Weinstraße", null], + ["160710103103", "Grammetal", null], + ["160715007032", "Hohenfelden", null], + ["160715007043", "Klettbach", null], + ["160715007046", "Kranichfeld, Stadt", null], + ["160715007059", "Nauendorf", null], + ["160715007079", "Rittersdorf", null], + ["160715007087", "Tonndorf", null], + ["160715008009", "Buchfart", null], + ["160715008013", "Döbritschen", null], + ["160715008019", "Frankendorf", null], + ["160715008025", "Großschwabhausen", null], + ["160715008027", "Hammerstedt", null], + ["160715008031", "Hetschburg", null], + ["160715008037", "Kapellendorf", null], + ["160715008038", "Kiliansroda", null], + ["160715008042", "Kleinschwabhausen", null], + ["160715008049", "Lehnstedt", null], + ["160715008053", "Magdala, Stadt", null], + ["160715008055", "Mechelroda", null], + ["160715008056", "Mellingen", null], + ["160715008071", "Oettern", null], + ["160715008089", "Umpferstedt", null], + ["160715008093", "Vollersroda", null], + ["160715008095", "Wiegendorf", null], + ["160715051004", "Bad Sulza, Stadt", null], + ["160715051015", "Eberstedt", null], + ["160715051022", "Großheringen", null], + ["160715051064", "Niedertrebra", null], + ["160715051069", "Obertrebra", null], + ["160715051077", "Rannstedt", null], + ["160715051083", "Schmiedehausen", null], + ["160715053005", "Ballstedt", null], + ["160715053017", "Ettersburg", null], + ["160715053061", "Neumark, Stadt", null], + ["160715053102", "Am Ettersberg", null], + ["160720011011", "Lauscha, Stadt", null], + ["160720015015", "Schalkau, Stadt", null], + ["160720018018", "Sonneberg, Stadt", null], + ["160720019019", "Steinach, Stadt", null], + ["160720023023", "Frankenblick", null], + ["160720024024", "Föritztal", null], + ["160725051006", "Goldisthal", null], + ["160725051013", "Neuhaus am Rennweg, Stadt", null], + ["160730005005", "Bad Blankenburg, Stadt", null], + ["160730076076", "Rudolstadt, Stadt", null], + ["160730077077", "Saalfeld/Saale, Stadt", null], + ["160730106106", "Leutenberg, Stadt", null], + ["160730109109", "Uhlstädt-Kirchhasel", null], + ["160730111111", "Unterwellenborn", null], + ["160735005028", "Gräfenthal, Stadt", null], + ["160735005046", "Lehesten, Stadt", null], + ["160735005067", "Probstzella", null], + ["160735012013", "Cursdorf", null], + ["160735012014", "Deesbach", null], + ["160735012017", "Döschnitz", null], + ["160735012037", "Katzhütte", null], + ["160735012055", "Meura", null], + ["160735012074", "Rohrbach", null], + ["160735012082", "Schwarzburg", null], + ["160735012084", "Sitzendorf", null], + ["160735012094", "Unterweißbach", null], + ["160735012113", "Schwarzatal, Stadt", null], + ["160735051002", "Altenbeuthen", null], + ["160735051035", "Hohenwarte", null], + ["160735051038", "Kaulsdorf", null], + ["160735051107", "Drognitz", null], + ["160735054001", "Allendorf", null], + ["160735054006", "Bechstedt", null], + ["160735054112", "Königsee, Stadt", null], + ["160740044044", "Kahla, Stadt", null], + ["160745005012", "Crossen an der Elster", null], + ["160745005038", "Hartmannsdorf", null], + ["160745005039", "Heideland", null], + ["160745005072", "Rauda", null], + ["160745005092", "Silbitz", null], + ["160745005106", "Walpernhain", null], + ["160745005116", "Schkölen, Stadt", null], + ["160745007007", "Bremsnitz", null], + ["160745007017", "Eineborn", null], + ["160745007022", "Geisenhain", null], + ["160745007024", "Gneus", null], + ["160745007029", "Großbockedra", null], + ["160745007045", "Karlsdorf", null], + ["160745007046", "Kleinbockedra", null], + ["160745007047", "Kleinebersdorf", null], + ["160745007053", "Lippersdorf-Erdmannsdorf", null], + ["160745007056", "Meusebach", null], + ["160745007064", "Oberbodnitz", null], + ["160745007066", "Ottendorf", null], + ["160745007071", "Rattelsdorf", null], + ["160745007074", "Rausdorf", null], + ["160745007077", "Renthendorf", null], + ["160745007097", "Tautendorf", null], + ["160745007101", "Tissa", null], + ["160745007102", "Trockenborn-Wolfersdorf", null], + ["160745007103", "Tröbnitz", null], + ["160745007104", "Unterbodnitz", null], + ["160745007107", "Waltersdorf", null], + ["160745007108", "Weißbach", null], + ["160745011002", "Altenberga", null], + ["160745011004", "Bibra", null], + ["160745011008", "Bucha", null], + ["160745011016", "Eichenberg", null], + ["160745011021", "Freienorla", null], + ["160745011031", "Großeutersdorf", null], + ["160745011033", "Großpürschütz", null], + ["160745011034", "Gumperda", null], + ["160745011042", "Hummelshain", null], + ["160745011048", "Kleineutersdorf", null], + ["160745011049", "Laasdorf", null], + ["160745011052", "Lindig", null], + ["160745011057", "Milda", null], + ["160745011065", "Orlamünde, Stadt", null], + ["160745011076", "Reinstädt", null], + ["160745011079", "Rothenstein", null], + ["160745011087", "Schöps", null], + ["160745011089", "Seitenroda", null], + ["160745011095", "Sulza", null], + ["160745011114", "Zöllnitz", null], + ["160745014041", "Hermsdorf, Stadt", null], + ["160745014059", "Mörsdorf", null], + ["160745014075", "Reichenbach", null], + ["160745014084", "Schleifreisen", null], + ["160745014093", "St.Gangloff", null], + ["160745015011", "Dornburg-Camburg, Stadt", null], + ["160745015019", "Frauenprießnitz", null], + ["160745015026", "Golmsdorf", null], + ["160745015032", "Großlöbichau", null], + ["160745015036", "Hainichen", null], + ["160745015043", "Jenalöbnitz", null], + ["160745015051", "Lehesten", null], + ["160745015054", "Löberschütz", null], + ["160745015063", "Neuengönna", null], + ["160745015096", "Tautenburg", null], + ["160745015099", "Thierschneck", null], + ["160745015112", "Wichmar", null], + ["160745015113", "Zimmern", null], + ["160745050058", "Möckern", null], + ["160745050081", "Ruttersdorf-Lotschen", null], + ["160745050094", "Stadtroda, Stadt", null], + ["160745051009", "Bürgel, Stadt", null], + ["160745051028", "Graitschen b. Bürgel", null], + ["160745051061", "Nausnitz", null], + ["160745051068", "Poxdorf", null], + ["160745052018", "Eisenberg, Stadt", null], + ["160745052025", "Gösen", null], + ["160745052037", "Hainspitz", null], + ["160745052055", "Mertendorf", null], + ["160745052067", "Petersberg", null], + ["160745052073", "Rauschwitz", null], + ["160745053001", "Albersdorf", null], + ["160745053003", "Bad Klosterlausnitz", null], + ["160745053005", "Bobeck", null], + ["160745053082", "Scheiditz", null], + ["160745053085", "Schlöben", null], + ["160745053086", "Schöngleina", null], + ["160745053091", "Serba", null], + ["160745053098", "Tautenhain", null], + ["160745053105", "Waldeck", null], + ["160745053109", "Weißenborn", null], + ["160750046046", "Hirschberg, Stadt", null], + ["160750062062", "Bad Lobenstein, Stadt", null], + ["160750085085", "Pößneck, Stadt", null], + ["160750098098", "Schleiz, Stadt", null], + ["160750131131", "Gefell, Stadt", null], + ["160750132132", "Tanna, Stadt", null], + ["160750133133", "Wurzbach, Stadt", null], + ["160750134134", "Remptendorf", null], + ["160750135135", "Saalburg-Ebersdorf, Stadt", null], + ["160750136136", "Rosenthal am Rennsteig", null], + ["160755004014", "Dittersdorf", null], + ["160755004033", "Görkwitz", null], + ["160755004034", "Göschitz", null], + ["160755004048", "Kirschkau", null], + ["160755004063", "Löhma", null], + ["160755004068", "Moßbach", null], + ["160755004072", "Neundorf (bei Schleiz)", null], + ["160755004076", "Oettersdorf", null], + ["160755004083", "Plothen", null], + ["160755004084", "Pörmitz", null], + ["160755004109", "Tegau", null], + ["160755004119", "Volkmannsdorf", null], + ["160755005006", "Bodelwitz", null], + ["160755005016", "Döbritz", null], + ["160755005031", "Gertewitz", null], + ["160755005039", "Grobengereuth", null], + ["160755005054", "Langenorla", null], + ["160755005056", "Lausnitz b. Neustadt an der Orla", null], + ["160755005074", "Nimritz", null], + ["160755005075", "Oberoppurg", null], + ["160755005077", "Oppurg", null], + ["160755005087", "Quaschwitz", null], + ["160755005105", "Solkwitz", null], + ["160755005121", "Weira", null], + ["160755005124", "Wernburg", null], + ["160755011019", "Dreitzsch", null], + ["160755011029", "Geroda", null], + ["160755011057", "Lemnitz", null], + ["160755011065", "Miesitz", null], + ["160755011066", "Mittelpöllnitz", null], + ["160755011093", "Rosendorf", null], + ["160755011099", "Schmieritz", null], + ["160755011114", "Tömmelsdorf", null], + ["160755011116", "Triptis, Stadt", null], + ["160755013023", "Eßbach", null], + ["160755013035", "Gössitz", null], + ["160755013047", "Keila", null], + ["160755013069", "Moxa", null], + ["160755013079", "Paska", null], + ["160755013081", "Peuschen", null], + ["160755013088", "Ranis, Stadt", null], + ["160755013101", "Schmorda", null], + ["160755013102", "Schöndorf", null], + ["160755013103", "Seisla", null], + ["160755013125", "Wilhelmsdorf", null], + ["160755013127", "Ziegenrück, Stadt", null], + ["160755013129", "Krölpa", null], + ["160755050051", "Kospoda", null], + ["160755050073", "Neustadt an der Orla, Stadt", null], + ["160760004004", "Berga/Elster, Stadt", null], + ["160760022022", "Greiz, Stadt", null], + ["160760061061", "Ronneburg, Stadt", null], + ["160760088088", "Harth-Pöllnitz", null], + ["160760089089", "Kraftsdorf", null], + ["160760092092", "Auma-Weidatal, Stadt", null], + ["160760093093", "Mohlsdorf-Teichwolframsdorf", null], + ["160765004009", "Braunichswalde", null], + ["160765004017", "Endschütz", null], + ["160765004019", "Gauern", null], + ["160765004027", "Hilbersdorf", null], + ["160765004034", "Kauern", null], + ["160765004043", "Linda b. Weida", null], + ["160765004055", "Paitzdorf", null], + ["160765004062", "Rückersdorf", null], + ["160765004069", "Seelingstädt", null], + ["160765004074", "Teichwitz", null], + ["160765004084", "Wünschendorf/Elster", null], + ["160765006007", "Bocka", null], + ["160765006033", "Hundhaupten", null], + ["160765006042", "Lederhose", null], + ["160765006044", "Lindenkreuz", null], + ["160765006049", "Münchenbernsdorf, Stadt", null], + ["160765006064", "Saara", null], + ["160765006068", "Schwarzbach", null], + ["160765006086", "Zedlitz", null], + ["160765008006", "Bethenhausen", null], + ["160765008008", "Brahmenau", null], + ["160765008023", "Großenstein", null], + ["160765008028", "Hirschfeld", null], + ["160765008036", "Korbußen", null], + ["160765008058", "Pölzig", null], + ["160765008059", "Reichstädt", null], + ["160765008067", "Schwaara", null], + ["160765051003", "Bad Köstritz, Stadt", null], + ["160765051012", "Caaschwitz", null], + ["160765051026", "Hartmannsdorf", null], + ["160765053014", "Crimla", null], + ["160765053079", "Weida, Stadt", null], + ["160765054041", "Langenwolschendorf", null], + ["160765054081", "Weißendorf", null], + ["160765054087", "Zeulenroda-Triebes, Stadt", null], + ["160765056029", "Hohenleuben, Stadt", null], + ["160765056038", "Kühdorf", null], + ["160765056039", "Langenwetzendorf", null], + ["160770001001", "Altenburg, Stadt", null], + ["160770028028", "Lucka, Stadt", null], + ["160770032032", "Meuselwitz, Stadt", null], + ["160775004005", "Fockendorf", null], + ["160775004007", "Gerstenberg", null], + ["160775004015", "Haselbach", null], + ["160775004048", "Treben", null], + ["160775004052", "Windischleuba", null], + ["160775005008", "Göhren", null], + ["160775005009", "Göllnitz", null], + ["160775005022", "Kriebitzsch", null], + ["160775005027", "Lödla", null], + ["160775005031", "Mehna", null], + ["160775005034", "Monstab", null], + ["160775005042", "Rositz", null], + ["160775005044", "Starkenberg", null], + ["160775009016", "Heukewalde", null], + ["160775009018", "Jonaswalde", null], + ["160775009026", "Löbichau", null], + ["160775009041", "Posterstein", null], + ["160775009047", "Thonhausen", null], + ["160775009049", "Vollmershain", null], + ["160775050012", "Gößnitz, Stadt", null], + ["160775050017", "Heyersdorf", null], + ["160775050039", "Ponitz", null], + ["160775051011", "Göpfersdorf", null], + ["160775051023", "Langenleuba-Niederhain", null], + ["160775051036", "Nobitz", null], + ["160775052003", "Dobitschen", null], + ["160775052043", "Schmölln, Stadt", null] + ] +} diff --git a/tests/components/nina/fixtures/sample_warning_details.json b/tests/components/nina/fixtures/sample_warning_details.json index 61c28dc9992..f9da183c553 100644 --- a/tests/components/nina/fixtures/sample_warning_details.json +++ b/tests/components/nina/fixtures/sample_warning_details.json @@ -1,167 +1,161 @@ { - "mow.DE-BW-S-SE018-20211102-18-001": { - "identifier": "mow.DE-BW-S-SE018-20211102-18-001", - "sender": "DE-NW-BN-SE030", - "sent": "2021-11-02T20:07:16+01:00", - "status": "Actual", - "msgType": "Update", - "scope": "Public", - "code": [ - "DVN:1", - "medien_ueberregional", - "nina", - "Materna:noPush", - "Materna:noMirror" - ], - "references": "DE-NW-BN-SE030-20200506-30-001 DE-NW-BN-SE030-20200422-30-000 DE-NW-BN-SE030-20200420-30-001 DE-NW-BN-SE030-20200416-30-001 DE-NW-BN-SE030-20200403-30-000 DE-NW-BN-W003,mow.DE-NW-BN-SE030-20200506-30-001 mow.DE-NW-BN-SE030-20200422-30-000 mow.DE-NW-BN-SE030-20200420-30-001 mow.DE-NW-BN-SE030-20200416-30-001 mow.DE-NW-BN-SE030-20200403-30-000 mow.DE-NW-BN-W003-20200403-000,2020-04-03T00:00:00+00:00", - "info": [ + "mow.DE-BW-S-SE018-20211102-18-001": { + "identifier": "mow.DE-BW-S-SE018-20211102-18-001", + "sender": "DE-NW-BN-SE030", + "sent": "2021-11-02T20:07:16+01:00", + "status": "Actual", + "msgType": "Update", + "scope": "Public", + "code": [ + "DVN:1", + "medien_ueberregional", + "nina", + "Materna:noPush", + "Materna:noMirror" + ], + "references": "DE-NW-BN-SE030-20200506-30-001 DE-NW-BN-SE030-20200422-30-000 DE-NW-BN-SE030-20200420-30-001 DE-NW-BN-SE030-20200416-30-001 DE-NW-BN-SE030-20200403-30-000 DE-NW-BN-W003,mow.DE-NW-BN-SE030-20200506-30-001 mow.DE-NW-BN-SE030-20200422-30-000 mow.DE-NW-BN-SE030-20200420-30-001 mow.DE-NW-BN-SE030-20200416-30-001 mow.DE-NW-BN-SE030-20200403-30-000 mow.DE-NW-BN-W003-20200403-000,2020-04-03T00:00:00+00:00", + "info": [ + { + "language": "DE", + "category": ["Health"], + "event": "Gefahreninformation", + "urgency": "Immediate", + "severity": "Minor", + "certainty": "Observed", + "eventCode": [ { - "language": "DE", - "category": [ - "Health" - ], - "event": "Gefahreninformation", - "urgency": "Immediate", - "severity": "Minor", - "certainty": "Observed", - "eventCode": [ + "valueName": "profile:DE-BBK-EVENTCODE", + "value": "BBK-EVC-040" + } + ], + "headline": "Corona-Verordnung des Landes: Warnstufe durch Landesgesundheitsamt ausgerufen", + "description": "Die Zahl der mit dem Corona-Virus infizierten Menschen steigt gegenwärtig stark an. Es wächst daher die Gefahr einer weiteren Verbreitung der Infektion und - je nach Einzelfall - auch von schweren Erkrankungen.", + "instruction": "Waschen Sie sich regelmäßig und gründlich die Hände.
- Beachten Sie die AHA + A + L - Regeln:
Abstand halten - 1,5 m Mindestabstand beachten, Körperkontakt vermeiden!
Hygiene - regelmäßiges Händewaschen, Husten- und Nieshygiene beachten!
Alltagsmaske (Mund-Nase-Bedeckung) tragen!
App - installieren und nutzen Sie die Corona-Warn-App!
Lüften: Sorgen Sie für eine regelmäßige und gründliche Lüftung von Räumen - auch und gerade in der kommenden kalten Jahreszeit!
- Bitte folgen Sie den behördlichen Anordnungen.
- Husten und niesen Sie in ein Taschentuch oder in die Armbeuge.
- Bleiben Sie bei Erkältungssymptomen nach Möglichkeit zu Hause. Kontaktieren Sie Ihre Hausarztpraxis per Telefon oder wenden sich an die Telefonnummer 116117 des Ärztlichen Bereitschaftsdienstes und besprechen Sie das weitere Vorgehen. Gehen Sie nicht unaufgefordert in eine Arztpraxis oder ins Krankenhaus.
- Seien Sie kritisch: Informieren Sie sich nur aus gesicherten Quellen.", + "contact": "Weitere Informationen und Empfehlungen finden Sie im Corona-Informations-Bereich der Warn-App NINA. Beachten Sie auch die Internetseiten der örtlichen Gesundheitsbehörde (Stadt- bzw. Kreisverwaltung) Ihres Aufenthaltsortes", + "parameter": [ + { + "valueName": "instructionText", + "value": "- Beachten Sie die AHA + A + L - Regeln:\nAbstand halten - 1,5 m Mindestabstand beachten, Körperkontakt vermeiden! \nHygiene - regelmäßiges Händewaschen, Husten- und Nieshygiene beachten! \nAlltagsmaske (Mund-Nase-Bedeckung) tragen! \nApp - installieren und nutzen Sie die Corona-Warn-App! \nLüften: Sorgen Sie für eine regelmäßige und gründliche Lüftung von Räumen - auch und gerade in der kommenden kalten Jahreszeit! \n- Bitte folgen Sie den behördlichen Anordnungen. \n- Husten und niesen Sie in ein Taschentuch oder in die Armbeuge. \n- Bleiben Sie bei Erkältungssymptomen nach Möglichkeit zu Hause. Kontaktieren Sie Ihre Hausarztpraxis per Telefon oder wenden sich an die Telefonnummer 116117 des Ärztlichen Bereitschaftsdienstes und besprechen Sie das weitere Vorgehen. Gehen Sie nicht unaufgefordert in eine Arztpraxis oder ins Krankenhaus. \n- Seien Sie kritisch: Informieren Sie sich nur aus gesicherten Quellen." + }, + { + "valueName": "warnVerwaltungsbereiche", + "value": "130000000000,140000000000,160000000000,110000000000,020000000000,070000000000,030000000000,050000000000,080000000000,120000000000,010000000000,150000000000,040000000000,060000000000,090000000000,100000000000" + }, + { + "valueName": "instructionCode", + "value": "BBK-ISC-132" + }, + { + "valueName": "sender_langname", + "value": "BBK, Nationale Warnzentrale Bonn" + }, + { + "valueName": "sender_signature", + "value": "Bundesamt für Bevölkerungsschutz und Katastrophenhilfe\nNationale Warnzentrale Bonn\nhttps://warnung.bund.de" + }, + { + "valueName": "PHGEM", + "value": "1+11057,100001" + }, + { + "valueName": "ZGEM", + "value": "1+11057,100001" + } + ], + "area": [ + { + "areaDesc": "Bundesland: Freie Hansestadt Bremen, Land Berlin, Land Hessen, Land Nordrhein-Westfalen, Land Brandenburg, Freistaat Bayern, Land Mecklenburg-Vorpommern, Land Rheinland-Pfalz, Freistaat Sachsen, Land Schleswig-Holstein, Freie und Hansestadt Hamburg, Freistaat Thüringen, Land Niedersachsen, Land Saarland, Land Sachsen-Anhalt, Land Baden-Württemberg", + "geocode": [ { - "valueName": "profile:DE-BBK-EVENTCODE", - "value": "BBK-EVC-040" - } - ], - "headline": "Corona-Verordnung des Landes: Warnstufe durch Landesgesundheitsamt ausgerufen", - "description": "Die Zahl der mit dem Corona-Virus infizierten Menschen steigt gegenwärtig stark an. Es wächst daher die Gefahr einer weiteren Verbreitung der Infektion und - je nach Einzelfall - auch von schweren Erkrankungen.", - "instruction": "Waschen Sie sich regelmäßig und gründlich die Hände.
- Beachten Sie die AHA + A + L - Regeln:
Abstand halten - 1,5 m Mindestabstand beachten, Körperkontakt vermeiden!
Hygiene - regelmäßiges Händewaschen, Husten- und Nieshygiene beachten!
Alltagsmaske (Mund-Nase-Bedeckung) tragen!
App - installieren und nutzen Sie die Corona-Warn-App!
Lüften: Sorgen Sie für eine regelmäßige und gründliche Lüftung von Räumen - auch und gerade in der kommenden kalten Jahreszeit!
- Bitte folgen Sie den behördlichen Anordnungen.
- Husten und niesen Sie in ein Taschentuch oder in die Armbeuge.
- Bleiben Sie bei Erkältungssymptomen nach Möglichkeit zu Hause. Kontaktieren Sie Ihre Hausarztpraxis per Telefon oder wenden sich an die Telefonnummer 116117 des Ärztlichen Bereitschaftsdienstes und besprechen Sie das weitere Vorgehen. Gehen Sie nicht unaufgefordert in eine Arztpraxis oder ins Krankenhaus.
- Seien Sie kritisch: Informieren Sie sich nur aus gesicherten Quellen.", - "contact": "Weitere Informationen und Empfehlungen finden Sie im Corona-Informations-Bereich der Warn-App NINA. Beachten Sie auch die Internetseiten der örtlichen Gesundheitsbehörde (Stadt- bzw. Kreisverwaltung) Ihres Aufenthaltsortes", - "parameter": [ - { - "valueName": "instructionText", - "value": "- Beachten Sie die AHA + A + L - Regeln:\nAbstand halten - 1,5 m Mindestabstand beachten, Körperkontakt vermeiden! \nHygiene - regelmäßiges Händewaschen, Husten- und Nieshygiene beachten! \nAlltagsmaske (Mund-Nase-Bedeckung) tragen! \nApp - installieren und nutzen Sie die Corona-Warn-App! \nLüften: Sorgen Sie für eine regelmäßige und gründliche Lüftung von Räumen - auch und gerade in der kommenden kalten Jahreszeit! \n- Bitte folgen Sie den behördlichen Anordnungen. \n- Husten und niesen Sie in ein Taschentuch oder in die Armbeuge. \n- Bleiben Sie bei Erkältungssymptomen nach Möglichkeit zu Hause. Kontaktieren Sie Ihre Hausarztpraxis per Telefon oder wenden sich an die Telefonnummer 116117 des Ärztlichen Bereitschaftsdienstes und besprechen Sie das weitere Vorgehen. Gehen Sie nicht unaufgefordert in eine Arztpraxis oder ins Krankenhaus. \n- Seien Sie kritisch: Informieren Sie sich nur aus gesicherten Quellen." - }, - { - "valueName": "warnVerwaltungsbereiche", - "value": "130000000000,140000000000,160000000000,110000000000,020000000000,070000000000,030000000000,050000000000,080000000000,120000000000,010000000000,150000000000,040000000000,060000000000,090000000000,100000000000" - }, - { - "valueName": "instructionCode", - "value": "BBK-ISC-132" - }, - { - "valueName": "sender_langname", - "value": "BBK, Nationale Warnzentrale Bonn" - }, - { - "valueName": "sender_signature", - "value": "Bundesamt für Bevölkerungsschutz und Katastrophenhilfe\nNationale Warnzentrale Bonn\nhttps://warnung.bund.de" - }, - { - "valueName": "PHGEM", - "value": "1+11057,100001" - }, - { - "valueName": "ZGEM", - "value": "1+11057,100001" - } - ], - "area": [ - { - "areaDesc": "Bundesland: Freie Hansestadt Bremen, Land Berlin, Land Hessen, Land Nordrhein-Westfalen, Land Brandenburg, Freistaat Bayern, Land Mecklenburg-Vorpommern, Land Rheinland-Pfalz, Freistaat Sachsen, Land Schleswig-Holstein, Freie und Hansestadt Hamburg, Freistaat Thüringen, Land Niedersachsen, Land Saarland, Land Sachsen-Anhalt, Land Baden-Württemberg", - "geocode": [ - { - "valueName": "AreaId", - "value": "0" - } - ] + "valueName": "AreaId", + "value": "0" } ] } ] - }, - "mow.DE-NW-BN-SE030-20201014-30-000" : { - "identifier": "mow.DE-NW-BN-SE030-20201014-30-000", - "sender": "opendata@dwd.de", - "sent": "2021-10-11T05:20:00+01:00", - "status": "Actual", - "msgType": "Alert", - "source": "PVW", - "scope": "Public", - "code": [ - "DVN:2", - "id:2.49.0.0.276.0.DWD.PVW.1645004040000.5a168da8-ac20-4b6d-86be-d616526a7914" - ], - "info": [ + } + ] + }, + "mow.DE-NW-BN-SE030-20201014-30-000": { + "identifier": "mow.DE-NW-BN-SE030-20201014-30-000", + "sender": "opendata@dwd.de", + "sent": "2021-10-11T05:20:00+01:00", + "status": "Actual", + "msgType": "Alert", + "source": "PVW", + "scope": "Public", + "code": [ + "DVN:2", + "id:2.49.0.0.276.0.DWD.PVW.1645004040000.5a168da8-ac20-4b6d-86be-d616526a7914" + ], + "info": [ + { + "language": "de-DE", + "category": ["Met"], + "event": "STURMBÖEN", + "responseType": ["Prepare"], + "urgency": "Immediate", + "severity": "Moderate", + "certainty": "Likely", + "eventCode": [ { - "language": "de-DE", - "category": [ - "Met" - ], - "event": "STURMBÖEN", - "responseType": [ - "Prepare" - ], - "urgency": "Immediate", - "severity": "Moderate", - "certainty": "Likely", - "eventCode": [ - { - "valueName": "PROFILE_VERSION", - "value": "2.1.11" - }, - { - "valueName": "LICENSE", - "value": "© GeoBasis-DE / BKG 2019 (Daten modifiziert)" - }, - { - "valueName": "II", - "value": "52" - }, - { - "valueName": "GROUP", - "value": "WIND" - }, - { - "valueName": "AREA_COLOR", - "value": "251 140 0" - } - ], - "effective": "2021-11-01T03:20:00+01:00", - "onset": "2021-11-01T05:20:00+01:00", - "expires": "3021-11-22T05:19:00+01:00", - "senderName": "Deutscher Wetterdienst", - "headline": "Ausfall Notruf 112", - "description": "Es treten Sturmböen mit Geschwindigkeiten zwischen 70 km/h (20m/s, 38kn, Bft 8) und 85 km/h (24m/s, 47kn, Bft 9) aus westlicher Richtung auf. In Schauernähe sowie in exponierten Lagen muss mit schweren Sturmböen bis 90 km/h (25m/s, 48kn, Bft 10) gerechnet werden.", - "instruction": "ACHTUNG! Hinweis auf mögliche Gefahren: Es können zum Beispiel einzelne Äste herabstürzen. Achten Sie besonders auf herabfallende Gegenstände.", - "web": "https://www.wettergefahren.de", - "contact": "Deutscher Wetterdienst", - "parameter": [ - { - "valueName": "gusts", - "value": "70-85 [km/h]" - }, - { - "valueName": "exposed gusts", - "value": "<90 [km/h]" - }, - { - "valueName": "wind direction", - "value": "west" - }, - { - "valueName": "PHGEM", - "value": "3243+168,3413+1,3424+52,3478+1,3495+2,3499,3639+2527,6168+1,6175+22,6199+36,6238,6241+7,6256,9956+184,10142,10154,10164+7,10173,10176+6,10186+1,10195+2,10199,10201+6,10214+4,10220,10249+117,10368,10373+2,10425+9,10436+1,10440+8,10450+1,10453+7,10462+1,10467+5,10474+2,10484+5,10773+68,10843+2,10847+9,10858,10867+8,10878+1,10882+68,10952+7,10961+2,11046,11056+1" - }, - { - "valueName": "ZGEM", - "value": "3243+168,3413+1,3424+52,3478+1,3495+2,3499,3639+2527,6168+1,6175+22,6199+36,6238,6241+7,6256,9956+184,10142,10154,10164+7,10173,10176+6,10186+1,10195+2,10199,10201+6,10214+4,10220,10249+117,10368,10373+2,10425+9,10436+1,10440+8,10450+1,10453+7,10462+1,10467+5,10474+2,10484+5,10773+68,10843+2,10847+9,10858,10867+8,10878+1,10882+68,10952+7,10961+2,11046,11056+1" - } - ], - "area": [ - { - "areaDesc": "Gemeinde Oberreichenbach, Gemeinde Neuweiler, Stadt Nagold, Stadt Neubulach, Gemeinde Schömberg, Gemeinde Simmersfeld, Gemeinde Simmozheim, Gemeinde Rohrdorf, Gemeinde Ostelsheim, Gemeinde Ebhausen, Gemeinde Egenhausen, Gemeinde Dobel, Stadt Bad Liebenzell, Stadt Solingen, Stadt Haiterbach, Stadt Bad Herrenalb, Gemeinde Höfen an der Enz, Gemeinde Gechingen, Gemeinde Enzklösterle, Gemeinde Gutach (Schwarzwaldbahn) und 3392 weitere." - } - ] + "valueName": "PROFILE_VERSION", + "value": "2.1.11" + }, + { + "valueName": "LICENSE", + "value": "© GeoBasis-DE / BKG 2019 (Daten modifiziert)" + }, + { + "valueName": "II", + "value": "52" + }, + { + "valueName": "GROUP", + "value": "WIND" + }, + { + "valueName": "AREA_COLOR", + "value": "251 140 0" + } + ], + "effective": "2021-11-01T03:20:00+01:00", + "onset": "2021-11-01T05:20:00+01:00", + "expires": "3021-11-22T05:19:00+01:00", + "senderName": "Deutscher Wetterdienst", + "headline": "Ausfall Notruf 112", + "description": "Es treten Sturmböen mit Geschwindigkeiten zwischen 70 km/h (20m/s, 38kn, Bft 8) und 85 km/h (24m/s, 47kn, Bft 9) aus westlicher Richtung auf. In Schauernähe sowie in exponierten Lagen muss mit schweren Sturmböen bis 90 km/h (25m/s, 48kn, Bft 10) gerechnet werden.", + "instruction": "ACHTUNG! Hinweis auf mögliche Gefahren: Es können zum Beispiel einzelne Äste herabstürzen. Achten Sie besonders auf herabfallende Gegenstände.", + "web": "https://www.wettergefahren.de", + "contact": "Deutscher Wetterdienst", + "parameter": [ + { + "valueName": "gusts", + "value": "70-85 [km/h]" + }, + { + "valueName": "exposed gusts", + "value": "<90 [km/h]" + }, + { + "valueName": "wind direction", + "value": "west" + }, + { + "valueName": "PHGEM", + "value": "3243+168,3413+1,3424+52,3478+1,3495+2,3499,3639+2527,6168+1,6175+22,6199+36,6238,6241+7,6256,9956+184,10142,10154,10164+7,10173,10176+6,10186+1,10195+2,10199,10201+6,10214+4,10220,10249+117,10368,10373+2,10425+9,10436+1,10440+8,10450+1,10453+7,10462+1,10467+5,10474+2,10484+5,10773+68,10843+2,10847+9,10858,10867+8,10878+1,10882+68,10952+7,10961+2,11046,11056+1" + }, + { + "valueName": "ZGEM", + "value": "3243+168,3413+1,3424+52,3478+1,3495+2,3499,3639+2527,6168+1,6175+22,6199+36,6238,6241+7,6256,9956+184,10142,10154,10164+7,10173,10176+6,10186+1,10195+2,10199,10201+6,10214+4,10220,10249+117,10368,10373+2,10425+9,10436+1,10440+8,10450+1,10453+7,10462+1,10467+5,10474+2,10484+5,10773+68,10843+2,10847+9,10858,10867+8,10878+1,10882+68,10952+7,10961+2,11046,11056+1" + } + ], + "area": [ + { + "areaDesc": "Gemeinde Oberreichenbach, Gemeinde Neuweiler, Stadt Nagold, Stadt Neubulach, Gemeinde Schömberg, Gemeinde Simmersfeld, Gemeinde Simmozheim, Gemeinde Rohrdorf, Gemeinde Ostelsheim, Gemeinde Ebhausen, Gemeinde Egenhausen, Gemeinde Dobel, Stadt Bad Liebenzell, Stadt Solingen, Stadt Haiterbach, Stadt Bad Herrenalb, Gemeinde Höfen an der Enz, Gemeinde Gechingen, Gemeinde Enzklösterle, Gemeinde Gutach (Schwarzwaldbahn) und 3392 weitere." } ] - } -} \ No newline at end of file + } + ] + } +} diff --git a/tests/components/nina/fixtures/sample_warnings.json b/tests/components/nina/fixtures/sample_warnings.json index b49e436ef8b..0a41611b7ee 100644 --- a/tests/components/nina/fixtures/sample_warnings.json +++ b/tests/components/nina/fixtures/sample_warnings.json @@ -1,44 +1,44 @@ [ - { - "id": "mow.DE-BW-S-SE018-20211102-18-001", - "payload": { - "version": 1, - "type": "ALERT", - "id": "mow.DE-BW-S-SE018-20211102-18-001", - "hash": "cae97b1c11bde900017305f681904ad5a6e8fd1c841241ced524b83eaa3522f4", - "data": { - "headline": "Corona-Verordnung des Landes: Warnstufe durch Landesgesundheitsamt ausgerufen", - "provider": "MOWAS", - "severity": "Minor", - "msgType": "Update", - "transKeys": {"event": "BBK-EVC-040"}, - "area": {"type": "ZGEM", "data": "9956+1102,100001"} - } - }, - "i18nTitle": { - "de": "Corona-Verordnung des Landes: Warnstufe durch Landesgesundheitsamt ausgerufen" - }, - "sent": "2021-11-02T20:07:16+01:00" + { + "id": "mow.DE-BW-S-SE018-20211102-18-001", + "payload": { + "version": 1, + "type": "ALERT", + "id": "mow.DE-BW-S-SE018-20211102-18-001", + "hash": "cae97b1c11bde900017305f681904ad5a6e8fd1c841241ced524b83eaa3522f4", + "data": { + "headline": "Corona-Verordnung des Landes: Warnstufe durch Landesgesundheitsamt ausgerufen", + "provider": "MOWAS", + "severity": "Minor", + "msgType": "Update", + "transKeys": { "event": "BBK-EVC-040" }, + "area": { "type": "ZGEM", "data": "9956+1102,100001" } + } }, - { - "id": "mow.DE-NW-BN-SE030-20201014-30-000", - "payload": { - "version": 1, - "type": "ALERT", - "id": "mow.DE-NW-BN-SE030-20201014-30-000", - "hash": "551db820a43be7e4f39283e1dfb71b212cd520c3ee478d44f43519e9c48fde4c", - "data": { - "headline": "Ausfall Notruf 112", - "provider": "MOWAS", - "severity": "Minor", - "msgType": "Update", - "transKeys": {"event": "BBK-EVC-040"}, - "area": {"type": "ZGEM", "data": "1+11057,100001"} - } - }, - "i18nTitle": {"de": "Ausfall Notruf 112"}, - "onset": "2021-11-01T05:20:00+01:00", - "sent": "2021-10-11T05:20:00+01:00", - "expires": "3021-11-22T05:19:00+01:00" - } -] \ No newline at end of file + "i18nTitle": { + "de": "Corona-Verordnung des Landes: Warnstufe durch Landesgesundheitsamt ausgerufen" + }, + "sent": "2021-11-02T20:07:16+01:00" + }, + { + "id": "mow.DE-NW-BN-SE030-20201014-30-000", + "payload": { + "version": 1, + "type": "ALERT", + "id": "mow.DE-NW-BN-SE030-20201014-30-000", + "hash": "551db820a43be7e4f39283e1dfb71b212cd520c3ee478d44f43519e9c48fde4c", + "data": { + "headline": "Ausfall Notruf 112", + "provider": "MOWAS", + "severity": "Minor", + "msgType": "Update", + "transKeys": { "event": "BBK-EVC-040" }, + "area": { "type": "ZGEM", "data": "1+11057,100001" } + } + }, + "i18nTitle": { "de": "Ausfall Notruf 112" }, + "onset": "2021-11-01T05:20:00+01:00", + "sent": "2021-10-11T05:20:00+01:00", + "expires": "3021-11-22T05:19:00+01:00" + } +] diff --git a/tests/components/nina/test_binary_sensor.py b/tests/components/nina/test_binary_sensor.py index d0a65e190f0..210ebc66a60 100644 --- a/tests/components/nina/test_binary_sensor.py +++ b/tests/components/nina/test_binary_sensor.py @@ -1,13 +1,18 @@ """Test the Nina binary sensor.""" +from __future__ import annotations + from typing import Any from unittest.mock import patch from homeassistant.components.binary_sensor import BinarySensorDeviceClass from homeassistant.components.nina.const import ( + ATTR_DESCRIPTION, ATTR_EXPIRES, ATTR_HEADLINE, ATTR_ID, + ATTR_SENDER, ATTR_SENT, + ATTR_SEVERITY, ATTR_START, DOMAIN, ) @@ -58,10 +63,16 @@ async def test_sensors(hass: HomeAssistant) -> None: assert state_w1.state == STATE_ON assert state_w1.attributes.get(ATTR_HEADLINE) == "Ausfall Notruf 112" + assert ( + state_w1.attributes.get(ATTR_DESCRIPTION) + == "Es treten Sturmböen mit Geschwindigkeiten zwischen 70 km/h (20m/s, 38kn, Bft 8) und 85 km/h (24m/s, 47kn, Bft 9) aus westlicher Richtung auf. In Schauernähe sowie in exponierten Lagen muss mit schweren Sturmböen bis 90 km/h (25m/s, 48kn, Bft 10) gerechnet werden." + ) + assert state_w1.attributes.get(ATTR_SENDER) == "Deutscher Wetterdienst" + assert state_w1.attributes.get(ATTR_SEVERITY) == "Minor" assert state_w1.attributes.get(ATTR_ID) == "mow.DE-NW-BN-SE030-20201014-30-000" - assert state_w1.attributes.get(ATTR_SENT) == "2021-10-11T04:20:00+00:00" - assert state_w1.attributes.get(ATTR_START) == "2021-11-01T04:20:00+00:00" - assert state_w1.attributes.get(ATTR_EXPIRES) == "3021-11-22T04:19:00+00:00" + assert state_w1.attributes.get(ATTR_SENT) == "2021-10-11T05:20:00+01:00" + assert state_w1.attributes.get(ATTR_START) == "2021-11-01T05:20:00+01:00" + assert state_w1.attributes.get(ATTR_EXPIRES) == "3021-11-22T05:19:00+01:00" assert entry_w1.unique_id == "083350000000-1" assert state_w1.attributes.get("device_class") == BinarySensorDeviceClass.SAFETY @@ -71,6 +82,9 @@ async def test_sensors(hass: HomeAssistant) -> None: assert state_w2.state == STATE_OFF assert state_w2.attributes.get(ATTR_HEADLINE) is None + assert state_w2.attributes.get(ATTR_DESCRIPTION) is None + assert state_w2.attributes.get(ATTR_SENDER) is None + assert state_w2.attributes.get(ATTR_SEVERITY) is None assert state_w2.attributes.get(ATTR_ID) is None assert state_w2.attributes.get(ATTR_SENT) is None assert state_w2.attributes.get(ATTR_START) is None @@ -84,6 +98,9 @@ async def test_sensors(hass: HomeAssistant) -> None: assert state_w3.state == STATE_OFF assert state_w3.attributes.get(ATTR_HEADLINE) is None + assert state_w3.attributes.get(ATTR_DESCRIPTION) is None + assert state_w3.attributes.get(ATTR_SENDER) is None + assert state_w3.attributes.get(ATTR_SEVERITY) is None assert state_w3.attributes.get(ATTR_ID) is None assert state_w3.attributes.get(ATTR_SENT) is None assert state_w3.attributes.get(ATTR_START) is None @@ -97,6 +114,9 @@ async def test_sensors(hass: HomeAssistant) -> None: assert state_w4.state == STATE_OFF assert state_w4.attributes.get(ATTR_HEADLINE) is None + assert state_w4.attributes.get(ATTR_DESCRIPTION) is None + assert state_w4.attributes.get(ATTR_SENDER) is None + assert state_w4.attributes.get(ATTR_SEVERITY) is None assert state_w4.attributes.get(ATTR_ID) is None assert state_w4.attributes.get(ATTR_SENT) is None assert state_w4.attributes.get(ATTR_START) is None @@ -110,6 +130,9 @@ async def test_sensors(hass: HomeAssistant) -> None: assert state_w5.state == STATE_OFF assert state_w5.attributes.get(ATTR_HEADLINE) is None + assert state_w5.attributes.get(ATTR_DESCRIPTION) is None + assert state_w5.attributes.get(ATTR_SENDER) is None + assert state_w5.attributes.get(ATTR_SEVERITY) is None assert state_w5.attributes.get(ATTR_ID) is None assert state_w5.attributes.get(ATTR_SENT) is None assert state_w5.attributes.get(ATTR_START) is None @@ -147,10 +170,16 @@ async def test_sensors_without_corona_filter(hass: HomeAssistant) -> None: state_w1.attributes.get(ATTR_HEADLINE) == "Corona-Verordnung des Landes: Warnstufe durch Landesgesundheitsamt ausgerufen" ) + assert ( + state_w1.attributes.get(ATTR_DESCRIPTION) + == "Die Zahl der mit dem Corona-Virus infizierten Menschen steigt gegenwärtig stark an. Es wächst daher die Gefahr einer weiteren Verbreitung der Infektion und - je nach Einzelfall - auch von schweren Erkrankungen." + ) + assert state_w1.attributes.get(ATTR_SENDER) == "" + assert state_w1.attributes.get(ATTR_SEVERITY) == "Minor" assert state_w1.attributes.get(ATTR_ID) == "mow.DE-BW-S-SE018-20211102-18-001" - assert state_w1.attributes.get(ATTR_SENT) == "2021-11-02T19:07:16+00:00" - assert state_w1.attributes.get(ATTR_START) is None - assert state_w1.attributes.get(ATTR_EXPIRES) is None + assert state_w1.attributes.get(ATTR_SENT) == "2021-11-02T20:07:16+01:00" + assert state_w1.attributes.get(ATTR_START) == "" + assert state_w1.attributes.get(ATTR_EXPIRES) == "" assert entry_w1.unique_id == "083350000000-1" assert state_w1.attributes.get("device_class") == BinarySensorDeviceClass.SAFETY @@ -160,10 +189,16 @@ async def test_sensors_without_corona_filter(hass: HomeAssistant) -> None: assert state_w2.state == STATE_ON assert state_w2.attributes.get(ATTR_HEADLINE) == "Ausfall Notruf 112" + assert ( + state_w2.attributes.get(ATTR_DESCRIPTION) + == "Es treten Sturmböen mit Geschwindigkeiten zwischen 70 km/h (20m/s, 38kn, Bft 8) und 85 km/h (24m/s, 47kn, Bft 9) aus westlicher Richtung auf. In Schauernähe sowie in exponierten Lagen muss mit schweren Sturmböen bis 90 km/h (25m/s, 48kn, Bft 10) gerechnet werden." + ) + assert state_w2.attributes.get(ATTR_SENDER) == "Deutscher Wetterdienst" + assert state_w2.attributes.get(ATTR_SEVERITY) == "Minor" assert state_w2.attributes.get(ATTR_ID) == "mow.DE-NW-BN-SE030-20201014-30-000" - assert state_w2.attributes.get(ATTR_SENT) == "2021-10-11T04:20:00+00:00" - assert state_w2.attributes.get(ATTR_START) == "2021-11-01T04:20:00+00:00" - assert state_w2.attributes.get(ATTR_EXPIRES) == "3021-11-22T04:19:00+00:00" + assert state_w2.attributes.get(ATTR_SENT) == "2021-10-11T05:20:00+01:00" + assert state_w2.attributes.get(ATTR_START) == "2021-11-01T05:20:00+01:00" + assert state_w2.attributes.get(ATTR_EXPIRES) == "3021-11-22T05:19:00+01:00" assert entry_w2.unique_id == "083350000000-2" assert state_w2.attributes.get("device_class") == BinarySensorDeviceClass.SAFETY @@ -173,6 +208,9 @@ async def test_sensors_without_corona_filter(hass: HomeAssistant) -> None: assert state_w3.state == STATE_OFF assert state_w3.attributes.get(ATTR_HEADLINE) is None + assert state_w3.attributes.get(ATTR_DESCRIPTION) is None + assert state_w3.attributes.get(ATTR_SENDER) is None + assert state_w3.attributes.get(ATTR_SEVERITY) is None assert state_w3.attributes.get(ATTR_ID) is None assert state_w3.attributes.get(ATTR_SENT) is None assert state_w3.attributes.get(ATTR_START) is None @@ -186,6 +224,9 @@ async def test_sensors_without_corona_filter(hass: HomeAssistant) -> None: assert state_w4.state == STATE_OFF assert state_w4.attributes.get(ATTR_HEADLINE) is None + assert state_w4.attributes.get(ATTR_DESCRIPTION) is None + assert state_w4.attributes.get(ATTR_SENDER) is None + assert state_w4.attributes.get(ATTR_SEVERITY) is None assert state_w4.attributes.get(ATTR_ID) is None assert state_w4.attributes.get(ATTR_SENT) is None assert state_w4.attributes.get(ATTR_START) is None @@ -199,6 +240,9 @@ async def test_sensors_without_corona_filter(hass: HomeAssistant) -> None: assert state_w5.state == STATE_OFF assert state_w5.attributes.get(ATTR_HEADLINE) is None + assert state_w5.attributes.get(ATTR_DESCRIPTION) is None + assert state_w5.attributes.get(ATTR_SENDER) is None + assert state_w5.attributes.get(ATTR_SEVERITY) is None assert state_w5.attributes.get(ATTR_ID) is None assert state_w5.attributes.get(ATTR_SENT) is None assert state_w5.attributes.get(ATTR_START) is None diff --git a/tests/components/notify/test_init.py b/tests/components/notify/test_init.py index 9dbbcf9b9b9..ae32884add7 100644 --- a/tests/components/notify/test_init.py +++ b/tests/components/notify/test_init.py @@ -1,8 +1,41 @@ """The tests for notify services that change targets.""" + +from unittest.mock import Mock, patch + +import yaml + +from homeassistant import config as hass_config from homeassistant.components import notify +from homeassistant.const import SERVICE_RELOAD from homeassistant.core import HomeAssistant +from homeassistant.helpers.discovery import async_load_platform +from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.setup import async_setup_component +from tests.common import MockPlatform, mock_platform + + +class MockNotifyPlatform(MockPlatform): + """Help to set up test notify service.""" + + def __init__(self, async_get_service=None, get_service=None): + """Return the notify service.""" + super().__init__() + if get_service: + self.get_service = get_service + if async_get_service: + self.async_get_service = async_get_service + + +def mock_notify_platform( + hass, tmp_path, integration="notify", async_get_service=None, get_service=None +): + """Specialize the mock platform for notify.""" + loaded_platform = MockNotifyPlatform(async_get_service, get_service) + mock_platform(hass, f"{integration}.notify", loaded_platform) + + return loaded_platform + async def test_same_targets(hass: HomeAssistant): """Test not changing the targets in a notify service.""" @@ -73,10 +106,16 @@ async def test_remove_targets(hass: HomeAssistant): class NotificationService(notify.BaseNotificationService): """A test class for notification services.""" - def __init__(self, hass): + def __init__(self, hass, target_list={"a": 1, "b": 2}, name="notify"): """Initialize the service.""" + + async def _async_make_reloadable(hass): + """Initialize the reload service.""" + await async_setup_reload_service(hass, name, [notify.DOMAIN]) + self.hass = hass - self.target_list = {"a": 1, "b": 2} + self.target_list = target_list + hass.async_create_task(_async_make_reloadable(hass)) @property def targets(self): @@ -97,3 +136,197 @@ async def test_warn_template(hass, caplog): # We should only log it once assert caplog.text.count("Passing templates to notify service is deprecated") == 1 assert hass.states.get("persistent_notification.notification") is not None + + +async def test_invalid_platform(hass, caplog, tmp_path): + """Test service setup with an invalid platform.""" + mock_notify_platform(hass, tmp_path, "testnotify1") + # Setup the platform + await async_setup_component( + hass, "notify", {"notify": [{"platform": "testnotify1"}]} + ) + await hass.async_block_till_done() + assert "Invalid notify platform" in caplog.text + caplog.clear() + # Setup the second testnotify2 platform dynamically + mock_notify_platform(hass, tmp_path, "testnotify2") + await async_load_platform( + hass, + "notify", + "testnotify2", + {}, + hass_config={"notify": [{"platform": "testnotify2"}]}, + ) + await hass.async_block_till_done() + assert "Invalid notify platform" in caplog.text + + +async def test_invalid_service(hass, caplog, tmp_path): + """Test service setup with an invalid service object or platform.""" + + def get_service(hass, config, discovery_info=None): + """Return None for an invalid notify service.""" + return None + + mock_notify_platform(hass, tmp_path, "testnotify", get_service=get_service) + # Setup the second testnotify2 platform dynamically + await async_load_platform( + hass, + "notify", + "testnotify", + {}, + hass_config={"notify": [{"platform": "testnotify"}]}, + ) + await hass.async_block_till_done() + assert "Failed to initialize notification service testnotify" in caplog.text + caplog.clear() + + await async_load_platform( + hass, + "notify", + "testnotifyinvalid", + {"notify": [{"platform": "testnotifyinvalid"}]}, + hass_config={"notify": [{"platform": "testnotifyinvalid"}]}, + ) + await hass.async_block_till_done() + assert "Unknown notification service specified" in caplog.text + + +async def test_platform_setup_with_error(hass, caplog, tmp_path): + """Test service setup with an invalid setup.""" + + async def async_get_service(hass, config, discovery_info=None): + """Return None for an invalid notify service.""" + raise Exception("Setup error") + + mock_notify_platform( + hass, tmp_path, "testnotify", async_get_service=async_get_service + ) + # Setup the second testnotify2 platform dynamically + await async_load_platform( + hass, + "notify", + "testnotify", + {}, + hass_config={"notify": [{"platform": "testnotify"}]}, + ) + await hass.async_block_till_done() + assert "Error setting up platform testnotify" in caplog.text + + +async def test_reload_with_notify_builtin_platform_reload(hass, caplog, tmp_path): + """Test reload using the notify platform reload method.""" + + async def async_get_service(hass, config, discovery_info=None): + """Get notify service for mocked platform.""" + targetlist = {"a": 1, "b": 2} + return NotificationService(hass, targetlist, "testnotify") + + # platform with service + mock_notify_platform( + hass, tmp_path, "testnotify", async_get_service=async_get_service + ) + + # Perform a reload using the notify module for testnotify (without services) + await notify.async_reload(hass, "testnotify") + + # Setup the platform + await async_setup_component( + hass, "notify", {"notify": [{"platform": "testnotify"}]} + ) + await hass.async_block_till_done() + assert hass.services.has_service(notify.DOMAIN, "testnotify_a") + assert hass.services.has_service(notify.DOMAIN, "testnotify_b") + + # Perform a reload using the notify module for testnotify (with services) + await notify.async_reload(hass, "testnotify") + assert hass.services.has_service(notify.DOMAIN, "testnotify_a") + assert hass.services.has_service(notify.DOMAIN, "testnotify_b") + + +async def test_setup_platform_and_reload(hass, caplog, tmp_path): + """Test service setup and reload.""" + get_service_called = Mock() + + async def async_get_service(hass, config, discovery_info=None): + """Get notify service for mocked platform.""" + get_service_called(config, discovery_info) + targetlist = {"a": 1, "b": 2} + return NotificationService(hass, targetlist, "testnotify") + + async def async_get_service2(hass, config, discovery_info=None): + """Get notify service for mocked platform.""" + get_service_called(config, discovery_info) + targetlist = {"c": 3, "d": 4} + return NotificationService(hass, targetlist, "testnotify2") + + # Mock first platform + mock_notify_platform( + hass, tmp_path, "testnotify", async_get_service=async_get_service + ) + + # Initialize a second platform testnotify2 + mock_notify_platform( + hass, tmp_path, "testnotify2", async_get_service=async_get_service2 + ) + + # Setup the testnotify platform + await async_setup_component( + hass, "notify", {"notify": [{"platform": "testnotify"}]} + ) + await hass.async_block_till_done() + assert hass.services.has_service("testnotify", SERVICE_RELOAD) + assert hass.services.has_service(notify.DOMAIN, "testnotify_a") + assert hass.services.has_service(notify.DOMAIN, "testnotify_b") + assert get_service_called.call_count == 1 + assert get_service_called.call_args[0][0] == {"platform": "testnotify"} + assert get_service_called.call_args[0][1] is None + get_service_called.reset_mock() + + # Setup the second testnotify2 platform dynamically + await async_load_platform( + hass, + "notify", + "testnotify2", + {}, + hass_config={"notify": [{"platform": "testnotify"}]}, + ) + await hass.async_block_till_done() + assert hass.services.has_service("testnotify2", SERVICE_RELOAD) + assert hass.services.has_service(notify.DOMAIN, "testnotify2_c") + assert hass.services.has_service(notify.DOMAIN, "testnotify2_d") + assert get_service_called.call_count == 1 + assert get_service_called.call_args[0][0] == {} + assert get_service_called.call_args[0][1] == {} + get_service_called.reset_mock() + + # Perform a reload + new_yaml_config_file = tmp_path / "configuration.yaml" + new_yaml_config = yaml.dump({"notify": [{"platform": "testnotify"}]}) + new_yaml_config_file.write_text(new_yaml_config) + + with patch.object(hass_config, "YAML_CONFIG_FILE", new_yaml_config_file): + await hass.services.async_call( + "testnotify", + SERVICE_RELOAD, + {}, + blocking=True, + ) + await hass.services.async_call( + "testnotify2", + SERVICE_RELOAD, + {}, + blocking=True, + ) + await hass.async_block_till_done() + + # Check if the notify services from setup still exist + assert hass.services.has_service(notify.DOMAIN, "testnotify_a") + assert hass.services.has_service(notify.DOMAIN, "testnotify_b") + assert get_service_called.call_count == 1 + assert get_service_called.call_args[0][0] == {"platform": "testnotify"} + assert get_service_called.call_args[0][1] is None + + # Check if the dynamically notify services from setup were removed + assert not hass.services.has_service(notify.DOMAIN, "testnotify2_c") + assert not hass.services.has_service(notify.DOMAIN, "testnotify2_d") diff --git a/tests/components/number/test_recorder.py b/tests/components/number/test_recorder.py new file mode 100644 index 00000000000..26eca4eb4da --- /dev/null +++ b/tests/components/number/test_recorder.py @@ -0,0 +1,46 @@ +"""The tests for number recorder.""" +from __future__ import annotations + +from datetime import timedelta + +from homeassistant.components import number +from homeassistant.components.number import ATTR_MAX, ATTR_MIN, ATTR_MODE, ATTR_STEP +from homeassistant.components.recorder.models import StateAttributes, States +from homeassistant.components.recorder.util import session_scope +from homeassistant.const import ATTR_FRIENDLY_NAME +from homeassistant.core import State +from homeassistant.setup import async_setup_component +from homeassistant.util import dt as dt_util + +from tests.common import async_fire_time_changed, async_init_recorder_component +from tests.components.recorder.common import async_wait_recording_done_without_instance + + +async def test_exclude_attributes(hass): + """Test number registered attributes to be excluded.""" + await async_init_recorder_component(hass) + await async_setup_component( + hass, number.DOMAIN, {number.DOMAIN: {"platform": "demo"}} + ) + await hass.async_block_till_done() + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5)) + await hass.async_block_till_done() + await async_wait_recording_done_without_instance(hass) + + def _fetch_states() -> list[State]: + with session_scope(hass=hass) as session: + native_states = [] + for db_state, db_state_attributes in session.query(States, StateAttributes): + state = db_state.to_native() + state.attributes = db_state_attributes.to_native() + native_states.append(state) + return native_states + + states: list[State] = await hass.async_add_executor_job(_fetch_states) + assert len(states) > 1 + for state in states: + assert ATTR_MIN not in state.attributes + assert ATTR_MAX not in state.attributes + assert ATTR_STEP not in state.attributes + assert ATTR_MODE not in state.attributes + assert ATTR_FRIENDLY_NAME in state.attributes diff --git a/tests/components/number/test_reproduce_state.py b/tests/components/number/test_reproduce_state.py index 654f87cbceb..44ff89de93c 100644 --- a/tests/components/number/test_reproduce_state.py +++ b/tests/components/number/test_reproduce_state.py @@ -6,6 +6,7 @@ from homeassistant.components.number.const import ( SERVICE_SET_VALUE, ) from homeassistant.core import State +from homeassistant.helpers.state import async_reproduce_state from tests.common import async_mock_service @@ -21,7 +22,8 @@ async def test_reproducing_states(hass, caplog): ) # These calls should do nothing as entities already in desired state - await hass.helpers.state.async_reproduce_state( + await async_reproduce_state( + hass, [ State("number.test_number", VALID_NUMBER1), # Should not raise @@ -33,7 +35,8 @@ async def test_reproducing_states(hass, caplog): # Test reproducing with different state calls = async_mock_service(hass, DOMAIN, SERVICE_SET_VALUE) - await hass.helpers.state.async_reproduce_state( + await async_reproduce_state( + hass, [ State("number.test_number", VALID_NUMBER2), # Should not raise @@ -46,8 +49,6 @@ async def test_reproducing_states(hass, caplog): assert calls[0].data == {"entity_id": "number.test_number", "value": VALID_NUMBER2} # Test invalid state - await hass.helpers.state.async_reproduce_state( - [State("number.test_number", "invalid_state")] - ) + await async_reproduce_state(hass, [State("number.test_number", "invalid_state")]) assert len(calls) == 1 diff --git a/tests/components/nut/fixtures/5E650I.json b/tests/components/nut/fixtures/5E650I.json index 2f5eae5a86a..4bd8f551775 100644 --- a/tests/components/nut/fixtures/5E650I.json +++ b/tests/components/nut/fixtures/5E650I.json @@ -1,36 +1,36 @@ { - "driver.version.internal" : "0.38", - "outlet.switchable" : "no", - "driver.parameter.port" : "auto", - "device.model" : "5E 650i", - "ups.model" : "5E 650i", - "driver.parameter.pollfreq" : "30", - "ups.timer.shutdown" : "-1", - "ups.productid" : "ffff", - "ups.load" : "28", - "ups.delay.shutdown" : "20", - "ups.power.nominal" : "650", - "output.voltage.nominal" : "230", - "outlet.1.status" : "on", - "battery.type" : "PbAc", - "driver.version.data" : "MGE HID 1.33", - "ups.vendorid" : "0463", - "driver.parameter.pollinterval" : "5", - "ups.status" : "OL CHRG", - "driver.version" : "DSM6-2-2-24922-broadwell-fmp-repack-24922-190507", - "ups.firmware" : "03.08.0018", - "ups.start.battery" : "yes", - "output.frequency.nominal" : "50", - "battery.charge" : "100", - "outlet.id" : "1", - "output.frequency" : "49.9", - "driver.name" : "usbhid-ups", - "battery.runtime" : "1032", - "input.voltage" : "239.0", - "ups.beeper.status" : "enabled", - "device.mfr" : "EATON", - "device.type" : "ups", - "ups.mfr" : "EATON", - "output.voltage" : "238.0", - "outlet.desc" : "Main Outlet" + "driver.version.internal": "0.38", + "outlet.switchable": "no", + "driver.parameter.port": "auto", + "device.model": "5E 650i", + "ups.model": "5E 650i", + "driver.parameter.pollfreq": "30", + "ups.timer.shutdown": "-1", + "ups.productid": "ffff", + "ups.load": "28", + "ups.delay.shutdown": "20", + "ups.power.nominal": "650", + "output.voltage.nominal": "230", + "outlet.1.status": "on", + "battery.type": "PbAc", + "driver.version.data": "MGE HID 1.33", + "ups.vendorid": "0463", + "driver.parameter.pollinterval": "5", + "ups.status": "OL CHRG", + "driver.version": "DSM6-2-2-24922-broadwell-fmp-repack-24922-190507", + "ups.firmware": "03.08.0018", + "ups.start.battery": "yes", + "output.frequency.nominal": "50", + "battery.charge": "100", + "outlet.id": "1", + "output.frequency": "49.9", + "driver.name": "usbhid-ups", + "battery.runtime": "1032", + "input.voltage": "239.0", + "ups.beeper.status": "enabled", + "device.mfr": "EATON", + "device.type": "ups", + "ups.mfr": "EATON", + "output.voltage": "238.0", + "outlet.desc": "Main Outlet" } diff --git a/tests/components/nut/fixtures/5E850I.json b/tests/components/nut/fixtures/5E850I.json index 6488a5498bf..0a7f7ee7dc9 100644 --- a/tests/components/nut/fixtures/5E850I.json +++ b/tests/components/nut/fixtures/5E850I.json @@ -1,37 +1,37 @@ { - "driver.parameter.pollfreq" : "30", - "ups.power.nominal" : "850", - "battery.type" : "PbAc", - "driver.parameter.synchronous" : "no", - "driver.version" : "2.7.4", - "battery.runtime" : "1759", - "driver.version.internal" : "0.41", - "driver.name" : "usbhid-ups", - "outlet.desc" : "Main Outlet", - "ups.productid" : "ffff", - "ups.firmware" : "03.08.0018", - "output.frequency" : "50.0", - "device.model" : "5E 850i", - "output.voltage" : "238.0", - "ups.mfr" : "EATON", - "ups.load" : "21", - "outlet.id" : "1", - "device.type" : "ups", - "ups.timer.shutdown" : "-1", - "output.frequency.nominal" : "50", - "ups.delay.shutdown" : "20", - "input.voltage" : "240.0", - "ups.vendorid" : "0463", - "ups.model" : "5E 850i", - "driver.version.data" : "MGE HID 1.39", - "outlet.switchable" : "no", - "outlet.1.status" : "on", - "output.voltage.nominal" : "230", - "driver.parameter.port" : "auto", - "device.mfr" : "EATON", - "ups.start.battery" : "yes", - "ups.beeper.status" : "enabled", - "ups.status" : "OL", - "driver.parameter.pollinterval" : "2", - "battery.charge" : "100" + "driver.parameter.pollfreq": "30", + "ups.power.nominal": "850", + "battery.type": "PbAc", + "driver.parameter.synchronous": "no", + "driver.version": "2.7.4", + "battery.runtime": "1759", + "driver.version.internal": "0.41", + "driver.name": "usbhid-ups", + "outlet.desc": "Main Outlet", + "ups.productid": "ffff", + "ups.firmware": "03.08.0018", + "output.frequency": "50.0", + "device.model": "5E 850i", + "output.voltage": "238.0", + "ups.mfr": "EATON", + "ups.load": "21", + "outlet.id": "1", + "device.type": "ups", + "ups.timer.shutdown": "-1", + "output.frequency.nominal": "50", + "ups.delay.shutdown": "20", + "input.voltage": "240.0", + "ups.vendorid": "0463", + "ups.model": "5E 850i", + "driver.version.data": "MGE HID 1.39", + "outlet.switchable": "no", + "outlet.1.status": "on", + "output.voltage.nominal": "230", + "driver.parameter.port": "auto", + "device.mfr": "EATON", + "ups.start.battery": "yes", + "ups.beeper.status": "enabled", + "ups.status": "OL", + "driver.parameter.pollinterval": "2", + "battery.charge": "100" } diff --git a/tests/components/nut/fixtures/BACKUPSES600M1.json b/tests/components/nut/fixtures/BACKUPSES600M1.json index 1acd0ef0444..5f356a89b8d 100644 --- a/tests/components/nut/fixtures/BACKUPSES600M1.json +++ b/tests/components/nut/fixtures/BACKUPSES600M1.json @@ -1,47 +1,47 @@ { - "ups.realpower.nominal" : "330", - "input.voltage" : "123.0", - "ups.mfr" : "American Power Conversion", - "driver.version" : "2.7.4", - "ups.test.result" : "No test initiated", - "input.voltage.nominal" : "120", - "input.transfer.low" : "92", - "driver.parameter.pollinterval" : "15", - "driver.version.data" : "APC HID 0.96", - "driver.parameter.pollfreq" : "30", - "battery.mfr.date" : "2017/04/01", - "ups.beeper.status" : "enabled", - "battery.date" : "2001/09/25", - "driver.name" : "usbhid-ups", - "battery.charge" : "100", - "ups.status" : "OL", - "ups.model" : "Back-UPS ES 600M1", - "battery.runtime.low" : "120", - "ups.firmware" : "928.a5 .D", - "ups.delay.shutdown" : "20", - "device.model" : "Back-UPS ES 600M1", - "device.serial" : "4B1713P32195 ", - "input.sensitivity" : "medium", - "ups.firmware.aux" : "a5 ", - "input.transfer.reason" : "input voltage out of range", - "ups.timer.reboot" : "0", - "battery.voltage.nominal" : "12.0", - "ups.vendorid" : "051d", - "input.transfer.high" : "139", - "battery.voltage" : "13.7", - "battery.charge.low" : "10", - "battery.type" : "PbAc", - "ups.mfr.date" : "2017/04/01", - "ups.timer.shutdown" : "-1", - "device.mfr" : "American Power Conversion", - "driver.parameter.port" : "auto", - "battery.charge.warning" : "50", - "device.type" : "ups", - "driver.parameter.vendorid" : "051d", - "ups.serial" : "4B1713P32195 ", - "ups.load" : "22", - "driver.version.internal" : "0.41", - "battery.runtime" : "1968", - "driver.parameter.synchronous" : "no", - "ups.productid" : "0002" + "ups.realpower.nominal": "330", + "input.voltage": "123.0", + "ups.mfr": "American Power Conversion", + "driver.version": "2.7.4", + "ups.test.result": "No test initiated", + "input.voltage.nominal": "120", + "input.transfer.low": "92", + "driver.parameter.pollinterval": "15", + "driver.version.data": "APC HID 0.96", + "driver.parameter.pollfreq": "30", + "battery.mfr.date": "2017/04/01", + "ups.beeper.status": "enabled", + "battery.date": "2001/09/25", + "driver.name": "usbhid-ups", + "battery.charge": "100", + "ups.status": "OL", + "ups.model": "Back-UPS ES 600M1", + "battery.runtime.low": "120", + "ups.firmware": "928.a5 .D", + "ups.delay.shutdown": "20", + "device.model": "Back-UPS ES 600M1", + "device.serial": "4B1713P32195 ", + "input.sensitivity": "medium", + "ups.firmware.aux": "a5 ", + "input.transfer.reason": "input voltage out of range", + "ups.timer.reboot": "0", + "battery.voltage.nominal": "12.0", + "ups.vendorid": "051d", + "input.transfer.high": "139", + "battery.voltage": "13.7", + "battery.charge.low": "10", + "battery.type": "PbAc", + "ups.mfr.date": "2017/04/01", + "ups.timer.shutdown": "-1", + "device.mfr": "American Power Conversion", + "driver.parameter.port": "auto", + "battery.charge.warning": "50", + "device.type": "ups", + "driver.parameter.vendorid": "051d", + "ups.serial": "4B1713P32195 ", + "ups.load": "22", + "driver.version.internal": "0.41", + "battery.runtime": "1968", + "driver.parameter.synchronous": "no", + "ups.productid": "0002" } diff --git a/tests/components/nut/fixtures/CP1350C.json b/tests/components/nut/fixtures/CP1350C.json index 5f66883172c..655acc15596 100644 --- a/tests/components/nut/fixtures/CP1350C.json +++ b/tests/components/nut/fixtures/CP1350C.json @@ -1,39 +1,39 @@ { - "device.model" : " CP 1350C", - "ups.model" : " CP 1350C", - "battery.voltage.nominal" : "12", - "ups.delay.start" : "30", - "ups.realpower.nominal" : "298", - "ups.vendorid" : "0764", - "ups.mfr" : "CPS", - "driver.version" : "2.7.4", - "ups.delay.shutdown" : "20", - "battery.type" : "PbAcid", - "battery.voltage" : "14.0", - "ups.load" : "27", - "battery.charge.low" : "10", - "ups.beeper.status" : "enabled", - "device.mfr" : "CPS", - "battery.runtime" : "1225", - "driver.version.internal" : "0.41", - "input.voltage.nominal" : "230", - "input.transfer.high" : "280", - "driver.version.data" : "CyberPower HID 0.4", - "driver.parameter.port" : "/dev/ttyS1", - "input.voltage" : "236.0", - "battery.runtime.low" : "300", - "driver.parameter.pollinterval" : "2", - "driver.parameter.pollfreq" : "30", - "ups.timer.shutdown" : "-60", - "input.transfer.low" : "180", - "device.type" : "ups", - "battery.mfr.date" : "CPS", - "battery.charge" : "100", - "battery.charge.warning" : "20", - "ups.productid" : "0501", - "driver.name" : "usbhid-ups", - "output.voltage" : "236.0", - "driver.parameter.synchronous" : "no", - "ups.timer.start" : "0", - "ups.status" : "OL" + "device.model": " CP 1350C", + "ups.model": " CP 1350C", + "battery.voltage.nominal": "12", + "ups.delay.start": "30", + "ups.realpower.nominal": "298", + "ups.vendorid": "0764", + "ups.mfr": "CPS", + "driver.version": "2.7.4", + "ups.delay.shutdown": "20", + "battery.type": "PbAcid", + "battery.voltage": "14.0", + "ups.load": "27", + "battery.charge.low": "10", + "ups.beeper.status": "enabled", + "device.mfr": "CPS", + "battery.runtime": "1225", + "driver.version.internal": "0.41", + "input.voltage.nominal": "230", + "input.transfer.high": "280", + "driver.version.data": "CyberPower HID 0.4", + "driver.parameter.port": "/dev/ttyS1", + "input.voltage": "236.0", + "battery.runtime.low": "300", + "driver.parameter.pollinterval": "2", + "driver.parameter.pollfreq": "30", + "ups.timer.shutdown": "-60", + "input.transfer.low": "180", + "device.type": "ups", + "battery.mfr.date": "CPS", + "battery.charge": "100", + "battery.charge.warning": "20", + "ups.productid": "0501", + "driver.name": "usbhid-ups", + "output.voltage": "236.0", + "driver.parameter.synchronous": "no", + "ups.timer.start": "0", + "ups.status": "OL" } diff --git a/tests/components/nut/fixtures/CP1500PFCLCD.json b/tests/components/nut/fixtures/CP1500PFCLCD.json index 8f12ae96df6..3a42a01b054 100644 --- a/tests/components/nut/fixtures/CP1500PFCLCD.json +++ b/tests/components/nut/fixtures/CP1500PFCLCD.json @@ -1,43 +1,43 @@ { - "battery.runtime.low" : "300", - "driver.parameter.port" : "auto", - "ups.delay.shutdown" : "20", - "driver.parameter.pollfreq" : "30", - "ups.beeper.status" : "disabled", - "input.voltage.nominal" : "120", - "device.serial" : "000000000000", - "ups.timer.shutdown" : "-60", - "input.voltage" : "122.0", - "ups.status" : "OL", - "ups.model" : "CP1500PFCLCD", - "device.mfr" : "CPS", - "device.model" : "CP1500PFCLCD", - "input.transfer.low" : "88", - "battery.mfr.date" : "CPS", - "driver.version" : "2.7.4", - "driver.version.data" : "CyberPower HID 0.4", - "driver.parameter.synchronous" : "no", - "ups.realpower.nominal" : "900", - "ups.productid" : "0501", - "ups.mfr" : "CPS", - "ups.vendorid" : "0764", - "driver.version.internal" : "0.41", - "output.voltage" : "138.0", - "battery.runtime" : "10530", - "device.type" : "ups", - "battery.charge.low" : "10", - "ups.timer.start" : "-60", - "driver.parameter.pollinterval" : "15", - "ups.load" : "0", - "ups.serial" : "000000000000", - "input.transfer.high" : "139", - "battery.charge.warning" : "20", - "battery.voltage.nominal" : "24", - "driver.parameter.vendorid" : "0764", - "driver.name" : "usbhid-ups", - "battery.type" : "PbAcid", - "ups.delay.start" : "30", - "battery.voltage" : "24.0", - "battery.charge" : "100", - "ups.test.result" : "No test initiated" + "battery.runtime.low": "300", + "driver.parameter.port": "auto", + "ups.delay.shutdown": "20", + "driver.parameter.pollfreq": "30", + "ups.beeper.status": "disabled", + "input.voltage.nominal": "120", + "device.serial": "000000000000", + "ups.timer.shutdown": "-60", + "input.voltage": "122.0", + "ups.status": "OL", + "ups.model": "CP1500PFCLCD", + "device.mfr": "CPS", + "device.model": "CP1500PFCLCD", + "input.transfer.low": "88", + "battery.mfr.date": "CPS", + "driver.version": "2.7.4", + "driver.version.data": "CyberPower HID 0.4", + "driver.parameter.synchronous": "no", + "ups.realpower.nominal": "900", + "ups.productid": "0501", + "ups.mfr": "CPS", + "ups.vendorid": "0764", + "driver.version.internal": "0.41", + "output.voltage": "138.0", + "battery.runtime": "10530", + "device.type": "ups", + "battery.charge.low": "10", + "ups.timer.start": "-60", + "driver.parameter.pollinterval": "15", + "ups.load": "0", + "ups.serial": "000000000000", + "input.transfer.high": "139", + "battery.charge.warning": "20", + "battery.voltage.nominal": "24", + "driver.parameter.vendorid": "0764", + "driver.name": "usbhid-ups", + "battery.type": "PbAcid", + "ups.delay.start": "30", + "battery.voltage": "24.0", + "battery.charge": "100", + "ups.test.result": "No test initiated" } diff --git a/tests/components/nut/fixtures/DL650ELCD.json b/tests/components/nut/fixtures/DL650ELCD.json index fe6686e7f6f..f52ca170e2e 100644 --- a/tests/components/nut/fixtures/DL650ELCD.json +++ b/tests/components/nut/fixtures/DL650ELCD.json @@ -1,39 +1,39 @@ { - "ups.delay.shutdown" : "20", - "battery.charge.warning" : "20", - "battery.runtime.low" : "300", - "device.type" : "ups", - "ups.load" : "33", - "driver.parameter.port" : "auto", - "driver.name" : "usbhid-ups", - "input.transfer.high" : "0", - "ups.mfr" : "CPS", - "ups.test.result" : "No test initiated", - "output.voltage" : "229.0", - "ups.vendorid" : "0764", - "ups.realpower.nominal" : "360", - "device.model" : "DL650ELCD", - "battery.voltage.nominal" : "12", - "battery.type" : "PbAcid", - "ups.model" : "DL650ELCD", - "ups.beeper.status" : "enabled", - "driver.version.data" : "CyberPower HID 0.3", - "device.mfr" : "CPS", - "driver.parameter.pollinterval" : "5", - "ups.status" : "OL", - "battery.mfr.date" : "CPS", - "battery.charge.low" : "10", - "input.voltage" : "230.0", - "driver.version" : "SDS5-2-2015Q1branch-5619-150904", - "input.transfer.low" : "0", - "driver.parameter.pollfreq" : "30", - "driver.version.internal" : "0.38", - "ups.productid" : "0501", - "ups.timer.shutdown" : "-60", - "input.voltage.nominal" : "230", - "battery.voltage" : "9.1", - "battery.charge" : "100", - "ups.timer.start" : "-60", - "battery.runtime" : "850", - "ups.delay.start" : "30" + "ups.delay.shutdown": "20", + "battery.charge.warning": "20", + "battery.runtime.low": "300", + "device.type": "ups", + "ups.load": "33", + "driver.parameter.port": "auto", + "driver.name": "usbhid-ups", + "input.transfer.high": "0", + "ups.mfr": "CPS", + "ups.test.result": "No test initiated", + "output.voltage": "229.0", + "ups.vendorid": "0764", + "ups.realpower.nominal": "360", + "device.model": "DL650ELCD", + "battery.voltage.nominal": "12", + "battery.type": "PbAcid", + "ups.model": "DL650ELCD", + "ups.beeper.status": "enabled", + "driver.version.data": "CyberPower HID 0.3", + "device.mfr": "CPS", + "driver.parameter.pollinterval": "5", + "ups.status": "OL", + "battery.mfr.date": "CPS", + "battery.charge.low": "10", + "input.voltage": "230.0", + "driver.version": "SDS5-2-2015Q1branch-5619-150904", + "input.transfer.low": "0", + "driver.parameter.pollfreq": "30", + "driver.version.internal": "0.38", + "ups.productid": "0501", + "ups.timer.shutdown": "-60", + "input.voltage.nominal": "230", + "battery.voltage": "9.1", + "battery.charge": "100", + "ups.timer.start": "-60", + "battery.runtime": "850", + "ups.delay.start": "30" } diff --git a/tests/components/nut/fixtures/PR3000RT2U.json b/tests/components/nut/fixtures/PR3000RT2U.json index 22742366d18..d8d237766e6 100644 --- a/tests/components/nut/fixtures/PR3000RT2U.json +++ b/tests/components/nut/fixtures/PR3000RT2U.json @@ -1,39 +1,39 @@ { - "ups.delay.shutdown" : "20", - "driver.parameter.pollfreq" : "30", - "ups.delay.start" : "30", - "ups.mfr" : "CPS", - "battery.charge.warning" : "35", - "battery.type" : "PbAcid", - "battery.charge" : "100", - "battery.mfr.date" : "CPS", - "output.voltage" : "264.0", - "ups.productid" : "0601", - "input.voltage.nominal" : "120", - "driver.parameter.port" : "auto", - "input.voltage" : "120.0", - "driver.version" : "DSM6-2-2-24922-broadwell-fmp-repack-24922-190507", - "ups.vendorid" : "0764", - "battery.voltage" : "1.2", - "device.model" : "PR3000RT2U", - "driver.parameter.pollinterval" : "5", - "device.type" : "ups", - "battery.charge.low" : "0", - "ups.model" : "PR3000RT2U", - "ups.test.result" : "No test initiated", - "driver.version.data" : "CyberPower HID 0.3", - "ups.serial" : "PYVJO2000034", - "battery.voltage.nominal" : "22", - "ups.beeper.status" : "enabled", - "battery.runtime.low" : "300", - "battery.runtime" : "2644", - "driver.version.internal" : "0.38", - "ups.realpower.nominal" : "3000", - "device.serial" : "PYVJO2000034", - "driver.name" : "usbhid-ups", - "device.mfr" : "CPS", - "ups.load" : "12", - "ups.timer.shutdown" : "0", - "ups.timer.start" : "0", - "ups.status" : "OL" + "ups.delay.shutdown": "20", + "driver.parameter.pollfreq": "30", + "ups.delay.start": "30", + "ups.mfr": "CPS", + "battery.charge.warning": "35", + "battery.type": "PbAcid", + "battery.charge": "100", + "battery.mfr.date": "CPS", + "output.voltage": "264.0", + "ups.productid": "0601", + "input.voltage.nominal": "120", + "driver.parameter.port": "auto", + "input.voltage": "120.0", + "driver.version": "DSM6-2-2-24922-broadwell-fmp-repack-24922-190507", + "ups.vendorid": "0764", + "battery.voltage": "1.2", + "device.model": "PR3000RT2U", + "driver.parameter.pollinterval": "5", + "device.type": "ups", + "battery.charge.low": "0", + "ups.model": "PR3000RT2U", + "ups.test.result": "No test initiated", + "driver.version.data": "CyberPower HID 0.3", + "ups.serial": "PYVJO2000034", + "battery.voltage.nominal": "22", + "ups.beeper.status": "enabled", + "battery.runtime.low": "300", + "battery.runtime": "2644", + "driver.version.internal": "0.38", + "ups.realpower.nominal": "3000", + "device.serial": "PYVJO2000034", + "driver.name": "usbhid-ups", + "device.mfr": "CPS", + "ups.load": "12", + "ups.timer.shutdown": "0", + "ups.timer.start": "0", + "ups.status": "OL" } diff --git a/tests/components/nut/fixtures/blazer_usb.json b/tests/components/nut/fixtures/blazer_usb.json index 4f9acf2a6f0..b39c84b97a3 100644 --- a/tests/components/nut/fixtures/blazer_usb.json +++ b/tests/components/nut/fixtures/blazer_usb.json @@ -1,28 +1,28 @@ { - "input.voltage.fault" : "228.4", - "ups.status" : "OL", - "ups.productid" : "0000", - "ups.beeper.status" : "enabled", - "input.current.nominal" : "4.0", - "driver.name" : "blazer_usb", - "input.voltage" : "228.4", - "battery.voltage.low" : "20.80", - "battery.charge" : "100", - "driver.version.internal" : "0.11", - "input.frequency.nominal" : "50", - "battery.voltage.high" : "26.00", - "driver.parameter.port" : "auto", - "ups.type" : "offline / line interactive", - "driver.parameter.pollinterval" : "5", - "device.type" : "ups", - "output.voltage" : "228.4", - "input.frequency" : "50.1", - "input.voltage.nominal" : "230", - "ups.delay.shutdown" : "30", - "battery.voltage.nominal" : "24.0", - "ups.load" : "7", - "driver.version" : "DSM6-2-2-24922-broadwell-fmp-repack-24922-190507", - "ups.vendorid" : "0001", - "battery.voltage" : "27.00", - "ups.delay.start" : "180" + "input.voltage.fault": "228.4", + "ups.status": "OL", + "ups.productid": "0000", + "ups.beeper.status": "enabled", + "input.current.nominal": "4.0", + "driver.name": "blazer_usb", + "input.voltage": "228.4", + "battery.voltage.low": "20.80", + "battery.charge": "100", + "driver.version.internal": "0.11", + "input.frequency.nominal": "50", + "battery.voltage.high": "26.00", + "driver.parameter.port": "auto", + "ups.type": "offline / line interactive", + "driver.parameter.pollinterval": "5", + "device.type": "ups", + "output.voltage": "228.4", + "input.frequency": "50.1", + "input.voltage.nominal": "230", + "ups.delay.shutdown": "30", + "battery.voltage.nominal": "24.0", + "ups.load": "7", + "driver.version": "DSM6-2-2-24922-broadwell-fmp-repack-24922-190507", + "ups.vendorid": "0001", + "battery.voltage": "27.00", + "ups.delay.start": "180" } diff --git a/tests/components/nws/const.py b/tests/components/nws/const.py index c7387d4bf8c..dcf591b83ae 100644 --- a/tests/components/nws/const.py +++ b/tests/components/nws/const.py @@ -157,7 +157,9 @@ EXPECTED_FORECAST_IMPERIAL = { EXPECTED_FORECAST_METRIC = { ATTR_FORECAST_CONDITION: ATTR_CONDITION_LIGHTNING_RAINY, ATTR_FORECAST_TIME: "2019-08-12T20:00:00-04:00", - ATTR_FORECAST_TEMP: round(convert_temperature(10, TEMP_FAHRENHEIT, TEMP_CELSIUS)), + ATTR_FORECAST_TEMP: round( + convert_temperature(10, TEMP_FAHRENHEIT, TEMP_CELSIUS), 1 + ), ATTR_FORECAST_WIND_SPEED: round( convert_speed(10, SPEED_MILES_PER_HOUR, SPEED_KILOMETERS_PER_HOUR) ), diff --git a/tests/components/nzbget/test_switch.py b/tests/components/nzbget/test_switch.py index debfd9a1be8..d2bad2a46e1 100644 --- a/tests/components/nzbget/test_switch.py +++ b/tests/components/nzbget/test_switch.py @@ -8,6 +8,7 @@ from homeassistant.const import ( STATE_ON, ) from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.entity_component import async_update_entity from . import init_integration @@ -32,7 +33,7 @@ async def test_download_switch(hass, nzbget_api) -> None: # test download paused instance.status.return_value["DownloadPaused"] = True - await hass.helpers.entity_component.async_update_entity(entity_id) + await async_update_entity(hass, entity_id) await hass.async_block_till_done() state = hass.states.get(entity_id) diff --git a/tests/components/onboarding/test_views.py b/tests/components/onboarding/test_views.py index 976e2b84c68..025459e73b7 100644 --- a/tests/components/onboarding/test_views.py +++ b/tests/components/onboarding/test_views.py @@ -14,12 +14,6 @@ from homeassistant.setup import async_setup_component from . import mock_storage from tests.common import CLIENT_ID, CLIENT_REDIRECT_URI, register_auth_provider -from tests.components.met.conftest import mock_weather # noqa: F401 - - -@pytest.fixture(autouse=True) -def always_mock_weather(mock_weather): # noqa: F811 - """Mock the Met weather provider.""" @pytest.fixture(autouse=True) @@ -90,6 +84,21 @@ async def mock_supervisor_fixture(hass, aioclient_mock): yield +@pytest.fixture +def mock_default_integrations(): + """Mock the default integrations set up during onboarding.""" + with patch( + "homeassistant.components.rpi_power.config_flow.new_under_voltage" + ), patch( + "homeassistant.components.rpi_power.binary_sensor.new_under_voltage" + ), patch( + "homeassistant.components.met.async_setup_entry", return_value=True + ), patch( + "homeassistant.components.radio_browser.async_setup_entry", return_value=True + ): + yield + + async def test_onboarding_progress(hass, hass_storage, hass_client_no_auth): """Test fetching progress.""" mock_storage(hass_storage, {"done": ["hello"]}) @@ -364,7 +373,9 @@ async def test_onboarding_integration_requires_auth( assert resp.status == 401 -async def test_onboarding_core_sets_up_met(hass, hass_storage, hass_client): +async def test_onboarding_core_sets_up_met( + hass, hass_storage, hass_client, mock_default_integrations +): """Test finishing the core step.""" mock_storage(hass_storage, {"done": [const.STEP_USER]}) @@ -372,16 +383,17 @@ async def test_onboarding_core_sets_up_met(hass, hass_storage, hass_client): await hass.async_block_till_done() client = await hass_client() - resp = await client.post("/api/onboarding/core_config") assert resp.status == 200 await hass.async_block_till_done() - assert len(hass.states.async_entity_ids("weather")) == 1 + assert len(hass.config_entries.async_entries("met")) == 1 -async def test_onboarding_core_sets_up_radio_browser(hass, hass_storage, hass_client): +async def test_onboarding_core_sets_up_radio_browser( + hass, hass_storage, hass_client, mock_default_integrations +): """Test finishing the core step set up the radio browser.""" mock_storage(hass_storage, {"done": [const.STEP_USER]}) @@ -389,7 +401,6 @@ async def test_onboarding_core_sets_up_radio_browser(hass, hass_storage, hass_cl await hass.async_block_till_done() client = await hass_client() - resp = await client.post("/api/onboarding/core_config") assert resp.status == 200 @@ -399,7 +410,7 @@ async def test_onboarding_core_sets_up_radio_browser(hass, hass_storage, hass_cl async def test_onboarding_core_sets_up_rpi_power( - hass, hass_storage, hass_client, aioclient_mock, rpi + hass, hass_storage, hass_client, aioclient_mock, rpi, mock_default_integrations ): """Test that the core step sets up rpi_power on RPi.""" mock_storage(hass_storage, {"done": [const.STEP_USER]}) @@ -409,21 +420,18 @@ async def test_onboarding_core_sets_up_rpi_power( client = await hass_client() - with patch( - "homeassistant.components.rpi_power.config_flow.new_under_voltage" - ), patch("homeassistant.components.rpi_power.binary_sensor.new_under_voltage"): - resp = await client.post("/api/onboarding/core_config") + resp = await client.post("/api/onboarding/core_config") - assert resp.status == 200 + assert resp.status == 200 - await hass.async_block_till_done() + await hass.async_block_till_done() rpi_power_state = hass.states.get("binary_sensor.rpi_power_status") assert rpi_power_state async def test_onboarding_core_no_rpi_power( - hass, hass_storage, hass_client, aioclient_mock, no_rpi + hass, hass_storage, hass_client, aioclient_mock, no_rpi, mock_default_integrations ): """Test that the core step do not set up rpi_power on non RPi.""" mock_storage(hass_storage, {"done": [const.STEP_USER]}) @@ -433,14 +441,11 @@ async def test_onboarding_core_no_rpi_power( client = await hass_client() - with patch( - "homeassistant.components.rpi_power.config_flow.new_under_voltage" - ), patch("homeassistant.components.rpi_power.binary_sensor.new_under_voltage"): - resp = await client.post("/api/onboarding/core_config") + resp = await client.post("/api/onboarding/core_config") - assert resp.status == 200 + assert resp.status == 200 - await hass.async_block_till_done() + await hass.async_block_till_done() rpi_power_state = hass.states.get("binary_sensor.rpi_power_status") assert not rpi_power_state diff --git a/tests/components/onewire/conftest.py b/tests/components/onewire/conftest.py index 189baa3e7da..ab456c7d7df 100644 --- a/tests/components/onewire/conftest.py +++ b/tests/components/onewire/conftest.py @@ -37,7 +37,12 @@ def get_config_entry(hass: HomeAssistant) -> ConfigEntry: CONF_HOST: "1.2.3.4", CONF_PORT: 1234, }, - options={}, + options={ + "device_options": { + "28.222222222222": {"precision": "temperature9"}, + "28.222222222223": {"precision": "temperature5"}, + } + }, entry_id="2", ) config_entry.add_to_hass(hass) diff --git a/tests/components/onewire/const.py b/tests/components/onewire/const.py index 8d3a8270752..d77b374c7c4 100644 --- a/tests/components/onewire/const.py +++ b/tests/components/onewire/const.py @@ -435,6 +435,54 @@ MOCK_OWPROXY_DEVICES = { }, ], }, + "28.222222222222": { + # This device has precision options in the config entry + ATTR_INJECT_READS: [ + b"DS18B20", # read device type + ], + ATTR_DEVICE_INFO: { + ATTR_IDENTIFIERS: {(DOMAIN, "28.222222222222")}, + ATTR_MANUFACTURER: MANUFACTURER_MAXIM, + ATTR_MODEL: "DS18B20", + ATTR_NAME: "28.222222222222", + }, + Platform.SENSOR: [ + { + ATTR_DEVICE_CLASS: SensorDeviceClass.TEMPERATURE, + ATTR_DEVICE_FILE: "/28.222222222222/temperature9", + ATTR_ENTITY_ID: "sensor.28_222222222222_temperature", + ATTR_INJECT_READS: b" 26.984", + ATTR_STATE: "27.0", + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, + ATTR_UNIQUE_ID: "/28.222222222222/temperature", + ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, + }, + ], + }, + "28.222222222223": { + # This device has an illegal precision option in the config entry + ATTR_INJECT_READS: [ + b"DS18B20", # read device type + ], + ATTR_DEVICE_INFO: { + ATTR_IDENTIFIERS: {(DOMAIN, "28.222222222223")}, + ATTR_MANUFACTURER: MANUFACTURER_MAXIM, + ATTR_MODEL: "DS18B20", + ATTR_NAME: "28.222222222223", + }, + Platform.SENSOR: [ + { + ATTR_DEVICE_CLASS: SensorDeviceClass.TEMPERATURE, + ATTR_DEVICE_FILE: "/28.222222222223/temperature", + ATTR_ENTITY_ID: "sensor.28_222222222223_temperature", + ATTR_INJECT_READS: b" 26.984", + ATTR_STATE: "27.0", + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, + ATTR_UNIQUE_ID: "/28.222222222223/temperature", + ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, + }, + ], + }, "29.111111111111": { ATTR_INJECT_READS: [ b"DS2408", # read device type diff --git a/tests/components/onewire/test_diagnostics.py b/tests/components/onewire/test_diagnostics.py index bc164a9b138..ded4811a586 100644 --- a/tests/components/onewire/test_diagnostics.py +++ b/tests/components/onewire/test_diagnostics.py @@ -54,7 +54,12 @@ async def test_entry_diagnostics( "port": 1234, "type": "OWServer", }, - "options": {}, + "options": { + "device_options": { + "28.222222222222": {"precision": "temperature9"}, + "28.222222222223": {"precision": "temperature5"}, + } + }, "title": "Mock Title", }, "devices": [DEVICE_DETAILS], diff --git a/tests/components/onewire/test_options_flow.py b/tests/components/onewire/test_options_flow.py new file mode 100644 index 00000000000..2edb9da7ffc --- /dev/null +++ b/tests/components/onewire/test_options_flow.py @@ -0,0 +1,237 @@ +"""Tests for 1-Wire config flow.""" +from unittest.mock import MagicMock, patch + +from homeassistant.components.onewire.const import ( + CONF_TYPE_SYSBUS, + DOMAIN, + INPUT_ENTRY_CLEAR_OPTIONS, + INPUT_ENTRY_DEVICE_SELECTION, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import ( + RESULT_TYPE_ABORT, + RESULT_TYPE_CREATE_ENTRY, + RESULT_TYPE_FORM, +) + +from . import setup_owproxy_mock_devices +from .const import MOCK_OWPROXY_DEVICES + + +class FakeDevice: + """Mock Class for mocking DeviceEntry.""" + + name_by_user = "Given Name" + + +class FakeOWHubSysBus: + """Mock Class for mocking onewire hub.""" + + type = CONF_TYPE_SYSBUS + + +async def test_user_owserver_options_clear( + hass: HomeAssistant, + config_entry: ConfigEntry, + owproxy: MagicMock, +): + """Test clearing the options.""" + setup_owproxy_mock_devices( + owproxy, Platform.SENSOR, [x for x in MOCK_OWPROXY_DEVICES if "28." in x] + ) + + # Verify that first config step comes back with a selection list of all the 28-family devices + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["data_schema"].schema["device_selection"].options == { + "28.111111111111": False, + "28.222222222222": False, + "28.222222222223": False, + } + + # Verify that the clear-input action clears the options dict + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={INPUT_ENTRY_CLEAR_OPTIONS: True}, + ) + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["data"] == {} + + +async def test_user_owserver_options_empty_selection( + hass: HomeAssistant, + config_entry: ConfigEntry, + owproxy: MagicMock, +): + """Test leaving the selection of devices empty.""" + setup_owproxy_mock_devices( + owproxy, Platform.SENSOR, [x for x in MOCK_OWPROXY_DEVICES if "28." in x] + ) + + # Verify that first config step comes back with a selection list of all the 28-family devices + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["data_schema"].schema["device_selection"].options == { + "28.111111111111": False, + "28.222222222222": False, + "28.222222222223": False, + } + + # Verify that an empty selection does not modify the options + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={INPUT_ENTRY_DEVICE_SELECTION: []}, + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "device_selection" + assert result["errors"] == {"base": "device_not_selected"} + + +async def test_user_owserver_options_set_single( + hass: HomeAssistant, + config_entry: ConfigEntry, + owproxy: MagicMock, +): + """Test configuring a single device.""" + setup_owproxy_mock_devices( + owproxy, Platform.SENSOR, [x for x in MOCK_OWPROXY_DEVICES if "28." in x] + ) + + # Clear config options to certify functionality when starting from scratch + config_entry.options = {} + + # Verify that first config step comes back with a selection list of all the 28-family devices + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["data_schema"].schema["device_selection"].options == { + "28.111111111111": False, + "28.222222222222": False, + "28.222222222223": False, + } + + # Verify that a single selected device to configure comes back as a form with the device to configure + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={INPUT_ENTRY_DEVICE_SELECTION: ["28.111111111111"]}, + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["description_placeholders"]["sensor_id"] == "28.111111111111" + + # Verify that the setting for the device comes back as default when no input is given + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={}, + ) + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert ( + result["data"]["device_options"]["28.111111111111"]["precision"] + == "temperature" + ) + + +async def test_user_owserver_options_set_multiple( + hass: HomeAssistant, + config_entry: ConfigEntry, + owproxy: MagicMock, +): + """Test configuring multiple consecutive devices in a row.""" + setup_owproxy_mock_devices( + owproxy, Platform.SENSOR, [x for x in MOCK_OWPROXY_DEVICES if "28." in x] + ) + + # Initialize onewire hub + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + # Verify that first config step comes back with a selection list of all the 28-family devices + with patch( + "homeassistant.helpers.device_registry.DeviceRegistry.async_get_device", + return_value=FakeDevice(), + ): + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["data_schema"].schema["device_selection"].options == { + "Given Name (28.111111111111)": False, + "Given Name (28.222222222222)": False, + "Given Name (28.222222222223)": False, + } + + # Verify that selecting two devices to configure comes back as a + # form with the first device to configure using it's long name as entry + with patch( + "homeassistant.helpers.device_registry.DeviceRegistry.async_get_device", + return_value=FakeDevice(), + ): + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + INPUT_ENTRY_DEVICE_SELECTION: [ + "Given Name (28.111111111111)", + "Given Name (28.222222222222)", + ] + }, + ) + assert result["type"] == RESULT_TYPE_FORM + assert ( + result["description_placeholders"]["sensor_id"] + == "Given Name (28.222222222222)" + ) + + # Verify that next sensor is coming up for configuration after the first + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={"precision": "temperature"}, + ) + assert result["type"] == RESULT_TYPE_FORM + assert ( + result["description_placeholders"]["sensor_id"] + == "Given Name (28.111111111111)" + ) + + # Verify that the setting for the device comes back as default when no input is given + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={"precision": "temperature9"}, + ) + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert ( + result["data"]["device_options"]["28.222222222222"]["precision"] + == "temperature" + ) + assert ( + result["data"]["device_options"]["28.111111111111"]["precision"] + == "temperature9" + ) + + +async def test_user_owserver_options_no_devices( + hass: HomeAssistant, + config_entry: ConfigEntry, + owproxy: MagicMock, +): + """Test that options does not change when no devices are available.""" + # Initialize onewire hub + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + # Verify that first config step comes back with an empty list of possible devices to choose from + result = await hass.config_entries.options.async_init(config_entry.entry_id) + await hass.async_block_till_done() + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "No configurable devices found." + + +async def test_user_sysbus_options( + hass: HomeAssistant, + config_entry: ConfigEntry, +): + """Test that SysBus options flow aborts on init.""" + hass.data[DOMAIN] = {config_entry.entry_id: FakeOWHubSysBus()} + result = await hass.config_entries.options.async_init(config_entry.entry_id) + await hass.async_block_till_done() + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "SysBus setup does not have any config options." diff --git a/tests/components/open_meteo/fixtures/forecast.json b/tests/components/open_meteo/fixtures/forecast.json index e9510cb2d2c..532b216a778 100644 --- a/tests/components/open_meteo/fixtures/forecast.json +++ b/tests/components/open_meteo/fixtures/forecast.json @@ -4,7 +4,7 @@ "daily_units": { "winddirection_10m_dominant": "°", "temperature_2m_max": "°C", - "windspeed_10m_max": "km\/h", + "windspeed_10m_max": "km/h", "sunrise": "iso8601", "precipitation_hours": "h", "temperature_2m_min": "°C", @@ -12,3411 +12,295 @@ "sunset": "iso8601", "apparent_temperature_max": "°C", "weathercode": "wmo code", - "windgusts_10m_max": "km\/h", - "shortwave_radiation_sum": "MJ\/m²", + "windgusts_10m_max": "km/h", + "shortwave_radiation_sum": "MJ/m²", "time": "iso8601", "precipitation_sum": "mm" }, "hourly": { "soil_moisture_3_9cm": [ - 0.308, - 0.308, - 0.308, - 0.308, - 0.309, - 0.309, - 0.309, - 0.308, - 0.308, - 0.308, - 0.308, - 0.308, - 0.308, - 0.308, - 0.308, - 0.307, - 0.307, - 0.307, - 0.307, - 0.307, - 0.307, - 0.307, - 0.307, - 0.307, - 0.307, - 0.307, - 0.307, - 0.307, - 0.307, - 0.307, - 0.307, - 0.307, - 0.307, - 0.307, - 0.307, - 0.307, - 0.307, - 0.306, - 0.306, - 0.306, - 0.307, - 0.307, - 0.306, - 0.306, - 0.307, - 0.307, - 0.308, - 0.307, - 0.307, - 0.307, - 0.307, - 0.307, - 0.307, - 0.307, - 0.307, - 0.307, - 0.306, - 0.306, - 0.306, - 0.306, - 0.306, - 0.306, - 0.306, - 0.305, - 0.305, - 0.306, - 0.308, - 0.31, - 0.306, - 0.306, - 0.306, - 0.307, - 0.307, - 0.308, - 0.308, - 0.307, - 0.307, - 0.307, - 0.307, - 0.307, - 0.307, - 0.307, - 0.307, - 0.306, - 0.306, - 0.307, - 0.308, - 0.308, - 0.308, - 0.31, - 0.311, - 0.311, - 0.311, - 0.311, - 0.31, - 0.31, - 0.309, - 0.309, - 0.309, - 0.309, - 0.309, - 0.308, - 0.308, - 0.308, - 0.308, - 0.308, - 0.308, - 0.307, - 0.307, - 0.307, - 0.307, - 0.307, - 0.307, - 0.307, - 0.306, - 0.306, - 0.306, - 0.306, - 0.306, - 0.306, - 0.306, - 0.306, - 0.306, - 0.306, - 0.306, - 0.306, - 0.306, - 0.306, - 0.306, - 0.306, - 0.306, - 0.306, - 0.306, - 0.306, - 0.301, - 0.301, - 0.301, - 0.301, - 0.301, - 0.301, - 0.301, - 0.301, - 0.301, - 0.301, - 0.301, - 0.301, - 0.302, - 0.303, - 0.305, - 0.305, - 0.306, - 0.306, - 0.306, - 0.305, - 0.305, - 0.304, - 0.304, - 0.304, - 0.303, - 0.303, - 0.303, - 0.303, - 0.303, - 0.303, - 0.302, - 0.302, - 0.303, - 0.308 + 0.308, 0.308, 0.308, 0.308, 0.309, 0.309, 0.309, 0.308, 0.308, 0.308, + 0.308, 0.308, 0.308, 0.308, 0.308, 0.307, 0.307, 0.307, 0.307, 0.307, + 0.307, 0.307, 0.307, 0.307, 0.307, 0.307, 0.307, 0.307, 0.307, 0.307, + 0.307, 0.307, 0.307, 0.307, 0.307, 0.307, 0.307, 0.306, 0.306, 0.306, + 0.307, 0.307, 0.306, 0.306, 0.307, 0.307, 0.308, 0.307, 0.307, 0.307, + 0.307, 0.307, 0.307, 0.307, 0.307, 0.307, 0.306, 0.306, 0.306, 0.306, + 0.306, 0.306, 0.306, 0.305, 0.305, 0.306, 0.308, 0.31, 0.306, 0.306, + 0.306, 0.307, 0.307, 0.308, 0.308, 0.307, 0.307, 0.307, 0.307, 0.307, + 0.307, 0.307, 0.307, 0.306, 0.306, 0.307, 0.308, 0.308, 0.308, 0.31, + 0.311, 0.311, 0.311, 0.311, 0.31, 0.31, 0.309, 0.309, 0.309, 0.309, 0.309, + 0.308, 0.308, 0.308, 0.308, 0.308, 0.308, 0.307, 0.307, 0.307, 0.307, + 0.307, 0.307, 0.307, 0.306, 0.306, 0.306, 0.306, 0.306, 0.306, 0.306, + 0.306, 0.306, 0.306, 0.306, 0.306, 0.306, 0.306, 0.306, 0.306, 0.306, + 0.306, 0.306, 0.306, 0.301, 0.301, 0.301, 0.301, 0.301, 0.301, 0.301, + 0.301, 0.301, 0.301, 0.301, 0.301, 0.302, 0.303, 0.305, 0.305, 0.306, + 0.306, 0.306, 0.305, 0.305, 0.304, 0.304, 0.304, 0.303, 0.303, 0.303, + 0.303, 0.303, 0.303, 0.302, 0.302, 0.303, 0.308 ], "soil_temperature_0cm": [ - 6.6, - 6.6, - 6.5, - 6.1, - 6.1, - 6, - 6.1, - 5.7, - 5.4, - 6.3, - 7, - 7.6, - 7.9, - 8.1, - 7.9, - 7.3, - 6.7, - 6.3, - 6.3, - 5.7, - 5.5, - 5.6, - 5.6, - 4.9, - 5.1, - 4.7, - 4.8, - 4.7, - 3.6, - 2.3, - 1.4, - 0.8, - 0.4, - 1.7, - 2.7, - 3.8, - 4.8, - 5.5, - 4.8, - 4.5, - 4.1, - 3.7, - 3.6, - 3.7, - 3.7, - 3.5, - 3.5, - 3.4, - 3.6, - 3.6, - 3.2, - 3.3, - 3.3, - 3.2, - 3, - 3.1, - 3.1, - 3.3, - 4.5, - 4.9, - 5.3, - 5.5, - 5.2, - 4.6, - 3.7, - 3.3, - 3.1, - 2.8, - 2.2, - 1.9, - 1.8, - 1.7, - 1.4, - 1.1, - 0.3, - -0, - -0, - -0, - -0.1, - -0.1, - -0.2, - -0.2, - 1.4, - 2.8, - 4.6, - 5.4, - 5, - 3.7, - 2.5, - 2.6, - 2.4, - 2.6, - 2.4, - 2.1, - 1.6, - 1.7, - 0.8, - -0, - -0.2, - -0.2, - -0.2, - -0.4, - -0.6, - -0.7, - -0.3, - 0.2, - 1, - 1.9, - 2.9, - 3.8, - 3.6, - 3.1, - 2.3, - 2, - 1.7, - 1.3, - 1, - 0.7, - 0.4, - 0.3, - 0.3, - 0.2, - 0.1, - 0.1, - -0.1, - -0.2, - -0.4, - -0.5, - -0.4, - -0.1, - 0.3, - 0.8, - 1.5, - 2.1, - 2.4, - 1.8, - 1.1, - 1.1, - 1.4, - 1.6, - 1.7, - 1.8, - 1.8, - 1.7, - 1.6, - 1.4, - 1.4, - 1.4, - 1.3, - 1.1, - 0.9, - 0.6, - 0.5, - 0.5, - 0.8, - 1.5, - 2.5, - 3.4, - 3.1, - 2.5, - 1.8, - 1.8, - 2.1, - 2.4, - 2.5, - 2.5, - 2.6, - 2.7 + 6.6, 6.6, 6.5, 6.1, 6.1, 6, 6.1, 5.7, 5.4, 6.3, 7, 7.6, 7.9, 8.1, 7.9, + 7.3, 6.7, 6.3, 6.3, 5.7, 5.5, 5.6, 5.6, 4.9, 5.1, 4.7, 4.8, 4.7, 3.6, 2.3, + 1.4, 0.8, 0.4, 1.7, 2.7, 3.8, 4.8, 5.5, 4.8, 4.5, 4.1, 3.7, 3.6, 3.7, 3.7, + 3.5, 3.5, 3.4, 3.6, 3.6, 3.2, 3.3, 3.3, 3.2, 3, 3.1, 3.1, 3.3, 4.5, 4.9, + 5.3, 5.5, 5.2, 4.6, 3.7, 3.3, 3.1, 2.8, 2.2, 1.9, 1.8, 1.7, 1.4, 1.1, 0.3, + -0, -0, -0, -0.1, -0.1, -0.2, -0.2, 1.4, 2.8, 4.6, 5.4, 5, 3.7, 2.5, 2.6, + 2.4, 2.6, 2.4, 2.1, 1.6, 1.7, 0.8, -0, -0.2, -0.2, -0.2, -0.4, -0.6, -0.7, + -0.3, 0.2, 1, 1.9, 2.9, 3.8, 3.6, 3.1, 2.3, 2, 1.7, 1.3, 1, 0.7, 0.4, 0.3, + 0.3, 0.2, 0.1, 0.1, -0.1, -0.2, -0.4, -0.5, -0.4, -0.1, 0.3, 0.8, 1.5, + 2.1, 2.4, 1.8, 1.1, 1.1, 1.4, 1.6, 1.7, 1.8, 1.8, 1.7, 1.6, 1.4, 1.4, 1.4, + 1.3, 1.1, 0.9, 0.6, 0.5, 0.5, 0.8, 1.5, 2.5, 3.4, 3.1, 2.5, 1.8, 1.8, 2.1, + 2.4, 2.5, 2.5, 2.6, 2.7 ], "weathercode": [ - 3, - 3, - 3, - 3, - 51, - 3, - 3, - 3, - 3, - 3, - 3, - 3, - 3, - 3, - 3, - 61, - 61, - 3, - 3, - 3, - 3, - 3, - 3, - 3, - 3, - 3, - 3, - 3, - 2, - 1, - 1, - 1, - 2, - 3, - 3, - 3, - 3, - 3, - 3, - 61, - 3, - 3, - 3, - 61, - 61, - 3, - 3, - 3, - 3, - 3, - 3, - 3, - 3, - 3, - 3, - 3, - 3, - 3, - 3, - 3, - 3, - 3, - 3, - 3, - 61, - 61, - 61, - 61, - 3, - 3, - 3, - 3, - 3, - 3, - 3, - 3, - 3, - 3, - 3, - 2, - 3, - 3, - 3, - 0, - 0, - 1, - 1, - 2, - 3, - 61, - 3, - 3, - 3, - 3, - 3, - 3, - 1, - 0, - 3, - 3, - 3, - 3, - 3, - 3, - 3, - 3, - 3, - 3, - 3, - 3, - 3, - 3, - 3, - 61, - 61, - 61, - 3, - 3, - 3, - 3, - 3, - 3, - 3, - 3, - 3, - 3, - 3, - 3, - 77, - 77, - 77, - 3, - 3, - 3, - 3, - 3, - 3, - 51, - 51, - 51, - 3, - 3, - 3, - 3, - 3, - 3, - 53, - 53, - 53, - 3, - 3, - 3, - 2, - 2, - 2, - 1, - 1, - 1, - 3, - 3, - 3, - 61, - 61, - 61, - 61, - 61, - 61, - 80 + 3, 3, 3, 3, 51, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 61, 61, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 2, 1, 1, 1, 2, 3, 3, 3, 3, 3, 3, 61, 3, 3, 3, 61, 61, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 61, 61, 61, 61, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, 2, 3, 3, 3, 0, 0, 1, 1, 2, 3, 61, 3, 3, 3, 3, 3, + 3, 1, 0, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 61, 61, 61, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 77, 77, 77, 3, 3, 3, 3, 3, 3, 51, 51, 51, 3, 3, + 3, 3, 3, 3, 53, 53, 53, 3, 3, 3, 2, 2, 2, 1, 1, 1, 3, 3, 3, 61, 61, 61, + 61, 61, 61, 80 ], "windspeed_180m": [ - 26.8, - 26.6, - 27.5, - 24.6, - 27.8, - 28.5, - 27, - 24.9, - 26.6, - 22.8, - 21.1, - 18.6, - 14.3, - 14, - 12.3, - 13.3, - 10.8, - 8.6, - 10.7, - 12, - 14, - 16.4, - 17.1, - 17.9, - 20, - 22.4, - 20.9, - 19.5, - 16.9, - 16.5, - 14.5, - 13.8, - 16.2, - 20.7, - 16.3, - 12.2, - 13.8, - 14.6, - 22.7, - 26.5, - 22.9, - 30.1, - 34.3, - 35.4, - 31.3, - 30.7, - 33.5, - 34.4, - 32.8, - 31.4, - 29.8, - 30.4, - 30.5, - 27.5, - 26.1, - 27.1, - 25.9, - 27.8, - 25.4, - 22.1, - 20.1, - 16.7, - 17.1, - 17, - 20.1, - 16.7, - 20.3, - 23.3, - 32.4, - 34.8, - 36.2, - 35.8, - 34.2, - 33, - 36.1, - 38.7, - 38.8, - 37, - 35.5, - 35.7, - 35.9, - 36.9, - 37.6, - 35.1, - 30, - 25, - 21.1, - 24.5, - 29.8, - 30.4, - 29.5, - 29.8, - 31.9, - 32.2, - 24.2, - 25.7, - 27.5, - 25.7, - 26.5, - 27.4, - 28.5, - 28.8, - 28.9, - 28.6, - 28, - 27.1, - 25.8, - 24.6, - 23.2, - 21.7, - 21.5, - 21.6, - 20.9, - 18.8, - 16.6, - 16.5, - 18.7, - 21.9, - 24.6, - 24.1, - 22.4, - 20.4, - 20.1, - 20.3, - 20, - 19.2, - 18.2, - 17.1, - 16.1, - 15.2, - 15.1, - 16, - 17.6, - 19.5, - 21.8, - 25.4, - 30, - 33.1, - 36.4, - 40, - 41.9, - 43.2, - 43.6, - 41.9, - 39, - 35.9, - 35.1, - 34.9, - 34.7, - 34.5, - 34.3, - 33, - 30.8, - 28, - 24.4, - 21.9, - 19.5, - 17.8, - 18.4, - 20, - 23.4, - 28.5, - 36.2, - 44.9, - 47.4, - 47.8, - 47.9, - 48.8 + 26.8, 26.6, 27.5, 24.6, 27.8, 28.5, 27, 24.9, 26.6, 22.8, 21.1, 18.6, + 14.3, 14, 12.3, 13.3, 10.8, 8.6, 10.7, 12, 14, 16.4, 17.1, 17.9, 20, 22.4, + 20.9, 19.5, 16.9, 16.5, 14.5, 13.8, 16.2, 20.7, 16.3, 12.2, 13.8, 14.6, + 22.7, 26.5, 22.9, 30.1, 34.3, 35.4, 31.3, 30.7, 33.5, 34.4, 32.8, 31.4, + 29.8, 30.4, 30.5, 27.5, 26.1, 27.1, 25.9, 27.8, 25.4, 22.1, 20.1, 16.7, + 17.1, 17, 20.1, 16.7, 20.3, 23.3, 32.4, 34.8, 36.2, 35.8, 34.2, 33, 36.1, + 38.7, 38.8, 37, 35.5, 35.7, 35.9, 36.9, 37.6, 35.1, 30, 25, 21.1, 24.5, + 29.8, 30.4, 29.5, 29.8, 31.9, 32.2, 24.2, 25.7, 27.5, 25.7, 26.5, 27.4, + 28.5, 28.8, 28.9, 28.6, 28, 27.1, 25.8, 24.6, 23.2, 21.7, 21.5, 21.6, + 20.9, 18.8, 16.6, 16.5, 18.7, 21.9, 24.6, 24.1, 22.4, 20.4, 20.1, 20.3, + 20, 19.2, 18.2, 17.1, 16.1, 15.2, 15.1, 16, 17.6, 19.5, 21.8, 25.4, 30, + 33.1, 36.4, 40, 41.9, 43.2, 43.6, 41.9, 39, 35.9, 35.1, 34.9, 34.7, 34.5, + 34.3, 33, 30.8, 28, 24.4, 21.9, 19.5, 17.8, 18.4, 20, 23.4, 28.5, 36.2, + 44.9, 47.4, 47.8, 47.9, 48.8 ], "soil_moisture_9_27cm": [ - 0.315, - 0.315, - 0.314, - 0.314, - 0.315, - 0.315, - 0.315, - 0.315, - 0.315, - 0.315, - 0.315, - 0.314, - 0.314, - 0.314, - 0.314, - 0.314, - 0.314, - 0.314, - 0.314, - 0.314, - 0.314, - 0.314, - 0.314, - 0.314, - 0.314, - 0.314, - 0.314, - 0.314, - 0.314, - 0.314, - 0.314, - 0.314, - 0.314, - 0.314, - 0.314, - 0.314, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.314, - 0.314, - 0.314, - 0.314, - 0.314, - 0.314, - 0.314, - 0.314, - 0.314, - 0.314, - 0.314, - 0.314, - 0.314, - 0.314, - 0.314, - 0.314, - 0.314, - 0.314, - 0.314, - 0.314, - 0.314, - 0.314, - 0.314, - 0.314, - 0.314, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.308, - 0.308, - 0.308, - 0.308, - 0.308, - 0.308, - 0.308, - 0.308, - 0.308, - 0.308, - 0.308, - 0.308, - 0.308, - 0.308, - 0.308, - 0.308, - 0.308, - 0.308, - 0.308, - 0.309, - 0.309, - 0.309, - 0.309, - 0.309, - 0.309, - 0.309, - 0.309, - 0.309, - 0.309, - 0.309, - 0.309, - 0.309, - 0.309, - 0.309 + 0.315, 0.315, 0.314, 0.314, 0.315, 0.315, 0.315, 0.315, 0.315, 0.315, + 0.315, 0.314, 0.314, 0.314, 0.314, 0.314, 0.314, 0.314, 0.314, 0.314, + 0.314, 0.314, 0.314, 0.314, 0.314, 0.314, 0.314, 0.314, 0.314, 0.314, + 0.314, 0.314, 0.314, 0.314, 0.314, 0.314, 0.313, 0.313, 0.313, 0.313, + 0.313, 0.313, 0.313, 0.313, 0.313, 0.313, 0.313, 0.313, 0.313, 0.313, + 0.313, 0.313, 0.313, 0.313, 0.313, 0.313, 0.313, 0.313, 0.313, 0.313, + 0.313, 0.313, 0.313, 0.313, 0.313, 0.313, 0.313, 0.313, 0.313, 0.313, + 0.313, 0.313, 0.313, 0.313, 0.313, 0.313, 0.313, 0.313, 0.313, 0.313, + 0.313, 0.313, 0.313, 0.313, 0.313, 0.313, 0.313, 0.313, 0.313, 0.313, + 0.313, 0.314, 0.314, 0.314, 0.314, 0.314, 0.314, 0.314, 0.314, 0.314, + 0.314, 0.314, 0.314, 0.314, 0.314, 0.314, 0.314, 0.314, 0.314, 0.314, + 0.314, 0.314, 0.314, 0.314, 0.314, 0.314, 0.313, 0.313, 0.313, 0.313, + 0.313, 0.313, 0.313, 0.313, 0.313, 0.313, 0.313, 0.313, 0.313, 0.313, + 0.313, 0.313, 0.313, 0.313, 0.308, 0.308, 0.308, 0.308, 0.308, 0.308, + 0.308, 0.308, 0.308, 0.308, 0.308, 0.308, 0.308, 0.308, 0.308, 0.308, + 0.308, 0.308, 0.308, 0.309, 0.309, 0.309, 0.309, 0.309, 0.309, 0.309, + 0.309, 0.309, 0.309, 0.309, 0.309, 0.309, 0.309, 0.309 ], "shortwave_radiation": [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.7, - 23, - 42.1, - 69.7, - 83, - 80.3, - 64.1, - 28, - 7.9, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.7, - 32.4, - 77.2, - 109.4, - 124.7, - 120.1, - 93.1, - 26.9, - 14.6, - 0, - -0, - 0, - -0, - 0, - 0, - -0, - -0, - -0, - 0, - 0, - -0, - -0, - 0, - -0, - 0.3, - 17.4, - 58.5, - 92.6, - 107.1, - 114.2, - 92.2, - 46.9, - 12.3, - 0, - -0, - 0, - -0, - 0.1, - -0, - -0.1, - 0.1, - -0.1, - 0, - 0.1, - -0, - 0.1, - 0, - -0.2, - 0.3, - 27.8, - 71.4, - 115.2, - 117.8, - 83.3, - 87.2, - 54.4, - 11.1, - -0.2, - -0.1, - -0, - -0, - -0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - -0, - -0, - 0, - 0, - 51.3, - 121.4, - 182, - 227.4, - 239.7, - 191.8, - 114.5, - 32.3, - 0, - 0, - 0, - -0, - -0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - -0, - -0, - 0, - 0, - 44.9, - 103.3, - 139.2, - 150.5, - 141.8, - 116.6, - 74.9, - 23.4, - 0, - 0, - 0, - -0, - -0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - -0, - -0, - 0, - 0, - 30.4, - 75.2, - 126.8, - 179.4, - 208.1, - 169.6, - 100.9, - 28.2, - 0, - 0, - 0, - -0, - -0, - 0, - 0 + 0, 0, 0, 0, 0, 0, 0, 0, 0.7, 23, 42.1, 69.7, 83, 80.3, 64.1, 28, 7.9, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.7, 32.4, 77.2, 109.4, 124.7, + 120.1, 93.1, 26.9, 14.6, 0, -0, 0, -0, 0, 0, -0, -0, -0, 0, 0, -0, -0, 0, + -0, 0.3, 17.4, 58.5, 92.6, 107.1, 114.2, 92.2, 46.9, 12.3, 0, -0, 0, -0, + 0.1, -0, -0.1, 0.1, -0.1, 0, 0.1, -0, 0.1, 0, -0.2, 0.3, 27.8, 71.4, + 115.2, 117.8, 83.3, 87.2, 54.4, 11.1, -0.2, -0.1, -0, -0, -0, 0, 0, 0, 0, + 0, 0, 0, -0, -0, 0, 0, 51.3, 121.4, 182, 227.4, 239.7, 191.8, 114.5, 32.3, + 0, 0, 0, -0, -0, 0, 0, 0, 0, 0, 0, 0, -0, -0, 0, 0, 44.9, 103.3, 139.2, + 150.5, 141.8, 116.6, 74.9, 23.4, 0, 0, 0, -0, -0, 0, 0, 0, 0, 0, 0, 0, -0, + -0, 0, 0, 30.4, 75.2, 126.8, 179.4, 208.1, 169.6, 100.9, 28.2, 0, 0, 0, + -0, -0, 0, 0 ], "cloudcover_mid": [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 66, - 88, - 100, - 64, - 63, - 0, - 0, - 0, - 0, - 0, - 0, - 6, - 62, - 62, - 78, - 100, - 92, - 100, - 100, - 100, - 100, - 100, - 100, - 100, - 100, - 100, - 100, - 100, - 100, - 100, - 98, - 94, - 95, - 100, - 100, - 100, - 85, - 29, - 21, - 10, - 51, - 0, - 5, - 10, - 16, - 52, - 98, - 100, - 100, - 100, - 100, - 64, - 39, - 35, - 12, - 0, - 33, - 67, - 100, - 95, - 90, - 85, - 90, - 95, - 100, - 100, - 100, - 100, - 100, - 100, - 100, - 90, - 81, - 71, - 77, - 82, - 88, - 92, - 96, - 100, - 100, - 100, - 100, - 100, - 100, - 100, - 93, - 86, - 79, - 71, - 63, - 55, - 44, - 64, - 84, - 86, - 88, - 90, - 41, - 42, - 42, - 50, - 59, - 67, - 56, - 44, - 33, - 22, - 11, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 20, - 40, - 60, - 73, - 87, - 100, - 100, - 100, - 100, - 94 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 66, 88, 100, 64, 63, 0, + 0, 0, 0, 0, 0, 6, 62, 62, 78, 100, 92, 100, 100, 100, 100, 100, 100, 100, + 100, 100, 100, 100, 100, 100, 98, 94, 95, 100, 100, 100, 85, 29, 21, 10, + 51, 0, 5, 10, 16, 52, 98, 100, 100, 100, 100, 64, 39, 35, 12, 0, 33, 67, + 100, 95, 90, 85, 90, 95, 100, 100, 100, 100, 100, 100, 100, 90, 81, 71, + 77, 82, 88, 92, 96, 100, 100, 100, 100, 100, 100, 100, 93, 86, 79, 71, 63, + 55, 44, 64, 84, 86, 88, 90, 41, 42, 42, 50, 59, 67, 56, 44, 33, 22, 11, 0, + 0, 0, 0, 0, 0, 0, 20, 40, 60, 73, 87, 100, 100, 100, 100, 94 ], "cloudcover": [ - 100, - 100, - 98, - 100, - 100, - 98, - 96, - 88, - 88, - 100, - 94, - 100, - 100, - 100, - 100, - 100, - 100, - 100, - 100, - 100, - 100, - 100, - 100, - 83, - 87, - 100, - 100, - 100, - 79, - 34, - 7, - 0, - 49, - 100, - 99, - 100, - 100, - 95, - 100, - 100, - 100, - 100, - 96, - 95, - 100, - 100, - 100, - 96, - 97, - 100, - 91, - 93, - 100, - 91, - 98, - 100, - 99, - 100, - 100, - 100, - 100, - 100, - 100, - 100, - 100, - 100, - 100, - 100, - 100, - 100, - 100, - 100, - 100, - 100, - 100, - 100, - 100, - 100, - 100, - 74, - 100, - 100, - 100, - 0, - 5, - 13, - 18, - 52, - 99, - 100, - 100, - 100, - 100, - 94, - 86, - 88, - 43, - 0, - 33, - 67, - 100, - 99, - 98, - 97, - 98, - 99, - 100, - 100, - 100, - 100, - 100, - 100, - 100, - 94, - 88, - 82, - 87, - 92, - 97, - 98, - 99, - 100, - 100, - 100, - 100, - 100, - 100, - 100, - 98, - 97, - 95, - 97, - 98, - 100, - 99, - 98, - 98, - 98, - 98, - 98, - 94, - 97, - 99, - 98, - 98, - 97, - 96, - 95, - 95, - 94, - 93, - 92, - 86, - 81, - 75, - 61, - 47, - 33, - 54, - 75, - 96, - 97, - 99, - 100, - 100, - 100, - 100, - 100 + 100, 100, 98, 100, 100, 98, 96, 88, 88, 100, 94, 100, 100, 100, 100, 100, + 100, 100, 100, 100, 100, 100, 100, 83, 87, 100, 100, 100, 79, 34, 7, 0, + 49, 100, 99, 100, 100, 95, 100, 100, 100, 100, 96, 95, 100, 100, 100, 96, + 97, 100, 91, 93, 100, 91, 98, 100, 99, 100, 100, 100, 100, 100, 100, 100, + 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, + 74, 100, 100, 100, 0, 5, 13, 18, 52, 99, 100, 100, 100, 100, 94, 86, 88, + 43, 0, 33, 67, 100, 99, 98, 97, 98, 99, 100, 100, 100, 100, 100, 100, 100, + 94, 88, 82, 87, 92, 97, 98, 99, 100, 100, 100, 100, 100, 100, 100, 98, 97, + 95, 97, 98, 100, 99, 98, 98, 98, 98, 98, 94, 97, 99, 98, 98, 97, 96, 95, + 95, 94, 93, 92, 86, 81, 75, 61, 47, 33, 54, 75, 96, 97, 99, 100, 100, 100, + 100, 100 ], "relativehumidity_2m": [ - 95, - 95, - 94, - 93, - 95, - 95, - 94, - 94, - 95, - 94, - 92, - 88, - 85, - 83, - 82, - 84, - 89, - 89, - 93, - 93, - 95, - 96, - 95, - 96, - 94, - 92, - 89, - 86, - 87, - 90, - 93, - 96, - 97, - 100, - 96, - 88, - 85, - 83, - 79, - 82, - 84, - 83, - 85, - 85, - 84, - 84, - 85, - 86, - 87, - 89, - 90, - 91, - 92, - 91, - 92, - 90, - 88, - 87, - 84, - 80, - 77, - 74, - 73, - 75, - 82, - 86, - 89, - 89, - 83, - 82, - 80, - 78, - 78, - 78, - 79, - 79, - 79, - 81, - 82, - 83, - 83, - 85, - 82, - 77, - 73, - 69, - 68, - 70, - 76, - 82, - 84, - 84, - 83, - 84, - 86, - 87, - 89, - 92, - 92, - 92, - 92, - 93, - 93, - 93, - 92, - 90, - 88, - 85, - 81, - 77, - 77, - 79, - 81, - 83, - 84, - 86, - 87, - 88, - 89, - 88, - 88, - 87, - 87, - 87, - 88, - 88, - 88, - 89, - 90, - 91, - 92, - 89, - 86, - 82, - 81, - 82, - 84, - 87, - 91, - 95, - 94, - 93, - 93, - 93, - 93, - 94, - 94, - 95, - 94, - 93, - 92, - 90, - 89, - 88, - 87, - 83, - 79, - 76, - 77, - 81, - 85, - 88, - 90, - 91, - 91, - 89, - 87, - 88 + 95, 95, 94, 93, 95, 95, 94, 94, 95, 94, 92, 88, 85, 83, 82, 84, 89, 89, + 93, 93, 95, 96, 95, 96, 94, 92, 89, 86, 87, 90, 93, 96, 97, 100, 96, 88, + 85, 83, 79, 82, 84, 83, 85, 85, 84, 84, 85, 86, 87, 89, 90, 91, 92, 91, + 92, 90, 88, 87, 84, 80, 77, 74, 73, 75, 82, 86, 89, 89, 83, 82, 80, 78, + 78, 78, 79, 79, 79, 81, 82, 83, 83, 85, 82, 77, 73, 69, 68, 70, 76, 82, + 84, 84, 83, 84, 86, 87, 89, 92, 92, 92, 92, 93, 93, 93, 92, 90, 88, 85, + 81, 77, 77, 79, 81, 83, 84, 86, 87, 88, 89, 88, 88, 87, 87, 87, 88, 88, + 88, 89, 90, 91, 92, 89, 86, 82, 81, 82, 84, 87, 91, 95, 94, 93, 93, 93, + 93, 94, 94, 95, 94, 93, 92, 90, 89, 88, 87, 83, 79, 76, 77, 81, 85, 88, + 90, 91, 91, 89, 87, 88 ], "cloudcover_low": [ - 100, - 100, - 98, - 100, - 100, - 98, - 96, - 88, - 88, - 100, - 94, - 100, - 100, - 100, - 100, - 100, - 100, - 100, - 100, - 100, - 100, - 100, - 100, - 83, - 87, - 100, - 100, - 100, - 79, - 34, - 7, - 0, - 49, - 100, - 99, - 100, - 100, - 95, - 100, - 100, - 100, - 91, - 92, - 92, - 91, - 85, - 90, - 96, - 97, - 100, - 91, - 93, - 100, - 86, - 95, - 100, - 97, - 100, - 96, - 93, - 64, - 81, - 62, - 80, - 83, - 89, - 86, - 79, - 20, - 20, - 48, - 71, - 64, - 49, - 19, - 0, - 0, - 0, - 31, - 4, - 0, - 0, - 0, - 0, - 0, - 0, - 2, - 0, - 70, - 80, - 89, - 100, - 64, - 88, - 75, - 76, - 34, - 0, - 3, - 6, - 9, - 11, - 12, - 14, - 25, - 36, - 47, - 38, - 29, - 19, - 40, - 61, - 81, - 79, - 77, - 75, - 75, - 75, - 74, - 69, - 64, - 59, - 59, - 58, - 58, - 65, - 72, - 79, - 81, - 84, - 86, - 91, - 95, - 100, - 99, - 98, - 97, - 93, - 90, - 87, - 88, - 93, - 98, - 97, - 96, - 95, - 93, - 91, - 89, - 90, - 91, - 92, - 86, - 81, - 75, - 61, - 47, - 33, - 52, - 71, - 90, - 93, - 96, - 99, - 89, - 80, - 70, - 80 + 100, 100, 98, 100, 100, 98, 96, 88, 88, 100, 94, 100, 100, 100, 100, 100, + 100, 100, 100, 100, 100, 100, 100, 83, 87, 100, 100, 100, 79, 34, 7, 0, + 49, 100, 99, 100, 100, 95, 100, 100, 100, 91, 92, 92, 91, 85, 90, 96, 97, + 100, 91, 93, 100, 86, 95, 100, 97, 100, 96, 93, 64, 81, 62, 80, 83, 89, + 86, 79, 20, 20, 48, 71, 64, 49, 19, 0, 0, 0, 31, 4, 0, 0, 0, 0, 0, 0, 2, + 0, 70, 80, 89, 100, 64, 88, 75, 76, 34, 0, 3, 6, 9, 11, 12, 14, 25, 36, + 47, 38, 29, 19, 40, 61, 81, 79, 77, 75, 75, 75, 74, 69, 64, 59, 59, 58, + 58, 65, 72, 79, 81, 84, 86, 91, 95, 100, 99, 98, 97, 93, 90, 87, 88, 93, + 98, 97, 96, 95, 93, 91, 89, 90, 91, 92, 86, 81, 75, 61, 47, 33, 52, 71, + 90, 93, 96, 99, 89, 80, 70, 80 ], "soil_moisture_27_81cm": [ - 0.332, - 0.332, - 0.332, - 0.332, - 0.331, - 0.331, - 0.331, - 0.331, - 0.331, - 0.331, - 0.331, - 0.331, - 0.331, - 0.331, - 0.331, - 0.331, - 0.331, - 0.331, - 0.331, - 0.331, - 0.331, - 0.331, - 0.331, - 0.331, - 0.331, - 0.331, - 0.331, - 0.331, - 0.331, - 0.331, - 0.331, - 0.331, - 0.331, - 0.331, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.329, - 0.329, - 0.329, - 0.329, - 0.329, - 0.329, - 0.329, - 0.329, - 0.329, - 0.329, - 0.329, - 0.329, - 0.329, - 0.329, - 0.329, - 0.329, - 0.329, - 0.329, - 0.329, - 0.329, - 0.329, - 0.329, - 0.329, - 0.329, - 0.329, - 0.329, - 0.329, - 0.329, - 0.329, - 0.329, - 0.329, - 0.329, - 0.329, - 0.329, - 0.329, - 0.329, - 0.329, - 0.329, - 0.324, - 0.324, - 0.324, - 0.324, - 0.324, - 0.324, - 0.324, - 0.324, - 0.324, - 0.324, - 0.324, - 0.324, - 0.324, - 0.324, - 0.324, - 0.324, - 0.324, - 0.324, - 0.324, - 0.324, - 0.324, - 0.324, - 0.324, - 0.324, - 0.324, - 0.324, - 0.324, - 0.324, - 0.324, - 0.324, - 0.324, - 0.324, - 0.324, - 0.324 + 0.332, 0.332, 0.332, 0.332, 0.331, 0.331, 0.331, 0.331, 0.331, 0.331, + 0.331, 0.331, 0.331, 0.331, 0.331, 0.331, 0.331, 0.331, 0.331, 0.331, + 0.331, 0.331, 0.331, 0.331, 0.331, 0.331, 0.331, 0.331, 0.331, 0.331, + 0.331, 0.331, 0.331, 0.331, 0.33, 0.33, 0.33, 0.33, 0.33, 0.33, 0.33, + 0.33, 0.33, 0.33, 0.33, 0.33, 0.33, 0.33, 0.33, 0.33, 0.33, 0.33, 0.33, + 0.33, 0.33, 0.33, 0.33, 0.33, 0.33, 0.33, 0.33, 0.33, 0.33, 0.33, 0.33, + 0.33, 0.33, 0.33, 0.33, 0.33, 0.33, 0.33, 0.33, 0.33, 0.33, 0.33, 0.33, + 0.33, 0.33, 0.33, 0.33, 0.33, 0.33, 0.33, 0.33, 0.33, 0.33, 0.33, 0.33, + 0.33, 0.33, 0.33, 0.33, 0.33, 0.33, 0.33, 0.329, 0.329, 0.329, 0.329, + 0.329, 0.329, 0.329, 0.329, 0.329, 0.329, 0.329, 0.329, 0.329, 0.329, + 0.329, 0.329, 0.329, 0.329, 0.329, 0.329, 0.329, 0.329, 0.329, 0.329, + 0.329, 0.329, 0.329, 0.329, 0.329, 0.329, 0.329, 0.329, 0.329, 0.329, + 0.329, 0.329, 0.329, 0.329, 0.324, 0.324, 0.324, 0.324, 0.324, 0.324, + 0.324, 0.324, 0.324, 0.324, 0.324, 0.324, 0.324, 0.324, 0.324, 0.324, + 0.324, 0.324, 0.324, 0.324, 0.324, 0.324, 0.324, 0.324, 0.324, 0.324, + 0.324, 0.324, 0.324, 0.324, 0.324, 0.324, 0.324, 0.324 ], "temperature_2m": [ - 6.9, - 6.8, - 6.8, - 6.6, - 6.5, - 6.4, - 6.4, - 6.2, - 5.9, - 6, - 6.5, - 6.9, - 7.3, - 7.6, - 7.6, - 7.5, - 7.2, - 6.9, - 6.6, - 6.4, - 6, - 5.9, - 5.8, - 5.5, - 5.4, - 5.1, - 5, - 4.9, - 4.2, - 3.2, - 2.2, - 1.5, - 0.8, - 0.2, - 0.6, - 1.8, - 2.7, - 3.8, - 4.5, - 4.5, - 4.4, - 4.2, - 3.9, - 3.9, - 4, - 3.9, - 3.9, - 3.8, - 3.7, - 3.6, - 3.4, - 3.3, - 3.2, - 3.2, - 3.1, - 3.1, - 3.1, - 3.1, - 3.5, - 3.9, - 4.3, - 4.6, - 4.8, - 4.6, - 4, - 3.5, - 3.3, - 3, - 2.7, - 2.3, - 2, - 1.8, - 1.6, - 1.2, - 0.7, - 0.4, - 0.3, - 0.2, - 0.2, - 0.1, - -0, - -0.1, - 0.6, - 1.7, - 2.9, - 4, - 4.5, - 4.2, - 3.5, - 3, - 2.8, - 2.8, - 2.8, - 2.6, - 2.3, - 2, - 1.6, - 0.9, - 0.6, - 0.4, - 0.1, - -0, - -0.2, - -0.2, - -0.1, - 0, - 0.5, - 1.2, - 2.2, - 3.1, - 3.4, - 3.4, - 3.2, - 3, - 2.7, - 2.2, - 1.9, - 1.5, - 1.1, - 0.9, - 0.8, - 0.6, - 0.4, - 0.2, - -0.1, - -0.3, - -0.4, - -0.5, - -0.5, - -0.5, - -0.3, - 0.3, - 1.1, - 2, - 2.2, - 2, - 1.7, - 1.4, - 1, - 0.7, - 2, - 1.9, - 1.8, - 1.8, - 1.7, - 1.6, - 1.5, - 1.5, - 1.3, - 1.1, - 0.8, - 0.5, - 0.1, - -0.2, - -0.3, - 0.2, - 1, - 1.9, - 2.2, - 2.4, - 2.6, - 2.6, - 2.5, - 2.4, - 2.5, - 2.8, - 3, - 3 + 6.9, 6.8, 6.8, 6.6, 6.5, 6.4, 6.4, 6.2, 5.9, 6, 6.5, 6.9, 7.3, 7.6, 7.6, + 7.5, 7.2, 6.9, 6.6, 6.4, 6, 5.9, 5.8, 5.5, 5.4, 5.1, 5, 4.9, 4.2, 3.2, + 2.2, 1.5, 0.8, 0.2, 0.6, 1.8, 2.7, 3.8, 4.5, 4.5, 4.4, 4.2, 3.9, 3.9, 4, + 3.9, 3.9, 3.8, 3.7, 3.6, 3.4, 3.3, 3.2, 3.2, 3.1, 3.1, 3.1, 3.1, 3.5, 3.9, + 4.3, 4.6, 4.8, 4.6, 4, 3.5, 3.3, 3, 2.7, 2.3, 2, 1.8, 1.6, 1.2, 0.7, 0.4, + 0.3, 0.2, 0.2, 0.1, -0, -0.1, 0.6, 1.7, 2.9, 4, 4.5, 4.2, 3.5, 3, 2.8, + 2.8, 2.8, 2.6, 2.3, 2, 1.6, 0.9, 0.6, 0.4, 0.1, -0, -0.2, -0.2, -0.1, 0, + 0.5, 1.2, 2.2, 3.1, 3.4, 3.4, 3.2, 3, 2.7, 2.2, 1.9, 1.5, 1.1, 0.9, 0.8, + 0.6, 0.4, 0.2, -0.1, -0.3, -0.4, -0.5, -0.5, -0.5, -0.3, 0.3, 1.1, 2, 2.2, + 2, 1.7, 1.4, 1, 0.7, 2, 1.9, 1.8, 1.8, 1.7, 1.6, 1.5, 1.5, 1.3, 1.1, 0.8, + 0.5, 0.1, -0.2, -0.3, 0.2, 1, 1.9, 2.2, 2.4, 2.6, 2.6, 2.5, 2.4, 2.5, 2.8, + 3, 3 ], "direct_radiation": [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.7, - 1.1, - 6.3, - 3.8, - 3.6, - 6.7, - 0.1, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.1, - 2.1, - 5.7, - 14.6, - 14.3, - 15.2, - 12.3, - 1.6, - 0.3, - 0, - -0, - 0, - -0, - 0, - 0, - -0, - -0, - 0, - 0, - 0, - -0, - 0, - 0, - -0, - 0, - 0.1, - 1.8, - 2.5, - 1.9, - 1.7, - 0.9, - 0, - -0, - 0, - -0, - 0, - 0, - -0, - -0, - -0.1, - 0.1, - -0.2, - 0, - 0, - -0, - 0.2, - -0.1, - -0.1, - 0.1, - 1.2, - 5.2, - 22, - 27.9, - 15.4, - 26.4, - 3.6, - 0.1, - -0.2, - 0, - -0, - -0, - -0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - -0, - -0, - 0, - 0, - 29.8, - 72.1, - 117.5, - 160.8, - 177.8, - 135.5, - 71.8, - 16.7, - 0, - 0, - 0, - -0, - -0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - -0, - -0, - 0, - 0, - 18.6, - 42.2, - 53.5, - 52.3, - 43.3, - 38.3, - 23.5, - 7, - 0, - 0, - 0, - -0, - -0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - -0, - -0, - 0, - 0, - 5.7, - 17.3, - 44.9, - 84.7, - 113.8, - 92.4, - 52, - 13.2, - 0, - 0, - 0, - -0, - -0, - 0, - 0 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.7, 1.1, 6.3, 3.8, 3.6, 6.7, 0.1, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.1, 2.1, 5.7, 14.6, 14.3, 15.2, 12.3, + 1.6, 0.3, 0, -0, 0, -0, 0, 0, -0, -0, 0, 0, 0, -0, 0, 0, -0, 0, 0.1, 1.8, + 2.5, 1.9, 1.7, 0.9, 0, -0, 0, -0, 0, 0, -0, -0, -0.1, 0.1, -0.2, 0, 0, -0, + 0.2, -0.1, -0.1, 0.1, 1.2, 5.2, 22, 27.9, 15.4, 26.4, 3.6, 0.1, -0.2, 0, + -0, -0, -0, 0, 0, 0, 0, 0, 0, 0, -0, -0, 0, 0, 29.8, 72.1, 117.5, 160.8, + 177.8, 135.5, 71.8, 16.7, 0, 0, 0, -0, -0, 0, 0, 0, 0, 0, 0, 0, -0, -0, 0, + 0, 18.6, 42.2, 53.5, 52.3, 43.3, 38.3, 23.5, 7, 0, 0, 0, -0, -0, 0, 0, 0, + 0, 0, 0, 0, -0, -0, 0, 0, 5.7, 17.3, 44.9, 84.7, 113.8, 92.4, 52, 13.2, 0, + 0, 0, -0, -0, 0, 0 ], "dewpoint_2m": [ - 6.2, - 6.1, - 5.9, - 5.6, - 5.7, - 5.7, - 5.6, - 5.3, - 5.2, - 5.2, - 5.3, - 5.1, - 4.9, - 4.9, - 4.8, - 5, - 5.6, - 5.2, - 5.5, - 5.4, - 5.3, - 5.2, - 5.1, - 4.8, - 4.5, - 3.9, - 3.4, - 2.7, - 2.3, - 1.7, - 1.2, - 0.9, - 0.4, - 0.1, - -0, - 0, - 0.5, - 1.1, - 1.2, - 1.7, - 1.9, - 1.4, - 1.6, - 1.6, - 1.5, - 1.5, - 1.6, - 1.7, - 1.8, - 2, - 2, - 2, - 2, - 1.9, - 1.9, - 1.6, - 1.3, - 1.2, - 1.1, - 0.8, - 0.6, - 0.4, - 0.4, - 0.6, - 1.2, - 1.3, - 1.6, - 1.4, - 0.7, - 0.5, - 0.1, - -0.4, - -0.6, - -1.1, - -1.7, - -2.3, - -2.3, - -2.5, - -2.5, - -2.5, - -2.4, - -2.1, - -1.8, - -1.4, - -0.9, - -0.5, - -0.2, - 0.1, - 0.6, - 0.9, - 1.1, - 1, - 1, - 1, - 0.8, - 0.4, - -0.3, - -1, - -1.3, - -1.4, - -1.5, - -1.6, - -1.7, - -1.7, - -1.7, - -1.7, - -1.6, - -1.4, - -1.2, - -0.9, - -0.8, - -0.8, - -0.7, - -0.6, - -0.5, - -0.5, - -0.5, - -0.6, - -0.7, - -0.8, - -1, - -1.2, - -1.2, - -1.2, - -1.1, - -1.1, - -1.1, - -1, - -1, - -1, - -1, - -0.8, - -0.6, - -0.4, - -0.4, - -0.5, - -0.5, - -0.1, - 0.5, - 1, - 1.1, - 1, - 0.8, - 0.7, - 0.7, - 0.7, - 0.7, - 0.7, - 0.5, - 0.1, - -0.3, - -1, - -1.4, - -1.9, - -2.3, - -2.4, - -2.3, - -2, - -1.4, - -0.6, - 0.3, - 0.7, - 1, - 1.2, - 1.2, - 1.1, - 1.1, - 1.2 + 6.2, 6.1, 5.9, 5.6, 5.7, 5.7, 5.6, 5.3, 5.2, 5.2, 5.3, 5.1, 4.9, 4.9, 4.8, + 5, 5.6, 5.2, 5.5, 5.4, 5.3, 5.2, 5.1, 4.8, 4.5, 3.9, 3.4, 2.7, 2.3, 1.7, + 1.2, 0.9, 0.4, 0.1, -0, 0, 0.5, 1.1, 1.2, 1.7, 1.9, 1.4, 1.6, 1.6, 1.5, + 1.5, 1.6, 1.7, 1.8, 2, 2, 2, 2, 1.9, 1.9, 1.6, 1.3, 1.2, 1.1, 0.8, 0.6, + 0.4, 0.4, 0.6, 1.2, 1.3, 1.6, 1.4, 0.7, 0.5, 0.1, -0.4, -0.6, -1.1, -1.7, + -2.3, -2.3, -2.5, -2.5, -2.5, -2.4, -2.1, -1.8, -1.4, -0.9, -0.5, -0.2, + 0.1, 0.6, 0.9, 1.1, 1, 1, 1, 0.8, 0.4, -0.3, -1, -1.3, -1.4, -1.5, -1.6, + -1.7, -1.7, -1.7, -1.7, -1.6, -1.4, -1.2, -0.9, -0.8, -0.8, -0.7, -0.6, + -0.5, -0.5, -0.5, -0.6, -0.7, -0.8, -1, -1.2, -1.2, -1.2, -1.1, -1.1, + -1.1, -1, -1, -1, -1, -0.8, -0.6, -0.4, -0.4, -0.5, -0.5, -0.1, 0.5, 1, + 1.1, 1, 0.8, 0.7, 0.7, 0.7, 0.7, 0.7, 0.5, 0.1, -0.3, -1, -1.4, -1.9, + -2.3, -2.4, -2.3, -2, -1.4, -0.6, 0.3, 0.7, 1, 1.2, 1.2, 1.1, 1.1, 1.2 ], "freezinglevel_height": [ - 2432, - 2434, - 2512, - 2448, - 2512, - 2524, - 2608, - 2458, - 2446, - 2454, - 2454, - 2438, - 2544, - 2554, - 2544, - 2540, - 2550, - 2578, - 2516, - 2586, - 2540, - 2516, - 2508, - 2440, - 2464, - 2464, - 2412, - 2432, - 2260, - 2003, - 1897, - 1774, - 702, - 702, - 693, - 1511, - 635, - 634, - 522, - 668, - 624, - 697, - 674, - 586, - 910, - 856, - 846, - 697, - 665, - 623, - 611, - 633, - 562, - 566, - 620, - 438, - 399, - 398, - 412, - 466, - 501, - 537, - 548, - 513, - 492, - 488, - 448, - 462, - 564, - 569, - 544, - 555, - 540, - 514, - 565, - 471, - 464, - 530, - 507, - 562, - 603, - 566, - 574, - 489, - 462, - 472, - 569, - 562, - 567, - 579, - 597, - 605, - 604, - 598, - 589, - 585, - 582, - 573, - 562, - 548, - 524, - 498, - 468, - 434, - 416, - 402, - 394, - 399, - 412, - 438, - 469, - 508, - 548, - 559, - 562, - 551, - 529, - 497, - 456, - 431, - 407, - 379, - 366, - 356, - 331, - 291, - 241, - 176, - 125, - 72, - 50, - 105, - 199, - 299, - 326, - 308, - 298, - 329, - 376, - 420, - 424, - 411, - 381, - 344, - 297, - 245, - 227, - 219, - 202, - 172, - 135, - 93, - 67, - 44, - 41, - 68, - 113, - 191, - 258, - 335, - 455, - 565, - 687, - 845, - 950, - 1049, - 1187, - 1320 + 2432, 2434, 2512, 2448, 2512, 2524, 2608, 2458, 2446, 2454, 2454, 2438, + 2544, 2554, 2544, 2540, 2550, 2578, 2516, 2586, 2540, 2516, 2508, 2440, + 2464, 2464, 2412, 2432, 2260, 2003, 1897, 1774, 702, 702, 693, 1511, 635, + 634, 522, 668, 624, 697, 674, 586, 910, 856, 846, 697, 665, 623, 611, 633, + 562, 566, 620, 438, 399, 398, 412, 466, 501, 537, 548, 513, 492, 488, 448, + 462, 564, 569, 544, 555, 540, 514, 565, 471, 464, 530, 507, 562, 603, 566, + 574, 489, 462, 472, 569, 562, 567, 579, 597, 605, 604, 598, 589, 585, 582, + 573, 562, 548, 524, 498, 468, 434, 416, 402, 394, 399, 412, 438, 469, 508, + 548, 559, 562, 551, 529, 497, 456, 431, 407, 379, 366, 356, 331, 291, 241, + 176, 125, 72, 50, 105, 199, 299, 326, 308, 298, 329, 376, 420, 424, 411, + 381, 344, 297, 245, 227, 219, 202, 172, 135, 93, 67, 44, 41, 68, 113, 191, + 258, 335, 455, 565, 687, 845, 950, 1049, 1187, 1320 ], "soil_temperature_54cm": [ - 8.5, - 8.5, - 8.5, - 8.5, - 8.4, - 8.4, - 8.4, - 8.4, - 8.4, - 8.4, - 8.4, - 8.4, - 8.4, - 8.4, - 8.4, - 8.4, - 8.4, - 8.4, - 8.4, - 8.4, - 8.4, - 8.4, - 8.4, - 8.4, - 8.4, - 8.4, - 8.3, - 8.3, - 8.3, - 8.3, - 8.3, - 8.3, - 8.3, - 8.3, - 8.3, - 8.3, - 8.3, - 8.2, - 8.2, - 8.2, - 8.2, - 8.2, - 8.2, - 8.2, - 8.1, - 8.1, - 8.1, - 8.1, - 8.1, - 8.1, - 8.1, - 8, - 8, - 8, - 8, - 8, - 8, - 8, - 7.9, - 7.9, - 7.9, - 7.9, - 7.9, - 7.9, - 7.9, - 7.8, - 7.8, - 7.8, - 7.5, - 7.5, - 7.5, - 7.5, - 7.5, - 7.4, - 7.4, - 7.4, - 7.4, - 7.4, - 7.4, - 7.3, - 7.3, - 7.3, - 7.3, - 7.3, - 7.2, - 7.2, - 7.2, - 7.2, - 7.2, - 7.1, - 7.1, - 7.1, - 7.1, - 7.1, - 7.1, - 7, - 7, - 7, - 7, - 7, - 7, - 6.9, - 6.9, - 6.9, - 6.9, - 6.9, - 6.8, - 6.8, - 6.8, - 6.8, - 6.8, - 6.8, - 6.7, - 6.7, - 6.7, - 6.7, - 6.7, - 6.7, - 6.6, - 6.6, - 6.6, - 6.6, - 6.6, - 6.6, - 6.5, - 6.5, - 6.5, - 6.5, - 6.5, - 6.4, - 6.4, - 6.4, - 6.4, - 6.4, - 6.4, - 6.3, - 6.3, - 6.3, - 6.3, - 6.3, - 6.3, - 6.3, - 6.2, - 6.2, - 6.2, - 6.2, - 6.2, - 6.2, - 6.2, - 6.1, - 6.1, - 6.1, - 6.1, - 6.1, - 6.1, - 6.1, - 6.1, - 6, - 6, - 6, - 6, - 6, - 6, - 6, - 6, - 6, - 5.9, - 5.9 + 8.5, 8.5, 8.5, 8.5, 8.4, 8.4, 8.4, 8.4, 8.4, 8.4, 8.4, 8.4, 8.4, 8.4, 8.4, + 8.4, 8.4, 8.4, 8.4, 8.4, 8.4, 8.4, 8.4, 8.4, 8.4, 8.4, 8.3, 8.3, 8.3, 8.3, + 8.3, 8.3, 8.3, 8.3, 8.3, 8.3, 8.3, 8.2, 8.2, 8.2, 8.2, 8.2, 8.2, 8.2, 8.1, + 8.1, 8.1, 8.1, 8.1, 8.1, 8.1, 8, 8, 8, 8, 8, 8, 8, 7.9, 7.9, 7.9, 7.9, + 7.9, 7.9, 7.9, 7.8, 7.8, 7.8, 7.5, 7.5, 7.5, 7.5, 7.5, 7.4, 7.4, 7.4, 7.4, + 7.4, 7.4, 7.3, 7.3, 7.3, 7.3, 7.3, 7.2, 7.2, 7.2, 7.2, 7.2, 7.1, 7.1, 7.1, + 7.1, 7.1, 7.1, 7, 7, 7, 7, 7, 7, 6.9, 6.9, 6.9, 6.9, 6.9, 6.8, 6.8, 6.8, + 6.8, 6.8, 6.8, 6.7, 6.7, 6.7, 6.7, 6.7, 6.7, 6.6, 6.6, 6.6, 6.6, 6.6, 6.6, + 6.5, 6.5, 6.5, 6.5, 6.5, 6.4, 6.4, 6.4, 6.4, 6.4, 6.4, 6.3, 6.3, 6.3, 6.3, + 6.3, 6.3, 6.3, 6.2, 6.2, 6.2, 6.2, 6.2, 6.2, 6.2, 6.1, 6.1, 6.1, 6.1, 6.1, + 6.1, 6.1, 6.1, 6, 6, 6, 6, 6, 6, 6, 6, 6, 5.9, 5.9 ], "soil_temperature_6cm": [ - 6.4, - 6.4, - 6.4, - 6.4, - 6.3, - 6.3, - 6.3, - 6.1, - 6, - 6, - 6.3, - 6.7, - 7.1, - 7.5, - 7.6, - 7.5, - 7.3, - 7, - 6.8, - 6.5, - 6.3, - 6.2, - 6.1, - 6, - 5.8, - 5.6, - 5.5, - 5.5, - 5.2, - 4.7, - 4, - 3.4, - 2.9, - 2.7, - 3.1, - 3.6, - 4.2, - 4.7, - 5, - 5, - 4.8, - 4.6, - 4.4, - 4.4, - 4.3, - 4.3, - 4.2, - 4.2, - 4.2, - 4.2, - 4.1, - 4, - 4, - 3.9, - 3.9, - 3.8, - 3.8, - 3.8, - 4, - 4.4, - 4.8, - 5.1, - 5.3, - 5.2, - 4.8, - 4.5, - 4.2, - 4, - 3.5, - 3.2, - 3, - 2.9, - 2.7, - 2.6, - 2.2, - 1.9, - 1.6, - 1.5, - 1.4, - 1.3, - 1.2, - 1.2, - 1.4, - 2, - 2.8, - 3.7, - 4.2, - 4.2, - 3.8, - 3.4, - 3.2, - 3.1, - 3.1, - 2.9, - 2.7, - 2.6, - 2.4, - 1.9, - 1.7, - 1.5, - 1.2, - 1.1, - 1, - 0.9, - 0.8, - 0.8, - 0.9, - 1.4, - 2, - 2.6, - 2.9, - 3.1, - 3.1, - 3, - 2.8, - 2.5, - 2.2, - 2, - 1.7, - 1.6, - 1.5, - 1.3, - 1.2, - 1.1, - 1, - 0.9, - 0.9, - 0.8, - 0.7, - 0.6, - 0.7, - 1, - 1.4, - 1.9, - 2.5, - 2.5, - 2.4, - 2.3, - 2.2, - 2.1, - 2.1, - 2.1, - 2.1, - 2.1, - 2.1, - 2.1, - 2.1, - 2.1, - 2.1, - 2, - 1.9, - 1.7, - 1.5, - 1.3, - 1.2, - 1.6, - 2.1, - 2.7, - 2.8, - 2.8, - 2.8, - 2.7, - 2.7, - 2.6, - 2.6, - 2.7, - 2.8, - 2.8 + 6.4, 6.4, 6.4, 6.4, 6.3, 6.3, 6.3, 6.1, 6, 6, 6.3, 6.7, 7.1, 7.5, 7.6, + 7.5, 7.3, 7, 6.8, 6.5, 6.3, 6.2, 6.1, 6, 5.8, 5.6, 5.5, 5.5, 5.2, 4.7, 4, + 3.4, 2.9, 2.7, 3.1, 3.6, 4.2, 4.7, 5, 5, 4.8, 4.6, 4.4, 4.4, 4.3, 4.3, + 4.2, 4.2, 4.2, 4.2, 4.1, 4, 4, 3.9, 3.9, 3.8, 3.8, 3.8, 4, 4.4, 4.8, 5.1, + 5.3, 5.2, 4.8, 4.5, 4.2, 4, 3.5, 3.2, 3, 2.9, 2.7, 2.6, 2.2, 1.9, 1.6, + 1.5, 1.4, 1.3, 1.2, 1.2, 1.4, 2, 2.8, 3.7, 4.2, 4.2, 3.8, 3.4, 3.2, 3.1, + 3.1, 2.9, 2.7, 2.6, 2.4, 1.9, 1.7, 1.5, 1.2, 1.1, 1, 0.9, 0.8, 0.8, 0.9, + 1.4, 2, 2.6, 2.9, 3.1, 3.1, 3, 2.8, 2.5, 2.2, 2, 1.7, 1.6, 1.5, 1.3, 1.2, + 1.1, 1, 0.9, 0.9, 0.8, 0.7, 0.6, 0.7, 1, 1.4, 1.9, 2.5, 2.5, 2.4, 2.3, + 2.2, 2.1, 2.1, 2.1, 2.1, 2.1, 2.1, 2.1, 2.1, 2.1, 2.1, 2, 1.9, 1.7, 1.5, + 1.3, 1.2, 1.6, 2.1, 2.7, 2.8, 2.8, 2.8, 2.7, 2.7, 2.6, 2.6, 2.7, 2.8, 2.8 ], "soil_moisture_1_3cm": [ - 0.305, - 0.305, - 0.305, - 0.305, - 0.306, - 0.306, - 0.306, - 0.306, - 0.306, - 0.306, - 0.306, - 0.305, - 0.305, - 0.305, - 0.305, - 0.305, - 0.305, - 0.305, - 0.305, - 0.305, - 0.305, - 0.305, - 0.305, - 0.305, - 0.305, - 0.305, - 0.305, - 0.305, - 0.304, - 0.304, - 0.304, - 0.304, - 0.304, - 0.304, - 0.304, - 0.304, - 0.304, - 0.304, - 0.303, - 0.304, - 0.304, - 0.304, - 0.304, - 0.304, - 0.306, - 0.306, - 0.305, - 0.305, - 0.305, - 0.305, - 0.304, - 0.304, - 0.304, - 0.304, - 0.304, - 0.304, - 0.304, - 0.304, - 0.304, - 0.303, - 0.303, - 0.303, - 0.303, - 0.302, - 0.303, - 0.308, - 0.31, - 0.312, - 0.303, - 0.303, - 0.303, - 0.303, - 0.303, - 0.303, - 0.303, - 0.303, - 0.303, - 0.303, - 0.303, - 0.303, - 0.303, - 0.303, - 0.303, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.303, - 0.303, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.303, - 0.303, - 0.303, - 0.303, - 0.303, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.303, - 0.303, - 0.303, - 0.303, - 0.303, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.303, - 0.304, - 0.304, - 0.299, - 0.299, - 0.298, - 0.298, - 0.299, - 0.299, - 0.299, - 0.299, - 0.299, - 0.299, - 0.3, - 0.301, - 0.303, - 0.305, - 0.307, - 0.307, - 0.306, - 0.304, - 0.304, - 0.303, - 0.303, - 0.302, - 0.302, - 0.301, - 0.301, - 0.301, - 0.301, - 0.301, - 0.301, - 0.301, - 0.3, - 0.299, - 0.302, - 0.312 + 0.305, 0.305, 0.305, 0.305, 0.306, 0.306, 0.306, 0.306, 0.306, 0.306, + 0.306, 0.305, 0.305, 0.305, 0.305, 0.305, 0.305, 0.305, 0.305, 0.305, + 0.305, 0.305, 0.305, 0.305, 0.305, 0.305, 0.305, 0.305, 0.304, 0.304, + 0.304, 0.304, 0.304, 0.304, 0.304, 0.304, 0.304, 0.304, 0.303, 0.304, + 0.304, 0.304, 0.304, 0.304, 0.306, 0.306, 0.305, 0.305, 0.305, 0.305, + 0.304, 0.304, 0.304, 0.304, 0.304, 0.304, 0.304, 0.304, 0.304, 0.303, + 0.303, 0.303, 0.303, 0.302, 0.303, 0.308, 0.31, 0.312, 0.303, 0.303, + 0.303, 0.303, 0.303, 0.303, 0.303, 0.303, 0.303, 0.303, 0.303, 0.303, + 0.303, 0.303, 0.303, 0.302, 0.302, 0.302, 0.302, 0.302, 0.302, 0.302, + 0.303, 0.303, 0.302, 0.302, 0.302, 0.302, 0.302, 0.302, 0.302, 0.302, + 0.302, 0.302, 0.302, 0.303, 0.303, 0.303, 0.303, 0.303, 0.302, 0.302, + 0.302, 0.302, 0.302, 0.302, 0.303, 0.303, 0.303, 0.303, 0.303, 0.302, + 0.302, 0.302, 0.302, 0.302, 0.302, 0.302, 0.302, 0.302, 0.302, 0.302, + 0.302, 0.303, 0.304, 0.304, 0.299, 0.299, 0.298, 0.298, 0.299, 0.299, + 0.299, 0.299, 0.299, 0.299, 0.3, 0.301, 0.303, 0.305, 0.307, 0.307, 0.306, + 0.304, 0.304, 0.303, 0.303, 0.302, 0.302, 0.301, 0.301, 0.301, 0.301, + 0.301, 0.301, 0.301, 0.3, 0.299, 0.302, 0.312 ], "winddirection_80m": [ - 263, - 268, - 277, - 271, - 278, - 278, - 276, - 273, - 264, - 271, - 263, - 257, - 257, - 266, - 253, - 258, - 240, - 232, - 194, - 198, - 183, - 186, - 179, - 178, - 165, - 167, - 171, - 168, - 154, - 156, - 166, - 173, - 182, - 192, - 202, - 199, - 204, - 208, - 246, - 217, - 230, - 227, - 232, - 235, - 241, - 239, - 245, - 255, - 257, - 260, - 254, - 251, - 257, - 252, - 240, - 243, - 247, - 256, - 253, - 252, - 244, - 239, - 234, - 217, - 204, - 208, - 173, - 172, - 179, - 177, - 173, - 170, - 167, - 159, - 158, - 159, - 158, - 158, - 152, - 147, - 145, - 143, - 141, - 140, - 145, - 138, - 142, - 143, - 148, - 150, - 151, - 144, - 153, - 169, - 167, - 170, - 179, - 181, - 178, - 171, - 162, - 157, - 152, - 148, - 148, - 150, - 152, - 150, - 145, - 140, - 142, - 146, - 153, - 160, - 171, - 188, - 194, - 195, - 195, - 196, - 196, - 198, - 199, - 201, - 203, - 206, - 210, - 216, - 220, - 225, - 235, - 247, - 258, - 266, - 270, - 267, - 265, - 266, - 268, - 270, - 290, - 287, - 285, - 285, - 287, - 289, - 290, - 290, - 290, - 291, - 291, - 291, - 289, - 286, - 281, - 278, - 274, - 268, - 260, - 249, - 231, - 218, - 208, - 202, - 200, - 199, - 199, - 198 + 263, 268, 277, 271, 278, 278, 276, 273, 264, 271, 263, 257, 257, 266, 253, + 258, 240, 232, 194, 198, 183, 186, 179, 178, 165, 167, 171, 168, 154, 156, + 166, 173, 182, 192, 202, 199, 204, 208, 246, 217, 230, 227, 232, 235, 241, + 239, 245, 255, 257, 260, 254, 251, 257, 252, 240, 243, 247, 256, 253, 252, + 244, 239, 234, 217, 204, 208, 173, 172, 179, 177, 173, 170, 167, 159, 158, + 159, 158, 158, 152, 147, 145, 143, 141, 140, 145, 138, 142, 143, 148, 150, + 151, 144, 153, 169, 167, 170, 179, 181, 178, 171, 162, 157, 152, 148, 148, + 150, 152, 150, 145, 140, 142, 146, 153, 160, 171, 188, 194, 195, 195, 196, + 196, 198, 199, 201, 203, 206, 210, 216, 220, 225, 235, 247, 258, 266, 270, + 267, 265, 266, 268, 270, 290, 287, 285, 285, 287, 289, 290, 290, 290, 291, + 291, 291, 289, 286, 281, 278, 274, 268, 260, 249, 231, 218, 208, 202, 200, + 199, 199, 198 ], "winddirection_10m": [ - 261, - 265, - 274, - 268, - 275, - 272, - 271, - 269, - 258, - 268, - 261, - 255, - 256, - 263, - 251, - 256, - 233, - 218, - 185, - 188, - 173, - 175, - 168, - 168, - 158, - 160, - 165, - 162, - 149, - 151, - 159, - 158, - 170, - 190, - 202, - 197, - 202, - 205, - 245, - 214, - 223, - 224, - 229, - 231, - 238, - 236, - 243, - 253, - 255, - 258, - 251, - 248, - 254, - 249, - 238, - 240, - 245, - 254, - 252, - 251, - 243, - 237, - 233, - 216, - 201, - 207, - 167, - 167, - 167, - 165, - 162, - 163, - 160, - 152, - 149, - 149, - 148, - 148, - 140, - 136, - 134, - 133, - 135, - 136, - 142, - 135, - 139, - 141, - 144, - 142, - 137, - 130, - 144, - 157, - 146, - 154, - 164, - 160, - 156, - 147, - 136, - 130, - 125, - 123, - 126, - 132, - 137, - 135, - 131, - 127, - 128, - 132, - 137, - 141, - 146, - 156, - 166, - 174, - 181, - 182, - 182, - 182, - 186, - 189, - 194, - 196, - 198, - 203, - 206, - 211, - 222, - 237, - 253, - 262, - 268, - 264, - 260, - 261, - 263, - 265, - 286, - 284, - 282, - 283, - 285, - 287, - 287, - 287, - 288, - 289, - 290, - 290, - 288, - 284, - 280, - 277, - 273, - 267, - 259, - 244, - 212, - 202, - 199, - 198, - 197, - 195, - 194, - 194 + 261, 265, 274, 268, 275, 272, 271, 269, 258, 268, 261, 255, 256, 263, 251, + 256, 233, 218, 185, 188, 173, 175, 168, 168, 158, 160, 165, 162, 149, 151, + 159, 158, 170, 190, 202, 197, 202, 205, 245, 214, 223, 224, 229, 231, 238, + 236, 243, 253, 255, 258, 251, 248, 254, 249, 238, 240, 245, 254, 252, 251, + 243, 237, 233, 216, 201, 207, 167, 167, 167, 165, 162, 163, 160, 152, 149, + 149, 148, 148, 140, 136, 134, 133, 135, 136, 142, 135, 139, 141, 144, 142, + 137, 130, 144, 157, 146, 154, 164, 160, 156, 147, 136, 130, 125, 123, 126, + 132, 137, 135, 131, 127, 128, 132, 137, 141, 146, 156, 166, 174, 181, 182, + 182, 182, 186, 189, 194, 196, 198, 203, 206, 211, 222, 237, 253, 262, 268, + 264, 260, 261, 263, 265, 286, 284, 282, 283, 285, 287, 287, 287, 288, 289, + 290, 290, 288, 284, 280, 277, 273, 267, 259, 244, 212, 202, 199, 198, 197, + 195, 194, 194 ], "time": [ "2021-11-24T00:00", @@ -3589,2951 +473,255 @@ "2021-11-30T23:00" ], "soil_moisture_0_1cm": [ - 0.304, - 0.304, - 0.304, - 0.304, - 0.305, - 0.305, - 0.305, - 0.305, - 0.305, - 0.305, - 0.305, - 0.304, - 0.304, - 0.304, - 0.303, - 0.304, - 0.304, - 0.304, - 0.304, - 0.304, - 0.304, - 0.304, - 0.304, - 0.304, - 0.304, - 0.304, - 0.304, - 0.304, - 0.303, - 0.303, - 0.303, - 0.304, - 0.304, - 0.303, - 0.303, - 0.303, - 0.303, - 0.302, - 0.303, - 0.304, - 0.303, - 0.303, - 0.303, - 0.305, - 0.306, - 0.305, - 0.305, - 0.304, - 0.304, - 0.304, - 0.303, - 0.303, - 0.303, - 0.303, - 0.303, - 0.303, - 0.303, - 0.303, - 0.303, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.303, - 0.31, - 0.312, - 0.313, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.301, - 0.301, - 0.301, - 0.301, - 0.301, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.301, - 0.301, - 0.301, - 0.301, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.303, - 0.304, - 0.304, - 0.298, - 0.298, - 0.297, - 0.297, - 0.298, - 0.298, - 0.298, - 0.298, - 0.298, - 0.299, - 0.3, - 0.302, - 0.304, - 0.307, - 0.309, - 0.308, - 0.306, - 0.303, - 0.302, - 0.302, - 0.302, - 0.301, - 0.301, - 0.3, - 0.3, - 0.3, - 0.3, - 0.3, - 0.3, - 0.301, - 0.3, - 0.299, - 0.302, - 0.314 + 0.304, 0.304, 0.304, 0.304, 0.305, 0.305, 0.305, 0.305, 0.305, 0.305, + 0.305, 0.304, 0.304, 0.304, 0.303, 0.304, 0.304, 0.304, 0.304, 0.304, + 0.304, 0.304, 0.304, 0.304, 0.304, 0.304, 0.304, 0.304, 0.303, 0.303, + 0.303, 0.304, 0.304, 0.303, 0.303, 0.303, 0.303, 0.302, 0.303, 0.304, + 0.303, 0.303, 0.303, 0.305, 0.306, 0.305, 0.305, 0.304, 0.304, 0.304, + 0.303, 0.303, 0.303, 0.303, 0.303, 0.303, 0.303, 0.303, 0.303, 0.302, + 0.302, 0.302, 0.302, 0.302, 0.303, 0.31, 0.312, 0.313, 0.302, 0.302, + 0.302, 0.302, 0.302, 0.302, 0.302, 0.302, 0.302, 0.302, 0.302, 0.302, + 0.302, 0.302, 0.302, 0.302, 0.301, 0.301, 0.301, 0.301, 0.301, 0.302, + 0.302, 0.302, 0.302, 0.302, 0.302, 0.302, 0.302, 0.302, 0.302, 0.302, + 0.302, 0.302, 0.302, 0.302, 0.302, 0.302, 0.302, 0.302, 0.302, 0.301, + 0.301, 0.301, 0.301, 0.302, 0.302, 0.302, 0.302, 0.302, 0.302, 0.302, + 0.302, 0.302, 0.302, 0.302, 0.302, 0.302, 0.302, 0.302, 0.302, 0.302, + 0.302, 0.303, 0.304, 0.304, 0.298, 0.298, 0.297, 0.297, 0.298, 0.298, + 0.298, 0.298, 0.298, 0.299, 0.3, 0.302, 0.304, 0.307, 0.309, 0.308, 0.306, + 0.303, 0.302, 0.302, 0.302, 0.301, 0.301, 0.3, 0.3, 0.3, 0.3, 0.3, 0.3, + 0.301, 0.3, 0.299, 0.302, 0.314 ], "direct_normal_irradiance": [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.4, - 8.5, - 5.8, - 25.1, - 13.4, - 13, - 28, - 0.5, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 1.5, - 24.6, - 32.2, - 58.9, - 50.7, - 54.7, - 52.4, - 10.4, - 3.5, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 1.4, - 10.5, - 10.4, - 6.8, - 6, - 4, - 0.3, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 1.4, - 13.3, - 30.5, - 91.3, - 101.2, - 56.8, - 114.8, - 23.7, - 1.5, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 341.5, - 430.3, - 493.5, - 589.6, - 660.9, - 596.3, - 477.7, - 191.5, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 213.7, - 256.9, - 227.8, - 193.9, - 162.6, - 170.3, - 158.4, - 80.4, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 65.4, - 107.1, - 193.9, - 317.5, - 431.7, - 415.9, - 356.1, - 151.7, - 0, - 0, - 0, - 0, - 0, - 0, - 0 + 0, 0, 0, 0, 0, 0, 0, 0, 0.4, 8.5, 5.8, 25.1, 13.4, 13, 28, 0.5, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1.5, 24.6, 32.2, 58.9, 50.7, 54.7, + 52.4, 10.4, 3.5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1.4, + 10.5, 10.4, 6.8, 6, 4, 0.3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 1.4, 13.3, 30.5, 91.3, 101.2, 56.8, 114.8, 23.7, 1.5, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 341.5, 430.3, 493.5, 589.6, 660.9, 596.3, + 477.7, 191.5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 213.7, + 256.9, 227.8, 193.9, 162.6, 170.3, 158.4, 80.4, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 65.4, 107.1, 193.9, 317.5, 431.7, 415.9, 356.1, + 151.7, 0, 0, 0, 0, 0, 0, 0 ], "soil_temperature_18cm": [ - 6.3, - 6.4, - 6.4, - 6.5, - 6.5, - 6.6, - 6.6, - 6.6, - 6.6, - 6.6, - 6.6, - 6.7, - 6.7, - 6.8, - 6.9, - 7, - 7.1, - 7.1, - 7.1, - 7.1, - 7.1, - 7.1, - 7, - 7, - 6.9, - 6.9, - 6.8, - 6.8, - 6.7, - 6.6, - 6.5, - 6.3, - 6.1, - 5.9, - 5.7, - 5.6, - 5.5, - 5.5, - 5.6, - 5.6, - 5.6, - 5.6, - 5.6, - 5.6, - 5.5, - 5.5, - 5.5, - 5.5, - 5.4, - 5.4, - 5.4, - 5.3, - 5.3, - 5.3, - 5.2, - 5.2, - 5.2, - 5.1, - 5.1, - 5.1, - 5.2, - 5.2, - 5.3, - 5.4, - 5.4, - 5.4, - 5.4, - 5.3, - 4.9, - 4.9, - 4.8, - 4.7, - 4.6, - 4.6, - 4.5, - 4.4, - 4.2, - 4.1, - 4, - 3.9, - 3.8, - 3.7, - 3.6, - 3.5, - 3.5, - 3.6, - 3.7, - 3.9, - 4, - 4, - 4.1, - 4.1, - 4.1, - 4.1, - 4.1, - 4, - 4, - 3.9, - 3.8, - 3.8, - 3.6, - 3.5, - 3.4, - 3.3, - 3.2, - 3.1, - 3.1, - 3, - 3, - 3.1, - 3.1, - 3.2, - 3.4, - 3.4, - 3.4, - 3.5, - 3.5, - 3.4, - 3.4, - 3.4, - 3.3, - 3.2, - 3.2, - 3.1, - 3, - 3, - 2.9, - 2.8, - 2.8, - 2.7, - 2.7, - 2.6, - 2.6, - 2.6, - 2.9, - 2.9, - 3, - 3.1, - 3.1, - 3.1, - 3.1, - 3.1, - 3.1, - 3.1, - 3.1, - 3.1, - 3.1, - 3.1, - 3.1, - 3.1, - 3.1, - 3.1, - 3.1, - 3, - 2.9, - 2.9, - 2.9, - 3, - 3, - 3.1, - 3.2, - 3.2, - 3.2, - 3.3, - 3.3, - 3.3, - 3.3, - 3.4 + 6.3, 6.4, 6.4, 6.5, 6.5, 6.6, 6.6, 6.6, 6.6, 6.6, 6.6, 6.7, 6.7, 6.8, 6.9, + 7, 7.1, 7.1, 7.1, 7.1, 7.1, 7.1, 7, 7, 6.9, 6.9, 6.8, 6.8, 6.7, 6.6, 6.5, + 6.3, 6.1, 5.9, 5.7, 5.6, 5.5, 5.5, 5.6, 5.6, 5.6, 5.6, 5.6, 5.6, 5.5, 5.5, + 5.5, 5.5, 5.4, 5.4, 5.4, 5.3, 5.3, 5.3, 5.2, 5.2, 5.2, 5.1, 5.1, 5.1, 5.2, + 5.2, 5.3, 5.4, 5.4, 5.4, 5.4, 5.3, 4.9, 4.9, 4.8, 4.7, 4.6, 4.6, 4.5, 4.4, + 4.2, 4.1, 4, 3.9, 3.8, 3.7, 3.6, 3.5, 3.5, 3.6, 3.7, 3.9, 4, 4, 4.1, 4.1, + 4.1, 4.1, 4.1, 4, 4, 3.9, 3.8, 3.8, 3.6, 3.5, 3.4, 3.3, 3.2, 3.1, 3.1, 3, + 3, 3.1, 3.1, 3.2, 3.4, 3.4, 3.4, 3.5, 3.5, 3.4, 3.4, 3.4, 3.3, 3.2, 3.2, + 3.1, 3, 3, 2.9, 2.8, 2.8, 2.7, 2.7, 2.6, 2.6, 2.6, 2.9, 2.9, 3, 3.1, 3.1, + 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3, + 2.9, 2.9, 2.9, 3, 3, 3.1, 3.2, 3.2, 3.2, 3.3, 3.3, 3.3, 3.3, 3.4 ], "winddirection_180m": [ - 266, - 271, - 284, - 279, - 287, - 290, - 283, - 277, - 277, - 274, - 274, - 267, - 258, - 267, - 254, - 259, - 253, - 246, - 208, - 208, - 196, - 199, - 194, - 191, - 174, - 178, - 181, - 177, - 165, - 168, - 186, - 199, - 201, - 207, - 219, - 222, - 222, - 214, - 247, - 229, - 241, - 236, - 245, - 247, - 247, - 243, - 250, - 257, - 259, - 263, - 257, - 255, - 261, - 256, - 247, - 248, - 250, - 257, - 254, - 253, - 245, - 240, - 234, - 218, - 205, - 209, - 185, - 186, - 200, - 198, - 196, - 193, - 192, - 187, - 183, - 180, - 180, - 181, - 178, - 173, - 170, - 169, - 167, - 168, - 168, - 156, - 144, - 145, - 157, - 166, - 175, - 168, - 177, - 192, - 200, - 195, - 200, - 202, - 199, - 196, - 192, - 190, - 188, - 186, - 185, - 184, - 183, - 183, - 182, - 182, - 181, - 180, - 182, - 189, - 203, - 219, - 221, - 218, - 216, - 217, - 219, - 222, - 223, - 223, - 225, - 230, - 236, - 246, - 253, - 260, - 268, - 271, - 271, - 272, - 302, - 298, - 297, - 300, - 305, - 308, - 306, - 303, - 298, - 296, - 294, - 291, - 291, - 291, - 292, - 293, - 293, - 293, - 291, - 287, - 282, - 278, - 275, - 268, - 264, - 261, - 252, - 239, - 228, - 220, - 215, - 211, - 207, - 206 + 266, 271, 284, 279, 287, 290, 283, 277, 277, 274, 274, 267, 258, 267, 254, + 259, 253, 246, 208, 208, 196, 199, 194, 191, 174, 178, 181, 177, 165, 168, + 186, 199, 201, 207, 219, 222, 222, 214, 247, 229, 241, 236, 245, 247, 247, + 243, 250, 257, 259, 263, 257, 255, 261, 256, 247, 248, 250, 257, 254, 253, + 245, 240, 234, 218, 205, 209, 185, 186, 200, 198, 196, 193, 192, 187, 183, + 180, 180, 181, 178, 173, 170, 169, 167, 168, 168, 156, 144, 145, 157, 166, + 175, 168, 177, 192, 200, 195, 200, 202, 199, 196, 192, 190, 188, 186, 185, + 184, 183, 183, 182, 182, 181, 180, 182, 189, 203, 219, 221, 218, 216, 217, + 219, 222, 223, 223, 225, 230, 236, 246, 253, 260, 268, 271, 271, 272, 302, + 298, 297, 300, 305, 308, 306, 303, 298, 296, 294, 291, 291, 291, 292, 293, + 293, 293, 291, 287, 282, 278, 275, 268, 264, 261, 252, 239, 228, 220, 215, + 211, 207, 206 ], "apparent_temperature": [ - 4.4, - 4.2, - 4.3, - 4.4, - 4.1, - 4, - 3.9, - 3.7, - 3.6, - 3.4, - 4.1, - 4.6, - 4.9, - 5.2, - 5.3, - 5.4, - 5.5, - 5.3, - 4.8, - 4.7, - 4.2, - 4, - 3.9, - 3.4, - 3.2, - 2.8, - 2.5, - 2.3, - 1.6, - 0.5, - -0.5, - -1.2, - -2, - -2.9, - -2.6, - -1.3, - -0.3, - 0.7, - 0.9, - 1.1, - 1.4, - 0.8, - 0.4, - 0.5, - 0.5, - 0.3, - 0.2, - 0.1, - 0.1, - -0, - -0.1, - -0.3, - -0.5, - -0.2, - -0.2, - -0.3, - -0.4, - -1, - -0.2, - 0.2, - 0.7, - 1.3, - 1.4, - 1.3, - 0.7, - 0.4, - 0.5, - 0, - -0.6, - -1.1, - -1.5, - -1.9, - -2.2, - -2.6, - -3.2, - -3.5, - -3.6, - -3.6, - -3.7, - -3.8, - -3.9, - -4, - -3.4, - -2, - -0.9, - 0.2, - 0.6, - 0.5, - 0, - -0.3, - -0.3, - -0.4, - -0.4, - -0.6, - -0.8, - -1, - -1.4, - -2.1, - -2.4, - -2.7, - -3, - -3.2, - -3.4, - -3.5, - -3.5, - -3.3, - -2.8, - -2, - -1, - 0, - 0.3, - 0.4, - 0.3, - 0.2, - -0.1, - -0.5, - -0.9, - -1.4, - -1.9, - -2.1, - -2.3, - -2.5, - -2.8, - -3, - -3.3, - -3.5, - -3.6, - -3.6, - -3.6, - -3.6, - -3.3, - -2.7, - -1.9, - -1.2, - -1.4, - -1.4, - -1.6, - -1.9, - -2.4, - -2.9, - -1.6, - -1.8, - -2, - -2.1, - -2.3, - -2.4, - -2.4, - -2.4, - -2.5, - -2.9, - -3.3, - -3.9, - -4.2, - -4.5, - -4.6, - -4, - -3.1, - -2, - -1.3, - -0.7, - -0.3, - -0.5, - -1, - -1.4, - -1.5, - -1.4, - -1.3, - -1.2 + 4.4, 4.2, 4.3, 4.4, 4.1, 4, 3.9, 3.7, 3.6, 3.4, 4.1, 4.6, 4.9, 5.2, 5.3, + 5.4, 5.5, 5.3, 4.8, 4.7, 4.2, 4, 3.9, 3.4, 3.2, 2.8, 2.5, 2.3, 1.6, 0.5, + -0.5, -1.2, -2, -2.9, -2.6, -1.3, -0.3, 0.7, 0.9, 1.1, 1.4, 0.8, 0.4, 0.5, + 0.5, 0.3, 0.2, 0.1, 0.1, -0, -0.1, -0.3, -0.5, -0.2, -0.2, -0.3, -0.4, -1, + -0.2, 0.2, 0.7, 1.3, 1.4, 1.3, 0.7, 0.4, 0.5, 0, -0.6, -1.1, -1.5, -1.9, + -2.2, -2.6, -3.2, -3.5, -3.6, -3.6, -3.7, -3.8, -3.9, -4, -3.4, -2, -0.9, + 0.2, 0.6, 0.5, 0, -0.3, -0.3, -0.4, -0.4, -0.6, -0.8, -1, -1.4, -2.1, + -2.4, -2.7, -3, -3.2, -3.4, -3.5, -3.5, -3.3, -2.8, -2, -1, 0, 0.3, 0.4, + 0.3, 0.2, -0.1, -0.5, -0.9, -1.4, -1.9, -2.1, -2.3, -2.5, -2.8, -3, -3.3, + -3.5, -3.6, -3.6, -3.6, -3.6, -3.3, -2.7, -1.9, -1.2, -1.4, -1.4, -1.6, + -1.9, -2.4, -2.9, -1.6, -1.8, -2, -2.1, -2.3, -2.4, -2.4, -2.4, -2.5, + -2.9, -3.3, -3.9, -4.2, -4.5, -4.6, -4, -3.1, -2, -1.3, -0.7, -0.3, -0.5, + -1, -1.4, -1.5, -1.4, -1.3, -1.2 ], "cloudcover_high": [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 3, - 28, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 92, - 1, - 0, - 0, - 0, - 0, - 0, - 83, - 100, - 0, - 7, - 100, - 100, - 100, - 3, - 0, - 0, - 0, - 0, - 0, - 14, - 62, - 1, - 58, - 100, - 100, - 100, - 100, - 100, - 100, - 100, - 100, - 100, - 100, - 100, - 100, - 100, - 100, - 87, - 99, - 100, - 100, - 100, - 100, - 100, - 100, - 55, - 100, - 100, - 100, - 0, - 0, - 4, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 33, - 67, - 100, - 94, - 87, - 81, - 75, - 70, - 65, - 43, - 22, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 7, - 15, - 22, - 15, - 7, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 11, - 22, - 33, - 22, - 11, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 8, - 16, - 24, - 49, - 75, - 100, - 100, - 100, - 100, - 67 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 28, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 92, 1, 0, 0, 0, 0, 0, 83, 100, 0, 7, 100, 100, + 100, 3, 0, 0, 0, 0, 0, 14, 62, 1, 58, 100, 100, 100, 100, 100, 100, 100, + 100, 100, 100, 100, 100, 100, 100, 87, 99, 100, 100, 100, 100, 100, 100, + 55, 100, 100, 100, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 33, 67, + 100, 94, 87, 81, 75, 70, 65, 43, 22, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 15, 22, 15, 7, 0, 0, 0, 0, 0, 0, + 0, 11, 22, 33, 22, 11, 0, 0, 0, 0, 0, 0, 0, 8, 16, 24, 49, 75, 100, 100, + 100, 100, 67 ], "pressure_msl": [ - 1025.2, - 1025.7, - 1025.7, - 1025.2, - 1024.7, - 1024.2, - 1024.2, - 1024.2, - 1024.2, - 1023.7, - 1024.2, - 1023.7, - 1023.2, - 1022.2, - 1021.2, - 1021.2, - 1020.7, - 1020.2, - 1020.2, - 1019.7, - 1018.7, - 1018.7, - 1018.2, - 1017.7, - 1016.7, - 1015.7, - 1015.2, - 1014.2, - 1013.7, - 1012.7, - 1012.2, - 1011.1, - 1011.1, - 1010.6, - 1010.1, - 1009.6, - 1008.6, - 1008.1, - 1007.6, - 1007.1, - 1007.1, - 1006.6, - 1006.1, - 1006.1, - 1005.6, - 1005.6, - 1005.1, - 1005.1, - 1005.1, - 1005.1, - 1004.6, - 1004.1, - 1004.1, - 1003.6, - 1003.6, - 1003.6, - 1003.1, - 1002.6, - 1002.6, - 1002.1, - 1001.6, - 1000.6, - 1000.1, - 999.6, - 999.6, - 999.1, - 998.6, - 998.1, - 997.2, - 996.7, - 996.2, - 995.7, - 995.2, - 994.7, - 994.2, - 993.7, - 993.2, - 992.7, - 992.7, - 992.2, - 992.7, - 992.7, - 992.7, - 992.7, - 992.2, - 991.7, - 991.7, - 991.7, - 992.2, - 992.2, - 992.7, - 992.7, - 992.7, - 993.2, - 993.2, - 993.2, - 993.7, - 993.2, - 993.7, - 993.7, - 993.7, - 993.7, - 994.2, - 994.2, - 994.7, - 994.7, - 995.2, - 995.2, - 995.2, - 995.2, - 995.2, - 995.2, - 995.7, - 995.7, - 996.2, - 996.2, - 996.7, - 996.7, - 996.7, - 996.7, - 997.2, - 997.2, - 997.2, - 997.7, - 997.7, - 997.7, - 998.2, - 998.7, - 999.3, - 999.3, - 999.8, - 1000.3, - 1000.3, - 1000.3, - 1000.6, - 1001.1, - 1001.1, - 1001.6, - 1002.1, - 1002.1, - 1006.6, - 1007.1, - 1007.1, - 1007.6, - 1007.6, - 1008.2, - 1008.2, - 1008.7, - 1008.7, - 1008.7, - 1008.7, - 1009.2, - 1009.2, - 1009.7, - 1009.7, - 1009.7, - 1009.2, - 1008.7, - 1008.2, - 1007.6, - 1006.6, - 1005.6, - 1004.6, - 1003.6, - 1002.1, - 1001.1, - 999.1, - 998.1 + 1025.2, 1025.7, 1025.7, 1025.2, 1024.7, 1024.2, 1024.2, 1024.2, 1024.2, + 1023.7, 1024.2, 1023.7, 1023.2, 1022.2, 1021.2, 1021.2, 1020.7, 1020.2, + 1020.2, 1019.7, 1018.7, 1018.7, 1018.2, 1017.7, 1016.7, 1015.7, 1015.2, + 1014.2, 1013.7, 1012.7, 1012.2, 1011.1, 1011.1, 1010.6, 1010.1, 1009.6, + 1008.6, 1008.1, 1007.6, 1007.1, 1007.1, 1006.6, 1006.1, 1006.1, 1005.6, + 1005.6, 1005.1, 1005.1, 1005.1, 1005.1, 1004.6, 1004.1, 1004.1, 1003.6, + 1003.6, 1003.6, 1003.1, 1002.6, 1002.6, 1002.1, 1001.6, 1000.6, 1000.1, + 999.6, 999.6, 999.1, 998.6, 998.1, 997.2, 996.7, 996.2, 995.7, 995.2, + 994.7, 994.2, 993.7, 993.2, 992.7, 992.7, 992.2, 992.7, 992.7, 992.7, + 992.7, 992.2, 991.7, 991.7, 991.7, 992.2, 992.2, 992.7, 992.7, 992.7, + 993.2, 993.2, 993.2, 993.7, 993.2, 993.7, 993.7, 993.7, 993.7, 994.2, + 994.2, 994.7, 994.7, 995.2, 995.2, 995.2, 995.2, 995.2, 995.2, 995.7, + 995.7, 996.2, 996.2, 996.7, 996.7, 996.7, 996.7, 997.2, 997.2, 997.2, + 997.7, 997.7, 997.7, 998.2, 998.7, 999.3, 999.3, 999.8, 1000.3, 1000.3, + 1000.3, 1000.6, 1001.1, 1001.1, 1001.6, 1002.1, 1002.1, 1006.6, 1007.1, + 1007.1, 1007.6, 1007.6, 1008.2, 1008.2, 1008.7, 1008.7, 1008.7, 1008.7, + 1009.2, 1009.2, 1009.7, 1009.7, 1009.7, 1009.2, 1008.7, 1008.2, 1007.6, + 1006.6, 1005.6, 1004.6, 1003.6, 1002.1, 1001.1, 999.1, 998.1 ], "diffuse_radiation": [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.7, - 22.3, - 41, - 63.3, - 79.1, - 76.7, - 57.4, - 27.9, - 7.9, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.6, - 30.3, - 71.4, - 94.8, - 110.4, - 104.9, - 80.8, - 25.3, - 14.3, - -0, - 0, - 0, - -0, - 0, - -0, - 0, - 0, - -0, - 0, - 0, - 0, - -0, - 0, - -0, - 0.3, - 17.3, - 56.7, - 90.1, - 105.3, - 112.5, - 91.3, - 46.8, - 12.3, - 0, - -0, - 0, - -0.1, - 0.1, - -0, - -0, - -0, - 0, - -0, - 0.1, - -0, - -0.1, - 0.1, - -0, - 0.2, - 26.7, - 66.2, - 93.2, - 89.9, - 67.9, - 60.8, - 50.8, - 11, - 0, - -0.1, - -0, - -0, - -0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - -0, - -0, - 0, - 0, - 21.5, - 49.3, - 64.5, - 66.6, - 62, - 56.3, - 42.7, - 15.6, - 0, - 0, - 0, - -0, - -0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - -0, - -0, - 0, - 0, - 26.2, - 61.1, - 85.6, - 98.2, - 98.6, - 78.3, - 51.4, - 16.4, - 0, - 0, - 0, - -0, - -0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - -0, - -0, - 0, - 0, - 24.8, - 57.9, - 81.8, - 94.8, - 94.4, - 77.1, - 48.9, - 15, - 0, - 0, - 0, - -0, - -0, - 0, - 0 + 0, 0, 0, 0, 0, 0, 0, 0, 0.7, 22.3, 41, 63.3, 79.1, 76.7, 57.4, 27.9, 7.9, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.6, 30.3, 71.4, 94.8, 110.4, + 104.9, 80.8, 25.3, 14.3, -0, 0, 0, -0, 0, -0, 0, 0, -0, 0, 0, 0, -0, 0, + -0, 0.3, 17.3, 56.7, 90.1, 105.3, 112.5, 91.3, 46.8, 12.3, 0, -0, 0, -0.1, + 0.1, -0, -0, -0, 0, -0, 0.1, -0, -0.1, 0.1, -0, 0.2, 26.7, 66.2, 93.2, + 89.9, 67.9, 60.8, 50.8, 11, 0, -0.1, -0, -0, -0, 0, 0, 0, 0, 0, 0, 0, -0, + -0, 0, 0, 21.5, 49.3, 64.5, 66.6, 62, 56.3, 42.7, 15.6, 0, 0, 0, -0, -0, + 0, 0, 0, 0, 0, 0, 0, -0, -0, 0, 0, 26.2, 61.1, 85.6, 98.2, 98.6, 78.3, + 51.4, 16.4, 0, 0, 0, -0, -0, 0, 0, 0, 0, 0, 0, 0, -0, -0, 0, 0, 24.8, + 57.9, 81.8, 94.8, 94.4, 77.1, 48.9, 15, 0, 0, 0, -0, -0, 0, 0 ], "snow_depth": [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ], "windspeed_10m": [ - 10.4, - 10.9, - 10.1, - 7.9, - 8.8, - 8.9, - 9.3, - 9.4, - 8.1, - 9.6, - 8.5, - 7.6, - 7.9, - 7.4, - 7.1, - 6.4, - 4, - 3.1, - 5, - 4.3, - 4.6, - 4.9, - 5.3, - 5.3, - 5.8, - 6, - 6.3, - 5.9, - 5.9, - 6, - 5.2, - 4.7, - 5.1, - 7, - 6.9, - 6.6, - 6.6, - 7.8, - 11.5, - 10.5, - 8.1, - 10.1, - 10.9, - 10.9, - 11.2, - 11.8, - 12.3, - 12.9, - 12.7, - 12.5, - 12.1, - 12.3, - 12.6, - 10.9, - 10, - 10.8, - 10.8, - 14.8, - 12.1, - 11.5, - 10.5, - 8.8, - 9.2, - 8.7, - 9.2, - 7.8, - 6.3, - 7.5, - 7.9, - 7.9, - 8.7, - 9.5, - 9.2, - 9.4, - 9.6, - 9.4, - 9.6, - 9.1, - 9.2, - 9.4, - 9.2, - 9.5, - 10.6, - 9.2, - 9.9, - 10.2, - 10.7, - 10.4, - 8.9, - 8.2, - 7.2, - 7.6, - 7.7, - 7.4, - 6.5, - 6.6, - 6.2, - 5.8, - 5.7, - 5.6, - 5.9, - 6.2, - 6.6, - 7, - 7, - 6.9, - 6.8, - 6.6, - 6.2, - 5.9, - 5.8, - 5.8, - 5.6, - 5.1, - 4.5, - 4.1, - 4.3, - 4.9, - 5.6, - 5.7, - 5.5, - 5.3, - 5.4, - 5.7, - 5.9, - 5.6, - 5.2, - 4.7, - 4.5, - 4.4, - 4.3, - 4.6, - 5.4, - 6.5, - 8.7, - 7.7, - 6.8, - 7.5, - 8.6, - 9.8, - 11.1, - 11.7, - 12.5, - 13, - 13.4, - 13.6, - 13.4, - 12.9, - 12.5, - 12.8, - 13.5, - 14, - 13.8, - 13.3, - 12.4, - 11.9, - 11.3, - 10, - 8.2, - 6, - 5.5, - 7.4, - 10, - 13, - 14.3, - 15.2, - 15.8, - 16.1 + 10.4, 10.9, 10.1, 7.9, 8.8, 8.9, 9.3, 9.4, 8.1, 9.6, 8.5, 7.6, 7.9, 7.4, + 7.1, 6.4, 4, 3.1, 5, 4.3, 4.6, 4.9, 5.3, 5.3, 5.8, 6, 6.3, 5.9, 5.9, 6, + 5.2, 4.7, 5.1, 7, 6.9, 6.6, 6.6, 7.8, 11.5, 10.5, 8.1, 10.1, 10.9, 10.9, + 11.2, 11.8, 12.3, 12.9, 12.7, 12.5, 12.1, 12.3, 12.6, 10.9, 10, 10.8, + 10.8, 14.8, 12.1, 11.5, 10.5, 8.8, 9.2, 8.7, 9.2, 7.8, 6.3, 7.5, 7.9, 7.9, + 8.7, 9.5, 9.2, 9.4, 9.6, 9.4, 9.6, 9.1, 9.2, 9.4, 9.2, 9.5, 10.6, 9.2, + 9.9, 10.2, 10.7, 10.4, 8.9, 8.2, 7.2, 7.6, 7.7, 7.4, 6.5, 6.6, 6.2, 5.8, + 5.7, 5.6, 5.9, 6.2, 6.6, 7, 7, 6.9, 6.8, 6.6, 6.2, 5.9, 5.8, 5.8, 5.6, + 5.1, 4.5, 4.1, 4.3, 4.9, 5.6, 5.7, 5.5, 5.3, 5.4, 5.7, 5.9, 5.6, 5.2, 4.7, + 4.5, 4.4, 4.3, 4.6, 5.4, 6.5, 8.7, 7.7, 6.8, 7.5, 8.6, 9.8, 11.1, 11.7, + 12.5, 13, 13.4, 13.6, 13.4, 12.9, 12.5, 12.8, 13.5, 14, 13.8, 13.3, 12.4, + 11.9, 11.3, 10, 8.2, 6, 5.5, 7.4, 10, 13, 14.3, 15.2, 15.8, 16.1 ], "vapor_pressure_deficit": [ - 0.05, - 0.05, - 0.06, - 0.07, - 0.05, - 0.05, - 0.05, - 0.05, - 0.05, - 0.05, - 0.08, - 0.12, - 0.16, - 0.17, - 0.19, - 0.16, - 0.11, - 0.11, - 0.07, - 0.06, - 0.05, - 0.04, - 0.04, - 0.04, - 0.05, - 0.07, - 0.09, - 0.12, - 0.1, - 0.08, - 0.05, - 0.03, - 0.02, - 0, - 0.03, - 0.08, - 0.11, - 0.14, - 0.18, - 0.15, - 0.14, - 0.14, - 0.12, - 0.12, - 0.13, - 0.13, - 0.12, - 0.11, - 0.1, - 0.08, - 0.08, - 0.07, - 0.06, - 0.07, - 0.06, - 0.08, - 0.09, - 0.1, - 0.13, - 0.16, - 0.19, - 0.22, - 0.23, - 0.21, - 0.14, - 0.11, - 0.09, - 0.08, - 0.13, - 0.13, - 0.14, - 0.15, - 0.15, - 0.15, - 0.14, - 0.14, - 0.13, - 0.12, - 0.11, - 0.11, - 0.1, - 0.09, - 0.12, - 0.16, - 0.21, - 0.25, - 0.27, - 0.25, - 0.18, - 0.14, - 0.12, - 0.12, - 0.13, - 0.12, - 0.1, - 0.09, - 0.08, - 0.06, - 0.05, - 0.05, - 0.05, - 0.05, - 0.04, - 0.04, - 0.05, - 0.06, - 0.08, - 0.1, - 0.14, - 0.17, - 0.18, - 0.17, - 0.15, - 0.13, - 0.12, - 0.1, - 0.09, - 0.08, - 0.08, - 0.08, - 0.08, - 0.08, - 0.08, - 0.08, - 0.08, - 0.07, - 0.07, - 0.07, - 0.06, - 0.05, - 0.05, - 0.07, - 0.09, - 0.13, - 0.14, - 0.13, - 0.11, - 0.09, - 0.06, - 0.03, - 0.04, - 0.05, - 0.05, - 0.05, - 0.05, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.05, - 0.06, - 0.07, - 0.07, - 0.08, - 0.1, - 0.14, - 0.17, - 0.16, - 0.14, - 0.11, - 0.09, - 0.07, - 0.06, - 0.07, - 0.08, - 0.1, - 0.09 + 0.05, 0.05, 0.06, 0.07, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.08, 0.12, + 0.16, 0.17, 0.19, 0.16, 0.11, 0.11, 0.07, 0.06, 0.05, 0.04, 0.04, 0.04, + 0.05, 0.07, 0.09, 0.12, 0.1, 0.08, 0.05, 0.03, 0.02, 0, 0.03, 0.08, 0.11, + 0.14, 0.18, 0.15, 0.14, 0.14, 0.12, 0.12, 0.13, 0.13, 0.12, 0.11, 0.1, + 0.08, 0.08, 0.07, 0.06, 0.07, 0.06, 0.08, 0.09, 0.1, 0.13, 0.16, 0.19, + 0.22, 0.23, 0.21, 0.14, 0.11, 0.09, 0.08, 0.13, 0.13, 0.14, 0.15, 0.15, + 0.15, 0.14, 0.14, 0.13, 0.12, 0.11, 0.11, 0.1, 0.09, 0.12, 0.16, 0.21, + 0.25, 0.27, 0.25, 0.18, 0.14, 0.12, 0.12, 0.13, 0.12, 0.1, 0.09, 0.08, + 0.06, 0.05, 0.05, 0.05, 0.05, 0.04, 0.04, 0.05, 0.06, 0.08, 0.1, 0.14, + 0.17, 0.18, 0.17, 0.15, 0.13, 0.12, 0.1, 0.09, 0.08, 0.08, 0.08, 0.08, + 0.08, 0.08, 0.08, 0.08, 0.07, 0.07, 0.07, 0.06, 0.05, 0.05, 0.07, 0.09, + 0.13, 0.14, 0.13, 0.11, 0.09, 0.06, 0.03, 0.04, 0.05, 0.05, 0.05, 0.05, + 0.04, 0.04, 0.04, 0.04, 0.04, 0.05, 0.06, 0.07, 0.07, 0.08, 0.1, 0.14, + 0.17, 0.16, 0.14, 0.11, 0.09, 0.07, 0.06, 0.07, 0.08, 0.1, 0.09 ], "windgusts_10m": [ - 22.1, - 23.8, - 23.5, - 21.5, - 18.4, - 19.6, - 19.9, - 20.3, - 19.6, - 20.8, - 25, - 19.1, - 17.9, - 17.6, - 16.8, - 15.7, - 14, - 8.5, - 10.4, - 11, - 9.5, - 10.2, - 11.1, - 11.8, - 12.2, - 13, - 14.4, - 13.3, - 13.2, - 12.5, - 12.7, - 10.6, - 10.4, - 15.6, - 16.8, - 18.8, - 15.9, - 17.8, - 25.3, - 24.6, - 22.5, - 21.4, - 23.4, - 23.3, - 24.7, - 25.3, - 26.2, - 29.2, - 28.2, - 27.9, - 26.9, - 26.8, - 28, - 27.2, - 23.5, - 24.9, - 24.6, - 31.8, - 32.6, - 27.5, - 25.1, - 23.1, - 20.4, - 20.2, - 24, - 20.1, - 16.8, - 16, - 20.5, - 20.1, - 21.1, - 22.2, - 23.6, - 24.4, - 25.7, - 26.4, - 26.3, - 27.2, - 27.1, - 27, - 26.4, - 26.4, - 27.1, - 28.7, - 28.1, - 27.2, - 26.5, - 25.5, - 23.6, - 23.6, - 22.2, - 21.4, - 20.4, - 19.4, - 18.4, - 18.1, - 17.7, - 17.4, - 17.6, - 17.9, - 18.1, - 18.4, - 18.6, - 18.9, - 19.2, - 19.5, - 19.9, - 21.5, - 23, - 24.6, - 24.1, - 23.6, - 23.1, - 21.4, - 19.8, - 18.1, - 15.8, - 13.5, - 11.2, - 10.9, - 10.6, - 10.3, - 8.9, - 7.5, - 6, - 5.1, - 4.2, - 3.3, - 5.5, - 7.8, - 10, - 14.4, - 18.8, - 23.2, - 24, - 25.6, - 27.2, - 26.1, - 25, - 24, - 25.6, - 27.1, - 28.7, - 29.6, - 30.5, - 31.4, - 30.5, - 29.7, - 28.8, - 30.3, - 31.8, - 33.3, - 32.3, - 31.3, - 30.4, - 28.3, - 26.2, - 24.2, - 21.1, - 18, - 14.9, - 19.9, - 24.9, - 29.9, - 32, - 34.1, - 36.3, - 36.9 + 22.1, 23.8, 23.5, 21.5, 18.4, 19.6, 19.9, 20.3, 19.6, 20.8, 25, 19.1, + 17.9, 17.6, 16.8, 15.7, 14, 8.5, 10.4, 11, 9.5, 10.2, 11.1, 11.8, 12.2, + 13, 14.4, 13.3, 13.2, 12.5, 12.7, 10.6, 10.4, 15.6, 16.8, 18.8, 15.9, + 17.8, 25.3, 24.6, 22.5, 21.4, 23.4, 23.3, 24.7, 25.3, 26.2, 29.2, 28.2, + 27.9, 26.9, 26.8, 28, 27.2, 23.5, 24.9, 24.6, 31.8, 32.6, 27.5, 25.1, + 23.1, 20.4, 20.2, 24, 20.1, 16.8, 16, 20.5, 20.1, 21.1, 22.2, 23.6, 24.4, + 25.7, 26.4, 26.3, 27.2, 27.1, 27, 26.4, 26.4, 27.1, 28.7, 28.1, 27.2, + 26.5, 25.5, 23.6, 23.6, 22.2, 21.4, 20.4, 19.4, 18.4, 18.1, 17.7, 17.4, + 17.6, 17.9, 18.1, 18.4, 18.6, 18.9, 19.2, 19.5, 19.9, 21.5, 23, 24.6, + 24.1, 23.6, 23.1, 21.4, 19.8, 18.1, 15.8, 13.5, 11.2, 10.9, 10.6, 10.3, + 8.9, 7.5, 6, 5.1, 4.2, 3.3, 5.5, 7.8, 10, 14.4, 18.8, 23.2, 24, 25.6, + 27.2, 26.1, 25, 24, 25.6, 27.1, 28.7, 29.6, 30.5, 31.4, 30.5, 29.7, 28.8, + 30.3, 31.8, 33.3, 32.3, 31.3, 30.4, 28.3, 26.2, 24.2, 21.1, 18, 14.9, + 19.9, 24.9, 29.9, 32, 34.1, 36.3, 36.9 ], "winddirection_120m": [ - 265, - 269, - 280, - 274, - 281, - 282, - 279, - 275, - 270, - 273, - 266, - 259, - 257, - 266, - 254, - 258, - 245, - 240, - 202, - 205, - 189, - 192, - 186, - 184, - 169, - 172, - 176, - 172, - 158, - 161, - 174, - 185, - 192, - 201, - 203, - 201, - 205, - 210, - 246, - 221, - 235, - 229, - 235, - 238, - 243, - 241, - 247, - 256, - 258, - 262, - 255, - 252, - 258, - 253, - 243, - 245, - 248, - 256, - 253, - 253, - 245, - 239, - 234, - 218, - 204, - 209, - 179, - 179, - 189, - 188, - 184, - 181, - 179, - 172, - 170, - 169, - 169, - 169, - 164, - 159, - 156, - 155, - 153, - 153, - 147, - 140, - 143, - 144, - 153, - 157, - 163, - 156, - 164, - 179, - 183, - 182, - 190, - 192, - 190, - 185, - 179, - 175, - 172, - 168, - 168, - 168, - 168, - 166, - 164, - 161, - 161, - 162, - 166, - 174, - 187, - 205, - 208, - 207, - 205, - 206, - 207, - 209, - 211, - 212, - 214, - 218, - 223, - 230, - 236, - 243, - 253, - 259, - 265, - 269, - 271, - 272, - 272, - 273, - 274, - 275, - 298, - 293, - 288, - 288, - 289, - 290, - 291, - 291, - 292, - 292, - 293, - 292, - 290, - 286, - 281, - 278, - 275, - 268, - 262, - 255, - 242, - 227, - 215, - 207, - 204, - 203, - 202, - 201 + 265, 269, 280, 274, 281, 282, 279, 275, 270, 273, 266, 259, 257, 266, 254, + 258, 245, 240, 202, 205, 189, 192, 186, 184, 169, 172, 176, 172, 158, 161, + 174, 185, 192, 201, 203, 201, 205, 210, 246, 221, 235, 229, 235, 238, 243, + 241, 247, 256, 258, 262, 255, 252, 258, 253, 243, 245, 248, 256, 253, 253, + 245, 239, 234, 218, 204, 209, 179, 179, 189, 188, 184, 181, 179, 172, 170, + 169, 169, 169, 164, 159, 156, 155, 153, 153, 147, 140, 143, 144, 153, 157, + 163, 156, 164, 179, 183, 182, 190, 192, 190, 185, 179, 175, 172, 168, 168, + 168, 168, 166, 164, 161, 161, 162, 166, 174, 187, 205, 208, 207, 205, 206, + 207, 209, 211, 212, 214, 218, 223, 230, 236, 243, 253, 259, 265, 269, 271, + 272, 272, 273, 274, 275, 298, 293, 288, 288, 289, 290, 291, 291, 292, 292, + 293, 292, 290, 286, 281, 278, 275, 268, 262, 255, 242, 227, 215, 207, 204, + 203, 202, 201 ], "windspeed_120m": [ - 24.7, - 24.5, - 23.8, - 20.2, - 21.5, - 22, - 23.5, - 22.3, - 22.2, - 21.2, - 18.6, - 14.6, - 13.8, - 13.2, - 12, - 12.7, - 9.1, - 8.1, - 11.3, - 12.5, - 13.6, - 15, - 15.3, - 16.1, - 17.6, - 18, - 17.9, - 16, - 14.8, - 15, - 14.5, - 12.9, - 14.8, - 18.2, - 10.1, - 9.9, - 9.7, - 13, - 21.6, - 22.1, - 19.3, - 23.4, - 26.6, - 25.5, - 26.5, - 27.7, - 29.1, - 30.2, - 29, - 27.8, - 27, - 26.9, - 27.4, - 23.8, - 22.1, - 23.9, - 23.4, - 28.4, - 23.9, - 21.3, - 19.3, - 15.9, - 16.5, - 16.4, - 19, - 15.9, - 16.9, - 19.5, - 27.9, - 29.4, - 30.7, - 31.2, - 29.3, - 28.6, - 31.6, - 34.1, - 35.2, - 33.5, - 32.8, - 33.5, - 33.4, - 34.1, - 33.3, - 27.9, - 19.2, - 18.6, - 20.4, - 23.3, - 27.3, - 27.1, - 26.5, - 27.4, - 29.3, - 28.8, - 22.7, - 23.9, - 25, - 24.2, - 24, - 23.5, - 23.2, - 23.4, - 23.7, - 24.1, - 24.4, - 24.6, - 24, - 22.1, - 19.5, - 17, - 17.2, - 18.2, - 18.5, - 16.5, - 14, - 13.5, - 15.8, - 19.2, - 22.4, - 22.1, - 20.5, - 18.9, - 19.1, - 19.8, - 20, - 19.1, - 17.7, - 15.9, - 14.8, - 13.8, - 13.4, - 14.1, - 15.5, - 17.2, - 18.2, - 20.8, - 23.9, - 26, - 28, - 30.1, - 34.7, - 33.6, - 32.6, - 32.7, - 32.9, - 33.2, - 33.3, - 33.4, - 33.2, - 33, - 32.6, - 31.4, - 29.6, - 27.3, - 24.3, - 22, - 19.6, - 17.3, - 16.7, - 17, - 18.5, - 21.8, - 27.2, - 34.1, - 37, - 38.7, - 39.8, - 40.2 + 24.7, 24.5, 23.8, 20.2, 21.5, 22, 23.5, 22.3, 22.2, 21.2, 18.6, 14.6, + 13.8, 13.2, 12, 12.7, 9.1, 8.1, 11.3, 12.5, 13.6, 15, 15.3, 16.1, 17.6, + 18, 17.9, 16, 14.8, 15, 14.5, 12.9, 14.8, 18.2, 10.1, 9.9, 9.7, 13, 21.6, + 22.1, 19.3, 23.4, 26.6, 25.5, 26.5, 27.7, 29.1, 30.2, 29, 27.8, 27, 26.9, + 27.4, 23.8, 22.1, 23.9, 23.4, 28.4, 23.9, 21.3, 19.3, 15.9, 16.5, 16.4, + 19, 15.9, 16.9, 19.5, 27.9, 29.4, 30.7, 31.2, 29.3, 28.6, 31.6, 34.1, + 35.2, 33.5, 32.8, 33.5, 33.4, 34.1, 33.3, 27.9, 19.2, 18.6, 20.4, 23.3, + 27.3, 27.1, 26.5, 27.4, 29.3, 28.8, 22.7, 23.9, 25, 24.2, 24, 23.5, 23.2, + 23.4, 23.7, 24.1, 24.4, 24.6, 24, 22.1, 19.5, 17, 17.2, 18.2, 18.5, 16.5, + 14, 13.5, 15.8, 19.2, 22.4, 22.1, 20.5, 18.9, 19.1, 19.8, 20, 19.1, 17.7, + 15.9, 14.8, 13.8, 13.4, 14.1, 15.5, 17.2, 18.2, 20.8, 23.9, 26, 28, 30.1, + 34.7, 33.6, 32.6, 32.7, 32.9, 33.2, 33.3, 33.4, 33.2, 33, 32.6, 31.4, + 29.6, 27.3, 24.3, 22, 19.6, 17.3, 16.7, 17, 18.5, 21.8, 27.2, 34.1, 37, + 38.7, 39.8, 40.2 ], "windspeed_80m": [ - 20.9, - 21.3, - 19.9, - 16.7, - 18.1, - 18.4, - 19.3, - 18.9, - 17.9, - 18.3, - 15.5, - 13, - 13, - 12.3, - 11.4, - 11.5, - 7.7, - 7.2, - 10.1, - 10.6, - 11.3, - 12, - 12.3, - 12.7, - 14.1, - 13.8, - 14, - 12.1, - 12.3, - 12.8, - 12.3, - 11.4, - 12.5, - 11.3, - 9.7, - 9.2, - 9.3, - 12, - 20, - 19.4, - 16.4, - 20.5, - 21.8, - 21.8, - 22.3, - 23.6, - 24.6, - 25.8, - 25.1, - 24.1, - 23.4, - 23.6, - 24, - 20.7, - 19.2, - 20.8, - 20.6, - 26.7, - 21.8, - 19.9, - 17.9, - 14.8, - 15.5, - 15.4, - 17.2, - 14.5, - 13.1, - 14.8, - 21.2, - 21.6, - 22.6, - 21.9, - 20.4, - 20.2, - 22.7, - 24.1, - 25.2, - 24.1, - 24.8, - 25, - 24.5, - 25.1, - 21.7, - 16.7, - 17.6, - 17.5, - 19.2, - 20.9, - 20.8, - 20.1, - 19.4, - 20.7, - 21.7, - 21.6, - 18.2, - 18.9, - 19, - 18.7, - 18.5, - 18.2, - 18.3, - 18.7, - 19.3, - 20, - 20.3, - 20.5, - 19.7, - 17.6, - 14.7, - 12.2, - 12.8, - 14.5, - 15.8, - 14.3, - 12.2, - 11.3, - 13, - 15.5, - 17.9, - 17.6, - 16.4, - 15.1, - 15, - 15.2, - 15.1, - 14.5, - 13.6, - 12.4, - 11.5, - 10.7, - 10.2, - 10.6, - 11.9, - 13.5, - 16.1, - 15.4, - 15.3, - 16.5, - 18.4, - 20.4, - 24.6, - 25.3, - 26.3, - 27.2, - 28, - 28.4, - 27.8, - 26.7, - 25.7, - 26.2, - 27.1, - 27.6, - 26.7, - 25.1, - 22.8, - 21, - 19, - 16.5, - 14.9, - 13.8, - 14.3, - 17, - 21.4, - 26.9, - 29.7, - 31.7, - 33.3, - 33.9 + 20.9, 21.3, 19.9, 16.7, 18.1, 18.4, 19.3, 18.9, 17.9, 18.3, 15.5, 13, 13, + 12.3, 11.4, 11.5, 7.7, 7.2, 10.1, 10.6, 11.3, 12, 12.3, 12.7, 14.1, 13.8, + 14, 12.1, 12.3, 12.8, 12.3, 11.4, 12.5, 11.3, 9.7, 9.2, 9.3, 12, 20, 19.4, + 16.4, 20.5, 21.8, 21.8, 22.3, 23.6, 24.6, 25.8, 25.1, 24.1, 23.4, 23.6, + 24, 20.7, 19.2, 20.8, 20.6, 26.7, 21.8, 19.9, 17.9, 14.8, 15.5, 15.4, + 17.2, 14.5, 13.1, 14.8, 21.2, 21.6, 22.6, 21.9, 20.4, 20.2, 22.7, 24.1, + 25.2, 24.1, 24.8, 25, 24.5, 25.1, 21.7, 16.7, 17.6, 17.5, 19.2, 20.9, + 20.8, 20.1, 19.4, 20.7, 21.7, 21.6, 18.2, 18.9, 19, 18.7, 18.5, 18.2, + 18.3, 18.7, 19.3, 20, 20.3, 20.5, 19.7, 17.6, 14.7, 12.2, 12.8, 14.5, + 15.8, 14.3, 12.2, 11.3, 13, 15.5, 17.9, 17.6, 16.4, 15.1, 15, 15.2, 15.1, + 14.5, 13.6, 12.4, 11.5, 10.7, 10.2, 10.6, 11.9, 13.5, 16.1, 15.4, 15.3, + 16.5, 18.4, 20.4, 24.6, 25.3, 26.3, 27.2, 28, 28.4, 27.8, 26.7, 25.7, + 26.2, 27.1, 27.6, 26.7, 25.1, 22.8, 21, 19, 16.5, 14.9, 13.8, 14.3, 17, + 21.4, 26.9, 29.7, 31.7, 33.3, 33.9 ], "evapotranspiration": [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.01, - 0.01, - 0.02, - 0.02, - 0.02, - 0.02, - 0.01, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.01, - 0.01, - 0.01, - 0.01, - 0, - 0, - 0, - 0, - 0.01, - 0.02, - 0.02, - 0.02, - 0.03, - 0.02, - 0.02, - 0.01, - 0.01, - 0.01, - 0.02, - 0.02, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.02, - 0.02, - 0.03, - 0.02, - 0.02, - 0.02, - 0.02, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.02, - 0.02, - 0.02, - 0.02, - 0.02, - 0.02, - 0.02, - 0.01, - 0.01, - 0.02, - 0.02, - 0.03, - 0.03, - 0.03, - 0.02, - 0.02, - 0.02, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.01, - 0.01, - 0.02, - 0.02, - 0.03, - 0.03, - 0.02, - 0.02, - 0.01, - 0.01, - 0.01, - 0.01, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.02, - 0.02, - 0.02, - 0.02, - 0.02, - 0.03, - 0.03, - 0.03, - 0.02, - 0.02, - 0.02, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.02 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.01, 0.01, 0.02, 0.02, 0.02, 0.02, 0.01, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0.01, 0.01, 0.01, 0.01, 0, 0, 0, 0, 0.01, 0.02, + 0.02, 0.02, 0.03, 0.02, 0.02, 0.01, 0.01, 0.01, 0.02, 0.02, 0.01, 0.01, + 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.02, + 0.02, 0.03, 0.02, 0.02, 0.02, 0.02, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, + 0.01, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.01, 0.01, 0.02, 0.02, + 0.03, 0.03, 0.03, 0.02, 0.02, 0.02, 0.01, 0.01, 0.01, 0.01, 0.01, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0.01, 0.01, 0.02, 0.02, 0.03, 0.03, 0.02, 0.02, + 0.01, 0.01, 0.01, 0.01, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.01, 0.01, + 0.01, 0.01, 0.01, 0.01, 0.01, 0, 0, 0, 0, 0, 0, 0, 0.01, 0.01, 0.01, 0.01, + 0.01, 0.02, 0.02, 0.02, 0.02, 0.02, 0.03, 0.03, 0.03, 0.02, 0.02, 0.02, + 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.02 ], "precipitation": [ - 0, - 0, - 0.01, - 0, - 0.03, - 0.01, - 0, - 0, - 0, - 0, - 0, - 0, - 0.01, - 0.01, - 0, - 0.06, - 0.06, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.03, - 0.07, - 0, - 0, - 0, - 0.09, - 0.09, - 0.01, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.01, - 0.1, - 0.3, - 0.2, - 0.15, - 0, - 0, - 0, - 0, - 0.01, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.1, - 0.01, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.05, - 0.05, - 0.05, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.01, - 0.01, - 0.01, - 0.03, - 0.03, - 0.03, - 0.02, - 0.02, - 0.02, - 0.13, - 0.13, - 0.13, - 0, - 0, - 0, - 0.07, - 0.07, - 0.07, - 0.16, - 0.16, - 0.16, - 0.01, - 0.01, - 0.01, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.03, - 0.03, - 0.03, - 0.04, - 0.04, - 0.04, - 0.88 + 0, 0, 0.01, 0, 0.03, 0.01, 0, 0, 0, 0, 0, 0, 0.01, 0.01, 0, 0.06, 0.06, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.03, 0.07, 0, + 0, 0, 0.09, 0.09, 0.01, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0.01, 0.1, 0.3, 0.2, 0.15, 0, 0, 0, 0, 0.01, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0.1, 0.01, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0.05, 0.05, 0.05, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0.01, 0.01, 0.01, 0.03, 0.03, 0.03, 0.02, 0.02, 0.02, 0.13, 0.13, 0.13, 0, + 0, 0, 0.07, 0.07, 0.07, 0.16, 0.16, 0.16, 0.01, 0.01, 0.01, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0.03, 0.03, 0.03, 0.04, 0.04, 0.04, 0.88 ] }, "daily": { - "temperature_2m_max": [ - 7.6, - 5.4, - 4.8, - 4.5, - 3.4, - 2.2, - 3 - ], - "precipitation_hours": [ - 7, - 5, - 5, - 3, - 3, - 13, - 15 - ], - "shortwave_radiation_sum": [ - 1.44, - 2.16, - 1.95, - 2.05, - 4.18, - 2.86, - 3.31 - ], - "winddirection_10m_dominant": [ - 251, - 210, - 230, - 143, - 143, - 248, - 256 - ], - "windspeed_10m_max": [ - 10.9, - 12.9, - 14.8, - 10.7, - 7, - 13, - 16.1 - ], - "apparent_temperature_min": [ - 3.4, - -2.9, - -1.9, - -4, - -3.5, - -3.6, - -4.6 - ], + "temperature_2m_max": [7.6, 5.4, 4.8, 4.5, 3.4, 2.2, 3], + "precipitation_hours": [7, 5, 5, 3, 3, 13, 15], + "shortwave_radiation_sum": [1.44, 2.16, 1.95, 2.05, 4.18, 2.86, 3.31], + "winddirection_10m_dominant": [251, 210, 230, 143, 143, 248, 256], + "windspeed_10m_max": [10.9, 12.9, 14.8, 10.7, 7, 13, 16.1], + "apparent_temperature_min": [3.4, -2.9, -1.9, -4, -3.5, -3.6, -4.6], "sunset": [ "2021-11-24T16:04", "2021-11-25T16:03", @@ -6543,15 +731,7 @@ "2021-11-29T15:59", "2021-11-30T15:59" ], - "weathercode": [ - 61, - 61, - 61, - 61, - 61, - 77, - 80 - ], + "weathercode": [61, 61, 61, 61, 61, 77, 80], "sunrise": [ "2021-11-24T07:41", "2021-11-25T07:43", @@ -6561,42 +741,10 @@ "2021-11-29T07:49", "2021-11-30T07:51" ], - "apparent_temperature_max": [ - 5.5, - 3.2, - 1.4, - 0.6, - 0.4, - -1.2, - -0.3 - ], - "temperature_2m_min": [ - 5.5, - 0.2, - 1.8, - -0.1, - -0.2, - -0.5, - -0.3 - ], - "windgusts_10m_max": [ - 8.5, - 10.4, - 16, - 18.1, - 10.9, - 3.3, - 14.9 - ], - "precipitation_sum": [ - 0.19, - 0.29, - 0.76, - 0.12, - 0.15, - 0.64, - 1.74 - ], + "apparent_temperature_max": [5.5, 3.2, 1.4, 0.6, 0.4, -1.2, -0.3], + "temperature_2m_min": [5.5, 0.2, 1.8, -0.1, -0.2, -0.5, -0.3], + "windgusts_10m_max": [8.5, 10.4, 16, 18.1, 10.9, 3.3, 14.9], + "precipitation_sum": [0.19, 0.29, 0.76, 0.12, 0.15, 0.64, 1.74], "time": [ "2021-11-24", "2021-11-25", @@ -6610,42 +758,42 @@ "utc_offset_seconds": 3600, "hourly_units": { "precipitation": "mm", - "shortwave_radiation": "W\/m²", - "soil_moisture_0_1cm": "m³\/m³", + "shortwave_radiation": "W/m²", + "soil_moisture_0_1cm": "m³/m³", "pressure_msl": "hPa", - "soil_moisture_3_9cm": "m³\/m³", + "soil_moisture_3_9cm": "m³/m³", "soil_temperature_54cm": "°C", "soil_temperature_18cm": "°C", "winddirection_120m": "°", "vapor_pressure_deficit": "kPa", "dewpoint_2m": "°C", "winddirection_180m": "°", - "windspeed_10m": "km\/h", + "windspeed_10m": "km/h", "cloudcover_low": "%", "cloudcover_mid": "%", "cloudcover_high": "%", - "windgusts_10m": "km\/h", - "soil_moisture_9_27cm": "m³\/m³", - "windspeed_120m": "km\/h", + "windgusts_10m": "km/h", + "soil_moisture_9_27cm": "m³/m³", + "windspeed_120m": "km/h", "winddirection_10m": "°", "time": "iso8601", "soil_temperature_6cm": "°C", "apparent_temperature": "°C", - "windspeed_80m": "km\/h", - "soil_moisture_1_3cm": "m³\/m³", - "diffuse_radiation": "W\/m²", + "windspeed_80m": "km/h", + "soil_moisture_1_3cm": "m³/m³", + "diffuse_radiation": "W/m²", "snow_depth": "m", - "windspeed_180m": "km\/h", + "windspeed_180m": "km/h", "weathercode": "wmo code", - "direct_normal_irradiance": "W\/m²", + "direct_normal_irradiance": "W/m²", "relativehumidity_2m": "%", - "soil_moisture_27_81cm": "m³\/m³", + "soil_moisture_27_81cm": "m³/m³", "winddirection_80m": "°", "freezinglevel_height": "m", "evapotranspiration": "mm", "cloudcover": "%", "soil_temperature_0cm": "°C", - "direct_radiation": "W\/m²", + "direct_radiation": "W/m²", "temperature_2m": "°C" }, "longitude": 13.419998, @@ -6657,4 +805,4 @@ "winddirection": 168, "time": "2021-11-24T23:00" } -} \ No newline at end of file +} diff --git a/tests/components/openuv/fixtures/protection_window_data.json b/tests/components/openuv/fixtures/protection_window_data.json index c27bd25d948..f926947547b 100644 --- a/tests/components/openuv/fixtures/protection_window_data.json +++ b/tests/components/openuv/fixtures/protection_window_data.json @@ -6,4 +6,3 @@ "to_uv": 3.6483 } } - diff --git a/tests/components/openuv/fixtures/uv_index_data.json b/tests/components/openuv/fixtures/uv_index_data.json index 76c2b2ed988..dbb0a73e630 100644 --- a/tests/components/openuv/fixtures/uv_index_data.json +++ b/tests/components/openuv/fixtures/uv_index_data.json @@ -38,4 +38,3 @@ } } } - diff --git a/tests/components/overkiz/test_config_flow.py b/tests/components/overkiz/test_config_flow.py index db86a4abc5c..967d6cbb8c8 100644 --- a/tests/components/overkiz/test_config_flow.py +++ b/tests/components/overkiz/test_config_flow.py @@ -7,6 +7,7 @@ from aiohttp import ClientError from pyoverkiz.exceptions import ( BadCredentialsException, MaintenanceException, + TooManyAttemptsBannedException, TooManyRequestsException, ) import pytest @@ -86,6 +87,7 @@ async def test_form(hass: HomeAssistant) -> None: (TimeoutError, "cannot_connect"), (ClientError, "cannot_connect"), (MaintenanceException, "server_in_maintenance"), + (TooManyAttemptsBannedException, "too_many_attempts"), (Exception, "unknown"), ], ) diff --git a/tests/components/ozw/__init__.py b/tests/components/ozw/__init__.py deleted file mode 100644 index ce419b9f55b..00000000000 --- a/tests/components/ozw/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Tests for the OZW integration.""" diff --git a/tests/components/ozw/common.py b/tests/components/ozw/common.py deleted file mode 100644 index 450066c5aed..00000000000 --- a/tests/components/ozw/common.py +++ /dev/null @@ -1,62 +0,0 @@ -"""Helpers for tests.""" -import json -from unittest.mock import Mock, patch - -from homeassistant import config_entries -from homeassistant.components.ozw.const import DOMAIN - -from tests.common import MockConfigEntry - - -async def setup_ozw(hass, entry=None, fixture=None): - """Set up OZW and load a dump.""" - mqtt_entry = MockConfigEntry( - domain="mqtt", state=config_entries.ConfigEntryState.LOADED - ) - mqtt_entry.add_to_hass(hass) - - if entry is None: - entry = MockConfigEntry( - domain=DOMAIN, - title="Z-Wave", - ) - - entry.add_to_hass(hass) - - with patch("homeassistant.components.mqtt.async_subscribe") as mock_subscribe: - mock_subscribe.return_value = Mock() - assert await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - assert "ozw" in hass.config.components - assert len(mock_subscribe.mock_calls) == 1 - receive_message = mock_subscribe.mock_calls[0][1][2] - - if fixture is not None: - for line in fixture.split("\n"): - line = line.strip() - if not line: - continue - topic, payload = line.split(",", 1) - receive_message(Mock(topic=topic, payload=payload)) - - await hass.async_block_till_done() - - return receive_message - - -class MQTTMessage: - """Represent a mock MQTT message.""" - - def __init__(self, topic, payload): - """Set up message.""" - self.topic = topic - self.payload = payload - - def decode(self): - """Decode message payload from a string to a json dict.""" - self.payload = json.loads(self.payload) - - def encode(self): - """Encode message payload into a string.""" - self.payload = json.dumps(self.payload) diff --git a/tests/components/ozw/conftest.py b/tests/components/ozw/conftest.py deleted file mode 100644 index d09259654de..00000000000 --- a/tests/components/ozw/conftest.py +++ /dev/null @@ -1,280 +0,0 @@ -"""Helpers for tests.""" -import json -from unittest.mock import patch - -import pytest - -from homeassistant.config_entries import ConfigEntryState - -from .common import MQTTMessage - -from tests.common import MockConfigEntry, load_fixture -from tests.components.light.conftest import mock_light_profiles # noqa: F401 - - -@pytest.fixture(name="generic_data", scope="session") -def generic_data_fixture(): - """Load generic MQTT data and return it.""" - return load_fixture("ozw/generic_network_dump.csv") - - -@pytest.fixture(name="migration_data", scope="session") -def migration_data_fixture(): - """Load migration MQTT data and return it.""" - return load_fixture("ozw/migration_fixture.csv") - - -@pytest.fixture(name="fan_data", scope="session") -def fan_data_fixture(): - """Load fan MQTT data and return it.""" - return load_fixture("ozw/fan_network_dump.csv") - - -@pytest.fixture(name="light_data", scope="session") -def light_data_fixture(): - """Load light dimmer MQTT data and return it.""" - return load_fixture("ozw/light_network_dump.csv") - - -@pytest.fixture(name="light_new_ozw_data", scope="session") -def light_new_ozw_data_fixture(): - """Load light dimmer MQTT data and return it.""" - return load_fixture("ozw/light_new_ozw_network_dump.csv") - - -@pytest.fixture(name="light_no_ww_data", scope="session") -def light_no_ww_data_fixture(): - """Load light dimmer MQTT data and return it.""" - return load_fixture("ozw/light_no_ww_network_dump.csv") - - -@pytest.fixture(name="light_no_cw_data", scope="session") -def light_no_cw_data_fixture(): - """Load light dimmer MQTT data and return it.""" - return load_fixture("ozw/light_no_cw_network_dump.csv") - - -@pytest.fixture(name="light_wc_data", scope="session") -def light_wc_only_data_fixture(): - """Load light dimmer MQTT data and return it.""" - return load_fixture("ozw/light_wc_network_dump.csv") - - -@pytest.fixture(name="cover_data", scope="session") -def cover_data_fixture(): - """Load cover MQTT data and return it.""" - return load_fixture("ozw/cover_network_dump.csv") - - -@pytest.fixture(name="cover_gdo_data", scope="session") -def cover_gdo_data_fixture(): - """Load cover_gdo MQTT data and return it.""" - return load_fixture("ozw/cover_gdo_network_dump.csv") - - -@pytest.fixture(name="climate_data", scope="session") -def climate_data_fixture(): - """Load climate MQTT data and return it.""" - return load_fixture("ozw/climate_network_dump.csv") - - -@pytest.fixture(name="lock_data", scope="session") -def lock_data_fixture(): - """Load lock MQTT data and return it.""" - return load_fixture("ozw/lock_network_dump.csv") - - -@pytest.fixture(name="string_sensor_data", scope="session") -def string_sensor_fixture(): - """Load string sensor MQTT data and return it.""" - return load_fixture("ozw/sensor_string_value_network_dump.csv") - - -@pytest.fixture(name="sent_messages") -def sent_messages_fixture(): - """Fixture to capture sent messages.""" - sent_messages = [] - - with patch( - "homeassistant.components.mqtt.async_publish", - side_effect=lambda hass, topic, payload: sent_messages.append( - {"topic": topic, "payload": json.loads(payload)} - ), - ): - yield sent_messages - - -@pytest.fixture(name="fan_msg") -async def fan_msg_fixture(hass): - """Return a mock MQTT msg with a fan actuator message.""" - fan_json = json.loads( - await hass.async_add_executor_job(load_fixture, "ozw/fan.json") - ) - message = MQTTMessage(topic=fan_json["topic"], payload=fan_json["payload"]) - message.encode() - return message - - -@pytest.fixture(name="light_msg") -async def light_msg_fixture(hass): - """Return a mock MQTT msg with a light actuator message.""" - light_json = json.loads( - await hass.async_add_executor_job(load_fixture, "ozw/light.json") - ) - message = MQTTMessage(topic=light_json["topic"], payload=light_json["payload"]) - message.encode() - return message - - -@pytest.fixture(name="light_no_rgb_msg") -async def light_no_rgb_msg_fixture(hass): - """Return a mock MQTT msg with a light actuator message.""" - light_json = json.loads( - await hass.async_add_executor_job(load_fixture, "ozw/light_no_rgb.json") - ) - message = MQTTMessage(topic=light_json["topic"], payload=light_json["payload"]) - message.encode() - return message - - -@pytest.fixture(name="light_rgb_msg") -async def light_rgb_msg_fixture(hass): - """Return a mock MQTT msg with a light actuator message.""" - light_json = json.loads( - await hass.async_add_executor_job(load_fixture, "ozw/light_rgb.json") - ) - message = MQTTMessage(topic=light_json["topic"], payload=light_json["payload"]) - message.encode() - return message - - -@pytest.fixture(name="light_pure_rgb_msg") -async def light_pure_rgb_msg_fixture(hass): - """Return a mock MQTT msg with a pure rgb light actuator message.""" - light_json = json.loads( - await hass.async_add_executor_job(load_fixture, "ozw/light_pure_rgb.json") - ) - message = MQTTMessage(topic=light_json["topic"], payload=light_json["payload"]) - message.encode() - return message - - -@pytest.fixture(name="switch_msg") -async def switch_msg_fixture(hass): - """Return a mock MQTT msg with a switch actuator message.""" - switch_json = json.loads( - await hass.async_add_executor_job(load_fixture, "ozw/switch.json") - ) - message = MQTTMessage(topic=switch_json["topic"], payload=switch_json["payload"]) - message.encode() - return message - - -@pytest.fixture(name="sensor_msg") -async def sensor_msg_fixture(hass): - """Return a mock MQTT msg with a sensor change message.""" - sensor_json = json.loads( - await hass.async_add_executor_job(load_fixture, "ozw/sensor.json") - ) - message = MQTTMessage(topic=sensor_json["topic"], payload=sensor_json["payload"]) - message.encode() - return message - - -@pytest.fixture(name="binary_sensor_msg") -async def binary_sensor_msg_fixture(hass): - """Return a mock MQTT msg with a binary_sensor change message.""" - sensor_json = json.loads( - await hass.async_add_executor_job(load_fixture, "ozw/binary_sensor.json") - ) - message = MQTTMessage(topic=sensor_json["topic"], payload=sensor_json["payload"]) - message.encode() - return message - - -@pytest.fixture(name="binary_sensor_alt_msg") -async def binary_sensor_alt_msg_fixture(hass): - """Return a mock MQTT msg with a binary_sensor change message.""" - sensor_json = json.loads( - await hass.async_add_executor_job(load_fixture, "ozw/binary_sensor_alt.json") - ) - message = MQTTMessage(topic=sensor_json["topic"], payload=sensor_json["payload"]) - message.encode() - return message - - -@pytest.fixture(name="cover_msg") -async def cover_msg_fixture(hass): - """Return a mock MQTT msg with a cover level change message.""" - sensor_json = json.loads( - await hass.async_add_executor_job(load_fixture, "ozw/cover.json") - ) - message = MQTTMessage(topic=sensor_json["topic"], payload=sensor_json["payload"]) - message.encode() - return message - - -@pytest.fixture(name="cover_gdo_msg") -async def cover_gdo_msg_fixture(hass): - """Return a mock MQTT msg with a cover barrier state change message.""" - sensor_json = json.loads( - await hass.async_add_executor_job(load_fixture, "ozw/cover_gdo.json") - ) - message = MQTTMessage(topic=sensor_json["topic"], payload=sensor_json["payload"]) - message.encode() - return message - - -@pytest.fixture(name="climate_msg") -async def climate_msg_fixture(hass): - """Return a mock MQTT msg with a climate mode change message.""" - sensor_json = json.loads( - await hass.async_add_executor_job(load_fixture, "ozw/climate.json") - ) - message = MQTTMessage(topic=sensor_json["topic"], payload=sensor_json["payload"]) - message.encode() - return message - - -@pytest.fixture(name="lock_msg") -async def lock_msg_fixture(hass): - """Return a mock MQTT msg with a lock actuator message.""" - lock_json = json.loads( - await hass.async_add_executor_job(load_fixture, "ozw/lock.json") - ) - message = MQTTMessage(topic=lock_json["topic"], payload=lock_json["payload"]) - message.encode() - return message - - -@pytest.fixture(name="stop_addon") -def mock_install_addon(): - """Mock stop add-on.""" - with patch("homeassistant.components.hassio.async_stop_addon") as stop_addon: - yield stop_addon - - -@pytest.fixture(name="uninstall_addon") -def mock_uninstall_addon(): - """Mock uninstall add-on.""" - with patch( - "homeassistant.components.hassio.async_uninstall_addon" - ) as uninstall_addon: - yield uninstall_addon - - -@pytest.fixture(name="get_addon_discovery_info") -def mock_get_addon_discovery_info(): - """Mock get add-on discovery info.""" - with patch( - "homeassistant.components.hassio.async_get_addon_discovery_info" - ) as get_addon_discovery_info: - yield get_addon_discovery_info - - -@pytest.fixture(name="mqtt") -async def mock_mqtt_fixture(hass): - """Mock the MQTT integration.""" - mqtt_entry = MockConfigEntry(domain="mqtt", state=ConfigEntryState.LOADED) - mqtt_entry.add_to_hass(hass) - return mqtt_entry diff --git a/tests/components/ozw/fixtures/binary_sensor.json b/tests/components/ozw/fixtures/binary_sensor.json deleted file mode 100644 index 4d6317827d1..00000000000 --- a/tests/components/ozw/fixtures/binary_sensor.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "topic": "OpenZWave/1/node/37/instance/1/commandclass/113/value/1970325463777300/", - "payload": { - "Label": "Home Security", - "Value": { - "List": [ - { - "Value": 0, - "Label": "Clear" - }, - { - "Value": 8, - "Label": "Motion Detected at Unknown Location" - } - ], - "Selected": "Motion Detected at Unknown Location", - "Selected_id": 8 - }, - "Units": "", - "Min": 0, - "Max": 0, - "Type": "List", - "Instance": 1, - "CommandClass": "COMMAND_CLASS_NOTIFICATION", - "Index": 7, - "Node": 37, - "Genre": "User", - "Help": "Home Security Alerts", - "ValueIDKey": 1970325463777300, - "ReadOnly": false, - "WriteOnly": false, - "ValueSet": false, - "ValuePolled": false, - "ChangeVerified": false, - "Event": "valueAdded", - "TimeStamp": 1579566891 - } -} \ No newline at end of file diff --git a/tests/components/ozw/fixtures/binary_sensor_alt.json b/tests/components/ozw/fixtures/binary_sensor_alt.json deleted file mode 100644 index 187028843ff..00000000000 --- a/tests/components/ozw/fixtures/binary_sensor_alt.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "topic": "OpenZWave/1/node/37/instance/1/commandclass/48/value/625737744/", - "payload": { - "Label": "Sensor", - "Value": true, - "Units": "", - "Min": 0, - "Max": 0, - "Type": "Bool", - "Instance": 1, - "CommandClass": "COMMAND_CLASS_SENSOR_BINARY", - "Index": 0, - "Node": 37, - "Genre": "User", - "Help": "Binary Sensor State", - "ValueIDKey": 625737744, - "ReadOnly": false, - "WriteOnly": false, - "ValueSet": false, - "ValuePolled": false, - "ChangeVerified": false, - "Event": "valueAdded", - "TimeStamp": 1579566891 - } -} \ No newline at end of file diff --git a/tests/components/ozw/fixtures/climate.json b/tests/components/ozw/fixtures/climate.json deleted file mode 100644 index 652dc9aef26..00000000000 --- a/tests/components/ozw/fixtures/climate.json +++ /dev/null @@ -1,54 +0,0 @@ -{ - "topic": "OpenZWave/1/node/7/instance/1/commandclass/64/value/122683412/", - "payload": { - "Label": "Mode", - "Value": { - "List": [ - { - "Value": 0, - "Label": "Off" - }, - { - "Value": 1, - "Label": "Heat" - }, - { - "Value": 2, - "Label": "Cool" - }, - { - "Value": 3, - "Label": "Auto" - }, - { - "Value": 11, - "Label": "Heat Econ" - }, - { - "Value": 12, - "Label": "Cool Econ" - } - ], - "Selected": "Auto", - "Selected_id": 3 - }, - "Units": "", - "Min": 0, - "Max": 0, - "Type": "List", - "Instance": 1, - "CommandClass": "COMMAND_CLASS_THERMOSTAT_MODE", - "Index": 0, - "Node": 7, - "Genre": "User", - "Help": "Set the Thermostat Mode", - "ValueIDKey": 122683412, - "ReadOnly": false, - "WriteOnly": false, - "ValueSet": false, - "ValuePolled": false, - "ChangeVerified": false, - "Event": "valueAdded", - "TimeStamp": 1588264894 - } -} \ No newline at end of file diff --git a/tests/components/ozw/fixtures/climate_network_dump.csv b/tests/components/ozw/fixtures/climate_network_dump.csv deleted file mode 100644 index 99cef9091c5..00000000000 --- a/tests/components/ozw/fixtures/climate_network_dump.csv +++ /dev/null @@ -1,208 +0,0 @@ -OpenZWave/1/status/,{ "OpenZWave_Version": "1.6.1008", "OZWDeamon_Version": "0.1", "QTOpenZWave_Version": "1.0.0", "QT_Version": "5.12.5", "Status": "driverAllNodesQueried", "TimeStamp": 1579566933, "ManufacturerSpecificDBReady": true, "homeID": 3245146787, "getControllerNodeId": 1, "getSUCNodeId": 1, "isPrimaryController": true, "isBridgeController": false, "hasExtendedTXStatistics": true, "getControllerLibraryVersion": "Z-Wave 3.95", "getControllerLibraryType": "Static Controller", "getControllerPath": "/dev/zwave"} -OpenZWave/1/node/7/,{ "NodeID": 7, "NodeQueryStage": "Complete", "isListening": true, "isFlirs": false, "isBeaming": false, "isRouting": false, "isSecurityv1": false, "isZWavePlus": true, "isNIFRecieved": true, "isAwake": true, "isFailed": false, "MetaData": { "OZWInfoURL": "", "ZWAProductURL": "", "ProductPic": "", "Description": "", "ProductManualURL": "", "ProductPageURL": "", "InclusionHelp": "", "ExclusionHelp": "", "ResetHelp": "", "WakeupHelp": "", "ProductSupportURL": "", "Frequency": "", "Name": "", "ProductPicBase64": "" }, "Event": "nodeQueriesComplete", "TimeStamp": 1588264908, "NodeManufacturerName": "2GIG Technologies", "NodeProductName": "CT32 Thermostat", "NodeBasicString": "Routing Slave", "NodeBasic": 4, "NodeGenericString": "Thermostat", "NodeGeneric": 8, "NodeSpecificString": "General Thermostat V2", "NodeSpecific": 6, "NodeManufacturerID": "0x0098", "NodeProductType": "0x2002", "NodeProductID": "0x0100", "NodeBaudRate": 100000, "NodeVersion": 4, "NodeName": "", "NodeLocation": "", "NodeDeviceTypeString": "Thermostat HVAC", "NodeDeviceType": 4608, "NodeRole": 7, "NodeRoleString": "Listening Sleeping Slave", "NodePlusType": 0, "NodePlusTypeString": "Z-Wave+ node", "Neighbors": [ 1, 2 ]} -OpenZWave/1/node/7/instance/1/,{ "Instance": 1, "TimeStamp": 1588264894} -OpenZWave/1/node/7/instance/1/commandclass/112/,{ "Instance": 1, "CommandClassId": 112, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "TimeStamp": 1588264893} -OpenZWave/1/node/7/instance/1/commandclass/112/value/281475104374804/,{ "Label": "Temperature Reporting Threshold", "Value": { "List": [ { "Value": 0, "Label": "Disabled" }, { "Value": 1, "Label": "0.5F" }, { "Value": 2, "Label": "1.0F" }, { "Value": 3, "Label": "1.5F" }, { "Value": 4, "Label": "2.0F" } ], "Selected": "1.0F", "Selected_id": 2 }, "Units": "", "Min": 0, "Max": 4, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 1, "Node": 7, "Genre": "Config", "Help": "The Temperature Reporting Threshold Configuration Set Command sets the reporting threshold for changes in the ambient temperature as detected by the thermostat.", "ValueIDKey": 281475104374804, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588264893} -OpenZWave/1/node/7/instance/1/commandclass/112/value/562950081085460/,{ "Label": "HVAC Settings", "Value": { "List": [ { "Value": 17891585, "Label": "HVAC: Normal, Aux Stages: 1, Aux Setup: Gas, Heat Pump Stages: 1, Cool Stages: 1" }, { "Value": 34668801, "Label": "HVAC: Heat Pump, Aux Stages: 1, Aux Setup: Gas, Heat Pump Stages: 1, Cool Stages: 1" }, { "Value": 18940161, "Label": "HVAC: Normal, Aux Stages: 2, Aux Setup: Gas, Heat Pump Stages: 1, Cool Stages: 1" }, { "Value": 35717377, "Label": "HVAC: Heat Pump, Aux Stages: 2, Aux Setup: Gas, Heat Pump Stages: 1, Cool Stages: 1" }, { "Value": 17957121, "Label": "HVAC: Normal, Aux Stages: 1, Aux Setup: Elec, Heat Pump Stages: 1, Cool Stages: 1" }, { "Value": 34734337, "Label": "HVAC: Heat Pump, Aux Stages: 1, Aux Setup: Elec, Heat Pump Stages: 1, Cool Stages: 1" }, { "Value": 19005697, "Label": "HVAC: Normal, Aux Stages: 2, Aux Setup: Elec, Heat Pump Stages: 1, Cool Stages: 1" }, { "Value": 35782913, "Label": "HVAC: Heat Pump, Aux Stages: 2, Aux Setup: Elec, Heat Pump Stages: 1, Cool Stages: 1" }, { "Value": 17891841, "Label": "HVAC: Normal, Aux Stages: 1, Aux Setup: Gas, Heat Pump Stages: 2, Cool Stages: 1" }, { "Value": 34669057, "Label": "HVAC: Heat Pump, Aux Stages: 1, Aux Setup: Gas, Heat Pump Stages: 2, Cool Stages: 1" }, { "Value": 18940417, "Label": "HVAC: Normal, Aux Stages: 2, Aux Setup: Gas, Heat Pump Stages: 2, Cool Stages: 1" }, { "Value": 35717633, "Label": "HVAC: Heat Pump, Aux Stages: 2, Aux Setup: Gas, Heat Pump Stages: 2, Cool Stages: 1" }, { "Value": 17957377, "Label": "HVAC: Normal, Aux Stages: 1, Aux Setup: Elec, Heat Pump Stages: 2, Cool Stages: 1" }, { "Value": 34734593, "Label": "HVAC: Heat Pump, Aux Stages: 1, Aux Setup: Elec, Heat Pump Stages: 2, Cool Stages: 1" }, { "Value": 19005953, "Label": "HVAC: Normal, Aux Stages: 2, Aux Setup: Elec, Heat Pump Stages: 2, Cool Stages: 1" }, { "Value": 35783169, "Label": "HVAC: Heat Pump, Aux Stages: 2, Aux Setup: Elec, Heat Pump Stages: 2, Cool Stages: 1" }, { "Value": 17891586, "Label": "HVAC: Normal, Aux Stages: 1, Aux Setup: Gas, Heat Pump Stages: 1, Cool Stages: 2" }, { "Value": 34668802, "Label": "HVAC: Heat Pump, Aux Stages: 1, Aux Setup: Gas, Heat Pump Stages: 1, Cool Stages: 2" }, { "Value": 18940162, "Label": "HVAC: Normal, Aux Stages: 2, Aux Setup: Gas, Heat Pump Stages: 1, Cool Stages: 2" }, { "Value": 35717378, "Label": "HVAC: Heat Pump, Aux Stages: 2, Aux Setup: Gas, Heat Pump Stages: 1, Cool Stages: 2" }, { "Value": 17957122, "Label": "HVAC: Normal, Aux Stages: 1, Aux Setup: Elec, Heat Pump Stages: 1, Cool Stages: 2" }, { "Value": 34734338, "Label": "HVAC: Heat Pump, Aux Stages: 1, Aux Setup: Elec, Heat Pump Stages: 1, Cool Stages: 2" }, { "Value": 19005698, "Label": "HVAC: Normal, Aux Stages: 2, Aux Setup: Elec, Heat Pump Stages: 1, Cool Stages: 2" }, { "Value": 35782914, "Label": "HVAC: Heat Pump, Aux Stages: 2, Aux Setup: Elec, Heat Pump Stages: 1, Cool Stages: 2" }, { "Value": 17891842, "Label": "HVAC: Normal, Aux Stages: 1, Aux Setup: Gas, Heat Pump Stages: 2, Cool Stages: 2" }, { "Value": 34669058, "Label": "HVAC: Heat Pump, Aux Stages: 1, Aux Setup: Gas, Heat Pump Stages: 2, Cool Stages: 2" }, { "Value": 18940418, "Label": "HVAC: Normal, Aux Stages: 2, Aux Setup: Gas, Heat Pump Stages: 2, Cool Stages: 2" }, { "Value": 35717634, "Label": "HVAC: Heat Pump, Aux Stages: 2, Aux Setup: Gas, Heat Pump Stages: 2, Cool Stages: 2" }, { "Value": 17957378, "Label": "HVAC: Normal, Aux Stages: 1, Aux Setup: Elec, Heat Pump Stages: 2, Cool Stages: 2" }, { "Value": 34734594, "Label": "HVAC: Heat Pump, Aux Stages: 1, Aux Setup: Elec, Heat Pump Stages: 2, Cool Stages: 2" }, { "Value": 19005954, "Label": "HVAC: Normal, Aux Stages: 2, Aux Setup: Elec, Heat Pump Stages: 2, Cool Stages: 2" }, { "Value": 35783170, "Label": "HVAC: Heat Pump, Aux Stages: 2, Aux Setup: Elec, Heat Pump Stages: 2, Cool Stages: 2" } ], "Selected": "HVAC: Normal, Aux Stages: 1, Aux Setup: Gas, Heat Pump Stages: 1, Cool Stages: 1", "Selected_id": 17891585 }, "Units": "", "Min": 0, "Max": 2147483647, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 2, "Node": 7, "Genre": "Config", "Help": "Bits 0 - 7 -> HVAC Setup: Normal (0x01) or Heat Pump (0x02) Bits 8 - 11 -> Number of Auxiliary Stages (Heat Pump) / Number of Heat Stages (Normal) Bits 12 - 15 -> Aux Setup: Gas (0x01) or Electric (0x02) Bits 16 - 23 -> Number of Heat Pump Stages Bits 24 - 31 -> Number of Cool Stages", "ValueIDKey": 562950081085460, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588264893} -OpenZWave/1/node/7/instance/1/commandclass/112/value/844425057796116/,{ "Label": "Utility Lock", "Value": { "List": [ { "Value": 0, "Label": "Disable" }, { "Value": 1, "Label": "Enable" } ], "Selected": "Disable", "Selected_id": 0 }, "Units": "", "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 3, "Node": 7, "Genre": "Config", "Help": "The Utility Lock Configuration Set command enables or disables the utility lock. If the utility lock is enabled, the setpoint cannot be modified directly via the thermostat screen.", "ValueIDKey": 844425057796116, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588264893} -OpenZWave/1/node/7/instance/1/commandclass/112/value/1125900034506772/,{ "Label": "C-Wire/Battery Status", "Value": { "List": [ { "Value": 1, "Label": "C-Wire" }, { "Value": 2, "Label": "Battery" } ], "Selected": "C-Wire", "Selected_id": 1 }, "Units": "", "Min": 1, "Max": 2, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 4, "Node": 7, "Genre": "Config", "Help": "1 -> C-Wire 2 -> Battery", "ValueIDKey": 1125900034506772, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588264893} -OpenZWave/1/node/7/instance/1/commandclass/112/value/1407375011217428/,{ "Label": "Humidity Reporting Threshold", "Value": { "List": [ { "Value": 0, "Label": "Disabled" }, { "Value": 1, "Label": "3% RH" }, { "Value": 2, "Label": "5% RH" }, { "Value": 3, "Label": "10% RH" } ], "Selected": "5% RH", "Selected_id": 2 }, "Units": "", "Min": 0, "Max": 3, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 5, "Node": 7, "Genre": "Config", "Help": "The Humidity Reporting Threshold Configuration Set Command sets the reporting threshold for changes in the ambient humidity as detected by the thermostat.", "ValueIDKey": 1407375011217428, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588264893} -OpenZWave/1/node/7/instance/1/commandclass/112/value/1688849987928084/,{ "Label": "Auxiliary/Emergency heat", "Value": { "List": [ { "Value": 0, "Label": "Disabled" }, { "Value": 1, "Label": "Enabled" } ], "Selected": "Disabled", "Selected_id": 0 }, "Units": "", "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 6, "Node": 7, "Genre": "Config", "Help": "The Auxiliary/Emergency configuration command enables or disables auxiliary / emergency heating in the thermostat. Auxiliary / emergency heating is only available if the thermostat is configured in heat pump mode and with at least one stage of auxiliary heating. This command enables auxiliary / emergency heating when the thermostat is in Auto mode. The Thermostat Set Mode command with mode Auxiliary/Emergency Heat will enable emergency heating but only if the thermostat is in Heat mode. This command should only be used on thermsotats that support Auxiliary/Emergency Heat thermostat mode.", "ValueIDKey": 1688849987928084, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588264893} -OpenZWave/1/node/7/instance/1/commandclass/112/value/1970324964638740/,{ "Label": "Thermostat Swing Temperature", "Value": { "List": [ { "Value": 1, "Label": "0.5F" }, { "Value": 2, "Label": "1.0F" }, { "Value": 3, "Label": "1.5F" }, { "Value": 4, "Label": "2.0F" }, { "Value": 5, "Label": "2.5F" }, { "Value": 6, "Label": "3.0F" }, { "Value": 7, "Label": "3.5F" }, { "Value": 8, "Label": "4.0F" } ], "Selected": "1.0F", "Selected_id": 2 }, "Units": "", "Min": 1, "Max": 8, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 7, "Node": 7, "Genre": "Config", "Help": "The Temperate Swing (HVAC cycling rate) is the desired variance in temperature between the thermostat setting and the room temperature required before the heating or cooling system will turn on.", "ValueIDKey": 1970324964638740, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588264893} -OpenZWave/1/node/7/instance/1/commandclass/112/value/2251799941349396/,{ "Label": "Thermostat Differential Temperature", "Value": { "List": [ { "Value": 4, "Label": "2.0F Heat" }, { "Value": 6, "Label": "3.0F Heat" }, { "Value": 8, "Label": "4.0F Heat" }, { "Value": 10, "Label": "5.0F Heat" }, { "Value": 12, "Label": "6.0F Heat" }, { "Value": 260, "Label": "2.0F Cool" }, { "Value": 262, "Label": "3.0F Cool" }, { "Value": 264, "Label": "4.0F Cool" }, { "Value": 266, "Label": "5.0F Cool" }, { "Value": 268, "Label": "6.0F Cool" } ], "Selected": "2.0F Heat", "Selected_id": 4 }, "Units": "F", "Min": 2, "Max": 32767, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 8, "Node": 7, "Genre": "Config", "Help": "(Set Only) The Thermostat Differential Temperature configuration command sets the differential temperature for multi-stage HVAC systems. The differential temperature delta defines when the thermostat will turn on additional stages. There are two differential temperatures, one for multistage cool systems and one for multistage heat systems. If the thermostat is not configured for multistage HVAC systems then these parameters have no effect.", "ValueIDKey": 2251799941349396, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588264893} -OpenZWave/1/node/7/instance/1/commandclass/112/value/2533274918060052/,{ "Label": "Thermostat Recovery Mode", "Value": { "List": [ { "Value": 1, "Label": "Fast" }, { "Value": 2, "Label": "Economy" } ], "Selected": "Economy", "Selected_id": 2 }, "Units": "", "Min": 1, "Max": 2, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 9, "Node": 7, "Genre": "Config", "Help": "The Thermostat Recovery Mode configuration command sets the HVAC recovery mode type. The recovery mode determines when additional HVAC stages are turned off as the ambient temperature returns to the target temperature. If the recovery mode is set to economy, the thermostat will turn off additional HVAC stages when the ambient temperature reaches the target temperature plus/minus the differential temperature. If the recovery mode is set to fast, the thermostat will leave all stages on (assuming they were already on) until the ambient temperature reaches the target temperature.", "ValueIDKey": 2533274918060052, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588264893} -OpenZWave/1/node/7/instance/1/commandclass/112/value/2814749894770710/,{ "Label": "Temperature Reporting Filter", "Value": 124, "Units": "F", "Min": 0, "Max": 124, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 10, "Node": 7, "Genre": "Config", "Help": "The Temperature Reporting Filter configuration command sets upper and lower bounds of the ambient temperature reporting. The thermostat won't report ambient temperature changes if the ambient temperature falls between these bounds. For example, if the upper bound is 80F and the lower bound is 60F, the thermostat will not send SENSOR_MULTI_LEVEL_REPORTS for ambient temperature values between 60F and 80F. The thermostat will only send ambient temperature changes if the thermostat has been added to an association group (see Command Class Association) and the temperature reporting threshold is non-zero (see Temperature Reporting Threshold). Input in hexadecimal only like so: 0x09 0x05 0x09 0x0A. It must always have four 1 byte sized numbers. The first two bytes control the lower temperature bound for the Temperature Reporting Filter the last two control the upper temperature bound. The first byte in the byte pair always refers to temperature scale (Celsius 0x01 or Fahrenheit 0x09). While the second byte in each byte pair is the bound temperature. The max/min temp you can use is 127 degrees. To convert decimal to hex goto: https://www.binaryhexconverter.com/decimal-to-hex-converter or you can use the built in Windows calculator program in Programmer mode. If you mess up your thermostat copy and paste 0x09 0x00 0x09 0x00 (for a F Thermostat) or 0x01 0x00 0x01 0x00 (for a C Thermostat). This will remove any bounds.", "ValueIDKey": 2814749894770710, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588264893} -OpenZWave/1/node/7/instance/1/commandclass/112/value/3096224871481364/,{ "Label": "Simple UI Mode", "Value": { "List": [ { "Value": 0, "Label": "Enable" }, { "Value": 1, "Label": "Disable" } ], "Selected": "Disable", "Selected_id": 1 }, "Units": "", "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 11, "Node": 7, "Genre": "Config", "Help": "If the value is set to Disable then Normal Mode is enabled. If the value is set to Enable then Simple Mode is enabled.", "ValueIDKey": 3096224871481364, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588264893} -OpenZWave/1/node/7/instance/1/commandclass/112/value/3377699848192017/,{ "Label": "Multicast", "Value": 0, "Units": "", "Min": 0, "Max": 1, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 12, "Node": 7, "Genre": "Config", "Help": "If set to 0, multicast is disabled, if set to 1, will enable the multicast.", "ValueIDKey": 3377699848192017, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588264893} -OpenZWave/1/node/7/instance/1/commandclass/66/,{ "Instance": 1, "CommandClassId": 66, "CommandClass": "COMMAND_CLASS_THERMOSTAT_OPERATING_STATE", "TimeStamp": 1588264893} -OpenZWave/1/node/7/instance/1/commandclass/66/value/122716183/,{ "Label": "Operating State", "Value": "Idle", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_THERMOSTAT_OPERATING_STATE", "Index": 0, "Node": 7, "Genre": "User", "Help": "Set the Thermostat Operating State", "ValueIDKey": 122716183, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588264893} -OpenZWave/1/node/7/instance/1/commandclass/69/,{ "Instance": 1, "CommandClassId": 69, "CommandClass": "COMMAND_CLASS_THERMOSTAT_FAN_STATE", "TimeStamp": 1588264893} -OpenZWave/1/node/7/instance/1/commandclass/69/value/122765335/,{ "Label": "Fan State", "Value": "Idle", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_THERMOSTAT_FAN_STATE", "Index": 0, "Node": 7, "Genre": "User", "Help": "Set the Fan State", "ValueIDKey": 122765335, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588264893} -OpenZWave/1/node/7/instance/1/commandclass/94/,{ "Instance": 1, "CommandClassId": 94, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "TimeStamp": 1588264893} -OpenZWave/1/node/7/instance/1/commandclass/94/value/131563537/,{ "Label": "ZWave+ Version", "Value": 1, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 0, "Node": 7, "Genre": "System", "Help": "ZWave+ Version Supported on the Device", "ValueIDKey": 131563537, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueChanged", "TimeStamp": 1588264894} -OpenZWave/1/node/7/instance/1/commandclass/94/value/281475108274198/,{ "Label": "InstallerIcon", "Value": 4608, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 1, "Node": 7, "Genre": "System", "Help": "Icon File to use for the Installer Application", "ValueIDKey": 281475108274198, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueChanged", "TimeStamp": 1588264894} -OpenZWave/1/node/7/instance/1/commandclass/94/value/562950084984854/,{ "Label": "UserIcon", "Value": 4608, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 2, "Node": 7, "Genre": "System", "Help": "Icon File to use for the User Application", "ValueIDKey": 562950084984854, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueChanged", "TimeStamp": 1588264894} -OpenZWave/1/node/7/instance/1/commandclass/114/,{ "Instance": 1, "CommandClassId": 114, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "TimeStamp": 1588264893} -OpenZWave/1/node/7/instance/1/commandclass/114/value/131891219/,{ "Label": "Loaded Config Revision", "Value": 0, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 0, "Node": 7, "Genre": "System", "Help": "Revision of the Config file currently loaded", "ValueIDKey": 131891219, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588264893} -OpenZWave/1/node/7/instance/1/commandclass/114/value/281475108601875/,{ "Label": "Config File Revision", "Value": 6, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 1, "Node": 7, "Genre": "System", "Help": "Revision of the Config file on the File System", "ValueIDKey": 281475108601875, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588264893} -OpenZWave/1/node/7/instance/1/commandclass/114/value/562950085312531/,{ "Label": "Latest Available Config File Revision", "Value": 6, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 2, "Node": 7, "Genre": "System", "Help": "Latest Revision of the Config file available for download", "ValueIDKey": 562950085312531, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588264893} -OpenZWave/1/node/7/instance/1/commandclass/114/value/844425062023191/,{ "Label": "Device ID", "Value": "", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 3, "Node": 7, "Genre": "System", "Help": "Manufacturer Specific Device ID/Model", "ValueIDKey": 844425062023191, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588264893} -OpenZWave/1/node/7/instance/1/commandclass/114/value/1125900038733847/,{ "Label": "Serial Number", "Value": "", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 4, "Node": 7, "Genre": "System", "Help": "Device Serial Number", "ValueIDKey": 1125900038733847, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588264893} -OpenZWave/1/node/7/instance/1/commandclass/115/,{ "Instance": 1, "CommandClassId": 115, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "TimeStamp": 1588264893} -OpenZWave/1/node/7/instance/1/commandclass/115/value/131907604/,{ "Label": "Powerlevel", "Value": { "List": [ { "Value": 0, "Label": "Normal" }, { "Value": 1, "Label": "-1dB" }, { "Value": 2, "Label": "-2dB" }, { "Value": 3, "Label": "-3dB" }, { "Value": 4, "Label": "-4dB" }, { "Value": 5, "Label": "-5dB" }, { "Value": 6, "Label": "-6dB" }, { "Value": 7, "Label": "-7dB" }, { "Value": 8, "Label": "-8dB" }, { "Value": 9, "Label": "-9dB" } ], "Selected": "Normal", "Selected_id": 0 }, "Units": "dB", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 0, "Node": 7, "Genre": "System", "Help": "Output RF PowerLevel", "ValueIDKey": 131907604, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588264893} -OpenZWave/1/node/7/instance/1/commandclass/115/value/281475108618257/,{ "Label": "Timeout", "Value": 0, "Units": "seconds", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 1, "Node": 7, "Genre": "System", "Help": "Timeout till the PowerLevel is reset to Normal", "ValueIDKey": 281475108618257, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588264893} -OpenZWave/1/node/7/instance/1/commandclass/115/value/562950085328920/,{ "Label": "Set Powerlevel", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 2, "Node": 7, "Genre": "System", "Help": "Apply the Output PowerLevel and Timeout Values", "ValueIDKey": 562950085328920, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588264893} -OpenZWave/1/node/7/instance/1/commandclass/115/value/844425062039569/,{ "Label": "Test Node", "Value": 0, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 3, "Node": 7, "Genre": "System", "Help": "Node to Perform a test against", "ValueIDKey": 844425062039569, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588264893} -OpenZWave/1/node/7/instance/1/commandclass/115/value/1125900038750228/,{ "Label": "Test Powerlevel", "Value": { "List": [ { "Value": 0, "Label": "Normal" }, { "Value": 1, "Label": "-1dB" }, { "Value": 2, "Label": "-2dB" }, { "Value": 3, "Label": "-3dB" }, { "Value": 4, "Label": "-4dB" }, { "Value": 5, "Label": "-5dB" }, { "Value": 6, "Label": "-6dB" }, { "Value": 7, "Label": "-7dB" }, { "Value": 8, "Label": "-8dB" }, { "Value": 9, "Label": "-9dB" } ], "Selected": "Normal", "Selected_id": 0 }, "Units": "dB", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 4, "Node": 7, "Genre": "System", "Help": "PowerLevel to use for the Test", "ValueIDKey": 1125900038750228, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588264893} -OpenZWave/1/node/7/instance/1/commandclass/115/value/1407375015460886/,{ "Label": "Frame Count", "Value": 0, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 5, "Node": 7, "Genre": "System", "Help": "How Many Messages to send to the Node for the Test", "ValueIDKey": 1407375015460886, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588264893} -OpenZWave/1/node/7/instance/1/commandclass/115/value/1688849992171544/,{ "Label": "Test", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 6, "Node": 7, "Genre": "System", "Help": "Perform a PowerLevel Test against the a Node", "ValueIDKey": 1688849992171544, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588264893} -OpenZWave/1/node/7/instance/1/commandclass/115/value/1970324968882200/,{ "Label": "Report", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 7, "Node": 7, "Genre": "System", "Help": "Get the results of the latest PowerLevel Test against a Node", "ValueIDKey": 1970324968882200, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588264893} -OpenZWave/1/node/7/instance/1/commandclass/115/value/2251799945592852/,{ "Label": "Test Status", "Value": { "List": [ { "Value": 0, "Label": "Failed" }, { "Value": 1, "Label": "Success" }, { "Value": 2, "Label": "In Progress" } ], "Selected": "Failed", "Selected_id": 0 }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 8, "Node": 7, "Genre": "System", "Help": "The Current Status of the last PowerNode Test Executed", "ValueIDKey": 2251799945592852, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588264893} -OpenZWave/1/node/7/instance/1/commandclass/115/value/2533274922303510/,{ "Label": "Acked Frames", "Value": 0, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 9, "Node": 7, "Genre": "System", "Help": "Number of Messages successfully Acked by the Target Node", "ValueIDKey": 2533274922303510, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588264893} -OpenZWave/1/node/7/instance/1/commandclass/128/,{ "Instance": 1, "CommandClassId": 128, "CommandClass": "COMMAND_CLASS_BATTERY", "TimeStamp": 1588264893} -OpenZWave/1/node/7/instance/1/commandclass/128/value/123731985/,{ "Label": "Battery Level", "Value": 65, "Units": "%", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_BATTERY", "Index": 0, "Node": 7, "Genre": "User", "Help": "Current Battery Level", "ValueIDKey": 123731985, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueChanged", "TimeStamp": 1588264908} -OpenZWave/1/node/7/instance/1/commandclass/129/,{ "Instance": 1, "CommandClassId": 129, "CommandClass": "COMMAND_CLASS_CLOCK", "TimeStamp": 1588264893} -OpenZWave/1/node/7/instance/1/commandclass/129/value/123748372/,{ "Label": "Day", "Value": { "List": [ { "Value": 1, "Label": "Monday" }, { "Value": 2, "Label": "Tuesday" }, { "Value": 3, "Label": "Wednesday" }, { "Value": 4, "Label": "Thursday" }, { "Value": 5, "Label": "Friday" }, { "Value": 6, "Label": "Saturday" }, { "Value": 7, "Label": "Sunday" } ], "Selected": "Thursday", "Selected_id": 4 }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CLOCK", "Index": 0, "Node": 7, "Genre": "User", "Help": "Day of Week", "ValueIDKey": 123748372, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueChanged", "TimeStamp": 1588264908} -OpenZWave/1/node/7/instance/1/commandclass/129/value/281475100459025/,{ "Label": "Hour", "Value": 2, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CLOCK", "Index": 1, "Node": 7, "Genre": "User", "Help": "Hour", "ValueIDKey": 281475100459025, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueChanged", "TimeStamp": 1588264908} -OpenZWave/1/node/7/instance/1/commandclass/129/value/562950077169681/,{ "Label": "Minute", "Value": 17, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CLOCK", "Index": 2, "Node": 7, "Genre": "User", "Help": "Minute", "ValueIDKey": 562950077169681, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueChanged", "TimeStamp": 1588264908} -OpenZWave/1/node/7/instance/1/commandclass/134/,{ "Instance": 1, "CommandClassId": 134, "CommandClass": "COMMAND_CLASS_VERSION", "TimeStamp": 1588264893} -OpenZWave/1/node/7/instance/1/commandclass/134/value/132218903/,{ "Label": "Library Version", "Value": "3", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 0, "Node": 7, "Genre": "System", "Help": "Z-Wave Library Version", "ValueIDKey": 132218903, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueChanged", "TimeStamp": 1588264905} -OpenZWave/1/node/7/instance/1/commandclass/134/value/281475108929559/,{ "Label": "Protocol Version", "Value": "3.83", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 1, "Node": 7, "Genre": "System", "Help": "Z-Wave Protocol Version", "ValueIDKey": 281475108929559, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueChanged", "TimeStamp": 1588264905} -OpenZWave/1/node/7/instance/1/commandclass/134/value/562950085640215/,{ "Label": "Application Version", "Value": "10.00", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 2, "Node": 7, "Genre": "System", "Help": "Application Version", "ValueIDKey": 562950085640215, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueChanged", "TimeStamp": 1588264905} -OpenZWave/1/node/7/instance/1/commandclass/135/,{ "Instance": 1, "CommandClassId": 135, "CommandClass": "COMMAND_CLASS_INDICATOR", "TimeStamp": 1588264893} -OpenZWave/1/node/7/instance/1/commandclass/135/value/123846673/,{ "Label": "Indicator", "Value": 0, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_INDICATOR", "Index": 0, "Node": 7, "Genre": "User", "Help": "Current Indicator State", "ValueIDKey": 123846673, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588264893} -OpenZWave/1/node/7/instance/1/commandclass/49/,{ "Instance": 1, "CommandClassId": 49, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "TimeStamp": 1588264893} -OpenZWave/1/node/7/instance/1/commandclass/49/value/281475099148306/,{ "Label": "Instance 1: Air Temperature", "Value": 73.5, "Units": "F", "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "Index": 1, "Node": 7, "Genre": "User", "Help": "Air Temperature Sensor Value", "ValueIDKey": 281475099148306, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueChanged", "TimeStamp": 1588266231} -OpenZWave/1/node/7/instance/1/commandclass/49/value/72057594168754196/,{ "Label": "Instance 1: Air Temperature Units", "Value": { "List": [ { "Value": 1, "Label": "Fahrenheit" } ], "Selected": "Fahrenheit", "Selected_id": 1 }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "Index": 256, "Node": 7, "Genre": "System", "Help": "Air Temperature Sensor Available Units", "ValueIDKey": 72057594168754196, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588264893} -OpenZWave/1/node/7/instance/1/commandclass/49/value/1407375005990930/,{ "Label": "Instance 1: Humidity", "Value": 55.0, "Units": "%", "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "Index": 5, "Node": 7, "Genre": "User", "Help": "Humidity Sensor Value", "ValueIDKey": 1407375005990930, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueChanged", "TimeStamp": 1588266231} -OpenZWave/1/node/7/instance/1/commandclass/49/value/73183494075596820/,{ "Label": "Instance 1: Humidity Units", "Value": { "List": [ { "Value": 0, "Label": "Percent" } ], "Selected": "Percent", "Selected_id": 0 }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "Index": 260, "Node": 7, "Genre": "System", "Help": "Humidity Sensor Available Units", "ValueIDKey": 73183494075596820, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588264893} -OpenZWave/1/node/7/instance/1/commandclass/64/,{ "Instance": 1, "CommandClassId": 64, "CommandClass": "COMMAND_CLASS_THERMOSTAT_MODE", "TimeStamp": 1588264894} -OpenZWave/1/node/7/instance/1/commandclass/64/value/122683412/,{ "Label": "Mode", "Value": { "List": [ { "Value": 0, "Label": "Off" }, { "Value": 1, "Label": "Heat" }, { "Value": 2, "Label": "Cool" }, { "Value": 3, "Label": "Auto" }, { "Value": 11, "Label": "Heat Econ" }, { "Value": 12, "Label": "Cool Econ" } ], "Selected": "Heat", "Selected_id": 1 }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_THERMOSTAT_MODE", "Index": 0, "Node": 7, "Genre": "User", "Help": "Set the Thermostat Mode", "ValueIDKey": 122683412, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588264894} -OpenZWave/1/node/7/instance/1/commandclass/67/,{ "Instance": 1, "CommandClassId": 67, "CommandClass": "COMMAND_CLASS_THERMOSTAT_SETPOINT", "TimeStamp": 1588264894} -OpenZWave/1/node/7/instance/1/commandclass/67/value/281475099443218/,{ "Label": "Heating 1", "Value": 70.0, "Units": "C", "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_THERMOSTAT_SETPOINT", "Index": 1, "Node": 7, "Genre": "User", "Help": "Set the Thermostat Setpoint Heating 1", "ValueIDKey": 281475099443218, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueChanged", "TimeStamp": 1588264906} -OpenZWave/1/node/7/instance/1/commandclass/67/value/562950076153874/,{ "Label": "Cooling 1", "Value": 78.0, "Units": "C", "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_THERMOSTAT_SETPOINT", "Index": 2, "Node": 7, "Genre": "User", "Help": "Set the Thermostat Setpoint Cooling 1", "ValueIDKey": 562950076153874, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueChanged", "TimeStamp": 1588264906} -OpenZWave/1/node/7/instance/1/commandclass/67/value/3096224866549778/,{ "Label": "Heating Econ", "Value": 62.0, "Units": "C", "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_THERMOSTAT_SETPOINT", "Index": 11, "Node": 7, "Genre": "User", "Help": "Set the Thermostat Setpoint Heating Econ", "ValueIDKey": 3096224866549778, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueChanged", "TimeStamp": 1588264906} -OpenZWave/1/node/7/instance/1/commandclass/67/value/3377699843260434/,{ "Label": "Cooling Econ", "Value": 85.0, "Units": "C", "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_THERMOSTAT_SETPOINT", "Index": 12, "Node": 7, "Genre": "User", "Help": "Set the Thermostat Setpoint Cooling Econ", "ValueIDKey": 3377699843260434, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueChanged", "TimeStamp": 1588264906} -OpenZWave/1/node/7/instance/1/commandclass/68/,{ "Instance": 1, "CommandClassId": 68, "CommandClass": "COMMAND_CLASS_THERMOSTAT_FAN_MODE", "TimeStamp": 1588264894} -OpenZWave/1/node/7/instance/1/commandclass/68/value/122748948/,{ "Label": "Fan Mode", "Value": { "List": [ { "Value": 0, "Label": "Auto Low" }, { "Value": 1, "Label": "On Low" } ], "Selected": "Auto Low", "Selected_id": 0 }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_THERMOSTAT_FAN_MODE", "Index": 0, "Node": 7, "Genre": "User", "Help": "Set the Fan Mode", "ValueIDKey": 122748948, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588264894} -OpenZWave/1/node/7/instance/2/,{ "Instance": 2, "TimeStamp": 1588264894} -OpenZWave/1/node/7/instance/2/commandclass/49/,{ "Instance": 2, "CommandClassId": 49, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "TimeStamp": 1588264894} -OpenZWave/1/node/7/instance/2/commandclass/49/value/281475099148322/,{ "Label": "Instance 2: Air Temperature", "Value": 72.5, "Units": "F", "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 2, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "Index": 1, "Node": 7, "Genre": "User", "Help": "Air Temperature Sensor Value", "ValueIDKey": 281475099148322, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueChanged", "TimeStamp": 1588264906} -OpenZWave/1/node/7/instance/2/commandclass/49/value/72057594168754212/,{ "Label": "Instance 2: Air Temperature Units", "Value": { "List": [ { "Value": 1, "Label": "Fahrenheit" } ], "Selected": "Fahrenheit", "Selected_id": 1 }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 2, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "Index": 256, "Node": 7, "Genre": "System", "Help": "Air Temperature Sensor Available Units", "ValueIDKey": 72057594168754212, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588264894} -OpenZWave/1/node/7/instance/2/commandclass/49/value/1407375005990946/,{ "Label": "Instance 2: Humidity", "Value": 56.0, "Units": "%", "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 2, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "Index": 5, "Node": 7, "Genre": "User", "Help": "Humidity Sensor Value", "ValueIDKey": 1407375005990946, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueChanged", "TimeStamp": 1588264907} -OpenZWave/1/node/7/instance/2/commandclass/49/value/73183494075596836/,{ "Label": "Instance 2: Humidity Units", "Value": { "List": [ { "Value": 0, "Label": "Percent" } ], "Selected": "Percent", "Selected_id": 0 }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 2, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "Index": 260, "Node": 7, "Genre": "System", "Help": "Humidity Sensor Available Units", "ValueIDKey": 73183494075596836, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588264894} -OpenZWave/1/node/7/association/1/,{ "Name": "Reporting", "Help": "", "MaxAssociations": 2, "Members": [ "1.0" ], "TimeStamp": 1588264906} -OpenZWave/1/node/8/,{ "NodeID": 8, "NodeQueryStage": "Complete", "isListening": false, "isFlirs": false, "isBeaming": true, "isRouting": true, "isSecurityv1": false, "isZWavePlus": false, "isNIFRecieved": true, "isAwake": true, "isFailed": false, "MetaData": { "OZWInfoURL": "http://www.openzwave.com/device-database/0002:0003:0005", "ZWAProductURL": "https://products.z-wavealliance.org/products/1507/", "ProductPic": "images/danfoss/z.png", "Description": "Electronic radiator thermostat", "ProductManualURL": "", "ProductPageURL": "http://heating.consumers.danfoss.com/xxTypex/585379.html", "InclusionHelp": "", "ExclusionHelp": "", "ResetHelp": "", "WakeupHelp": "", "ProductSupportURL": "", "Frequency": "CEPT (Europe)", "Name": "Danfoss Living Connect Z v1.06 014G0013", "ProductPicBase64": "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" }, "Event": "nodeQueriesComplete", "TimeStamp": 1594159718, "NodeManufacturerName": "Danfoss", "NodeProductName": "Z Thermostat 014G0013", "NodeBasicString": "Routing Slave", "NodeBasic": 4, "NodeGenericString": "Thermostat", "NodeGeneric": 8, "NodeSpecificString": "Setpoint Thermostat", "NodeSpecific": 4, "NodeManufacturerID": "0x0002", "NodeProductType": "0x0005", "NodeProductID": "0x0004", "NodeBaudRate": 40000, "NodeVersion": 4, "NodeGroups": 0, "NodeName": "", "NodeLocation": "", "NodeDeviceTypeString": "Unknown Type (0x0000)", "NodeDeviceType": 0, "NodeRole": 0, "NodeRoleString": "Central Controller", "NodePlusType": 0, "NodePlusTypeString": "Z-Wave+ node", "Neighbors": [ 1 ], "Neighbors": [ 1 ]} -OpenZWave/1/node/8/instance/1/commandclass/70/value/2251799953244180/,{ "Label": "Override State", "Value": { "List": [ { "Value": 0, "Label": "None" }, { "Value": 1, "Label": "Temporary" }, { "Value": 2, "Label": "Permanent" } ], "Selected": "None", "Selected_id": 0 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CLIMATE_CONTROL_SCHEDULE", "Index": 8, "Node": 8, "Genre": "User", "Help": "Override Schedule", "ValueIDKey": 2251799953244180, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1594159422} -OpenZWave/1/node/8/instance/1/commandclass/70/value/2533274929954833/,{ "Label": "Override Setback", "Value": 127, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CLIMATE_CONTROL_SCHEDULE", "Index": 9, "Node": 8, "Genre": "User", "Help": "Override Setback", "ValueIDKey": 2533274929954833, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1594159422} -OpenZWave/1/node/8/instance/1/commandclass/70/value/281475116269589/,{ "Label": "Monday", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Schedule", "Instance": 1, "CommandClass": "COMMAND_CLASS_CLIMATE_CONTROL_SCHEDULE", "Index": 1, "Node": 8, "Genre": "User", "Help": "Schedule for Monday", "ValueIDKey": 281475116269589, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1594159418} -OpenZWave/1/node/8/instance/1/commandclass/70/value/562950092980245/,{ "Label": "Tuesday", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Schedule", "Instance": 1, "CommandClass": "COMMAND_CLASS_CLIMATE_CONTROL_SCHEDULE", "Index": 2, "Node": 8, "Genre": "User", "Help": "Schedule for Tuesday", "ValueIDKey": 562950092980245, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1594159418} -OpenZWave/1/node/8/instance/1/commandclass/70/value/844425069690901/,{ "Label": "Wednesday", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Schedule", "Instance": 1, "CommandClass": "COMMAND_CLASS_CLIMATE_CONTROL_SCHEDULE", "Index": 3, "Node": 8, "Genre": "User", "Help": "Schedule for Wednesday", "ValueIDKey": 844425069690901, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1594159418} -OpenZWave/1/node/8/instance/1/commandclass/70/value/1125900046401557/,{ "Label": "Thursday", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Schedule", "Instance": 1, "CommandClass": "COMMAND_CLASS_CLIMATE_CONTROL_SCHEDULE", "Index": 4, "Node": 8, "Genre": "User", "Help": "Schedule for Thursday", "ValueIDKey": 1125900046401557, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1594159418} -OpenZWave/1/node/8/instance/1/commandclass/70/value/1407375023112213/,{ "Label": "Friday", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Schedule", "Instance": 1, "CommandClass": "COMMAND_CLASS_CLIMATE_CONTROL_SCHEDULE", "Index": 5, "Node": 8, "Genre": "User", "Help": "Schedule for Friday", "ValueIDKey": 1407375023112213, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1594159418} -OpenZWave/1/node/8/instance/1/commandclass/70/value/1688849999822869/,{ "Label": "Saturday", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Schedule", "Instance": 1, "CommandClass": "COMMAND_CLASS_CLIMATE_CONTROL_SCHEDULE", "Index": 6, "Node": 8, "Genre": "User", "Help": "Schedule for Saturday", "ValueIDKey": 1688849999822869, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1594159418} -OpenZWave/1/node/8/instance/1/commandclass/70/value/1970324976533525/,{ "Label": "Sunday", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Schedule", "Instance": 1, "CommandClass": "COMMAND_CLASS_CLIMATE_CONTROL_SCHEDULE", "Index": 7, "Node": 8, "Genre": "User", "Help": "Schedule for Sunday", "ValueIDKey": 1970324976533525, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1594159418} -OpenZWave/1/node/8/instance/1/commandclass/70/,{ "Instance": 1, "CommandClassId": 70, "CommandClass": "COMMAND_CLASS_CLIMATE_CONTROL_SCHEDULE", "CommandClassVersion": 1, "TimeStamp": 1594159418} -OpenZWave/1/node/8/instance/1/commandclass/117/value/148717588/,{ "Label": "Protection", "Value": { "List": [ { "Value": 0, "Label": "Unprotected" }, { "Value": 1, "Label": "Protection by Sequence" }, { "Value": 2, "Label": "No Operation Possible" } ], "Selected": "Unprotected", "Selected_id": 0 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_PROTECTION", "Index": 0, "Node": 8, "Genre": "System", "Help": "Protect a device against unintentional control", "ValueIDKey": 148717588, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1594159718} -OpenZWave/1/node/8/instance/1/commandclass/117/,{ "Instance": 1, "CommandClassId": 117, "CommandClass": "COMMAND_CLASS_PROTECTION", "CommandClassVersion": 1, "TimeStamp": 1594159418} -OpenZWave/1/node/8/instance/1/commandclass/128/value/140509201/,{ "Label": "Battery Level", "Value": 79, "Units": "%", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_BATTERY", "Index": 0, "Node": 8, "Genre": "User", "Help": "Current Battery Level", "ValueIDKey": 140509201, "ReadOnly": true, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1594159422} -OpenZWave/1/node/8/instance/1/commandclass/128/,{ "Instance": 1, "CommandClassId": 128, "CommandClass": "COMMAND_CLASS_BATTERY", "CommandClassVersion": 1, "TimeStamp": 1594159418} -OpenZWave/1/node/8/instance/1/commandclass/129/value/140525588/,{ "Label": "Day", "Value": { "List": [ { "Value": 1, "Label": "Monday" }, { "Value": 2, "Label": "Tuesday" }, { "Value": 3, "Label": "Wednesday" }, { "Value": 4, "Label": "Thursday" }, { "Value": 5, "Label": "Friday" }, { "Value": 6, "Label": "Saturday" }, { "Value": 7, "Label": "Sunday" } ], "Selected": "Wednesday", "Selected_id": 3 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CLOCK", "Index": 0, "Node": 8, "Genre": "User", "Help": "Day of Week", "ValueIDKey": 140525588, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1594159718} -OpenZWave/1/node/8/instance/1/commandclass/129/value/281475117236241/,{ "Label": "Hour", "Value": 13, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CLOCK", "Index": 1, "Node": 8, "Genre": "User", "Help": "Hour", "ValueIDKey": 281475117236241, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1594159718} -OpenZWave/1/node/8/instance/1/commandclass/129/value/562950093946897/,{ "Label": "Minute", "Value": 17, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CLOCK", "Index": 2, "Node": 8, "Genre": "User", "Help": "Minute", "ValueIDKey": 562950093946897, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1594159718} -OpenZWave/1/node/8/instance/1/commandclass/129/,{ "Instance": 1, "CommandClassId": 129, "CommandClass": "COMMAND_CLASS_CLOCK", "CommandClassVersion": 1, "TimeStamp": 1594159418} -OpenZWave/1/node/8/instance/1/commandclass/67/value/281475116220434/,{ "Label": "Heating 1", "Value": 21.0, "Units": "C", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_THERMOSTAT_SETPOINT", "Index": 1, "Node": 8, "Genre": "User", "Help": "Set the Thermostat Setpoint Heating 1", "ValueIDKey": 281475116220434, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1594159422} -OpenZWave/1/node/8/instance/1/commandclass/67/,{ "Instance": 1, "CommandClassId": 67, "CommandClass": "COMMAND_CLASS_THERMOSTAT_SETPOINT", "CommandClassVersion": 2, "TimeStamp": 1594159418} -OpenZWave/1/node/8/instance/1/commandclass/114/,{ "Instance": 1, "CommandClassId": 114, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "CommandClassVersion": 1, "TimeStamp": 1594159418} -OpenZWave/1/node/8/instance/1/commandclass/114/value/148668435/,{ "Label": "Loaded Config Revision", "Value": 10, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 0, "Node": 8, "Genre": "System", "Help": "Revision of the Config file currently loaded", "ValueIDKey": 148668435, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1594159418} -OpenZWave/1/node/8/instance/1/commandclass/114/value/281475125379091/,{ "Label": "Config File Revision", "Value": 10, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 1, "Node": 8, "Genre": "System", "Help": "Revision of the Config file on the File System", "ValueIDKey": 281475125379091, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1594159418} -OpenZWave/1/node/8/instance/1/commandclass/114/value/562950102089747/,{ "Label": "Latest Available Config File Revision", "Value": 10, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 2, "Node": 8, "Genre": "System", "Help": "Latest Revision of the Config file available for download", "ValueIDKey": 562950102089747, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1594159418} -OpenZWave/1/node/8/instance/1/commandclass/132/,{ "Instance": 1, "CommandClassId": 132, "CommandClass": "COMMAND_CLASS_WAKE_UP", "CommandClassVersion": 2, "TimeStamp": 1594159418} -OpenZWave/1/node/8/instance/1/commandclass/132/value/148963347/,{ "Label": "Wake-up Interval", "Value": 300, "Units": "Seconds", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_WAKE_UP", "Index": 0, "Node": 8, "Genre": "System", "Help": "How often the Device will Wake up to check for pending commands", "ValueIDKey": 148963347, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1594159418} -OpenZWave/1/node/8/instance/1/commandclass/132/value/281475125674003/,{ "Label": "Minimum Wake-up Interval", "Value": 60, "Units": "Seconds", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_WAKE_UP", "Index": 1, "Node": 8, "Genre": "System", "Help": "Minimum Time in seconds the device will wake up", "ValueIDKey": 281475125674003, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1594159418} -OpenZWave/1/node/8/instance/1/commandclass/132/value/562950102384659/,{ "Label": "Maximum Wake-up Interval", "Value": 1800, "Units": "Seconds", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_WAKE_UP", "Index": 2, "Node": 8, "Genre": "System", "Help": "Maximum Time in seconds the device will wake up", "ValueIDKey": 562950102384659, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1594159418} -OpenZWave/1/node/8/instance/1/commandclass/132/value/844425079095315/,{ "Label": "Default Wake-up Interval", "Value": 300, "Units": "Seconds", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_WAKE_UP", "Index": 3, "Node": 8, "Genre": "System", "Help": "The Default Wake-Up Interval the device will wake up", "ValueIDKey": 844425079095315, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1594159418} -OpenZWave/1/node/8/instance/1/commandclass/132/value/1125900055805971/,{ "Label": "Wake-up Interval Step", "Value": 60, "Units": "Seconds", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_WAKE_UP", "Index": 4, "Node": 8, "Genre": "System", "Help": "Step Size on Wake-up interval", "ValueIDKey": 1125900055805971, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1594159418} -OpenZWave/1/node/8/instance/1/commandclass/134/,{ "Instance": 1, "CommandClassId": 134, "CommandClass": "COMMAND_CLASS_VERSION", "CommandClassVersion": 1, "TimeStamp": 1594159418} -OpenZWave/1/node/8/instance/1/commandclass/134/value/148996119/,{ "Label": "Library Version", "Value": "6", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 0, "Node": 8, "Genre": "System", "Help": "Z-Wave Library Version", "ValueIDKey": 148996119, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1594159418} -OpenZWave/1/node/8/instance/1/commandclass/134/value/281475125706775/,{ "Label": "Protocol Version", "Value": "3.67", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 1, "Node": 8, "Genre": "System", "Help": "Z-Wave Protocol Version", "ValueIDKey": 281475125706775, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1594159418} -OpenZWave/1/node/8/instance/1/commandclass/134/value/562950102417431/,{ "Label": "Application Version", "Value": "1.01", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 2, "Node": 8, "Genre": "System", "Help": "Application Version", "ValueIDKey": 562950102417431, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1594159418} -OpenZWave/1/node/8/instance/1/,{ "Instance": 1, "TimeStamp": 1594159418} -OpenZWave/1/node/16/,{ "NodeID": 16, "NodeQueryStage": "Complete", "isListening": false, "isFlirs": true, "isBeaming": true, "isRouting": true, "isSecurityv1": false, "isZWavePlus": true, "isNIFRecieved": true, "isAwake": true, "isFailed": false, "MetaData": { "OZWInfoURL": "http://www.openzwave.com/device-database/0148:0001:0003", "ZWAProductURL": "https://products.z-wavealliance.org/products/2543/", "ProductPic": "images/eurotronic/eur_spiritz.png", "Description": "• Easy control for water radiators from any Z-Wave Controller • Fits most European water radiators (wide range of additional adaptors for different manufacturers available) • FLiRS for quick response time • LED Backlit LCD • Metal nut for reliable connection to the radiator • 2 buttons for easy temperature regulation • Battery level indicator • Child Lock • Over the Air update • UK-Mode for upside down installation • Open Window detection • Automatic frost protection", "ProductManualURL": "https://Products.Z-WaveAlliance.org/ProductManual/File?folder=&filename=Manuals/2650/Spirit_Z-Wave_BAL_web_EN_view_05.pdf", "ProductPageURL": "", "InclusionHelp": "Start Inclusion mode of your primary Z-Wave Controller. Press the Boost-Button.", "ExclusionHelp": "Start Exclusion mode of your primary Z-Wave Controller. Now press and hold the boost button of the Spirit Z-Wave Plus for at least 5 seconds.", "ResetHelp": "Please use this procedure only when the network primary controller is missing or otherwise inoperable. Remove batteries. Press and hold boost button. While still holding boost button insert batteries. The LCD shows RES. Release boost button. To perform the factory reset press boost button.", "WakeupHelp": "", "ProductSupportURL": "", "Frequency": "UAE", "Name": "KOMFORTHAUS Spirit Z-Wave Plus", "ProductPicBase64": "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" }, "Event": "nodeQueriesComplete", "TimeStamp": 1588422766, "NodeManufacturerName": "EUROtronic", "NodeProductName": "EUR_SPIRITZ Wall Radiator Thermostat", "NodeBasicString": "Routing Slave", "NodeBasic": 4, "NodeGenericString": "Thermostat", "NodeGeneric": 8, "NodeSpecificString": "General Thermostat V2", "NodeSpecific": 6, "NodeManufacturerID": "0x0148", "NodeProductType": "0x0003", "NodeProductID": "0x0001", "NodeBaudRate": 100000, "NodeVersion": 4, "NodeGroups": 1, "NodeName": "", "NodeLocation": "", "NodeDeviceTypeString": "Thermostat HVAC", "NodeDeviceType": 4608, "NodeRole": 7, "NodeRoleString": "Listening Sleeping Slave", "NodePlusType": 0, "NodePlusTypeString": "Z-Wave+ node", "Neighbors": [ 1, 3, 7, 8, 9, 10, 12, 13, 14 ]} -OpenZWave/1/node/16/instance/1/,{ "Instance": 1, "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/38/,{ "Instance": 1, "CommandClassId": 38, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/38/value/273252369/,{ "Label": "Level", "Value": 94, "Units": "%", "Min": 0, "Max": 100, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 0, "Node": 16, "Genre": "User", "Help": "The Current Level of the Device", "ValueIDKey": 273252369, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueChanged", "TimeStamp": 1588422759} -OpenZWave/1/node/16/instance/1/commandclass/38/value/281475249963032/,{ "Label": "Up", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 1, "Node": 16, "Genre": "User", "Help": "Increase the Brightness of the Device", "ValueIDKey": 281475249963032, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/38/value/562950226673688/,{ "Label": "Down", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 2, "Node": 16, "Genre": "User", "Help": "Decrease the Brightness of the Device", "ValueIDKey": 562950226673688, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/38/value/844425211772944/,{ "Label": "Ignore Start Level", "Value": true, "Units": "", "Min": 0, "Max": 0, "Type": "Bool", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 3, "Node": 16, "Genre": "System", "Help": "Ignore the Start Level of the Device when increasing/decreasing brightness", "ValueIDKey": 844425211772944, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/38/value/1125900188483601/,{ "Label": "Start Level", "Value": 0, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 4, "Node": 16, "Genre": "System", "Help": "Start Level when Changing the Brightness of a Device", "ValueIDKey": 1125900188483601, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/64/,{ "Instance": 1, "CommandClassId": 64, "CommandClass": "COMMAND_CLASS_THERMOSTAT_MODE", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/64/value/273678356/,{ "Label": "Mode", "Value": { "List": [ { "Value": 0, "Label": "Off" }, { "Value": 1, "Label": "Heat" }, { "Value": 11, "Label": "Heat Eco" }, { "Value": 15, "Label": "Full Power" }, { "Value": 31, "Label": "Manufacturer Specific" } ], "Selected": "Heat", "Selected_id": 1 }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_THERMOSTAT_MODE", "Index": 0, "Node": 16, "Genre": "User", "Help": "Off: No heating, only frost protection. Heat: Room temperature will be kept at the configured setpoint. Heat Eco: Energy save heating mode. Room temperature will be lowered to the configured eco setpoint in order to save energy. Full Power: Full power heating. This mode is left automatically after 5 minutes. Manufacturer Specific: Direct valve control mode. The valve opening percentage can be controlled using the switch multilevel command class.", "ValueIDKey": 273678356, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/67/,{ "Instance": 1, "CommandClassId": 67, "CommandClass": "COMMAND_CLASS_THERMOSTAT_SETPOINT", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/67/value/281475250438162/,{ "Label": "Heating 1", "Value": 19.0, "Units": "C", "Min": 8, "Max": 28, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_THERMOSTAT_SETPOINT", "Index": 1, "Node": 16, "Genre": "User", "Help": "Set the Thermostat Setpoint Heating 1", "ValueIDKey": 281475250438162, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/67/value/3096225017544722/,{ "Label": "Heating Econ", "Value": 18.0, "Units": "C", "Min": 8, "Max": 28, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_THERMOSTAT_SETPOINT", "Index": 11, "Node": 16, "Genre": "User", "Help": "Set the Thermostat Setpoint Heating Econ", "ValueIDKey": 3096225017544722, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/67/value/28428972921503762/,{ "Label": "Heating 1_minimum", "Value": 8.0, "Units": "C", "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_THERMOSTAT_SETPOINT", "Index": 101, "Node": 16, "Genre": "User", "Help": "", "ValueIDKey": 28428972921503762, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/67/value/56576470592569362/,{ "Label": "Heating 1_maximum", "Value": 28.0, "Units": "C", "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_THERMOSTAT_SETPOINT", "Index": 201, "Node": 16, "Genre": "User", "Help": "", "ValueIDKey": 56576470592569362, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/67/value/31243722688610322/,{ "Label": "Heating Econ_minimum", "Value": 8.0, "Units": "C", "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_THERMOSTAT_SETPOINT", "Index": 111, "Node": 16, "Genre": "User", "Help": "", "ValueIDKey": 31243722688610322, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/67/value/59391220359675922/,{ "Label": "Heating Econ_maximum", "Value": 28.0, "Units": "C", "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_THERMOSTAT_SETPOINT", "Index": 211, "Node": 16, "Genre": "User", "Help": "", "ValueIDKey": 59391220359675922, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/112/,{ "Instance": 1, "CommandClassId": 112, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/112/value/281475255369748/,{ "Label": "LCD Invert", "Value": { "List": [ { "Value": 0, "Label": "Normal" }, { "Value": 1, "Label": "Upside Down" } ], "Selected": "Normal", "Selected_id": 0 }, "Units": "", "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 1, "Node": 16, "Genre": "Config", "Help": "Allows rotating the LCD contents by 180 degrees. Default: Normal", "ValueIDKey": 281475255369748, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/112/value/562950232080401/,{ "Label": "LCD Timeout", "Value": 0, "Units": "sec", "Min": 0, "Max": 30, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 2, "Node": 16, "Genre": "Config", "Help": "0: No Timeout, LCD always on. 5-30: Timeout after 5-30s. Default: 0 (LCD always on)", "ValueIDKey": 562950232080401, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/112/value/844425208791060/,{ "Label": "Backlight", "Value": { "List": [ { "Value": 0, "Label": "Backlight disabled" }, { "Value": 1, "Label": "Backlight enabled" } ], "Selected": "Backlight enabled", "Selected_id": 1 }, "Units": "", "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 3, "Node": 16, "Genre": "Config", "Help": "Default: Backlight enabled", "ValueIDKey": 844425208791060, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/112/value/1125900185501716/,{ "Label": "Battery Report", "Value": { "List": [ { "Value": 0, "Label": "Only send battery status as notification" }, { "Value": 1, "Label": "Send once a day" } ], "Selected": "Send once a day", "Selected_id": 1 }, "Units": "", "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 4, "Node": 16, "Genre": "Config", "Help": "Default: Send once a day", "ValueIDKey": 1125900185501716, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/112/value/1407375162212369/,{ "Label": "Temperature Report Threshold", "Value": 1, "Units": "0.1°C", "Min": 0, "Max": 50, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 5, "Node": 16, "Genre": "Config", "Help": "0: Don't send temperature automatically. 1-50: Report temperature at 0.1-5.0°C temperature difference. Default: 5 (Delta = 0.5°C)", "ValueIDKey": 1407375162212369, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueChanged", "TimeStamp": 1588422810} -OpenZWave/1/node/16/instance/1/commandclass/112/value/1688850138923025/,{ "Label": "Valve Opening Percentage Report", "Value": 5, "Units": "", "Min": 0, "Max": 100, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 6, "Node": 16, "Genre": "Config", "Help": "0: Don't send Valve opening percentage automatically. 1-100: Report valve opening percentage at a delta of 1-100%. Default: 0", "ValueIDKey": 1688850138923025, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueChanged", "TimeStamp": 1588422811} -OpenZWave/1/node/16/instance/1/commandclass/112/value/1970325115633684/,{ "Label": "Open Window Detection", "Value": { "List": [ { "Value": 0, "Label": "Disabled" }, { "Value": 1, "Label": "Low sensibility" }, { "Value": 2, "Label": "Medium sensibility" }, { "Value": 3, "Label": "High sensibility" } ], "Selected": "Medium sensibility", "Selected_id": 2 }, "Units": "", "Min": 0, "Max": 3, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 7, "Node": 16, "Genre": "Config", "Help": "Default: Medium sensibility", "ValueIDKey": 1970325115633684, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/112/value/2251800092344337/,{ "Label": "Measured Temperature Offset", "Value": 0, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 8, "Node": 16, "Genre": "Config", "Help": "206-255: -5.0 to -0.1°C. 0-50: 0°C-5°C. 128: External Temperature Sensor. Default: 0 (0.0°C Offset)", "ValueIDKey": 2251800092344337, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/94/,{ "Instance": 1, "CommandClassId": 94, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/94/value/282558481/,{ "Label": "ZWave+ Version", "Value": 1, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 0, "Node": 16, "Genre": "System", "Help": "ZWave+ Version Supported on the Device", "ValueIDKey": 282558481, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/94/value/281475259269142/,{ "Label": "InstallerIcon", "Value": 4608, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 1, "Node": 16, "Genre": "System", "Help": "Icon File to use for the Installer Application", "ValueIDKey": 281475259269142, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/94/value/562950235979798/,{ "Label": "UserIcon", "Value": 4608, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 2, "Node": 16, "Genre": "System", "Help": "Icon File to use for the User Application", "ValueIDKey": 562950235979798, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/114/,{ "Instance": 1, "CommandClassId": 114, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/114/value/282886163/,{ "Label": "Loaded Config Revision", "Value": 0, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 0, "Node": 16, "Genre": "System", "Help": "Revision of the Config file currently loaded", "ValueIDKey": 282886163, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/114/value/281475259596819/,{ "Label": "Config File Revision", "Value": 5, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 1, "Node": 16, "Genre": "System", "Help": "Revision of the Config file on the File System", "ValueIDKey": 281475259596819, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/114/value/562950236307475/,{ "Label": "Latest Available Config File Revision", "Value": 5, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 2, "Node": 16, "Genre": "System", "Help": "Latest Revision of the Config file available for download", "ValueIDKey": 562950236307475, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/114/value/844425213018135/,{ "Label": "Device ID", "Value": "", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 3, "Node": 16, "Genre": "System", "Help": "Manufacturer Specific Device ID/Model", "ValueIDKey": 844425213018135, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/114/value/1125900189728791/,{ "Label": "Serial Number", "Value": "", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 4, "Node": 16, "Genre": "System", "Help": "Device Serial Number", "ValueIDKey": 1125900189728791, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/115/,{ "Instance": 1, "CommandClassId": 115, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/115/value/282902548/,{ "Label": "Powerlevel", "Value": { "List": [ { "Value": 0, "Label": "Normal" }, { "Value": 1, "Label": "-1dB" }, { "Value": 2, "Label": "-2dB" }, { "Value": 3, "Label": "-3dB" }, { "Value": 4, "Label": "-4dB" }, { "Value": 5, "Label": "-5dB" }, { "Value": 6, "Label": "-6dB" }, { "Value": 7, "Label": "-7dB" }, { "Value": 8, "Label": "-8dB" }, { "Value": 9, "Label": "-9dB" } ], "Selected": "Normal", "Selected_id": 0 }, "Units": "dB", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 0, "Node": 16, "Genre": "System", "Help": "Output RF PowerLevel", "ValueIDKey": 282902548, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/115/value/281475259613201/,{ "Label": "Timeout", "Value": 0, "Units": "seconds", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 1, "Node": 16, "Genre": "System", "Help": "Timeout till the PowerLevel is reset to Normal", "ValueIDKey": 281475259613201, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/115/value/562950236323864/,{ "Label": "Set Powerlevel", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 2, "Node": 16, "Genre": "System", "Help": "Apply the Output PowerLevel and Timeout Values", "ValueIDKey": 562950236323864, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/115/value/844425213034513/,{ "Label": "Test Node", "Value": 0, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 3, "Node": 16, "Genre": "System", "Help": "Node to Perform a test against", "ValueIDKey": 844425213034513, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/115/value/1125900189745172/,{ "Label": "Test Powerlevel", "Value": { "List": [ { "Value": 0, "Label": "Normal" }, { "Value": 1, "Label": "-1dB" }, { "Value": 2, "Label": "-2dB" }, { "Value": 3, "Label": "-3dB" }, { "Value": 4, "Label": "-4dB" }, { "Value": 5, "Label": "-5dB" }, { "Value": 6, "Label": "-6dB" }, { "Value": 7, "Label": "-7dB" }, { "Value": 8, "Label": "-8dB" }, { "Value": 9, "Label": "-9dB" } ], "Selected": "Normal", "Selected_id": 0 }, "Units": "dB", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 4, "Node": 16, "Genre": "System", "Help": "PowerLevel to use for the Test", "ValueIDKey": 1125900189745172, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/115/value/1407375166455830/,{ "Label": "Frame Count", "Value": 0, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 5, "Node": 16, "Genre": "System", "Help": "How Many Messages to send to the Node for the Test", "ValueIDKey": 1407375166455830, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/115/value/1688850143166488/,{ "Label": "Test", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 6, "Node": 16, "Genre": "System", "Help": "Perform a PowerLevel Test against the a Node", "ValueIDKey": 1688850143166488, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/115/value/1970325119877144/,{ "Label": "Report", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 7, "Node": 16, "Genre": "System", "Help": "Get the results of the latest PowerLevel Test against a Node", "ValueIDKey": 1970325119877144, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/115/value/2251800096587796/,{ "Label": "Test Status", "Value": { "List": [ { "Value": 0, "Label": "Failed" }, { "Value": 1, "Label": "Success" }, { "Value": 2, "Label": "In Progress" } ], "Selected": "Failed", "Selected_id": 0 }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 8, "Node": 16, "Genre": "System", "Help": "The Current Status of the last PowerNode Test Executed", "ValueIDKey": 2251800096587796, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/115/value/2533275073298454/,{ "Label": "Acked Frames", "Value": 0, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 9, "Node": 16, "Genre": "System", "Help": "Number of Messages successfully Acked by the Target Node", "ValueIDKey": 2533275073298454, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/117/,{ "Instance": 1, "CommandClassId": 117, "CommandClass": "COMMAND_CLASS_PROTECTION", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/117/value/282935316/,{ "Label": "Protection", "Value": { "List": [ { "Value": 0, "Label": "Unprotected" }, { "Value": 1, "Label": "Protection by Sequence" }, { "Value": 2, "Label": "No Operation Possible" } ], "Selected": "Protection by Sequence", "Selected_id": 1 }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_PROTECTION", "Index": 0, "Node": 16, "Genre": "System", "Help": "Protect a device against unintentional control", "ValueIDKey": 282935316, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/128/,{ "Instance": 1, "CommandClassId": 128, "CommandClass": "COMMAND_CLASS_BATTERY", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/128/value/274726929/,{ "Label": "Battery Level", "Value": 90, "Units": "%", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_BATTERY", "Index": 0, "Node": 16, "Genre": "User", "Help": "Current Battery Level", "ValueIDKey": 274726929, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/134/,{ "Instance": 1, "CommandClassId": 134, "CommandClass": "COMMAND_CLASS_VERSION", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/134/value/283213847/,{ "Label": "Library Version", "Value": "3", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 0, "Node": 16, "Genre": "System", "Help": "Z-Wave Library Version", "ValueIDKey": 283213847, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/134/value/281475259924503/,{ "Label": "Protocol Version", "Value": "4.61", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 1, "Node": 16, "Genre": "System", "Help": "Z-Wave Protocol Version", "ValueIDKey": 281475259924503, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/134/value/562950236635159/,{ "Label": "Application Version", "Value": "0.15", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 2, "Node": 16, "Genre": "System", "Help": "Application Version", "ValueIDKey": 562950236635159, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/152/,{ "Instance": 1, "CommandClassId": 152, "CommandClass": "COMMAND_CLASS_SECURITY", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/152/value/283508752/,{ "Label": "Secured", "Value": true, "Units": "", "Min": 0, "Max": 0, "Type": "Bool", "Instance": 1, "CommandClass": "COMMAND_CLASS_SECURITY", "Index": 0, "Node": 16, "Genre": "System", "Help": "Is Communication with Device Encrypted", "ValueIDKey": 283508752, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/49/,{ "Instance": 1, "CommandClassId": 49, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/49/value/281475250143250/,{ "Label": "Air Temperature", "Value": 17.260000228881837, "Units": "C", "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "Index": 1, "Node": 16, "Genre": "User", "Help": "Air Temperature Sensor Value", "ValueIDKey": 281475250143250, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueChanged", "TimeStamp": 1588422760} -OpenZWave/1/node/16/instance/1/commandclass/49/value/72057594319749140/,{ "Label": "Air Temperature Units", "Value": { "List": [ { "Value": 0, "Label": "Celsius" } ], "Selected": "Celsius", "Selected_id": 0 }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "Index": 256, "Node": 16, "Genre": "System", "Help": "Air Temperature Sensor Available Units", "ValueIDKey": 72057594319749140, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/113/,{ "Instance": 1, "CommandClassId": 113, "CommandClass": "COMMAND_CLASS_NOTIFICATION", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/113/value/72057594312409105/,{ "Label": "Previous Event Cleared", "Value": 0, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_NOTIFICATION", "Index": 256, "Node": 16, "Genre": "User", "Help": "Previous Event that was sent", "ValueIDKey": 72057594312409105, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/113/value/2251800088166420/,{ "Label": "Power Management", "Value": { "List": [ { "Value": 0, "Label": "Clear" }, { "Value": 10, "Label": "Replace Battery Soon" }, { "Value": 11, "Label": "Replace Battery Now" } ], "Selected": "Clear", "Selected_id": 0 }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_NOTIFICATION", "Index": 8, "Node": 16, "Genre": "User", "Help": "Power Management Alerts", "ValueIDKey": 2251800088166420, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/113/value/74872344079515671/,{ "Label": "Error Code", "Value": "", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_NOTIFICATION", "Index": 266, "Node": 16, "Genre": "User", "Help": "The Error Code returned by the device", "ValueIDKey": 74872344079515671, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/113/value/2533275064877076/,{ "Label": "System", "Value": { "List": [ { "Value": 0, "Label": "Clear" }, { "Value": 3, "Label": "Hardware Failure Code" } ], "Selected": "Clear", "Selected_id": 0 }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_NOTIFICATION", "Index": 9, "Node": 16, "Genre": "User", "Help": "System Alerts", "ValueIDKey": 2533275064877076, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} -OpenZWave/1/node/16/association/1/,{ "Name": "Group 1", "Help": "", "MaxAssociations": 1, "Members": [ "1.0" ], "TimeStamp": 1588422682} -OpenZWave/1/node/17/,{ "NodeID": 17, "NodeQueryStage": "CacheLoad", "isListening": false, "isFlirs": false, "isBeaming": true, "isRouting": false, "isSecurityv1": false, "isZWavePlus": false, "isNIFRecieved": true, "isAwake": true, "isFailed": false, "MetaData": { "OZWInfoURL": "http://www.openzwave.com/device-database/0059:0003:0001", "ZWAProductURL": "https://products.z-wavealliance.org/products/115/", "ProductPic": "images/horstmann/hrt4zw.png", "Description": "ThermostatThe innovative Horstmann CentaurPlus ZW combined wireless room stat and time control offers installers and householders the opportunity to easily and cost effectively update existing combi boiler controls. The CentaurPlus has an integral transmitter and receiver, enabling wireless communication with the latest generation Horstmann HRT4-ZW TPI room thermostat. Suitable for combi boilers Volt free contacts Automatic BST /GMT time change Back lit display Boost and Advance Helps to meet Part L1 of 2010 Building Regs for existing installations Built in Z Wave receiver Industry Standard 6 terminal wall plate ZW wireless technology TPI energy saving software Clear backlit display Temperature range 5-30°C Battery operated for wire free installation", "ProductManualURL": "", "ProductPageURL": "http://www.securetogether.eu/", "InclusionHelp": "", "ExclusionHelp": "", "ResetHelp": "", "WakeupHelp": "", "ProductSupportURL": "", "Frequency": "CEPT (Europe)", "Name": "Secure SRT321 Zwave Stat (Tx)", "ProductPicBase64": "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" }, "Event": "nodeNaming", "TimeStamp": 1596278310, "NodeManufacturerName": "Horstmann (Secure Meters)", "NodeProductName": "HRT4-ZW Thermostat Transmitter", "NodeBasicString": "Controller", "NodeBasic": 1, "NodeGenericString": "Thermostat", "NodeGeneric": 8, "NodeSpecificString": "Thermostat", "NodeSpecific": 0, "NodeManufacturerID": "0x0059", "NodeProductType": "0x0001", "NodeProductID": "0x0003", "NodeBaudRate": 40000, "NodeVersion": 3, "NodeGroups": 5, "NodeName": "", "NodeLocation": ""} -OpenZWave/1/node/17/instance/1/,{ "Instance": 1, "TimeStamp": 1596278310} -OpenZWave/1/node/17/instance/1/commandclass/112/,{ "Instance": 1, "CommandClassId": 112, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "CommandClassVersion": 1, "TimeStamp": 1596278310} -OpenZWave/1/node/17/instance/1/commandclass/112/value/281475272146964/,{ "Label": "Temperature sensor reading", "Value": { "List": [ { "Value": 0, "Label": "Disable" }, { "Value": 255, "Label": "Enable" } ], "Selected": "Enable", "Selected_id": 255 }, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 1, "Node": 17, "Genre": "Config", "Help": "", "ValueIDKey": 281475272146964, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1596278310} -OpenZWave/1/node/17/instance/1/commandclass/112/value/562950248857620/,{ "Label": "Temperature Scale", "Value": { "List": [ { "Value": 0, "Label": "Celsius" }, { "Value": 255, "Label": "Fahrenheit" } ], "Selected": "Celsius", "Selected_id": 0 }, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 2, "Node": 17, "Genre": "Config", "Help": "", "ValueIDKey": 562950248857620, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1596278310} -OpenZWave/1/node/17/instance/1/commandclass/112/value/844425225568273/,{ "Label": "Temperature Delta T", "Value": 10, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 3, "Node": 17, "Genre": "Config", "Help": "Delta T in steps of 0.1 degree.", "ValueIDKey": 844425225568273, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1596278310} -OpenZWave/1/node/17/instance/1/commandclass/32/,{ "Instance": 1, "CommandClassId": 32, "CommandClass": "COMMAND_CLASS_BASIC", "CommandClassVersion": 2, "TimeStamp": 1596278310} -OpenZWave/1/node/17/instance/1/commandclass/32/value/285736977/,{ "Label": "Basic", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_BASIC", "Index": 0, "Node": 17, "Genre": "Basic", "Help": "Basic status of the node", "ValueIDKey": 285736977, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1596278310} -OpenZWave/1/node/17/instance/1/commandclass/32/value/281475262447633/,{ "Label": "Basic Target", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_BASIC", "Index": 1, "Node": 17, "Genre": "Basic", "Help": "", "ValueIDKey": 281475262447633, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1596278310} -OpenZWave/1/node/17/instance/1/commandclass/32/value/562950239158291/,{ "Label": "Basic Duration", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_BASIC", "Index": 2, "Node": 17, "Genre": "Basic", "Help": "", "ValueIDKey": 562950239158291, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1596278310} -OpenZWave/1/node/17/instance/1/commandclass/37/,{ "Instance": 1, "CommandClassId": 37, "CommandClass": "COMMAND_CLASS_SWITCH_BINARY", "CommandClassVersion": 0, "TimeStamp": 1596278310} -OpenZWave/1/node/17/instance/1/commandclass/37/value/290013200/,{ "Label": "Switch", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Bool", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_BINARY", "Index": 0, "Node": 17, "Genre": "User", "Help": "Turn On/Off Device", "ValueIDKey": 290013200, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1596278310} -OpenZWave/1/node/17/instance/1/commandclass/114/,{ "Instance": 1, "CommandClassId": 114, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "CommandClassVersion": 1, "TimeStamp": 1596278310} -OpenZWave/1/node/17/instance/1/commandclass/114/value/299663379/,{ "Label": "Loaded Config Revision", "Value": 4, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 0, "Node": 17, "Genre": "System", "Help": "Revision of the Config file currently loaded", "ValueIDKey": 299663379, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1596278310} -OpenZWave/1/node/17/instance/1/commandclass/114/value/281475276374035/,{ "Label": "Config File Revision", "Value": 4, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 1, "Node": 17, "Genre": "System", "Help": "Revision of the Config file on the File System", "ValueIDKey": 281475276374035, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1596278310} -OpenZWave/1/node/17/instance/1/commandclass/114/value/562950253084691/,{ "Label": "Latest Available Config File Revision", "Value": 4, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 2, "Node": 17, "Genre": "System", "Help": "Latest Revision of the Config file available for download", "ValueIDKey": 562950253084691, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1596278310} -OpenZWave/1/node/17/instance/1/commandclass/128/,{ "Instance": 1, "CommandClassId": 128, "CommandClass": "COMMAND_CLASS_BATTERY", "CommandClassVersion": 1, "TimeStamp": 1596278310} -OpenZWave/1/node/17/instance/1/commandclass/128/value/291504145/,{ "Label": "Battery Level", "Value": 100, "Units": "%", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_BATTERY", "Index": 0, "Node": 17, "Genre": "User", "Help": "Current Battery Level", "ValueIDKey": 291504145, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1596278310} -OpenZWave/1/node/17/instance/1/commandclass/132/,{ "Instance": 1, "CommandClassId": 132, "CommandClass": "COMMAND_CLASS_WAKE_UP", "CommandClassVersion": 2, "TimeStamp": 1596278310} -OpenZWave/1/node/17/instance/1/commandclass/132/value/281475276668947/,{ "Label": "Minimum Wake-up Interval", "Value": 256, "Units": "Seconds", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_WAKE_UP", "Index": 1, "Node": 17, "Genre": "System", "Help": "Minimum Time in seconds the device will wake up", "ValueIDKey": 281475276668947, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1596278310} -OpenZWave/1/node/17/instance/1/commandclass/132/value/562950253379603/,{ "Label": "Maximum Wake-up Interval", "Value": 131071, "Units": "Seconds", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_WAKE_UP", "Index": 2, "Node": 17, "Genre": "System", "Help": "Maximum Time in seconds the device will wake up", "ValueIDKey": 562950253379603, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1596278310} -OpenZWave/1/node/17/instance/1/commandclass/132/value/844425230090259/,{ "Label": "Default Wake-up Interval", "Value": 86400, "Units": "Seconds", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_WAKE_UP", "Index": 3, "Node": 17, "Genre": "System", "Help": "The Default Wake-Up Interval the device will wake up", "ValueIDKey": 844425230090259, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1596278310} -OpenZWave/1/node/17/instance/1/commandclass/132/value/1125900206800915/,{ "Label": "Wake-up Interval Step", "Value": 1, "Units": "Seconds", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_WAKE_UP", "Index": 4, "Node": 17, "Genre": "System", "Help": "Step Size on Wake-up interval", "ValueIDKey": 1125900206800915, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1596278310} -OpenZWave/1/node/17/instance/1/commandclass/132/value/299958291/,{ "Label": "Wake-up Interval", "Value": 86400, "Units": "Seconds", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_WAKE_UP", "Index": 0, "Node": 17, "Genre": "System", "Help": "How often the Device will Wake up to check for pending commands", "ValueIDKey": 299958291, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1596278310} -OpenZWave/1/node/17/instance/1/commandclass/134/,{ "Instance": 1, "CommandClassId": 134, "CommandClass": "COMMAND_CLASS_VERSION", "CommandClassVersion": 1, "TimeStamp": 1596278310} -OpenZWave/1/node/17/instance/1/commandclass/134/value/299991063/,{ "Label": "Library Version", "Value": "2", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 0, "Node": 17, "Genre": "System", "Help": "Z-Wave Library Version", "ValueIDKey": 299991063, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1596278310} -OpenZWave/1/node/17/instance/1/commandclass/134/value/281475276701719/,{ "Label": "Protocol Version", "Value": "2.78", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 1, "Node": 17, "Genre": "System", "Help": "Z-Wave Protocol Version", "ValueIDKey": 281475276701719, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1596278310} -OpenZWave/1/node/17/instance/1/commandclass/134/value/562950253412375/,{ "Label": "Application Version", "Value": "5.00", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 2, "Node": 17, "Genre": "System", "Help": "Application Version", "ValueIDKey": 562950253412375, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1596278310} -OpenZWave/1/node/17/instance/1/commandclass/49/,{ "Instance": 1, "CommandClassId": 49, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "CommandClassVersion": 1, "TimeStamp": 1596278310} -OpenZWave/1/node/17/instance/1/commandclass/49/value/281475266920466/,{ "Label": "Air Temperature", "Value": 29.0, "Units": "C", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "Index": 1, "Node": 17, "Genre": "User", "Help": "Air Temperature Sensor Value", "ValueIDKey": 281475266920466, "ReadOnly": true, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1596284337} -OpenZWave/1/node/17/instance/1/commandclass/67/,{ "Instance": 1, "CommandClassId": 67, "CommandClass": "COMMAND_CLASS_THERMOSTAT_SETPOINT", "CommandClassVersion": 1, "TimeStamp": 1596278310} -OpenZWave/1/node/17/instance/1/commandclass/67/value/281475267215378/,{ "Label": "Heating 1", "Value": 16.0, "Units": "C", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_THERMOSTAT_SETPOINT", "Index": 1, "Node": 17, "Genre": "User", "Help": "Set the Thermostat Setpoint Heating 1", "ValueIDKey": 281475267215378, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1596278310} \ No newline at end of file diff --git a/tests/components/ozw/fixtures/cover.json b/tests/components/ozw/fixtures/cover.json deleted file mode 100644 index ece62617edd..00000000000 --- a/tests/components/ozw/fixtures/cover.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "topic": "OpenZWave/1/node/37/instance/1/commandclass/38/value/625573905/", - "payload": { - "Label": "Instance 1: Level", - "Value": 0, - "Units": "", - "ValueSet": true, - "ValuePolled": false, - "ChangeVerified": false, - "Min": 0, - "Max": 255, - "Type": "Byte", - "Instance": 1, - "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", - "Index": 0, - "Node": 37, - "Genre": "User", - "Help": "The Current Level of the Device", - "ValueIDKey": 625573905, - "ReadOnly": false, - "WriteOnly": false, - "Event": "valueChanged", - "TimeStamp": 1593370642 - } -} \ No newline at end of file diff --git a/tests/components/ozw/fixtures/cover_gdo.json b/tests/components/ozw/fixtures/cover_gdo.json deleted file mode 100644 index d171c4f2071..00000000000 --- a/tests/components/ozw/fixtures/cover_gdo.json +++ /dev/null @@ -1,54 +0,0 @@ -{ - "topic": "OpenZWave/1/node/6/instance/1/commandclass/102/value/281475083239444/", - "payload": { - "Label": "Barrier State", - "Value": { - "List": [ - { - "Value": 0, - "Label": "Closed" - }, - { - "Value": 1, - "Label": "Closing" - }, - { - "Value": 2, - "Label": "Stopped" - }, - { - "Value": 3, - "Label": "Opening" - }, - { - "Value": 4, - "Label": "Opened" - }, - { - "Value": 5, - "Label": "Unknown" - } - ], - "Selected": "Closed", - "Selected_id": 0 - }, - "Units": "", - "ValueSet": true, - "ValuePolled": false, - "ChangeVerified": false, - "Min": 0, - "Max": 0, - "Type": "List", - "Instance": 1, - "CommandClass": "COMMAND_CLASS_BARRIER_OPERATOR", - "Index": 1, - "Node": 6, - "Genre": "User", - "Help": "The Current State of the Barrier", - "ValueIDKey": 281475083239444, - "ReadOnly": false, - "WriteOnly": false, - "Event": "valueChanged", - "TimeStamp": 1593634453 - } -} \ No newline at end of file diff --git a/tests/components/ozw/fixtures/cover_gdo_network_dump.csv b/tests/components/ozw/fixtures/cover_gdo_network_dump.csv deleted file mode 100644 index 0fc08b4a34d..00000000000 --- a/tests/components/ozw/fixtures/cover_gdo_network_dump.csv +++ /dev/null @@ -1,45 +0,0 @@ -OpenZWave/1/status/,{ "OpenZWave_Version": "1.6.1173", "OZWDaemon_Version": "0.1.149", "QTOpenZWave_Version": "1.2.0", "QT_Version": "5.12.5", "Status": "driverAllNodesQueried", "TimeStamp": 1593370382, "ManufacturerSpecificDBReady": true, "homeID": 3716538409, "getControllerNodeId": 1, "getSUCNodeId": 1, "isPrimaryController": true, "isBridgeController": false, "hasExtendedTXStatistics": true, "getControllerLibraryVersion": "Z-Wave 4.61", "getControllerLibraryType": "Static Controller", "getControllerPath": "/dev/ttyACM0"} -OpenZWave/1/node/6/,{ "NodeID": 6, "NodeQueryStage": "Complete", "isListening": true, "isFlirs": false, "isBeaming": true, "isRouting": true, "isSecurityv1": false, "isZWavePlus": true, "isNIFRecieved": true, "isAwake": true, "isFailed": false, "MetaData": { "OZWInfoURL": "http://www.openzwave.com/device-database/014F:3530:4744", "ZWAProductURL": "", "ProductPic": "images/linear/ngd00z.png", "Description": "Garage Door Remote Controller Accessory opens or closes a sectional garage door remotely through a Z-Wave certified Gateway or Security Panel. Compatible with virtually any garage door opener connected to a sectional garage door. Audible and visual warnings prior to remotely-activated door movement, meeting UL 325 safety requirements. Included tilt sensor reports door \"open\" or \"close\" information.", "ProductManualURL": "https://Products.Z-WaveAlliance.org/ProductManual/File?folder=&filename=Manuals/1458/236956BX1 GD00Z-4 GoControl Instructions.pdf", "ProductPageURL": "", "InclusionHelp": "With the hub in \"Add\" mode, press the release the button on the side of the GD00Z.", "ExclusionHelp": "With the hub in \"Remove\" mode, press the release the button on the side of the GD00Z.", "ResetHelp": "Press and release the button in the side of the unit 5 times within 10 seconds only when the primary controller is not available.", "WakeupHelp": "n/a", "ProductSupportURL": "", "Frequency": "", "Name": "GD00Z-4", "ProductPicBase64": "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" }, "Event": "nodeQueriesComplete", "TimeStamp": 1593368191, "NodeManufacturerName": "Linear (Nortek Security Control LLC)", "NodeProductName": "GD00Z-4 Garage Door Opener Remote Controller", "NodeBasicString": "Routing Slave", "NodeBasic": 4, "NodeGenericString": "Entry Control", "NodeGeneric": 64, "NodeSpecificString": "Secure Barrier AddOn", "NodeSpecific": 7, "NodeManufacturerID": "0x014f", "NodeProductType": "0x4744", "NodeProductID": "0x3530", "NodeBaudRate": 100000, "NodeVersion": 4, "NodeGroups": 1, "NodeName": "", "NodeLocation": "", "NodeDeviceTypeString": "Access Control Sensor", "NodeDeviceType": 3078, "NodeRole": 5, "NodeRoleString": "Always On Slave", "NodePlusType": 0, "NodePlusTypeString": "Z-Wave+ node", "Neighbors": [ 1, 2, 8, 9, 10, 11, 12, 14, 15, 16, 18, 19, 20, 21, 22, 23, 26, 27, 28, 29, 32, 33 ], "Neighbors": [ 1, 2, 8, 9, 10, 11, 12, 14, 15, 16, 18, 19, 20, 21, 22, 23, 26, 27, 28, 29, 32, 33 ]} -OpenZWave/1/node/6/instance/1/,{ "Instance": 1, "TimeStamp": 1593368054} -OpenZWave/1/node/6/instance/1/commandclass/32/,{ "Instance": 1, "CommandClassId": 32, "CommandClass": "COMMAND_CLASS_BASIC", "CommandClassVersion": 1, "TimeStamp": 1593368054} -OpenZWave/1/node/6/instance/1/commandclass/32/value/101187601/,{ "Label": "Basic", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_BASIC", "Index": 0, "Node": 6, "Genre": "Basic", "Help": "Basic status of the node", "ValueIDKey": 101187601, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593368054} -OpenZWave/1/node/6/instance/1/commandclass/48/,{ "Instance": 1, "CommandClassId": 48, "CommandClass": "COMMAND_CLASS_SENSOR_BINARY", "CommandClassVersion": 1, "TimeStamp": 1593368054} -OpenZWave/1/node/6/instance/1/commandclass/48/value/105644048/,{ "Label": "Sensor", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Bool", "Instance": 1, "CommandClass": "COMMAND_CLASS_SENSOR_BINARY", "Index": 0, "Node": 6, "Genre": "User", "Help": "Binary Sensor State", "ValueIDKey": 105644048, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593368054} -OpenZWave/1/node/6/instance/1/commandclass/94/,{ "Instance": 1, "CommandClassId": 94, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "CommandClassVersion": 1, "TimeStamp": 1593368054} -OpenZWave/1/node/6/instance/1/commandclass/94/value/114786321/,{ "Label": "ZWave+ Version", "Value": 1, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 0, "Node": 6, "Genre": "System", "Help": "ZWave+ Version Supported on the Device", "ValueIDKey": 114786321, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593368054} -OpenZWave/1/node/6/instance/1/commandclass/94/value/281475091496982/,{ "Label": "InstallerIcon", "Value": 3078, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 1, "Node": 6, "Genre": "System", "Help": "Icon File to use for the Installer Application", "ValueIDKey": 281475091496982, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593368054} -OpenZWave/1/node/6/instance/1/commandclass/94/value/562950068207638/,{ "Label": "UserIcon", "Value": 3078, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 2, "Node": 6, "Genre": "System", "Help": "Icon File to use for the User Application", "ValueIDKey": 562950068207638, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593368054} -OpenZWave/1/node/6/instance/1/commandclass/102/,{ "Instance": 1, "CommandClassId": 102, "CommandClass": "COMMAND_CLASS_BARRIER_OPERATOR", "CommandClassVersion": 1, "TimeStamp": 1593368054} -OpenZWave/1/node/6/instance/1/commandclass/102/value/281475083239444/,{ "Label": "Barrier State", "Value": { "List": [ { "Value": 0, "Label": "Closed" }, { "Value": 1, "Label": "Closing" }, { "Value": 2, "Label": "Stopped" }, { "Value": 3, "Label": "Opening" }, { "Value": 4, "Label": "Opened" }, { "Value": 5, "Label": "Unknown" } ], "Selected": "Closed", "Selected_id": 0 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_BARRIER_OPERATOR", "Index": 1, "Node": 6, "Genre": "User", "Help": "The Current State of the Barrier", "ValueIDKey": 281475083239444, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593634453} -OpenZWave/1/node/6/instance/1/commandclass/102/value/562950064144404/,{ "Label": "Supported Signals", "Value": { "List": [ { "Value": 0, "Label": "None" }, { "Value": 1, "Label": "Audible" }, { "Value": 2, "Label": "Visual" }, { "Value": 3, "Label": "Both" } ], "Selected": "Both", "Selected_id": 3 }, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_BARRIER_OPERATOR", "Index": 2, "Node": 6, "Genre": "Config", "Help": "Supported Operations for the Barrier", "ValueIDKey": 562950064144404, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593368054} -OpenZWave/1/node/6/instance/1/commandclass/102/value/844425040855056/,{ "Label": "Audible Notification", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Bool", "Instance": 1, "CommandClass": "COMMAND_CLASS_BARRIER_OPERATOR", "Index": 3, "Node": 6, "Genre": "Config", "Help": "Enable Audible Notifications of Barrier State Change", "ValueIDKey": 844425040855056, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593368054} -OpenZWave/1/node/6/instance/1/commandclass/102/value/1125900017565712/,{ "Label": "Visual Notification", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Bool", "Instance": 1, "CommandClass": "COMMAND_CLASS_BARRIER_OPERATOR", "Index": 4, "Node": 6, "Genre": "Config", "Help": "Enable Visual Notifications of Barrier State Change", "ValueIDKey": 1125900017565712, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593368054} -OpenZWave/1/node/6/instance/1/commandclass/114/,{ "Instance": 1, "CommandClassId": 114, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "CommandClassVersion": 2, "TimeStamp": 1593368054} -OpenZWave/1/node/6/instance/1/commandclass/114/value/115114003/,{ "Label": "Loaded Config Revision", "Value": 2, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 0, "Node": 6, "Genre": "System", "Help": "Revision of the Config file currently loaded", "ValueIDKey": 115114003, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593368054} -OpenZWave/1/node/6/instance/1/commandclass/114/value/281475091824659/,{ "Label": "Config File Revision", "Value": 2, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 1, "Node": 6, "Genre": "System", "Help": "Revision of the Config file on the File System", "ValueIDKey": 281475091824659, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593368054} -OpenZWave/1/node/6/instance/1/commandclass/114/value/562950068535315/,{ "Label": "Latest Available Config File Revision", "Value": 2, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 2, "Node": 6, "Genre": "System", "Help": "Latest Revision of the Config file available for download", "ValueIDKey": 562950068535315, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593368054} -OpenZWave/1/node/6/instance/1/commandclass/114/value/844425045245975/,{ "Label": "Device ID", "Value": "", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 3, "Node": 6, "Genre": "System", "Help": "Manufacturer Specific Device ID/Model", "ValueIDKey": 844425045245975, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593368054} -OpenZWave/1/node/6/instance/1/commandclass/114/value/1125900021956631/,{ "Label": "Serial Number", "Value": "", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 4, "Node": 6, "Genre": "System", "Help": "Device Serial Number", "ValueIDKey": 1125900021956631, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593368054} -OpenZWave/1/node/6/instance/1/commandclass/115/,{ "Instance": 1, "CommandClassId": 115, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "CommandClassVersion": 1, "TimeStamp": 1593368054} -OpenZWave/1/node/6/instance/1/commandclass/115/value/115130388/,{ "Label": "Powerlevel", "Value": { "List": [ { "Value": 0, "Label": "Normal" }, { "Value": 1, "Label": "-1dB" }, { "Value": 2, "Label": "-2dB" }, { "Value": 3, "Label": "-3dB" }, { "Value": 4, "Label": "-4dB" }, { "Value": 5, "Label": "-5dB" }, { "Value": 6, "Label": "-6dB" }, { "Value": 7, "Label": "-7dB" }, { "Value": 8, "Label": "-8dB" }, { "Value": 9, "Label": "-9dB" } ], "Selected": "Normal", "Selected_id": 0 }, "Units": "dB", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 0, "Node": 6, "Genre": "System", "Help": "Output RF PowerLevel", "ValueIDKey": 115130388, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593368077} -OpenZWave/1/node/6/instance/1/commandclass/115/value/281475091841041/,{ "Label": "Timeout", "Value": 0, "Units": "seconds", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 1, "Node": 6, "Genre": "System", "Help": "Timeout till the PowerLevel is reset to Normal", "ValueIDKey": 281475091841041, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593368077} -OpenZWave/1/node/6/instance/1/commandclass/115/value/562950068551704/,{ "Label": "Set Powerlevel", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 2, "Node": 6, "Genre": "System", "Help": "Apply the Output PowerLevel and Timeout Values", "ValueIDKey": 562950068551704, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1593368054} -OpenZWave/1/node/6/instance/1/commandclass/115/value/844425045262353/,{ "Label": "Test Node", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 3, "Node": 6, "Genre": "System", "Help": "Node to Perform a test against", "ValueIDKey": 844425045262353, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593368054} -OpenZWave/1/node/6/instance/1/commandclass/115/value/1125900021973012/,{ "Label": "Test Powerlevel", "Value": { "List": [ { "Value": 0, "Label": "Normal" }, { "Value": 1, "Label": "-1dB" }, { "Value": 2, "Label": "-2dB" }, { "Value": 3, "Label": "-3dB" }, { "Value": 4, "Label": "-4dB" }, { "Value": 5, "Label": "-5dB" }, { "Value": 6, "Label": "-6dB" }, { "Value": 7, "Label": "-7dB" }, { "Value": 8, "Label": "-8dB" }, { "Value": 9, "Label": "-9dB" } ], "Selected": "Normal", "Selected_id": 0 }, "Units": "dB", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 4, "Node": 6, "Genre": "System", "Help": "PowerLevel to use for the Test", "ValueIDKey": 1125900021973012, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593368054} -OpenZWave/1/node/6/instance/1/commandclass/115/value/1407374998683670/,{ "Label": "Frame Count", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 5, "Node": 6, "Genre": "System", "Help": "How Many Messages to send to the Node for the Test", "ValueIDKey": 1407374998683670, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593368054} -OpenZWave/1/node/6/instance/1/commandclass/115/value/1688849975394328/,{ "Label": "Test", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 6, "Node": 6, "Genre": "System", "Help": "Perform a PowerLevel Test against the a Node", "ValueIDKey": 1688849975394328, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1593368054} -OpenZWave/1/node/6/instance/1/commandclass/115/value/1970324952104984/,{ "Label": "Report", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 7, "Node": 6, "Genre": "System", "Help": "Get the results of the latest PowerLevel Test against a Node", "ValueIDKey": 1970324952104984, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1593368054} -OpenZWave/1/node/6/instance/1/commandclass/115/value/2251799928815636/,{ "Label": "Test Status", "Value": { "List": [ { "Value": 0, "Label": "Failed" }, { "Value": 1, "Label": "Success" }, { "Value": 2, "Label": "In Progress" } ], "Selected": "Failed", "Selected_id": 0 }, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 8, "Node": 6, "Genre": "System", "Help": "The Current Status of the last PowerNode Test Executed", "ValueIDKey": 2251799928815636, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593368054} -OpenZWave/1/node/6/instance/1/commandclass/115/value/2533274905526294/,{ "Label": "Acked Frames", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 9, "Node": 6, "Genre": "System", "Help": "Number of Messages successfully Acked by the Target Node", "ValueIDKey": 2533274905526294, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593368054} -OpenZWave/1/node/6/instance/1/commandclass/134/,{ "Instance": 1, "CommandClassId": 134, "CommandClass": "COMMAND_CLASS_VERSION", "CommandClassVersion": 1, "TimeStamp": 1593368054} -OpenZWave/1/node/6/instance/1/commandclass/134/value/115441687/,{ "Label": "Library Version", "Value": "3", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 0, "Node": 6, "Genre": "System", "Help": "Z-Wave Library Version", "ValueIDKey": 115441687, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593368054} -OpenZWave/1/node/6/instance/1/commandclass/134/value/281475092152343/,{ "Label": "Protocol Version", "Value": "4.05", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 1, "Node": 6, "Genre": "System", "Help": "Z-Wave Protocol Version", "ValueIDKey": 281475092152343, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593368054} -OpenZWave/1/node/6/instance/1/commandclass/134/value/562950068862999/,{ "Label": "Application Version", "Value": "2.01", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 2, "Node": 6, "Genre": "System", "Help": "Application Version", "ValueIDKey": 562950068862999, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593368054} -OpenZWave/1/node/6/instance/1/commandclass/152/,{ "Instance": 1, "CommandClassId": 152, "CommandClass": "COMMAND_CLASS_SECURITY", "CommandClassVersion": 1, "TimeStamp": 1593368054} -OpenZWave/1/node/6/instance/1/commandclass/152/value/115736592/,{ "Label": "Secured", "Value": true, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Bool", "Instance": 1, "CommandClass": "COMMAND_CLASS_SECURITY", "Index": 0, "Node": 6, "Genre": "System", "Help": "Is Communication with Device Encrypted", "ValueIDKey": 115736592, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593368054} -OpenZWave/1/node/6/instance/1/commandclass/113/,{ "Instance": 1, "CommandClassId": 113, "CommandClass": "COMMAND_CLASS_NOTIFICATION", "CommandClassVersion": 4, "TimeStamp": 1593368054} -OpenZWave/1/node/6/instance/1/commandclass/113/value/72057594144636945/,{ "Label": "Previous Event Cleared", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_NOTIFICATION", "Index": 256, "Node": 6, "Genre": "User", "Help": "Previous Event that was sent", "ValueIDKey": 72057594144636945, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593368054} -OpenZWave/1/node/6/instance/1/commandclass/113/value/74590868935032849/,{ "Label": "Sensor ID", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_NOTIFICATION", "Index": 265, "Node": 6, "Genre": "User", "Help": "The ID of the Sensor that triggered the alert", "ValueIDKey": 74590868935032849, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593368054} -OpenZWave/1/node/6/instance/1/commandclass/113/value/1688849966972948/,{ "Label": "Access Control", "Value": { "List": [ { "Value": 0, "Label": "Clear" }, { "Value": 68, "Label": "Barrier Failed Operation" }, { "Value": 69, "Label": "Barrier Unattended Operation Disabled" }, { "Value": 70, "Label": "Barrier Malfunction" }, { "Value": 73, "Label": "Barrier Sensor Not Detected" }, { "Value": 74, "Label": "Barrier Battery Low" } ], "Selected": "Clear", "Selected_id": 0 }, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_NOTIFICATION", "Index": 6, "Node": 6, "Genre": "User", "Help": "Access Control Alerts", "ValueIDKey": 1688849966972948, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593368054} -OpenZWave/1/node/6/instance/1/commandclass/113/value/1970324943683604/,{ "Label": "Home Security", "Value": { "List": [ { "Value": 0, "Label": "Clear" }, { "Value": 3, "Label": "Tampering - Cover Removed" } ], "Selected": "Clear", "Selected_id": 0 }, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_NOTIFICATION", "Index": 7, "Node": 6, "Genre": "User", "Help": "Home Security Alerts", "ValueIDKey": 1970324943683604, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593368054} -OpenZWave/1/node/6/association/1/,{ "Name": "Group 1", "Help": "", "MaxAssociations": 1, "Members": [ "1.0" ], "TimeStamp": 1593368054} diff --git a/tests/components/ozw/fixtures/cover_network_dump.csv b/tests/components/ozw/fixtures/cover_network_dump.csv deleted file mode 100644 index 6c469361ed5..00000000000 --- a/tests/components/ozw/fixtures/cover_network_dump.csv +++ /dev/null @@ -1,134 +0,0 @@ -OpenZWave/1/status/,{ "OpenZWave_Version": "1.6.1173", "OZWDaemon_Version": "0.1.149", "QTOpenZWave_Version": "1.2.0", "QT_Version": "5.12.5", "Status": "driverAllNodesQueried", "TimeStamp": 1593370382, "ManufacturerSpecificDBReady": true, "homeID": 3716538409, "getControllerNodeId": 1, "getSUCNodeId": 1, "isPrimaryController": true, "isBridgeController": false, "hasExtendedTXStatistics": true, "getControllerLibraryVersion": "Z-Wave 4.61", "getControllerLibraryType": "Static Controller", "getControllerPath": "/dev/ttyACM0"} -OpenZWave/1/node/37/,{ "NodeID": 37, "NodeQueryStage": "Complete", "isListening": true, "isFlirs": false, "isBeaming": true, "isRouting": true, "isSecurityv1": false, "isZWavePlus": true, "isNIFRecieved": true, "isAwake": true, "isFailed": false, "MetaData": { "OZWInfoURL": "http://www.openzwave.com/device-database/010F:1000:0303", "ZWAProductURL": "", "ProductPic": "images/fibaro/fgr223.png", "Description": "FIBARO Roller Shutter 3 is a device designed to control roller blinds, awnings, venetian blinds, gates and other single phase, AC powered devices. Roller Shutter 3 allows precise positioning of roller blinds or venetian blind lamellas. The device is equipped with power and energy monitoring. It allows to control connected devices either via the Z-Wave network or via a switch connected directly to it. Main features of FIBARO Roller Shutter 3: - Compatible with any Z-Wave or Z-Wave Plus Controller, - Supports Z-Wave network Security Modes: S0 with AES-128 encryption and S2 with PRNG-based encryption, - To be installed with roller blind motors with electronic or mechanical limit switches, - Advanced microprocessor control, - Active power and energy metering functionality, - Works with various types of switches – momentary, toggle and dedicated roller blind switches, - To be installed in wall switch boxes, - Works as a Z-Wave signal repeater.", "ProductManualURL": "https://Products.Z-WaveAlliance.org/ProductManual/File?folder=&filename=Manuals/3278/FGR-223-EN-T-v1.3.pdf", "ProductPageURL": "", "InclusionHelp": "To add the device to the Z-Wave network manually: 1. Power the device. 2. Identify the S1 switch. 3. Set the main controller in (Security/non-Security Mode) add mode (see the controller’s manual). 4. Quickly, triple click the S1 switch. 5. If you are adding in Security S2, scan the DSK QR code or input the underlined part of the DSK (label on the bottom of the box). 6. Wait for the adding process to end. 7. Successful adding will be confirmed by the Z-Wave controller’s message. To add the device to the Z-Wave network using Smart Start: 1. Set the main controller in Security S2 Authenticated add mode (see the controller’s manual) 2. Scan the DSK QR code or input the underlined part of the DSK 3. (label on the bottom of the box). 4. Power the device (turn on the mains voltage). 5. LED will start blinking yellow, wait for the adding process to end. 6. Successful adding will be confirmed by the Z-Wave controller’s message.", "ExclusionHelp": "To remove the device from the Z-Wave network: 1. Make sure the device is powered. 2. Identify the S1 switch. 3. Set the main controller in remove mode (see the controller’s manual). 4. Quickly, triple click the S1 switch. 5. Wait for the removing process to end. 6. Successful removing will be confirmed by the Z-Wave controller’s message.", "ResetHelp": "Reset procedure allows to restore the device back to its factory settings, which means all information about the Z-Wave controller and user configuration will be deleted. 1. Switch off the mains voltage (disable the fuse). 2. Remove the device from the wall switch box. 3. Switch on the mains voltage. 4. Press and hold the B-button to enter the menu. 5. Wait for the LED indicator to glow yellow. 6. Quickly release and click the B-button again. 7. After few seconds the device will be restarted, which is signalled with the red LED indicator colour. Please use this procedure only when the network primary controller is missing or otherwise inoperable.\"", "WakeupHelp": "FIBARO Roller Shutter 3 is powered with mains voltage so it is always awake.", "ProductSupportURL": "", "Frequency": "", "Name": "Roller Shutter 3", "ProductPicBase64": "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" }, "Event": "nodeNaming", "TimeStamp": 1593370496, "NodeManufacturerName": "FIBARO System", "NodeProductName": "FGR223 Roller Shutter Controller 3", "NodeBasicString": "Routing Slave", "NodeBasic": 4, "NodeGenericString": "Multilevel Switch", "NodeGeneric": 17, "NodeSpecificString": "Motor Control Class B", "NodeSpecific": 6, "NodeManufacturerID": "0x010f", "NodeProductType": "0x0303", "NodeProductID": "0x1000", "NodeBaudRate": 100000, "NodeVersion": 4, "NodeGroups": 3, "NodeName": "", "NodeLocation": "", "NodeDeviceTypeString": "Window Covering Endpoint Aware", "NodeDeviceType": 6400, "NodeRole": 5, "NodeRoleString": "Always On Slave", "NodePlusType": 0, "NodePlusTypeString": "Z-Wave+ node", "Neighbors": [ 3, 6, 7, 12, 13, 14, 25, 34 ], "Neighbors": [ 3, 6, 7, 12, 13, 14, 25, 34 ], "Neighbors": [ 3, 6, 7, 12, 13, 14, 25, 34 ]} -OpenZWave/1/node/37/instance/1/,{ "Instance": 1, "TimeStamp": 1593369823} -OpenZWave/1/node/37/instance/1/commandclass/112/,{ "Instance": 1, "CommandClassId": 112, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "CommandClassVersion": 1, "TimeStamp": 1593369823} -OpenZWave/1/node/37/instance/1/commandclass/112/value/5629500165193748/,{ "Label": "Switch type", "Value": { "List": [ { "Value": 0, "Label": "Momentary switches" }, { "Value": 1, "Label": "Toggle switches" }, { "Value": 2, "Label": "Single, momentary switch" } ], "Selected": "Momentary switches", "Selected_id": 0 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 20, "Node": 37, "Genre": "Config", "Help": "This parameter defines as what type the device should treat the switch connected to the S1 and S2 terminals. This parameter is not relevant in gate operating modes (parameter 151 set to 3 or 4). In this case switch always works as a momentary and has to be connected to S1 terminal.", "ValueIDKey": 5629500165193748, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370186} -OpenZWave/1/node/37/instance/1/commandclass/112/value/6755400072036372/,{ "Label": "Inputs orientation", "Value": { "List": [ { "Value": 0, "Label": "Default" }, { "Value": 1, "Label": "Reversed" } ], "Selected": "Default", "Selected_id": 0 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 24, "Node": 37, "Genre": "Config", "Help": "This parameter allows reversing the operation of switches connected to S1 and S2 without changing the wiring. Default: S1 -> 1st channel, S2 -> 2nd channel. Reversed: S1 -> 2nd channel, S2 -> 1st channel.", "ValueIDKey": 6755400072036372, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370377} -OpenZWave/1/node/37/instance/1/commandclass/112/value/7036875048747028/,{ "Label": "Outputs orientation", "Value": { "List": [ { "Value": 0, "Label": "Default" }, { "Value": 1, "Label": "Reversed" } ], "Selected": "Reversed", "Selected_id": 1 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 25, "Node": 37, "Genre": "Config", "Help": "This parameter allows reversing the operation of Q1 and Q2 without changing the wiring (in case of invalid motor connection) to ensure proper operation. - Default: Q1 -> 1st channel, Q2 -> 2nd channel. - Reversed: Q1 -> 2nd channel, Q2 -> 1st channel.", "ValueIDKey": 7036875048747028, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370377} -OpenZWave/1/node/37/instance/1/commandclass/112/value/8444249932300307/,{ "Label": "Alarm configuration - 1st slot", "Value": 0, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 30, "Node": 37, "Genre": "Config", "Help": "This parameter determines to which alarm frames and how the device should react. The parameters consist of 4 bytes, three most significant bytes are set according to the official Z-Wave protocol specification. 1B [MSB] - notification Type. 2B - notification Status. 3B - Event/State Parameters. 4B [lSB] action: 0 - no action, 1 - open blinds, 2 - close blinds. Default setting: [0x00, 0x00, 0x00, 0x00]", "ValueIDKey": 8444249932300307, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370377} -OpenZWave/1/node/37/instance/1/commandclass/112/value/8725724909010963/,{ "Label": "Alarm configuration - 2st slot (water)", "Value": 100597760, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 31, "Node": 37, "Genre": "Config", "Help": "This parameter determines to which alarm frames and how the device should react. The parameters consist of 4 bytes, three most significant bytes are set according to the official Z-Wave protocol specification. 1B [MSB] - notification Type. 2B - notification Status. 3B - Event/State Parameters. 4B [lSB] action: 0 - no action, 1 - open blinds, 2 - close blinds. Default setting: [0x05, 0xFF, 0x00, 0x00] (Water Alarm, any notification, no action)", "ValueIDKey": 8725724909010963, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370378} -OpenZWave/1/node/37/instance/1/commandclass/112/value/9007199885721619/,{ "Label": "Alarm configuration - 3st slot (smoke)", "Value": 33488896, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 32, "Node": 37, "Genre": "Config", "Help": "This parameter determines to which alarm frames and how the device should react. The parameters consist of 4 bytes, three most significant bytes are set according to the official Z-Wave protocol specification. 1B [MSB] - notification Type. 2B - notification Status. 3B - Event/State Parameters. 4B [lSB] action: 0 - no action, 1 - open blinds, 2 - close blinds. Default setting: [0x01, 0xFF, 0x00, 0x00] (Smoke Alarm, any notification, no action)", "ValueIDKey": 9007199885721619, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370378} -OpenZWave/1/node/37/instance/1/commandclass/112/value/9288674862432275/,{ "Label": "Alarm configuration - 4st slot (CO)", "Value": 50266112, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 33, "Node": 37, "Genre": "Config", "Help": "This parameter determines to which alarm frames and how the device should react. The parameters consist of 4 bytes, three most significant bytes are set according to the official Z-Wave protocol specification. 1B [MSB] - notification Type. 2B - notification Status. 3B - Event/State Parameters. 4B [lSB] action: 0 - no action, 1 - open blinds, 2 - close blinds. Default setting: [0x02, 0xFF, 0x00, 0x00] (CO Alarm, any notification, no action)", "ValueIDKey": 9288674862432275, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370378} -OpenZWave/1/node/37/instance/1/commandclass/112/value/9570149839142931/,{ "Label": "Alarm configuration - 5st slot (heat)", "Value": 83820544, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 34, "Node": 37, "Genre": "Config", "Help": "This parameter determines to which alarm frames and how the device should react. The parameters consist of 4 bytes, three most significant bytes are set according to the official Z-Wave protocol specification. 1B [MSB] - notification Type. 2B - notification Status. 3B - Event/State Parameters. 4B [lSB] action: 0 - no action, 1 - open blinds, 2 - close blinds. Default setting: [0x04, 0xFF, 0x00, 0x00] (Heat Alarm, any notification, no action)", "ValueIDKey": 9570149839142931, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370378} -OpenZWave/1/node/37/instance/1/commandclass/112/value/11258999699406865/,{ "Label": "S1 switch - scenes sent", "Value": 0, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 40, "Node": 37, "Genre": "Config", "Help": "This parameter determines which actions result in sending scene IDs assigned to them. Sum of: 1 - Key pressed 1 time. 2 - Key pressed 2 times. 4 - Key pressed 3 times. 8 - Key hold down and key released. Default setting: 0.", "ValueIDKey": 11258999699406865, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370378} -OpenZWave/1/node/37/instance/1/commandclass/112/value/11540474676117521/,{ "Label": "S2 switch - scenes sent", "Value": 0, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 41, "Node": 37, "Genre": "Config", "Help": "This parameter determines which actions result in sending scene IDs assigned to them. Sum of: 1 - Key pressed 1 time. 2 - Key pressed 2 times. 4 - Key pressed 3 times. 8 - Key hold down and key released. Default setting: 0.", "ValueIDKey": 11540474676117521, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370379} -OpenZWave/1/node/37/instance/1/commandclass/112/value/16888499233619988/,{ "Label": "Measuring power consumed by the device itself", "Value": { "List": [ { "Value": 0, "Label": "Disabled" }, { "Value": 1, "Label": "Enabled" } ], "Selected": "Disabled", "Selected_id": 0 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 60, "Node": 37, "Genre": "Config", "Help": "This parameter determines whether the power metering should include the amount of active power consumed by the device itself.", "ValueIDKey": 16888499233619988, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370379} -OpenZWave/1/node/37/instance/1/commandclass/112/value/17169974210330646/,{ "Label": "Power reports - on change", "Value": 15, "Units": "%", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 500, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 61, "Node": 37, "Genre": "Config", "Help": "This parameter determines the minimum change in consumed power that will result in sending new power report to the main controller. For loads under 50W, the parameter is not relevant and reports are sent every 5W change. Power reports are sent no often than every 30 seconds. 0: reports are disabled. 1-500 (1-500%): change in power. Default setting: 15.", "ValueIDKey": 17169974210330646, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370379} -OpenZWave/1/node/37/instance/1/commandclass/112/value/17451449187041302/,{ "Label": "Power reports - periodic", "Value": 3600, "Units": "second", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 32400, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 62, "Node": 37, "Genre": "Config", "Help": "This parameter determines in what time intervals the periodic power reports are sent to the main controller. Periodic reports do not depend on power change (parameter 61). 0: periodic reports are disabled 30-32400 (30-32400s): report interval. Default setting: 3600 (1h).", "ValueIDKey": 17451449187041302, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370379} -OpenZWave/1/node/37/instance/1/commandclass/112/value/18295874117173270/,{ "Label": "Energy reports - on change", "Value": 10, "Units": "0.01 kWh", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 500, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 65, "Node": 37, "Genre": "Config", "Help": "This parameter determines the minimum change in consumed energy that will result in sending new energy report to the main controller. 0: reports are disabled. 1-500 (0.01 - 5 kWh): change in energy. Default setting: 10 (0.1 kWh).", "ValueIDKey": 18295874117173270, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370380} -OpenZWave/1/node/37/instance/1/commandclass/112/value/18577349093883926/,{ "Label": "Energy reports - periodic", "Value": 3600, "Units": "second", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 32400, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 66, "Node": 37, "Genre": "Config", "Help": "This parameter determines in what time intervals the periodic energy reports are sent to the main controller. Periodic reports do not depend on energy change (parameter 65). 0: periodic reports are disabled. 30-32400 (30-32400s): report interval. Default setting: 3600 (1h)", "ValueIDKey": 18577349093883926, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370380} -OpenZWave/1/node/37/instance/1/commandclass/112/value/42221247137579028/,{ "Label": "Force calibration", "Value": { "List": [ { "Value": 0, "Label": "Device is not calibrated" }, { "Value": 1, "Label": "Device is calibrated" }, { "Value": 2, "Label": "Force device calibration" } ], "Selected": "Device is calibrated", "Selected_id": 1 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 150, "Node": 37, "Genre": "Config", "Help": "By setting this parameter to 2 the device enters the calibration mode. The parameter relevant only if the device is set to work in positioning mode (parameter 151 set to 1, 2 or 4).", "ValueIDKey": 42221247137579028, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370129} -OpenZWave/1/node/37/instance/1/commandclass/112/value/42502722114289684/,{ "Label": "Operating mode", "Value": { "List": [ { "Value": 1, "Label": "Roller blind" }, { "Value": 2, "Label": "Venetian blind" }, { "Value": 3, "Label": "gate without positioning" }, { "Value": 4, "Label": "gate with positioning" }, { "Value": 5, "Label": "roller blind with built-in driver" }, { "Value": 6, "Label": "roller blind with built-in driver (impulse)" } ], "Selected": "Roller blind", "Selected_id": 1 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 151, "Node": 37, "Genre": "Config", "Help": "This parameter allows adjusting operation according to the connected device.", "ValueIDKey": 42502722114289684, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370380} -OpenZWave/1/node/37/instance/1/commandclass/112/value/42784197091000339/,{ "Label": "Venetian blind - time of full turn of the slats", "Value": 150, "Units": "0.1 second", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 90000, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 152, "Node": 37, "Genre": "Config", "Help": "For Venetian blinds (parameter 151 set to 2) the parameter determines time of full turn cycle of the slats. For gates (parameter 151 set to 3 or 4) the parameter determines time after which open gate will start closing automatically (if set to 0, gate will not close). The parameter is irrelevant for other modes. 0-90000 (0 - 900s, every 0.01s) time of turn. Default setting: 150 (1.5s).", "ValueIDKey": 42784197091000339, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370381} -OpenZWave/1/node/37/instance/1/commandclass/112/value/43065672067710996/,{ "Label": "Set slats back to previous position", "Value": { "List": [ { "Value": 0, "Label": "Only in case of the main controller operation" }, { "Value": 1, "Label": "In case of the main controller operation, momentary switch operation, or when the limit switch is reached." }, { "Value": 2, "Label": "In case of the main controller operation, momentary switch operation, when the limit switch is reached or after receiving the Switch Multilevel Stop control frame" } ], "Selected": "In case of the main controller operation, momentary switch operation, or when the limit switch is reached.", "Selected_id": 1 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 153, "Node": 37, "Genre": "Config", "Help": "For Venetian blinds (parameter 151 set to 2) the parameter determines slats positioning in various situations. The parameter is irrelevant for other modes.", "ValueIDKey": 43065672067710996, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370381} -OpenZWave/1/node/37/instance/1/commandclass/112/value/43347147044421654/,{ "Label": "Delay motor stop after reaching end switch", "Value": 10, "Units": "0.1 second", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 600, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 154, "Node": 37, "Genre": "Config", "Help": "For blinds (parameter 151 set to 1, 2, 5 or 6) the parameter determines the time after which the motor will be stopped after end switch contacts are closed. For gates (parameter 151 set to 3 or 4) the parameter determines the time after which the gate will start closing automatically if S2 contacts are opened (if set to 0, gate will not close). 0-600 (0 - 60s). Default setting: 10 (1s).", "ValueIDKey": 43347147044421654, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370381} -OpenZWave/1/node/37/instance/1/commandclass/112/value/43628622021132310/,{ "Label": "Motor operation detection", "Value": 10, "Units": "watt", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 155, "Node": 37, "Genre": "Config", "Help": "Power threshold to be interpreted as reaching a limit switch. 0: reaching a limit switch will not be detected 1-255 (1-255W): report interval. Default setting: 10.", "ValueIDKey": 43628622021132310, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370381} -OpenZWave/1/node/37/instance/1/commandclass/112/value/43910096997842963/,{ "Label": "Time of up movement", "Value": 1500, "Units": "0.01 second", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 90000, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 156, "Node": 37, "Genre": "Config", "Help": "This parameter determines the time needed for roller blinds to reach the top. For modes with positioning value is set automatically during calibration, otherwise, it must be set manually. 1-90000 (0.01 - 900.00s). Default setting: 6000 (60s).", "ValueIDKey": 43910096997842963, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370140} -OpenZWave/1/node/37/instance/1/commandclass/112/value/44191571974553619/,{ "Label": "Time of down movement", "Value": 1318, "Units": "0.01 second", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 90000, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 157, "Node": 37, "Genre": "Config", "Help": "This parameter determines time needed for roller blinds to reach the bottom. For modes with positioning value is set automatically during calibration, otherwise, it must be set manually. 1-90000 (0.01 - 900.00s). Default setting: 6000 (60s).", "ValueIDKey": 44191571974553619, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370382} -OpenZWave/1/node/37/instance/1/commandclass/145/,{ "Instance": 1, "CommandClassId": 145, "CommandClass": "COMMAND_CLASS_MANUFACTURER_PROPRIETARY", "CommandClassVersion": 1, "TimeStamp": 1593369823} -OpenZWave/1/node/37/instance/1/commandclass/145/value/627326993/,{ "Label": "Venetian Blind slat position", "Value": 0, "Units": "%", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 100, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_PROPRIETARY", "Index": 0, "Node": 37, "Genre": "User", "Help": "", "ValueIDKey": 627326993, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369823} -OpenZWave/1/node/37/instance/1/commandclass/145/value/281475604037649/,{ "Label": "Venetian blind tilt position", "Value": 0, "Units": "%", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 100, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_PROPRIETARY", "Index": 1, "Node": 37, "Genre": "User", "Help": "", "ValueIDKey": 281475604037649, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369823} -OpenZWave/1/node/37/instance/1/commandclass/38/,{ "Instance": 1, "CommandClassId": 38, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "CommandClassVersion": 4, "TimeStamp": 1593369822} -OpenZWave/1/node/37/instance/1/commandclass/38/value/2533275424358417/,{ "Label": "Instance 1: Target Value", "Value": 99, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 9, "Node": 37, "Genre": "System", "Help": "", "ValueIDKey": 2533275424358417, "ReadOnly": true, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370645} -OpenZWave/1/node/37/instance/1/commandclass/38/value/1688850485837841/,{ "Label": "Instance 1: Step Size", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 6, "Node": 37, "Genre": "User", "Help": "How Many Percent Change when incrementing/decrementing the Level of a Device", "ValueIDKey": 1688850485837841, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369822} -OpenZWave/1/node/37/instance/1/commandclass/38/value/1970325462548504/,{ "Label": "Instance 1: Inc", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 7, "Node": 37, "Genre": "User", "Help": "Increment the Level of a Device", "ValueIDKey": 1970325462548504, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1593369822} -OpenZWave/1/node/37/instance/1/commandclass/38/value/2251800439259160/,{ "Label": "Instance 1: Dec", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 8, "Node": 37, "Genre": "User", "Help": "Decrement the Level of a Device", "ValueIDKey": 2251800439259160, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1593369822} -OpenZWave/1/node/37/instance/1/commandclass/38/value/1407375517515793/,{ "Label": "Instance 1: Dimming Duration", "Value": 16, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 5, "Node": 37, "Genre": "System", "Help": "Duration taken when changing the Level of a Device", "ValueIDKey": 1407375517515793, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370645} -OpenZWave/1/node/37/instance/1/commandclass/38/value/625573905/,{ "Label": "Instance 1: Level", "Value": 0, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 0, "Node": 37, "Genre": "User", "Help": "The Current Level of the Device", "ValueIDKey": 625573905, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370642} -OpenZWave/1/node/37/instance/1/commandclass/38/value/281475602284568/,{ "Label": "Instance 1: Up", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 1, "Node": 37, "Genre": "User", "Help": "Increase the Brightness of the Device", "ValueIDKey": 281475602284568, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1593369822} -OpenZWave/1/node/37/instance/1/commandclass/38/value/562950578995224/,{ "Label": "Instance 1: Down", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 2, "Node": 37, "Genre": "User", "Help": "Decrease the Brightness of the Device", "ValueIDKey": 562950578995224, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1593369822} -OpenZWave/1/node/37/instance/1/commandclass/38/value/844425564094480/,{ "Label": "Instance 1: Ignore Start Level", "Value": true, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Bool", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 3, "Node": 37, "Genre": "System", "Help": "Ignore the Start Level of the Device when increasing/decreasing brightness", "ValueIDKey": 844425564094480, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369822} -OpenZWave/1/node/37/instance/1/commandclass/38/value/1125900540805137/,{ "Label": "Instance 1: Start Level", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 4, "Node": 37, "Genre": "System", "Help": "Start Level when Changing the Brightness of a Device", "ValueIDKey": 1125900540805137, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369822} -OpenZWave/1/node/37/instance/1/commandclass/91/,{ "Instance": 1, "CommandClassId": 91, "CommandClass": "COMMAND_CLASS_CENTRAL_SCENE", "CommandClassVersion": 3, "TimeStamp": 1593369822} -OpenZWave/1/node/37/instance/1/commandclass/91/value/72057594664370195/,{ "Label": "Scene Count", "Value": 2, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CENTRAL_SCENE", "Index": 256, "Node": 37, "Genre": "User", "Help": "", "ValueIDKey": 72057594664370195, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369822} -OpenZWave/1/node/37/instance/1/commandclass/91/value/72339069645275155/,{ "Label": "Scene Reset Timeout", "Value": 1000, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CENTRAL_SCENE", "Index": 257, "Node": 37, "Genre": "Config", "Help": "", "ValueIDKey": 72339069645275155, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369822} -OpenZWave/1/node/37/instance/1/commandclass/91/value/281475603152916/,{ "Label": "Scene 1", "Value": { "List": [ { "Value": 0, "Label": "Inactive" }, { "Value": 1, "Label": "Pressed 1 Time" }, { "Value": 2, "Label": "Key Released" }, { "Value": 3, "Label": "Key Held down" }, { "Value": 4, "Label": "Pressed 2 Times" }, { "Value": 5, "Label": "Pressed 3 Times" } ], "Selected": "Inactive", "Selected_id": 0 }, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CENTRAL_SCENE", "Index": 1, "Node": 37, "Genre": "User", "Help": "", "ValueIDKey": 281475603152916, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369822} -OpenZWave/1/node/37/instance/1/commandclass/91/value/562950579863572/,{ "Label": "Scene 2", "Value": { "List": [ { "Value": 0, "Label": "Inactive" }, { "Value": 1, "Label": "Pressed 1 Time" }, { "Value": 2, "Label": "Key Released" }, { "Value": 3, "Label": "Key Held down" }, { "Value": 4, "Label": "Pressed 2 Times" }, { "Value": 5, "Label": "Pressed 3 Times" } ], "Selected": "Inactive", "Selected_id": 0 }, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CENTRAL_SCENE", "Index": 2, "Node": 37, "Genre": "User", "Help": "", "ValueIDKey": 562950579863572, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369822} -OpenZWave/1/node/37/instance/1/commandclass/94/,{ "Instance": 1, "CommandClassId": 94, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "CommandClassVersion": 1, "TimeStamp": 1593369822} -OpenZWave/1/node/37/instance/1/commandclass/94/value/634880017/,{ "Label": "Instance 1: ZWave+ Version", "Value": 1, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 0, "Node": 37, "Genre": "System", "Help": "ZWave+ Version Supported on the Device", "ValueIDKey": 634880017, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369822} -OpenZWave/1/node/37/instance/1/commandclass/94/value/281475611590678/,{ "Label": "Instance 1: InstallerIcon", "Value": 6400, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 1, "Node": 37, "Genre": "System", "Help": "Icon File to use for the Installer Application", "ValueIDKey": 281475611590678, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369822} -OpenZWave/1/node/37/instance/1/commandclass/94/value/562950588301334/,{ "Label": "Instance 1: UserIcon", "Value": 6400, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 2, "Node": 37, "Genre": "System", "Help": "Icon File to use for the User Application", "ValueIDKey": 562950588301334, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369822} -OpenZWave/1/node/37/instance/1/commandclass/114/,{ "Instance": 1, "CommandClassId": 114, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "CommandClassVersion": 2, "TimeStamp": 1593369823} -OpenZWave/1/node/37/instance/1/commandclass/114/value/635207699/,{ "Label": "Loaded Config Revision", "Value": 4, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 0, "Node": 37, "Genre": "System", "Help": "Revision of the Config file currently loaded", "ValueIDKey": 635207699, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369823} -OpenZWave/1/node/37/instance/1/commandclass/114/value/281475611918355/,{ "Label": "Config File Revision", "Value": 4, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 1, "Node": 37, "Genre": "System", "Help": "Revision of the Config file on the File System", "ValueIDKey": 281475611918355, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369823} -OpenZWave/1/node/37/instance/1/commandclass/114/value/562950588629011/,{ "Label": "Latest Available Config File Revision", "Value": 4, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 2, "Node": 37, "Genre": "System", "Help": "Latest Revision of the Config file available for download", "ValueIDKey": 562950588629011, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369823} -OpenZWave/1/node/37/instance/1/commandclass/114/value/844425565339671/,{ "Label": "Device ID", "Value": "", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 3, "Node": 37, "Genre": "System", "Help": "Manufacturer Specific Device ID/Model", "ValueIDKey": 844425565339671, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369823} -OpenZWave/1/node/37/instance/1/commandclass/114/value/1125900542050327/,{ "Label": "Serial Number", "Value": "0000000000000dd0", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 4, "Node": 37, "Genre": "System", "Help": "Device Serial Number", "ValueIDKey": 1125900542050327, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369823} -OpenZWave/1/node/37/instance/1/commandclass/115/,{ "Instance": 1, "CommandClassId": 115, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "CommandClassVersion": 1, "TimeStamp": 1593369823} -OpenZWave/1/node/37/instance/1/commandclass/115/value/635224084/,{ "Label": "Powerlevel", "Value": { "List": [ { "Value": 0, "Label": "Normal" }, { "Value": 1, "Label": "-1dB" }, { "Value": 2, "Label": "-2dB" }, { "Value": 3, "Label": "-3dB" }, { "Value": 4, "Label": "-4dB" }, { "Value": 5, "Label": "-5dB" }, { "Value": 6, "Label": "-6dB" }, { "Value": 7, "Label": "-7dB" }, { "Value": 8, "Label": "-8dB" }, { "Value": 9, "Label": "-9dB" } ], "Selected": "Normal", "Selected_id": 0 }, "Units": "dB", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 0, "Node": 37, "Genre": "System", "Help": "Output RF PowerLevel", "ValueIDKey": 635224084, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370033} -OpenZWave/1/node/37/instance/1/commandclass/115/value/281475611934737/,{ "Label": "Timeout", "Value": 0, "Units": "seconds", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 1, "Node": 37, "Genre": "System", "Help": "Timeout till the PowerLevel is reset to Normal", "ValueIDKey": 281475611934737, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370033} -OpenZWave/1/node/37/instance/1/commandclass/115/value/562950588645400/,{ "Label": "Set Powerlevel", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 2, "Node": 37, "Genre": "System", "Help": "Apply the Output PowerLevel and Timeout Values", "ValueIDKey": 562950588645400, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1593369823} -OpenZWave/1/node/37/instance/1/commandclass/115/value/844425565356049/,{ "Label": "Test Node", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 3, "Node": 37, "Genre": "System", "Help": "Node to Perform a test against", "ValueIDKey": 844425565356049, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369823} -OpenZWave/1/node/37/instance/1/commandclass/115/value/1125900542066708/,{ "Label": "Test Powerlevel", "Value": { "List": [ { "Value": 0, "Label": "Normal" }, { "Value": 1, "Label": "-1dB" }, { "Value": 2, "Label": "-2dB" }, { "Value": 3, "Label": "-3dB" }, { "Value": 4, "Label": "-4dB" }, { "Value": 5, "Label": "-5dB" }, { "Value": 6, "Label": "-6dB" }, { "Value": 7, "Label": "-7dB" }, { "Value": 8, "Label": "-8dB" }, { "Value": 9, "Label": "-9dB" } ], "Selected": "Normal", "Selected_id": 0 }, "Units": "dB", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 4, "Node": 37, "Genre": "System", "Help": "PowerLevel to use for the Test", "ValueIDKey": 1125900542066708, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369823} -OpenZWave/1/node/37/instance/1/commandclass/115/value/1407375518777366/,{ "Label": "Frame Count", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 5, "Node": 37, "Genre": "System", "Help": "How Many Messages to send to the Node for the Test", "ValueIDKey": 1407375518777366, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369823} -OpenZWave/1/node/37/instance/1/commandclass/115/value/1688850495488024/,{ "Label": "Test", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 6, "Node": 37, "Genre": "System", "Help": "Perform a PowerLevel Test against the a Node", "ValueIDKey": 1688850495488024, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1593369823} -OpenZWave/1/node/37/instance/1/commandclass/115/value/1970325472198680/,{ "Label": "Report", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 7, "Node": 37, "Genre": "System", "Help": "Get the results of the latest PowerLevel Test against a Node", "ValueIDKey": 1970325472198680, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1593369823} -OpenZWave/1/node/37/instance/1/commandclass/115/value/2251800448909332/,{ "Label": "Test Status", "Value": { "List": [ { "Value": 0, "Label": "Failed" }, { "Value": 1, "Label": "Success" }, { "Value": 2, "Label": "In Progress" } ], "Selected": "Failed", "Selected_id": 0 }, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 8, "Node": 37, "Genre": "System", "Help": "The Current Status of the last PowerNode Test Executed", "ValueIDKey": 2251800448909332, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369823} -OpenZWave/1/node/37/instance/1/commandclass/115/value/2533275425619990/,{ "Label": "Acked Frames", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 9, "Node": 37, "Genre": "System", "Help": "Number of Messages successfully Acked by the Target Node", "ValueIDKey": 2533275425619990, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369823} -OpenZWave/1/node/37/instance/1/commandclass/117/,{ "Instance": 1, "CommandClassId": 117, "CommandClass": "COMMAND_CLASS_PROTECTION", "CommandClassVersion": 1, "TimeStamp": 1593369823} -OpenZWave/1/node/37/instance/1/commandclass/117/value/635256852/,{ "Label": "Protection", "Value": { "List": [ { "Value": 0, "Label": "Unprotected" }, { "Value": 1, "Label": "Protection by Sequence" }, { "Value": 2, "Label": "No Operation Possible" } ], "Selected": "Unprotected", "Selected_id": 0 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_PROTECTION", "Index": 0, "Node": 37, "Genre": "System", "Help": "Protect a device against unintentional control", "ValueIDKey": 635256852, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370033} -OpenZWave/1/node/37/instance/1/commandclass/134/,{ "Instance": 1, "CommandClassId": 134, "CommandClass": "COMMAND_CLASS_VERSION", "CommandClassVersion": 1, "TimeStamp": 1593369823} -OpenZWave/1/node/37/instance/1/commandclass/134/value/635535383/,{ "Label": "Library Version", "Value": "3", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 0, "Node": 37, "Genre": "System", "Help": "Z-Wave Library Version", "ValueIDKey": 635535383, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369823} -OpenZWave/1/node/37/instance/1/commandclass/134/value/281475612246039/,{ "Label": "Protocol Version", "Value": "6.02", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 1, "Node": 37, "Genre": "System", "Help": "Z-Wave Protocol Version", "ValueIDKey": 281475612246039, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369823} -OpenZWave/1/node/37/instance/1/commandclass/134/value/562950588956695/,{ "Label": "Application Version", "Value": "5.01", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 2, "Node": 37, "Genre": "System", "Help": "Application Version", "ValueIDKey": 562950588956695, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369823} -OpenZWave/1/node/37/instance/1/commandclass/152/,{ "Instance": 1, "CommandClassId": 152, "CommandClass": "COMMAND_CLASS_SECURITY", "CommandClassVersion": 1, "TimeStamp": 1593369823} -OpenZWave/1/node/37/instance/1/commandclass/152/value/635830288/,{ "Label": "Instance 1: Secured", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Bool", "Instance": 1, "CommandClass": "COMMAND_CLASS_SECURITY", "Index": 0, "Node": 37, "Genre": "System", "Help": "Is Communication with Device Encrypted", "ValueIDKey": 635830288, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369823} -OpenZWave/1/node/37/instance/1/commandclass/50/,{ "Instance": 1, "CommandClassId": 50, "CommandClass": "COMMAND_CLASS_METER", "CommandClassVersion": 3, "TimeStamp": 1593369822} -OpenZWave/1/node/37/instance/1/commandclass/50/value/625770514/,{ "Label": "Electric - kWh", "Value": 0.0, "Units": "kWh", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_METER", "Index": 0, "Node": 37, "Genre": "User", "Help": "", "ValueIDKey": 625770514, "ReadOnly": true, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370034} -OpenZWave/1/node/37/instance/1/commandclass/50/value/562950579191826/,{ "Label": "Electric - W", "Value": 0.0, "Units": "W", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_METER", "Index": 2, "Node": 37, "Genre": "User", "Help": "", "ValueIDKey": 562950579191826, "ReadOnly": true, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370034} -OpenZWave/1/node/37/instance/1/commandclass/50/value/72057594663698448/,{ "Label": "Exporting", "Value": false, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Bool", "Instance": 1, "CommandClass": "COMMAND_CLASS_METER", "Index": 256, "Node": 37, "Genre": "User", "Help": "", "ValueIDKey": 72057594663698448, "ReadOnly": true, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370034} -OpenZWave/1/node/37/instance/1/commandclass/50/value/72339069648797720/,{ "Label": "Reset", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_METER", "Index": 257, "Node": 37, "Genre": "System", "Help": "", "ValueIDKey": 72339069648797720, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1593369822} -OpenZWave/1/node/37/instance/1/commandclass/113/,{ "Instance": 1, "CommandClassId": 113, "CommandClass": "COMMAND_CLASS_NOTIFICATION", "CommandClassVersion": 8, "TimeStamp": 1593369823} -OpenZWave/1/node/37/instance/1/commandclass/113/value/72057594664730641/,{ "Label": "Previous Event Cleared", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_NOTIFICATION", "Index": 256, "Node": 37, "Genre": "User", "Help": "Previous Event that was sent", "ValueIDKey": 72057594664730641, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369823} -OpenZWave/1/node/37/instance/1/commandclass/113/value/2251800440487956/,{ "Label": "Power Management", "Value": { "List": [ { "Value": 0, "Label": "Clear" }, { "Value": 6, "Label": "Over Current Detected" } ], "Selected": "Clear", "Selected_id": 0 }, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_NOTIFICATION", "Index": 8, "Node": 37, "Genre": "User", "Help": "Power Management Alerts", "ValueIDKey": 2251800440487956, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369823} -OpenZWave/1/node/37/instance/1/commandclass/113/value/74872344431837207/,{ "Label": "Error Code", "Value": "", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_NOTIFICATION", "Index": 266, "Node": 37, "Genre": "User", "Help": "The Error Code returned by the device", "ValueIDKey": 74872344431837207, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369823} -OpenZWave/1/node/37/instance/1/commandclass/113/value/2533275417198612/,{ "Label": "System", "Value": { "List": [ { "Value": 0, "Label": "Clear" }, { "Value": 3, "Label": "Hardware Failure Code" } ], "Selected": "Clear", "Selected_id": 0 }, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_NOTIFICATION", "Index": 9, "Node": 37, "Genre": "User", "Help": "System Alerts", "ValueIDKey": 2533275417198612, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369823} -OpenZWave/1/node/37/instance/2/,{ "Instance": 2, "TimeStamp": 1593369823} -OpenZWave/1/node/37/instance/2/commandclass/38/,{ "Instance": 2, "CommandClassId": 38, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "CommandClassVersion": 4, "TimeStamp": 1593369822} -OpenZWave/1/node/37/instance/2/commandclass/38/value/2533275424358433/,{ "Label": "Instance 2: Target Value", "Value": 99, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 2, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 9, "Node": 37, "Genre": "System", "Help": "", "ValueIDKey": 2533275424358433, "ReadOnly": true, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370142} -OpenZWave/1/node/37/instance/2/commandclass/38/value/1688850485837857/,{ "Label": "Instance 2: Step Size", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 2, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 6, "Node": 37, "Genre": "User", "Help": "How Many Percent Change when incrementing/decrementing the Level of a Device", "ValueIDKey": 1688850485837857, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369822} -OpenZWave/1/node/37/instance/2/commandclass/38/value/1970325462548520/,{ "Label": "Instance 2: Inc", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 2, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 7, "Node": 37, "Genre": "User", "Help": "Increment the Level of a Device", "ValueIDKey": 1970325462548520, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1593369822} -OpenZWave/1/node/37/instance/2/commandclass/38/value/2251800439259176/,{ "Label": "Instance 2: Dec", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 2, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 8, "Node": 37, "Genre": "User", "Help": "Decrement the Level of a Device", "ValueIDKey": 2251800439259176, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1593369822} -OpenZWave/1/node/37/instance/2/commandclass/38/value/1407375517515809/,{ "Label": "Instance 2: Dimming Duration", "Value": 254, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 2, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 5, "Node": 37, "Genre": "System", "Help": "Duration taken when changing the Level of a Device", "ValueIDKey": 1407375517515809, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370034} -OpenZWave/1/node/37/instance/2/commandclass/38/value/625573921/,{ "Label": "Instance 2: Level", "Value": 99, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 2, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 0, "Node": 37, "Genre": "User", "Help": "The Current Level of the Device", "ValueIDKey": 625573921, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370142} -OpenZWave/1/node/37/instance/2/commandclass/38/value/281475602284584/,{ "Label": "Instance 2: Up", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 2, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 1, "Node": 37, "Genre": "User", "Help": "Increase the Brightness of the Device", "ValueIDKey": 281475602284584, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1593369822} -OpenZWave/1/node/37/instance/2/commandclass/38/value/562950578995240/,{ "Label": "Instance 2: Down", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 2, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 2, "Node": 37, "Genre": "User", "Help": "Decrease the Brightness of the Device", "ValueIDKey": 562950578995240, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1593369822} -OpenZWave/1/node/37/instance/2/commandclass/38/value/844425564094496/,{ "Label": "Instance 2: Ignore Start Level", "Value": true, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Bool", "Instance": 2, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 3, "Node": 37, "Genre": "System", "Help": "Ignore the Start Level of the Device when increasing/decreasing brightness", "ValueIDKey": 844425564094496, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369822} -OpenZWave/1/node/37/instance/2/commandclass/38/value/1125900540805153/,{ "Label": "Instance 2: Start Level", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 2, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 4, "Node": 37, "Genre": "System", "Help": "Start Level when Changing the Brightness of a Device", "ValueIDKey": 1125900540805153, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369822} -OpenZWave/1/node/37/instance/2/commandclass/94/,{ "Instance": 2, "CommandClassId": 94, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "CommandClassVersion": 1, "TimeStamp": 1593369822} -OpenZWave/1/node/37/instance/2/commandclass/94/value/634880033/,{ "Label": "Instance 2: ZWave+ Version", "Value": 1, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 2, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 0, "Node": 37, "Genre": "System", "Help": "ZWave+ Version Supported on the Device", "ValueIDKey": 634880033, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369822} -OpenZWave/1/node/37/instance/2/commandclass/94/value/281475611590694/,{ "Label": "Instance 2: InstallerIcon", "Value": 6400, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 2, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 1, "Node": 37, "Genre": "System", "Help": "Icon File to use for the Installer Application", "ValueIDKey": 281475611590694, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369822} -OpenZWave/1/node/37/instance/2/commandclass/94/value/562950588301350/,{ "Label": "Instance 2: UserIcon", "Value": 6400, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 2, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 2, "Node": 37, "Genre": "System", "Help": "Icon File to use for the User Application", "ValueIDKey": 562950588301350, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369822} -OpenZWave/1/node/37/instance/2/commandclass/152/,{ "Instance": 2, "CommandClassId": 152, "CommandClass": "COMMAND_CLASS_SECURITY", "CommandClassVersion": 1, "TimeStamp": 1593369823} -OpenZWave/1/node/37/instance/2/commandclass/152/value/635830304/,{ "Label": "Instance 2: Secured", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Bool", "Instance": 2, "CommandClass": "COMMAND_CLASS_SECURITY", "Index": 0, "Node": 37, "Genre": "System", "Help": "Is Communication with Device Encrypted", "ValueIDKey": 635830304, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369823} -OpenZWave/1/node/37/instance/2/commandclass/50/,{ "Instance": 2, "CommandClassId": 50, "CommandClass": "COMMAND_CLASS_METER", "CommandClassVersion": 3, "TimeStamp": 1593369822} -OpenZWave/1/node/37/instance/2/commandclass/50/value/625770530/,{ "Label": "Electric - kWh", "Value": 0.0, "Units": "kWh", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 2, "CommandClass": "COMMAND_CLASS_METER", "Index": 0, "Node": 37, "Genre": "User", "Help": "", "ValueIDKey": 625770530, "ReadOnly": true, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370035} -OpenZWave/1/node/37/instance/2/commandclass/50/value/562950579191842/,{ "Label": "Electric - W", "Value": 0.0, "Units": "W", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 2, "CommandClass": "COMMAND_CLASS_METER", "Index": 2, "Node": 37, "Genre": "User", "Help": "", "ValueIDKey": 562950579191842, "ReadOnly": true, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370663} -OpenZWave/1/node/37/instance/2/commandclass/50/value/72057594663698464/,{ "Label": "Exporting", "Value": false, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Bool", "Instance": 2, "CommandClass": "COMMAND_CLASS_METER", "Index": 256, "Node": 37, "Genre": "User", "Help": "", "ValueIDKey": 72057594663698464, "ReadOnly": true, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370035} -OpenZWave/1/node/37/instance/2/commandclass/50/value/72339069648797736/,{ "Label": "Reset", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 2, "CommandClass": "COMMAND_CLASS_METER", "Index": 257, "Node": 37, "Genre": "System", "Help": "", "ValueIDKey": 72339069648797736, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1593369822} -OpenZWave/1/node/37/instance/2/commandclass/113/,{ "Instance": 2, "CommandClassId": 113, "CommandClass": "COMMAND_CLASS_NOTIFICATION", "CommandClassVersion": 8, "TimeStamp": 1593369823} -OpenZWave/1/node/37/instance/2/commandclass/113/value/72057594664730657/,{ "Label": "Previous Event Cleared", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 2, "CommandClass": "COMMAND_CLASS_NOTIFICATION", "Index": 256, "Node": 37, "Genre": "User", "Help": "Previous Event that was sent", "ValueIDKey": 72057594664730657, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369823} -OpenZWave/1/node/37/instance/2/commandclass/113/value/2251800440487972/,{ "Label": "Power Management", "Value": { "List": [ { "Value": 0, "Label": "Clear" }, { "Value": 6, "Label": "Over Current Detected" } ], "Selected": "Clear", "Selected_id": 0 }, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 2, "CommandClass": "COMMAND_CLASS_NOTIFICATION", "Index": 8, "Node": 37, "Genre": "User", "Help": "Power Management Alerts", "ValueIDKey": 2251800440487972, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369823} -OpenZWave/1/node/37/instance/2/commandclass/113/value/74872344431837223/,{ "Label": "Error Code", "Value": "", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 2, "CommandClass": "COMMAND_CLASS_NOTIFICATION", "Index": 266, "Node": 37, "Genre": "User", "Help": "The Error Code returned by the device", "ValueIDKey": 74872344431837223, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369823} -OpenZWave/1/node/37/instance/2/commandclass/113/value/2533275417198628/,{ "Label": "System", "Value": { "List": [ { "Value": 0, "Label": "Clear" }, { "Value": 3, "Label": "Hardware Failure Code" } ], "Selected": "Clear", "Selected_id": 0 }, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 2, "CommandClass": "COMMAND_CLASS_NOTIFICATION", "Index": 9, "Node": 37, "Genre": "User", "Help": "System Alerts", "ValueIDKey": 2533275417198628, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369823} -OpenZWave/1/node/37/instance/3/,{ "Instance": 3, "TimeStamp": 1593369823} -OpenZWave/1/node/37/instance/3/commandclass/38/,{ "Instance": 3, "CommandClassId": 38, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "CommandClassVersion": 4, "TimeStamp": 1593369822} -OpenZWave/1/node/37/instance/3/commandclass/38/value/2533275424358449/,{ "Label": "Instance 3: Target Value", "Value": 254, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 3, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 9, "Node": 37, "Genre": "System", "Help": "", "ValueIDKey": 2533275424358449, "ReadOnly": true, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370034} -OpenZWave/1/node/37/instance/3/commandclass/38/value/1688850485837873/,{ "Label": "Instance 3: Step Size", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 3, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 6, "Node": 37, "Genre": "User", "Help": "How Many Percent Change when incrementing/decrementing the Level of a Device", "ValueIDKey": 1688850485837873, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369822} -OpenZWave/1/node/37/instance/3/commandclass/38/value/1970325462548536/,{ "Label": "Instance 3: Inc", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 3, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 7, "Node": 37, "Genre": "User", "Help": "Increment the Level of a Device", "ValueIDKey": 1970325462548536, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1593369822} -OpenZWave/1/node/37/instance/3/commandclass/38/value/2251800439259192/,{ "Label": "Instance 3: Dec", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 3, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 8, "Node": 37, "Genre": "User", "Help": "Decrement the Level of a Device", "ValueIDKey": 2251800439259192, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1593369822} -OpenZWave/1/node/37/instance/3/commandclass/38/value/1407375517515825/,{ "Label": "Instance 3: Dimming Duration", "Value": 254, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 3, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 5, "Node": 37, "Genre": "System", "Help": "Duration taken when changing the Level of a Device", "ValueIDKey": 1407375517515825, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370034} -OpenZWave/1/node/37/instance/3/commandclass/38/value/625573937/,{ "Label": "Instance 3: Level", "Value": 99, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 3, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 0, "Node": 37, "Genre": "User", "Help": "The Current Level of the Device", "ValueIDKey": 625573937, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370034} -OpenZWave/1/node/37/instance/3/commandclass/38/value/281475602284600/,{ "Label": "Instance 3: Up", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 3, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 1, "Node": 37, "Genre": "User", "Help": "Increase the Brightness of the Device", "ValueIDKey": 281475602284600, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1593369822} -OpenZWave/1/node/37/instance/3/commandclass/38/value/562950578995256/,{ "Label": "Instance 3: Down", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 3, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 2, "Node": 37, "Genre": "User", "Help": "Decrease the Brightness of the Device", "ValueIDKey": 562950578995256, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1593369822} -OpenZWave/1/node/37/instance/3/commandclass/38/value/844425564094512/,{ "Label": "Instance 3: Ignore Start Level", "Value": true, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Bool", "Instance": 3, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 3, "Node": 37, "Genre": "System", "Help": "Ignore the Start Level of the Device when increasing/decreasing brightness", "ValueIDKey": 844425564094512, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369822} -OpenZWave/1/node/37/instance/3/commandclass/38/value/1125900540805169/,{ "Label": "Instance 3: Start Level", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 3, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 4, "Node": 37, "Genre": "System", "Help": "Start Level when Changing the Brightness of a Device", "ValueIDKey": 1125900540805169, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369822} -OpenZWave/1/node/37/instance/3/commandclass/94/,{ "Instance": 3, "CommandClassId": 94, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "CommandClassVersion": 1, "TimeStamp": 1593369822} -OpenZWave/1/node/37/instance/3/commandclass/94/value/634880049/,{ "Label": "Instance 3: ZWave+ Version", "Value": 1, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 3, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 0, "Node": 37, "Genre": "System", "Help": "ZWave+ Version Supported on the Device", "ValueIDKey": 634880049, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369822} -OpenZWave/1/node/37/instance/3/commandclass/94/value/281475611590710/,{ "Label": "Instance 3: InstallerIcon", "Value": 6400, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 3, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 1, "Node": 37, "Genre": "System", "Help": "Icon File to use for the Installer Application", "ValueIDKey": 281475611590710, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369822} -OpenZWave/1/node/37/instance/3/commandclass/94/value/562950588301366/,{ "Label": "Instance 3: UserIcon", "Value": 6400, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 3, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 2, "Node": 37, "Genre": "System", "Help": "Icon File to use for the User Application", "ValueIDKey": 562950588301366, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369822} -OpenZWave/1/node/37/instance/3/commandclass/152/,{ "Instance": 3, "CommandClassId": 152, "CommandClass": "COMMAND_CLASS_SECURITY", "CommandClassVersion": 1, "TimeStamp": 1593369823} -OpenZWave/1/node/37/instance/3/commandclass/152/value/635830320/,{ "Label": "Instance 3: Secured", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Bool", "Instance": 3, "CommandClass": "COMMAND_CLASS_SECURITY", "Index": 0, "Node": 37, "Genre": "System", "Help": "Is Communication with Device Encrypted", "ValueIDKey": 635830320, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369823} -OpenZWave/1/node/37/association/1/,{ "Name": "Lifeline", "Help": "", "MaxAssociations": 1, "Members": [ "1.1" ], "TimeStamp": 1593369823} -OpenZWave/1/node/37/association/2/,{ "Name": "Roller Shutter", "Help": "", "MaxAssociations": 5, "Members": [], "TimeStamp": 1593369913} -OpenZWave/1/node/37/association/3/,{ "Name": "Slats", "Help": "", "MaxAssociations": 5, "Members": [], "TimeStamp": 1593369913} diff --git a/tests/components/ozw/fixtures/fan.json b/tests/components/ozw/fixtures/fan.json deleted file mode 100644 index 2684e5f7385..00000000000 --- a/tests/components/ozw/fixtures/fan.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "topic": "OpenZWave/1/node/10/instance/1/commandclass/38/value/172589073/", - "payload": { - "Label": "Level", - "Value": 41, - "Units": "", - "ValueSet": false, - "ValuePolled": false, - "ChangeVerified": false, - "Min": 0, - "Max": 255, - "Type": "Byte", - "Instance": 1, - "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", - "Index": 0, - "Node": 10, - "Genre": "User", - "Help": "The Current Level of the Device", - "ValueIDKey": 172589073, - "ReadOnly": false, - "WriteOnly": false, - "Event": "valueAdded", - "TimeStamp": 1589997977 - } -} diff --git a/tests/components/ozw/fixtures/fan_network_dump.csv b/tests/components/ozw/fixtures/fan_network_dump.csv deleted file mode 100644 index 54541271d14..00000000000 --- a/tests/components/ozw/fixtures/fan_network_dump.csv +++ /dev/null @@ -1,51 +0,0 @@ -OpenZWave/1/status/,{ "OpenZWave_Version": "1.6.1123", "OZWDaemon_Version": "0.1.98", "QTOpenZWave_Version": "1.0.0", "QT_Version": "5.12.5", "Status": "driverAllNodesQueried", "TimeStamp": 1589998153, "ManufacturerSpecificDBReady": true, "homeID": 4188283268, "getControllerNodeId": 1, "getSUCNodeId": 1, "isPrimaryController": true, "isBridgeController": false, "hasExtendedTXStatistics": false, "getControllerLibraryVersion": "Z-Wave 4.54", "getControllerLibraryType": "Static Controller", "getControllerPath": "/dev/ttyACM0"} -OpenZWave/1/node/10/,{ "NodeID": 10, "NodeQueryStage": "Complete", "isListening": true, "isFlirs": false, "isBeaming": true, "isRouting": true, "isSecurityv1": false, "isZWavePlus": true, "isNIFRecieved": true, "isAwake": true, "isFailed": false, "MetaData": { "OZWInfoURL": "http://www.openzwave.com/device-database/0063:3031:4944", "ZWAProductURL": "", "ProductPic": "images/ge/12724-dimmer.png", "Description": "Transform any home into a smart home with the GE Z-Wave Smart Fan Control. The in-wall fan control easily replaces any standard in-wall switch remotely controls a ceiling fan in your home and features a three-speed control system. Your home will be equipped with ultimate flexibility with the GE Z-Wave Smart Fan Control, capable of being used by itself or with up to four GE add-on switches. Screw terminal installation provides improved space efficiency when replacing existing switches and the integrated LED indicator light allows you to easily locate the switch in a dark room. The GE Z-Wave Smart Fan Control is compatible with any Z-Wave certified gateway, providing access to many popular home automation systems. Take control of your home lighting with GE Z-Wave Smart Lighting Controls!", "ProductManualURL": "https://Products.Z-WaveAlliance.org/ProductManual/File?folder=&filename=Manuals/2506/Binder2.pdf", "ProductPageURL": "http://www.ezzwave.com", "InclusionHelp": "1. Follow the instructions for your Z-Wave certified controller to include a device to the Z-Wave network. 2. Once the controller is ready to include your device, press and release the top or bottom of the smart fan control switch (rocker) to include it in the network. 3. Once your controller has confirmed the device has been included, refresh the Z-Wave network to optimize performance.", "ExclusionHelp": "1. Follow the instructions for your Z-Wave certified controller to exclude a device from the Z-Wave network. 2. Once the controller is ready to Exclude your device, press and release the top or bottom of the wireless smart switch (rocker) to exclude it from the network.", "ResetHelp": "1. Quickly press ON (Top) button three (3) times then immediately press the OFF (Bottom) button three (3) times. The LED will flash ON/OFF 5 times when completed successfully. Note: This should only be used in the event your network’s primary controller is missing or otherwise inoperable.", "WakeupHelp": "", "ProductSupportURL": "", "Frequency": "", "Name": "In-Wall Smart Fan Control" }, "Event": "nodeQueriesComplete", "TimeStamp": 1589998151, "NodeManufacturerName": "GE (Jasco Products)", "NodeProductName": "14287 Fan Control Switch", "NodeBasicString": "Routing Slave", "NodeBasic": 4, "NodeGenericString": "Multilevel Switch", "NodeGeneric": 17, "NodeSpecificString": "Fan Switch", "NodeSpecific": 8, "NodeManufacturerID": "0x0063", "NodeProductType": "0x4944", "NodeProductID": "0x3131", "NodeBaudRate": 100000, "NodeVersion": 4, "NodeGroups": 3, "NodeName": "", "NodeLocation": "", "NodeDeviceTypeString": "Fan Switch", "NodeDeviceType": 1024, "NodeRole": 5, "NodeRoleString": "Always On Slave", "NodePlusType": 0, "NodePlusTypeString": "Z-Wave+ node", "Neighbors": [ 1, 9, 11, 12, 13, 14, 15, 16, 17, 18, 19, 21, 22, 29, 30, 32, 33 ]} -OpenZWave/1/node/10/instance/1/,{ "Instance": 1, "TimeStamp": 1589997977} -OpenZWave/1/node/10/instance/1/commandclass/38/,{ "Instance": 1, "CommandClassId": 38, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "CommandClassVersion": 1, "TimeStamp": 1589997977} -OpenZWave/1/node/10/instance/1/commandclass/38/value/172589073/,{ "Label": "Level", "Value": 41, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 0, "Node": 10, "Genre": "User", "Help": "The Current Level of the Device", "ValueIDKey": 172589073, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1589997977} -OpenZWave/1/node/10/instance/1/commandclass/38/value/281475149299736/,{ "Label": "Bright", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 1, "Node": 10, "Genre": "User", "Help": "Increase the Brightness of the Device", "ValueIDKey": 281475149299736, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1589997977} -OpenZWave/1/node/10/instance/1/commandclass/38/value/562950126010392/,{ "Label": "Dim", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 2, "Node": 10, "Genre": "User", "Help": "Decrease the Brightness of the Device", "ValueIDKey": 562950126010392, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1589997977} -OpenZWave/1/node/10/instance/1/commandclass/38/value/844425111109648/,{ "Label": "Ignore Start Level", "Value": true, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Bool", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 3, "Node": 10, "Genre": "System", "Help": "Ignore the Start Level of the Device when increasing/decreasing brightness", "ValueIDKey": 844425111109648, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1589997977} -OpenZWave/1/node/10/instance/1/commandclass/38/value/1125900087820305/,{ "Label": "Start Level", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 4, "Node": 10, "Genre": "System", "Help": "Start Level when Changing the Brightness of a Device", "ValueIDKey": 1125900087820305, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1589997977} -OpenZWave/1/node/10/instance/1/commandclass/39/,{ "Instance": 1, "CommandClassId": 39, "CommandClass": "COMMAND_CLASS_SWITCH_ALL", "CommandClassVersion": 1, "TimeStamp": 1589997977} -OpenZWave/1/node/10/instance/1/commandclass/39/value/180994068/,{ "Label": "Switch All", "Value": { "List": [ { "Value": 0, "Label": "Disabled" }, { "Value": 1, "Label": "Off Enabled" }, { "Value": 2, "Label": "On Enabled" }, { "Value": 255, "Label": "On and Off Enabled" } ], "Selected": "On and Off Enabled", "Selected_id": 255 }, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_ALL", "Index": 0, "Node": 10, "Genre": "System", "Help": "Switch All Devices On/Off", "ValueIDKey": 180994068, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1589997977} -OpenZWave/1/node/10/instance/1/commandclass/43/,{ "Instance": 1, "CommandClassId": 43, "CommandClass": "COMMAND_CLASS_SCENE_ACTIVATION", "CommandClassVersion": 1, "TimeStamp": 1589997977} -OpenZWave/1/node/10/instance/1/commandclass/43/value/172670995/,{ "Label": "Scene", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_SCENE_ACTIVATION", "Index": 0, "Node": 10, "Genre": "User", "Help": "", "ValueIDKey": 172670995, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1589997977} -OpenZWave/1/node/10/instance/1/commandclass/43/value/281475149381651/,{ "Label": "Duration", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_SCENE_ACTIVATION", "Index": 1, "Node": 10, "Genre": "User", "Help": "", "ValueIDKey": 281475149381651, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1589997977} -OpenZWave/1/node/10/instance/1/commandclass/94/,{ "Instance": 1, "CommandClassId": 94, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "CommandClassVersion": 1, "TimeStamp": 1589997977} -OpenZWave/1/node/10/instance/1/commandclass/94/value/181895185/,{ "Label": "ZWave+ Version", "Value": 1, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 0, "Node": 10, "Genre": "System", "Help": "ZWave+ Version Supported on the Device", "ValueIDKey": 181895185, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1589997977} -OpenZWave/1/node/10/instance/1/commandclass/94/value/281475158605846/,{ "Label": "InstallerIcon", "Value": 1024, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 1, "Node": 10, "Genre": "System", "Help": "Icon File to use for the Installer Application", "ValueIDKey": 281475158605846, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1589997977} -OpenZWave/1/node/10/instance/1/commandclass/94/value/562950135316502/,{ "Label": "UserIcon", "Value": 1024, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 2, "Node": 10, "Genre": "System", "Help": "Icon File to use for the User Application", "ValueIDKey": 562950135316502, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1589997977} -OpenZWave/1/node/10/instance/1/commandclass/112/,{ "Instance": 1, "CommandClassId": 112, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "CommandClassVersion": 1, "TimeStamp": 1589997977} -OpenZWave/1/node/10/instance/1/commandclass/112/value/844425108127764/,{ "Label": "LED Light", "Value": { "List": [ { "Value": 0, "Label": "LED on when light off" }, { "Value": 1, "Label": "LED on when light on" }, { "Value": 2, "Label": "LED always off" } ], "Selected": "LED on when light off", "Selected_id": 0 }, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 2, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 3, "Node": 10, "Genre": "Config", "Help": "Sets when the LED on the switch is lit.", "ValueIDKey": 844425108127764, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1589997977} -OpenZWave/1/node/10/instance/1/commandclass/112/value/1125900084838420/,{ "Label": "Invert Switch", "Value": { "List": [ { "Value": 0, "Label": "No" }, { "Value": 1, "Label": "Yes" } ], "Selected": "No", "Selected_id": 0 }, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 4, "Node": 10, "Genre": "Config", "Help": "Change the top of the switch to OFF and the bottom of the switch to ON, if the switch was installed upside down.", "ValueIDKey": 1125900084838420, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1589997977} -OpenZWave/1/node/10/instance/1/commandclass/112/value/1970325014970385/,{ "Label": "Z-Wave Command Dim Step", "Value": 1, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 1, "Max": 99, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 7, "Node": 10, "Genre": "Config", "Help": "Indicates how many levels the dimmer will change for each dimming step.", "ValueIDKey": 1970325014970385, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1589997977} -OpenZWave/1/node/10/instance/1/commandclass/112/value/2251799991681041/,{ "Label": "Z-Wave Command Dim Rate", "Value": 3, "Units": "x 10 milliseconds", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 1, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 8, "Node": 10, "Genre": "Config", "Help": "This value indicates in 10 millisecond resolution, how often the dim level will change. For example, if you set this parameter to 1, then every 10ms the dim level will change. If you set it to 255, then every 2.55 seconds the dim level will change.", "ValueIDKey": 2251799991681041, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1589997977} -OpenZWave/1/node/10/instance/1/commandclass/112/value/2533274968391697/,{ "Label": "Local Control Dim Step", "Value": 1, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 1, "Max": 99, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 9, "Node": 10, "Genre": "Config", "Help": "Indicates how many levels the dimmer will change for each dimming step.", "ValueIDKey": 2533274968391697, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1589997977} -OpenZWave/1/node/10/instance/1/commandclass/112/value/2814749945102353/,{ "Label": "Local Control Dim Rate", "Value": 3, "Units": "x 10 milliseconds", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 1, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 10, "Node": 10, "Genre": "Config", "Help": "This value indicates in 10 millisecond resolution, how often the dim level will change. For example, if you set this parameter to 1, then every 10ms the dim level will change. If you set it to 255, then every 2.55 seconds the dim level will change.", "ValueIDKey": 2814749945102353, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1589997977} -OpenZWave/1/node/10/instance/1/commandclass/112/value/3096224921813009/,{ "Label": "ALL ON/ALL OFF Dim Step", "Value": 1, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 1, "Max": 99, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 11, "Node": 10, "Genre": "Config", "Help": "Indicates how many levels the dimmer will change for each dimming step.", "ValueIDKey": 3096224921813009, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1589997977} -OpenZWave/1/node/10/instance/1/commandclass/112/value/3377699898523665/,{ "Label": "ALL ON/ALL OFF Dim Rate", "Value": 3, "Units": "x 10 milliseconds", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 1, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 12, "Node": 10, "Genre": "Config", "Help": "This value indicates in 10 millisecond resolution, how often the dim level will change. For example, if you set this parameter to 1, then every 10ms the dim level will change. If you set it to 255, then every 2.55 seconds the dim level will change.", "ValueIDKey": 3377699898523665, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1589997977} -OpenZWave/1/node/10/instance/1/commandclass/114/,{ "Instance": 1, "CommandClassId": 114, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "CommandClassVersion": 2, "TimeStamp": 1589997977} -OpenZWave/1/node/10/instance/1/commandclass/114/value/182222867/,{ "Label": "Loaded Config Revision", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 0, "Node": 10, "Genre": "System", "Help": "Revision of the Config file currently loaded", "ValueIDKey": 182222867, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1589997977} -OpenZWave/1/node/10/instance/1/commandclass/114/value/281475158933523/,{ "Label": "Config File Revision", "Value": 9, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 1, "Node": 10, "Genre": "System", "Help": "Revision of the Config file on the File System", "ValueIDKey": 281475158933523, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1589997977} -OpenZWave/1/node/10/instance/1/commandclass/114/value/562950135644179/,{ "Label": "Latest Available Config File Revision", "Value": 9, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 2, "Node": 10, "Genre": "System", "Help": "Latest Revision of the Config file available for download", "ValueIDKey": 562950135644179, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1589997977} -OpenZWave/1/node/10/instance/1/commandclass/114/value/844425112354839/,{ "Label": "Device ID", "Value": "", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 3, "Node": 10, "Genre": "System", "Help": "Manufacturer Specific Device ID/Model", "ValueIDKey": 844425112354839, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1589997977} -OpenZWave/1/node/10/instance/1/commandclass/114/value/1125900089065495/,{ "Label": "Serial Number", "Value": "", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 4, "Node": 10, "Genre": "System", "Help": "Device Serial Number", "ValueIDKey": 1125900089065495, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1589997977} -OpenZWave/1/node/10/instance/1/commandclass/115/,{ "Instance": 1, "CommandClassId": 115, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "CommandClassVersion": 1, "TimeStamp": 1589997977} -OpenZWave/1/node/10/instance/1/commandclass/115/value/182239252/,{ "Label": "Powerlevel", "Value": { "List": [ { "Value": 0, "Label": "Normal" }, { "Value": 1, "Label": "-1dB" }, { "Value": 2, "Label": "-2dB" }, { "Value": 3, "Label": "-3dB" }, { "Value": 4, "Label": "-4dB" }, { "Value": 5, "Label": "-5dB" }, { "Value": 6, "Label": "-6dB" }, { "Value": 7, "Label": "-7dB" }, { "Value": 8, "Label": "-8dB" }, { "Value": 9, "Label": "-9dB" } ], "Selected": "Normal", "Selected_id": 0 }, "Units": "dB", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 0, "Node": 10, "Genre": "System", "Help": "Output RF PowerLevel", "ValueIDKey": 182239252, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1589997977} -OpenZWave/1/node/10/instance/1/commandclass/115/value/281475158949905/,{ "Label": "Timeout", "Value": 0, "Units": "seconds", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 1, "Node": 10, "Genre": "System", "Help": "Timeout till the PowerLevel is reset to Normal", "ValueIDKey": 281475158949905, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1589997977} -OpenZWave/1/node/10/instance/1/commandclass/115/value/562950135660568/,{ "Label": "Set Powerlevel", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 2, "Node": 10, "Genre": "System", "Help": "Apply the Output PowerLevel and Timeout Values", "ValueIDKey": 562950135660568, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1589997977} -OpenZWave/1/node/10/instance/1/commandclass/115/value/844425112371217/,{ "Label": "Test Node", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 3, "Node": 10, "Genre": "System", "Help": "Node to Perform a test against", "ValueIDKey": 844425112371217, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1589997977} -OpenZWave/1/node/10/instance/1/commandclass/115/value/1125900089081876/,{ "Label": "Test Powerlevel", "Value": { "List": [ { "Value": 0, "Label": "Normal" }, { "Value": 1, "Label": "-1dB" }, { "Value": 2, "Label": "-2dB" }, { "Value": 3, "Label": "-3dB" }, { "Value": 4, "Label": "-4dB" }, { "Value": 5, "Label": "-5dB" }, { "Value": 6, "Label": "-6dB" }, { "Value": 7, "Label": "-7dB" }, { "Value": 8, "Label": "-8dB" }, { "Value": 9, "Label": "-9dB" } ], "Selected": "Normal", "Selected_id": 0 }, "Units": "dB", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 4, "Node": 10, "Genre": "System", "Help": "PowerLevel to use for the Test", "ValueIDKey": 1125900089081876, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1589997977} -OpenZWave/1/node/10/instance/1/commandclass/115/value/1407375065792534/,{ "Label": "Frame Count", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 5, "Node": 10, "Genre": "System", "Help": "How Many Messages to send to the Node for the Test", "ValueIDKey": 1407375065792534, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1589997977} -OpenZWave/1/node/10/instance/1/commandclass/115/value/1688850042503192/,{ "Label": "Test", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 6, "Node": 10, "Genre": "System", "Help": "Perform a PowerLevel Test against the a Node", "ValueIDKey": 1688850042503192, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1589997977} -OpenZWave/1/node/10/instance/1/commandclass/115/value/1970325019213848/,{ "Label": "Report", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 7, "Node": 10, "Genre": "System", "Help": "Get the results of the latest PowerLevel Test against a Node", "ValueIDKey": 1970325019213848, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1589997977} -OpenZWave/1/node/10/instance/1/commandclass/115/value/2251799995924500/,{ "Label": "Test Status", "Value": { "List": [ { "Value": 0, "Label": "Failed" }, { "Value": 1, "Label": "Success" }, { "Value": 2, "Label": "In Progress" } ], "Selected": "Failed", "Selected_id": 0 }, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 8, "Node": 10, "Genre": "System", "Help": "The Current Status of the last PowerNode Test Executed", "ValueIDKey": 2251799995924500, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1589997977} -OpenZWave/1/node/10/instance/1/commandclass/115/value/2533274972635158/,{ "Label": "Acked Frames", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 9, "Node": 10, "Genre": "System", "Help": "Number of Messages successfully Acked by the Target Node", "ValueIDKey": 2533274972635158, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1589997977} -OpenZWave/1/node/10/instance/1/commandclass/134/,{ "Instance": 1, "CommandClassId": 134, "CommandClass": "COMMAND_CLASS_VERSION", "CommandClassVersion": 1, "TimeStamp": 1589997977} -OpenZWave/1/node/10/instance/1/commandclass/134/value/182550551/,{ "Label": "Library Version", "Value": "3", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 0, "Node": 10, "Genre": "System", "Help": "Z-Wave Library Version", "ValueIDKey": 182550551, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1589997977} -OpenZWave/1/node/10/instance/1/commandclass/134/value/281475159261207/,{ "Label": "Protocol Version", "Value": "4.54", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 1, "Node": 10, "Genre": "System", "Help": "Z-Wave Protocol Version", "ValueIDKey": 281475159261207, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1589997977} -OpenZWave/1/node/10/instance/1/commandclass/134/value/562950135971863/,{ "Label": "Application Version", "Value": "5.22", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 2, "Node": 10, "Genre": "System", "Help": "Application Version", "ValueIDKey": 562950135971863, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1589997977} -OpenZWave/1/node/10/association/1/,{ "Name": "Group 1", "Help": "", "MaxAssociations": 5, "Members": [ "1.0" ], "TimeStamp": 1589997977} -OpenZWave/1/node/10/association/2/,{ "Name": "Group 2", "Help": "", "MaxAssociations": 5, "Members": [], "TimeStamp": 1589998004} -OpenZWave/1/node/10/association/3/,{ "Name": "Group 3", "Help": "", "MaxAssociations": 5, "Members": [], "TimeStamp": 1589998004} diff --git a/tests/components/ozw/fixtures/generic_network_dump.csv b/tests/components/ozw/fixtures/generic_network_dump.csv deleted file mode 100644 index 5ca41e879ab..00000000000 --- a/tests/components/ozw/fixtures/generic_network_dump.csv +++ /dev/null @@ -1,284 +0,0 @@ -OpenZWave/1/status/,{ "OpenZWave_Version": "1.6.1008", "OZWDeamon_Version": "0.1", "QTOpenZWave_Version": "1.0.0", "QT_Version": "5.12.5", "Status": "driverAllNodesQueried", "TimeStamp": 1579566933, "ManufacturerSpecificDBReady": true, "homeID": 3245146787, "getControllerNodeId": 1, "getSUCNodeId": 1, "isPrimaryController": true, "isBridgeController": false, "hasExtendedTXStatistics": true, "getControllerLibraryVersion": "Z-Wave 3.95", "getControllerLibraryType": "Static Controller", "getControllerPath": "/dev/zwave"} -OpenZWave/1/node/1/,{ "NodeID": 1, "NodeQueryStage": "Complete", "isListening": true, "isFlirs": false, "isBeaming": true, "isRouting": false, "isSecurityv1": false, "isZWavePlus": false, "isNIFRecieved": true, "isAwake": true, "isFailed": false, "MetaData": { "OZWInfoURL": "http://www.openzwave.com/device-database/0086:005A:0101", "ZWAProductURL": "", "ProductPic": "images/aeotec/zw090.png", "Description": "Aeotec Z-Stick Gen5 is a USB controller. When connected to a host controller via USB, it enables the host controller to take part in the Z-Wave network. Products that are Z-Wave certified can be used and communicate with other Z-Wave certified devices.", "ProductManualURL": "https://Products.Z-WaveAlliance.org/ProductManual/File?folder=&filename=Manuals/1355/Z Stick Gen5 manual 1.pdf", "ProductPageURL": "", "InclusionHelp": "Plug the Z-Stick into USB port of your host Controller and then click the “Inclusion” button on your PC/host Controller application.", "ExclusionHelp": "Plug the Z-Stick into USB port of your host Controller and then click the “Exclusion” button on your PC/host Controller application.", "ResetHelp": "Use this procedure only in the event that the primary controller is missing or otherwise inoperable. Press and hold the Action Button on Z-Stick for 20 seconds and then release.", "WakeupHelp": "N/A", "ProductSupportURL": "", "Frequency": "", "Name": "Z-Stick Gen5", "ProductPicBase64": "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" }, "Event": "nodeQueriesComplete", "TimeStamp": 1579566911, "NodeManufacturerName": "AEON Labs", "NodeProductName": "ZW090 Z-Stick Gen5 US", "NodeBasicString": "Static Controller", "NodeBasic": 2, "NodeGenericString": "Static Controller", "NodeGeneric": 2, "NodeSpecificString": "Static PC Controller", "NodeSpecific": 1, "NodeManufacturerID": "0x0086", "NodeProductType": "0x0101", "NodeProductID": "0x005a", "NodeBaudRate": 100000, "NodeVersion": 4, "NodeGroups": 0, "NodeName": "", "NodeLocation": "", "NodeDeviceTypeString": "Unknown Type (0x0000)", "NodeDeviceType": 0, "NodeRole": 0, "NodeRoleString": "Central Controller", "NodePlusType": 0, "NodePlusTypeString": "Z-Wave+ node", "Neighbors": [ 31, 32, 33, 36, 37, 39 ]} -OpenZWave/1/node/1/instance/1/,{ "Instance": 1, "TimeStamp": 1579566891} -OpenZWave/1/node/1/instance/1/commandclass/32/,{ "Instance": 1, "CommandClassId": 32, "CommandClass": "COMMAND_CLASS_BASIC", "TimeStamp": 1579566891} -OpenZWave/1/node/1/instance/1/commandclass/32/value/17301521/,{ "Label": "Basic", "Value": 0, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_BASIC", "Index": 0, "Node": 1, "Genre": "Basic", "Help": "Basic status of the node", "ValueIDKey": 17301521, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/1/instance/1/commandclass/112/,{ "Instance": 1, "CommandClassId": 112, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "TimeStamp": 1579566891} -OpenZWave/1/node/1/instance/1/commandclass/112/value/22799473140563988/,{ "Label": "LED indicator configuration", "Value": { "List": [ { "Value": 0, "Label": "Disable" }, { "Value": 1, "Label": "Enable" } ], "Selected": "Enable" }, "Units": "", "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 81, "Node": 1, "Genre": "Config", "Help": "Enable/Disable LED indicator when plugged in", "ValueIDKey": 22799473140563988, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/1/instance/1/commandclass/112/value/61924494903345172/,{ "Label": "Configuration of the RF power level", "Value": { "List": [ { "Value": 1, "Label": "1" }, { "Value": 2, "Label": "2" }, { "Value": 3, "Label": "3" }, { "Value": 4, "Label": "4" }, { "Value": 5, "Label": "5" }, { "Value": 6, "Label": "6" }, { "Value": 7, "Label": "7" }, { "Value": 8, "Label": "8" }, { "Value": 9, "Label": "9" }, { "Value": 10, "Label": "10" } ], "Selected": "10" }, "Units": "", "Min": 1, "Max": 10, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 220, "Node": 1, "Genre": "Config", "Help": "1~10, other= ignore. A total of 10 levels, level 1 as the weak output power, and so on, 10 for most output power level", "ValueIDKey": 61924494903345172, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/1/instance/1/commandclass/112/value/68116944390979604/,{ "Label": "Security network enabled", "Value": { "List": [ { "Value": 0, "Label": "Disable" }, { "Value": 1, "Label": "Enable" } ], "Selected": "Disable" }, "Units": "", "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 242, "Node": 1, "Genre": "Config", "Help": "", "ValueIDKey": 68116944390979604, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/1/instance/1/commandclass/112/value/68398419367690259/,{ "Label": "Security network key", "Value": 0, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 243, "Node": 1, "Genre": "Config", "Help": "", "ValueIDKey": 68398419367690259, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/1/instance/1/commandclass/112/value/70931694158086164/,{ "Label": "Lock/Unlock Configuration", "Value": { "List": [ { "Value": 0, "Label": "Unlock" }, { "Value": 1, "Label": "Lock" } ], "Selected": "Unlock" }, "Units": "", "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 252, "Node": 1, "Genre": "Config", "Help": "Lock/ unlock all configuration parameters", "ValueIDKey": 70931694158086164, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/1/instance/1/commandclass/112/value/71776119088218131/,{ "Label": "Reset default configuration", "Value": 0, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 255, "Node": 1, "Genre": "Config", "Help": "Reset to the default configuration", "ValueIDKey": 71776119088218131, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/1/instance/1/commandclass/114/,{ "Instance": 1, "CommandClassId": 114, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "TimeStamp": 1579566891} -OpenZWave/1/node/1/instance/1/commandclass/114/value/31227923/,{ "Label": "Loaded Config Revision", "Value": 6, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 0, "Node": 1, "Genre": "System", "Help": "Revision of the Config file currently loaded", "ValueIDKey": 31227923, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/1/instance/1/commandclass/114/value/281475007938579/,{ "Label": "Config File Revision", "Value": 6, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 1, "Node": 1, "Genre": "System", "Help": "Revision of the Config file on the File System", "ValueIDKey": 281475007938579, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/1/instance/1/commandclass/114/value/562949984649235/,{ "Label": "Latest Available Config File Revision", "Value": 6, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 2, "Node": 1, "Genre": "System", "Help": "Latest Revision of the Config file available for download", "ValueIDKey": 562949984649235, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/1/instance/1/commandclass/114/value/844424961359895/,{ "Label": "Device ID", "Value": "", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 3, "Node": 1, "Genre": "System", "Help": "Manufacturer Specific Device ID/Model", "ValueIDKey": 844424961359895, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/1/instance/1/commandclass/114/value/1125899938070551/,{ "Label": "Serial Number", "Value": "", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 4, "Node": 1, "Genre": "System", "Help": "Device Serial Number", "ValueIDKey": 1125899938070551, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/32/,{ "NodeID": 32, "NodeQueryStage": "Complete", "isListening": true, "isFlirs": false, "isBeaming": true, "isRouting": true, "isSecurityv1": false, "isZWavePlus": true, "isNIFRecieved": true, "isAwake": true, "isFailed": false, "MetaData": { "OZWInfoURL": "http://www.openzwave.com/device-database/0208:0005:0101", "ZWAProductURL": "", "ProductPic": "images/hank/hkzw-so01-smartplug.png", "Description": "Smart Plug is a Z-Wave Switch plugin module specifically used to enable Z-Wave command and control (on/off) of any plug-in tool. It can report wattage consumption or kWh energy usage.Smart Plug is also a security Z-Wave device and supports the Over The Air (OTA) feature for the product’s firmware upgrade.", "ProductManualURL": "https://Products.Z-WaveAlliance.org/ProductManual/File?folder=&filename=Manuals/1789/HKZW-SO01_manual.pdf", "ProductPageURL": "", "InclusionHelp": "Set the Z-Wave network main controller into learning mode. Short press the Z-button, the LED will keep turning on or off, which indicates the inclusion is successful.", "ExclusionHelp": "Set the Z-Wave network main controller into remove mode. Triple click the Z-button, the LED will blink slowly, which indicates the inclusion is successful.", "ResetHelp": "Press and hold the Z-button for more than 20 seconds, the LED will blink faster and faster when the button is pressed. If holding time more than 20seconds, the LED indicator will be on for 3 seconds then it blinking slowly, which indicates the reseting is successful. Use this procedure only in the event that the network primary controller is missing or otherwise inoperable.", "WakeupHelp": "", "ProductSupportURL": "", "Frequency": "", "Name": "Smart Plug", "ProductPicBase64": "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" }, "Event": "nodeQueriesComplete", "TimeStamp": 1579566933, "NodeManufacturerName": "HANK Electronics Ltd", "NodeProductName": "HKZW-SO01 Smart Plug", "NodeBasicString": "Routing Slave", "NodeBasic": 4, "NodeGenericString": "Binary Switch", "NodeGeneric": 16, "NodeSpecificString": "Binary Power Switch", "NodeSpecific": 1, "NodeManufacturerID": "0x0208", "NodeProductType": "0x0101", "NodeProductID": "0x0005", "NodeBaudRate": 100000, "NodeVersion": 4, "NodeGroups": 1, "NodeName": "", "NodeLocation": "", "NodeDeviceTypeString": "On/Off Power Switch", "NodeDeviceType": 1792, "NodeRole": 5, "NodeRoleString": "Always On Slave", "NodePlusType": 0, "NodePlusTypeString": "Z-Wave+ node", "Neighbors": [ 1, 33, 36, 37, 39 ]} -OpenZWave/1/node/32/instance/1/,{ "Instance": 1, "TimeStamp": 1579566891} -OpenZWave/1/node/32/instance/1/commandclass/37/,{ "Instance": 1, "CommandClassId": 37, "CommandClass": "COMMAND_CLASS_SWITCH_BINARY", "TimeStamp": 1579566891} -OpenZWave/1/node/32/instance/1/commandclass/37/value/541671440/,{ "Label": "Switch", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Bool", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_BINARY", "Index": 0, "Node": 32, "Genre": "User", "Help": "Turn On/Off Device", "ValueIDKey": 541671440, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/32/instance/1/commandclass/39/,{ "Instance": 1, "CommandClassId": 39, "CommandClass": "COMMAND_CLASS_SWITCH_ALL", "TimeStamp": 1579566891} -OpenZWave/1/node/32/instance/1/commandclass/39/value/550092820/,{ "Label": "Switch All", "Value": { "List": [ { "Value": 0, "Label": "Disabled" }, { "Value": 1, "Label": "Off Enabled" }, { "Value": 2, "Label": "On Enabled" }, { "Value": 255, "Label": "On and Off Enabled" } ], "Selected": "On and Off Enabled" }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_ALL", "Index": 0, "Node": 32, "Genre": "System", "Help": "Switch All Devices On/Off", "ValueIDKey": 550092820, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/32/instance/1/commandclass/43/,{ "Instance": 1, "CommandClassId": 43, "CommandClass": "COMMAND_CLASS_SCENE_ACTIVATION", "TimeStamp": 1579566891} -OpenZWave/1/node/32/instance/1/commandclass/43/value/541769747/,{ "Label": "Scene", "Value": 0, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_SCENE_ACTIVATION", "Index": 0, "Node": 32, "Genre": "User", "Help": "", "ValueIDKey": 541769747, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/32/instance/1/commandclass/43/value/281475518480403/,{ "Label": "Duration", "Value": 0, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_SCENE_ACTIVATION", "Index": 1, "Node": 32, "Genre": "User", "Help": "", "ValueIDKey": 281475518480403, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/32/instance/1/commandclass/50/,{ "Instance": 1, "CommandClassId": 50, "CommandClass": "COMMAND_CLASS_METER", "TimeStamp": 1579566891} -OpenZWave/1/node/32/instance/1/commandclass/50/value/541884434/,{ "Label": "Electric - kWh", "Value": 0.06199999898672104, "Units": "kWh", "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_METER", "Index": 0, "Node": 32, "Genre": "User", "Help": "", "ValueIDKey": 541884434, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueChanged", "TimeStamp": 1579566931} -OpenZWave/1/node/32/instance/1/commandclass/50/value/562950495305746/,{ "Label": "Electric - W", "Value": 0.0, "Units": "W", "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_METER", "Index": 2, "Node": 32, "Genre": "User", "Help": "", "ValueIDKey": 562950495305746, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/32/instance/1/commandclass/50/value/1125900448727058/,{ "Label": "Electric - V", "Value": 123.90499877929688, "Units": "V", "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_METER", "Index": 4, "Node": 32, "Genre": "User", "Help": "", "ValueIDKey": 1125900448727058, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueChanged", "TimeStamp": 1579566933} -OpenZWave/1/node/32/instance/1/commandclass/50/value/1407375425437714/,{ "Label": "Electric - A", "Value": 0.0, "Units": "A", "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_METER", "Index": 5, "Node": 32, "Genre": "User", "Help": "", "ValueIDKey": 1407375425437714, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/32/instance/1/commandclass/50/value/72057594579812368/,{ "Label": "Exporting", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Bool", "Instance": 1, "CommandClass": "COMMAND_CLASS_METER", "Index": 256, "Node": 32, "Genre": "User", "Help": "", "ValueIDKey": 72057594579812368, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/32/instance/1/commandclass/50/value/72339069564911640/,{ "Label": "Reset", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_METER", "Index": 257, "Node": 32, "Genre": "System", "Help": "", "ValueIDKey": 72339069564911640, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/32/instance/1/commandclass/94/,{ "Instance": 1, "CommandClassId": 94, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "TimeStamp": 1579566891} -OpenZWave/1/node/32/instance/1/commandclass/94/value/550993937/,{ "Label": "ZWave+ Version", "Value": 1, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 0, "Node": 32, "Genre": "System", "Help": "ZWave+ Version Supported on the Device", "ValueIDKey": 550993937, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/32/instance/1/commandclass/94/value/281475527704598/,{ "Label": "InstallerIcon", "Value": 1792, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 1, "Node": 32, "Genre": "System", "Help": "Icon File to use for the Installer Application", "ValueIDKey": 281475527704598, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/32/instance/1/commandclass/94/value/562950504415254/,{ "Label": "UserIcon", "Value": 1792, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 2, "Node": 32, "Genre": "System", "Help": "Icon File to use for the User Application", "ValueIDKey": 562950504415254, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/32/instance/1/commandclass/112/,{ "Instance": 1, "CommandClassId": 112, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "TimeStamp": 1579566891} -OpenZWave/1/node/32/instance/1/commandclass/112/value/5629500081307668/,{ "Label": "Overload Protection", "Value": { "List": [ { "Value": 0, "Label": "Disabled" }, { "Value": 1, "Label": "Enabled" } ], "Selected": "Enabled" }, "Units": "", "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 20, "Node": 32, "Genre": "Config", "Help": "Smart Plug keep detecting the load power, once the current exceeds 16.5a for more than 5s, smart plug's relay will turn off", "ValueIDKey": 5629500081307668, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/32/instance/1/commandclass/112/value/5910975058018324/,{ "Label": "Device status after power failure", "Value": { "List": [ { "Value": 0, "Label": "Memorize" }, { "Value": 1, "Label": "On" }, { "Value": 2, "Label": "Off" } ], "Selected": "Memorize" }, "Units": "", "Min": 0, "Max": 2, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 21, "Node": 32, "Genre": "Config", "Help": "Define how the plug reacts after the power supply is back on. 0 - Smart Plug memorizes its state after a power failure. 1 - Smart Plug does not memorize its state after a power failure. Connected device will be on after the power supply is reconnected. 2 - Smart Plug does not memorize its state after a power failure. Connected device will be off after the power supply is reconnected.", "ValueIDKey": 5910975058018324, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/32/instance/1/commandclass/112/value/6755399988150292/,{ "Label": "Notification when load status change", "Value": { "List": [ { "Value": 0, "Label": "Disabled" }, { "Value": 1, "Label": "Basic" }, { "Value": 2, "Label": "Basic without Z-WAVE Command" } ], "Selected": "Basic" }, "Units": "", "Min": 0, "Max": 2, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 24, "Node": 32, "Genre": "Config", "Help": "Smart Plug can send notifications to association device(Group Lifeline) when state of smart plug's load change 0 - The function is disabled 1 - Send Basic report. 2 - Send Basic report only when Load condition is not changed by Z-WAVE Command", "ValueIDKey": 6755399988150292, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/32/instance/1/commandclass/112/value/7599824918282260/,{ "Label": "Indicator Modes", "Value": { "List": [ { "Value": 0, "Label": "Enabled" }, { "Value": 1, "Label": "Disabled" } ], "Selected": "Enabled" }, "Units": "", "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 27, "Node": 32, "Genre": "Config", "Help": "After smart plug being included into a Z-Wave network, the LED in the device will indicator the state of load. 0 - The LED will follow the status(on/off) of its load 1 - When the state of Switch's load changed, THe LED will follow the status(on/off) of its load, but the red LED will turn off after 5 seconds if there is no any switch action.", "ValueIDKey": 7599824918282260, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/32/instance/1/commandclass/112/value/42502722030403606/,{ "Label": "Threshold of power report", "Value": 50, "Units": "W", "Min": 0, "Max": 65535, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 151, "Node": 32, "Genre": "Config", "Help": "Power threshold to be interpereted, when the change value of load power exceeds the setting threshold, the smart plug will send meter report to association device(Group Lifeline)", "ValueIDKey": 42502722030403606, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/32/instance/1/commandclass/112/value/42784197007114257/,{ "Label": "Percentage threshold of power report", "Value": 10, "Units": "%", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 152, "Node": 32, "Genre": "Config", "Help": "Power percentage threshold to be interpreted, when change value of the load power exceeds the setting threshold, the smart plug will send meter report to association device(Group Lifeline).", "ValueIDKey": 42784197007114257, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/32/instance/1/commandclass/112/value/48132221564616723/,{ "Label": "Power report frequency", "Value": 30, "Units": "seconds", "Min": 5, "Max": 2678400, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 171, "Node": 32, "Genre": "Config", "Help": "The interval of sending power report to association device(Group Lifeline). 0 - The function is disabled.", "ValueIDKey": 48132221564616723, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/32/instance/1/commandclass/112/value/48413696541327379/,{ "Label": "Energy report frequency", "Value": 300, "Units": "seconds", "Min": 5, "Max": 2678400, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 172, "Node": 32, "Genre": "Config", "Help": "The interval of sending power report to association device(Group Lifeline). 0 - The function is disabled.", "ValueIDKey": 48413696541327379, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/32/instance/1/commandclass/112/value/48695171518038035/,{ "Label": "Voltage report frequency", "Value": 0, "Units": "seconds", "Min": 5, "Max": 2678400, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 173, "Node": 32, "Genre": "Config", "Help": "The interval of sending voltage report to association device(Group Lifeline). 0 - The function is disabled.", "ValueIDKey": 48695171518038035, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/32/instance/1/commandclass/112/value/48976646494748691/,{ "Label": "Electricity report frequency", "Value": 0, "Units": "seconds", "Min": 5, "Max": 2678400, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 174, "Node": 32, "Genre": "Config", "Help": "The interval of sending electricity report to association device(Group Lifeline). 0 - The function is disabled.", "ValueIDKey": 48976646494748691, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/32/instance/1/commandclass/114/,{ "Instance": 1, "CommandClassId": 114, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "TimeStamp": 1579566891} -OpenZWave/1/node/32/instance/1/commandclass/114/value/551321619/,{ "Label": "Loaded Config Revision", "Value": 2, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 0, "Node": 32, "Genre": "System", "Help": "Revision of the Config file currently loaded", "ValueIDKey": 551321619, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/32/instance/1/commandclass/114/value/281475528032275/,{ "Label": "Config File Revision", "Value": 2, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 1, "Node": 32, "Genre": "System", "Help": "Revision of the Config file on the File System", "ValueIDKey": 281475528032275, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/32/instance/1/commandclass/114/value/562950504742931/,{ "Label": "Latest Available Config File Revision", "Value": 2, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 2, "Node": 32, "Genre": "System", "Help": "Latest Revision of the Config file available for download", "ValueIDKey": 562950504742931, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/32/instance/1/commandclass/114/value/844425481453591/,{ "Label": "Device ID", "Value": "", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 3, "Node": 32, "Genre": "System", "Help": "Manufacturer Specific Device ID/Model", "ValueIDKey": 844425481453591, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/32/instance/1/commandclass/114/value/1125900458164247/,{ "Label": "Serial Number", "Value": "0107020900000000000607034800010001000000", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 4, "Node": 32, "Genre": "System", "Help": "Device Serial Number", "ValueIDKey": 1125900458164247, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/32/instance/1/commandclass/115/,{ "Instance": 1, "CommandClassId": 115, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "TimeStamp": 1579566891} -OpenZWave/1/node/32/instance/1/commandclass/115/value/551338004/,{ "Label": "Powerlevel", "Value": { "List": [ { "Value": 0, "Label": "Normal" }, { "Value": 1, "Label": "-1dB" }, { "Value": 2, "Label": "-2dB" }, { "Value": 3, "Label": "-3dB" }, { "Value": 4, "Label": "-4dB" }, { "Value": 5, "Label": "-5dB" }, { "Value": 6, "Label": "-6dB" }, { "Value": 7, "Label": "-7dB" }, { "Value": 8, "Label": "-8dB" }, { "Value": 9, "Label": "-9dB" } ], "Selected": "Normal" }, "Units": "dB", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 0, "Node": 32, "Genre": "System", "Help": "Output RF PowerLevel", "ValueIDKey": 551338004, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/32/instance/1/commandclass/115/value/281475528048657/,{ "Label": "Timeout", "Value": 0, "Units": "seconds", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 1, "Node": 32, "Genre": "System", "Help": "Timeout till the PowerLevel is reset to Normal", "ValueIDKey": 281475528048657, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/32/instance/1/commandclass/115/value/562950504759320/,{ "Label": "Set Powerlevel", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 2, "Node": 32, "Genre": "System", "Help": "Apply the Output PowerLevel and Timeout Values", "ValueIDKey": 562950504759320, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/32/instance/1/commandclass/115/value/844425481469969/,{ "Label": "Test Node", "Value": 0, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 3, "Node": 32, "Genre": "System", "Help": "Node to Perform a test against", "ValueIDKey": 844425481469969, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/32/instance/1/commandclass/115/value/1125900458180628/,{ "Label": "Test Powerlevel", "Value": { "List": [ { "Value": 0, "Label": "Normal" }, { "Value": 1, "Label": "-1dB" }, { "Value": 2, "Label": "-2dB" }, { "Value": 3, "Label": "-3dB" }, { "Value": 4, "Label": "-4dB" }, { "Value": 5, "Label": "-5dB" }, { "Value": 6, "Label": "-6dB" }, { "Value": 7, "Label": "-7dB" }, { "Value": 8, "Label": "-8dB" }, { "Value": 9, "Label": "-9dB" } ], "Selected": "Normal" }, "Units": "dB", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 4, "Node": 32, "Genre": "System", "Help": "PowerLevel to use for the Test", "ValueIDKey": 1125900458180628, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/32/instance/1/commandclass/115/value/1407375434891286/,{ "Label": "Frame Count", "Value": 0, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 5, "Node": 32, "Genre": "System", "Help": "How Many Messages to send to the Note for the Test", "ValueIDKey": 1407375434891286, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/32/instance/1/commandclass/115/value/1688850411601944/,{ "Label": "Test", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 6, "Node": 32, "Genre": "System", "Help": "Perform a PowerLevel Test against the a Node", "ValueIDKey": 1688850411601944, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/32/instance/1/commandclass/115/value/1970325388312600/,{ "Label": "Report", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 7, "Node": 32, "Genre": "System", "Help": "Get the results of the latest PowerLevel Test against a Node", "ValueIDKey": 1970325388312600, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/32/instance/1/commandclass/115/value/2251800365023252/,{ "Label": "Test Status", "Value": { "List": [ { "Value": 0, "Label": "Failed" }, { "Value": 1, "Label": "Success" }, { "Value": 2, "Label": "In Progress" } ], "Selected": "Failed" }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 8, "Node": 32, "Genre": "System", "Help": "The Current Status of the last PowerNode Test Executed", "ValueIDKey": 2251800365023252, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/32/instance/1/commandclass/115/value/2533275341733910/,{ "Label": "Acked Frames", "Value": 0, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 9, "Node": 32, "Genre": "System", "Help": "Number of Messages successfully Acked by the Target Node", "ValueIDKey": 2533275341733910, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/32/instance/1/commandclass/134/,{ "Instance": 1, "CommandClassId": 134, "CommandClass": "COMMAND_CLASS_VERSION", "TimeStamp": 1579566891} -OpenZWave/1/node/32/instance/1/commandclass/134/value/551649303/,{ "Label": "Library Version", "Value": "3", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 0, "Node": 32, "Genre": "System", "Help": "Z-Wave Library Version", "ValueIDKey": 551649303, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/32/instance/1/commandclass/134/value/281475528359959/,{ "Label": "Protocol Version", "Value": "4.24", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 1, "Node": 32, "Genre": "System", "Help": "Z-Wave Protocol Version", "ValueIDKey": 281475528359959, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/32/instance/1/commandclass/134/value/562950505070615/,{ "Label": "Application Version", "Value": "1.05", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 2, "Node": 32, "Genre": "System", "Help": "Application Version", "ValueIDKey": 562950505070615, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/32/association/1/,{ "Name": "Lifeline", "Help": "", "MaxAssociations": 5, "Members": [ "1.0", "255.0" ], "TimeStamp": 1579566915} -OpenZWave/1/node/36/,{ "NodeID": 36, "NodeQueryStage": "CacheLoad", "isListening": false, "isFlirs": false, "isBeaming": true, "isRouting": true, "isSecurityv1": false, "isZWavePlus": false, "isNIFRecieved": true, "isAwake": true, "isFailed": false, "MetaData": { "OZWInfoURL": "http://www.openzwave.com/device-database/0086:007A:0102", "ZWAProductURL": "", "ProductPic": "images/aeotec/zw122.png", "Description": "Aeotec by Aeon Labs Water Sensor 6 brings intelligence to a new level, one that is suited to both safety and convenience. It contains 4 sensing points, which would be more accurately to detect the presence and absence of water or detect whether there is water leak in some places of your home. The Water Sensor 6 has an inbuilt buzzer that can play alarm sounds to let you know when the water is detected. The Water Sensor 6 is also a security Z-Wave device that supports Over The Air (OTA) for firmware updates.", "ProductManualURL": "https://Products.Z-WaveAlliance.org/ProductManual/File?folder=&filename=Manuals/2437/Aeon Labs Water Sensor 6 manual.pdf", "ProductPageURL": "", "InclusionHelp": "Turn the primary controller of Z-Wave network into inclusion mode, short press the product’s Action Button that you can find on the product.", "ExclusionHelp": "Turn the primary controller of Z-Wave network into exclusion mode, short press the product’s Action Button that you can find on the product.", "ResetHelp": "Press and hold the Action Button that you can find on the product for 20 seconds and then release. This procedure should only be used when the primary controller is inoperable.", "WakeupHelp": "Pressing the Action Button once will trigger sending the Wake up notification command. If press and hold the Z-Wave button for 3 seconds, the Water Sensor will wake up for 10 minutes.", "ProductSupportURL": "", "Frequency": "", "Name": "Water Sensor 6", "ProductPicBase64": "iVBORw0KGgoAAAANSUhEUgAAAGYAAADICAIAAACVqwOrAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAgAElEQVR4nO19S7Adx3ne9/fMOefei3sBiCRAkAIfEi0zD7lix7FTeXiRqmxSSSqLbLJIFko5SaVclUVW2aRUWXjhpJIssvEuKZcUS6Yky5Ys25JNPfgUSZCUSAsQSQAkQRAAARLAvffce885M/1n0a+/e3rmzDn3AExi/wX07dPd049//v5f/c8Maa2JiJmJCA7MT5nK8mwegGlsMs02Eub2mfTT/8IlJtYnL8H2la1uoqxt9h0DfLzQhrLDgDJ/Ovpqq0rK/y/ElwEzsRVOj7TWq+rrzwmoj3sC/+/BX6BsYbB8sT8YYQEwa82aGbnrmQEGKbAWl5kB3U/JW8zP/ESICRQGSX5GgzLYjZCTV7acbQtShVJUlLbf3syu7NnOjMlgZq5mB1xVYtaUtvMC1K2N2xCSGyV0yRYFbNbDAJgYTNQck5n90C09s8/YQYioKAaD0WA4BFQ7tiNYgMqYua6m1Wzqx/YUI3/mr4UlJdk4HZhlvbgwyVFc0hyVwY1+NAOGPP1dDBmoshytrRXlkNC4GQ3oizJmnk0nupoSiBRBFUr520IEZuawDVs7Mc3NujJDt82WDamFfPsQph9mcC16zW5kMMBaa601azCP1jfK4drcHdoDZQwGV7NJPZsSERVlURRobH5mZGfm2VRz96Yl8UyEGSDLyaOgc+Lz6cAPp7VmXeuqYub1I1uqHHRjrYfEJNZ1Vc+mBChVlGVJRM1OCWyKXerLQyqXQzx/aYE9RZ2xYV1E7Jhk81+PZQlQRWnkwMHerhcjbVB6q6jZo6tCPZsCgCpUWXZQpeslapBvz4202UTQV8KW2Qm/7MwXBSICWCnSpDTr2XQyGK2BwsylvcXMKr4STQpi1rquAKiisG3CMmIisj24EsfoSHYrV09m2f5S2E0nVAqAQMo0pdDM164KiFRhhM9sOgm3yk9LDKzmmmB1XYMIpIioSRxRyoHFRJxNdi7p0DBgUxK0EaSk6np0jTnq69BA7n4qpQjQVcVao2Gc+p8quTjJMDPrCmDLog69C7JzRiBD8nTZvuNINF7hLIiUAsDgDrubmbt4k21Ta3E7e8+SEkqLy6PUtGMwoptC5nJ3p1rNg5WBISACWOsOtMzV/uNtS3FH8eLD2lr7ci1CanccWS5GgA5autnTTAS2ar+RiLEnclVgbAxGRhOW0MNg4qBDkZg3gQyJuNTWEEtMkhze5V0qCEdUpXxVVMVmh8/fYepLoJcngwLtsBCRTKkqlK6IGyUZPU2KzAhb3MgbHue2ckNOHBIifbId+jl/3HIskowSafOAR1ggbIdad7kX2gCZqYkUSRoujBibwL79v2IR0NOZ0deTQRQb+iTz8eZIFH+fZw5OGwKxMxvtDidiyf+ttspgklQbd7vyHdkHZ/2dP9rvMynukvGCYpYD4cMxFySeC28PibwXqew2jtyVq2NlVsXvgbP+KAsEAXE3Ig5u/Q2C9blSOFU0XJxPxZSNrPQka3vRkqoJq8NZb1gIZak3iSLRSA5jSXlYFbHTtWxqtqLtnp0IZcvnKSfsA50K4boynPXpKKBsjonLOq+XwttRIp+lILfFyKlhsPIv6gyuIMZXpOCanyvV/V3vPXCWoTLhw7AdsGP/LR2S4+Qun2/m9miGUCHKKWpsO1VwiquRE4lac4cge8yeoqxb0Eo6zLH/dCtJZLBU1kVTs9P8Jna/ILtnsNeH2FoYxlrg7EyWB2Fltx2wK+ntaVrtmR69C9H552A1QLILkVUggKeTg4vn32CtTXdXLr27t71tlro/Hr/3zkWLL9bvXnhrur9nerh54/r1a1dNt7PJ9J3zb7C2mHn/3bfH27eIGaDxzvb7l96xLg6t3zn/VjWdHAZnYq3R8j0G8wEGPlCjrUtqlLj9lIg9Bqnzb5w989wz169dBTCbHDzz/Sdfe+UMMzPzT3708nNPfe9gbwzwrZs3X3ru6TfO/sRsvDM/fPaFp79v6O/i+Tdfeu6ZD65cJlA1nT731HdfPfOiEcGvv/LSsz/4bjU5AHDj2tWXn3/6/BtnD0V1kSot1U230A6Wb6qme2OGVuWgLEuLhTznFZJQqhag2cH+lcuXTj/6M6QIzFffu7R1/NiRzaMA9nZ2bn1044GHHyVFrPV7b184eerB0foGM9/88Iauq3tPniKi6eTg6uVLpx99jJSCrq9dfu/I1tGtY8eZaLx9e/vWrVOnHwLArN97++Kp0w8PhsMlcMXMs8lBXVVrRzaHo/W2ZnMcwcycoqyJmbY07sgkVpu3DIwpxq9V/wPLs7WmWUZq+lkKdWVp5mZRVldrG10oWzTAgNno4iFlkcYljj2bJbE/huDalBAArllrr42xrgDHg+ua69qtnVnXDmnErFnXhv0Tm0NK2CqdU4YWW+Kc+kVRZs+pRSr/yTZGGyVo/e6FN7/227+1/dGHBOhq9o2v/s5rr75k5vaTH736+1/57elkAqKbH17797/2r3/4zA/MGc9v/o//+t//868zazBeffHZf/dvf/XalfcB6Gr6ja9++dUXnzfy9ccvv/B7X/tyXc0A3P7o+te//MVLF946hPd4/tHvIgEGrYOENJI47u/6xpGT999fDocASBX33nfv1pFN02Dz2PH77juhCsXMg7I8eeK+I6aK+eTJE/sHM2IQ8ebm5v0n7hsMSgKI1In7Tx09dtwQ4+bW0ZMnTihVABiORidOnlzfOLL0WoLC17HehXmZ7zsxI70fwqfWKIJT/LU/8GbnwJV2ubWCWKi7wfsaK73RsbsT7iRqlwrAY+bpZKKrWTf7XzZYqqllZDyFJO6adgotM4wXyHN0YWQxyS3uD1AsGxAHi9HWI3NwZ/paEl/ty0tBcSekzR0TZ2Yr2Rgu5UZqL9q5fevlF56bTaem3Z+9+vK1q++beX30wbUfnXkBzCCuZ9Nvfv2J61evgBnMr774/AvPPm3Gu3Xjg9//ypemBwdgsNavvXLm2pXLZoBrVy6//spLJhxkNp2eeeHZ3e3bh8KWk+ZtsBiVMRnS8cTD7nTRlIiU2ASTALj6/uW3z791++ZHAFWz6Zvnzl66eN6Q2KV3Lp5/4+xkcgDGB1cuf+G3/udLzz9rbvHXv/qlJ778BeYarH/0yotf/ML/evfSOwTW9ez8m2ffvniemMH60sXzb507W1cVgNs3b166eOHKe5eWZv+O8rsunx8rK3mZo7tE8fE/MwoRAVrr8c725rHjRjne29kejdaK0YiY62q2P97bPHYMALP+4Mp79554oBgMAIxv39S13jp+Dwi6nl2/euX+B06jUADt7+wMRsNyMGSgrqrJ/t7G1lEAYN65fWvz6FFSxSKIspDwsuYmsw6LNpR5uzSHsgVACgnr6WEGkfdQk93ZTomlmMeDgg0f+kqqupwF/cGgjOtqtHEkQZn0ZKg2m7x7/T4fp5SU+GHGOzs2C97fH1dVRYCJKRqPd90ex3jnNtc2Lmx6sD/Z3zMdaV3tbt/0fR7sjc1OBFBXs4O9sR9pvLN9qCP92L+VdU8ojh9xmHPK4kJSAOfA8HkI0ekVXhCY3zz7Z9/82u98eO0qgGoy+ebvPvHKD58GAMarLz7/za89MTnYB3D18rv/6nP/4k/+6FtgDa1/49c//5/+438w2vwz3//TX/3cP7/41psGR3/w9a+++PzTYA3WL/3wmW/87leqyQTAhzeuf/PrX3nr3E8Opcq24MsTXfH5z38+dzAWrqxnM4BJFUqpZTYmqeFwVJTFJx9+pCxKVRQEPHj69JGtowwMh6P19fUHTp8motFohLr6xV/+m0ZNHZV49FOPPfrYZwCsrw2HZfmLv/y3B4OBUkqBHzz90ObWUUANy2Lz6LGTpx4A0aAoiPihRx4drh9ZboPWdQ2ty8GwKAeNhVjMzHkgBykvQ2MqknuknIRCGTG00ZwNQbI9WAkWOPl4A8mzyPdhD2Is63OkTfYYptVm7w8JL2tr1qVk5FyMcHMVbkW/VdGohdNKOOCL4Xl/hC9OlPh0QZHrwt8KBttjJ1ulwXW3LtoNc3fS/GeYJBAYxCZFM08AOG2j9fVrV7777T/c390xOs0zT3774hvnjDV06eL5H/zJt+vZFKDp5OD73/nW1cvvgpm0fu2Vl848/7QZ98a1K0/+8bf2dneMS+PZ73/3wk/PGnK78OZPn3ryOzybAdjf3f3ed/7wxgfX5gY6d6xwLir6xWTYvwwm/4+YSPy0/huLNNuAQSDa39sdb9+eTKYEcF3vbt8aj3cYgNbj7e3xzi2tNcCz2Wx3Z3dvPCaACePt2zs7O4aRHxzs7+1uTycTY0/tbN/eHW8bEt7b2d7Z3dFcA5hOJ+OdnX0vQJcAv+s7sNHHLAe0GgyLoug6CXdbJTca6XpGRen4pqawl1nXtSoKY6iz1kopc57JugZgwuRMlXsShLnWpJS1/E0smCoAEEjripQC1BLszPGy2Whjazhaa2vWWzu1yiPF3ldxdGsiACIfBrv2rMoBW9cFQEXoAKSKEpaLk1JF4FKqkAIfhQo/Cq/cEyvlkcNgp/cvq2Q4YdOBll4b04skRhzwI+PILScz7goSJfann4+Rc85xYQ0CkGGU8Ge/CbU6nW8FbopDwmIuRulzkValE355cyWUBsvAG0umhELqZHByn4NKKUutCL6rsKhX1kYQdHLIYDb5H8ISDAoUGYcHDLt30Y/kCToagVMhKLB8GAspgc6ezI1cFGWZXRPuvosFbcOmVWJ9GIYPfnH+RYfF7B1JS7wyt+ASOqFz2xvCz/CyzicBvIIq89LwbGkTmbHs6qN4LrtLKbF5o1n5vNi5K2VuDQKX3gqTT58uyXSSL+6kpL5t4gaCl7V0kqGpFcuDiKnkO1c9RyX7TA+7k0kAcJ5ZjtPYpc0uFRXejyuFue9BYsUWgtlaReR+smtwJ14nwJ6mjPUtUdQ38sdoXcgIN6dsSXmHsEd9KTOTrGiMFjGE2OWShssmwct3RmZmXRVInmHKehnnRqlR648lgQNVNv2ilA4Yk8BhQPSTwYlvs6AnY+6ofW44N9JmefOiVJOwfC1/ErYUGB6S5ZgS5mv/fafjbfK0pNGQRMOmtGhhDI1//n6vbFcSSZ9VKyx59NsklCi8xZaYiZg2YouJtGMEZi1uODX+3Rnogf/5ZrnkrrJxBmve59dCYTZOIDLs4cx4N1qkbbX4HM0ZVf6EcHngoGR3ORoXpLKGoIyPS6jRRrQEHBUiTlm4KZtqVzRyWurU6MVW0QKin8PxsggiovIamnvSRhqRzZYmS6LY5inJ25bWW+2IyBKjdAsEcl4J9BQji6EsQ0BRLTVK0jYZEQlO8+aqWOQLDPpCj8Q7ope1wcKejNYfkh/l+XPCw2Q3nMvPnYlnjauDzs6W8WREAhFh+WxTtmmzFmbX5aUC4HEl+ne+MGrcAme9zV/kwkDRnwTMoK0oazr5AIQnft1vj600RbPE+GmbYk7kQ+CYfaoO7BcQxuXQkkm6PQ8HJIZsVrF74qb1gZyWbrt19u5Urh/IMD3fgOHOcRKOJppZ1K9K9UfE/iOWagwMj5bwdEk/f4ZpCZ+6csCz/2Z5JLwZycPDRmAS4HUNN1Z7sEirLnIYaN6erOldtlwzp98m1uxALJAH0YYISbyMqI19GPD0laMgSQjpHTokSN1YRq/Im0dEeV7Wirv2XWCpxKeNWs/U2zuIqpxWweInmox5hX7/eDIu10DFYq/VY8n44XR9r1JR8qIMqVXAcW3xgLloTkD0rIn0YUat2Rob3rPYyrIXhp7a/4J6GQn+LCiG7DO75nQ30IeI5rF/yG0ldhfKCfrYFgZczCMsViyORJ7977uqyy56jukdjna+ZB5ohnsimhrlcK8q4OjsKZWXZDc1YFvbSyhtGDKrlJYOenTYhTIrX5PC5G/g9SGVbWIftHt8vJm6IaXh6NHl3whE4hJu3MEVQLhvrdCLyigokexL0KQUt2Lkag0nMxvbi9YE2xR0U0Ojfmf6h+vMNFy3McZSCbI8dHH2+Sjz0tWmCU9Hk2ooqvVtDNJipLus0E3dnmdKPLwkfjgbLOZkK8BXjy56ezL8tDLvrZEpIalFaA8g+Ne819hIBCLzPhvAqdZ5MiXE1LQ6tSyv/Tdhscgf8ytwmCiTpNxEl4UsQ/f5+CWILU1jJ2WfNfQEiv5kYWHfv4jpdC/FiOOn4CnHxDIKgZDoWhK7jVqjZggtJr5WMsGVsf5+ymkpb2lybpi3V9KdYddDQVn37KgxJ5MK9RVeRgktxV0sNp/ggGnXd0b9T9AisVG2m75ZrFEjE1oDQsTlptF2aSw3zXvDmxLEroSk6DRDWpt0ldhrosWXqOa5tP+Z1RS5ZSu5lJrlvNjesfNr3yZtL2JfYIx5Ewh9NTGjkt89u0zZEtyUm0dEsXYQLsps+uZ9sghngXkbUyak2wqdGfF0goyWP/Psv4MR2t1DIfXX+Pq2nSdZWK5RRP+Zy6UUsfftjjAyCYn4tl7ZRrTT/OuBiH6jnSiaJEzbnq06CRE8GQ3JIcZoQvhoj7/z0WnAYcCvqB0nix/9SqUfjnBy1k/a3vwS+hREPiqfO4vg8FvZuS8CavKSx8OC55hiK0SaklfQmuhggQhK8GN2V5raXRdCerIbncT8Vxb800ctXvDZ8s68jMaIy5M5efy0kJnDsVNUteP3HN2K0PdqCI2CV6HrBqzg1SL9wTNSy8lg/WHOq+E1WtMqRkSerSVK2qEhL70i6EtlMbciIAldyaXUkKzceEuQiYJjJg7vPJhrOYad7l68ujKO1gP5fVHGEdsWmpGZcSOFXXyirJknEPMpQ57QRZJAxigDUruxl/dcxXzw5ls7LL8xG/ZkpCPI8vAGAvOOjTbxCsTmgxjL+XxsI+mluSM2ZlefrY9KtAEJcSXU2phtB5XVCtEgXqNHOP3jriScH/YiDvw+XUAcpk2eslcEc2i2BFJjvUNgdz1q41+YnhJOaIDGi2VtoWde7pEedoLBdyStbndIzP6BqniYQ0CPq1PtHw0MxtB+B4wHInpzcXMunvxcPKc8nUKSj4bLhNT486d+3tRekONliZlZymco2h4OCBcLLMob6i0iABLVFg2CQCQbiq6ygbTsSxqT4YAjlxf6+gogu70SLxAzp6++mKNGR8ET7oWpicKBuI0pCSLOVJqrDAMj6/GHU1/CXCUvy0a1kO9jHkL6gVNlvY+s+VxB+oWcuWPL9oL9u1+yNmrpG0lkksRy7KTM+DuFEy0ITytlVsH/ifJvFZQuAHRH/mTYWWT0cdQWbM4gczw4lCQ0HWqDW8OWWA7rB0uRwpbzOVU2CXs5BETUkxDT4pE/nn9bFiWKM2e5UjJKRc1jyR0YOOspVu9Y0JxprCGQaMgL4j4e0gPE3CvwdtEwFqGqmr/ipfMRoSBvMpML8JFY8nXkbMzYZPJ48dwgt7JDBxq3bcwElnkgRzB5+dtqs76MpK7pt7J5IYnFvc8TpB6bqn8N9csp1Jy812w1MAfzCz/2ldt+aQ01SsOPxEsWmQ0sGmS6dkpMVCFF0OrcjV39zEdZdDU7zsFpg8gmlxc07EXb3OQbEhPBIBcaBimQcm/9F305hDvzf5V2Uxss4ch2EiCtylEectzBIhfBqI5TElwwCFoTFh2ENbtBezGgBaEL9at2MfaZfUA6xSWS1lhUwJcIsyLtUlYfDuasYWH2z3PS/Kw5zXrmlemjc3+FKm/5Nce4o7B4GEtLGvsx8lflLg0/Y0daYGQNph6ZUETKfHN0dSdNc2DZFz43IHMY3AcaYiT/K3dlcL3Y3wsOvSxEkT+IXRrZyJ/ESyHzUq1ttokulrWOjyfXer9F+9HcHRSRMrogyedjMtoflTDV8HIqOBRcRVyedhDb5PLakE+MO+RbuTzy+SUhIMAiyD9d4pGTfxtL991z+oHQAiItQZY7PT307FPv7GJRLiuSKSHY6qk0CTb8YcDGP8TQ9PFkvLLdkVJJGpdTwlIaXZAzq33aVpgFiqWkVEdWAIJ0OMGGhMgrm9RlsCa9F+1Dt5RzwGpIYd8paJ033Iq3cDsk1d4p1aJDAHe9JDVXyE6ZilISwReUL3cGUksa1+amy0iQ5Q8Pega/9IIeXS32cnV7HGK/Wsn2jeqwz/WSe1Ja5GHerug8DiSZTmuawYEwodzrliz7cyLn8MzM8TK3uBZY+KNC3kFjfro5x0sOKYTmCSCrCjetpSaVBfqi5HicAR/jcjjoqQ0v+Dh+poQA97J+w2+iE0ehsEklhiGpFa5O5uMBqUUurJKVCQWwq9tFvbJZLiPd0A0jwJMSp28dcyl5RuU02oD0rBbLoSRQxuHV2jtCZTJOPCgFgnlz7L5P7xe5TZw0gtjcXo9gcZoQC+rmZ3DujqfMQN8n5Sy4DxNy+ONr41MigS2X9XvTYV4wQBPqEn2sNnqrRkShTOij7NwhmP/6SkhKaX5FQlKCbZEri3UDcV5rz259awpnBnBDuTaRKLFmxSr4vltmO6lK5AQq60fbcRu7RseJEsJrsDWIGP00dMWj2G9Yzuhn8iFNNwWDyhXgrYtoxGlp+nnkxJ/RuFJQi7jhQi2i9unHJlxG2xBp9ABEQ0cJhSRlxZ2ABC1o8rLuSBb3uFCDt5N9DUaI7A/FYlHBUxJFPrHTTGzwHrmZBJFgpsReK4anttXhSx79SopJyKjMVrdCEvQFp5A1GzlOHxGLuZzthbYw/DCKin2Nsx9JSFP/M5wEr1haym0gZXcIB3Fm+ULBLJ6vR6eHDbdsHKhjiIjjJnZKFDUXqljEzfydynayAmhbdRJxJL9s3Qe4JR+rBJnUZF28JyjNw4lDbot8IpGhwPP6zbsPxMFzbW477nobS04COIvZmkGSK5ldJz5yIC0BDlcn2qvXLjjZg4JTig1rSc/y0tj3vVpow8kS55jRvA3LDsqFeDCEySmo8Cpq0NV8M5GXIpOjSsH14TikNwlz/HQZkC7GjmYLoyxWluwrU9KhbUuIv0tSQxIOZwYVvz4GG+BQp+UyltGXpW1cutziWkKcIxq8i/YlsPw5ZmOWLWEsS3YZh3my+Cebr/rVlTH7b4NlqazZ5zJnv16fiEyu+LSI2mnUM4m7SmaLObIPyWVbLpdWf9BZnVkn1DTIhpnLDwnZE6Ym9PJkiF4PkfZpIrz+Zl8KLT8/l+5NtBAIg6mrz8O88LmZUletbNKWdky4UXhHdiPN7ziclsvIg2WH67GKOUgXaaLTtva9ctQRxKMSEDgxGRNKGZDlPRl53bdtELG7+oiBpjbbMhr586S8L2ilNnnkQhA2TeLdUYuTFQmcindqAWlK6V2CXXl46ZZFhjeanLdX+mbjJSBF+MpYWQTNnRc8GRKFcz0ZZodEJ2di08hgDqcphLztgWUPoki2lOaRbOIx3pjTSiCN5WqxMbuozGJ6zgZty+e7jFLK0WYyOifyXloTvEqE9YYeqmxT0SepbIpqG5DSqPHqVhwZaw0f+1ROqJJUnHhTXCGCWnK37aV+72IMaxffpyWSOPGfPo6u9NZg8yWpABBcRdJmDM+iu0FTH1Qkr3otszf00MsW/9ij6FnmLQt3T/3mbBy2Xg+P8jAzig1G7kNC3n12l3fnYgZTcCeKsoaGn33/qXW4QqgLga48LjOM1RCUilxMXMf1qwM3SAdaym4lI90RlHzrV6Z21e6wKE5h0O08r35PuascLiOLXKyEIzFEBYHDV0pXCI4Nd6BlcU9Gm5RrU2elUoa0xLUx83SxabaIAxsI2LUQPm26Uqz12eWLhuRxQl5uJOewhoyqyLQEAP9REtsEURyGCyEKstNw+hztAQTUzYqlIQTFdWzMxbpsbEx5XOLK7TFK2lJMDERGTphdSwGHXNfVtZs3r+2MD7RWRXFsODh17OixjS3pBYrlcgH7KbVVgRTfGVjC9w/kdp58gRaJpmHnRRpHVGs01lvbN7938d0Xr9+4VSgolIRBoYZKDVk9uLb2Cyfv//kHHyiL0hGp3N0K0KvboXMUctI6+oqpV4J8ZrI3BrQqB2XZjV9JVN2QttFV/d1z5/7g8vvbBSZ6CuKCMFRqQDRQNCA1LAYlDY6Xo3/8qU8/dt89gOJ066wAZcw8nU64qkYbm4PhyJcnOCGtdUf8AYDJ/hjcB2VLzBFMqKazL54589x4PK73K+i1go4U5dagXCuUIlUzV7qumZlVoRT08B888ujfffghsGaWtLZClM1GG1vD0VpbLE94VEIiK/I9xCay4f/yzTxOx4gefoqCU8jqCMYD4EOqAeZaf/mVV5/dH+/ofaX0icHgvrXRkXJQEimCIqUZU13vVbP9qjqoK+bqiQtvMfOvPHQ6FimHRFe8QASEJD4xTr7D1B32Y0DoEiQ1BKlxyCiN4M4JvM+oaWDgqXM/fXZ3d7c+KIhPjEYnRqM15s9uHvuZT9x3bOMIiD482H/j1o3XP7xeqeKg1hNd13zwxTfffGjryCPHPyGsqpVhzXvosj4eABnC82Cqpntj7sXLgH6czPf+wc2P/ssrr13nSaVnnxgOTowGD5ejf/L4X/3E0ePGfaEUGbF6bbz9pXOvXRrv7Fb1lFGzemR49PN/528hvBd3RRtzMuF6NtrYHI7W25p9HN/6tcBPXbq8X7BGvV4Wx0r1AJX/7Od+4RNbx2B1FXcoSnT/5rHPffavHxuuMcDMmquz450fX7m66PT6LqITFj/65UYalVNaksszYzqZvn57p+K6JHV8UA41/8NPf2ZttN42483h2j997C/V2vklqX7y0qV8/4cAit0DWVgcZU1TqVmO+fmrt2/vERN4o6ANVTw6GD1438mkZeRfJHr8nhMPrq9rZiJoql/56CbrauH5d4K0Sdrgrhz9ZngzXd3bV8QDRWuqKAifOn4PSEkjKTjEvQKk1OPH7724N1YgZnxQ6dvj3eNbx8NnrFYHvY5+FwBq0Exkk7s2lNYdm9YAAAcbSURBVLPAbTver+qBovWCRoUCcHxtnUUbEphijzjm46M1cz6rmWuF2wdT32SZhSwFS2mn3OBSLHgbo7PEAgGlUoyiJCZgFmvUntKS21PVNYE0QwOaQU1X8OGAAN3if/KwzLd+ZT5OKVseGZ6uZGtYDqgYKFJEAK7cvs2skYkmkwvQl3ZuE1HNrJmo5uNra8yr9GTISbfBohuT/JZr/AttKJT4zUmRlgvcv7FOoAEVADTotZsf6WqWibXycRKEvYP91z+6CWDCYNBJwtbG2oLznwvzA7B6oUxqvv5VsElqal3qjkaSls5zw+ATx45vMohIs6oYHw4GT5076590lffBHgNr/Y2fnt0j1EwVE6P4G/fco4oBIQpGO0yARE/IRP60jKqNCWEYSDOFdR1CvAG70ZJtGyIaDAaPHzuuudSgikmDfu+9axcuv8cm/sZ+kgiwalx95u0Lf/zBdSKaMDEUquLvP/RAImEig2xJyFlI8f1IX/qQRRzDtCQnm7L/IM8imw3iB98YwC998lQxIw1VMZhUvb72314/96ev/3g2PXA3BwDvjXe/cubMb771Fgo10VQxoMu/XJY/d/pBucj0jHhF0LwBwfmTBHF4+RXZmJ2z8t6yhhKW+G9Dy5cvvvPEe9emqGasAa0Zkxrrk/3PHj96YmO91nx5d/fc7s5kNGDgQPNEM7MqDvg3fumzn37ggWQ44SNZRoVk5tlkUleztSObg+FaFl/MPOeEqYEVF+sfI4icU8j8yPQotLfgkAb//KMPX9rZf/Kj3RmqGlqRHhSo1tdePpjq/YlZu15bqzUONFdmw8+KX/vZT37q1ClHtBzvJl7IOdC+0FaHRf5RidYwFnGI0bQ1jZLU5+4GI4jwjz77mfpHb/7RjZ1ZoWrUREwgBWZiZq4ZFbi2xzRqWOPffObBX/nMY24eZp5yi5g5LLtFaY4hQR24tGtjnu6NAU12Y/olS5xy+6aUJSld2kKCruuXL17+3xeuXWdmxWwVrzAJzcQ1fnZY/su/8qlHTp5oUzU94pZDGTPPpvv1rF470uX86Y2yoiwHZaSbhyWl57wRYmP8NC8n98mTvYODZ99+/6krN9+Z6JkyDTWYN5gfPzL6e6fv+2unTxXtPrvD4wvAbLpfV/Vap79sEZSVZTgRsvKR4HFiJIY/zXSpa9+SD0Y4zCOdqPXOePfa7v7OtCLCPaPBia2NtbX1XOxBYzHzNk33SgFMJ/u6mkNlfW3M3Hyp7Vf8NFKoTvezX5499zRqjzp69OjRraMwGtldcLTIWfbooBfKGKyZFcLDI5EiZ05bAMhXD+fYGELq964VsQT3wgiBbdZ3zz9B7oXSc6HnSx/cPsruDqc7WLzINn7fSa3MM7zwne7wwmxyHd09bNlJMfrR6aKvFplTl+hkYXyvmBhqDPpBvIXt16QXmtTKoOe+7mOWR3fc+vpsXqYM2JdC+ZJUv5PeDbuTZXmM7rsd0Nn3VrV+uLatq/AiFBey6DZXYEgWA0zp9vKkRiKf24UfA77coOQUgLZmXR+utb00dyPFGUqyorY5LgmpFNDtaPNj2pJuOgwARKSKDjuyx2vFixIAWM+58460erSBFRahPQU/yMcBCe9Xqgst0bd+hWJpfxKRUoUx+EyMUI6LyZSSkhaICHUh1wAJ6H/VXGBm1poBUgUJlDVxEqEz6ywrytK85oO1Zh+gKk6AvFEepxy2W1rJImvapLEzbeDarJIg2YEGAB4MB9J91MRJH+cPFeVAz6ZgrZkLFXZXLOhEuQtgYf8OMpKpv1Y8s+TIxy9DLiqXX/E2ZmZd1wQ1GK13+8TLhPCaHQEYDEeTagZmripdtmJZklIo9PKw4cgg8a1fkRdWZ+TSyU7tsGAP5Jl1XYMxXF8jSr9NlUAvO9bcgenBWBGhKJVSkkG2bhXhseC4ZV5IkB2rtS6dlangPpZCMw6M3R3WWrPWdTVVg9H6xmbYAy2QBn52gK6r6eSAwFAF+Sis7om27B8bWyyMVjSf1unuWDBSCh+isCWuwrdl52QSGhXgGbTWuhyORusbPda0CMrMCLPJQV1Vlu1k3jQJSUy2wvzVGmS/5UL2lJdWFE5hvl4S2bbMGkqRdkdaLUoSg4nUaG2jHA774AuLoszNR1fTST2badZB6HVfIkb0c4Wjsqy+DElLXT2z3Z3BDSIYaLYHZqUUKSrKwWA4KoejnsiyC1gCZdFs+0qurCkQ95a55I6BjLtZEA4TZC1jMPq1//8CVvYhjj8/8Bco6wVSQen/3fL8tup2hCzU1aL93LkOu/tJw1iSkI25g3Vowtlr+6B+buO2HnoeYHf00NatXGYaxhJF9IrLEu25G60dLZPO28p7Limv0Pfrp+cSEuMcTYlJ+TjCaG3UeOapbVod+Z7kRuKRq+ZisjeyaRt1TyzBWhup+sz/ATefof5JBRJjAAAAAElFTkSuQmCC" }, "Event": "nodeNaming", "TimeStamp": 1579566891, "NodeManufacturerName": "AEON Labs", "NodeProductName": "ZW122 Water Sensor 6", "NodeBasicString": "Routing Slave", "NodeBasic": 4, "NodeGenericString": "Notification Sensor", "NodeGeneric": 7, "NodeSpecificString": "Notification Sensor", "NodeSpecific": 1, "NodeManufacturerID": "0x0086", "NodeProductType": "0x0102", "NodeProductID": "0x007a", "NodeBaudRate": 100000, "NodeVersion": 4, "NodeGroups": 4} -OpenZWave/1/node/36/instance/1/,{ "Instance": 1, "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/32/,{ "Instance": 1, "CommandClassId": 32, "CommandClass": "COMMAND_CLASS_BASIC", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/32/value/604504081/,{ "Label": "Basic", "Value": 0, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_BASIC", "Index": 0, "Node": 36, "Genre": "Basic", "Help": "Basic status of the node", "ValueIDKey": 604504081, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/49/,{ "Instance": 1, "CommandClassId": 49, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/49/value/281475585687570/,{ "Label": "Air Temperature", "Value": 17.100000381469728, "Units": "C", "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "Index": 1, "Node": 36, "Genre": "User", "Help": "Air Temperature Sensor Value", "ValueIDKey": 281475585687570, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/49/value/72057594655293460/,{ "Label": "Air Temperature Units", "Value": { "List": [ { "Value": 0, "Label": "Celsius" }, { "Value": 1, "Label": "Fahrenheit" } ], "Selected": "Celsius" }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "Index": 256, "Node": 36, "Genre": "System", "Help": "Air Temperature Sensor Available Units", "ValueIDKey": 72057594655293460, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/94/,{ "Instance": 1, "CommandClassId": 94, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/94/value/618102801/,{ "Label": "Instance 1: ZWave+ Version", "Value": 1, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 0, "Node": 36, "Genre": "System", "Help": "ZWave+ Version Supported on the Device", "ValueIDKey": 618102801, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/94/value/281475594813462/,{ "Label": "Instance 1: InstallerIcon", "Value": 3079, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 1, "Node": 36, "Genre": "System", "Help": "Icon File to use for the Installer Application", "ValueIDKey": 281475594813462, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/94/value/562950571524118/,{ "Label": "Instance 1: UserIcon", "Value": 3079, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 2, "Node": 36, "Genre": "System", "Help": "Icon File to use for the User Application", "ValueIDKey": 562950571524118, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/112/,{ "Instance": 1, "CommandClassId": 112, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/112/value/562950567624724/,{ "Label": "Waking up for 10 minutes when re-power on", "Value": { "List": [ { "Value": 0, "Label": "Disabled" }, { "Value": 1, "Label": "Enabled" } ], "Selected": "Disabled" }, "Units": "", "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 2, "Node": 36, "Genre": "Config", "Help": "Enable/Disable waking up for 10 minutes when re-power on (battery mode) the Water Sensor.", "ValueIDKey": 562950567624724, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/112/value/2251800427888657/,{ "Label": "Timeout of awake after the Wake Up CC is sent out", "Value": 30, "Units": "seconds", "Min": 8, "Max": 127, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 8, "Node": 36, "Genre": "Config", "Help": "Set the timeout of awake after the Wake Up CC is sent out. Available rang is 8 to 127 seconds.", "ValueIDKey": 2251800427888657, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/112/value/2533275404599316/,{ "Label": "Current power mode", "Value": { "List": [ { "Value": 0, "Label": "USB power, sleeping mode after re-power on" }, { "Value": 1, "Label": "USB power, keep awake for 10 minutes after re-power on" }, { "Value": 2, "Label": "USB power, always awake state" }, { "Value": 256, "Label": "Battery power, sleeping mode after re-power on" }, { "Value": 257, "Label": "Battery power, keep awake for 10 minutes after re-power on" }, { "Value": 258, "Label": "Battery power, always awake state" } ], "Selected": "USB power, sleeping mode after re-power on" }, "Units": "", "Min": 0, "Max": 258, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 9, "Node": 36, "Genre": "Config", "Help": "Report the current power mode and the product state for battery power mode", "ValueIDKey": 2533275404599316, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/112/value/2814750381309971/,{ "Label": "Alarm time for the Buzzer", "Value": 1968650, "Units": "", "Min": 655360, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 10, "Node": 36, "Genre": "Config", "Help": "Set the alarm time for the Buzzer when the sensor is triggered. 1 to 255 Repeated cycle of Buzzer alarm. 256 to 65535 the time of Buzzer keeping ON state (MSB). 65536 to 2147483647 The time of Buzzer keeping OFF state.", "ValueIDKey": 2814750381309971, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/112/value/10977524705918993/,{ "Label": "Set the low battery value", "Value": 20, "Units": "%", "Min": 10, "Max": 50, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 39, "Node": 36, "Genre": "Config", "Help": "10% to 50%", "ValueIDKey": 10977524705918993, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/112/value/13510799496314897/,{ "Label": "Sensor report", "Value": 55, "Units": "", "Min": 0, "Max": 55, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 48, "Node": 36, "Genre": "Config", "Help": "Enable/disable the sensor report: Bit 7 - Bit 6 - Bit 5 Notification Report for Overheat alarm. Bit 4 Notification Report for Under heat alarm. Bit 3 - Bit 2 Configuration Report for Tilt sensor. Bit 1 Notification Report for Vibration event. Bit 0 Notification Report for Water Leak event. Note: if the value = 1+2+4+16+32=55, which means if any sensor will report alarm.", "ValueIDKey": 13510799496314897, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/112/value/13792274473025555/,{ "Label": "Upper limit value", "Value": 26214400, "Units": "", "Min": 65536, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 49, "Node": 36, "Genre": "Config", "Help": "Set the upper limit value (overheat). 0 Celsius unit 1 Fahrenheit unit 65536 to 2147483647 Temperature value. Default: 0x01900000 => 40.0C", "ValueIDKey": 13792274473025555, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/112/value/14073749449736211/,{ "Label": "Lower limit value", "Value": 0, "Units": "", "Min": 65536, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 50, "Node": 36, "Genre": "Config", "Help": "Set the lower limit value (under heat). 0 Celsius unit 1 Fahrenheit unit 65536 to 2147483647 Temperature value", "ValueIDKey": 14073749449736211, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/112/value/16044074286710806/,{ "Label": "Recover limit value of temperature sensor", "Value": 5120, "Units": "", "Min": 100, "Max": 4080, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 57, "Node": 36, "Genre": "Config", "Help": "Set the recover limit value of temperature sensor. Note: 1. When the current measurement less than or equal (Upper limit - Recover limit), the upper limit report is enabled and then it would send out a sensor report when the next measurement is more than the upper limit. After that the upper limit report would be disabled again until the measurement less than or equal (Upper limit - Recover limit). 2. When the current measurement greater than or equal (Lower limit + Recover limit), the lower limit report is enabled and then it would send out a sensor report when the next measurement is less than the lower limit. After that the lower limit report would be disabled again until the measurement >= (Lower limit + Recover limit). 3. High byte is the recover limit value. Low byte is the unit (0x00=Celsius, 0x01=Fahrenheit). 4. Recover limit range: 1.0 to 25.5 C/F (0x0100 to 0xFF00 or 0x0101 to 0xFF01). E.g. The default recover limit value is 2.0 C/F (0x1400/0x1401), when the measurement is less than (Upper limit - 2), the upper limit report would be enabled one time or when the measurement is more than (Lower limit + 2), the lower limit report would be enabled one time.", "ValueIDKey": 16044074286710806, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/112/value/18014399123685396/,{ "Label": "Unit of the automatic temperature report", "Value": { "List": [ { "Value": 0, "Label": "Celsius" }, { "Value": 1, "Label": "Fahrenheit" } ], "Selected": "Celsius" }, "Units": "", "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 64, "Node": 36, "Genre": "Config", "Help": "Set the default unit of the automatic temperature report in parameter 101-103", "ValueIDKey": 18014399123685396, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/112/value/23643898657898516/,{ "Label": "Get the state of tilt sensor", "Value": { "List": [ { "Value": 0, "Label": "Horizontal" }, { "Value": 1, "Label": "Vertical" } ], "Selected": "Horizontal" }, "Units": "", "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 84, "Node": 36, "Genre": "Config", "Help": "Get the state of tilt sensor", "ValueIDKey": 23643898657898516, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/112/value/24206848611319828/,{ "Label": "Buzzer", "Value": { "List": [ { "Value": 0, "Label": "Disabled" }, { "Value": 1, "Label": "Enabled" } ], "Selected": "Enabled" }, "Units": "", "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 86, "Node": 36, "Genre": "Config", "Help": "Enable/ disable the buzzer.", "ValueIDKey": 24206848611319828, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/112/value/24488323588030490/,{ "Label": "Sensor is triggered the buzzer will alarm", "Value": [ { "Label": "Vibration", "Help": "If the vibration is triggered, the buzzer will alarm.", "Value": 1, "Position": 1 }, { "Label": "Tilt Sensor", "Help": "If the Tilt Sensor is triggered, the buzzer will alarm.", "Value": 1, "Position": 2 }, { "Label": "UnderHeat", "Help": "If the Under Heat Temperature is triggered, the buzzer will alarm.", "Value": 1, "Position": 4 }, { "Label": "OverHeat", "Help": "If the Over Heat Temperature is triggered, the buzzer will alarm.", "Value": 1, "Position": 5 } ], "Units": "", "Min": 0, "Max": 55, "Type": "BitSet", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 87, "Node": 36, "Genre": "Config", "Help": "What Sensors Trigger the Buzzer", "ValueIDKey": 24488323588030490, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/112/value/24769798564741140/,{ "Label": "Probe 1 Basic Set on grp 3", "Value": { "List": [ { "Value": 0, "Label": "Send nothing" }, { "Value": 1, "Label": "Presence/absence of water 0xFF/0x00" }, { "Value": 2, "Label": "Presence/absence of water 0x00/0xFF" } ], "Selected": "Send nothing" }, "Units": "", "Min": 0, "Max": 2, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 88, "Node": 36, "Genre": "Config", "Help": "To set which value of the Basic Set will be sent to the associated nodes in association Group 3 when the Sensor probe 1 is triggered.", "ValueIDKey": 24769798564741140, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/112/value/25051273541451796/,{ "Label": "Probe 2 Basic Set on grp 4", "Value": { "List": [ { "Value": 0, "Label": "Send nothing" }, { "Value": 1, "Label": "Presence/absence of water 0xFF/0x00" }, { "Value": 2, "Label": "Presence/absence of water 0x00/0xFF" } ], "Selected": "Send nothing" }, "Units": "", "Min": 0, "Max": 2, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 89, "Node": 36, "Genre": "Config", "Help": "To set which value of the Basic Set will be sent to the associated nodes in association Group 4 when the Sensor probe 2 is triggered.", "ValueIDKey": 25051273541451796, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/112/value/26458648425005076/,{ "Label": "Battery report selection", "Value": { "List": [ { "Value": 0, "Label": "USB power level" }, { "Value": 1, "Label": "CR123A battery level" } ], "Selected": "USB power level" }, "Units": "", "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 94, "Node": 36, "Genre": "Config", "Help": "To set which power source level is reported via the Battery CC.", "ValueIDKey": 26458648425005076, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/112/value/28428973261979668/,{ "Label": "Unsolicited report", "Value": { "List": [ { "Value": 0, "Label": "Send Nothing" }, { "Value": 1, "Label": "Battery Report" }, { "Value": 2, "Label": "Multilevel sensor report for temperature" }, { "Value": 3, "Label": "Battery Report and Multilevel sensor report for temperature" } ], "Selected": "Battery Report and Multilevel sensor report for temperature" }, "Units": "", "Min": 0, "Max": 3, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 101, "Node": 36, "Genre": "Config", "Help": "To set what unsolicited report would be sent to the Lifeline group.", "ValueIDKey": 28428973261979668, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/112/value/31243723029086227/,{ "Label": "Unsolicited report interval time", "Value": 3600, "Units": "seconds", "Min": 5, "Max": 2678400, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 111, "Node": 36, "Genre": "Config", "Help": "To set the interval time of sending reports in Report group 1", "ValueIDKey": 31243723029086227, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/112/value/37999122470141972/,{ "Label": "Water leak event report selection", "Value": { "List": [ { "Value": 0, "Label": "Send nothing" }, { "Value": 1, "Label": "Send notification report to association group 1" }, { "Value": 2, "Label": "Send configuration 0x88 report to association group 2" }, { "Value": 3, "Label": "Send notification report to association group 1 and Send configuration 0x88 report to association group 2" } ], "Selected": "Send notification report to association group 1" }, "Units": "", "Min": 0, "Max": 3, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 135, "Node": 36, "Genre": "Config", "Help": "To set which sensor report can be sent when the water leak event is triggered and if the receiving device is a non-multichannel device.", "ValueIDKey": 37999122470141972, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/112/value/38280597446852628/,{ "Label": "Report Type to Send", "Value": { "List": [ { "Value": 0, "Label": "Absence of water is triggered by probe 1 and 2" }, { "Value": 1, "Label": "Presence of water is triggered by probe 1" }, { "Value": 2, "Label": "Presence of water is triggered by probe 2" }, { "Value": 3, "Label": "Presence of water is triggered by probe 1 and 2" } ], "Selected": "Absence of water is triggered by probe 1 and 2" }, "Units": "", "Min": 0, "Max": 2, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 136, "Node": 36, "Genre": "Config", "Help": "When the parameter 0x87 is set to 2 or 3, it can get the sensor probes status through this configuration value.", "ValueIDKey": 38280597446852628, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/112/value/56576470933045270/,{ "Label": "Temperature sensor calibration", "Value": 0, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 201, "Node": 36, "Genre": "Config", "Help": "Temperature calibration (the available value range is [-128, 127] or [-12.8C, 12.7C]). Note: 1. High byte is the calibration value. Low byte is the unit (0x00=Celsius, 0x01=Fahrenheit). 2. The calibration value (high byte) contains one decimal point. E.g. if the value is set to 20 (0x1400), the calibration value is 2.0 C (EU/AU version) or if the value is set to 20 (0x1401), the calibration value is 2.0 F(US version). 3. The calibration value (high byte) = standard value - measure value. E.g. If measure value =25.3C and the standard value = 23.2C, so the calibration value= 23.2C - 25.3C= -2.1C (0xEB). If the measure value =30.1C and the standard value = 33.2C, so the calibration value= 33.2C - 30.1C=3.1C (0x1F).", "ValueIDKey": 56576470933045270, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/112/value/70931694745288724/,{ "Label": "Lock/Unlock Configuration", "Value": { "List": [ { "Value": 0, "Label": "Unlock" }, { "Value": 1, "Label": "Lock" } ], "Selected": "Unlock" }, "Units": "", "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 252, "Node": 36, "Genre": "Config", "Help": "Lock/ unlock all configuration parameters", "ValueIDKey": 70931694745288724, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/112/value/71776119675420692/,{ "Label": "Reset To Factory Defaults", "Value": { "List": [ { "Value": 0, "Label": "Normal" }, { "Value": 1, "Label": "Reset to factory default setting" }, { "Value": 1431655765, "Label": "Reset to factory default setting and removed from the z-wave network" } ], "Selected": "Reset to factory default setting" }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 255, "Node": 36, "Genre": "Config", "Help": "Reset to factory defaults", "ValueIDKey": 71776119675420692, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/113/,{ "Instance": 1, "CommandClassId": 113, "CommandClass": "COMMAND_CLASS_NOTIFICATION", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/113/value/1407375493578772/,{ "Label": "Instance 1: Water", "Value": { "List": [ { "Value": 0, "Label": "Clear" }, { "Value": 2, "Label": "Water Leak at Unknown Location" } ], "Selected": "Clear", "Selected_id": 0 }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_NOTIFICATION", "Index": 5, "Node": 36, "Genre": "User", "Help": "Water Alerts", "ValueIDKey": 1407375493578772, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/113/value/72057594647953425/,{ "Label": "Instance 1: Previous Event Cleared", "Value": 0, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_NOTIFICATION", "Index": 256, "Node": 36, "Genre": "User", "Help": "Previous Event that was sent", "ValueIDKey": 72057594647953425, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/114/,{ "Instance": 1, "CommandClassId": 114, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/114/value/618430483/,{ "Label": "Loaded Config Revision", "Value": 10, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 0, "Node": 36, "Genre": "System", "Help": "Revision of the Config file currently loaded", "ValueIDKey": 618430483, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/114/value/281475595141139/,{ "Label": "Config File Revision", "Value": 10, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 1, "Node": 36, "Genre": "System", "Help": "Revision of the Config file on the File System", "ValueIDKey": 281475595141139, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/114/value/562950571851795/,{ "Label": "Latest Available Config File Revision", "Value": 10, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 2, "Node": 36, "Genre": "System", "Help": "Latest Revision of the Config file available for download", "ValueIDKey": 562950571851795, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/114/value/844425548562455/,{ "Label": "Device ID", "Value": "", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 3, "Node": 36, "Genre": "System", "Help": "Manufacturer Specific Device ID/Model", "ValueIDKey": 844425548562455, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/114/value/1125900525273111/,{ "Label": "Serial Number", "Value": "0d000100010108010100000004030800000000", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 4, "Node": 36, "Genre": "System", "Help": "Device Serial Number", "ValueIDKey": 1125900525273111, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/115/,{ "Instance": 1, "CommandClassId": 115, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/115/value/618446868/,{ "Label": "Powerlevel", "Value": { "List": [ { "Value": 0, "Label": "Normal" }, { "Value": 1, "Label": "-1dB" }, { "Value": 2, "Label": "-2dB" }, { "Value": 3, "Label": "-3dB" }, { "Value": 4, "Label": "-4dB" }, { "Value": 5, "Label": "-5dB" }, { "Value": 6, "Label": "-6dB" }, { "Value": 7, "Label": "-7dB" }, { "Value": 8, "Label": "-8dB" }, { "Value": 9, "Label": "-9dB" } ], "Selected": "Normal" }, "Units": "dB", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 0, "Node": 36, "Genre": "System", "Help": "Output RF PowerLevel", "ValueIDKey": 618446868, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/115/value/281475595157521/,{ "Label": "Timeout", "Value": 0, "Units": "seconds", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 1, "Node": 36, "Genre": "System", "Help": "Timeout till the PowerLevel is reset to Normal", "ValueIDKey": 281475595157521, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/115/value/562950571868184/,{ "Label": "Set Powerlevel", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 2, "Node": 36, "Genre": "System", "Help": "Apply the Output PowerLevel and Timeout Values", "ValueIDKey": 562950571868184, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/115/value/844425548578833/,{ "Label": "Test Node", "Value": 0, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 3, "Node": 36, "Genre": "System", "Help": "Node to Perform a test against", "ValueIDKey": 844425548578833, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/115/value/1125900525289492/,{ "Label": "Test Powerlevel", "Value": { "List": [ { "Value": 0, "Label": "Normal" }, { "Value": 1, "Label": "-1dB" }, { "Value": 2, "Label": "-2dB" }, { "Value": 3, "Label": "-3dB" }, { "Value": 4, "Label": "-4dB" }, { "Value": 5, "Label": "-5dB" }, { "Value": 6, "Label": "-6dB" }, { "Value": 7, "Label": "-7dB" }, { "Value": 8, "Label": "-8dB" }, { "Value": 9, "Label": "-9dB" } ], "Selected": "Normal" }, "Units": "dB", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 4, "Node": 36, "Genre": "System", "Help": "PowerLevel to use for the Test", "ValueIDKey": 1125900525289492, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/115/value/1407375502000150/,{ "Label": "Frame Count", "Value": 0, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 5, "Node": 36, "Genre": "System", "Help": "How Many Messages to send to the Note for the Test", "ValueIDKey": 1407375502000150, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/115/value/1688850478710808/,{ "Label": "Test", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 6, "Node": 36, "Genre": "System", "Help": "Perform a PowerLevel Test against the a Node", "ValueIDKey": 1688850478710808, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/115/value/1970325455421464/,{ "Label": "Report", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 7, "Node": 36, "Genre": "System", "Help": "Get the results of the latest PowerLevel Test against a Node", "ValueIDKey": 1970325455421464, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/115/value/2251800432132116/,{ "Label": "Test Status", "Value": { "List": [ { "Value": 0, "Label": "Failed" }, { "Value": 1, "Label": "Success" }, { "Value": 2, "Label": "In Progress" } ], "Selected": "Failed" }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 8, "Node": 36, "Genre": "System", "Help": "The Current Status of the last PowerNode Test Executed", "ValueIDKey": 2251800432132116, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/115/value/2533275408842774/,{ "Label": "Acked Frames", "Value": 0, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 9, "Node": 36, "Genre": "System", "Help": "Number of Messages successfully Acked by the Target Node", "ValueIDKey": 2533275408842774, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/128/,{ "Instance": 1, "CommandClassId": 128, "CommandClass": "COMMAND_CLASS_BATTERY", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/128/value/610271249/,{ "Label": "Battery Level", "Value": 100, "Units": "%", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_BATTERY", "Index": 0, "Node": 36, "Genre": "User", "Help": "Current Battery Level", "ValueIDKey": 610271249, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/132/,{ "Instance": 1, "CommandClassId": 132, "CommandClass": "COMMAND_CLASS_WAKE_UP", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/132/value/281475595436051/,{ "Label": "Minimum Wake-up Interval", "Value": 240, "Units": "Seconds", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_WAKE_UP", "Index": 1, "Node": 36, "Genre": "System", "Help": "Minimum Time in seconds the device will wake up", "ValueIDKey": 281475595436051, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/132/value/562950572146707/,{ "Label": "Maximum Wake-up Interval", "Value": 16777200, "Units": "Seconds", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_WAKE_UP", "Index": 2, "Node": 36, "Genre": "System", "Help": "Maximum Time in seconds the device will wake up", "ValueIDKey": 562950572146707, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/132/value/844425548857363/,{ "Label": "Default Wake-up Interval", "Value": 3600, "Units": "Seconds", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_WAKE_UP", "Index": 3, "Node": 36, "Genre": "System", "Help": "The Default Wake-Up Interval the device will wake up", "ValueIDKey": 844425548857363, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/132/value/1125900525568019/,{ "Label": "Wake-up Interval Step", "Value": 240, "Units": "Seconds", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_WAKE_UP", "Index": 4, "Node": 36, "Genre": "System", "Help": "Step Size on Wake-up interval", "ValueIDKey": 1125900525568019, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/132/value/618725395/,{ "Label": "Wake-up Interval", "Value": 3600, "Units": "Seconds", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_WAKE_UP", "Index": 0, "Node": 36, "Genre": "System", "Help": "How often the Device will Wake up to check for pending commands", "ValueIDKey": 618725395, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/134/,{ "Instance": 1, "CommandClassId": 134, "CommandClass": "COMMAND_CLASS_VERSION", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/134/value/618758167/,{ "Label": "Library Version", "Value": "3", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 0, "Node": 36, "Genre": "System", "Help": "Z-Wave Library Version", "ValueIDKey": 618758167, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/134/value/281475595468823/,{ "Label": "Protocol Version", "Value": "4.54", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 1, "Node": 36, "Genre": "System", "Help": "Z-Wave Protocol Version", "ValueIDKey": 281475595468823, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/134/value/562950572179479/,{ "Label": "Application Version", "Value": "1.05", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 2, "Node": 36, "Genre": "System", "Help": "Application Version", "ValueIDKey": 562950572179479, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/2/,{ "Instance": 2, "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/2/commandclass/94/,{ "Instance": 2, "CommandClassId": 94, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/2/commandclass/94/value/618102817/,{ "Label": "Instance 2: ZWave+ Version", "Value": 1, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 2, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 0, "Node": 36, "Genre": "System", "Help": "ZWave+ Version Supported on the Device", "ValueIDKey": 618102817, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/2/commandclass/94/value/281475594813478/,{ "Label": "Instance 2: InstallerIcon", "Value": 3079, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 2, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 1, "Node": 36, "Genre": "System", "Help": "Icon File to use for the Installer Application", "ValueIDKey": 281475594813478, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/2/commandclass/94/value/562950571524134/,{ "Label": "Instance 2: UserIcon", "Value": 3079, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 2, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 2, "Node": 36, "Genre": "System", "Help": "Icon File to use for the User Application", "ValueIDKey": 562950571524134, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/2/commandclass/113/,{ "Instance": 2, "CommandClassId": 113, "CommandClass": "COMMAND_CLASS_NOTIFICATION", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/2/commandclass/113/value/1407375493578788/,{ "Label": "Instance 2: Water", "Value": { "List": [ { "Value": 0, "Label": "Clear" }, { "Value": 2, "Label": "Water Leak at Unknown Location" } ], "Selected": "Clear", "Selected_id": 0 }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 2, "CommandClass": "COMMAND_CLASS_NOTIFICATION", "Index": 5, "Node": 36, "Genre": "User", "Help": "Water Alerts", "ValueIDKey": 1407375493578788, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/2/commandclass/113/value/72057594647953441/,{ "Label": "Instance 2: Previous Event Cleared", "Value": 0, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 2, "CommandClass": "COMMAND_CLASS_NOTIFICATION", "Index": 256, "Node": 36, "Genre": "User", "Help": "Previous Event that was sent", "ValueIDKey": 72057594647953441, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/association/1/,{ "Name": "Lifeline", "Help": "", "MaxAssociations": 5, "Members": [ "1.1" ], "TimeStamp": 1579566891} -OpenZWave/1/node/36/association/2/,{ "Name": "Send the configuration parameter 0x88", "Help": "", "MaxAssociations": 5, "Members": [], "TimeStamp": 1579566891} -OpenZWave/1/node/36/association/3/,{ "Name": "Send Basic Set when the Sensor probe 1 is triggered", "Help": "", "MaxAssociations": 5, "Members": [], "TimeStamp": 1579566891} -OpenZWave/1/node/36/association/4/,{ "Name": "Send Basic Set when the Sensor probe 2 is triggered", "Help": "", "MaxAssociations": 5, "Members": [], "TimeStamp": 1579566891} -OpenZWave/1/node/37/,{ "NodeID": 37, "NodeQueryStage": "CacheLoad", "isListening": false, "isFlirs": false, "isBeaming": true, "isRouting": true, "isSecurityv1": false, "isZWavePlus": false, "isNIFRecieved": true, "isAwake": true, "isFailed": false, "MetaData": { "OZWInfoURL": "http://www.openzwave.com/device-database/0371:0005:0002", "ZWAProductURL": "", "ProductPic": "images/aeotec/zwa005.png", "Description": "Aeotec TriSensor is a universal Z-Wave Plus compatible product, consists of temperature, lighting and motion sensors, powered by a CR123A battery. It can be included and operated in any Z-Wave network with other Z-Wave certified devices from other manufacturers and/or other applications. By the built-in motion sensor, an alam will be sent to the gateway when the motion sensor is triggered.", "ProductManualURL": "https://Products.Z-WaveAlliance.org/ProductManual/File?folder=&filename=Manuals/2919/TriSensor user manual 20180416.pdf", "ProductPageURL": "", "InclusionHelp": "Press once TriSensor’s Action Button. If it is the first installation, the yellow LED will keep solid until whole network processing is complete. If successful, the LED will flash white -> green -> white -> green, after 2 seconds finished. If failed, the yellow LED lasts for 30 seconds, then the green LED flashes once. If it is the S2 encryption network, please enter the first 5 digits of DSK.", "ExclusionHelp": "Press once TriSensor’s Action Button, the Purple LED will keep solid until whole network processing is complete. If the exclusion is successful, the LED will flash white -> green - >white -> green and then LED will pulse a blue. If failed, the yellow LED lasts for 30 seconds, then the green LED flashes once.", "ResetHelp": "1. Power up the device. 2. Press and hold the button for 15s until Red Led is blinking,then release the button. Note: Please use this procedure only when the network primary controller is missing or otherwise inoperable.", "WakeupHelp": "Press and hold the button at least 2s until Red Led is on and then release the button,device will send wakeup notification to controller if device is in a Z-Wave network.", "ProductSupportURL": "", "Frequency": "", "Name": "TriSensor", "ProductPicBase64": "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" }, "Event": "nodeNaming", "TimeStamp": 1579566891, "NodeManufacturerName": "Aeotec Limited", "NodeProductName": "ZWA005 TriSensor", "NodeBasicString": "Routing Slave", "NodeBasic": 4, "NodeGenericString": "Notification Sensor", "NodeGeneric": 7, "NodeSpecificString": "Notification Sensor", "NodeSpecific": 1, "NodeManufacturerID": "0x0371", "NodeProductType": "0x0102", "NodeProductID": "0x0005", "NodeBaudRate": 100000, "NodeVersion": 4, "NodeGroups": 3} -OpenZWave/1/node/37/instance/1/,{ "Instance": 1, "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/32/,{ "Instance": 1, "CommandClassId": 32, "CommandClass": "COMMAND_CLASS_BASIC", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/32/value/621281297/,{ "Label": "Basic", "Value": 0, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_BASIC", "Index": 0, "Node": 37, "Genre": "Basic", "Help": "Basic status of the node", "ValueIDKey": 621281297, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/48/,{ "Instance": 1, "CommandClassId": 48, "CommandClass": "COMMAND_CLASS_SENSOR_BINARY", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/48/value/625737744/,{ "Label": "Sensor", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Bool", "Instance": 1, "CommandClass": "COMMAND_CLASS_SENSOR_BINARY", "Index": 0, "Node": 37, "Genre": "User", "Help": "Binary Sensor State", "ValueIDKey": 625737744, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/49/,{ "Instance": 1, "CommandClassId": 49, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/49/value/281475602464786/,{ "Label": "Air Temperature", "Value": 20.700000762939454, "Units": "C", "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "Index": 1, "Node": 37, "Genre": "User", "Help": "Air Temperature Sensor Value", "ValueIDKey": 281475602464786, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/49/value/844425555886098/,{ "Label": "Illuminance", "Value": 0.0, "Units": "Lux", "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "Index": 3, "Node": 37, "Genre": "User", "Help": "Luminance Sensor Value", "ValueIDKey": 844425555886098, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/49/value/1234567890/,{ "Label": "Relative Humidity", "Value": 56.7, "Units": "%", "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "Index": 3, "Node": 37, "Genre": "User", "Help": "Humidity Sensor Value", "ValueIDKey": 1234567890, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/49/value/12345678901/,{ "Label": "Pressure", "Value": 123, "Units": "inHg", "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "Index": 8, "Node": 37, "Genre": "User", "Help": "Pressure Sensor Value", "ValueIDKey": 12345678901, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/49/value/72057594672070676/,{ "Label": "Air Temperature Units", "Value": { "List": [ { "Value": 0, "Label": "Celsius" }, { "Value": 1, "Label": "Fahrenheit" } ], "Selected": "Celsius" }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "Index": 256, "Node": 37, "Genre": "System", "Help": "Air Temperature Sensor Available Units", "ValueIDKey": 72057594672070676, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/49/value/12345678902/,{ "Label": "Fake Power", "Value": 123, "Units": "W", "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "Index": 90, "Node": 37, "Genre": "User", "Help": "Power Sensor Value", "ValueIDKey": 12345678902, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/49/value/12345678903/,{ "Label": "Fake Energy", "Value": 456, "Units": "W", "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "Index": 91, "Node": 37, "Genre": "User", "Help": "Energy Sensor Value", "ValueIDKey": 12345678903, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/49/value/12345678904/,{ "Label": "Fake Electric", "Value": 789, "Units": "W", "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "Index": 92, "Node": 37, "Genre": "User", "Help": "Electric Sensor Value", "ValueIDKey": 12345678904, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/49/value/12345678905/,{ "Label": "Fake String", "Value": "fake", "Units": "", "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "Index": 8, "Node": 37, "Genre": "User", "Help": "Fake String Sensor Value", "ValueIDKey": 12345678901, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/49/value/72620544625491988/,{ "Label": "Illuminance Units", "Value": { "List": [ { "Value": 1, "Label": "Lux" } ], "Selected": "Lux" }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "Index": 258, "Node": 37, "Genre": "System", "Help": "Luminance Sensor Available Units", "ValueIDKey": 72620544625491988, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/94/,{ "Instance": 1, "CommandClassId": 94, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/94/value/634880017/,{ "Label": "ZWave+ Version", "Value": 1, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 0, "Node": 37, "Genre": "System", "Help": "ZWave+ Version Supported on the Device", "ValueIDKey": 634880017, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/94/value/281475611590678/,{ "Label": "InstallerIcon", "Value": 3079, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 1, "Node": 37, "Genre": "System", "Help": "Icon File to use for the Installer Application", "ValueIDKey": 281475611590678, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/94/value/562950588301334/,{ "Label": "UserIcon", "Value": 3079, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 2, "Node": 37, "Genre": "System", "Help": "Icon File to use for the User Application", "ValueIDKey": 562950588301334, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/112/,{ "Instance": 1, "CommandClassId": 112, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/112/value/281475607691286/,{ "Label": "Motion Re-trigger Time", "Value": 30, "Units": "Second", "Min": 0, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 1, "Node": 37, "Genre": "Config", "Help": "This parameter is configured the delay time before PIR sensor can be triggered again to reset motion timeout counter. Value = 0 will disable PIR sensor from triggering until motion timeout has finished.", "ValueIDKey": 281475607691286, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/112/value/562950584401942/,{ "Label": "Motion clear time", "Value": 240, "Units": "Second", "Min": 1, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 2, "Node": 37, "Genre": "Config", "Help": "This configures the clear time when your motion sensor times out and sends a no motion status.", "ValueIDKey": 562950584401942, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/112/value/844425561112596/,{ "Label": "Motion Sensitivity", "Value": { "List": [ { "Value": 0, "Label": "Disabled" }, { "Value": 1, "Label": "1" }, { "Value": 2, "Label": "2" }, { "Value": 3, "Label": "3" }, { "Value": 4, "Label": "4" }, { "Value": 5, "Label": "5" }, { "Value": 6, "Label": "6" }, { "Value": 7, "Label": "7" }, { "Value": 8, "Label": "8" }, { "Value": 9, "Label": "9" }, { "Value": 10, "Label": "10" }, { "Value": 11, "Label": "11" } ], "Selected": "8" }, "Units": "", "Min": 0, "Max": 11, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 3, "Node": 37, "Genre": "Config", "Help": "This parameter is configured the sensitivity that motion detect. 0 - PIR sensor disabled. 1 - Lowest sensitivity. 11 - Highest sensitivity.", "ValueIDKey": 844425561112596, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/112/value/1125900537823252/,{ "Label": "Binary Sensor Report", "Value": { "List": [ { "Value": 0, "Label": "Disable" }, { "Value": 1, "Label": "Enable" } ], "Selected": "Disable" }, "Units": "", "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 4, "Node": 37, "Genre": "Config", "Help": "Enable/disable sensor binary report when motion event is detected or cleared", "ValueIDKey": 1125900537823252, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/112/value/1407375514533908/,{ "Label": "Disable BASIC_SET to Associated nodes", "Value": { "List": [ { "Value": 0, "Label": "Disabled All Group Basic Set Command" }, { "Value": 1, "Label": "Enabled Group 2" }, { "Value": 2, "Label": "Enabled Group 3 " }, { "Value": 3, "Label": "Enabled Group 2 and Group 3" } ], "Selected": "Enabled Group 2 and Group 3" }, "Units": "", "Min": 0, "Max": 3, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 5, "Node": 37, "Genre": "Config", "Help": "This parameter is configured the enabled or disabled send BASIC_SET command to nodes that associated in group 2 and group 3.", "ValueIDKey": 1407375514533908, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/112/value/1688850491244564/,{ "Label": "Basic Set Value Settings for Group 2", "Value": { "List": [ { "Value": 0, "Label": "0xFF when motion is triggered and 0x00 when motion is cleared" }, { "Value": 1, "Label": "0x00 when motion is triggered and 0xFF when motion is cleared" }, { "Value": 2, "Label": "0xFF when motion is triggered" }, { "Value": 3, "Label": "0x00 when motion is triggered" }, { "Value": 4, "Label": "0x00 when motion event is cleared" }, { "Value": 5, "Label": "0xFF when motion event is cleared" } ], "Selected": "0xFF when motion is triggered and 0x00 when motion is cleared" }, "Units": "", "Min": 0, "Max": 5, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 6, "Node": 37, "Genre": "Config", "Help": "Define Basic Set Value when motion event is triggered and / or cleared", "ValueIDKey": 1688850491244564, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/112/value/1970325467955222/,{ "Label": "Temperature Alarm Value", "Value": 239, "Units": "0.1", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 7, "Node": 37, "Genre": "Config", "Help": "This parameter is configured the threshold value that alarm level for temperature. When the current ambient temperature value is larger than this configuration value, device will send a BASIC_SET = 0xFF to nodes associated in group 3. If current temperature value is less than this value, device will send a BASIC_SET = 0x00 to nodes associated in group 3. Value = [Value] x 0.1(Celsius / Fahrenheit) Available Settings: -400 to 850 (40.0 to 85.0 Celsius) or -400 to 1185 (-40.0 to 118.5 Fahrenheit). Default value: 239 (23.9 Celsius) or 750 (75.0 Fahrenheit)", "ValueIDKey": 1970325467955222, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/112/value/2814750398087188/,{ "Label": "LED over TriSensor", "Value": { "List": [ { "Value": 0, "Label": "Disable" }, { "Value": 1, "Label": "Enable" } ], "Selected": "Enable" }, "Units": "", "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 10, "Node": 37, "Genre": "Config", "Help": "Enable or Disable LED over TriSensor This completely disables all LED reaction regardless of Parameter 9 - 13 settings", "ValueIDKey": 2814750398087188, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/112/value/3096225374797844/,{ "Label": "Motion report LED", "Value": { "List": [ { "Value": 0, "Label": "Disabled" }, { "Value": 1, "Label": "Red" }, { "Value": 2, "Label": "Green" }, { "Value": 3, "Label": "Blue" }, { "Value": 4, "Label": "Yellow" }, { "Value": 5, "Label": "Pink" }, { "Value": 6, "Label": "Cyan" }, { "Value": 7, "Label": "Purple" }, { "Value": 8, "Label": "Orange" } ], "Selected": "Green" }, "Units": "", "Min": 0, "Max": 8, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 11, "Node": 37, "Genre": "Config", "Help": "This setting changes the color of the LED when your TriSensor sends a motion report.", "ValueIDKey": 3096225374797844, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/112/value/3377700351508500/,{ "Label": "Temperature report LED", "Value": { "List": [ { "Value": 0, "Label": "Disabled" }, { "Value": 1, "Label": "Red" }, { "Value": 2, "Label": "Green" }, { "Value": 3, "Label": "Blue" }, { "Value": 4, "Label": "Yellow" }, { "Value": 5, "Label": "Pink" }, { "Value": 6, "Label": "Cyan" }, { "Value": 7, "Label": "Purple" }, { "Value": 8, "Label": "Orange" } ], "Selected": "Disabled" }, "Units": "", "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 12, "Node": 37, "Genre": "Config", "Help": "This setting changes the color of the LED when your TriSensor sends a temperature report.", "ValueIDKey": 3377700351508500, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/112/value/3659175328219156/,{ "Label": "Light report LED", "Value": { "List": [ { "Value": 0, "Label": "Disabled" }, { "Value": 1, "Label": "Red" }, { "Value": 2, "Label": "Green" }, { "Value": 3, "Label": "Blue" }, { "Value": 4, "Label": "Yellow" }, { "Value": 5, "Label": "Pink" }, { "Value": 6, "Label": "Cyan" }, { "Value": 7, "Label": "Purple" }, { "Value": 8, "Label": "Orange" } ], "Selected": "Disabled" }, "Units": "", "Min": 0, "Max": 8, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 13, "Node": 37, "Genre": "Config", "Help": "This setting changes the color of the LED when your TriSensor sends a light report.", "ValueIDKey": 3659175328219156, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/112/value/3940650304929812/,{ "Label": "Battery report LED", "Value": { "List": [ { "Value": 0, "Label": "Disabled" }, { "Value": 1, "Label": "Red" }, { "Value": 2, "Label": "Green" }, { "Value": 3, "Label": "Blue" }, { "Value": 4, "Label": "Yellow" }, { "Value": 5, "Label": "Pink" }, { "Value": 6, "Label": "Cyan" }, { "Value": 7, "Label": "Purple" }, { "Value": 8, "Label": "Orange" } ], "Selected": "Disabled" }, "Units": "", "Min": 0, "Max": 8, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 14, "Node": 37, "Genre": "Config", "Help": "It is possible to change the color of what the LED blinks when your TriSensor sends a battery report.", "ValueIDKey": 3940650304929812, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/112/value/4222125281640468/,{ "Label": "Wakeup report LED", "Value": { "List": [ { "Value": 0, "Label": "Disabled" }, { "Value": 1, "Label": "Red" }, { "Value": 2, "Label": "Green" }, { "Value": 3, "Label": "Blue" }, { "Value": 4, "Label": "Yellow" }, { "Value": 5, "Label": "Pink" }, { "Value": 6, "Label": "Cyan" }, { "Value": 7, "Label": "Purple" }, { "Value": 8, "Label": "Orange" } ], "Selected": "Disabled" }, "Units": "", "Min": 0, "Max": 8, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 15, "Node": 37, "Genre": "Config", "Help": "This setting changes the color of the LED when your TriSensor sends a wakeup report.", "ValueIDKey": 4222125281640468, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/112/value/5629500165193748/,{ "Label": "Temperature Scale Setting", "Value": { "List": [ { "Value": 0, "Label": "Celsius" }, { "Value": 1, "Label": "Fahrenheit" } ], "Selected": "Celsius" }, "Units": "", "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 20, "Node": 37, "Genre": "Config", "Help": "Configure temperature sensor scale type, Temperature to report in Celsius or Fahrenheit", "ValueIDKey": 5629500165193748, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/112/value/5910975141904406/,{ "Label": "Temperature Threshold reporting", "Value": 20, "Units": "0.1", "Min": 0, "Max": 250, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 21, "Node": 37, "Genre": "Config", "Help": "Change threshold value for change in temperature to induce an automatic report for temperature sensor. Scale is identical setting in Parameter No.20. 0-> Disable Threshold Report for Temperature Sensor. Setting of value 20 can be a change of -2.0 or +2.0 (C or F depending on Parameter No.20) to induce automatic report or setting a value of 2 will be a change of 0.2(C or F). Available Settings: 0 to 250.", "ValueIDKey": 5910975141904406, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/112/value/6192450118615062/,{ "Label": "Light intensity Threshold Value to Report", "Value": 100, "Units": "Lux", "Min": 0, "Max": 10000, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 22, "Node": 37, "Genre": "Config", "Help": "Change threshold value for change in lux to induce an automatic report for light sensor.", "ValueIDKey": 6192450118615062, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/112/value/6473925095325718/,{ "Label": "Temperature Sensor Report Interval", "Value": 3600, "Units": "Second", "Min": 0, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 23, "Node": 37, "Genre": "Config", "Help": "This parameter is configured the time interval for temperature sensor report. This value is larger, the battery life is longer. And the temperature value changed is not obvious.", "ValueIDKey": 6473925095325718, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/112/value/6755400072036374/,{ "Label": "Light Sensor Report Interval", "Value": 3600, "Units": "Second", "Min": 0, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 24, "Node": 37, "Genre": "Config", "Help": "This parameter is configured the time interval for light sensor report. This value is larger, the battery life is longer. And the light intensity changed is not obvious.", "ValueIDKey": 6755400072036374, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/112/value/8444249932300310/,{ "Label": "Temperature Offset Value", "Value": 0, "Units": "0.1", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 30, "Node": 37, "Genre": "Config", "Help": "The current measuring temperature value can be add and minus a value by this setting. The scale can be decided by Parameter Number 20. Temperature Offset Value = [Value] * 0.1(Celsius / Fahrenheit) Available Settings: -200 to 200.", "ValueIDKey": 8444249932300310, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/112/value/8725724909010966/,{ "Label": "Light Intensity Offset Value", "Value": 0, "Units": "Lux", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 31, "Node": 37, "Genre": "Config", "Help": "The current measuring light intensity value can be add and minus a value by this setting. Available Settings: -1000 to 1000.", "ValueIDKey": 8725724909010966, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/112/value/28147498302046230/,{ "Label": "Light Sensor Calibrated Coefficient", "Value": 1024, "Units": "", "Min": 0, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 100, "Node": 37, "Genre": "Config", "Help": "This configuration defines the calibrated scale for ambient light intensity. Because the method and position that the sensor mounted and the cover of sensor will bring measurement error, user can get more real light intensity by this parameter setting. User should run the steps as blows for calibrating 1) Set this parameter value to default (Assumes the sensor has been added in a Z- Wave Network). 2) Place a digital light meter close to sensor and keep the same direction, monitor the light intensity value (Vm) which report to controller and record it. The same time user should record the value (Vs) of light meter. 3) The scale calibration formula: k = Vm / Vs. 4) The value of k is then multiplied by 1024 and rounded to the nearest whole number. 5) Set the value getting in 5) to this parameter, calibrate finished. For example, Vm = 300, Vs = 2600, then k = 2600 / 300 = 8.6667 k = 8.6667 * 1024 = 8874.7 => 8875. The parameter should be set to 8875.", "ValueIDKey": 28147498302046230, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/113/,{ "Instance": 1, "CommandClassId": 113, "CommandClass": "COMMAND_CLASS_NOTIFICATION", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/113/value/1970325463777300/,{ "Label": "Home Security", "Value": { "List": [ { "Value": 0, "Label": "Clear" }, { "Value": 8, "Label": "Motion Detected at Unknown Location" } ], "Selected": "Clear", "Selected_id": 0 }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_NOTIFICATION", "Index": 7, "Node": 37, "Genre": "User", "Help": "Home Security Alerts", "ValueIDKey": 1970325463777300, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/113/value/72057594664730641/,{ "Label": "Previous Event Cleared", "Value": 0, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_NOTIFICATION", "Index": 256, "Node": 37, "Genre": "User", "Help": "Previous Event that was sent", "ValueIDKey": 72057594664730641, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/114/,{ "Instance": 1, "CommandClassId": 114, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/114/value/635207699/,{ "Label": "Loaded Config Revision", "Value": 6, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 0, "Node": 37, "Genre": "System", "Help": "Revision of the Config file currently loaded", "ValueIDKey": 635207699, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/114/value/281475611918355/,{ "Label": "Config File Revision", "Value": 6, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 1, "Node": 37, "Genre": "System", "Help": "Revision of the Config file on the File System", "ValueIDKey": 281475611918355, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/114/value/562950588629011/,{ "Label": "Latest Available Config File Revision", "Value": 6, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 2, "Node": 37, "Genre": "System", "Help": "Latest Revision of the Config file available for download", "ValueIDKey": 562950588629011, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/114/value/844425565339671/,{ "Label": "Device ID", "Value": "", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 3, "Node": 37, "Genre": "System", "Help": "Manufacturer Specific Device ID/Model", "ValueIDKey": 844425565339671, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/114/value/1125900542050327/,{ "Label": "Serial Number", "Value": "", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 4, "Node": 37, "Genre": "System", "Help": "Device Serial Number", "ValueIDKey": 1125900542050327, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/115/,{ "Instance": 1, "CommandClassId": 115, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/115/value/635224084/,{ "Label": "Powerlevel", "Value": { "List": [ { "Value": 0, "Label": "Normal" }, { "Value": 1, "Label": "-1dB" }, { "Value": 2, "Label": "-2dB" }, { "Value": 3, "Label": "-3dB" }, { "Value": 4, "Label": "-4dB" }, { "Value": 5, "Label": "-5dB" }, { "Value": 6, "Label": "-6dB" }, { "Value": 7, "Label": "-7dB" }, { "Value": 8, "Label": "-8dB" }, { "Value": 9, "Label": "-9dB" } ], "Selected": "Normal" }, "Units": "dB", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 0, "Node": 37, "Genre": "System", "Help": "Output RF PowerLevel", "ValueIDKey": 635224084, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/115/value/281475611934737/,{ "Label": "Timeout", "Value": 0, "Units": "seconds", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 1, "Node": 37, "Genre": "System", "Help": "Timeout till the PowerLevel is reset to Normal", "ValueIDKey": 281475611934737, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/115/value/562950588645400/,{ "Label": "Set Powerlevel", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 2, "Node": 37, "Genre": "System", "Help": "Apply the Output PowerLevel and Timeout Values", "ValueIDKey": 562950588645400, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/115/value/844425565356049/,{ "Label": "Test Node", "Value": 0, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 3, "Node": 37, "Genre": "System", "Help": "Node to Perform a test against", "ValueIDKey": 844425565356049, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/115/value/1125900542066708/,{ "Label": "Test Powerlevel", "Value": { "List": [ { "Value": 0, "Label": "Normal" }, { "Value": 1, "Label": "-1dB" }, { "Value": 2, "Label": "-2dB" }, { "Value": 3, "Label": "-3dB" }, { "Value": 4, "Label": "-4dB" }, { "Value": 5, "Label": "-5dB" }, { "Value": 6, "Label": "-6dB" }, { "Value": 7, "Label": "-7dB" }, { "Value": 8, "Label": "-8dB" }, { "Value": 9, "Label": "-9dB" } ], "Selected": "Normal" }, "Units": "dB", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 4, "Node": 37, "Genre": "System", "Help": "PowerLevel to use for the Test", "ValueIDKey": 1125900542066708, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/115/value/1407375518777366/,{ "Label": "Frame Count", "Value": 0, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 5, "Node": 37, "Genre": "System", "Help": "How Many Messages to send to the Note for the Test", "ValueIDKey": 1407375518777366, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/115/value/1688850495488024/,{ "Label": "Test", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 6, "Node": 37, "Genre": "System", "Help": "Perform a PowerLevel Test against the a Node", "ValueIDKey": 1688850495488024, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/115/value/1970325472198680/,{ "Label": "Report", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 7, "Node": 37, "Genre": "System", "Help": "Get the results of the latest PowerLevel Test against a Node", "ValueIDKey": 1970325472198680, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/115/value/2251800448909332/,{ "Label": "Test Status", "Value": { "List": [ { "Value": 0, "Label": "Failed" }, { "Value": 1, "Label": "Success" }, { "Value": 2, "Label": "In Progress" } ], "Selected": "Failed" }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 8, "Node": 37, "Genre": "System", "Help": "The Current Status of the last PowerNode Test Executed", "ValueIDKey": 2251800448909332, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/115/value/2533275425619990/,{ "Label": "Acked Frames", "Value": 0, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 9, "Node": 37, "Genre": "System", "Help": "Number of Messages successfully Acked by the Target Node", "ValueIDKey": 2533275425619990, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/128/,{ "Instance": 1, "CommandClassId": 128, "CommandClass": "COMMAND_CLASS_BATTERY", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/128/value/627048465/,{ "Label": "Battery Level", "Value": 90, "Units": "%", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_BATTERY", "Index": 0, "Node": 37, "Genre": "User", "Help": "Current Battery Level", "ValueIDKey": 627048465, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/132/,{ "Instance": 1, "CommandClassId": 132, "CommandClass": "COMMAND_CLASS_WAKE_UP", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/132/value/281475612213267/,{ "Label": "Minimum Wake-up Interval", "Value": 1800, "Units": "Seconds", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_WAKE_UP", "Index": 1, "Node": 37, "Genre": "System", "Help": "Minimum Time in seconds the device will wake up", "ValueIDKey": 281475612213267, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/132/value/562950588923923/,{ "Label": "Maximum Wake-up Interval", "Value": 64800, "Units": "Seconds", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_WAKE_UP", "Index": 2, "Node": 37, "Genre": "System", "Help": "Maximum Time in seconds the device will wake up", "ValueIDKey": 562950588923923, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/132/value/844425565634579/,{ "Label": "Default Wake-up Interval", "Value": 28800, "Units": "Seconds", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_WAKE_UP", "Index": 3, "Node": 37, "Genre": "System", "Help": "The Default Wake-Up Interval the device will wake up", "ValueIDKey": 844425565634579, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/132/value/1125900542345235/,{ "Label": "Wake-up Interval Step", "Value": 60, "Units": "Seconds", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_WAKE_UP", "Index": 4, "Node": 37, "Genre": "System", "Help": "Step Size on Wake-up interval", "ValueIDKey": 1125900542345235, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/132/value/635502611/,{ "Label": "Wake-up Interval", "Value": 28800, "Units": "Seconds", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_WAKE_UP", "Index": 0, "Node": 37, "Genre": "System", "Help": "How often the Device will Wake up to check for pending commands", "ValueIDKey": 635502611, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/134/,{ "Instance": 1, "CommandClassId": 134, "CommandClass": "COMMAND_CLASS_VERSION", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/134/value/635535383/,{ "Label": "Library Version", "Value": "3", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 0, "Node": 37, "Genre": "System", "Help": "Z-Wave Library Version", "ValueIDKey": 635535383, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/134/value/281475612246039/,{ "Label": "Protocol Version", "Value": "4.61", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 1, "Node": 37, "Genre": "System", "Help": "Z-Wave Protocol Version", "ValueIDKey": 281475612246039, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/134/value/562950588956695/,{ "Label": "Application Version", "Value": "2.15", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 2, "Node": 37, "Genre": "System", "Help": "Application Version", "ValueIDKey": 562950588956695, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/association/1/,{ "Name": "Lifeline", "Help": "", "MaxAssociations": 1, "Members": [ "1.0", "1.1" ], "TimeStamp": 1579566891} -OpenZWave/1/node/37/association/2/,{ "Name": "BasicSet report", "Help": "", "MaxAssociations": 5, "Members": [], "TimeStamp": 1579566891} -OpenZWave/1/node/37/association/3/,{ "Name": "Temperature Alarm report", "Help": "", "MaxAssociations": 5, "Members": [], "TimeStamp": 1579566891} -OpenZWave/1/node/39/,{ "NodeID": 39, "NodeQueryStage": "CacheLoad", "isListening": true, "isFlirs": false, "isBeaming": true, "isRouting": true, "isSecurityv1": false, "isZWavePlus": false, "isNIFRecieved": true, "isAwake": true, "isFailed": false, "MetaData": { "OZWInfoURL": "http://www.openzwave.com/device-database/0371:0002:0103", "ZWAProductURL": "", "ProductPic": "images/aeotec/zwa002.png", "Description": "✓ Standard form factor and appearance of the light bulb with 800 lm output ✓ RGBW: dimmable from 5% to 100%, tunable from 1800K to 6500K, and 16 million colors ✓ Possible to be included in groups, scenes, or schedules ✓ Suitable for indoor lighting: Corridors, Bedroom, Living Room, etc.", "ProductManualURL": "https://Products.Z-WaveAlliance.org/ProductManual/File?folder=&filename=Manuals/2881/AA LED Bulb 6 说明书(RGBW-AL001)_转曲-2dd.pdf", "ProductPageURL": "", "InclusionHelp": "Add for inclusion 1. Ensure the led bulb has been excluded outside the network. 2. Triggered by OFF ->ON (between 0.5-2 seconds each time) 3. LED solid yellow Color (0xFFFF00) during the pairing(Timeout is 10 seconds).  Failure: Blinks between 100% White and Red 0x0000FF color for 3 seconds (at a rate of 200ms per flash), Once 3 seconds have passed, the LED should return to a Warm White LED at 100%  Success: Blinks between 100% White and Green 0x00FF00 color for 3 seconds (at a rate of 200ms per flash). Once 3 seconds have passed, the LED should return to a Warm White LED at 100%.", "ExclusionHelp": "Remove for exclusion 1. Assuming led bulb was added to controller. 2. Triggered by OFF -> ON -> OFF -> ON -> OFF -> ON (between 0.5-2 seconds each time). 3. LED Solid Purple/Violet Color (0xEE82EE) during the unpairing process. (Timeout is 10 seconds).  Failure: Blinks between 100% White and Red 0x0000FF color for 3 seconds (at a rate of 200ms per flash), Once 3 seconds have passed, the LED should return to the last color ( memory status(color cc set)) of LED Bulb.  Success: Blinks between 100% White and Blue 0x0000FF color for 3 seconds (at a rate of 200ms per flash). Once 3 seconds have passed, the LED should return to a Warm White LED at 100%.", "ResetHelp": "Reset the Device. 1. Assuming led bulb was added to controller and was power on. 2. RGBW bulb re-power 6 times (between 0.5-2 seconds each time). Note: ON -> OFF -> ON -> OFF -> ON -> OFF -> ON -> OFF -> ON -> OFF -> ON -> OFF -> ON 3. If the 6th power on, the led bulb change to Yellow color(into pairing process ), which means that the reset factory settings are successf. Using this action in case of the primary controller is missing or inoperable.", "WakeupHelp": "", "ProductSupportURL": "", "Frequency": "", "Name": "LED Bulb 6:Multi-Colour", "ProductPicBase64": "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" }, "Event": "nodeNaming", "TimeStamp": 1579566891, "NodeManufacturerName": "Aeotec Limited", "NodeProductName": "ZWA002 LED Bulb 6 Multi-Color", "NodeBasicString": "Routing Slave", "NodeBasic": 4, "NodeGenericString": "Multilevel Switch", "NodeGeneric": 17, "NodeSpecificString": "Multilevel Power Switch", "NodeSpecific": 1, "NodeManufacturerID": "0x0371", "NodeProductType": "0x0103", "NodeProductID": "0x0002", "NodeBaudRate": 100000, "NodeVersion": 4, "NodeGroups": 1} -OpenZWave/1/node/39/instance/1/,{ "Instance": 1, "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/38/,{ "Instance": 1, "CommandClassId": 38, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/38/value/1407375551070225/,{ "Label": "Dimming Duration", "Value": 255, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 5, "Node": 39, "Genre": "System", "Help": "Duration taken when changing the Level of a Device", "ValueIDKey": 1407375551070225, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/38/value/659128337/,{ "Label": "Level", "Value": 0, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 0, "Node": 39, "Genre": "User", "Help": "The Current Level of the Device", "ValueIDKey": 659128337, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/38/value/281475635839000/,{ "Label": "Bright", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 1, "Node": 39, "Genre": "User", "Help": "Increase the Brightness of the Device", "ValueIDKey": 281475635839000, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/38/value/562950612549656/,{ "Label": "Dim", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 2, "Node": 39, "Genre": "User", "Help": "Decrease the Brightness of the Device", "ValueIDKey": 562950612549656, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/38/value/844425597648912/,{ "Label": "Ignore Start Level", "Value": true, "Units": "", "Min": 0, "Max": 0, "Type": "Bool", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 3, "Node": 39, "Genre": "System", "Help": "Ignore the Start Level of the Device when increasing/decreasing brightness", "ValueIDKey": 844425597648912, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/38/value/1125900574359569/,{ "Label": "Start Level", "Value": 0, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 4, "Node": 39, "Genre": "System", "Help": "Start Level when Changing the Brightness of a Device", "ValueIDKey": 1125900574359569, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/39/,{ "Instance": 1, "CommandClassId": 39, "CommandClass": "COMMAND_CLASS_SWITCH_ALL", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/39/value/667533332/,{ "Label": "Switch All", "Value": { "List": [ { "Value": 0, "Label": "Disabled" }, { "Value": 1, "Label": "Off Enabled" }, { "Value": 2, "Label": "On Enabled" }, { "Value": 255, "Label": "On and Off Enabled" } ], "Selected": "On and Off Enabled" }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_ALL", "Index": 0, "Node": 39, "Genre": "System", "Help": "Switch All Devices On/Off", "ValueIDKey": 667533332, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/51/,{ "Instance": 1, "CommandClassId": 51, "CommandClass": "COMMAND_CLASS_COLOR", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/51/value/562950621151251/,{ "Label": "Color Channels", "Value": 31, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_COLOR", "Index": 2, "Node": 39, "Genre": "System", "Help": "Color Capabilities of the device", "ValueIDKey": 562950621151251, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/51/value/659341335/,{ "Label": "Color", "Value": "#000000FF00", "Units": "#RRGGBBWWCW", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_COLOR", "Index": 0, "Node": 39, "Genre": "User", "Help": "Color (in RGB format)", "ValueIDKey": 659341335, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/51/value/281475636051988/,{ "Label": "Color Index", "Value": { "List": [ { "Value": 0, "Label": "Off" }, { "Value": 1, "Label": "Cool White" }, { "Value": 2, "Label": "Warm White" }, { "Value": 3, "Label": "Red" }, { "Value": 4, "Label": "Lime" }, { "Value": 5, "Label": "Blue" }, { "Value": 6, "Label": "Yellow" }, { "Value": 7, "Label": "Cyan" }, { "Value": 8, "Label": "Magenta" }, { "Value": 9, "Label": "Silver" }, { "Value": 10, "Label": "Gray" }, { "Value": 11, "Label": "Maroon" }, { "Value": 12, "Label": "Olive" }, { "Value": 13, "Label": "Green" }, { "Value": 14, "Label": "Purple" }, { "Value": 15, "Label": "Teal" }, { "Value": 16, "Label": "Navy" }, { "Value": 17, "Label": "Custom" } ], "Selected": "Warm White" }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_COLOR", "Index": 1, "Node": 39, "Genre": "User", "Help": "Preset Color", "ValueIDKey": 281475636051988, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/94/,{ "Instance": 1, "CommandClassId": 94, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/94/value/668434449/,{ "Label": "ZWave+ Version", "Value": 1, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 0, "Node": 39, "Genre": "System", "Help": "ZWave+ Version Supported on the Device", "ValueIDKey": 668434449, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/94/value/281475645145110/,{ "Label": "InstallerIcon", "Value": 1536, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 1, "Node": 39, "Genre": "System", "Help": "Icon File to use for the Installer Application", "ValueIDKey": 281475645145110, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/94/value/562950621855766/,{ "Label": "UserIcon", "Value": 1536, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 2, "Node": 39, "Genre": "System", "Help": "Icon File to use for the User Application", "ValueIDKey": 562950621855766, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/112/,{ "Instance": 1, "CommandClassId": 112, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/112/value/281475641245716/,{ "Label": "User custom mode LED animations", "Value": { "List": [ { "Value": 0, "Label": "Disable" }, { "Value": 1, "Label": "Blink Colors in order mode" }, { "Value": 2, "Label": "Randomized blink color mode" } ], "Selected": "Disable" }, "Units": "", "Min": 0, "Max": 2, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 1, "Node": 39, "Genre": "Config", "Help": "User custom mode for LED animations", "ValueIDKey": 281475641245716, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/112/value/562950617956372/,{ "Label": "Strobe over Custom Color", "Value": { "List": [ { "Value": 0, "Label": "Disable" }, { "Value": 1, "Label": "Enable" } ], "Selected": "Disable" }, "Units": "", "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 2, "Node": 39, "Genre": "Config", "Help": "Enable/Disable Strobe over Custom Color.", "ValueIDKey": 562950617956372, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/112/value/844425594667027/,{ "Label": "Set the rate of change to next color in Custom Mode", "Value": 50, "Units": "ms", "Min": 5, "Max": 8640000, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 3, "Node": 39, "Genre": "Config", "Help": "Set the rate of change to next color in Custom Mode.", "ValueIDKey": 844425594667027, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/112/value/1125900571377681/,{ "Label": "Set color that LED Bulb blinks", "Value": 1, "Units": "", "Min": 1, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 4, "Node": 39, "Genre": "Config", "Help": "Set color that LED Bulb blinks in Blink Mode.", "ValueIDKey": 1125900571377681, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/112/value/4503600291905553/,{ "Label": "Ramp rate when dimming using Multilevel Switch", "Value": 20, "Units": "100ms", "Min": 0, "Max": 100, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 16, "Node": 39, "Genre": "Config", "Help": "Specifying the ramp rate when dimming using Multilevel Switch V1 CC in 100ms.", "ValueIDKey": 4503600291905553, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/112/value/22517998801387540/,{ "Label": "Notification", "Value": { "List": [ { "Value": 0, "Label": "Nothing" }, { "Value": 1, "Label": "Basic CC report" } ], "Selected": "Basic CC report" }, "Units": "", "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 80, "Node": 39, "Genre": "Config", "Help": "Enable to send notifications to associated devices (Group 1) when the state of LED Bulb is changed.", "ValueIDKey": 22517998801387540, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/112/value/22799473778098198/,{ "Label": "Warm White temperature", "Value": 2700, "Units": "k", "Min": 2700, "Max": 4999, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 81, "Node": 39, "Genre": "Config", "Help": "Adjusting the color temperature in warm white color component. available value: 2700k to 4999k", "ValueIDKey": 22799473778098198, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/112/value/23080948754808854/,{ "Label": "cold white temperature", "Value": 6500, "Units": "k", "Min": 5000, "Max": 6500, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 82, "Node": 39, "Genre": "Config", "Help": "Adjusting the color temperature in cold white color component. available value:5000k to 6500k", "ValueIDKey": 23080948754808854, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/114/,{ "Instance": 1, "CommandClassId": 114, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/114/value/668762131/,{ "Label": "Loaded Config Revision", "Value": 3, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 0, "Node": 39, "Genre": "System", "Help": "Revision of the Config file currently loaded", "ValueIDKey": 668762131, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/114/value/281475645472787/,{ "Label": "Config File Revision", "Value": 3, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 1, "Node": 39, "Genre": "System", "Help": "Revision of the Config file on the File System", "ValueIDKey": 281475645472787, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/114/value/562950622183443/,{ "Label": "Latest Available Config File Revision", "Value": 3, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 2, "Node": 39, "Genre": "System", "Help": "Latest Revision of the Config file available for download", "ValueIDKey": 562950622183443, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/114/value/844425598894103/,{ "Label": "Device ID", "Value": "", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 3, "Node": 39, "Genre": "System", "Help": "Manufacturer Specific Device ID/Model", "ValueIDKey": 844425598894103, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/114/value/1125900575604759/,{ "Label": "Serial Number", "Value": "00001cd6bda18c83", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 4, "Node": 39, "Genre": "System", "Help": "Device Serial Number", "ValueIDKey": 1125900575604759, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/,{ "Instance": 1, "CommandClassId": 115, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/668778516/,{ "Label": "Powerlevel", "Value": { "List": [ { "Value": 0, "Label": "Normal" }, { "Value": 1, "Label": "-1dB" }, { "Value": 2, "Label": "-2dB" }, { "Value": 3, "Label": "-3dB" }, { "Value": 4, "Label": "-4dB" }, { "Value": 5, "Label": "-5dB" }, { "Value": 6, "Label": "-6dB" }, { "Value": 7, "Label": "-7dB" }, { "Value": 8, "Label": "-8dB" }, { "Value": 9, "Label": "-9dB" } ], "Selected": "Normal" }, "Units": "dB", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 0, "Node": 39, "Genre": "System", "Help": "Output RF PowerLevel", "ValueIDKey": 668778516, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/281475645489169/,{ "Label": "Timeout", "Value": 0, "Units": "seconds", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 1, "Node": 39, "Genre": "System", "Help": "Timeout till the PowerLevel is reset to Normal", "ValueIDKey": 281475645489169, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/562950622199832/,{ "Label": "Set Powerlevel", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 2, "Node": 39, "Genre": "System", "Help": "Apply the Output PowerLevel and Timeout Values", "ValueIDKey": 562950622199832, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/844425598910481/,{ "Label": "Test Node", "Value": 0, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 3, "Node": 39, "Genre": "System", "Help": "Node to Perform a test against", "ValueIDKey": 844425598910481, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/1125900575621140/,{ "Label": "Test Powerlevel", "Value": { "List": [ { "Value": 0, "Label": "Normal" }, { "Value": 1, "Label": "-1dB" }, { "Value": 2, "Label": "-2dB" }, { "Value": 3, "Label": "-3dB" }, { "Value": 4, "Label": "-4dB" }, { "Value": 5, "Label": "-5dB" }, { "Value": 6, "Label": "-6dB" }, { "Value": 7, "Label": "-7dB" }, { "Value": 8, "Label": "-8dB" }, { "Value": 9, "Label": "-9dB" } ], "Selected": "Normal" }, "Units": "dB", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 4, "Node": 39, "Genre": "System", "Help": "PowerLevel to use for the Test", "ValueIDKey": 1125900575621140, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/1407375552331798/,{ "Label": "Frame Count", "Value": 0, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 5, "Node": 39, "Genre": "System", "Help": "How Many Messages to send to the Note for the Test", "ValueIDKey": 1407375552331798, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/1688850529042456/,{ "Label": "Test", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 6, "Node": 39, "Genre": "System", "Help": "Perform a PowerLevel Test against the a Node", "ValueIDKey": 1688850529042456, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/1970325505753112/,{ "Label": "Report", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 7, "Node": 39, "Genre": "System", "Help": "Get the results of the latest PowerLevel Test against a Node", "ValueIDKey": 1970325505753112, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/2251800482463764/,{ "Label": "Test Status", "Value": { "List": [ { "Value": 0, "Label": "Failed" }, { "Value": 1, "Label": "Success" }, { "Value": 2, "Label": "In Progress" } ], "Selected": "Failed" }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 8, "Node": 39, "Genre": "System", "Help": "The Current Status of the last PowerNode Test Executed", "ValueIDKey": 2251800482463764, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/2533275459174422/,{ "Label": "Acked Frames", "Value": 0, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 9, "Node": 39, "Genre": "System", "Help": "Number of Messages successfully Acked by the Target Node", "ValueIDKey": 2533275459174422, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/134/,{ "Instance": 1, "CommandClassId": 134, "CommandClass": "COMMAND_CLASS_VERSION", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/134/value/669089815/,{ "Label": "Library Version", "Value": "3", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 0, "Node": 39, "Genre": "System", "Help": "Z-Wave Library Version", "ValueIDKey": 669089815, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/134/value/281475645800471/,{ "Label": "Protocol Version", "Value": "4.38", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 1, "Node": 39, "Genre": "System", "Help": "Z-Wave Protocol Version", "ValueIDKey": 281475645800471, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/134/value/562950622511127/,{ "Label": "Application Version", "Value": "2.00", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 2, "Node": 39, "Genre": "System", "Help": "Application Version", "ValueIDKey": 562950622511127, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/association/1/,{ "Name": "Lifeline", "Help": "", "MaxAssociations": 1, "Members": [ "1.0" ], "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/43/,{ "Instance": 1, "CommandClassId": 43, "CommandClass": "COMMAND_CLASS_SCENE_ACTIVATION", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/43/value/562950622511127/,{ "Label": "Scene", "Value": 0, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_SCENE_ACTIVATION", "Index": 0, "Node": 7, "Genre": "User", "Help": "", "ValueIDKey": 122339347, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueChanged", "TimeStamp": 1579630367} -OpenZWave/1/node/39/instance/1/commandclass/91/,{ "Instance": 1, "CommandClassId": 91, "CommandClass": "COMMAND_CLASS_CENTRAL_SCENE", "TimeStamp": 1579630630} -OpenZWave/1/node/39/instance/1/commandclass/91/value/281476005806100/,{ "Label": "Scene 1", "Value": { "List": [ { "Value": 0, "Label": "Inactive" }, { "Value": 1, "Label": "Pressed 1 Time" }, { "Value": 2, "Label": "Key Released" }, { "Value": 3, "Label": "Key Held down" } ], "Selected": "Inactive", "Selected_id": 0 }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CENTRAL_SCENE", "Index": 1, "Node": 61, "Genre": "User", "Help": "", "ValueIDKey": 281476005806100, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueChanged", "TimeStamp": 1579640710} -OpenZWave/1/node/39/statistics/,{ "sendCount": 57, "sentFailed": 0, "retries": 1, "receivedPackets": 3594, "receivedDupPackets": 12, "receivedUnsolicited": 3546, "lastSentTimeStamp": 1595764791, "lastReceivedTimeStamp": 1595802261, "lastRequestRTT": 26, "averageRequestRTT": 29, "lastResponseRTT": 38, "averageResponseRTT": 37, "quality": 0, "extendedTXSupported": false, "txTime": 0, "hops": 0, "rssi_1": "", "rssi_2": "", "rssi_3": "", "rssi_4": "", "rssi_5": "", "route_1": 0, "route_2": 0, "route_3": 0, "route_4": 0, "ackChannel": 0, "lastTXChannel": 0, "routeScheme": "Idle", "routeUsed": "", "routeSpeed": "Auto", "routeTries": 0, "lastFailedLinkFrom": 0, "lastFailedLinkTo": 0} -OpenZWave/1/statistics/,{ "SOFCnt": 92220, "ACKWaiting": 0, "readAborts": 0, "badChecksum": 0, "readCnt": 92220, "writeCnt": 2150, "CANCnt": 0, "NAKCnt": 0, "ACKCnt": 2150, "OOFCnt": 0, "dropped": 27, "retries": 0, "callbacks": 1, "badroutes": 0, "noack": 18, "netbusy": 0, "notidle": 0, "txverified": 0, "nondelivery": 0, "routedbusy": 0, "broadcastReadCnt": 42190, "broadcastWriteCnt": 25} \ No newline at end of file diff --git a/tests/components/ozw/fixtures/light.json b/tests/components/ozw/fixtures/light.json deleted file mode 100644 index 81fd82a4a2b..00000000000 --- a/tests/components/ozw/fixtures/light.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "topic": "OpenZWave/1/node/39/instance/1/commandclass/38/value/659128337/", - "payload": { - "Label": "Level", - "Value": 0, - "Units": "", - "Min": 0, - "Max": 255, - "Type": "Byte", - "Instance": 1, - "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", - "Index": 0, - "Node": 39, - "Genre": "User", - "Help": "The Current Level of the Device", - "ValueIDKey": 659128337, - "ReadOnly": false, - "WriteOnly": false, - "ValueSet": false, - "ValuePolled": false, - "ChangeVerified": false, - "Event": "valueAdded", - "TimeStamp": 1579566891 - } -} diff --git a/tests/components/ozw/fixtures/light_network_dump.csv b/tests/components/ozw/fixtures/light_network_dump.csv deleted file mode 100644 index e9c0d8fb74b..00000000000 --- a/tests/components/ozw/fixtures/light_network_dump.csv +++ /dev/null @@ -1,320 +0,0 @@ -OpenZWave/1/status/,{ "OpenZWave_Version": "1.6.1008", "OZWDeamon_Version": "0.1", "QTOpenZWave_Version": "1.0.0", "QT_Version": "5.12.5", "Status": "driverAllNodesQueried", "TimeStamp": 1579566933, "ManufacturerSpecificDBReady": true, "homeID": 3245146787, "getControllerNodeId": 1, "getSUCNodeId": 1, "isPrimaryController": true, "isBridgeController": false, "hasExtendedTXStatistics": true, "getControllerLibraryVersion": "Z-Wave 3.95", "getControllerLibraryType": "Static Controller", "getControllerPath": "/dev/zwave"} -OpenZWave/1/node/39/,{ "NodeID": 39, "NodeQueryStage": "CacheLoad", "isListening": true, "isFlirs": false, "isBeaming": true, "isRouting": true, "isSecurityv1": false, "isZWavePlus": false, "isNIFRecieved": true, "isAwake": true, "isFailed": false, "MetaData": { "OZWInfoURL": "http://www.openzwave.com/device-database/0371:0002:0103", "ZWAProductURL": "", "ProductPic": "images/aeotec/zwa002.png", "Description": "✓ Standard form factor and appearance of the light bulb with 800 lm output ✓ RGBW: dimmable from 5% to 100%, tunable from 1800K to 6500K, and 16 million colors ✓ Possible to be included in groups, scenes, or schedules ✓ Suitable for indoor lighting: Corridors, Bedroom, Living Room, etc.", "ProductManualURL": "https://Products.Z-WaveAlliance.org/ProductManual/File?folder=&filename=Manuals/2881/AA LED Bulb 6 说明书(RGBW-AL001)_转曲-2dd.pdf", "ProductPageURL": "", "InclusionHelp": "Add for inclusion 1. Ensure the led bulb has been excluded outside the network. 2. Triggered by OFF ->ON (between 0.5-2 seconds each time) 3. LED solid yellow Color (0xFFFF00) during the pairing(Timeout is 10 seconds).  Failure: Blinks between 100% White and Red 0x0000FF color for 3 seconds (at a rate of 200ms per flash), Once 3 seconds have passed, the LED should return to a Warm White LED at 100%  Success: Blinks between 100% White and Green 0x00FF00 color for 3 seconds (at a rate of 200ms per flash). Once 3 seconds have passed, the LED should return to a Warm White LED at 100%.", "ExclusionHelp": "Remove for exclusion 1. Assuming led bulb was added to controller. 2. Triggered by OFF -> ON -> OFF -> ON -> OFF -> ON (between 0.5-2 seconds each time). 3. LED Solid Purple/Violet Color (0xEE82EE) during the unpairing process. (Timeout is 10 seconds).  Failure: Blinks between 100% White and Red 0x0000FF color for 3 seconds (at a rate of 200ms per flash), Once 3 seconds have passed, the LED should return to the last color ( memory status(color cc set)) of LED Bulb.  Success: Blinks between 100% White and Blue 0x0000FF color for 3 seconds (at a rate of 200ms per flash). Once 3 seconds have passed, the LED should return to a Warm White LED at 100%.", "ResetHelp": "Reset the Device. 1. Assuming led bulb was added to controller and was power on. 2. RGBW bulb re-power 6 times (between 0.5-2 seconds each time). Note: ON -> OFF -> ON -> OFF -> ON -> OFF -> ON -> OFF -> ON -> OFF -> ON -> OFF -> ON 3. If the 6th power on, the led bulb change to Yellow color(into pairing process ), which means that the reset factory settings are successf. Using this action in case of the primary controller is missing or inoperable.", "WakeupHelp": "", "ProductSupportURL": "", "Frequency": "", "Name": "LED Bulb 6:Multi-Colour", "ProductPicBase64": "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" }, "Event": "nodeNaming", "TimeStamp": 1579566891, "NodeManufacturerName": "Aeotec Limited", "NodeProductName": "ZWA002 LED Bulb 6 Multi-Color", "NodeBasicString": "Routing Slave", "NodeBasic": 4, "NodeGenericString": "Multilevel Switch", "NodeGeneric": 17, "NodeSpecificString": "Multilevel Power Switch", "NodeSpecific": 1, "NodeManufacturerID": "0x0371", "NodeProductType": "0x0103", "NodeProductID": "0x0002", "NodeBaudRate": 100000, "NodeVersion": 4, "NodeGroups": 1} -OpenZWave/1/node/39/instance/1/,{ "Instance": 1, "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/38/,{ "Instance": 1, "CommandClassId": 38, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/38/value/1407375551070225/,{ "Label": "Dimming Duration", "Value": 255, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 5, "Node": 39, "Genre": "System", "Help": "Duration taken when changing the Level of a Device", "ValueIDKey": 1407375551070225, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/38/value/659128337/,{ "Label": "Level", "Value": 0, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 0, "Node": 39, "Genre": "User", "Help": "The Current Level of the Device", "ValueIDKey": 659128337, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/38/value/281475635839000/,{ "Label": "Bright", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 1, "Node": 39, "Genre": "User", "Help": "Increase the Brightness of the Device", "ValueIDKey": 281475635839000, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/38/value/562950612549656/,{ "Label": "Dim", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 2, "Node": 39, "Genre": "User", "Help": "Decrease the Brightness of the Device", "ValueIDKey": 562950612549656, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/38/value/844425597648912/,{ "Label": "Ignore Start Level", "Value": true, "Units": "", "Min": 0, "Max": 0, "Type": "Bool", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 3, "Node": 39, "Genre": "System", "Help": "Ignore the Start Level of the Device when increasing/decreasing brightness", "ValueIDKey": 844425597648912, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/38/value/1125900574359569/,{ "Label": "Start Level", "Value": 0, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 4, "Node": 39, "Genre": "System", "Help": "Start Level when Changing the Brightness of a Device", "ValueIDKey": 1125900574359569, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/39/,{ "Instance": 1, "CommandClassId": 39, "CommandClass": "COMMAND_CLASS_SWITCH_ALL", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/39/value/667533332/,{ "Label": "Switch All", "Value": { "List": [ { "Value": 0, "Label": "Disabled" }, { "Value": 1, "Label": "Off Enabled" }, { "Value": 2, "Label": "On Enabled" }, { "Value": 255, "Label": "On and Off Enabled" } ], "Selected": "On and Off Enabled" }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_ALL", "Index": 0, "Node": 39, "Genre": "System", "Help": "Switch All Devices On/Off", "ValueIDKey": 667533332, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/51/,{ "Instance": 1, "CommandClassId": 51, "CommandClass": "COMMAND_CLASS_COLOR", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/51/value/562950621151251/,{ "Label": "Color Channels", "Value": 31, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_COLOR", "Index": 2, "Node": 39, "Genre": "System", "Help": "Color Capabilities of the device", "ValueIDKey": 562950621151251, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/51/value/659341335/,{ "Label": "Color", "Value": "#000000FF00", "Units": "#RRGGBBWWCW", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_COLOR", "Index": 0, "Node": 39, "Genre": "User", "Help": "Color (in RGB format)", "ValueIDKey": 659341335, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/51/value/281475636051988/,{ "Label": "Color Index", "Value": { "List": [ { "Value": 0, "Label": "Off" }, { "Value": 1, "Label": "Cool White" }, { "Value": 2, "Label": "Warm White" }, { "Value": 3, "Label": "Red" }, { "Value": 4, "Label": "Lime" }, { "Value": 5, "Label": "Blue" }, { "Value": 6, "Label": "Yellow" }, { "Value": 7, "Label": "Cyan" }, { "Value": 8, "Label": "Magenta" }, { "Value": 9, "Label": "Silver" }, { "Value": 10, "Label": "Gray" }, { "Value": 11, "Label": "Maroon" }, { "Value": 12, "Label": "Olive" }, { "Value": 13, "Label": "Green" }, { "Value": 14, "Label": "Purple" }, { "Value": 15, "Label": "Teal" }, { "Value": 16, "Label": "Navy" }, { "Value": 17, "Label": "Custom" } ], "Selected": "Warm White" }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_COLOR", "Index": 1, "Node": 39, "Genre": "User", "Help": "Preset Color", "ValueIDKey": 281475636051988, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/94/,{ "Instance": 1, "CommandClassId": 94, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/94/value/668434449/,{ "Label": "ZWave+ Version", "Value": 1, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 0, "Node": 39, "Genre": "System", "Help": "ZWave+ Version Supported on the Device", "ValueIDKey": 668434449, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/94/value/281475645145110/,{ "Label": "InstallerIcon", "Value": 1536, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 1, "Node": 39, "Genre": "System", "Help": "Icon File to use for the Installer Application", "ValueIDKey": 281475645145110, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/94/value/562950621855766/,{ "Label": "UserIcon", "Value": 1536, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 2, "Node": 39, "Genre": "System", "Help": "Icon File to use for the User Application", "ValueIDKey": 562950621855766, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/112/,{ "Instance": 1, "CommandClassId": 112, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/112/value/281475641245716/,{ "Label": "User custom mode LED animations", "Value": { "List": [ { "Value": 0, "Label": "Disable" }, { "Value": 1, "Label": "Blink Colors in order mode" }, { "Value": 2, "Label": "Randomized blink color mode" } ], "Selected": "Disable" }, "Units": "", "Min": 0, "Max": 2, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 1, "Node": 39, "Genre": "Config", "Help": "User custom mode for LED animations", "ValueIDKey": 281475641245716, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/112/value/562950617956372/,{ "Label": "Strobe over Custom Color", "Value": { "List": [ { "Value": 0, "Label": "Disable" }, { "Value": 1, "Label": "Enable" } ], "Selected": "Disable" }, "Units": "", "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 2, "Node": 39, "Genre": "Config", "Help": "Enable/Disable Strobe over Custom Color.", "ValueIDKey": 562950617956372, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/112/value/844425594667027/,{ "Label": "Set the rate of change to next color in Custom Mode", "Value": 50, "Units": "ms", "Min": 5, "Max": 8640000, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 3, "Node": 39, "Genre": "Config", "Help": "Set the rate of change to next color in Custom Mode.", "ValueIDKey": 844425594667027, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/112/value/1125900571377681/,{ "Label": "Set color that LED Bulb blinks", "Value": 1, "Units": "", "Min": 1, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 4, "Node": 39, "Genre": "Config", "Help": "Set color that LED Bulb blinks in Blink Mode.", "ValueIDKey": 1125900571377681, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/112/value/4503600291905553/,{ "Label": "Ramp rate when dimming using Multilevel Switch", "Value": 20, "Units": "100ms", "Min": 0, "Max": 100, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 16, "Node": 39, "Genre": "Config", "Help": "Specifying the ramp rate when dimming using Multilevel Switch V1 CC in 100ms.", "ValueIDKey": 4503600291905553, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/112/value/22517998801387540/,{ "Label": "Notification", "Value": { "List": [ { "Value": 0, "Label": "Nothing" }, { "Value": 1, "Label": "Basic CC report" } ], "Selected": "Basic CC report" }, "Units": "", "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 80, "Node": 39, "Genre": "Config", "Help": "Enable to send notifications to associated devices (Group 1) when the state of LED Bulb is changed.", "ValueIDKey": 22517998801387540, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/112/value/22799473778098198/,{ "Label": "Warm White temperature", "Value": 2700, "Units": "k", "Min": 2700, "Max": 4999, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 81, "Node": 39, "Genre": "Config", "Help": "Adjusting the color temperature in warm white color component. available value: 2700k to 4999k", "ValueIDKey": 22799473778098198, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/112/value/23080948754808854/,{ "Label": "cold white temperature", "Value": 6500, "Units": "k", "Min": 5000, "Max": 6500, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 82, "Node": 39, "Genre": "Config", "Help": "Adjusting the color temperature in cold white color component. available value:5000k to 6500k", "ValueIDKey": 23080948754808854, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/114/,{ "Instance": 1, "CommandClassId": 114, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/114/value/668762131/,{ "Label": "Loaded Config Revision", "Value": 3, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 0, "Node": 39, "Genre": "System", "Help": "Revision of the Config file currently loaded", "ValueIDKey": 668762131, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/114/value/281475645472787/,{ "Label": "Config File Revision", "Value": 3, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 1, "Node": 39, "Genre": "System", "Help": "Revision of the Config file on the File System", "ValueIDKey": 281475645472787, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/114/value/562950622183443/,{ "Label": "Latest Available Config File Revision", "Value": 3, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 2, "Node": 39, "Genre": "System", "Help": "Latest Revision of the Config file available for download", "ValueIDKey": 562950622183443, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/114/value/844425598894103/,{ "Label": "Device ID", "Value": "", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 3, "Node": 39, "Genre": "System", "Help": "Manufacturer Specific Device ID/Model", "ValueIDKey": 844425598894103, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/114/value/1125900575604759/,{ "Label": "Serial Number", "Value": "00001cd6bda18c83", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 4, "Node": 39, "Genre": "System", "Help": "Device Serial Number", "ValueIDKey": 1125900575604759, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/,{ "Instance": 1, "CommandClassId": 115, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/668778516/,{ "Label": "Powerlevel", "Value": { "List": [ { "Value": 0, "Label": "Normal" }, { "Value": 1, "Label": "-1dB" }, { "Value": 2, "Label": "-2dB" }, { "Value": 3, "Label": "-3dB" }, { "Value": 4, "Label": "-4dB" }, { "Value": 5, "Label": "-5dB" }, { "Value": 6, "Label": "-6dB" }, { "Value": 7, "Label": "-7dB" }, { "Value": 8, "Label": "-8dB" }, { "Value": 9, "Label": "-9dB" } ], "Selected": "Normal" }, "Units": "dB", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 0, "Node": 39, "Genre": "System", "Help": "Output RF PowerLevel", "ValueIDKey": 668778516, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/281475645489169/,{ "Label": "Timeout", "Value": 0, "Units": "seconds", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 1, "Node": 39, "Genre": "System", "Help": "Timeout till the PowerLevel is reset to Normal", "ValueIDKey": 281475645489169, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/562950622199832/,{ "Label": "Set Powerlevel", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 2, "Node": 39, "Genre": "System", "Help": "Apply the Output PowerLevel and Timeout Values", "ValueIDKey": 562950622199832, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/844425598910481/,{ "Label": "Test Node", "Value": 0, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 3, "Node": 39, "Genre": "System", "Help": "Node to Perform a test against", "ValueIDKey": 844425598910481, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/1125900575621140/,{ "Label": "Test Powerlevel", "Value": { "List": [ { "Value": 0, "Label": "Normal" }, { "Value": 1, "Label": "-1dB" }, { "Value": 2, "Label": "-2dB" }, { "Value": 3, "Label": "-3dB" }, { "Value": 4, "Label": "-4dB" }, { "Value": 5, "Label": "-5dB" }, { "Value": 6, "Label": "-6dB" }, { "Value": 7, "Label": "-7dB" }, { "Value": 8, "Label": "-8dB" }, { "Value": 9, "Label": "-9dB" } ], "Selected": "Normal" }, "Units": "dB", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 4, "Node": 39, "Genre": "System", "Help": "PowerLevel to use for the Test", "ValueIDKey": 1125900575621140, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/1407375552331798/,{ "Label": "Frame Count", "Value": 0, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 5, "Node": 39, "Genre": "System", "Help": "How Many Messages to send to the Note for the Test", "ValueIDKey": 1407375552331798, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/1688850529042456/,{ "Label": "Test", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 6, "Node": 39, "Genre": "System", "Help": "Perform a PowerLevel Test against the a Node", "ValueIDKey": 1688850529042456, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/1970325505753112/,{ "Label": "Report", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 7, "Node": 39, "Genre": "System", "Help": "Get the results of the latest PowerLevel Test against a Node", "ValueIDKey": 1970325505753112, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/2251800482463764/,{ "Label": "Test Status", "Value": { "List": [ { "Value": 0, "Label": "Failed" }, { "Value": 1, "Label": "Success" }, { "Value": 2, "Label": "In Progress" } ], "Selected": "Failed" }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 8, "Node": 39, "Genre": "System", "Help": "The Current Status of the last PowerNode Test Executed", "ValueIDKey": 2251800482463764, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/2533275459174422/,{ "Label": "Acked Frames", "Value": 0, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 9, "Node": 39, "Genre": "System", "Help": "Number of Messages successfully Acked by the Target Node", "ValueIDKey": 2533275459174422, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/134/,{ "Instance": 1, "CommandClassId": 134, "CommandClass": "COMMAND_CLASS_VERSION", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/134/value/669089815/,{ "Label": "Library Version", "Value": "3", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 0, "Node": 39, "Genre": "System", "Help": "Z-Wave Library Version", "ValueIDKey": 669089815, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/134/value/281475645800471/,{ "Label": "Protocol Version", "Value": "4.38", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 1, "Node": 39, "Genre": "System", "Help": "Z-Wave Protocol Version", "ValueIDKey": 281475645800471, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/134/value/562950622511127/,{ "Label": "Application Version", "Value": "2.00", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 2, "Node": 39, "Genre": "System", "Help": "Application Version", "ValueIDKey": 562950622511127, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/association/1/,{ "Name": "Lifeline", "Help": "", "MaxAssociations": 1, "Members": [ "1.0" ], "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/43/,{ "Instance": 1, "CommandClassId": 43, "CommandClass": "COMMAND_CLASS_SCENE_ACTIVATION", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/43/value/562950622511127/,{ "Label": "Scene", "Value": 0, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_SCENE_ACTIVATION", "Index": 0, "Node": 7, "Genre": "User", "Help": "", "ValueIDKey": 122339347, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueChanged", "TimeStamp": 1579630367} -OpenZWave/1/node/39/instance/1/commandclass/91/,{ "Instance": 1, "CommandClassId": 91, "CommandClass": "COMMAND_CLASS_CENTRAL_SCENE", "TimeStamp": 1579630630} -OpenZWave/1/node/39/instance/1/commandclass/91/value/281476005806100/,{ "Label": "Scene 1", "Value": { "List": [ { "Value": 0, "Label": "Inactive" }, { "Value": 1, "Label": "Pressed 1 Time" }, { "Value": 2, "Label": "Key Released" }, { "Value": 3, "Label": "Key Held down" } ], "Selected": "Inactive", "Selected_id": 0 }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CENTRAL_SCENE", "Index": 1, "Node": 61, "Genre": "User", "Help": "", "ValueIDKey": 281476005806100, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueChanged", "TimeStamp": 1579640710} -OpenZWave/1/status/,{ "OpenZWave_Version": "1.6.1008", "OZWDeamon_Version": "0.1", "QTOpenZWave_Version": "1.0.0", "QT_Version": "5.12.5", "Status": "driverAllNodesQueried", "TimeStamp": 1579566933, "ManufacturerSpecificDBReady": true, "homeID": 3245146787, "getControllerNodeId": 1, "getSUCNodeId": 1, "isPrimaryController": true, "isBridgeController": false, "hasExtendedTXStatistics": true, "getControllerLibraryVersion": "Z-Wave 3.95", "getControllerLibraryType": "Static Controller", "getControllerPath": "/dev/zwave"} -OpenZWave/1/node/7/,{ "NodeID": 7, "NodeQueryStage": "Complete", "isListening": true, "isFlirs": false, "isBeaming": true, "isRouting": true, "isSecurityv1": false, "isZWavePlus": false, "isNIFRecieved": true, "isAwake": true, "isFailed": false, "MetaData": { "OZWInfoURL": "http://www.openzwave.com/device-database/010F:1000:0900", "ZWAProductURL": "", "ProductPic": "images/fibaro/fgrgbwm441.png", "Description": "RGBW Controller", "ProductManualURL": "", "ProductPageURL": "", "InclusionHelp": "", "ExclusionHelp": "", "ResetHelp": "", "WakeupHelp": "", "ProductSupportURL": "", "Frequency": "", "Name": "FIBARO RGBW Dimmer", "ProductPicBase64": "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" }, "Event": "nodeQueriesComplete", "TimeStamp": 1597142985, "NodeManufacturerName": "FIBARO System", "NodeProductName": "FGRGBWM441 RGBW Controller", "NodeBasicString": "Routing Slave", "NodeBasic": 4, "NodeGenericString": "Multilevel Switch", "NodeGeneric": 17, "NodeSpecificString": "Multilevel Power Switch", "NodeSpecific": 1, "NodeManufacturerID": "0x010f", "NodeProductType": "0x0900", "NodeProductID": "0x1000", "NodeBaudRate": 40000, "NodeVersion": 4, "NodeGroups": 5, "NodeName": "Kitchen RGB Strip", "NodeLocation": "", "NodeDeviceTypeString": "Unknown Type (0x0000)", "NodeDeviceType": 0, "NodeRole": 0, "NodeRoleString": "Central Controller", "NodePlusType": 0, "NodePlusTypeString": "Z-Wave+ node", "Neighbors": [ 1, 3, 6, 8, 12, 13, 14, 18, 22, 26, 27, 28 ]} -OpenZWave/1/node/7/instance/1/,{ "Instance": 1, "TimeStamp": 1597142799} -OpenZWave/1/node/7/instance/1/commandclass/51/,{ "Instance": 1, "CommandClassId": 51, "CommandClass": "COMMAND_CLASS_COLOR", "CommandClassVersion": 0, "TimeStamp": 1597142799} -OpenZWave/1/node/7/instance/1/commandclass/51/value/122470423/,{ "Label": "Color", "Value": "#FFFFFF00", "Units": "#RRGGBBWW", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_COLOR", "Index": 0, "Node": 7, "Genre": "User", "Help": "Color (in RGB format)", "ValueIDKey": 122470423, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1597142799} -OpenZWave/1/node/7/instance/1/commandclass/51/value/281475099181076/,{ "Label": "Color Index", "Value": { "List": [ { "Value": 0, "Label": "Off" }, { "Value": 1, "Label": "Cool White" }, { "Value": 2, "Label": "Warm White" }, { "Value": 3, "Label": "Red" }, { "Value": 4, "Label": "Lime" }, { "Value": 5, "Label": "Blue" }, { "Value": 6, "Label": "Yellow" }, { "Value": 7, "Label": "Cyan" }, { "Value": 8, "Label": "Magenta" }, { "Value": 9, "Label": "Silver" }, { "Value": 10, "Label": "Gray" }, { "Value": 11, "Label": "Maroon" }, { "Value": 12, "Label": "Olive" }, { "Value": 13, "Label": "Green" }, { "Value": 14, "Label": "Purple" }, { "Value": 15, "Label": "Teal" }, { "Value": 16, "Label": "Navy" }, { "Value": 17, "Label": "Custom" } ], "Selected": "Cool White", "Selected_id": 1 }, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_COLOR", "Index": 1, "Node": 7, "Genre": "User", "Help": "Preset Color", "ValueIDKey": 281475099181076, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1597142799} -OpenZWave/1/node/7/instance/1/commandclass/112/,{ "Instance": 1, "CommandClassId": 112, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "CommandClassVersion": 1, "TimeStamp": 1597142799} -OpenZWave/1/node/7/instance/1/commandclass/112/value/281475104374804/,{ "Label": "Enable/Disable ALL ON/OFF", "Value": { "List": [ { "Value": 0, "Label": "ALL ON disabled/ ALL OFF disabled" }, { "Value": 1, "Label": "ALL ON disabled/ ALL OFF active" }, { "Value": 2, "Label": "ALL ON active / ALL OFF disabled" }, { "Value": 255, "Label": "ALL ON active / ALL OFF active" } ], "Selected": "ALL ON active / ALL OFF active", "Selected_id": 255 }, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 1, "Node": 7, "Genre": "Config", "Help": "Enable/Disable ALL ON/OFF", "ValueIDKey": 281475104374804, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1597142799} -OpenZWave/1/node/7/instance/1/commandclass/112/value/1688849987928084/,{ "Label": "Associations command class choice", "Value": { "List": [ { "Value": 0, "Label": "Normal (Dimmer) - BASIC SET/SWITCH_MULTILEVEL_START/STOP" }, { "Value": 1, "Label": "Normal (RGBW) - COLOR_CONTROL_SET/START/STOP_STATE_CHANGE" }, { "Value": 2, "Label": "Normal (RGBW) - COLOR_CONTROL_SET" }, { "Value": 3, "Label": "Brightness - BASIC SET/SWITCH_MULTILEVEL_START/STOP" }, { "Value": 4, "Label": "Rainbow (RGBW) - COLOR_CONTROL_SET" } ], "Selected": "Normal (Dimmer) - BASIC SET/SWITCH_MULTILEVEL_START/STOP", "Selected_id": 0 }, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 6, "Node": 7, "Genre": "Config", "Help": "Choose which command classes are sent to associated devices.", "ValueIDKey": 1688849987928084, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1597142799} -OpenZWave/1/node/7/instance/1/commandclass/112/value/2251799941349396/,{ "Label": "Outputs state change mode", "Value": { "List": [ { "Value": 0, "Label": "MODE 1 - Constant Speed (speed is defined by parameters 9 and 10)" }, { "Value": 1, "Label": "MODE 2 - Constant Time (RGB/RBGW only. Time is defined by parameter 11)" } ], "Selected": "MODE 1 - Constant Speed (speed is defined by parameters 9 and 10)", "Selected_id": 0 }, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 8, "Node": 7, "Genre": "Config", "Help": "Choose the behaviour of transitions between different levels.", "ValueIDKey": 2251799941349396, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1597142799} -OpenZWave/1/node/7/instance/1/commandclass/112/value/2533274918060049/,{ "Label": "Dimming step value (for MODE 1)", "Value": 1, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 1, "Max": 99, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 9, "Node": 7, "Genre": "Config", "Help": "Size of the step for each change in level during the transition.", "ValueIDKey": 2533274918060049, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1597142799} -OpenZWave/1/node/7/instance/1/commandclass/112/value/2814749894770710/,{ "Label": "Time between dimming steps (for MODE 1)", "Value": 10, "Units": "ms", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 60000, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 10, "Node": 7, "Genre": "Config", "Help": "Time between each step in a transition between levels. Setting this to zero means an instantaneous change.", "ValueIDKey": 2814749894770710, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1597142799} -OpenZWave/1/node/7/instance/1/commandclass/112/value/3096224871481361/,{ "Label": "Time to complete the entire transition (for MODE 2)", "Value": 67, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 11, "Node": 7, "Genre": "Config", "Help": "0 - immediate change; 1->63: 20ms->126ms (value*20ms); 65->127: 1s->63s (value-64)*1s; 129->191: 10s->630s (value-128)*10s; 193->255: 1min->63min (value-192)*1min. Default setting: 67 (3s)", "ValueIDKey": 3096224871481361, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1597142799} -OpenZWave/1/node/7/instance/1/commandclass/112/value/3377699848192017/,{ "Label": "Maximum dimmer level", "Value": 255, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 3, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 12, "Node": 7, "Genre": "Config", "Help": "Maximum brightness level for the dimmer", "ValueIDKey": 3377699848192017, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1597142799} -OpenZWave/1/node/7/instance/1/commandclass/112/value/3659174824902673/,{ "Label": "Minimum dimmer level", "Value": 2, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 2, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 13, "Node": 7, "Genre": "Config", "Help": "Minimum brightness level for the dimmer", "ValueIDKey": 3659174824902673, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1597142799} -OpenZWave/1/node/7/instance/1/commandclass/112/value/3940649801613334/,{ "Label": "Inputs / Outputs configuration", "Value": 4369, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 14, "Node": 7, "Genre": "Config", "Help": "This is too complex to describe here, since this value is built up from 4-bits for each of the 4 channels. Refer to the table in the product manual. Default value is 4369 (1111 in hex).", "ValueIDKey": 3940649801613334, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1597142799} -OpenZWave/1/node/7/instance/1/commandclass/112/value/4222124778323988/,{ "Label": "Option double click", "Value": { "List": [ { "Value": 0, "Label": "Double click disabled" }, { "Value": 1, "Label": "Double click enabled" } ], "Selected": "Double click enabled", "Selected_id": 1 }, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 15, "Node": 7, "Genre": "Config", "Help": "Option double click (lighting set at 100%). 0 - Double click disabled, 1 - Double click enabled. Default setting 1", "ValueIDKey": 4222124778323988, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1597142799} -OpenZWave/1/node/7/instance/1/commandclass/112/value/4503599755034644/,{ "Label": "Saving state before power failure", "Value": { "List": [ { "Value": 0, "Label": "State NOT saved at power failure, all outputs are set to OFF upon power restore" }, { "Value": 1, "Label": "State saved at power failure, all outputs are set to previous state upon power restore" } ], "Selected": "State saved at power failure, all outputs are set to previous state upon power restore", "Selected_id": 1 }, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 16, "Node": 7, "Genre": "Config", "Help": "Saving state before power failure", "ValueIDKey": 4503599755034644, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1597142799} -OpenZWave/1/node/7/instance/1/commandclass/112/value/8444249428983828/,{ "Label": "Alarm", "Value": { "List": [ { "Value": 0, "Label": "INACTIVE - no response to alarm frames" }, { "Value": 1, "Label": "ALARM ON - the device turns on once alarm is detected (all channels set to 99%)" }, { "Value": 2, "Label": "ALARM OFF - the device turns off once alarm is detected (all channels set to 0%)" }, { "Value": 3, "Label": "ALARM PROGRAM - alarm sequence turns on (program selected in parameter 38)" } ], "Selected": "INACTIVE - no response to alarm frames", "Selected_id": 0 }, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 30, "Node": 7, "Genre": "Config", "Help": "Alarm of any type (general alarm, water flooding alarm, smoke alarm: CO, CO2, temperature alarm). Default setting 0 (Inactive)", "ValueIDKey": 8444249428983828, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1597142799} -OpenZWave/1/node/7/instance/1/commandclass/112/value/10696049242669073/,{ "Label": "Alarm sequence program", "Value": 10, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 1, "Max": 10, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 38, "Node": 7, "Genre": "Config", "Help": "Program number selected from the 10 available.", "ValueIDKey": 10696049242669073, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1597142799} -OpenZWave/1/node/7/instance/1/commandclass/112/value/10977524219379734/,{ "Label": "Active PROGRAM alarm time", "Value": 600, "Units": "s", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 1, "Max": 65534, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 39, "Node": 7, "Genre": "Config", "Help": "In ALARM PROGRAM mode (see parameter 30), this defines the time in seconds the program lasts (1s->65534s)", "ValueIDKey": 10977524219379734, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1597142799} -OpenZWave/1/node/7/instance/1/commandclass/112/value/11821949149511700/,{ "Label": "Command class reporting Outputs status change", "Value": { "List": [ { "Value": 0, "Label": "Reporting as a result of inputs and controllers actions (SWITCH MULTILEVEL)" }, { "Value": 1, "Label": "Reporting as a result inputs actions (SWITCH MULTILEVEL)" }, { "Value": 2, "Label": "Reporting as a result inputs actions (COLOUR_CONTROL)" } ], "Selected": "Reporting as a result of inputs and controllers actions (SWITCH MULTILEVEL)", "Selected_id": 0 }, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 42, "Node": 7, "Genre": "Config", "Help": "Specify which command class is used to report output status changes", "ValueIDKey": 11821949149511700, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1597142799} -OpenZWave/1/node/7/instance/1/commandclass/112/value/12103424126222353/,{ "Label": "Reporting 0-10v analog inputs change threshold", "Value": 5, "Units": "*0.1V", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 1, "Max": 100, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 43, "Node": 7, "Genre": "Config", "Help": "Parameter defines a value by which input voltage must change in order to be reported to the main controller. New value is calculated based on last reported value: Default setting: 5 (0.5V). Range: 1->100 - (0.1V->10V).", "ValueIDKey": 12103424126222353, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1597142799} -OpenZWave/1/node/7/instance/1/commandclass/112/value/12384899102933014/,{ "Label": "Power load reporting frequency", "Value": 30, "Units": "s", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 1, "Max": 65534, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 44, "Node": 7, "Genre": "Config", "Help": "Sent if last reported value differs from the current value. Reports will also be sent in case of polling. Default setting: 30 (30s). Range: 1->65534 (1s->65534s) - interval between reports. Zero means reports are only sent in the case of polling, or at turning OFF the device", "ValueIDKey": 12384899102933014, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1597142799} -OpenZWave/1/node/7/instance/1/commandclass/112/value/12666374079643665/,{ "Label": "Reporting changes in energy consumed by controlled devices", "Value": 10, "Units": "*0.01kWh", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 1, "Max": 254, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 45, "Node": 7, "Genre": "Config", "Help": "Interval between energy consumption reports (in kWh). New reported energy consumption value is calculated based on last reported value. 1->254 (0.01kWh->2.54kWh). Zero means changes in consumed energy will not be reported, except in case of polling.", "ValueIDKey": 12666374079643665, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1597142799} -OpenZWave/1/node/7/instance/1/commandclass/112/value/19984723474120724/,{ "Label": "Response to BRIGHTNESS set to 0%", "Value": { "List": [ { "Value": 0, "Label": "Illumination colour set to white (all channels controlled together)" }, { "Value": 1, "Label": "Last set colour is memorized" } ], "Selected": "Last set colour is memorized", "Selected_id": 1 }, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 71, "Node": 7, "Genre": "Config", "Help": "Set whether to remember the previous RGB mix after the brightness has fallen to zero (black)", "ValueIDKey": 19984723474120724, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1597142799} -OpenZWave/1/node/7/instance/1/commandclass/112/value/20266198450831377/,{ "Label": "Starting predefined program", "Value": 1, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 1, "Max": 10, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 72, "Node": 7, "Genre": "Config", "Help": "First predefined program to use when device is set to work in RGB/RGBW mode (parameter 14)", "ValueIDKey": 20266198450831377, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1597142799} -OpenZWave/1/node/7/instance/1/commandclass/112/value/20547673427542036/,{ "Label": "Triple Click Action", "Value": { "List": [ { "Value": 0, "Label": "NODE INFO control frame is sent" }, { "Value": 1, "Label": "Start favourite program" } ], "Selected": "NODE INFO control frame is sent", "Selected_id": 0 }, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 73, "Node": 7, "Genre": "Config", "Help": "Behaviour when an input is triple-clicked", "ValueIDKey": 20547673427542036, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1597142799} -OpenZWave/1/node/7/instance/1/commandclass/38/,{ "Instance": 1, "CommandClassId": 38, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "CommandClassVersion": 0, "TimeStamp": 1597142799} -OpenZWave/1/node/7/instance/1/commandclass/38/value/122257425/,{ "Label": "Level", "Value": 99, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 0, "Node": 7, "Genre": "User", "Help": "The Current Level of the Device", "ValueIDKey": 122257425, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1597142969} -OpenZWave/1/node/7/instance/1/commandclass/38/value/281475098968088/,{ "Label": "Bright", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 1, "Node": 7, "Genre": "User", "Help": "Increase the Brightness of the Device", "ValueIDKey": 281475098968088, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1597142799} -OpenZWave/1/node/7/instance/1/commandclass/38/value/562950075678744/,{ "Label": "Dim", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 2, "Node": 7, "Genre": "User", "Help": "Decrease the Brightness of the Device", "ValueIDKey": 562950075678744, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1597142799} -OpenZWave/1/node/7/instance/1/commandclass/38/value/844425060778000/,{ "Label": "Ignore Start Level", "Value": true, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Bool", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 3, "Node": 7, "Genre": "System", "Help": "Ignore the Start Level of the Device when increasing/decreasing brightness", "ValueIDKey": 844425060778000, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1597142799} -OpenZWave/1/node/7/instance/1/commandclass/38/value/1125900037488657/,{ "Label": "Start Level", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 4, "Node": 7, "Genre": "System", "Help": "Start Level when Changing the Brightness of a Device", "ValueIDKey": 1125900037488657, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1597142799} -OpenZWave/1/node/7/instance/1/commandclass/39/,{ "Instance": 1, "CommandClassId": 39, "CommandClass": "COMMAND_CLASS_SWITCH_ALL", "CommandClassVersion": 1, "TimeStamp": 1597142799} -OpenZWave/1/node/7/instance/1/commandclass/39/value/130662420/,{ "Label": "Switch All", "Value": { "List": [ { "Value": 0, "Label": "Disabled" }, { "Value": 1, "Label": "Off Enabled" }, { "Value": 2, "Label": "On Enabled" }, { "Value": 255, "Label": "On and Off Enabled" } ], "Selected": "On and Off Enabled", "Selected_id": 255 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_ALL", "Index": 0, "Node": 7, "Genre": "System", "Help": "Switch All Devices On/Off", "ValueIDKey": 130662420, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1597142969} -OpenZWave/1/node/7/instance/1/commandclass/114/,{ "Instance": 1, "CommandClassId": 114, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "CommandClassVersion": 0, "TimeStamp": 1597142799} -OpenZWave/1/node/7/instance/1/commandclass/114/value/131891219/,{ "Label": "Loaded Config Revision", "Value": 5, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 0, "Node": 7, "Genre": "System", "Help": "Revision of the Config file currently loaded", "ValueIDKey": 131891219, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1597142799} -OpenZWave/1/node/7/instance/1/commandclass/114/value/281475108601875/,{ "Label": "Config File Revision", "Value": 5, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 1, "Node": 7, "Genre": "System", "Help": "Revision of the Config file on the File System", "ValueIDKey": 281475108601875, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1597142799} -OpenZWave/1/node/7/instance/1/commandclass/114/value/562950085312531/,{ "Label": "Latest Available Config File Revision", "Value": 5, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 2, "Node": 7, "Genre": "System", "Help": "Latest Revision of the Config file available for download", "ValueIDKey": 562950085312531, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1597142799} -OpenZWave/1/node/7/association/1/,{ "Name": "Input 1", "Help": "", "MaxAssociations": 5, "Members": [], "TimeStamp": 1597142866} -OpenZWave/1/node/7/association/2/,{ "Name": "Input 2", "Help": "", "MaxAssociations": 5, "Members": [], "TimeStamp": 1597142866} -OpenZWave/1/node/7/association/3/,{ "Name": "Input 3", "Help": "", "MaxAssociations": 5, "Members": [], "TimeStamp": 1597142866} -OpenZWave/1/node/7/association/4/,{ "Name": "Input 4", "Help": "", "MaxAssociations": 5, "Members": [], "TimeStamp": 1597142866} -OpenZWave/1/node/7/association/5/,{ "Name": "Lifeline", "Help": "", "MaxAssociations": 1, "Members": [ "1.0" ], "TimeStamp": 1597142799} -OpenZWave/1/node/7/statistics/,{ "sendCount": 9, "sentFailed": 0, "retries": 0, "receivedPackets": 8, "receivedDupPackets": 1, "receivedUnsolicited": 0, "lastSentTimeStamp": 1597142969, "lastReceivedTimeStamp": 1597142969, "lastRequestRTT": 26, "averageRequestRTT": 42, "lastResponseRTT": 55, "averageResponseRTT": 62, "quality": 0, "extendedTXSupported": false, "txTime": 0, "hops": 0, "rssi_1": "", "rssi_2": "", "rssi_3": "", "rssi_4": "", "rssi_5": "", "route_1": 0, "route_2": 0, "route_3": 0, "route_4": 0, "ackChannel": 0, "lastTXChannel": 0, "routeScheme": "Idle", "routeUsed": "", "routeSpeed": "Auto", "routeTries": 0, "lastFailedLinkFrom": 0, "lastFailedLinkTo": 0} -OpenZWave/1/node/7/instance/1/commandclass/112/value/281475104374804/,{ "Label": "Enable/Disable ALL ON/OFF", "Value": { "List": [ { "Value": 0, "Label": "ALL ON disabled/ ALL OFF disabled" }, { "Value": 1, "Label": "ALL ON disabled/ ALL OFF active" }, { "Value": 2, "Label": "ALL ON active / ALL OFF disabled" }, { "Value": 255, "Label": "ALL ON active / ALL OFF active" } ], "Selected": "ALL ON active / ALL OFF active", "Selected_id": 255 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 1, "Node": 7, "Genre": "Config", "Help": "Enable/Disable ALL ON/OFF", "ValueIDKey": 281475104374804, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1597143008} -OpenZWave/1/node/7/instance/1/commandclass/112/value/1688849987928084/,{ "Label": "Associations command class choice", "Value": { "List": [ { "Value": 0, "Label": "Normal (Dimmer) - BASIC SET/SWITCH_MULTILEVEL_START/STOP" }, { "Value": 1, "Label": "Normal (RGBW) - COLOR_CONTROL_SET/START/STOP_STATE_CHANGE" }, { "Value": 2, "Label": "Normal (RGBW) - COLOR_CONTROL_SET" }, { "Value": 3, "Label": "Brightness - BASIC SET/SWITCH_MULTILEVEL_START/STOP" }, { "Value": 4, "Label": "Rainbow (RGBW) - COLOR_CONTROL_SET" } ], "Selected": "Normal (Dimmer) - BASIC SET/SWITCH_MULTILEVEL_START/STOP", "Selected_id": 0 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 6, "Node": 7, "Genre": "Config", "Help": "Choose which command classes are sent to associated devices.", "ValueIDKey": 1688849987928084, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1597143008} -OpenZWave/1/node/7/instance/1/commandclass/112/value/2251799941349396/,{ "Label": "Outputs state change mode", "Value": { "List": [ { "Value": 0, "Label": "MODE 1 - Constant Speed (speed is defined by parameters 9 and 10)" }, { "Value": 1, "Label": "MODE 2 - Constant Time (RGB/RBGW only. Time is defined by parameter 11)" } ], "Selected": "MODE 1 - Constant Speed (speed is defined by parameters 9 and 10)", "Selected_id": 0 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 8, "Node": 7, "Genre": "Config", "Help": "Choose the behaviour of transitions between different levels.", "ValueIDKey": 2251799941349396, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1597143008} -OpenZWave/1/node/7/instance/1/commandclass/112/value/2533274918060049/,{ "Label": "Dimming step value (for MODE 1)", "Value": 1, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 1, "Max": 99, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 9, "Node": 7, "Genre": "Config", "Help": "Size of the step for each change in level during the transition.", "ValueIDKey": 2533274918060049, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1597143009} -OpenZWave/1/node/7/instance/1/commandclass/112/value/2814749894770710/,{ "Label": "Time between dimming steps (for MODE 1)", "Value": 10, "Units": "ms", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 60000, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 10, "Node": 7, "Genre": "Config", "Help": "Time between each step in a transition between levels. Setting this to zero means an instantaneous change.", "ValueIDKey": 2814749894770710, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1597143009} -OpenZWave/1/node/7/instance/1/commandclass/112/value/3096224871481361/,{ "Label": "Time to complete the entire transition (for MODE 2)", "Value": 67, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 11, "Node": 7, "Genre": "Config", "Help": "0 - immediate change; 1->63: 20ms->126ms (value*20ms); 65->127: 1s->63s (value-64)*1s; 129->191: 10s->630s (value-128)*10s; 193->255: 1min->63min (value-192)*1min. Default setting: 67 (3s)", "ValueIDKey": 3096224871481361, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1597143009} -OpenZWave/1/node/7/instance/1/commandclass/112/value/3377699848192017/,{ "Label": "Maximum dimmer level", "Value": 255, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 3, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 12, "Node": 7, "Genre": "Config", "Help": "Maximum brightness level for the dimmer", "ValueIDKey": 3377699848192017, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1597143009} -OpenZWave/1/node/7/instance/1/commandclass/112/value/3659174824902673/,{ "Label": "Minimum dimmer level", "Value": 2, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 2, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 13, "Node": 7, "Genre": "Config", "Help": "Minimum brightness level for the dimmer", "ValueIDKey": 3659174824902673, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1597143009} -OpenZWave/1/node/7/instance/1/commandclass/112/value/3940649801613334/,{ "Label": "Inputs / Outputs configuration", "Value": 4369, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 14, "Node": 7, "Genre": "Config", "Help": "This is too complex to describe here, since this value is built up from 4-bits for each of the 4 channels. Refer to the table in the product manual. Default value is 4369 (1111 in hex).", "ValueIDKey": 3940649801613334, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1597143009} -OpenZWave/1/node/7/instance/1/commandclass/112/value/4222124778323988/,{ "Label": "Option double click", "Value": { "List": [ { "Value": 0, "Label": "Double click disabled" }, { "Value": 1, "Label": "Double click enabled" } ], "Selected": "Double click enabled", "Selected_id": 1 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 15, "Node": 7, "Genre": "Config", "Help": "Option double click (lighting set at 100%). 0 - Double click disabled, 1 - Double click enabled. Default setting 1", "ValueIDKey": 4222124778323988, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1597143009} -OpenZWave/1/node/7/instance/1/commandclass/112/value/4503599755034644/,{ "Label": "Saving state before power failure", "Value": { "List": [ { "Value": 0, "Label": "State NOT saved at power failure, all outputs are set to OFF upon power restore" }, { "Value": 1, "Label": "State saved at power failure, all outputs are set to previous state upon power restore" } ], "Selected": "State saved at power failure, all outputs are set to previous state upon power restore", "Selected_id": 1 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 16, "Node": 7, "Genre": "Config", "Help": "Saving state before power failure", "ValueIDKey": 4503599755034644, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1597143009} -OpenZWave/1/node/7/instance/1/commandclass/112/value/8444249428983828/,{ "Label": "Alarm", "Value": { "List": [ { "Value": 0, "Label": "INACTIVE - no response to alarm frames" }, { "Value": 1, "Label": "ALARM ON - the device turns on once alarm is detected (all channels set to 99%)" }, { "Value": 2, "Label": "ALARM OFF - the device turns off once alarm is detected (all channels set to 0%)" }, { "Value": 3, "Label": "ALARM PROGRAM - alarm sequence turns on (program selected in parameter 38)" } ], "Selected": "INACTIVE - no response to alarm frames", "Selected_id": 0 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 30, "Node": 7, "Genre": "Config", "Help": "Alarm of any type (general alarm, water flooding alarm, smoke alarm: CO, CO2, temperature alarm). Default setting 0 (Inactive)", "ValueIDKey": 8444249428983828, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1597143009} -OpenZWave/1/node/7/instance/1/commandclass/112/value/10696049242669073/,{ "Label": "Alarm sequence program", "Value": 10, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 1, "Max": 10, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 38, "Node": 7, "Genre": "Config", "Help": "Program number selected from the 10 available.", "ValueIDKey": 10696049242669073, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1597143009} -OpenZWave/1/node/7/instance/1/commandclass/112/value/10977524219379734/,{ "Label": "Active PROGRAM alarm time", "Value": 600, "Units": "s", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 1, "Max": 65534, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 39, "Node": 7, "Genre": "Config", "Help": "In ALARM PROGRAM mode (see parameter 30), this defines the time in seconds the program lasts (1s->65534s)", "ValueIDKey": 10977524219379734, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1597143009} -OpenZWave/1/node/7/instance/1/commandclass/112/value/11821949149511700/,{ "Label": "Command class reporting Outputs status change", "Value": { "List": [ { "Value": 0, "Label": "Reporting as a result of inputs and controllers actions (SWITCH MULTILEVEL)" }, { "Value": 1, "Label": "Reporting as a result inputs actions (SWITCH MULTILEVEL)" }, { "Value": 2, "Label": "Reporting as a result inputs actions (COLOUR_CONTROL)" } ], "Selected": "Reporting as a result of inputs and controllers actions (SWITCH MULTILEVEL)", "Selected_id": 0 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 42, "Node": 7, "Genre": "Config", "Help": "Specify which command class is used to report output status changes", "ValueIDKey": 11821949149511700, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1597143009} -OpenZWave/1/node/7/instance/1/commandclass/112/value/12103424126222353/,{ "Label": "Reporting 0-10v analog inputs change threshold", "Value": 5, "Units": "*0.1V", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 1, "Max": 100, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 43, "Node": 7, "Genre": "Config", "Help": "Parameter defines a value by which input voltage must change in order to be reported to the main controller. New value is calculated based on last reported value: Default setting: 5 (0.5V). Range: 1->100 - (0.1V->10V).", "ValueIDKey": 12103424126222353, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1597143009} -OpenZWave/1/node/7/instance/1/commandclass/112/value/12384899102933014/,{ "Label": "Power load reporting frequency", "Value": 30, "Units": "s", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 1, "Max": 65534, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 44, "Node": 7, "Genre": "Config", "Help": "Sent if last reported value differs from the current value. Reports will also be sent in case of polling. Default setting: 30 (30s). Range: 1->65534 (1s->65534s) - interval between reports. Zero means reports are only sent in the case of polling, or at turning OFF the device", "ValueIDKey": 12384899102933014, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1597143009} -OpenZWave/1/node/7/instance/1/commandclass/112/value/12666374079643665/,{ "Label": "Reporting changes in energy consumed by controlled devices", "Value": 10, "Units": "*0.01kWh", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 1, "Max": 254, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 45, "Node": 7, "Genre": "Config", "Help": "Interval between energy consumption reports (in kWh). New reported energy consumption value is calculated based on last reported value. 1->254 (0.01kWh->2.54kWh). Zero means changes in consumed energy will not be reported, except in case of polling.", "ValueIDKey": 12666374079643665, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1597143009} -OpenZWave/1/node/7/instance/1/commandclass/112/value/19984723474120724/,{ "Label": "Response to BRIGHTNESS set to 0%", "Value": { "List": [ { "Value": 0, "Label": "Illumination colour set to white (all channels controlled together)" }, { "Value": 1, "Label": "Last set colour is memorized" } ], "Selected": "Last set colour is memorized", "Selected_id": 1 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 71, "Node": 7, "Genre": "Config", "Help": "Set whether to remember the previous RGB mix after the brightness has fallen to zero (black)", "ValueIDKey": 19984723474120724, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1597143009} -OpenZWave/1/node/7/instance/1/commandclass/112/value/20266198450831377/,{ "Label": "Starting predefined program", "Value": 1, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 1, "Max": 10, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 72, "Node": 7, "Genre": "Config", "Help": "First predefined program to use when device is set to work in RGB/RGBW mode (parameter 14)", "ValueIDKey": 20266198450831377, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1597143009} -OpenZWave/1/node/7/instance/1/commandclass/112/value/20547673427542036/,{ "Label": "Triple Click Action", "Value": { "List": [ { "Value": 0, "Label": "NODE INFO control frame is sent" }, { "Value": 1, "Label": "Start favourite program" } ], "Selected": "NODE INFO control frame is sent", "Selected_id": 0 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 73, "Node": 7, "Genre": "Config", "Help": "Behaviour when an input is triple-clicked", "ValueIDKey": 20547673427542036, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1597143009} -OpenZWave/1/node/7/,{ "NodeID": 7, "NodeQueryStage": "Complete", "isListening": true, "isFlirs": false, "isBeaming": true, "isRouting": true, "isSecurityv1": false, "isZWavePlus": false, "isNIFRecieved": true, "isAwake": true, "isFailed": false, "MetaData": { "OZWInfoURL": "http://www.openzwave.com/device-database/010F:1000:0900", "ZWAProductURL": "", "ProductPic": "images/fibaro/fgrgbwm441.png", "Description": "RGBW Controller", "ProductManualURL": "", "ProductPageURL": "", "InclusionHelp": "", "ExclusionHelp": "", "ResetHelp": "", "WakeupHelp": "", "ProductSupportURL": "", "Frequency": "", "Name": "FIBARO RGBW Dimmer", "ProductPicBase64": "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" }, "Event": "nodeQueriesComplete", "TimeStamp": 1597143009, "NodeManufacturerName": "FIBARO System", "NodeProductName": "FGRGBWM441 RGBW Controller", "NodeBasicString": "Routing Slave", "NodeBasic": 4, "NodeGenericString": "Multilevel Switch", "NodeGeneric": 17, "NodeSpecificString": "Multilevel Power Switch", "NodeSpecific": 1, "NodeManufacturerID": "0x010f", "NodeProductType": "0x0900", "NodeProductID": "0x1000", "NodeBaudRate": 40000, "NodeVersion": 4, "NodeGroups": 5, "NodeName": "Kitchen RGB Strip", "NodeLocation": "", "NodeDeviceTypeString": "Unknown Type (0x0000)", "NodeDeviceType": 0, "NodeRole": 0, "NodeRoleString": "Central Controller", "NodePlusType": 0, "NodePlusTypeString": "Z-Wave+ node", "Neighbors": [ 1, 3, 6, 8, 12, 13, 14, 18, 22, 26, 27, 28 ], "Neighbors": [ 1, 3, 6, 8, 12, 13, 14, 18, 22, 26, 27, 28 ]} -OpenZWave/1/node/7/statistics/,{ "sendCount": 30, "sentFailed": 0, "retries": 0, "receivedPackets": 29, "receivedDupPackets": 1, "receivedUnsolicited": 0, "lastSentTimeStamp": 1597143009, "lastReceivedTimeStamp": 1597143009, "lastRequestRTT": 26, "averageRequestRTT": 25, "lastResponseRTT": 37, "averageResponseRTT": 37, "quality": 0, "extendedTXSupported": false, "txTime": 0, "hops": 0, "rssi_1": "", "rssi_2": "", "rssi_3": "", "rssi_4": "", "rssi_5": "", "route_1": 0, "route_2": 0, "route_3": 0, "route_4": 0, "ackChannel": 0, "lastTXChannel": 0, "routeScheme": "Idle", "routeUsed": "", "routeSpeed": "Auto", "routeTries": 0, "lastFailedLinkFrom": 0, "lastFailedLinkTo": 0} -OpenZWave/1/node/7/statistics/,{ "sendCount": 30, "sentFailed": 0, "retries": 0, "receivedPackets": 29, "receivedDupPackets": 1, "receivedUnsolicited": 0, "lastSentTimeStamp": 1597143009, "lastReceivedTimeStamp": 1597143009, "lastRequestRTT": 26, "averageRequestRTT": 25, "lastResponseRTT": 37, "averageResponseRTT": 37, "quality": 0, "extendedTXSupported": false, "txTime": 0, "hops": 0, "rssi_1": "", "rssi_2": "", "rssi_3": "", "rssi_4": "", "rssi_5": "", "route_1": 0, "route_2": 0, "route_3": 0, "route_4": 0, "ackChannel": 0, "lastTXChannel": 0, "routeScheme": "Idle", "routeUsed": "", "routeSpeed": "Auto", "routeTries": 0, "lastFailedLinkFrom": 0, "lastFailedLinkTo": 0} -OpenZWave/1/node/7/statistics/,{ "sendCount": 30, "sentFailed": 0, "retries": 0, "receivedPackets": 29, "receivedDupPackets": 1, "receivedUnsolicited": 0, "lastSentTimeStamp": 1597143009, "lastReceivedTimeStamp": 1597143009, "lastRequestRTT": 26, "averageRequestRTT": 25, "lastResponseRTT": 37, "averageResponseRTT": 37, "quality": 0, "extendedTXSupported": false, "txTime": 0, "hops": 0, "rssi_1": "", "rssi_2": "", "rssi_3": "", "rssi_4": "", "rssi_5": "", "route_1": 0, "route_2": 0, "route_3": 0, "route_4": 0, "ackChannel": 0, "lastTXChannel": 0, "routeScheme": "Idle", "routeUsed": "", "routeSpeed": "Auto", "routeTries": 0, "lastFailedLinkFrom": 0, "lastFailedLinkTo": 0} -OpenZWave/1/node/7/statistics/,{ "sendCount": 30, "sentFailed": 0, "retries": 0, "receivedPackets": 29, "receivedDupPackets": 1, "receivedUnsolicited": 0, "lastSentTimeStamp": 1597143009, "lastReceivedTimeStamp": 1597143009, "lastRequestRTT": 26, "averageRequestRTT": 25, "lastResponseRTT": 37, "averageResponseRTT": 37, "quality": 0, "extendedTXSupported": false, "txTime": 0, "hops": 0, "rssi_1": "", "rssi_2": "", "rssi_3": "", "rssi_4": "", "rssi_5": "", "route_1": 0, "route_2": 0, "route_3": 0, "route_4": 0, "ackChannel": 0, "lastTXChannel": 0, "routeScheme": "Idle", "routeUsed": "", "routeSpeed": "Auto", "routeTries": 0, "lastFailedLinkFrom": 0, "lastFailedLinkTo": 0} -OpenZWave/1/node/7/statistics/,{ "sendCount": 30, "sentFailed": 0, "retries": 0, "receivedPackets": 29, "receivedDupPackets": 1, "receivedUnsolicited": 0, "lastSentTimeStamp": 1597143009, "lastReceivedTimeStamp": 1597143009, "lastRequestRTT": 26, "averageRequestRTT": 25, "lastResponseRTT": 37, "averageResponseRTT": 37, "quality": 0, "extendedTXSupported": false, "txTime": 0, "hops": 0, "rssi_1": "", "rssi_2": "", "rssi_3": "", "rssi_4": "", "rssi_5": "", "route_1": 0, "route_2": 0, "route_3": 0, "route_4": 0, "ackChannel": 0, "lastTXChannel": 0, "routeScheme": "Idle", "routeUsed": "", "routeSpeed": "Auto", "routeTries": 0, "lastFailedLinkFrom": 0, "lastFailedLinkTo": 0} -OpenZWave/1/node/7/statistics/,{ "sendCount": 30, "sentFailed": 0, "retries": 0, "receivedPackets": 29, "receivedDupPackets": 1, "receivedUnsolicited": 0, "lastSentTimeStamp": 1597143009, "lastReceivedTimeStamp": 1597143009, "lastRequestRTT": 26, "averageRequestRTT": 25, "lastResponseRTT": 37, "averageResponseRTT": 37, "quality": 0, "extendedTXSupported": false, "txTime": 0, "hops": 0, "rssi_1": "", "rssi_2": "", "rssi_3": "", "rssi_4": "", "rssi_5": "", "route_1": 0, "route_2": 0, "route_3": 0, "route_4": 0, "ackChannel": 0, "lastTXChannel": 0, "routeScheme": "Idle", "routeUsed": "", "routeSpeed": "Auto", "routeTries": 0, "lastFailedLinkFrom": 0, "lastFailedLinkTo": 0} -OpenZWave/1/node/7/statistics/,{ "sendCount": 30, "sentFailed": 0, "retries": 0, "receivedPackets": 29, "receivedDupPackets": 1, "receivedUnsolicited": 0, "lastSentTimeStamp": 1597143009, "lastReceivedTimeStamp": 1597143009, "lastRequestRTT": 26, "averageRequestRTT": 25, "lastResponseRTT": 37, "averageResponseRTT": 37, "quality": 0, "extendedTXSupported": false, "txTime": 0, "hops": 0, "rssi_1": "", "rssi_2": "", "rssi_3": "", "rssi_4": "", "rssi_5": "", "route_1": 0, "route_2": 0, "route_3": 0, "route_4": 0, "ackChannel": 0, "lastTXChannel": 0, "routeScheme": "Idle", "routeUsed": "", "routeSpeed": "Auto", "routeTries": 0, "lastFailedLinkFrom": 0, "lastFailedLinkTo": 0} -OpenZWave/1/node/7/statistics/,{ "sendCount": 30, "sentFailed": 0, "retries": 0, "receivedPackets": 29, "receivedDupPackets": 1, "receivedUnsolicited": 0, "lastSentTimeStamp": 1597143009, "lastReceivedTimeStamp": 1597143009, "lastRequestRTT": 26, "averageRequestRTT": 25, "lastResponseRTT": 37, "averageResponseRTT": 37, "quality": 0, "extendedTXSupported": false, "txTime": 0, "hops": 0, "rssi_1": "", "rssi_2": "", "rssi_3": "", "rssi_4": "", "rssi_5": "", "route_1": 0, "route_2": 0, "route_3": 0, "route_4": 0, "ackChannel": 0, "lastTXChannel": 0, "routeScheme": "Idle", "routeUsed": "", "routeSpeed": "Auto", "routeTries": 0, "lastFailedLinkFrom": 0, "lastFailedLinkTo": 0} -OpenZWave/1/node/7/statistics/,{ "sendCount": 30, "sentFailed": 0, "retries": 0, "receivedPackets": 29, "receivedDupPackets": 1, "receivedUnsolicited": 0, "lastSentTimeStamp": 1597143009, "lastReceivedTimeStamp": 1597143009, "lastRequestRTT": 26, "averageRequestRTT": 25, "lastResponseRTT": 37, "averageResponseRTT": 37, "quality": 0, "extendedTXSupported": false, "txTime": 0, "hops": 0, "rssi_1": "", "rssi_2": "", "rssi_3": "", "rssi_4": "", "rssi_5": "", "route_1": 0, "route_2": 0, "route_3": 0, "route_4": 0, "ackChannel": 0, "lastTXChannel": 0, "routeScheme": "Idle", "routeUsed": "", "routeSpeed": "Auto", "routeTries": 0, "lastFailedLinkFrom": 0, "lastFailedLinkTo": 0} -OpenZWave/1/node/7/statistics/,{ "sendCount": 30, "sentFailed": 0, "retries": 0, "receivedPackets": 29, "receivedDupPackets": 1, "receivedUnsolicited": 0, "lastSentTimeStamp": 1597143009, "lastReceivedTimeStamp": 1597143009, "lastRequestRTT": 26, "averageRequestRTT": 25, "lastResponseRTT": 37, "averageResponseRTT": 37, "quality": 0, "extendedTXSupported": false, "txTime": 0, "hops": 0, "rssi_1": "", "rssi_2": "", "rssi_3": "", "rssi_4": "", "rssi_5": "", "route_1": 0, "route_2": 0, "route_3": 0, "route_4": 0, "ackChannel": 0, "lastTXChannel": 0, "routeScheme": "Idle", "routeUsed": "", "routeSpeed": "Auto", "routeTries": 0, "lastFailedLinkFrom": 0, "lastFailedLinkTo": 0} -OpenZWave/1/node/7/statistics/,{ "sendCount": 30, "sentFailed": 0, "retries": 0, "receivedPackets": 29, "receivedDupPackets": 1, "receivedUnsolicited": 0, "lastSentTimeStamp": 1597143009, "lastReceivedTimeStamp": 1597143009, "lastRequestRTT": 26, "averageRequestRTT": 25, "lastResponseRTT": 37, "averageResponseRTT": 37, "quality": 0, "extendedTXSupported": false, "txTime": 0, "hops": 0, "rssi_1": "", "rssi_2": "", "rssi_3": "", "rssi_4": "", "rssi_5": "", "route_1": 0, "route_2": 0, "route_3": 0, "route_4": 0, "ackChannel": 0, "lastTXChannel": 0, "routeScheme": "Idle", "routeUsed": "", "routeSpeed": "Auto", "routeTries": 0, "lastFailedLinkFrom": 0, "lastFailedLinkTo": 0} -OpenZWave/1/node/7/statistics/,{ "sendCount": 30, "sentFailed": 0, "retries": 0, "receivedPackets": 29, "receivedDupPackets": 1, "receivedUnsolicited": 0, "lastSentTimeStamp": 1597143009, "lastReceivedTimeStamp": 1597143009, "lastRequestRTT": 26, "averageRequestRTT": 25, "lastResponseRTT": 37, "averageResponseRTT": 37, "quality": 0, "extendedTXSupported": false, "txTime": 0, "hops": 0, "rssi_1": "", "rssi_2": "", "rssi_3": "", "rssi_4": "", "rssi_5": "", "route_1": 0, "route_2": 0, "route_3": 0, "route_4": 0, "ackChannel": 0, "lastTXChannel": 0, "routeScheme": "Idle", "routeUsed": "", "routeSpeed": "Auto", "routeTries": 0, "lastFailedLinkFrom": 0, "lastFailedLinkTo": 0} -OpenZWave/1/node/7/statistics/,{ "sendCount": 30, "sentFailed": 0, "retries": 0, "receivedPackets": 29, "receivedDupPackets": 1, "receivedUnsolicited": 0, "lastSentTimeStamp": 1597143009, "lastReceivedTimeStamp": 1597143009, "lastRequestRTT": 26, "averageRequestRTT": 25, "lastResponseRTT": 37, "averageResponseRTT": 37, "quality": 0, "extendedTXSupported": false, "txTime": 0, "hops": 0, "rssi_1": "", "rssi_2": "", "rssi_3": "", "rssi_4": "", "rssi_5": "", "route_1": 0, "route_2": 0, "route_3": 0, "route_4": 0, "ackChannel": 0, "lastTXChannel": 0, "routeScheme": "Idle", "routeUsed": "", "routeSpeed": "Auto", "routeTries": 0, "lastFailedLinkFrom": 0, "lastFailedLinkTo": 0} -OpenZWave/1/node/7/statistics/,{ "sendCount": 30, "sentFailed": 0, "retries": 0, "receivedPackets": 29, "receivedDupPackets": 1, "receivedUnsolicited": 0, "lastSentTimeStamp": 1597143009, "lastReceivedTimeStamp": 1597143009, "lastRequestRTT": 26, "averageRequestRTT": 25, "lastResponseRTT": 37, "averageResponseRTT": 37, "quality": 0, "extendedTXSupported": false, "txTime": 0, "hops": 0, "rssi_1": "", "rssi_2": "", "rssi_3": "", "rssi_4": "", "rssi_5": "", "route_1": 0, "route_2": 0, "route_3": 0, "route_4": 0, "ackChannel": 0, "lastTXChannel": 0, "routeScheme": "Idle", "routeUsed": "", "routeSpeed": "Auto", "routeTries": 0, "lastFailedLinkFrom": 0, "lastFailedLinkTo": 0} -OpenZWave/1/node/7/statistics/,{ "sendCount": 30, "sentFailed": 0, "retries": 0, "receivedPackets": 29, "receivedDupPackets": 1, "receivedUnsolicited": 0, "lastSentTimeStamp": 1597143009, "lastReceivedTimeStamp": 1597143009, "lastRequestRTT": 26, "averageRequestRTT": 25, "lastResponseRTT": 37, "averageResponseRTT": 37, "quality": 0, "extendedTXSupported": false, "txTime": 0, "hops": 0, "rssi_1": "", "rssi_2": "", "rssi_3": "", "rssi_4": "", "rssi_5": "", "route_1": 0, "route_2": 0, "route_3": 0, "route_4": 0, "ackChannel": 0, "lastTXChannel": 0, "routeScheme": "Idle", "routeUsed": "", "routeSpeed": "Auto", "routeTries": 0, "lastFailedLinkFrom": 0, "lastFailedLinkTo": 0} -OpenZWave/1/node/7/statistics/,{ "sendCount": 30, "sentFailed": 0, "retries": 0, "receivedPackets": 29, "receivedDupPackets": 1, "receivedUnsolicited": 0, "lastSentTimeStamp": 1597143009, "lastReceivedTimeStamp": 1597143009, "lastRequestRTT": 26, "averageRequestRTT": 25, "lastResponseRTT": 37, "averageResponseRTT": 37, "quality": 0, "extendedTXSupported": false, "txTime": 0, "hops": 0, "rssi_1": "", "rssi_2": "", "rssi_3": "", "rssi_4": "", "rssi_5": "", "route_1": 0, "route_2": 0, "route_3": 0, "route_4": 0, "ackChannel": 0, "lastTXChannel": 0, "routeScheme": "Idle", "routeUsed": "", "routeSpeed": "Auto", "routeTries": 0, "lastFailedLinkFrom": 0, "lastFailedLinkTo": 0} -OpenZWave/1/node/7/statistics/,{ "sendCount": 30, "sentFailed": 0, "retries": 0, "receivedPackets": 29, "receivedDupPackets": 1, "receivedUnsolicited": 0, "lastSentTimeStamp": 1597143009, "lastReceivedTimeStamp": 1597143009, "lastRequestRTT": 26, "averageRequestRTT": 25, "lastResponseRTT": 37, "averageResponseRTT": 37, "quality": 0, "extendedTXSupported": false, "txTime": 0, "hops": 0, "rssi_1": "", "rssi_2": "", "rssi_3": "", "rssi_4": "", "rssi_5": "", "route_1": 0, "route_2": 0, "route_3": 0, "route_4": 0, "ackChannel": 0, "lastTXChannel": 0, "routeScheme": "Idle", "routeUsed": "", "routeSpeed": "Auto", "routeTries": 0, "lastFailedLinkFrom": 0, "lastFailedLinkTo": 0} -OpenZWave/1/node/7/statistics/,{ "sendCount": 30, "sentFailed": 0, "retries": 0, "receivedPackets": 29, "receivedDupPackets": 1, "receivedUnsolicited": 0, "lastSentTimeStamp": 1597143009, "lastReceivedTimeStamp": 1597143009, "lastRequestRTT": 26, "averageRequestRTT": 25, "lastResponseRTT": 37, "averageResponseRTT": 37, "quality": 0, "extendedTXSupported": false, "txTime": 0, "hops": 0, "rssi_1": "", "rssi_2": "", "rssi_3": "", "rssi_4": "", "rssi_5": "", "route_1": 0, "route_2": 0, "route_3": 0, "route_4": 0, "ackChannel": 0, "lastTXChannel": 0, "routeScheme": "Idle", "routeUsed": "", "routeSpeed": "Auto", "routeTries": 0, "lastFailedLinkFrom": 0, "lastFailedLinkTo": 0} -OpenZWave/1/node/7/,{ "NodeID": 7, "NodeQueryStage": "Complete", "isListening": true, "isFlirs": false, "isBeaming": true, "isRouting": true, "isSecurityv1": false, "isZWavePlus": false, "isNIFRecieved": true, "isAwake": true, "isFailed": false, "MetaData": { "OZWInfoURL": "http://www.openzwave.com/device-database/010F:1000:0900", "ZWAProductURL": "", "ProductPic": "images/fibaro/fgrgbwm441.png", "Description": "RGBW Controller", "ProductManualURL": "", "ProductPageURL": "", "InclusionHelp": "", "ExclusionHelp": "", "ResetHelp": "", "WakeupHelp": "", "ProductSupportURL": "", "Frequency": "", "Name": "FIBARO RGBW Dimmer", "ProductPicBase64": "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" }, "Event": "nodeQueriesComplete", "TimeStamp": 1597143009, "NodeManufacturerName": "FIBARO System", "NodeProductName": "FGRGBWM441 RGBW Controller", "NodeBasicString": "Routing Slave", "NodeBasic": 4, "NodeGenericString": "Multilevel Switch", "NodeGeneric": 17, "NodeSpecificString": "Multilevel Power Switch", "NodeSpecific": 1, "NodeManufacturerID": "0x010f", "NodeProductType": "0x0900", "NodeProductID": "0x1000", "NodeBaudRate": 40000, "NodeVersion": 4, "NodeGroups": 5, "NodeName": "Kitchen RGB Strip", "NodeLocation": "", "NodeDeviceTypeString": "Unknown Type (0x0000)", "NodeDeviceType": 0, "NodeRole": 0, "NodeRoleString": "Central Controller", "NodePlusType": 0, "NodePlusTypeString": "Z-Wave+ node", "Neighbors": [ 1, 3, 6, 8, 12, 13, 14, 18, 22, 26, 27, 28 ], "Neighbors": [ 1, 3, 6, 8, 12, 13, 14, 18, 22, 26, 27, 28 ]} -OpenZWave/1/node/7/instance/1/,{ "Instance": 1, "TimeStamp": 1597142799} -OpenZWave/1/node/7/instance/1/commandclass/51/,{ "Instance": 1, "CommandClassId": 51, "CommandClass": "COMMAND_CLASS_COLOR", "CommandClassVersion": 0, "TimeStamp": 1597142799} -OpenZWave/1/node/7/instance/1/commandclass/51/value/122470423/,{ "Label": "Color", "Value": "#FFFFFF00", "Units": "#RRGGBBWW", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_COLOR", "Index": 0, "Node": 7, "Genre": "User", "Help": "Color (in RGB format)", "ValueIDKey": 122470423, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1597142799} -OpenZWave/1/node/7/instance/1/commandclass/51/value/281475099181076/,{ "Label": "Color Index", "Value": { "List": [ { "Value": 0, "Label": "Off" }, { "Value": 1, "Label": "Cool White" }, { "Value": 2, "Label": "Warm White" }, { "Value": 3, "Label": "Red" }, { "Value": 4, "Label": "Lime" }, { "Value": 5, "Label": "Blue" }, { "Value": 6, "Label": "Yellow" }, { "Value": 7, "Label": "Cyan" }, { "Value": 8, "Label": "Magenta" }, { "Value": 9, "Label": "Silver" }, { "Value": 10, "Label": "Gray" }, { "Value": 11, "Label": "Maroon" }, { "Value": 12, "Label": "Olive" }, { "Value": 13, "Label": "Green" }, { "Value": 14, "Label": "Purple" }, { "Value": 15, "Label": "Teal" }, { "Value": 16, "Label": "Navy" }, { "Value": 17, "Label": "Custom" } ], "Selected": "Cool White", "Selected_id": 1 }, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_COLOR", "Index": 1, "Node": 7, "Genre": "User", "Help": "Preset Color", "ValueIDKey": 281475099181076, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1597142799} -OpenZWave/1/node/7/instance/1/commandclass/112/,{ "Instance": 1, "CommandClassId": 112, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "CommandClassVersion": 1, "TimeStamp": 1597142799} -OpenZWave/1/node/7/instance/1/commandclass/112/value/281475104374804/,{ "Label": "Enable/Disable ALL ON/OFF", "Value": { "List": [ { "Value": 0, "Label": "ALL ON disabled/ ALL OFF disabled" }, { "Value": 1, "Label": "ALL ON disabled/ ALL OFF active" }, { "Value": 2, "Label": "ALL ON active / ALL OFF disabled" }, { "Value": 255, "Label": "ALL ON active / ALL OFF active" } ], "Selected": "ALL ON active / ALL OFF active", "Selected_id": 255 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 1, "Node": 7, "Genre": "Config", "Help": "Enable/Disable ALL ON/OFF", "ValueIDKey": 281475104374804, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1597143008} -OpenZWave/1/node/7/instance/1/commandclass/112/value/1688849987928084/,{ "Label": "Associations command class choice", "Value": { "List": [ { "Value": 0, "Label": "Normal (Dimmer) - BASIC SET/SWITCH_MULTILEVEL_START/STOP" }, { "Value": 1, "Label": "Normal (RGBW) - COLOR_CONTROL_SET/START/STOP_STATE_CHANGE" }, { "Value": 2, "Label": "Normal (RGBW) - COLOR_CONTROL_SET" }, { "Value": 3, "Label": "Brightness - BASIC SET/SWITCH_MULTILEVEL_START/STOP" }, { "Value": 4, "Label": "Rainbow (RGBW) - COLOR_CONTROL_SET" } ], "Selected": "Normal (Dimmer) - BASIC SET/SWITCH_MULTILEVEL_START/STOP", "Selected_id": 0 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 6, "Node": 7, "Genre": "Config", "Help": "Choose which command classes are sent to associated devices.", "ValueIDKey": 1688849987928084, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1597143008} -OpenZWave/1/node/7/instance/1/commandclass/112/value/2251799941349396/,{ "Label": "Outputs state change mode", "Value": { "List": [ { "Value": 0, "Label": "MODE 1 - Constant Speed (speed is defined by parameters 9 and 10)" }, { "Value": 1, "Label": "MODE 2 - Constant Time (RGB/RBGW only. Time is defined by parameter 11)" } ], "Selected": "MODE 1 - Constant Speed (speed is defined by parameters 9 and 10)", "Selected_id": 0 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 8, "Node": 7, "Genre": "Config", "Help": "Choose the behaviour of transitions between different levels.", "ValueIDKey": 2251799941349396, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1597143008} -OpenZWave/1/node/7/instance/1/commandclass/112/value/2533274918060049/,{ "Label": "Dimming step value (for MODE 1)", "Value": 1, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 1, "Max": 99, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 9, "Node": 7, "Genre": "Config", "Help": "Size of the step for each change in level during the transition.", "ValueIDKey": 2533274918060049, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1597143009} -OpenZWave/1/node/7/instance/1/commandclass/112/value/2814749894770710/,{ "Label": "Time between dimming steps (for MODE 1)", "Value": 10, "Units": "ms", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 60000, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 10, "Node": 7, "Genre": "Config", "Help": "Time between each step in a transition between levels. Setting this to zero means an instantaneous change.", "ValueIDKey": 2814749894770710, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1597143009} -OpenZWave/1/node/7/instance/1/commandclass/112/value/3096224871481361/,{ "Label": "Time to complete the entire transition (for MODE 2)", "Value": 67, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 11, "Node": 7, "Genre": "Config", "Help": "0 - immediate change; 1->63: 20ms->126ms (value*20ms); 65->127: 1s->63s (value-64)*1s; 129->191: 10s->630s (value-128)*10s; 193->255: 1min->63min (value-192)*1min. Default setting: 67 (3s)", "ValueIDKey": 3096224871481361, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1597143009} -OpenZWave/1/node/7/instance/1/commandclass/112/value/3377699848192017/,{ "Label": "Maximum dimmer level", "Value": 255, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 3, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 12, "Node": 7, "Genre": "Config", "Help": "Maximum brightness level for the dimmer", "ValueIDKey": 3377699848192017, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1597143009} -OpenZWave/1/node/7/instance/1/commandclass/112/value/3659174824902673/,{ "Label": "Minimum dimmer level", "Value": 2, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 2, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 13, "Node": 7, "Genre": "Config", "Help": "Minimum brightness level for the dimmer", "ValueIDKey": 3659174824902673, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1597143009} -OpenZWave/1/node/7/instance/1/commandclass/112/value/3940649801613334/,{ "Label": "Inputs / Outputs configuration", "Value": 4369, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 14, "Node": 7, "Genre": "Config", "Help": "This is too complex to describe here, since this value is built up from 4-bits for each of the 4 channels. Refer to the table in the product manual. Default value is 4369 (1111 in hex).", "ValueIDKey": 3940649801613334, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1597143009} -OpenZWave/1/node/7/instance/1/commandclass/112/value/4222124778323988/,{ "Label": "Option double click", "Value": { "List": [ { "Value": 0, "Label": "Double click disabled" }, { "Value": 1, "Label": "Double click enabled" } ], "Selected": "Double click enabled", "Selected_id": 1 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 15, "Node": 7, "Genre": "Config", "Help": "Option double click (lighting set at 100%). 0 - Double click disabled, 1 - Double click enabled. Default setting 1", "ValueIDKey": 4222124778323988, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1597143009} -OpenZWave/1/node/7/instance/1/commandclass/112/value/4503599755034644/,{ "Label": "Saving state before power failure", "Value": { "List": [ { "Value": 0, "Label": "State NOT saved at power failure, all outputs are set to OFF upon power restore" }, { "Value": 1, "Label": "State saved at power failure, all outputs are set to previous state upon power restore" } ], "Selected": "State saved at power failure, all outputs are set to previous state upon power restore", "Selected_id": 1 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 16, "Node": 7, "Genre": "Config", "Help": "Saving state before power failure", "ValueIDKey": 4503599755034644, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1597143009} -OpenZWave/1/node/7/instance/1/commandclass/112/value/8444249428983828/,{ "Label": "Alarm", "Value": { "List": [ { "Value": 0, "Label": "INACTIVE - no response to alarm frames" }, { "Value": 1, "Label": "ALARM ON - the device turns on once alarm is detected (all channels set to 99%)" }, { "Value": 2, "Label": "ALARM OFF - the device turns off once alarm is detected (all channels set to 0%)" }, { "Value": 3, "Label": "ALARM PROGRAM - alarm sequence turns on (program selected in parameter 38)" } ], "Selected": "INACTIVE - no response to alarm frames", "Selected_id": 0 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 30, "Node": 7, "Genre": "Config", "Help": "Alarm of any type (general alarm, water flooding alarm, smoke alarm: CO, CO2, temperature alarm). Default setting 0 (Inactive)", "ValueIDKey": 8444249428983828, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1597143009} -OpenZWave/1/node/7/instance/1/commandclass/112/value/10696049242669073/,{ "Label": "Alarm sequence program", "Value": 10, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 1, "Max": 10, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 38, "Node": 7, "Genre": "Config", "Help": "Program number selected from the 10 available.", "ValueIDKey": 10696049242669073, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1597143009} -OpenZWave/1/node/7/instance/1/commandclass/112/value/10977524219379734/,{ "Label": "Active PROGRAM alarm time", "Value": 600, "Units": "s", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 1, "Max": 65534, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 39, "Node": 7, "Genre": "Config", "Help": "In ALARM PROGRAM mode (see parameter 30), this defines the time in seconds the program lasts (1s->65534s)", "ValueIDKey": 10977524219379734, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1597143009} -OpenZWave/1/node/7/instance/1/commandclass/112/value/11821949149511700/,{ "Label": "Command class reporting Outputs status change", "Value": { "List": [ { "Value": 0, "Label": "Reporting as a result of inputs and controllers actions (SWITCH MULTILEVEL)" }, { "Value": 1, "Label": "Reporting as a result inputs actions (SWITCH MULTILEVEL)" }, { "Value": 2, "Label": "Reporting as a result inputs actions (COLOUR_CONTROL)" } ], "Selected": "Reporting as a result of inputs and controllers actions (SWITCH MULTILEVEL)", "Selected_id": 0 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 42, "Node": 7, "Genre": "Config", "Help": "Specify which command class is used to report output status changes", "ValueIDKey": 11821949149511700, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1597143009} -OpenZWave/1/node/7/instance/1/commandclass/112/value/12103424126222353/,{ "Label": "Reporting 0-10v analog inputs change threshold", "Value": 5, "Units": "*0.1V", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 1, "Max": 100, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 43, "Node": 7, "Genre": "Config", "Help": "Parameter defines a value by which input voltage must change in order to be reported to the main controller. New value is calculated based on last reported value: Default setting: 5 (0.5V). Range: 1->100 - (0.1V->10V).", "ValueIDKey": 12103424126222353, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1597143009} -OpenZWave/1/node/7/instance/1/commandclass/112/value/12384899102933014/,{ "Label": "Power load reporting frequency", "Value": 30, "Units": "s", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 1, "Max": 65534, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 44, "Node": 7, "Genre": "Config", "Help": "Sent if last reported value differs from the current value. Reports will also be sent in case of polling. Default setting: 30 (30s). Range: 1->65534 (1s->65534s) - interval between reports. Zero means reports are only sent in the case of polling, or at turning OFF the device", "ValueIDKey": 12384899102933014, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1597143009} -OpenZWave/1/node/7/instance/1/commandclass/112/value/12666374079643665/,{ "Label": "Reporting changes in energy consumed by controlled devices", "Value": 10, "Units": "*0.01kWh", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 1, "Max": 254, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 45, "Node": 7, "Genre": "Config", "Help": "Interval between energy consumption reports (in kWh). New reported energy consumption value is calculated based on last reported value. 1->254 (0.01kWh->2.54kWh). Zero means changes in consumed energy will not be reported, except in case of polling.", "ValueIDKey": 12666374079643665, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1597143009} -OpenZWave/1/node/7/instance/1/commandclass/112/value/19984723474120724/,{ "Label": "Response to BRIGHTNESS set to 0%", "Value": { "List": [ { "Value": 0, "Label": "Illumination colour set to white (all channels controlled together)" }, { "Value": 1, "Label": "Last set colour is memorized" } ], "Selected": "Last set colour is memorized", "Selected_id": 1 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 71, "Node": 7, "Genre": "Config", "Help": "Set whether to remember the previous RGB mix after the brightness has fallen to zero (black)", "ValueIDKey": 19984723474120724, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1597143009} -OpenZWave/1/node/7/instance/1/commandclass/112/value/20266198450831377/,{ "Label": "Starting predefined program", "Value": 1, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 1, "Max": 10, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 72, "Node": 7, "Genre": "Config", "Help": "First predefined program to use when device is set to work in RGB/RGBW mode (parameter 14)", "ValueIDKey": 20266198450831377, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1597143009} -OpenZWave/1/node/7/instance/1/commandclass/112/value/20547673427542036/,{ "Label": "Triple Click Action", "Value": { "List": [ { "Value": 0, "Label": "NODE INFO control frame is sent" }, { "Value": 1, "Label": "Start favourite program" } ], "Selected": "NODE INFO control frame is sent", "Selected_id": 0 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 73, "Node": 7, "Genre": "Config", "Help": "Behaviour when an input is triple-clicked", "ValueIDKey": 20547673427542036, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1597143009} -OpenZWave/1/node/7/instance/1/commandclass/38/,{ "Instance": 1, "CommandClassId": 38, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "CommandClassVersion": 0, "TimeStamp": 1597142799} -OpenZWave/1/node/7/instance/1/commandclass/38/value/122257425/,{ "Label": "Level", "Value": 99, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 0, "Node": 7, "Genre": "User", "Help": "The Current Level of the Device", "ValueIDKey": 122257425, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1597142969} -OpenZWave/1/node/7/instance/1/commandclass/38/value/281475098968088/,{ "Label": "Bright", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 1, "Node": 7, "Genre": "User", "Help": "Increase the Brightness of the Device", "ValueIDKey": 281475098968088, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1597142799} -OpenZWave/1/node/7/instance/1/commandclass/38/value/562950075678744/,{ "Label": "Dim", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 2, "Node": 7, "Genre": "User", "Help": "Decrease the Brightness of the Device", "ValueIDKey": 562950075678744, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1597142799} -OpenZWave/1/node/7/instance/1/commandclass/38/value/844425060778000/,{ "Label": "Ignore Start Level", "Value": true, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Bool", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 3, "Node": 7, "Genre": "System", "Help": "Ignore the Start Level of the Device when increasing/decreasing brightness", "ValueIDKey": 844425060778000, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1597142799} -OpenZWave/1/node/7/instance/1/commandclass/38/value/1125900037488657/,{ "Label": "Start Level", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 4, "Node": 7, "Genre": "System", "Help": "Start Level when Changing the Brightness of a Device", "ValueIDKey": 1125900037488657, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1597142799} -OpenZWave/1/node/7/instance/1/commandclass/39/,{ "Instance": 1, "CommandClassId": 39, "CommandClass": "COMMAND_CLASS_SWITCH_ALL", "CommandClassVersion": 1, "TimeStamp": 1597142799} -OpenZWave/1/node/7/instance/1/commandclass/39/value/130662420/,{ "Label": "Switch All", "Value": { "List": [ { "Value": 0, "Label": "Disabled" }, { "Value": 1, "Label": "Off Enabled" }, { "Value": 2, "Label": "On Enabled" }, { "Value": 255, "Label": "On and Off Enabled" } ], "Selected": "On and Off Enabled", "Selected_id": 255 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_ALL", "Index": 0, "Node": 7, "Genre": "System", "Help": "Switch All Devices On/Off", "ValueIDKey": 130662420, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1597142969} -OpenZWave/1/node/7/instance/1/commandclass/114/,{ "Instance": 1, "CommandClassId": 114, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "CommandClassVersion": 0, "TimeStamp": 1597142799} -OpenZWave/1/node/7/instance/1/commandclass/114/value/131891219/,{ "Label": "Loaded Config Revision", "Value": 5, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 0, "Node": 7, "Genre": "System", "Help": "Revision of the Config file currently loaded", "ValueIDKey": 131891219, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1597142799} -OpenZWave/1/node/7/instance/1/commandclass/114/value/281475108601875/,{ "Label": "Config File Revision", "Value": 5, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 1, "Node": 7, "Genre": "System", "Help": "Revision of the Config file on the File System", "ValueIDKey": 281475108601875, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1597142799} -OpenZWave/1/node/7/instance/1/commandclass/114/value/562950085312531/,{ "Label": "Latest Available Config File Revision", "Value": 5, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 2, "Node": 7, "Genre": "System", "Help": "Latest Revision of the Config file available for download", "ValueIDKey": 562950085312531, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1597142799} -OpenZWave/1/node/7/association/1/,{ "Name": "Input 1", "Help": "", "MaxAssociations": 5, "Members": [], "TimeStamp": 1597142866} -OpenZWave/1/node/7/association/2/,{ "Name": "Input 2", "Help": "", "MaxAssociations": 5, "Members": [], "TimeStamp": 1597142866} -OpenZWave/1/node/7/association/3/,{ "Name": "Input 3", "Help": "", "MaxAssociations": 5, "Members": [], "TimeStamp": 1597142866} -OpenZWave/1/node/7/association/4/,{ "Name": "Input 4", "Help": "", "MaxAssociations": 5, "Members": [], "TimeStamp": 1597142866} -OpenZWave/1/node/7/association/5/,{ "Name": "Lifeline", "Help": "", "MaxAssociations": 1, "Members": [ "1.0" ], "TimeStamp": 1597142799} -OpenZWave/1/node/7/statistics/,{ "sendCount": 30, "sentFailed": 0, "retries": 0, "receivedPackets": 29, "receivedDupPackets": 1, "receivedUnsolicited": 0, "lastSentTimeStamp": 1597143009, "lastReceivedTimeStamp": 1597143009, "lastRequestRTT": 26, "averageRequestRTT": 25, "lastResponseRTT": 37, "averageResponseRTT": 37, "quality": 0, "extendedTXSupported": false, "txTime": 0, "hops": 0, "rssi_1": "", "rssi_2": "", "rssi_3": "", "rssi_4": "", "rssi_5": "", "route_1": 0, "route_2": 0, "route_3": 0, "route_4": 0, "ackChannel": 0, "lastTXChannel": 0, "routeScheme": "Idle", "routeUsed": "", "routeSpeed": "Auto", "routeTries": 0, "lastFailedLinkFrom": 0, "lastFailedLinkTo": 0} -OpenZWave/1/status/,{ "OpenZWave_Version": "1.6.1008", "OZWDeamon_Version": "0.1", "QTOpenZWave_Version": "1.0.0", "QT_Version": "5.12.5", "Status": "driverAllNodesQueried", "TimeStamp": 1579566933, "ManufacturerSpecificDBReady": true, "homeID": 3245146787, "getControllerNodeId": 1, "getSUCNodeId": 1, "isPrimaryController": true, "isBridgeController": false, "hasExtendedTXStatistics": true, "getControllerLibraryVersion": "Z-Wave 3.95", "getControllerLibraryType": "Static Controller", "getControllerPath": "/dev/zwave"} -OpenZWave/1/node/2/,{ "NodeID": 2, "NodeQueryStage": "Complete", "isListening": true, "isFlirs": false, "isBeaming": true, "isRouting": true, "isSecurityv1": false, "isZWavePlus": false, "isNIFRecieved": true, "isAwake": true, "isFailed": false, "MetaData": { "OZWInfoURL": "http://www.openzwave.com/device-database/0063:3031:4944", "ZWAProductURL": "", "ProductPic": "images/ge/12724-dimmer.png", "Description": "Transform any home into a smart home with the GE Z-Wave Smart Fan Control. The in-wall fan control easily replaces any standard in-wall switch remotely controls a ceiling fan in your home and features a three-speed control system. Your home will be equipped with ultimate flexibility with the GE Z-Wave Smart Fan Control, capable of being used by itself or with up to four GE add-on switches. Screw terminal installation provides improved space efficiency when replacing existing switches and the integrated LED indicator light allows you to easily locate the switch in a dark room. The GE Z-Wave Smart Fan Control is compatible with any Z-Wave certified gateway, providing access to many popular home automation systems. Take control of your home lighting with GE Z-Wave Smart Lighting Controls!", "ProductManualURL": "https://Products.Z-WaveAlliance.org/ProductManual/File?folder=&filename=Manuals/2506/Binder2.pdf", "ProductPageURL": "http://www.ezzwave.com", "InclusionHelp": "1. Follow the instructions for your Z-Wave certified controller to include a device to the Z-Wave network. 2. Once the controller is ready to include your device, press and release the top or bottom of the smart fan control switch (rocker) to include it in the network. 3. Once your controller has confirmed the device has been included, refresh the Z-Wave network to optimize performance.", "ExclusionHelp": "1. Follow the instructions for your Z-Wave certified controller to exclude a device from the Z-Wave network. 2. Once the controller is ready to Exclude your device, press and release the top or bottom of the wireless smart switch (rocker) to exclude it from the network.", "ResetHelp": "1. Quickly press ON (Top) button three (3) times then immediately press the OFF (Bottom) button three (3) times. The LED will flash ON/OFF 5 times when completed successfully. Note: This should only be used in the event your network’s primary controller is missing or otherwise inoperable.", "WakeupHelp": "", "ProductSupportURL": "", "Frequency": "", "Name": "In-Wall Smart Fan Control", "ProductPicBase64": "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" }, "Event": "nodeQueriesComplete", "TimeStamp": 1594407756, "NodeManufacturerName": "GE (Jasco Products)", "NodeProductName": "12724 3-Way Dimmer Switch", "NodeBasicString": "Routing Slave", "NodeBasic": 4, "NodeGenericString": "Multilevel Switch", "NodeGeneric": 17, "NodeSpecificString": "Multilevel Power Switch", "NodeSpecific": 1, "NodeManufacturerID": "0x0063", "NodeProductType": "0x4944", "NodeProductID": "0x3031", "NodeBaudRate": 40000, "NodeVersion": 4, "NodeGroups": 0, "NodeName": "Master_Bedroom_L", "NodeLocation": "Master Bedroom", "NodeDeviceTypeString": "Unknown Type (0x0000)", "NodeDeviceType": 0, "NodeRole": 0, "NodeRoleString": "Central Controller", "NodePlusType": 0, "NodePlusTypeString": "Z-Wave+ node", "Neighbors": [ 1, 6, 9, 10, 11, 12, 15, 16, 17, 18, 19, 20, 21, 23, 24, 26, 27, 28, 29, 30, 33 ], "Neighbors": [ 1, 6, 9, 10, 11, 12, 15, 16, 17, 18, 19, 20, 21, 23, 24, 26, 27, 28, 29, 30, 33 ]} -OpenZWave/1/node/2/instance/1/,{ "Instance": 1, "TimeStamp": 1594407617} -OpenZWave/1/node/2/instance/1/commandclass/112/,{ "Instance": 1, "CommandClassId": 112, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "CommandClassVersion": 1, "TimeStamp": 1594407617} -OpenZWave/1/node/2/instance/1/commandclass/112/value/844424973910036/,{ "Label": "LED Light", "Value": { "List": [ { "Value": 0, "Label": "LED on when light off" }, { "Value": 1, "Label": "LED on when light on" }, { "Value": 2, "Label": "LED always off" } ], "Selected": "LED on when light on", "Selected_id": 1 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 2, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 3, "Node": 2, "Genre": "Config", "Help": "Sets when the LED on the switch is lit.", "ValueIDKey": 844424973910036, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1594407754} -OpenZWave/1/node/2/instance/1/commandclass/112/value/1125899950620692/,{ "Label": "Invert Switch", "Value": { "List": [ { "Value": 0, "Label": "No" }, { "Value": 1, "Label": "Yes" } ], "Selected": "No", "Selected_id": 0 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 4, "Node": 2, "Genre": "Config", "Help": "Change the top of the switch to OFF and the bottom of the switch to ON, if the switch was installed upside down.", "ValueIDKey": 1125899950620692, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1594407754} -OpenZWave/1/node/2/instance/1/commandclass/112/value/1970324880752657/,{ "Label": "Z-Wave Command Dim Step", "Value": 1, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 1, "Max": 99, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 7, "Node": 2, "Genre": "Config", "Help": "Indicates how many levels the dimmer will change for each dimming step.", "ValueIDKey": 1970324880752657, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1594407755} -OpenZWave/1/node/2/instance/1/commandclass/112/value/2251799857463313/,{ "Label": "Z-Wave Command Dim Rate", "Value": 1, "Units": "x 10 milliseconds", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 1, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 8, "Node": 2, "Genre": "Config", "Help": "This value indicates in 10 millisecond resolution, how often the dim level will change. For example, if you set this parameter to 1, then every 10ms the dim level will change. If you set it to 255, then every 2.55 seconds the dim level will change.", "ValueIDKey": 2251799857463313, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1594407755} -OpenZWave/1/node/2/instance/1/commandclass/112/value/2533274834173969/,{ "Label": "Local Control Dim Step", "Value": 1, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 1, "Max": 99, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 9, "Node": 2, "Genre": "Config", "Help": "Indicates how many levels the dimmer will change for each dimming step.", "ValueIDKey": 2533274834173969, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1594407755} -OpenZWave/1/node/2/instance/1/commandclass/112/value/2814749810884625/,{ "Label": "Local Control Dim Rate", "Value": 5, "Units": "x 10 milliseconds", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 1, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 10, "Node": 2, "Genre": "Config", "Help": "This value indicates in 10 millisecond resolution, how often the dim level will change. For example, if you set this parameter to 1, then every 10ms the dim level will change. If you set it to 255, then every 2.55 seconds the dim level will change.", "ValueIDKey": 2814749810884625, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1594407755} -OpenZWave/1/node/2/instance/1/commandclass/112/value/3096224787595281/,{ "Label": "ALL ON/ALL OFF Dim Step", "Value": 99, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 1, "Max": 99, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 11, "Node": 2, "Genre": "Config", "Help": "Indicates how many levels the dimmer will change for each dimming step.", "ValueIDKey": 3096224787595281, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1594407755} -OpenZWave/1/node/2/instance/1/commandclass/112/value/3377699764305937/,{ "Label": "ALL ON/ALL OFF Dim Rate", "Value": 5, "Units": "x 10 milliseconds", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 1, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 12, "Node": 2, "Genre": "Config", "Help": "This value indicates in 10 millisecond resolution, how often the dim level will change. For example, if you set this parameter to 1, then every 10ms the dim level will change. If you set it to 255, then every 2.55 seconds the dim level will change.", "ValueIDKey": 3377699764305937, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1594407756} -OpenZWave/1/node/2/instance/1/commandclass/38/,{ "Instance": 1, "CommandClassId": 38, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "CommandClassVersion": 1, "TimeStamp": 1594407617} -OpenZWave/1/node/2/instance/1/commandclass/38/value/38371345/,{ "Label": "Level", "Value": 0, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 0, "Node": 2, "Genre": "User", "Help": "The Current Level of the Device", "ValueIDKey": 38371345, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1594407637} -OpenZWave/1/node/2/instance/1/commandclass/38/value/281475015082008/,{ "Label": "Bright", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 1, "Node": 2, "Genre": "User", "Help": "Increase the Brightness of the Device", "ValueIDKey": 281475015082008, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1594407617} -OpenZWave/1/node/2/instance/1/commandclass/38/value/562949991792664/,{ "Label": "Dim", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 2, "Node": 2, "Genre": "User", "Help": "Decrease the Brightness of the Device", "ValueIDKey": 562949991792664, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1594407617} -OpenZWave/1/node/2/instance/1/commandclass/38/value/844424976891920/,{ "Label": "Ignore Start Level", "Value": true, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Bool", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 3, "Node": 2, "Genre": "System", "Help": "Ignore the Start Level of the Device when increasing/decreasing brightness", "ValueIDKey": 844424976891920, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1594407617} -OpenZWave/1/node/2/instance/1/commandclass/38/value/1125899953602577/,{ "Label": "Start Level", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 4, "Node": 2, "Genre": "System", "Help": "Start Level when Changing the Brightness of a Device", "ValueIDKey": 1125899953602577, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1594407617} -OpenZWave/1/node/2/instance/1/commandclass/39/,{ "Instance": 1, "CommandClassId": 39, "CommandClass": "COMMAND_CLASS_SWITCH_ALL", "CommandClassVersion": 1, "TimeStamp": 1594407617} -OpenZWave/1/node/2/instance/1/commandclass/39/value/46776340/,{ "Label": "Switch All", "Value": { "List": [ { "Value": 0, "Label": "Disabled" }, { "Value": 1, "Label": "Off Enabled" }, { "Value": 2, "Label": "On Enabled" }, { "Value": 255, "Label": "On and Off Enabled" } ], "Selected": "On and Off Enabled", "Selected_id": 255 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_ALL", "Index": 0, "Node": 2, "Genre": "System", "Help": "Switch All Devices On/Off", "ValueIDKey": 46776340, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1594407636} -OpenZWave/1/node/2/instance/1/commandclass/114/,{ "Instance": 1, "CommandClassId": 114, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "CommandClassVersion": 1, "TimeStamp": 1594407617} -OpenZWave/1/node/2/instance/1/commandclass/114/value/48005139/,{ "Label": "Loaded Config Revision", "Value": 9, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 0, "Node": 2, "Genre": "System", "Help": "Revision of the Config file currently loaded", "ValueIDKey": 48005139, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1594407617} -OpenZWave/1/node/2/instance/1/commandclass/114/value/281475024715795/,{ "Label": "Config File Revision", "Value": 9, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 1, "Node": 2, "Genre": "System", "Help": "Revision of the Config file on the File System", "ValueIDKey": 281475024715795, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1594407617} -OpenZWave/1/node/2/instance/1/commandclass/114/value/562950001426451/,{ "Label": "Latest Available Config File Revision", "Value": 9, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 2, "Node": 2, "Genre": "System", "Help": "Latest Revision of the Config file available for download", "ValueIDKey": 562950001426451, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1594407617} -OpenZWave/1/node/2/instance/1/commandclass/114/value/844424978137111/,{ "Label": "Device ID", "Value": "", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 3, "Node": 2, "Genre": "System", "Help": "Manufacturer Specific Device ID/Model", "ValueIDKey": 844424978137111, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1594407617} -OpenZWave/1/node/2/instance/1/commandclass/114/value/1125899954847767/,{ "Label": "Serial Number", "Value": "", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 4, "Node": 2, "Genre": "System", "Help": "Device Serial Number", "ValueIDKey": 1125899954847767, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1594407617} -OpenZWave/1/node/2/instance/1/commandclass/115/,{ "Instance": 1, "CommandClassId": 115, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "CommandClassVersion": 1, "TimeStamp": 1594407617} -OpenZWave/1/node/2/instance/1/commandclass/115/value/48021524/,{ "Label": "Powerlevel", "Value": { "List": [ { "Value": 0, "Label": "Normal" }, { "Value": 1, "Label": "-1dB" }, { "Value": 2, "Label": "-2dB" }, { "Value": 3, "Label": "-3dB" }, { "Value": 4, "Label": "-4dB" }, { "Value": 5, "Label": "-5dB" }, { "Value": 6, "Label": "-6dB" }, { "Value": 7, "Label": "-7dB" }, { "Value": 8, "Label": "-8dB" }, { "Value": 9, "Label": "-9dB" } ], "Selected": "Normal", "Selected_id": 0 }, "Units": "dB", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 0, "Node": 2, "Genre": "System", "Help": "Output RF PowerLevel", "ValueIDKey": 48021524, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1594407636} -OpenZWave/1/node/2/instance/1/commandclass/115/value/281475024732177/,{ "Label": "Timeout", "Value": 0, "Units": "seconds", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 1, "Node": 2, "Genre": "System", "Help": "Timeout till the PowerLevel is reset to Normal", "ValueIDKey": 281475024732177, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1594407636} -OpenZWave/1/node/2/instance/1/commandclass/115/value/562950001442840/,{ "Label": "Set Powerlevel", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 2, "Node": 2, "Genre": "System", "Help": "Apply the Output PowerLevel and Timeout Values", "ValueIDKey": 562950001442840, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1594407617} -OpenZWave/1/node/2/instance/1/commandclass/115/value/844424978153489/,{ "Label": "Test Node", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 3, "Node": 2, "Genre": "System", "Help": "Node to Perform a test against", "ValueIDKey": 844424978153489, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1594407617} -OpenZWave/1/node/2/instance/1/commandclass/115/value/1125899954864148/,{ "Label": "Test Powerlevel", "Value": { "List": [ { "Value": 0, "Label": "Normal" }, { "Value": 1, "Label": "-1dB" }, { "Value": 2, "Label": "-2dB" }, { "Value": 3, "Label": "-3dB" }, { "Value": 4, "Label": "-4dB" }, { "Value": 5, "Label": "-5dB" }, { "Value": 6, "Label": "-6dB" }, { "Value": 7, "Label": "-7dB" }, { "Value": 8, "Label": "-8dB" }, { "Value": 9, "Label": "-9dB" } ], "Selected": "Normal", "Selected_id": 0 }, "Units": "dB", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 4, "Node": 2, "Genre": "System", "Help": "PowerLevel to use for the Test", "ValueIDKey": 1125899954864148, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1594407617} -OpenZWave/1/node/2/instance/1/commandclass/115/value/1407374931574806/,{ "Label": "Frame Count", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 5, "Node": 2, "Genre": "System", "Help": "How Many Messages to send to the Node for the Test", "ValueIDKey": 1407374931574806, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1594407617} -OpenZWave/1/node/2/instance/1/commandclass/115/value/1688849908285464/,{ "Label": "Test", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 6, "Node": 2, "Genre": "System", "Help": "Perform a PowerLevel Test against the a Node", "ValueIDKey": 1688849908285464, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1594407617} -OpenZWave/1/node/2/instance/1/commandclass/115/value/1970324884996120/,{ "Label": "Report", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 7, "Node": 2, "Genre": "System", "Help": "Get the results of the latest PowerLevel Test against a Node", "ValueIDKey": 1970324884996120, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1594407617} -OpenZWave/1/node/2/instance/1/commandclass/115/value/2251799861706772/,{ "Label": "Test Status", "Value": { "List": [ { "Value": 0, "Label": "Failed" }, { "Value": 1, "Label": "Success" }, { "Value": 2, "Label": "In Progress" } ], "Selected": "Failed", "Selected_id": 0 }, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 8, "Node": 2, "Genre": "System", "Help": "The Current Status of the last PowerNode Test Executed", "ValueIDKey": 2251799861706772, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1594407617} -OpenZWave/1/node/2/instance/1/commandclass/115/value/2533274838417430/,{ "Label": "Acked Frames", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 9, "Node": 2, "Genre": "System", "Help": "Number of Messages successfully Acked by the Target Node", "ValueIDKey": 2533274838417430, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1594407617} -OpenZWave/1/node/2/instance/1/commandclass/134/,{ "Instance": 1, "CommandClassId": 134, "CommandClass": "COMMAND_CLASS_VERSION", "CommandClassVersion": 1, "TimeStamp": 1594407617} -OpenZWave/1/node/2/instance/1/commandclass/134/value/48332823/,{ "Label": "Library Version", "Value": "6", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 0, "Node": 2, "Genre": "System", "Help": "Z-Wave Library Version", "ValueIDKey": 48332823, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1594407617} -OpenZWave/1/node/2/instance/1/commandclass/134/value/281475025043479/,{ "Label": "Protocol Version", "Value": "3.67", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 1, "Node": 2, "Genre": "System", "Help": "Z-Wave Protocol Version", "ValueIDKey": 281475025043479, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1594407617} -OpenZWave/1/node/2/instance/1/commandclass/134/value/562950001754135/,{ "Label": "Application Version", "Value": "3.37", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 2, "Node": 2, "Genre": "System", "Help": "Application Version", "ValueIDKey": 562950001754135, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1594407617} -OpenZWave/1/node/12/,{ "NodeID": 12, "NodeQueryStage": "Complete", "isListening": true, "isFlirs": false, "isBeaming": true, "isRouting": true, "isSecurityv1": false, "isZWavePlus": true, "isNIFRecieved": true, "isAwake": true, "isFailed": false, "MetaData": { "OZWInfoURL": "http://www.openzwave.com/device-database/0086:0063:0103", "ZWAProductURL": "", "ProductPic": "images/aeotec/zw099.png", "Description": "Aeotec Smart Dimmer 6 is a low-cost Z-Wave Dimmer plug-in module specifically used to enable Z-Wave command and control (on/off/dim) of any plug-in tool. It can report immediate wattage consumption or kWh energy usage over a period of time. In the event of power failure, non-volatile memory retains all programmed information relating to the unit’s operating status. Its surface has a Smart RGB LED, which can be used for indicating the output load status or strength of the wireless signal. You can configure its indication colour according to your favour. The Smart Dimmer 6 is also a security Z-Wave device and supports Over The Air (OTA) feature for the products firmware upgrade.", "ProductManualURL": "https://Products.Z-WaveAlliance.org/ProductManual/File?folder=&filename=Manuals/2246/Aeon Labs Smart Dimmer 6 manual.pdf", "ProductPageURL": "", "InclusionHelp": "Turn the primary controller of Z-Wave network into inclusion mode, short press the product’s Action button that you can find on the product's housing.", "ExclusionHelp": "Turn the primary controller of Z-Wave network into exclusion mode, short press the product’s Action button that you can find on the product's housing.", "ResetHelp": "Press and hold the Action button that you can find on the product's housing for 20 seconds and then release. This procedure should only be used when the primary controller is inoperable.", "WakeupHelp": "", "ProductSupportURL": "", "Frequency": "", "Name": "Smart Dimmer 6", "ProductPicBase64": "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" }, "Event": "nodeQueriesComplete", "TimeStamp": 1605630092, "NodeManufacturerName": "AEON Labs", "NodeProductName": "ZW099 Smart Dimmer 6", "NodeBasicString": "Routing Slave", "NodeBasic": 4, "NodeGenericString": "Multilevel Switch", "NodeGeneric": 17, "NodeSpecificString": "Multilevel Power Switch", "NodeSpecific": 1, "NodeManufacturerID": "0x0086", "NodeProductType": "0x0103", "NodeProductID": "0x0063", "NodeBaudRate": 100000, "NodeVersion": 4, "NodeGroups": 2, "NodeName": "", "NodeLocation": "", "NodeDeviceTypeString": "Light Dimmer Switch", "NodeDeviceType": 1536, "NodeRole": 5, "NodeRoleString": "Always On Slave", "NodePlusType": 0, "NodePlusTypeString": "Z-Wave+ node", "Neighbors": [ 4, 13, 16, 17, 18, 21, 25, 27, 28, 29, 30, 31 ], "Neighbors": [ 4, 13, 16, 17, 18, 21, 25, 27, 28, 29, 30, 31 ], "Neighbors": [ 4, 13, 16, 17, 18, 21, 25, 27, 28, 29, 30, 31 ]} -OpenZWave/1/node/12/instance/1/,{ "Instance": 1, "TimeStamp": 1605629028} -OpenZWave/1/node/12/instance/1/commandclass/112/,{ "Instance": 1, "CommandClassId": 112, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "CommandClassVersion": 1, "TimeStamp": 1605629028} -OpenZWave/1/node/12/instance/1/commandclass/112/value/844425141682196/,{ "Label": "Current Overload Protection", "Value": { "List": [ { "Value": 0, "Label": "Deactivate Overload Protection (Default)" }, { "Value": 1, "Label": "Active Overload Protection" } ], "Selected": "Deactivate Overload Protection (Default)", "Selected_id": 0 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 3, "Node": 12, "Genre": "Config", "Help": "Load will be closed when the Current overruns (US 15.5A, Others 16.2) for more than 2 minutes", "ValueIDKey": 844425141682196, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1605629157} -OpenZWave/1/node/12/instance/1/commandclass/112/value/5629499745763348/,{ "Label": "Output Load Status", "Value": { "List": [ { "Value": 0, "Label": "Last status (Default)" }, { "Value": 1, "Label": "Always on" }, { "Value": 2, "Label": "Always off" } ], "Selected": "Last status (Default)", "Selected_id": 0 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 2, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 20, "Node": 12, "Genre": "Config", "Help": "Configure the output load status after re-power on.", "ValueIDKey": 5629499745763348, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1605629157} -OpenZWave/1/node/12/instance/1/commandclass/112/value/22517998348402708/,{ "Label": "Notification status", "Value": { "List": [ { "Value": 0, "Label": "Nothing" }, { "Value": 1, "Label": "Hail" }, { "Value": 2, "Label": "Basic" } ], "Selected": "Nothing", "Selected_id": 0 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 2, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 80, "Node": 12, "Genre": "Config", "Help": "Defines the automated status notification of an associated device when status changes", "ValueIDKey": 22517998348402708, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1605629158} -OpenZWave/1/node/12/instance/1/commandclass/112/value/22799473325113364/,{ "Label": "Configure the state of the LED", "Value": { "List": [ { "Value": 0, "Label": "The LED will follow the status (on/off) of its load. (Default)" }, { "Value": 1, "Label": "When the state of the Switch changes, the LED will follow the status (on/off) of its load, but the LED will turn off after 5 seconds." }, { "Value": 2, "Label": "Night Light Mode" } ], "Selected": "The LED will follow the status (on/off) of its load. (Default)", "Selected_id": 0 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 81, "Node": 12, "Genre": "Config", "Help": "Configure what the LED Ring displays during operations", "ValueIDKey": 22799473325113364, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1605629158} -OpenZWave/1/node/12/instance/1/commandclass/112/value/23362423278534675/,{ "Label": "Night Light Color", "Value": 0, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 16777215, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 83, "Node": 12, "Genre": "Config", "Help": "Configure the RGB Value when in Night Light Mode. Byte 1: Red Color Byte 2: Green Color Byte 3: Blue Color", "ValueIDKey": 23362423278534675, "ReadOnly": true, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1605629158} -OpenZWave/1/node/12/instance/1/commandclass/112/value/23643898255245331/,{ "Label": "RGB Brightness in Energy Mode", "Value": 0, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 16777215, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 84, "Node": 12, "Genre": "Config", "Help": "Configure the brightness level of RGB LED (0%-100%) when it is in Energy Mode/momentary indicate mode. Byte 1: Red Color Byte 2: Green Color Byte 3: Blue Color", "ValueIDKey": 23643898255245331, "ReadOnly": true, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1605629158} -OpenZWave/1/node/12/instance/1/commandclass/112/value/25332748115509264/,{ "Label": "Enables/disables parameter 91/92", "Value": false, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Bool", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 90, "Node": 12, "Genre": "Config", "Help": "Enable/disable Wattage threshold and percent.", "ValueIDKey": 25332748115509264, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1605629158} -OpenZWave/1/node/12/instance/1/commandclass/112/value/25614223092219926/,{ "Label": "Minimum Change to send Report (Watt)", "Value": 25, "Units": "watts", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 32000, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 91, "Node": 12, "Genre": "Config", "Help": "The value represents the minimum change in wattage for a Report to be sent (default 25 W)", "ValueIDKey": 25614223092219926, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1605629159} -OpenZWave/1/node/12/instance/1/commandclass/112/value/25895698068930577/,{ "Label": "Minimum Change to send Report (%)", "Value": 5, "Units": "%", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 100, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 92, "Node": 12, "Genre": "Config", "Help": "The value represents the minimum percentage change in wattage for a Report to be sent (Default 5)", "ValueIDKey": 25895698068930577, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1605629159} -OpenZWave/1/node/12/instance/1/commandclass/112/value/28147497882615832/,{ "Label": "Default Group Reports", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 100, "Node": 12, "Genre": "Config", "Help": "Set report types for groups 1, 2 and 3 to default.", "ValueIDKey": 28147497882615832, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1605629028} -OpenZWave/1/node/12/instance/1/commandclass/112/value/28428972859326483/,{ "Label": "Report type sent in Reporting Group 1", "Value": 0, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 101, "Node": 12, "Genre": "Config", "Help": "Defines the type of report sent for reporting group 1. 2 is multisensor report. 4 is meter report for watts. 8 is meter report for kilowatts. Value 1 (msb) Reserved Value 2 Reserved Value 3 Reserved Value 4 (lsb) bits 7-4 reserved bit 3 KWH bit 2 Watt bit 1 Current bit 0 Voltage", "ValueIDKey": 28428972859326483, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1605629159} -OpenZWave/1/node/12/instance/1/commandclass/112/value/28710447836037139/,{ "Label": "Report type sent in Reporting Group 2", "Value": 0, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 102, "Node": 12, "Genre": "Config", "Help": "Defines the type of report sent for reporting group 1. 2 is multisensor report. 4 is meter report for watts. 8 is meter report for kilowatts. Value 1 (msb) Reserved Value 2 Reserved Value 3 Reserved Value 4 (lsb) bits 7-4 reserved bit 3 KWH bit 2 Watt bit 1 Current bit 0 Voltage", "ValueIDKey": 28710447836037139, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1605629159} -OpenZWave/1/node/12/instance/1/commandclass/112/value/28991922812747795/,{ "Label": "Report type sent in Reporting Group 3", "Value": 0, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 103, "Node": 12, "Genre": "Config", "Help": "Defines the type of report sent for reporting group 1. 2 is multisensor report. 4 is meter report for watts. 8 is meter report for kilowatts. Value 1 (msb) Reserved Value 2 Reserved Value 3 Reserved Value 4 (lsb) bits 7-4 reserved bit 3 KWH bit 2 Watt bit 1 Current bit 0 Voltage", "ValueIDKey": 28991922812747795, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1605629159} -OpenZWave/1/node/12/instance/1/commandclass/112/value/30962247649722392/,{ "Label": "Set 111 to 113 to default", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 110, "Node": 12, "Genre": "Config", "Help": "Set time interval for sending reports for groups 1, 2 and 3 to default.", "ValueIDKey": 30962247649722392, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1605629028} -OpenZWave/1/node/12/instance/1/commandclass/112/value/31243722626433043/,{ "Label": "Send Interval for Reporting Group 1", "Value": 3, "Units": "seconds", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": -1, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 111, "Node": 12, "Genre": "Config", "Help": "Defines the time interval when the defined report for group 1 is sent.", "ValueIDKey": 31243722626433043, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1605629160} -OpenZWave/1/node/12/instance/1/commandclass/112/value/31525197603143699/,{ "Label": "Send Interval for Reporting Group 2", "Value": 600, "Units": "seconds", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": -1, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 112, "Node": 12, "Genre": "Config", "Help": "Defines the time interval when the defined report for group 2 is sent.", "ValueIDKey": 31525197603143699, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1605629160} -OpenZWave/1/node/12/instance/1/commandclass/112/value/31806672579854355/,{ "Label": "Send Interval for Reporting Group 3", "Value": 600, "Units": "seconds", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": -1, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 113, "Node": 12, "Genre": "Config", "Help": "Defines the time interval when the defined report for group 3 is sent.", "ValueIDKey": 31806672579854355, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1605629160} -OpenZWave/1/node/12/instance/1/commandclass/112/value/56294995553681428/,{ "Label": "Partner ID", "Value": { "List": [ { "Value": 0, "Label": "Aeon Labs Standard (Default)" }, { "Value": 1, "Label": "Others" } ], "Selected": "Aeon Labs Standard (Default)", "Selected_id": 0 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 200, "Node": 12, "Genre": "Config", "Help": "Partner ID", "ValueIDKey": 56294995553681428, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1605629160} -OpenZWave/1/node/12/instance/1/commandclass/112/value/70931694342635540/,{ "Label": "Configuration Locked", "Value": { "List": [ { "Value": 0, "Label": "Disable" }, { "Value": 1, "Label": "Enable" } ], "Selected": "Disable", "Selected_id": 0 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 252, "Node": 12, "Genre": "Config", "Help": "Enable/disable Configuration Locked", "ValueIDKey": 70931694342635540, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1605629160} -OpenZWave/1/node/12/instance/1/commandclass/112/value/71494644296056854/,{ "Label": "Device tag", "Value": 0, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 65535, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 254, "Node": 12, "Genre": "Config", "Help": "Device tag.", "ValueIDKey": 71494644296056854, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1605629161} -OpenZWave/1/node/12/instance/1/commandclass/112/value/71776119272767512/,{ "Label": "Reset device", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 255, "Node": 12, "Genre": "Config", "Help": "Reset to the default configuration.", "ValueIDKey": 71776119272767512, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1605629028} -OpenZWave/1/node/12/instance/1/commandclass/38/,{ "Instance": 1, "CommandClassId": 38, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "CommandClassVersion": 2, "TimeStamp": 1605629028} -OpenZWave/1/node/12/instance/1/commandclass/38/value/1407375098085395/,{ "Label": "Instance 1: Dimming Duration", "Value": 0, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 5, "Node": 12, "Genre": "System", "Help": "Duration taken when changing the Level of a Device (Values above 7620 use the devices default duration)", "ValueIDKey": 1407375098085395, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1605630293} -OpenZWave/1/node/12/instance/1/commandclass/38/value/206143505/,{ "Label": "Instance 1: Level", "Value": 99, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 0, "Node": 12, "Genre": "User", "Help": "The Current Level of the Device", "ValueIDKey": 206143505, "ReadOnly": false, "WriteOnly": false, "Event": "valueRefreshed", "TimeStamp": 1605630090} -OpenZWave/1/node/12/instance/1/commandclass/38/value/281475182854168/,{ "Label": "Instance 1: Bright", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 1, "Node": 12, "Genre": "User", "Help": "Increase the Brightness of the Device", "ValueIDKey": 281475182854168, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1605629028} -OpenZWave/1/node/12/instance/1/commandclass/38/value/562950159564824/,{ "Label": "Instance 1: Dim", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 2, "Node": 12, "Genre": "User", "Help": "Decrease the Brightness of the Device", "ValueIDKey": 562950159564824, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1605629028} -OpenZWave/1/node/12/instance/1/commandclass/38/value/844425144664080/,{ "Label": "Instance 1: Ignore Start Level", "Value": true, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Bool", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 3, "Node": 12, "Genre": "System", "Help": "Ignore the Start Level of the Device when increasing/decreasing brightness", "ValueIDKey": 844425144664080, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1605629028} -OpenZWave/1/node/12/instance/1/commandclass/38/value/1125900121374737/,{ "Label": "Instance 1: Start Level", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 4, "Node": 12, "Genre": "System", "Help": "Start Level when Changing the Brightness of a Device", "ValueIDKey": 1125900121374737, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1605629028} -OpenZWave/1/node/12/instance/1/commandclass/39/,{ "Instance": 1, "CommandClassId": 39, "CommandClass": "COMMAND_CLASS_SWITCH_ALL", "CommandClassVersion": 1, "TimeStamp": 1605629028} -OpenZWave/1/node/12/instance/1/commandclass/39/value/214548500/,{ "Label": "Switch All", "Value": { "List": [ { "Value": 0, "Label": "Disabled" }, { "Value": 1, "Label": "Off Enabled" }, { "Value": 2, "Label": "On Enabled" }, { "Value": 255, "Label": "On and Off Enabled" } ], "Selected": "On and Off Enabled", "Selected_id": 255 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_ALL", "Index": 0, "Node": 12, "Genre": "System", "Help": "Switch All Devices On/Off", "ValueIDKey": 214548500, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1605629055} -OpenZWave/1/node/12/instance/1/commandclass/51/,{ "Instance": 1, "CommandClassId": 51, "CommandClass": "COMMAND_CLASS_COLOR", "CommandClassVersion": 1, "TimeStamp": 1605629028} -OpenZWave/1/node/12/instance/1/commandclass/51/value/562950168166419/,{ "Label": "Color Channels", "Value": 28, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_COLOR", "Index": 2, "Node": 12, "Genre": "System", "Help": "Color Capabilities of the device", "ValueIDKey": 562950168166419, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1605629028} -OpenZWave/1/node/12/instance/1/commandclass/51/value/206356503/,{ "Label": "Color", "Value": "#000000", "Units": "#RRGGBB", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_COLOR", "Index": 0, "Node": 12, "Genre": "User", "Help": "Color (in RGB format)", "ValueIDKey": 206356503, "ReadOnly": false, "WriteOnly": false, "Event": "valueRefreshed", "TimeStamp": 1605630092} -OpenZWave/1/node/12/instance/1/commandclass/51/value/281475183067156/,{ "Label": "Color Index", "Value": { "List": [ { "Value": 0, "Label": "Off" }, { "Value": 1, "Label": "Cool White" }, { "Value": 2, "Label": "Warm White" }, { "Value": 3, "Label": "Red" }, { "Value": 4, "Label": "Lime" }, { "Value": 5, "Label": "Blue" }, { "Value": 6, "Label": "Yellow" }, { "Value": 7, "Label": "Cyan" }, { "Value": 8, "Label": "Magenta" }, { "Value": 9, "Label": "Silver" }, { "Value": 10, "Label": "Gray" }, { "Value": 11, "Label": "Maroon" }, { "Value": 12, "Label": "Olive" }, { "Value": 13, "Label": "Green" }, { "Value": 14, "Label": "Purple" }, { "Value": 15, "Label": "Teal" }, { "Value": 16, "Label": "Navy" }, { "Value": 17, "Label": "Custom" } ], "Selected": "Off", "Selected_id": 0 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_COLOR", "Index": 1, "Node": 12, "Genre": "User", "Help": "Preset Color", "ValueIDKey": 281475183067156, "ReadOnly": false, "WriteOnly": false, "Event": "valueRefreshed", "TimeStamp": 1605630092} -OpenZWave/1/node/12/instance/1/commandclass/94/,{ "Instance": 1, "CommandClassId": 94, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "CommandClassVersion": 1, "TimeStamp": 1605629028} -OpenZWave/1/node/12/instance/1/commandclass/94/value/215449617/,{ "Label": "Instance 1: ZWave+ Version", "Value": 1, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 0, "Node": 12, "Genre": "System", "Help": "ZWave+ Version Supported on the Device", "ValueIDKey": 215449617, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1605629028} -OpenZWave/1/node/12/instance/1/commandclass/94/value/281475192160278/,{ "Label": "Instance 1: InstallerIcon", "Value": 1536, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 1, "Node": 12, "Genre": "System", "Help": "Icon File to use for the Installer Application", "ValueIDKey": 281475192160278, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1605629028} -OpenZWave/1/node/12/instance/1/commandclass/94/value/562950168870934/,{ "Label": "Instance 1: UserIcon", "Value": 1536, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 2, "Node": 12, "Genre": "System", "Help": "Icon File to use for the User Application", "ValueIDKey": 562950168870934, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1605629028} -OpenZWave/1/node/12/instance/1/commandclass/114/,{ "Instance": 1, "CommandClassId": 114, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "CommandClassVersion": 2, "TimeStamp": 1605629028} -OpenZWave/1/node/12/instance/1/commandclass/114/value/215777299/,{ "Label": "Loaded Config Revision", "Value": 5, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 0, "Node": 12, "Genre": "System", "Help": "Revision of the Config file currently loaded", "ValueIDKey": 215777299, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1605629028} -OpenZWave/1/node/12/instance/1/commandclass/114/value/281475192487955/,{ "Label": "Config File Revision", "Value": 5, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 1, "Node": 12, "Genre": "System", "Help": "Revision of the Config file on the File System", "ValueIDKey": 281475192487955, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1605629028} -OpenZWave/1/node/12/instance/1/commandclass/114/value/562950169198611/,{ "Label": "Latest Available Config File Revision", "Value": 5, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 2, "Node": 12, "Genre": "System", "Help": "Latest Revision of the Config file available for download", "ValueIDKey": 562950169198611, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1605629028} -OpenZWave/1/node/12/instance/1/commandclass/114/value/1125900122619927/,{ "Label": "Serial Number", "Value": "0a000100010106040700000108010000000000", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 4, "Node": 12, "Genre": "System", "Help": "Device Serial Number", "ValueIDKey": 1125900122619927, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1605629028} -OpenZWave/1/node/12/instance/1/commandclass/115/,{ "Instance": 1, "CommandClassId": 115, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "CommandClassVersion": 1, "TimeStamp": 1605629028} -OpenZWave/1/node/12/instance/1/commandclass/115/value/215793684/,{ "Label": "Powerlevel", "Value": { "List": [ { "Value": 0, "Label": "Normal" }, { "Value": 1, "Label": "-1dB" }, { "Value": 2, "Label": "-2dB" }, { "Value": 3, "Label": "-3dB" }, { "Value": 4, "Label": "-4dB" }, { "Value": 5, "Label": "-5dB" }, { "Value": 6, "Label": "-6dB" }, { "Value": 7, "Label": "-7dB" }, { "Value": 8, "Label": "-8dB" }, { "Value": 9, "Label": "-9dB" } ], "Selected": "Normal", "Selected_id": 0 }, "Units": "dB", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 0, "Node": 12, "Genre": "System", "Help": "Output RF PowerLevel", "ValueIDKey": 215793684, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1605629055} -OpenZWave/1/node/12/instance/1/commandclass/115/value/281475192504337/,{ "Label": "Timeout", "Value": 0, "Units": "seconds", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 1, "Node": 12, "Genre": "System", "Help": "Timeout till the PowerLevel is reset to Normal", "ValueIDKey": 281475192504337, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1605629055} -OpenZWave/1/node/12/instance/1/commandclass/115/value/562950169215000/,{ "Label": "Set Powerlevel", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 2, "Node": 12, "Genre": "System", "Help": "Apply the Output PowerLevel and Timeout Values", "ValueIDKey": 562950169215000, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1605629028} -OpenZWave/1/node/12/instance/1/commandclass/115/value/844425145925649/,{ "Label": "Test Node", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 3, "Node": 12, "Genre": "System", "Help": "Node to Perform a test against", "ValueIDKey": 844425145925649, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1605629028} -OpenZWave/1/node/12/instance/1/commandclass/115/value/1125900122636308/,{ "Label": "Test Powerlevel", "Value": { "List": [ { "Value": 0, "Label": "Normal" }, { "Value": 1, "Label": "-1dB" }, { "Value": 2, "Label": "-2dB" }, { "Value": 3, "Label": "-3dB" }, { "Value": 4, "Label": "-4dB" }, { "Value": 5, "Label": "-5dB" }, { "Value": 6, "Label": "-6dB" }, { "Value": 7, "Label": "-7dB" }, { "Value": 8, "Label": "-8dB" }, { "Value": 9, "Label": "-9dB" } ], "Selected": "Normal", "Selected_id": 0 }, "Units": "dB", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 4, "Node": 12, "Genre": "System", "Help": "PowerLevel to use for the Test", "ValueIDKey": 1125900122636308, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1605629028} -OpenZWave/1/node/12/instance/1/commandclass/115/value/1407375099346966/,{ "Label": "Frame Count", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 5, "Node": 12, "Genre": "System", "Help": "How Many Messages to send to the Node for the Test", "ValueIDKey": 1407375099346966, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1605629028} -OpenZWave/1/node/12/instance/1/commandclass/115/value/1688850076057624/,{ "Label": "Test", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 6, "Node": 12, "Genre": "System", "Help": "Perform a PowerLevel Test against the a Node", "ValueIDKey": 1688850076057624, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1605629028} -OpenZWave/1/node/12/instance/1/commandclass/115/value/1970325052768280/,{ "Label": "Report", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 7, "Node": 12, "Genre": "System", "Help": "Get the results of the latest PowerLevel Test against a Node", "ValueIDKey": 1970325052768280, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1605629028} -OpenZWave/1/node/12/instance/1/commandclass/115/value/2251800029478932/,{ "Label": "Test Status", "Value": { "List": [ { "Value": 0, "Label": "Failed" }, { "Value": 1, "Label": "Success" }, { "Value": 2, "Label": "In Progress" } ], "Selected": "Failed", "Selected_id": 0 }, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 8, "Node": 12, "Genre": "System", "Help": "The Current Status of the last PowerNode Test Executed", "ValueIDKey": 2251800029478932, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1605629028} -OpenZWave/1/node/12/instance/1/commandclass/115/value/2533275006189590/,{ "Label": "Acked Frames", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 9, "Node": 12, "Genre": "System", "Help": "Number of Messages successfully Acked by the Target Node", "ValueIDKey": 2533275006189590, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1605629028} -OpenZWave/1/node/12/instance/1/commandclass/129/,{ "Instance": 1, "CommandClassId": 129, "CommandClass": "COMMAND_CLASS_CLOCK", "CommandClassVersion": 1, "TimeStamp": 1605629028} -OpenZWave/1/node/12/instance/1/commandclass/129/value/207634452/,{ "Label": "Day", "Value": { "List": [ { "Value": 1, "Label": "Monday" }, { "Value": 2, "Label": "Tuesday" }, { "Value": 3, "Label": "Wednesday" }, { "Value": 4, "Label": "Thursday" }, { "Value": 5, "Label": "Friday" }, { "Value": 6, "Label": "Saturday" }, { "Value": 7, "Label": "Sunday" } ], "Selected": "Tuesday", "Selected_id": 2 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CLOCK", "Index": 0, "Node": 12, "Genre": "User", "Help": "Day of Week", "ValueIDKey": 207634452, "ReadOnly": false, "WriteOnly": false, "Event": "valueRefreshed", "TimeStamp": 1605630092} -OpenZWave/1/node/12/instance/1/commandclass/129/value/281475184345105/,{ "Label": "Hour", "Value": 11, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CLOCK", "Index": 1, "Node": 12, "Genre": "User", "Help": "Hour", "ValueIDKey": 281475184345105, "ReadOnly": false, "WriteOnly": false, "Event": "valueRefreshed", "TimeStamp": 1605630092} -OpenZWave/1/node/12/instance/1/commandclass/129/value/562950161055761/,{ "Label": "Minute", "Value": 21, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CLOCK", "Index": 2, "Node": 12, "Genre": "User", "Help": "Minute", "ValueIDKey": 562950161055761, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1605630092} -OpenZWave/1/node/12/instance/1/commandclass/134/,{ "Instance": 1, "CommandClassId": 134, "CommandClass": "COMMAND_CLASS_VERSION", "CommandClassVersion": 1, "TimeStamp": 1605629028} -OpenZWave/1/node/12/instance/1/commandclass/134/value/216104983/,{ "Label": "Library Version", "Value": "3", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 0, "Node": 12, "Genre": "System", "Help": "Z-Wave Library Version", "ValueIDKey": 216104983, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1605629028} -OpenZWave/1/node/12/instance/1/commandclass/134/value/281475192815639/,{ "Label": "Protocol Version", "Value": "4.38", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 1, "Node": 12, "Genre": "System", "Help": "Z-Wave Protocol Version", "ValueIDKey": 281475192815639, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1605629028} -OpenZWave/1/node/12/instance/1/commandclass/134/value/562950169526295/,{ "Label": "Application Version", "Value": "1.12", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 2, "Node": 12, "Genre": "System", "Help": "Application Version", "ValueIDKey": 562950169526295, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1605629028} -OpenZWave/1/node/12/instance/1/commandclass/50/,{ "Instance": 1, "CommandClassId": 50, "CommandClass": "COMMAND_CLASS_METER", "CommandClassVersion": 3, "TimeStamp": 1605629028} -OpenZWave/1/node/12/instance/1/commandclass/50/value/206340114/,{ "Label": "Electric - kWh", "Value": 17.562999725341798, "Units": "kWh", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_METER", "Index": 0, "Node": 12, "Genre": "User", "Help": "", "ValueIDKey": 206340114, "ReadOnly": true, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1605630091} -OpenZWave/1/node/12/instance/1/commandclass/50/value/562950159761426/,{ "Label": "Electric - W", "Value": 9.6899995803833, "Units": "W", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_METER", "Index": 2, "Node": 12, "Genre": "User", "Help": "", "ValueIDKey": 562950159761426, "ReadOnly": true, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1605630091} -OpenZWave/1/node/12/instance/1/commandclass/50/value/1125900113182738/,{ "Label": "Electric - V", "Value": 123.04900360107422, "Units": "V", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_METER", "Index": 4, "Node": 12, "Genre": "User", "Help": "", "ValueIDKey": 1125900113182738, "ReadOnly": true, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1605630091} -OpenZWave/1/node/12/instance/1/commandclass/50/value/1407375089893394/,{ "Label": "Electric - A", "Value": 0.08299999684095383, "Units": "A", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_METER", "Index": 5, "Node": 12, "Genre": "User", "Help": "", "ValueIDKey": 1407375089893394, "ReadOnly": true, "WriteOnly": false, "Event": "valueRefreshed", "TimeStamp": 1605630091} -OpenZWave/1/node/12/instance/1/commandclass/50/value/72057594244268048/,{ "Label": "Exporting", "Value": false, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Bool", "Instance": 1, "CommandClass": "COMMAND_CLASS_METER", "Index": 256, "Node": 12, "Genre": "User", "Help": "", "ValueIDKey": 72057594244268048, "ReadOnly": true, "WriteOnly": false, "Event": "valueRefreshed", "TimeStamp": 1605630091} -OpenZWave/1/node/12/instance/1/commandclass/50/value/72339069229367320/,{ "Label": "Reset", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_METER", "Index": 257, "Node": 12, "Genre": "System", "Help": "", "ValueIDKey": 72339069229367320, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1605629028} -OpenZWave/1/node/12/instance/1/commandclass/43/,{ "Instance": 1, "CommandClassId": 43, "CommandClass": "COMMAND_CLASS_SCENE_ACTIVATION", "CommandClassVersion": 1, "TimeStamp": 1605629028} -OpenZWave/1/node/12/instance/1/commandclass/43/value/206225427/,{ "Label": "Scene", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_SCENE_ACTIVATION", "Index": 0, "Node": 12, "Genre": "User", "Help": "", "ValueIDKey": 206225427, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1605629028} -OpenZWave/1/node/12/instance/1/commandclass/43/value/281475182936083/,{ "Label": "Duration", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_SCENE_ACTIVATION", "Index": 1, "Node": 12, "Genre": "User", "Help": "", "ValueIDKey": 281475182936083, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1605629028} -OpenZWave/1/node/12/instance/1/commandclass/152/,{ "Instance": 1, "CommandClassId": 152, "CommandClass": "COMMAND_CLASS_SECURITY", "CommandClassVersion": 1, "TimeStamp": 1605629028} -OpenZWave/1/node/12/instance/1/commandclass/152/value/216399888/,{ "Label": "Instance 1: Secured", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Bool", "Instance": 1, "CommandClass": "COMMAND_CLASS_SECURITY", "Index": 0, "Node": 12, "Genre": "System", "Help": "Is Communication with Device Encrypted", "ValueIDKey": 216399888, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1605629028} -OpenZWave/1/node/12/instance/2/,{ "Instance": 2, "TimeStamp": 1605629028} -OpenZWave/1/node/12/instance/2/commandclass/38/,{ "Instance": 2, "CommandClassId": 38, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "CommandClassVersion": 2, "TimeStamp": 1605629028} -OpenZWave/1/node/12/instance/2/commandclass/38/value/1407375098085411/,{ "Label": "Instance 2: Dimming Duration", "Value": 0, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 2, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 5, "Node": 12, "Genre": "System", "Help": "Duration taken when changing the Level of a Device (Values above 7620 use the devices default duration)", "ValueIDKey": 1407375098085411, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1605630295} -OpenZWave/1/node/12/instance/2/commandclass/38/value/206143521/,{ "Label": "Instance 2: Level", "Value": 99, "Units": "", "ValueSet": true, "ValuePolled": true, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 2, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 0, "Node": 12, "Genre": "User", "Help": "The Current Level of the Device", "ValueIDKey": 206143521, "ReadOnly": false, "WriteOnly": false, "Event": "valueRefreshed", "TimeStamp": 1605630132} -OpenZWave/1/node/12/instance/2/commandclass/38/value/281475182854184/,{ "Label": "Instance 2: Bright", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 2, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 1, "Node": 12, "Genre": "User", "Help": "Increase the Brightness of the Device", "ValueIDKey": 281475182854184, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1605629028} -OpenZWave/1/node/12/instance/2/commandclass/38/value/562950159564840/,{ "Label": "Instance 2: Dim", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 2, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 2, "Node": 12, "Genre": "User", "Help": "Decrease the Brightness of the Device", "ValueIDKey": 562950159564840, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1605629028} -OpenZWave/1/node/12/instance/2/commandclass/38/value/844425144664096/,{ "Label": "Instance 2: Ignore Start Level", "Value": true, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Bool", "Instance": 2, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 3, "Node": 12, "Genre": "System", "Help": "Ignore the Start Level of the Device when increasing/decreasing brightness", "ValueIDKey": 844425144664096, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1605629028} -OpenZWave/1/node/12/instance/2/commandclass/38/value/1125900121374753/,{ "Label": "Instance 2: Start Level", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 2, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 4, "Node": 12, "Genre": "System", "Help": "Start Level when Changing the Brightness of a Device", "ValueIDKey": 1125900121374753, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1605629028} -OpenZWave/1/node/12/instance/2/commandclass/94/,{ "Instance": 2, "CommandClassId": 94, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "CommandClassVersion": 1, "TimeStamp": 1605629028} -OpenZWave/1/node/12/instance/2/commandclass/94/value/215449633/,{ "Label": "Instance 2: ZWave+ Version", "Value": 1, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 2, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 0, "Node": 12, "Genre": "System", "Help": "ZWave+ Version Supported on the Device", "ValueIDKey": 215449633, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1605629028} -OpenZWave/1/node/12/instance/2/commandclass/94/value/281475192160294/,{ "Label": "Instance 2: InstallerIcon", "Value": 1536, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 2, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 1, "Node": 12, "Genre": "System", "Help": "Icon File to use for the Installer Application", "ValueIDKey": 281475192160294, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1605629028} -OpenZWave/1/node/12/instance/2/commandclass/94/value/562950168870950/,{ "Label": "Instance 2: UserIcon", "Value": 1536, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 2, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 2, "Node": 12, "Genre": "System", "Help": "Icon File to use for the User Application", "ValueIDKey": 562950168870950, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1605629028} -OpenZWave/1/node/12/instance/2/commandclass/152/,{ "Instance": 2, "CommandClassId": 152, "CommandClass": "COMMAND_CLASS_SECURITY", "CommandClassVersion": 1, "TimeStamp": 1605629028} -OpenZWave/1/node/12/instance/2/commandclass/152/value/216399904/,{ "Label": "Instance 2: Secured", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Bool", "Instance": 2, "CommandClass": "COMMAND_CLASS_SECURITY", "Index": 0, "Node": 12, "Genre": "System", "Help": "Is Communication with Device Encrypted", "ValueIDKey": 216399904, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1605629028} -OpenZWave/1/node/12/association/1/,{ "Name": "LifeLine", "Help": "", "MaxAssociations": 5, "Members": [ "1.1" ], "TimeStamp": 1605629028} -OpenZWave/1/node/12/association/2/,{ "Name": "Retransmit Switch CC", "Help": "", "MaxAssociations": 5, "Members": [], "TimeStamp": 1605629035} \ No newline at end of file diff --git a/tests/components/ozw/fixtures/light_new_ozw_network_dump.csv b/tests/components/ozw/fixtures/light_new_ozw_network_dump.csv deleted file mode 100644 index df810f64102..00000000000 --- a/tests/components/ozw/fixtures/light_new_ozw_network_dump.csv +++ /dev/null @@ -1,55 +0,0 @@ -OpenZWave/1/status/,{ "OpenZWave_Version": "1.6.1214", "OZWDeamon_Version": "0.1", "QTOpenZWave_Version": "1.0.0", "QT_Version": "5.12.5", "Status": "driverAllNodesQueried", "TimeStamp": 1579566933, "ManufacturerSpecificDBReady": true, "homeID": 3245146787, "getControllerNodeId": 1, "getSUCNodeId": 1, "isPrimaryController": true, "isBridgeController": false, "hasExtendedTXStatistics": true, "getControllerLibraryVersion": "Z-Wave 3.95", "getControllerLibraryType": "Static Controller", "getControllerPath": "/dev/zwave"} -OpenZWave/1/node/39/,{ "NodeID": 39, "NodeQueryStage": "CacheLoad", "isListening": true, "isFlirs": false, "isBeaming": true, "isRouting": true, "isSecurityv1": false, "isZWavePlus": false, "isNIFRecieved": true, "isAwake": true, "isFailed": false, "MetaData": { "OZWInfoURL": "http://www.openzwave.com/device-database/0371:0002:0103", "ZWAProductURL": "", "ProductPic": "images/aeotec/zwa002.png", "Description": "✓ Standard form factor and appearance of the light bulb with 800 lm output ✓ RGBW: dimmable from 5% to 100%, tunable from 1800K to 6500K, and 16 million colors ✓ Possible to be included in groups, scenes, or schedules ✓ Suitable for indoor lighting: Corridors, Bedroom, Living Room, etc.", "ProductManualURL": "https://Products.Z-WaveAlliance.org/ProductManual/File?folder=&filename=Manuals/2881/AA LED Bulb 6 说明书(RGBW-AL001)_转曲-2dd.pdf", "ProductPageURL": "", "InclusionHelp": "Add for inclusion 1. Ensure the led bulb has been excluded outside the network. 2. Triggered by OFF ->ON (between 0.5-2 seconds each time) 3. LED solid yellow Color (0xFFFF00) during the pairing(Timeout is 10 seconds).  Failure: Blinks between 100% White and Red 0x0000FF color for 3 seconds (at a rate of 200ms per flash), Once 3 seconds have passed, the LED should return to a Warm White LED at 100%  Success: Blinks between 100% White and Green 0x00FF00 color for 3 seconds (at a rate of 200ms per flash). Once 3 seconds have passed, the LED should return to a Warm White LED at 100%.", "ExclusionHelp": "Remove for exclusion 1. Assuming led bulb was added to controller. 2. Triggered by OFF -> ON -> OFF -> ON -> OFF -> ON (between 0.5-2 seconds each time). 3. LED Solid Purple/Violet Color (0xEE82EE) during the unpairing process. (Timeout is 10 seconds).  Failure: Blinks between 100% White and Red 0x0000FF color for 3 seconds (at a rate of 200ms per flash), Once 3 seconds have passed, the LED should return to the last color ( memory status(color cc set)) of LED Bulb.  Success: Blinks between 100% White and Blue 0x0000FF color for 3 seconds (at a rate of 200ms per flash). Once 3 seconds have passed, the LED should return to a Warm White LED at 100%.", "ResetHelp": "Reset the Device. 1. Assuming led bulb was added to controller and was power on. 2. RGBW bulb re-power 6 times (between 0.5-2 seconds each time). Note: ON -> OFF -> ON -> OFF -> ON -> OFF -> ON -> OFF -> ON -> OFF -> ON -> OFF -> ON 3. If the 6th power on, the led bulb change to Yellow color(into pairing process ), which means that the reset factory settings are successf. Using this action in case of the primary controller is missing or inoperable.", "WakeupHelp": "", "ProductSupportURL": "", "Frequency": "", "Name": "LED Bulb 6:Multi-Colour", "ProductPicBase64": "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" }, "Event": "nodeNaming", "TimeStamp": 1579566891, "NodeManufacturerName": "Aeotec Limited", "NodeProductName": "ZWA002 LED Bulb 6 Multi-Color", "NodeBasicString": "Routing Slave", "NodeBasic": 4, "NodeGenericString": "Multilevel Switch", "NodeGeneric": 17, "NodeSpecificString": "Multilevel Power Switch", "NodeSpecific": 1, "NodeManufacturerID": "0x0371", "NodeProductType": "0x0103", "NodeProductID": "0x0002", "NodeBaudRate": 100000, "NodeVersion": 4, "NodeGroups": 1} -OpenZWave/1/node/39/instance/1/,{ "Instance": 1, "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/38/,{ "Instance": 1, "CommandClassId": 38, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/38/value/1407375551070225/,{ "Label": "Dimming Duration", "Value": 255, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 5, "Node": 39, "Genre": "System", "Help": "Duration taken when changing the Level of a Device", "ValueIDKey": 1407375551070225, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/38/value/659128337/,{ "Label": "Level", "Value": 0, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 0, "Node": 39, "Genre": "User", "Help": "The Current Level of the Device", "ValueIDKey": 659128337, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/38/value/281475635839000/,{ "Label": "Bright", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 1, "Node": 39, "Genre": "User", "Help": "Increase the Brightness of the Device", "ValueIDKey": 281475635839000, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/38/value/562950612549656/,{ "Label": "Dim", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 2, "Node": 39, "Genre": "User", "Help": "Decrease the Brightness of the Device", "ValueIDKey": 562950612549656, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/38/value/844425597648912/,{ "Label": "Ignore Start Level", "Value": true, "Units": "", "Min": 0, "Max": 0, "Type": "Bool", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 3, "Node": 39, "Genre": "System", "Help": "Ignore the Start Level of the Device when increasing/decreasing brightness", "ValueIDKey": 844425597648912, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/38/value/1125900574359569/,{ "Label": "Start Level", "Value": 0, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 4, "Node": 39, "Genre": "System", "Help": "Start Level when Changing the Brightness of a Device", "ValueIDKey": 1125900574359569, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/39/,{ "Instance": 1, "CommandClassId": 39, "CommandClass": "COMMAND_CLASS_SWITCH_ALL", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/39/value/667533332/,{ "Label": "Switch All", "Value": { "List": [ { "Value": 0, "Label": "Disabled" }, { "Value": 1, "Label": "Off Enabled" }, { "Value": 2, "Label": "On Enabled" }, { "Value": 255, "Label": "On and Off Enabled" } ], "Selected": "On and Off Enabled" }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_ALL", "Index": 0, "Node": 39, "Genre": "System", "Help": "Switch All Devices On/Off", "ValueIDKey": 667533332, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/51/,{ "Instance": 1, "CommandClassId": 51, "CommandClass": "COMMAND_CLASS_COLOR", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/51/value/562950621151251/,{ "Label": "Color Channels", "Value": 31, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_COLOR", "Index": 2, "Node": 39, "Genre": "System", "Help": "Color Capabilities of the device", "ValueIDKey": 562950621151251, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/51/value/659341335/,{ "Label": "Color", "Value": "#000000FF00", "Units": "#RRGGBBWWCW", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_COLOR", "Index": 0, "Node": 39, "Genre": "User", "Help": "Color (in RGB format)", "ValueIDKey": 659341335, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/51/value/281475636051988/,{ "Label": "Color Index", "Value": { "List": [ { "Value": 0, "Label": "Off" }, { "Value": 1, "Label": "Cool White" }, { "Value": 2, "Label": "Warm White" }, { "Value": 3, "Label": "Red" }, { "Value": 4, "Label": "Lime" }, { "Value": 5, "Label": "Blue" }, { "Value": 6, "Label": "Yellow" }, { "Value": 7, "Label": "Cyan" }, { "Value": 8, "Label": "Magenta" }, { "Value": 9, "Label": "Silver" }, { "Value": 10, "Label": "Gray" }, { "Value": 11, "Label": "Maroon" }, { "Value": 12, "Label": "Olive" }, { "Value": 13, "Label": "Green" }, { "Value": 14, "Label": "Purple" }, { "Value": 15, "Label": "Teal" }, { "Value": 16, "Label": "Navy" }, { "Value": 17, "Label": "Custom" } ], "Selected": "Warm White" }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_COLOR", "Index": 1, "Node": 39, "Genre": "User", "Help": "Preset Color", "ValueIDKey": 281475636051988, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/94/,{ "Instance": 1, "CommandClassId": 94, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/94/value/668434449/,{ "Label": "ZWave+ Version", "Value": 1, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 0, "Node": 39, "Genre": "System", "Help": "ZWave+ Version Supported on the Device", "ValueIDKey": 668434449, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/94/value/281475645145110/,{ "Label": "InstallerIcon", "Value": 1536, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 1, "Node": 39, "Genre": "System", "Help": "Icon File to use for the Installer Application", "ValueIDKey": 281475645145110, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/94/value/562950621855766/,{ "Label": "UserIcon", "Value": 1536, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 2, "Node": 39, "Genre": "System", "Help": "Icon File to use for the User Application", "ValueIDKey": 562950621855766, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/112/,{ "Instance": 1, "CommandClassId": 112, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/112/value/281475641245716/,{ "Label": "User custom mode LED animations", "Value": { "List": [ { "Value": 0, "Label": "Disable" }, { "Value": 1, "Label": "Blink Colors in order mode" }, { "Value": 2, "Label": "Randomized blink color mode" } ], "Selected": "Disable" }, "Units": "", "Min": 0, "Max": 2, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 1, "Node": 39, "Genre": "Config", "Help": "User custom mode for LED animations", "ValueIDKey": 281475641245716, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/112/value/562950617956372/,{ "Label": "Strobe over Custom Color", "Value": { "List": [ { "Value": 0, "Label": "Disable" }, { "Value": 1, "Label": "Enable" } ], "Selected": "Disable" }, "Units": "", "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 2, "Node": 39, "Genre": "Config", "Help": "Enable/Disable Strobe over Custom Color.", "ValueIDKey": 562950617956372, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/112/value/844425594667027/,{ "Label": "Set the rate of change to next color in Custom Mode", "Value": 50, "Units": "ms", "Min": 5, "Max": 8640000, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 3, "Node": 39, "Genre": "Config", "Help": "Set the rate of change to next color in Custom Mode.", "ValueIDKey": 844425594667027, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/112/value/1125900571377681/,{ "Label": "Set color that LED Bulb blinks", "Value": 1, "Units": "", "Min": 1, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 4, "Node": 39, "Genre": "Config", "Help": "Set color that LED Bulb blinks in Blink Mode.", "ValueIDKey": 1125900571377681, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/112/value/4503600291905553/,{ "Label": "Ramp rate when dimming using Multilevel Switch", "Value": 20, "Units": "100ms", "Min": 0, "Max": 100, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 16, "Node": 39, "Genre": "Config", "Help": "Specifying the ramp rate when dimming using Multilevel Switch V1 CC in 100ms.", "ValueIDKey": 4503600291905553, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/112/value/22517998801387540/,{ "Label": "Notification", "Value": { "List": [ { "Value": 0, "Label": "Nothing" }, { "Value": 1, "Label": "Basic CC report" } ], "Selected": "Basic CC report" }, "Units": "", "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 80, "Node": 39, "Genre": "Config", "Help": "Enable to send notifications to associated devices (Group 1) when the state of LED Bulb is changed.", "ValueIDKey": 22517998801387540, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/112/value/22799473778098198/,{ "Label": "Warm White temperature", "Value": 2700, "Units": "k", "Min": 2700, "Max": 4999, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 81, "Node": 39, "Genre": "Config", "Help": "Adjusting the color temperature in warm white color component. available value: 2700k to 4999k", "ValueIDKey": 22799473778098198, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/112/value/23080948754808854/,{ "Label": "cold white temperature", "Value": 6500, "Units": "k", "Min": 5000, "Max": 6500, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 82, "Node": 39, "Genre": "Config", "Help": "Adjusting the color temperature in cold white color component. available value:5000k to 6500k", "ValueIDKey": 23080948754808854, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/114/,{ "Instance": 1, "CommandClassId": 114, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/114/value/668762131/,{ "Label": "Loaded Config Revision", "Value": 3, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 0, "Node": 39, "Genre": "System", "Help": "Revision of the Config file currently loaded", "ValueIDKey": 668762131, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/114/value/281475645472787/,{ "Label": "Config File Revision", "Value": 3, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 1, "Node": 39, "Genre": "System", "Help": "Revision of the Config file on the File System", "ValueIDKey": 281475645472787, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/114/value/562950622183443/,{ "Label": "Latest Available Config File Revision", "Value": 3, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 2, "Node": 39, "Genre": "System", "Help": "Latest Revision of the Config file available for download", "ValueIDKey": 562950622183443, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/114/value/844425598894103/,{ "Label": "Device ID", "Value": "", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 3, "Node": 39, "Genre": "System", "Help": "Manufacturer Specific Device ID/Model", "ValueIDKey": 844425598894103, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/114/value/1125900575604759/,{ "Label": "Serial Number", "Value": "00001cd6bda18c83", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 4, "Node": 39, "Genre": "System", "Help": "Device Serial Number", "ValueIDKey": 1125900575604759, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/,{ "Instance": 1, "CommandClassId": 115, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/668778516/,{ "Label": "Powerlevel", "Value": { "List": [ { "Value": 0, "Label": "Normal" }, { "Value": 1, "Label": "-1dB" }, { "Value": 2, "Label": "-2dB" }, { "Value": 3, "Label": "-3dB" }, { "Value": 4, "Label": "-4dB" }, { "Value": 5, "Label": "-5dB" }, { "Value": 6, "Label": "-6dB" }, { "Value": 7, "Label": "-7dB" }, { "Value": 8, "Label": "-8dB" }, { "Value": 9, "Label": "-9dB" } ], "Selected": "Normal" }, "Units": "dB", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 0, "Node": 39, "Genre": "System", "Help": "Output RF PowerLevel", "ValueIDKey": 668778516, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/281475645489169/,{ "Label": "Timeout", "Value": 0, "Units": "seconds", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 1, "Node": 39, "Genre": "System", "Help": "Timeout till the PowerLevel is reset to Normal", "ValueIDKey": 281475645489169, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/562950622199832/,{ "Label": "Set Powerlevel", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 2, "Node": 39, "Genre": "System", "Help": "Apply the Output PowerLevel and Timeout Values", "ValueIDKey": 562950622199832, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/844425598910481/,{ "Label": "Test Node", "Value": 0, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 3, "Node": 39, "Genre": "System", "Help": "Node to Perform a test against", "ValueIDKey": 844425598910481, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/1125900575621140/,{ "Label": "Test Powerlevel", "Value": { "List": [ { "Value": 0, "Label": "Normal" }, { "Value": 1, "Label": "-1dB" }, { "Value": 2, "Label": "-2dB" }, { "Value": 3, "Label": "-3dB" }, { "Value": 4, "Label": "-4dB" }, { "Value": 5, "Label": "-5dB" }, { "Value": 6, "Label": "-6dB" }, { "Value": 7, "Label": "-7dB" }, { "Value": 8, "Label": "-8dB" }, { "Value": 9, "Label": "-9dB" } ], "Selected": "Normal" }, "Units": "dB", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 4, "Node": 39, "Genre": "System", "Help": "PowerLevel to use for the Test", "ValueIDKey": 1125900575621140, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/1407375552331798/,{ "Label": "Frame Count", "Value": 0, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 5, "Node": 39, "Genre": "System", "Help": "How Many Messages to send to the Note for the Test", "ValueIDKey": 1407375552331798, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/1688850529042456/,{ "Label": "Test", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 6, "Node": 39, "Genre": "System", "Help": "Perform a PowerLevel Test against the a Node", "ValueIDKey": 1688850529042456, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/1970325505753112/,{ "Label": "Report", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 7, "Node": 39, "Genre": "System", "Help": "Get the results of the latest PowerLevel Test against a Node", "ValueIDKey": 1970325505753112, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/2251800482463764/,{ "Label": "Test Status", "Value": { "List": [ { "Value": 0, "Label": "Failed" }, { "Value": 1, "Label": "Success" }, { "Value": 2, "Label": "In Progress" } ], "Selected": "Failed" }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 8, "Node": 39, "Genre": "System", "Help": "The Current Status of the last PowerNode Test Executed", "ValueIDKey": 2251800482463764, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/2533275459174422/,{ "Label": "Acked Frames", "Value": 0, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 9, "Node": 39, "Genre": "System", "Help": "Number of Messages successfully Acked by the Target Node", "ValueIDKey": 2533275459174422, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/134/,{ "Instance": 1, "CommandClassId": 134, "CommandClass": "COMMAND_CLASS_VERSION", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/134/value/669089815/,{ "Label": "Library Version", "Value": "3", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 0, "Node": 39, "Genre": "System", "Help": "Z-Wave Library Version", "ValueIDKey": 669089815, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/134/value/281475645800471/,{ "Label": "Protocol Version", "Value": "4.38", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 1, "Node": 39, "Genre": "System", "Help": "Z-Wave Protocol Version", "ValueIDKey": 281475645800471, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/134/value/562950622511127/,{ "Label": "Application Version", "Value": "2.00", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 2, "Node": 39, "Genre": "System", "Help": "Application Version", "ValueIDKey": 562950622511127, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/association/1/,{ "Name": "Lifeline", "Help": "", "MaxAssociations": 1, "Members": [ "1.0" ], "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/43/,{ "Instance": 1, "CommandClassId": 43, "CommandClass": "COMMAND_CLASS_SCENE_ACTIVATION", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/43/value/562950622511127/,{ "Label": "Scene", "Value": 0, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_SCENE_ACTIVATION", "Index": 0, "Node": 7, "Genre": "User", "Help": "", "ValueIDKey": 122339347, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueChanged", "TimeStamp": 1579630367} -OpenZWave/1/node/39/instance/1/commandclass/91/,{ "Instance": 1, "CommandClassId": 91, "CommandClass": "COMMAND_CLASS_CENTRAL_SCENE", "TimeStamp": 1579630630} -OpenZWave/1/node/39/instance/1/commandclass/91/value/281476005806100/,{ "Label": "Scene 1", "Value": { "List": [ { "Value": 0, "Label": "Inactive" }, { "Value": 1, "Label": "Pressed 1 Time" }, { "Value": 2, "Label": "Key Released" }, { "Value": 3, "Label": "Key Held down" } ], "Selected": "Inactive", "Selected_id": 0 }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CENTRAL_SCENE", "Index": 1, "Node": 61, "Genre": "User", "Help": "", "ValueIDKey": 281476005806100, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueChanged", "TimeStamp": 1579640710} \ No newline at end of file diff --git a/tests/components/ozw/fixtures/light_no_cw_network_dump.csv b/tests/components/ozw/fixtures/light_no_cw_network_dump.csv deleted file mode 100644 index 4120bc34dce..00000000000 --- a/tests/components/ozw/fixtures/light_no_cw_network_dump.csv +++ /dev/null @@ -1,54 +0,0 @@ -OpenZWave/1/status/,{ "OpenZWave_Version": "1.6.1008", "OZWDeamon_Version": "0.1", "QTOpenZWave_Version": "1.0.0", "QT_Version": "5.12.5", "Status": "driverAllNodesQueried", "TimeStamp": 1579566933, "ManufacturerSpecificDBReady": true, "homeID": 3245146787, "getControllerNodeId": 1, "getSUCNodeId": 1, "isPrimaryController": true, "isBridgeController": false, "hasExtendedTXStatistics": true, "getControllerLibraryVersion": "Z-Wave 3.95", "getControllerLibraryType": "Static Controller", "getControllerPath": "/dev/zwave"} -OpenZWave/1/node/39/,{ "NodeID": 39, "NodeQueryStage": "CacheLoad", "isListening": true, "isFlirs": false, "isBeaming": true, "isRouting": true, "isSecurityv1": false, "isZWavePlus": false, "isNIFRecieved": true, "isAwake": true, "isFailed": false, "MetaData": { "OZWInfoURL": "http://www.openzwave.com/device-database/0371:0002:0103", "ZWAProductURL": "", "ProductPic": "images/aeotec/zwa002.png", "Description": "✓ Standard form factor and appearance of the light bulb with 800 lm output ✓ RGBW: dimmable from 5% to 100%, tunable from 1800K to 6500K, and 16 million colors ✓ Possible to be included in groups, scenes, or schedules ✓ Suitable for indoor lighting: Corridors, Bedroom, Living Room, etc.", "ProductManualURL": "https://Products.Z-WaveAlliance.org/ProductManual/File?folder=&filename=Manuals/2881/AA LED Bulb 6 说明书(RGBW-AL001)_转曲-2dd.pdf", "ProductPageURL": "", "InclusionHelp": "Add for inclusion 1. Ensure the led bulb has been excluded outside the network. 2. Triggered by OFF ->ON (between 0.5-2 seconds each time) 3. LED solid yellow Color (0xFFFF00) during the pairing(Timeout is 10 seconds).  Failure: Blinks between 100% White and Red 0x0000FF color for 3 seconds (at a rate of 200ms per flash), Once 3 seconds have passed, the LED should return to a Warm White LED at 100%  Success: Blinks between 100% White and Green 0x00FF00 color for 3 seconds (at a rate of 200ms per flash). Once 3 seconds have passed, the LED should return to a Warm White LED at 100%.", "ExclusionHelp": "Remove for exclusion 1. Assuming led bulb was added to controller. 2. Triggered by OFF -> ON -> OFF -> ON -> OFF -> ON (between 0.5-2 seconds each time). 3. LED Solid Purple/Violet Color (0xEE82EE) during the unpairing process. (Timeout is 10 seconds).  Failure: Blinks between 100% White and Red 0x0000FF color for 3 seconds (at a rate of 200ms per flash), Once 3 seconds have passed, the LED should return to the last color ( memory status(color cc set)) of LED Bulb.  Success: Blinks between 100% White and Blue 0x0000FF color for 3 seconds (at a rate of 200ms per flash). Once 3 seconds have passed, the LED should return to a Warm White LED at 100%.", "ResetHelp": "Reset the Device. 1. Assuming led bulb was added to controller and was power on. 2. RGBW bulb re-power 6 times (between 0.5-2 seconds each time). Note: ON -> OFF -> ON -> OFF -> ON -> OFF -> ON -> OFF -> ON -> OFF -> ON -> OFF -> ON 3. If the 6th power on, the led bulb change to Yellow color(into pairing process ), which means that the reset factory settings are successf. Using this action in case of the primary controller is missing or inoperable.", "WakeupHelp": "", "ProductSupportURL": "", "Frequency": "", "Name": "LED Bulb 6:Multi-Colour", "ProductPicBase64": "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" }, "Event": "nodeNaming", "TimeStamp": 1579566891, "NodeManufacturerName": "Aeotec Limited", "NodeProductName": "ZWA002 LED Bulb 6 Multi-Color", "NodeBasicString": "Routing Slave", "NodeBasic": 4, "NodeGenericString": "Multilevel Switch", "NodeGeneric": 17, "NodeSpecificString": "Multilevel Power Switch", "NodeSpecific": 1, "NodeManufacturerID": "0x0371", "NodeProductType": "0x0103", "NodeProductID": "0x0002", "NodeBaudRate": 100000, "NodeVersion": 4, "NodeGroups": 1} -OpenZWave/1/node/39/instance/1/,{ "Instance": 1, "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/38/,{ "Instance": 1, "CommandClassId": 38, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/38/value/659128337/,{ "Label": "Level", "Value": 0, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 0, "Node": 39, "Genre": "User", "Help": "The Current Level of the Device", "ValueIDKey": 659128337, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/38/value/281475635839000/,{ "Label": "Bright", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 1, "Node": 39, "Genre": "User", "Help": "Increase the Brightness of the Device", "ValueIDKey": 281475635839000, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/38/value/562950612549656/,{ "Label": "Dim", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 2, "Node": 39, "Genre": "User", "Help": "Decrease the Brightness of the Device", "ValueIDKey": 562950612549656, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/38/value/844425597648912/,{ "Label": "Ignore Start Level", "Value": true, "Units": "", "Min": 0, "Max": 0, "Type": "Bool", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 3, "Node": 39, "Genre": "System", "Help": "Ignore the Start Level of the Device when increasing/decreasing brightness", "ValueIDKey": 844425597648912, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/38/value/1125900574359569/,{ "Label": "Start Level", "Value": 0, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 4, "Node": 39, "Genre": "System", "Help": "Start Level when Changing the Brightness of a Device", "ValueIDKey": 1125900574359569, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/39/,{ "Instance": 1, "CommandClassId": 39, "CommandClass": "COMMAND_CLASS_SWITCH_ALL", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/39/value/667533332/,{ "Label": "Switch All", "Value": { "List": [ { "Value": 0, "Label": "Disabled" }, { "Value": 1, "Label": "Off Enabled" }, { "Value": 2, "Label": "On Enabled" }, { "Value": 255, "Label": "On and Off Enabled" } ], "Selected": "On and Off Enabled" }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_ALL", "Index": 0, "Node": 39, "Genre": "System", "Help": "Switch All Devices On/Off", "ValueIDKey": 667533332, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/51/,{ "Instance": 1, "CommandClassId": 51, "CommandClass": "COMMAND_CLASS_COLOR", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/51/value/562950621151251/,{ "Label": "Color Channels", "Value": 29, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_COLOR", "Index": 2, "Node": 39, "Genre": "System", "Help": "Color Capabilities of the device", "ValueIDKey": 562950621151251, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/51/value/659341335/,{ "Label": "Color", "Value": "#0000000000", "Units": "#RRGGBBWWCW", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_COLOR", "Index": 0, "Node": 39, "Genre": "User", "Help": "Color (in RGB format)", "ValueIDKey": 659341335, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/51/value/281475636051988/,{ "Label": "Color Index", "Value": { "List": [ { "Value": 0, "Label": "Off" }, { "Value": 1, "Label": "Cool White" }, { "Value": 2, "Label": "Warm White" }, { "Value": 3, "Label": "Red" }, { "Value": 4, "Label": "Lime" }, { "Value": 5, "Label": "Blue" }, { "Value": 6, "Label": "Yellow" }, { "Value": 7, "Label": "Cyan" }, { "Value": 8, "Label": "Magenta" }, { "Value": 9, "Label": "Silver" }, { "Value": 10, "Label": "Gray" }, { "Value": 11, "Label": "Maroon" }, { "Value": 12, "Label": "Olive" }, { "Value": 13, "Label": "Green" }, { "Value": 14, "Label": "Purple" }, { "Value": 15, "Label": "Teal" }, { "Value": 16, "Label": "Navy" }, { "Value": 17, "Label": "Custom" } ], "Selected": "Warm White" }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_COLOR", "Index": 1, "Node": 39, "Genre": "User", "Help": "Preset Color", "ValueIDKey": 281475636051988, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/94/,{ "Instance": 1, "CommandClassId": 94, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/94/value/668434449/,{ "Label": "ZWave+ Version", "Value": 1, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 0, "Node": 39, "Genre": "System", "Help": "ZWave+ Version Supported on the Device", "ValueIDKey": 668434449, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/94/value/281475645145110/,{ "Label": "InstallerIcon", "Value": 1536, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 1, "Node": 39, "Genre": "System", "Help": "Icon File to use for the Installer Application", "ValueIDKey": 281475645145110, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/94/value/562950621855766/,{ "Label": "UserIcon", "Value": 1536, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 2, "Node": 39, "Genre": "System", "Help": "Icon File to use for the User Application", "ValueIDKey": 562950621855766, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/112/,{ "Instance": 1, "CommandClassId": 112, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/112/value/281475641245716/,{ "Label": "User custom mode LED animations", "Value": { "List": [ { "Value": 0, "Label": "Disable" }, { "Value": 1, "Label": "Blink Colors in order mode" }, { "Value": 2, "Label": "Randomized blink color mode" } ], "Selected": "Disable" }, "Units": "", "Min": 0, "Max": 2, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 1, "Node": 39, "Genre": "Config", "Help": "User custom mode for LED animations", "ValueIDKey": 281475641245716, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/112/value/562950617956372/,{ "Label": "Strobe over Custom Color", "Value": { "List": [ { "Value": 0, "Label": "Disable" }, { "Value": 1, "Label": "Enable" } ], "Selected": "Disable" }, "Units": "", "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 2, "Node": 39, "Genre": "Config", "Help": "Enable/Disable Strobe over Custom Color.", "ValueIDKey": 562950617956372, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/112/value/844425594667027/,{ "Label": "Set the rate of change to next color in Custom Mode", "Value": 50, "Units": "ms", "Min": 5, "Max": 8640000, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 3, "Node": 39, "Genre": "Config", "Help": "Set the rate of change to next color in Custom Mode.", "ValueIDKey": 844425594667027, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/112/value/1125900571377681/,{ "Label": "Set color that LED Bulb blinks", "Value": 1, "Units": "", "Min": 1, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 4, "Node": 39, "Genre": "Config", "Help": "Set color that LED Bulb blinks in Blink Mode.", "ValueIDKey": 1125900571377681, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/112/value/4503600291905553/,{ "Label": "Ramp rate when dimming using Multilevel Switch", "Value": 20, "Units": "100ms", "Min": 0, "Max": 100, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 16, "Node": 39, "Genre": "Config", "Help": "Specifying the ramp rate when dimming using Multilevel Switch V1 CC in 100ms.", "ValueIDKey": 4503600291905553, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/112/value/22517998801387540/,{ "Label": "Notification", "Value": { "List": [ { "Value": 0, "Label": "Nothing" }, { "Value": 1, "Label": "Basic CC report" } ], "Selected": "Basic CC report" }, "Units": "", "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 80, "Node": 39, "Genre": "Config", "Help": "Enable to send notifications to associated devices (Group 1) when the state of LED Bulb is changed.", "ValueIDKey": 22517998801387540, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/112/value/22799473778098198/,{ "Label": "Warm White temperature", "Value": 2700, "Units": "k", "Min": 2700, "Max": 4999, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 81, "Node": 39, "Genre": "Config", "Help": "Adjusting the color temperature in warm white color component. available value: 2700k to 4999k", "ValueIDKey": 22799473778098198, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/112/value/23080948754808854/,{ "Label": "cold white temperature", "Value": 6500, "Units": "k", "Min": 5000, "Max": 6500, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 82, "Node": 39, "Genre": "Config", "Help": "Adjusting the color temperature in cold white color component. available value:5000k to 6500k", "ValueIDKey": 23080948754808854, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/114/,{ "Instance": 1, "CommandClassId": 114, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/114/value/668762131/,{ "Label": "Loaded Config Revision", "Value": 3, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 0, "Node": 39, "Genre": "System", "Help": "Revision of the Config file currently loaded", "ValueIDKey": 668762131, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/114/value/281475645472787/,{ "Label": "Config File Revision", "Value": 3, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 1, "Node": 39, "Genre": "System", "Help": "Revision of the Config file on the File System", "ValueIDKey": 281475645472787, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/114/value/562950622183443/,{ "Label": "Latest Available Config File Revision", "Value": 3, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 2, "Node": 39, "Genre": "System", "Help": "Latest Revision of the Config file available for download", "ValueIDKey": 562950622183443, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/114/value/844425598894103/,{ "Label": "Device ID", "Value": "", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 3, "Node": 39, "Genre": "System", "Help": "Manufacturer Specific Device ID/Model", "ValueIDKey": 844425598894103, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/114/value/1125900575604759/,{ "Label": "Serial Number", "Value": "00001cd6bda18c83", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 4, "Node": 39, "Genre": "System", "Help": "Device Serial Number", "ValueIDKey": 1125900575604759, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/,{ "Instance": 1, "CommandClassId": 115, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/668778516/,{ "Label": "Powerlevel", "Value": { "List": [ { "Value": 0, "Label": "Normal" }, { "Value": 1, "Label": "-1dB" }, { "Value": 2, "Label": "-2dB" }, { "Value": 3, "Label": "-3dB" }, { "Value": 4, "Label": "-4dB" }, { "Value": 5, "Label": "-5dB" }, { "Value": 6, "Label": "-6dB" }, { "Value": 7, "Label": "-7dB" }, { "Value": 8, "Label": "-8dB" }, { "Value": 9, "Label": "-9dB" } ], "Selected": "Normal" }, "Units": "dB", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 0, "Node": 39, "Genre": "System", "Help": "Output RF PowerLevel", "ValueIDKey": 668778516, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/281475645489169/,{ "Label": "Timeout", "Value": 0, "Units": "seconds", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 1, "Node": 39, "Genre": "System", "Help": "Timeout till the PowerLevel is reset to Normal", "ValueIDKey": 281475645489169, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/562950622199832/,{ "Label": "Set Powerlevel", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 2, "Node": 39, "Genre": "System", "Help": "Apply the Output PowerLevel and Timeout Values", "ValueIDKey": 562950622199832, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/844425598910481/,{ "Label": "Test Node", "Value": 0, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 3, "Node": 39, "Genre": "System", "Help": "Node to Perform a test against", "ValueIDKey": 844425598910481, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/1125900575621140/,{ "Label": "Test Powerlevel", "Value": { "List": [ { "Value": 0, "Label": "Normal" }, { "Value": 1, "Label": "-1dB" }, { "Value": 2, "Label": "-2dB" }, { "Value": 3, "Label": "-3dB" }, { "Value": 4, "Label": "-4dB" }, { "Value": 5, "Label": "-5dB" }, { "Value": 6, "Label": "-6dB" }, { "Value": 7, "Label": "-7dB" }, { "Value": 8, "Label": "-8dB" }, { "Value": 9, "Label": "-9dB" } ], "Selected": "Normal" }, "Units": "dB", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 4, "Node": 39, "Genre": "System", "Help": "PowerLevel to use for the Test", "ValueIDKey": 1125900575621140, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/1407375552331798/,{ "Label": "Frame Count", "Value": 0, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 5, "Node": 39, "Genre": "System", "Help": "How Many Messages to send to the Note for the Test", "ValueIDKey": 1407375552331798, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/1688850529042456/,{ "Label": "Test", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 6, "Node": 39, "Genre": "System", "Help": "Perform a PowerLevel Test against the a Node", "ValueIDKey": 1688850529042456, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/1970325505753112/,{ "Label": "Report", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 7, "Node": 39, "Genre": "System", "Help": "Get the results of the latest PowerLevel Test against a Node", "ValueIDKey": 1970325505753112, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/2251800482463764/,{ "Label": "Test Status", "Value": { "List": [ { "Value": 0, "Label": "Failed" }, { "Value": 1, "Label": "Success" }, { "Value": 2, "Label": "In Progress" } ], "Selected": "Failed" }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 8, "Node": 39, "Genre": "System", "Help": "The Current Status of the last PowerNode Test Executed", "ValueIDKey": 2251800482463764, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/2533275459174422/,{ "Label": "Acked Frames", "Value": 0, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 9, "Node": 39, "Genre": "System", "Help": "Number of Messages successfully Acked by the Target Node", "ValueIDKey": 2533275459174422, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/134/,{ "Instance": 1, "CommandClassId": 134, "CommandClass": "COMMAND_CLASS_VERSION", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/134/value/669089815/,{ "Label": "Library Version", "Value": "3", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 0, "Node": 39, "Genre": "System", "Help": "Z-Wave Library Version", "ValueIDKey": 669089815, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/134/value/281475645800471/,{ "Label": "Protocol Version", "Value": "4.38", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 1, "Node": 39, "Genre": "System", "Help": "Z-Wave Protocol Version", "ValueIDKey": 281475645800471, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/134/value/562950622511127/,{ "Label": "Application Version", "Value": "2.00", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 2, "Node": 39, "Genre": "System", "Help": "Application Version", "ValueIDKey": 562950622511127, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/association/1/,{ "Name": "Lifeline", "Help": "", "MaxAssociations": 1, "Members": [ "1.0" ], "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/43/,{ "Instance": 1, "CommandClassId": 43, "CommandClass": "COMMAND_CLASS_SCENE_ACTIVATION", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/43/value/562950622511127/,{ "Label": "Scene", "Value": 0, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_SCENE_ACTIVATION", "Index": 0, "Node": 7, "Genre": "User", "Help": "", "ValueIDKey": 122339347, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueChanged", "TimeStamp": 1579630367} -OpenZWave/1/node/39/instance/1/commandclass/91/,{ "Instance": 1, "CommandClassId": 91, "CommandClass": "COMMAND_CLASS_CENTRAL_SCENE", "TimeStamp": 1579630630} -OpenZWave/1/node/39/instance/1/commandclass/91/value/281476005806100/,{ "Label": "Scene 1", "Value": { "List": [ { "Value": 0, "Label": "Inactive" }, { "Value": 1, "Label": "Pressed 1 Time" }, { "Value": 2, "Label": "Key Released" }, { "Value": 3, "Label": "Key Held down" } ], "Selected": "Inactive", "Selected_id": 0 }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CENTRAL_SCENE", "Index": 1, "Node": 61, "Genre": "User", "Help": "", "ValueIDKey": 281476005806100, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueChanged", "TimeStamp": 1579640710} \ No newline at end of file diff --git a/tests/components/ozw/fixtures/light_no_rgb.json b/tests/components/ozw/fixtures/light_no_rgb.json deleted file mode 100644 index 85226b8a71a..00000000000 --- a/tests/components/ozw/fixtures/light_no_rgb.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "topic": "OpenZWave/1/node/2/instance/1/commandclass/38/value/38371345/", - "payload": { - "Label": "Level", - "Value": 0, - "Units": "", - "Min": 0, - "Max": 255, - "Type": "Byte", - "Instance": 1, - "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", - "Index": 0, - "Node": 2, - "Genre": "User", - "Help": "The Current Level of the Device", - "ValueIDKey": 38371345, - "ReadOnly": false, - "WriteOnly": false, - "ValueSet": false, - "ValuePolled": false, - "ChangeVerified": false, - "Event": "valueAdded", - "TimeStamp": 1579566891 - } -} diff --git a/tests/components/ozw/fixtures/light_no_ww_network_dump.csv b/tests/components/ozw/fixtures/light_no_ww_network_dump.csv deleted file mode 100644 index c001750973d..00000000000 --- a/tests/components/ozw/fixtures/light_no_ww_network_dump.csv +++ /dev/null @@ -1,54 +0,0 @@ -OpenZWave/1/status/,{ "OpenZWave_Version": "1.6.1008", "OZWDeamon_Version": "0.1", "QTOpenZWave_Version": "1.0.0", "QT_Version": "5.12.5", "Status": "driverAllNodesQueried", "TimeStamp": 1579566933, "ManufacturerSpecificDBReady": true, "homeID": 3245146787, "getControllerNodeId": 1, "getSUCNodeId": 1, "isPrimaryController": true, "isBridgeController": false, "hasExtendedTXStatistics": true, "getControllerLibraryVersion": "Z-Wave 3.95", "getControllerLibraryType": "Static Controller", "getControllerPath": "/dev/zwave"} -OpenZWave/1/node/39/,{ "NodeID": 39, "NodeQueryStage": "CacheLoad", "isListening": true, "isFlirs": false, "isBeaming": true, "isRouting": true, "isSecurityv1": false, "isZWavePlus": false, "isNIFRecieved": true, "isAwake": true, "isFailed": false, "MetaData": { "OZWInfoURL": "http://www.openzwave.com/device-database/0371:0002:0103", "ZWAProductURL": "", "ProductPic": "images/aeotec/zwa002.png", "Description": "✓ Standard form factor and appearance of the light bulb with 800 lm output ✓ RGBW: dimmable from 5% to 100%, tunable from 1800K to 6500K, and 16 million colors ✓ Possible to be included in groups, scenes, or schedules ✓ Suitable for indoor lighting: Corridors, Bedroom, Living Room, etc.", "ProductManualURL": "https://Products.Z-WaveAlliance.org/ProductManual/File?folder=&filename=Manuals/2881/AA LED Bulb 6 说明书(RGBW-AL001)_转曲-2dd.pdf", "ProductPageURL": "", "InclusionHelp": "Add for inclusion 1. Ensure the led bulb has been excluded outside the network. 2. Triggered by OFF ->ON (between 0.5-2 seconds each time) 3. LED solid yellow Color (0xFFFF00) during the pairing(Timeout is 10 seconds).  Failure: Blinks between 100% White and Red 0x0000FF color for 3 seconds (at a rate of 200ms per flash), Once 3 seconds have passed, the LED should return to a Warm White LED at 100%  Success: Blinks between 100% White and Green 0x00FF00 color for 3 seconds (at a rate of 200ms per flash). Once 3 seconds have passed, the LED should return to a Warm White LED at 100%.", "ExclusionHelp": "Remove for exclusion 1. Assuming led bulb was added to controller. 2. Triggered by OFF -> ON -> OFF -> ON -> OFF -> ON (between 0.5-2 seconds each time). 3. LED Solid Purple/Violet Color (0xEE82EE) during the unpairing process. (Timeout is 10 seconds).  Failure: Blinks between 100% White and Red 0x0000FF color for 3 seconds (at a rate of 200ms per flash), Once 3 seconds have passed, the LED should return to the last color ( memory status(color cc set)) of LED Bulb.  Success: Blinks between 100% White and Blue 0x0000FF color for 3 seconds (at a rate of 200ms per flash). Once 3 seconds have passed, the LED should return to a Warm White LED at 100%.", "ResetHelp": "Reset the Device. 1. Assuming led bulb was added to controller and was power on. 2. RGBW bulb re-power 6 times (between 0.5-2 seconds each time). Note: ON -> OFF -> ON -> OFF -> ON -> OFF -> ON -> OFF -> ON -> OFF -> ON -> OFF -> ON 3. If the 6th power on, the led bulb change to Yellow color(into pairing process ), which means that the reset factory settings are successf. Using this action in case of the primary controller is missing or inoperable.", "WakeupHelp": "", "ProductSupportURL": "", "Frequency": "", "Name": "LED Bulb 6:Multi-Colour", "ProductPicBase64": "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" }, "Event": "nodeNaming", "TimeStamp": 1579566891, "NodeManufacturerName": "Aeotec Limited", "NodeProductName": "ZWA002 LED Bulb 6 Multi-Color", "NodeBasicString": "Routing Slave", "NodeBasic": 4, "NodeGenericString": "Multilevel Switch", "NodeGeneric": 17, "NodeSpecificString": "Multilevel Power Switch", "NodeSpecific": 1, "NodeManufacturerID": "0x0371", "NodeProductType": "0x0103", "NodeProductID": "0x0002", "NodeBaudRate": 100000, "NodeVersion": 4, "NodeGroups": 1} -OpenZWave/1/node/39/instance/1/,{ "Instance": 1, "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/38/,{ "Instance": 1, "CommandClassId": 38, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/38/value/659128337/,{ "Label": "Level", "Value": 0, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 0, "Node": 39, "Genre": "User", "Help": "The Current Level of the Device", "ValueIDKey": 659128337, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/38/value/281475635839000/,{ "Label": "Bright", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 1, "Node": 39, "Genre": "User", "Help": "Increase the Brightness of the Device", "ValueIDKey": 281475635839000, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/38/value/562950612549656/,{ "Label": "Dim", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 2, "Node": 39, "Genre": "User", "Help": "Decrease the Brightness of the Device", "ValueIDKey": 562950612549656, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/38/value/844425597648912/,{ "Label": "Ignore Start Level", "Value": true, "Units": "", "Min": 0, "Max": 0, "Type": "Bool", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 3, "Node": 39, "Genre": "System", "Help": "Ignore the Start Level of the Device when increasing/decreasing brightness", "ValueIDKey": 844425597648912, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/38/value/1125900574359569/,{ "Label": "Start Level", "Value": 0, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 4, "Node": 39, "Genre": "System", "Help": "Start Level when Changing the Brightness of a Device", "ValueIDKey": 1125900574359569, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/39/,{ "Instance": 1, "CommandClassId": 39, "CommandClass": "COMMAND_CLASS_SWITCH_ALL", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/39/value/667533332/,{ "Label": "Switch All", "Value": { "List": [ { "Value": 0, "Label": "Disabled" }, { "Value": 1, "Label": "Off Enabled" }, { "Value": 2, "Label": "On Enabled" }, { "Value": 255, "Label": "On and Off Enabled" } ], "Selected": "On and Off Enabled" }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_ALL", "Index": 0, "Node": 39, "Genre": "System", "Help": "Switch All Devices On/Off", "ValueIDKey": 667533332, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/51/,{ "Instance": 1, "CommandClassId": 51, "CommandClass": "COMMAND_CLASS_COLOR", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/51/value/562950621151251/,{ "Label": "Color Channels", "Value": 30, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_COLOR", "Index": 2, "Node": 39, "Genre": "System", "Help": "Color Capabilities of the device", "ValueIDKey": 562950621151251, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/51/value/659341335/,{ "Label": "Color", "Value": "#0000000000", "Units": "#RRGGBBWWCW", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_COLOR", "Index": 0, "Node": 39, "Genre": "User", "Help": "Color (in RGB format)", "ValueIDKey": 659341335, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/51/value/281475636051988/,{ "Label": "Color Index", "Value": { "List": [ { "Value": 0, "Label": "Off" }, { "Value": 1, "Label": "Cool White" }, { "Value": 2, "Label": "Warm White" }, { "Value": 3, "Label": "Red" }, { "Value": 4, "Label": "Lime" }, { "Value": 5, "Label": "Blue" }, { "Value": 6, "Label": "Yellow" }, { "Value": 7, "Label": "Cyan" }, { "Value": 8, "Label": "Magenta" }, { "Value": 9, "Label": "Silver" }, { "Value": 10, "Label": "Gray" }, { "Value": 11, "Label": "Maroon" }, { "Value": 12, "Label": "Olive" }, { "Value": 13, "Label": "Green" }, { "Value": 14, "Label": "Purple" }, { "Value": 15, "Label": "Teal" }, { "Value": 16, "Label": "Navy" }, { "Value": 17, "Label": "Custom" } ], "Selected": "Warm White" }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_COLOR", "Index": 1, "Node": 39, "Genre": "User", "Help": "Preset Color", "ValueIDKey": 281475636051988, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/94/,{ "Instance": 1, "CommandClassId": 94, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/94/value/668434449/,{ "Label": "ZWave+ Version", "Value": 1, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 0, "Node": 39, "Genre": "System", "Help": "ZWave+ Version Supported on the Device", "ValueIDKey": 668434449, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/94/value/281475645145110/,{ "Label": "InstallerIcon", "Value": 1536, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 1, "Node": 39, "Genre": "System", "Help": "Icon File to use for the Installer Application", "ValueIDKey": 281475645145110, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/94/value/562950621855766/,{ "Label": "UserIcon", "Value": 1536, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 2, "Node": 39, "Genre": "System", "Help": "Icon File to use for the User Application", "ValueIDKey": 562950621855766, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/112/,{ "Instance": 1, "CommandClassId": 112, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/112/value/281475641245716/,{ "Label": "User custom mode LED animations", "Value": { "List": [ { "Value": 0, "Label": "Disable" }, { "Value": 1, "Label": "Blink Colors in order mode" }, { "Value": 2, "Label": "Randomized blink color mode" } ], "Selected": "Disable" }, "Units": "", "Min": 0, "Max": 2, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 1, "Node": 39, "Genre": "Config", "Help": "User custom mode for LED animations", "ValueIDKey": 281475641245716, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/112/value/562950617956372/,{ "Label": "Strobe over Custom Color", "Value": { "List": [ { "Value": 0, "Label": "Disable" }, { "Value": 1, "Label": "Enable" } ], "Selected": "Disable" }, "Units": "", "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 2, "Node": 39, "Genre": "Config", "Help": "Enable/Disable Strobe over Custom Color.", "ValueIDKey": 562950617956372, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/112/value/844425594667027/,{ "Label": "Set the rate of change to next color in Custom Mode", "Value": 50, "Units": "ms", "Min": 5, "Max": 8640000, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 3, "Node": 39, "Genre": "Config", "Help": "Set the rate of change to next color in Custom Mode.", "ValueIDKey": 844425594667027, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/112/value/1125900571377681/,{ "Label": "Set color that LED Bulb blinks", "Value": 1, "Units": "", "Min": 1, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 4, "Node": 39, "Genre": "Config", "Help": "Set color that LED Bulb blinks in Blink Mode.", "ValueIDKey": 1125900571377681, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/112/value/4503600291905553/,{ "Label": "Ramp rate when dimming using Multilevel Switch", "Value": 20, "Units": "100ms", "Min": 0, "Max": 100, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 16, "Node": 39, "Genre": "Config", "Help": "Specifying the ramp rate when dimming using Multilevel Switch V1 CC in 100ms.", "ValueIDKey": 4503600291905553, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/112/value/22517998801387540/,{ "Label": "Notification", "Value": { "List": [ { "Value": 0, "Label": "Nothing" }, { "Value": 1, "Label": "Basic CC report" } ], "Selected": "Basic CC report" }, "Units": "", "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 80, "Node": 39, "Genre": "Config", "Help": "Enable to send notifications to associated devices (Group 1) when the state of LED Bulb is changed.", "ValueIDKey": 22517998801387540, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/112/value/22799473778098198/,{ "Label": "Warm White temperature", "Value": 2700, "Units": "k", "Min": 2700, "Max": 4999, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 81, "Node": 39, "Genre": "Config", "Help": "Adjusting the color temperature in warm white color component. available value: 2700k to 4999k", "ValueIDKey": 22799473778098198, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/112/value/23080948754808854/,{ "Label": "cold white temperature", "Value": 6500, "Units": "k", "Min": 5000, "Max": 6500, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 82, "Node": 39, "Genre": "Config", "Help": "Adjusting the color temperature in cold white color component. available value:5000k to 6500k", "ValueIDKey": 23080948754808854, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/114/,{ "Instance": 1, "CommandClassId": 114, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/114/value/668762131/,{ "Label": "Loaded Config Revision", "Value": 3, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 0, "Node": 39, "Genre": "System", "Help": "Revision of the Config file currently loaded", "ValueIDKey": 668762131, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/114/value/281475645472787/,{ "Label": "Config File Revision", "Value": 3, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 1, "Node": 39, "Genre": "System", "Help": "Revision of the Config file on the File System", "ValueIDKey": 281475645472787, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/114/value/562950622183443/,{ "Label": "Latest Available Config File Revision", "Value": 3, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 2, "Node": 39, "Genre": "System", "Help": "Latest Revision of the Config file available for download", "ValueIDKey": 562950622183443, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/114/value/844425598894103/,{ "Label": "Device ID", "Value": "", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 3, "Node": 39, "Genre": "System", "Help": "Manufacturer Specific Device ID/Model", "ValueIDKey": 844425598894103, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/114/value/1125900575604759/,{ "Label": "Serial Number", "Value": "00001cd6bda18c83", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 4, "Node": 39, "Genre": "System", "Help": "Device Serial Number", "ValueIDKey": 1125900575604759, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/,{ "Instance": 1, "CommandClassId": 115, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/668778516/,{ "Label": "Powerlevel", "Value": { "List": [ { "Value": 0, "Label": "Normal" }, { "Value": 1, "Label": "-1dB" }, { "Value": 2, "Label": "-2dB" }, { "Value": 3, "Label": "-3dB" }, { "Value": 4, "Label": "-4dB" }, { "Value": 5, "Label": "-5dB" }, { "Value": 6, "Label": "-6dB" }, { "Value": 7, "Label": "-7dB" }, { "Value": 8, "Label": "-8dB" }, { "Value": 9, "Label": "-9dB" } ], "Selected": "Normal" }, "Units": "dB", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 0, "Node": 39, "Genre": "System", "Help": "Output RF PowerLevel", "ValueIDKey": 668778516, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/281475645489169/,{ "Label": "Timeout", "Value": 0, "Units": "seconds", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 1, "Node": 39, "Genre": "System", "Help": "Timeout till the PowerLevel is reset to Normal", "ValueIDKey": 281475645489169, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/562950622199832/,{ "Label": "Set Powerlevel", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 2, "Node": 39, "Genre": "System", "Help": "Apply the Output PowerLevel and Timeout Values", "ValueIDKey": 562950622199832, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/844425598910481/,{ "Label": "Test Node", "Value": 0, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 3, "Node": 39, "Genre": "System", "Help": "Node to Perform a test against", "ValueIDKey": 844425598910481, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/1125900575621140/,{ "Label": "Test Powerlevel", "Value": { "List": [ { "Value": 0, "Label": "Normal" }, { "Value": 1, "Label": "-1dB" }, { "Value": 2, "Label": "-2dB" }, { "Value": 3, "Label": "-3dB" }, { "Value": 4, "Label": "-4dB" }, { "Value": 5, "Label": "-5dB" }, { "Value": 6, "Label": "-6dB" }, { "Value": 7, "Label": "-7dB" }, { "Value": 8, "Label": "-8dB" }, { "Value": 9, "Label": "-9dB" } ], "Selected": "Normal" }, "Units": "dB", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 4, "Node": 39, "Genre": "System", "Help": "PowerLevel to use for the Test", "ValueIDKey": 1125900575621140, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/1407375552331798/,{ "Label": "Frame Count", "Value": 0, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 5, "Node": 39, "Genre": "System", "Help": "How Many Messages to send to the Note for the Test", "ValueIDKey": 1407375552331798, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/1688850529042456/,{ "Label": "Test", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 6, "Node": 39, "Genre": "System", "Help": "Perform a PowerLevel Test against the a Node", "ValueIDKey": 1688850529042456, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/1970325505753112/,{ "Label": "Report", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 7, "Node": 39, "Genre": "System", "Help": "Get the results of the latest PowerLevel Test against a Node", "ValueIDKey": 1970325505753112, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/2251800482463764/,{ "Label": "Test Status", "Value": { "List": [ { "Value": 0, "Label": "Failed" }, { "Value": 1, "Label": "Success" }, { "Value": 2, "Label": "In Progress" } ], "Selected": "Failed" }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 8, "Node": 39, "Genre": "System", "Help": "The Current Status of the last PowerNode Test Executed", "ValueIDKey": 2251800482463764, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/2533275459174422/,{ "Label": "Acked Frames", "Value": 0, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 9, "Node": 39, "Genre": "System", "Help": "Number of Messages successfully Acked by the Target Node", "ValueIDKey": 2533275459174422, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/134/,{ "Instance": 1, "CommandClassId": 134, "CommandClass": "COMMAND_CLASS_VERSION", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/134/value/669089815/,{ "Label": "Library Version", "Value": "3", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 0, "Node": 39, "Genre": "System", "Help": "Z-Wave Library Version", "ValueIDKey": 669089815, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/134/value/281475645800471/,{ "Label": "Protocol Version", "Value": "4.38", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 1, "Node": 39, "Genre": "System", "Help": "Z-Wave Protocol Version", "ValueIDKey": 281475645800471, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/134/value/562950622511127/,{ "Label": "Application Version", "Value": "2.00", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 2, "Node": 39, "Genre": "System", "Help": "Application Version", "ValueIDKey": 562950622511127, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/association/1/,{ "Name": "Lifeline", "Help": "", "MaxAssociations": 1, "Members": [ "1.0" ], "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/43/,{ "Instance": 1, "CommandClassId": 43, "CommandClass": "COMMAND_CLASS_SCENE_ACTIVATION", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/43/value/562950622511127/,{ "Label": "Scene", "Value": 0, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_SCENE_ACTIVATION", "Index": 0, "Node": 7, "Genre": "User", "Help": "", "ValueIDKey": 122339347, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueChanged", "TimeStamp": 1579630367} -OpenZWave/1/node/39/instance/1/commandclass/91/,{ "Instance": 1, "CommandClassId": 91, "CommandClass": "COMMAND_CLASS_CENTRAL_SCENE", "TimeStamp": 1579630630} -OpenZWave/1/node/39/instance/1/commandclass/91/value/281476005806100/,{ "Label": "Scene 1", "Value": { "List": [ { "Value": 0, "Label": "Inactive" }, { "Value": 1, "Label": "Pressed 1 Time" }, { "Value": 2, "Label": "Key Released" }, { "Value": 3, "Label": "Key Held down" } ], "Selected": "Inactive", "Selected_id": 0 }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CENTRAL_SCENE", "Index": 1, "Node": 61, "Genre": "User", "Help": "", "ValueIDKey": 281476005806100, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueChanged", "TimeStamp": 1579640710} \ No newline at end of file diff --git a/tests/components/ozw/fixtures/light_pure_rgb.json b/tests/components/ozw/fixtures/light_pure_rgb.json deleted file mode 100644 index 4e66e8459e7..00000000000 --- a/tests/components/ozw/fixtures/light_pure_rgb.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "topic": "OpenZWave/1/node/7/instance/1/commandclass/51/value/122470423/", - "payload": { - "Label": "Color", - "Value": "#ff00000000", - "Units": "#RRGGBBWW", - "ValueSet": false, - "ValuePolled": false, - "ChangeVerified": false, - "Min": 0, - "Max": 0, - "Type": "String", - "Instance": 1, - "CommandClass": "COMMAND_CLASS_COLOR", - "Index": 0, - "Node": 7, - "Genre": "User", - "Help": "Color (in RGB format)", - "ValueIDKey": 122470423, - "ReadOnly": false, - "WriteOnly": false, - "Event": "valueAdded", - "TimeStamp": 1597142799 - } -} diff --git a/tests/components/ozw/fixtures/light_rgb.json b/tests/components/ozw/fixtures/light_rgb.json deleted file mode 100644 index 0945b77db2d..00000000000 --- a/tests/components/ozw/fixtures/light_rgb.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "topic": "OpenZWave/1/node/39/instance/1/commandclass/51/value/659341335/", - "payload": { - "Label": "Color", - "Value": "#000000FF00", - "Units": "#RRGGBBWWCW", - "Min": 0, - "Max": 0, - "Type": "String", - "Instance": 1, - "CommandClass": "COMMAND_CLASS_COLOR", - "Index": 0, - "Node": 39, - "Genre": "User", - "Help": "Color (in RGB format)", - "ValueIDKey": 659341335, - "ReadOnly": false, - "WriteOnly": false, - "ValueSet": false, - "ValuePolled": false, - "ChangeVerified": false, - "Event": "valueAdded", - "TimeStamp": 1579566891 - } -} diff --git a/tests/components/ozw/fixtures/light_wc_network_dump.csv b/tests/components/ozw/fixtures/light_wc_network_dump.csv deleted file mode 100644 index 7af15f9926a..00000000000 --- a/tests/components/ozw/fixtures/light_wc_network_dump.csv +++ /dev/null @@ -1,54 +0,0 @@ -OpenZWave/1/status/,{ "OpenZWave_Version": "1.6.1214", "OZWDeamon_Version": "0.1", "QTOpenZWave_Version": "1.0.0", "QT_Version": "5.12.5", "Status": "driverAllNodesQueried", "TimeStamp": 1579566933, "ManufacturerSpecificDBReady": true, "homeID": 3245146787, "getControllerNodeId": 1, "getSUCNodeId": 1, "isPrimaryController": true, "isBridgeController": false, "hasExtendedTXStatistics": true, "getControllerLibraryVersion": "Z-Wave 3.95", "getControllerLibraryType": "Static Controller", "getControllerPath": "/dev/zwave"} -OpenZWave/1/node/39/,{ "NodeID": 39, "NodeQueryStage": "CacheLoad", "isListening": true, "isFlirs": false, "isBeaming": true, "isRouting": true, "isSecurityv1": false, "isZWavePlus": false, "isNIFRecieved": true, "isAwake": true, "isFailed": false, "MetaData": { "OZWInfoURL": "http://www.openzwave.com/device-database/0371:0002:0103", "ZWAProductURL": "", "ProductPic": "images/aeotec/zwa002.png", "Description": "✓ Standard form factor and appearance of the light bulb with 800 lm output ✓ RGBW: dimmable from 5% to 100%, tunable from 1800K to 6500K, and 16 million colors ✓ Possible to be included in groups, scenes, or schedules ✓ Suitable for indoor lighting: Corridors, Bedroom, Living Room, etc.", "ProductManualURL": "https://Products.Z-WaveAlliance.org/ProductManual/File?folder=&filename=Manuals/2881/AA LED Bulb 6 说明书(RGBW-AL001)_转曲-2dd.pdf", "ProductPageURL": "", "InclusionHelp": "Add for inclusion 1. Ensure the led bulb has been excluded outside the network. 2. Triggered by OFF ->ON (between 0.5-2 seconds each time) 3. LED solid yellow Color (0xFFFF00) during the pairing(Timeout is 10 seconds).  Failure: Blinks between 100% White and Red 0x0000FF color for 3 seconds (at a rate of 200ms per flash), Once 3 seconds have passed, the LED should return to a Warm White LED at 100%  Success: Blinks between 100% White and Green 0x00FF00 color for 3 seconds (at a rate of 200ms per flash). Once 3 seconds have passed, the LED should return to a Warm White LED at 100%.", "ExclusionHelp": "Remove for exclusion 1. Assuming led bulb was added to controller. 2. Triggered by OFF -> ON -> OFF -> ON -> OFF -> ON (between 0.5-2 seconds each time). 3. LED Solid Purple/Violet Color (0xEE82EE) during the unpairing process. (Timeout is 10 seconds).  Failure: Blinks between 100% White and Red 0x0000FF color for 3 seconds (at a rate of 200ms per flash), Once 3 seconds have passed, the LED should return to the last color ( memory status(color cc set)) of LED Bulb.  Success: Blinks between 100% White and Blue 0x0000FF color for 3 seconds (at a rate of 200ms per flash). Once 3 seconds have passed, the LED should return to a Warm White LED at 100%.", "ResetHelp": "Reset the Device. 1. Assuming led bulb was added to controller and was power on. 2. RGBW bulb re-power 6 times (between 0.5-2 seconds each time). Note: ON -> OFF -> ON -> OFF -> ON -> OFF -> ON -> OFF -> ON -> OFF -> ON -> OFF -> ON 3. If the 6th power on, the led bulb change to Yellow color(into pairing process ), which means that the reset factory settings are successf. Using this action in case of the primary controller is missing or inoperable.", "WakeupHelp": "", "ProductSupportURL": "", "Frequency": "", "Name": "LED Bulb 6:Multi-Colour", "ProductPicBase64": "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" }, "Event": "nodeNaming", "TimeStamp": 1579566891, "NodeManufacturerName": "Aeotec Limited", "NodeProductName": "ZWA002 LED Bulb 6 Multi-Color", "NodeBasicString": "Routing Slave", "NodeBasic": 4, "NodeGenericString": "Multilevel Switch", "NodeGeneric": 17, "NodeSpecificString": "Multilevel Power Switch", "NodeSpecific": 1, "NodeManufacturerID": "0x0371", "NodeProductType": "0x0103", "NodeProductID": "0x0002", "NodeBaudRate": 100000, "NodeVersion": 4, "NodeGroups": 1} -OpenZWave/1/node/39/instance/1/,{ "Instance": 1, "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/38/,{ "Instance": 1, "CommandClassId": 38, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/38/value/659128337/,{ "Label": "Level", "Value": 0, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 0, "Node": 39, "Genre": "User", "Help": "The Current Level of the Device", "ValueIDKey": 659128337, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/38/value/281475635839000/,{ "Label": "Bright", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 1, "Node": 39, "Genre": "User", "Help": "Increase the Brightness of the Device", "ValueIDKey": 281475635839000, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/38/value/562950612549656/,{ "Label": "Dim", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 2, "Node": 39, "Genre": "User", "Help": "Decrease the Brightness of the Device", "ValueIDKey": 562950612549656, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/38/value/844425597648912/,{ "Label": "Ignore Start Level", "Value": true, "Units": "", "Min": 0, "Max": 0, "Type": "Bool", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 3, "Node": 39, "Genre": "System", "Help": "Ignore the Start Level of the Device when increasing/decreasing brightness", "ValueIDKey": 844425597648912, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/38/value/1125900574359569/,{ "Label": "Start Level", "Value": 0, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 4, "Node": 39, "Genre": "System", "Help": "Start Level when Changing the Brightness of a Device", "ValueIDKey": 1125900574359569, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/39/,{ "Instance": 1, "CommandClassId": 39, "CommandClass": "COMMAND_CLASS_SWITCH_ALL", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/39/value/667533332/,{ "Label": "Switch All", "Value": { "List": [ { "Value": 0, "Label": "Disabled" }, { "Value": 1, "Label": "Off Enabled" }, { "Value": 2, "Label": "On Enabled" }, { "Value": 255, "Label": "On and Off Enabled" } ], "Selected": "On and Off Enabled" }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_ALL", "Index": 0, "Node": 39, "Genre": "System", "Help": "Switch All Devices On/Off", "ValueIDKey": 667533332, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/51/,{ "Instance": 1, "CommandClassId": 51, "CommandClass": "COMMAND_CLASS_COLOR", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/51/value/562950621151251/,{ "Label": "Color Channels", "Value": 3, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_COLOR", "Index": 2, "Node": 39, "Genre": "System", "Help": "Color Capabilities of the device", "ValueIDKey": 562950621151251, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/51/value/659341335/,{ "Label": "Color", "Value": "#0000000000", "Units": "#RRGGBBWWCW", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_COLOR", "Index": 0, "Node": 39, "Genre": "User", "Help": "Color (in RGB format)", "ValueIDKey": 659341335, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/51/value/281475636051988/,{ "Label": "Color Index", "Value": { "List": [ { "Value": 0, "Label": "Off" }, { "Value": 1, "Label": "Cool White" }, { "Value": 2, "Label": "Warm White" }, { "Value": 3, "Label": "Red" }, { "Value": 4, "Label": "Lime" }, { "Value": 5, "Label": "Blue" }, { "Value": 6, "Label": "Yellow" }, { "Value": 7, "Label": "Cyan" }, { "Value": 8, "Label": "Magenta" }, { "Value": 9, "Label": "Silver" }, { "Value": 10, "Label": "Gray" }, { "Value": 11, "Label": "Maroon" }, { "Value": 12, "Label": "Olive" }, { "Value": 13, "Label": "Green" }, { "Value": 14, "Label": "Purple" }, { "Value": 15, "Label": "Teal" }, { "Value": 16, "Label": "Navy" }, { "Value": 17, "Label": "Custom" } ], "Selected": "Warm White" }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_COLOR", "Index": 1, "Node": 39, "Genre": "User", "Help": "Preset Color", "ValueIDKey": 281475636051988, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/94/,{ "Instance": 1, "CommandClassId": 94, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/94/value/668434449/,{ "Label": "ZWave+ Version", "Value": 1, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 0, "Node": 39, "Genre": "System", "Help": "ZWave+ Version Supported on the Device", "ValueIDKey": 668434449, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/94/value/281475645145110/,{ "Label": "InstallerIcon", "Value": 1536, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 1, "Node": 39, "Genre": "System", "Help": "Icon File to use for the Installer Application", "ValueIDKey": 281475645145110, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/94/value/562950621855766/,{ "Label": "UserIcon", "Value": 1536, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 2, "Node": 39, "Genre": "System", "Help": "Icon File to use for the User Application", "ValueIDKey": 562950621855766, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/112/,{ "Instance": 1, "CommandClassId": 112, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/112/value/281475641245716/,{ "Label": "User custom mode LED animations", "Value": { "List": [ { "Value": 0, "Label": "Disable" }, { "Value": 1, "Label": "Blink Colors in order mode" }, { "Value": 2, "Label": "Randomized blink color mode" } ], "Selected": "Disable" }, "Units": "", "Min": 0, "Max": 2, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 1, "Node": 39, "Genre": "Config", "Help": "User custom mode for LED animations", "ValueIDKey": 281475641245716, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/112/value/562950617956372/,{ "Label": "Strobe over Custom Color", "Value": { "List": [ { "Value": 0, "Label": "Disable" }, { "Value": 1, "Label": "Enable" } ], "Selected": "Disable" }, "Units": "", "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 2, "Node": 39, "Genre": "Config", "Help": "Enable/Disable Strobe over Custom Color.", "ValueIDKey": 562950617956372, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/112/value/844425594667027/,{ "Label": "Set the rate of change to next color in Custom Mode", "Value": 50, "Units": "ms", "Min": 5, "Max": 8640000, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 3, "Node": 39, "Genre": "Config", "Help": "Set the rate of change to next color in Custom Mode.", "ValueIDKey": 844425594667027, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/112/value/1125900571377681/,{ "Label": "Set color that LED Bulb blinks", "Value": 1, "Units": "", "Min": 1, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 4, "Node": 39, "Genre": "Config", "Help": "Set color that LED Bulb blinks in Blink Mode.", "ValueIDKey": 1125900571377681, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/112/value/4503600291905553/,{ "Label": "Ramp rate when dimming using Multilevel Switch", "Value": 20, "Units": "100ms", "Min": 0, "Max": 100, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 16, "Node": 39, "Genre": "Config", "Help": "Specifying the ramp rate when dimming using Multilevel Switch V1 CC in 100ms.", "ValueIDKey": 4503600291905553, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/112/value/22517998801387540/,{ "Label": "Notification", "Value": { "List": [ { "Value": 0, "Label": "Nothing" }, { "Value": 1, "Label": "Basic CC report" } ], "Selected": "Basic CC report" }, "Units": "", "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 80, "Node": 39, "Genre": "Config", "Help": "Enable to send notifications to associated devices (Group 1) when the state of LED Bulb is changed.", "ValueIDKey": 22517998801387540, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/112/value/22799473778098198/,{ "Label": "Warm White temperature", "Value": 2700, "Units": "k", "Min": 2700, "Max": 4999, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 81, "Node": 39, "Genre": "Config", "Help": "Adjusting the color temperature in warm white color component. available value: 2700k to 4999k", "ValueIDKey": 22799473778098198, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/112/value/23080948754808854/,{ "Label": "cold white temperature", "Value": 6500, "Units": "k", "Min": 5000, "Max": 6500, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 82, "Node": 39, "Genre": "Config", "Help": "Adjusting the color temperature in cold white color component. available value:5000k to 6500k", "ValueIDKey": 23080948754808854, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/114/,{ "Instance": 1, "CommandClassId": 114, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/114/value/668762131/,{ "Label": "Loaded Config Revision", "Value": 3, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 0, "Node": 39, "Genre": "System", "Help": "Revision of the Config file currently loaded", "ValueIDKey": 668762131, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/114/value/281475645472787/,{ "Label": "Config File Revision", "Value": 3, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 1, "Node": 39, "Genre": "System", "Help": "Revision of the Config file on the File System", "ValueIDKey": 281475645472787, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/114/value/562950622183443/,{ "Label": "Latest Available Config File Revision", "Value": 3, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 2, "Node": 39, "Genre": "System", "Help": "Latest Revision of the Config file available for download", "ValueIDKey": 562950622183443, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/114/value/844425598894103/,{ "Label": "Device ID", "Value": "", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 3, "Node": 39, "Genre": "System", "Help": "Manufacturer Specific Device ID/Model", "ValueIDKey": 844425598894103, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/114/value/1125900575604759/,{ "Label": "Serial Number", "Value": "00001cd6bda18c83", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 4, "Node": 39, "Genre": "System", "Help": "Device Serial Number", "ValueIDKey": 1125900575604759, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/,{ "Instance": 1, "CommandClassId": 115, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/668778516/,{ "Label": "Powerlevel", "Value": { "List": [ { "Value": 0, "Label": "Normal" }, { "Value": 1, "Label": "-1dB" }, { "Value": 2, "Label": "-2dB" }, { "Value": 3, "Label": "-3dB" }, { "Value": 4, "Label": "-4dB" }, { "Value": 5, "Label": "-5dB" }, { "Value": 6, "Label": "-6dB" }, { "Value": 7, "Label": "-7dB" }, { "Value": 8, "Label": "-8dB" }, { "Value": 9, "Label": "-9dB" } ], "Selected": "Normal" }, "Units": "dB", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 0, "Node": 39, "Genre": "System", "Help": "Output RF PowerLevel", "ValueIDKey": 668778516, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/281475645489169/,{ "Label": "Timeout", "Value": 0, "Units": "seconds", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 1, "Node": 39, "Genre": "System", "Help": "Timeout till the PowerLevel is reset to Normal", "ValueIDKey": 281475645489169, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/562950622199832/,{ "Label": "Set Powerlevel", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 2, "Node": 39, "Genre": "System", "Help": "Apply the Output PowerLevel and Timeout Values", "ValueIDKey": 562950622199832, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/844425598910481/,{ "Label": "Test Node", "Value": 0, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 3, "Node": 39, "Genre": "System", "Help": "Node to Perform a test against", "ValueIDKey": 844425598910481, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/1125900575621140/,{ "Label": "Test Powerlevel", "Value": { "List": [ { "Value": 0, "Label": "Normal" }, { "Value": 1, "Label": "-1dB" }, { "Value": 2, "Label": "-2dB" }, { "Value": 3, "Label": "-3dB" }, { "Value": 4, "Label": "-4dB" }, { "Value": 5, "Label": "-5dB" }, { "Value": 6, "Label": "-6dB" }, { "Value": 7, "Label": "-7dB" }, { "Value": 8, "Label": "-8dB" }, { "Value": 9, "Label": "-9dB" } ], "Selected": "Normal" }, "Units": "dB", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 4, "Node": 39, "Genre": "System", "Help": "PowerLevel to use for the Test", "ValueIDKey": 1125900575621140, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/1407375552331798/,{ "Label": "Frame Count", "Value": 0, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 5, "Node": 39, "Genre": "System", "Help": "How Many Messages to send to the Note for the Test", "ValueIDKey": 1407375552331798, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/1688850529042456/,{ "Label": "Test", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 6, "Node": 39, "Genre": "System", "Help": "Perform a PowerLevel Test against the a Node", "ValueIDKey": 1688850529042456, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/1970325505753112/,{ "Label": "Report", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 7, "Node": 39, "Genre": "System", "Help": "Get the results of the latest PowerLevel Test against a Node", "ValueIDKey": 1970325505753112, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/2251800482463764/,{ "Label": "Test Status", "Value": { "List": [ { "Value": 0, "Label": "Failed" }, { "Value": 1, "Label": "Success" }, { "Value": 2, "Label": "In Progress" } ], "Selected": "Failed" }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 8, "Node": 39, "Genre": "System", "Help": "The Current Status of the last PowerNode Test Executed", "ValueIDKey": 2251800482463764, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/2533275459174422/,{ "Label": "Acked Frames", "Value": 0, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 9, "Node": 39, "Genre": "System", "Help": "Number of Messages successfully Acked by the Target Node", "ValueIDKey": 2533275459174422, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/134/,{ "Instance": 1, "CommandClassId": 134, "CommandClass": "COMMAND_CLASS_VERSION", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/134/value/669089815/,{ "Label": "Library Version", "Value": "3", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 0, "Node": 39, "Genre": "System", "Help": "Z-Wave Library Version", "ValueIDKey": 669089815, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/134/value/281475645800471/,{ "Label": "Protocol Version", "Value": "4.38", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 1, "Node": 39, "Genre": "System", "Help": "Z-Wave Protocol Version", "ValueIDKey": 281475645800471, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/134/value/562950622511127/,{ "Label": "Application Version", "Value": "2.00", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 2, "Node": 39, "Genre": "System", "Help": "Application Version", "ValueIDKey": 562950622511127, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/association/1/,{ "Name": "Lifeline", "Help": "", "MaxAssociations": 1, "Members": [ "1.0" ], "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/43/,{ "Instance": 1, "CommandClassId": 43, "CommandClass": "COMMAND_CLASS_SCENE_ACTIVATION", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/43/value/562950622511127/,{ "Label": "Scene", "Value": 0, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_SCENE_ACTIVATION", "Index": 0, "Node": 7, "Genre": "User", "Help": "", "ValueIDKey": 122339347, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueChanged", "TimeStamp": 1579630367} -OpenZWave/1/node/39/instance/1/commandclass/91/,{ "Instance": 1, "CommandClassId": 91, "CommandClass": "COMMAND_CLASS_CENTRAL_SCENE", "TimeStamp": 1579630630} -OpenZWave/1/node/39/instance/1/commandclass/91/value/281476005806100/,{ "Label": "Scene 1", "Value": { "List": [ { "Value": 0, "Label": "Inactive" }, { "Value": 1, "Label": "Pressed 1 Time" }, { "Value": 2, "Label": "Key Released" }, { "Value": 3, "Label": "Key Held down" } ], "Selected": "Inactive", "Selected_id": 0 }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CENTRAL_SCENE", "Index": 1, "Node": 61, "Genre": "User", "Help": "", "ValueIDKey": 281476005806100, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueChanged", "TimeStamp": 1579640710} \ No newline at end of file diff --git a/tests/components/ozw/fixtures/lock.json b/tests/components/ozw/fixtures/lock.json deleted file mode 100644 index 1ec2187abcb..00000000000 --- a/tests/components/ozw/fixtures/lock.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "topic": "OpenZWave/1/node/10/instance/1/commandclass/98/value/173572112/", - "payload": { - "Label": "Lock", - "Value": false, - "Units": "", - "Min": 0, - "Max": 0, - "Type": "Bool", - "Instance": 1, - "CommandClass": "COMMAND_CLASS_DOOR_LOCK", - "Index": 0, - "Node": 10, - "Genre": "User", - "Help": "Lock / Unlock Device", - "ValueIDKey": 173572112, - "ReadOnly": false, - "WriteOnly": false, - "ValueSet": false, - "ValuePolled": false, - "ChangeVerified": false, - "Event": "valueAdded", - "TimeStamp": 1579566891 - } -} diff --git a/tests/components/ozw/fixtures/lock_network_dump.csv b/tests/components/ozw/fixtures/lock_network_dump.csv deleted file mode 100644 index fdb4ce7353e..00000000000 --- a/tests/components/ozw/fixtures/lock_network_dump.csv +++ /dev/null @@ -1,79 +0,0 @@ -OpenZWave/1/status/,{ "OpenZWave_Version": "1.6.1131", "OZWDaemon_Version": "0.1.101", "QTOpenZWave_Version": "1.0.0", "QT_Version": "5.12.5", "Status": "driverAllNodesQueriedSomeDead", "TimeStamp": 1590178891, "ManufacturerSpecificDBReady": true, "homeID": 4075923038, "getControllerNodeId": 1, "getSUCNodeId": 0, "isPrimaryController": false, "isBridgeController": false, "hasExtendedTXStatistics": true, "getControllerLibraryVersion": "Z-Wave 4.05", "getControllerLibraryType": "Static Controller", "getControllerPath": "/dev/zwave"} -OpenZWave/1/node/10/,{ "NodeID": 10, "NodeQueryStage": "Complete", "isListening": false, "isFlirs": true, "isBeaming": true, "isRouting": true, "isSecurityv1": false, "isZWavePlus": true, "isNIFRecieved": true, "isAwake": true, "isFailed": false, "MetaData": { "OZWInfoURL": "", "ZWAProductURL": "", "ProductPic": "", "Description": "", "ProductManualURL": "", "ProductPageURL": "", "InclusionHelp": "", "ExclusionHelp": "", "ResetHelp": "", "WakeupHelp": "", "ProductSupportURL": "", "Frequency": "", "Name": "", "ProductPicBase64": "" }, "Event": "nodeQueriesComplete", "TimeStamp": 1590178891, "NodeManufacturerName": "Poly-control", "NodeProductName": "Danalock V3 BTZE", "NodeBasicString": "Routing Slave", "NodeBasic": 4, "NodeGenericString": "Entry Control", "NodeGeneric": 64, "NodeSpecificString": "Secure Keypad Door Lock", "NodeSpecific": 3, "NodeManufacturerID": "0x010e", "NodeProductType": "0x0009", "NodeProductID": "0x0001", "NodeBaudRate": 100000, "NodeVersion": 4, "NodeName": "", "NodeLocation": "", "NodeGroups": 1, "NodeDeviceTypeString": "Door Lock Keypad", "NodeDeviceType": 768, "NodeRole": 7, "NodeRoleString": "Listening Sleeping Slave", "NodePlusType": 0, "NodePlusTypeString": "Z-Wave+ node", "Neighbors": [ 1, 5, 9 ]} -OpenZWave/1/node/10/instance/1/,{ "Instance": 1, "TimeStamp": 1590178857} -OpenZWave/1/node/10/instance/1/commandclass/112/,{ "Instance": 1, "CommandClassId": 112, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "CommandClassVersion": 1, "TimeStamp": 1590178855} -OpenZWave/1/node/10/instance/1/commandclass/112/value/281475154706452/,{ "Label": "Twist Assist", "Value": { "List": [ { "Value": 0, "Label": "Disabled" }, { "Value": 1, "Label": "Enabled" } ], "Selected": "Disabled", "Selected_id": 0 }, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 1, "Node": 10, "Genre": "Config", "Help": "", "ValueIDKey": 281475154706452, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178855} -OpenZWave/1/node/10/instance/1/commandclass/112/value/562950131417107/,{ "Label": "Hold and Release", "Value": 0, "Units": "seconds", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 2, "Node": 10, "Genre": "Config", "Help": "0 Disable. 1 to 2147483647 Enable, time in seconds.", "ValueIDKey": 562950131417107, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178855} -OpenZWave/1/node/10/instance/1/commandclass/112/value/844425108127764/,{ "Label": "Block to Block", "Value": { "List": [ { "Value": 0, "Label": "Disable" }, { "Value": 1, "Label": "Enable" } ], "Selected": "Disable", "Selected_id": 0 }, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 3, "Node": 10, "Genre": "Config", "Help": "", "ValueIDKey": 844425108127764, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178855} -OpenZWave/1/node/10/instance/1/commandclass/112/value/1125900084838419/,{ "Label": "BLE Temporary Allowed", "Value": 0, "Units": "seconds", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 4, "Node": 10, "Genre": "Config", "Help": "0 Disable. 1 to 2147483647 Enable, time in seconds.", "ValueIDKey": 1125900084838419, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178855} -OpenZWave/1/node/10/instance/1/commandclass/112/value/1407375061549076/,{ "Label": "BLE Always Allowed", "Value": { "List": [ { "Value": 0, "Label": "Disable" }, { "Value": 1, "Label": "Enable" } ], "Selected": "Disable", "Selected_id": 0 }, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 5, "Node": 10, "Genre": "Config", "Help": "", "ValueIDKey": 1407375061549076, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178855} -OpenZWave/1/node/10/instance/1/commandclass/112/value/1688850038259731/,{ "Label": "Autolock", "Value": 0, "Units": "seconds", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 6, "Node": 10, "Genre": "Config", "Help": "0 Disable. 1 to 2147483647 Enable, time in seconds.", "ValueIDKey": 1688850038259731, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/94/,{ "Instance": 1, "CommandClassId": 94, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "CommandClassVersion": 1, "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/94/value/181895185/,{ "Label": "ZWave+ Version", "Value": 1, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 0, "Node": 10, "Genre": "System", "Help": "ZWave+ Version Supported on the Device", "ValueIDKey": 181895185, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/94/value/281475158605846/,{ "Label": "InstallerIcon", "Value": 768, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 1, "Node": 10, "Genre": "System", "Help": "Icon File to use for the Installer Application", "ValueIDKey": 281475158605846, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/94/value/562950135316502/,{ "Label": "UserIcon", "Value": 768, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 2, "Node": 10, "Genre": "System", "Help": "Icon File to use for the User Application", "ValueIDKey": 562950135316502, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/98/,{ "Instance": 1, "CommandClassId": 98, "CommandClass": "COMMAND_CLASS_DOOR_LOCK", "CommandClassVersion": 1, "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/98/value/173572112/,{ "Label": "Locked", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Bool", "Instance": 1, "CommandClass": "COMMAND_CLASS_DOOR_LOCK", "Index": 0, "Node": 10, "Genre": "User", "Help": "State of the Lock", "ValueIDKey": 173572112, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1590345913} -OpenZWave/1/node/10/instance/1/commandclass/98/value/281475150282772/,{ "Label": "Locked (Advanced)", "Value": { "List": [ { "Value": 0, "Label": "Unsecure" }, { "Value": 1, "Label": "Unsecured with Timeout" }, { "Value": 2, "Label": "Inside Handle Unsecured" }, { "Value": 3, "Label": "Inside Handle Unsecured with Timeout" }, { "Value": 4, "Label": "Outside Handle Unsecured" }, { "Value": 5, "Label": "Outside Handle Unsecured with Timeout" }, { "Value": 6, "Label": "Secured" }, { "Value": 255, "Label": "Invalid" } ], "Selected": "Unsecure", "Selected_id": 0 }, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_DOOR_LOCK", "Index": 1, "Node": 10, "Genre": "User", "Help": "State of the Lock (Advanced)", "ValueIDKey": 281475150282772, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1590345913} -OpenZWave/1/node/10/instance/1/commandclass/98/value/562950135382036/,{ "Label": "Timeout Mode", "Value": { "List": [ { "Value": 1, "Label": "No Timeout" }, { "Value": 2, "Label": "Secure Lock after Timeout" } ], "Selected": "No Timeout", "Selected_id": 1 }, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_DOOR_LOCK", "Index": 2, "Node": 10, "Genre": "System", "Help": "Timeout Mode for Reverting Lock State", "ValueIDKey": 562950135382036, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/98/value/1407375065514001/,{ "Label": "Outside Handle Control", "Value": 1, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_DOOR_LOCK", "Index": 5, "Node": 10, "Genre": "System", "Help": "State of the Exterior Handle Control", "ValueIDKey": 1407375065514001, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/98/value/1688850042224657/,{ "Label": "Inside Handle Control", "Value": 1, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_DOOR_LOCK", "Index": 6, "Node": 10, "Genre": "System", "Help": "State of the Interior Handle Control", "ValueIDKey": 1688850042224657, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/99/,{ "Instance": 1, "CommandClassId": 99, "CommandClass": "COMMAND_CLASS_USER_CODE", "CommandClassVersion": 1, "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/99/value/72339069196615702/,{ "Label": "Code Count", "Value": 20, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_USER_CODE", "Index": 257, "Node": 10, "Genre": "System", "Help": "Number of User Codes supported by the Device", "ValueIDKey": 72339069196615702, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/99/value/71776119243194392/,{ "Label": "Refresh All UserCodes", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_USER_CODE", "Index": 255, "Node": 10, "Genre": "System", "Help": "Refresh All UserCodes Stored on Device", "ValueIDKey": 71776119243194392, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/99/value/72057594219905046/,{ "Label": "Remove User Code", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_USER_CODE", "Index": 256, "Node": 10, "Genre": "System", "Help": "Remove A UserCode at the Specified Index", "ValueIDKey": 72057594219905046, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/99/value/173588503/,{ "Label": "Enrollment Code", "Value": "", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_USER_CODE", "Index": 0, "Node": 10, "Genre": "User", "Help": "Enrollment Code", "ValueIDKey": 173588503, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/99/value/281475150299159/,{ "Label": "Code 1:", "Value": "", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_USER_CODE", "Index": 1, "Node": 10, "Genre": "User", "Help": "UserCode 1", "ValueIDKey": 281475150299159, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/99/value/562950127009815/,{ "Label": "Code 2:", "Value": "", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_USER_CODE", "Index": 2, "Node": 10, "Genre": "User", "Help": "UserCode 2", "ValueIDKey": 562950127009815, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/99/value/844425103720471/,{ "Label": "Code 3:", "Value": "", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_USER_CODE", "Index": 3, "Node": 10, "Genre": "User", "Help": "UserCode 3", "ValueIDKey": 844425103720471, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/99/value/1125900080431127/,{ "Label": "Code 4:", "Value": "", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_USER_CODE", "Index": 4, "Node": 10, "Genre": "User", "Help": "UserCode 4", "ValueIDKey": 1125900080431127, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/99/value/1407375057141783/,{ "Label": "Code 5:", "Value": "", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_USER_CODE", "Index": 5, "Node": 10, "Genre": "User", "Help": "UserCode 5", "ValueIDKey": 1407375057141783, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/99/value/1688850033852439/,{ "Label": "Code 6:", "Value": "", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_USER_CODE", "Index": 6, "Node": 10, "Genre": "User", "Help": "UserCode 6", "ValueIDKey": 1688850033852439, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/99/value/1970325010563095/,{ "Label": "Code 7:", "Value": "", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_USER_CODE", "Index": 7, "Node": 10, "Genre": "User", "Help": "UserCode 7", "ValueIDKey": 1970325010563095, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/99/value/2251799987273751/,{ "Label": "Code 8:", "Value": "", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_USER_CODE", "Index": 8, "Node": 10, "Genre": "User", "Help": "UserCode 8", "ValueIDKey": 2251799987273751, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/99/value/2533274963984407/,{ "Label": "Code 9:", "Value": "", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_USER_CODE", "Index": 9, "Node": 10, "Genre": "User", "Help": "UserCode 9", "ValueIDKey": 2533274963984407, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/99/value/2814749940695063/,{ "Label": "Code 10:", "Value": "", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_USER_CODE", "Index": 10, "Node": 10, "Genre": "User", "Help": "UserCode 10", "ValueIDKey": 2814749940695063, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/99/value/3096224917405719/,{ "Label": "Code 11:", "Value": "", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_USER_CODE", "Index": 11, "Node": 10, "Genre": "User", "Help": "UserCode 11", "ValueIDKey": 3096224917405719, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/99/value/3377699894116375/,{ "Label": "Code 12:", "Value": "", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_USER_CODE", "Index": 12, "Node": 10, "Genre": "User", "Help": "UserCode 12", "ValueIDKey": 3377699894116375, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/99/value/3659174870827031/,{ "Label": "Code 13:", "Value": "", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_USER_CODE", "Index": 13, "Node": 10, "Genre": "User", "Help": "UserCode 13", "ValueIDKey": 3659174870827031, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/99/value/3940649847537687/,{ "Label": "Code 14:", "Value": "", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_USER_CODE", "Index": 14, "Node": 10, "Genre": "User", "Help": "UserCode 14", "ValueIDKey": 3940649847537687, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/99/value/4222124824248343/,{ "Label": "Code 15:", "Value": "", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_USER_CODE", "Index": 15, "Node": 10, "Genre": "User", "Help": "UserCode 15", "ValueIDKey": 4222124824248343, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/99/value/4503599800958999/,{ "Label": "Code 16:", "Value": "", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_USER_CODE", "Index": 16, "Node": 10, "Genre": "User", "Help": "UserCode 16", "ValueIDKey": 4503599800958999, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/99/value/4785074777669655/,{ "Label": "Code 17:", "Value": "", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_USER_CODE", "Index": 17, "Node": 10, "Genre": "User", "Help": "UserCode 17", "ValueIDKey": 4785074777669655, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/99/value/5066549754380311/,{ "Label": "Code 18:", "Value": "", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_USER_CODE", "Index": 18, "Node": 10, "Genre": "User", "Help": "UserCode 18", "ValueIDKey": 5066549754380311, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/99/value/5348024731090967/,{ "Label": "Code 19:", "Value": "", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_USER_CODE", "Index": 19, "Node": 10, "Genre": "User", "Help": "UserCode 19", "ValueIDKey": 5348024731090967, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/99/value/5629499707801623/,{ "Label": "Code 20:", "Value": "", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_USER_CODE", "Index": 20, "Node": 10, "Genre": "User", "Help": "UserCode 20", "ValueIDKey": 5629499707801623, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/114/,{ "Instance": 1, "CommandClassId": 114, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "CommandClassVersion": 2, "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/114/value/182222867/,{ "Label": "Loaded Config Revision", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 0, "Node": 10, "Genre": "System", "Help": "Revision of the Config file currently loaded", "ValueIDKey": 182222867, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/114/value/281475158933523/,{ "Label": "Config File Revision", "Value": 15, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 1, "Node": 10, "Genre": "System", "Help": "Revision of the Config file on the File System", "ValueIDKey": 281475158933523, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/114/value/562950135644179/,{ "Label": "Latest Available Config File Revision", "Value": 15, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 2, "Node": 10, "Genre": "System", "Help": "Latest Revision of the Config file available for download", "ValueIDKey": 562950135644179, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/114/value/844425112354839/,{ "Label": "Device ID", "Value": "", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 3, "Node": 10, "Genre": "System", "Help": "Manufacturer Specific Device ID/Model", "ValueIDKey": 844425112354839, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/114/value/1125900089065495/,{ "Label": "Serial Number", "Value": "3b548b972bf8", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 4, "Node": 10, "Genre": "System", "Help": "Device Serial Number", "ValueIDKey": 1125900089065495, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1590178857} -OpenZWave/1/node/10/instance/1/commandclass/115/,{ "Instance": 1, "CommandClassId": 115, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "CommandClassVersion": 1, "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/115/value/182239252/,{ "Label": "Powerlevel", "Value": { "List": [ { "Value": 0, "Label": "Normal" }, { "Value": 1, "Label": "-1dB" }, { "Value": 2, "Label": "-2dB" }, { "Value": 3, "Label": "-3dB" }, { "Value": 4, "Label": "-4dB" }, { "Value": 5, "Label": "-5dB" }, { "Value": 6, "Label": "-6dB" }, { "Value": 7, "Label": "-7dB" }, { "Value": 8, "Label": "-8dB" }, { "Value": 9, "Label": "-9dB" } ], "Selected": "Normal", "Selected_id": 0 }, "Units": "dB", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 0, "Node": 10, "Genre": "System", "Help": "Output RF PowerLevel", "ValueIDKey": 182239252, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/115/value/281475158949905/,{ "Label": "Timeout", "Value": 0, "Units": "seconds", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 1, "Node": 10, "Genre": "System", "Help": "Timeout till the PowerLevel is reset to Normal", "ValueIDKey": 281475158949905, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/115/value/562950135660568/,{ "Label": "Set Powerlevel", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 2, "Node": 10, "Genre": "System", "Help": "Apply the Output PowerLevel and Timeout Values", "ValueIDKey": 562950135660568, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/115/value/844425112371217/,{ "Label": "Test Node", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 3, "Node": 10, "Genre": "System", "Help": "Node to Perform a test against", "ValueIDKey": 844425112371217, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/115/value/1125900089081876/,{ "Label": "Test Powerlevel", "Value": { "List": [ { "Value": 0, "Label": "Normal" }, { "Value": 1, "Label": "-1dB" }, { "Value": 2, "Label": "-2dB" }, { "Value": 3, "Label": "-3dB" }, { "Value": 4, "Label": "-4dB" }, { "Value": 5, "Label": "-5dB" }, { "Value": 6, "Label": "-6dB" }, { "Value": 7, "Label": "-7dB" }, { "Value": 8, "Label": "-8dB" }, { "Value": 9, "Label": "-9dB" } ], "Selected": "Normal", "Selected_id": 0 }, "Units": "dB", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 4, "Node": 10, "Genre": "System", "Help": "PowerLevel to use for the Test", "ValueIDKey": 1125900089081876, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/115/value/1407375065792534/,{ "Label": "Frame Count", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 5, "Node": 10, "Genre": "System", "Help": "How Many Messages to send to the Node for the Test", "ValueIDKey": 1407375065792534, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/115/value/1688850042503192/,{ "Label": "Test", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 6, "Node": 10, "Genre": "System", "Help": "Perform a PowerLevel Test against the a Node", "ValueIDKey": 1688850042503192, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/115/value/1970325019213848/,{ "Label": "Report", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 7, "Node": 10, "Genre": "System", "Help": "Get the results of the latest PowerLevel Test against a Node", "ValueIDKey": 1970325019213848, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/115/value/2251799995924500/,{ "Label": "Test Status", "Value": { "List": [ { "Value": 0, "Label": "Failed" }, { "Value": 1, "Label": "Success" }, { "Value": 2, "Label": "In Progress" } ], "Selected": "Failed", "Selected_id": 0 }, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 8, "Node": 10, "Genre": "System", "Help": "The Current Status of the last PowerNode Test Executed", "ValueIDKey": 2251799995924500, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/115/value/2533274972635158/,{ "Label": "Acked Frames", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 9, "Node": 10, "Genre": "System", "Help": "Number of Messages successfully Acked by the Target Node", "ValueIDKey": 2533274972635158, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/128/,{ "Instance": 1, "CommandClassId": 128, "CommandClass": "COMMAND_CLASS_BATTERY", "CommandClassVersion": 1, "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/128/value/174063633/,{ "Label": "Battery Level", "Value": 94, "Units": "%", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_BATTERY", "Index": 0, "Node": 10, "Genre": "User", "Help": "Current Battery Level", "ValueIDKey": 174063633, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1590178891} -OpenZWave/1/node/10/instance/1/commandclass/134/,{ "Instance": 1, "CommandClassId": 134, "CommandClass": "COMMAND_CLASS_VERSION", "CommandClassVersion": 1, "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/134/value/182550551/,{ "Label": "Library Version", "Value": "3", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 0, "Node": 10, "Genre": "System", "Help": "Z-Wave Library Version", "ValueIDKey": 182550551, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1590178857} -OpenZWave/1/node/10/instance/1/commandclass/134/value/281475159261207/,{ "Label": "Protocol Version", "Value": "4.61", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 1, "Node": 10, "Genre": "System", "Help": "Z-Wave Protocol Version", "ValueIDKey": 281475159261207, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1590178857} -OpenZWave/1/node/10/instance/1/commandclass/134/value/562950135971863/,{ "Label": "Application Version", "Value": "1.02", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 2, "Node": 10, "Genre": "System", "Help": "Application Version", "ValueIDKey": 562950135971863, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1590178857} -OpenZWave/1/node/10/instance/1/commandclass/139/,{ "Instance": 1, "CommandClassId": 139, "CommandClass": "COMMAND_CLASS_TIME_PARAMETERS", "CommandClassVersion": 1, "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/139/value/182632471/,{ "Label": "Date", "Value": "22/05/2020", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_TIME_PARAMETERS", "Index": 0, "Node": 10, "Genre": "System", "Help": "Current Date", "ValueIDKey": 182632471, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1590178858} -OpenZWave/1/node/10/instance/1/commandclass/139/value/281475159343127/,{ "Label": "Time", "Value": "20:20:57", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_TIME_PARAMETERS", "Index": 1, "Node": 10, "Genre": "System", "Help": "Current Time", "ValueIDKey": 281475159343127, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1590178858} -OpenZWave/1/node/10/instance/1/commandclass/139/value/562950136053784/,{ "Label": "Set Date/Time", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_TIME_PARAMETERS", "Index": 2, "Node": 10, "Genre": "System", "Help": "Set the Date/Time", "ValueIDKey": 562950136053784, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/139/value/844425112764440/,{ "Label": "Refresh Date/Time", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_TIME_PARAMETERS", "Index": 3, "Node": 10, "Genre": "System", "Help": "Refresh the Date/Time", "ValueIDKey": 844425112764440, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/152/,{ "Instance": 1, "CommandClassId": 152, "CommandClass": "COMMAND_CLASS_SECURITY", "CommandClassVersion": 1, "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/152/value/182845456/,{ "Label": "Secured", "Value": true, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Bool", "Instance": 1, "CommandClass": "COMMAND_CLASS_SECURITY", "Index": 0, "Node": 10, "Genre": "System", "Help": "Is Communication with Device Encrypted", "ValueIDKey": 182845456, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/113/,{ "Instance": 1, "CommandClassId": 113, "CommandClass": "COMMAND_CLASS_NOTIFICATION", "CommandClassVersion": 8, "TimeStamp": 1590178857} -OpenZWave/1/node/10/instance/1/commandclass/113/value/72057594211745809/,{ "Label": "Previous Event Cleared", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_NOTIFICATION", "Index": 256, "Node": 10, "Genre": "User", "Help": "Previous Event that was sent", "ValueIDKey": 72057594211745809, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178857} -OpenZWave/1/node/10/instance/1/commandclass/113/value/1688850034081812/,{ "Label": "Access Control", "Value": { "List": [ { "Value": 0, "Label": "Clear" }, { "Value": 1, "Label": "Manual Lock Operation" }, { "Value": 2, "Label": "Manual Unlock Operation" }, { "Value": 3, "Label": "Wireless Lock Operation" }, { "Value": 4, "Label": "Wireless Unlock Operation" }, { "Value": 9, "Label": "Auto Lock" }, { "Value": 11, "Label": "Lock Jammed" } ], "Selected": "Wireless Unlock Operation", "Selected_id": 4 }, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_NOTIFICATION", "Index": 6, "Node": 10, "Genre": "User", "Help": "Access Control Alerts", "ValueIDKey": 1688850034081812, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1590345911} -OpenZWave/1/node/10/association/1/,{ "Name": "Lifeline", "Help": "", "MaxAssociations": 1, "Members": [ "1.0" ], "TimeStamp": 1590178858} \ No newline at end of file diff --git a/tests/components/ozw/fixtures/migration_fixture.csv b/tests/components/ozw/fixtures/migration_fixture.csv deleted file mode 100644 index 92b68f448f6..00000000000 --- a/tests/components/ozw/fixtures/migration_fixture.csv +++ /dev/null @@ -1,9 +0,0 @@ -OpenZWave/1/status/,{ "OpenZWave_Version": "1.6.1008", "OZWDeamon_Version": "0.1", "QTOpenZWave_Version": "1.0.0", "QT_Version": "5.12.5", "Status": "driverAllNodesQueried", "TimeStamp": 1579566933, "ManufacturerSpecificDBReady": true, "homeID": 3245146787, "getControllerNodeId": 1, "getSUCNodeId": 1, "isPrimaryController": true, "isBridgeController": false, "hasExtendedTXStatistics": true, "getControllerLibraryVersion": "Z-Wave 3.95", "getControllerLibraryType": "Static Controller", "getControllerPath": "/dev/zwave"} -OpenZWave/1/node/32/,{ "NodeID": 32, "NodeQueryStage": "Complete", "isListening": true, "isFlirs": false, "isBeaming": true, "isRouting": true, "isSecurityv1": false, "isZWavePlus": true, "isNIFRecieved": true, "isAwake": true, "isFailed": false, "MetaData": { "OZWInfoURL": "http://www.openzwave.com/device-database/0208:0005:0101", "ZWAProductURL": "", "ProductPic": "images/hank/hkzw-so01-smartplug.png", "Description": "fixture description", "WakeupHelp": "", "ProductSupportURL": "", "Frequency": "", "Name": "Smart Plug", "ProductPicBase64": "iVBORggg==" }, "Event": "nodeQueriesComplete", "TimeStamp": 1579566933, "NodeManufacturerName": "HANK Electronics Ltd", "NodeProductName": "HKZW-SO01 Smart Plug", "NodeBasicString": "Routing Slave", "NodeBasic": 4, "NodeGenericString": "Binary Switch", "NodeGeneric": 16, "NodeSpecificString": "Binary Power Switch", "NodeSpecific": 1, "NodeManufacturerID": "0x0208", "NodeProductType": "0x0101", "NodeProductID": "0x0005", "NodeBaudRate": 100000, "NodeVersion": 4, "NodeGroups": 1, "NodeName": "", "NodeLocation": "", "NodeDeviceTypeString": "On/Off Power Switch", "NodeDeviceType": 1792, "NodeRole": 5, "NodeRoleString": "Always On Slave", "NodePlusType": 0, "NodePlusTypeString": "Z-Wave+ node", "Neighbors": [ 1, 33, 36, 37, 39 ]} -OpenZWave/1/node/32/instance/1/,{ "Instance": 1, "TimeStamp": 1579566891} -OpenZWave/1/node/32/instance/1/commandclass/50/,{ "Instance": 1, "CommandClassId": 50, "CommandClass": "COMMAND_CLASS_METER", "TimeStamp": 1579566891} -OpenZWave/1/node/32/instance/1/commandclass/50/value/562950495305746/,{ "Label": "Electric - W", "Value": 0.0, "Units": "W", "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_METER", "Index": 2, "Node": 32, "Genre": "User", "Help": "", "ValueIDKey": 562950495305746, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/,{ "NodeID": 36, "NodeQueryStage": "CacheLoad", "isListening": false, "isFlirs": false, "isBeaming": true, "isRouting": true, "isSecurityv1": false, "isZWavePlus": false, "isNIFRecieved": true, "isAwake": true, "isFailed": false, "MetaData": { "OZWInfoURL": "http://www.openzwave.com/device-database/0086:007A:0102", "ZWAProductURL": "", "ProductPic": "images/aeotec/zw122.png", "Description": "fixture description", "WakeupHelp": "Pressing the Action Button once will trigger sending the Wake up notification command. If press and hold the Z-Wave button for 3 seconds, the Water Sensor will wake up for 10 minutes.", "ProductSupportURL": "", "Frequency": "", "Name": "Water Sensor 6", "ProductPicBase64": "kSuQmCC" }, "Event": "nodeNaming", "TimeStamp": 1579566891, "NodeManufacturerName": "AEON Labs", "NodeProductName": "ZW122 Water Sensor 6", "NodeBasicString": "Routing Slave", "NodeBasic": 4, "NodeGenericString": "Notification Sensor", "NodeGeneric": 7, "NodeSpecificString": "Notification Sensor", "NodeSpecific": 1, "NodeManufacturerID": "0x0086", "NodeProductType": "0x0102", "NodeProductID": "0x007a", "NodeBaudRate": 100000, "NodeVersion": 4, "NodeGroups": 4} -OpenZWave/1/node/36/instance/1/,{ "Instance": 1, "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/128/,{ "Instance": 1, "CommandClassId": 128, "CommandClass": "COMMAND_CLASS_BATTERY", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/128/value/610271249/,{ "Label": "Battery Level", "Value": 100, "Units": "%", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_BATTERY", "Index": 0, "Node": 36, "Genre": "User", "Help": "Current Battery Level", "ValueIDKey": 610271249, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} diff --git a/tests/components/ozw/fixtures/sensor.json b/tests/components/ozw/fixtures/sensor.json deleted file mode 100644 index 17b86f90809..00000000000 --- a/tests/components/ozw/fixtures/sensor.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "topic": "OpenZWave/1/node/36/instance/1/commandclass/113/value/1407375493578772/", - "payload": { - "Label": "Instance 1: Water", - "Value": { - "List": [ - { - "Value": 0, - "Label": "Clear" - }, - { - "Value": 2, - "Label": "Water Leak at Unknown Location" - } - ], - "Selected": "Clear", - "Selected_id": 0 - }, - "Units": "", - "Min": 0, - "Max": 0, - "Type": "List", - "Instance": 1, - "CommandClass": "COMMAND_CLASS_NOTIFICATION", - "Index": 5, - "Node": 36, - "Genre": "User", - "Help": "Water Alerts", - "ValueIDKey": 1407375493578772, - "ReadOnly": false, - "WriteOnly": false, - "ValueSet": false, - "ValuePolled": false, - "ChangeVerified": false, - "Event": "valueAdded", - "TimeStamp": 1579566891 - } -} \ No newline at end of file diff --git a/tests/components/ozw/fixtures/sensor_string_value_network_dump.csv b/tests/components/ozw/fixtures/sensor_string_value_network_dump.csv deleted file mode 100644 index 071d92da0d0..00000000000 --- a/tests/components/ozw/fixtures/sensor_string_value_network_dump.csv +++ /dev/null @@ -1,5 +0,0 @@ -OpenZWave/1/status/,{ "OpenZWave_Version": "1.6.1240", "OZWDaemon_Version": "0.1.170", "QTOpenZWave_Version": "1.2.0", "QT_Version": "5.12.9", "Status": "driverAllNodesQueried", "TimeStamp": 1598022319, "ManufacturerSpecificDBReady": true, "homeID": 3389163831, "getControllerNodeId": 1, "getSUCNodeId": 1, "isPrimaryController": true, "isBridgeController": false, "hasExtendedTXStatistics": true, "getControllerLibraryVersion": "Z-Wave 3.95", "getControllerLibraryType": "Static Controller", "getControllerPath": "/dev/zwave"} -OpenZWave/1/node/49/,{ "NodeID": 49, "NodeQueryStage": "Complete", "isListening": false, "isFlirs": true, "isBeaming": true, "isRouting": true, "isSecurityv1": false, "isZWavePlus": true, "isNIFRecieved": true, "isAwake": true, "isFailed": false, "MetaData": { "OZWInfoURL": "http://www.openzwave.com/device-database/0373:0001:0003", "ZWAProductURL": "https://products.z-wavealliance.org/products/2780/", "ProductPic": "images/idlock/idlock150.png", "Description": "A module enabling your ID Lock digital door lock to a Z-Wave Plus enabled digital door Lock. The module is compatible with ID Lock 101 and ID Lock 150. It enables your ID Lock to operate in a Z-Wave network with numerous access control funtions and notifications.", "ProductManualURL": "https://idlock.no/wp-content/uploads/2019/08/IDLock150_ZWave_UserManual_v3.02.pdf", "ProductPageURL": "https://idlock.no/z-wave/", "InclusionHelp": "Inclusion – (Puts your device in inclusion mode) • Push and hold key button until all LEDs on touch panel activates (with ID Lock in an unlocked state). • Release button. • Enter Master PIN followed by * on touch panel. • Press digit \"2\" for settings followed by * on touch panel. • Press digit “5” on touch panel. Inclusion mode starts immediately. LED indicator below logo signals this by flashing blue.", "ExclusionHelp": "Exclusion – (Puts your device in exclusion mode) • Push and hold key button until all LEDs on touch panel activates (with ID Lock in an unlocked state). • Release button. • Enter Master PIN followed by * on touch panel. • Press digit \"2\" for settings followed by * on touch panel. • Press digit “5” on touch panel. Exclusion mode starts immediately. LED indicator below logo signals this by flashing blue.", "ResetHelp": "Device reset – (This will reset RF interface module to factory default settings) Warning: Please do only proceed with the following reset procedure, if primary network controller is missing or otherwise inoperable. RESET Z-WAVE MODULE: • Push and hold key button until all LEDs on touch panel activates (with ID Lock in an unlocked state). • Release button. • Enter Master PIN followed by * on keypad. • Press digit \"2\" for settings followed by * on keypad. • Press digit “0” on keypad. If the Z-wave module is not included in a Z-wave network the door lock will also return to factory settings when following the above procedure. FACTORY RESET DOOR LOCK FIRMWARE: • Push and hold inside lock/unlock button while inserting the fourth battery. • Receive reset sound. • Release button. • Receive confirmation sound.", "WakeupHelp": "Activate by touching the touch panel with finger(s), the palm of the hand on the outside unit or by pushing the key button on the inside unit.", "ProductSupportURL": "https://idlock.no/kundesenter/", "Frequency": "CEPT (Europe)", "Name": "ID Lock 150 Z-Wave module", "ProductPicBase64": "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" }, "Event": "nodeQueriesComplete", "TimeStamp": 1598022340, "NodeManufacturerName": "ID Lock AS", "NodeProductName": "ID-150 Z-Wave Module", "NodeBasicString": "Routing Slave", "NodeBasic": 4, "NodeGenericString": "Entry Control", "NodeGeneric": 64, "NodeSpecificString": "Secure Keypad Door Lock", "NodeSpecific": 3, "NodeManufacturerID": "0x0373", "NodeProductType": "0x0003", "NodeProductID": "0x0001", "NodeBaudRate": 100000, "NodeVersion": 4, "NodeGroups": 1, "NodeName": "", "NodeLocation": "", "NodeDeviceTypeString": "Door Lock Keypad", "NodeDeviceType": 768, "NodeRole": 7, "NodeRoleString": "Listening Sleeping Slave", "NodePlusType": 0, "NodePlusTypeString": "Z-Wave+ node", "Neighbors": [ 1, 46, 48, 50 ], "Neighbors": [ 1, 46, 48, 50 ]} -OpenZWave/1/node/49/instance/1/,{ "Instance": 1, "TimeStamp": 1598022021} -OpenZWave/1/node/49/instance/1/commandclass/113/,{ "Instance": 1, "CommandClassId": 113, "CommandClass": "COMMAND_CLASS_NOTIFICATION", "CommandClassVersion": 4, "TimeStamp": 1598022021} -OpenZWave/1/node/49/instance/1/commandclass/113/value/73464969749610519/,{ "Label": "User Code", "Value": "asdfgh", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_NOTIFICATION", "Index": 261, "Node": 49, "Genre": "User", "Help": "User Code that was used", "ValueIDKey": 73464969749610519, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1598022021} \ No newline at end of file diff --git a/tests/components/ozw/fixtures/switch.json b/tests/components/ozw/fixtures/switch.json deleted file mode 100644 index 0d3fc37e9b2..00000000000 --- a/tests/components/ozw/fixtures/switch.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "topic": "OpenZWave/1/node/32/instance/1/commandclass/37/value/541671440/", - "payload": { - "Label": "Switch", - "Value": false, - "Units": "", - "Min": 0, - "Max": 0, - "Type": "Bool", - "Instance": 1, - "CommandClass": "COMMAND_CLASS_SWITCH_BINARY", - "Index": 0, - "Node": 32, - "Genre": "User", - "Help": "Turn On/Off Device", - "ValueIDKey": 541671440, - "ReadOnly": false, - "WriteOnly": false, - "ValueSet": false, - "ValuePolled": false, - "ChangeVerified": false, - "Event": "valueAdded", - "TimeStamp": 1579566891 - } -} diff --git a/tests/components/ozw/test_binary_sensor.py b/tests/components/ozw/test_binary_sensor.py deleted file mode 100644 index d0852a5caf0..00000000000 --- a/tests/components/ozw/test_binary_sensor.py +++ /dev/null @@ -1,67 +0,0 @@ -"""Test Z-Wave Sensors.""" -from homeassistant.components.binary_sensor import ( - DOMAIN as BINARY_SENSOR_DOMAIN, - BinarySensorDeviceClass, -) -from homeassistant.components.ozw.const import DOMAIN -from homeassistant.const import ATTR_DEVICE_CLASS -from homeassistant.helpers import entity_registry as er - -from .common import setup_ozw - - -async def test_binary_sensor(hass, generic_data, binary_sensor_msg): - """Test setting up config entry.""" - receive_msg = await setup_ozw(hass, fixture=generic_data) - - # Test Legacy sensor (disabled by default) - registry = er.async_get(hass) - entity_id = "binary_sensor.trisensor_sensor" - state = hass.states.get(entity_id) - assert state is None - entry = registry.async_get(entity_id) - assert entry - assert entry.disabled - assert entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION - - # Test enabling legacy entity - updated_entry = registry.async_update_entity( - entry.entity_id, **{"disabled_by": None} - ) - assert updated_entry != entry - assert updated_entry.disabled is False - - # Test Sensor for Notification CC - state = hass.states.get("binary_sensor.trisensor_home_security_motion_detected") - assert state - assert state.state == "off" - assert state.attributes[ATTR_DEVICE_CLASS] == BinarySensorDeviceClass.MOTION - - # Test incoming state change - receive_msg(binary_sensor_msg) - await hass.async_block_till_done() - state = hass.states.get("binary_sensor.trisensor_home_security_motion_detected") - assert state.state == "on" - - -async def test_sensor_enabled(hass, generic_data, binary_sensor_alt_msg): - """Test enabling a legacy binary_sensor.""" - - registry = er.async_get(hass) - - entry = registry.async_get_or_create( - BINARY_SENSOR_DOMAIN, - DOMAIN, - "1-37-625737744", - suggested_object_id="trisensor_sensor_instance_1_sensor", - disabled_by=None, - ) - assert entry.disabled is False - - receive_msg = await setup_ozw(hass, fixture=generic_data) - receive_msg(binary_sensor_alt_msg) - await hass.async_block_till_done() - - state = hass.states.get(entry.entity_id) - assert state is not None - assert state.state == "on" diff --git a/tests/components/ozw/test_climate.py b/tests/components/ozw/test_climate.py deleted file mode 100644 index 3414e6c4832..00000000000 --- a/tests/components/ozw/test_climate.py +++ /dev/null @@ -1,327 +0,0 @@ -"""Test Z-Wave Multi-setpoint Climate entities.""" -from homeassistant.components.climate import ATTR_TEMPERATURE -from homeassistant.components.climate.const import ( - ATTR_CURRENT_TEMPERATURE, - ATTR_FAN_MODE, - ATTR_FAN_MODES, - ATTR_HVAC_ACTION, - ATTR_HVAC_MODES, - ATTR_PRESET_MODE, - ATTR_PRESET_MODES, - ATTR_TARGET_TEMP_HIGH, - ATTR_TARGET_TEMP_LOW, - CURRENT_HVAC_IDLE, - HVAC_MODE_COOL, - HVAC_MODE_HEAT, - HVAC_MODE_HEAT_COOL, - HVAC_MODE_OFF, -) - -from .common import setup_ozw - - -async def test_climate(hass, climate_data, sent_messages, climate_msg, caplog): - """Test setting up config entry.""" - receive_message = await setup_ozw(hass, fixture=climate_data) - - # Test multi-setpoint thermostat (node 7 in dump) - # mode is heat, this should be single setpoint - state = hass.states.get("climate.ct32_thermostat_mode") - assert state is not None - assert state.state == HVAC_MODE_HEAT - assert state.attributes[ATTR_HVAC_MODES] == [ - HVAC_MODE_OFF, - HVAC_MODE_HEAT, - HVAC_MODE_COOL, - HVAC_MODE_HEAT_COOL, - ] - assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_IDLE - assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 23.1 - assert state.attributes[ATTR_TEMPERATURE] == 21.1 - assert state.attributes.get(ATTR_TARGET_TEMP_LOW) is None - assert state.attributes.get(ATTR_TARGET_TEMP_HIGH) is None - assert state.attributes[ATTR_FAN_MODE] == "Auto Low" - assert state.attributes[ATTR_FAN_MODES] == ["Auto Low", "On Low"] - - # Test set target temperature - await hass.services.async_call( - "climate", - "set_temperature", - {"entity_id": "climate.ct32_thermostat_mode", "temperature": 26.1}, - blocking=True, - ) - assert len(sent_messages) == 1 - msg = sent_messages[-1] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - # Celsius is converted to Fahrenheit here! - assert round(msg["payload"]["Value"], 2) == 78.98 - assert msg["payload"]["ValueIDKey"] == 281475099443218 - - # Test hvac_mode with set_temperature - await hass.services.async_call( - "climate", - "set_temperature", - { - "entity_id": "climate.ct32_thermostat_mode", - "temperature": 24.1, - "hvac_mode": "cool", - }, - blocking=True, - ) - assert len(sent_messages) == 3 # 2 messages - msg = sent_messages[-1] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - # Celsius is converted to Fahrenheit here! - assert round(msg["payload"]["Value"], 2) == 75.38 - assert msg["payload"]["ValueIDKey"] == 281475099443218 - - # Test set mode - await hass.services.async_call( - "climate", - "set_hvac_mode", - {"entity_id": "climate.ct32_thermostat_mode", "hvac_mode": HVAC_MODE_HEAT_COOL}, - blocking=True, - ) - assert len(sent_messages) == 4 - msg = sent_messages[-1] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": 3, "ValueIDKey": 122683412} - - # Test set missing mode - await hass.services.async_call( - "climate", - "set_hvac_mode", - {"entity_id": "climate.ct32_thermostat_mode", "hvac_mode": "fan_only"}, - blocking=True, - ) - assert len(sent_messages) == 4 - assert "Received an invalid hvac mode: fan_only" in caplog.text - - # Test set fan mode - await hass.services.async_call( - "climate", - "set_fan_mode", - {"entity_id": "climate.ct32_thermostat_mode", "fan_mode": "On Low"}, - blocking=True, - ) - assert len(sent_messages) == 5 - msg = sent_messages[-1] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": 1, "ValueIDKey": 122748948} - - # Test set invalid fan mode - await hass.services.async_call( - "climate", - "set_fan_mode", - {"entity_id": "climate.ct32_thermostat_mode", "fan_mode": "invalid fan mode"}, - blocking=True, - ) - assert len(sent_messages) == 5 - assert "Received an invalid fan mode: invalid fan mode" in caplog.text - - # Test incoming mode change to auto, - # resulting in multiple setpoints - receive_message(climate_msg) - await hass.async_block_till_done() - state = hass.states.get("climate.ct32_thermostat_mode") - assert state is not None - assert state.state == HVAC_MODE_HEAT_COOL - assert state.attributes.get(ATTR_TEMPERATURE) is None - assert state.attributes[ATTR_TARGET_TEMP_LOW] == 21.1 - assert state.attributes[ATTR_TARGET_TEMP_HIGH] == 25.6 - - # Test setting high/low temp on multiple setpoints - await hass.services.async_call( - "climate", - "set_temperature", - { - "entity_id": "climate.ct32_thermostat_mode", - "target_temp_low": 20, - "target_temp_high": 25, - }, - blocking=True, - ) - assert len(sent_messages) == 7 # 2 messages ! - msg = sent_messages[-2] # low setpoint - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert round(msg["payload"]["Value"], 2) == 68.0 - assert msg["payload"]["ValueIDKey"] == 281475099443218 - msg = sent_messages[-1] # high setpoint - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert round(msg["payload"]["Value"], 2) == 77.0 - assert msg["payload"]["ValueIDKey"] == 562950076153874 - - # Test basic/single-setpoint thermostat (node 16 in dump) - state = hass.states.get("climate.komforthaus_spirit_z_wave_plus_mode") - assert state is not None - assert state.state == HVAC_MODE_HEAT - assert state.attributes[ATTR_HVAC_MODES] == [ - HVAC_MODE_OFF, - HVAC_MODE_HEAT, - ] - assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 17.3 - assert round(state.attributes[ATTR_TEMPERATURE], 0) == 19 - assert state.attributes.get(ATTR_TARGET_TEMP_LOW) is None - assert state.attributes.get(ATTR_TARGET_TEMP_HIGH) is None - assert state.attributes[ATTR_PRESET_MODES] == [ - "none", - "Heat Eco", - "Full Power", - "Manufacturer Specific", - ] - - # Test set target temperature - await hass.services.async_call( - "climate", - "set_temperature", - { - "entity_id": "climate.komforthaus_spirit_z_wave_plus_mode", - "temperature": 28.0, - }, - blocking=True, - ) - assert len(sent_messages) == 8 - msg = sent_messages[-1] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == { - "Value": 28.0, - "ValueIDKey": 281475250438162, - } - - # Test set preset mode - await hass.services.async_call( - "climate", - "set_preset_mode", - { - "entity_id": "climate.komforthaus_spirit_z_wave_plus_mode", - "preset_mode": "Heat Eco", - }, - blocking=True, - ) - assert len(sent_messages) == 9 - msg = sent_messages[-1] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == { - "Value": 11, - "ValueIDKey": 273678356, - } - - # Test set preset mode None - # This preset should set and return to current hvac mode - await hass.services.async_call( - "climate", - "set_preset_mode", - { - "entity_id": "climate.komforthaus_spirit_z_wave_plus_mode", - "preset_mode": "none", - }, - blocking=True, - ) - assert len(sent_messages) == 10 - msg = sent_messages[-1] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == { - "Value": 1, - "ValueIDKey": 273678356, - } - - # Test set invalid preset mode - await hass.services.async_call( - "climate", - "set_preset_mode", - { - "entity_id": "climate.komforthaus_spirit_z_wave_plus_mode", - "preset_mode": "invalid preset mode", - }, - blocking=True, - ) - assert len(sent_messages) == 10 - assert "Received an invalid preset mode: invalid preset mode" in caplog.text - - # test thermostat device without a mode commandclass - state = hass.states.get("climate.danfoss_living_connect_z_v1_06_014g0013_heating_1") - assert state is not None - assert state.state == HVAC_MODE_HEAT - assert state.attributes[ATTR_HVAC_MODES] == [ - HVAC_MODE_HEAT, - ] - assert state.attributes.get(ATTR_CURRENT_TEMPERATURE) is None - assert round(state.attributes[ATTR_TEMPERATURE], 0) == 21 - assert state.attributes.get(ATTR_TARGET_TEMP_LOW) is None - assert state.attributes.get(ATTR_TARGET_TEMP_HIGH) is None - assert state.attributes.get(ATTR_PRESET_MODE) is None - assert state.attributes.get(ATTR_PRESET_MODES) is None - - # Test set target temperature - await hass.services.async_call( - "climate", - "set_temperature", - { - "entity_id": "climate.danfoss_living_connect_z_v1_06_014g0013_heating_1", - "temperature": 28.0, - }, - blocking=True, - ) - assert len(sent_messages) == 11 - msg = sent_messages[-1] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == { - "Value": 28.0, - "ValueIDKey": 281475116220434, - } - - await hass.services.async_call( - "climate", - "set_hvac_mode", - { - "entity_id": "climate.danfoss_living_connect_z_v1_06_014g0013_heating_1", - "hvac_mode": HVAC_MODE_HEAT, - }, - blocking=True, - ) - assert len(sent_messages) == 11 - assert "does not support setting a mode" in caplog.text - - # test thermostat device without a mode commandclass - state = hass.states.get("climate.secure_srt321_zwave_stat_tx_heating_1") - assert state is not None - assert state.state == HVAC_MODE_HEAT - assert state.attributes[ATTR_HVAC_MODES] == [ - HVAC_MODE_HEAT, - ] - assert state.attributes.get(ATTR_CURRENT_TEMPERATURE) == 29.0 - assert round(state.attributes[ATTR_TEMPERATURE], 0) == 16 - assert state.attributes.get(ATTR_TARGET_TEMP_LOW) is None - assert state.attributes.get(ATTR_TARGET_TEMP_HIGH) is None - assert state.attributes.get(ATTR_PRESET_MODE) is None - assert state.attributes.get(ATTR_PRESET_MODES) is None - - # Test set target temperature - await hass.services.async_call( - "climate", - "set_temperature", - { - "entity_id": "climate.secure_srt321_zwave_stat_tx_heating_1", - "temperature": 28.0, - }, - blocking=True, - ) - assert len(sent_messages) == 12 - msg = sent_messages[-1] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == { - "Value": 28.0, - "ValueIDKey": 281475267215378, - } - - await hass.services.async_call( - "climate", - "set_hvac_mode", - { - "entity_id": "climate.secure_srt321_zwave_stat_tx_heating_1", - "hvac_mode": HVAC_MODE_HEAT, - }, - blocking=True, - ) - assert len(sent_messages) == 12 - assert "does not support setting a mode" in caplog.text diff --git a/tests/components/ozw/test_config_flow.py b/tests/components/ozw/test_config_flow.py deleted file mode 100644 index 9c65372ca98..00000000000 --- a/tests/components/ozw/test_config_flow.py +++ /dev/null @@ -1,502 +0,0 @@ -"""Test the Z-Wave over MQTT config flow.""" -from unittest.mock import patch - -import pytest - -from homeassistant import config_entries -from homeassistant.components.hassio import HassioServiceInfo -from homeassistant.components.hassio.handler import HassioAPIError -from homeassistant.components.ozw.config_flow import TITLE -from homeassistant.components.ozw.const import DOMAIN - -from tests.common import MockConfigEntry - -ADDON_DISCOVERY_INFO = HassioServiceInfo( - config={ - "addon": "OpenZWave", - "host": "host1", - "port": 1234, - "username": "name1", - "password": "pass1", - } -) - - -@pytest.fixture(name="supervisor") -def mock_supervisor_fixture(): - """Mock Supervisor.""" - with patch("homeassistant.components.hassio.is_hassio", return_value=True): - yield - - -@pytest.fixture(name="addon_info") -def mock_addon_info(): - """Mock Supervisor add-on info.""" - with patch("homeassistant.components.hassio.async_get_addon_info") as addon_info: - addon_info.return_value = {} - yield addon_info - - -@pytest.fixture(name="addon_running") -def mock_addon_running(addon_info): - """Mock add-on already running.""" - addon_info.return_value["state"] = "started" - return addon_info - - -@pytest.fixture(name="addon_installed") -def mock_addon_installed(addon_info): - """Mock add-on already installed but not running.""" - addon_info.return_value["state"] = "stopped" - addon_info.return_value["version"] = "1.0" - return addon_info - - -@pytest.fixture(name="addon_options") -def mock_addon_options(addon_info): - """Mock add-on options.""" - addon_info.return_value["options"] = {} - return addon_info.return_value["options"] - - -@pytest.fixture(name="set_addon_options") -def mock_set_addon_options(): - """Mock set add-on options.""" - with patch( - "homeassistant.components.hassio.async_set_addon_options" - ) as set_options: - yield set_options - - -@pytest.fixture(name="install_addon") -def mock_install_addon(): - """Mock install add-on.""" - with patch("homeassistant.components.hassio.async_install_addon") as install_addon: - yield install_addon - - -@pytest.fixture(name="start_addon") -def mock_start_addon(): - """Mock start add-on.""" - with patch("homeassistant.components.hassio.async_start_addon") as start_addon: - yield start_addon - - -async def test_user_not_supervisor_create_entry(hass, mqtt): - """Test the user step creates an entry not on Supervisor.""" - - with patch( - "homeassistant.components.ozw.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - await hass.async_block_till_done() - - assert result["type"] == "create_entry" - assert result["title"] == TITLE - assert result["data"] == { - "usb_path": None, - "network_key": None, - "use_addon": False, - "integration_created_addon": False, - } - assert len(mock_setup_entry.mock_calls) == 1 - - -async def test_mqtt_not_setup(hass): - """Test that mqtt is required.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - assert result["type"] == "abort" - assert result["reason"] == "mqtt_required" - - -async def test_one_instance_allowed(hass): - """Test that only one instance is allowed.""" - entry = MockConfigEntry(domain=DOMAIN, data={}, title=TITLE) - entry.add_to_hass(hass) - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - assert result["type"] == "abort" - assert result["reason"] == "single_instance_allowed" - - -async def test_not_addon(hass, supervisor, mqtt): - """Test opting out of add-on on Supervisor.""" - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - with patch( - "homeassistant.components.ozw.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result = await hass.config_entries.flow.async_configure( - result["flow_id"], {"use_addon": False} - ) - await hass.async_block_till_done() - - assert result["type"] == "create_entry" - assert result["title"] == TITLE - assert result["data"] == { - "usb_path": None, - "network_key": None, - "use_addon": False, - "integration_created_addon": False, - } - assert len(mock_setup_entry.mock_calls) == 1 - - -async def test_addon_running(hass, supervisor, addon_running, addon_options): - """Test add-on already running on Supervisor.""" - addon_options["device"] = "/test" - addon_options["network_key"] = "abc123" - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - with patch( - "homeassistant.components.ozw.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result = await hass.config_entries.flow.async_configure( - result["flow_id"], {"use_addon": True} - ) - await hass.async_block_till_done() - - assert result["type"] == "create_entry" - assert result["title"] == TITLE - assert result["data"] == { - "usb_path": "/test", - "network_key": "abc123", - "use_addon": True, - "integration_created_addon": False, - } - assert len(mock_setup_entry.mock_calls) == 1 - - -async def test_addon_info_failure(hass, supervisor, addon_info): - """Test add-on info failure.""" - addon_info.side_effect = HassioAPIError() - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], {"use_addon": True} - ) - - assert result["type"] == "abort" - assert result["reason"] == "addon_info_failed" - - -async def test_addon_installed( - hass, supervisor, addon_installed, addon_options, set_addon_options, start_addon -): - """Test add-on already installed but not running on Supervisor.""" - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - result = await hass.config_entries.flow.async_configure( - result["flow_id"], {"use_addon": True} - ) - - with patch( - "homeassistant.components.ozw.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result = await hass.config_entries.flow.async_configure( - result["flow_id"], {"usb_path": "/test", "network_key": "abc123"} - ) - await hass.async_block_till_done() - - assert result["type"] == "create_entry" - assert result["title"] == TITLE - assert result["data"] == { - "usb_path": "/test", - "network_key": "abc123", - "use_addon": True, - "integration_created_addon": False, - } - assert len(mock_setup_entry.mock_calls) == 1 - - -async def test_set_addon_config_failure( - hass, supervisor, addon_installed, addon_options, set_addon_options -): - """Test add-on set config failure.""" - set_addon_options.side_effect = HassioAPIError() - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - result = await hass.config_entries.flow.async_configure( - result["flow_id"], {"use_addon": True} - ) - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], {"usb_path": "/test", "network_key": "abc123"} - ) - - assert result["type"] == "abort" - assert result["reason"] == "addon_set_config_failed" - - -async def test_start_addon_failure( - hass, supervisor, addon_installed, addon_options, set_addon_options, start_addon -): - """Test add-on start failure.""" - start_addon.side_effect = HassioAPIError() - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - result = await hass.config_entries.flow.async_configure( - result["flow_id"], {"use_addon": True} - ) - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], {"usb_path": "/test", "network_key": "abc123"} - ) - - assert result["type"] == "form" - assert result["errors"] == {"base": "addon_start_failed"} - - -async def test_addon_not_installed( - hass, - supervisor, - addon_installed, - install_addon, - addon_options, - set_addon_options, - start_addon, -): - """Test add-on not installed.""" - addon_installed.return_value["version"] = None - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - result = await hass.config_entries.flow.async_configure( - result["flow_id"], {"use_addon": True} - ) - - assert result["type"] == "progress" - - # Make sure the flow continues when the progress task is done. - await hass.async_block_till_done() - - result = await hass.config_entries.flow.async_configure(result["flow_id"]) - - assert result["type"] == "form" - assert result["step_id"] == "start_addon" - - with patch( - "homeassistant.components.ozw.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result = await hass.config_entries.flow.async_configure( - result["flow_id"], {"usb_path": "/test", "network_key": "abc123"} - ) - await hass.async_block_till_done() - - assert result["type"] == "create_entry" - assert result["title"] == TITLE - assert result["data"] == { - "usb_path": "/test", - "network_key": "abc123", - "use_addon": True, - "integration_created_addon": True, - } - assert len(mock_setup_entry.mock_calls) == 1 - - -async def test_install_addon_failure(hass, supervisor, addon_installed, install_addon): - """Test add-on install failure.""" - addon_installed.return_value["version"] = None - install_addon.side_effect = HassioAPIError() - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - result = await hass.config_entries.flow.async_configure( - result["flow_id"], {"use_addon": True} - ) - - assert result["type"] == "progress" - - # Make sure the flow continues when the progress task is done. - await hass.async_block_till_done() - - result = await hass.config_entries.flow.async_configure(result["flow_id"]) - - assert result["type"] == "abort" - assert result["reason"] == "addon_install_failed" - - -async def test_supervisor_discovery(hass, supervisor, addon_running, addon_options): - """Test flow started from Supervisor discovery.""" - - addon_options["device"] = "/test" - addon_options["network_key"] = "abc123" - - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_HASSIO}, - data=ADDON_DISCOVERY_INFO, - ) - - with patch( - "homeassistant.components.ozw.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - await hass.async_block_till_done() - - assert result["type"] == "create_entry" - assert result["title"] == TITLE - assert result["data"] == { - "usb_path": "/test", - "network_key": "abc123", - "use_addon": True, - "integration_created_addon": False, - } - assert len(mock_setup_entry.mock_calls) == 1 - - -async def test_clean_discovery_on_user_create( - hass, supervisor, addon_running, addon_options -): - """Test discovery flow is cleaned up when a user flow is finished.""" - - addon_options["device"] = "/test" - addon_options["network_key"] = "abc123" - - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_HASSIO}, - data=ADDON_DISCOVERY_INFO, - ) - - assert result["type"] == "form" - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - with patch( - "homeassistant.components.ozw.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result = await hass.config_entries.flow.async_configure( - result["flow_id"], {"use_addon": False} - ) - await hass.async_block_till_done() - - assert len(hass.config_entries.flow.async_progress()) == 0 - assert result["type"] == "create_entry" - assert result["title"] == TITLE - assert result["data"] == { - "usb_path": None, - "network_key": None, - "use_addon": False, - "integration_created_addon": False, - } - assert len(mock_setup_entry.mock_calls) == 1 - - -async def test_abort_discovery_with_user_flow( - hass, supervisor, addon_running, addon_options -): - """Test discovery flow is aborted if a user flow is in progress.""" - - await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_HASSIO}, - data=ADDON_DISCOVERY_INFO, - ) - - assert result["type"] == "abort" - assert result["reason"] == "already_in_progress" - assert len(hass.config_entries.flow.async_progress()) == 1 - - -async def test_abort_discovery_with_existing_entry( - hass, supervisor, addon_running, addon_options -): - """Test discovery flow is aborted if an entry already exists.""" - - entry = MockConfigEntry(domain=DOMAIN, data={}, title=TITLE, unique_id=DOMAIN) - entry.add_to_hass(hass) - - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_HASSIO}, - data=ADDON_DISCOVERY_INFO, - ) - - assert result["type"] == "abort" - assert result["reason"] == "already_configured" - - -async def test_discovery_addon_not_running( - hass, supervisor, addon_installed, addon_options, set_addon_options, start_addon -): - """Test discovery with add-on already installed but not running.""" - addon_options["device"] = None - - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_HASSIO}, - data=ADDON_DISCOVERY_INFO, - ) - - assert result["step_id"] == "hassio_confirm" - assert result["type"] == "form" - - result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - - assert result["step_id"] == "start_addon" - assert result["type"] == "form" - - -async def test_discovery_addon_not_installed( - hass, supervisor, addon_installed, install_addon, addon_options -): - """Test discovery with add-on not installed.""" - addon_installed.return_value["version"] = None - - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_HASSIO}, - data=ADDON_DISCOVERY_INFO, - ) - - assert result["step_id"] == "hassio_confirm" - assert result["type"] == "form" - - result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - - assert result["step_id"] == "install_addon" - assert result["type"] == "progress" - - await hass.async_block_till_done() - - result = await hass.config_entries.flow.async_configure(result["flow_id"]) - - assert result["type"] == "form" - assert result["step_id"] == "start_addon" diff --git a/tests/components/ozw/test_cover.py b/tests/components/ozw/test_cover.py deleted file mode 100644 index 2b3b1e06862..00000000000 --- a/tests/components/ozw/test_cover.py +++ /dev/null @@ -1,177 +0,0 @@ -"""Test Z-Wave Covers.""" -from homeassistant.components.cover import ATTR_CURRENT_POSITION -from homeassistant.components.ozw.cover import VALUE_SELECTED_ID - -from .common import setup_ozw - -VALUE_ID = "Value" - - -async def test_cover(hass, cover_data, sent_messages, cover_msg): - """Test setting up config entry.""" - receive_message = await setup_ozw(hass, fixture=cover_data) - # Test loaded - state = hass.states.get("cover.roller_shutter_3_instance_1_level") - assert state is not None - assert state.state == "closed" - assert state.attributes[ATTR_CURRENT_POSITION] == 0 - - # Test setting position - await hass.services.async_call( - "cover", - "set_cover_position", - {"entity_id": "cover.roller_shutter_3_instance_1_level", "position": 50}, - blocking=True, - ) - assert len(sent_messages) == 1 - msg = sent_messages[0] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": 50, "ValueIDKey": 625573905} - - # Feedback on state - cover_msg.decode() - cover_msg.payload["Value"] = 50 - cover_msg.encode() - receive_message(cover_msg) - await hass.async_block_till_done() - - # Test opening - await hass.services.async_call( - "cover", - "open_cover", - {"entity_id": "cover.roller_shutter_3_instance_1_level"}, - blocking=True, - ) - assert len(sent_messages) == 2 - msg = sent_messages[1] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": True, "ValueIDKey": 281475602284568} - - # Test stopping after opening - await hass.services.async_call( - "cover", - "stop_cover", - {"entity_id": "cover.roller_shutter_3_instance_1_level"}, - blocking=True, - ) - assert len(sent_messages) == 4 - msg = sent_messages[2] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": False, "ValueIDKey": 281475602284568} - - msg = sent_messages[3] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": False, "ValueIDKey": 562950578995224} - - # Test closing - await hass.services.async_call( - "cover", - "close_cover", - {"entity_id": "cover.roller_shutter_3_instance_1_level"}, - blocking=True, - ) - assert len(sent_messages) == 5 - msg = sent_messages[4] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": True, "ValueIDKey": 562950578995224} - - # Test stopping after closing - await hass.services.async_call( - "cover", - "stop_cover", - {"entity_id": "cover.roller_shutter_3_instance_1_level"}, - blocking=True, - ) - assert len(sent_messages) == 7 - msg = sent_messages[5] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": False, "ValueIDKey": 281475602284568} - - msg = sent_messages[6] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": False, "ValueIDKey": 562950578995224} - - # Test stopping after no open/close - await hass.services.async_call( - "cover", - "stop_cover", - {"entity_id": "cover.roller_shutter_3_instance_1_level"}, - blocking=True, - ) - # both stop open/close messages sent - assert len(sent_messages) == 9 - msg = sent_messages[7] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": False, "ValueIDKey": 281475602284568} - - msg = sent_messages[8] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": False, "ValueIDKey": 562950578995224} - - # Test converting position to zwave range for position > 0 - await hass.services.async_call( - "cover", - "set_cover_position", - {"entity_id": "cover.roller_shutter_3_instance_1_level", "position": 100}, - blocking=True, - ) - assert len(sent_messages) == 10 - msg = sent_messages[9] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": 99, "ValueIDKey": 625573905} - - # Test converting position to zwave range for position = 0 - await hass.services.async_call( - "cover", - "set_cover_position", - {"entity_id": "cover.roller_shutter_3_instance_1_level", "position": 0}, - blocking=True, - ) - assert len(sent_messages) == 11 - msg = sent_messages[10] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": 0, "ValueIDKey": 625573905} - - -async def test_barrier(hass, cover_gdo_data, sent_messages, cover_gdo_msg): - """Test setting up config entry.""" - receive_message = await setup_ozw(hass, fixture=cover_gdo_data) - # Test loaded - state = hass.states.get("cover.gd00z_4_barrier_state") - assert state is not None - assert state.state == "closed" - - # Test opening - await hass.services.async_call( - "cover", - "open_cover", - {"entity_id": "cover.gd00z_4_barrier_state"}, - blocking=True, - ) - assert len(sent_messages) == 1 - msg = sent_messages[0] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": 4, "ValueIDKey": 281475083239444} - - # Feedback on state - cover_gdo_msg.decode() - cover_gdo_msg.payload[VALUE_ID][VALUE_SELECTED_ID] = 4 - cover_gdo_msg.encode() - receive_message(cover_gdo_msg) - await hass.async_block_till_done() - - state = hass.states.get("cover.gd00z_4_barrier_state") - assert state is not None - assert state.state == "open" - - # Test closing - await hass.services.async_call( - "cover", - "close_cover", - {"entity_id": "cover.gd00z_4_barrier_state"}, - blocking=True, - ) - assert len(sent_messages) == 2 - msg = sent_messages[1] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": 0, "ValueIDKey": 281475083239444} diff --git a/tests/components/ozw/test_fan.py b/tests/components/ozw/test_fan.py deleted file mode 100644 index 5556b663f6f..00000000000 --- a/tests/components/ozw/test_fan.py +++ /dev/null @@ -1,131 +0,0 @@ -"""Test Z-Wave Fans.""" -import pytest - -from .common import setup_ozw - - -async def test_fan(hass, fan_data, fan_msg, sent_messages, caplog): - """Test fan.""" - receive_message = await setup_ozw(hass, fixture=fan_data) - - # Test loaded - state = hass.states.get("fan.in_wall_smart_fan_control_level") - assert state is not None - assert state.state == "on" - - # Test turning off - await hass.services.async_call( - "fan", - "turn_off", - {"entity_id": "fan.in_wall_smart_fan_control_level"}, - blocking=True, - ) - - assert len(sent_messages) == 1 - msg = sent_messages[-1] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": 0, "ValueIDKey": 172589073} - - # Feedback on state - fan_msg.decode() - fan_msg.payload["Value"] = 0 - fan_msg.encode() - receive_message(fan_msg) - await hass.async_block_till_done() - - state = hass.states.get("fan.in_wall_smart_fan_control_level") - assert state is not None - assert state.state == "off" - - # Test turning on - await hass.services.async_call( - "fan", - "turn_on", - {"entity_id": "fan.in_wall_smart_fan_control_level", "percentage": 66}, - blocking=True, - ) - - assert len(sent_messages) == 2 - msg = sent_messages[-1] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == { - "Value": 66, - "ValueIDKey": 172589073, - } - - # Feedback on state - fan_msg.decode() - fan_msg.payload["Value"] = 66 - fan_msg.encode() - receive_message(fan_msg) - await hass.async_block_till_done() - - state = hass.states.get("fan.in_wall_smart_fan_control_level") - assert state is not None - assert state.state == "on" - assert state.attributes["percentage"] == 66 - - # Test turn on without speed - await hass.services.async_call( - "fan", - "turn_on", - {"entity_id": "fan.in_wall_smart_fan_control_level"}, - blocking=True, - ) - - assert len(sent_messages) == 3 - msg = sent_messages[-1] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == { - "Value": 255, - "ValueIDKey": 172589073, - } - - # Feedback on state - fan_msg.decode() - fan_msg.payload["Value"] = 99 - fan_msg.encode() - receive_message(fan_msg) - await hass.async_block_till_done() - - state = hass.states.get("fan.in_wall_smart_fan_control_level") - assert state is not None - assert state.state == "on" - assert state.attributes["percentage"] == 100 - - # Test set percentage to 0 - await hass.services.async_call( - "fan", - "set_percentage", - {"entity_id": "fan.in_wall_smart_fan_control_level", "percentage": 0}, - blocking=True, - ) - - assert len(sent_messages) == 4 - msg = sent_messages[-1] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == { - "Value": 0, - "ValueIDKey": 172589073, - } - - # Feedback on state - fan_msg.decode() - fan_msg.payload["Value"] = 0 - fan_msg.encode() - receive_message(fan_msg) - await hass.async_block_till_done() - - state = hass.states.get("fan.in_wall_smart_fan_control_level") - assert state is not None - assert state.state == "off" - - # Test invalid speed - new_speed = "invalid" - with pytest.raises(ValueError): - await hass.services.async_call( - "fan", - "set_speed", - {"entity_id": "fan.in_wall_smart_fan_control_level", "speed": new_speed}, - blocking=True, - ) diff --git a/tests/components/ozw/test_init.py b/tests/components/ozw/test_init.py deleted file mode 100644 index 9719c483800..00000000000 --- a/tests/components/ozw/test_init.py +++ /dev/null @@ -1,238 +0,0 @@ -"""Test integration initialization.""" -from unittest.mock import patch - -from homeassistant import config_entries -from homeassistant.components.hassio.handler import HassioAPIError -from homeassistant.components.ozw import DOMAIN, PLATFORMS, const -from homeassistant.const import ATTR_RESTORED, STATE_UNAVAILABLE - -from .common import setup_ozw - -from tests.common import MockConfigEntry - - -async def test_init_entry(hass, generic_data): - """Test setting up config entry.""" - await setup_ozw(hass, fixture=generic_data) - - # Verify integration + platform loaded. - assert "ozw" in hass.config.components - for platform in PLATFORMS: - assert platform in hass.config.components, platform - assert f"{platform}.{DOMAIN}" in hass.config.components, f"{platform}.{DOMAIN}" - - # Verify services registered - assert hass.services.has_service(DOMAIN, const.SERVICE_ADD_NODE) - assert hass.services.has_service(DOMAIN, const.SERVICE_REMOVE_NODE) - - -async def test_setup_entry_without_mqtt(hass): - """Test setting up config entry without mqtt integration setup.""" - entry = MockConfigEntry( - domain=DOMAIN, - title="OpenZWave", - ) - entry.add_to_hass(hass) - - assert not await hass.config_entries.async_setup(entry.entry_id) - - -async def test_publish_without_mqtt(hass, caplog): - """Test publish without mqtt integration setup.""" - with patch("homeassistant.components.ozw.OZWOptions") as ozw_options: - await setup_ozw(hass) - - send_message = ozw_options.call_args[1]["send_message"] - - mqtt_entries = hass.config_entries.async_entries("mqtt") - mqtt_entry = mqtt_entries[0] - await hass.config_entries.async_remove(mqtt_entry.entry_id) - await hass.async_block_till_done() - - assert not hass.config_entries.async_entries("mqtt") - - # Sending a message should not error with the MQTT integration not set up. - send_message("test_topic", "test_payload") - await hass.async_block_till_done() - - assert "MQTT integration is not set up" in caplog.text - - -async def test_unload_entry(hass, generic_data, switch_msg, caplog): - """Test unload the config entry.""" - entry = MockConfigEntry( - domain=DOMAIN, - title="Z-Wave", - ) - entry.add_to_hass(hass) - assert entry.state is config_entries.ConfigEntryState.NOT_LOADED - - receive_message = await setup_ozw(hass, entry=entry, fixture=generic_data) - - assert entry.state is config_entries.ConfigEntryState.LOADED - assert len(hass.states.async_entity_ids("switch")) == 1 - - await hass.config_entries.async_unload(entry.entry_id) - - assert entry.state is config_entries.ConfigEntryState.NOT_LOADED - entities = hass.states.async_entity_ids("switch") - assert len(entities) == 1 - for entity in entities: - assert hass.states.get(entity).state == STATE_UNAVAILABLE - assert hass.states.get(entity).attributes.get(ATTR_RESTORED) - - # Send a message for a switch from the broker to check that - # all entity topic subscribers are unsubscribed. - receive_message(switch_msg) - await hass.async_block_till_done() - - assert len(hass.states.async_entity_ids("switch")) == 1 - for entity in entities: - assert hass.states.get(entity).state == STATE_UNAVAILABLE - assert hass.states.get(entity).attributes.get(ATTR_RESTORED) - - # Load the integration again and check that there are no errors when - # adding the entities. - # This asserts that we have unsubscribed the entity addition signals - # when unloading the integration previously. - await setup_ozw(hass, entry=entry, fixture=generic_data) - await hass.async_block_till_done() - - assert entry.state is config_entries.ConfigEntryState.LOADED - assert len(hass.states.async_entity_ids("switch")) == 1 - for record in caplog.records: - assert record.levelname != "ERROR" - - -async def test_remove_entry(hass, stop_addon, uninstall_addon, caplog): - """Test remove the config entry.""" - # test successful remove without created add-on - entry = MockConfigEntry( - domain=DOMAIN, - title="Z-Wave", - data={"integration_created_addon": False}, - ) - entry.add_to_hass(hass) - assert entry.state is config_entries.ConfigEntryState.NOT_LOADED - assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - - await hass.config_entries.async_remove(entry.entry_id) - - assert entry.state is config_entries.ConfigEntryState.NOT_LOADED - assert len(hass.config_entries.async_entries(DOMAIN)) == 0 - - # test successful remove with created add-on - entry = MockConfigEntry( - domain=DOMAIN, - title="Z-Wave", - data={"integration_created_addon": True}, - ) - entry.add_to_hass(hass) - assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - - await hass.config_entries.async_remove(entry.entry_id) - - assert stop_addon.call_count == 1 - assert uninstall_addon.call_count == 1 - assert entry.state is config_entries.ConfigEntryState.NOT_LOADED - assert len(hass.config_entries.async_entries(DOMAIN)) == 0 - stop_addon.reset_mock() - uninstall_addon.reset_mock() - - # test add-on stop failure - entry.add_to_hass(hass) - assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - stop_addon.side_effect = HassioAPIError() - - await hass.config_entries.async_remove(entry.entry_id) - - assert stop_addon.call_count == 1 - assert uninstall_addon.call_count == 0 - assert entry.state is config_entries.ConfigEntryState.NOT_LOADED - assert len(hass.config_entries.async_entries(DOMAIN)) == 0 - assert "Failed to stop the OpenZWave add-on" in caplog.text - stop_addon.side_effect = None - stop_addon.reset_mock() - uninstall_addon.reset_mock() - - # test add-on uninstall failure - entry.add_to_hass(hass) - assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - uninstall_addon.side_effect = HassioAPIError() - - await hass.config_entries.async_remove(entry.entry_id) - - assert stop_addon.call_count == 1 - assert uninstall_addon.call_count == 1 - assert entry.state is config_entries.ConfigEntryState.NOT_LOADED - assert len(hass.config_entries.async_entries(DOMAIN)) == 0 - assert "Failed to uninstall the OpenZWave add-on" in caplog.text - - -async def test_setup_entry_with_addon(hass, get_addon_discovery_info): - """Test set up entry using OpenZWave add-on.""" - entry = MockConfigEntry( - domain=DOMAIN, - title="OpenZWave", - data={"use_addon": True}, - ) - entry.add_to_hass(hass) - - with patch("homeassistant.components.ozw.MQTTClient", autospec=True) as mock_client: - assert await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - assert mock_client.return_value.start_client.call_count == 1 - - # Verify integration + platform loaded. - assert "ozw" in hass.config.components - for platform in PLATFORMS: - assert platform in hass.config.components, platform - assert f"{platform}.{DOMAIN}" in hass.config.components, f"{platform}.{DOMAIN}" - - # Verify services registered - assert hass.services.has_service(DOMAIN, const.SERVICE_ADD_NODE) - assert hass.services.has_service(DOMAIN, const.SERVICE_REMOVE_NODE) - - -async def test_setup_entry_without_addon_info(hass, get_addon_discovery_info): - """Test set up entry using OpenZWave add-on but missing discovery info.""" - entry = MockConfigEntry( - domain=DOMAIN, - title="OpenZWave", - data={"use_addon": True}, - ) - entry.add_to_hass(hass) - - get_addon_discovery_info.return_value = None - - with patch("homeassistant.components.ozw.MQTTClient", autospec=True) as mock_client: - assert not await hass.config_entries.async_setup(entry.entry_id) - - assert mock_client.return_value.start_client.call_count == 0 - assert entry.state is config_entries.ConfigEntryState.SETUP_RETRY - - -async def test_unload_entry_with_addon( - hass, get_addon_discovery_info, generic_data, switch_msg, caplog -): - """Test unload the config entry using the OpenZWave add-on.""" - entry = MockConfigEntry( - domain=DOMAIN, - title="OpenZWave", - data={"use_addon": True}, - ) - entry.add_to_hass(hass) - - assert entry.state is config_entries.ConfigEntryState.NOT_LOADED - - with patch("homeassistant.components.ozw.MQTTClient", autospec=True) as mock_client: - assert await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - assert mock_client.return_value.start_client.call_count == 1 - assert entry.state is config_entries.ConfigEntryState.LOADED - - await hass.config_entries.async_unload(entry.entry_id) - - assert entry.state is config_entries.ConfigEntryState.NOT_LOADED diff --git a/tests/components/ozw/test_light.py b/tests/components/ozw/test_light.py deleted file mode 100644 index a8ed4352f9a..00000000000 --- a/tests/components/ozw/test_light.py +++ /dev/null @@ -1,683 +0,0 @@ -"""Test Z-Wave Lights.""" -from homeassistant.components.light import SUPPORT_TRANSITION -from homeassistant.components.ozw.light import byte_to_zwave_brightness - -from .common import setup_ozw - - -async def test_light(hass, light_data, light_msg, light_rgb_msg, sent_messages): - """Test setting up config entry.""" - receive_message = await setup_ozw(hass, fixture=light_data) - - # Test loaded - state = hass.states.get("light.led_bulb_6_multi_colour_level") - assert state is not None - assert state.state == "off" - assert state.attributes["supported_features"] == SUPPORT_TRANSITION - assert state.attributes["supported_color_modes"] == ["color_temp", "hs"] - - # Test turning on - # Beware that due to rounding, a roundtrip conversion does not always work - new_brightness = 44 - new_transition = 0 - await hass.services.async_call( - "light", - "turn_on", - { - "entity_id": "light.led_bulb_6_multi_colour_level", - "brightness": new_brightness, - "transition": new_transition, - }, - blocking=True, - ) - assert len(sent_messages) == 2 - - msg = sent_messages[0] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": 0, "ValueIDKey": 1407375551070225} - - msg = sent_messages[1] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == { - "Value": byte_to_zwave_brightness(new_brightness), - "ValueIDKey": 659128337, - } - - # Feedback on state - light_msg.decode() - light_msg.payload["Value"] = byte_to_zwave_brightness(new_brightness) - light_msg.encode() - receive_message(light_msg) - await hass.async_block_till_done() - - state = hass.states.get("light.led_bulb_6_multi_colour_level") - assert state is not None - assert state.state == "on" - assert state.attributes["brightness"] == new_brightness - assert state.attributes["color_mode"] == "color_temp" - - # Test turning off - new_transition = 6553 - await hass.services.async_call( - "light", - "turn_off", - { - "entity_id": "light.led_bulb_6_multi_colour_level", - "transition": new_transition, - }, - blocking=True, - ) - assert len(sent_messages) == 4 - - msg = sent_messages[-2] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": 237, "ValueIDKey": 1407375551070225} - - msg = sent_messages[-1] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": 0, "ValueIDKey": 659128337} - - # Feedback on state - light_msg.decode() - light_msg.payload["Value"] = 0 - light_msg.encode() - receive_message(light_msg) - await hass.async_block_till_done() - - state = hass.states.get("light.led_bulb_6_multi_colour_level") - assert state is not None - assert state.state == "off" - - # Test turn on without brightness - new_transition = 127.0 - await hass.services.async_call( - "light", - "turn_on", - { - "entity_id": "light.led_bulb_6_multi_colour_level", - "transition": new_transition, - }, - blocking=True, - ) - assert len(sent_messages) == 6 - - msg = sent_messages[-2] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": 127, "ValueIDKey": 1407375551070225} - - msg = sent_messages[-1] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == { - "Value": 255, - "ValueIDKey": 659128337, - } - - # Feedback on state - light_msg.decode() - light_msg.payload["Value"] = byte_to_zwave_brightness(new_brightness) - light_msg.encode() - receive_message(light_msg) - await hass.async_block_till_done() - - state = hass.states.get("light.led_bulb_6_multi_colour_level") - assert state is not None - assert state.state == "on" - assert state.attributes["brightness"] == new_brightness - assert state.attributes["color_mode"] == "color_temp" - - # Test set brightness to 0 - new_brightness = 0 - await hass.services.async_call( - "light", - "turn_on", - { - "entity_id": "light.led_bulb_6_multi_colour_level", - "brightness": new_brightness, - }, - blocking=True, - ) - assert len(sent_messages) == 7 - msg = sent_messages[-1] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == { - "Value": byte_to_zwave_brightness(new_brightness), - "ValueIDKey": 659128337, - } - - # Feedback on state - light_msg.decode() - light_msg.payload["Value"] = byte_to_zwave_brightness(new_brightness) - light_msg.encode() - receive_message(light_msg) - await hass.async_block_till_done() - - state = hass.states.get("light.led_bulb_6_multi_colour_level") - assert state is not None - assert state.state == "off" - - # Test setting color_name - new_color = "blue" - await hass.services.async_call( - "light", - "turn_on", - {"entity_id": "light.led_bulb_6_multi_colour_level", "color_name": new_color}, - blocking=True, - ) - assert len(sent_messages) == 9 - - msg = sent_messages[-2] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": "#0000ff0000", "ValueIDKey": 659341335} - - msg = sent_messages[-1] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": 255, "ValueIDKey": 659128337} - - # Feedback on state - light_msg.decode() - light_msg.payload["Value"] = byte_to_zwave_brightness(255) - light_msg.encode() - light_rgb_msg.decode() - light_rgb_msg.payload["Value"] = "#0000ff0000" - light_rgb_msg.encode() - receive_message(light_msg) - receive_message(light_rgb_msg) - await hass.async_block_till_done() - - state = hass.states.get("light.led_bulb_6_multi_colour_level") - assert state is not None - assert state.state == "on" - assert state.attributes["rgb_color"] == (0, 0, 255) - assert state.attributes["color_mode"] == "hs" - - # Test setting hs_color - new_color = [300, 70] - await hass.services.async_call( - "light", - "turn_on", - {"entity_id": "light.led_bulb_6_multi_colour_level", "hs_color": new_color}, - blocking=True, - ) - assert len(sent_messages) == 11 - msg = sent_messages[-1] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": 255, "ValueIDKey": 659128337} - - msg = sent_messages[-2] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": "#ff4cff0000", "ValueIDKey": 659341335} - - # Feedback on state - light_msg.decode() - light_msg.payload["Value"] = byte_to_zwave_brightness(255) - light_msg.encode() - light_rgb_msg.decode() - light_rgb_msg.payload["Value"] = "#ff4cff0000" - light_rgb_msg.encode() - receive_message(light_msg) - receive_message(light_rgb_msg) - await hass.async_block_till_done() - - state = hass.states.get("light.led_bulb_6_multi_colour_level") - assert state is not None - assert state.state == "on" - assert state.attributes["hs_color"] == (300.0, 70.196) - assert state.attributes["color_mode"] == "hs" - - # Test setting rgb_color - new_color = [255, 154, 0] - await hass.services.async_call( - "light", - "turn_on", - {"entity_id": "light.led_bulb_6_multi_colour_level", "rgb_color": new_color}, - blocking=True, - ) - assert len(sent_messages) == 13 - msg = sent_messages[-1] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": 255, "ValueIDKey": 659128337} - - msg = sent_messages[-2] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": "#ff99000000", "ValueIDKey": 659341335} - - # Feedback on state - light_msg.decode() - light_msg.payload["Value"] = byte_to_zwave_brightness(255) - light_msg.encode() - light_rgb_msg.decode() - light_rgb_msg.payload["Value"] = "#ff99000000" - light_rgb_msg.encode() - receive_message(light_msg) - receive_message(light_rgb_msg) - await hass.async_block_till_done() - - state = hass.states.get("light.led_bulb_6_multi_colour_level") - assert state is not None - assert state.state == "on" - assert state.attributes["rgb_color"] == (255, 153, 0) - assert state.attributes["color_mode"] == "hs" - - # Test setting xy_color - new_color = [0.52, 0.43] - await hass.services.async_call( - "light", - "turn_on", - {"entity_id": "light.led_bulb_6_multi_colour_level", "xy_color": new_color}, - blocking=True, - ) - assert len(sent_messages) == 15 - msg = sent_messages[-1] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": 255, "ValueIDKey": 659128337} - - msg = sent_messages[-2] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": "#ffbb370000", "ValueIDKey": 659341335} - - # Feedback on state - light_msg.decode() - light_msg.payload["Value"] = byte_to_zwave_brightness(255) - light_msg.encode() - light_rgb_msg.decode() - light_rgb_msg.payload["Value"] = "#ffbb370000" - light_rgb_msg.encode() - receive_message(light_msg) - receive_message(light_rgb_msg) - await hass.async_block_till_done() - - state = hass.states.get("light.led_bulb_6_multi_colour_level") - assert state is not None - assert state.state == "on" - assert state.attributes["xy_color"] == (0.519, 0.429) - assert state.attributes["color_mode"] == "hs" - - # Test setting color temp - new_color = 200 - await hass.services.async_call( - "light", - "turn_on", - {"entity_id": "light.led_bulb_6_multi_colour_level", "color_temp": new_color}, - blocking=True, - ) - assert len(sent_messages) == 17 - msg = sent_messages[-1] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": 255, "ValueIDKey": 659128337} - - msg = sent_messages[-2] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": "#00000037c8", "ValueIDKey": 659341335} - - # Feedback on state - light_msg.decode() - light_msg.payload["Value"] = byte_to_zwave_brightness(255) - light_msg.encode() - light_rgb_msg.decode() - light_rgb_msg.payload["Value"] = "#00000037c8" - light_rgb_msg.encode() - receive_message(light_msg) - receive_message(light_rgb_msg) - await hass.async_block_till_done() - - state = hass.states.get("light.led_bulb_6_multi_colour_level") - assert state is not None - assert state.state == "on" - assert state.attributes["color_temp"] == 200 - assert state.attributes["color_mode"] == "color_temp" - - # Test setting invalid color temp - new_color = 120 - await hass.services.async_call( - "light", - "turn_on", - {"entity_id": "light.led_bulb_6_multi_colour_level", "color_temp": new_color}, - blocking=True, - ) - assert len(sent_messages) == 19 - msg = sent_messages[-1] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": 255, "ValueIDKey": 659128337} - - msg = sent_messages[-2] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": "#00000000ff", "ValueIDKey": 659341335} - - # Feedback on state - light_msg.decode() - light_msg.payload["Value"] = byte_to_zwave_brightness(255) - light_msg.encode() - light_rgb_msg.decode() - light_rgb_msg.payload["Value"] = "#00000000ff" - light_rgb_msg.encode() - receive_message(light_msg) - receive_message(light_rgb_msg) - await hass.async_block_till_done() - - state = hass.states.get("light.led_bulb_6_multi_colour_level") - assert state is not None - assert state.state == "on" - assert state.attributes["color_temp"] == 153 - assert state.attributes["color_mode"] == "color_temp" - - -async def test_pure_rgb_dimmer_light( - hass, light_data, light_pure_rgb_msg, sent_messages -): - """Test light with no color channels command class.""" - receive_message = await setup_ozw(hass, fixture=light_data) - - # Test loaded - state = hass.states.get("light.kitchen_rgb_strip_level") - assert state is not None - assert state.state == "on" - assert state.attributes["supported_features"] == 0 - assert state.attributes["supported_color_modes"] == ["hs"] - assert state.attributes["color_mode"] == "hs" - - # Test setting hs_color - new_color = [300, 70] - await hass.services.async_call( - "light", - "turn_on", - {"entity_id": "light.kitchen_rgb_strip_level", "hs_color": new_color}, - blocking=True, - ) - assert len(sent_messages) == 2 - msg = sent_messages[-1] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": 255, "ValueIDKey": 122257425} - - msg = sent_messages[-2] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": "#ff4cff00", "ValueIDKey": 122470423} - - # Feedback on state - light_pure_rgb_msg.decode() - light_pure_rgb_msg.payload["Value"] = "#ff4cff00" - light_pure_rgb_msg.encode() - receive_message(light_pure_rgb_msg) - await hass.async_block_till_done() - - state = hass.states.get("light.kitchen_rgb_strip_level") - assert state is not None - assert state.state == "on" - assert state.attributes["hs_color"] == (300.0, 70.196) - assert state.attributes["color_mode"] == "hs" - - -async def test_no_rgb_light(hass, light_data, light_no_rgb_msg, sent_messages): - """Test setting up config entry.""" - receive_message = await setup_ozw(hass, fixture=light_data) - - # Test loaded no RGBW support (dimmer only) - state = hass.states.get("light.master_bedroom_l_level") - assert state is not None - assert state.state == "off" - assert state.attributes["supported_features"] == 0 - assert state.attributes["supported_color_modes"] == ["brightness"] - - # Turn on the light - new_brightness = 44 - await hass.services.async_call( - "light", - "turn_on", - {"entity_id": "light.master_bedroom_l_level", "brightness": new_brightness}, - blocking=True, - ) - assert len(sent_messages) == 1 - msg = sent_messages[-1] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == { - "Value": byte_to_zwave_brightness(new_brightness), - "ValueIDKey": 38371345, - } - - # Feedback on state - - light_no_rgb_msg.decode() - light_no_rgb_msg.payload["Value"] = byte_to_zwave_brightness(new_brightness) - light_no_rgb_msg.encode() - receive_message(light_no_rgb_msg) - await hass.async_block_till_done() - - state = hass.states.get("light.master_bedroom_l_level") - assert state is not None - assert state.state == "on" - assert state.attributes["brightness"] == new_brightness - assert state.attributes["color_mode"] == "brightness" - - -async def test_no_ww_light( - hass, light_no_ww_data, light_msg, light_rgb_msg, sent_messages -): - """Test setting up config entry.""" - receive_message = await setup_ozw(hass, fixture=light_no_ww_data) - - # Test loaded no ww support - state = hass.states.get("light.led_bulb_6_multi_colour_level") - assert state is not None - assert state.state == "off" - assert state.attributes["supported_features"] == 0 - assert state.attributes["supported_color_modes"] == ["rgbw"] - - # Turn on the light - white_color = 190 - await hass.services.async_call( - "light", - "turn_on", - { - "entity_id": "light.led_bulb_6_multi_colour_level", - "rgbw_color": [0, 0, 0, white_color], - }, - blocking=True, - ) - assert len(sent_messages) == 2 - msg = sent_messages[-2] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": "#00000000be", "ValueIDKey": 659341335} - - # Feedback on state - light_msg.decode() - light_msg.payload["Value"] = byte_to_zwave_brightness(255) - light_msg.encode() - light_rgb_msg.decode() - light_rgb_msg.payload["Value"] = "#00000000be" - light_rgb_msg.encode() - receive_message(light_msg) - receive_message(light_rgb_msg) - await hass.async_block_till_done() - - state = hass.states.get("light.led_bulb_6_multi_colour_level") - assert state is not None - assert state.state == "on" - assert state.attributes["color_mode"] == "rgbw" - assert state.attributes["rgbw_color"] == (0, 0, 0, 190) - - -async def test_no_cw_light( - hass, light_no_cw_data, light_msg, light_rgb_msg, sent_messages -): - """Test setting up config entry.""" - receive_message = await setup_ozw(hass, fixture=light_no_cw_data) - - # Test loaded no cw support - state = hass.states.get("light.led_bulb_6_multi_colour_level") - assert state is not None - assert state.state == "off" - assert state.attributes["supported_features"] == 0 - assert state.attributes["supported_color_modes"] == ["rgbw"] - - # Turn on the light - white_color = 190 - await hass.services.async_call( - "light", - "turn_on", - { - "entity_id": "light.led_bulb_6_multi_colour_level", - "rgbw_color": [0, 0, 0, white_color], - }, - blocking=True, - ) - assert len(sent_messages) == 2 - msg = sent_messages[-2] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": "#000000be", "ValueIDKey": 659341335} - - # Feedback on state - light_msg.decode() - light_msg.payload["Value"] = byte_to_zwave_brightness(255) - light_msg.encode() - light_rgb_msg.decode() - light_rgb_msg.payload["Value"] = "#000000be" - light_rgb_msg.encode() - receive_message(light_msg) - receive_message(light_rgb_msg) - await hass.async_block_till_done() - - state = hass.states.get("light.led_bulb_6_multi_colour_level") - assert state is not None - assert state.state == "on" - assert state.attributes["color_mode"] == "rgbw" - assert state.attributes["rgbw_color"] == (0, 0, 0, 190) - - -async def test_wc_light(hass, light_wc_data, light_msg, light_rgb_msg, sent_messages): - """Test setting up config entry.""" - receive_message = await setup_ozw(hass, fixture=light_wc_data) - - # Test loaded only white LED support - state = hass.states.get("light.led_bulb_6_multi_colour_level") - assert state is not None - assert state.state == "off" - assert state.attributes["supported_features"] == 0 - assert state.attributes["supported_color_modes"] == ["color_temp", "hs"] - - assert state.attributes["min_mireds"] == 153 - assert state.attributes["max_mireds"] == 370 - - # Turn on the light - new_color = 190 - await hass.services.async_call( - "light", - "turn_on", - {"entity_id": "light.led_bulb_6_multi_colour_level", "color_temp": new_color}, - blocking=True, - ) - assert len(sent_messages) == 2 - msg = sent_messages[-2] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": "#0000002bd4", "ValueIDKey": 659341335} - - # Feedback on state - light_msg.decode() - light_msg.payload["Value"] = byte_to_zwave_brightness(255) - light_msg.encode() - light_rgb_msg.decode() - light_rgb_msg.payload["Value"] = "#0000002bd4" - light_rgb_msg.encode() - receive_message(light_msg) - receive_message(light_rgb_msg) - await hass.async_block_till_done() - - state = hass.states.get("light.led_bulb_6_multi_colour_level") - assert state is not None - assert state.state == "on" - assert state.attributes["color_temp"] == 190 - assert state.attributes["color_mode"] == "color_temp" - - -async def test_new_ozw_light(hass, light_new_ozw_data, light_msg, sent_messages): - """Test setting up config entry.""" - receive_message = await setup_ozw(hass, fixture=light_new_ozw_data) - - # Test loaded only white LED support - state = hass.states.get("light.led_bulb_6_multi_colour_level") - assert state is not None - assert state.state == "off" - assert state.attributes["supported_features"] == SUPPORT_TRANSITION - assert state.attributes["supported_color_modes"] == ["color_temp", "hs"] - - # Test turning on with new duration (newer openzwave) - new_transition = 4180 - await hass.services.async_call( - "light", - "turn_on", - { - "entity_id": "light.led_bulb_6_multi_colour_level", - "transition": new_transition, - }, - blocking=True, - ) - assert len(sent_messages) == 2 - - msg = sent_messages[-2] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": 4180, "ValueIDKey": 1407375551070225} - - msg = sent_messages[-1] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": 255, "ValueIDKey": 659128337} - - # Feedback on state - light_msg.decode() - light_msg.payload["Value"] = 255 - light_msg.encode() - receive_message(light_msg) - await hass.async_block_till_done() - state = hass.states.get("light.led_bulb_6_multi_colour_level") - assert state.attributes["color_mode"] == "color_temp" - - # Test turning off with new duration (newer openzwave)(new max) - await hass.services.async_call( - "light", - "turn_off", - {"entity_id": "light.led_bulb_6_multi_colour_level"}, - blocking=True, - ) - assert len(sent_messages) == 4 - - msg = sent_messages[-2] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": 7621, "ValueIDKey": 1407375551070225} - - msg = sent_messages[-1] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": 0, "ValueIDKey": 659128337} - - # Feedback on state - light_msg.decode() - light_msg.payload["Value"] = 0 - light_msg.encode() - receive_message(light_msg) - await hass.async_block_till_done() - - # Test turning on with new duration (newer openzwave)(factory default) - new_transition = 8000 - await hass.services.async_call( - "light", - "turn_on", - { - "entity_id": "light.led_bulb_6_multi_colour_level", - "transition": new_transition, - }, - blocking=True, - ) - assert len(sent_messages) == 6 - - msg = sent_messages[-2] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": 6553, "ValueIDKey": 1407375551070225} - - msg = sent_messages[-1] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": 255, "ValueIDKey": 659128337} - - # Feedback on state - light_msg.decode() - light_msg.payload["Value"] = 255 - light_msg.encode() - receive_message(light_msg) - await hass.async_block_till_done() - state = hass.states.get("light.led_bulb_6_multi_colour_level") - assert state.attributes["color_mode"] == "color_temp" diff --git a/tests/components/ozw/test_lock.py b/tests/components/ozw/test_lock.py deleted file mode 100644 index f32d073c562..00000000000 --- a/tests/components/ozw/test_lock.py +++ /dev/null @@ -1,83 +0,0 @@ -"""Test Z-Wave Locks.""" -from .common import setup_ozw - - -async def test_lock(hass, lock_data, sent_messages, lock_msg, caplog): - """Test lock.""" - receive_message = await setup_ozw(hass, fixture=lock_data) - - # Test loaded - state = hass.states.get("lock.danalock_v3_btze_locked") - assert state is not None - assert state.state == "unlocked" - - # Test locking - await hass.services.async_call( - "lock", "lock", {"entity_id": "lock.danalock_v3_btze_locked"}, blocking=True - ) - assert len(sent_messages) == 1 - msg = sent_messages[0] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": True, "ValueIDKey": 173572112} - - # Feedback on state - lock_msg.decode() - lock_msg.payload["Value"] = True - lock_msg.encode() - receive_message(lock_msg) - await hass.async_block_till_done() - - state = hass.states.get("lock.danalock_v3_btze_locked") - assert state is not None - assert state.state == "locked" - - # Test unlocking - await hass.services.async_call( - "lock", "unlock", {"entity_id": "lock.danalock_v3_btze_locked"}, blocking=True - ) - assert len(sent_messages) == 2 - msg = sent_messages[1] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": False, "ValueIDKey": 173572112} - - # Test set_usercode - await hass.services.async_call( - "ozw", - "set_usercode", - { - "entity_id": "lock.danalock_v3_btze_locked", - "usercode": 123456, - "code_slot": 1, - }, - blocking=True, - ) - assert len(sent_messages) == 3 - msg = sent_messages[2] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": "123456", "ValueIDKey": 281475150299159} - - # Test clear_usercode - await hass.services.async_call( - "ozw", - "clear_usercode", - {"entity_id": "lock.danalock_v3_btze_locked", "code_slot": 1}, - blocking=True, - ) - assert len(sent_messages) == 5 - msg = sent_messages[4] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": 1, "ValueIDKey": 72057594219905046} - - # Test set_usercode invalid length - await hass.services.async_call( - "ozw", - "set_usercode", - { - "entity_id": "lock.danalock_v3_btze_locked", - "usercode": "123", - "code_slot": 1, - }, - blocking=True, - ) - assert len(sent_messages) == 5 - assert "User code must be at least 4 digits" in caplog.text diff --git a/tests/components/ozw/test_scenes.py b/tests/components/ozw/test_scenes.py deleted file mode 100644 index 1c510d58a3c..00000000000 --- a/tests/components/ozw/test_scenes.py +++ /dev/null @@ -1,89 +0,0 @@ -"""Test Z-Wave (central) Scenes.""" -from .common import MQTTMessage, setup_ozw - -from tests.common import async_capture_events - - -async def test_scenes(hass, generic_data, sent_messages): - """Test setting up config entry.""" - - receive_message = await setup_ozw(hass, fixture=generic_data) - events = async_capture_events(hass, "ozw.scene_activated") - - # Publish fake scene event on mqtt - message = MQTTMessage( - topic="OpenZWave/1/node/39/instance/1/commandclass/43/value/562950622511127/", - payload={ - "Label": "Scene", - "Value": 16, - "Units": "", - "Min": -2147483648, - "Max": 2147483647, - "Type": "Int", - "Instance": 1, - "CommandClass": "COMMAND_CLASS_SCENE_ACTIVATION", - "Index": 0, - "Node": 7, - "Genre": "User", - "Help": "", - "ValueIDKey": 122339347, - "ReadOnly": False, - "WriteOnly": False, - "ValueSet": False, - "ValuePolled": False, - "ChangeVerified": False, - "Event": "valueChanged", - "TimeStamp": 1579630367, - }, - ) - message.encode() - receive_message(message) - # wait for the event - await hass.async_block_till_done() - assert len(events) == 1 - assert events[0].data["scene_value_id"] == 16 - - # Publish fake central scene event on mqtt - message = MQTTMessage( - topic="OpenZWave/1/node/39/instance/1/commandclass/91/value/281476005806100/", - payload={ - "Label": "Scene 1", - "Value": { - "List": [ - {"Value": 0, "Label": "Inactive"}, - {"Value": 1, "Label": "Pressed 1 Time"}, - {"Value": 2, "Label": "Key Released"}, - {"Value": 3, "Label": "Key Held down"}, - ], - "Selected": "Pressed 1 Time", - "Selected_id": 1, - }, - "Units": "", - "Min": 0, - "Max": 0, - "Type": "List", - "Instance": 1, - "CommandClass": "COMMAND_CLASS_CENTRAL_SCENE", - "Index": 1, - "Node": 61, - "Genre": "User", - "Help": "", - "ValueIDKey": 281476005806100, - "ReadOnly": False, - "WriteOnly": False, - "ValueSet": False, - "ValuePolled": False, - "ChangeVerified": False, - "Event": "valueChanged", - "TimeStamp": 1579640710, - }, - ) - message.encode() - receive_message(message) - # wait for the event - await hass.async_block_till_done() - assert len(events) == 2 - assert events[1].data["scene_id"] == 1 - assert events[1].data["scene_label"] == "Scene 1" - assert events[1].data["scene_value_label"] == "Pressed 1 Time" - assert events[1].data["instance_id"] == 1 diff --git a/tests/components/ozw/test_sensor.py b/tests/components/ozw/test_sensor.py deleted file mode 100644 index 2eddb9722e5..00000000000 --- a/tests/components/ozw/test_sensor.py +++ /dev/null @@ -1,93 +0,0 @@ -"""Test Z-Wave Sensors.""" -from homeassistant.components.ozw.const import DOMAIN -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN, SensorDeviceClass -from homeassistant.const import ATTR_DEVICE_CLASS -from homeassistant.helpers import entity_registry as er - -from .common import setup_ozw - - -async def test_sensor(hass, generic_data): - """Test setting up config entry.""" - await setup_ozw(hass, fixture=generic_data) - - # Test standard sensor - state = hass.states.get("sensor.smart_plug_electric_v") - assert state is not None - assert state.state == "123.9" - assert state.attributes["unit_of_measurement"] == "V" - - # Test device classes - state = hass.states.get("sensor.trisensor_relative_humidity") - assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.HUMIDITY - state = hass.states.get("sensor.trisensor_pressure") - assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.PRESSURE - state = hass.states.get("sensor.trisensor_fake_power") - assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.POWER - state = hass.states.get("sensor.trisensor_fake_energy") - assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.POWER - state = hass.states.get("sensor.trisensor_fake_electric") - assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.POWER - - # Test ZWaveListSensor disabled by default - registry = er.async_get(hass) - entity_id = "sensor.water_sensor_6_instance_1_water" - state = hass.states.get(entity_id) - assert state is None - - entry = registry.async_get(entity_id) - assert entry - assert entry.disabled - assert entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION - - # Test enabling entity - updated_entry = registry.async_update_entity( - entry.entity_id, **{"disabled_by": None} - ) - assert updated_entry != entry - assert updated_entry.disabled is False - - -async def test_sensor_enabled(hass, generic_data, sensor_msg): - """Test enabling an advanced sensor.""" - - registry = er.async_get(hass) - - entry = registry.async_get_or_create( - SENSOR_DOMAIN, - DOMAIN, - "1-36-1407375493578772", - suggested_object_id="water_sensor_6_instance_1_water", - disabled_by=None, - ) - assert entry.disabled is False - - receive_msg = await setup_ozw(hass, fixture=generic_data) - receive_msg(sensor_msg) - await hass.async_block_till_done() - - state = hass.states.get(entry.entity_id) - assert state is not None - assert state.state == "0" - assert state.attributes["label"] == "Clear" - - -async def test_string_sensor(hass, string_sensor_data): - """Test so the returned type is a string sensor.""" - - registry = er.async_get(hass) - - entry = registry.async_get_or_create( - SENSOR_DOMAIN, - DOMAIN, - "1-49-73464969749610519", - suggested_object_id="id_150_z_wave_module_user_code", - disabled_by=None, - ) - - await setup_ozw(hass, fixture=string_sensor_data) - await hass.async_block_till_done() - - state = hass.states.get(entry.entity_id) - assert state is not None - assert state.state == "asdfgh" diff --git a/tests/components/ozw/test_services.py b/tests/components/ozw/test_services.py deleted file mode 100644 index 7c71b234242..00000000000 --- a/tests/components/ozw/test_services.py +++ /dev/null @@ -1,115 +0,0 @@ -"""Test Z-Wave Services.""" -from openzwavemqtt.const import ATTR_POSITION, ATTR_VALUE -from openzwavemqtt.exceptions import InvalidValueError, NotFoundError, WrongTypeError -import pytest - -from .common import setup_ozw - - -async def test_services(hass, light_data, sent_messages): - """Test services on lock.""" - await setup_ozw(hass, fixture=light_data) - - # Test set_config_parameter list by label - await hass.services.async_call( - "ozw", - "set_config_parameter", - {"node_id": 39, "parameter": 1, "value": "Disable"}, - blocking=True, - ) - assert len(sent_messages) == 1 - msg = sent_messages[0] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": 0, "ValueIDKey": 281475641245716} - - # Test set_config_parameter list by index int - await hass.services.async_call( - "ozw", - "set_config_parameter", - {"node_id": 39, "parameter": 1, "value": 1}, - blocking=True, - ) - assert len(sent_messages) == 2 - msg = sent_messages[1] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": 1, "ValueIDKey": 281475641245716} - - # Test set_config_parameter int - await hass.services.async_call( - "ozw", - "set_config_parameter", - {"node_id": 39, "parameter": 3, "value": 55}, - blocking=True, - ) - assert len(sent_messages) == 3 - msg = sent_messages[2] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": 55, "ValueIDKey": 844425594667027} - - # Test set_config_parameter invalid list int - with pytest.raises(NotFoundError): - assert await hass.services.async_call( - "ozw", - "set_config_parameter", - {"node_id": 39, "parameter": 1, "value": 12}, - blocking=True, - ) - assert len(sent_messages) == 3 - - # Test set_config_parameter invalid list value - with pytest.raises(NotFoundError): - assert await hass.services.async_call( - "ozw", - "set_config_parameter", - {"node_id": 39, "parameter": 1, "value": "Blah"}, - blocking=True, - ) - assert len(sent_messages) == 3 - - # Test set_config_parameter invalid list value type - with pytest.raises(WrongTypeError): - assert await hass.services.async_call( - "ozw", - "set_config_parameter", - { - "node_id": 39, - "parameter": 1, - "value": {ATTR_VALUE: True, ATTR_POSITION: 1}, - }, - blocking=True, - ) - assert len(sent_messages) == 3 - - # Test set_config_parameter int out of range - with pytest.raises(InvalidValueError): - assert await hass.services.async_call( - "ozw", - "set_config_parameter", - {"node_id": 39, "parameter": 3, "value": 2147483657}, - blocking=True, - ) - assert len(sent_messages) == 3 - - # Test set_config_parameter short - await hass.services.async_call( - "ozw", - "set_config_parameter", - {"node_id": 39, "parameter": 81, "value": 3000}, - blocking=True, - ) - assert len(sent_messages) == 4 - msg = sent_messages[3] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": 3000, "ValueIDKey": 22799473778098198} - - # Test set_config_parameter byte - await hass.services.async_call( - "ozw", - "set_config_parameter", - {"node_id": 39, "parameter": 16, "value": 20}, - blocking=True, - ) - assert len(sent_messages) == 5 - msg = sent_messages[4] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": 20, "ValueIDKey": 4503600291905553} diff --git a/tests/components/ozw/test_switch.py b/tests/components/ozw/test_switch.py deleted file mode 100644 index 7af331b3e0f..00000000000 --- a/tests/components/ozw/test_switch.py +++ /dev/null @@ -1,41 +0,0 @@ -"""Test Z-Wave Switches.""" -from .common import setup_ozw - - -async def test_switch(hass, generic_data, sent_messages, switch_msg): - """Test setting up config entry.""" - receive_message = await setup_ozw(hass, fixture=generic_data) - - # Test loaded - state = hass.states.get("switch.smart_plug_switch") - assert state is not None - assert state.state == "off" - - # Test turning on - await hass.services.async_call( - "switch", "turn_on", {"entity_id": "switch.smart_plug_switch"}, blocking=True - ) - assert len(sent_messages) == 1 - msg = sent_messages[0] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": True, "ValueIDKey": 541671440} - - # Feedback on state - switch_msg.decode() - switch_msg.payload["Value"] = True - switch_msg.encode() - receive_message(switch_msg) - await hass.async_block_till_done() - - state = hass.states.get("switch.smart_plug_switch") - assert state is not None - assert state.state == "on" - - # Test turning off - await hass.services.async_call( - "switch", "turn_off", {"entity_id": "switch.smart_plug_switch"}, blocking=True - ) - assert len(sent_messages) == 2 - msg = sent_messages[1] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": False, "ValueIDKey": 541671440} diff --git a/tests/components/ozw/test_websocket_api.py b/tests/components/ozw/test_websocket_api.py deleted file mode 100644 index 2cbe69f1c98..00000000000 --- a/tests/components/ozw/test_websocket_api.py +++ /dev/null @@ -1,387 +0,0 @@ -"""Test OpenZWave Websocket API.""" -from unittest.mock import patch - -from openzwavemqtt.const import ( - ATTR_CODE_SLOT, - ATTR_LABEL, - ATTR_OPTIONS, - ATTR_POSITION, - ATTR_VALUE, - ValueType, -) - -from homeassistant.components.ozw.const import ATTR_CONFIG_PARAMETER -from homeassistant.components.ozw.lock import ATTR_USERCODE -from homeassistant.components.ozw.websocket_api import ( - ATTR_IS_AWAKE, - ATTR_IS_BEAMING, - ATTR_IS_FAILED, - ATTR_IS_FLIRS, - ATTR_IS_ROUTING, - ATTR_IS_SECURITYV1, - ATTR_IS_ZWAVE_PLUS, - ATTR_NEIGHBORS, - ATTR_NODE_BASIC_STRING, - ATTR_NODE_BAUD_RATE, - ATTR_NODE_GENERIC_STRING, - ATTR_NODE_QUERY_STAGE, - ATTR_NODE_SPECIFIC_STRING, - ID, - NODE_ID, - OZW_INSTANCE, - PARAMETER, - SCHEMA, - TYPE, - VALUE, -) -from homeassistant.components.websocket_api.const import ( - ERR_INVALID_FORMAT, - ERR_NOT_FOUND, - ERR_NOT_SUPPORTED, -) - -from .common import MQTTMessage, setup_ozw - - -async def test_websocket_api(hass, generic_data, hass_ws_client, mqtt_mock): - """Test the ozw websocket api.""" - await setup_ozw(hass, fixture=generic_data) - client = await hass_ws_client(hass) - - # Test instance list - await client.send_json({ID: 4, TYPE: "ozw/get_instances"}) - msg = await client.receive_json() - assert len(msg["result"]) == 1 - result = msg["result"][0] - assert result[OZW_INSTANCE] == 1 - assert result["Status"] == "driverAllNodesQueried" - assert result["OpenZWave_Version"] == "1.6.1008" - - # Test network status - await client.send_json({ID: 5, TYPE: "ozw/network_status"}) - msg = await client.receive_json() - result = msg["result"] - - assert result["Status"] == "driverAllNodesQueried" - assert result[OZW_INSTANCE] == 1 - - # Test node status - await client.send_json({ID: 6, TYPE: "ozw/node_status", NODE_ID: 32}) - msg = await client.receive_json() - result = msg["result"] - - assert result[OZW_INSTANCE] == 1 - assert result[NODE_ID] == 32 - assert result[ATTR_NODE_QUERY_STAGE] == "Complete" - assert result[ATTR_IS_ZWAVE_PLUS] - assert result[ATTR_IS_AWAKE] - assert not result[ATTR_IS_FAILED] - assert result[ATTR_NODE_BAUD_RATE] == 100000 - assert result[ATTR_IS_BEAMING] - assert not result[ATTR_IS_FLIRS] - assert result[ATTR_IS_ROUTING] - assert not result[ATTR_IS_SECURITYV1] - assert result[ATTR_NODE_BASIC_STRING] == "Routing Slave" - assert result[ATTR_NODE_GENERIC_STRING] == "Binary Switch" - assert result[ATTR_NODE_SPECIFIC_STRING] == "Binary Power Switch" - assert result[ATTR_NEIGHBORS] == [1, 33, 36, 37, 39] - - await client.send_json({ID: 7, TYPE: "ozw/node_status", NODE_ID: 999}) - msg = await client.receive_json() - result = msg["error"] - assert result["code"] == ERR_NOT_FOUND - - # Test node statistics - await client.send_json({ID: 8, TYPE: "ozw/node_statistics", NODE_ID: 39}) - msg = await client.receive_json() - result = msg["result"] - - assert result[OZW_INSTANCE] == 1 - assert result[NODE_ID] == 39 - assert result["send_count"] == 57 - assert result["sent_failed"] == 0 - assert result["retries"] == 1 - assert result["last_request_rtt"] == 26 - assert result["last_response_rtt"] == 38 - assert result["average_request_rtt"] == 29 - assert result["average_response_rtt"] == 37 - assert result["received_packets"] == 3594 - assert result["received_dup_packets"] == 12 - assert result["received_unsolicited"] == 3546 - - # Test node metadata - await client.send_json({ID: 9, TYPE: "ozw/node_metadata", NODE_ID: 39}) - msg = await client.receive_json() - result = msg["result"] - assert result["metadata"]["ProductPic"] == "images/aeotec/zwa002.png" - - await client.send_json({ID: 10, TYPE: "ozw/node_metadata", NODE_ID: 999}) - msg = await client.receive_json() - result = msg["error"] - assert result["code"] == ERR_NOT_FOUND - - # Test network statistics - await client.send_json({ID: 11, TYPE: "ozw/network_statistics"}) - msg = await client.receive_json() - result = msg["result"] - assert result["readCnt"] == 92220 - assert result[OZW_INSTANCE] == 1 - assert result["node_count"] == 5 - - # Test get nodes - await client.send_json({ID: 12, TYPE: "ozw/get_nodes"}) - msg = await client.receive_json() - result = msg["result"] - assert len(result) == 5 - assert result[2][ATTR_IS_AWAKE] - assert not result[1][ATTR_IS_FAILED] - - # Test get config parameters - await client.send_json({ID: 13, TYPE: "ozw/get_config_parameters", NODE_ID: 39}) - msg = await client.receive_json() - result = msg["result"] - assert len(result) == 8 - for config_param in result: - assert config_param["type"] in ( - ValueType.LIST.value, - ValueType.BOOL.value, - ValueType.INT.value, - ValueType.BYTE.value, - ValueType.SHORT.value, - ValueType.BITSET.value, - ) - - # Test set config parameter - config_param = result[0] - current_val = config_param[ATTR_VALUE] - new_val = next( - option[0] - for option in config_param[SCHEMA][0][ATTR_OPTIONS] - if option[0] != current_val - ) - new_label = next( - option[1] - for option in config_param[SCHEMA][0][ATTR_OPTIONS] - if option[1] != current_val and option[0] != new_val - ) - await client.send_json( - { - ID: 14, - TYPE: "ozw/set_config_parameter", - NODE_ID: 39, - PARAMETER: config_param[ATTR_CONFIG_PARAMETER], - VALUE: new_val, - } - ) - msg = await client.receive_json() - assert msg["success"] - await client.send_json( - { - ID: 15, - TYPE: "ozw/set_config_parameter", - NODE_ID: 39, - PARAMETER: config_param[ATTR_CONFIG_PARAMETER], - VALUE: new_label, - } - ) - msg = await client.receive_json() - assert msg["success"] - - # Test OZW Instance not found error - await client.send_json( - {ID: 16, TYPE: "ozw/get_config_parameters", OZW_INSTANCE: 999, NODE_ID: 1} - ) - msg = await client.receive_json() - result = msg["error"] - assert result["code"] == ERR_NOT_FOUND - - # Test OZW Node not found error - await client.send_json( - { - ID: 18, - TYPE: "ozw/set_config_parameter", - NODE_ID: 999, - PARAMETER: 0, - VALUE: "test", - } - ) - msg = await client.receive_json() - result = msg["error"] - assert result["code"] == ERR_NOT_FOUND - - # Test parameter not found - await client.send_json( - { - ID: 19, - TYPE: "ozw/set_config_parameter", - NODE_ID: 39, - PARAMETER: 45, - VALUE: "test", - } - ) - msg = await client.receive_json() - result = msg["error"] - assert result["code"] == ERR_NOT_FOUND - - # Test list value not found - await client.send_json( - { - ID: 20, - TYPE: "ozw/set_config_parameter", - NODE_ID: 39, - PARAMETER: config_param[ATTR_CONFIG_PARAMETER], - VALUE: "test", - } - ) - msg = await client.receive_json() - result = msg["error"] - assert result["code"] == ERR_NOT_FOUND - - # Test value type invalid - await client.send_json( - { - ID: 21, - TYPE: "ozw/set_config_parameter", - NODE_ID: 39, - PARAMETER: 3, - VALUE: 0, - } - ) - msg = await client.receive_json() - result = msg["error"] - assert result["code"] == ERR_NOT_SUPPORTED - - # Test invalid bitset format - await client.send_json( - { - ID: 22, - TYPE: "ozw/set_config_parameter", - NODE_ID: 39, - PARAMETER: 3, - VALUE: {ATTR_POSITION: 1, ATTR_VALUE: True, ATTR_LABEL: "test"}, - } - ) - msg = await client.receive_json() - result = msg["error"] - assert result["code"] == ERR_INVALID_FORMAT - - # Test valid bitset format passes validation - await client.send_json( - { - ID: 23, - TYPE: "ozw/set_config_parameter", - NODE_ID: 39, - PARAMETER: 10000, - VALUE: {ATTR_POSITION: 1, ATTR_VALUE: True}, - } - ) - msg = await client.receive_json() - result = msg["error"] - assert result["code"] == ERR_NOT_FOUND - - -async def test_ws_locks(hass, lock_data, hass_ws_client, mqtt_mock): - """Test lock websocket apis.""" - await setup_ozw(hass, fixture=lock_data) - client = await hass_ws_client(hass) - - await client.send_json( - { - ID: 1, - TYPE: "ozw/get_code_slots", - NODE_ID: 10, - } - ) - msg = await client.receive_json() - assert msg["success"] - - await client.send_json( - { - ID: 2, - TYPE: "ozw/set_usercode", - NODE_ID: 10, - ATTR_CODE_SLOT: 1, - ATTR_USERCODE: "1234", - } - ) - msg = await client.receive_json() - assert msg["success"] - - await client.send_json( - { - ID: 3, - TYPE: "ozw/clear_usercode", - NODE_ID: 10, - ATTR_CODE_SLOT: 1, - } - ) - msg = await client.receive_json() - assert msg["success"] - - -async def test_refresh_node( - hass, generic_data, sent_messages, hass_ws_client, mqtt_mock -): - """Test the ozw refresh node api.""" - receive_message = await setup_ozw(hass, fixture=generic_data) - client = await hass_ws_client(hass) - - # Send the refresh_node_info command - await client.send_json({ID: 9, TYPE: "ozw/refresh_node_info", NODE_ID: 39}) - msg = await client.receive_json() - - assert len(sent_messages) == 1 - assert msg["success"] - - # Receive a mock status update from OZW - message = MQTTMessage( - topic="OpenZWave/1/node/39/", - payload={"NodeID": 39, "NodeQueryStage": "initializing"}, - ) - message.encode() - receive_message(message) - - # Verify we got expected data on the websocket - msg = await client.receive_json() - result = msg["event"] - assert result["type"] == "node_updated" - assert result["node_query_stage"] == "initializing" - - # Send another mock status update from OZW - message = MQTTMessage( - topic="OpenZWave/1/node/39/", - payload={"NodeID": 39, "NodeQueryStage": "versions"}, - ) - message.encode() - receive_message(message) - - # Send a mock status update for a different node - message = MQTTMessage( - topic="OpenZWave/1/node/35/", - payload={"NodeID": 35, "NodeQueryStage": "fake_shouldnt_be_received"}, - ) - message.encode() - receive_message(message) - - # Verify we received the message for node 39 but not for node 35 - msg = await client.receive_json() - result = msg["event"] - assert result["type"] == "node_updated" - assert result["node_query_stage"] == "versions" - - -async def test_refresh_node_unsubscribe(hass, generic_data, hass_ws_client, mqtt_mock): - """Test unsubscribing the ozw refresh node api.""" - await setup_ozw(hass, fixture=generic_data) - client = await hass_ws_client(hass) - - with patch("openzwavemqtt.OZWOptions.listen") as mock_listen: - # Send the refresh_node_info command - await client.send_json({ID: 9, TYPE: "ozw/refresh_node_info", NODE_ID: 39}) - await client.receive_json() - - # Send the unsubscribe command - await client.send_json({ID: 10, TYPE: "unsubscribe_events", "subscription": 9}) - await client.receive_json() - - assert mock_listen.return_value.called diff --git a/tests/components/p1_monitor/fixtures/phases.json b/tests/components/p1_monitor/fixtures/phases.json index b756f092c05..84fcd03db69 100644 --- a/tests/components/p1_monitor/fixtures/phases.json +++ b/tests/components/p1_monitor/fixtures/phases.json @@ -1,74 +1,74 @@ [ - { - "LABEL": "Huidige KW verbruik L1 (21.7.0)", - "SECURITY": 0, - "STATUS": "0.315", - "STATUS_ID": 74 - }, - { - "LABEL": "Huidige KW verbruik L2 (41.7.0)", - "SECURITY": 0, - "STATUS": "0.0", - "STATUS_ID": 75 - }, - { - "LABEL": "Huidige KW verbruik L3 (61.7.0)", - "SECURITY": 0, - "STATUS": "0.624", - "STATUS_ID": 76 - }, - { - "LABEL": "Huidige KW levering L1 (22.7.0)", - "SECURITY": 0, - "STATUS": "0.0", - "STATUS_ID": 77 - }, - { - "LABEL": "Huidige KW levering L2 (42.7.0)", - "SECURITY": 0, - "STATUS": "0.0", - "STATUS_ID": 78 - }, - { - "LABEL": "Huidige KW levering L3 (62.7.0)", - "SECURITY": 0, - "STATUS": "0.0", - "STATUS_ID": 79 - }, - { - "LABEL": "Huidige Amperage L1 (31.7.0)", - "SECURITY": 0, - "STATUS": "1.6", - "STATUS_ID": 100 - }, - { - "LABEL": "Huidige Amperage L2 (51.7.0)", - "SECURITY": 0, - "STATUS": "4.44", - "STATUS_ID": 101 - }, - { - "LABEL": "Huidige Amperage L2 (71.7.0)", - "SECURITY": 0, - "STATUS": "3.51", - "STATUS_ID": 102 - }, - { - "LABEL": "Huidige Voltage L1 (32.7.0)", - "SECURITY": 0, - "STATUS": "233.6", - "STATUS_ID": 103 - }, - { - "LABEL": "Huidige Voltage L2 (52.7.0)", - "SECURITY": 0, - "STATUS": "0.0", - "STATUS_ID": 104 - }, - { - "LABEL": "Huidige Voltage L2 (72.7.0)", - "SECURITY": 0, - "STATUS": "233.0", - "STATUS_ID": 105 - } -] \ No newline at end of file + { + "LABEL": "Huidige KW verbruik L1 (21.7.0)", + "SECURITY": 0, + "STATUS": "0.315", + "STATUS_ID": 74 + }, + { + "LABEL": "Huidige KW verbruik L2 (41.7.0)", + "SECURITY": 0, + "STATUS": "0.0", + "STATUS_ID": 75 + }, + { + "LABEL": "Huidige KW verbruik L3 (61.7.0)", + "SECURITY": 0, + "STATUS": "0.624", + "STATUS_ID": 76 + }, + { + "LABEL": "Huidige KW levering L1 (22.7.0)", + "SECURITY": 0, + "STATUS": "0.0", + "STATUS_ID": 77 + }, + { + "LABEL": "Huidige KW levering L2 (42.7.0)", + "SECURITY": 0, + "STATUS": "0.0", + "STATUS_ID": 78 + }, + { + "LABEL": "Huidige KW levering L3 (62.7.0)", + "SECURITY": 0, + "STATUS": "0.0", + "STATUS_ID": 79 + }, + { + "LABEL": "Huidige Amperage L1 (31.7.0)", + "SECURITY": 0, + "STATUS": "1.6", + "STATUS_ID": 100 + }, + { + "LABEL": "Huidige Amperage L2 (51.7.0)", + "SECURITY": 0, + "STATUS": "4.44", + "STATUS_ID": 101 + }, + { + "LABEL": "Huidige Amperage L2 (71.7.0)", + "SECURITY": 0, + "STATUS": "3.51", + "STATUS_ID": 102 + }, + { + "LABEL": "Huidige Voltage L1 (32.7.0)", + "SECURITY": 0, + "STATUS": "233.6", + "STATUS_ID": 103 + }, + { + "LABEL": "Huidige Voltage L2 (52.7.0)", + "SECURITY": 0, + "STATUS": "0.0", + "STATUS_ID": 104 + }, + { + "LABEL": "Huidige Voltage L2 (72.7.0)", + "SECURITY": 0, + "STATUS": "233.0", + "STATUS_ID": 105 + } +] diff --git a/tests/components/p1_monitor/fixtures/settings.json b/tests/components/p1_monitor/fixtures/settings.json index eaa14765566..d14615644a5 100644 --- a/tests/components/p1_monitor/fixtures/settings.json +++ b/tests/components/p1_monitor/fixtures/settings.json @@ -1,27 +1,27 @@ [ - { - "CONFIGURATION_ID": 1, - "LABEL": "Verbruik tarief elektriciteit dal/nacht in euro.", - "PARAMETER": "0.20522" - }, - { - "CONFIGURATION_ID": 2, - "LABEL": "Verbruik tarief elektriciteit piek/dag in euro.", - "PARAMETER": "0.20522" - }, - { - "CONFIGURATION_ID": 3, - "LABEL": "Geleverd tarief elektriciteit dal/nacht in euro.", - "PARAMETER": "0.20522" - }, - { - "CONFIGURATION_ID": 4, - "LABEL": "Geleverd tarief elektriciteit piek/dag in euro.", - "PARAMETER": "0.20522" - }, - { - "CONFIGURATION_ID": 15, - "LABEL": "Verbruik tarief gas in euro.", - "PARAMETER": "0.64" - } -] \ No newline at end of file + { + "CONFIGURATION_ID": 1, + "LABEL": "Verbruik tarief elektriciteit dal/nacht in euro.", + "PARAMETER": "0.20522" + }, + { + "CONFIGURATION_ID": 2, + "LABEL": "Verbruik tarief elektriciteit piek/dag in euro.", + "PARAMETER": "0.20522" + }, + { + "CONFIGURATION_ID": 3, + "LABEL": "Geleverd tarief elektriciteit dal/nacht in euro.", + "PARAMETER": "0.20522" + }, + { + "CONFIGURATION_ID": 4, + "LABEL": "Geleverd tarief elektriciteit piek/dag in euro.", + "PARAMETER": "0.20522" + }, + { + "CONFIGURATION_ID": 15, + "LABEL": "Verbruik tarief gas in euro.", + "PARAMETER": "0.64" + } +] diff --git a/tests/components/p1_monitor/fixtures/smartmeter.json b/tests/components/p1_monitor/fixtures/smartmeter.json index d2ca0b38002..654d77ce729 100644 --- a/tests/components/p1_monitor/fixtures/smartmeter.json +++ b/tests/components/p1_monitor/fixtures/smartmeter.json @@ -1,15 +1,15 @@ [ - { - "CONSUMPTION_GAS_M3": 2273.447, - "CONSUMPTION_KWH_HIGH": 2770.133, - "CONSUMPTION_KWH_LOW": 4988.071, - "CONSUMPTION_W": 877, - "PRODUCTION_KWH_HIGH": 3971.604, - "PRODUCTION_KWH_LOW": 1432.279, - "PRODUCTION_W": 0, - "RECORD_IS_PROCESSED": 0, - "TARIFCODE": "P", - "TIMESTAMP_UTC": 1629134632, - "TIMESTAMP_lOCAL": "2021-08-16 19:23:52" - } -] \ No newline at end of file + { + "CONSUMPTION_GAS_M3": 2273.447, + "CONSUMPTION_KWH_HIGH": 2770.133, + "CONSUMPTION_KWH_LOW": 4988.071, + "CONSUMPTION_W": 877, + "PRODUCTION_KWH_HIGH": 3971.604, + "PRODUCTION_KWH_LOW": 1432.279, + "PRODUCTION_W": 0, + "RECORD_IS_PROCESSED": 0, + "TARIFCODE": "P", + "TIMESTAMP_UTC": 1629134632, + "TIMESTAMP_lOCAL": "2021-08-16 19:23:52" + } +] diff --git a/tests/components/peco/__init__.py b/tests/components/peco/__init__.py new file mode 100644 index 00000000000..090a46cdf7b --- /dev/null +++ b/tests/components/peco/__init__.py @@ -0,0 +1 @@ +"""Tests for the PECO Outage Counter integration.""" diff --git a/tests/components/peco/test_config_flow.py b/tests/components/peco/test_config_flow.py new file mode 100644 index 00000000000..98f704e3af8 --- /dev/null +++ b/tests/components/peco/test_config_flow.py @@ -0,0 +1,79 @@ +"""Test the PECO Outage Counter config flow.""" +from unittest.mock import patch + +from pytest import raises +from voluptuous.error import MultipleInvalid + +from homeassistant import config_entries +from homeassistant.components.peco.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM + + +async def test_form(hass: HomeAssistant) -> None: + """Test we get the form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] is None + + with patch( + "homeassistant.components.peco.async_setup_entry", + return_value=True, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "county": "PHILADELPHIA", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["title"] == "Philadelphia Outage Count" + assert result2["data"] == { + "county": "PHILADELPHIA", + } + + +async def test_invalid_county(hass: HomeAssistant) -> None: + """Test if the InvalidCounty error works.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] is None + + with raises(MultipleInvalid): + await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "county": "INVALID_COUNTY_THAT_SHOULD_NOT_EXIST", + }, + ) + await hass.async_block_till_done() + + second_result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert second_result["type"] == RESULT_TYPE_FORM + assert second_result["errors"] is None + + with patch( + "homeassistant.components.peco.async_setup_entry", + return_value=True, + ): + second_result2 = await hass.config_entries.flow.async_configure( + second_result["flow_id"], + { + "county": "PHILADELPHIA", + }, + ) + await hass.async_block_till_done() + + assert second_result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert second_result2["title"] == "Philadelphia Outage Count" + assert second_result2["data"] == { + "county": "PHILADELPHIA", + } diff --git a/tests/components/peco/test_init.py b/tests/components/peco/test_init.py new file mode 100644 index 00000000000..2a9b862c1a6 --- /dev/null +++ b/tests/components/peco/test_init.py @@ -0,0 +1,146 @@ +"""Test the PECO Outage Counter init file.""" +import asyncio +from unittest.mock import patch + +from peco import BadJSONError, HttpError +import pytest + +from homeassistant.components.peco.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + +MOCK_ENTRY_DATA = {"county": "TOTAL"} +COUNTY_ENTRY_DATA = {"county": "BUCKS"} +INVALID_COUNTY_DATA = {"county": "INVALID"} + + +async def test_unload_entry(hass: HomeAssistant) -> None: + """Test the unload entry.""" + config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_ENTRY_DATA) + config_entry.add_to_hass(hass) + + with patch( + "peco.PecoOutageApi.get_outage_totals", + return_value={ + "customers_out": 0, + "percent_customers_out": 0, + "outage_count": 0, + "customers_served": 350394, + }, + ): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert hass.data[DOMAIN] + + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + assert entries[0].state == ConfigEntryState.LOADED + + await hass.config_entries.async_unload(entries[0].entry_id) + await hass.async_block_till_done() + assert entries[0].state == ConfigEntryState.NOT_LOADED + + +@pytest.mark.parametrize( + "sensor", + [ + "bucks_customers_out", + "bucks_percent_customers_out", + "bucks_outage_count", + "bucks_customers_served", + ], +) +async def test_update_timeout(hass: HomeAssistant, sensor): + """Test if it raises an error when there is a timeout.""" + + config_entry = MockConfigEntry(domain=DOMAIN, data=COUNTY_ENTRY_DATA) + config_entry.add_to_hass(hass) + + with patch( + "peco.PecoOutageApi.get_outage_count", + side_effect=asyncio.TimeoutError(), + ): + assert not await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert hass.states.get(f"sensor.{sensor}") is None + assert config_entry.state == ConfigEntryState.SETUP_RETRY + + +@pytest.mark.parametrize( + "sensor", + [ + "total_customers_out", + "total_percent_customers_out", + "total_outage_count", + "total_customers_served", + ], +) +async def test_total_update_timeout(hass: HomeAssistant, sensor): + """Test if it raises an error when there is a timeout.""" + + config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_ENTRY_DATA) + config_entry.add_to_hass(hass) + with patch( + "peco.PecoOutageApi.get_outage_totals", + side_effect=asyncio.TimeoutError(), + ): + assert not await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert hass.states.get(f"sensor.{sensor}") is None + assert config_entry.state == ConfigEntryState.SETUP_RETRY + + +@pytest.mark.parametrize( + "sensor", + [ + "bucks_customers_out", + "bucks_percent_customers_out", + "bucks_outage_count", + "bucks_customers_served", + ], +) +async def test_http_error(hass: HomeAssistant, sensor: str): + """Test if it raises an error when an abnormal status code is returned.""" + + config_entry = MockConfigEntry(domain=DOMAIN, data=COUNTY_ENTRY_DATA) + config_entry.add_to_hass(hass) + + with patch( + "peco.PecoOutageApi.get_outage_count", + side_effect=HttpError(), + ): + assert not await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert hass.states.get(f"sensor.{sensor}") is None + assert config_entry.state == ConfigEntryState.SETUP_RETRY + + +@pytest.mark.parametrize( + "sensor", + [ + "bucks_customers_out", + "bucks_percent_customers_out", + "bucks_outage_count", + "bucks_customers_served", + ], +) +async def test_bad_json(hass: HomeAssistant, sensor: str): + """Test if it raises an error when abnormal JSON is returned.""" + + config_entry = MockConfigEntry(domain=DOMAIN, data=COUNTY_ENTRY_DATA) + config_entry.add_to_hass(hass) + + with patch( + "peco.PecoOutageApi.get_outage_count", + side_effect=BadJSONError(), + ): + assert not await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert hass.states.get(f"sensor.{sensor}") is None + assert config_entry.state == ConfigEntryState.SETUP_RETRY diff --git a/tests/components/peco/test_sensor.py b/tests/components/peco/test_sensor.py new file mode 100644 index 00000000000..e66b28641bc --- /dev/null +++ b/tests/components/peco/test_sensor.py @@ -0,0 +1,82 @@ +"""Test the PECO Outage Counter sensors.""" +from unittest.mock import patch + +from peco import OutageResults +import pytest + +from homeassistant.components.peco.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + +MOCK_ENTRY_DATA = {"county": "TOTAL"} +COUNTY_ENTRY_DATA = {"county": "BUCKS"} +INVALID_COUNTY_DATA = {"county": "INVALID"} + + +@pytest.mark.parametrize( + "sensor,expected", + [ + ("customers_out", "123"), + ("percent_customers_out", "15"), + ("outage_count", "456"), + ("customers_served", "789"), + ], +) +async def test_sensor_available( + hass: HomeAssistant, sensor: str, expected: str +) -> None: + """Test that the sensors are working.""" + # Totals Test + + config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_ENTRY_DATA) + config_entry.add_to_hass(hass) + + with patch( + "peco.PecoOutageApi.get_outage_totals", + return_value=OutageResults( + customers_out=123, + percent_customers_out=15.589, + outage_count=456, + customers_served=789, + ), + ): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert hass.data[DOMAIN] + + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + assert config_entry.state == ConfigEntryState.LOADED + + sensor_entity = hass.states.get(f"sensor.total_{sensor}") + assert sensor_entity is not None + assert sensor_entity.state != "unavailable" + assert sensor_entity.state == expected + + # County Test + + config_entry = MockConfigEntry(domain=DOMAIN, data=COUNTY_ENTRY_DATA) + config_entry.add_to_hass(hass) + + with patch( + "peco.PecoOutageApi.get_outage_count", + return_value=OutageResults( + customers_out=123, + percent_customers_out=15.589, + outage_count=456, + customers_served=789, + ), + ): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 2 + assert config_entry.state == ConfigEntryState.LOADED + + sensor_entity = hass.states.get(f"sensor.bucks_{sensor}") + assert sensor_entity is not None + assert sensor_entity.state != "unavailable" + assert sensor_entity.state == expected diff --git a/tests/components/persistent_notification/test_init.py b/tests/components/persistent_notification/test_init.py index a26e243df11..bca6fe3b93d 100644 --- a/tests/components/persistent_notification/test_init.py +++ b/tests/components/persistent_notification/test_init.py @@ -20,7 +20,7 @@ async def test_create(hass): assert len(hass.states.async_entity_ids(pn.DOMAIN)) == 0 assert len(notifications) == 0 - pn.async_create(hass, "Hello World {{ 1 + 1 }}", title="{{ 1 + 1 }} beers") + pn.async_create(hass, "Hello World 2", title="2 beers") entity_ids = hass.states.async_entity_ids(pn.DOMAIN) assert len(entity_ids) == 1 @@ -68,27 +68,6 @@ async def test_create_notification_id(hass): assert notification["message"] == "test 2" -async def test_create_template_error(hass): - """Ensure we output templates if contain error.""" - notifications = hass.data[pn.DOMAIN] - assert len(hass.states.async_entity_ids(pn.DOMAIN)) == 0 - assert len(notifications) == 0 - - pn.async_create(hass, "{{ message + 1 }}", "{{ title + 1 }}") - - entity_ids = hass.states.async_entity_ids(pn.DOMAIN) - assert len(entity_ids) == 1 - assert len(notifications) == 1 - - state = hass.states.get(entity_ids[0]) - assert state.attributes.get("message") == "{{ message + 1 }}" - assert state.attributes.get("title") == "{{ title + 1 }}" - - notification = notifications.get(entity_ids[0]) - assert notification["message"] == "{{ message + 1 }}" - assert notification["title"] == "{{ title + 1 }}" - - async def test_dismiss_notification(hass): """Ensure removal of specific notification.""" notifications = hass.data[pn.DOMAIN] diff --git a/tests/components/pi_hole/__init__.py b/tests/components/pi_hole/__init__.py index 235cce92a4b..57ea89fc7e0 100644 --- a/tests/components/pi_hole/__init__.py +++ b/tests/components/pi_hole/__init__.py @@ -3,10 +3,11 @@ from unittest.mock import AsyncMock, MagicMock, patch from hole.exceptions import HoleError -from homeassistant.components.pi_hole.const import CONF_LOCATION, CONF_STATISTICS_ONLY +from homeassistant.components.pi_hole.const import CONF_STATISTICS_ONLY from homeassistant.const import ( CONF_API_KEY, CONF_HOST, + CONF_LOCATION, CONF_NAME, CONF_PORT, CONF_SSL, @@ -82,7 +83,7 @@ CONF_CONFIG_ENTRY = { SWITCH_ENTITY_ID = "switch.pi_hole" -def _create_mocked_hole(raise_exception=False): +def _create_mocked_hole(raise_exception=False, has_versions=True): mocked_hole = MagicMock() type(mocked_hole).get_data = AsyncMock( side_effect=HoleError("") if raise_exception else None @@ -93,7 +94,10 @@ def _create_mocked_hole(raise_exception=False): type(mocked_hole).enable = AsyncMock() type(mocked_hole).disable = AsyncMock() mocked_hole.data = ZERO_DATA - mocked_hole.versions = SAMPLE_VERSIONS + if has_versions: + mocked_hole.versions = SAMPLE_VERSIONS + else: + mocked_hole.versions = None return mocked_hole diff --git a/tests/components/pi_hole/test_init.py b/tests/components/pi_hole/test_init.py index e96f0d7b33f..dce3773acdc 100644 --- a/tests/components/pi_hole/test_init.py +++ b/tests/components/pi_hole/test_init.py @@ -6,7 +6,6 @@ from hole.exceptions import HoleError from homeassistant.components import pi_hole, switch from homeassistant.components.pi_hole.const import ( - CONF_LOCATION, CONF_STATISTICS_ONLY, DEFAULT_LOCATION, DEFAULT_NAME, @@ -19,6 +18,7 @@ from homeassistant.const import ( ATTR_ENTITY_ID, CONF_API_KEY, CONF_HOST, + CONF_LOCATION, CONF_NAME, CONF_SSL, CONF_VERIFY_SSL, @@ -47,106 +47,45 @@ async def test_setup_minimal_config(hass): await hass.async_block_till_done() - assert ( - hass.states.get("sensor.pi_hole_ads_blocked_today").name - == "Pi-Hole Ads Blocked Today" - ) - assert ( - hass.states.get("sensor.pi_hole_ads_percentage_blocked_today").name - == "Pi-Hole Ads Percentage Blocked Today" - ) - assert ( - hass.states.get("sensor.pi_hole_dns_queries_cached").name - == "Pi-Hole DNS Queries Cached" - ) - assert ( - hass.states.get("sensor.pi_hole_dns_queries_forwarded").name - == "Pi-Hole DNS Queries Forwarded" - ) - assert ( - hass.states.get("sensor.pi_hole_dns_queries_today").name - == "Pi-Hole DNS Queries Today" - ) - assert ( - hass.states.get("sensor.pi_hole_dns_unique_clients").name - == "Pi-Hole DNS Unique Clients" - ) - assert ( - hass.states.get("sensor.pi_hole_dns_unique_domains").name - == "Pi-Hole DNS Unique Domains" - ) - assert ( - hass.states.get("sensor.pi_hole_domains_blocked").name - == "Pi-Hole Domains Blocked" - ) - assert hass.states.get("sensor.pi_hole_seen_clients").name == "Pi-Hole Seen Clients" + state = hass.states.get("sensor.pi_hole_ads_blocked_today") + assert state.name == "Pi-Hole Ads Blocked Today" + assert state.state == "0" - assert hass.states.get("sensor.pi_hole_ads_blocked_today").state == "0" - assert hass.states.get("sensor.pi_hole_ads_percentage_blocked_today").state == "0" - assert hass.states.get("sensor.pi_hole_dns_queries_cached").state == "0" - assert hass.states.get("sensor.pi_hole_dns_queries_forwarded").state == "0" - assert hass.states.get("sensor.pi_hole_dns_queries_today").state == "0" - assert hass.states.get("sensor.pi_hole_dns_unique_clients").state == "0" - assert hass.states.get("sensor.pi_hole_dns_unique_domains").state == "0" - assert hass.states.get("sensor.pi_hole_domains_blocked").state == "0" - assert hass.states.get("sensor.pi_hole_seen_clients").state == "0" + state = hass.states.get("sensor.pi_hole_ads_percentage_blocked_today") + assert state.name == "Pi-Hole Ads Percentage Blocked Today" + assert state.state == "0" - assert hass.states.get("binary_sensor.pi_hole").name == "Pi-Hole" - assert hass.states.get("binary_sensor.pi_hole").state == "off" + state = hass.states.get("sensor.pi_hole_dns_queries_cached") + assert state.name == "Pi-Hole DNS Queries Cached" + assert state.state == "0" - assert ( - hass.states.get("binary_sensor.pi_hole_core_update_available").name - == "Pi-Hole Core Update Available" - ) - assert hass.states.get("binary_sensor.pi_hole_core_update_available").state == "on" - assert ( - hass.states.get("binary_sensor.pi_hole_core_update_available").attributes[ - "current_version" - ] - == "v5.5" - ) - assert ( - hass.states.get("binary_sensor.pi_hole_core_update_available").attributes[ - "latest_version" - ] - == "v5.6" - ) + state = hass.states.get("sensor.pi_hole_dns_queries_forwarded") + assert state.name == "Pi-Hole DNS Queries Forwarded" + assert state.state == "0" - assert ( - hass.states.get("binary_sensor.pi_hole_ftl_update_available").name - == "Pi-Hole FTL Update Available" - ) - assert hass.states.get("binary_sensor.pi_hole_ftl_update_available").state == "on" - assert ( - hass.states.get("binary_sensor.pi_hole_ftl_update_available").attributes[ - "current_version" - ] - == "v5.10" - ) - assert ( - hass.states.get("binary_sensor.pi_hole_ftl_update_available").attributes[ - "latest_version" - ] - == "v5.11" - ) + state = hass.states.get("sensor.pi_hole_dns_queries_today") + assert state.name == "Pi-Hole DNS Queries Today" + assert state.state == "0" - assert ( - hass.states.get("binary_sensor.pi_hole_web_update_available").name - == "Pi-Hole Web Update Available" - ) - assert hass.states.get("binary_sensor.pi_hole_web_update_available").state == "on" - assert ( - hass.states.get("binary_sensor.pi_hole_web_update_available").attributes[ - "current_version" - ] - == "v5.7" - ) - assert ( - hass.states.get("binary_sensor.pi_hole_web_update_available").attributes[ - "latest_version" - ] - == "v5.8" - ) + state = hass.states.get("sensor.pi_hole_dns_unique_clients") + assert state.name == "Pi-Hole DNS Unique Clients" + assert state.state == "0" + + state = hass.states.get("sensor.pi_hole_dns_unique_domains") + assert state.name == "Pi-Hole DNS Unique Domains" + assert state.state == "0" + + state = hass.states.get("sensor.pi_hole_domains_blocked") + assert state.name == "Pi-Hole Domains Blocked" + assert state.state == "0" + + state = hass.states.get("sensor.pi_hole_seen_clients") + assert state.name == "Pi-Hole Seen Clients" + assert state.state == "0" + + state = hass.states.get("binary_sensor.pi_hole") + assert state.name == "Pi-Hole" + assert state.state == "off" async def test_setup_name_config(hass): diff --git a/tests/components/pi_hole/test_update.py b/tests/components/pi_hole/test_update.py new file mode 100644 index 00000000000..8da4ec263d5 --- /dev/null +++ b/tests/components/pi_hole/test_update.py @@ -0,0 +1,80 @@ +"""Test pi_hole component.""" + +from homeassistant.components import pi_hole +from homeassistant.const import STATE_ON, STATE_UNKNOWN +from homeassistant.setup import async_setup_component + +from . import _create_mocked_hole, _patch_config_flow_hole, _patch_init_hole + + +async def test_update(hass): + """Tests update entity.""" + mocked_hole = _create_mocked_hole() + with _patch_config_flow_hole(mocked_hole), _patch_init_hole(mocked_hole): + assert await async_setup_component( + hass, pi_hole.DOMAIN, {pi_hole.DOMAIN: [{"host": "pi.hole"}]} + ) + + await hass.async_block_till_done() + + state = hass.states.get("update.pi_hole_core_update_available") + assert state.name == "Pi-Hole Core Update Available" + assert state.state == STATE_ON + assert state.attributes["installed_version"] == "v5.5" + assert state.attributes["latest_version"] == "v5.6" + assert ( + state.attributes["release_url"] + == "https://github.com/pi-hole/pi-hole/releases/tag/v5.6" + ) + + state = hass.states.get("update.pi_hole_ftl_update_available") + assert state.name == "Pi-Hole FTL Update Available" + assert state.state == STATE_ON + assert state.attributes["installed_version"] == "v5.10" + assert state.attributes["latest_version"] == "v5.11" + assert ( + state.attributes["release_url"] + == "https://github.com/pi-hole/FTL/releases/tag/v5.11" + ) + + state = hass.states.get("update.pi_hole_web_update_available") + assert state.name == "Pi-Hole Web Update Available" + assert state.state == STATE_ON + assert state.attributes["installed_version"] == "v5.7" + assert state.attributes["latest_version"] == "v5.8" + assert ( + state.attributes["release_url"] + == "https://github.com/pi-hole/AdminLTE/releases/tag/v5.8" + ) + + +async def test_update_no_versions(hass): + """Tests update entity when no version data available.""" + mocked_hole = _create_mocked_hole(has_versions=False) + with _patch_config_flow_hole(mocked_hole), _patch_init_hole(mocked_hole): + assert await async_setup_component( + hass, pi_hole.DOMAIN, {pi_hole.DOMAIN: [{"host": "pi.hole"}]} + ) + + await hass.async_block_till_done() + + state = hass.states.get("update.pi_hole_core_update_available") + assert state.name == "Pi-Hole Core Update Available" + assert state.state == STATE_UNKNOWN + assert state.attributes["installed_version"] is None + assert state.attributes["latest_version"] is None + assert state.attributes["release_url"] is None + + state = hass.states.get("update.pi_hole_ftl_update_available") + assert state.name == "Pi-Hole FTL Update Available" + assert state.state == STATE_UNKNOWN + assert state.attributes["installed_version"] is None + assert state.attributes["latest_version"] is None + assert state.attributes["release_url"] is None + + state = hass.states.get("update.pi_hole_web_update_available") + assert state.name == "Pi-Hole Web Update Available" + assert state.state == STATE_UNKNOWN + assert state.attributes["installed_version"] is None + assert state.attributes["latest_version"] is None + assert state.attributes["release_url"] is None diff --git a/tests/components/plant/test_init.py b/tests/components/plant/test_init.py index 14e0f3668b0..602f368abcd 100644 --- a/tests/components/plant/test_init.py +++ b/tests/components/plant/test_init.py @@ -1,9 +1,6 @@ """Unit tests for platform/plant.py.""" from datetime import datetime, timedelta -import pytest - -from homeassistant.components import recorder import homeassistant.components.plant as plant from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, @@ -12,12 +9,12 @@ from homeassistant.const import ( STATE_OK, STATE_PROBLEM, STATE_UNAVAILABLE, - STATE_UNKNOWN, ) from homeassistant.core import State from homeassistant.setup import async_setup_component -from tests.common import init_recorder_component +from tests.common import async_init_recorder_component +from tests.components.recorder.common import async_wait_recording_done_without_instance GOOD_DATA = { "moisture": 50, @@ -148,19 +145,13 @@ async def test_state_problem_if_unavailable(hass): assert state.attributes[plant.READING_MOISTURE] == STATE_UNAVAILABLE -@pytest.mark.skipif( - plant.ENABLE_LOAD_HISTORY is False, - reason="tests for loading from DB are unstable, thus" - "this feature is turned of until tests become" - "stable", -) async def test_load_from_db(hass): """Test bootstrapping the brightness history from the database. This test can should only be executed if the loading of the history is enabled via plant.ENABLE_LOAD_HISTORY. """ - init_recorder_component(hass) + await async_init_recorder_component(hass) plant_name = "wise_plant" for value in [20, 30, 10]: @@ -169,7 +160,7 @@ async def test_load_from_db(hass): ) await hass.async_block_till_done() # wait for the recorder to really store the data - hass.data[recorder.DATA_INSTANCE].block_till_done() + await async_wait_recording_done_without_instance(hass) assert await async_setup_component( hass, plant.DOMAIN, {plant.DOMAIN: {plant_name: GOOD_CONFIG}} @@ -177,7 +168,7 @@ async def test_load_from_db(hass): await hass.async_block_till_done() state = hass.states.get(f"plant.{plant_name}") - assert state.state == STATE_UNKNOWN + assert state.state == STATE_PROBLEM max_brightness = state.attributes.get(plant.ATTR_MAX_BRIGHTNESS_HISTORY) assert max_brightness == 30 diff --git a/tests/components/plex/fixtures/media_1.xml b/tests/components/plex/fixtures/media_1.xml index 838afb2959c..7392eaeef92 100644 --- a/tests/components/plex/fixtures/media_1.xml +++ b/tests/components/plex/fixtures/media_1.xml @@ -1,4 +1,4 @@ -